feat: Implement initial Process Scheduling Simulator application with core logic, algorithms, UI components, and project setup.

This commit is contained in:
Lucas Guerra committed 2026-03-19 14:12:21 -03:00
commit 083fc0027e
54 files changed
+8815

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
!**/glob-import/dir/node_modules
.DS_Store
.idea
.pnpm-store
*.cpuprofile
*.local
*.log
/.vscode/
/docs/.vitepress/cache
/docs/.vitepress/.temp
/packages/vite/LICENSE
dist
dist-ssr
explorations
node_modules
playground-temp
temp
TODOs.md
.eslintcache
+713
View File
@@ -0,0 +1,713 @@
# Process Scheduler Simulator — Especificação Técnica Completa
> **Versão:** 1.0
> **Data:** 2026-03-19
> **Autor:** Lucas Rayan Guerra
> **Stack alvo:** React + Vite (Single Page Application, browser-only, sem backend)
---
## 1. Visão Geral
O **Process Scheduler Simulator** é uma ferramenta educacional interativa que roda inteiramente no navegador. Ele permite ao usuário criar processos, escolher um algoritmo de escalonamento, executar a simulação passo a passo ou de forma automática, visualizar o diagrama de Gantt em tempo real e analisar métricas detalhadas por processo e globais.
### 1.1 Objetivos
| # | Objetivo |
|---|----------|
| O1 | Simular fielmente os algoritmos clássicos de escalonamento de SO |
| O2 | Permitir execução passo a passo para fins didáticos |
| O3 | Exibir diagrama de Gantt animado com estados de CPU e I/O |
| O4 | Calcular e exibir métricas completas por processo e agregadas |
| O5 | Ser completamente autocontido — sem servidor, sem dependências externas de dados |
### 1.2 Público-Alvo
- Estudantes de Sistemas Operacionais (graduação/pós)
- Professores que queiram usar em sala de aula
- Profissionais revisando fundamentos de SO
---
## 2. Requisitos Funcionais
### RF-01 — Gerenciamento de Processos
| ID | Descrição |
|----|-----------|
| RF-01.1 | O usuário pode **criar** um processo com os atributos listados na seção 3.1 |
| RF-01.2 | O usuário pode **editar** um processo antes de iniciar a simulação |
| RF-01.3 | O usuário pode **remover** um processo individual ou **limpar** todos |
| RF-01.4 | O usuário pode **gerar processos aleatórios** com parâmetros configuráveis (quantidade, faixas de valores) |
| RF-01.5 | A lista de processos exibe uma prévia das propriedades e a classificação CPU/I/O Bound estimada |
| RF-01.6 | Mínimo de 1 processo, máximo de 16 processos por simulação |
### RF-02 — Configuração do Escalonador
| ID | Descrição |
|----|-----------|
| RF-02.1 | O usuário seleciona **um** algoritmo de escalonamento por simulação |
| RF-02.2 | Parâmetros específicos de algoritmo são exibidos dinamicamente (ex.: quantum para RR) |
| RF-02.3 | O usuário configura o **tempo de context switch** (0–10 unidades de tempo, padrão 0) |
| RF-02.4 | O usuário configura se o escalonador é **preemptivo ou cooperativo** (onde aplicável) |
### RF-03 — Simulação
| ID | Descrição |
|----|-----------|
| RF-03.1 | **Play** inicia/retoma a simulação com velocidade configurável |
| RF-03.2 | **Pause** suspende a execução sem perder o estado |
| RF-03.3 | **Step** avança exatamente 1 unidade de tempo (tick) |
| RF-03.4 | **Reset** retorna ao estado inicial sem perder a configuração de processos |
| RF-03.5 | **Velocidade** ajustável: 0.25×, 0.5×, 1×, 2×, 4× (padrão 1×) |
| RF-03.6 | A simulação termina quando todos os processos atingem estado **Terminated** |
| RF-03.7 | O tick atual é exibido em destaque no painel de controle |
### RF-04 — Diagrama de Gantt
| ID | Descrição |
|----|-----------|
| RF-04.1 | Cada processo ocupa uma linha horizontal no diagrama |
| RF-04.2 | Cada tick é representado por uma célula colorida conforme o estado |
| RF-04.3 | O tick atual é destacado com uma linha vertical animada |
| RF-04.4 | Legenda de cores dos estados exibida abaixo do diagrama |
| RF-04.5 | O diagrama faz scroll horizontal automático para acompanhar o tick atual |
| RF-04.6 | Hovering sobre uma célula exibe tooltip: processo, tick, estado, detalhes |
### RF-05 — Métricas
| ID | Descrição |
|----|-----------|
| RF-05.1 | Métricas são calculadas por processo ao final da simulação |
| RF-05.2 | Métricas globais (médias, totais) são exibidas em painel separado |
| RF-05.3 | Um gráfico de barras comparativo exibe o turnaround e waiting time por processo |
| RF-05.4 | A classificação CPU Bound / I/O Bound é determinada automaticamente (ver seção 5) |
| RF-05.5 | As métricas podem ser exportadas como CSV |
### RF-06 — Persistência Local
| ID | Descrição |
|----|-----------|
| RF-06.1 | O estado atual (processos + configuração) é salvo automaticamente no `localStorage` |
| RF-06.2 | O usuário pode exportar/importar configurações como JSON |
| RF-06.3 | Cenários pré-definidos (presets) estão disponíveis para carregamento rápido |
---
## 3. Modelo de Dados
### 3.1 Processo (`Process`)
```typescript
interface BurstSegment {
type: "cpu" | "io";
duration: number; // unidades de tempo
}
interface Process {
id: string; // UUID gerado na criação
name: string; // "P1", "P2" ou nome personalizado
arrivalTime: number; // tick de chegada na fila ready (≥ 0)
priority: number; // 1 (maior prioridade) a 10 (menor); padrão 5
color: string; // cor HEX para o diagrama
bursts: BurstSegment[]; // sequência alternada CPU→I/O→CPU→...
// deve começar e terminar com CPU
}
```
**Exemplo de processo:**
```json
{
"id": "p1",
"name": "P1",
"arrivalTime": 0,
"priority": 3,
"color": "#4FC3F7",
"bursts": [
{ "type": "cpu", "duration": 4 },
{ "type": "io", "duration": 3 },
{ "type": "cpu", "duration": 2 }
]
}
```
> **Regra:** `bursts` deve ter estrutura CPU → (IO → CPU)\* — sempre começa com CPU. Mínimo: 1 burst CPU.
### 3.2 Configuração do Escalonador (`SchedulerConfig`)
```typescript
interface SchedulerConfig {
algorithm: SchedulingAlgorithm;
quantum: number; // apenas para RR (padrão: 2)
contextSwitchTime: number; // overhead entre processos (padrão: 0)
isPreemptive: boolean; // aplicável a SJF e Priority
agingEnabled: boolean; // evitar starvation em Priority
agingInterval: number; // ticks para incremento de prioridade
}
```
### 3.3 Estado de Execução do Processo (`ProcessState`)
```typescript
type StateLabel =
| "New" // antes do arrivalTime
| "Ready" // na fila, aguardando CPU
| "Running" // executando burst CPU
| "Waiting" // executando burst I/O
| "Terminated"; // concluído
interface ProcessRuntime {
processId: string;
state: StateLabel;
remainingBurst: number; // ticks restantes no burst atual
burstIndex: number; // índice atual em Process.bursts
arrivalTick: number; // tick em que entrou na fila Ready pela 1ª vez
startTick: number | null; // tick da 1ª vez em Running
finishTick: number | null; // tick em que terminou
waitingTime: number; // acumulado em Ready
cpuTime: number; // acumulado em Running
ioTime: number; // acumulado em Waiting
responseTime: number | null;
}
```
### 3.4 Tick de Simulação (`SimTick`)
```typescript
interface SimTick {
tick: number;
cpuProcess: string | null; // PID executando na CPU
ioProcesses: string[]; // PIDs em I/O
readyQueue: string[]; // ordem da fila ready
states: Record<string, StateLabel>;
contextSwitching: boolean; // overhead ativo
}
```
---
## 4. Algoritmos de Escalonamento
### 4.1 Lista de Algoritmos Suportados
| Código | Nome | Preemptivo? | Parâmetro extra |
|--------|------|-------------|-----------------|
| `FCFS` | First Come First Served | Não | — |
| `SJF_NP` | Shortest Job First (não-preemptivo) | Não | — |
| `SJF_P` | Shortest Remaining Time First (preemptivo) | Sim | — |
| `RR` | Round Robin | Sim | Quantum |
| `PRIORITY_NP` | Priority Scheduling (não-preemptivo) | Não | — |
| `PRIORITY_P` | Priority Scheduling (preemptivo) | Sim | — |
| `MULTILEVEL` | Multilevel Queue | Sim | Nº de filas, quantum por fila |
| `HRRN` | Highest Response Ratio Next | Não | — |
### 4.2 Regras de Desempate
Para todos os algoritmos, quando dois processos têm o mesmo critério de seleção, o desempate é feito por ordem de chegada (`arrivalTime`) e depois por `id` lexicográfico.
### 4.3 Especificação de Comportamento por Algoritmo
#### FCFS
- A fila ready é ordenada por `arrivalTime` ascendente.
- O processo na cabeça da fila ocupa a CPU até terminar seu burst CPU atual.
- Ao término do burst CPU, se houver burst I/O, transita para Waiting.
- Ao terminar o I/O, retorna para a fila Ready (cauda).
#### SJF Não-Preemptivo
- Na seleção, escolhe o processo com **menor burst CPU atual** (`remainingBurst`).
- Não interrompe o processo em execução.
#### SRTF (SJF Preemptivo)
- A cada tick, verifica se algum processo na fila Ready tem `remainingBurst` menor que o processo atual.
- Se sim, realiza preempção (com context switch se configurado).
#### Round Robin
- Usa fila FIFO com quantum `q`.
- Ao expirar o quantum, o processo retorna ao fim da fila Ready se ainda tiver burst CPU restante.
- I/O bursts não consomem quantum.
#### Priority (Não-Preemptivo / Preemptivo)
- Menor valor numérico = maior prioridade.
- Com aging habilitado: a cada `agingInterval` ticks em Ready, `priority--` (até mínimo 1).
#### HRRN
- `ratio = (waitingTime + burstTime) / burstTime`
- A cada seleção, escolhe o processo com maior ratio.
- Não-preemptivo.
---
## 5. Definição de CPU Bound vs. I/O Bound
A classificação é feita **por processo**, baseada na proporção de tempo total gasto:
```
cpuRatio = cpuTime / (cpuTime + ioTime)
ioRatio = ioTime / (cpuTime + ioTime)
if cpuRatio >= 0.65 → "CPU Bound"
if ioRatio >= 0.65 → "I/O Bound"
else → "Balanced"
```
A classificação aparece:
- Na lista de processos (badge de preview, calculado sobre os bursts declarados)
- Na tabela de métricas (calculado sobre os tempos reais simulados)
---
## 6. Métricas
### 6.1 Métricas por Processo
| Métrica | Fórmula | Descrição |
|---------|---------|-----------|
| **Arrival Time** | `arrivalTime` | Tick de chegada na fila |
| **First CPU Tick** | `startTick` | Primeiro tick em Running |
| **Finish Time** | `finishTick` | Tick de término |
| **Response Time** | `startTick − arrivalTime` | Tempo até 1ª vez na CPU |
| **Turnaround Time** | `finishTick − arrivalTime` | Tempo total de vida |
| **Waiting Time** | `turnaround − cpuTime − ioTime` | Tempo na fila Ready |
| **CPU Time** | Σ Running ticks | Total de CPU consumida |
| **I/O Time** | Σ Waiting ticks | Total de I/O |
| **CPU Utilization** | `cpuTime / turnaround × 100` | % do turnaround em CPU |
| **Bound Type** | ver seção 5 | CPU Bound / I/O Bound / Balanced |
### 6.2 Métricas Globais
| Métrica | Fórmula |
|---------|---------|
| **Avg Response Time** | Média dos response times |
| **Avg Turnaround Time** | Média dos turnarounds |
| **Avg Waiting Time** | Média dos waiting times |
| **CPU Throughput** | `processos concluídos / tempo total` |
| **CPU Utilization (global)** | `Σ Running ticks / tempo total × 100` |
| **Total Simulation Time** | Tick final |
---
## 7. Arquitetura da Aplicação
### 7.1 Stack Tecnológica
```
Runtime: React 18 + Vite 5
Linguagem: TypeScript 5
Estilos: Tailwind CSS 3 + CSS Modules (para componentes específicos)
Gráficos: Recharts (gráfico de barras de métricas)
Estado: Zustand (store global)
Persistência: localStorage (auto-save) + JSON import/export
Testes: Vitest + @testing-library/react
```
### 7.2 Estrutura de Pastas
```
src/
├── core/ # Lógica pura — sem React
│ ├── algorithms/
│ │ ├── fcfs.ts
│ │ ├── sjf.ts
│ │ ├── rr.ts
│ │ ├── priority.ts
│ │ ├── hrrn.ts
│ │ ├── multilevel.ts
│ │ └── index.ts # dispatch por algoritmo
│ ├── engine.ts # SimulationEngine — step(), runAll()
│ ├── metrics.ts # cálculo de métricas
│ ├── types.ts # todas as interfaces
│ └── presets.ts # cenários pré-definidos
│
├── store/
│ ├── processStore.ts # CRUD de processos
│ ├── simulationStore.ts # estado da simulação, ticks
│ └── uiStore.ts # estado da UI (painéis abertos, velocidade)
│
├── components/
│ ├── ProcessPanel/ # criação/edição de processos
│ │ ├── ProcessList.tsx
│ │ ├── ProcessForm.tsx
│ │ └── ProcessCard.tsx
│ ├── SchedulerPanel/ # seleção de algoritmo e parâmetros
│ │ ├── AlgorithmSelector.tsx
│ │ └── SchedulerConfig.tsx
│ ├── ControlBar/ # play, pause, step, reset, velocidade
│ │ └── ControlBar.tsx
│ ├── GanttChart/ # diagrama de Gantt
│ │ ├── GanttChart.tsx
│ │ ├── GanttRow.tsx
│ │ ├── GanttCell.tsx
│ │ └── GanttLegend.tsx
│ ├── MetricsPanel/ # tabela + gráficos
│ │ ├── MetricsTable.tsx
│ │ ├── MetricsChart.tsx
│ │ └── GlobalMetrics.tsx
│ └── shared/ # botões, badges, tooltips, modais
│
├── hooks/
│ ├── useSimulation.ts # loop de simulação com requestAnimationFrame
│ ├── useLocalStorage.ts
│ └── useExport.ts
│
├── App.tsx
└── main.tsx
```
### 7.3 Fluxo de Dados
```
[ProcessForm] ──create/edit──► [processStore]
│
[AlgorithmSelector] ──config──► [simulationStore]
│
[SimulationEngine]
┌──────┴──────┐
step() runAll()
└──────┬──────┘
SimTick[]
│
┌─────────────────┼────────────────┐
[GanttChart] [ControlBar] [MetricsPanel]
```
---
## 8. Motor de Simulação (`SimulationEngine`)
### 8.1 Interface Pública
```typescript
class SimulationEngine {
constructor(processes: Process[], config: SchedulerConfig);
// Avança 1 tick; retorna o snapshot daquele tick
step(): SimTick;
// Executa todos os ticks até o fim; retorna array completo
runAll(): SimTick[];
// Verdadeiro se todos os processos estão Terminated
get isFinished(): boolean;
// Tick atual
get currentTick(): number;
// Estado atual de todos os processos
get runtimeStates(): ProcessRuntime[];
}
```
### 8.2 Loop Principal (pseudocódigo)
```
function step():
tick++
// 1. Verificar chegadas de novos processos
for each process where arrivalTime == tick:
transitionTo(process, "Ready")
addToReadyQueue(process)
// 2. Aging (se habilitado)
if agingEnabled and tick % agingInterval == 0:
for each process in readyQueue:
process.priority = max(1, process.priority - 1)
// 3. Verificar fim de I/O
for each process in ioQueue:
process.remainingBurst--
if process.remainingBurst == 0:
advanceBurst(process)
if hasMoreBursts(process):
transitionTo(process, "Ready")
addToReadyQueue(process)
else:
transitionTo(process, "Terminated")
process.finishTick = tick
// 4. Verificar fim do burst CPU atual
if cpuProcess != null:
cpuProcess.remainingBurst--
cpuProcess.cpuTime++
if cpuProcess.remainingBurst == 0:
advanceBurst(cpuProcess)
if hasIoBurst(cpuProcess):
transitionTo(cpuProcess, "Waiting")
addToIoQueue(cpuProcess)
cpuProcess = null
else if hasMoreCpuBurst(cpuProcess):
// volta pra ready (ex: após I/O futuro)
else:
transitionTo(cpuProcess, "Terminated")
cpuProcess = null
// 5. Verificar preempção (algoritmos preemptivos)
if isPreemptive and cpuProcess != null:
candidate = algorithm.selectFromReady(readyQueue)
if shouldPreempt(candidate, cpuProcess):
preempt(cpuProcess)
cpuProcess = candidate
// 6. Selecionar próximo processo (se CPU livre)
if cpuProcess == null and readyQueue.length > 0:
cpuProcess = algorithm.selectFromReady(readyQueue)
if cpuProcess.startTick == null:
cpuProcess.startTick = tick
transitionTo(cpuProcess, "Running")
// 7. Acumular waiting time
for each process in readyQueue:
process.waitingTime++
return snapshot()
```
---
## 9. Interface do Usuário
### 9.1 Layout Geral (Desktop)
```
┌─────────────────────────────────────────────────────────────────────┐
│ HEADER: título, botão de preset, import/export │
├──────────────┬──────────────────────────────────────────────────────┤
│ │ │
│ ESQUERDA │ CENTRO/DIREITA │
│ (320px) │ │
│ │ ┌─────────────────────────────────────────────────┐ │
│ [Processos] │ │ BARRA DE CONTROLE (play/pause/step/reset/vel.) │ │
│ + lista │ └─────────────────────────────────────────────────┘ │
│ + formulário│ │
│ │ ┌─────────────────────────────────────────────────┐ │
│ ────────── │ │ GANTT CHART (scroll horizontal) │ │
│ │ │ Tick: 00000000000001111111111122222222222233... │ │
│ [Algoritmo] │ │ P1: ██████░░░░░░██████░░░░░█████ │ │
│ + parâmetros│ │ P2: ░░░░░░██████░░░░░░████░░░░░ │ │
│ │ └─────────────────────────────────────────────────┘ │
│ │ │
│ │ ┌─────────────────────────────────────────────────┐ │
│ │ │ MÉTRICAS (tabela por processo + gráficos) │ │
│ │ └─────────────────────────────────────────────────┘ │
└──────────────┴──────────────────────────────────────────────────────┘
```
### 9.2 Cores de Estado no Gantt
| Estado | Cor | Token CSS |
|--------|-----|-----------|
| New | Cinza escuro | `--state-new` |
| Ready | Amarelo âmbar | `--state-ready` |
| Running | Verde brilhante | `--state-running` |
| Waiting (I/O) | Azul ciano | `--state-waiting` |
| Terminated | Cinza claro | `--state-terminated` |
| Context Switch | Vermelho/laranja | `--state-ctx-switch` |
### 9.3 Formulário de Processo
Campos:
- **Nome** (text, max 8 chars)
- **Arrival Time** (number, ≥ 0)
- **Priority** (1–10)
- **Cor** (color picker)
- **Bursts** — editor visual de bursts com botões +/− por segmento
- Tipo: `CPU` | `I/O`
- Duração (number, ≥ 1)
- Validação: deve iniciar com CPU, alternar CPU/IO
### 9.4 Painel de Algoritmo
Exibe:
- Dropdown de seleção de algoritmo
- Campos condicionais:
- RR → `quantum`
- Priority → `preemptive toggle`, `aging toggle + interval`
- Multilevel → configuração de filas
- Context switch time (sempre)
- Tooltip explicativo por algoritmo
### 9.5 Barra de Controle
```
[⏮ Reset] [⏭ Step] [▶ Play / ⏸ Pause] [Tick: 0042] [Velocidade: 1×▾]
```
### 9.6 Painel de Métricas (após simulação)
**Tab 1 — Por Processo:**
Tabela com colunas:
`Processo | Arrival | Start | Finish | Response | Turnaround | Waiting | CPU Time | I/O Time | CPU% | Bound`
**Tab 2 — Globais:**
Cards com:
- Avg Response Time
- Avg Turnaround Time
- Avg Waiting Time
- CPU Throughput
- CPU Utilization %
- Total Simulation Time
**Tab 3 — Gráfico:**
Gráfico de barras agrupadas por processo:
- Barras: Waiting Time, CPU Time, I/O Time (empilhadas para turnaround)
---
## 10. Cenários Pré-definidos (Presets)
| Nome | Descrição | Algoritmo sugerido |
|------|-----------|-------------------|
| `classic_fcfs` | 4 processos simples, sem I/O | FCFS |
| `convoy_effect` | Demonstra o efeito de comboio do FCFS | FCFS vs SJF |
| `rr_demo` | 5 processos com quantum 3 | Round Robin |
| `starvation` | Prioridades que causam starvation | Priority NP |
| `aging_fix` | Mesmo cenário, com aging ativo | Priority P + Aging |
| `io_heavy` | Processos I/O Bound vs CPU Bound misturados | SRTF |
| `multilevel_demo` | Demonstra MLQ | Multilevel |
---
## 11. Requisitos Não-Funcionais
| RNF | Requisito |
|-----|-----------|
| RNF-01 | Desempenho: simulações com até 16 processos e 1000 ticks sem travamento |
| RNF-02 | Responsividade: layout funcional em telas ≥ 768px |
| RNF-03 | Acessibilidade: ARIA labels nos controles principais, contraste WCAG AA |
| RNF-04 | Offline-first: 100% funcional sem internet após build |
| RNF-05 | Build size < 500 KB gzipped |
| RNF-06 | Suporte a Chrome 120+, Firefox 120+, Safari 17+ |
---
## 12. Plano de Implementação (Fases)
### Fase 1 — Core Engine (sem UI) — *~2 dias*
- [ ] Definir todos os tipos em `core/types.ts`
- [ ] Implementar `SimulationEngine` com suporte a FCFS
- [ ] Testes unitários: FCFS, estados de transição, métricas
- [ ] Implementar SJF NP e RR com testes
- [ ] Implementar Priority NP/P com aging e testes
- [ ] Implementar SRTF e HRRN com testes
### Fase 2 — Store & State Management — *~1 dia*
- [ ] Configurar Zustand stores
- [ ] Hook `useSimulation` com loop de animação
- [ ] Integração com localStorage
### Fase 3 — UI Base — *~2 dias*
- [ ] Layout geral (sidebar + área principal)
- [ ] `ProcessForm` e `ProcessList`
- [ ] `AlgorithmSelector` e `SchedulerConfig`
- [ ] `ControlBar`
### Fase 4 — Gantt Chart — *~2 dias*
- [ ] Renderização de células por tick e processo
- [ ] Auto-scroll horizontal
- [ ] Tooltip por célula
- [ ] Legenda de estados
### Fase 5 — Métricas e Gráficos — *~1 dia*
- [ ] Tabela de métricas por processo
- [ ] Cards de métricas globais
- [ ] Gráfico de barras com Recharts
- [ ] Export CSV
### Fase 6 — Polimento e Presets — *~1 dia*
- [ ] Presets prontos para carregar
- [ ] Gerador de processos aleatórios
- [ ] Import/export JSON
- [ ] Responsividade mobile (≥768px)
- [ ] Refinamento visual
**Total estimado: ~9 dias de trabalho focado**
---
## 13. Casos de Teste Funcionais
### CT-01: FCFS Básico
**Entrada:**
```
P1: arrival=0, bursts=[CPU:4]
P2: arrival=1, bursts=[CPU:3]
P3: arrival=2, bursts=[CPU:1]
```
**Esperado:**
```
Gantt: P1(0-4), P2(4-7), P3(7-8)
P1: response=0, turnaround=4, waiting=0
P2: response=3, turnaround=6, waiting=3
P3: response=5, turnaround=6, waiting=5
```
### CT-02: Round Robin (quantum=2)
**Entrada:**
```
P1: arrival=0, bursts=[CPU:5]
P2: arrival=0, bursts=[CPU:3]
P3: arrival=0, bursts=[CPU:1]
quantum=2
```
**Esperado:**
```
Gantt: P1(0-2), P2(2-4), P3(4-5), P1(5-7), P2(7-8), P1(8-9)
P1: response=0, turnaround=9, waiting=4
P2: response=2, turnaround=8, waiting=5
P3: response=4, turnaround=5, waiting=4
```
### CT-03: Classificação Bound
**Entrada:**
```
P1: bursts=[CPU:8, IO:1, CPU:1] → cpuRatio = 9/10 = 90% → CPU Bound
P2: bursts=[CPU:1, IO:8, CPU:1] → cpuRatio = 2/10 = 20% → I/O Bound
P3: bursts=[CPU:4, IO:2, CPU:4] → cpuRatio = 8/10 = 80% → CPU Bound
```
### CT-04: Preempção SRTF
**Entrada:**
```
P1: arrival=0, bursts=[CPU:8]
P2: arrival=1, bursts=[CPU:4]
quantum=∞ (SRTF)
```
**Esperado:**
```
P1 executa tick 0-1, P2 preempta no tick 1 (remaining=3<7), P2 termina tick 5,
P1 retoma e termina tick 12
```
---
## 14. Glossário
| Termo | Definição |
|-------|-----------|
| **Tick** | Unidade atômica de tempo na simulação (equivale a 1 unidade de tempo de CPU) |
| **Burst** | Segmento contínuo de execução (CPU ou I/O) |
| **Quantum** | Fatia máxima de CPU no Round Robin |
| **Context Switch** | Overhead ao trocar o processo em execução na CPU |
| **Starvation** | Processo que nunca recebe CPU por processos de maior prioridade sempre chegando |
| **Aging** | Mecanismo que aumenta gradualmente a prioridade de processos esperando há muito tempo |
| **Turnaround Time** | Tempo total desde a chegada até o término do processo |
| **Response Time** | Tempo entre a chegada e o primeiro uso de CPU |
| **Waiting Time** | Tempo total em que o processo ficou na fila Ready |
| **CPU Bound** | Processo que passa a maior parte do tempo usando CPU |
| **I/O Bound** | Processo que passa a maior parte do tempo aguardando I/O |
| **FCFS** | First Come, First Served — ordem de chegada |
| **SJF** | Shortest Job First — menor burst next |
| **SRTF** | Shortest Remaining Time First — SJF preemptivo |
| **RR** | Round Robin — revezamento com quantum |
| **HRRN** | Highest Response Ratio Next — balanceia tempo de espera e burst |
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="pt-BR">
<head>
<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" />
<title>Process Scheduler Simulator</title>
</head>
<body class="bg-slate-900 text-slate-100 antialiased overflow-hidden selection:bg-blue-500/30">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+4563
View File
File diff suppressed because it is too large. Load diff
+50
View File
@@ -0,0 +1,50 @@
{
"name": "pss",
"version": "1.0.0",
"description": "O Process Scheduler Simulator é uma ferramenta educacional interativa que roda inteiramente no navegador",
"main": "index.js",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"clsx": "^2.1.1",
"lucide-react": "^0.577.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwind-merge": "^3.5.0",
"zustand": "^5.0.12"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.2",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^25.5.0",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^3.0.9",
"jsdom": "^29.0.0",
"tailwindcss": "^4.2.2",
"typescript": "~5.6.2",
"vite": "^6.2.2",
"vitest": "^3.0.9"
},
"repository": {
"type": "git",
"url": "git+https://github.com/lucasrguerra/pss.git"
},
"keywords": [],
"author": "Lucas Rayan Guerra",
"license": "ISC",
"type": "commonjs",
"bugs": {
"url": "https://github.com/lucasrguerra/pss/issues"
},
"homepage": "https://github.com/lucasrguerra/pss#readme"
}
+9
View File
@@ -0,0 +1,9 @@
import AppLayout from './components/Layout/AppLayout';
function App() {
return (
<AppLayout />
);
}
export default App;
+167
View File
@@ -0,0 +1,167 @@
import {
SkipBack, StepForward, Play, Pause, RotateCcw
} from 'lucide-react';
import { useProcessStore } from '../../store/processStore';
import { useSimulationStore, type SimStatus } from '../../store/simulationStore';
import { useUiStore, SPEED_OPTIONS } from '../../store/uiStore';
import IconButton from '../shared/IconButton';
const SPEED_LABELS: Record<number, string> = {
0.25: '0.25×',
0.5: '0.5×',
1: '1×',
2: '2×',
4: '4×',
};
const statusLabel: Record<SimStatus, string> = {
idle: 'Pronto',
paused: 'Pausado',
running: 'Executando',
finished: 'Concluído',
};
const statusDotClass: Record<SimStatus, string> = {
idle: 'bg-slate-500',
paused: 'bg-amber-400',
running: 'bg-emerald-400 animate-pulse',
finished: 'bg-blue-400',
};
const ControlBar = () => {
const processes = useProcessStore(s => s.processes);
const config = useProcessStore(s => s.config);
const status = useSimulationStore(s => s.status);
const ticks = useSimulationStore(s => s.ticks);
const { init, stepOnce, play, pause, reset } = useSimulationStore();
const speed = useUiStore(s => s.speed);
const setSpeed = useUiStore(s => s.setSpeed);
const currentTick = ticks.length;
const isIdle = status === 'idle';
const isRunning = status === 'running';
const isFinished = status === 'finished';
const canPlay = (status === 'paused' || status === 'idle') && !isFinished && processes.length > 0;
const handlePlay = () => {
if (isIdle) {
init(processes, config);
}
play();
};
const handleStep = () => {
if (isIdle) {
init(processes, config);
}
stepOnce();
};
const handleReset = () => {
if (isIdle) return;
reset();
};
return (
<div
className="h-16 flex items-center px-5 gap-3 bg-slate-800 border-b border-slate-700 shadow-sm"
role="toolbar"
aria-label="Controles da simulação"
>
{/* Left group: simulation controls */}
<div className="flex items-center gap-1.5">
{/* Reset */}
<IconButton
label="Reset"
variant="ghost"
onClick={handleReset}
disabled={isIdle}
data-testid="btn-reset"
>
<RotateCcw size={16} />
</IconButton>
{/* Step */}
<IconButton
label="Avançar 1 tick (Step)"
variant="default"
onClick={handleStep}
disabled={isFinished || processes.length === 0 || isRunning}
data-testid="btn-step"
>
<StepForward size={18} />
</IconButton>
{/* Play / Pause */}
{isRunning ? (
<IconButton
label="Pausar simulação"
variant="primary"
onClick={pause}
data-testid="btn-pause"
>
<Pause size={18} />
</IconButton>
) : (
<IconButton
label="Iniciar / Retomar simulação"
variant="primary"
onClick={handlePlay}
disabled={!canPlay && !isRunning}
data-testid="btn-play"
>
<Play size={18} />
</IconButton>
)}
</div>
{/* Divider */}
<div className="h-6 w-px bg-slate-700 mx-1" aria-hidden="true" />
{/* Tick display */}
<div
className="flex items-center gap-2 px-3 py-1 rounded-md bg-slate-900 border border-slate-700"
aria-live="polite"
aria-label={`Tick atual: ${currentTick}`}
>
<span className="text-[10px] text-slate-500 uppercase tracking-wide font-medium">Tick</span>
<span
className="font-mono font-bold text-blue-300 text-sm min-w-[3ch] text-right"
data-testid="tick-display"
>
{String(currentTick).padStart(4, '0')}
</span>
</div>
{/* Status badge */}
<div className="flex items-center gap-1.5">
<span className={`h-2 w-2 rounded-full ${statusDotClass[status]}`} aria-hidden="true" />
<span className="text-xs text-slate-400">{statusLabel[status]}</span>
</div>
{/* Right group: speed selector */}
<div className="ml-auto flex items-center gap-2">
<SkipBack size={14} className="text-slate-500" aria-hidden="true" />
<label htmlFor="speed-select" className="text-xs text-slate-400 sr-only">
Velocidade
</label>
<select
id="speed-select"
value={speed}
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"
aria-label="Velocidade de simulação"
data-testid="speed-select"
>
{SPEED_OPTIONS.map(s => (
<option key={s} value={s}>{SPEED_LABELS[s]}</option>
))}
</select>
</div>
</div>
);
};
export default ControlBar;
+13
View File
@@ -0,0 +1,13 @@
import Sidebar from './Sidebar';
import MainArea from './MainArea';
const AppLayout = () => {
return (
<div className="flex h-screen w-full bg-slate-900 text-slate-200 overflow-hidden font-sans">
<Sidebar />
<MainArea />
</div>
);
};
export default AppLayout;
+25
View File
@@ -0,0 +1,25 @@
import ControlBar from '../ControlBar/ControlBar';
const MainArea = () => {
return (
<main className="flex-1 flex flex-col h-full bg-slate-900 overflow-hidden relative">
<div className="flex-1 flex flex-col">
<ControlBar />
{/* Placeholder for GanttChart — Phase 4 */}
<div className="border-b border-slate-800 h-80 flex items-center justify-center text-slate-600 bg-slate-900/50 text-sm">
[ Gantt Chart — Fase 4 ]
</div>
{/* Placeholder for MetricsPanel — Phase 5 */}
<div className="flex-1 flex items-center justify-center text-slate-600 bg-slate-950 p-6">
<div className="w-full h-full border border-dashed border-slate-700 rounded-xl flex items-center justify-center text-sm">
[ Metrics Panel — Fase 5 ]
</div>
</div>
</div>
</main>
);
};
export default MainArea;
+27
View File
@@ -0,0 +1,27 @@
import ProcessPanel from '../ProcessPanel/ProcessPanel';
import SchedulerPanel from '../SchedulerPanel/SchedulerPanel';
const Sidebar = () => {
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">
<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">
Process Simulator
</h1>
</div>
<div className="flex-1 overflow-y-auto overflow-x-hidden p-4 space-y-6 scrollbar-hide">
<ProcessPanel />
<div className="border-t border-slate-700 pt-4">
<SchedulerPanel />
</div>
</div>
<div className="h-12 border-t border-slate-700 flex items-center justify-center text-xs text-slate-500">
PSS v1.0
</div>
</aside>
);
};
export default Sidebar;
@@ -0,0 +1,73 @@
import { Pencil, Trash2 } from 'lucide-react';
import type { Process } from '@core/types';
import IconButton from '../shared/IconButton';
import Badge from '../shared/Badge';
import { classifyBound } from '../shared/processUtils';
interface ProcessCardProps {
process: Process;
onEdit: (process: Process) => void;
onRemove: (id: string) => void;
}
const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
const bound = classifyBound(process.bursts);
const totalCpu = process.bursts.filter(b => b.type === 'cpu').reduce((a, b) => a + b.duration, 0);
const totalIo = process.bursts.filter(b => b.type === 'io').reduce((a, b) => a + b.duration, 0);
const boundVariant = bound === 'CPU Bound' ? 'cpu' : bound === 'I/O Bound' ? 'io' : 'balanced';
return (
<div
className="group relative flex items-start gap-3 rounded-lg bg-slate-800 border border-slate-700 p-3 hover:border-slate-500 transition-colors"
data-testid={`process-card-${process.id}`}
>
{/* Color indicator */}
<div
className="mt-0.5 h-8 w-1.5 rounded-full flex-shrink-0"
style={{ backgroundColor: process.color }}
aria-hidden="true"
/>
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="font-semibold text-sm text-slate-100 truncate">{process.name}</span>
<Badge variant={boundVariant}>{bound}</Badge>
</div>
<div className="flex flex-wrap gap-x-3 gap-y-0.5 text-[11px] text-slate-400">
<span>Arrival: <span className="text-slate-300 font-medium">{process.arrivalTime}</span></span>
<span>Priority: <span className="text-slate-300 font-medium">{process.priority}</span></span>
<span>CPU: <span className="text-emerald-400 font-medium">{totalCpu}</span></span>
{totalIo > 0 && (
<span>I/O: <span className="text-cyan-400 font-medium">{totalIo}</span></span>
)}
<span>Bursts: <span className="text-slate-300 font-medium">{process.bursts.length}</span></span>
</div>
</div>
{/* Actions */}
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
<IconButton
size="sm"
variant="ghost"
label={`Editar ${process.name}`}
onClick={() => onEdit(process)}
>
<Pencil size={13} />
</IconButton>
<IconButton
size="sm"
variant="ghost"
label={`Remover ${process.name}`}
onClick={() => onRemove(process.id)}
className="hover:text-red-400 hover:bg-red-900/20"
>
<Trash2 size={13} />
</IconButton>
</div>
</div>
);
};
export default ProcessCard;
+256
View File
@@ -0,0 +1,256 @@
import { useState, useEffect } from 'react';
import { Plus, Minus, Check, X } from 'lucide-react';
import type { Process, BurstSegment } from '@core/types';
import { useProcessStore } from '../../store/processStore';
import IconButton from '../shared/IconButton';
import { generateId, nextColor } from '../shared/processUtils';
interface ProcessFormProps {
/** If provided, the form edits this process; otherwise creates a new one. */
editingProcess?: Process | null;
onClose: () => void;
}
const DEFAULT_BURSTS: BurstSegment[] = [{ type: 'cpu', duration: 4 }];
const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
const addProcess = useProcessStore(s => s.addProcess);
const updateProcess = useProcessStore(s => s.updateProcess);
const isEditing = !!editingProcess;
const [name, setName] = useState('');
const [arrivalTime, setArrivalTime] = useState(0);
const [priority, setPriority] = useState(5);
const [color, setColor] = useState(nextColor);
const [bursts, setBursts] = useState<BurstSegment[]>(DEFAULT_BURSTS);
const [errors, setErrors] = useState<string[]>([]);
useEffect(() => {
if (editingProcess) {
setName(editingProcess.name);
setArrivalTime(editingProcess.arrivalTime);
setPriority(editingProcess.priority);
setColor(editingProcess.color);
setBursts([...editingProcess.bursts.map(b => ({ ...b }))]);
} else {
setName('');
setArrivalTime(0);
setPriority(5);
setColor(nextColor());
setBursts([{ type: 'cpu', duration: 4 }]);
}
setErrors([]);
}, [editingProcess]);
// ---- Burst helpers ----
const updateBurstDuration = (index: number, value: number) => {
setBursts(prev => prev.map((b, i) => i === index ? { ...b, duration: Math.max(1, value) } : b));
};
const addBurst = () => {
// enforce alternating cpu/io pattern
const lastType = bursts[bursts.length - 1]?.type ?? 'io';
const nextType: BurstSegment['type'] = lastType === 'cpu' ? 'io' : 'cpu';
setBursts(prev => [...prev, { type: nextType, duration: 2 }]);
};
const removeBurst = (index: number) => {
if (bursts.length <= 1) return;
setBursts(prev => {
const next = prev.filter((_, i) => i !== index);
// Safe-guard: ensure it still starts (and ends) with cpu
return next;
});
};
// ---- Validation ----
const validate = (): boolean => {
const errs: string[] = [];
if (!name.trim()) errs.push('Nome é obrigatório');
if (name.trim().length > 8) errs.push('Nome deve ter no máximo 8 caracteres');
if (arrivalTime < 0) errs.push('Arrival Time deve ser ≥ 0');
if (priority < 1 || priority > 10) errs.push('Priority deve estar entre 1 e 10');
if (bursts.length === 0) errs.push('Deve haver ao menos 1 burst CPU');
if (bursts[0]?.type !== 'cpu') errs.push('O primeiro burst deve ser CPU');
if (bursts.at(-1)?.type !== 'cpu') errs.push('O último burst deve ser CPU');
bursts.forEach((b, i) => {
if (b.duration < 1) errs.push(`Burst ${i + 1}: duração deve ser ≥ 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)`);
});
setErrors(errs);
return errs.length === 0;
};
const handleSubmit = () => {
if (!validate()) return;
if (isEditing && editingProcess) {
updateProcess(editingProcess.id, { name: name.trim(), arrivalTime, priority, color, bursts });
} else {
const p: Process = {
id: generateId(),
name: name.trim(),
arrivalTime,
priority,
color,
bursts,
};
addProcess(p);
}
onClose();
};
const inputCls = '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';
const labelCls = 'block text-xs text-slate-400 mb-1';
return (
<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">
<h3 className="text-sm font-semibold text-slate-200">
{isEditing ? 'Editar Processo' : 'Novo Processo'}
</h3>
<IconButton size="sm" variant="ghost" label="Fechar" onClick={onClose}>
<X size={14} />
</IconButton>
</div>
{/* Basic fields */}
<div className="grid grid-cols-2 gap-3">
<div className="col-span-2 flex gap-3">
{/* Name */}
<div className="flex-1">
<label className={labelCls} htmlFor="pf-name">Nome</label>
<input
id="pf-name"
type="text"
maxLength={8}
value={name}
onChange={e => setName(e.target.value)}
placeholder="P1"
className={inputCls}
aria-label="Nome do processo"
/>
</div>
{/* Color */}
<div>
<label className={labelCls} htmlFor="pf-color">Cor</label>
<input
id="pf-color"
type="color"
value={color}
onChange={e => setColor(e.target.value)}
className="h-[34px] w-12 rounded-md border border-slate-600 bg-slate-700 cursor-pointer"
aria-label="Cor do processo"
/>
</div>
</div>
{/* Arrival */}
<div>
<label className={labelCls} htmlFor="pf-arrival">Arrival Time</label>
<input
id="pf-arrival"
type="number"
min={0}
value={arrivalTime}
onChange={e => setArrivalTime(Number(e.target.value))}
className={inputCls}
aria-label="Tempo de chegada"
/>
</div>
{/* Priority */}
<div>
<label className={labelCls} htmlFor="pf-priority">Priority (1–10)</label>
<input
id="pf-priority"
type="number"
min={1}
max={10}
value={priority}
onChange={e => setPriority(Number(e.target.value))}
className={inputCls}
aria-label="Prioridade do processo"
/>
</div>
</div>
{/* Bursts editor */}
<div>
<div className="flex items-center justify-between mb-2">
<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}>
<Plus size={13} />
</IconButton>
</div>
<div className="space-y-1.5" data-testid="burst-list">
{bursts.map((burst, idx) => (
<div
key={idx}
className={`flex items-center gap-2 px-2 py-1.5 rounded-md border ${
burst.type === 'cpu'
? 'border-emerald-700/50 bg-emerald-900/20'
: 'border-cyan-700/50 bg-cyan-900/20'
}`}
>
<span className={`text-[10px] font-bold uppercase w-7 tracking-wider ${burst.type === 'cpu' ? 'text-emerald-400' : 'text-cyan-400'}`}>
{burst.type}
</span>
<input
type="number"
min={1}
value={burst.duration}
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"
aria-label={`Duração do burst ${idx + 1}`}
/>
<span className="text-[10px] text-slate-500">ticks</span>
<IconButton
size="sm"
variant="ghost"
label={`Remover burst ${idx + 1}`}
onClick={() => removeBurst(idx)}
disabled={bursts.length <= 1}
className="hover:text-red-400"
>
<Minus size={12} />
</IconButton>
</div>
))}
</div>
</div>
{/* Validation errors */}
{errors.length > 0 && (
<ul className="space-y-0.5" role="alert" aria-live="polite">
{errors.map((e, i) => (
<li key={i} className="text-[11px] text-red-400">{e}</li>
))}
</ul>
)}
{/* Actions */}
<div className="flex gap-2 pt-1">
<button
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"
aria-label={isEditing ? 'Salvar processo' : 'Criar processo'}
>
<Check size={15} />
{isEditing ? 'Salvar' : 'Criar'}
</button>
<button
onClick={onClose}
className="px-4 bg-slate-700 hover:bg-slate-600 text-slate-300 text-sm rounded-lg transition-colors"
aria-label="Cancelar"
>
Cancelar
</button>
</div>
</div>
);
};
export default ProcessForm;
+109
View File
@@ -0,0 +1,109 @@
import { Plus, Shuffle, Trash2 } from 'lucide-react';
import type { Process } from '@core/types';
import { useProcessStore } from '../../store/processStore';
import ProcessCard from './ProcessCard';
import IconButton from '../shared/IconButton';
import { generateId, nextColor } from '../shared/processUtils';
interface ProcessListProps {
onEditProcess: (process: Process) => void;
onAddNew: () => void;
}
const MAX_PROCESSES = 16;
/** Generates a random process for quick testing */
function makeRandomProcess(index: number): Process {
const cpuDur = Math.floor(Math.random() * 6) + 1;
const ioDur = Math.floor(Math.random() * 5) + 1;
const hasBothBursts = Math.random() > 0.4;
return {
id: generateId(),
name: `P${index}`,
arrivalTime: Math.floor(Math.random() * 8),
priority: Math.floor(Math.random() * 10) + 1,
color: nextColor(),
bursts: hasBothBursts
? [{ type: 'cpu', duration: cpuDur }, { type: 'io', duration: ioDur }, { type: 'cpu', duration: Math.floor(Math.random() * 4) + 1 }]
: [{ type: 'cpu', duration: cpuDur }],
};
}
const ProcessList = ({ onEditProcess, onAddNew }: ProcessListProps) => {
const processes = useProcessStore(s => s.processes);
const removeProcess = useProcessStore(s => s.removeProcess);
const clearProcesses = useProcessStore(s => s.clearProcesses);
const addProcess = useProcessStore(s => s.addProcess);
const canAdd = processes.length < MAX_PROCESSES;
const handleRandomize = () => {
const p = makeRandomProcess(processes.length + 1);
addProcess(p);
};
return (
<div data-testid="process-list">
{/* List header */}
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-slate-400">
{processes.length}/{MAX_PROCESSES} processos
</span>
<div className="flex gap-1">
<IconButton
size="sm"
variant="ghost"
label="Gerar processo aleatório"
onClick={handleRandomize}
disabled={!canAdd}
>
<Shuffle size={13} />
</IconButton>
<IconButton
size="sm"
variant="ghost"
label="Limpar todos os processos"
onClick={clearProcesses}
disabled={processes.length === 0}
className="hover:text-red-400 hover:bg-red-900/20"
>
<Trash2 size={13} />
</IconButton>
</div>
</div>
{/* Cards */}
<div className="space-y-2 max-h-64 overflow-y-auto pr-0.5 scrollbar-hide">
{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">
<span>Nenhum processo adicionado</span>
<span className="text-xs">Clique em + para criar</span>
</div>
) : (
processes.map(p => (
<ProcessCard
key={p.id}
process={p}
onEdit={onEditProcess}
onRemove={removeProcess}
/>
))
)}
</div>
{/* Add button */}
{canAdd && (
<button
onClick={onAddNew}
aria-label="Adicionar novo processo"
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} />
Adicionar Processo
</button>
)}
</div>
);
};
export default ProcessList;
@@ -0,0 +1,42 @@
import { useState } from 'react';
import { ListTree } from 'lucide-react';
import type { Process } from '@core/types';
import ProcessList from './ProcessList';
import ProcessForm from './ProcessForm';
const ProcessPanel = () => {
const [editingProcess, setEditingProcess] = useState<Process | null>(null);
const [showForm, setShowForm] = useState(false);
const handleEditProcess = (process: Process) => {
setEditingProcess(process);
setShowForm(true);
};
const handleAddNew = () => {
setEditingProcess(null);
setShowForm(true);
};
const handleClose = () => {
setShowForm(false);
setEditingProcess(null);
};
return (
<section aria-label="Painel de Processos">
<div className="flex items-center gap-2 mb-3 text-slate-300">
<ListTree size={18} className="text-blue-400" />
<h2 className="font-semibold uppercase text-xs tracking-wider">Processos</h2>
</div>
{showForm ? (
<ProcessForm editingProcess={editingProcess} onClose={handleClose} />
) : (
<ProcessList onEditProcess={handleEditProcess} onAddNew={handleAddNew} />
)}
</section>
);
};
export default ProcessPanel;
@@ -0,0 +1,44 @@
import type { SchedulingAlgorithm } from '@core/types';
import { useProcessStore } from '../../store/processStore';
const ALGORITHM_OPTIONS: { value: SchedulingAlgorithm; label: string }[] = [
{ value: 'FCFS', label: 'FCFS — First Come First Served' },
{ value: 'SJF_NP', label: 'SJF — Shortest Job First (NP)' },
{ value: 'SJF_P', label: 'SRTF — Shortest Remaining Time (P)' },
{ value: 'RR', label: 'RR — Round Robin' },
{ value: 'PRIORITY_NP',label: 'Priority (Não-preemptivo)' },
{ value: 'PRIORITY_P', label: 'Priority (Preemptivo)' },
{ value: 'HRRN', label: 'HRRN — Highest Response Ratio Next' },
{ value: 'MULTILEVEL', label: 'Multilevel Queue' },
];
const AlgorithmSelector = () => {
const algorithm = useProcessStore(s => s.config.algorithm);
const setConfig = useProcessStore(s => s.setConfig);
return (
<div>
<label
htmlFor="algorithm-select"
className="block text-xs text-slate-400 mb-1"
>
Algoritmo
</label>
<select
id="algorithm-select"
value={algorithm}
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"
aria-label="Selecionar algoritmo de escalonamento"
>
{ALGORITHM_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
);
};
export default AlgorithmSelector;
@@ -0,0 +1,140 @@
import { useProcessStore } from '../../store/processStore';
import type { SchedulingAlgorithm } from '@core/types';
// Algorithms that require a quantum parameter
const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'MULTILEVEL']);
// Algorithms where aging (anti-starvation) is applicable
const AGING_ALGORITHMS = new Set<SchedulingAlgorithm>(['PRIORITY_NP', 'PRIORITY_P']);
// Algorithms that are preemptive by definition (used for read-only info only)
const PREEMPTIVE_BY_DESIGN = new Set<SchedulingAlgorithm>(['SJF_P', 'RR', 'PRIORITY_P', 'MULTILEVEL']);
const inputCls = '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';
const labelCls = 'block text-xs text-slate-400 mb-1';
const ToggleRow = ({
id,
label,
checked,
onChange,
}: {
id: string;
label: string;
checked: boolean;
onChange: (v: boolean) => void;
}) => (
<div className="flex items-center justify-between">
<label htmlFor={id} className="text-xs text-slate-300 cursor-pointer">{label}</label>
<button
id={id}
role="switch"
aria-checked={checked}
onClick={() => onChange(!checked)}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
checked ? 'bg-blue-600' : 'bg-slate-600'
}`}
>
<span
className={`inline-block h-3.5 w-3.5 rounded-full bg-white shadow transition-transform duration-200 ${
checked ? 'translate-x-4' : 'translate-x-1'
}`}
/>
</button>
</div>
);
const SchedulerConfig = () => {
const config = useProcessStore(s => s.config);
const setConfig = useProcessStore(s => s.setConfig);
const algo = config.algorithm;
const showQuantum = QUANTUM_ALGORITHMS.has(algo);
const showAging = AGING_ALGORITHMS.has(algo);
const isPreemptive = PREEMPTIVE_BY_DESIGN.has(algo);
return (
<div className="space-y-3" data-testid="scheduler-config">
{/* Read-only preemptive indicator */}
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400">Tipo de escalonamento</span>
<span
className={`text-[10px] font-semibold uppercase tracking-wide px-2 py-0.5 rounded border ${
isPreemptive
? 'bg-amber-500/15 text-amber-400 border-amber-500/30'
: 'bg-slate-700 text-slate-400 border-slate-600'
}`}
data-testid="preemptive-badge"
>
{isPreemptive ? 'Preemptivo' : 'Não-preemptivo'}
</span>
</div>
{/* Context Switch */}
<div>
<label htmlFor="ctx-switch" className={labelCls}>
Context Switch (0–10 ticks)
</label>
<input
id="ctx-switch"
type="number"
min={0}
max={10}
value={config.contextSwitchTime}
onChange={e => setConfig({ contextSwitchTime: Math.min(10, Math.max(0, Number(e.target.value))) })}
className={inputCls}
aria-label="Tempo de context switch"
/>
</div>
{/* Quantum — RR and MULTILEVEL */}
{showQuantum && (
<div data-testid="quantum-field">
<label htmlFor="quantum" className={labelCls}>
Quantum (ticks)
</label>
<input
id="quantum"
type="number"
min={1}
value={config.quantum}
onChange={e => setConfig({ quantum: Math.max(1, Number(e.target.value)) })}
className={inputCls}
aria-label="Quantum do Round Robin"
/>
</div>
)}
{/* Aging — Priority only */}
{showAging && (
<div className="space-y-2 pt-0.5">
<ToggleRow
id="aging-toggle"
label="Aging (anti-starvation)"
checked={config.agingEnabled}
onChange={v => setConfig({ agingEnabled: v })}
/>
{config.agingEnabled && (
<div data-testid="aging-interval-field">
<label htmlFor="aging-interval" className={labelCls}>
Intervalo de Aging (ticks)
</label>
<input
id="aging-interval"
type="number"
min={1}
value={config.agingInterval}
onChange={e => setConfig({ agingInterval: Math.max(1, Number(e.target.value)) })}
className={inputCls}
aria-label="Intervalo de aging"
/>
</div>
)}
</div>
)}
</div>
);
};
export default SchedulerConfig;
@@ -0,0 +1,19 @@
import { Settings } from 'lucide-react';
import AlgorithmSelector from './AlgorithmSelector';
import SchedulerConfig from './SchedulerConfig';
const SchedulerPanel = () => {
return (
<section aria-label="Painel do Escalonador" className="space-y-3">
<div className="flex items-center gap-2 text-slate-300">
<Settings size={18} className="text-emerald-400" />
<h2 className="font-semibold uppercase text-xs tracking-wider">Escalonador</h2>
</div>
<AlgorithmSelector />
<SchedulerConfig />
</section>
);
};
export default SchedulerPanel;
@@ -0,0 +1,100 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ControlBar from '../ControlBar/ControlBar';
import { useProcessStore } from '../../store/processStore';
import { useSimulationStore } from '../../store/simulationStore';
import { useUiStore } from '../../store/uiStore';
import type { Process } from '@core/types';
const MOCK_PROCESS: Process = {
id: 'p1',
name: 'P1',
arrivalTime: 0,
priority: 5,
color: '#60a5fa',
bursts: [{ type: 'cpu', duration: 4 }],
};
beforeEach(() => {
useProcessStore.setState({ processes: [], config: useProcessStore.getState().config });
useSimulationStore.setState({ status: 'idle', ticks: [], engine: null, _processes: [], _config: null });
useUiStore.setState({ speed: 1 });
});
describe('ControlBar', () => {
it('renders the toolbar with all main controls', () => {
render(<ControlBar />);
expect(screen.getByRole('toolbar')).toBeInTheDocument();
expect(screen.getByTestId('btn-play')).toBeInTheDocument();
expect(screen.getByTestId('btn-step')).toBeInTheDocument();
expect(screen.getByTestId('btn-reset')).toBeInTheDocument();
expect(screen.getByTestId('tick-display')).toBeInTheDocument();
expect(screen.getByTestId('speed-select')).toBeInTheDocument();
});
it('displays tick 0000 on initial render', () => {
render(<ControlBar />);
expect(screen.getByTestId('tick-display')).toHaveTextContent('0000');
});
it('shows "Pronto" status when idle', () => {
render(<ControlBar />);
expect(screen.getByText('Pronto')).toBeInTheDocument();
});
it('Play button is disabled when no processes are loaded', () => {
render(<ControlBar />);
expect(screen.getByTestId('btn-play')).toBeDisabled();
});
it('Step button initialises simulation and advances tick', async () => {
const user = userEvent.setup();
useProcessStore.setState({ processes: [MOCK_PROCESS] });
render(<ControlBar />);
const stepBtn = screen.getByTestId('btn-step');
await user.click(stepBtn);
expect(screen.getByTestId('tick-display')).toHaveTextContent('0001');
});
it('Play button triggers init and sets status to running', async () => {
const user = userEvent.setup();
useProcessStore.setState({ processes: [MOCK_PROCESS] });
render(<ControlBar />);
await user.click(screen.getByTestId('btn-play'));
expect(useSimulationStore.getState().status).toBe('running');
});
it('Pause button is shown (and plays) when simulation is running', async () => {
const user = userEvent.setup();
useProcessStore.setState({ processes: [MOCK_PROCESS] });
render(<ControlBar />);
await user.click(screen.getByTestId('btn-play'));
expect(screen.getByTestId('btn-pause')).toBeInTheDocument();
await user.click(screen.getByTestId('btn-pause'));
expect(useSimulationStore.getState().status).toBe('paused');
});
it('changing speed updates uiStore', async () => {
const user = userEvent.setup();
render(<ControlBar />);
const speedSelect = screen.getByTestId('speed-select');
await user.selectOptions(speedSelect, '2');
expect(useUiStore.getState().speed).toBe(2);
});
it('Reset is disabled when status is idle', () => {
render(<ControlBar />);
expect(screen.getByTestId('btn-reset')).toBeDisabled();
});
it('Reset becomes enabled after simulation starts and returns tick to 0', async () => {
const user = userEvent.setup();
useProcessStore.setState({ processes: [MOCK_PROCESS] });
render(<ControlBar />);
await user.click(screen.getByTestId('btn-step'));
expect(screen.getByTestId('tick-display')).toHaveTextContent('0001');
await user.click(screen.getByTestId('btn-reset'));
expect(screen.getByTestId('tick-display')).toHaveTextContent('0000');
});
});
@@ -0,0 +1,100 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ProcessForm from '../ProcessPanel/ProcessForm';
import { useProcessStore } from '../../store/processStore';
// Reset Zustand processStore between tests
beforeEach(() => {
useProcessStore.setState({ processes: [], config: useProcessStore.getState().config });
vi.clearAllMocks();
});
describe('ProcessForm', () => {
it('renders the "Novo Processo" form with default fields', () => {
render(<ProcessForm onClose={() => {}} />);
expect(screen.getByTestId('process-form')).toBeInTheDocument();
expect(screen.getByText('Novo Processo')).toBeInTheDocument();
expect(screen.getByLabelText('Nome do processo')).toBeInTheDocument();
expect(screen.getByLabelText('Tempo de chegada')).toBeInTheDocument();
expect(screen.getByLabelText('Prioridade do processo')).toBeInTheDocument();
});
it('shows "Editar Processo" title when editingProcess is provided', () => {
const proc = {
id: 'p1', name: 'P1', arrivalTime: 0, priority: 5, color: '#fff',
bursts: [{ type: 'cpu' as const, duration: 4 }],
};
render(<ProcessForm editingProcess={proc} onClose={() => {}} />);
expect(screen.getByText('Editar Processo')).toBeInTheDocument();
});
it('shows validation error when name is empty', async () => {
const user = userEvent.setup();
render(<ProcessForm onClose={() => {}} />);
const nameInput = screen.getByLabelText('Nome do processo');
await user.clear(nameInput);
const submitBtn = screen.getByRole('button', { name: /Criar/i });
await user.click(submitBtn);
expect(screen.getByRole('alert')).toBeInTheDocument();
expect(screen.getByText(/Nome é obrigatório/i)).toBeInTheDocument();
});
it('shows validation error when arrival time is negative', async () => {
const user = userEvent.setup();
render(<ProcessForm onClose={() => {}} />);
const nameInput = screen.getByLabelText('Nome do processo');
await user.type(nameInput, 'P1');
const arrivalInput = screen.getByLabelText('Tempo de chegada');
// fireEvent.change used to reliably set a negative number bypassing HTML min attribute
fireEvent.change(arrivalInput, { target: { value: '-1' } });
const submitBtn = screen.getByRole('button', { name: /Criar/i });
await user.click(submitBtn);
expect(screen.getByText(/Arrival Time deve ser ≥ 0/i)).toBeInTheDocument();
});
it('shows validation error for name longer than 8 chars', async () => {
const user = userEvent.setup();
render(<ProcessForm onClose={() => {}} />);
const nameInput = screen.getByLabelText('Nome do processo');
// Bypass the HTML maxLength attribute via fireEvent to test the JS validation
fireEvent.change(nameInput, { target: { value: 'ProcessXXLong' } });
await user.click(screen.getByRole('button', { name: /Criar/i }));
expect(screen.getByText(/Nome deve ter no máximo 8 caracteres/i)).toBeInTheDocument();
});
it('adds a new burst with alternating type when clicking + button', async () => {
const user = userEvent.setup();
render(<ProcessForm onClose={() => {}} />);
const addBurstBtn = screen.getByLabelText('Adicionar burst');
await user.click(addBurstBtn);
// After one CPU burst at start, clicking + adds IO burst
// getAllByText with exact matches for the type labels
const cpuLabels = screen.getAllByText((_, el) => el?.tagName === 'SPAN' && el.textContent?.trim().toUpperCase() === 'CPU');
const ioLabels = screen.getAllByText((_, el) => el?.tagName === 'SPAN' && el.textContent?.trim().toUpperCase() === 'IO');
expect(cpuLabels.length).toBeGreaterThanOrEqual(1);
expect(ioLabels.length).toBeGreaterThanOrEqual(1);
});
it('calls onClose after successful form submission', async () => {
const user = userEvent.setup();
const onClose = vi.fn();
render(<ProcessForm onClose={onClose} />);
const nameInput = screen.getByLabelText('Nome do processo');
await user.type(nameInput, 'P1');
const submitBtn = screen.getByRole('button', { name: /Criar/i });
await user.click(submitBtn);
expect(onClose).toHaveBeenCalledOnce();
});
it('adds the process to the store after successful submission', async () => {
const user = userEvent.setup();
render(<ProcessForm onClose={() => {}} />);
const nameInput = screen.getByLabelText('Nome do processo');
await user.type(nameInput, 'Test1');
await user.click(screen.getByRole('button', { name: /Criar/i }));
const processes = useProcessStore.getState().processes;
expect(processes).toHaveLength(1);
expect(processes[0]?.name).toBe('Test1');
});
});
@@ -0,0 +1,123 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import SchedulerConfig from '../SchedulerPanel/SchedulerConfig';
import { useProcessStore } from '../../store/processStore';
import type { SchedulingAlgorithm } from '@core/types';
const setAlgorithm = (algo: SchedulingAlgorithm) => {
useProcessStore.setState(s => ({ config: { ...s.config, algorithm: algo } }));
};
beforeEach(() => {
useProcessStore.setState(s => ({
config: {
...s.config,
algorithm: 'FCFS',
quantum: 2,
contextSwitchTime: 0,
isPreemptive: false,
agingEnabled: false,
agingInterval: 5,
},
}));
});
describe('SchedulerConfig', () => {
it('always shows Context Switch field regardless of algorithm', () => {
render(<SchedulerConfig />);
expect(screen.getByLabelText('Tempo de context switch')).toBeInTheDocument();
});
it('always shows read-only preemptive badge', () => {
render(<SchedulerConfig />);
expect(screen.getByTestId('preemptive-badge')).toBeInTheDocument();
});
it('badge shows "Não-preemptivo" for FCFS', () => {
render(<SchedulerConfig />);
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Não-preemptivo');
});
it('badge shows "Preemptivo" for SJF_P (SRTF)', () => {
setAlgorithm('SJF_P');
render(<SchedulerConfig />);
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Preemptivo');
});
it('badge shows "Não-preemptivo" for SJF_NP (preemptive toggle no longer exists)', () => {
setAlgorithm('SJF_NP');
render(<SchedulerConfig />);
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Não-preemptivo');
// Confirm the old interactive preemptive switch is gone
expect(screen.queryByRole('switch', { name: /Preemptivo/i })).not.toBeInTheDocument();
});
it('does NOT show Quantum field for FCFS', () => {
render(<SchedulerConfig />);
expect(screen.queryByTestId('quantum-field')).not.toBeInTheDocument();
});
it('shows Quantum field when algorithm is RR', () => {
setAlgorithm('RR');
render(<SchedulerConfig />);
expect(screen.getByTestId('quantum-field')).toBeInTheDocument();
expect(screen.getByLabelText('Quantum do Round Robin')).toBeInTheDocument();
});
it('shows Quantum field when algorithm is MULTILEVEL', () => {
setAlgorithm('MULTILEVEL');
render(<SchedulerConfig />);
expect(screen.getByTestId('quantum-field')).toBeInTheDocument();
});
it('does NOT show Quantum field for SJF_NP', () => {
setAlgorithm('SJF_NP');
render(<SchedulerConfig />);
expect(screen.queryByTestId('quantum-field')).not.toBeInTheDocument();
});
it('shows Aging toggle for PRIORITY_NP', () => {
setAlgorithm('PRIORITY_NP');
render(<SchedulerConfig />);
expect(screen.getByRole('switch', { name: /Aging/i })).toBeInTheDocument();
});
it('shows Aging toggle for PRIORITY_P', () => {
setAlgorithm('PRIORITY_P');
render(<SchedulerConfig />);
expect(screen.getByRole('switch', { name: /Aging/i })).toBeInTheDocument();
});
it('shows Aging interval field only when aging is enabled', async () => {
const user = userEvent.setup();
setAlgorithm('PRIORITY_NP');
render(<SchedulerConfig />);
expect(screen.queryByTestId('aging-interval-field')).not.toBeInTheDocument();
const agingToggle = screen.getByRole('switch', { name: /Aging/i });
await user.click(agingToggle);
expect(screen.getByTestId('aging-interval-field')).toBeInTheDocument();
expect(screen.getByLabelText('Intervalo de aging')).toBeInTheDocument();
});
it('updates the store when Quantum field is changed', () => {
setAlgorithm('RR');
render(<SchedulerConfig />);
const quantumInput = screen.getByLabelText('Quantum do Round Robin');
fireEvent.change(quantumInput, { target: { value: '5' } });
expect(useProcessStore.getState().config.quantum).toBe(5);
});
it('does NOT show Aging toggle for FCFS', () => {
setAlgorithm('FCFS');
render(<SchedulerConfig />);
expect(screen.queryByRole('switch', { name: /Aging/i })).not.toBeInTheDocument();
});
it('does NOT show Aging toggle for HRRN', () => {
setAlgorithm('HRRN');
render(<SchedulerConfig />);
expect(screen.queryByRole('switch', { name: /Aging/i })).not.toBeInTheDocument();
});
});
+30
View File
@@ -0,0 +1,30 @@
import { clsx } from 'clsx';
type BadgeVariant = 'cpu' | 'io' | 'balanced' | 'slate';
interface BadgeProps {
variant?: BadgeVariant;
children: React.ReactNode;
className?: string;
}
const variantClasses: Record<BadgeVariant, string> = {
cpu: 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30',
io: 'bg-cyan-500/20 text-cyan-400 border border-cyan-500/30',
balanced: 'bg-violet-500/20 text-violet-400 border border-violet-500/30',
slate: 'bg-slate-700 text-slate-400 border border-slate-600',
};
const Badge = ({ variant = 'slate', children, className }: BadgeProps) => (
<span
className={clsx(
'inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
variantClasses[variant],
className,
)}
>
{children}
</span>
);
export default Badge;
+51
View File
@@ -0,0 +1,51 @@
import { type ButtonHTMLAttributes, type ReactNode } from 'react';
import { clsx } from 'clsx';
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
variant?: 'default' | 'primary' | 'danger' | 'ghost';
size?: 'sm' | 'md';
label: string;
}
const variantClasses = {
default: 'bg-slate-700 hover:bg-slate-600 text-slate-200 border-slate-600',
primary: 'bg-blue-600 hover:bg-blue-500 text-white border-blue-500',
danger: 'bg-red-600/80 hover:bg-red-500 text-white border-red-500',
ghost: 'bg-transparent hover:bg-slate-700 text-slate-400 hover:text-slate-200 border-transparent',
};
const sizeClasses = {
sm: 'h-7 w-7 text-sm',
md: 'h-9 w-9 text-base',
};
const IconButton = ({
children,
variant = 'default',
size = 'md',
label,
className,
disabled,
...rest
}: IconButtonProps) => {
return (
<button
aria-label={label}
title={label}
disabled={disabled}
className={clsx(
'inline-flex items-center justify-center rounded-lg border transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400',
'disabled:opacity-40 disabled:cursor-not-allowed',
variantClasses[variant],
sizeClasses[size],
className,
)}
{...rest}
>
{children}
</button>
);
};
export default IconButton;
+32
View File
@@ -0,0 +1,32 @@
import type { BurstSegment } from '@core/types';
/**
* Classifies a process as CPU Bound, I/O Bound, or Balanced
* based on declared burst segments (used for preview in ProcessCard).
*/
export function classifyBound(bursts: BurstSegment[]): 'CPU Bound' | 'I/O Bound' | 'Balanced' {
const cpuTotal = bursts.filter(b => b.type === 'cpu').reduce((a, b) => a + b.duration, 0);
const ioTotal = bursts.filter(b => b.type === 'io').reduce((a, b) => a + b.duration, 0);
const total = cpuTotal + ioTotal;
if (total === 0) return 'Balanced';
const cpuRatio = cpuTotal / total;
const ioRatio = ioTotal / total;
if (cpuRatio >= 0.65) return 'CPU Bound';
if (ioRatio >= 0.65) return 'I/O Bound';
return 'Balanced';
}
/** Generate a short unique id */
export function generateId(): string {
return Math.random().toString(36).slice(2, 9);
}
/** Random vivid HEX color */
const PALETTE = [
'#60a5fa','#34d399','#f472b6','#fb923c','#a78bfa',
'#38bdf8','#facc15','#4ade80','#f87171','#c084fc',
];
let _paletteIndex = 0;
export function nextColor(): string {
return PALETTE[_paletteIndex++ % PALETTE.length];
}
+281
View File
@@ -0,0 +1,281 @@
import { describe, it, expect } from "vitest";
import { SimulationEngine } from "../engine";
import type { Process, SchedulerConfig } from "../types";
// ============================================================
// Helpers
// ============================================================
const cfg = (overrides: Partial<SchedulerConfig> = {}): SchedulerConfig => ({
algorithm: "FCFS",
quantum: 2,
contextSwitchTime: 0,
isPreemptive: false,
agingEnabled: false,
agingInterval: 5,
...overrides,
});
const p = (
id: string,
arrivalTime: number,
bursts: { type: "cpu" | "io"; duration: number }[],
priority = 5,
): Process => ({
id,
name: id.toUpperCase(),
arrivalTime,
priority,
color: "#fff",
bursts,
});
// ============================================================
// CT-01: FCFS Basic
// Input: P1(arrival=0, CPU:4), P2(arrival=1, CPU:3), P3(arrival=2, CPU:1)
// Expected Gantt: P1(0-4), P2(4-7), P3(7-8)
// P1: response=0, turnaround=4, waiting=0
// P2: response=3, turnaround=6, waiting=3
// P3: response=5, turnaround=6, waiting=5
// ============================================================
describe("CT-01: FCFS Basic", () => {
const processes = [
p("p1", 0, [{ type: "cpu", duration: 4 }]),
p("p2", 1, [{ type: "cpu", duration: 3 }]),
p("p3", 2, [{ type: "cpu", duration: 1 }]),
];
it("produces correct Gantt sequence", () => {
const engine = new SimulationEngine(processes, cfg());
const ticks = engine.runAll();
// ticks[i].tick should equal i
expect(ticks.length).toBe(8);
// P1 runs ticks 0–3
for (let i = 0; i <= 3; i++) {
expect(ticks[i]?.cpuProcess).toBe("p1");
}
// P2 runs ticks 4–6
for (let i = 4; i <= 6; i++) {
expect(ticks[i]?.cpuProcess).toBe("p2");
}
// P3 runs tick 7
expect(ticks[7]?.cpuProcess).toBe("p3");
});
it("sets correct finishTick values", () => {
const engine = new SimulationEngine(processes, cfg());
engine.runAll();
const rts = engine.runtimeStates;
const rt1 = rts.find((r) => r.processId === "p1")!;
const rt2 = rts.find((r) => r.processId === "p2")!;
const rt3 = rts.find((r) => r.processId === "p3")!;
expect(rt1.finishTick).toBe(4);
expect(rt2.finishTick).toBe(7);
expect(rt3.finishTick).toBe(8);
});
it("computes correct per-process metrics", () => {
const engine = new SimulationEngine(processes, cfg());
engine.runAll();
const rts = engine.runtimeStates;
const rt1 = rts.find((r) => r.processId === "p1")!;
const rt2 = rts.find((r) => r.processId === "p2")!;
const rt3 = rts.find((r) => r.processId === "p3")!;
// P1: arrivalTick=0, startTick=0, finishTick=4
expect(rt1.startTick).toBe(0);
expect(rt1.waitingTime).toBe(0);
// turnaround = 4 - 0 = 4; waiting = 4 - 4 - 0 = 0
expect(rt1.finishTick! - rt1.arrivalTick).toBe(4); // turnaround
// P2: arrivalTick=1, startTick=4, finishTick=7
expect(rt2.startTick).toBe(4);
expect(rt2.waitingTime).toBe(3); // waited ticks 1,2,3
// response = 4 - 1 = 3
expect(rt2.startTick! - rt2.arrivalTick).toBe(3);
// P3: arrivalTick=2, startTick=7, finishTick=8
expect(rt3.startTick).toBe(7);
expect(rt3.waitingTime).toBe(5); // waited ticks 2,3,4,5,6
expect(rt3.startTick! - rt3.arrivalTick).toBe(5); // response
expect(rt3.finishTick! - rt3.arrivalTick).toBe(6); // turnaround
});
});
// ============================================================
// CT-02: Round Robin (quantum=2)
// Input: P1,P2,P3 all arrival=0; CPU:5, CPU:3, CPU:1
// Expected: P1(0-2), P2(2-4), P3(4-5), P1(5-7), P2(7-8), P1(8-9)
// ============================================================
describe("CT-02: Round Robin (quantum=2)", () => {
const processes = [
p("p1", 0, [{ type: "cpu", duration: 5 }]),
p("p2", 0, [{ type: "cpu", duration: 3 }]),
p("p3", 0, [{ type: "cpu", duration: 1 }]),
];
it("produces correct Gantt sequence", () => {
const engine = new SimulationEngine(processes, cfg({ algorithm: "RR", quantum: 2 }));
const ticks = engine.runAll();
expect(ticks.length).toBe(9);
const cpu = ticks.map((t) => t.cpuProcess);
expect(cpu[0]).toBe("p1");
expect(cpu[1]).toBe("p1");
expect(cpu[2]).toBe("p2");
expect(cpu[3]).toBe("p2");
expect(cpu[4]).toBe("p3");
expect(cpu[5]).toBe("p1");
expect(cpu[6]).toBe("p1");
expect(cpu[7]).toBe("p2");
expect(cpu[8]).toBe("p1");
});
it("computes waiting times", () => {
const engine = new SimulationEngine(processes, cfg({ algorithm: "RR", quantum: 2 }));
engine.runAll();
const rts = engine.runtimeStates;
const rt1 = rts.find((r) => r.processId === "p1")!;
const rt2 = rts.find((r) => r.processId === "p2")!;
const rt3 = rts.find((r) => r.processId === "p3")!;
// P1: turnaround=9, cpuTime=5, ioTime=0 → waiting=4
expect(rt1.waitingTime).toBe(4);
// P2: finishTick=8, arrival=0 → turnaround=8; cpuTime=3 → waiting=5
expect(rt2.waitingTime).toBe(5);
// P3: finishTick=5, arrival=0 → turnaround=5; cpuTime=1 → waiting=4
expect(rt3.waitingTime).toBe(4);
});
});
// ============================================================
// CT-04: SRTF Preemption
// P1(arrival=0, CPU:8), P2(arrival=1, CPU:4)
// P1 runs tick 0, P2 preempts at tick 1 (P2 remaining=4 < P1 remaining=7)
// P2 finishes tick 5 (runs ticks 1-4), P1 resumes ticks 5-11, finishes tick 12
// ============================================================
describe("CT-04: SRTF Preemption", () => {
const processes = [
p("p1", 0, [{ type: "cpu", duration: 8 }]),
p("p2", 1, [{ type: "cpu", duration: 4 }]),
];
it("P2 preempts P1 at tick 1", () => {
const engine = new SimulationEngine(processes, cfg({ algorithm: "SJF_P", isPreemptive: true }));
const ticks = engine.runAll();
expect(ticks[0]?.cpuProcess).toBe("p1"); // tick 0: P1 running
expect(ticks[1]?.cpuProcess).toBe("p2"); // tick 1: P2 preempts
expect(ticks[2]?.cpuProcess).toBe("p2");
expect(ticks[3]?.cpuProcess).toBe("p2");
expect(ticks[4]?.cpuProcess).toBe("p2"); // tick 4: P2 last tick
});
it("P2 finishes at tick 5 and P1 resumes", () => {
const engine = new SimulationEngine(processes, cfg({ algorithm: "SJF_P", isPreemptive: true }));
const ticks = engine.runAll();
const rts = engine.runtimeStates;
const rt2 = rts.find((r) => r.processId === "p2")!;
const rt1 = rts.find((r) => r.processId === "p1")!;
expect(rt2.finishTick).toBe(5);
expect(rt1.finishTick).toBe(12);
// P1 resumes at tick 5
expect(ticks[5]?.cpuProcess).toBe("p1");
});
});
// ============================================================
// General engine behaviour
// ============================================================
describe("SimulationEngine general", () => {
it("isFinished returns false mid-run, true after runAll", () => {
const engine = new SimulationEngine(
[p("p1", 0, [{ type: "cpu", duration: 3 }])],
cfg(),
);
expect(engine.isFinished).toBe(false);
engine.step();
expect(engine.isFinished).toBe(false);
engine.runAll();
expect(engine.isFinished).toBe(true);
});
it("step() after isFinished throws", () => {
const engine = new SimulationEngine(
[p("p1", 0, [{ type: "cpu", duration: 1 }])],
cfg(),
);
engine.runAll();
expect(() => engine.step()).toThrow();
});
it("runAll() and repeated step() produce same tick sequence", () => {
const procs = [
p("p1", 0, [{ type: "cpu", duration: 3 }]),
p("p2", 1, [{ type: "cpu", duration: 2 }]),
];
const e1 = new SimulationEngine(procs, cfg());
const e2 = new SimulationEngine(procs, cfg());
const fromRunAll = e1.runAll();
const fromStep: ReturnType<typeof e2.step>[] = [];
while (!e2.isFinished) fromStep.push(e2.step());
expect(fromRunAll).toEqual(fromStep);
});
it("process with I/O: transitions through Waiting state", () => {
const engine = new SimulationEngine(
[p("p1", 0, [{ type: "cpu", duration: 2 }, { type: "io", duration: 2 }, { type: "cpu", duration: 1 }])],
cfg(),
);
const ticks = engine.runAll();
const states = ticks.map((t) => t.states["p1"]);
// Running for 2, Waiting for 2, Running for 1
expect(states[0]).toBe("Running");
expect(states[1]).toBe("Running");
expect(states[2]).toBe("Waiting");
expect(states[3]).toBe("Waiting");
expect(states[4]).toBe("Running");
});
it("process arriving after simulation starts is correctly scheduled", () => {
const engine = new SimulationEngine(
[
p("p1", 0, [{ type: "cpu", duration: 3 }]),
p("p2", 5, [{ type: "cpu", duration: 2 }]),
],
cfg(),
);
const ticks = engine.runAll();
// P1 finishes at tick 3; CPU idle 3,4; P2 arrives at 5
expect(ticks[4]?.cpuProcess).toBeNull(); // tick 4: idle
expect(ticks[5]?.cpuProcess).toBe("p2");
});
it("context switch inserts idle ticks between processes", () => {
const engine = new SimulationEngine(
[
p("p1", 0, [{ type: "cpu", duration: 2 }]),
p("p2", 0, [{ type: "cpu", duration: 2 }]),
],
cfg({ algorithm: "FCFS", contextSwitchTime: 1 }),
);
const ticks = engine.runAll();
// P1 runs ticks 0-1, context switch tick 2, P2 runs ticks 3-4
expect(ticks[0]?.cpuProcess).toBe("p1");
expect(ticks[1]?.cpuProcess).toBe("p1");
expect(ticks[2]?.contextSwitching).toBe(true);
expect(ticks[2]?.cpuProcess).toBeNull();
expect(ticks[3]?.cpuProcess).toBe("p2");
});
});
+54
View File
@@ -0,0 +1,54 @@
import { describe, it, expect } from "vitest";
import { fcfs } from "../algorithms/fcfs";
import type { ProcessRuntime } from "../types";
function makeRt(id: string, arrivalTick: number): ProcessRuntime {
return {
processId: id, state: "Ready", remainingBurst: 4, burstIndex: 0,
arrivalTick, startTick: null, finishTick: null,
waitingTime: 0, cpuTime: 0, ioTime: 0, responseTime: null,
quantumRemaining: 2, currentPriority: 5,
};
}
const config = { algorithm: "FCFS" as const, quantum: 2, contextSwitchTime: 0, isPreemptive: false, agingEnabled: false, agingInterval: 5 };
describe("FCFS select()", () => {
it("returns null for empty queue", () => {
expect(fcfs.select([], config)).toBeNull();
});
it("returns single process", () => {
const rt = makeRt("p1", 0);
expect(fcfs.select([rt], config)).toBe(rt);
});
it("returns earliest-arriving process", () => {
const r1 = makeRt("p1", 5);
const r2 = makeRt("p2", 2);
const r3 = makeRt("p3", 8);
expect(fcfs.select([r1, r2, r3], config)?.processId).toBe("p2");
});
it("tiebreaks by processId lexicographic", () => {
const r1 = makeRt("p2", 0);
const r2 = makeRt("p1", 0);
expect(fcfs.select([r1, r2], config)?.processId).toBe("p1");
});
});
describe("FCFS shouldPreempt()", () => {
it("always returns false", () => {
const a = makeRt("p1", 0);
const b = makeRt("p2", 0);
expect(fcfs.shouldPreempt(a, b, config)).toBe(false);
});
});
describe("FCFS isQuantumExpired()", () => {
it("always returns false", () => {
const rt = makeRt("p1", 0);
rt.quantumRemaining = 0;
expect(fcfs.isQuantumExpired(rt, config)).toBe(false);
});
});
+83
View File
@@ -0,0 +1,83 @@
import { describe, it, expect } from "vitest";
import { hrrn } from "../algorithms/hrrn";
import { SimulationEngine } from "../engine";
import type { Process, ProcessRuntime, SchedulerConfig } from "../types";
function makeRt(id: string, remainingBurst: number, waitingTime: number, arrivalTick = 0): ProcessRuntime {
return {
processId: id, state: "Ready", remainingBurst, burstIndex: 0,
arrivalTick, startTick: null, finishTick: null,
waitingTime, cpuTime: 0, ioTime: 0, responseTime: null,
quantumRemaining: 2, currentPriority: 5,
};
}
const config: SchedulerConfig = {
algorithm: "HRRN", quantum: 2, contextSwitchTime: 0,
isPreemptive: false, agingEnabled: false, agingInterval: 5,
};
const p = (id: string, arrival: number, cpuDuration: number): Process => ({
id, name: id, arrivalTime: arrival, priority: 5, color: "#fff",
bursts: [{ type: "cpu", duration: cpuDuration }],
});
describe("HRRN select()", () => {
it("returns null for empty queue", () => {
expect(hrrn.select([], config)).toBeNull();
});
it("picks process with highest response ratio", () => {
// ratio = (waitingTime + burstTime) / burstTime
// P1: (0 + 8) / 8 = 1.0
// P2: (6 + 3) / 3 = 3.0 ← highest
// P3: (2 + 5) / 5 = 1.4
const r1 = makeRt("p1", 8, 0);
const r2 = makeRt("p2", 3, 6);
const r3 = makeRt("p3", 5, 2);
expect(hrrn.select([r1, r2, r3], config)?.processId).toBe("p2");
});
it("tiebreaks equal ratio by arrival then id", () => {
// Both ratio = (4 + 4) / 4 = 2.0
const r1 = makeRt("p2", 4, 4, 1); // arrives at 1
const r2 = makeRt("p1", 4, 4, 2); // arrives at 2
expect(hrrn.select([r1, r2], config)?.processId).toBe("p2");
});
it("shouldPreempt always false", () => {
const a = makeRt("p1", 4, 0);
const b = makeRt("p2", 4, 10);
expect(hrrn.shouldPreempt(a, b, config)).toBe(false);
});
});
describe("HRRN integration", () => {
it("long-waiting short process wins over fresh long process", () => {
// P1 arrives at 0 with CPU:8 — occupies CPU first
// P2 arrives at 0 with CPU:4, waits for 8 ticks
// After P1 finishes: P2 has been waiting → ratio for P2 is high
// (No other choice here, but validates engine runs HRRN correctly)
const processes = [p("p1", 0, 4), p("p2", 4, 2)];
const engine = new SimulationEngine(processes, config);
const ticks = engine.runAll();
// P1 runs ticks 0-3, P2 arrives at tick 4 and runs immediately
expect(ticks[4]?.cpuProcess).toBe("p2");
expect(engine.isFinished).toBe(true);
});
it("HRRN selects high-ratio process over long process that just arrived", () => {
// P1(arrival=0, CPU:10) starts running
// P2(arrival=1, CPU:2), P3(arrival=1, CPU:8) both arrive at tick 1
// When P1 finishes, P2 has waited longer relative to its burst → wins HRRN
const processes = [
p("p1", 0, 3), // short; finishes tick 3
p("p2", 0, 6), // medium; waits 3 ticks → ratio = (3+6)/6 = 1.5
p("p3", 0, 2), // short; waits 3 ticks → ratio = (3+2)/2 = 2.5 ← wins
];
const engine = new SimulationEngine(processes, config);
const ticks = engine.runAll();
// After P1 (ticks 0-2), P3 should win (ratio 2.5 > 1.5)
expect(ticks[3]?.cpuProcess).toBe("p3");
});
});
+163
View File
@@ -0,0 +1,163 @@
import { describe, it, expect } from "vitest";
import { computeProcessMetrics, computeGlobalMetrics, classifyBound } from "../metrics";
import { SimulationEngine } from "../engine";
import type { Process, SchedulerConfig } from "../types";
const cfg = (overrides: Partial<SchedulerConfig> = {}): SchedulerConfig => ({
algorithm: "FCFS",
quantum: 2,
contextSwitchTime: 0,
isPreemptive: false,
agingEnabled: false,
agingInterval: 5,
...overrides,
});
const p = (id: string, arrivalTime: number, bursts: { type: "cpu" | "io"; duration: number }[]): Process => ({
id, name: id, arrivalTime, priority: 5, color: "#fff", bursts,
});
// ============================================================
// CT-03: Bound classification (static, on declared bursts)
// P1: CPU:8, IO:1, CPU:1 → cpuRatio = 9/10 = 90% → CPU Bound
// P2: CPU:1, IO:8, CPU:1 → cpuRatio = 2/10 = 20% → I/O Bound
// P3: CPU:4, IO:2, CPU:4 → cpuRatio = 8/10 = 80% → CPU Bound
// ============================================================
describe("CT-03: classifyBound", () => {
it("P1 (CPU:8,IO:1,CPU:1) → CPU Bound", () => {
expect(classifyBound([{ type: "cpu", duration: 8 }, { type: "io", duration: 1 }, { type: "cpu", duration: 1 }]))
.toBe("CPU Bound");
});
it("P2 (CPU:1,IO:8,CPU:1) → I/O Bound", () => {
expect(classifyBound([{ type: "cpu", duration: 1 }, { type: "io", duration: 8 }, { type: "cpu", duration: 1 }]))
.toBe("I/O Bound");
});
it("P3 (CPU:4,IO:2,CPU:4) → CPU Bound", () => {
expect(classifyBound([{ type: "cpu", duration: 4 }, { type: "io", duration: 2 }, { type: "cpu", duration: 4 }]))
.toBe("CPU Bound");
});
it("balanced process (CPU:5,IO:5) → Balanced", () => {
expect(classifyBound([{ type: "cpu", duration: 5 }, { type: "io", duration: 5 }]))
.toBe("Balanced");
});
it("CPU-only process → CPU Bound", () => {
expect(classifyBound([{ type: "cpu", duration: 10 }])).toBe("CPU Bound");
});
});
// ============================================================
// computeProcessMetrics
// ============================================================
describe("computeProcessMetrics", () => {
it("throws for non-terminated process", () => {
const engine = new SimulationEngine(
[p("p1", 0, [{ type: "cpu", duration: 5 }])],
cfg(),
);
engine.step(); // tick 0, not finished
const rt = engine.runtimeStates[0]!;
expect(() => computeProcessMetrics(rt)).toThrow();
});
it("CT-01 P1: response=0, turnaround=4, waiting=0, cpuTime=4", () => {
const processes = [
p("p1", 0, [{ type: "cpu", duration: 4 }]),
p("p2", 1, [{ type: "cpu", duration: 3 }]),
p("p3", 2, [{ type: "cpu", duration: 1 }]),
];
const engine = new SimulationEngine(processes, cfg());
engine.runAll();
const rt1 = engine.runtimeStates.find((r) => r.processId === "p1")!;
const m = computeProcessMetrics(rt1);
expect(m.responseTime).toBe(0);
expect(m.turnaroundTime).toBe(4);
expect(m.waitingTime).toBe(0);
expect(m.cpuTime).toBe(4);
expect(m.ioTime).toBe(0);
expect(m.boundType).toBe("CPU Bound");
});
it("CT-01 P2: response=3, turnaround=6, waiting=3", () => {
const processes = [
p("p1", 0, [{ type: "cpu", duration: 4 }]),
p("p2", 1, [{ type: "cpu", duration: 3 }]),
p("p3", 2, [{ type: "cpu", duration: 1 }]),
];
const engine = new SimulationEngine(processes, cfg());
engine.runAll();
const rt2 = engine.runtimeStates.find((r) => r.processId === "p2")!;
const m = computeProcessMetrics(rt2);
expect(m.responseTime).toBe(3);
expect(m.turnaroundTime).toBe(6);
expect(m.waitingTime).toBe(3);
});
it("process with I/O: correct ioTime, boundType", () => {
const engine = new SimulationEngine(
[p("p1", 0, [{ type: "cpu", duration: 2 }, { type: "io", duration: 8 }, { type: "cpu", duration: 2 }])],
cfg(),
);
engine.runAll();
const rt = engine.runtimeStates[0]!;
const m = computeProcessMetrics(rt);
expect(m.cpuTime).toBe(4);
expect(m.ioTime).toBe(8);
expect(m.boundType).toBe("I/O Bound");
});
it("cpuUtilization: CPU-only → 100%", () => {
const engine = new SimulationEngine(
[p("p1", 0, [{ type: "cpu", duration: 4 }])],
cfg(),
);
engine.runAll();
const rt = engine.runtimeStates[0]!;
const m = computeProcessMetrics(rt);
expect(m.cpuUtilization).toBe(100);
});
});
// ============================================================
// computeGlobalMetrics
// ============================================================
describe("computeGlobalMetrics", () => {
it("CT-01 global metrics", () => {
const processes = [
p("p1", 0, [{ type: "cpu", duration: 4 }]),
p("p2", 1, [{ type: "cpu", duration: 3 }]),
p("p3", 2, [{ type: "cpu", duration: 1 }]),
];
const engine = new SimulationEngine(processes, cfg());
engine.runAll();
const rts = engine.runtimeStates;
const pMetrics = rts.map(computeProcessMetrics);
const g = computeGlobalMetrics(pMetrics, engine.currentTick + 1);
// Avg response: (0 + 3 + 5) / 3 = 8/3 ≈ 2.667
expect(g.avgResponseTime).toBeCloseTo(8 / 3, 5);
// Avg turnaround: (4 + 6 + 6) / 3 = 16/3 ≈ 5.333
expect(g.avgTurnaroundTime).toBeCloseTo(16 / 3, 5);
// Avg waiting: (0 + 3 + 5) / 3 = 8/3 ≈ 2.667
expect(g.avgWaitingTime).toBeCloseTo(8 / 3, 5);
// Throughput: 3 processes / 8 ticks = 0.375
expect(g.cpuThroughput).toBeCloseTo(3 / 8, 5);
// CPU utilization: (4+3+1)/8 * 100 = 100%
expect(g.cpuUtilization).toBeCloseTo(100, 5);
expect(g.totalSimulationTime).toBe(8);
});
it("handles empty metrics array", () => {
const g = computeGlobalMetrics([], 0);
expect(g.avgResponseTime).toBe(0);
expect(g.cpuUtilization).toBe(0);
});
});
+118
View File
@@ -0,0 +1,118 @@
import { describe, it, expect } from "vitest";
import { priorityNp, priorityP } from "../algorithms/priority";
import { SimulationEngine } from "../engine";
import type { Process, ProcessRuntime, SchedulerConfig } from "../types";
function makeRt(id: string, priority: number, arrivalTick = 0): ProcessRuntime {
return {
processId: id, state: "Ready", remainingBurst: 4, burstIndex: 0,
arrivalTick, startTick: null, finishTick: null,
waitingTime: 0, cpuTime: 0, ioTime: 0, responseTime: null,
quantumRemaining: 2, currentPriority: priority,
};
}
const cfgNp: SchedulerConfig = {
algorithm: "PRIORITY_NP", quantum: 2, contextSwitchTime: 0,
isPreemptive: false, agingEnabled: false, agingInterval: 5,
};
const cfgP: SchedulerConfig = {
algorithm: "PRIORITY_P", quantum: 2, contextSwitchTime: 0,
isPreemptive: true, agingEnabled: false, agingInterval: 5,
};
const p = (id: string, arrival: number, priority: number, cpuDuration: number): Process => ({
id, name: id, arrivalTime: arrival, priority, color: "#fff",
bursts: [{ type: "cpu", duration: cpuDuration }],
});
describe("PRIORITY_NP select()", () => {
it("picks lowest priority number (highest priority)", () => {
const r1 = makeRt("p1", 5);
const r2 = makeRt("p2", 2);
const r3 = makeRt("p3", 8);
expect(priorityNp.select([r1, r2, r3], cfgNp)?.processId).toBe("p2");
});
it("tiebreaks by arrival then id", () => {
const r1 = makeRt("p2", 3, 2);
const r2 = makeRt("p1", 3, 5); // p1 arrives later
expect(priorityNp.select([r1, r2], cfgNp)?.processId).toBe("p2");
});
it("shouldPreempt always false", () => {
const a = makeRt("p1", 5);
const b = makeRt("p2", 1);
expect(priorityNp.shouldPreempt(a, b, cfgNp)).toBe(false);
});
});
describe("PRIORITY_P shouldPreempt()", () => {
it("preempts when candidate has higher priority (lower number)", () => {
const current = makeRt("p1", 5);
const candidate = makeRt("p2", 2);
expect(priorityP.shouldPreempt(current, candidate, cfgP)).toBe(true);
});
it("does NOT preempt on equal priority", () => {
const current = makeRt("p1", 3);
const candidate = makeRt("p2", 3);
expect(priorityP.shouldPreempt(current, candidate, cfgP)).toBe(false);
});
it("does NOT preempt when current has higher priority", () => {
const current = makeRt("p1", 2);
const candidate = makeRt("p2", 5);
expect(priorityP.shouldPreempt(current, candidate, cfgP)).toBe(false);
});
});
describe("Priority NP integration", () => {
it("selects highest-priority process regardless of arrival", () => {
const processes = [
p("p1", 0, 5, 3), // low priority
p("p2", 0, 1, 3), // high priority
];
const engine = new SimulationEngine(processes, cfgNp);
const ticks = engine.runAll();
// P2 (priority 1) should run first
expect(ticks[0]?.cpuProcess).toBe("p2");
expect(ticks[1]?.cpuProcess).toBe("p2");
expect(ticks[2]?.cpuProcess).toBe("p2");
expect(ticks[3]?.cpuProcess).toBe("p1");
});
});
describe("Priority P + aging", () => {
it("aging decrements currentPriority for waiting processes", () => {
// P1 priority=1 runs first; P2 priority=10 waits
// After agingInterval=2 ticks, P2's priority should drop
const processes = [
p("p1", 0, 1, 10), // runs for 10 ticks
p("p2", 0, 10, 5), // waits, aging should help
];
const engine = new SimulationEngine(processes, {
...cfgNp, algorithm: "PRIORITY_NP", agingEnabled: true, agingInterval: 2,
});
engine.step(); // tick 0
engine.step(); // tick 1
engine.step(); // tick 2 — aging fires (tick % 2 === 0), P2 priority 10→9
const rts = engine.runtimeStates;
const rt2 = rts.find((r) => r.processId === "p2")!;
expect(rt2.currentPriority).toBe(9);
});
it("aging does not reduce priority below 1", () => {
const processes = [p("p1", 0, 1, 5)];
// Construct a runtime with priority already at 1 and test the engine handles it
const engine = new SimulationEngine(processes, {
...cfgNp, agingEnabled: true, agingInterval: 1,
});
// Run a few ticks — priority should never go below 1
for (let i = 0; i < 5; i++) {
if (!engine.isFinished) engine.step();
}
const rt = engine.runtimeStates[0]!;
expect(rt.currentPriority).toBeGreaterThanOrEqual(1);
});
});
+116
View File
@@ -0,0 +1,116 @@
import { describe, it, expect } from "vitest";
import { rr } from "../algorithms/rr";
import { SimulationEngine } from "../engine";
import type { Process, ProcessRuntime, SchedulerConfig } from "../types";
function makeRt(id: string, quantumRemaining = 2): ProcessRuntime {
return {
processId: id, state: "Running", remainingBurst: 4, burstIndex: 0,
arrivalTick: 0, startTick: 0, finishTick: null,
waitingTime: 0, cpuTime: 0, ioTime: 0, responseTime: null,
quantumRemaining, currentPriority: 5,
};
}
const config: SchedulerConfig = {
algorithm: "RR", quantum: 2, contextSwitchTime: 0,
isPreemptive: false, agingEnabled: false, agingInterval: 5,
};
const p = (id: string, arrival: number, cpuDuration: number): Process => ({
id, name: id, arrivalTime: arrival, priority: 5, color: "#fff",
bursts: [{ type: "cpu", duration: cpuDuration }],
});
describe("RR select()", () => {
it("returns front of queue (FIFO)", () => {
const r1: ProcessRuntime = { ...makeRt("p1"), state: "Ready" };
const r2: ProcessRuntime = { ...makeRt("p2"), state: "Ready" };
expect(rr.select([r1, r2], config)?.processId).toBe("p1");
expect(rr.select([r2, r1], config)?.processId).toBe("p2");
});
it("returns null for empty queue", () => {
expect(rr.select([], config)).toBeNull();
});
});
describe("RR isQuantumExpired()", () => {
it("returns true when quantumRemaining <= 0", () => {
expect(rr.isQuantumExpired(makeRt("p1", 0), config)).toBe(true);
expect(rr.isQuantumExpired(makeRt("p1", -1), config)).toBe(true);
});
it("returns false when quantum not yet exhausted", () => {
expect(rr.isQuantumExpired(makeRt("p1", 1), config)).toBe(false);
expect(rr.isQuantumExpired(makeRt("p1", 2), config)).toBe(false);
});
});
describe("RR shouldPreempt()", () => {
it("always returns false (preemption via quantum expiry only)", () => {
const a = makeRt("p1");
const b = makeRt("p2");
expect(rr.shouldPreempt(a, b, config)).toBe(false);
});
});
// CT-02 integration
describe("CT-02: Round Robin integration", () => {
it("produces correct Gantt: P1(0-2),P2(2-4),P3(4-5),P1(5-7),P2(7-8),P1(8-9)", () => {
const processes = [
p("p1", 0, 5),
p("p2", 0, 3),
p("p3", 0, 1),
];
const engine = new SimulationEngine(processes, config);
const ticks = engine.runAll();
const cpu = ticks.map((t) => t.cpuProcess);
expect(cpu).toHaveLength(9);
expect(cpu[0]).toBe("p1");
expect(cpu[1]).toBe("p1");
expect(cpu[2]).toBe("p2");
expect(cpu[3]).toBe("p2");
expect(cpu[4]).toBe("p3");
expect(cpu[5]).toBe("p1");
expect(cpu[6]).toBe("p1");
expect(cpu[7]).toBe("p2");
expect(cpu[8]).toBe("p1");
});
it("process finishing before quantum exhausted does not rotate", () => {
const engine = new SimulationEngine(
[p("p1", 0, 1), p("p2", 0, 3)],
config,
);
const ticks = engine.runAll();
// P1 finishes at tick 1 (1 < quantum 2), P2 gets CPU immediately
expect(ticks[0]?.cpuProcess).toBe("p1");
expect(ticks[1]?.cpuProcess).toBe("p2");
expect(ticks[2]?.cpuProcess).toBe("p2");
expect(ticks[3]?.cpuProcess).toBe("p2");
});
it("process returning from I/O enters tail of queue", () => {
// P1: CPU:1 → IO:1 → CPU:1; P2: CPU:3
// With quantum=2: P1 runs tick 0, goes to IO; P2 runs ticks 1-2,3 (or until P1 returns)
// P1 IO done at tick 2, enters tail → P2 finishes first if still running
const processes: Process[] = [
{ id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#fff", bursts: [{ type: "cpu", duration: 1 }, { type: "io", duration: 2 }, { type: "cpu", duration: 1 }] },
{ id: "p2", name: "P2", arrivalTime: 0, priority: 5, color: "#fff", bursts: [{ type: "cpu", duration: 3 }] },
];
const engine = new SimulationEngine(processes, config);
const ticks = engine.runAll();
const rts = engine.runtimeStates;
// Both should terminate
expect(rts.every((r) => r.state === "Terminated")).toBe(true);
// P1's second CPU burst should happen after P2 gets at least one quantum
const rt2 = rts.find((r) => r.processId === "p2")!;
const rt1 = rts.find((r) => r.processId === "p1")!;
// P2 has 3 CPU ticks and P1 re-enters after IO — verify both complete
expect(rt2.cpuTime).toBe(3);
expect(rt1.cpuTime).toBe(2);
void ticks; // used for debugging if needed
});
});
+94
View File
@@ -0,0 +1,94 @@
import { describe, it, expect } from "vitest";
import { sjfNp, srtf } from "../algorithms/sjf";
import { SimulationEngine } from "../engine";
import type { Process, ProcessRuntime, SchedulerConfig } from "../types";
function makeRt(id: string, remainingBurst: number, arrivalTick = 0): ProcessRuntime {
return {
processId: id, state: "Ready", remainingBurst, burstIndex: 0,
arrivalTick, startTick: null, finishTick: null,
waitingTime: 0, cpuTime: 0, ioTime: 0, responseTime: null,
quantumRemaining: 2, currentPriority: 5,
};
}
const config: SchedulerConfig = {
algorithm: "SJF_NP", quantum: 2, contextSwitchTime: 0,
isPreemptive: false, agingEnabled: false, agingInterval: 5,
};
const p = (id: string, arrival: number, bursts: { type: "cpu" | "io"; duration: number }[]): Process => ({
id, name: id, arrivalTime: arrival, priority: 5, color: "#fff", bursts,
});
describe("SJF_NP select()", () => {
it("returns null for empty queue", () => {
expect(sjfNp.select([], config)).toBeNull();
});
it("picks shortest remaining burst", () => {
const r1 = makeRt("p1", 8);
const r2 = makeRt("p2", 3);
const r3 = makeRt("p3", 5);
expect(sjfNp.select([r1, r2, r3], config)?.processId).toBe("p2");
});
it("tiebreaks equal burst by arrival then id", () => {
const r1 = makeRt("p2", 4, 1);
const r2 = makeRt("p1", 4, 2);
// p2 arrives earlier
expect(sjfNp.select([r1, r2], config)?.processId).toBe("p2");
});
it("shouldPreempt always false", () => {
const a = makeRt("p1", 4);
const b = makeRt("p2", 2);
expect(sjfNp.shouldPreempt(a, b, config)).toBe(false);
});
});
describe("SRTF shouldPreempt()", () => {
it("preempts when candidate has strictly smaller burst", () => {
const current = makeRt("p1", 7);
const candidate = makeRt("p2", 4);
expect(srtf.shouldPreempt(current, candidate, config)).toBe(true);
});
it("does NOT preempt on equal remaining burst", () => {
const current = makeRt("p1", 4);
const candidate = makeRt("p2", 4);
expect(srtf.shouldPreempt(current, candidate, config)).toBe(false);
});
it("does NOT preempt when current has smaller burst", () => {
const current = makeRt("p1", 3);
const candidate = makeRt("p2", 6);
expect(srtf.shouldPreempt(current, candidate, config)).toBe(false);
});
});
// CT-04 integration via engine
describe("CT-04: SRTF Preemption (integration)", () => {
it("P2 preempts P1 at tick 1, finishes tick 5, P1 finishes tick 12", () => {
const processes = [
p("p1", 0, [{ type: "cpu", duration: 8 }]),
p("p2", 1, [{ type: "cpu", duration: 4 }]),
];
const engine = new SimulationEngine(processes, {
...config, algorithm: "SJF_P",
});
const ticks = engine.runAll();
const rts = engine.runtimeStates;
expect(ticks[0]?.cpuProcess).toBe("p1");
expect(ticks[1]?.cpuProcess).toBe("p2");
expect(ticks[2]?.cpuProcess).toBe("p2");
expect(ticks[3]?.cpuProcess).toBe("p2");
expect(ticks[4]?.cpuProcess).toBe("p2");
const rt2 = rts.find((r) => r.processId === "p2")!;
const rt1 = rts.find((r) => r.processId === "p1")!;
expect(rt2.finishTick).toBe(5);
expect(rt1.finishTick).toBe(12);
});
});
+21
View File
@@ -0,0 +1,21 @@
import type { SchedulerAlgorithm } from "./index";
import { tiebreak } from "./utils";
/**
* FCFS — First Come, First Served
* Non-preemptive. Selects the process with earliest arrival; tiebreak by id.
*/
export const fcfs: SchedulerAlgorithm = {
select(readyQueue) {
if (readyQueue.length === 0) return null;
return [...readyQueue].sort(tiebreak)[0] ?? null;
},
shouldPreempt() {
return false;
},
isQuantumExpired() {
return false;
},
};
+33
View File
@@ -0,0 +1,33 @@
import type { ProcessRuntime } from "../types";
import type { SchedulerAlgorithm } from "./index";
import { tiebreak } from "./utils";
function hrrnRatio(rt: ProcessRuntime): number {
// ratio = (waitingTime + burstTime) / burstTime
// remainingBurst > 0 is guaranteed for processes in the ready queue
if (rt.remainingBurst <= 0) return Infinity;
return (rt.waitingTime + rt.remainingBurst) / rt.remainingBurst;
}
/**
* HRRN — Highest Response Ratio Next
* Non-preemptive. Balances waiting time and burst length to prevent starvation.
*/
export const hrrn: SchedulerAlgorithm = {
select(readyQueue) {
if (readyQueue.length === 0) return null;
return [...readyQueue].sort((a, b) => {
const ratioDiff = hrrnRatio(b) - hrrnRatio(a); // descending
if (ratioDiff !== 0) return ratioDiff;
return tiebreak(a, b);
})[0] ?? null;
},
shouldPreempt() {
return false;
},
isQuantumExpired() {
return false;
},
};
+59
View File
@@ -0,0 +1,59 @@
import type { ProcessRuntime, SchedulerConfig, SchedulingAlgorithm } from "../types";
import { fcfs } from "./fcfs";
import { sjfNp, srtf } from "./sjf";
import { rr } from "./rr";
import { priorityNp, priorityP } from "./priority";
import { hrrn } from "./hrrn";
import { multilevel } from "./multilevel";
// ============================================================
// SchedulerAlgorithm interface
// All implementations must be pure (no mutation of the queue).
// ============================================================
export interface SchedulerAlgorithm {
/**
* Select the best process to dispatch from the ready queue.
* Returns null if the queue is empty.
* Must NOT mutate the array; the engine handles removal.
*/
select(
readyQueue: readonly ProcessRuntime[],
config: SchedulerConfig,
): ProcessRuntime | null;
/**
* For preemptive algorithms: should the currently-running process
* be replaced by `candidate`?
* Called only when cpuProcess != null and a candidate exists.
*/
shouldPreempt(
current: ProcessRuntime,
candidate: ProcessRuntime,
config: SchedulerConfig,
): boolean;
/**
* RR only: has the process exhausted its quantum?
* For all other algorithms this must return false.
*/
isQuantumExpired(
runtime: ProcessRuntime,
config: SchedulerConfig,
): boolean;
}
// ============================================================
// Dispatch table — engine looks up algorithm by name
// ============================================================
export const algorithms: Record<SchedulingAlgorithm, SchedulerAlgorithm> = {
FCFS: fcfs,
SJF_NP: sjfNp,
SJF_P: srtf,
RR: rr,
PRIORITY_NP: priorityNp,
PRIORITY_P: priorityP,
HRRN: hrrn,
MULTILEVEL: multilevel,
};
+19
View File
@@ -0,0 +1,19 @@
import type { SchedulerAlgorithm } from "./index";
/**
* MULTILEVEL — Multilevel Queue (stub)
* Not yet implemented. Deferred to a later phase.
*/
export const multilevel: SchedulerAlgorithm = {
select() {
throw new Error("MULTILEVEL scheduling is not yet implemented");
},
shouldPreempt() {
throw new Error("MULTILEVEL scheduling is not yet implemented");
},
isQuantumExpired() {
throw new Error("MULTILEVEL scheduling is not yet implemented");
},
};
+49
View File
@@ -0,0 +1,49 @@
import type { ProcessRuntime } from "../types";
import type { SchedulerAlgorithm } from "./index";
import { tiebreak } from "./utils";
function prioritySort(a: ProcessRuntime, b: ProcessRuntime): number {
// Lower number = higher priority
if (a.currentPriority !== b.currentPriority)
return a.currentPriority - b.currentPriority;
return tiebreak(a, b);
}
/**
* Priority Non-Preemptive
* Selects process with lowest priority number; does not interrupt running process.
*/
export const priorityNp: SchedulerAlgorithm = {
select(readyQueue) {
if (readyQueue.length === 0) return null;
return [...readyQueue].sort(prioritySort)[0] ?? null;
},
shouldPreempt() {
return false;
},
isQuantumExpired() {
return false;
},
};
/**
* Priority Preemptive
* Preempts running process if a higher-priority (lower number) process arrives.
* Aging is applied by the engine — currentPriority decrements over time.
*/
export const priorityP: SchedulerAlgorithm = {
select(readyQueue) {
if (readyQueue.length === 0) return null;
return [...readyQueue].sort(prioritySort)[0] ?? null;
},
shouldPreempt(current, candidate) {
return candidate.currentPriority < current.currentPriority;
},
isQuantumExpired() {
return false;
},
};
+22
View File
@@ -0,0 +1,22 @@
import type { SchedulerAlgorithm } from "./index";
/**
* RR — Round Robin
* FIFO queue with a configurable quantum.
* select() returns the front of the queue (insertion-order maintained by engine).
* Preemption is driven by quantum expiry, not by a better candidate.
*/
export const rr: SchedulerAlgorithm = {
select(readyQueue) {
return readyQueue[0] ?? null;
},
// RR does not preempt based on candidate priority; quantum expiry handles it.
shouldPreempt() {
return false;
},
isQuantumExpired(runtime) {
return runtime.quantumRemaining <= 0;
},
};
+48
View File
@@ -0,0 +1,48 @@
import type { ProcessRuntime } from "../types";
import type { SchedulerAlgorithm } from "./index";
import { tiebreak } from "./utils";
function sjfSort(a: ProcessRuntime, b: ProcessRuntime): number {
if (a.remainingBurst !== b.remainingBurst)
return a.remainingBurst - b.remainingBurst;
return tiebreak(a, b);
}
/**
* SJF Non-Preemptive — Shortest Job First
* Selects the process with the smallest remaining burst; no preemption.
*/
export const sjfNp: SchedulerAlgorithm = {
select(readyQueue) {
if (readyQueue.length === 0) return null;
return [...readyQueue].sort(sjfSort)[0] ?? null;
},
shouldPreempt() {
return false;
},
isQuantumExpired() {
return false;
},
};
/**
* SRTF — Shortest Remaining Time First (SJF Preemptive)
* At each tick, preempts if a ready process has strictly smaller remainingBurst.
* Equal remaining burst → no preemption (avoids unnecessary context switches).
*/
export const srtf: SchedulerAlgorithm = {
select(readyQueue) {
if (readyQueue.length === 0) return null;
return [...readyQueue].sort(sjfSort)[0] ?? null;
},
shouldPreempt(current, candidate) {
return candidate.remainingBurst < current.remainingBurst;
},
isQuantumExpired() {
return false;
},
};
+11
View File
@@ -0,0 +1,11 @@
import type { ProcessRuntime } from "../types";
// ============================================================
// Shared tiebreak helper (all algorithms use this)
// Primary: arrivalTick ASC Secondary: processId lexicographic ASC
// ============================================================
export function tiebreak(a: ProcessRuntime, b: ProcessRuntime): number {
if (a.arrivalTick !== b.arrivalTick) return a.arrivalTick - b.arrivalTick;
return a.processId.localeCompare(b.processId);
}
+270
View File
@@ -0,0 +1,270 @@
import type {
Process,
ProcessRuntime,
SchedulerConfig,
SimTick,
StateLabel,
} from "./types";
import { algorithms } from "./algorithms/index";
import type { SchedulerAlgorithm } from "./algorithms/index";
// ============================================================
// SimulationEngine
// ============================================================
export class SimulationEngine {
private readonly _processes: Process[];
private readonly _config: SchedulerConfig;
private readonly _algorithm: SchedulerAlgorithm;
private _tick: number; // starts at -1; incremented at top of step()
private _runtimes: Map<string, ProcessRuntime>;
private _cpuProcess: ProcessRuntime | null;
private _readyQueue: ProcessRuntime[]; // ordered array (FIFO for RR)
private _ioQueue: ProcessRuntime[];
private _contextSwitchRemaining: number;
private _prevCpuProcessId: string | null; // track context switch detection
constructor(processes: Process[], config: SchedulerConfig) {
if (processes.length === 0) throw new Error("At least one process required");
this._processes = processes;
this._config = config;
this._algorithm = algorithms[config.algorithm];
this._tick = -1;
this._cpuProcess = null;
this._readyQueue = [];
this._ioQueue = [];
this._contextSwitchRemaining = 0;
this._prevCpuProcessId = null;
this._runtimes = new Map(processes.map((p) => [p.id, initRuntime(p, config)]));
}
// ----------------------------------------------------------
// Public API
// ----------------------------------------------------------
/** Advance one tick; return snapshot of that tick. */
step(): SimTick {
if (this.isFinished) throw new Error("Simulation is already finished");
this._tick++;
// 1. Arrivals
for (const p of this._processes) {
const rt = this._runtimes.get(p.id)!;
if (p.arrivalTime === this._tick && rt.state === "New") {
this._transition(rt, "Ready");
this._readyQueue.push(rt);
}
}
// 2. Preemption & Quantum Check (from previous tick's state)
if (this._cpuProcess !== null && this._readyQueue.length > 0) {
const candidate = this._algorithm.select(this._readyQueue, this._config);
if (
candidate !== null &&
this._algorithm.shouldPreempt(this._cpuProcess, candidate, this._config)
) {
const cpu = this._cpuProcess;
this._transition(cpu, "Ready");
this._readyQueue.push(cpu);
this._cpuProcess = null;
}
}
// 3. Dispatch / Context Switch
if (this._contextSwitchRemaining > 0) {
// Doing context switch this tick
// We don't dispatch.
} else {
if (this._cpuProcess === null && this._readyQueue.length > 0) {
const next = this._algorithm.select(this._readyQueue, this._config);
if (next !== null) {
const idx = this._readyQueue.indexOf(next);
if (idx !== -1) this._readyQueue.splice(idx, 1);
if (
this._config.contextSwitchTime > 0 &&
this._prevCpuProcessId !== null &&
this._prevCpuProcessId !== next.processId
) {
this._readyQueue.unshift(next);
this._contextSwitchRemaining = this._config.contextSwitchTime;
this._prevCpuProcessId = next.processId;
} else {
if (next.startTick === null) next.startTick = this._tick;
next.quantumRemaining = this._config.quantum;
this._transition(next, "Running");
this._prevCpuProcessId = next.processId;
this._cpuProcess = next;
}
}
}
}
// 4. Take Snapshot representing what happens DURING this tick
const snapshot = this._snapshot();
// 5. Passage of Time (Simulate execution for 1 tick)
if (this._contextSwitchRemaining > 0) {
this._contextSwitchRemaining--;
}
if (this._cpuProcess !== null) {
const cpu = this._cpuProcess;
cpu.remainingBurst--;
cpu.cpuTime++;
cpu.quantumRemaining--;
}
for (const rt of this._ioQueue) {
rt.remainingBurst--;
rt.ioTime++;
}
for (const rt of this._readyQueue) {
rt.waitingTime++;
}
// 6. Aging (Priority)
if (
this._config.agingEnabled &&
this._tick > 0 &&
this._tick % this._config.agingInterval === 0
) {
for (const rt of this._readyQueue) {
rt.currentPriority = Math.max(1, rt.currentPriority - 1);
}
}
// 7. Process completions / transition to next state for NEXT tick
if (this._cpuProcess !== null) {
const cpu = this._cpuProcess;
if (cpu.remainingBurst === 0) {
this._advanceBurst(cpu);
const process = this._processes.find((p) => p.id === cpu.processId)!;
const nextBurst = process.bursts[cpu.burstIndex];
if (nextBurst !== undefined && nextBurst.type === "io") {
this._transition(cpu, "Waiting");
this._ioQueue.push(cpu);
} else if (nextBurst !== undefined && nextBurst.type === "cpu") {
this._transition(cpu, "Ready");
this._readyQueue.push(cpu);
} else {
this._transition(cpu, "Terminated");
cpu.finishTick = this._tick + 1;
}
this._cpuProcess = null;
} else if (this._algorithm.isQuantumExpired(cpu, this._config)) {
this._transition(cpu, "Ready");
this._readyQueue.push(cpu);
this._cpuProcess = null;
}
}
// IO completions
const nextIoQueue: ProcessRuntime[] = [];
for (const rt of this._ioQueue) {
if (rt.remainingBurst === 0) {
this._advanceBurst(rt);
const process = this._processes.find((p) => p.id === rt.processId)!;
if (rt.burstIndex < process.bursts.length) {
this._transition(rt, "Ready");
this._readyQueue.push(rt);
} else {
this._transition(rt, "Terminated");
rt.finishTick = this._tick + 1;
}
} else {
nextIoQueue.push(rt);
}
}
this._ioQueue = nextIoQueue;
return snapshot;
}
/** Run to completion; return all tick snapshots. */
runAll(): SimTick[] {
const results: SimTick[] = [];
while (!this.isFinished) {
results.push(this.step());
if (results.length > 10_000) {
throw new Error("Simulation exceeded 10,000 ticks — check for infinite loop");
}
}
return results;
}
get isFinished(): boolean {
for (const rt of this._runtimes.values()) {
if (rt.state !== "Terminated") return false;
}
return true;
}
get currentTick(): number {
return this._tick;
}
get runtimeStates(): ProcessRuntime[] {
return Array.from(this._runtimes.values());
}
// ----------------------------------------------------------
// Private helpers
// ----------------------------------------------------------
private _transition(rt: ProcessRuntime, state: StateLabel): void {
rt.state = state;
}
private _advanceBurst(rt: ProcessRuntime): void {
rt.burstIndex++;
const process = this._processes.find((p) => p.id === rt.processId)!;
const nextBurst = process.bursts[rt.burstIndex];
rt.remainingBurst = nextBurst !== undefined ? nextBurst.duration : 0;
}
private _snapshot(): SimTick {
const states: Record<string, StateLabel> = {};
for (const [id, rt] of this._runtimes) {
states[id] = rt.state;
}
const contextSwitching = this._contextSwitchRemaining > 0;
return {
tick: this._tick,
cpuProcess: contextSwitching ? null : (this._cpuProcess?.processId ?? null),
ioProcesses: this._ioQueue.map((rt) => rt.processId),
readyQueue: this._readyQueue.map((rt) => rt.processId),
states,
contextSwitching,
};
}
}
// ============================================================
// Helper: initialise a ProcessRuntime from a Process
// ============================================================
function initRuntime(p: Process, config: SchedulerConfig): ProcessRuntime {
const firstBurst = p.bursts[0];
if (firstBurst === undefined || firstBurst.type !== "cpu") {
throw new Error(`Process ${p.id}: first burst must be a CPU burst`);
}
return {
processId: p.id,
state: "New",
remainingBurst: firstBurst.duration,
burstIndex: 0,
arrivalTick: p.arrivalTime,
startTick: null,
finishTick: null,
waitingTime: 0,
cpuTime: 0,
ioTime: 0,
responseTime: null,
quantumRemaining: config.quantum,
currentPriority: p.priority,
};
}
+107
View File
@@ -0,0 +1,107 @@
import type {
GlobalMetrics,
ProcessMetrics,
ProcessRuntime,
} from "./types";
// ============================================================
// Per-process metrics
// ============================================================
export function computeProcessMetrics(rt: ProcessRuntime): ProcessMetrics {
if (rt.startTick === null || rt.finishTick === null) {
throw new Error(
`Cannot compute metrics for process ${rt.processId}: not yet terminated`,
);
}
const turnaroundTime = rt.finishTick - rt.arrivalTick;
const waitingTime = turnaroundTime - rt.cpuTime - rt.ioTime;
const responseTime = rt.startTick - rt.arrivalTick;
const cpuUtilization =
turnaroundTime > 0 ? (rt.cpuTime / turnaroundTime) * 100 : 0;
const total = rt.cpuTime + rt.ioTime;
const cpuRatio = total > 0 ? rt.cpuTime / total : 1;
const ioRatio = total > 0 ? rt.ioTime / total : 0;
let boundType: "CPU Bound" | "I/O Bound" | "Balanced";
if (cpuRatio >= 0.65) {
boundType = "CPU Bound";
} else if (ioRatio >= 0.65) {
boundType = "I/O Bound";
} else {
boundType = "Balanced";
}
return {
processId: rt.processId,
arrivalTime: rt.arrivalTick,
startTick: rt.startTick,
finishTick: rt.finishTick,
responseTime,
turnaroundTime,
waitingTime,
cpuTime: rt.cpuTime,
ioTime: rt.ioTime,
cpuUtilization,
boundType,
};
}
// ============================================================
// Global metrics (across all completed processes)
// ============================================================
export function computeGlobalMetrics(
metrics: ProcessMetrics[],
totalTicks: number,
): GlobalMetrics {
if (metrics.length === 0) {
return {
avgResponseTime: 0,
avgTurnaroundTime: 0,
avgWaitingTime: 0,
cpuThroughput: 0,
cpuUtilization: 0,
totalSimulationTime: totalTicks,
};
}
const mean = (values: number[]) =>
values.reduce((a, b) => a + b, 0) / values.length;
const totalCpuTime = metrics.reduce((s, m) => s + m.cpuTime, 0);
return {
avgResponseTime: mean(metrics.map((m) => m.responseTime)),
avgTurnaroundTime: mean(metrics.map((m) => m.turnaroundTime)),
avgWaitingTime: mean(metrics.map((m) => m.waitingTime)),
cpuThroughput: metrics.length / totalTicks,
cpuUtilization: totalTicks > 0 ? (totalCpuTime / totalTicks) * 100 : 0,
totalSimulationTime: totalTicks,
};
}
// ============================================================
// Classify process by declared burst ratios (static, pre-simulation)
// Used in the process list preview badge.
// ============================================================
export function classifyBound(
bursts: { type: "cpu" | "io"; duration: number }[],
): "CPU Bound" | "I/O Bound" | "Balanced" {
let cpu = 0;
let io = 0;
for (const b of bursts) {
if (b.type === "cpu") cpu += b.duration;
else io += b.duration;
}
const total = cpu + io;
if (total === 0) return "Balanced";
const cpuRatio = cpu / total;
const ioRatio = io / total;
if (cpuRatio >= 0.65) return "CPU Bound";
if (ioRatio >= 0.65) return "I/O Bound";
return "Balanced";
}
+80
View File
@@ -0,0 +1,80 @@
import type { Process, SchedulerConfig } from "./types";
export interface Preset {
name: string;
description: string;
processes: Process[];
config: SchedulerConfig;
}
const defaultConfig = (
overrides: Partial<SchedulerConfig> = {},
): SchedulerConfig => ({
algorithm: "FCFS",
quantum: 2,
contextSwitchTime: 0,
isPreemptive: false,
agingEnabled: false,
agingInterval: 5,
...overrides,
});
export const presets: Preset[] = [
{
name: "classic_fcfs",
description: "4 simple processes, no I/O — demonstrates FCFS order",
processes: [
{ id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 5 }] },
{ id: "p2", name: "P2", arrivalTime: 1, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p3", name: "P3", arrivalTime: 2, priority: 5, color: "#FFB74D", bursts: [{ type: "cpu", duration: 1 }] },
{ id: "p4", name: "P4", arrivalTime: 3, priority: 5, color: "#F06292", bursts: [{ type: "cpu", duration: 7 }] },
],
config: defaultConfig({ algorithm: "FCFS" }),
},
{
name: "rr_demo",
description: "5 processes with quantum 3 — Round Robin rotation",
processes: [
{ id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 6 }] },
{ id: "p2", name: "P2", arrivalTime: 0, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 4 }] },
{ id: "p3", name: "P3", arrivalTime: 1, priority: 5, color: "#FFB74D", bursts: [{ type: "cpu", duration: 5 }] },
{ id: "p4", name: "P4", arrivalTime: 2, priority: 5, color: "#F06292", bursts: [{ type: "cpu", duration: 2 }] },
{ id: "p5", name: "P5", arrivalTime: 3, priority: 5, color: "#CE93D8", bursts: [{ type: "cpu", duration: 3 }] },
],
config: defaultConfig({ algorithm: "RR", quantum: 3 }),
},
{
name: "starvation",
description: "High-priority processes starve P5 — demonstrates Priority NP starvation",
processes: [
{ id: "p1", name: "P1", arrivalTime: 0, priority: 1, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p2", name: "P2", arrivalTime: 1, priority: 1, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p3", name: "P3", arrivalTime: 2, priority: 1, color: "#FFB74D", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p4", name: "P4", arrivalTime: 3, priority: 1, color: "#F06292", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p5", name: "P5", arrivalTime: 0, priority: 10, color: "#CE93D8", bursts: [{ type: "cpu", duration: 5 }] },
],
config: defaultConfig({ algorithm: "PRIORITY_NP" }),
},
{
name: "aging_fix",
description: "Same starvation scenario fixed with aging",
processes: [
{ id: "p1", name: "P1", arrivalTime: 0, priority: 1, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p2", name: "P2", arrivalTime: 1, priority: 1, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p3", name: "P3", arrivalTime: 2, priority: 1, color: "#FFB74D", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p4", name: "P4", arrivalTime: 3, priority: 1, color: "#F06292", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p5", name: "P5", arrivalTime: 0, priority: 10, color: "#CE93D8", bursts: [{ type: "cpu", duration: 5 }] },
],
config: defaultConfig({ algorithm: "PRIORITY_P", isPreemptive: true, agingEnabled: true, agingInterval: 3 }),
},
{
name: "io_heavy",
description: "Mixed CPU-bound and I/O-bound processes",
processes: [
{ id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 6 }, { type: "io", duration: 1 }, { type: "cpu", duration: 2 }] },
{ id: "p2", name: "P2", arrivalTime: 0, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 1 }, { type: "io", duration: 8 }, { type: "cpu", duration: 1 }] },
{ id: "p3", name: "P3", arrivalTime: 2, priority: 5, color: "#FFB74D", bursts: [{ type: "cpu", duration: 4 }, { type: "io", duration: 2 }, { type: "cpu", duration: 4 }] },
],
config: defaultConfig({ algorithm: "SJF_P", isPreemptive: true }),
},
];
+103
View File
@@ -0,0 +1,103 @@
// ============================================================
// Core types — Process Scheduler Simulator
// ============================================================
export type SchedulingAlgorithm =
| "FCFS"
| "SJF_NP"
| "SJF_P"
| "RR"
| "PRIORITY_NP"
| "PRIORITY_P"
| "MULTILEVEL"
| "HRRN";
// --------------- Static process definition ------------------
export interface BurstSegment {
type: "cpu" | "io";
duration: number; // must be ≥ 1
}
export interface Process {
id: string;
name: string;
arrivalTime: number; // tick of arrival (≥ 0)
priority: number; // 1 (highest) – 10 (lowest); default 5
color: string; // HEX color for Gantt display
bursts: BurstSegment[]; // must start (and end) with a CPU burst
}
// --------------- Scheduler configuration --------------------
export interface SchedulerConfig {
algorithm: SchedulingAlgorithm;
quantum: number; // for RR (default 2)
contextSwitchTime: number; // overhead between processes (default 0)
isPreemptive: boolean; // applicable to SJF and Priority
agingEnabled: boolean; // prevent starvation in Priority scheduling
agingInterval: number; // ticks in Ready before priority-- (min 1)
}
// --------------- Runtime state ------------------------------
export type StateLabel =
| "New" // before arrivalTime
| "Ready" // in ready queue, waiting for CPU
| "Running" // executing CPU burst
| "Waiting" // executing I/O burst
| "Terminated"; // finished all bursts
export interface ProcessRuntime {
// ---- spec fields ----
processId: string;
state: StateLabel;
remainingBurst: number; // ticks left in current burst
burstIndex: number; // index into Process.bursts
arrivalTick: number; // tick when process first entered Ready
startTick: number | null; // tick of first Running state
finishTick: number | null; // tick when process Terminated
waitingTime: number; // accumulated ticks in Ready
cpuTime: number; // accumulated ticks in Running
ioTime: number; // accumulated ticks in Waiting
responseTime: number | null; // startTick - arrivalTick (set once)
// ---- engine-internal fields ----
quantumRemaining: number; // remaining quantum for RR; reset on dispatch
currentPriority: number; // mutable via aging; starts = Process.priority
}
// --------------- Tick snapshot ------------------------------
export interface SimTick {
tick: number;
cpuProcess: string | null; // processId running on CPU (null if idle)
ioProcesses: string[]; // processIds in I/O
readyQueue: string[]; // processIds in ready queue (in queue order)
states: Record<string, StateLabel>; // all processId → state
contextSwitching: boolean; // true during context-switch overhead
}
// --------------- Metrics ------------------------------------
export interface ProcessMetrics {
processId: string;
arrivalTime: number;
startTick: number;
finishTick: number;
responseTime: number;
turnaroundTime: number;
waitingTime: number;
cpuTime: number;
ioTime: number;
cpuUtilization: number; // cpuTime / turnaroundTime * 100
boundType: "CPU Bound" | "I/O Bound" | "Balanced";
}
export interface GlobalMetrics {
avgResponseTime: number;
avgTurnaroundTime: number;
avgWaitingTime: number;
cpuThroughput: number; // processes completed / total ticks
cpuUtilization: number; // Σ cpuTime / totalTicks * 100
totalSimulationTime: number;
}
+50
View File
@@ -0,0 +1,50 @@
import { useState, useCallback } from "react";
/**
* Generic hook for reading/writing typed values to localStorage.
*
* Returns [value, setValue, removeValue].
* Read/write errors are silently ignored.
*/
export function useLocalStorage<T>(
key: string,
initialValue: T,
): readonly [T, (value: T | ((prev: T) => T)) => void, () => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item !== null ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback(
(value: T | ((prev: T) => T)) => {
setStoredValue((prev) => {
const next =
typeof value === "function"
? (value as (prev: T) => T)(prev)
: value;
try {
window.localStorage.setItem(key, JSON.stringify(next));
} catch {
// ignore write errors (e.g. private mode quota exceeded)
}
return next;
});
},
[key],
);
const removeValue = useCallback(() => {
try {
window.localStorage.removeItem(key);
} catch {
// ignore
}
setStoredValue(initialValue);
}, [key, initialValue]);
return [storedValue, setValue, removeValue] as const;
}
+50
View File
@@ -0,0 +1,50 @@
import { useEffect, useRef } from "react";
import { useSimulationStore } from "../store/simulationStore";
import { useUiStore } from "../store/uiStore";
/**
* Drives the simulation animation loop using requestAnimationFrame.
* Call once at the app root. Returns nothing — side-effect only.
*
* Tick interval = 1000 / speed (ms).
* The loop is active only while status === "running".
*/
export function useSimulation(): void {
const status = useSimulationStore((s) => s.status);
const stepOnce = useSimulationStore((s) => s.stepOnce);
const speed = useUiStore((s) => s.speed);
const rafRef = useRef<number | undefined>(undefined);
const lastTickTime = useRef<number>(0);
useEffect(() => {
if (status !== "running") {
if (rafRef.current !== undefined) {
cancelAnimationFrame(rafRef.current);
rafRef.current = undefined;
}
return;
}
const interval = 1000 / speed;
const loop = (timestamp: number) => {
if (timestamp - lastTickTime.current >= interval) {
lastTickTime.current = timestamp;
stepOnce();
}
rafRef.current = requestAnimationFrame(loop);
};
// Reset timing so the first tick fires immediately after play
lastTickTime.current = 0;
rafRef.current = requestAnimationFrame(loop);
return () => {
if (rafRef.current !== undefined) {
cancelAnimationFrame(rafRef.current);
rafRef.current = undefined;
}
};
}, [status, speed, stepOnce]);
}
+20
View File
@@ -0,0 +1,20 @@
@import "tailwindcss";
@theme {
--color-state-new: #475569; /* slate-600 */
--color-state-ready: #f59e0b; /* amber-500 */
--color-state-running: #10b981; /* emerald-500 */
--color-state-waiting: #06b6d4; /* cyan-500 */
--color-state-terminated: #94a3b8; /* slate-400 */
--color-state-ctx: #ef4444; /* red-500 */
}
@layer utilities {
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);
+62
View File
@@ -0,0 +1,62 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import type { Process, SchedulerConfig } from "@core/types";
// ============================================================
// Defaults
// ============================================================
const DEFAULT_CONFIG: SchedulerConfig = {
algorithm: "FCFS",
quantum: 2,
contextSwitchTime: 0,
isPreemptive: false,
agingEnabled: false,
agingInterval: 5,
};
// ============================================================
// Store
// ============================================================
interface ProcessStore {
processes: Process[];
config: SchedulerConfig;
addProcess: (p: Process) => void;
updateProcess: (id: string, updates: Partial<Process>) => void;
removeProcess: (id: string) => void;
clearProcesses: () => void;
setConfig: (updates: Partial<SchedulerConfig>) => void;
loadPreset: (processes: Process[], config: SchedulerConfig) => void;
}
export const useProcessStore = create<ProcessStore>()(
persist(
(set) => ({
processes: [],
config: DEFAULT_CONFIG,
addProcess: (p) =>
set((s) => ({ processes: [...s.processes, p] })),
updateProcess: (id, updates) =>
set((s) => ({
processes: s.processes.map((p) =>
p.id === id ? { ...p, ...updates } : p,
),
})),
removeProcess: (id) =>
set((s) => ({ processes: s.processes.filter((p) => p.id !== id) })),
clearProcesses: () => set({ processes: [] }),
setConfig: (updates) =>
set((s) => ({ config: { ...s.config, ...updates } })),
loadPreset: (processes, config) => set({ processes, config }),
}),
{ name: "pss:session" },
),
);
+69
View File
@@ -0,0 +1,69 @@
import { create } from "zustand";
import { SimulationEngine } from "@core/engine";
import type { Process, SchedulerConfig, SimTick } from "@core/types";
// ============================================================
// Types
// ============================================================
export type SimStatus = "idle" | "running" | "paused" | "finished";
interface SimulationStore {
status: SimStatus;
ticks: SimTick[];
// Mutable engine instance — reactivity comes from ticks[] and status, not engine itself
engine: SimulationEngine | null;
// Saved so reset() can recreate the engine without coupling to processStore
_processes: Process[];
_config: SchedulerConfig | null;
init: (processes: Process[], config: SchedulerConfig) => void;
stepOnce: () => void;
play: () => void;
pause: () => void;
reset: () => void;
}
// ============================================================
// Store
// ============================================================
export const useSimulationStore = create<SimulationStore>()((set, get) => ({
status: "idle",
ticks: [],
engine: null,
_processes: [],
_config: null,
init: (processes, config) => {
const engine = new SimulationEngine(processes, config);
set({
engine,
ticks: [],
status: "paused",
_processes: processes,
_config: config,
});
},
stepOnce: () => {
const { engine, ticks, status } = get();
if (!engine || engine.isFinished || status === "finished") return;
const tick = engine.step();
set({
ticks: [...ticks, tick],
status: engine.isFinished ? "finished" : status,
});
},
play: () => set({ status: "running" }),
pause: () => set({ status: "paused" }),
reset: () => {
const { _processes, _config } = get();
if (_processes.length === 0 || _config === null) return;
const engine = new SimulationEngine(_processes, _config);
set({ engine, ticks: [], status: "paused" });
},
}));
+28
View File
@@ -0,0 +1,28 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
// ============================================================
// Speed options
// ============================================================
export const SPEED_OPTIONS = [0.25, 0.5, 1, 2, 4] as const;
export type SpeedOption = (typeof SPEED_OPTIONS)[number];
// ============================================================
// Store
// ============================================================
interface UiStore {
speed: number;
setSpeed: (speed: number) => void;
}
export const useUiStore = create<UiStore>()(
persist(
(set) => ({
speed: 1,
setSpeed: (speed) => set({ speed }),
}),
{ name: "pss:ui" },
),
);
+1
View File
@@ -0,0 +1 @@
import '@testing-library/jest-dom';
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"paths": {
"@core/*": ["./src/core/*"],
"@components/*": ["./src/components/*"],
"@store/*": ["./src/store/*"]
}
},
"include": ["src"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+18
View File
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true
},
"include": ["vite.config.ts"]
}
+25
View File
@@ -0,0 +1,25 @@
/// <reference types="vitest" />
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import path from "path";
export default defineConfig({
plugins: [react(), tailwindcss()],
test: {
globals: true,
environment: "jsdom",
setupFiles: "./src/test/setup.ts",
coverage: {
provider: "v8",
include: ["src/core/**", "src/components/**"],
},
},
resolve: {
alias: {
"@core": path.resolve(__dirname, "./src/core"),
"@components": path.resolve(__dirname, "./src/components"),
"@store": path.resolve(__dirname, "./src/store"),
},
},
});