refactor: Convert scheduling algorithms to class-based BaseAlgorithm structure, update engine and tests, and add new algorithm and metrics documentation.

This commit is contained in:
Lucas Guerra committed 2026-03-23 16:04:14 -03:00
1 parent 9c552e53fa
commit 571d8e51d2
26 files changed
+1583 -1221

No files matched your search

+121 -631
View File
@@ -1,713 +1,203 @@
# Process Scheduler Simulator — Especificação Técnica Completa
# 🖥️ Process Scheduler Simulator
> **Versão:** 1.0
> **Data:** 2026-03-19
> **Autor:** Lucas Rayan Guerra
> **Stack alvo:** React + Vite (Single Page Application, browser-only, sem backend)
> Uma ferramenta educacional interativa para visualizar e comparar algoritmos clássicos de escalonamento de processos de Sistemas Operacionais.
[![React](https://img.shields.io/badge/React-18-61DAFB?logo=react&logoColor=white)](https://react.dev/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Vite](https://img.shields.io/badge/Vite-6-646CFF?logo=vite&logoColor=white)](https://vitejs.dev/)
[![Tailwind CSS](https://img.shields.io/badge/Tailwind-4-06B6D4?logo=tailwindcss&logoColor=white)](https://tailwindcss.com/)
[![Vitest](https://img.shields.io/badge/Tested_with-Vitest-6E9F18?logo=vitest&logoColor=white)](https://vitest.dev/)
[![License: ISC](https://img.shields.io/badge/License-ISC-yellow.svg)](https://opensource.org/licenses/ISC)
---
## 1. Visão Geral
## ✨ O que é?
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.
O **Process Scheduler Simulator (PSS)** roda completamente no navegador — sem servidor, sem instalação de dependências de sistema. Você define os processos, escolhe o algoritmo, e assiste à simulação acontecer em tempo real através do diagrama de Gantt.
### 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
Ideal para:
- 📚 Estudantes de Sistemas Operacionais querendo visualizar o que antes era só teoria
- 👩‍🏫 Professores buscando uma ferramenta interativa para sala de aula
- 🔍 Profissionais revisitando fundamentos de SO
---
## 2. Requisitos Funcionais
## 🚀 Quick Start
### RF-01 — Gerenciamento de Processos
### Pré-requisitos
| 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 |
- [Node.js](https://nodejs.org/) 20+
- npm 10+
### RF-02 — Configuração do Escalonador
### Instalação e execução local
| 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) |
```bash
# Clone o repositório
git clone https://github.com/lucasrguerra/pss.git
cd pss
### RF-03 — Simulação
# Instale as dependências
npm install
| 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
}
# Inicie o servidor de desenvolvimento
npm run dev
```
**Exemplo de processo:**
Acesse [http://localhost:5173](http://localhost:5173) no seu navegador.
```json
{
"id": "p1",
"name": "P1",
"arrivalTime": 0,
"priority": 3,
"color": "#4FC3F7",
"bursts": [
{ "type": "cpu", "duration": 4 },
{ "type": "io", "duration": 3 },
{ "type": "cpu", "duration": 2 }
]
}
### Build de produção
```bash
npm run build
npm run preview
```
> **Regra:** `bursts` deve ter estrutura CPU → (IO → CPU)\* — sempre começa com CPU. Mínimo: 1 burst CPU.
### Com Docker
### 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
}
```bash
docker compose up
```
---
## 4. Algoritmos de Escalonamento
## 🎮 Como usar
### 4.1 Lista de Algoritmos Suportados
1. **Crie seus processos** — No painel esquerdo, adicione processos definindo nome, tempo de chegada, prioridade e sequência de bursts (CPU e I/O). Ou use o gerador aleatório para começar rápido.
2. **Configure o escalonador** — Escolha o algoritmo e ajuste os parâmetros (quantum para RR, aging para Priority, etc.).
3. **Execute a simulação** — Use os controles de Play/Pause/Step para rodar a simulação na velocidade que quiser (0.25× até 4×).
4. **Analise os resultados** — O diagrama de Gantt mostra em tempo real o estado de cada processo. Ao final, o painel de métricas exibe turnaround, waiting time, throughput e mais.
| 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.
> 💡 **Dica:** Carregue um dos **presets** disponíveis para ver fenômenos clássicos como o efeito de comboio do FCFS ou starvation com Priority Scheduling.
---
## 5. Definição de CPU Bound vs. I/O Bound
## ⚙️ Algoritmos Suportados
A classificação é feita **por processo**, baseada na proporção de tempo total gasto:
| Algoritmo | Preemptivo? | Parâmetros |
|-----------|:-----------:|------------|
| **FCFS** — First Come, First Served | ❌ | — |
| **SJF** — Shortest Job First | ❌ | — |
| **SRTF** — Shortest Remaining Time First | ✅ | — |
| **Round Robin (RR)** | ✅ | Quantum |
| **Priority NP** — Prioridade Não-Preemptivo | ❌ | Aging (opcional) |
| **Priority P** — Prioridade Preemptivo | ✅ | Aging (opcional) |
| **HRRN** — Highest Response Ratio Next | ❌ | — |
| **Multilevel Queue** | ✅ | Quantum por fila |
```
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)
> Consulte a [documentação dos algoritmos](./docs/algorithms.md) para detalhes sobre como cada um funciona.
---
## 6. Métricas
## 📊 Métricas calculadas
### 6.1 Métricas por Processo
Para cada processo simulado, o PSS calcula automaticamente:
| 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 |
- **Response Time** — tempo até o primeiro uso de CPU
- **Turnaround Time** — tempo total desde a chegada até o término
- **Waiting Time** — tempo total na fila Ready
- **CPU Time / I/O Time** — tempo efetivo em cada tipo de burst
- **CPU Utilization** — percentual do turnaround em CPU
- **Bound Type** — classificação automática em CPU Bound, I/O Bound ou 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 |
E globalmente: médias de response/turnaround/waiting, throughput e utilização global de CPU.
---
## 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
## 🗂️ Estrutura do Projeto
```
src/
├── core/ # Lógica pura — sem React
│ ├── algorithms/
│ ├── algorithms/ # Implementação de cada algoritmo
│ │ ├── base.ts # Classe abstrata BaseScheduler
│ │ ├── fcfs.ts
│ │ ├── sjf.ts
│ │ ├── rr.ts
│ │ ├── priority.ts
│ │ ├── priority_rr.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
│ │ └── index.ts # Dispatch por algoritmo
│ ├── engine.ts # SimulationEngine — orquestra os ticks
│ ├── metrics.ts # Cálculo de métricas por processo e globais
│ ├── types.ts # Todas as interfaces TypeScript
│ └── 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)
├── store/ # Estado global com Zustand
├── components/ # Componentes React
│ ├── ProcessPanel/ # Criação e edição de processos
│ ├── SchedulerPanel/ # Seleção de algoritmo e parâmetros
│ ├── ControlBar/ # Play, Pause, Step, Reset, velocidade
│ ├── GanttChart/ # Diagrama de Gantt interativo
│ ├── MetricsPanel/ # Tabela, gráficos e métricas globais
│ └── shared/ # Botões, badges, tooltips, modais
│
├── 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]
└── hooks/ # Hooks personalizados (loop de animação, export, etc.)
```
---
## 8. Motor de Simulação (`SimulationEngine`)
## 📦 Presets disponíveis
### 8.1 Interface Pública
Carregue cenários clássicos com um clique para explorar fenômenos de SO:
```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()
```
| Preset | O que demonstra |
|--------|-----------------|
| `classic_fcfs` | Execução básica com FCFS, sem I/O |
| `convoy_effect` | Efeito de comboio: um processo longo bloqueando vários curtos |
| `rr_demo` | Revezamento justo com Round Robin (quantum=3) |
| `starvation` | Processos de baixa prioridade nunca recebem CPU |
| `aging_fix` | O mesmo cenário de starvation, resolvido com aging |
| `io_heavy` | Mix de processos CPU Bound vs I/O Bound |
| `multilevel_demo` | Múltiplas filas com prioridades diferentes |
---
## 9. Interface do Usuário
## 🧪 Testes
### 9.1 Layout Geral (Desktop)
```bash
# Roda todos os testes unitários
npm test
```
┌─────────────────────────────────────────────────────────────────────┐
│ 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) │ │
│ │ └─────────────────────────────────────────────────┘ │
└──────────────┴──────────────────────────────────────────────────────┘
# Modo watch (re-executa ao salvar)
npm run test:watch
# Com relatório de cobertura
npm run test:coverage
```
### 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)
Os testes cobrem a engine de simulação, todos os algoritmos de escalonamento, e o cálculo de métricas.
---
## 10. Cenários Pré-definidos (Presets)
## 🛠️ Stack Tecnológica
| 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 |
| Camada | Tecnologia |
|--------|-----------|
| Framework UI | React 18 |
| Linguagem | TypeScript 5 |
| Build tool | Vite 6 |
| Estilos | Tailwind CSS 4 |
| Estado global | Zustand 5 |
| Gráficos | Recharts |
| Ícones | Lucide React |
| Testes | Vitest + Testing Library |
---
## 11. Requisitos Não-Funcionais
## 📄 Documentação adicional
| 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+ |
- [📐 Algoritmos de Escalonamento](./docs/algorithms.md) — descrição detalhada de cada algoritmo, regras de desempate e exemplos
- [📏 Métricas](./docs/metrics.md) — fórmulas e definições de todas as métricas calculadas
---
## 12. Plano de Implementação (Fases)
## 🤝 Contribuindo
### 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**
Contribuições são bem-vindas! Sinta-se livre para abrir uma [issue](https://github.com/lucasrguerra/pss/issues) ou enviar um Pull Request.
---
## 13. Casos de Teste Funcionais
## 📜 Licença
### 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 |
Distribuído sob a [Licença ISC](./LICENSE). Desenvolvido por [Lucas Rayan Guerra](https://github.com/lucasrguerra).
BIN
View File
Binary file not shown.
+162
View File
@@ -0,0 +1,162 @@
# 📐 Algoritmos de Escalonamento
Este documento descreve em detalhes os algoritmos de escalonamento implementados no PSS. Para cada algoritmo são apresentados o princípio de funcionamento, parâmetros, regras de desempate e exemplos didáticos.
---
## Regras Gerais
- **Unidade de tempo:** o simulador opera em *ticks* discretos. Cada tick representa 1 unidade de tempo de CPU.
- **Bursts:** cada processo possui uma sequência de bursts alternados: `CPU → I/O → CPU → ...`, sempre começando com CPU.
- **Desempate:** quando dois processos estão empatados no critério de seleção do algoritmo, a prioridade vai para o que chegou primeiro (`arrivalTime`). Em segundo desempate, usa-se o `id` em ordem lexicográfica.
- **Context Switch:** se configurado (≥ 1 tick), um overhead é inserido toda vez que a CPU troca de processo.
---
## FCFS — First Come, First Served
**Tipo:** Não-preemptivo
### Como funciona
Talvez o algoritmo mais intuitivo: o processo que chegou primeiro é atendido primeiro. A fila Ready é ordenada por `arrivalTime`. Quando um processo ocupa a CPU, ele permanece lá até completar seu burst de CPU atual — sem interrupções.
### Exemplo
```
P1: chegada=0, CPU: 4 ticks
P2: chegada=1, CPU: 3 ticks
P3: chegada=2, CPU: 1 tick
Gantt: | P1 P1 P1 P1 | P2 P2 P2 | P3 |
Tick: 0 1 2 3 4 5 6 7
```
| Processo | Turnaround | Waiting |
|----------|:----------:|:-------:|
| P1 | 4 | 0 |
| P2 | 6 | 3 |
| P3 | 6 | 5 |
### Observação: Efeito de Comboio
Se um processo longo chega antes de vários processos curtos, estes ficam esperando por muito tempo — mesmo que a CPU esteja "disponível" para eles. Este fenômeno é chamado de **Convoy Effect** e é um dos principais problemas do FCFS. Experimente o preset `convoy_effect` para visualizá-lo.
---
## SJF — Shortest Job First (Não-Preemptivo)
**Tipo:** Não-preemptivo
### Como funciona
Ao selecionar o próximo processo, o SJF escolhe aquele com o **menor burst de CPU restante** na fila Ready. Uma vez na CPU, o processo não é interrompido até completar seu burst.
O SJF é ótimo para minimizar o tempo médio de espera, mas tem um problema prático: em sistemas reais, a duração dos bursts futuros é desconhecida. No simulador, você define os bursts explicitamente, então o SJF pode ser aplicado perfeitamente.
### Parâmetros
Nenhum parâmetro adicional.
---
## SRTF — Shortest Remaining Time First (SJF Preemptivo)
**Tipo:** Preemptivo
### Como funciona
Versão preemptiva do SJF. A cada tick, o escalonador verifica se algum processo na fila Ready possui tempo de burst restante **menor** que o processo atualmente em execução. Se sim, ocorre uma preempção: o processo atual é devolvido à fila Ready e o novo candidato assume a CPU.
### Exemplo
```
P1: chegada=0, CPU: 8 ticks
P2: chegada=1, CPU: 4 ticks
Tick 0: P1 começa (remaining=8)
Tick 1: P2 chega (remaining=4 < 7) → preempção!
Tick 5: P2 termina → P1 retoma (remaining=7)
Tick 12: P1 termina
```
---
## Round Robin (RR)
**Tipo:** Preemptivo
**Parâmetro:** `quantum` (fatia de tempo, padrão: 2)
### Como funciona
A fila Ready é tratada como uma fila circular. Cada processo recebe no máximo `quantum` ticks de CPU consecutivos. Se ao final do quantum o processo ainda tiver burst restante, ele retorna ao **fim** da fila Ready e aguarda sua vez.
I/O bursts **não** consomem quantum — o processo sai voluntariamente da CPU quando inicia I/O.
### Tradeoff
Quanto menor o quantum, mais justo o revezamento, mas maior o overhead de context switches. Quanto maior, o RR se aproxima do FCFS.
---
## Priority Scheduling (Não-Preemptivo e Preemptivo)
**Tipo:** Não-preemptivo ou Preemptivo (selecione no painel)
**Parâmetros:** Aging (opcional) + intervalo de aging
### Como funciona
Cada processo tem uma prioridade de `1` (maior) a `10` (menor). O escalonador sempre seleciona o processo de maior prioridade na fila Ready.
- **Não-preemptivo:** uma vez na CPU, o processo termina seu burst sem ser interrompido.
- **Preemptivo:** se um processo de prioridade maior chegar, o atual é preemptado imediatamente.
### Starvation e Aging
O problema clássico do Priority Scheduling é o **starvation**: processos de baixa prioridade podem *nunca* receber a CPU se processos de alta prioridade chegam continuamente.
A solução é o **aging**: a cada `agingInterval` ticks que um processo permanece na fila Ready, sua prioridade é incrementada em 1 (ou seja, o valor numérico diminui), até o mínimo de `1`. Com o tempo, mesmo processos de baixa prioridade chegam à cabeça da fila.
Experimente os presets `starvation` e `aging_fix` para ver esta diferença.
---
## HRRN — Highest Response Ratio Next
**Tipo:** Não-preemptivo
### Como funciona
O HRRN tenta ser mais justo que o SJF levando em conta o tempo de espera do processo. A cada seleção, calcula-se o **Response Ratio** de cada processo na fila:
```
ratio = (waitingTime + burstTime) / burstTime
```
O processo com o **maior ratio** é selecionado. Isso faz com que processos que esperam há muito tempo tenham sua prioridade aumentada naturalmente, evitando starvation.
| Situação | Efeito no ratio |
|----------|----------------|
| Processo com burst curto | ratio alto desde o início |
| Processo esperando há muito tempo | ratio cresce com o tempo |
| Processo com burst longo + pouco wait | ratio baixo |
---
## Multilevel Queue
**Tipo:** Preemptivo
**Parâmetros:** número de filas, quantum por fila
### Como funciona
Os processos são distribuídos em múltiplas filas com diferentes prioridades. Cada fila tem seu próprio quantum de Round Robin. Filas de maior prioridade sempre são servidas primeiro — uma fila de menor prioridade só recebe CPU se todas as filas acima dela estiverem vazias.
Isso permite tratar diferentes classes de processos de forma distinta: por exemplo, processos interativos em filas de alta prioridade (quantum pequeno) e processos batch em filas de baixa prioridade (quantum maior).
---
## Referências
- Silberschatz, A., Galvin, P. B., & Gagne, G. — *Operating System Concepts*, 10ª edição
- Tanenbaum, A. S. — *Modern Operating Systems*, 4ª edição
+114
View File
@@ -0,0 +1,114 @@
# 📏 Métricas
O PSS calcula automaticamente um conjunto de métricas ao final (ou durante) a simulação. Este documento explica cada métrica, sua fórmula, o que ela representa e como lê-la nos gráficos.
---
## Métricas por Processo
Estas métricas são calculadas individualmente para cada processo simulado.
| Métrica | Fórmula | O que significa |
|---------|---------|----------------|
| **Arrival Time** | `arrivalTime` | Tick em que o processo entrou na fila Ready pela primeira vez |
| **First CPU Tick** | `startTick` | Tick em que o processo usou a CPU pela primeira vez |
| **Finish Time** | `finishTick` | Tick em que o processo encerrou sua execução |
| **Response Time** | `startTick − arrivalTime` | Quanto tempo o processo esperou antes de receber a CPU pela primeira vez |
| **Turnaround Time** | `finishTick − arrivalTime` | Tempo total de vida do processo (chegada → término) |
| **Waiting Time** | `turnaround − cpuTime − ioTime` | Tempo total na fila Ready (sem fazer nada útil) |
| **CPU Time** | Σ ticks em estado Running | Total de CPU efetivamente consumido |
| **I/O Time** | Σ ticks em estado Waiting | Total de tempo aguardando dispositivos de I/O |
| **CPU Utilization** | `cpuTime / turnaround × 100` | Percentual do turnaround em que o processo estava de fato usando CPU |
| **Bound Type** | ver abaixo | Classificação do processo como CPU Bound, I/O Bound ou Balanced |
### Visualização na tabela
Na aba **Por Processo**, cada linha exibe mini barras de progresso horizontais inline ao lado dos valores numéricos. As barras são proporcionais ao **maior valor da mesma métrica entre todos os processos**, permitindo comparação visual imediata:
| Coluna | Cor da barra |
|--------|-------------|
| Response Time | Âmbar |
| Turnaround Time | Azul |
| Waiting Time | Laranja |
| CPU Time | Esmeralda |
| I/O Time | Ciano |
| CPU Utilization | Gradiente violeta → azul |
---
## Métricas Globais
Calculadas sobre o conjunto completo de processos da simulação.
| Métrica | Fórmula |
|---------|---------|
| **Avg Response Time** | Média dos response times de todos os processos |
| **Avg Turnaround Time** | Média dos turnaround times |
| **Avg Waiting Time** | Média dos waiting times |
| **CPU Throughput** | `processos concluídos / tempo total de simulação` |
| **CPU Utilization (global)** | `Σ ticks em Running / tempo total × 100` |
| **Total Simulation Time** | Tick do último evento de término |
Na aba **Global**, cada card de tempo (Response, Turnaround, Waiting) exibe uma barra proporcional ao **tempo total de simulação**, indicando "quanto desse tempo total o processo médio passa nesse estado". A barra de CPU Utilization usa um gradiente âmbar → verde para indicar saúde da utilização.
---
## Classificação CPU Bound vs. I/O Bound
Ao final da simulação, cada processo é classificado automaticamente com base na proporção de tempo gasto em CPU e I/O:
```
cpuRatio = cpuTime / (cpuTime + ioTime)
ioRatio = ioTime / (cpuTime + ioTime)
cpuRatio ≥ 0.65 → CPU Bound
ioRatio ≥ 0.65 → I/O Bound
caso contrário → Balanced
```
- **CPU Bound:** o processo passa a maior parte do tempo usando o processador.
- **I/O Bound:** o processo passa a maior parte do tempo esperando dispositivos de entrada/saída.
- **Balanced:** o processo usa CPU e I/O de forma equilibrada.
> **Na lista de processos:** estimativa calculada sobre os bursts declarados (antes da simulação).
> **Na tabela de métricas:** classificação calculada sobre os tempos reais registrados.
---
## Gráficos (aba "Gráfico")
### Perfil Comparativo — Radar Chart
Exibido quando há **2 ou mais processos**. Cada eixo representa uma dimensão de tempo, normalizada de 0–100% em relação ao maior valor dessa dimensão entre todos os processos:
| Eixo | Significado |
|------|------------|
| **Turnaround** | Tempo total de vida relativo |
| **Response** | Tempo até a primeira CPU relativo |
| **Waiting** | Tempo na fila Ready relativo |
| **CPU Time** | Uso de CPU relativo |
| **I/O Time** | Uso de I/O relativo |
Cada processo é desenhado como um polígono colorido (a cor configurada no processo). Polígonos mais "amplos" indicam processos com tempos maiores nas dimensões correspondentes. O radar é ideal para identificar **diferenças de perfil** entre processos — por exemplo, um processo CPU Bound terá o eixo CPU Time muito maior que o eixo I/O Time.
### Breakdown por Processo — Stacked Bar Chart
Sempre exibido. Cada barra representa um processo, com os segmentos empilhados:
- **Âmbar** — Waiting Time
- **Verde** — CPU Time
- **Ciano** — I/O Time
A altura total de cada barra equivale ao **Turnaround Time** do processo.
---
## Exemplo
Considere três processos com os seguintes bursts:
| Processo | Bursts | cpuTime | ioTime | cpuRatio | Classificação |
|----------|--------|:-------:|:------:|:--------:|:-------------:|
| P1 | CPU:8, IO:1, CPU:1 | 9 | 1 | 90% | CPU Bound |
| P2 | CPU:1, IO:8, CPU:1 | 2 | 8 | 20% | I/O Bound |
| P3 | CPU:4, IO:2, CPU:4 | 8 | 2 | 80% | CPU Bound |
+74 -18
View File
@@ -1,3 +1,11 @@
import {
Clock,
Timer,
Hourglass,
Cpu,
Zap,
Activity,
} from 'lucide-react';
import type { GlobalMetrics as GlobalMetricsType } from '@core/types';
interface GlobalMetricsProps {
@@ -8,14 +16,36 @@ interface MetricCardProps {
label: string;
value: string;
sub?: string;
accent?: string;
accentColor: string; // hex or CSS color for the bar fill
accentClass: string; // tailwind text class for the value
icon: React.ReactNode;
barPct?: number; // 0–100; undefined = no bar
}
const MetricCard = ({ label, value, sub, accent = 'text-blue-300' }: MetricCardProps) => (
<div className="bg-slate-800 border border-slate-700 rounded-lg px-4 py-3 flex flex-col gap-1">
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-medium">{label}</span>
<span className={`text-xl font-bold font-mono ${accent}`}>{value}</span>
const MetricCard = ({
label,
value,
sub,
accentColor,
accentClass,
icon,
barPct,
}: MetricCardProps) => (
<div className="bg-slate-800/70 border border-slate-700/80 rounded-xl px-4 py-3 flex flex-col gap-1.5 hover:border-slate-600 transition-colors">
<div className="flex items-center justify-between">
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-medium">{label}</span>
<span className="text-slate-500">{icon}</span>
</div>
<span className={`text-2xl font-bold font-mono leading-none ${accentClass}`}>{value}</span>
{sub && <span className="text-[10px] text-slate-500">{sub}</span>}
{barPct !== undefined && (
<div className="mt-1 h-1.5 w-full bg-slate-700 rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all duration-500"
style={{ width: `${Math.min(100, barPct)}%`, backgroundColor: accentColor }}
/>
</div>
)}
</div>
);
@@ -30,48 +60,74 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
} = metrics;
const utilizationPct = Math.min(100, cpuUtilization);
// Bars for time metrics are proportional to the total simulation time
const toPct = (v: number) =>
totalSimulationTime > 0 ? Math.min(100, (v / totalSimulationTime) * 100) : 0;
return (
<div className="p-4 space-y-4">
<div className="p-4">
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
<MetricCard
label="Avg Response Time"
value={avgResponseTime.toFixed(2)}
sub="unidades de tempo"
accent="text-amber-300"
accentColor="#f59e0b"
accentClass="text-amber-300"
icon={<Clock size={13} />}
barPct={toPct(avgResponseTime)}
/>
<MetricCard
label="Avg Turnaround Time"
value={avgTurnaroundTime.toFixed(2)}
sub="unidades de tempo"
accent="text-blue-300"
accentColor="#60a5fa"
accentClass="text-blue-300"
icon={<Timer size={13} />}
barPct={toPct(avgTurnaroundTime)}
/>
<MetricCard
label="Avg Waiting Time"
value={avgWaitingTime.toFixed(2)}
sub="unidades de tempo"
accent="text-orange-300"
accentColor="#fb923c"
accentClass="text-orange-300"
icon={<Hourglass size={13} />}
barPct={toPct(avgWaitingTime)}
/>
<MetricCard
label="CPU Throughput"
value={cpuThroughput.toFixed(3)}
sub="processos / u.t."
accent="text-emerald-300"
accentColor="#34d399"
accentClass="text-emerald-300"
icon={<Zap size={13} />}
/>
<MetricCard
label="Total Simulation Time"
value={String(totalSimulationTime)}
sub="ticks"
accent="text-slate-200"
accentColor="#94a3b8"
accentClass="text-slate-200"
icon={<Activity size={13} />}
/>
{/* CPU Utilization card with progress bar */}
<div className="bg-slate-800 border border-slate-700 rounded-lg px-4 py-3 flex flex-col gap-1">
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-medium">CPU Utilization</span>
<span className="text-xl font-bold font-mono text-violet-300">{utilizationPct.toFixed(1)}%</span>
<div className="mt-1 h-1.5 w-full bg-slate-700 rounded-full overflow-hidden">
{/* CPU Utilization — gradient bar */}
<div className="bg-slate-800/70 border border-slate-700/80 rounded-xl px-4 py-3 flex flex-col gap-1.5 hover:border-slate-600 transition-colors">
<div className="flex items-center justify-between">
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-medium">CPU Utilization</span>
<span className="text-slate-500"><Cpu size={13} /></span>
</div>
<span className="text-2xl font-bold font-mono leading-none text-violet-300">
{utilizationPct.toFixed(1)}%
</span>
<span className="text-[10px] text-slate-500">do tempo total em CPU</span>
<div className="mt-1 h-2 w-full bg-slate-700 rounded-full overflow-hidden">
<div
className="h-full bg-violet-500 rounded-full transition-all duration-500"
style={{ width: `${utilizationPct}%` }}
className="h-full rounded-full transition-all duration-500"
style={{
width: `${utilizationPct}%`,
background: `linear-gradient(90deg, #f59e0b ${0}%, #34d399 ${100}%)`,
}}
/>
</div>
</div>
+135 -48
View File
@@ -7,6 +7,10 @@ import {
Legend,
ResponsiveContainer,
CartesianGrid,
RadarChart,
PolarGrid,
PolarAngleAxis,
Radar,
} from 'recharts';
import type { ProcessMetrics, Process } from '@core/types';
@@ -15,6 +19,15 @@ interface MetricsChartProps {
processMap: Record<string, Process>;
}
const TOOLTIP_STYLE = {
backgroundColor: '#1e293b',
border: '1px solid #334155',
borderRadius: '8px',
fontSize: '11px',
color: '#e2e8f0',
boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
};
const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
if (metrics.length === 0) {
return (
@@ -24,57 +37,131 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
);
}
const data = metrics.map((m) => ({
name: processMap[m.processId]?.name ?? m.processId,
'Waiting Time': m.waitingTime,
'CPU Time': m.cpuTime,
'I/O Time': m.ioTime,
const getName = (id: string) => processMap[id]?.name ?? id;
const getColor = (id: string) => processMap[id]?.color ?? '#94a3b8';
// ── Stacked bar data ──────────────────────────────────────────
const barData = metrics.map((m) => ({
name: getName(m.processId),
'Waiting': m.waitingTime,
'CPU': m.cpuTime,
'I/O': m.ioTime,
_color: getColor(m.processId),
}));
// ── Radar chart data (normalized 0-100 per dimension) ─────────
// Each "subject" is a process, each series is a dimension.
// We invert "Response" and "Waiting" so higher = better.
const maxT = Math.max(...metrics.map(m => m.turnaroundTime), 1);
const maxR = Math.max(...metrics.map(m => m.responseTime), 1);
const maxW = Math.max(...metrics.map(m => m.waitingTime), 1);
const maxCpu = Math.max(...metrics.map(m => m.cpuTime), 1);
const maxIo = Math.max(...metrics.map(m => m.ioTime), 1);
const radarDimensions = [
'Turnaround', 'Response', 'Waiting', 'CPU Time', 'I/O Time',
] as const;
// Radar: rows = dimensions, column per process → Recharts RadarChart needs
// one row per "subject" (dimension), with one field per process.
const radarData = radarDimensions.map(dim => {
const row: Record<string, string | number> = { dimension: dim };
metrics.forEach(m => {
const key = getName(m.processId);
switch (dim) {
case 'Turnaround': row[key] = (m.turnaroundTime / maxT) * 100; break;
case 'Response': row[key] = (m.responseTime / maxR) * 100; break;
case 'Waiting': row[key] = (m.waitingTime / maxW) * 100; break;
case 'CPU Time': row[key] = (m.cpuTime / maxCpu)* 100; break;
case 'I/O Time': row[key] = (m.ioTime / maxIo) * 100; break;
}
});
return row;
});
const showRadar = metrics.length >= 2;
return (
<div className="p-4">
<ResponsiveContainer width="100%" height={220}>
<BarChart data={data} margin={{ top: 4, right: 16, left: 0, bottom: 4 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" vertical={false} />
<XAxis
dataKey="name"
tick={{ fill: '#94a3b8', fontSize: 11 }}
axisLine={{ stroke: '#475569' }}
tickLine={false}
/>
<YAxis
tick={{ fill: '#94a3b8', fontSize: 11 }}
axisLine={false}
tickLine={false}
label={{
value: 'ticks',
angle: -90,
position: 'insideLeft',
offset: 10,
style: { fill: '#64748b', fontSize: 10 },
}}
/>
<Tooltip
contentStyle={{
backgroundColor: '#1e293b',
border: '1px solid #334155',
borderRadius: '6px',
fontSize: '12px',
color: '#e2e8f0',
}}
cursor={{ fill: 'rgba(255,255,255,0.05)' }}
/>
<Legend
wrapperStyle={{ fontSize: '11px', paddingTop: '8px', color: '#94a3b8' }}
/>
<Bar dataKey="Waiting Time" stackId="a" fill="#f59e0b" radius={[0, 0, 0, 0]} />
<Bar dataKey="CPU Time" stackId="a" fill="#22c55e" radius={[0, 0, 0, 0]} />
<Bar dataKey="I/O Time" stackId="a" fill="#06b6d4" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
<p className="text-center text-[10px] text-slate-500 mt-1">
Barras empilhadas = Turnaround Time (Waiting + CPU + I/O)
</p>
<div className={`p-3 flex gap-3 ${showRadar ? 'flex-row' : 'flex-col'}`}>
{/* ── Radar chart ───────────────────────────────────────── */}
{showRadar && (
<div className="flex-1 min-w-0">
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
Perfil Comparativo (normalizado)
</p>
<ResponsiveContainer width="100%" height={220}>
<RadarChart data={radarData} margin={{ top: 8, right: 20, bottom: 8, left: 20 }}>
<PolarGrid stroke="#334155" />
<PolarAngleAxis
dataKey="dimension"
tick={{ fill: '#94a3b8', fontSize: 10 }}
/>
{metrics.map((m) => (
<Radar
key={m.processId}
name={getName(m.processId)}
dataKey={getName(m.processId)}
stroke={getColor(m.processId)}
fill={getColor(m.processId)}
fillOpacity={0.15}
strokeWidth={2}
dot={{ r: 3, fill: getColor(m.processId) }}
/>
))}
<Tooltip
contentStyle={TOOLTIP_STYLE}
formatter={(value) => [typeof value === 'number' ? `${value.toFixed(1)}%` : '-', '']}
/>
<Legend
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
/>
</RadarChart>
</ResponsiveContainer>
</div>
)}
{/* ── Stacked bar chart ─────────────────────────────────── */}
<div className={showRadar ? 'flex-1 min-w-0' : 'w-full'}>
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
Breakdown por Processo (ticks)
</p>
<ResponsiveContainer width="100%" height={220}>
<BarChart data={barData} margin={{ top: 8, right: 12, left: -8, bottom: 4 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
<XAxis
dataKey="name"
tick={{ fill: '#94a3b8', fontSize: 11 }}
axisLine={{ stroke: '#334155' }}
tickLine={false}
/>
<YAxis
tick={{ fill: '#94a3b8', fontSize: 10 }}
axisLine={false}
tickLine={false}
label={{
value: 'ticks',
angle: -90,
position: 'insideLeft',
offset: 14,
style: { fill: '#475569', fontSize: 9 },
}}
/>
<Tooltip
contentStyle={TOOLTIP_STYLE}
cursor={{ fill: 'rgba(255,255,255,0.04)' }}
/>
<Legend
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
/>
<Bar dataKey="Waiting" stackId="a" fill="#f59e0b" radius={[0, 0, 0, 0]} />
<Bar dataKey="CPU" stackId="a" fill="#22c55e" radius={[0, 0, 0, 0]} />
<Bar dataKey="I/O" stackId="a" fill="#06b6d4" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
<p className="text-center text-[9px] text-slate-600 mt-0.5">
Waiting + CPU + I/O = Turnaround
</p>
</div>
</div>
);
};
+1 -1
View File
@@ -46,7 +46,7 @@ const MetricsPanel = () => {
const canExportCSV = status === 'finished' && processMetrics.length > 0;
return (
<div className="border-t border-slate-700 bg-slate-950 flex flex-col shrink-0" style={{ height: '280px' }}>
<div className="border-t border-slate-700 bg-slate-950 flex flex-col shrink-0" style={{ height: '340px' }}>
{/* Panel header with tabs + export button */}
<div className="flex items-center px-3 border-b border-slate-700 shrink-0 h-10 gap-1">
{TAB_CONFIG.map(tab => (
+81 -20
View File
@@ -12,6 +12,26 @@ function boundVariant(type: ProcessMetrics['boundType']) {
return 'balanced';
}
// Inline mini bar rendered behind the numeric value
interface MiniBarProps {
value: number;
max: number;
color: string; // tailwind bg class
}
const MiniBar = ({ value, max, color }: MiniBarProps) => {
const pct = max > 0 ? (value / max) * 100 : 0;
return (
<div className="relative flex items-center justify-end gap-1.5 min-w-[52px]">
<div
className="absolute inset-y-0 right-0 rounded-sm opacity-20 transition-all duration-300"
style={{ width: `${pct}%`, backgroundColor: color }}
/>
<span className="relative z-10 font-mono text-xs">{value}</span>
</div>
);
};
const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
if (metrics.length === 0) {
return (
@@ -21,34 +41,42 @@ const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
);
}
// Pre-compute maximums for proportional bars
const maxResponse = Math.max(...metrics.map(m => m.responseTime), 1);
const maxTurnaround = Math.max(...metrics.map(m => m.turnaroundTime), 1);
const maxWaiting = Math.max(...metrics.map(m => m.waitingTime), 1);
const maxCpu = Math.max(...metrics.map(m => m.cpuTime), 1);
const maxIo = Math.max(...metrics.map(m => m.ioTime), 1);
return (
<div className="overflow-x-auto">
<table className="w-full text-xs text-slate-300 border-collapse min-w-175">
<table className="w-full text-xs text-slate-300 border-collapse min-w-[780px]">
<thead>
<tr className="text-[10px] text-slate-400 uppercase tracking-wider border-b border-slate-700">
<tr className="text-[10px] text-slate-400 uppercase tracking-wider border-b border-slate-700 sticky top-0 bg-slate-950 z-10">
<th className="text-left px-3 py-2 font-medium">Processo</th>
<th className="text-right px-3 py-2 font-medium">Arrival</th>
<th className="text-right px-3 py-2 font-medium">Start</th>
<th className="text-right px-3 py-2 font-medium">Finish</th>
<th className="text-right px-3 py-2 font-medium">Response</th>
<th className="text-right px-3 py-2 font-medium">Turnaround</th>
<th className="text-right px-3 py-2 font-medium">Waiting</th>
<th className="text-right px-3 py-2 font-medium">CPU Time</th>
<th className="text-right px-3 py-2 font-medium">I/O Time</th>
<th className="text-right px-3 py-2 font-medium text-amber-400">Response</th>
<th className="text-right px-3 py-2 font-medium text-blue-400">Turnaround</th>
<th className="text-right px-3 py-2 font-medium text-orange-400">Waiting</th>
<th className="text-right px-3 py-2 font-medium text-emerald-400">CPU</th>
<th className="text-right px-3 py-2 font-medium text-cyan-400">I/O</th>
<th className="text-right px-3 py-2 font-medium">CPU%</th>
<th className="text-center px-3 py-2 font-medium">Bound</th>
</tr>
</thead>
<tbody>
{metrics.map((m, i) => {
const proc = processMap[m.processId];
const name = proc?.name ?? m.processId;
const proc = processMap[m.processId];
const name = proc?.name ?? m.processId;
const color = proc?.color ?? '#94a3b8';
return (
<tr
key={m.processId}
className={`border-b border-slate-800 hover:bg-slate-800/60 transition-colors ${i % 2 === 0 ? '' : 'bg-slate-900/30'}`}
className={`border-b border-slate-800/80 hover:bg-slate-800/50 transition-colors ${i % 2 === 0 ? '' : 'bg-slate-900/20'}`}
>
{/* Process name */}
<td className="px-3 py-2">
<div className="flex items-center gap-2">
<span
@@ -56,22 +84,55 @@ const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
style={{ backgroundColor: color }}
/>
{proc?.pid !== undefined && (
<span className="text-[11px] font-mono font-semibold text-slate-400 bg-slate-700 px-1.5 py-0.5 rounded shrink-0">
<span className="text-[10px] font-mono font-semibold text-slate-400 bg-slate-700 px-1.5 py-0.5 rounded shrink-0">
#{proc.pid}
</span>
)}
<span className="font-medium text-slate-200">{name}</span>
</div>
</td>
<td className="text-right px-3 py-2 font-mono">{m.arrivalTime}</td>
<td className="text-right px-3 py-2 font-mono">{m.startTick}</td>
<td className="text-right px-3 py-2 font-mono">{m.finishTick}</td>
<td className="text-right px-3 py-2 font-mono text-amber-300">{m.responseTime}</td>
<td className="text-right px-3 py-2 font-mono text-blue-300">{m.turnaroundTime}</td>
<td className="text-right px-3 py-2 font-mono text-orange-300">{m.waitingTime}</td>
<td className="text-right px-3 py-2 font-mono text-emerald-300">{m.cpuTime}</td>
<td className="text-right px-3 py-2 font-mono text-cyan-300">{m.ioTime}</td>
<td className="text-right px-3 py-2 font-mono">{m.cpuUtilization.toFixed(1)}%</td>
{/* Plain timestamps */}
<td className="text-right px-3 py-2 font-mono text-slate-400">{m.arrivalTime}</td>
<td className="text-right px-3 py-2 font-mono text-slate-400">{m.startTick}</td>
<td className="text-right px-3 py-2 font-mono text-slate-400">{m.finishTick}</td>
{/* Cells with inline mini-bars */}
<td className="px-3 py-2 text-amber-300">
<MiniBar value={m.responseTime} max={maxResponse} color="#f59e0b" />
</td>
<td className="px-3 py-2 text-blue-300">
<MiniBar value={m.turnaroundTime} max={maxTurnaround} color="#60a5fa" />
</td>
<td className="px-3 py-2 text-orange-300">
<MiniBar value={m.waitingTime} max={maxWaiting} color="#fb923c" />
</td>
<td className="px-3 py-2 text-emerald-300">
<MiniBar value={m.cpuTime} max={maxCpu} color="#34d399" />
</td>
<td className="px-3 py-2 text-cyan-300">
<MiniBar value={m.ioTime} max={maxIo} color="#22d3ee" />
</td>
{/* CPU utilization with a tiny arc/bar */}
<td className="px-3 py-2">
<div className="flex items-center justify-end gap-1.5">
<div className="w-10 h-1.5 bg-slate-700 rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all duration-300"
style={{
width: `${Math.min(100, m.cpuUtilization)}%`,
background: `linear-gradient(90deg, #a78bfa, #60a5fa)`,
}}
/>
</div>
<span className="font-mono text-slate-300 w-9 text-right">
{m.cpuUtilization.toFixed(0)}%
</span>
</div>
</td>
{/* Bound badge */}
<td className="text-center px-3 py-2">
<Badge variant={boundVariant(m.boundType)}>{m.boundType}</Badge>
</td>
+38 -16
View File
@@ -193,10 +193,11 @@ describe("CT-04: SRTF Preemption", () => {
});
// ============================================================
// General engine behaviour
// SimulationEngine — ciclo de vida
// Tests: isFinished, step() guard, runAll vs step equivalence
// ============================================================
describe("SimulationEngine general", () => {
describe("SimulationEngine — ciclo de vida", () => {
it("isFinished returns false mid-run, true after runAll", () => {
const engine = new SimulationEngine(
[p("p1", 0, [{ type: "cpu", duration: 3 }])],
@@ -232,22 +233,14 @@ describe("SimulationEngine general", () => {
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");
});
// ============================================================
// SimulationEngine — chegada de processos
// Tests: late arrivals, idle ticks while waiting for process
// ============================================================
describe("SimulationEngine — chegada de processos", () => {
it("process arriving after simulation starts is correctly scheduled", () => {
const engine = new SimulationEngine(
[
@@ -261,7 +254,14 @@ describe("SimulationEngine general", () => {
expect(ticks[4]?.cpuProcess).toBeNull(); // tick 4: idle
expect(ticks[5]?.cpuProcess).toBe("p2");
});
});
// ============================================================
// SimulationEngine — troca de contexto
// Tests: idle ticks during switch, ctxSwitchForProcess across overhead
// ============================================================
describe("SimulationEngine — troca de contexto", () => {
it("context switch inserts idle ticks between processes", () => {
const engine = new SimulationEngine(
[
@@ -298,3 +298,25 @@ describe("SimulationEngine general", () => {
expect(ticks[4]?.ctxSwitchForProcess).toBeNull();
});
});
// ============================================================
// SimulationEngine — I/O
// Tests: Waiting state, IO burst transitions
// ============================================================
describe("SimulationEngine — I/O", () => {
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");
});
});
+17 -11
View File
@@ -1,7 +1,9 @@
import { describe, it, expect } from "vitest";
import { fcfs } from "../algorithms/fcfs";
import { FCFSAlgorithm } from "../algorithms/fcfs";
import type { ProcessRuntime } from "../types";
const fcfs = new FCFSAlgorithm();
function makeRt(id: string, arrivalTick: number): ProcessRuntime {
return {
processId: id, state: "Ready", remainingBurst: 4, burstIndex: 0,
@@ -11,37 +13,33 @@ function makeRt(id: string, arrivalTick: number): ProcessRuntime {
};
}
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();
expect(fcfs.select([])).toBeNull();
});
it("returns single process", () => {
const rt = makeRt("p1", 0);
expect(fcfs.select([rt], config)).toBe(rt);
expect(fcfs.select([rt])).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");
expect(fcfs.select([r1, r2, r3])?.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");
expect(fcfs.select([r1, r2])?.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);
expect(fcfs.shouldPreempt()).toBe(false);
});
});
@@ -49,6 +47,14 @@ describe("FCFS isQuantumExpired()", () => {
it("always returns false", () => {
const rt = makeRt("p1", 0);
rt.quantumRemaining = 0;
expect(fcfs.isQuantumExpired(rt, config)).toBe(false);
expect(fcfs.isQuantumExpired()).toBe(false);
});
});
describe("FCFSAlgorithm — metadados", () => {
it("expõe propriedades educacionais corretas", () => {
expect(fcfs.name).toContain("First Come");
expect(fcfs.isPreemptiveCapable).toBe(false);
expect(fcfs.usesQuantum).toBe(false);
});
});
+15 -7
View File
@@ -1,8 +1,10 @@
import { describe, it, expect } from "vitest";
import { hrrn } from "../algorithms/hrrn";
import { HRRNAlgorithm } from "../algorithms/hrrn";
import { SimulationEngine } from "../engine";
import type { Process, ProcessRuntime, SchedulerConfig } from "../types";
const hrrn = new HRRNAlgorithm();
function makeRt(id: string, remainingBurst: number, waitingTime: number, arrivalTick = 0): ProcessRuntime {
return {
processId: id, state: "Ready", remainingBurst, burstIndex: 0,
@@ -24,7 +26,7 @@ const p = (id: string, arrival: number, cpuDuration: number): Process => ({
describe("HRRN select()", () => {
it("returns null for empty queue", () => {
expect(hrrn.select([], config)).toBeNull();
expect(hrrn.select([])).toBeNull();
});
it("picks process with highest response ratio", () => {
@@ -35,20 +37,18 @@ describe("HRRN select()", () => {
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");
expect(hrrn.select([r1, r2, r3])?.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");
expect(hrrn.select([r1, r2])?.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);
expect(hrrn.shouldPreempt()).toBe(false);
});
});
@@ -81,3 +81,11 @@ describe("HRRN integration", () => {
expect(ticks[3]?.cpuProcess).toBe("p3");
});
});
describe("HRRNAlgorithm — metadados", () => {
it("expõe propriedades educacionais corretas", () => {
expect(hrrn.name).toContain("HRRN");
expect(hrrn.isPreemptiveCapable).toBe(false);
expect(hrrn.usesQuantum).toBe(false);
});
});
+11 -1
View File
@@ -1,8 +1,10 @@
import { describe, it, expect } from "vitest";
import { multilevel } from "../algorithms/multilevel";
import { MultilevelQueueAlgorithm } from "../algorithms/multilevel";
import { SimulationEngine } from "../engine";
import type { MlqQueueDef, Process, ProcessRuntime, SchedulerConfig } from "../types";
const multilevel = new MultilevelQueueAlgorithm();
// ----------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------
@@ -232,3 +234,11 @@ describe("MULTILEVEL integration — strict queue ordering", () => {
expect(rts.find((r) => r.processId === "p3")!.cpuTime).toBe(3);
});
});
describe("MultilevelQueueAlgorithm — metadados", () => {
it("expõe propriedades educacionais corretas", () => {
expect(multilevel.name).toContain("Multilevel");
expect(multilevel.isPreemptiveCapable).toBe(true);
expect(multilevel.usesQuantum).toBe(true);
});
});
+21 -13
View File
@@ -1,8 +1,11 @@
import { describe, it, expect } from "vitest";
import { priorityNp, priorityP } from "../algorithms/priority";
import { PriorityNonPreemptiveAlgorithm, PriorityPreemptiveAlgorithm } from "../algorithms/priority";
import { SimulationEngine } from "../engine";
import type { Process, ProcessRuntime, SchedulerConfig } from "../types";
const priorityNp = new PriorityNonPreemptiveAlgorithm();
const priorityP = new PriorityPreemptiveAlgorithm();
function makeRt(id: string, priority: number, arrivalTick = 0): ProcessRuntime {
return {
processId: id, state: "Ready", remainingBurst: 4, burstIndex: 0,
@@ -16,10 +19,6 @@ 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",
@@ -31,19 +30,17 @@ describe("PRIORITY_NP select()", () => {
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");
expect(priorityNp.select([r1, r2, r3])?.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");
expect(priorityNp.select([r1, r2])?.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);
expect(priorityNp.shouldPreempt()).toBe(false);
});
});
@@ -51,19 +48,19 @@ 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);
expect(priorityP.shouldPreempt(current, candidate)).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);
expect(priorityP.shouldPreempt(current, candidate)).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);
expect(priorityP.shouldPreempt(current, candidate)).toBe(false);
});
});
@@ -116,3 +113,14 @@ describe("Priority P + aging", () => {
expect(rt.currentPriority).toBeGreaterThanOrEqual(1);
});
});
describe("Priority — metadados", () => {
it("PriorityNonPreemptiveAlgorithm: isPreemptiveCapable=false", () => {
expect(priorityNp.isPreemptiveCapable).toBe(false);
expect(priorityNp.usesQuantum).toBe(false);
});
it("PriorityPreemptiveAlgorithm: isPreemptiveCapable=true", () => {
expect(priorityP.isPreemptiveCapable).toBe(true);
expect(priorityP.usesQuantum).toBe(false);
});
});
+24 -14
View File
@@ -1,8 +1,10 @@
import { describe, it, expect } from "vitest";
import { priorityRr } from "../algorithms/priority_rr";
import { PriorityRoundRobinAlgorithm } from "../algorithms/priority_rr";
import { SimulationEngine } from "../engine";
import type { Process, ProcessRuntime, SchedulerConfig } from "../types";
const priorityRr = new PriorityRoundRobinAlgorithm();
// ----------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------
@@ -58,22 +60,22 @@ const p = (
describe("PRIORITY_RR select()", () => {
it("returns null for empty queue", () => {
expect(priorityRr.select([], cfg())).toBeNull();
expect(priorityRr.select([])).toBeNull();
});
it("picks the process with the lowest priority number (highest priority)", () => {
const high = makeRt("high", 1);
const low = makeRt("low", 5);
expect(priorityRr.select([low, high], cfg())?.processId).toBe("high");
expect(priorityRr.select([high, low], cfg())?.processId).toBe("high");
expect(priorityRr.select([low, high])?.processId).toBe("high");
expect(priorityRr.select([high, low])?.processId).toBe("high");
});
it("picks the FIRST in insertion order when priorities are equal (FIFO = RR)", () => {
const a = makeRt("a", 3);
const b = makeRt("b", 3);
const c = makeRt("c", 3);
expect(priorityRr.select([a, b, c], cfg())?.processId).toBe("a");
expect(priorityRr.select([b, c, a], cfg())?.processId).toBe("b");
expect(priorityRr.select([a, b, c])?.processId).toBe("a");
expect(priorityRr.select([b, c, a])?.processId).toBe("b");
});
it("ignores lower-priority processes when a higher-priority one is present", () => {
@@ -81,7 +83,7 @@ describe("PRIORITY_RR select()", () => {
const normal1 = makeRt("n1", 5);
const normal2 = makeRt("n2", 5);
expect(
priorityRr.select([normal1, normal2, critical], cfg())?.processId,
priorityRr.select([normal1, normal2, critical])?.processId,
).toBe("critical");
});
});
@@ -94,19 +96,19 @@ describe("PRIORITY_RR shouldPreempt()", () => {
it("preempts when candidate has strictly higher priority", () => {
const running = makeRt("current", 5);
const higher = makeRt("higher", 1);
expect(priorityRr.shouldPreempt(running, higher, cfg())).toBe(true);
expect(priorityRr.shouldPreempt(running, higher)).toBe(true);
});
it("does NOT preempt for same priority (quantum handles rotation)", () => {
const a = makeRt("a", 3);
const b = makeRt("b", 3);
expect(priorityRr.shouldPreempt(a, b, cfg())).toBe(false);
expect(priorityRr.shouldPreempt(a, b)).toBe(false);
});
it("does NOT preempt when candidate has lower priority", () => {
const running = makeRt("current", 1);
const lower = makeRt("lower", 5);
expect(priorityRr.shouldPreempt(running, lower, cfg())).toBe(false);
expect(priorityRr.shouldPreempt(running, lower)).toBe(false);
});
});
@@ -116,13 +118,13 @@ describe("PRIORITY_RR shouldPreempt()", () => {
describe("PRIORITY_RR isQuantumExpired()", () => {
it("returns true when quantumRemaining <= 0", () => {
expect(priorityRr.isQuantumExpired(makeRt("p", 5, 0), cfg())).toBe(true);
expect(priorityRr.isQuantumExpired(makeRt("p", 5, -1), cfg())).toBe(true);
expect(priorityRr.isQuantumExpired(makeRt("p", 5, 0))).toBe(true);
expect(priorityRr.isQuantumExpired(makeRt("p", 5, -1))).toBe(true);
});
it("returns false when quantum is not exhausted", () => {
expect(priorityRr.isQuantumExpired(makeRt("p", 5, 1), cfg())).toBe(false);
expect(priorityRr.isQuantumExpired(makeRt("p", 5, 2), cfg())).toBe(false);
expect(priorityRr.isQuantumExpired(makeRt("p", 5, 1))).toBe(false);
expect(priorityRr.isQuantumExpired(makeRt("p", 5, 2))).toBe(false);
});
});
@@ -246,3 +248,11 @@ describe("PRIORITY_RR integration — combined preemption + RR", () => {
expect(rts.find((r) => r.processId === "p3")!.cpuTime).toBe(2);
});
});
describe("PriorityRoundRobinAlgorithm — metadados", () => {
it("expõe propriedades educacionais corretas", () => {
expect(priorityRr.name).toContain("Priority Round Robin");
expect(priorityRr.isPreemptiveCapable).toBe(true);
expect(priorityRr.usesQuantum).toBe(true);
});
});
+19 -11
View File
@@ -1,8 +1,10 @@
import { describe, it, expect } from "vitest";
import { rr } from "../algorithms/rr";
import { RoundRobinAlgorithm } from "../algorithms/rr";
import { SimulationEngine } from "../engine";
import type { Process, ProcessRuntime, SchedulerConfig } from "../types";
const rr = new RoundRobinAlgorithm();
function makeRt(id: string, quantumRemaining = 2): ProcessRuntime {
return {
processId: id, state: "Running", remainingBurst: 4, burstIndex: 0,
@@ -26,32 +28,30 @@ 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");
expect(rr.select([r1, r2])?.processId).toBe("p1");
expect(rr.select([r2, r1])?.processId).toBe("p2");
});
it("returns null for empty queue", () => {
expect(rr.select([], config)).toBeNull();
expect(rr.select([])).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);
expect(rr.isQuantumExpired(makeRt("p1", 0))).toBe(true);
expect(rr.isQuantumExpired(makeRt("p1", -1))).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);
expect(rr.isQuantumExpired(makeRt("p1", 1))).toBe(false);
expect(rr.isQuantumExpired(makeRt("p1", 2))).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);
expect(rr.shouldPreempt()).toBe(false);
});
});
@@ -114,3 +114,11 @@ describe("CT-02: Round Robin integration", () => {
void ticks; // used for debugging if needed
});
});
describe("RoundRobinAlgorithm — metadados", () => {
it("expõe propriedades educacionais corretas", () => {
expect(rr.name).toContain("Round Robin");
expect(rr.isPreemptiveCapable).toBe(false);
expect(rr.usesQuantum).toBe(true);
});
});
+26 -10
View File
@@ -1,8 +1,13 @@
import { describe, it, expect } from "vitest";
import { sjfNp, srtf } from "../algorithms/sjf";
import { SJFNonPreemptiveAlgorithm, SRTFAlgorithm } from "../algorithms/sjf";
import { SimulationEngine } from "../engine";
import type { Process, ProcessRuntime, SchedulerConfig } from "../types";
const sjfNp = new SJFNonPreemptiveAlgorithm();
const srtf = new SRTFAlgorithm();
function makeRt(id: string, remainingBurst: number, arrivalTick = 0): ProcessRuntime {
return {
processId: id, state: "Ready", remainingBurst, burstIndex: 0,
@@ -23,27 +28,25 @@ const p = (id: string, arrival: number, bursts: { type: "cpu" | "io"; duration:
describe("SJF_NP select()", () => {
it("returns null for empty queue", () => {
expect(sjfNp.select([], config)).toBeNull();
expect(sjfNp.select([])).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");
expect(sjfNp.select([r1, r2, r3])?.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");
expect(sjfNp.select([r1, r2])?.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);
expect(sjfNp.shouldPreempt()).toBe(false);
});
});
@@ -51,19 +54,19 @@ 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);
expect(srtf.shouldPreempt(current, candidate)).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);
expect(srtf.shouldPreempt(current, candidate)).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);
expect(srtf.shouldPreempt(current, candidate)).toBe(false);
});
});
@@ -92,3 +95,16 @@ describe("CT-04: SRTF Preemption (integration)", () => {
expect(rt1.finishTick).toBe(12);
});
});
describe("SJF/SRTF — metadados", () => {
it("SJFNonPreemptiveAlgorithm: isPreemptiveCapable=false, usesQuantum=false", () => {
expect(sjfNp.isPreemptiveCapable).toBe(false);
expect(sjfNp.usesQuantum).toBe(false);
expect(sjfNp.name).toContain("Shortest Job First");
});
it("SRTFAlgorithm: isPreemptiveCapable=true, usesQuantum=false", () => {
expect(srtf.isPreemptiveCapable).toBe(true);
expect(srtf.usesQuantum).toBe(false);
expect(srtf.name).toContain("SRTF");
});
});
+69
View File
@@ -0,0 +1,69 @@
import type { ProcessRuntime, SchedulerConfig } from "../types";
import { tiebreak as _tiebreak } from "./utils";
/**
* BaseAlgorithm — Classe abstrata para algoritmos de escalonamento.
*
* Cada algoritmo concreto deve implementar:
* - Propriedades educacionais (name, description, isPreemptiveCapable, usesQuantum)
* - Os três métodos do contrato de escalonamento (select, shouldPreempt, isQuantumExpired)
*
* O método estático `tiebreak` é disponibilizado a todas as subclasses como
* critério de desempate padrão: chegada mais cedo → ID lexicográfico menor.
*/
export abstract class BaseAlgorithm {
/** Nome completo do algoritmo para exibição. */
abstract readonly name: string;
/** Breve descrição educacional do comportamento do algoritmo. */
abstract readonly description: string;
/** Indica se o algoritmo suporta preempção por candidato mais prioritário. */
abstract readonly isPreemptiveCapable: boolean;
/** Indica se o algoritmo usa fatia de tempo (quantum). */
abstract readonly usesQuantum: boolean;
// ─── Contrato de escalonamento ───────────────────────────────────────────
/**
* Seleciona o próximo processo a executar a partir da fila de prontos.
* Não deve modificar o array — o engine remove o processo selecionado.
* Retorna null se a fila estiver vazia.
*/
abstract select(
readyQueue: readonly ProcessRuntime[],
config: SchedulerConfig,
): ProcessRuntime | null;
/**
* Para algoritmos preemptivos: indica se o processo em execução deve ser
* interrompido em favor do candidato selecionado.
* Chamado apenas quando há um processo rodando e um candidato disponível.
*/
abstract shouldPreempt(
current: ProcessRuntime,
candidate: ProcessRuntime,
config: SchedulerConfig,
): boolean;
/**
* Para Round Robin: indica se o quantum do processo expirou.
* Todos os outros algoritmos devem retornar false.
*/
abstract isQuantumExpired(
runtime: ProcessRuntime,
config: SchedulerConfig,
): boolean;
// ─── Helper compartilhado ────────────────────────────────────────────────
/**
* Critério de desempate padrão: chegada mais cedo (arrivalTick ASC),
* depois ID do processo em ordem lexicográfica.
* Disponível a todas as subclasses como método estático herdado.
*/
public static tiebreak(a: ProcessRuntime, b: ProcessRuntime): number {
return _tiebreak(a, b);
}
}
+24 -13
View File
@@ -1,21 +1,32 @@
import type { SchedulerAlgorithm } from "./index";
import { tiebreak } from "./utils";
import type { ProcessRuntime } from "../types";
import { BaseAlgorithm } from "./base";
/**
* FCFS — First Come, First Served
* Non-preemptive. Selects the process with earliest arrival; tiebreak by id.
* FCFS — First Come, First Served (Primeiro a Chegar, Primeiro a Ser Servido)
*
* Algoritmo não-preemptivo mais simples: processa os jobs na ordem exata
* de chegada. Fácil de implementar, mas pode causar o "efeito comboio"
* (convoy effect), onde processos curtos ficam presos atrás de um longo.
*/
export const fcfs: SchedulerAlgorithm = {
select(readyQueue) {
export class FCFSAlgorithm extends BaseAlgorithm {
readonly name = "First Come, First Served (FCFS)";
readonly description =
"Não-preemptivo. Despacha processos na ordem de chegada. " +
"Simples, mas pode causar o efeito comboio: processos curtos " +
"aguardam atrás de um processo longo.";
readonly isPreemptiveCapable = false;
readonly usesQuantum = false;
select(readyQueue: readonly ProcessRuntime[]): ProcessRuntime | null {
if (readyQueue.length === 0) return null;
return [...readyQueue].sort(tiebreak)[0] ?? null;
},
return [...readyQueue].sort(FCFSAlgorithm.tiebreak)[0] ?? null;
}
shouldPreempt() {
shouldPreempt(): boolean {
return false;
},
}
isQuantumExpired() {
isQuantumExpired(): boolean {
return false;
},
};
}
}
+39 -20
View File
@@ -1,33 +1,52 @@
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;
}
import { BaseAlgorithm } from "./base";
/**
* HRRN — Highest Response Ratio Next
* Non-preemptive. Balances waiting time and burst length to prevent starvation.
*
* Algoritmo não-preemptivo que equilibra o tempo de burst e o tempo de espera
* para evitar inanição. A cada decisão de escalonamento, calcula a razão de
* resposta de cada processo na fila e despacha o de maior razão.
*
* Fórmula: ratio = (waitingTime + remainingBurst) / remainingBurst
*
* - Processos longos que esperam muito aumentam sua razão, evitando inanição.
* - Processos curtos naturalmente têm razão alta (denominador pequeno).
* - Oferece um balanço entre SJF (favorece curtos) e FCFS (favorece chegada).
*/
export const hrrn: SchedulerAlgorithm = {
select(readyQueue) {
export class HRRNAlgorithm extends BaseAlgorithm {
readonly name = "Highest Response Ratio Next (HRRN)";
readonly description =
"Não-preemptivo. Despacha o processo com maior razão de resposta: " +
"(espera + burst) / burst. Equilibra duração e tempo de espera, " +
"prevenindo inanição sem precisar de aging explícito.";
readonly isPreemptiveCapable = false;
readonly usesQuantum = false;
/**
* Calcula a razão de resposta de um processo.
* Retorna Infinity se remainingBurst <= 0 (não deve ocorrer na fila de prontos,
* mas garante ordenação segura em casos de borda).
*/
private static hrrnRatio(rt: ProcessRuntime): number {
if (rt.remainingBurst <= 0) return Infinity;
return (rt.waitingTime + rt.remainingBurst) / rt.remainingBurst;
}
select(readyQueue: readonly ProcessRuntime[]): ProcessRuntime | null {
if (readyQueue.length === 0) return null;
return [...readyQueue].sort((a, b) => {
const ratioDiff = hrrnRatio(b) - hrrnRatio(a); // descending
const ratioDiff = HRRNAlgorithm.hrrnRatio(b) - HRRNAlgorithm.hrrnRatio(a); // descendente
if (ratioDiff !== 0) return ratioDiff;
return tiebreak(a, b);
return HRRNAlgorithm.tiebreak(a, b);
})[0] ?? null;
},
}
shouldPreempt() {
shouldPreempt(): boolean {
return false;
},
}
isQuantumExpired() {
isQuantumExpired(): boolean {
return false;
},
};
}
}
+26 -56
View File
@@ -1,61 +1,31 @@
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 { priorityRr } from "./priority_rr";
import { hrrn } from "./hrrn";
import { multilevel } from "./multilevel";
import type { SchedulingAlgorithm } from "../types";
import { BaseAlgorithm } from "./base";
import { FCFSAlgorithm } from "./fcfs";
import { SJFNonPreemptiveAlgorithm, SRTFAlgorithm } from "./sjf";
import { RoundRobinAlgorithm } from "./rr";
import { PriorityNonPreemptiveAlgorithm, PriorityPreemptiveAlgorithm } from "./priority";
import { PriorityRoundRobinAlgorithm } from "./priority_rr";
import { HRRNAlgorithm } from "./hrrn";
import { MultilevelQueueAlgorithm } from "./multilevel";
export type { BaseAlgorithm };
// ============================================================
// SchedulerAlgorithm interface
// All implementations must be pure (no mutation of the queue).
// Tabela de despacho — o engine busca o algoritmo pelo nome
// ============================================================
//
// As instâncias são criadas uma única vez no carregamento do módulo.
// Isso é seguro pois todas as classes são stateless: todo estado mutável
// da simulação vive em ProcessRuntime, gerenciado pelo SimulationEngine.
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,
PRIORITY_RR: priorityRr,
HRRN: hrrn,
MULTILEVEL: multilevel,
export const algorithms: Record<SchedulingAlgorithm, BaseAlgorithm> = {
FCFS: new FCFSAlgorithm(),
SJF_NP: new SJFNonPreemptiveAlgorithm(),
SJF_P: new SRTFAlgorithm(),
RR: new RoundRobinAlgorithm(),
PRIORITY_NP: new PriorityNonPreemptiveAlgorithm(),
PRIORITY_P: new PriorityPreemptiveAlgorithm(),
PRIORITY_RR: new PriorityRoundRobinAlgorithm(),
HRRN: new HRRNAlgorithm(),
MULTILEVEL: new MultilevelQueueAlgorithm(),
};
+104 -81
View File
@@ -1,109 +1,132 @@
import type { MlqQueueDef, ProcessRuntime } from "../types";
import type { SchedulerAlgorithm } from "./index";
import { tiebreak } from "./utils";
import type { MlqQueueDef, ProcessRuntime, SchedulerConfig } from "../types";
import { BaseAlgorithm } from "./base";
/**
* MULTILEVEL — Multilevel Queue Scheduling
* MLQ — Multilevel Queue (Fila Multinível)
*
* Processes are permanently assigned to a queue based on their currentPriority.
* The scheduler always dispatches from the highest-priority non-empty queue.
* Each queue has its own scheduling algorithm (FCFS, RR, or PRIORITY_NP).
* Processos são atribuídos permanentemente a filas com base em sua prioridade.
* Cada fila tem seu próprio algoritmo interno (FCFS, RR ou PRIORITY_NP).
*
* Inter-queue behaviour:
* - A process from a higher-priority queue always preempts a process
* from a lower-priority queue.
* - No process from a lower-priority queue runs while a higher-priority
* queue has processes.
* Comportamento entre filas:
* - O escalonador sempre despacha da fila de maior prioridade não-vazia (índice 0).
* - Um processo de fila superior sempre preempta um processo de fila inferior.
* - Nenhum processo de fila inferior executa enquanto houver processos em fila superior.
*
* Intra-queue behaviour:
* - FCFS: dispatch in arrival order; no preemption within the queue.
* - RR: round-robin using the queue's own quantum; no cross-queue switching.
* - PRIORITY_NP: dispatch the highest currentPriority process; no preemption.
* Comportamento dentro de cada fila:
* - FCFS: despacha na ordem de chegada; sem preempção interna.
* - RR: rodízio usando o quantum da fila; sem preempção interna.
* - PRIORITY_NP: despacha o de maior prioridade; sem preempção interna.
*
* Queue assignment is derived from config.mlqQueues. Each queue definition
* specifies [priorityMin, priorityMax] (both inclusive). If a process's
* currentPriority falls outside all queues, it is assigned to the last queue.
* A configuração das filas vem de `config.mlqQueues`; se ausente, usa as
* filas padrão definidas em `DEFAULT_QUEUES`.
*/
export class MultilevelQueueAlgorithm extends BaseAlgorithm {
readonly name = "Multilevel Queue (MLQ)";
readonly description =
"Filas de prioridade fixas, cada uma com seu algoritmo interno (FCFS, RR, PRIORITY_NP). " +
"A fila de maior prioridade sempre executa primeiro. Processos são atribuídos " +
"permanentemente a uma fila conforme sua prioridade.";
readonly isPreemptiveCapable = true;
readonly usesQuantum = true; // verdadeiro porque a fila RR usa quantum
/** Returns the index of the queue that owns this priority value (0 = highest). */
function getQueueIndex(priority: number, queues: readonly MlqQueueDef[]): number {
for (let i = 0; i < queues.length; i++) {
const q = queues[i]!;
if (priority >= q.priorityMin && priority <= q.priorityMax) return i;
}
// Fallback: assign to last queue if priority is out of all defined ranges.
return queues.length - 1;
}
/** Configuração padrão: 3 filas cobrindo prioridades 1–10. */
private static readonly DEFAULT_QUEUES: MlqQueueDef[] = [
{ priorityMin: 1, priorityMax: 3, algorithm: "RR", quantum: 2 },
{ priorityMin: 4, priorityMax: 7, algorithm: "PRIORITY_NP", quantum: 2 },
{ priorityMin: 8, priorityMax: 10, algorithm: "FCFS", quantum: 2 },
];
/** Select the best candidate from a sub-queue using the queue's algorithm. */
function selectWithinQueue(
candidates: readonly ProcessRuntime[],
queueDef: MlqQueueDef,
): ProcessRuntime | null {
if (candidates.length === 0) return null;
if (queueDef.algorithm === "RR") {
// RR: dispatch in insertion order (engine tail-appends on preemption).
return candidates[0]!;
}
if (queueDef.algorithm === "FCFS") {
// FCFS: dispatch earliest-arriving process (by arrivalTick, then processId).
return candidates.reduce((best, rt) =>
tiebreak(rt, best) < 0 ? rt : best,
);
}
// PRIORITY_NP: dispatch the process with the lowest currentPriority number.
let best = candidates[0]!;
for (let i = 1; i < candidates.length; i++) {
const rt = candidates[i]!;
if (
rt.currentPriority < best.currentPriority ||
(rt.currentPriority === best.currentPriority && tiebreak(rt, best) < 0)
) {
best = rt;
/**
* Retorna o índice da fila à qual pertence o valor de prioridade informado.
* Se a prioridade não se enquadrar em nenhuma fila definida, usa a última.
*/
private static getQueueIndex(
priority: number,
queues: readonly MlqQueueDef[],
): number {
for (let i = 0; i < queues.length; i++) {
const q = queues[i]!;
if (priority >= q.priorityMin && priority <= q.priorityMax) return i;
}
return queues.length - 1;
}
return best;
}
const DEFAULT_QUEUES: MlqQueueDef[] = [
{ priorityMin: 1, priorityMax: 3, algorithm: "RR", quantum: 2 },
{ priorityMin: 4, priorityMax: 7, algorithm: "PRIORITY_NP", quantum: 2 },
{ priorityMin: 8, priorityMax: 10, algorithm: "FCFS", quantum: 2 },
];
/**
* Seleciona o melhor candidato dentro de uma sub-fila usando o algoritmo
* definido para ela (RR, FCFS ou PRIORITY_NP).
*/
private static selectWithinQueue(
candidates: readonly ProcessRuntime[],
queueDef: MlqQueueDef,
): ProcessRuntime | null {
if (candidates.length === 0) return null;
export const multilevel: SchedulerAlgorithm = {
select(readyQueue, config): ProcessRuntime | null {
const queues = config.mlqQueues ?? DEFAULT_QUEUES;
if (queueDef.algorithm === "RR") {
// RR: despacha na ordem de inserção (engine mantém FIFO).
return candidates[0]!;
}
if (queueDef.algorithm === "FCFS") {
// FCFS: despacha o de chegada mais antiga (arrivalTick, depois processId).
return candidates.reduce((best, rt) =>
BaseAlgorithm.tiebreak(rt, best) < 0 ? rt : best,
);
}
// PRIORITY_NP: despacha o de maior prioridade (menor número).
let best = candidates[0]!;
for (let i = 1; i < candidates.length; i++) {
const rt = candidates[i]!;
if (
rt.currentPriority < best.currentPriority ||
(rt.currentPriority === best.currentPriority &&
BaseAlgorithm.tiebreak(rt, best) < 0)
) {
best = rt;
}
}
return best;
}
select(
readyQueue: readonly ProcessRuntime[],
config: SchedulerConfig,
): ProcessRuntime | null {
const queues = config.mlqQueues ?? MultilevelQueueAlgorithm.DEFAULT_QUEUES;
if (queues.length === 0) return null;
// Iterate from highest-priority queue (index 0) to lowest.
// Itera da fila de maior prioridade (índice 0) para a de menor.
for (let qi = 0; qi < queues.length; qi++) {
const queueDef = queues[qi]!;
const candidates = readyQueue.filter(
(rt) => getQueueIndex(rt.currentPriority, queues) === qi,
(rt) => MultilevelQueueAlgorithm.getQueueIndex(rt.currentPriority, queues) === qi,
);
const selected = selectWithinQueue(candidates, queueDef);
const selected = MultilevelQueueAlgorithm.selectWithinQueue(candidates, queueDef);
if (selected !== null) return selected;
}
return null;
},
}
shouldPreempt(current, candidate, config): boolean {
const queues = config.mlqQueues ?? DEFAULT_QUEUES;
const currentQueueIdx = getQueueIndex(current.currentPriority, queues);
const candidateQueueIdx = getQueueIndex(candidate.currentPriority, queues);
// Preempt only if the candidate belongs to a strictly higher-priority queue.
shouldPreempt(
current: ProcessRuntime,
candidate: ProcessRuntime,
config: SchedulerConfig,
): boolean {
const queues = config.mlqQueues ?? MultilevelQueueAlgorithm.DEFAULT_QUEUES;
const currentQueueIdx = MultilevelQueueAlgorithm.getQueueIndex(current.currentPriority, queues);
const candidateQueueIdx = MultilevelQueueAlgorithm.getQueueIndex(candidate.currentPriority, queues);
// Preempta apenas se o candidato pertence a uma fila de prioridade estritamente maior.
return candidateQueueIdx < currentQueueIdx;
},
}
isQuantumExpired(runtime, config): boolean {
const queues = config.mlqQueues ?? DEFAULT_QUEUES;
const queueIdx = getQueueIndex(runtime.currentPriority, queues);
isQuantumExpired(
runtime: ProcessRuntime,
config: SchedulerConfig,
): boolean {
const queues = config.mlqQueues ?? MultilevelQueueAlgorithm.DEFAULT_QUEUES;
const queueIdx = MultilevelQueueAlgorithm.getQueueIndex(runtime.currentPriority, queues);
const queueDef = queues[queueIdx];
if (!queueDef) return false;
return queueDef.algorithm === "RR" && runtime.quantumRemaining <= 0;
},
};
}
}
+63 -33
View File
@@ -1,49 +1,79 @@
import type { ProcessRuntime } from "../types";
import type { SchedulerAlgorithm } from "./index";
import { tiebreak } from "./utils";
import { BaseAlgorithm } from "./base";
/**
* Comparador interno compartilhado pelas duas variantes de Prioridade.
* Menor número = maior prioridade. Usa tiebreak padrão em caso de empate.
*/
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);
return BaseAlgorithm.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.
* Priority Não-Preemptivo
*
* Seleciona o processo com maior prioridade (menor número). Uma vez que
* um processo ocupa a CPU, ele roda até terminar — novos processos de alta
* prioridade precisam esperar. Pode causar inanição (starvation) de processos
* de baixa prioridade. O mecanismo de aging, quando ativado, incrementa
* gradualmente a prioridade de processos em espera.
*/
export const priorityP: SchedulerAlgorithm = {
select(readyQueue) {
export class PriorityNonPreemptiveAlgorithm extends BaseAlgorithm {
readonly name = "Escalonamento por Prioridade — Não-Preemptivo";
readonly description =
"Despacha o processo de maior prioridade (menor número). Não-preemptivo: " +
"o processo atual não é interrompido por novos de alta prioridade. " +
"Pode causar inanição; use aging para mitigar.";
readonly isPreemptiveCapable = false;
readonly usesQuantum = false;
select(readyQueue: readonly ProcessRuntime[]): ProcessRuntime | null {
if (readyQueue.length === 0) return null;
return [...readyQueue].sort(prioritySort)[0] ?? null;
},
}
shouldPreempt(current, candidate) {
shouldPreempt(): boolean {
return false;
}
isQuantumExpired(): boolean {
return false;
}
}
// ─────────────────────────────────────────────────────────────────────────────
/**
* Priority Preemptivo
*
* Variante preemptiva: o processo em execução é interrompido imediatamente
* ao chegar um processo com prioridade estritamente maior (número menor).
* Prioridade igual não causa preempção (evita context switches desnecessários).
* O aging, quando ativado no engine, decrementa `currentPriority` a cada
* intervalo, prevenindo inanição de processos de baixa prioridade.
*/
export class PriorityPreemptiveAlgorithm extends BaseAlgorithm {
readonly name = "Escalonamento por Prioridade — Preemptivo";
readonly description =
"Preempta o processo em execução quando um de maior prioridade fica pronto. " +
"Prioridade igual não gera preempção. Suporta aging para evitar inanição.";
readonly isPreemptiveCapable = true;
readonly usesQuantum = false;
select(readyQueue: readonly ProcessRuntime[]): ProcessRuntime | null {
if (readyQueue.length === 0) return null;
return [...readyQueue].sort(prioritySort)[0] ?? null;
}
shouldPreempt(current: ProcessRuntime, candidate: ProcessRuntime): boolean {
return candidate.currentPriority < current.currentPriority;
},
}
isQuantumExpired() {
isQuantumExpired(): boolean {
return false;
},
};
}
}
+41 -31
View File
@@ -1,49 +1,59 @@
import type { SchedulerAlgorithm } from "./index";
import type { ProcessRuntime } from "../types";
import { BaseAlgorithm } from "./base";
/**
* Priority Round Robin (PRIORITY_RR)
*
* Mimics real-OS scheduling (Windows priority bands, POSIX SCHED_RR):
* Combina escalonamento por prioridade com rodízio dentro de cada banda:
*
* - The ready queue is conceptually divided into priority bands.
* - The scheduler always dispatches from the highest-priority band
* (lowest numeric value), leaving lower-priority processes waiting.
* - Within the same priority band, processes share the CPU via round-robin
* with the configured quantum — fairness among equals.
* - A running process is preempted immediately when a STRICTLY higher-priority
* process becomes ready (arrival or I/O completion).
* - Quantum expiry only rotates within the same priority band; it does not
* allow a lower-priority process to run while a same-priority one is waiting.
* - Compatible with the aging mechanism: as low-priority processes age their
* currentPriority decreases, eventually entering a higher-priority band.
* - A fila de prontos é dividida conceitualmente em bandas de prioridade.
* - O escalonador sempre despacha da banda de maior prioridade (menor número),
* mantendo processos de menor prioridade em espera.
* - Dentro de uma mesma banda, os processos compartilham a CPU via round-robin
* com o quantum configurado — garantindo justiça entre iguais.
* - Preempção ocorre imediatamente ao chegar um processo de prioridade
* estritamente maior; prioridade igual é tratada pelo quantum.
* - Compatível com aging: ao diminuir `currentPriority`, processos de baixa
* prioridade eventualmente sobem de banda, prevenindo inanição.
*
* Simula o comportamento de SOs reais como Windows (priority bands)
* e POSIX SCHED_RR.
*/
export const priorityRr: SchedulerAlgorithm = {
select(readyQueue) {
export class PriorityRoundRobinAlgorithm extends BaseAlgorithm {
readonly name = "Priority Round Robin (PRR)";
readonly description =
"Combina prioridade com round-robin dentro de cada banda. A banda mais " +
"prioritária sempre executa primeiro; dentro da mesma banda, os processos " +
"se revezam pelo quantum. Simula o escalonamento de SOs reais (Windows, POSIX).";
readonly isPreemptiveCapable = true;
readonly usesQuantum = true;
select(readyQueue: readonly ProcessRuntime[]): ProcessRuntime | null {
if (readyQueue.length === 0) return null;
// Find the highest priority level (smallest numeric value) present.
// Localiza o nível de prioridade mais alto (menor valor numérico) presente.
let bestPriority = Infinity;
for (const rt of readyQueue) {
if (rt.currentPriority < bestPriority) bestPriority = rt.currentPriority;
}
// Within that band, return the first process (FIFO insertion order).
// The engine maintains insertion order, so this gives round-robin rotation
// after quantum expiry: the expired process goes to the tail, the next
// same-priority process is now at the head.
// Dentro da banda, retorna o primeiro processo (ordem de inserção = FIFO).
// O engine insere processos expirados no final da fila, garantindo a rotação
// round-robin: o processo que esgotou o quantum vai para o fim,
// o próximo da mesma prioridade assume.
return readyQueue.find(rt => rt.currentPriority === bestPriority) ?? null;
},
}
// Preempt ONLY for strictly higher priority.
// Same-priority candidates are handled by quantum expiry — no unnecessary
// context switches within a priority band.
shouldPreempt(current, candidate) {
// Preempta APENAS por prioridade estritamente maior.
// Processos de mesma prioridade são tratados pelo mecanismo de quantum —
// evita trocas de contexto desnecessárias dentro da mesma banda.
shouldPreempt(current: ProcessRuntime, candidate: ProcessRuntime): boolean {
return candidate.currentPriority < current.currentPriority;
},
}
// Quantum expiry drives round-robin rotation within a priority band,
// exactly like plain Round Robin.
isQuantumExpired(runtime) {
// A expiração do quantum conduz a rotação round-robin dentro da banda,
// exatamente como no Round Robin puro.
isQuantumExpired(runtime: ProcessRuntime): boolean {
return runtime.quantumRemaining <= 0;
},
};
}
}
+27 -13
View File
@@ -1,22 +1,36 @@
import type { SchedulerAlgorithm } from "./index";
import type { ProcessRuntime } from "../types";
import { BaseAlgorithm } from "./base";
/**
* 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.
*
* Mantém a fila de prontos como FIFO e concede a cada processo uma fatia
* de tempo (quantum). Ao esgotar o quantum, o processo vai para o fim da
* fila e o próximo assume a CPU. Garante justiça e baixo tempo de resposta,
* ao custo de maior tempo de turnaround para processos curtos.
*
* A rotação é garantida pelo engine: ao expirar o quantum, o processo é
* reinserido no final da fila — select() simplesmente retorna o primeiro.
*/
export const rr: SchedulerAlgorithm = {
select(readyQueue) {
export class RoundRobinAlgorithm extends BaseAlgorithm {
readonly name = "Round Robin (RR)";
readonly description =
"Fila FIFO com quantum configurável. Cada processo recebe uma fatia " +
"de tempo igual. Garante justiça e baixo tempo de resposta, mas pode " +
"aumentar o turnaround de processos longos.";
readonly isPreemptiveCapable = false; // preempção é por expiração de quantum, não por candidato
readonly usesQuantum = true;
select(readyQueue: readonly ProcessRuntime[]): ProcessRuntime | null {
return readyQueue[0] ?? null;
},
}
// RR does not preempt based on candidate priority; quantum expiry handles it.
shouldPreempt() {
// RR não preempta por candidato mais prioritário; a troca ocorre via quantum.
shouldPreempt(): boolean {
return false;
},
}
isQuantumExpired(runtime) {
isQuantumExpired(runtime: ProcessRuntime): boolean {
return runtime.quantumRemaining <= 0;
},
};
}
}
+63 -32
View File
@@ -1,48 +1,79 @@
import type { ProcessRuntime } from "../types";
import type { SchedulerAlgorithm } from "./index";
import { tiebreak } from "./utils";
import { BaseAlgorithm } from "./base";
/**
* Comparador interno compartilhado pelas duas variantes do SJF.
* Ordena por menor burst restante; usa tiebreak padrão em caso de empate.
*/
function sjfSort(a: ProcessRuntime, b: ProcessRuntime): number {
if (a.remainingBurst !== b.remainingBurst)
return a.remainingBurst - b.remainingBurst;
return tiebreak(a, b);
return BaseAlgorithm.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).
* SJF Não-Preemptivo — Shortest Job First
*
* Seleciona o processo com o menor burst de CPU restante. Uma vez que um
* processo ocupa a CPU, ele roda até terminar (não pode ser interrompido).
* Ótimo em tempo médio de espera, mas pode causar inanição (starvation)
* de processos longos se processos curtos chegam continuamente.
*/
export const srtf: SchedulerAlgorithm = {
select(readyQueue) {
export class SJFNonPreemptiveAlgorithm extends BaseAlgorithm {
readonly name = "Shortest Job First — Não-Preemptivo (SJF)";
readonly description =
"Seleciona o processo com menor burst restante. Não-preemptivo: " +
"o processo rodando não pode ser interrompido. Minimiza o tempo " +
"médio de espera, mas pode causar inanição de processos longos.";
readonly isPreemptiveCapable = false;
readonly usesQuantum = false;
select(readyQueue: readonly ProcessRuntime[]): ProcessRuntime | null {
if (readyQueue.length === 0) return null;
return [...readyQueue].sort(sjfSort)[0] ?? null;
},
}
shouldPreempt(current, candidate) {
shouldPreempt(): boolean {
return false;
}
isQuantumExpired(): boolean {
return false;
}
}
// ─────────────────────────────────────────────────────────────────────────────
/**
* SRTF — Shortest Remaining Time First (SJF Preemptivo)
*
* Variante preemptiva do SJF. A cada tick, se um processo recém-chegado
* tem burst restante estritamente menor que o processo em execução, ocorre
* preempção. Burst igual não gera troca (evita context switches desnecessários).
* Produz o menor tempo médio de espera possível entre todos os algoritmos,
* porém com maior overhead de trocas de contexto.
*/
export class SRTFAlgorithm extends BaseAlgorithm {
readonly name = "Shortest Remaining Time First (SRTF)";
readonly description =
"Variante preemptiva do SJF. Preempta o processo rodando sempre que " +
"um processo mais curto fica disponível. Otimiza o tempo médio de " +
"espera, mas gera mais trocas de contexto.";
readonly isPreemptiveCapable = true;
readonly usesQuantum = false;
select(readyQueue: readonly ProcessRuntime[]): ProcessRuntime | null {
if (readyQueue.length === 0) return null;
return [...readyQueue].sort(sjfSort)[0] ?? null;
}
shouldPreempt(current: ProcessRuntime, candidate: ProcessRuntime): boolean {
return candidate.remainingBurst < current.remainingBurst;
},
}
isQuantumExpired() {
isQuantumExpired(): boolean {
return false;
},
};
}
}
+268 -141
View File
@@ -6,24 +6,52 @@ import type {
StateLabel,
} from "./types";
import { algorithms } from "./algorithms/index";
import type { SchedulerAlgorithm } from "./algorithms/index";
// ============================================================
// SimulationEngine
// ============================================================
import type { BaseAlgorithm } from "./algorithms/index";
/**
* SimulationEngine — Núcleo de execução da simulação de escalonamento.
*
* Responsável por avançar o simulador tick a tick, aplicando o algoritmo
* de escalonamento configurado e mantendo o estado de todos os processos.
*
* Ciclo de vida de um único tick (método `step()`):
* 1. Chegada — processos cujo `arrivalTime` é igual ao tick atual
* transitam New → Ready e entram na fila de prontos.
* 2. Preempção — o algoritmo decide se o processo em CPU deve ceder
* lugar a um candidato mais prioritário.
* 3. Despacho — seleciona e despacha o próximo processo (com overhead
* de troca de contexto, se configurado).
* 4. Snapshot — captura o estado de todos os processos neste tick.
* 5. Passagem do tempo — decrementa bursts, acumula tempos de CPU/IO/espera.
* 6. Aging — decrementa prioridade dos processos em espera prolongada.
* 7. Conclusões — trata término de bursts de CPU e IO; avança para o
* próximo burst ou termina o processo.
*
* Uso típico:
* ```ts
* const engine = new SimulationEngine(processes, config);
* const ticks = engine.runAll(); // executa até todos os processos terminarem
* ```
*
* Invariante: após `runAll()`, `isFinished` é `true` e todos os runtimes
* têm `state === "Terminated"`.
*/
export class SimulationEngine {
// ─── Campos privados ──────────────────────────────────────────────────────
private readonly _processes: Process[];
private readonly _config: SchedulerConfig;
private readonly _algorithm: SchedulerAlgorithm;
private readonly _algorithm: BaseAlgorithm;
private _tick: number; // starts at -1; incremented at top of step()
private _tick: number; // começa em -1; incrementado no início de step()
private _runtimes: Map<string, ProcessRuntime>;
private _cpuProcess: ProcessRuntime | null;
private _readyQueue: ProcessRuntime[]; // ordered array (FIFO for RR)
private _readyQueue: ProcessRuntime[]; // array ordenado (FIFO para RR)
private _ioQueue: ProcessRuntime[];
private _contextSwitchRemaining: number;
private _prevCpuProcessId: string | null; // track context switch detection
private _prevCpuProcessId: string | null; // para detectar troca de contexto
// ─── Construtor ───────────────────────────────────────────────────────────
constructor(processes: Process[], config: SchedulerConfig) {
if (processes.length === 0) throw new Error("At least one process required");
@@ -36,20 +64,75 @@ export class SimulationEngine {
this._ioQueue = [];
this._contextSwitchRemaining = 0;
this._prevCpuProcessId = null;
this._runtimes = new Map(processes.map((p) => [p.id, initRuntime(p, config)]));
this._runtimes = new Map(
processes.map((p) => [p.id, SimulationEngine._initRuntime(p, config)]),
);
}
// ----------------------------------------------------------
// Public API
// ----------------------------------------------------------
// ─── API pública ──────────────────────────────────────────────────────────
/** Advance one tick; return snapshot of that tick. */
/**
* Avança a simulação por um tick e retorna o snapshot do estado neste tick.
*
* Lança um erro se a simulação já tiver terminado (`isFinished === true`).
* O número do tick é incrementado no início; o primeiro tick válido é 0.
*/
step(): SimTick {
if (this.isFinished) throw new Error("Simulation is already finished");
this._tick++;
// 1. Arrivals
this._handleArrivals(); // 1. New → Ready para quem chega agora
this._checkPreemption(); // 2. Preempta CPU se candidato melhor chegou
this._dispatchNext(); // 3. Despacha próximo (ou inicia context switch)
const snapshot = this._snapshot(); // 4. Captura estado DURANTE este tick
this._advanceTime(); // 5. Decrementa bursts, acumula métricas
this._applyAging(); // 6. Aging de prioridade (se habilitado)
this._handleCpuCompletion(); // 7a. Finaliza burst/quantum de CPU
this._handleIoCompletions(); // 7b. Finaliza bursts de I/O
return snapshot;
}
/** Executa até o fim e retorna todos os snapshots de tick. */
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;
}
/** Verdadeiro quando todos os processos estão no estado "Terminated". */
get isFinished(): boolean {
for (const rt of this._runtimes.values()) {
if (rt.state !== "Terminated") return false;
}
return true;
}
/** Número do tick atual (−1 antes do primeiro `step()`). */
get currentTick(): number {
return this._tick;
}
/** Array com o estado de runtime de todos os processos. */
get runtimeStates(): ProcessRuntime[] {
return Array.from(this._runtimes.values());
}
// ─── Fases do ciclo de simulação ──────────────────────────────────────────
/**
* Fase 1 — Chegada de processos.
*
* Processos cujo `arrivalTime` é igual ao tick atual transitam do estado
* "New" para "Ready" e são inseridos na fila de prontos.
*/
private _handleArrivals(): void {
for (const p of this._processes) {
const rt = this._runtimes.get(p.id)!;
if (p.arrivalTime === this._tick && rt.state === "New") {
@@ -57,55 +140,79 @@ export class SimulationEngine {
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;
}
/**
* Fase 2 — Verificação de preempção.
*
* Consulta o algoritmo: se houver um candidato na fila de prontos que deva
* preemptar o processo em CPU, o processo atual retorna à fila de prontos
* e `_cpuProcess` é zerado. Chamado após a chegada para que processos
* recém-chegados possam imediatamente preemptar.
*/
private _checkPreemption(): void {
if (this._cpuProcess === null || this._readyQueue.length === 0) return;
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.
/**
* Fase 3 — Despacho / troca de contexto.
*
* Se um context switch estiver em andamento, nenhum processo é despachado
* neste tick. Caso contrário, e se a CPU estiver ociosa com processos
* prontos, o algoritmo seleciona o próximo processo:
*
* - Se houver overhead de troca de contexto configurado e o processo
* selecionado for diferente do último, o processo volta ao início da
* fila e o contador de context-switch é iniciado.
* - Caso contrário, o processo é despachado imediatamente para a CPU.
*/
private _dispatchNext(): void {
if (this._contextSwitchRemaining > 0) return;
if (this._cpuProcess !== null || this._readyQueue.length === 0) return;
const next = this._algorithm.select(this._readyQueue, this._config);
if (next === null) return;
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 (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;
}
}
}
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)
/**
* Fase 5 — Passagem do tempo.
*
* Simula a execução de 1 tick:
* - Decrementa o contador de context-switch restante (se ativo).
* - Decrementa o burst restante do processo em CPU e acumula cpuTime/quantum.
* - Decrementa o burst restante de cada processo em I/O e acumula ioTime.
* - Incrementa o waitingTime de todos os processos na fila de prontos.
*/
private _advanceTime(): void {
if (this._contextSwitchRemaining > 0) {
this._contextSwitchRemaining--;
}
@@ -125,44 +232,80 @@ export class SimulationEngine {
for (const rt of this._readyQueue) {
rt.waitingTime++;
}
}
// 6. Aging (Priority)
/**
* Fase 6 — Aging (envelhecimento de prioridade).
*
* Quando o aging está habilitado, a cada `agingInterval` ticks o valor de
* `currentPriority` de todos os processos na fila de prontos é decrementado
* em 1 (até o mínimo de 1). Isso previne inanição em algoritmos de prioridade.
*
* Não é executado no tick 0 para evitar decremento prematuro.
*/
private _applyAging(): void {
if (
this._config.agingEnabled &&
this._tick > 0 &&
this._tick % this._config.agingInterval === 0
!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);
}
return;
}
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)) {
/**
* Fase 7a — Conclusão de burst de CPU.
*
* Após a passagem do tempo, verifica se o processo em CPU terminou seu burst
* ou esgotou o quantum:
*
* - Burst zerado: avança para o próximo burst do processo.
* · Se o próximo for I/O → transita para "Waiting" e entra na ioQueue.
* · Se o próximo for CPU → retorna à fila de prontos ("Ready").
* · Se não houver próximo → termina o processo ("Terminated").
*
* - Quantum expirado (sem término de burst): retorna à fila de prontos.
*/
private _handleCpuCompletion(): void {
if (this._cpuProcess === null) return;
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);
this._cpuProcess = null;
} 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
/**
* Fase 7b — Conclusão de bursts de I/O.
*
* Percorre a ioQueue e, para cada processo cujo burst de I/O zerou:
* - Se houver próximo burst → transita para "Ready" e insere na readyQueue.
* - Caso contrário → termina o processo ("Terminated").
*
* Processos ainda com I/O pendente permanecem na ioQueue.
*/
private _handleIoCompletions(): void {
const nextIoQueue: ProcessRuntime[] = [];
for (const rt of this._ioQueue) {
if (rt.remainingBurst === 0) {
@@ -180,45 +323,22 @@ export class SimulationEngine {
}
}
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
// ----------------------------------------------------------
// ─── Helpers internos ─────────────────────────────────────────────────────
/**
* Aplica uma transição de estado a um runtime, atualizando `state`.
* Centraliza todas as transições para facilitar rastreamento e depuração.
*/
private _transition(rt: ProcessRuntime, state: StateLabel): void {
rt.state = state;
}
/**
* Avança o índice de burst de um runtime e atualiza `remainingBurst`
* com a duração do próximo burst. Se não houver próximo burst, `remainingBurst` é 0.
*/
private _advanceBurst(rt: ProcessRuntime): void {
rt.burstIndex++;
const process = this._processes.find((p) => p.id === rt.processId)!;
@@ -226,6 +346,11 @@ export class SimulationEngine {
rt.remainingBurst = nextBurst !== undefined ? nextBurst.duration : 0;
}
/**
* Captura o estado instantâneo de todos os processos no tick atual.
* Se um context switch estiver em andamento, `cpuProcess` é null e
* `ctxSwitchForProcess` identifica o processo prestes a executar.
*/
private _snapshot(): SimTick {
const states: Record<string, StateLabel> = {};
for (const [id, rt] of this._runtimes) {
@@ -244,30 +369,32 @@ export class SimulationEngine {
: null,
};
}
}
// ============================================================
// Helper: initialise a ProcessRuntime from a Process
// ============================================================
// ─── Inicialização ────────────────────────────────────────────────────────
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`);
/**
* Inicializa o estado de runtime de um processo a partir de sua definição.
* O primeiro burst deve ser do tipo "cpu"; caso contrário, um erro é lançado.
*/
private static _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,
};
}
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,
};
}