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