chore: final pre-submission polish for JOSS

- Add CITATION.cff for GitHub citation support and Zenodo/JOSS integration
- Fix ossim reference URL in references.bib (was pointing to unrelated repo)
- Remove broken favicon reference from index.html (no public/ directory exists)
- Add meta description to index.html
- Fix sidebar title inconsistency: "Process Simulator" → "Process Scheduler Simulator"
- Rebuild dist.tar.gz with production assets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 4.6 committed 2026-03-23 19:50:45 -03:00
1 parent fa009102eb
commit 22925578b1
34 files changed
+304 -255

No files matched your search

+24
View File
@@ -0,0 +1,24 @@
cff-version: 1.2.0
message: "If you use this software, please cite it as below."
type: software
title: "PSS: A Browser-Based Interactive Simulator for CPU Scheduling Algorithms"
version: 1.2.0
date-released: "2026-03-23"
license: MIT
url: "https://pss.lucasrguerra.dev.br"
repository-code: "https://github.com/lucasrguerra/pss"
authors:
- family-names: Guerra
given-names: Lucas Rayan
orcid: "https://orcid.org/0009-0002-3170-4126"
affiliation: "Universidade Federal Rural de Pernambuco (UFRPE), Brazil"
email: lucas.guerra@ufrpe.br
keywords:
- operating systems
- CPU scheduling
- process scheduling
- computer science education
- simulation
- visualization
- TypeScript
- React
+22 -22
View File
@@ -13,7 +13,7 @@
## ✨ What is it? ## ✨ What is it?
The **Process Scheduler Simulator (PSS)** runs entirely in the browser — no server, no system dependencies. You define the processes, choose the algorithm, and watch the simulation unfold in real time through the Gantt chart. The **Process Scheduler Simulator (PSS)** runs entirely in the browser, completely free of servers and system dependencies. You define the processes, choose the algorithm, and watch the simulation unfold in real time through the Gantt chart.
Ideal for: Ideal for:
- 📚 Operating Systems students who want to visualize what was once only theory - 📚 Operating Systems students who want to visualize what was once only theory
@@ -64,10 +64,10 @@ docker compose up
## 🎮 How to use ## 🎮 How to use
1. **Create your processes** — In the left panel, add processes by defining name, arrival time, priority, and a sequence of bursts (CPU and I/O). Or use the random generator to get started quickly. 1. **Create your processes**: In the left panel, add processes by defining name, arrival time, priority, and a sequence of bursts (CPU and I/O). Or use the random generator to get started quickly.
2. **Configure the scheduler** — Choose an algorithm and adjust the parameters (quantum for RR, aging for Priority, etc.). 2. **Configure the scheduler**: Choose an algorithm and adjust the parameters (quantum for RR, aging for Priority, etc.).
3. **Run the simulation** — Use the Play/Pause/Step controls to run the simulation at the speed you want (0.25× to 4×). 3. **Run the simulation**: Use the Play/Pause/Step controls to run the simulation at the speed you want (0.25× to 4×).
4. **Analyze the results** — The Gantt chart shows each process's state in real time. At the end, the metrics panel displays turnaround, waiting time, throughput, and more. 4. **Analyze the results**: The Gantt chart shows each process's state in real time. At the end, the metrics panel displays turnaround, waiting time, throughput, and more.
> 💡 **Tip:** Load one of the built-in **presets** to see classic phenomena such as the FCFS convoy effect or priority starvation. > 💡 **Tip:** Load one of the built-in **presets** to see classic phenomena such as the FCFS convoy effect or priority starvation.
@@ -77,13 +77,13 @@ docker compose up
| Algorithm | Preemptive? | Parameters | | Algorithm | Preemptive? | Parameters |
|-----------|:-----------:|------------| |-----------|:-----------:|------------|
| **FCFS** — First Come, First Served | ❌ | — | | **FCFS**: First Come, First Served | ❌ | None |
| **SJF** — Shortest Job First | ❌ | — | | **SJF**: Shortest Job First | ❌ | None |
| **SRTF** — Shortest Remaining Time First | ✅ | — | | **SRTF**: Shortest Remaining Time First | ✅ | None |
| **Round Robin (RR)** | ✅ | Quantum | | **Round Robin (RR)** | ✅ | Quantum |
| **Priority NP** — Non-Preemptive Priority | ❌ | Aging (optional) | | **Priority NP**: Non-Preemptive Priority | ❌ | Aging (optional) |
| **Priority P** — Preemptive Priority | ✅ | Aging (optional) | | **Priority P**: Preemptive Priority | ✅ | Aging (optional) |
| **HRRN** — Highest Response Ratio Next | ❌ | — | | **HRRN**: Highest Response Ratio Next | ❌ | None |
| **Multilevel Queue** | ✅ | Quantum per queue | | **Multilevel Queue** | ✅ | Quantum per queue |
> See the [algorithms documentation](./docs/algorithms.en.md) for details on how each one works. > See the [algorithms documentation](./docs/algorithms.en.md) for details on how each one works.
@@ -94,12 +94,12 @@ docker compose up
For each simulated process, PSS automatically computes: For each simulated process, PSS automatically computes:
- **Response Time** — time until the first CPU usage - **Response Time**: time until the first CPU usage
- **Turnaround Time** — total time from arrival to completion - **Turnaround Time**: total time from arrival to completion
- **Waiting Time** — total time spent in the Ready queue - **Waiting Time**: total time spent in the Ready queue
- **CPU Time / I/O Time** — effective time in each type of burst - **CPU Time / I/O Time**: effective time in each type of burst
- **CPU Utilization** — percentage of turnaround time spent on CPU - **CPU Utilization**: percentage of turnaround time spent on CPU
- **Bound Type** — automatic classification as CPU Bound, I/O Bound, or Balanced - **Bound Type**: automatic classification as CPU Bound, I/O Bound, or Balanced
Globally: averages for response/turnaround/waiting, throughput, and overall CPU utilization. Globally: averages for response/turnaround/waiting, throughput, and overall CPU utilization.
@@ -109,7 +109,7 @@ Globally: averages for response/turnaround/waiting, throughput, and overall CPU
``` ```
src/ src/
├── core/ # Pure logic — no React ├── core/ # Pure logic (no React)
│ ├── algorithms/ # Implementation of each algorithm │ ├── algorithms/ # Implementation of each algorithm
│ │ ├── base.ts # Abstract class BaseScheduler │ │ ├── base.ts # Abstract class BaseScheduler
│ │ ├── fcfs.ts │ │ ├── fcfs.ts
@@ -120,7 +120,7 @@ src/
│ │ ├── hrrn.ts │ │ ├── hrrn.ts
│ │ ├── multilevel.ts │ │ ├── multilevel.ts
│ │ └── index.ts # Dispatch by algorithm │ │ └── index.ts # Dispatch by algorithm
│ ├── engine.ts # SimulationEngine — orchestrates the ticks │ ├── engine.ts # SimulationEngine, orchestrates the ticks
│ ├── metrics.ts # Per-process and global metrics computation │ ├── metrics.ts # Per-process and global metrics computation
│ ├── types.ts # All TypeScript interfaces │ ├── types.ts # All TypeScript interfaces
│ └── presets.ts # Pre-defined scenarios │ └── presets.ts # Pre-defined scenarios
@@ -189,9 +189,9 @@ Tests cover the simulation engine, all scheduling algorithms, and metrics comput
## 📄 Additional Documentation ## 📄 Additional Documentation
- [📐 Scheduling Algorithms](./docs/algorithms.en.md) — detailed description of each algorithm, tie-breaking rules, and examples - [📐 Scheduling Algorithms](./docs/algorithms.en.md): detailed description of each algorithm, tie-breaking rules, and examples
- [📏 Metrics](./docs/metrics.en.md) — formulas and definitions for all computed metrics - [📏 Metrics](./docs/metrics.en.md): formulas and definitions for all computed metrics
- [📝 JOSS Paper](./paper.md) — scientific paper submitted to the Journal of Open Source Software - [📝 JOSS Paper](./paper.md): scientific paper submitted to the Journal of Open Source Software
*Também disponível em: [🇧🇷 Português](./README.md)* *Também disponível em: [🇧🇷 Português](./README.md)*
+22 -22
View File
@@ -13,7 +13,7 @@
## ✨ O que é? ## ✨ O que é?
O **Process Scheduler Simulator (PSS)** roda completamente no navegador — sem servidor, sem instalação de dependências de sistema. Você define os processos, escolhe o algoritmo, e assiste à simulação acontecer em tempo real através do diagrama de Gantt. O **Process Scheduler Simulator (PSS)** roda completamente no navegador, sem servidor e sem instalação de dependências de sistema. Você define os processos, escolhe o algoritmo, e assiste à simulação acontecer em tempo real através do diagrama de Gantt.
Ideal para: Ideal para:
- 📚 Estudantes de Sistemas Operacionais querendo visualizar o que antes era só teoria - 📚 Estudantes de Sistemas Operacionais querendo visualizar o que antes era só teoria
@@ -62,10 +62,10 @@ docker compose up
## 🎮 Como usar ## 🎮 Como usar
1. **Crie seus processos** — No painel esquerdo, adicione processos definindo nome, tempo de chegada, prioridade e sequência de bursts (CPU e I/O). Ou use o gerador aleatório para começar rápido. 1. **Crie seus processos**: No painel esquerdo, adicione processos definindo nome, tempo de chegada, prioridade e sequência de bursts (CPU e I/O). Ou use o gerador aleatório para começar rápido.
2. **Configure o escalonador** — Escolha o algoritmo e ajuste os parâmetros (quantum para RR, aging para Priority, etc.). 2. **Configure o escalonador**: Escolha o algoritmo e ajuste os parâmetros (quantum para RR, aging para Priority, etc.).
3. **Execute a simulação** — Use os controles de Play/Pause/Step para rodar a simulação na velocidade que quiser (0.25× até 4×). 3. **Execute a simulação**: Use os controles de Play/Pause/Step para rodar a simulação na velocidade que quiser (0.25× até 4×).
4. **Analise os resultados** — O diagrama de Gantt mostra em tempo real o estado de cada processo. Ao final, o painel de métricas exibe turnaround, waiting time, throughput e mais. 4. **Analise os resultados**: O diagrama de Gantt mostra em tempo real o estado de cada processo. Ao final, o painel de métricas exibe turnaround, waiting time, throughput e mais.
> 💡 **Dica:** Carregue um dos **presets** disponíveis para ver fenômenos clássicos como o efeito de comboio do FCFS ou starvation com Priority Scheduling. > 💡 **Dica:** Carregue um dos **presets** disponíveis para ver fenômenos clássicos como o efeito de comboio do FCFS ou starvation com Priority Scheduling.
@@ -75,13 +75,13 @@ docker compose up
| Algoritmo | Preemptivo? | Parâmetros | | Algoritmo | Preemptivo? | Parâmetros |
|-----------|:-----------:|------------| |-----------|:-----------:|------------|
| **FCFS** — First Come, First Served | ❌ | — | | **FCFS**: First Come, First Served | ❌ | Nenhum |
| **SJF** — Shortest Job First | ❌ | — | | **SJF**: Shortest Job First | ❌ | Nenhum |
| **SRTF** — Shortest Remaining Time First | ✅ | — | | **SRTF**: Shortest Remaining Time First | ✅ | Nenhum |
| **Round Robin (RR)** | ✅ | Quantum | | **Round Robin (RR)** | ✅ | Quantum |
| **Priority NP** — Prioridade Não-Preemptivo | ❌ | Aging (opcional) | | **Priority NP**: Prioridade Não-Preemptiva | ❌ | Aging (opcional) |
| **Priority P** — Prioridade Preemptivo | ✅ | Aging (opcional) | | **Priority P**: Prioridade Preemptiva | ✅ | Aging (opcional) |
| **HRRN** — Highest Response Ratio Next | ❌ | — | | **HRRN**: Highest Response Ratio Next | ❌ | Nenhum |
| **Multilevel Queue** | ✅ | Quantum por fila | | **Multilevel Queue** | ✅ | Quantum por fila |
> Consulte a [documentação dos algoritmos](./docs/algorithms.md) para detalhes sobre como cada um funciona. > Consulte a [documentação dos algoritmos](./docs/algorithms.md) para detalhes sobre como cada um funciona.
@@ -92,12 +92,12 @@ docker compose up
Para cada processo simulado, o PSS calcula automaticamente: Para cada processo simulado, o PSS calcula automaticamente:
- **Response Time** — tempo até o primeiro uso de CPU - **Response Time**: tempo até o primeiro uso de CPU
- **Turnaround Time** — tempo total desde a chegada até o término - **Turnaround Time**: tempo total desde a chegada até o término
- **Waiting Time** — tempo total na fila Ready - **Waiting Time**: tempo total na fila Ready
- **CPU Time / I/O Time** — tempo efetivo em cada tipo de burst - **CPU Time / I/O Time**: tempo efetivo em cada tipo de burst
- **CPU Utilization** — percentual do turnaround em CPU - **CPU Utilization**: percentual do turnaround em CPU
- **Bound Type** — classificação automática em CPU Bound, I/O Bound ou Balanced - **Bound Type**: classificação automática em CPU Bound, I/O Bound ou Balanced
E globalmente: médias de response/turnaround/waiting, throughput e utilização global de CPU. E globalmente: médias de response/turnaround/waiting, throughput e utilização global de CPU.
@@ -107,7 +107,7 @@ E globalmente: médias de response/turnaround/waiting, throughput e utilização
``` ```
src/ src/
├── core/ # Lógica pura — sem React ├── core/ # Lógica pura (sem React)
│ ├── algorithms/ # Implementação de cada algoritmo │ ├── algorithms/ # Implementação de cada algoritmo
│ │ ├── base.ts # Classe abstrata BaseScheduler │ │ ├── base.ts # Classe abstrata BaseScheduler
│ │ ├── fcfs.ts │ │ ├── fcfs.ts
@@ -118,7 +118,7 @@ src/
│ │ ├── hrrn.ts │ │ ├── hrrn.ts
│ │ ├── multilevel.ts │ │ ├── multilevel.ts
│ │ └── index.ts # Dispatch por algoritmo │ │ └── index.ts # Dispatch por algoritmo
│ ├── engine.ts # SimulationEngine — orquestra os ticks │ ├── engine.ts # SimulationEngine, orquestra os ticks
│ ├── metrics.ts # Cálculo de métricas por processo e globais │ ├── metrics.ts # Cálculo de métricas por processo e globais
│ ├── types.ts # Todas as interfaces TypeScript │ ├── types.ts # Todas as interfaces TypeScript
│ └── presets.ts # Cenários pré-definidos │ └── presets.ts # Cenários pré-definidos
@@ -187,9 +187,9 @@ Os testes cobrem a engine de simulação, todos os algoritmos de escalonamento,
## 📄 Documentação adicional ## 📄 Documentação adicional
- [📐 Algoritmos de Escalonamento](./docs/algorithms.md) — descrição detalhada de cada algoritmo, regras de desempate e exemplos - [📐 Algoritmos de Escalonamento](./docs/algorithms.md): descrição detalhada de cada algoritmo, regras de desempate e exemplos
- [📏 Métricas](./docs/metrics.md) — fórmulas e definições de todas as métricas calculadas - [📏 Métricas](./docs/metrics.md): fórmulas e definições de todas as métricas calculadas
- [📝 JOSS Paper](./paper.md) — artigo científico submetido ao Journal of Open Source Software - [📝 JOSS Paper](./paper.md): artigo científico submetido ao Journal of Open Source Software
*Also available in: [🇺🇸 English](./README.en.md)* *Also available in: [🇺🇸 English](./README.en.md)*
BIN
View File
Binary file not shown.
+10 -10
View File
@@ -15,13 +15,13 @@ This document describes in detail the scheduling algorithms implemented in PSS.
--- ---
## FCFS — First Come, First Served ## FCFS (First Come, First Served)
**Type:** Non-preemptive **Type:** Non-preemptive
### How it works ### How it works
Perhaps the most intuitive algorithm: the process that arrived first is served first. The Ready queue is sorted by `arrivalTime`. Once a process occupies the CPU, it stays there until its current CPU burst is complete — no interruptions. Perhaps the most intuitive algorithm: the process that arrived first is served first. The Ready queue is sorted by `arrivalTime`. Once a process occupies the CPU, it stays there until its current CPU burst is complete, with no interruptions.
### Example ### Example
@@ -42,11 +42,11 @@ Tick: 0 1 2 3 4 5 6 7
### Note: Convoy Effect ### Note: Convoy Effect
If a long process arrives before several short ones, the short ones wait a long time — even though the CPU could be serving them. This phenomenon is called the **Convoy Effect** and is one of the main drawbacks of FCFS. Try the `convoy_effect` preset to visualize it. If a long process arrives before several short ones, the short ones wait a long time, even though the CPU could be serving them. This phenomenon is called the **Convoy Effect** and is one of the main drawbacks of FCFS. Try the `convoy_effect` preset to visualize it.
--- ---
## SJF — Shortest Job First (Non-Preemptive) ## SJF (Shortest Job First - Non-Preemptive)
**Type:** Non-preemptive **Type:** Non-preemptive
@@ -62,7 +62,7 @@ No additional parameters.
--- ---
## SRTF — Shortest Remaining Time First (Preemptive SJF) ## SRTF (Shortest Remaining Time First - Preemptive SJF)
**Type:** Preemptive **Type:** Preemptive
@@ -93,7 +93,7 @@ Tick 12: P1 finishes
The Ready queue is treated as a circular queue. Each process receives at most `quantum` consecutive CPU ticks. If the process still has remaining burst at the end of the quantum, it goes back to the **end** of the Ready queue and waits its turn. The Ready queue is treated as a circular queue. Each process receives at most `quantum` consecutive CPU ticks. If the process still has remaining burst at the end of the quantum, it goes back to the **end** of the Ready queue and waits its turn.
I/O bursts **do not** consume quantum — the process voluntarily leaves the CPU when initiating I/O. I/O bursts **do not** consume quantum, as the process voluntarily leaves the CPU when initiating I/O.
### Trade-off ### Trade-off
@@ -123,7 +123,7 @@ Try the `starvation` and `aging_fix` presets to see this difference.
--- ---
## HRRN — Highest Response Ratio Next ## HRRN (Highest Response Ratio Next)
**Type:** Non-preemptive **Type:** Non-preemptive
@@ -152,7 +152,7 @@ The process with the **highest ratio** is selected. This naturally increases the
### How it works ### How it works
Processes are distributed across multiple queues with different priorities. Each queue has its own Round Robin quantum. Higher-priority queues are always served first — a lower-priority queue only receives CPU if all queues above it are empty. Processes are distributed across multiple queues with different priorities. Each queue has its own Round Robin quantum. Higher-priority queues are always served first. A lower-priority queue only receives CPU if all queues above it are empty.
This allows different classes of processes to be treated distinctly: for example, interactive processes in high-priority queues (small quantum) and batch processes in low-priority queues (larger quantum). This allows different classes of processes to be treated distinctly: for example, interactive processes in high-priority queues (small quantum) and batch processes in low-priority queues (larger quantum).
@@ -160,5 +160,5 @@ This allows different classes of processes to be treated distinctly: for example
## References ## References
- Silberschatz, A., Galvin, P. B., & Gagne, G. — *Operating System Concepts*, 10th edition - Silberschatz, A., Galvin, P. B., & Gagne, G., *Operating System Concepts*, 10th edition.
- Tanenbaum, A. S. — *Modern Operating Systems*, 4th edition - Tanenbaum, A. S., *Modern Operating Systems*, 4th edition.
+10 -10
View File
@@ -15,13 +15,13 @@ Este documento descreve em detalhes os algoritmos de escalonamento implementados
--- ---
## FCFS — First Come, First Served ## FCFS (First Come, First Served)
**Tipo:** Não-preemptivo **Tipo:** Não-preemptivo
### Como funciona ### Como funciona
Talvez o algoritmo mais intuitivo: o processo que chegou primeiro é atendido primeiro. A fila Ready é ordenada por `arrivalTime`. Quando um processo ocupa a CPU, ele permanece lá até completar seu burst de CPU atual — sem interrupções. Talvez o algoritmo mais intuitivo: o processo que chegou primeiro é atendido primeiro. A fila Ready é ordenada por `arrivalTime`. Quando um processo ocupa a CPU, ele permanece lá até completar seu burst de CPU atual, sem interrupções.
### Exemplo ### Exemplo
@@ -42,11 +42,11 @@ Tick: 0 1 2 3 4 5 6 7
### Observação: Efeito de Comboio ### Observação: Efeito de Comboio
Se um processo longo chega antes de vários processos curtos, estes ficam esperando por muito tempo — mesmo que a CPU esteja "disponível" para eles. Este fenômeno é chamado de **Convoy Effect** e é um dos principais problemas do FCFS. Experimente o preset `convoy_effect` para visualizá-lo. Se um processo longo chega antes de vários processos curtos, estes ficam esperando por muito tempo, mesmo que a CPU esteja "disponível" para eles. Este fenômeno é chamado de **Convoy Effect** e é um dos principais problemas do FCFS. Experimente o preset `convoy_effect` para visualizá-lo.
--- ---
## SJF — Shortest Job First (Não-Preemptivo) ## SJF (Shortest Job First - Não-Preemptivo)
**Tipo:** Não-preemptivo **Tipo:** Não-preemptivo
@@ -62,7 +62,7 @@ Nenhum parâmetro adicional.
--- ---
## SRTF — Shortest Remaining Time First (SJF Preemptivo) ## SRTF (Shortest Remaining Time First - SJF Preemptivo)
**Tipo:** Preemptivo **Tipo:** Preemptivo
@@ -93,7 +93,7 @@ Tick 12: P1 termina
A fila Ready é tratada como uma fila circular. Cada processo recebe no máximo `quantum` ticks de CPU consecutivos. Se ao final do quantum o processo ainda tiver burst restante, ele retorna ao **fim** da fila Ready e aguarda sua vez. A fila Ready é tratada como uma fila circular. Cada processo recebe no máximo `quantum` ticks de CPU consecutivos. Se ao final do quantum o processo ainda tiver burst restante, ele retorna ao **fim** da fila Ready e aguarda sua vez.
I/O bursts **não** consomem quantum — o processo sai voluntariamente da CPU quando inicia I/O. I/O bursts **não** consomem quantum, pois o processo sai voluntariamente da CPU quando inicia I/O.
### Tradeoff ### Tradeoff
@@ -123,7 +123,7 @@ Experimente os presets `starvation` e `aging_fix` para ver esta diferença.
--- ---
## HRRN — Highest Response Ratio Next ## HRRN (Highest Response Ratio Next)
**Tipo:** Não-preemptivo **Tipo:** Não-preemptivo
@@ -152,7 +152,7 @@ O processo com o **maior ratio** é selecionado. Isso faz com que processos que
### Como funciona ### Como funciona
Os processos são distribuídos em múltiplas filas com diferentes prioridades. Cada fila tem seu próprio quantum de Round Robin. Filas de maior prioridade sempre são servidas primeiro — uma fila de menor prioridade só recebe CPU se todas as filas acima dela estiverem vazias. Os processos são distribuídos em múltiplas filas com diferentes prioridades. Cada fila tem seu próprio quantum de Round Robin. Filas de maior prioridade sempre são servidas primeiro. Uma fila de menor prioridade só recebe CPU se todas as filas acima dela estiverem vazias.
Isso permite tratar diferentes classes de processos de forma distinta: por exemplo, processos interativos em filas de alta prioridade (quantum pequeno) e processos batch em filas de baixa prioridade (quantum maior). Isso permite tratar diferentes classes de processos de forma distinta: por exemplo, processos interativos em filas de alta prioridade (quantum pequeno) e processos batch em filas de baixa prioridade (quantum maior).
@@ -160,5 +160,5 @@ Isso permite tratar diferentes classes de processos de forma distinta: por exemp
## Referências ## Referências
- Silberschatz, A., Galvin, P. B., & Gagne, G. — *Operating System Concepts*, 10ª edição - Silberschatz, A., Galvin, P. B., & Gagne, G., *Operating System Concepts*, 10ª edição.
- Tanenbaum, A. S. — *Modern Operating Systems*, 4ª edição - Tanenbaum, A. S., *Modern Operating Systems*, 4ª edição.
+6 -6
View File
@@ -79,7 +79,7 @@ otherwise → Balanced
## Charts ("Chart" tab) ## Charts ("Chart" tab)
### Comparative Profile — Radar Chart ### Comparative Profile (Radar Chart)
Displayed when there are **2 or more processes**. Each axis represents a time dimension, normalized from 0–100% relative to the largest value in that dimension among all processes: Displayed when there are **2 or more processes**. Each axis represents a time dimension, normalized from 0–100% relative to the largest value in that dimension among all processes:
@@ -91,15 +91,15 @@ Displayed when there are **2 or more processes**. Each axis represents a time di
| **CPU Time** | Relative CPU usage | | **CPU Time** | Relative CPU usage |
| **I/O Time** | Relative I/O usage | | **I/O Time** | Relative I/O usage |
Each process is drawn as a colored polygon (the color configured for the process). Wider polygons indicate processes with larger times in the corresponding dimensions. The radar chart is ideal for identifying **profile differences** between processes — for example, a CPU Bound process will have a much larger CPU Time axis than I/O Time. Each process is drawn as a colored polygon (the color configured for the process). Wider polygons indicate processes with larger times in the corresponding dimensions. The radar chart is ideal for identifying **profile differences** between processes (for example, a CPU Bound process will have a much larger CPU Time axis than I/O Time).
### Per-Process Breakdown — Stacked Bar Chart ### Per-Process Breakdown (Stacked Bar Chart)
Always displayed. Each bar represents a process, with stacked segments: Always displayed. Each bar represents a process, with stacked segments:
- **Amber** — Waiting Time - **Amber**: Waiting Time
- **Green** — CPU Time - **Green**: CPU Time
- **Cyan** — I/O Time - **Cyan**: I/O Time
The total height of each bar equals the **Turnaround Time** of the process. The total height of each bar equals the **Turnaround Time** of the process.
+6 -6
View File
@@ -79,7 +79,7 @@ caso contrário → Balanced
## Gráficos (aba "Gráfico") ## Gráficos (aba "Gráfico")
### Perfil Comparativo — Radar Chart ### Perfil Comparativo (Radar Chart)
Exibido quando há **2 ou mais processos**. Cada eixo representa uma dimensão de tempo, normalizada de 0–100% em relação ao maior valor dessa dimensão entre todos os processos: Exibido quando há **2 ou mais processos**. Cada eixo representa uma dimensão de tempo, normalizada de 0–100% em relação ao maior valor dessa dimensão entre todos os processos:
@@ -91,15 +91,15 @@ Exibido quando há **2 ou mais processos**. Cada eixo representa uma dimensão d
| **CPU Time** | Uso de CPU relativo | | **CPU Time** | Uso de CPU relativo |
| **I/O Time** | Uso de I/O relativo | | **I/O Time** | Uso de I/O relativo |
Cada processo é desenhado como um polígono colorido (a cor configurada no processo). Polígonos mais "amplos" indicam processos com tempos maiores nas dimensões correspondentes. O radar é ideal para identificar **diferenças de perfil** entre processos — por exemplo, um processo CPU Bound terá o eixo CPU Time muito maior que o eixo I/O Time. Cada processo é desenhado como um polígono colorido (a cor configurada no processo). Polígonos mais "amplos" indicam processos com tempos maiores nas dimensões correspondentes. O radar é ideal para identificar **diferenças de perfil** entre processos (por exemplo, um processo CPU Bound terá o eixo CPU Time muito maior que o eixo I/O Time).
### Breakdown por Processo — Stacked Bar Chart ### Breakdown por Processo (Stacked Bar Chart)
Sempre exibido. Cada barra representa um processo, com os segmentos empilhados: Sempre exibido. Cada barra representa um processo, com os segmentos empilhados:
- **Âmbar** — Waiting Time - **Âmbar**: Waiting Time
- **Verde** — CPU Time - **Verde**: CPU Time
- **Ciano** — I/O Time - **Ciano**: I/O Time
A altura total de cada barra equivale ao **Turnaround Time** do processo. A altura total de cada barra equivale ao **Turnaround Time** do processo.
+2 -2
View File
@@ -1,9 +1,9 @@
<!doctype html> <!doctype html>
<html lang="pt-BR"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Process Scheduler Simulator — interactive browser-based tool for visualizing CPU scheduling algorithms" />
<title>Process Scheduler Simulator</title> <title>Process Scheduler Simulator</title>
</head> </head>
<body class="bg-slate-900 text-slate-100 antialiased overflow-hidden selection:bg-blue-500/30"> <body class="bg-slate-900 text-slate-100 antialiased overflow-hidden selection:bg-blue-500/30">
+12 -20
View File
@@ -1,13 +1,13 @@
{ {
"name": "pss", "name": "pss",
"version": "1.0.0", "version": "1.2.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pss", "name": "pss",
"version": "1.0.0", "version": "1.2.0",
"license": "ISC", "license": "MIT",
"dependencies": { "dependencies": {
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.577.0", "lucide-react": "^0.577.0",
@@ -147,7 +147,6 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.0",
@@ -520,7 +519,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=20.19.0" "node": ">=20.19.0"
}, },
@@ -569,7 +567,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=20.19.0" "node": ">=20.19.0"
} }
@@ -1895,7 +1892,8 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
@@ -2036,7 +2034,6 @@
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.18.0" "undici-types": "~7.18.0"
} }
@@ -2054,7 +2051,6 @@
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.2.2" "csstype": "^3.2.2"
@@ -2066,7 +2062,6 @@
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^18.0.0" "@types/react": "^18.0.0"
} }
@@ -2378,7 +2373,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -2731,7 +2725,8 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/eastasianwidth": { "node_modules/eastasianwidth": {
"version": "0.2.0", "version": "0.2.0",
@@ -3549,6 +3544,7 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"lz-string": "bin/bin.js" "lz-string": "bin/bin.js"
} }
@@ -3764,7 +3760,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -3807,6 +3802,7 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1", "ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0", "ansi-styles": "^5.0.0",
@@ -3822,6 +3818,7 @@
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -3832,6 +3829,7 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
}, },
@@ -3854,7 +3852,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
}, },
@@ -3867,7 +3864,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"scheduler": "^0.23.2" "scheduler": "^0.23.2"
@@ -3888,7 +3884,6 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/use-sync-external-store": "^0.0.6", "@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0" "use-sync-external-store": "^1.4.0"
@@ -3965,8 +3960,7 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/redux-thunk": { "node_modules/redux-thunk": {
"version": "3.1.0", "version": "3.1.0",
@@ -4552,7 +4546,6 @@
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.4", "fdir": "^6.4.4",
@@ -4651,7 +4644,6 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/chai": "^5.2.2", "@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4", "@vitest/expect": "3.2.4",
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "pss", "name": "pss",
"version": "1.1.0", "version": "1.2.0",
"description": "O Process Scheduler Simulator é uma ferramenta educacional interativa que roda inteiramente no navegador", "description": "Process Scheduler Simulator — an interactive educational tool that runs entirely in the browser",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+5 -5
View File
@@ -23,7 +23,7 @@ bibliography: references.bib
# Summary # Summary
The **Process Scheduler Simulator (PSS)** is an open-source, browser-based interactive tool designed to help students, instructors, and practitioners understand and compare classical CPU scheduling algorithms from the field of Operating Systems (OS). PSS runs entirely client-side — no server, no installation — making it instantly accessible via any modern web browser at [https://pss.lucasrguerra.dev.br](https://pss.lucasrguerra.dev.br). The **Process Scheduler Simulator (PSS)** is an open-source, browser-based interactive tool designed to help students, instructors, and practitioners understand and compare classical CPU scheduling algorithms from the field of Operating Systems (OS). PSS runs entirely client-side, requiring no server or installation, which makes it instantly accessible via any modern web browser at [https://pss.lucasrguerra.dev.br](https://pss.lucasrguerra.dev.br).
Users define a set of processes, each with an arbitrary sequence of alternating CPU and I/O bursts, and select one of eight scheduling algorithms. The simulator then executes the schedule tick by tick, rendering a live Gantt chart and computing a comprehensive set of performance metrics for each process and for the simulation as a whole. PSS supports First Come First Served (FCFS), Shortest Job First (SJF), Shortest Remaining Time First (SRTF), Round Robin (RR), non-preemptive and preemptive Priority Scheduling (with optional aging), Highest Response Ratio Next (HRRN), and Multilevel Queue scheduling. Users define a set of processes, each with an arbitrary sequence of alternating CPU and I/O bursts, and select one of eight scheduling algorithms. The simulator then executes the schedule tick by tick, rendering a live Gantt chart and computing a comprehensive set of performance metrics for each process and for the simulation as a whole. PSS supports First Come First Served (FCFS), Shortest Job First (SJF), Shortest Remaining Time First (SRTF), Round Robin (RR), non-preemptive and preemptive Priority Scheduling (with optional aging), Highest Response Ratio Next (HRRN), and Multilevel Queue scheduling.
@@ -39,13 +39,13 @@ PSS addresses this gap by providing a zero-installation, zero-configuration simu
- **Instructors** seeking a live, configurable demonstration tool for classroom or remote teaching; - **Instructors** seeking a live, configurable demonstration tool for classroom or remote teaching;
- **Researchers and practitioners** who need a quick, reproducible environment to illustrate scheduling concepts. - **Researchers and practitioners** who need a quick, reproducible environment to illustrate scheduling concepts.
The configurable parameters — quantum for Round Robin, aging interval and threshold for Priority Scheduling, and per-queue quantum for Multilevel — enable learners to observe how parameter choices affect fairness, throughput, and starvation, all within a single interactive session. The configurable parameters (such as quantum for Round Robin, aging interval and threshold for Priority Scheduling, and per-queue quantum for Multilevel) enable learners to observe how parameter choices affect fairness, throughput, and starvation, all within a single interactive session.
# State of the Field # State of the Field
Several browser-based and desktop scheduling simulators exist. OS-sim [@ossim] offers a graphical simulation of a full operating system, including scheduling, but targets upper-division students and requires local installation. The CPU Scheduling Simulator by Mitchell [@mitchelljava] is a Java desktop application that covers a subset of algorithms without I/O burst modeling. Web-based tools such as Process Scheduling Simulator by Nikhil Anand cover FCFS, SJF, and RR, but do not support aging, HRRN, or Multilevel Queue, and lack composable I/O burst sequences. Several browser-based and desktop scheduling simulators exist. OS-sim [@ossim] offers a graphical simulation of a full operating system, including scheduling, but targets upper-division students and requires local installation. The CPU Scheduling Simulator by Mitchell [@mitchelljava] is a Java desktop application that covers a subset of algorithms without I/O burst modeling. Web-based tools such as Process Scheduling Simulator by Nikhil Anand cover FCFS, SJF, and RR, but do not support aging, HRRN, or Multilevel Queue, and lack composable I/O burst sequences.
PSS distinguishes itself in three key ways. First, it models **full CPU-I/O burst sequences** for each process, rather than a single CPU burst, enabling simulation of realistic workloads and the CPU Bound vs. I/O Bound classification. Second, it implements a broader set of algorithms — including HRRN and Multilevel Queue with per-queue quantum — than most comparable tools. Third, the entire application is deployed as a **static single-page application** with no backend dependency, enabling one-click access and offline use via Docker, which lowers the barrier for classroom adoption. PSS distinguishes itself in three key ways. First, it models **full CPU-I/O burst sequences** for each process, rather than a single CPU burst, enabling simulation of realistic workloads and the CPU Bound vs. I/O Bound classification. Second, it implements a broader set of algorithms than most comparable tools, including HRRN and Multilevel Queue with per-queue quantum. Third, the entire application is deployed as a **static single-page application** with no backend dependency, enabling one-click access and offline use via Docker, which lowers the barrier for classroom adoption.
# Software Design # Software Design
@@ -65,7 +65,7 @@ PSS was developed as part of the Computer Science undergraduate program at the U
The availability of PSS as a fully open-source project under the MIT license, with Docker and `npm run dev` deployment paths, enables any institution to adopt or adapt the simulator without cost or proprietary dependency. The live deployment at [https://pss.lucasrguerra.dev.br](https://pss.lucasrguerra.dev.br) provides a stable public URL suitable for sharing in syllabi, assignments, and online course materials. The availability of PSS as a fully open-source project under the MIT license, with Docker and `npm run dev` deployment paths, enables any institution to adopt or adapt the simulator without cost or proprietary dependency. The live deployment at [https://pss.lucasrguerra.dev.br](https://pss.lucasrguerra.dev.br) provides a stable public URL suitable for sharing in syllabi, assignments, and online course materials.
The built-in preset scenarios — including `convoy_effect`, `starvation`, and `aging_fix` — provide reproducible, named examples of classical OS phenomena that instructors can reference directly in lectures, assignments, or exam questions, facilitating standardized discussion of algorithm trade-offs. The built-in preset scenarios, which include `convoy_effect`, `starvation`, and `aging_fix`, provide reproducible, named examples of classical OS phenomena that instructors can reference directly in lectures, assignments, or exam questions, facilitating standardized discussion of algorithm trade-offs.
# AI Usage Disclosure # AI Usage Disclosure
@@ -73,6 +73,6 @@ The author used AI-assisted coding tools (GitHub Copilot and Google Gemini) duri
# Acknowledgements # Acknowledgements
The author thanks the professors and students of the Operating Systems course at UFRPE whose feedback shaped the features and usability of PSS. This software was developed to address specific requirements identified during a personal case study. The author acknowledges the professors and students of the Operating Systems course at UFRPE for their valuable comments and feedback provided after the tool was presented.
# References # References
+1 -1
View File
@@ -36,7 +36,7 @@
author = {Robbins, Kay A.}, author = {Robbins, Kay A.},
title = {{OS-sim}: A Graphical, Interprocess Operating System Simulator}, title = {{OS-sim}: A Graphical, Interprocess Operating System Simulator},
year = {2012}, year = {2012},
howpublished = {\url{https://github.com/mayank-02/multithreading-library}}, howpublished = {\url{https://sourceforge.net/projects/oscsimulator/}},
note = {Used in OS coursework at several universities} note = {Used in OS coursework at several universities}
} }
+11 -11
View File
@@ -15,10 +15,10 @@ const SPEED_LABELS: Record<number, string> = {
}; };
const statusLabel: Record<SimStatus, string> = { const statusLabel: Record<SimStatus, string> = {
idle: 'Pronto', idle: 'Ready',
paused: 'Pausado', paused: 'Paused',
running: 'Executando', running: 'Running',
finished: 'Concluído', finished: 'Finished',
}; };
const statusDotClass: Record<SimStatus, string> = { const statusDotClass: Record<SimStatus, string> = {
@@ -68,7 +68,7 @@ const ControlBar = () => {
<div <div
className="h-16 flex items-center px-5 gap-3 bg-slate-800 border-b border-slate-700 shadow-sm" className="h-16 flex items-center px-5 gap-3 bg-slate-800 border-b border-slate-700 shadow-sm"
role="toolbar" role="toolbar"
aria-label="Controles da simulação" aria-label="Simulation controls"
> >
{/* Left group: simulation controls */} {/* Left group: simulation controls */}
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
@@ -85,7 +85,7 @@ const ControlBar = () => {
{/* Step */} {/* Step */}
<IconButton <IconButton
label="Avançar 1 tick (Step)" label="Advance 1 tick (Step)"
variant="default" variant="default"
onClick={handleStep} onClick={handleStep}
disabled={isFinished || processes.length === 0 || isRunning} disabled={isFinished || processes.length === 0 || isRunning}
@@ -97,7 +97,7 @@ const ControlBar = () => {
{/* Play / Pause */} {/* Play / Pause */}
{isRunning ? ( {isRunning ? (
<IconButton <IconButton
label="Pausar simulação" label="Pause simulation"
variant="primary" variant="primary"
onClick={pause} onClick={pause}
data-testid="btn-pause" data-testid="btn-pause"
@@ -106,7 +106,7 @@ const ControlBar = () => {
</IconButton> </IconButton>
) : ( ) : (
<IconButton <IconButton
label="Iniciar / Retomar simulação" label="Start / Resume simulation"
variant="primary" variant="primary"
onClick={handlePlay} onClick={handlePlay}
disabled={!canPlay && !isRunning} disabled={!canPlay && !isRunning}
@@ -124,7 +124,7 @@ const ControlBar = () => {
<div <div
className="flex items-center gap-2 px-3 py-1 rounded-md bg-slate-900 border border-slate-700" className="flex items-center gap-2 px-3 py-1 rounded-md bg-slate-900 border border-slate-700"
aria-live="polite" aria-live="polite"
aria-label={`Tick atual: ${currentTick}`} aria-label={`Current tick: ${currentTick}`}
> >
<span className="text-[10px] text-slate-500 uppercase tracking-wide font-medium">Tick</span> <span className="text-[10px] text-slate-500 uppercase tracking-wide font-medium">Tick</span>
<span <span
@@ -145,14 +145,14 @@ const ControlBar = () => {
<div className="ml-auto flex items-center gap-2"> <div className="ml-auto flex items-center gap-2">
<SkipBack size={14} className="text-slate-500" aria-hidden="true" /> <SkipBack size={14} className="text-slate-500" aria-hidden="true" />
<label htmlFor="speed-select" className="text-xs text-slate-400 sr-only"> <label htmlFor="speed-select" className="text-xs text-slate-400 sr-only">
Velocidade Speed
</label> </label>
<select <select
id="speed-select" id="speed-select"
value={speed} value={speed}
onChange={e => setSpeed(Number(e.target.value))} onChange={e => setSpeed(Number(e.target.value))}
className="rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer" className="rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
aria-label="Velocidade de simulação" aria-label="Simulation speed"
data-testid="speed-select" data-testid="speed-select"
> >
{SPEED_OPTIONS.map(s => ( {SPEED_OPTIONS.map(s => (
+1 -1
View File
@@ -27,7 +27,7 @@ const GanttChart = () => {
return ( return (
<div className="border-b border-slate-800 flex items-center justify-center text-slate-600 bg-slate-900/50 text-sm select-none" <div className="border-b border-slate-800 flex items-center justify-center text-slate-600 bg-slate-900/50 text-sm select-none"
style={{ minHeight: 80 }}> style={{ minHeight: 80 }}>
Inicie a simulação para ver o diagrama de Gantt Start the simulation to see the Gantt chart
</div> </div>
); );
} }
+9 -9
View File
@@ -66,7 +66,7 @@ const AppHeader = () => {
<button <button
onClick={() => setPresetsOpen(o => !o)} onClick={() => setPresetsOpen(o => !o)}
className="flex items-center gap-1.5 text-xs text-slate-300 hover:text-white px-2.5 py-1.5 rounded-md hover:bg-slate-700 transition-colors" className="flex items-center gap-1.5 text-xs text-slate-300 hover:text-white px-2.5 py-1.5 rounded-md hover:bg-slate-700 transition-colors"
aria-label="Carregar cenário pré-definido" aria-label="Load preset scenario"
> >
<BookOpen size={13} /> <BookOpen size={13} />
Presets Presets
@@ -83,7 +83,7 @@ const AppHeader = () => {
{/* Dropdown */} {/* Dropdown */}
<div className="absolute right-0 top-full mt-1 w-72 bg-slate-800 border border-slate-600 rounded-lg shadow-xl z-50 overflow-hidden flex flex-col max-h-[min(32rem,calc(100vh-4rem))]"> <div className="absolute right-0 top-full mt-1 w-72 bg-slate-800 border border-slate-600 rounded-lg shadow-xl z-50 overflow-hidden flex flex-col max-h-[min(32rem,calc(100vh-4rem))]">
<div className="px-3 py-2 border-b border-slate-700 text-[10px] text-slate-400 uppercase tracking-wider font-medium shrink-0"> <div className="px-3 py-2 border-b border-slate-700 text-[10px] text-slate-400 uppercase tracking-wider font-medium shrink-0">
Cenários pré-definidos Preset Scenarios
</div> </div>
<div className="overflow-y-auto"> <div className="overflow-y-auto">
{presets.map((preset, i) => ( {presets.map((preset, i) => (
@@ -112,16 +112,16 @@ const AppHeader = () => {
{/* Import JSON */} {/* Import JSON */}
<label <label
htmlFor="import-json" htmlFor="import-json"
title={isSimulationActive ? 'Reinicie a simulação antes de importar' : 'Importar configuração JSON'} title={isSimulationActive ? 'Reset the simulation before importing' : 'Import JSON configuration'}
className={`flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md transition-colors cursor-pointer ${ className={`flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md transition-colors cursor-pointer ${
isSimulationActive isSimulationActive
? 'text-slate-600 cursor-not-allowed' ? 'text-slate-600 cursor-not-allowed'
: 'text-slate-300 hover:text-white hover:bg-slate-700' : 'text-slate-300 hover:text-white hover:bg-slate-700'
}`} }`}
aria-label="Importar configuração JSON" aria-label="Import JSON configuration"
> >
<Upload size={13} /> <Upload size={13} />
Importar Import
</label> </label>
<input <input
id="import-json" id="import-json"
@@ -131,19 +131,19 @@ const AppHeader = () => {
className="sr-only" className="sr-only"
onChange={handleImportJSON} onChange={handleImportJSON}
disabled={isSimulationActive} disabled={isSimulationActive}
aria-label="Selecionar arquivo JSON para importar" aria-label="Select JSON file to import"
/> />
{/* Export JSON */} {/* Export JSON */}
<button <button
onClick={handleExportJSON} onClick={handleExportJSON}
disabled={processes.length === 0} disabled={processes.length === 0}
title={processes.length === 0 ? 'Adicione processos antes de exportar' : 'Exportar configuração como JSON'} title={processes.length === 0 ? 'Add processes before exporting' : 'Export configuration as JSON'}
className="flex items-center gap-1.5 text-xs text-slate-300 hover:text-white px-2.5 py-1.5 rounded-md hover:bg-slate-700 transition-colors disabled:opacity-40 disabled:cursor-not-allowed" className="flex items-center gap-1.5 text-xs text-slate-300 hover:text-white px-2.5 py-1.5 rounded-md hover:bg-slate-700 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
aria-label="Exportar configuração JSON" aria-label="Export JSON configuration"
> >
<Download size={13} /> <Download size={13} />
Exportar Export
</button> </button>
</header> </header>
); );
+3 -3
View File
@@ -5,8 +5,8 @@ const Sidebar = () => {
return ( return (
<aside className="w-80 min-w-[20rem] h-full bg-slate-800 border-r border-slate-700 flex flex-col shadow-xl z-20"> <aside className="w-80 min-w-[20rem] h-full bg-slate-800 border-r border-slate-700 flex flex-col shadow-xl z-20">
<div className="h-16 flex items-center px-6 border-b border-slate-700 bg-slate-800/80 backdrop-blur-sm"> <div className="h-16 flex items-center px-6 border-b border-slate-700 bg-slate-800/80 backdrop-blur-sm">
<h1 className="text-xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent truncate"> <h1 className="text-xl font-bold bg-linear-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent truncate">
Process Simulator Process Scheduler Simulator
</h1> </h1>
</div> </div>
@@ -18,7 +18,7 @@ const Sidebar = () => {
</div> </div>
<div className="h-12 border-t border-slate-700 flex items-center justify-center text-xs text-slate-500"> <div className="h-12 border-t border-slate-700 flex items-center justify-center text-xs text-slate-500">
PSS v1.0 PSS v1.2.0
</div> </div>
</aside> </aside>
); );
@@ -70,7 +70,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
<MetricCard <MetricCard
label="Avg Response Time" label="Avg Response Time"
value={avgResponseTime.toFixed(2)} value={avgResponseTime.toFixed(2)}
sub="unidades de tempo" sub="time units"
accentColor="#f59e0b" accentColor="#f59e0b"
accentClass="text-amber-300" accentClass="text-amber-300"
icon={<Clock size={13} />} icon={<Clock size={13} />}
@@ -79,7 +79,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
<MetricCard <MetricCard
label="Avg Turnaround Time" label="Avg Turnaround Time"
value={avgTurnaroundTime.toFixed(2)} value={avgTurnaroundTime.toFixed(2)}
sub="unidades de tempo" sub="time units"
accentColor="#60a5fa" accentColor="#60a5fa"
accentClass="text-blue-300" accentClass="text-blue-300"
icon={<Timer size={13} />} icon={<Timer size={13} />}
@@ -88,7 +88,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
<MetricCard <MetricCard
label="Avg Waiting Time" label="Avg Waiting Time"
value={avgWaitingTime.toFixed(2)} value={avgWaitingTime.toFixed(2)}
sub="unidades de tempo" sub="time units"
accentColor="#fb923c" accentColor="#fb923c"
accentClass="text-orange-300" accentClass="text-orange-300"
icon={<Hourglass size={13} />} icon={<Hourglass size={13} />}
@@ -97,7 +97,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
<MetricCard <MetricCard
label="CPU Throughput" label="CPU Throughput"
value={cpuThroughput.toFixed(3)} value={cpuThroughput.toFixed(3)}
sub="processos / u.t." sub="processes / t.u."
accentColor="#34d399" accentColor="#34d399"
accentClass="text-emerald-300" accentClass="text-emerald-300"
icon={<Zap size={13} />} icon={<Zap size={13} />}
@@ -120,7 +120,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
<span className="text-2xl font-bold font-mono leading-none text-violet-300"> <span className="text-2xl font-bold font-mono leading-none text-violet-300">
{utilizationPct.toFixed(1)}% {utilizationPct.toFixed(1)}%
</span> </span>
<span className="text-[10px] text-slate-500">do tempo total em CPU</span> <span className="text-[10px] text-slate-500">of total CPU time</span>
<div className="mt-1 h-2 w-full bg-slate-700 rounded-full overflow-hidden"> <div className="mt-1 h-2 w-full bg-slate-700 rounded-full overflow-hidden">
<div <div
className="h-full rounded-full transition-all duration-500" className="h-full rounded-full transition-all duration-500"
+35 -9
View File
@@ -32,7 +32,7 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
if (metrics.length === 0) { if (metrics.length === 0) {
return ( return (
<div className="flex items-center justify-center h-48 text-slate-500 text-sm"> <div className="flex items-center justify-center h-48 text-slate-500 text-sm">
Nenhum dado disponível. No data available.
</div> </div>
); );
} }
@@ -40,6 +40,35 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
const getName = (id: string) => processMap[id]?.name ?? id; const getName = (id: string) => processMap[id]?.name ?? id;
const getColor = (id: string) => processMap[id]?.color ?? '#94a3b8'; const getColor = (id: string) => processMap[id]?.color ?? '#94a3b8';
// Reverse map: process name → process (for radar tooltip lookup)
const nameToProcess = Object.fromEntries(
Object.values(processMap).map(p => [p.name, p])
);
const radarTooltipContent = ({ active, payload, label }: { active?: boolean; payload?: readonly any[]; label?: string | number }) => {
if (!active || !payload?.length) return null;
return (
<div style={{ ...TOOLTIP_STYLE, padding: '8px 12px' }}>
<p style={{ fontSize: '10px', color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 600, marginBottom: 4 }}>
{label}
</p>
{payload.map((entry: any) => {
const proc = nameToProcess[entry.name];
const displayName = proc?.pid != null ? `#${proc.pid} ${proc.name}` : entry.name;
return (
<div key={entry.name} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: '11px', marginBottom: 2 }}>
<span style={{ width: 8, height: 8, borderRadius: '50%', backgroundColor: entry.color, flexShrink: 0, display: 'inline-block' }} />
<span style={{ color: '#cbd5e1' }}>{displayName}</span>
<span style={{ marginLeft: 'auto', paddingLeft: 12, fontFamily: 'monospace', color: '#e2e8f0' }}>
{typeof entry.value === 'number' ? `${entry.value.toFixed(1)}%` : '-'}
</span>
</div>
);
})}
</div>
);
};
// ── Stacked bar data ────────────────────────────────────────── // ── Stacked bar data ──────────────────────────────────────────
const barData = metrics.map((m) => ({ const barData = metrics.map((m) => ({
name: getName(m.processId), name: getName(m.processId),
@@ -87,9 +116,9 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
{showRadar && ( {showRadar && (
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center"> <p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
Perfil Comparativo (normalizado) Comparative Profile (normalized)
</p> </p>
<ResponsiveContainer width="100%" height={220}> <ResponsiveContainer width="100%" height={400}>
<RadarChart data={radarData} margin={{ top: 8, right: 20, bottom: 8, left: 20 }}> <RadarChart data={radarData} margin={{ top: 8, right: 20, bottom: 8, left: 20 }}>
<PolarGrid stroke="#334155" /> <PolarGrid stroke="#334155" />
<PolarAngleAxis <PolarAngleAxis
@@ -108,10 +137,7 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
dot={{ r: 3, fill: getColor(m.processId) }} dot={{ r: 3, fill: getColor(m.processId) }}
/> />
))} ))}
<Tooltip <Tooltip content={radarTooltipContent} />
contentStyle={TOOLTIP_STYLE}
formatter={(value) => [typeof value === 'number' ? `${value.toFixed(1)}%` : '-', '']}
/>
<Legend <Legend
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }} wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
/> />
@@ -123,9 +149,9 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
{/* ── Stacked bar chart ─────────────────────────────────── */} {/* ── Stacked bar chart ─────────────────────────────────── */}
<div className={showRadar ? 'flex-1 min-w-0' : 'w-full'}> <div className={showRadar ? 'flex-1 min-w-0' : 'w-full'}>
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center"> <p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
Breakdown por Processo (ticks) Breakdown per Process (ticks)
</p> </p>
<ResponsiveContainer width="100%" height={220}> <ResponsiveContainer width="100%" height={400}>
<BarChart data={barData} margin={{ top: 8, right: 12, left: -8, bottom: 4 }}> <BarChart data={barData} margin={{ top: 8, right: 12, left: -8, bottom: 4 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} /> <CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
<XAxis <XAxis
+8 -8
View File
@@ -8,12 +8,12 @@ import MetricsTable from './MetricsTable';
import GlobalMetrics from './GlobalMetrics'; import GlobalMetrics from './GlobalMetrics';
import MetricsChart from './MetricsChart'; import MetricsChart from './MetricsChart';
type Tab = 'table' | 'global' | 'chart'; type Tab = 'table' | 'global' | 'charts';
const TAB_CONFIG: { id: Tab; label: string; icon: React.ReactNode }[] = [ const TAB_CONFIG: { id: Tab; label: string; icon: React.ReactNode }[] = [
{ id: 'table', label: 'Por Processo', icon: <Table size={13} /> }, { id: 'table', label: 'Per Process', icon: <Table size={13} /> },
{ id: 'global', label: 'Global', icon: <Globe size={13} /> }, { id: 'global', label: 'Global', icon: <Globe size={13} /> },
{ id: 'chart', label: 'Gráfico', icon: <BarChart2 size={13} /> }, { id: 'charts', label: 'Charts', icon: <BarChart2 size={13} /> },
]; ];
const MetricsPanel = () => { const MetricsPanel = () => {
@@ -31,7 +31,7 @@ const MetricsPanel = () => {
if (!hasData) { if (!hasData) {
return ( return (
<div className="border-t border-slate-700 bg-slate-950 flex items-center justify-center h-32 text-slate-600 text-sm shrink-0"> <div className="border-t border-slate-700 bg-slate-950 flex items-center justify-center h-32 text-slate-600 text-sm shrink-0">
Execute a simulação para ver as métricas. Run the simulation to see metrics.
</div> </div>
); );
} }
@@ -46,7 +46,7 @@ const MetricsPanel = () => {
const canExportCSV = status === 'finished' && processMetrics.length > 0; const canExportCSV = status === 'finished' && processMetrics.length > 0;
return ( return (
<div className="border-t border-slate-700 bg-slate-950 flex flex-col shrink-0" style={{ height: '340px' }}> <div className="border-t border-slate-700 bg-slate-950 flex flex-col shrink-0" style={{ height: '600px' }}>
{/* Panel header with tabs + export button */} {/* Panel header with tabs + export button */}
<div className="flex items-center px-3 border-b border-slate-700 shrink-0 h-10 gap-1"> <div className="flex items-center px-3 border-b border-slate-700 shrink-0 h-10 gap-1">
{TAB_CONFIG.map(tab => ( {TAB_CONFIG.map(tab => (
@@ -69,9 +69,9 @@ const MetricsPanel = () => {
<button <button
onClick={() => exportCSV(processMetrics, processMap)} onClick={() => exportCSV(processMetrics, processMap)}
disabled={!canExportCSV} disabled={!canExportCSV}
title={!canExportCSV ? 'Disponível ao concluir a simulação' : 'Exportar métricas como CSV'} title={!canExportCSV ? 'Available when simulation completes' : 'Export metrics as CSV'}
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-slate-200 px-2.5 py-1 rounded-md hover:bg-slate-800 transition-colors disabled:opacity-40 disabled:cursor-not-allowed" className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-slate-200 px-2.5 py-1 rounded-md hover:bg-slate-800 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
aria-label="Exportar métricas CSV" aria-label="Export metrics as CSV"
> >
<Download size={12} /> <Download size={12} />
CSV CSV
@@ -87,7 +87,7 @@ const MetricsPanel = () => {
{activeTab === 'global' && ( {activeTab === 'global' && (
<GlobalMetrics metrics={globalMetrics} /> <GlobalMetrics metrics={globalMetrics} />
)} )}
{activeTab === 'chart' && ( {activeTab === 'charts' && (
<MetricsChart metrics={processMetrics} processMap={processMap} /> <MetricsChart metrics={processMetrics} processMap={processMap} />
)} )}
</div> </div>
+4 -4
View File
@@ -22,7 +22,7 @@ interface MiniBarProps {
const MiniBar = ({ value, max, color }: MiniBarProps) => { const MiniBar = ({ value, max, color }: MiniBarProps) => {
const pct = max > 0 ? (value / max) * 100 : 0; const pct = max > 0 ? (value / max) * 100 : 0;
return ( return (
<div className="relative flex items-center justify-end gap-1.5 min-w-[52px]"> <div className="relative flex items-center justify-end gap-1.5 min-w-13">
<div <div
className="absolute inset-y-0 right-0 rounded-sm opacity-20 transition-all duration-300" className="absolute inset-y-0 right-0 rounded-sm opacity-20 transition-all duration-300"
style={{ width: `${pct}%`, backgroundColor: color }} style={{ width: `${pct}%`, backgroundColor: color }}
@@ -36,7 +36,7 @@ const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
if (metrics.length === 0) { if (metrics.length === 0) {
return ( return (
<div className="flex items-center justify-center h-32 text-slate-500 text-sm"> <div className="flex items-center justify-center h-32 text-slate-500 text-sm">
Nenhum processo concluído ainda. No completed processes yet.
</div> </div>
); );
} }
@@ -50,10 +50,10 @@ const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
return ( return (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-xs text-slate-300 border-collapse min-w-[780px]"> <table className="w-full text-xs text-slate-300 border-collapse min-w-195">
<thead> <thead>
<tr className="text-[10px] text-slate-400 uppercase tracking-wider border-b border-slate-700 sticky top-0 bg-slate-950 z-10"> <tr className="text-[10px] text-slate-400 uppercase tracking-wider border-b border-slate-700 sticky top-0 bg-slate-950 z-10">
<th className="text-left px-3 py-2 font-medium">Processo</th> <th className="text-left px-3 py-2 font-medium">Process</th>
<th className="text-right px-3 py-2 font-medium">Arrival</th> <th className="text-right px-3 py-2 font-medium">Arrival</th>
<th className="text-right px-3 py-2 font-medium">Start</th> <th className="text-right px-3 py-2 font-medium">Start</th>
<th className="text-right px-3 py-2 font-medium">Finish</th> <th className="text-right px-3 py-2 font-medium">Finish</th>
+2 -2
View File
@@ -56,7 +56,7 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
<IconButton <IconButton
size="sm" size="sm"
variant="ghost" variant="ghost"
label={`Editar ${process.name}`} label={`Edit ${process.name}`}
onClick={() => onEdit(process)} onClick={() => onEdit(process)}
> >
<Pencil size={13} /> <Pencil size={13} />
@@ -64,7 +64,7 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
<IconButton <IconButton
size="sm" size="sm"
variant="ghost" variant="ghost"
label={`Remover ${process.name}`} label={`Remove ${process.name}`}
onClick={() => onRemove(process.id)} onClick={() => onRemove(process.id)}
className="hover:text-red-400 hover:bg-red-900/20" className="hover:text-red-400 hover:bg-red-900/20"
> >
+24 -24
View File
@@ -69,16 +69,16 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
const validate = (): boolean => { const validate = (): boolean => {
const errs: string[] = []; const errs: string[] = [];
if (!name.trim()) errs.push('Nome é obrigatório'); if (!name.trim()) errs.push('Name is required');
if (name.trim().length > 8) errs.push('Nome deve ter no máximo 8 caracteres'); if (name.trim().length > 8) errs.push('Name must be at most 8 characters');
if (arrivalTime < 0) errs.push('Arrival Time deve ser ≥ 0'); if (arrivalTime < 0) errs.push('Arrival Time must be ≥ 0');
if (priority < 1 || priority > 10) errs.push('Priority deve estar entre 1 e 10'); if (priority < 1 || priority > 10) errs.push('Priority must be between 1 and 10');
if (bursts.length === 0) errs.push('Deve haver ao menos 1 burst CPU'); if (bursts.length === 0) errs.push('There must be at least 1 CPU burst');
if (bursts[0]?.type !== 'cpu') errs.push('O primeiro burst deve ser CPU'); if (bursts[0]?.type !== 'cpu') errs.push('The first burst must be CPU');
if (bursts.at(-1)?.type !== 'cpu') errs.push('O último burst deve ser CPU'); if (bursts.at(-1)?.type !== 'cpu') errs.push('The last burst must be CPU');
bursts.forEach((b, i) => { bursts.forEach((b, i) => {
if (b.duration < 1) errs.push(`Burst ${i + 1}: duração deve ser ≥ 1`); if (b.duration < 1) errs.push(`Burst ${i + 1}: duration must be ≥ 1`);
if (i > 0 && bursts[i - 1]?.type === b.type) errs.push(`Bursts ${i} e ${i + 1} não podem ser do mesmo tipo (alternar CPU/IO)`); if (i > 0 && bursts[i - 1]?.type === b.type) errs.push(`Bursts ${i} and ${i + 1} cannot be the same type (alternate CPU/IO)`);
}); });
setErrors(errs); setErrors(errs);
return errs.length === 0; return errs.length === 0;
@@ -108,9 +108,9 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
<div className="bg-slate-800/60 rounded-xl border border-slate-600 shadow-xl p-4 space-y-4" data-testid="process-form"> <div className="bg-slate-800/60 rounded-xl border border-slate-600 shadow-xl p-4 space-y-4" data-testid="process-form">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-slate-200"> <h3 className="text-sm font-semibold text-slate-200">
{isEditing ? 'Editar Processo' : 'Novo Processo'} {isEditing ? 'Edit Process' : 'New Process'}
</h3> </h3>
<IconButton size="sm" variant="ghost" label="Fechar" onClick={onClose}> <IconButton size="sm" variant="ghost" label="Close" onClick={onClose}>
<X size={14} /> <X size={14} />
</IconButton> </IconButton>
</div> </div>
@@ -120,7 +120,7 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
<div className="col-span-2 flex gap-3"> <div className="col-span-2 flex gap-3">
{/* Name */} {/* Name */}
<div className="flex-1"> <div className="flex-1">
<label className={labelCls} htmlFor="pf-name">Nome</label> <label className={labelCls} htmlFor="pf-name">Name</label>
<input <input
id="pf-name" id="pf-name"
type="text" type="text"
@@ -129,19 +129,19 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
onChange={e => setName(e.target.value)} onChange={e => setName(e.target.value)}
placeholder="P1" placeholder="P1"
className={inputCls} className={inputCls}
aria-label="Nome do processo" aria-label="Process name"
/> />
</div> </div>
{/* Color */} {/* Color */}
<div> <div>
<label className={labelCls} htmlFor="pf-color">Cor</label> <label className={labelCls} htmlFor="pf-color">Color</label>
<input <input
id="pf-color" id="pf-color"
type="color" type="color"
value={color} value={color}
onChange={e => setColor(e.target.value)} onChange={e => setColor(e.target.value)}
className="h-8.5 w-12 rounded-md border border-slate-600 bg-slate-700 cursor-pointer" className="h-8.5 w-12 rounded-md border border-slate-600 bg-slate-700 cursor-pointer"
aria-label="Cor do processo" aria-label="Process color"
/> />
</div> </div>
</div> </div>
@@ -156,7 +156,7 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
value={arrivalTime} value={arrivalTime}
onChange={e => setArrivalTime(Number(e.target.value))} onChange={e => setArrivalTime(Number(e.target.value))}
className={inputCls} className={inputCls}
aria-label="Tempo de chegada" aria-label="Arrival time"
/> />
</div> </div>
@@ -171,7 +171,7 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
value={priority} value={priority}
onChange={e => setPriority(Number(e.target.value))} onChange={e => setPriority(Number(e.target.value))}
className={inputCls} className={inputCls}
aria-label="Prioridade do processo" aria-label="Process priority"
/> />
</div> </div>
</div> </div>
@@ -180,7 +180,7 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
<div> <div>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<label className="text-xs text-slate-400 font-medium">Bursts (CPU → I/O → CPU…)</label> <label className="text-xs text-slate-400 font-medium">Bursts (CPU → I/O → CPU…)</label>
<IconButton size="sm" variant="ghost" label="Adicionar burst" onClick={addBurst}> <IconButton size="sm" variant="ghost" label="Add burst" onClick={addBurst}>
<Plus size={13} /> <Plus size={13} />
</IconButton> </IconButton>
</div> </div>
@@ -203,13 +203,13 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
value={burst.duration} value={burst.duration}
onChange={e => updateBurstDuration(idx, Number(e.target.value))} onChange={e => updateBurstDuration(idx, Number(e.target.value))}
className="w-full rounded bg-slate-700/60 border border-slate-600/50 text-slate-200 text-xs px-2 py-0.5 focus:outline-none focus:ring-1 focus:ring-blue-500" className="w-full rounded bg-slate-700/60 border border-slate-600/50 text-slate-200 text-xs px-2 py-0.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
aria-label={`Duração do burst ${idx + 1}`} aria-label={`Burst ${idx + 1} duration`}
/> />
<span className="text-[10px] text-slate-500">ticks</span> <span className="text-[10px] text-slate-500">ticks</span>
<IconButton <IconButton
size="sm" size="sm"
variant="ghost" variant="ghost"
label={`Remover burst ${idx + 1}`} label={`Remove burst ${idx + 1}`}
onClick={() => removeBurst(idx)} onClick={() => removeBurst(idx)}
disabled={bursts.length <= 1} disabled={bursts.length <= 1}
className="hover:text-red-400" className="hover:text-red-400"
@@ -235,17 +235,17 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
<button <button
onClick={handleSubmit} onClick={handleSubmit}
className="flex-1 flex items-center justify-center gap-1.5 bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium py-1.5 rounded-lg transition-colors" className="flex-1 flex items-center justify-center gap-1.5 bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium py-1.5 rounded-lg transition-colors"
aria-label={isEditing ? 'Salvar processo' : 'Criar processo'} aria-label={isEditing ? 'Save process' : 'Create process'}
> >
<Check size={15} /> <Check size={15} />
{isEditing ? 'Salvar' : 'Criar'} {isEditing ? 'Save' : 'Create'}
</button> </button>
<button <button
onClick={onClose} onClick={onClose}
className="px-4 bg-slate-700 hover:bg-slate-600 text-slate-300 text-sm rounded-lg transition-colors" className="px-4 bg-slate-700 hover:bg-slate-600 text-slate-300 text-sm rounded-lg transition-colors"
aria-label="Cancelar" aria-label="Cancel"
> >
Cancelar Cancel
</button> </button>
</div> </div>
</div> </div>
+7 -7
View File
@@ -47,13 +47,13 @@ const ProcessList = ({ onEditProcess, onAddNew }: ProcessListProps) => {
{/* List header */} {/* List header */}
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-xs text-slate-400"> <span className="text-xs text-slate-400">
{processes.length}/{MAX_PROCESSES} processos {processes.length}/{MAX_PROCESSES} processes
</span> </span>
<div className="flex gap-1"> <div className="flex gap-1">
<IconButton <IconButton
size="sm" size="sm"
variant="ghost" variant="ghost"
label="Gerar processo aleatório" label="Generate random process"
onClick={handleRandomize} onClick={handleRandomize}
disabled={!canAdd} disabled={!canAdd}
> >
@@ -62,7 +62,7 @@ const ProcessList = ({ onEditProcess, onAddNew }: ProcessListProps) => {
<IconButton <IconButton
size="sm" size="sm"
variant="ghost" variant="ghost"
label="Limpar todos os processos" label="Clear all processes"
onClick={clearProcesses} onClick={clearProcesses}
disabled={processes.length === 0} disabled={processes.length === 0}
className="hover:text-red-400 hover:bg-red-900/20" className="hover:text-red-400 hover:bg-red-900/20"
@@ -76,8 +76,8 @@ const ProcessList = ({ onEditProcess, onAddNew }: ProcessListProps) => {
<div className="space-y-2 max-h-64 overflow-y-auto pr-0.5 scrollbar-hide"> <div className="space-y-2 max-h-64 overflow-y-auto pr-0.5 scrollbar-hide">
{processes.length === 0 ? ( {processes.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-2 text-slate-500 text-sm py-8 border border-dashed border-slate-700 rounded-lg"> <div className="flex flex-col items-center justify-center gap-2 text-slate-500 text-sm py-8 border border-dashed border-slate-700 rounded-lg">
<span>Nenhum processo adicionado</span> <span>No processes added</span>
<span className="text-xs">Clique em + para criar</span> <span className="text-xs">Click + to create</span>
</div> </div>
) : ( ) : (
processes.map(p => ( processes.map(p => (
@@ -95,11 +95,11 @@ const ProcessList = ({ onEditProcess, onAddNew }: ProcessListProps) => {
{canAdd && ( {canAdd && (
<button <button
onClick={onAddNew} onClick={onAddNew}
aria-label="Adicionar novo processo" aria-label="Add new process"
className="mt-2 w-full flex items-center justify-center gap-2 py-2 rounded-lg border border-dashed border-slate-600 text-slate-500 hover:border-blue-500 hover:text-blue-400 text-xs transition-colors" className="mt-2 w-full flex items-center justify-center gap-2 py-2 rounded-lg border border-dashed border-slate-600 text-slate-500 hover:border-blue-500 hover:text-blue-400 text-xs transition-colors"
> >
<Plus size={14} /> <Plus size={14} />
Adicionar Processo Add Process
</button> </button>
)} )}
</div> </div>
+2 -2
View File
@@ -24,10 +24,10 @@ const ProcessPanel = () => {
}; };
return ( return (
<section aria-label="Painel de Processos"> <section aria-label="Processes Panel">
<div className="flex items-center gap-2 mb-3 text-slate-300"> <div className="flex items-center gap-2 mb-3 text-slate-300">
<ListTree size={18} className="text-blue-400" /> <ListTree size={18} className="text-blue-400" />
<h2 className="font-semibold uppercase text-xs tracking-wider">Processos</h2> <h2 className="font-semibold uppercase text-xs tracking-wider">Processes</h2>
</div> </div>
{showForm ? ( {showForm ? (
@@ -6,9 +6,9 @@ const ALGORITHM_OPTIONS: { value: SchedulingAlgorithm; label: string; disabled?:
{ value: 'SJF_NP', label: 'SJF — Shortest Job First (NP)' }, { value: 'SJF_NP', label: 'SJF — Shortest Job First (NP)' },
{ value: 'SJF_P', label: 'SRTF — Shortest Remaining Time (P)' }, { value: 'SJF_P', label: 'SRTF — Shortest Remaining Time (P)' },
{ value: 'RR', label: 'RR — Round Robin' }, { value: 'RR', label: 'RR — Round Robin' },
{ value: 'PRIORITY_NP',label: 'Priority (Não-preemptivo)' }, { value: 'PRIORITY_NP',label: 'Priority (Non-preemptive)' },
{ value: 'PRIORITY_P', label: 'Priority (Preemptivo)' }, { value: 'PRIORITY_P', label: 'Priority (Preemptive)' },
{ value: 'PRIORITY_RR',label: 'Priority Round Robin (tempo real)' }, { value: 'PRIORITY_RR',label: 'Priority Round Robin (real-time)' },
{ value: 'HRRN', label: 'HRRN — Highest Response Ratio Next' }, { value: 'HRRN', label: 'HRRN — Highest Response Ratio Next' },
{ value: 'MULTILEVEL', label: 'Multilevel Queue (MLQ)' }, { value: 'MULTILEVEL', label: 'Multilevel Queue (MLQ)' },
]; ];
@@ -23,14 +23,14 @@ const AlgorithmSelector = () => {
htmlFor="algorithm-select" htmlFor="algorithm-select"
className="block text-xs text-slate-400 mb-1" className="block text-xs text-slate-400 mb-1"
> >
Algoritmo Algorithm
</label> </label>
<select <select
id="algorithm-select" id="algorithm-select"
value={algorithm} value={algorithm}
onChange={e => setConfig({ algorithm: e.target.value as SchedulingAlgorithm })} onChange={e => setConfig({ algorithm: e.target.value as SchedulingAlgorithm })}
className="w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-sm px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer" className="w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-sm px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
aria-label="Selecionar algoritmo de escalonamento" aria-label="Select scheduling algorithm"
> >
{ALGORITHM_OPTIONS.map(opt => ( {ALGORITHM_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value} disabled={opt.disabled}> <option key={opt.value} value={opt.value} disabled={opt.disabled}>
@@ -71,14 +71,14 @@ const MlqQueuesConfig = ({
return ( return (
<div className="space-y-2 pt-1" data-testid="mlq-queues-config"> <div className="space-y-2 pt-1" data-testid="mlq-queues-config">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">Filas MLQ (prioridade decrescente)</span> <span className="text-xs text-slate-400 font-medium">MLQ Queues (decreasing priority)</span>
<button <button
onClick={addQueue} onClick={addQueue}
disabled={queues.length >= 5} disabled={queues.length >= 5}
className="flex items-center gap-1 text-[10px] text-blue-400 hover:text-blue-300 disabled:text-slate-600 disabled:cursor-not-allowed transition-colors" className="flex items-center gap-1 text-[10px] text-blue-400 hover:text-blue-300 disabled:text-slate-600 disabled:cursor-not-allowed transition-colors"
aria-label="Adicionar fila" aria-label="Add queue"
> >
<Plus size={11} /> Fila <Plus size={11} /> Queue
</button> </button>
</div> </div>
{queues.map((q, idx) => ( {queues.map((q, idx) => (
@@ -89,46 +89,46 @@ const MlqQueuesConfig = ({
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-[10px] text-slate-400 font-semibold uppercase tracking-wide"> <span className="text-[10px] text-slate-400 font-semibold uppercase tracking-wide">
Fila {idx} {idx === 0 ? '(mais alta)' : idx === queues.length - 1 ? '(mais baixa)' : ''} Queue {idx} {idx === 0 ? '(highest)' : idx === queues.length - 1 ? '(lowest)' : ''}
</span> </span>
<button <button
onClick={() => removeQueue(idx)} onClick={() => removeQueue(idx)}
disabled={queues.length <= 1} disabled={queues.length <= 1}
className="text-slate-500 hover:text-red-400 disabled:text-slate-700 disabled:cursor-not-allowed transition-colors" className="text-slate-500 hover:text-red-400 disabled:text-slate-700 disabled:cursor-not-allowed transition-colors"
aria-label={`Remover fila ${idx}`} aria-label={`Remove queue ${idx}`}
> >
<Trash2 size={11} /> <Trash2 size={11} />
</button> </button>
</div> </div>
<div className="grid grid-cols-2 gap-1.5"> <div className="grid grid-cols-2 gap-1.5">
<div> <div>
<label className="block text-[10px] text-slate-500 mb-0.5">Prio. mín</label> <label className="block text-[10px] text-slate-500 mb-0.5">Min. Prio</label>
<input <input
type="number" min={1} max={10} type="number" min={1} max={10}
value={q.priorityMin} value={q.priorityMin}
onChange={e => update(idx, { priorityMin: Math.min(10, Math.max(1, Number(e.target.value))) })} onChange={e => update(idx, { priorityMin: Math.min(10, Math.max(1, Number(e.target.value))) })}
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500" className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
aria-label={`Fila ${idx}: prioridade mínima`} aria-label={`Queue ${idx}: minimum priority`}
/> />
</div> </div>
<div> <div>
<label className="block text-[10px] text-slate-500 mb-0.5">Prio. máx</label> <label className="block text-[10px] text-slate-500 mb-0.5">Max. Prio</label>
<input <input
type="number" min={1} max={10} type="number" min={1} max={10}
value={q.priorityMax} value={q.priorityMax}
onChange={e => update(idx, { priorityMax: Math.min(10, Math.max(1, Number(e.target.value))) })} onChange={e => update(idx, { priorityMax: Math.min(10, Math.max(1, Number(e.target.value))) })}
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500" className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
aria-label={`Fila ${idx}: prioridade máxima`} aria-label={`Queue ${idx}: maximum priority`}
/> />
</div> </div>
</div> </div>
<div> <div>
<label className="block text-[10px] text-slate-500 mb-0.5">Algoritmo</label> <label className="block text-[10px] text-slate-500 mb-0.5">Algorithm</label>
<select <select
value={q.algorithm} value={q.algorithm}
onChange={e => update(idx, { algorithm: e.target.value as MlqQueueDef['algorithm'] })} onChange={e => update(idx, { algorithm: e.target.value as MlqQueueDef['algorithm'] })}
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer" className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
aria-label={`Fila ${idx}: algoritmo`} aria-label={`Queue ${idx}: algorithm`}
> >
{MLQ_ALGO_OPTIONS.map(a => ( {MLQ_ALGO_OPTIONS.map(a => (
<option key={a} value={a}>{a}</option> <option key={a} value={a}>{a}</option>
@@ -167,7 +167,7 @@ const SchedulerConfig = () => {
{/* Read-only preemptive indicator */} {/* Read-only preemptive indicator */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-xs text-slate-400">Tipo de escalonamento</span> <span className="text-xs text-slate-400">Scheduling type</span>
<span <span
className={`text-[10px] font-semibold uppercase tracking-wide px-2 py-0.5 rounded border ${ className={`text-[10px] font-semibold uppercase tracking-wide px-2 py-0.5 rounded border ${
isPreemptive isPreemptive
@@ -176,7 +176,7 @@ const SchedulerConfig = () => {
}`} }`}
data-testid="preemptive-badge" data-testid="preemptive-badge"
> >
{isPreemptive ? 'Preemptivo' : 'Não-preemptivo'} {isPreemptive ? 'Preemptive' : 'Non-preemptive'}
</span> </span>
</div> </div>
@@ -193,7 +193,7 @@ const SchedulerConfig = () => {
value={config.contextSwitchTime} value={config.contextSwitchTime}
onChange={e => setConfig({ contextSwitchTime: Math.min(10, Math.max(0, Number(e.target.value))) })} onChange={e => setConfig({ contextSwitchTime: Math.min(10, Math.max(0, Number(e.target.value))) })}
className={inputCls} className={inputCls}
aria-label="Tempo de context switch" aria-label="Context switch time"
/> />
</div> </div>
@@ -210,7 +210,7 @@ const SchedulerConfig = () => {
value={config.quantum} value={config.quantum}
onChange={e => setConfig({ quantum: Math.max(1, Number(e.target.value)) })} onChange={e => setConfig({ quantum: Math.max(1, Number(e.target.value)) })}
className={inputCls} className={inputCls}
aria-label="Quantum do Round Robin" aria-label="Round Robin quantum"
/> />
</div> </div>
)} )}
@@ -227,7 +227,7 @@ const SchedulerConfig = () => {
{config.agingEnabled && ( {config.agingEnabled && (
<div data-testid="aging-interval-field"> <div data-testid="aging-interval-field">
<label htmlFor="aging-interval" className={labelCls}> <label htmlFor="aging-interval" className={labelCls}>
Intervalo de Aging (ticks) Aging Interval (ticks)
</label> </label>
<input <input
id="aging-interval" id="aging-interval"
@@ -236,7 +236,7 @@ const SchedulerConfig = () => {
value={config.agingInterval} value={config.agingInterval}
onChange={e => setConfig({ agingInterval: Math.max(1, Number(e.target.value)) })} onChange={e => setConfig({ agingInterval: Math.max(1, Number(e.target.value)) })}
className={inputCls} className={inputCls}
aria-label="Intervalo de aging" aria-label="Aging interval"
/> />
</div> </div>
)} )}
@@ -4,10 +4,10 @@ import SchedulerConfig from './SchedulerConfig';
const SchedulerPanel = () => { const SchedulerPanel = () => {
return ( return (
<section aria-label="Painel do Escalonador" className="space-y-3"> <section aria-label="Scheduler Panel" className="space-y-3">
<div className="flex items-center gap-2 text-slate-300"> <div className="flex items-center gap-2 text-slate-300">
<Settings size={18} className="text-emerald-400" /> <Settings size={18} className="text-emerald-400" />
<h2 className="font-semibold uppercase text-xs tracking-wider">Escalonador</h2> <h2 className="font-semibold uppercase text-xs tracking-wider">Scheduler</h2>
</div> </div>
<AlgorithmSelector /> <AlgorithmSelector />
+2 -2
View File
@@ -38,9 +38,9 @@ describe('ControlBar', () => {
expect(screen.getByTestId('tick-display')).toHaveTextContent('0000'); expect(screen.getByTestId('tick-display')).toHaveTextContent('0000');
}); });
it('shows "Pronto" status when idle', () => { it('shows "Ready" status when idle', () => {
render(<ControlBar />); render(<ControlBar />);
expect(screen.getByText('Pronto')).toBeInTheDocument(); expect(screen.getByText('Ready')).toBeInTheDocument();
}); });
it('Play button is disabled when no processes are loaded', () => { it('Play button is disabled when no processes are loaded', () => {
+22 -22
View File
@@ -11,62 +11,62 @@ beforeEach(() => {
}); });
describe('ProcessForm', () => { describe('ProcessForm', () => {
it('renders the "Novo Processo" form with default fields', () => { it('renders the "New Process" form with default fields', () => {
render(<ProcessForm onClose={() => {}} />); render(<ProcessForm onClose={() => {}} />);
expect(screen.getByTestId('process-form')).toBeInTheDocument(); expect(screen.getByTestId('process-form')).toBeInTheDocument();
expect(screen.getByText('Novo Processo')).toBeInTheDocument(); expect(screen.getByText('New Process')).toBeInTheDocument();
expect(screen.getByLabelText('Nome do processo')).toBeInTheDocument(); expect(screen.getByLabelText('Process name')).toBeInTheDocument();
expect(screen.getByLabelText('Tempo de chegada')).toBeInTheDocument(); expect(screen.getByLabelText('Arrival time')).toBeInTheDocument();
expect(screen.getByLabelText('Prioridade do processo')).toBeInTheDocument(); expect(screen.getByLabelText('Process priority')).toBeInTheDocument();
}); });
it('shows "Editar Processo" title when editingProcess is provided', () => { it('shows "Edit Process" title when editingProcess is provided', () => {
const proc = { const proc = {
id: 'p1', name: 'P1', arrivalTime: 0, priority: 5, color: '#fff', id: 'p1', name: 'P1', arrivalTime: 0, priority: 5, color: '#fff',
bursts: [{ type: 'cpu' as const, duration: 4 }], bursts: [{ type: 'cpu' as const, duration: 4 }],
}; };
render(<ProcessForm editingProcess={proc} onClose={() => {}} />); render(<ProcessForm editingProcess={proc} onClose={() => {}} />);
expect(screen.getByText('Editar Processo')).toBeInTheDocument(); expect(screen.getByText('Edit Process')).toBeInTheDocument();
}); });
it('shows validation error when name is empty', async () => { it('shows validation error when name is empty', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<ProcessForm onClose={() => {}} />); render(<ProcessForm onClose={() => {}} />);
const nameInput = screen.getByLabelText('Nome do processo'); const nameInput = screen.getByLabelText('Process name');
await user.clear(nameInput); await user.clear(nameInput);
const submitBtn = screen.getByRole('button', { name: /Criar/i }); const submitBtn = screen.getByRole('button', { name: /Create/i });
await user.click(submitBtn); await user.click(submitBtn);
expect(screen.getByRole('alert')).toBeInTheDocument(); expect(screen.getByRole('alert')).toBeInTheDocument();
expect(screen.getByText(/Nome é obrigatório/i)).toBeInTheDocument(); expect(screen.getByText(/Name is required/i)).toBeInTheDocument();
}); });
it('shows validation error when arrival time is negative', async () => { it('shows validation error when arrival time is negative', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<ProcessForm onClose={() => {}} />); render(<ProcessForm onClose={() => {}} />);
const nameInput = screen.getByLabelText('Nome do processo'); const nameInput = screen.getByLabelText('Process name');
await user.type(nameInput, 'P1'); await user.type(nameInput, 'P1');
const arrivalInput = screen.getByLabelText('Tempo de chegada'); const arrivalInput = screen.getByLabelText('Arrival time');
// fireEvent.change used to reliably set a negative number bypassing HTML min attribute // fireEvent.change used to reliably set a negative number bypassing HTML min attribute
fireEvent.change(arrivalInput, { target: { value: '-1' } }); fireEvent.change(arrivalInput, { target: { value: '-1' } });
const submitBtn = screen.getByRole('button', { name: /Criar/i }); const submitBtn = screen.getByRole('button', { name: /Create/i });
await user.click(submitBtn); await user.click(submitBtn);
expect(screen.getByText(/Arrival Time deve ser ≥ 0/i)).toBeInTheDocument(); expect(screen.getByText(/Arrival Time must be ≥ 0/i)).toBeInTheDocument();
}); });
it('shows validation error for name longer than 8 chars', async () => { it('shows validation error for name longer than 8 chars', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<ProcessForm onClose={() => {}} />); render(<ProcessForm onClose={() => {}} />);
const nameInput = screen.getByLabelText('Nome do processo'); const nameInput = screen.getByLabelText('Process name');
// Bypass the HTML maxLength attribute via fireEvent to test the JS validation // Bypass the HTML maxLength attribute via fireEvent to test the JS validation
fireEvent.change(nameInput, { target: { value: 'ProcessXXLong' } }); fireEvent.change(nameInput, { target: { value: 'ProcessXXLong' } });
await user.click(screen.getByRole('button', { name: /Criar/i })); await user.click(screen.getByRole('button', { name: /Create/i }));
expect(screen.getByText(/Nome deve ter no máximo 8 caracteres/i)).toBeInTheDocument(); expect(screen.getByText(/Name must be at most 8 characters/i)).toBeInTheDocument();
}); });
it('adds a new burst with alternating type when clicking + button', async () => { it('adds a new burst with alternating type when clicking + button', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<ProcessForm onClose={() => {}} />); render(<ProcessForm onClose={() => {}} />);
const addBurstBtn = screen.getByLabelText('Adicionar burst'); const addBurstBtn = screen.getByLabelText('Add burst');
await user.click(addBurstBtn); await user.click(addBurstBtn);
// After one CPU burst at start, clicking + adds IO burst // After one CPU burst at start, clicking + adds IO burst
// getAllByText with exact matches for the type labels // getAllByText with exact matches for the type labels
@@ -80,9 +80,9 @@ describe('ProcessForm', () => {
const user = userEvent.setup(); const user = userEvent.setup();
const onClose = vi.fn(); const onClose = vi.fn();
render(<ProcessForm onClose={onClose} />); render(<ProcessForm onClose={onClose} />);
const nameInput = screen.getByLabelText('Nome do processo'); const nameInput = screen.getByLabelText('Process name');
await user.type(nameInput, 'P1'); await user.type(nameInput, 'P1');
const submitBtn = screen.getByRole('button', { name: /Criar/i }); const submitBtn = screen.getByRole('button', { name: /Create/i });
await user.click(submitBtn); await user.click(submitBtn);
expect(onClose).toHaveBeenCalledOnce(); expect(onClose).toHaveBeenCalledOnce();
}); });
@@ -90,9 +90,9 @@ describe('ProcessForm', () => {
it('adds the process to the store after successful submission', async () => { it('adds the process to the store after successful submission', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<ProcessForm onClose={() => {}} />); render(<ProcessForm onClose={() => {}} />);
const nameInput = screen.getByLabelText('Nome do processo'); const nameInput = screen.getByLabelText('Process name');
await user.type(nameInput, 'Test1'); await user.type(nameInput, 'Test1');
await user.click(screen.getByRole('button', { name: /Criar/i })); await user.click(screen.getByRole('button', { name: /Create/i }));
const processes = useProcessStore.getState().processes; const processes = useProcessStore.getState().processes;
expect(processes).toHaveLength(1); expect(processes).toHaveLength(1);
expect(processes[0]?.name).toBe('Test1'); expect(processes[0]?.name).toBe('Test1');
@@ -26,7 +26,7 @@ beforeEach(() => {
describe('SchedulerConfig', () => { describe('SchedulerConfig', () => {
it('always shows Context Switch field regardless of algorithm', () => { it('always shows Context Switch field regardless of algorithm', () => {
render(<SchedulerConfig />); render(<SchedulerConfig />);
expect(screen.getByLabelText('Tempo de context switch')).toBeInTheDocument(); expect(screen.getByLabelText('Context switch time')).toBeInTheDocument();
}); });
it('always shows read-only preemptive badge', () => { it('always shows read-only preemptive badge', () => {
@@ -34,23 +34,23 @@ describe('SchedulerConfig', () => {
expect(screen.getByTestId('preemptive-badge')).toBeInTheDocument(); expect(screen.getByTestId('preemptive-badge')).toBeInTheDocument();
}); });
it('badge shows "Não-preemptivo" for FCFS', () => { it('badge shows "Non-preemptive" for FCFS', () => {
render(<SchedulerConfig />); render(<SchedulerConfig />);
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Não-preemptivo'); expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Non-preemptive');
}); });
it('badge shows "Preemptivo" for SJF_P (SRTF)', () => { it('badge shows "Preemptive" for SJF_P (SRTF)', () => {
setAlgorithm('SJF_P'); setAlgorithm('SJF_P');
render(<SchedulerConfig />); render(<SchedulerConfig />);
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Preemptivo'); expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Preemptive');
}); });
it('badge shows "Não-preemptivo" for SJF_NP (preemptive toggle no longer exists)', () => { it('badge shows "Non-preemptive" for SJF_NP (preemptive toggle no longer exists)', () => {
setAlgorithm('SJF_NP'); setAlgorithm('SJF_NP');
render(<SchedulerConfig />); render(<SchedulerConfig />);
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Não-preemptivo'); expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Non-preemptive');
// Confirm the old interactive preemptive switch is gone // Confirm the old interactive preemptive switch is gone
expect(screen.queryByRole('switch', { name: /Preemptivo/i })).not.toBeInTheDocument(); expect(screen.queryByRole('switch', { name: /Preemptive/i })).not.toBeInTheDocument();
}); });
it('does NOT show Quantum field for FCFS', () => { it('does NOT show Quantum field for FCFS', () => {
@@ -62,7 +62,7 @@ describe('SchedulerConfig', () => {
setAlgorithm('RR'); setAlgorithm('RR');
render(<SchedulerConfig />); render(<SchedulerConfig />);
expect(screen.getByTestId('quantum-field')).toBeInTheDocument(); expect(screen.getByTestId('quantum-field')).toBeInTheDocument();
expect(screen.getByLabelText('Quantum do Round Robin')).toBeInTheDocument(); expect(screen.getByLabelText('Round Robin quantum')).toBeInTheDocument();
}); });
it('shows MLQ queue config panel when algorithm is MULTILEVEL', () => { it('shows MLQ queue config panel when algorithm is MULTILEVEL', () => {
@@ -97,13 +97,13 @@ describe('SchedulerConfig', () => {
const agingToggle = screen.getByRole('switch', { name: /Aging/i }); const agingToggle = screen.getByRole('switch', { name: /Aging/i });
await user.click(agingToggle); await user.click(agingToggle);
expect(screen.getByTestId('aging-interval-field')).toBeInTheDocument(); expect(screen.getByTestId('aging-interval-field')).toBeInTheDocument();
expect(screen.getByLabelText('Intervalo de aging')).toBeInTheDocument(); expect(screen.getByLabelText('Aging interval')).toBeInTheDocument();
}); });
it('updates the store when Quantum field is changed', () => { it('updates the store when Quantum field is changed', () => {
setAlgorithm('RR'); setAlgorithm('RR');
render(<SchedulerConfig />); render(<SchedulerConfig />);
const quantumInput = screen.getByLabelText('Quantum do Round Robin'); const quantumInput = screen.getByLabelText('Round Robin quantum');
fireEvent.change(quantumInput, { target: { value: '5' } }); fireEvent.change(quantumInput, { target: { value: '5' } });
expect(useProcessStore.getState().config.quantum).toBe(5); expect(useProcessStore.getState().config.quantum).toBe(5);
}); });
+2 -2
View File
@@ -236,11 +236,11 @@ describe("SimulationEngine — ciclo de vida", () => {
}); });
// ============================================================ // ============================================================
// SimulationEngine — chegada de processos // SimulationEngine — process arrival
// Tests: late arrivals, idle ticks while waiting for process // Tests: late arrivals, idle ticks while waiting for process
// ============================================================ // ============================================================
describe("SimulationEngine — chegada de processos", () => { describe("SimulationEngine — process arrival", () => {
it("process arriving after simulation starts is correctly scheduled", () => { it("process arriving after simulation starts is correctly scheduled", () => {
const engine = new SimulationEngine( const engine = new SimulationEngine(
[ [
+1 -1
View File
@@ -107,7 +107,7 @@ export const presets: Preset[] = [
}, },
{ {
name: "context_switch_demo", name: "context_switch_demo",
description: "3 processos FCFS com 2 ticks de overhead — veja as células vermelhas de Context Switch", description: "3 FCFS processes with 2 ticks of overhead — see the red Context Switch cells",
processes: [ processes: [
{ pid: 1, id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 3 }] }, { pid: 1, id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 3 }] },
{ pid: 2, id: "p2", name: "P2", arrivalTime: 0, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] }, { pid: 2, id: "p2", name: "P2", arrivalTime: 0, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] },
+9 -2
View File
@@ -18,7 +18,9 @@ function downloadBlob(content: string, filename: string, mimeType: string) {
export function useExport() { export function useExport() {
const exportCSV = (metrics: ProcessMetrics[], processMap: ProcessMap) => { const exportCSV = (metrics: ProcessMetrics[], processMap: ProcessMap) => {
const headers = [ const headers = [
"Processo", "PID",
"Process",
"Priority",
"Arrival", "Arrival",
"Start", "Start",
"Finish", "Finish",
@@ -32,9 +34,14 @@ export function useExport() {
]; ];
const rows = metrics.map((m) => { const rows = metrics.map((m) => {
const name = processMap[m.processId]?.name ?? m.processId; const proc = processMap[m.processId];
const pid = proc?.pid ?? "";
const name = proc?.name ?? m.processId;
const priority = proc?.priority ?? "";
return [ return [
pid,
name, name,
priority,
m.arrivalTime, m.arrivalTime,
m.startTick, m.startTick,
m.finishTick, m.finishTick,