feat: Implement initial Process Scheduling Simulator application with core logic, algorithms, UI components, and project setup.
This commit is contained in:
commit
083fc0027e
54 files changed
+8815
No files matched your search
+19
@@ -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
|
||||||
@@ -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
@@ -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>
|
||||||
Generated
+4563
File diff suppressed because it is too large.
Load diff
@@ -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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import AppLayout from './components/Layout/AppLayout';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<AppLayout />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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");
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
@@ -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 }),
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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]);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
@@ -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" },
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -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" });
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -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" },
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in new issue
Block a user