feat: add MLFQ scheduling algorithm and thread scheduler

- Implemented MLFQ (Multilevel Feedback Queue) algorithm with dynamic priority levels and quantum management.
- Created unit tests for MLFQ algorithm covering initialization, demotion, selection, priority boost, preemption, and quantum expiration.
- Developed a ThreadScheduler class supporting FCFS, RR, and PRIORITY scheduling policies for intra-process thread management.
- Added unit tests for ThreadScheduler to validate thread selection and quantum expiration logic.
This commit is contained in:
Lucas Guerra committed 2026-03-24 23:09:16 -03:00
1 parent 532c14e382
commit 5761dfe70a
34 files changed
+3689 -234

No files matched your search

+30 -6
View File
@@ -64,10 +64,10 @@ docker compose up
## 🎮 How to use
1. **Create your processes**: In the left panel, add processes by defining name, arrival time, priority, and a sequence of bursts (CPU and I/O). Or use the random generator to get started quickly.
2. **Configure the scheduler**: Choose an algorithm and adjust the parameters (quantum for RR, aging for Priority, etc.).
1. **Create your processes**: In the left panel, add processes by defining name, arrival time, priority, and a sequence of bursts (CPU and I/O). Enable the **Use Threads** toggle to create threads within a process, choosing the model (MANY_TO_ONE, ONE_TO_ONE, or MANY_TO_MANY) and the intra-process policy (FCFS, RR, or Priority).
2. **Configure the scheduler**: Choose an algorithm and adjust the parameters (quantum for RR, aging for Priority, quantums and boost interval for MLFQ, etc.).
3. **Run the simulation**: Use the Play/Pause/Step controls to run the simulation at the speed you want (0.25× to 4×).
4. **Analyze the results**: The Gantt chart shows each process's state in real time. At the end, the metrics panel displays turnaround, waiting time, throughput, and more.
4. **Analyze the results**: The Gantt chart shows each process's state in real time (and its threads, when configured). At the end, the metrics panel displays turnaround, waiting time, throughput, and — in the Threads tab — individual per-thread metrics.
> 💡 **Tip:** Load one of the built-in **presets** to see classic phenomena such as the FCFS convoy effect or priority starvation.
@@ -83,8 +83,10 @@ docker compose up
| **Round Robin (RR)** | ✅ | Quantum |
| **Priority NP**: Non-Preemptive Priority | ❌ | Aging (optional) |
| **Priority P**: Preemptive Priority | ✅ | Aging (optional) |
| **Priority RR**: Priority with Round Robin | ✅ | Quantum + Aging (optional) |
| **HRRN**: Highest Response Ratio Next | ❌ | None |
| **Multilevel Queue** | ✅ | Quantum per queue |
| **MLFQ**: Multilevel Feedback Queue | ✅ | Quantums per queue + boost interval |
> See the [algorithms documentation](./docs/algorithms.en.md) for details on how each one works.
@@ -119,20 +121,30 @@ src/
│ │ ├── priority_rr.ts
│ │ ├── hrrn.ts
│ │ ├── multilevel.ts
│ │ ├── mlfq.ts # Multilevel Feedback Queue
│ │ └── index.ts # Dispatch by algorithm
│ ├── engine.ts # SimulationEngine, orchestrates the ticks
│ ├── metrics.ts # Per-process and global metrics computation
│ ├── engine.ts # SimulationEngine, orchestrates the ticks (thread-aware)
│ ├── thread-scheduler.ts # Intra-process scheduler (FCFS / RR / Priority)
│ ├── metrics.ts # Per-process, global, and per-thread metrics computation
│ ├── types.ts # All TypeScript interfaces
│ └── presets.ts # Pre-defined scenarios
│ └── presets.ts # Pre-defined scenarios (processes and threads)
│
├── store/ # Global state with Zustand
├── components/ # React components
│ ├── ProcessPanel/ # Process creation and editing
│ │ ├── ThreadEditor.tsx # Thread editor (model, policy, per-thread bursts)
│ │ └── ...
│ ├── SchedulerPanel/ # Algorithm selection and parameters
│ ├── ControlBar/ # Play, Pause, Step, Reset, speed
│ ├── GanttChart/ # Interactive Gantt chart
│ │ ├── GanttThreadRow.tsx # Thread sub-row in the Gantt chart
│ │ └── ...
│ ├── MetricsPanel/ # Table, charts, and global metrics
│ │ ├── ThreadMetricsTable.tsx # Per-thread metrics table
│ │ └── ...
│ └── shared/ # Buttons, badges, tooltips, modals
│ ├── threadUtils.ts # Thread color derivation (HSL)
│ └── ...
│
└── hooks/ # Custom hooks (animation loop, export, etc.)
```
@@ -143,6 +155,8 @@ src/
Load classic scenarios with a single click to explore OS phenomena:
**Processes:**
| Preset | What it demonstrates |
|--------|----------------------|
| `classic_fcfs` | Basic execution with FCFS, no I/O |
@@ -153,6 +167,16 @@ Load classic scenarios with a single click to explore OS phenomena:
| `io_heavy` | Mix of CPU Bound vs. I/O Bound processes |
| `multilevel_demo` | Multiple queues with different priorities |
**Threads:**
| Preset | What it demonstrates |
|--------|----------------------|
| `many_to_one` | N:1 model — any thread's I/O blocks the entire process |
| `one_to_one` | 1:1 model — threads block independently |
| `many_to_many` | N:M model — kernel slots limit parallelism |
| `thread_starvation` | Intra-process starvation with Priority policy |
| `threads_vs_processes` | Direct comparison: threaded process vs single-burst process |
---
## 🧪 Tests
+30 -6
View File
@@ -62,10 +62,10 @@ docker compose up
## 🎮 Como usar
1. **Crie seus processos**: No painel esquerdo, adicione processos definindo nome, tempo de chegada, prioridade e sequência de bursts (CPU e I/O). Ou use o gerador aleatório para começar rápido.
2. **Configure o escalonador**: Escolha o algoritmo e ajuste os parâmetros (quantum para RR, aging para Priority, etc.).
1. **Crie seus processos**: No painel esquerdo, adicione processos definindo nome, tempo de chegada, prioridade e sequência de bursts (CPU e I/O). Ative a opção **Usar Threads** para criar threads dentro do processo, escolhendo o modelo (MANY_TO_ONE, ONE_TO_ONE ou MANY_TO_MANY) e a política intra-processo (FCFS, RR ou Priority).
2. **Configure o escalonador**: Escolha o algoritmo e ajuste os parâmetros (quantum para RR, aging para Priority, quantuns e boost para MLFQ, 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.
4. **Analise os resultados**: O diagrama de Gantt mostra em tempo real o estado de cada processo (e suas threads, quando configuradas). Ao final, o painel de métricas exibe turnaround, waiting time, throughput, e — na aba Threads — métricas individuais por thread.
> 💡 **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.
@@ -81,8 +81,10 @@ docker compose up
| **Round Robin (RR)** | ✅ | Quantum |
| **Priority NP**: Prioridade Não-Preemptiva | ❌ | Aging (opcional) |
| **Priority P**: Prioridade Preemptiva | ✅ | Aging (opcional) |
| **Priority RR**: Prioridade com Round Robin | ✅ | Quantum + Aging (opcional) |
| **HRRN**: Highest Response Ratio Next | ❌ | Nenhum |
| **Multilevel Queue** | ✅ | Quantum por fila |
| **MLFQ**: Multilevel Feedback Queue | ✅ | Quantuns por fila + intervalo de boost |
> Consulte a [documentação dos algoritmos](./docs/algorithms.md) para detalhes sobre como cada um funciona.
@@ -117,20 +119,30 @@ src/
│ │ ├── priority_rr.ts
│ │ ├── hrrn.ts
│ │ ├── multilevel.ts
│ │ ├── mlfq.ts # Multilevel Feedback Queue
│ │ └── index.ts # Dispatch por algoritmo
│ ├── engine.ts # SimulationEngine, orquestra os ticks
│ ├── metrics.ts # Cálculo de métricas por processo e globais
│ ├── engine.ts # SimulationEngine, orquestra os ticks (suporte a threads)
│ ├── thread-scheduler.ts # Escalonador intra-processo (FCFS / RR / Priority)
│ ├── metrics.ts # Cálculo de métricas por processo, globais e por thread
│ ├── types.ts # Todas as interfaces TypeScript
│ └── presets.ts # Cenários pré-definidos
│ └── presets.ts # Cenários pré-definidos (processos e threads)
│
├── store/ # Estado global com Zustand
├── components/ # Componentes React
│ ├── ProcessPanel/ # Criação e edição de processos
│ │ ├── ThreadEditor.tsx # Editor de threads (modelo, política, bursts por thread)
│ │ └── ...
│ ├── SchedulerPanel/ # Seleção de algoritmo e parâmetros
│ ├── ControlBar/ # Play, Pause, Step, Reset, velocidade
│ ├── GanttChart/ # Diagrama de Gantt interativo
│ │ ├── GanttThreadRow.tsx # Sub-linha de thread no Gantt
│ │ └── ...
│ ├── MetricsPanel/ # Tabela, gráficos e métricas globais
│ │ ├── ThreadMetricsTable.tsx # Tabela de métricas por thread
│ │ └── ...
│ └── shared/ # Botões, badges, tooltips, modais
│ ├── threadUtils.ts # Derivação de cores de threads (HSL)
│ └── ...
│
└── hooks/ # Hooks personalizados (loop de animação, export, etc.)
```
@@ -141,6 +153,8 @@ src/
Carregue cenários clássicos com um clique para explorar fenômenos de SO:
**Processos:**
| Preset | O que demonstra |
|--------|-----------------|
| `classic_fcfs` | Execução básica com FCFS, sem I/O |
@@ -151,6 +165,16 @@ Carregue cenários clássicos com um clique para explorar fenômenos de SO:
| `io_heavy` | Mix de processos CPU Bound vs I/O Bound |
| `multilevel_demo` | Múltiplas filas com prioridades diferentes |
**Threads:**
| Preset | O que demonstra |
|--------|-----------------|
| `many_to_one` | Modelo N:1 — I/O de uma thread bloqueia todo o processo |
| `one_to_one` | Modelo 1:1 — threads bloqueiam independentemente |
| `many_to_many` | Modelo N:M — slots de kernel limitam o paralelismo |
| `thread_starvation` | Starvation intra-processo com política Priority |
| `threads_vs_processes` | Comparação direta: processo com threads vs processo único |
---
## 🧪 Testes
BIN
View File
Binary file not shown.
+100
View File
@@ -158,6 +158,106 @@ This allows different classes of processes to be treated distinctly: for example
---
## MLFQ (Multilevel Feedback Queue)
**Type:** Preemptive
**Parameters:** number of queues, quantum per queue (default: [2, 4, 8]), boost interval
### How it works
MLFQ is an extension of the Multilevel Queue that allows **processes to move between queues** based on their observed runtime behavior — eliminating the need to know burst durations in advance.
#### Movement rules
| Event | Action |
|-------|--------|
| Newly arrived process | Enters **queue 0** (highest priority) |
| Process exhausts the quantum of its current queue | **Demoted** to the next queue (lower priority) |
| Process returns from I/O | Stays in the **same queue** (cooperative behavior is rewarded) |
| Boost tick (configurable interval) | **All** processes are moved to queue 0 |
#### Why is MLFQ efficient?
- Interactive processes (I/O Bound) naturally stay in high-priority queues because they leave the CPU before exhausting the quantum.
- Batch processes (CPU Bound) progressively migrate to lower-priority queues where they receive larger quantums.
- The periodic boost prevents starvation: even processes that fell to lower queues eventually return to the top.
#### Example with 3 queues (quantums: 2, 4, 8)
```
Process A (CPU Bound, burst=12):
Queue 0 → uses 2 ticks, demoted → Queue 1
Queue 1 → uses 4 ticks, demoted → Queue 2
Queue 2 → uses remaining 6 ticks (quantum=8, not exhausted)
Process B (I/O Bound, burst=1 CPU + I/O + burst=1 CPU):
Queue 0 → uses 1 tick → goes to I/O → returns to Queue 0
→ always stays in the highest-priority queue
```
### Parameters
| Parameter | Default | Description |
|-----------|---------|-------------|
| Number of queues | 3 | Number of priority queues |
| Quantum per queue | [2, 4, 8] | Time slice for each queue |
| Boost interval | 20 ticks | Every N ticks, all processes move up to queue 0 |
---
## Thread Models
PSS supports **thread** simulation within processes, implementing the three classical models described in Operating Systems textbooks.
### General Concepts
- A process can have **N user threads**, each with its own burst sequence (CPU and I/O).
- The scheduling policy among threads within the same process is configurable: **FCFS**, **Round Robin**, or **Priority**.
- The thread model determines how user threads map to kernel threads.
### MANY_TO_ONE (N:1)
**N user threads → 1 kernel thread**
- Only one thread can use the CPU at a time within the process.
- If any thread initiates I/O, **the entire process blocks** (the kernel thread is stuck).
- No real parallelism; very simple to implement.
- Simulated by the `many_to_one` presets.
### ONE_TO_ONE (1:1)
**Each user thread → 1 dedicated kernel thread**
- Threads block **independently**: if Thread A does I/O, Threads B and C remain in the Ready queue.
- The process only terminates when **all** threads have terminated.
- The process blocks only when all threads are simultaneously in I/O.
- Simulated by the `one_to_one` presets.
### MANY_TO_MANY (N:M)
**N user threads → M kernel threads (M ≤ N)**
- The number of kernel threads (`kernelThreadCount`) is configurable.
- If all M kernel slots are occupied, additional user threads wait in the **kernel wait queue** (`KernelWait`).
- Combines flexibility with resource control: more active threads than N:1 but less overhead than 1:1.
- Simulated by the `many_to_many` presets.
### Intra-Process Policy
Defines how the scheduler selects the next thread when multiple threads are Ready within the same process:
| Policy | Behavior |
|--------|----------|
| **FCFS** | The oldest thread in the Ready queue is selected |
| **Round Robin (RR)** | Each thread receives `threadQuantum` ticks; when exhausted, it goes to the end of the queue |
| **Priority** | The highest-priority thread is selected; immediate preemption if a higher-priority thread becomes ready |
### Gantt Chart with Threads
When a process has threads, the Gantt chart displays **sub-rows** below the process row, one per thread, with colors derived from the process color (40° hue shift per thread). The state of each thread (New, Ready, Running, Waiting, Terminated) is colored using the same CSS variables as process states.
---
## References
- Silberschatz, A., Galvin, P. B., & Gagne, G., *Operating System Concepts*, 10th edition.
+100
View File
@@ -158,6 +158,106 @@ Isso permite tratar diferentes classes de processos de forma distinta: por exemp
---
## MLFQ (Multilevel Feedback Queue)
**Tipo:** Preemptivo
**Parâmetros:** número de filas, quantum por fila (padrão: [2, 4, 8]), intervalo de boost
### Como funciona
O MLFQ é uma extensão do Multilevel Queue que permite **mover processos entre filas** com base em seu comportamento observado em tempo de execução — eliminando a necessidade de conhecer os bursts antecipadamente.
#### Regras de movimentação
| Evento | Ação |
|--------|------|
| Processo recém-chegado | Entra na **fila 0** (maior prioridade) |
| Processo esgota o quantum da fila atual | **Rebaixado** para a fila seguinte (menor prioridade) |
| Processo retorna do I/O | Permanece na **mesma fila** (comportamento cooperativo é recompensado) |
| Tick de boost (intervalo configurável) | **Todos** os processos são movidos para a fila 0 |
#### Por que o MLFQ é eficiente?
- Processos interativos (I/O Bound) naturalmente ficam nas filas de alta prioridade porque abandonam a CPU antes de esgotar o quantum.
- Processos batch (CPU Bound) migram progressivamente para filas de baixa prioridade onde recebem quantuns maiores.
- O boost periódico evita starvation: mesmo processos que caíram para as filas inferiores eventual­mente retornam ao topo.
#### Exemplo com 3 filas (quantuns: 2, 4, 8)
```
Processo A (CPU Bound, burst=12):
Fila 0 → usa 2 ticks, rebaixado → Fila 1
Fila 1 → usa 4 ticks, rebaixado → Fila 2
Fila 2 → usa 6 ticks restantes (quantum=8, não esgota)
Processo B (I/O Bound, burst=1 CPU + I/O + burst=1 CPU):
Fila 0 → usa 1 tick → vai para I/O → retorna à Fila 0
→ sempre permanece na fila de maior prioridade
```
### Parâmetros
| Parâmetro | Padrão | Descrição |
|-----------|--------|-----------|
| Número de filas | 3 | Quantidade de filas de prioridade |
| Quantuns por fila | [2, 4, 8] | Fatia de tempo de cada fila |
| Intervalo de boost | 20 ticks | A cada N ticks, todos os processos sobem para a fila 0 |
---
## Modelos de Threads
O PSS suporta simulação de **threads** dentro de processos, implementando os três modelos clássicos descritos em Sistemas Operacionais.
### Conceitos Gerais
- Um processo pode ter **N threads de usuário**, cada uma com sua própria sequência de bursts (CPU e I/O).
- A política de escalonamento entre threads de um mesmo processo é configurável: **FCFS**, **Round Robin** ou **Prioridade**.
- O modelo de thread determina como threads de usuário se mapeiam para threads de kernel.
### MANY_TO_ONE (N:1)
**N threads de usuário → 1 thread de kernel**
- Apenas uma thread pode usar a CPU por vez dentro do processo.
- Se qualquer thread iniciar I/O, **todo o processo bloqueia** (a thread de kernel fica presa).
- Sem paralelismo real; muito simples de implementar.
- Simulado pelos presets `many_to_one`.
### ONE_TO_ONE (1:1)
**Cada thread de usuário → 1 thread de kernel dedicada**
- Threads bloqueiam **independentemente**: se a Thread A faz I/O, as Threads B e C continuam na fila Ready.
- Processo só termina quando **todas** as threads terminam.
- Processo bloqueia somente quando todas as threads estão simultâneamente em I/O.
- Simulado pelos presets `one_to_one`.
### MANY_TO_MANY (N:M)
**N threads de usuário → M threads de kernel (M ≤ N)**
- O número de threads de kernel (`kernelThreadCount`) é configurável.
- Se todas as M slots de kernel estiverem ocupadas, threads de usuário adicionais ficam em **fila de espera de kernel** (`KernelWait`).
- Combina flexibilidade com controle de recursos: mais threads ativas que N:1 mas menos overhead que 1:1.
- Simulado pelos presets `many_to_many`.
### Política Intra-Processo
Define como o escalonador seleciona a próxima thread quando há múltiplas threads Ready no mesmo processo:
| Política | Comportamento |
|----------|--------------|
| **FCFS** | Thread mais antiga na fila Ready é selecionada |
| **Round Robin (RR)** | Cada thread recebe `threadQuantum` ticks; ao esgotar, vai para o fim da fila |
| **Priority** | Thread de maior prioridade é selecionada; preempção imediata se nova thread de maior prioridade chegar |
### Diagrama de Gantt com Threads
Quando um processo possui threads, o Gantt exibe **sub-linhas** abaixo da linha do processo, uma por thread, com cores derivadas da cor do processo (deslocamento de matiz de 40° por thread). O estado de cada thread (New, Ready, Running, Waiting, Terminated) é colorido com as mesmas variáveis CSS dos processos.
---
## Referências
- Silberschatz, A., Galvin, P. B., & Gagne, G., *Operating System Concepts*, 10ª edição.
+35
View File
@@ -105,6 +105,41 @@ The total height of each bar equals the **Turnaround Time** of the process.
---
## Thread Metrics
When a process has threads, PSS computes individual metrics for each thread. These metrics are displayed in the **Threads** tab of the metrics panel (visible only when there are processes with threads).
### Per-Thread Metrics
| Metric | Formula | What it means |
|--------|---------|----------------|
| **Arrival Time** | `arrivalTick` | Tick at which the thread first entered the Ready queue |
| **Start** | `startTick` | Tick at which the thread first used the CPU |
| **Finish** | `finishTick` | Tick at which the thread completed all of its bursts |
| **Response Time** | `startTick − arrivalTick` | How long the thread waited before receiving the CPU for the first time |
| **Turnaround Time** | `finishTick − arrivalTick` | Total lifetime of the thread (arrival → completion) |
| **Waiting Time** | `turnaround − cpuTime − ioTime` | Time in the Ready queue without doing anything useful |
| **CPU Time** | Σ ticks in Running state | Total CPU consumed by the thread |
| **I/O Time** | Σ ticks in Waiting state | Total time waiting for I/O |
### Thread Models and their impact on metrics
The thread model directly affects the recorded times:
| Model | Impact on Waiting Time |
|-------|------------------------|
| **MANY_TO_ONE** | Waiting Time tends to be high: any thread's I/O blocks the entire process, forcing other threads to wait |
| **ONE_TO_ONE** | Lower Waiting Time: threads block independently; while one does I/O, the others remain in the Ready queue |
| **MANY_TO_MANY** | Intermediate Waiting Time: threads that exceed the kernel slots enter `KernelWait` (shown with reduced opacity in the Gantt chart) |
### Visualization
The thread table displays inline horizontal mini progress bars in the Response, Turnaround, Waiting, CPU, and I/O columns, normalized by the largest value in each column across all threads — just like the process metrics table.
Each thread has a color derived from the parent process color (40° HSL hue shift per thread), shown as a colored dot in the table and as a sub-row in the Gantt chart.
---
## Example
Consider three processes with the following bursts:
+35
View File
@@ -105,6 +105,41 @@ A altura total de cada barra equivale ao **Turnaround Time** do processo.
---
## Métricas de Threads
Quando um processo possui threads, o PSS calcula métricas individuais para cada thread. Essas métricas são exibidas na aba **Threads** do painel de métricas (visível apenas quando há processos com threads).
### Métricas por Thread
| Métrica | Fórmula | O que significa |
|---------|---------|----------------|
| **Arrival Time** | `arrivalTick` | Tick em que a thread entrou pela primeira vez na fila Ready |
| **Start** | `startTick` | Tick em que a thread usou a CPU pela primeira vez |
| **Finish** | `finishTick` | Tick em que a thread encerrou todos os seus bursts |
| **Response Time** | `startTick − arrivalTick` | Quanto a thread esperou antes de receber a CPU pela primeira vez |
| **Turnaround Time** | `finishTick − arrivalTick` | Tempo total de vida da thread (chegada → término) |
| **Waiting Time** | `turnaround − cpuTime − ioTime` | Tempo na fila Ready sem executar nada útil |
| **CPU Time** | Σ ticks em estado Running | Total de CPU consumido pela thread |
| **I/O Time** | Σ ticks em estado Waiting | Total de tempo aguardando I/O |
### Modelos de Thread e seu impacto nas métricas
O modelo de thread afeta diretamente os tempos registrados:
| Modelo | Impacto no Waiting Time |
|--------|------------------------|
| **MANY_TO_ONE** | Waiting Time tende a ser alto: qualquer I/O de uma thread bloqueia todo o processo, forçando as demais threads a esperarem |
| **ONE_TO_ONE** | Waiting Time menor: threads bloqueiam independentemente; enquanto uma faz I/O, as outras continuam na fila Ready |
| **MANY_TO_MANY** | Waiting Time intermediário: threads que excedem os slots de kernel ficam em `KernelWait` (indicadas com `opacity` reduzida no Gantt) |
### Visualização
A tabela de threads exibe mini barras de progresso horizontais nas colunas de Response, Turnaround, Waiting, CPU e I/O, normalizadas pelo maior valor de cada coluna entre todas as threads — assim como na tabela de processos.
Cada thread possui uma cor derivada da cor do processo pai (deslocamento de matiz HSL de 40° por thread), exibida como ponto colorido na tabela e como sub-linha no Gantt.
---
## Exemplo
Considere três processos com os seguintes bursts:
+17 -9
View File
@@ -147,6 +147,7 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -519,6 +520,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -567,6 +569,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -1892,8 +1895,7 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -2034,6 +2036,7 @@
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.18.0"
}
@@ -2051,6 +2054,7 @@
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -2062,6 +2066,7 @@
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
@@ -2373,6 +2378,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -2725,8 +2731,7 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
@@ -3544,7 +3549,6 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -3760,6 +3764,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -3802,7 +3807,6 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -3818,7 +3822,6 @@
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
}
@@ -3829,7 +3832,6 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -3852,6 +3854,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -3864,6 +3867,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -3884,6 +3888,7 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
@@ -3960,7 +3965,8 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/redux-thunk": {
"version": "3.1.0",
@@ -4546,6 +4552,7 @@
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -4644,6 +4651,7 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pss",
"version": "1.2.0",
"version": "1.3.0",
"description": "Process Scheduler Simulator — an interactive educational tool that runs entirely in the browser",
"main": "index.js",
"scripts": {
+1 -1
View File
@@ -66,7 +66,7 @@ const ControlBar = () => {
return (
<div
className="h-16 flex items-center px-5 gap-3 bg-slate-800 border-b border-slate-700 shadow-sm"
className="h-16 py-2 flex items-center px-5 gap-3 bg-slate-800 border-b border-slate-700 shadow-sm"
role="toolbar"
aria-label="Simulation controls"
>
+44 -23
View File
@@ -3,6 +3,7 @@ import { useSimulationStore } from "@store/simulationStore";
import { useProcessStore } from "@store/processStore";
import { useGanttScroll } from "@hooks/useGanttScroll";
import GanttRow from "./GanttRow";
import GanttThreadRow from "./GanttThreadRow";
import GanttLegend from "./GanttLegend";
import GanttTooltip, { type TooltipData } from "./GanttTooltip";
import { CELL_H, CELL_W } from "./constants";
@@ -13,33 +14,29 @@ const GanttChart = () => {
const status = useSimulationStore((s) => s.status);
const processes = useProcessStore((s) => s.processes);
// Scrollable container ref for auto-scroll
const scrollRef = useRef<HTMLDivElement>(null);
const currentTickIndex = ticks.length - 1;
useGanttScroll(scrollRef, currentTickIndex, status === "running");
// Tooltip state
const [tooltip, setTooltip] = useState<TooltipData | null>(null);
const handleMouseLeave = useCallback(() => setTooltip(null), []);
// Empty state
if (ticks.length === 0) {
return (
<div className="border-b border-slate-800 flex items-center justify-center text-slate-600 bg-slate-900/50 text-sm select-none"
style={{ minHeight: 80 }}>
<div
className="border-b border-slate-800 flex items-center justify-center text-slate-600 bg-slate-900/50 text-sm select-none"
style={{ minHeight: 80 }}
>
Start the simulation to see the Gantt chart
</div>
);
}
// ── ruler (tick numbers) ──────────────────────────────────
const tickNumbers = ticks.map((t) => t.tick);
const totalWidth = ticks.length * CELL_W;
return (
<div className="border-b border-slate-800 bg-slate-950 flex flex-col">
{/* ── scrollable grid + cursor ── */}
<div
ref={scrollRef}
className="overflow-x-auto overflow-y-hidden gantt-scrollbar relative"
@@ -48,9 +45,10 @@ const GanttChart = () => {
{/* Tick ruler */}
<div className="flex sticky top-0 z-10 bg-slate-950 border-b border-slate-800">
{/* Spacer for sticky process labels column */}
<div className="sticky left-0 z-20 bg-slate-950 border-r border-slate-700 shrink-0" style={{ width: 48, height: 16 }} />
<div
className="sticky left-0 z-20 bg-slate-950 border-r border-slate-700 shrink-0"
style={{ width: 48, height: 16 }}
/>
{tickNumbers.map((t) => (
<div
key={t}
@@ -62,16 +60,25 @@ const GanttChart = () => {
))}
</div>
{/* Process rows */}
{/* Process rows (+ thread sub-rows) */}
<div className="relative">
{processes.map((process) => (
<div key={process.id} className="flex items-center border-b border-white/5 last:border-b-0">
{/* Process label (sticky on the left) */}
{processes.map((process) => {
const hasThreads = (process.threads?.length ?? 0) > 0;
return (
<div key={process.id}>
{/* ── Linha do processo ─────────────────────────── */}
<div
className={`flex items-center border-b border-white/5 ${hasThreads ? 'border-b-0' : ''}`}
>
<div
className="sticky left-0 z-20 bg-slate-900 border-r border-slate-700 px-2 flex items-center shrink-0"
style={{ width: 48, height: CELL_H }}
>
<span className="text-[10px] font-bold truncate" style={{ color: process.color }}>
<span
className="text-[10px] font-bold truncate"
style={{ color: process.color }}
>
{process.name}
</span>
</div>
@@ -83,8 +90,29 @@ const GanttChart = () => {
onCellMouseLeave={handleMouseLeave}
/>
</div>
{/* ── Sub-linhas de threads ─────────────────────── */}
{hasThreads &&
process.threads!.map((thread, threadIdx) => (
<GanttThreadRow
key={thread.tid}
thread={thread}
threadIndex={threadIdx}
process={process}
ticks={ticks}
onCellMouseEnter={setTooltip}
onCellMouseLeave={handleMouseLeave}
/>
))}
{/* Separador após o bloco de threads */}
{hasThreads && (
<div className="border-b border-white/5" />
)}
</div>
);
})}
{/* Current-tick cursor line */}
{currentTickIndex >= 0 && (
<div
@@ -97,14 +125,7 @@ const GanttChart = () => {
</div>
</div>
{/* ── process name labels (sticky left, overlay) ── */}
{/* Rendered OUTSIDE the scrollable div so they stay fixed on the left */}
{/* We achieve this with a separate absolute-positioned column */}
{/* ── legend ── */}
<GanttLegend />
{/* ── tooltip ── */}
{tooltip && <GanttTooltip data={tooltip} />}
</div>
);
@@ -0,0 +1,106 @@
import React from "react";
import type { Process, SimTick, StateLabel, Thread } from "@core/types";
import { CELL_W } from "./constants";
import type { TooltipData } from "./GanttTooltip";
import { deriveThreadColor } from "../shared/threadUtils";
const THREAD_CELL_H = 20;
// ── cores de estado (mesmas do processo) ────────────────────
const STATE_CLASS: Record<StateLabel, string> = {
New: "bg-[var(--color-state-new)]",
Ready: "bg-[var(--color-state-ready)]",
Running: "bg-[var(--color-state-running)]",
Waiting: "bg-[var(--color-state-waiting)]",
Terminated: "bg-[var(--color-state-terminated)]",
};
interface GanttThreadRowProps {
thread: Thread;
threadIndex: number;
process: Process;
ticks: SimTick[];
onCellMouseEnter: (data: TooltipData) => void;
onCellMouseLeave: () => void;
}
const GanttThreadRow = React.memo(function GanttThreadRow({
thread,
threadIndex,
process,
ticks,
onCellMouseEnter,
onCellMouseLeave,
}: GanttThreadRowProps) {
const threadColor = deriveThreadColor(process.color, threadIndex);
return (
<div className="flex items-center border-b border-white/3">
{/* Rótulo da thread (sticky) */}
<div
className="sticky left-0 z-20 bg-slate-900/90 border-r border-slate-700/50 px-2 flex items-center shrink-0 gap-1"
style={{ width: 48, height: THREAD_CELL_H }}
>
<span
className="w-1.5 h-1.5 rounded-full shrink-0"
style={{ backgroundColor: threadColor }}
/>
<span
className="text-[8px] font-medium truncate"
style={{ color: threadColor }}
>
{thread.name}
</span>
</div>
{/* Células da thread */}
{ticks.map((tick) => {
const threadState: StateLabel =
tick.threadStates?.[process.id]?.[thread.tid] ?? "New";
const isActive = tick.cpuThreads?.[process.id] === thread.tid;
const isKernelWait =
tick.kernelWaitingThreads?.[process.id]?.includes(thread.tid) ?? false;
const bg = STATE_CLASS[threadState];
// Borda colorida para thread ativa (Running)
const borderStyle =
isActive && threadState === "Running"
? { borderLeft: `2px solid ${threadColor}` }
: undefined;
// Padrão diagonal para kernel wait (MANY_TO_MANY)
const extraClass = isKernelWait
? "opacity-50"
: "opacity-80 hover:opacity-100";
return (
<div
key={tick.tick}
className={`${bg} ${extraClass} shrink-0 transition-opacity cursor-default`}
style={{ width: CELL_W, height: THREAD_CELL_H, ...borderStyle }}
title={`${thread.name} — tick ${tick.tick}: ${threadState}${isKernelWait ? " (KernelWait)" : ""}`}
onMouseEnter={(e) =>
onCellMouseEnter({
tick,
process,
state: threadState,
isCtxSwitch: false,
x: e.clientX,
y: e.clientY,
})
}
onMouseLeave={onCellMouseLeave}
/>
);
})}
{ticks.length === 0 && (
<div style={{ width: CELL_W, height: THREAD_CELL_H }} className="bg-slate-800/40" />
)}
</div>
);
});
export default GanttThreadRow;
+40 -2
View File
@@ -86,7 +86,15 @@ const AppHeader = () => {
Preset Scenarios
</div>
<div className="overflow-y-auto">
{presets.map((preset, i) => (
{/* Classic process presets */}
<div className="px-3 py-1 text-[9px] text-slate-500 uppercase tracking-wider font-semibold bg-slate-800/80 sticky top-0">
Processes
</div>
{presets
.filter(p => !['many_to_one','one_to_one','many_to_many','thread_starvation','threads_vs_processes'].includes(p.name))
.map((preset) => {
const i = presets.indexOf(preset);
return (
<button
key={preset.name}
onClick={() => handleLoadPreset(i)}
@@ -102,7 +110,37 @@ const AppHeader = () => {
{preset.config.algorithm}
</div>
</button>
))}
);
})}
{/* Thread presets */}
<div className="px-3 py-1 text-[9px] text-slate-500 uppercase tracking-wider font-semibold bg-slate-800/80 sticky top-0 border-t border-slate-700 mt-1">
Threads
</div>
{presets
.filter(p => ['many_to_one','one_to_one','many_to_many','thread_starvation','threads_vs_processes'].includes(p.name))
.map((preset) => {
const i = presets.indexOf(preset);
return (
<button
key={preset.name}
onClick={() => handleLoadPreset(i)}
className="w-full text-left px-3 py-2.5 hover:bg-slate-700 transition-colors group"
>
<div className="flex items-center gap-1.5">
<div className="text-xs font-medium text-slate-200 group-hover:text-white">
{preset.name}
</div>
<span className="text-[8px] px-1 py-0.5 rounded bg-blue-500/20 text-blue-400 border border-blue-500/30 shrink-0">
THREADS
</span>
</div>
<div className="text-[11px] text-slate-400 mt-0.5 leading-snug">
{preset.description}
</div>
</button>
);
})}
</div>
</div>
</>
-6
View File
@@ -4,12 +4,6 @@ import SchedulerPanel from '../SchedulerPanel/SchedulerPanel';
const Sidebar = () => {
return (
<aside className="w-80 min-w-[20rem] h-full bg-slate-800 border-r border-slate-700 flex flex-col shadow-xl z-20">
<div className="h-16 flex items-center px-6 border-b border-slate-700 bg-slate-800/80 backdrop-blur-sm">
<h1 className="text-xl font-bold bg-linear-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent truncate">
Process Scheduler Simulator
</h1>
</div>
<div className="flex-1 overflow-y-auto overflow-x-hidden p-4 space-y-6 scrollbar-hide">
<ProcessPanel />
<div className="border-t border-slate-700 pt-4">
+27 -9
View File
@@ -28,6 +28,22 @@ const TOOLTIP_STYLE = {
boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
};
/**
* Wrapper that gives ResponsiveContainer a bounded, non-growing parent.
*
* Without this, recharts' ResizeObserver inside a scroll container can trigger
* a resize loop: scrollbar appears → available width shrinks → chart redraws →
* height shifts → scrollbar disappears → repeat. The explicit height + overflow
* hidden caps the chart's DOM footprint and breaks the loop.
*/
const ChartBox = ({ height, children }: { height: number; children: React.ReactNode }) => (
<div style={{ width: '100%', height, overflow: 'hidden', flexShrink: 0 }}>
<ResponsiveContainer width="100%" height="100%">
{children as React.ReactElement}
</ResponsiveContainer>
</div>
);
const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
if (metrics.length === 0) {
return (
@@ -80,7 +96,6 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
// ── 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);
@@ -101,7 +116,7 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
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 'CPU Time': row[key] = (m.cpuTime / maxCpu) * 100; break;
case 'I/O Time': row[key] = (m.ioTime / maxIo) * 100; break;
}
});
@@ -110,15 +125,18 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
const showRadar = metrics.length >= 2;
// Charts are always stacked vertically (flex-col) so that the total height
// exceeds the panel's viewport, making the scroll area in MetricsPanel
// active and accessible on any screen size.
return (
<div className={`p-3 flex gap-3 ${showRadar ? 'flex-row' : 'flex-col'}`}>
<div className="p-3 flex flex-col gap-6">
{/* ── Radar chart ───────────────────────────────────────── */}
{showRadar && (
<div className="flex-1 min-w-0">
<div className="w-full">
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
Comparative Profile (normalized)
</p>
<ResponsiveContainer width="100%" height={400}>
<ChartBox height={360}>
<RadarChart data={radarData} margin={{ top: 8, right: 20, bottom: 8, left: 20 }}>
<PolarGrid stroke="#334155" />
<PolarAngleAxis
@@ -142,16 +160,16 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
/>
</RadarChart>
</ResponsiveContainer>
</ChartBox>
</div>
)}
{/* ── Stacked bar chart ─────────────────────────────────── */}
<div className={showRadar ? 'flex-1 min-w-0' : 'w-full'}>
<div className="w-full">
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
Breakdown per Process (ticks)
</p>
<ResponsiveContainer width="100%" height={400}>
<ChartBox height={360}>
<BarChart data={barData} margin={{ top: 8, right: 12, left: -8, bottom: 4 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
<XAxis
@@ -183,7 +201,7 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
<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>
</ChartBox>
<p className="text-center text-[9px] text-slate-600 mt-0.5">
Waiting + CPU + I/O = Turnaround
</p>
+35 -13
View File
@@ -1,20 +1,15 @@
import { useState } from 'react';
import { Download, BarChart2, Table, Globe } from 'lucide-react';
import { computeProcessMetrics, computeGlobalMetrics } from '@core/metrics';
import { Download, BarChart2, Table, Globe, Layers } from 'lucide-react';
import { computeProcessMetrics, computeGlobalMetrics, computeThreadMetrics } from '@core/metrics';
import { useSimulationStore } from '../../store/simulationStore';
import { useProcessStore } from '../../store/processStore';
import { useExport } from '../../hooks/useExport';
import MetricsTable from './MetricsTable';
import GlobalMetrics from './GlobalMetrics';
import MetricsChart from './MetricsChart';
import ThreadMetricsTable from './ThreadMetricsTable';
type Tab = 'table' | 'global' | 'charts';
const TAB_CONFIG: { id: Tab; label: string; icon: React.ReactNode }[] = [
{ id: 'table', label: 'Per Process', icon: <Table size={13} /> },
{ id: 'global', label: 'Global', icon: <Globe size={13} /> },
{ id: 'charts', label: 'Charts', icon: <BarChart2 size={13} /> },
];
type Tab = 'table' | 'global' | 'charts' | 'threads';
const MetricsPanel = () => {
const [activeTab, setActiveTab] = useState<Tab>('table');
@@ -36,20 +31,39 @@ const MetricsPanel = () => {
);
}
// Derive metrics from engine runtime state
// ── Métricas de processos ──────────────────────────────────────────────
const runtimes = engine?.runtimeStates ?? [];
const completedRuntimes = runtimes.filter(rt => rt.finishTick !== null);
const processMetrics = completedRuntimes.map(rt => computeProcessMetrics(rt));
const globalMetrics = computeGlobalMetrics(processMetrics, ticks.length);
const processMap = Object.fromEntries(processes.map(p => [p.id, p]));
// ── Métricas de threads ────────────────────────────────────────────────
const threadRuntimes = engine?.threadRuntimeStates ?? [];
const completedThreadRts = threadRuntimes.filter(trt => trt.finishTick !== null);
const threadMetrics = completedThreadRts.map(trt => {
const process = processMap[trt.processId];
const thread = process?.threads?.find(t => t.tid === trt.threadId);
if (!process || !thread) return null;
return computeThreadMetrics(trt, thread, process);
}).filter(Boolean) as ReturnType<typeof computeThreadMetrics>[];
const hasThreads = processes.some(p => (p.threads?.length ?? 0) > 0);
const canExportCSV = status === 'finished' && processMetrics.length > 0;
const TAB_CONFIG: { id: Tab; label: string; icon: React.ReactNode; show: boolean }[] = [
{ id: 'table', label: 'Per Process', icon: <Table size={13} />, show: true },
{ id: 'global', label: 'Global', icon: <Globe size={13} />, show: true },
{ id: 'charts', label: 'Charts', icon: <BarChart2 size={13} />, show: true },
{ id: 'threads', label: 'Threads', icon: <Layers size={13} />, show: hasThreads },
];
return (
<div className="border-t border-slate-700 bg-slate-950 flex flex-col shrink-0" style={{ height: '600px' }}>
<div className="border-t border-slate-700 bg-slate-950 flex flex-col overflow-hidden" style={{ height: '600px' }}>
{/* 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 => (
{TAB_CONFIG.filter(t => t.show).map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
@@ -62,6 +76,11 @@ const MetricsPanel = () => {
>
{tab.icon}
{tab.label}
{tab.id === 'threads' && threadMetrics.length > 0 && (
<span className="text-[9px] bg-blue-500/20 text-blue-400 px-1 rounded">
{threadMetrics.length}
</span>
)}
</button>
))}
@@ -80,7 +99,7 @@ const MetricsPanel = () => {
</div>
{/* Tab content */}
<div className="flex-1 overflow-y-auto">
<div className="flex-1 overflow-y-auto min-h-0">
{activeTab === 'table' && (
<MetricsTable metrics={processMetrics} processMap={processMap} />
)}
@@ -90,6 +109,9 @@ const MetricsPanel = () => {
{activeTab === 'charts' && (
<MetricsChart metrics={processMetrics} processMap={processMap} />
)}
{activeTab === 'threads' && (
<ThreadMetricsTable metrics={threadMetrics} processMap={processMap} />
)}
</div>
</div>
);
@@ -0,0 +1,138 @@
import type { Process, ThreadMetrics } from "@core/types";
import { deriveThreadColor } from "../shared/threadUtils";
interface ThreadMetricsTableProps {
metrics: ThreadMetrics[];
processMap: Record<string, Process>;
}
const MiniBar = ({
value,
max,
color,
}: {
value: number;
max: number;
color: string;
}) => {
const pct = max > 0 ? (value / max) * 100 : 0;
return (
<div className="relative flex items-center justify-end gap-1.5 min-w-13">
<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 ThreadMetricsTable = ({ metrics, processMap }: ThreadMetricsTableProps) => {
if (metrics.length === 0) {
return (
<div className="flex items-center justify-center h-32 text-slate-500 text-sm">
No completed threads yet.
</div>
);
}
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);
// Agrupa por processo para mostrar o índice de thread e cor correta
const threadIndexByProcess: Record<string, number> = {};
return (
<div className="overflow-x-auto">
<table className="w-full text-xs text-slate-300 border-collapse min-w-195">
<thead>
<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">Thread</th>
<th className="text-left px-3 py-2 font-medium">Process</th>
<th className="text-right px-3 py-2 font-medium">Arrival</th>
<th className="text-right px-3 py-2 font-medium">Start</th>
<th className="text-right px-3 py-2 font-medium">Finish</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>
</tr>
</thead>
<tbody>
{metrics.map((m, i) => {
const proc = processMap[m.processId];
const procColor = proc?.color ?? "#94a3b8";
// Obtém o índice da thread dentro do processo para cor
if (threadIndexByProcess[m.processId] === undefined) {
threadIndexByProcess[m.processId] = 0;
}
const threadIdx = threadIndexByProcess[m.processId]!;
threadIndexByProcess[m.processId] = threadIdx + 1;
const threadColor = deriveThreadColor(procColor, threadIdx);
return (
<tr
key={`${m.processId}-${m.threadId}`}
className={`border-b border-slate-800/80 hover:bg-slate-800/50 transition-colors ${
i % 2 === 0 ? "" : "bg-slate-900/20"
}`}
>
{/* Thread name */}
<td className="px-3 py-2">
<div className="flex items-center gap-2">
<span
className="inline-block w-2 h-2 rounded-full shrink-0"
style={{ backgroundColor: threadColor }}
/>
<span className="font-medium text-slate-200">{m.threadName}</span>
</div>
</td>
{/* Process name */}
<td className="px-3 py-2">
<div className="flex items-center gap-1.5">
<span
className="inline-block w-2 h-2 rounded-full shrink-0"
style={{ backgroundColor: procColor }}
/>
<span className="text-slate-400 text-[10px]">
{proc?.name ?? m.processId}
</span>
</div>
</td>
<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>
<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>
</tr>
);
})}
</tbody>
</table>
</div>
);
};
export default ThreadMetricsTable;
+31 -3
View File
@@ -10,10 +10,28 @@ interface ProcessCardProps {
onRemove: (id: string) => void;
}
const MODEL_LABEL: Record<string, string> = {
MANY_TO_ONE: 'M:1',
ONE_TO_ONE: '1:1',
MANY_TO_MANY: 'M:M',
};
const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
const bound = classifyBound(process.bursts);
const totalCpu = process.bursts.filter(b => b.type === 'cpu').reduce((a, b) => a + b.duration, 0);
const totalIo = process.bursts.filter(b => b.type === 'io').reduce((a, b) => a + b.duration, 0);
const hasThreads = (process.threads?.length ?? 0) > 0;
const bound = hasThreads ? 'Balanced' : classifyBound(process.bursts);
const totalCpu = hasThreads
? process.threads!.reduce(
(sum, t) => sum + t.bursts.filter(b => b.type === 'cpu').reduce((a, b) => a + b.duration, 0),
0,
)
: process.bursts.filter(b => b.type === 'cpu').reduce((a, b) => a + b.duration, 0);
const totalIo = hasThreads
? process.threads!.reduce(
(sum, t) => sum + t.bursts.filter(b => b.type === 'io').reduce((a, b) => a + b.duration, 0),
0,
)
: process.bursts.filter(b => b.type === 'io').reduce((a, b) => a + b.duration, 0);
const boundVariant = bound === 'CPU Bound' ? 'cpu' : bound === 'I/O Bound' ? 'io' : 'balanced';
@@ -38,7 +56,13 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
</span>
)}
<span className="font-semibold text-sm text-slate-100 truncate">{process.name}</span>
{hasThreads ? (
<span className="text-[9px] font-semibold px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-400 border border-blue-500/30 shrink-0">
{MODEL_LABEL[process.threadModel ?? 'ONE_TO_ONE']} · {process.threads!.length}T
</span>
) : (
<Badge variant={boundVariant}>{bound}</Badge>
)}
</div>
<div className="flex flex-wrap gap-x-3 gap-y-0.5 text-[11px] text-slate-400">
<span>Arrival: <span className="text-slate-300 font-medium">{process.arrivalTime}</span></span>
@@ -47,7 +71,11 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
{totalIo > 0 && (
<span>I/O: <span className="text-cyan-400 font-medium">{totalIo}</span></span>
)}
{hasThreads ? (
<span>Threads: <span className="text-slate-300 font-medium">{process.threads!.length}</span></span>
) : (
<span>Bursts: <span className="text-slate-300 font-medium">{process.bursts.length}</span></span>
)}
</div>
</div>
+145 -19
View File
@@ -1,16 +1,18 @@
import { useState, useEffect } from 'react';
import { Plus, Minus, Check, X } from 'lucide-react';
import type { BurstSegment, Process } from '@core/types';
import type { BurstSegment, IntraProcessPolicy, Process, Thread, ThreadModel } from '@core/types';
import { useProcessStore } from '../../store/processStore';
import IconButton from '../shared/IconButton';
import { generateId, nextColor } from '../shared/processUtils';
import ThreadEditor from './ThreadEditor';
interface ProcessFormProps {
/** If provided, the form edits this process; otherwise creates a new one. */
editingProcess?: Process | null;
onClose: () => void;
}
type FormTab = 'bursts' | 'threads';
const DEFAULT_BURSTS: BurstSegment[] = [{ type: 'cpu', duration: 4 }];
const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
@@ -19,12 +21,22 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
const isEditing = !!editingProcess;
// ── Campos base ─────────────────────────────────────────────────────────
const [name, setName] = useState('');
const [arrivalTime, setArrivalTime] = useState(0);
const [priority, setPriority] = useState(5);
const [color, setColor] = useState(nextColor);
const [bursts, setBursts] = useState<BurstSegment[]>(DEFAULT_BURSTS);
const [errors, setErrors] = useState<string[]>([]);
const [, setActiveTab] = useState<FormTab>('bursts');
// ── Campos de threads ────────────────────────────────────────────────────
const [hasThreads, setHasThreads] = useState(false);
const [threads, setThreads] = useState<Thread[]>([]);
const [threadModel, setThreadModel] = useState<ThreadModel>('ONE_TO_ONE');
const [threadPolicy, setThreadPolicy] = useState<IntraProcessPolicy>('FCFS');
const [threadQuantum, setThreadQuantum] = useState(2);
const [kernelThreadCount, setKernelThreadCount] = useState(2);
useEffect(() => {
if (editingProcess) {
@@ -33,24 +45,38 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
setPriority(editingProcess.priority);
setColor(editingProcess.color);
setBursts([...editingProcess.bursts.map((b: BurstSegment) => ({ ...b }))]);
const hasT = (editingProcess.threads?.length ?? 0) > 0;
setHasThreads(hasT);
setThreads(editingProcess.threads ? [...editingProcess.threads] : []);
setThreadModel(editingProcess.threadModel ?? 'ONE_TO_ONE');
setThreadPolicy(editingProcess.threadSchedulingPolicy ?? 'FCFS');
setThreadQuantum(editingProcess.threadQuantum ?? 2);
setKernelThreadCount(editingProcess.kernelThreadCount ?? 2);
setActiveTab(hasT ? 'threads' : 'bursts');
} else {
setName('');
setArrivalTime(0);
setPriority(5);
setColor(nextColor());
setBursts([{ type: 'cpu', duration: 4 }]);
setHasThreads(false);
setThreads([]);
setThreadModel('ONE_TO_ONE');
setThreadPolicy('FCFS');
setThreadQuantum(2);
setKernelThreadCount(2);
setActiveTab('bursts');
}
setErrors([]);
}, [editingProcess]);
// ---- Burst helpers ----
// ── Helpers de bursts ───────────────────────────────────────────────────
const updateBurstDuration = (index: number, value: number) => {
setBursts(prev => prev.map((b, i) => i === index ? { ...b, duration: Math.max(1, value) } : b));
};
const addBurst = () => {
// enforce alternating cpu/io pattern
const lastType = bursts[bursts.length - 1]?.type ?? 'io';
const nextType: BurstSegment['type'] = lastType === 'cpu' ? 'io' : 'cpu';
setBursts(prev => [...prev, { type: nextType, duration: 2 }]);
@@ -58,36 +84,90 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
const removeBurst = (index: number) => {
if (bursts.length <= 1) return;
setBursts(prev => {
const next = prev.filter((_, i) => i !== index);
// Safe-guard: ensure it still starts (and ends) with cpu
return next;
});
setBursts(prev => prev.filter((_, i) => i !== index));
};
// ---- Validation ----
const toggleThreads = (enabled: boolean) => {
setHasThreads(enabled);
if (enabled && threads.length === 0) {
setThreads([
{ tid: `t_${generateId()}`, name: 'T1', priority: 5, bursts: [{ type: 'cpu', duration: 3 }] },
{ tid: `t_${generateId()}`, name: 'T2', priority: 5, bursts: [{ type: 'cpu', duration: 2 }] },
]);
}
setActiveTab(enabled ? 'threads' : 'bursts');
};
// ── Validação ───────────────────────────────────────────────────────────
const validate = (): boolean => {
const errs: string[] = [];
if (!name.trim()) errs.push('Name is required');
if (name.trim().length > 8) errs.push('Name must be at most 8 characters');
if (arrivalTime < 0) errs.push('Arrival Time must be ≥ 0');
if (priority < 1 || priority > 10) errs.push('Priority must be between 1 and 10');
if (hasThreads) {
if (threads.length === 0) errs.push('At least one thread is required');
threads.forEach((t, ti) => {
if (!t.name.trim()) errs.push(`Thread ${ti + 1}: name is required`);
if (t.bursts.length === 0) errs.push(`Thread ${ti + 1}: at least 1 burst required`);
if (t.bursts[0]?.type !== 'cpu') errs.push(`Thread ${ti + 1}: first burst must be CPU`);
if (t.bursts.at(-1)?.type !== 'cpu') errs.push(`Thread ${ti + 1}: last burst must be CPU`);
t.bursts.forEach((b, bi) => {
if (b.duration < 1) errs.push(`Thread ${ti + 1}, burst ${bi + 1}: duration must be ≥ 1`);
if (bi > 0 && t.bursts[bi - 1]?.type === b.type)
errs.push(`Thread ${ti + 1}: bursts must alternate CPU/IO`);
});
});
if (threadModel === 'MANY_TO_MANY' && kernelThreadCount > threads.length) {
errs.push(`Kernel threads (${kernelThreadCount}) cannot exceed user threads (${threads.length})`);
}
} else {
if (bursts.length === 0) errs.push('There must be at least 1 CPU burst');
if (bursts[0]?.type !== 'cpu') errs.push('The first burst must be CPU');
if (bursts.at(-1)?.type !== 'cpu') errs.push('The last burst must be CPU');
bursts.forEach((b, i) => {
if (b.duration < 1) errs.push(`Burst ${i + 1}: duration must be ≥ 1`);
if (i > 0 && bursts[i - 1]?.type === b.type) errs.push(`Bursts ${i} and ${i + 1} cannot be the same type (alternate CPU/IO)`);
if (i > 0 && bursts[i - 1]?.type === b.type)
errs.push(`Bursts ${i} and ${i + 1} cannot be the same type`);
});
}
setErrors(errs);
return errs.length === 0;
};
const handleSubmit = () => {
if (!validate()) return;
const processData: Partial<Process> = {
name: name.trim(),
arrivalTime,
priority,
color,
bursts: hasThreads ? [] : bursts,
...(hasThreads
? {
threads,
threadModel,
threadSchedulingPolicy: threadPolicy,
threadQuantum,
kernelThreadCount:
threadModel === 'MANY_TO_MANY'
? kernelThreadCount
: undefined,
}
: {
threads: undefined,
threadModel: undefined,
threadSchedulingPolicy: undefined,
threadQuantum: undefined,
kernelThreadCount: undefined,
}),
};
if (isEditing && editingProcess) {
updateProcess(editingProcess.id, { name: name.trim(), arrivalTime, priority, color, bursts });
updateProcess(editingProcess.id, processData);
} else {
addProcess({
id: generateId(),
@@ -95,7 +175,17 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
arrivalTime,
priority,
color,
bursts,
bursts: hasThreads ? [] : bursts,
...(hasThreads
? {
threads,
threadModel,
threadSchedulingPolicy: threadPolicy,
threadQuantum,
kernelThreadCount:
threadModel === 'MANY_TO_MANY' ? kernelThreadCount : undefined,
}
: {}),
});
}
onClose();
@@ -118,7 +208,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
{/* Basic fields */}
<div className="grid grid-cols-2 gap-3">
<div className="col-span-2 flex gap-3">
{/* Name */}
<div className="flex-1">
<label className={labelCls} htmlFor="pf-name">Name</label>
<input
@@ -132,7 +221,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
aria-label="Process name"
/>
</div>
{/* Color */}
<div>
<label className={labelCls} htmlFor="pf-color">Color</label>
<input
@@ -146,7 +234,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
</div>
</div>
{/* Arrival */}
<div>
<label className={labelCls} htmlFor="pf-arrival">Arrival Time</label>
<input
@@ -160,7 +247,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
/>
</div>
{/* Priority */}
<div>
<label className={labelCls} htmlFor="pf-priority">Priority (1–10)</label>
<input
@@ -176,7 +262,31 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
</div>
</div>
{/* Bursts editor */}
{/* Toggle threads */}
<div className="flex items-center justify-between border-t border-slate-700 pt-3">
<div>
<span className="text-xs text-slate-300 font-medium">Use Threads</span>
<p className="text-[10px] text-slate-500">Substitui bursts por threads com execução própria</p>
</div>
<button
role="switch"
aria-checked={hasThreads}
onClick={() => toggleThreads(!hasThreads)}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
hasThreads ? 'bg-blue-600' : 'bg-slate-600'
}`}
>
<span
className={`inline-block h-3.5 w-3.5 rounded-full bg-white shadow transition-transform duration-200 ${
hasThreads ? 'translate-x-4' : 'translate-x-1'
}`}
/>
</button>
</div>
{/* Tabs: Bursts / Threads */}
{!hasThreads ? (
/* ── Editor de bursts (original) ─────────────────────────── */
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-xs text-slate-400 font-medium">Bursts (CPU → I/O → CPU…)</label>
@@ -220,6 +330,22 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
))}
</div>
</div>
) : (
/* ── Editor de threads ────────────────────────────────────── */
<ThreadEditor
threads={threads}
threadModel={threadModel}
threadSchedulingPolicy={threadPolicy}
threadQuantum={threadQuantum}
kernelThreadCount={kernelThreadCount}
processColor={color}
onChange={setThreads}
onModelChange={setThreadModel}
onPolicyChange={setThreadPolicy}
onQuantumChange={setThreadQuantum}
onKernelCountChange={setKernelThreadCount}
/>
)}
{/* Validation errors */}
{errors.length > 0 && (
@@ -0,0 +1,337 @@
import { useState } from 'react';
import { Plus, Minus, Trash2, ChevronDown, ChevronUp } from 'lucide-react';
import type { BurstSegment, IntraProcessPolicy, Thread, ThreadModel } from '@core/types';
import { generateId } from '../shared/processUtils';
import { deriveThreadColor } from '../shared/threadUtils';
import IconButton from '../shared/IconButton';
interface ThreadEditorProps {
threads: Thread[];
threadModel: ThreadModel;
threadSchedulingPolicy: IntraProcessPolicy;
threadQuantum: number;
kernelThreadCount: number;
processColor: string;
onChange: (threads: Thread[]) => void;
onModelChange: (model: ThreadModel) => void;
onPolicyChange: (policy: IntraProcessPolicy) => void;
onQuantumChange: (q: number) => void;
onKernelCountChange: (k: number) => void;
}
const MODEL_OPTIONS: { value: ThreadModel; label: string; desc: string }[] = [
{
value: 'MANY_TO_ONE',
label: 'Many-to-One',
desc: 'N threads → 1 kernel. I/O bloqueia todo o processo (green threads).',
},
{
value: 'ONE_TO_ONE',
label: 'One-to-One',
desc: 'N threads → N kernel threads. Bloqueios são independentes (pthreads).',
},
{
value: 'MANY_TO_MANY',
label: 'Many-to-Many',
desc: 'N threads → M kernel threads (M ≤ N). Slots liberados em I/O.',
},
];
const POLICY_OPTIONS: { value: IntraProcessPolicy; label: string }[] = [
{ value: 'FCFS', label: 'FCFS' },
{ value: 'RR', label: 'Round Robin' },
{ value: 'PRIORITY', label: 'Priority' },
];
const inputCls = 'w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500';
const labelCls = 'block text-[10px] text-slate-400 mb-0.5';
// ── Burst editor interna de cada thread ───────────────────────────────────
const ThreadBurstEditor = ({
bursts,
onChange,
}: {
bursts: BurstSegment[];
onChange: (b: BurstSegment[]) => void;
}) => {
const addBurst = () => {
const lastType = bursts[bursts.length - 1]?.type ?? 'io';
const nextType: BurstSegment['type'] = lastType === 'cpu' ? 'io' : 'cpu';
onChange([...bursts, { type: nextType, duration: 2 }]);
};
const removeBurst = (idx: number) => {
if (bursts.length <= 1) return;
onChange(bursts.filter((_, i) => i !== idx));
};
const updateDuration = (idx: number, val: number) => {
onChange(bursts.map((b, i) => (i === idx ? { ...b, duration: Math.max(1, val) } : b)));
};
return (
<div className="space-y-1 mt-1.5">
{bursts.map((b, idx) => (
<div
key={idx}
className={`flex items-center gap-1.5 px-2 py-1 rounded border ${
b.type === 'cpu'
? 'border-emerald-700/40 bg-emerald-900/15'
: 'border-cyan-700/40 bg-cyan-900/15'
}`}
>
<span className={`text-[9px] font-bold uppercase w-6 tracking-wide shrink-0 ${b.type === 'cpu' ? 'text-emerald-400' : 'text-cyan-400'}`}>
{b.type}
</span>
<input
type="number"
min={1}
value={b.duration}
onChange={e => updateDuration(idx, Number(e.target.value))}
className="w-14 rounded bg-slate-700/60 border border-slate-600/50 text-slate-200 text-[10px] px-1.5 py-0.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
<span className="text-[9px] text-slate-500">ticks</span>
<IconButton
size="sm"
variant="ghost"
label="Remove burst"
onClick={() => removeBurst(idx)}
disabled={bursts.length <= 1}
className="ml-auto hover:text-red-400"
>
<Minus size={10} />
</IconButton>
</div>
))}
<button
onClick={addBurst}
className="flex items-center gap-1 text-[10px] text-blue-400 hover:text-blue-300 transition-colors"
>
<Plus size={10} /> burst
</button>
</div>
);
};
// ── Card de thread individual ─────────────────────────────────────────────
const ThreadCard = ({
thread,
index,
processColor,
onUpdate,
onRemove,
canRemove,
}: {
thread: Thread;
index: number;
processColor: string;
onUpdate: (t: Thread) => void;
onRemove: () => void;
canRemove: boolean;
}) => {
const [expanded, setExpanded] = useState(false);
const color = deriveThreadColor(processColor, index);
return (
<div className="rounded-md border border-slate-600 bg-slate-800/50 overflow-hidden">
<div className="flex items-center gap-2 px-2.5 py-1.5">
<span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: color }} />
<input
type="text"
maxLength={8}
value={thread.name}
onChange={e => onUpdate({ ...thread, name: e.target.value })}
className="flex-1 bg-transparent text-slate-200 text-xs font-medium focus:outline-none focus:ring-1 focus:ring-blue-500 rounded px-1"
aria-label={`Thread ${index + 1} name`}
/>
<span className="text-[9px] text-slate-500 font-mono shrink-0">T{index + 1}</span>
{/* Priority */}
<input
type="number"
min={1}
max={10}
title="Priority (1=high, 10=low)"
value={thread.priority ?? 5}
onChange={e => onUpdate({ ...thread, priority: Math.min(10, Math.max(1, Number(e.target.value))) })}
className="w-10 rounded bg-slate-700 border border-slate-600 text-slate-200 text-[10px] px-1 py-0.5 focus:outline-none focus:ring-1 focus:ring-blue-500 text-center"
aria-label={`Thread ${index + 1} priority`}
/>
<button
onClick={() => setExpanded(v => !v)}
className="text-slate-500 hover:text-slate-300 transition-colors"
aria-label={expanded ? 'Collapse' : 'Expand'}
>
{expanded ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
</button>
<IconButton
size="sm"
variant="ghost"
label="Remove thread"
onClick={onRemove}
disabled={!canRemove}
className="hover:text-red-400"
>
<Trash2 size={11} />
</IconButton>
</div>
{expanded && (
<div className="px-2.5 pb-2 border-t border-slate-700/50">
<ThreadBurstEditor
bursts={thread.bursts}
onChange={bursts => onUpdate({ ...thread, bursts })}
/>
</div>
)}
</div>
);
};
// ── ThreadEditor principal ────────────────────────────────────────────────
const ThreadEditor = ({
threads,
threadModel,
threadSchedulingPolicy,
threadQuantum,
kernelThreadCount,
processColor,
onChange,
onModelChange,
onPolicyChange,
onQuantumChange,
onKernelCountChange,
}: ThreadEditorProps) => {
const addThread = () => {
const newThread: Thread = {
tid: `t_${generateId()}`,
name: `T${threads.length + 1}`,
priority: 5,
bursts: [{ type: 'cpu', duration: 3 }],
};
onChange([...threads, newThread]);
};
const updateThread = (idx: number, updated: Thread) => {
onChange(threads.map((t, i) => (i === idx ? updated : t)));
};
const removeThread = (idx: number) => {
onChange(threads.filter((_, i) => i !== idx));
};
const selectedModel = MODEL_OPTIONS.find(m => m.value === threadModel);
return (
<div className="space-y-3">
{/* Modelo de threads */}
<div>
<label className={labelCls}>Thread Model</label>
<select
value={threadModel}
onChange={e => onModelChange(e.target.value as ThreadModel)}
className="w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
aria-label="Thread model"
>
{MODEL_OPTIONS.map(m => (
<option key={m.value} value={m.value}>{m.label}</option>
))}
</select>
{selectedModel && (
<p className="text-[10px] text-slate-500 mt-1">{selectedModel.desc}</p>
)}
</div>
{/* Kernel thread count — apenas MANY_TO_MANY */}
{threadModel === 'MANY_TO_MANY' && (
<div>
<label className={labelCls}>
Kernel Threads (M ≤ {threads.length || '?'})
</label>
<input
type="number"
min={1}
max={Math.max(1, threads.length)}
value={kernelThreadCount}
onChange={e =>
onKernelCountChange(Math.min(threads.length || 1, Math.max(1, Number(e.target.value))))
}
className={inputCls}
aria-label="Kernel thread count"
/>
</div>
)}
{/* Política intra-processo */}
<div className="grid grid-cols-2 gap-2">
<div>
<label className={labelCls}>Intra-process Policy</label>
<select
value={threadSchedulingPolicy}
onChange={e => onPolicyChange(e.target.value as IntraProcessPolicy)}
className="w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
aria-label="Thread scheduling policy"
>
{POLICY_OPTIONS.map(p => (
<option key={p.value} value={p.value}>{p.label}</option>
))}
</select>
</div>
{threadSchedulingPolicy === 'RR' && (
<div>
<label className={labelCls}>Thread Quantum</label>
<input
type="number"
min={1}
value={threadQuantum}
onChange={e => onQuantumChange(Math.max(1, Number(e.target.value)))}
className={inputCls}
aria-label="Thread quantum"
/>
</div>
)}
</div>
{/* Lista de threads */}
<div>
<div className="flex items-center justify-between mb-1.5">
<label className="text-[10px] text-slate-400 font-medium">
Threads ({threads.length})
</label>
<button
onClick={addThread}
disabled={threads.length >= 8}
className="flex items-center gap-1 text-[10px] text-blue-400 hover:text-blue-300 disabled:text-slate-600 disabled:cursor-not-allowed transition-colors"
aria-label="Add thread"
>
<Plus size={11} /> Thread
</button>
</div>
<div className="space-y-1.5">
{threads.map((thread, idx) => (
<ThreadCard
key={thread.tid}
thread={thread}
index={idx}
processColor={processColor}
onUpdate={updated => updateThread(idx, updated)}
onRemove={() => removeThread(idx)}
canRemove={threads.length > 1}
/>
))}
</div>
{threads.length === 0 && (
<div className="text-center py-4 text-[10px] text-slate-500">
Nenhuma thread. Clique em "+ Thread" para adicionar.
</div>
)}
</div>
</div>
);
};
export default ThreadEditor;
@@ -3,14 +3,15 @@ import { useProcessStore } from '../../store/processStore';
const ALGORITHM_OPTIONS: { value: SchedulingAlgorithm; label: string; disabled?: boolean }[] = [
{ value: 'FCFS', label: 'FCFS — First Come First Served' },
{ value: 'SJF_NP', label: 'SJF — Shortest Job First (NP)' },
{ value: 'SJF_P', label: 'SRTF — Shortest Remaining Time (P)' },
{ value: 'SJF_NP', label: 'SJF — Shortest Job First (Non-preemptive)' },
{ value: 'SJF_P', label: 'SRTF — Shortest Remaining Time (Preemptive)' },
{ value: 'RR', label: 'RR — Round Robin' },
{ value: 'PRIORITY_NP',label: 'Priority (Non-preemptive)' },
{ value: 'PRIORITY_P', label: 'Priority (Preemptive)' },
{ value: 'PRIORITY_RR',label: 'Priority Round Robin (real-time)' },
{ value: 'HRRN', label: 'HRRN — Highest Response Ratio Next' },
{ value: 'MULTILEVEL', label: 'Multilevel Queue (MLQ)' },
{ value: 'MULTILEVEL', label: 'MLQ — Multilevel Queue' },
{ value: 'MLFQ', label: 'MLFQ — Multilevel Feedback Queue' },
];
const AlgorithmSelector = () => {
@@ -1,6 +1,7 @@
import { Plus, Trash2 } from 'lucide-react';
import { useProcessStore } from '../../store/processStore';
import type { MlqQueueDef, SchedulingAlgorithm } from '@core/types';
import type { MlfqLevelDef, MlqQueueDef, SchedulingAlgorithm } from '@core/types';
import { MLFQAlgorithm } from '@core/algorithms/mlfq';
// Algorithms that require a global quantum parameter
const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'PRIORITY_RR']);
@@ -9,7 +10,7 @@ const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'PRIORITY_RR']);
const AGING_ALGORITHMS = new Set<SchedulingAlgorithm>(['PRIORITY_NP', 'PRIORITY_P', 'PRIORITY_RR']);
// Algorithms that are preemptive by definition (used for read-only info only)
const PREEMPTIVE_BY_DESIGN = new Set<SchedulingAlgorithm>(['SJF_P', 'RR', 'PRIORITY_P', 'PRIORITY_RR', 'MULTILEVEL']);
const PREEMPTIVE_BY_DESIGN = new Set<SchedulingAlgorithm>(['SJF_P', 'RR', 'PRIORITY_P', 'PRIORITY_RR', 'MULTILEVEL', 'MLFQ']);
const inputCls = 'w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-sm px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500';
const labelCls = 'block text-xs text-slate-400 mb-1';
@@ -153,6 +154,94 @@ const MlqQueuesConfig = ({
);
};
const MlfqLevelsConfig = ({
levels,
boostInterval,
onLevelsChange,
onBoostChange,
}: {
levels: MlfqLevelDef[];
boostInterval: number;
onLevelsChange: (levels: MlfqLevelDef[]) => void;
onBoostChange: (v: number) => void;
}) => {
const update = (idx: number, quantum: number) => {
onLevelsChange(levels.map((l, i) => (i === idx ? { quantum } : l)));
};
const addLevel = () => {
if (levels.length >= 5) return;
onLevelsChange([...levels, { quantum: levels[levels.length - 1]!.quantum * 2 }]);
};
const removeLevel = (idx: number) => {
if (levels.length <= 1) return;
onLevelsChange(levels.filter((_, i) => i !== idx));
};
return (
<div className="space-y-2 pt-1" data-testid="mlfq-levels-config">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">MLFQ Levels (decreasing priority)</span>
<button
onClick={addLevel}
disabled={levels.length >= 5}
className="flex items-center gap-1 text-[10px] text-blue-400 hover:text-blue-300 disabled:text-slate-600 disabled:cursor-not-allowed transition-colors"
aria-label="Add MLFQ level"
>
<Plus size={11} /> Level
</button>
</div>
{levels.map((l, idx) => (
<div
key={idx}
className="rounded-md border border-slate-600 bg-slate-800/60 p-2 space-y-1.5"
data-testid={`mlfq-level-${idx}`}
>
<div className="flex items-center justify-between">
<span className="text-[10px] text-slate-400 font-semibold uppercase tracking-wide">
Level {idx} {idx === 0 ? '(highest)' : idx === levels.length - 1 ? '(lowest)' : ''}
</span>
<button
onClick={() => removeLevel(idx)}
disabled={levels.length <= 1}
className="text-slate-500 hover:text-red-400 disabled:text-slate-700 disabled:cursor-not-allowed transition-colors"
aria-label={`Remove level ${idx}`}
>
<Trash2 size={11} />
</button>
</div>
<div>
<label className="block text-[10px] text-slate-500 mb-0.5">Quantum (ticks)</label>
<input
type="number"
min={1}
value={l.quantum}
onChange={e => update(idx, Math.max(1, Number(e.target.value)))}
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
aria-label={`Level ${idx}: quantum`}
/>
</div>
</div>
))}
<div>
<label className="block text-[10px] text-slate-500 mb-0.5">
Boost Interval (ticks, 0 = disabled)
</label>
<input
type="number"
min={0}
value={boostInterval}
onChange={e => onBoostChange(Math.max(0, Number(e.target.value)))}
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
aria-label="MLFQ boost interval"
data-testid="mlfq-boost-interval"
/>
</div>
</div>
);
};
const SchedulerConfig = () => {
const config = useProcessStore(s => s.config);
const setConfig = useProcessStore(s => s.setConfig);
@@ -250,6 +339,16 @@ const SchedulerConfig = () => {
onChange={queues => setConfig({ mlqQueues: queues })}
/>
)}
{/* MLFQ Level Configuration */}
{algo === 'MLFQ' && (
<MlfqLevelsConfig
levels={config.mlfqLevels ?? MLFQAlgorithm.DEFAULT_LEVELS}
boostInterval={config.mlfqBoostInterval ?? 0}
onLevelsChange={levels => setConfig({ mlfqLevels: levels })}
onBoostChange={v => setConfig({ mlfqBoostInterval: v })}
/>
)}
</div>
);
};
+61
View File
@@ -0,0 +1,61 @@
/**
* Deriva uma cor para uma thread a partir da cor base do processo.
* Desloca o matiz (hue) em 35° por índice de thread para garantir
* cores visualmente distintas dentro do mesmo processo.
*/
export function deriveThreadColor(processColor: string, threadIndex: number): string {
const [h, s, l] = hexToHsl(processColor);
const newH = (h + threadIndex * 40) % 360;
const newL = Math.min(80, Math.max(35, l + (threadIndex % 2 === 0 ? 8 : -8)));
return hslToHex(newH, Math.min(100, s + 10), newL);
}
function hexToHsl(hex: string): [number, number, number] {
const r = parseInt(hex.slice(1, 3), 16) / 255;
const g = parseInt(hex.slice(3, 5), 16) / 255;
const b = parseInt(hex.slice(5, 7), 16) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h = 0;
let s = 0;
const l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
case g: h = ((b - r) / d + 2) / 6; break;
case b: h = ((r - g) / d + 4) / 6; break;
}
}
return [h * 360, s * 100, l * 100];
}
function hslToHex(h: number, s: number, l: number): string {
h /= 360; s /= 100; l /= 100;
let r: number, g: number, b: number;
if (s === 0) {
r = g = b = l;
} else {
const hue2rgb = (p: number, q: number, t: number) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
const toHex = (x: number) =>
Math.round(x * 255).toString(16).padStart(2, "0");
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
+444
View File
@@ -0,0 +1,444 @@
import { describe, it, expect } from "vitest";
import { SimulationEngine } from "../engine";
import type { Process, SchedulerConfig } from "../types";
const BASE_CONFIG: SchedulerConfig = {
algorithm: "FCFS",
quantum: 10,
contextSwitchTime: 0,
isPreemptive: false,
agingEnabled: false,
agingInterval: 5,
};
function makeThreadedProcess(
overrides: Partial<Process> = {},
): Process {
return {
pid: 1,
id: "p1",
name: "P1",
arrivalTime: 0,
priority: 5,
color: "#4FC3F7",
bursts: [],
threadModel: "ONE_TO_ONE",
threadSchedulingPolicy: "FCFS",
threads: [
{
tid: "t1",
name: "T1",
bursts: [{ type: "cpu", duration: 3 }],
},
{
tid: "t2",
name: "T2",
bursts: [{ type: "cpu", duration: 2 }],
},
],
...overrides,
};
}
// ── ONE_TO_ONE ─────────────────────────────────────────────────────────────
describe("Engine — ONE_TO_ONE básico", () => {
it("processo termina quando todas as threads terminam", () => {
const p = makeThreadedProcess({ threadModel: "ONE_TO_ONE" });
const engine = new SimulationEngine([p], BASE_CONFIG);
engine.runAll();
expect(engine.isFinished).toBe(true);
const rt = engine.runtimeStates[0]!;
expect(rt.state).toBe("Terminated");
expect(rt.finishTick).not.toBeNull();
});
it("runtimes de threads são gerados corretamente", () => {
const p = makeThreadedProcess({ threadModel: "ONE_TO_ONE" });
const engine = new SimulationEngine([p], BASE_CONFIG);
engine.runAll();
const threadRts = engine.threadRuntimeStates;
expect(threadRts).toHaveLength(2);
for (const trt of threadRts) {
expect(trt.state).toBe("Terminated");
expect(trt.cpuTime).toBeGreaterThan(0);
}
});
it("cpuTime total do processo = soma dos cpuTimes das threads", () => {
const p = makeThreadedProcess({ threadModel: "ONE_TO_ONE" });
const engine = new SimulationEngine([p], BASE_CONFIG);
engine.runAll();
const pRt = engine.runtimeStates[0]!;
const threadTotal = engine.threadRuntimeStates.reduce(
(sum, trt) => sum + trt.cpuTime,
0,
);
expect(pRt.cpuTime).toBe(threadTotal);
});
it("processHasThreads retorna true para processo com threads", () => {
const p = makeThreadedProcess();
const engine = new SimulationEngine([p], BASE_CONFIG);
expect(engine.processHasThreads("p1")).toBe(true);
});
it("processo sem threads não é afetado", () => {
const p: Process = {
pid: 1,
id: "p1",
name: "P1",
arrivalTime: 0,
priority: 5,
color: "#aaa",
bursts: [{ type: "cpu", duration: 4 }],
};
const engine = new SimulationEngine([p], BASE_CONFIG);
engine.runAll();
expect(engine.isFinished).toBe(true);
expect(engine.processHasThreads("p1")).toBe(false);
});
it("ONE_TO_ONE: processo com thread em I/O pode ter outra thread rodando", () => {
const p = makeThreadedProcess({
threadModel: "ONE_TO_ONE",
threadSchedulingPolicy: "FCFS",
threads: [
{
tid: "t1",
name: "T1",
bursts: [
{ type: "cpu", duration: 2 },
{ type: "io", duration: 3 },
{ type: "cpu", duration: 1 },
],
},
{
tid: "t2",
name: "T2",
bursts: [{ type: "cpu", duration: 4 }],
},
],
});
const engine = new SimulationEngine([p], BASE_CONFIG);
const ticks = engine.runAll();
// O processo NÃO deve ir para Waiting enquanto T2 está ready/running
const waitingTicks = ticks.filter(
(tick) => tick.states["p1"] === "Waiting",
);
// Pode ter algum tick Waiting quando AMBAS as threads estão em I/O,
// mas aqui T2 não tem I/O, então o processo nunca deve ficar Waiting
expect(waitingTicks.length).toBe(0);
expect(engine.isFinished).toBe(true);
});
it("ONE_TO_ONE: processo vai para Waiting apenas quando todas as threads estão em I/O", () => {
const p = makeThreadedProcess({
threadModel: "ONE_TO_ONE",
threads: [
{
tid: "t1",
name: "T1",
bursts: [
{ type: "cpu", duration: 1 },
{ type: "io", duration: 3 },
{ type: "cpu", duration: 1 },
],
},
{
tid: "t2",
name: "T2",
bursts: [
{ type: "cpu", duration: 1 },
{ type: "io", duration: 3 },
{ type: "cpu", duration: 1 },
],
},
],
});
const engine = new SimulationEngine([p], BASE_CONFIG);
const ticks = engine.runAll();
// Deve haver ticks em que o processo está Waiting (ambas as threads em I/O)
const waitingTicks = ticks.filter(
(tick) => tick.states["p1"] === "Waiting",
);
expect(waitingTicks.length).toBeGreaterThan(0);
expect(engine.isFinished).toBe(true);
});
});
// ── MANY_TO_ONE ────────────────────────────────────────────────────────────
describe("Engine — MANY_TO_ONE", () => {
it("processo vai para Waiting quando qualquer thread está em I/O", () => {
const p = makeThreadedProcess({
threadModel: "MANY_TO_ONE",
threadSchedulingPolicy: "FCFS",
threads: [
{
tid: "t1",
name: "T1",
bursts: [
{ type: "cpu", duration: 2 },
{ type: "io", duration: 3 },
{ type: "cpu", duration: 1 },
],
},
{
tid: "t2",
name: "T2",
// Esta thread ficaria pronta enquanto T1 está em I/O,
// mas em MANY_TO_ONE o processo bloqueia inteiro
bursts: [{ type: "cpu", duration: 3 }],
},
],
});
const engine = new SimulationEngine([p], BASE_CONFIG);
const ticks = engine.runAll();
// Deve haver ticks em Waiting (T1 em I/O bloqueia o processo)
const waitingTicks = ticks.filter(
(tick) => tick.states["p1"] === "Waiting",
);
expect(waitingTicks.length).toBeGreaterThan(0);
expect(engine.isFinished).toBe(true);
});
it("processo termina quando todas as threads terminam", () => {
const p = makeThreadedProcess({ threadModel: "MANY_TO_ONE" });
const engine = new SimulationEngine([p], BASE_CONFIG);
engine.runAll();
expect(engine.isFinished).toBe(true);
expect(engine.runtimeStates[0]!.state).toBe("Terminated");
const allTerminated = engine.threadRuntimeStates.every(
(trt) => trt.state === "Terminated",
);
expect(allTerminated).toBe(true);
});
it("MANY_TO_ONE tem mais ticks em Waiting que ONE_TO_ONE para mesmo workload", () => {
const threads = [
{
tid: "t1",
name: "T1",
bursts: [
{ type: "cpu" as const, duration: 2 },
{ type: "io" as const, duration: 4 },
{ type: "cpu" as const, duration: 1 },
],
},
{
tid: "t2",
name: "T2",
bursts: [{ type: "cpu" as const, duration: 3 }],
},
];
const m2o = makeThreadedProcess({ threadModel: "MANY_TO_ONE", threads });
const o2o = makeThreadedProcess({ threadModel: "ONE_TO_ONE", threads });
const engine1 = new SimulationEngine([m2o], BASE_CONFIG);
const ticks1 = engine1.runAll();
const engine2 = new SimulationEngine([o2o], BASE_CONFIG);
const ticks2 = engine2.runAll();
const waitingM2O = ticks1.filter(
(t) => t.states["p1"] === "Waiting",
).length;
const waitingO2O = ticks2.filter(
(t) => t.states["p1"] === "Waiting",
).length;
// MANY_TO_ONE bloqueia o processo durante I/O da T1 mesmo com T2 pronta
expect(waitingM2O).toBeGreaterThanOrEqual(waitingO2O);
});
});
// ── MANY_TO_MANY ───────────────────────────────────────────────────────────
describe("Engine — MANY_TO_MANY", () => {
it("respeita limite de kernel threads (M=2 para 3 user threads)", () => {
const p: Process = {
pid: 1,
id: "p1",
name: "App",
arrivalTime: 0,
priority: 5,
color: "#CE93D8",
bursts: [],
threadModel: "MANY_TO_MANY",
kernelThreadCount: 2,
threadSchedulingPolicy: "FCFS",
threads: [
{ tid: "t1", name: "T1", bursts: [{ type: "cpu", duration: 4 }] },
{ tid: "t2", name: "T2", bursts: [{ type: "cpu", duration: 3 }] },
{ tid: "t3", name: "T3", bursts: [{ type: "cpu", duration: 2 }] },
],
};
const engine = new SimulationEngine([p], BASE_CONFIG);
const ticks = engine.runAll();
expect(engine.isFinished).toBe(true);
// Todos os threads devem ter terminado
const allTerminated = engine.threadRuntimeStates.every(
(trt) => trt.state === "Terminated",
);
expect(allTerminated).toBe(true);
// Deve haver kernel wait info em algum tick
const hasKernelWait = ticks.some(
(tick) =>
tick.kernelWaitingThreads !== undefined &&
Object.keys(tick.kernelWaitingThreads).length > 0,
);
expect(hasKernelWait).toBe(true);
});
it("termina corretamente com M=N (todos têm kernel thread)", () => {
const p: Process = {
pid: 1,
id: "p1",
name: "App",
arrivalTime: 0,
priority: 5,
color: "#CE93D8",
bursts: [],
threadModel: "MANY_TO_MANY",
kernelThreadCount: 3, // igual ao número de threads
threadSchedulingPolicy: "FCFS",
threads: [
{ tid: "t1", name: "T1", bursts: [{ type: "cpu", duration: 2 }] },
{ tid: "t2", name: "T2", bursts: [{ type: "cpu", duration: 3 }] },
{ tid: "t3", name: "T3", bursts: [{ type: "cpu", duration: 1 }] },
],
};
const engine = new SimulationEngine([p], BASE_CONFIG);
engine.runAll();
expect(engine.isFinished).toBe(true);
});
});
// ── Snapshot de threads ────────────────────────────────────────────────────
describe("Engine — snapshot com threads", () => {
it("threadStates presentes no snapshot quando processo tem threads", () => {
const p = makeThreadedProcess({ threadModel: "ONE_TO_ONE" });
const engine = new SimulationEngine([p], BASE_CONFIG);
const tick = engine.step();
expect(tick.threadStates).toBeDefined();
expect(tick.threadStates!["p1"]).toBeDefined();
expect(Object.keys(tick.threadStates!["p1"]!)).toContain("t1");
});
it("cpuThreads indica thread ativa no processo", () => {
const p = makeThreadedProcess({ threadModel: "ONE_TO_ONE" });
const engine = new SimulationEngine([p], BASE_CONFIG);
engine.step(); // tick 0 — despacha processo e thread
const tick = engine.step(); // tick 1 — thread em execução
expect(tick.cpuThreads).toBeDefined();
expect(tick.cpuThreads!["p1"]).not.toBeNull();
});
});
// ── Múltiplos processos com e sem threads ─────────────────────────────────
describe("Engine — mix de processos com e sem threads", () => {
it("processa corretamente processos com e sem threads simultaneamente", () => {
const pThreaded: Process = {
pid: 1,
id: "p1",
name: "Threaded",
arrivalTime: 0,
priority: 5,
color: "#4FC3F7",
bursts: [],
threadModel: "ONE_TO_ONE",
threads: [
{ tid: "t1", name: "T1", bursts: [{ type: "cpu", duration: 3 }] },
],
};
const pSimple: Process = {
pid: 2,
id: "p2",
name: "Simple",
arrivalTime: 0,
priority: 5,
color: "#81C784",
bursts: [{ type: "cpu", duration: 4 }],
};
const engine = new SimulationEngine([pThreaded, pSimple], BASE_CONFIG);
engine.runAll();
expect(engine.isFinished).toBe(true);
const rts = engine.runtimeStates;
expect(rts.every((rt) => rt.state === "Terminated")).toBe(true);
});
});
// ── Política intra-processo RR ─────────────────────────────────────────────
describe("Engine — RR intra-processo", () => {
it("rotaciona threads dentro do processo com quantum 2", () => {
const p = makeThreadedProcess({
threadModel: "ONE_TO_ONE",
threadSchedulingPolicy: "RR",
threadQuantum: 2,
threads: [
{ tid: "t1", name: "T1", bursts: [{ type: "cpu", duration: 6 }] },
{ tid: "t2", name: "T2", bursts: [{ type: "cpu", duration: 4 }] },
],
});
const engine = new SimulationEngine([p], BASE_CONFIG);
const ticks = engine.runAll();
// Verifica que ambas as threads ficaram ativas em ticks diferentes
const t1Active = ticks.filter(
(tick) => tick.cpuThreads?.["p1"] === "t1",
).length;
const t2Active = ticks.filter(
(tick) => tick.cpuThreads?.["p1"] === "t2",
).length;
expect(t1Active).toBeGreaterThan(0);
expect(t2Active).toBeGreaterThan(0);
expect(engine.isFinished).toBe(true);
});
});
// ── Política intra-processo PRIORITY ──────────────────────────────────────
describe("Engine — Priority intra-processo", () => {
it("thread de maior prioridade roda primeiro (menor número = maior prio)", () => {
const p = makeThreadedProcess({
threadModel: "ONE_TO_ONE",
threadSchedulingPolicy: "PRIORITY",
threads: [
{
tid: "t1",
name: "Low",
priority: 8,
bursts: [{ type: "cpu", duration: 3 }],
},
{
tid: "t2",
name: "High",
priority: 1,
bursts: [{ type: "cpu", duration: 2 }],
},
],
});
const engine = new SimulationEngine([p], BASE_CONFIG);
const firstTick = engine.step();
// t2 (prioridade 1) deve ser selecionada primeiro
expect(firstTick.cpuThreads?.["p1"]).toBe("t2");
});
});
+396
View File
@@ -0,0 +1,396 @@
import { describe, it, expect } from "vitest";
import { MLFQAlgorithm } from "../algorithms/mlfq";
import { SimulationEngine } from "../engine";
import type { MlfqLevelDef, Process, ProcessRuntime, SchedulerConfig } from "../types";
const mlfq = new MLFQAlgorithm();
// ----------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------
const DEFAULT_LEVELS: MlfqLevelDef[] = [
{ quantum: 2 },
{ quantum: 4 },
{ quantum: 8 },
];
function makeRt(
id: string,
opts: {
arrivalTick?: number;
quantumRemaining?: number;
mlfqLevel?: number;
waitingTime?: number;
remainingBurst?: number;
} = {},
): ProcessRuntime {
return {
processId: id,
state: "Ready",
remainingBurst: opts.remainingBurst ?? 10,
burstIndex: 0,
arrivalTick: opts.arrivalTick ?? 0,
startTick: null,
finishTick: null,
waitingTime: opts.waitingTime ?? 0,
cpuTime: 0,
ioTime: 0,
responseTime: null,
quantumRemaining: opts.quantumRemaining ?? 2,
currentPriority: 5,
mlfqLevel: opts.mlfqLevel,
};
}
const cfg = (overrides: Partial<SchedulerConfig> = {}): SchedulerConfig => ({
algorithm: "MLFQ",
quantum: 2,
contextSwitchTime: 0,
isPreemptive: false,
agingEnabled: false,
agingInterval: 5,
mlfqLevels: DEFAULT_LEVELS,
mlfqBoostInterval: 0,
...overrides,
});
const p = (
id: string,
arrival: number,
cpuDuration: number,
): Process => ({
id,
name: id,
arrivalTime: arrival,
priority: 5,
color: "#fff",
bursts: [{ type: "cpu", duration: cpuDuration }],
});
const pWithIo = (
id: string,
arrival: number,
cpu1: number,
io: number,
cpu2: number,
): Process => ({
id,
name: id,
arrivalTime: arrival,
priority: 5,
color: "#fff",
bursts: [
{ type: "cpu", duration: cpu1 },
{ type: "io", duration: io },
{ type: "cpu", duration: cpu2 },
],
});
// ----------------------------------------------------------------
// Unit: select() — initialization
// ----------------------------------------------------------------
describe("MLFQ select() — initialization", () => {
it("returns null for empty queue", () => {
expect(mlfq.select([], cfg())).toBeNull();
});
it("assigns level 0 to a new process (mlfqLevel undefined)", () => {
const rt = makeRt("p1", { mlfqLevel: undefined });
mlfq.select([rt], cfg());
expect(rt.mlfqLevel).toBe(0);
});
it("does not re-initialize a process that already has mlfqLevel 0", () => {
const rt = makeRt("p1", { mlfqLevel: 0, quantumRemaining: 2 });
mlfq.select([rt], cfg());
expect(rt.mlfqLevel).toBe(0);
});
});
// ----------------------------------------------------------------
// Unit: select() — demotion
// ----------------------------------------------------------------
describe("MLFQ select() — demotion", () => {
it("demotes a quantum-expired process (quantumRemaining <= 0) from level 0 to 1", () => {
const rt = makeRt("p1", { mlfqLevel: 0, quantumRemaining: 0 });
mlfq.select([rt], cfg());
expect(rt.mlfqLevel).toBe(1);
});
it("demotes from level 1 to level 2", () => {
const rt = makeRt("p1", { mlfqLevel: 1, quantumRemaining: 0 });
mlfq.select([rt], cfg());
expect(rt.mlfqLevel).toBe(2);
});
it("caps demotion at the last level (no demotion beyond level N-1)", () => {
const rt = makeRt("p1", { mlfqLevel: 2, quantumRemaining: 0 });
mlfq.select([rt], cfg());
expect(rt.mlfqLevel).toBe(2);
});
it("sets quantumRemaining sentinel to 1 after demotion", () => {
const rt = makeRt("p1", { mlfqLevel: 0, quantumRemaining: 0 });
mlfq.select([rt], cfg());
expect(rt.quantumRemaining).toBe(1);
});
it("does NOT demote a process returning from I/O (quantumRemaining > 0)", () => {
const rt = makeRt("p1", { mlfqLevel: 0, quantumRemaining: 1 });
mlfq.select([rt], cfg());
expect(rt.mlfqLevel).toBe(0);
});
});
// ----------------------------------------------------------------
// Unit: select() — priority selection within/across levels
// ----------------------------------------------------------------
describe("MLFQ select() — level-based selection", () => {
it("selects the process at the highest-priority level (lowest index)", () => {
const low = makeRt("lo", { mlfqLevel: 2, quantumRemaining: 2 });
const mid = makeRt("mid", { mlfqLevel: 1, quantumRemaining: 2 });
const high = makeRt("hi", { mlfqLevel: 0, quantumRemaining: 2 });
expect(mlfq.select([low, mid, high], cfg())?.processId).toBe("hi");
expect(mlfq.select([mid, low], cfg())?.processId).toBe("mid");
});
it("uses FIFO (arrivalTick) within the same level", () => {
const a = makeRt("a", { mlfqLevel: 0, arrivalTick: 5, quantumRemaining: 2 });
const b = makeRt("b", { mlfqLevel: 0, arrivalTick: 2, quantumRemaining: 2 });
const c = makeRt("c", { mlfqLevel: 0, arrivalTick: 8, quantumRemaining: 2 });
expect(mlfq.select([a, b, c], cfg())?.processId).toBe("b");
});
it("uses processId as tiebreak when arrivalTick is equal", () => {
const a = makeRt("z", { mlfqLevel: 1, arrivalTick: 3, quantumRemaining: 2 });
const b = makeRt("a", { mlfqLevel: 1, arrivalTick: 3, quantumRemaining: 2 });
expect(mlfq.select([a, b], cfg())?.processId).toBe("a");
});
});
// ----------------------------------------------------------------
// Unit: select() — priority boost
// ----------------------------------------------------------------
describe("MLFQ select() — priority boost", () => {
it("does NOT boost when boostInterval is 0 (disabled)", () => {
const rt = makeRt("p1", { mlfqLevel: 2, waitingTime: 100, quantumRemaining: 2 });
mlfq.select([rt], cfg({ mlfqBoostInterval: 0 }));
expect(rt.mlfqLevel).toBe(2);
});
it("boosts a process to level 0 when waitingTime >= boostInterval", () => {
const rt = makeRt("p1", { mlfqLevel: 2, waitingTime: 10, quantumRemaining: 2 });
mlfq.select([rt], cfg({ mlfqBoostInterval: 10 }));
expect(rt.mlfqLevel).toBe(0);
});
it("does NOT boost a process that hasn't waited long enough", () => {
const rt = makeRt("p1", { mlfqLevel: 2, waitingTime: 9, quantumRemaining: 2 });
mlfq.select([rt], cfg({ mlfqBoostInterval: 10 }));
expect(rt.mlfqLevel).toBe(2);
});
it("does NOT boost a process already at level 0", () => {
const rt = makeRt("p1", { mlfqLevel: 0, waitingTime: 100, quantumRemaining: 2 });
mlfq.select([rt], cfg({ mlfqBoostInterval: 5 }));
expect(rt.mlfqLevel).toBe(0);
});
});
// ----------------------------------------------------------------
// Unit: shouldPreempt()
// ----------------------------------------------------------------
describe("MLFQ shouldPreempt()", () => {
it("preempts when candidate is at a higher-priority level (lower index)", () => {
const running = makeRt("r", { mlfqLevel: 2 });
const candidate = makeRt("c", { mlfqLevel: 0 });
expect(mlfq.shouldPreempt(running, candidate, cfg())).toBe(true);
});
it("preempts from level 1 by level 0", () => {
const running = makeRt("r", { mlfqLevel: 1 });
const candidate = makeRt("c", { mlfqLevel: 0 });
expect(mlfq.shouldPreempt(running, candidate, cfg())).toBe(true);
});
it("does NOT preempt when candidate is at the same level", () => {
const running = makeRt("r", { mlfqLevel: 1 });
const candidate = makeRt("c", { mlfqLevel: 1 });
expect(mlfq.shouldPreempt(running, candidate, cfg())).toBe(false);
});
it("does NOT preempt when candidate is at a lower-priority level", () => {
const running = makeRt("r", { mlfqLevel: 0 });
const candidate = makeRt("c", { mlfqLevel: 2 });
expect(mlfq.shouldPreempt(running, candidate, cfg())).toBe(false);
});
});
// ----------------------------------------------------------------
// Unit: isQuantumExpired()
// ----------------------------------------------------------------
describe("MLFQ isQuantumExpired()", () => {
it("returns true when quantumRemaining <= 0", () => {
expect(mlfq.isQuantumExpired(makeRt("p", { quantumRemaining: 0 }), cfg())).toBe(true);
});
it("returns true when quantumRemaining is negative", () => {
expect(mlfq.isQuantumExpired(makeRt("p", { quantumRemaining: -1 }), cfg())).toBe(true);
});
it("returns false when quantumRemaining > 0", () => {
expect(mlfq.isQuantumExpired(makeRt("p", { quantumRemaining: 1 }), cfg())).toBe(false);
});
});
// ----------------------------------------------------------------
// Unit: getQuantumForProcess()
// ----------------------------------------------------------------
describe("MLFQ getQuantumForProcess()", () => {
it("returns the quantum for level 0 (default: 2)", () => {
const rt = makeRt("p", { mlfqLevel: 0 });
expect(mlfq.getQuantumForProcess(rt, cfg())).toBe(2);
});
it("returns the quantum for level 1 (default: 4)", () => {
const rt = makeRt("p", { mlfqLevel: 1 });
expect(mlfq.getQuantumForProcess(rt, cfg())).toBe(4);
});
it("returns the quantum for level 2 (default: 8)", () => {
const rt = makeRt("p", { mlfqLevel: 2 });
expect(mlfq.getQuantumForProcess(rt, cfg())).toBe(8);
});
it("falls back to config.quantum when mlfqLevel is undefined", () => {
const rt = makeRt("p", { mlfqLevel: undefined });
expect(mlfq.getQuantumForProcess(rt, cfg({ quantum: 3 }))).toBe(2); // level 0 quantum
});
it("respects custom level quantums", () => {
const custom = cfg({ mlfqLevels: [{ quantum: 1 }, { quantum: 3 }, { quantum: 7 }] });
expect(mlfq.getQuantumForProcess(makeRt("p", { mlfqLevel: 0 }), custom)).toBe(1);
expect(mlfq.getQuantumForProcess(makeRt("p", { mlfqLevel: 1 }), custom)).toBe(3);
expect(mlfq.getQuantumForProcess(makeRt("p", { mlfqLevel: 2 }), custom)).toBe(7);
});
});
// ----------------------------------------------------------------
// Integration — CT-MLFQ-01: single process demotion sequence
// ----------------------------------------------------------------
describe("MLFQ integration — CT-MLFQ-01: demotion sequence", () => {
it("single process is demoted through levels, with per-level quantum (CT-MLFQ-01)", () => {
// P1: CPU burst of 14 ticks
// Level 0: quantum=2 → runs ticks 0-1, demoted to level 1
// Level 1: quantum=4 → runs ticks 2-5, demoted to level 2
// Level 2: quantum=8 → runs ticks 6-13 (burst completes)
const processes = [p("p1", 0, 14)];
const engine = new SimulationEngine(processes, cfg());
const ticks = engine.runAll();
const cpu = ticks.map((t) => t.cpuProcess);
expect(cpu).toHaveLength(14);
expect(cpu.every((id) => id === "p1")).toBe(true);
// Verify total cpuTime
const rt = engine.runtimeStates.find((r) => r.processId === "p1")!;
expect(rt.cpuTime).toBe(14);
expect(rt.state).toBe("Terminated");
});
});
// ----------------------------------------------------------------
// Integration — CT-MLFQ-02: new arrival preempts demoted process
// ----------------------------------------------------------------
describe("MLFQ integration — CT-MLFQ-02: preemption by new process", () => {
it("a newly arrived process (level 0) preempts a level-1 running process (CT-MLFQ-02)", () => {
// P1: arrives 0, CPU:10 — will be at level 1 after tick 2
// P2: arrives 3, CPU:2 — enters at level 0, should preempt P1 at tick 3
const processes = [p("p1", 0, 10), p("p2", 3, 2)];
const engine = new SimulationEngine(processes, cfg());
const ticks = engine.runAll();
const cpu = ticks.map((t) => t.cpuProcess);
// P1 runs ticks 0-1 (quantum=2, level 0)
expect(cpu[0]).toBe("p1");
expect(cpu[1]).toBe("p1");
// P1 is demoted to level 1 at tick 2, starts running with quantum=4
expect(cpu[2]).toBe("p1");
// P2 arrives at tick 3 (level 0), preempts P1 (level 1)
expect(cpu[3]).toBe("p2");
expect(cpu[4]).toBe("p2");
// P2 done; P1 resumes at level 1
expect(cpu[5]).toBe("p1");
});
});
// ----------------------------------------------------------------
// Integration — CT-MLFQ-03: I/O return does NOT demote
// ----------------------------------------------------------------
describe("MLFQ integration — CT-MLFQ-03: no demotion after I/O", () => {
it("process returning from I/O stays at the same level (CT-MLFQ-03)", () => {
// P1: CPU:1 → IO:2 → CPU:4
// P1 uses only 1 tick at level 0 (quantum=2, returns from I/O with qR=1)
// After I/O it should still be at level 0 and get a fresh quantum=2 on dispatch
const processes = [pWithIo("p1", 0, 1, 2, 4)];
const engine = new SimulationEngine(processes, cfg());
const ticks = engine.runAll();
const cpu = ticks.map((t) => t.cpuProcess);
// Tick 0: P1 runs (CPU burst 1 tick)
expect(cpu[0]).toBe("p1");
// Ticks 1-2: P1 in I/O (CPU=null)
expect(cpu[1]).toBeNull();
expect(cpu[2]).toBeNull();
// Ticks 3-6: P1 runs remaining CPU burst (4 ticks)
expect(cpu[3]).toBe("p1");
expect(cpu[4]).toBe("p1");
expect(cpu[5]).toBe("p1");
expect(cpu[6]).toBe("p1");
const rt = engine.runtimeStates.find((r) => r.processId === "p1")!;
expect(rt.state).toBe("Terminated");
expect(rt.cpuTime).toBe(5);
expect(rt.ioTime).toBe(2);
});
});
// ----------------------------------------------------------------
// Integration — CT-MLFQ-04: all processes terminate correctly
// ----------------------------------------------------------------
describe("MLFQ integration — CT-MLFQ-04: correct termination", () => {
it("all processes terminate with correct cpuTime", () => {
const processes = [p("p1", 0, 3), p("p2", 0, 5), p("p3", 2, 4)];
const engine = new SimulationEngine(processes, cfg());
engine.runAll();
const rts = engine.runtimeStates;
expect(rts.every((rt) => rt.state === "Terminated")).toBe(true);
expect(rts.find((r) => r.processId === "p1")!.cpuTime).toBe(3);
expect(rts.find((r) => r.processId === "p2")!.cpuTime).toBe(5);
expect(rts.find((r) => r.processId === "p3")!.cpuTime).toBe(4);
});
});
// ----------------------------------------------------------------
// Metadata
// ----------------------------------------------------------------
describe("MLFQAlgorithm — metadata", () => {
it("exposes correct educational properties", () => {
expect(mlfq.name).toContain("MLFQ");
expect(mlfq.isPreemptiveCapable).toBe(true);
expect(mlfq.usesQuantum).toBe(true);
});
});
+129
View File
@@ -0,0 +1,129 @@
import { describe, it, expect } from "vitest";
import { ThreadScheduler } from "../thread-scheduler";
import type { ThreadRuntime } from "../types";
function makeThread(
threadId: string,
arrivalTick = 0,
priority = 5,
quantumRemaining = 2,
): ThreadRuntime {
return {
threadId,
processId: "p1",
state: "Ready",
remainingBurst: 4,
burstIndex: 0,
arrivalTick,
startTick: null,
finishTick: null,
waitingTime: 0,
cpuTime: 0,
ioTime: 0,
responseTime: null,
quantumRemaining,
currentPriority: priority,
};
}
describe("ThreadScheduler — política FCFS", () => {
const sched = new ThreadScheduler("FCFS", 2);
it("retorna null para fila vazia", () => {
expect(sched.select([])).toBeNull();
});
it("seleciona thread com menor arrivalTick", () => {
const t1 = makeThread("t1", 5);
const t2 = makeThread("t2", 2);
const t3 = makeThread("t3", 8);
expect(sched.select([t1, t2, t3])?.threadId).toBe("t2");
});
it("desempate lexicográfico por threadId", () => {
const ta = makeThread("ta", 0);
const tb = makeThread("tb", 0);
expect(sched.select([tb, ta])?.threadId).toBe("ta");
});
it("isQuantumExpired sempre false em FCFS", () => {
const t = makeThread("t1");
t.quantumRemaining = -1;
expect(sched.isQuantumExpired(t)).toBe(false);
});
it("shouldPreemptThread sempre false em FCFS", () => {
const curr = makeThread("curr", 0, 5);
const cand = makeThread("cand", 0, 1);
expect(sched.shouldPreemptThread(curr, cand)).toBe(false);
});
});
describe("ThreadScheduler — política RR", () => {
const sched = new ThreadScheduler("RR", 3);
it("seleciona primeira da fila", () => {
const t1 = makeThread("t1");
const t2 = makeThread("t2");
expect(sched.select([t1, t2])?.threadId).toBe("t1");
});
it("isQuantumExpired true quando quantumRemaining ≤ 0", () => {
const t = makeThread("t1");
t.quantumRemaining = 0;
expect(sched.isQuantumExpired(t)).toBe(true);
t.quantumRemaining = -1;
expect(sched.isQuantumExpired(t)).toBe(true);
});
it("isQuantumExpired false quando quantum ainda disponível", () => {
const t = makeThread("t1");
t.quantumRemaining = 1;
expect(sched.isQuantumExpired(t)).toBe(false);
});
it("quantum correto", () => {
expect(sched.quantum).toBe(3);
});
});
describe("ThreadScheduler — política PRIORITY", () => {
const sched = new ThreadScheduler("PRIORITY", 2);
it("seleciona thread com menor currentPriority (maior prioridade)", () => {
const t1 = makeThread("t1", 0, 5);
const t2 = makeThread("t2", 0, 1);
const t3 = makeThread("t3", 0, 3);
expect(sched.select([t1, t2, t3])?.threadId).toBe("t2");
});
it("desempate por arrivalTick quando prioridades iguais", () => {
const t1 = makeThread("t1", 3, 2);
const t2 = makeThread("t2", 1, 2);
expect(sched.select([t1, t2])?.threadId).toBe("t2");
});
it("shouldPreemptThread true quando candidata tem prioridade maior", () => {
const curr = makeThread("curr", 0, 5);
const cand = makeThread("cand", 0, 2);
expect(sched.shouldPreemptThread(curr, cand)).toBe(true);
});
it("shouldPreemptThread false quando prioridade igual", () => {
const curr = makeThread("curr", 0, 3);
const cand = makeThread("cand", 0, 3);
expect(sched.shouldPreemptThread(curr, cand)).toBe(false);
});
it("shouldPreemptThread false quando candidata tem prioridade menor", () => {
const curr = makeThread("curr", 0, 2);
const cand = makeThread("cand", 0, 5);
expect(sched.shouldPreemptThread(curr, cand)).toBe(false);
});
it("isQuantumExpired sempre false em PRIORITY", () => {
const t = makeThread("t1");
t.quantumRemaining = -5;
expect(sched.isQuantumExpired(t)).toBe(false);
});
});
+12
View File
@@ -66,4 +66,16 @@ export abstract class BaseAlgorithm {
public static tiebreak(a: ProcessRuntime, b: ProcessRuntime): number {
return _tiebreak(a, b);
}
/**
* Retorna o quantum a ser atribuído ao processo no momento do dispatch.
* Algoritmos com quantum por processo (ex.: MLFQ) devem sobrescrever este método.
* O padrão usa o quantum global da configuração.
*/
public getQuantumForProcess(
_runtime: ProcessRuntime,
config: SchedulerConfig,
): number {
return config.quantum;
}
}
+2
View File
@@ -7,6 +7,7 @@ import { PriorityNonPreemptiveAlgorithm, PriorityPreemptiveAlgorithm } from "./p
import { PriorityRoundRobinAlgorithm } from "./priority_rr";
import { HRRNAlgorithm } from "./hrrn";
import { MultilevelQueueAlgorithm } from "./multilevel";
import { MLFQAlgorithm } from "./mlfq";
export type { BaseAlgorithm };
@@ -28,4 +29,5 @@ export const algorithms: Record<SchedulingAlgorithm, BaseAlgorithm> = {
PRIORITY_RR: new PriorityRoundRobinAlgorithm(),
HRRN: new HRRNAlgorithm(),
MULTILEVEL: new MultilevelQueueAlgorithm(),
MLFQ: new MLFQAlgorithm(),
};
+114
View File
@@ -0,0 +1,114 @@
import type { MlfqLevelDef, ProcessRuntime, SchedulerConfig } from "../types";
import { BaseAlgorithm } from "./base";
/**
* MLFQ — Multilevel Feedback Queue (Fila Multinível com Retroalimentação)
*
* Ao contrário do MLQ, os processos não são atribuídos permanentemente a filas.
* Em vez disso, o comportamento de CPU de cada processo determina dinamicamente
* em qual nível ele se encontra:
*
* - Todos os novos processos entram no nível 0 (maior prioridade).
* - Quando um processo esgota seu quantum, é rebaixado ao nível seguinte.
* - Quando um processo retorna de I/O, permanece no mesmo nível (sem rebaixamento).
* - Processos em níveis mais altos preemptam processos em níveis mais baixos.
* - Se `mlfqBoostInterval > 0`, processos que esperarem ≥ N ticks são
* promovidos ao nível 0 para evitar inanição.
*
* Cada nível tem seu próprio quantum. O último nível não sofre mais rebaixamentos.
* Dentro de um nível, a seleção é FIFO (por arrivalTick, depois processId).
*/
export class MLFQAlgorithm extends BaseAlgorithm {
readonly name = "Multilevel Feedback Queue (MLFQ)";
readonly description =
"Filas de prioridade dinâmicas: novos processos entram no nível 0 e são " +
"rebaixados ao usar todo o quantum. Retorno de I/O não causa rebaixamento. " +
"Boost opcional promove processos que esperaram muito tempo de volta ao nível 0.";
readonly isPreemptiveCapable = true;
readonly usesQuantum = true;
static readonly DEFAULT_LEVELS: MlfqLevelDef[] = [
{ quantum: 2 },
{ quantum: 4 },
{ quantum: 8 },
];
select(
readyQueue: readonly ProcessRuntime[],
config: SchedulerConfig,
): ProcessRuntime | null {
if (readyQueue.length === 0) return null;
const levels = config.mlfqLevels ?? MLFQAlgorithm.DEFAULT_LEVELS;
const maxLevel = levels.length - 1;
const boostInterval = config.mlfqBoostInterval ?? 0;
// Phase 1 — initialize new processes and apply demotion.
// A process with quantumRemaining <= 0 and an already-initialized mlfqLevel
// was preempted by quantum expiry → demote it.
for (const rt of readyQueue) {
if (rt.mlfqLevel === undefined) {
// New process: assign level 0.
rt.mlfqLevel = 0;
} else if (rt.quantumRemaining <= 0) {
// Quantum-expired: demote (capped at last level).
rt.mlfqLevel = Math.min(rt.mlfqLevel + 1, maxLevel);
// Set sentinel so we don't demote again before the next dispatch.
rt.quantumRemaining = 1;
}
// quantumRemaining > 0 → returned from I/O or priority-preempted; no demotion.
}
// Phase 2 — priority boost.
if (boostInterval > 0) {
for (const rt of readyQueue) {
if ((rt.mlfqLevel ?? 0) > 0 && rt.waitingTime >= boostInterval) {
rt.mlfqLevel = 0;
}
}
}
// Phase 3 — pick the process at the highest level (lowest index), FIFO within.
let best: ProcessRuntime | null = null;
for (const rt of readyQueue) {
if (best === null) {
best = rt;
continue;
}
const rtLevel = rt.mlfqLevel ?? 0;
const bestLevel = best.mlfqLevel ?? 0;
if (
rtLevel < bestLevel ||
(rtLevel === bestLevel && BaseAlgorithm.tiebreak(rt, best) < 0)
) {
best = rt;
}
}
return best;
}
shouldPreempt(
current: ProcessRuntime,
candidate: ProcessRuntime,
_config: SchedulerConfig,
): boolean {
// A candidate at a higher-priority level (lower index) preempts the running process.
return (candidate.mlfqLevel ?? 0) < (current.mlfqLevel ?? 0);
}
isQuantumExpired(
runtime: ProcessRuntime,
_config: SchedulerConfig,
): boolean {
return runtime.quantumRemaining <= 0;
}
override getQuantumForProcess(
runtime: ProcessRuntime,
config: SchedulerConfig,
): number {
const levels = config.mlfqLevels ?? MLFQAlgorithm.DEFAULT_LEVELS;
const level = runtime.mlfqLevel ?? 0;
return levels[level]?.quantum ?? config.quantum;
}
}
+659 -129
View File
@@ -4,9 +4,12 @@ import type {
SchedulerConfig,
SimTick,
StateLabel,
Thread,
ThreadRuntime,
} from "./types";
import { algorithms } from "./algorithms/index";
import type { BaseAlgorithm } from "./algorithms/index";
import { ThreadScheduler } from "./thread-scheduler";
/**
* SimulationEngine — Núcleo de execução da simulação de escalonamento.
@@ -14,42 +17,66 @@ import type { BaseAlgorithm } from "./algorithms/index";
* Responsável por avançar o simulador tick a tick, aplicando o algoritmo
* de escalonamento configurado e mantendo o estado de todos os processos.
*
* Suporta dois modos de operação:
* - Processos simples: comportamento original, sem alterações.
* - Processos com threads: escalonamento em dois níveis:
* 1. Nível de processo — algoritmo global do SchedulerConfig.
* 2. Nível de thread — ThreadScheduler com política intra-processo.
*
* Modelos de threads suportados:
* MANY_TO_ONE — Uma kernel thread por processo. I/O bloqueia todo o processo.
* ONE_TO_ONE — Uma kernel thread por thread de usuário. Threads bloqueiam
* independentemente; o processo só bloqueia se todas as threads
* estiverem em I/O.
* MANY_TO_MANY — M kernel threads para N threads de usuário (M ≤ N).
* Slots são liberados durante I/O e reatribuídos dinamicamente.
*
* 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"`.
* 1. Chegada — New → Ready para processos/threads que chegam neste tick.
* 2. Preempção — Algoritmo decide se processo em CPU deve ceder lugar.
* 3. Despacho — Seleciona e despacha próximo processo (e thread).
* 4. Snapshot — Captura estado completo (processos + threads).
* 5. Passagem do tempo — Decrementa bursts, acumula tempos.
* 6. Aging — Envelhecimento de prioridade anti-starvation.
* 7. Conclusões — Trata término de bursts de CPU/IO (processo e threads).
*/
export class SimulationEngine {
// ─── Campos privados ──────────────────────────────────────────────────────
// ─── Campos privados — processos ──────────────────────────────────────────
private readonly _processes: Process[];
private readonly _config: SchedulerConfig;
private readonly _algorithm: BaseAlgorithm;
private _tick: number; // começa em -1; incrementado no início de step()
private _tick: number;
private _runtimes: Map<string, ProcessRuntime>;
private _cpuProcess: ProcessRuntime | null;
private _readyQueue: ProcessRuntime[]; // array ordenado (FIFO para RR)
private _readyQueue: ProcessRuntime[];
private _ioQueue: ProcessRuntime[];
private _contextSwitchRemaining: number;
private _prevCpuProcessId: string | null; // para detectar troca de contexto
private _prevCpuProcessId: string | null;
// ─── Campos privados — threads ────────────────────────────────────────────
/** threadRuntimes[processId][threadId] = ThreadRuntime */
private _threadRuntimes: Map<string, Map<string, ThreadRuntime>>;
/** Escalonador intra-processo por processo */
private _threadSchedulers: Map<string, ThreadScheduler>;
/** Thread atualmente ativa na CPU para cada processo com threads */
private _activeThreads: Map<string, ThreadRuntime | null>;
/** Threads em I/O por processo */
private _threadIoQueues: Map<string, ThreadRuntime[]>;
/** Fila de threads prontas intra-processo */
private _readyThreadQueues: Map<string, ThreadRuntime[]>;
/** MANY_TO_MANY: slots de kernel disponíveis por processo */
private _kernelSlotsAvailable: Map<string, number>;
/** MANY_TO_MANY: threads aguardando slot de kernel */
private _kernelWaitQueues: Map<string, ThreadRuntime[]>;
// ─── Construtor ───────────────────────────────────────────────────────────
@@ -64,37 +91,47 @@ export class SimulationEngine {
this._ioQueue = [];
this._contextSwitchRemaining = 0;
this._prevCpuProcessId = null;
// Inicializa runtimes de processo
this._runtimes = new Map(
processes.map((p) => [p.id, SimulationEngine._initRuntime(p, config)]),
processes.map((p) => [p.id, SimulationEngine._initProcessRuntime(p, config)]),
);
// Inicializa estruturas de threads
this._threadRuntimes = new Map();
this._threadSchedulers = new Map();
this._activeThreads = new Map();
this._threadIoQueues = new Map();
this._readyThreadQueues = new Map();
this._kernelSlotsAvailable = new Map();
this._kernelWaitQueues = new Map();
for (const p of processes) {
if (this._hasThreads(p.id)) {
this._initThreadStructures(p);
}
}
}
// ─── API pública ──────────────────────────────────────────────────────────
/**
* 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++;
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
this._handleArrivals();
this._checkPreemption();
this._dispatchNext();
const snapshot = this._snapshot();
this._advanceTime();
this._applyAging();
this._handleCpuCompletion();
this._handleIoCompletions();
return snapshot;
}
/** Executa até o fim e retorna todos os snapshots de tick. */
runAll(): SimTick[] {
const results: SimTick[] = [];
while (!this.isFinished) {
@@ -106,7 +143,6 @@ export class SimulationEngine {
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;
@@ -114,23 +150,34 @@ export class SimulationEngine {
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());
}
/** Retorna todos os runtimes de threads de todos os processos. */
get threadRuntimeStates(): ThreadRuntime[] {
const result: ThreadRuntime[] = [];
for (const threadMap of this._threadRuntimes.values()) {
for (const trt of threadMap.values()) {
result.push(trt);
}
}
return result;
}
/** Informa se um processo possui threads definidas. */
processHasThreads(processId: string): boolean {
return this._hasThreads(processId);
}
// ─── 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.
* Fase 1 — Chegada de processos (e inicialização de threads).
*/
private _handleArrivals(): void {
for (const p of this._processes) {
@@ -138,17 +185,40 @@ export class SimulationEngine {
if (p.arrivalTime === this._tick && rt.state === "New") {
this._transition(rt, "Ready");
this._readyQueue.push(rt);
if (this._hasThreads(p.id)) {
// Transiciona todas as threads New → Ready na fila intra-processo
const threadMap = this._threadRuntimes.get(p.id)!;
const readyQueue = this._readyThreadQueues.get(p.id)!;
const model = p.threadModel ?? "ONE_TO_ONE";
for (const trt of threadMap.values()) {
if (trt.state === "New") {
if (model === "MANY_TO_MANY") {
const slotsAvail = this._kernelSlotsAvailable.get(p.id) ?? 0;
if (slotsAvail > 0) {
this._kernelSlotsAvailable.set(p.id, slotsAvail - 1);
this._transitionThread(trt, "Ready");
readyQueue.push(trt);
} else {
// Sem slot disponível → espera kernel thread
trt.kernelWaiting = true;
this._transitionThread(trt, "Ready");
this._kernelWaitQueues.get(p.id)!.push(trt);
}
} else {
this._transitionThread(trt, "Ready");
readyQueue.push(trt);
}
}
}
}
}
}
}
/**
* 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.
* Fase 2 — Verificação de preempção no nível de processo.
*/
private _checkPreemption(): void {
if (this._cpuProcess === null || this._readyQueue.length === 0) return;
@@ -158,6 +228,17 @@ export class SimulationEngine {
this._algorithm.shouldPreempt(this._cpuProcess, candidate, this._config)
) {
const cpu = this._cpuProcess;
// Retorna thread ativa à fila intra-processo (se processo com threads)
if (this._hasThreads(cpu.processId)) {
const activeThread = this._activeThreads.get(cpu.processId) ?? null;
if (activeThread !== null) {
this._transitionThread(activeThread, "Ready");
this._readyThreadQueues.get(cpu.processId)!.unshift(activeThread);
this._activeThreads.set(cpu.processId, null);
}
}
this._transition(cpu, "Ready");
this._readyQueue.push(cpu);
this._cpuProcess = null;
@@ -165,16 +246,7 @@ export class SimulationEngine {
}
/**
* 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.
* Fase 3 — Despacho de processo (e thread).
*/
private _dispatchNext(): void {
if (this._contextSwitchRemaining > 0) return;
@@ -196,21 +268,71 @@ export class SimulationEngine {
this._prevCpuProcessId = next.processId;
} else {
if (next.startTick === null) next.startTick = this._tick;
next.quantumRemaining = this._config.quantum;
next.quantumRemaining = this._algorithm.getQuantumForProcess(next, this._config);
this._transition(next, "Running");
this._prevCpuProcessId = next.processId;
this._cpuProcess = next;
// Despacha thread intra-processo (se aplicável)
if (this._hasThreads(next.processId)) {
this._dispatchThreadForProcess(next);
}
}
}
/**
* Fase 4 — Snapshot do estado.
*/
private _snapshot(): SimTick {
const states: Record<string, StateLabel> = {};
for (const [id, rt] of this._runtimes) {
states[id] = rt.state;
}
const contextSwitching = this._contextSwitchRemaining > 0;
const snap: SimTick = {
tick: this._tick,
cpuProcess: contextSwitching ? null : (this._cpuProcess?.processId ?? null),
ioProcesses: this._ioQueue.map((rt) => rt.processId),
readyQueue: this._readyQueue.map((rt) => rt.processId),
states,
contextSwitching,
ctxSwitchForProcess: contextSwitching
? (this._readyQueue[0]?.processId ?? null)
: null,
};
// Inclui estado de threads se houver processos com threads
if (this._threadRuntimes.size > 0) {
const threadStates: Record<string, Record<string, StateLabel>> = {};
const cpuThreads: Record<string, string | null> = {};
const kernelWaitingThreads: Record<string, string[]> = {};
for (const [processId, threadMap] of this._threadRuntimes) {
threadStates[processId] = {};
for (const [threadId, trt] of threadMap) {
threadStates[processId][threadId] = trt.state;
}
const active = this._activeThreads.get(processId) ?? null;
cpuThreads[processId] = active?.threadId ?? null;
const kwQueue = this._kernelWaitQueues.get(processId) ?? [];
if (kwQueue.length > 0) {
kernelWaitingThreads[processId] = kwQueue.map((t) => t.threadId);
}
}
snap.threadStates = threadStates;
snap.cpuThreads = cpuThreads;
if (Object.keys(kernelWaitingThreads).length > 0) {
snap.kernelWaitingThreads = kernelWaitingThreads;
}
}
return snap;
}
/**
* 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) {
@@ -219,29 +341,67 @@ export class SimulationEngine {
if (this._cpuProcess !== null) {
const cpu = this._cpuProcess;
if (this._hasThreads(cpu.processId)) {
// CPU time acumulado no nível de processo
cpu.cpuTime++;
cpu.quantumRemaining--;
// Decrementa burst e tempo da thread ativa
const activeThread = this._activeThreads.get(cpu.processId) ?? null;
if (activeThread !== null) {
activeThread.remainingBurst--;
activeThread.cpuTime++;
activeThread.quantumRemaining--;
// Sincroniza processo com thread ativa
cpu.remainingBurst = activeThread.remainingBurst;
}
} else {
cpu.remainingBurst--;
cpu.cpuTime++;
cpu.quantumRemaining--;
}
}
// I/O de processos sem threads
for (const rt of this._ioQueue) {
if (!this._hasThreads(rt.processId)) {
rt.remainingBurst--;
rt.ioTime++;
}
// Processos com threads (MANY_TO_ONE): tempo de I/O acumulado abaixo
}
// I/O de threads (todos os modelos)
for (const [, threadIoQueue] of this._threadIoQueues) {
for (const trt of threadIoQueue) {
trt.remainingBurst--;
trt.ioTime++;
}
}
// Tempo de espera — fila de prontos de processos
for (const rt of this._readyQueue) {
rt.waitingTime++;
}
// Tempo de espera — fila de prontas de threads intra-processo
for (const [, threadReadyQueue] of this._readyThreadQueues) {
for (const trt of threadReadyQueue) {
trt.waitingTime++;
}
}
// Tempo de espera — threads em kernel wait (MANY_TO_MANY)
for (const [, kwQueue] of this._kernelWaitQueues) {
for (const trt of kwQueue) {
trt.waitingTime++;
}
}
}
/**
* 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.
* Fase 6 — Aging de prioridade.
*/
private _applyAging(): void {
if (
@@ -258,24 +418,20 @@ export class SimulationEngine {
/**
* 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 (this._hasThreads(cpu.processId)) {
this._handleThreadedCpuCompletion();
return;
}
// ── Processo sem threads (comportamento original) ──────────────────────
if (cpu.remainingBurst === 0) {
this._advanceBurst(cpu);
cpu.quantumRemaining = 1;
const process = this._processes.find((p) => p.id === cpu.processId)!;
const nextBurst = process.bursts[cpu.burstIndex];
if (nextBurst !== undefined && nextBurst.type === "io") {
@@ -298,16 +454,16 @@ export class SimulationEngine {
/**
* 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 {
// ── Processos sem threads ──────────────────────────────────────────────
const nextIoQueue: ProcessRuntime[] = [];
for (const rt of this._ioQueue) {
if (this._hasThreads(rt.processId)) {
// Processos com threads gerenciados em _handleThreadIoCompletions
nextIoQueue.push(rt);
continue;
}
if (rt.remainingBurst === 0) {
this._advanceBurst(rt);
const process = this._processes.find((p) => p.id === rt.processId)!;
@@ -323,22 +479,341 @@ export class SimulationEngine {
}
}
this._ioQueue = nextIoQueue;
// ── Threads em I/O ────────────────────────────────────────────────────
this._handleThreadIoCompletions();
}
// ─── Lógica de threads ────────────────────────────────────────────────────
/**
* Despacha a próxima thread intra-processo quando o processo recebe CPU.
*/
private _dispatchThreadForProcess(processRt: ProcessRuntime): void {
const processId = processRt.processId;
const readyThreadQueue = this._readyThreadQueues.get(processId)!;
const scheduler = this._threadSchedulers.get(processId)!;
const nextThread = scheduler.select(readyThreadQueue);
if (nextThread === null) {
// Nenhuma thread pronta — mantém processo em Running mas sem thread ativa
// (Pode ocorrer se todas as threads estão em I/O; ONE_TO_ONE pode despachar
// o processo mesmo assim para que ele "exista" na CPU enquanto threads esperam)
this._activeThreads.set(processId, null);
return;
}
const idx = readyThreadQueue.indexOf(nextThread);
if (idx !== -1) readyThreadQueue.splice(idx, 1);
if (nextThread.startTick === null) {
nextThread.startTick = this._tick;
nextThread.responseTime = this._tick - nextThread.arrivalTick;
}
nextThread.quantumRemaining = scheduler.quantum;
nextThread.kernelWaiting = false;
this._transitionThread(nextThread, "Running");
this._activeThreads.set(processId, nextThread);
// Sincroniza process runtime com thread ativa
processRt.remainingBurst = nextThread.remainingBurst;
if (processRt.startTick === null) {
processRt.startTick = this._tick;
processRt.responseTime = this._tick - processRt.arrivalTick;
}
}
/**
* Gerencia conclusão de burst de CPU para processos com threads.
*/
private _handleThreadedCpuCompletion(): void {
const cpu = this._cpuProcess!;
const processId = cpu.processId;
const process = this._processes.find((p) => p.id === processId)!;
const policy = process.threadSchedulingPolicy ?? "FCFS";
const scheduler = this._threadSchedulers.get(processId)!;
const activeThread = this._activeThreads.get(processId) ?? null;
if (activeThread === null) {
// Sem thread ativa — verifica se processo deve continuar ou aguardar
if (this._readyThreadQueues.get(processId)!.length > 0) {
this._dispatchThreadForProcess(cpu);
} else {
// Todas as threads em I/O: processo vai para Waiting
this._transition(cpu, "Waiting");
if (!this._ioQueue.includes(cpu)) this._ioQueue.push(cpu);
this._cpuProcess = null;
}
return;
}
if (activeThread.remainingBurst === 0) {
// ── CPU burst da thread concluído ────────────────────────────────────
this._activeThreads.set(processId, null);
this._handleThreadBurstComplete(cpu, activeThread, process);
} else if (scheduler.isQuantumExpired(activeThread)) {
// ── Quantum intra-processo expirou (RR): rotaciona thread ────────────
this._transitionThread(activeThread, "Ready");
this._readyThreadQueues.get(processId)!.push(activeThread); // reinserido no fim (FIFO)
this._activeThreads.set(processId, null);
// Seleciona próxima thread — processo permanece na CPU
this._dispatchThreadForProcess(cpu);
// Se não há próxima thread pronta, processo aguarda
if (this._activeThreads.get(processId) === null) {
this._transition(cpu, "Waiting");
if (!this._ioQueue.includes(cpu)) this._ioQueue.push(cpu);
this._cpuProcess = null;
}
} else if (this._algorithm.isQuantumExpired(cpu, this._config)) {
// ── Quantum de processo expirou: devolve processo à fila global ───────
// Thread volta à fila intra-processo
this._transitionThread(activeThread, "Ready");
if (policy === "RR") {
this._readyThreadQueues.get(processId)!.push(activeThread);
} else {
this._readyThreadQueues.get(processId)!.unshift(activeThread);
}
this._activeThreads.set(processId, null);
this._transition(cpu, "Ready");
this._readyQueue.push(cpu);
this._cpuProcess = null;
}
}
/**
* Trata conclusão do burst de CPU de uma thread específica.
* Decide o próximo estado da thread e do processo conforme o modelo.
*/
private _handleThreadBurstComplete(
processRt: ProcessRuntime,
thread: ThreadRuntime,
process: Process,
): void {
const processId = process.id;
const model = process.threadModel ?? "ONE_TO_ONE";
const threadDef = process.threads!.find((t) => t.tid === thread.threadId)!;
// Avança ao próximo burst da thread
thread.burstIndex++;
const nextBurst = threadDef.bursts[thread.burstIndex];
thread.quantumRemaining = 1; // sentinela anti-falsa-demoção
if (nextBurst !== undefined && nextBurst.type === "io") {
// ── Thread vai para I/O ───────────────────────────────────────────────
thread.remainingBurst = nextBurst.duration;
this._transitionThread(thread, "Waiting");
this._threadIoQueues.get(processId)!.push(thread);
// Para MANY_TO_MANY: libera slot de kernel
if (model === "MANY_TO_MANY") {
this._releaseKernelSlot(processId);
}
if (model === "MANY_TO_ONE") {
// Kernel thread bloqueado → bloqueia todo o processo
this._transition(processRt, "Waiting");
this._ioQueue.push(processRt);
this._cpuProcess = null;
} else {
// ONE_TO_ONE / MANY_TO_MANY: verifica se há outra thread pronta
this._trySelectNextThreadOrBlock(processRt, process);
}
} else if (nextBurst !== undefined && nextBurst.type === "cpu") {
// ── Thread tem próximo burst de CPU ───────────────────────────────────
thread.remainingBurst = nextBurst.duration;
this._transitionThread(thread, "Ready");
this._readyThreadQueues.get(processId)!.push(thread);
// Processo permanece na CPU; seleciona próxima thread
this._dispatchThreadForProcess(processRt);
} else {
// ── Thread terminada ──────────────────────────────────────────────────
this._transitionThread(thread, "Terminated");
thread.finishTick = this._tick + 1;
// Para MANY_TO_MANY: libera slot de kernel
if (model === "MANY_TO_MANY") {
this._releaseKernelSlot(processId);
}
// Verifica se processo deve terminar ou continuar com outra thread
if (this._allThreadsTerminated(processId)) {
this._transition(processRt, "Terminated");
processRt.finishTick = this._tick + 1;
this._cpuProcess = null;
} else if (model === "MANY_TO_ONE") {
// Seleciona próxima thread pronta (se houver)
this._trySelectNextThreadOrBlock(processRt, process);
} else {
this._trySelectNextThreadOrBlock(processRt, process);
}
}
}
/**
* Tenta selecionar a próxima thread pronta para o processo.
* Se não houver threads prontas e há threads em I/O: processo aguarda.
* Se não houver threads prontas e todas terminadas: processo termina.
*/
private _trySelectNextThreadOrBlock(
processRt: ProcessRuntime,
process: Process,
): void {
const processId = process.id;
const readyThreadQueue = this._readyThreadQueues.get(processId)!;
if (readyThreadQueue.length > 0) {
// Há threads prontas: seleciona a próxima
this._dispatchThreadForProcess(processRt);
} else if (this._allThreadsTerminated(processId)) {
this._transition(processRt, "Terminated");
processRt.finishTick = this._tick + 1;
this._cpuProcess = null;
} else {
// Todas as threads em I/O: processo aguarda
this._transition(processRt, "Waiting");
if (!this._ioQueue.includes(processRt)) {
this._ioQueue.push(processRt);
}
this._cpuProcess = null;
}
}
/**
* Gerencia conclusão de bursts de I/O para threads.
*/
private _handleThreadIoCompletions(): void {
for (const [processId, threadIoQueue] of this._threadIoQueues) {
const processRt = this._runtimes.get(processId)!;
const process = this._processes.find((p) => p.id === processId)!;
const model = process.threadModel ?? "ONE_TO_ONE";
const remaining: ThreadRuntime[] = [];
for (const trt of threadIoQueue) {
if (trt.remainingBurst === 0) {
// ── I/O da thread concluído ────────────────────────────────────────
const threadDef = process.threads!.find((t) => t.tid === trt.threadId)!;
trt.burstIndex++;
const nextBurst = threadDef.bursts[trt.burstIndex];
trt.quantumRemaining = 1; // sentinela
if (nextBurst !== undefined && nextBurst.type === "cpu") {
trt.remainingBurst = nextBurst.duration;
if (model === "MANY_TO_MANY") {
// Tenta adquirir slot de kernel
const slotsAvail = this._kernelSlotsAvailable.get(processId) ?? 0;
if (slotsAvail > 0) {
this._kernelSlotsAvailable.set(processId, slotsAvail - 1);
trt.kernelWaiting = false;
this._transitionThread(trt, "Ready");
this._readyThreadQueues.get(processId)!.push(trt);
} else {
// Sem slot: vai para kernel wait queue
trt.kernelWaiting = true;
this._kernelWaitQueues.get(processId)!.push(trt);
}
} else {
this._transitionThread(trt, "Ready");
this._readyThreadQueues.get(processId)!.push(trt);
}
} else {
// Thread termina após I/O (ou sem próximo burst)
this._transitionThread(trt, "Terminated");
trt.finishTick = this._tick + 1;
if (model === "MANY_TO_MANY") {
// Não havia slot em uso durante I/O; nada a liberar
}
}
// Decide estado do processo com base no modelo
this._updateProcessStateAfterThreadIo(processRt, process, model);
} else {
remaining.push(trt);
}
}
this._threadIoQueues.set(processId, remaining);
}
}
/**
* Atualiza o estado do processo após conclusão de I/O de uma thread.
*/
private _updateProcessStateAfterThreadIo(
processRt: ProcessRuntime,
process: Process,
model: string,
): void {
const processId = process.id;
if (model === "MANY_TO_ONE") {
// O processo estava bloqueado (Waiting) devido ao I/O da thread
// Agora a thread está pronta → processo volta para Ready
if (processRt.state === "Waiting") {
this._ioQueue = this._ioQueue.filter((r) => r.processId !== processId);
this._transition(processRt, "Ready");
this._readyQueue.push(processRt);
this._activeThreads.set(processId, null); // será selecionada no dispatch
}
} else {
// ONE_TO_ONE / MANY_TO_MANY
if (processRt.state === "Waiting") {
// Processo estava aguardando porque todas as threads estavam em I/O
// Verifica se alguma thread ficou pronta
const readyThreadQueue = this._readyThreadQueues.get(processId)!;
if (readyThreadQueue.length > 0) {
this._ioQueue = this._ioQueue.filter((r) => r.processId !== processId);
this._transition(processRt, "Ready");
this._readyQueue.push(processRt);
}
}
// Se processo está Running: a thread retornou; será selecionada na próxima rodada
// Se processo está Ready: a thread apenas entra na fila intra-processo
}
// Verifica se todas as threads terminaram
if (this._allThreadsTerminated(processId) && processRt.state !== "Terminated") {
this._ioQueue = this._ioQueue.filter((r) => r.processId !== processId);
this._readyQueue = this._readyQueue.filter((r) => r.processId !== processId);
this._transition(processRt, "Terminated");
processRt.finishTick = this._tick + 1;
}
}
// ─── MANY_TO_MANY — gerenciamento de slots de kernel ─────────────────────
/**
* Libera um slot de kernel e tenta promover a próxima thread da kernel wait queue.
*/
private _releaseKernelSlot(processId: string): void {
const kwQueue = this._kernelWaitQueues.get(processId)!;
if (kwQueue.length > 0) {
// Repassa o slot diretamente para a próxima thread aguardando
const next = kwQueue.shift()!;
next.kernelWaiting = false;
this._transitionThread(next, "Ready");
this._readyThreadQueues.get(processId)!.push(next);
} else {
this._kernelSlotsAvailable.set(
processId,
(this._kernelSlotsAvailable.get(processId) ?? 0) + 1,
);
}
}
// ─── 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 _transitionThread(trt: ThreadRuntime, state: StateLabel): void {
trt.state = state;
}
private _advanceBurst(rt: ProcessRuntime): void {
rt.burstIndex++;
const process = this._processes.find((p) => p.id === rt.processId)!;
@@ -346,45 +821,36 @@ 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) {
states[id] = rt.state;
private _hasThreads(processId: string): boolean {
const p = this._processes.find((proc) => proc.id === processId);
return (p?.threads?.length ?? 0) > 0;
}
const contextSwitching = this._contextSwitchRemaining > 0;
return {
tick: this._tick,
cpuProcess: contextSwitching ? null : (this._cpuProcess?.processId ?? null),
ioProcesses: this._ioQueue.map((rt) => rt.processId),
readyQueue: this._readyQueue.map((rt) => rt.processId),
states,
contextSwitching,
ctxSwitchForProcess: contextSwitching
? (this._readyQueue[0]?.processId ?? null)
: null,
};
private _allThreadsTerminated(processId: string): boolean {
const threadMap = this._threadRuntimes.get(processId);
if (!threadMap) return true;
for (const trt of threadMap.values()) {
if (trt.state !== "Terminated") return false;
}
return true;
}
// ─── Inicialização ────────────────────────────────────────────────────────
/**
* 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 {
private static _initProcessRuntime(p: Process, config: SchedulerConfig): ProcessRuntime {
const isThreaded = (p.threads?.length ?? 0) > 0;
if (!isThreaded) {
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,
remainingBurst: isThreaded ? 1 : (p.bursts[0]?.duration ?? 0),
burstIndex: 0,
arrivalTick: p.arrivalTime,
startTick: null,
@@ -397,4 +863,68 @@ export class SimulationEngine {
currentPriority: p.priority,
};
}
private _initThreadStructures(p: Process): void {
const processId = p.id;
const model = p.threadModel ?? "ONE_TO_ONE";
const policy = p.threadSchedulingPolicy ?? "FCFS";
const quantum = p.threadQuantum ?? 2;
const threads = p.threads!;
// Criação do mapa de runtimes de threads
const threadMap = new Map<string, ThreadRuntime>(
threads.map((t) => [
t.tid,
SimulationEngine._initThreadRuntime(t, processId, p.arrivalTime, quantum),
]),
);
this._threadRuntimes.set(processId, threadMap);
// Escalonador intra-processo
this._threadSchedulers.set(processId, new ThreadScheduler(policy, quantum));
// Estruturas de estado
this._activeThreads.set(processId, null);
this._threadIoQueues.set(processId, []);
this._readyThreadQueues.set(processId, []);
// MANY_TO_MANY: inicializa slots de kernel
if (model === "MANY_TO_MANY") {
const M =
p.kernelThreadCount ??
Math.max(1, Math.ceil(threads.length / 2));
this._kernelSlotsAvailable.set(processId, M);
this._kernelWaitQueues.set(processId, []);
} else {
this._kernelWaitQueues.set(processId, []);
}
}
private static _initThreadRuntime(
t: Thread,
processId: string,
arrivalTick: number,
quantum: number,
): ThreadRuntime {
const firstBurst = t.bursts[0];
if (firstBurst === undefined || firstBurst.type !== "cpu") {
throw new Error(`Thread ${t.tid}: first burst must be a CPU burst`);
}
return {
threadId: t.tid,
processId,
state: "New",
remainingBurst: firstBurst.duration,
burstIndex: 0,
arrivalTick,
startTick: null,
finishTick: null,
waitingTime: 0,
cpuTime: 0,
ioTime: 0,
responseTime: null,
quantumRemaining: quantum,
currentPriority: t.priority ?? 5,
};
}
}
+39
View File
@@ -1,7 +1,11 @@
import type {
GlobalMetrics,
Process,
ProcessMetrics,
ProcessRuntime,
Thread,
ThreadMetrics,
ThreadRuntime,
} from "./types";
// ============================================================
@@ -83,6 +87,41 @@ export function computeGlobalMetrics(
};
}
// ============================================================
// Thread metrics
// ============================================================
export function computeThreadMetrics(
trt: ThreadRuntime,
thread: Thread,
process: Process,
): ThreadMetrics {
if (trt.startTick === null || trt.finishTick === null) {
throw new Error(
`Cannot compute metrics for thread ${trt.threadId}: not yet terminated`,
);
}
const turnaroundTime = trt.finishTick - trt.arrivalTick;
const responseTime = trt.startTick - trt.arrivalTick;
const waitingTime = turnaroundTime - trt.cpuTime - trt.ioTime;
return {
threadId: trt.threadId,
processId: trt.processId,
threadName: thread.name,
processName: process.name,
arrivalTime: trt.arrivalTick,
startTick: trt.startTick,
finishTick: trt.finishTick,
responseTime,
turnaroundTime,
waitingTime: Math.max(0, waitingTime),
cpuTime: trt.cpuTime,
ioTime: trt.ioTime,
};
}
// ============================================================
// Classify process by declared burst ratios (static, pre-simulation)
// Used in the process list preview badge.
+316
View File
@@ -1,4 +1,5 @@
import type { MlqQueueDef, Process, SchedulerConfig } from "./types";
// Thread preset IDs are prefixed with "t_" to avoid conflicts
export interface Preset {
name: string;
@@ -127,4 +128,319 @@ export const presets: Preset[] = [
],
config: defaultConfig({ algorithm: "MULTILEVEL", quantum: 2, mlqQueues: DEFAULT_MLQ_QUEUES }),
},
// ──────────────────────────────────────────────────────────────────────────
// PRESETS DE THREADS — demonstram os três modelos e conceitos de concorrência
// ──────────────────────────────────────────────────────────────────────────
{
name: "many_to_one",
description:
"MANY_TO_ONE: 2 processos com 3 threads cada. Uma thread em I/O bloqueia todo o processo — observe como o processo fica Waiting enquanto as outras threads ficam ociosas.",
processes: [
{
pid: 1,
id: "t_p1",
name: "Srv-A",
arrivalTime: 0,
priority: 5,
color: "#4FC3F7",
bursts: [],
threadModel: "MANY_TO_ONE",
threadSchedulingPolicy: "RR",
threadQuantum: 2,
threads: [
{
tid: "t_p1_t1",
name: "A-T1",
bursts: [
{ type: "cpu", duration: 3 },
{ type: "io", duration: 4 },
{ type: "cpu", duration: 2 },
],
},
{
tid: "t_p1_t2",
name: "A-T2",
bursts: [
{ type: "cpu", duration: 2 },
{ type: "io", duration: 2 },
{ type: "cpu", duration: 3 },
],
},
{
tid: "t_p1_t3",
name: "A-T3",
bursts: [{ type: "cpu", duration: 4 }],
},
],
},
{
pid: 2,
id: "t_p2",
name: "Srv-B",
arrivalTime: 2,
priority: 5,
color: "#81C784",
bursts: [],
threadModel: "MANY_TO_ONE",
threadSchedulingPolicy: "FCFS",
threads: [
{
tid: "t_p2_t1",
name: "B-T1",
bursts: [
{ type: "cpu", duration: 2 },
{ type: "io", duration: 3 },
{ type: "cpu", duration: 2 },
],
},
{
tid: "t_p2_t2",
name: "B-T2",
bursts: [{ type: "cpu", duration: 5 }],
},
],
},
],
config: defaultConfig({ algorithm: "RR", quantum: 4 }),
},
{
name: "one_to_one",
description:
"ONE_TO_ONE: mesmo cenário do Many-to-One mas com kernel threads independentes. Threads em I/O não bloqueiam o processo — compare o throughput com o preset many_to_one.",
processes: [
{
pid: 1,
id: "t_p1",
name: "Srv-A",
arrivalTime: 0,
priority: 5,
color: "#4FC3F7",
bursts: [],
threadModel: "ONE_TO_ONE",
threadSchedulingPolicy: "RR",
threadQuantum: 2,
threads: [
{
tid: "t_p1_t1",
name: "A-T1",
bursts: [
{ type: "cpu", duration: 3 },
{ type: "io", duration: 4 },
{ type: "cpu", duration: 2 },
],
},
{
tid: "t_p1_t2",
name: "A-T2",
bursts: [
{ type: "cpu", duration: 2 },
{ type: "io", duration: 2 },
{ type: "cpu", duration: 3 },
],
},
{
tid: "t_p1_t3",
name: "A-T3",
bursts: [{ type: "cpu", duration: 4 }],
},
],
},
{
pid: 2,
id: "t_p2",
name: "Srv-B",
arrivalTime: 2,
priority: 5,
color: "#81C784",
bursts: [],
threadModel: "ONE_TO_ONE",
threadSchedulingPolicy: "FCFS",
threads: [
{
tid: "t_p2_t1",
name: "B-T1",
bursts: [
{ type: "cpu", duration: 2 },
{ type: "io", duration: 3 },
{ type: "cpu", duration: 2 },
],
},
{
tid: "t_p2_t2",
name: "B-T2",
bursts: [{ type: "cpu", duration: 5 }],
},
],
},
],
config: defaultConfig({ algorithm: "RR", quantum: 4 }),
},
{
name: "many_to_many",
description:
"MANY_TO_MANY: processo com 4 threads de usuário mas apenas 2 kernel threads (M=2). Observe threads em KernelWait aguardando slot disponível.",
processes: [
{
pid: 1,
id: "t_p1",
name: "App",
arrivalTime: 0,
priority: 5,
color: "#CE93D8",
bursts: [],
threadModel: "MANY_TO_MANY",
kernelThreadCount: 2,
threadSchedulingPolicy: "PRIORITY",
threads: [
{
tid: "t_p1_t1",
name: "Worker1",
priority: 1,
bursts: [
{ type: "cpu", duration: 3 },
{ type: "io", duration: 2 },
{ type: "cpu", duration: 2 },
],
},
{
tid: "t_p1_t2",
name: "Worker2",
priority: 2,
bursts: [
{ type: "cpu", duration: 2 },
{ type: "io", duration: 3 },
{ type: "cpu", duration: 2 },
],
},
{
tid: "t_p1_t3",
name: "Worker3",
priority: 3,
bursts: [
{ type: "cpu", duration: 4 },
{ type: "io", duration: 1 },
{ type: "cpu", duration: 1 },
],
},
{
tid: "t_p1_t4",
name: "Worker4",
priority: 4,
bursts: [{ type: "cpu", duration: 3 }],
},
],
},
],
config: defaultConfig({ algorithm: "FCFS" }),
},
{
name: "thread_starvation",
description:
"Starvation de thread: processo com 3 threads em política Priority. A thread de baixa prioridade (T3) é indefinidamente preterida pelas de alta prioridade.",
processes: [
{
pid: 1,
id: "t_p1",
name: "App",
arrivalTime: 0,
priority: 5,
color: "#F06292",
bursts: [],
threadModel: "ONE_TO_ONE",
threadSchedulingPolicy: "PRIORITY",
threads: [
{
tid: "t_p1_t1",
name: "High-1",
priority: 1,
bursts: [
{ type: "cpu", duration: 2 },
{ type: "io", duration: 1 },
{ type: "cpu", duration: 2 },
{ type: "io", duration: 1 },
{ type: "cpu", duration: 2 },
],
},
{
tid: "t_p1_t2",
name: "High-2",
priority: 2,
bursts: [
{ type: "cpu", duration: 2 },
{ type: "io", duration: 1 },
{ type: "cpu", duration: 2 },
{ type: "io", duration: 1 },
{ type: "cpu", duration: 2 },
],
},
{
tid: "t_p1_t3",
name: "Low",
priority: 10,
bursts: [{ type: "cpu", duration: 3 }],
},
],
},
],
config: defaultConfig({ algorithm: "FCFS" }),
},
{
name: "threads_vs_processes",
description:
"Threads vs Processos: P1 usa 2 threads ONE_TO_ONE para uma tarefa. P2 faz o mesmo trabalho como processo único. Compare turnaround e throughput.",
processes: [
{
pid: 1,
id: "t_p1",
name: "Multi-T",
arrivalTime: 0,
priority: 5,
color: "#4FC3F7",
bursts: [],
threadModel: "ONE_TO_ONE",
threadSchedulingPolicy: "RR",
threadQuantum: 3,
threads: [
{
tid: "t_p1_t1",
name: "T1",
bursts: [
{ type: "cpu", duration: 3 },
{ type: "io", duration: 2 },
{ type: "cpu", duration: 2 },
],
},
{
tid: "t_p1_t2",
name: "T2",
bursts: [
{ type: "cpu", duration: 2 },
{ type: "io", duration: 3 },
{ type: "cpu", duration: 3 },
],
},
],
},
{
pid: 2,
id: "t_p2",
name: "Single-P",
arrivalTime: 0,
priority: 5,
color: "#FFB74D",
bursts: [
{ type: "cpu", duration: 5 },
{ type: "io", duration: 3 },
{ type: "cpu", duration: 5 },
],
},
],
config: defaultConfig({ algorithm: "RR", quantum: 4 }),
},
];
+74
View File
@@ -0,0 +1,74 @@
// ============================================================
// ThreadScheduler — Escalonamento intra-processo de threads
// ============================================================
//
// Responsável por selecionar qual thread executa dentro de um processo
// quando ele recebe tempo de CPU. Suporta três políticas:
//
// FCFS — FIFO estrito por ordem de chegada (arrivalTick)
// RR — Round-Robin com quantum intra-processo
// PRIORITY — Prioridade da thread (currentPriority, menor = maior prioridade)
import type { IntraProcessPolicy, ThreadRuntime } from "./types";
export class ThreadScheduler {
readonly policy: IntraProcessPolicy;
readonly quantum: number;
constructor(policy: IntraProcessPolicy = "FCFS", quantum = 2) {
this.policy = policy;
this.quantum = quantum;
}
/**
* Seleciona a próxima thread a executar a partir da fila de prontas.
* Não modifica o array — o engine remove a thread selecionada.
*/
select(readyThreads: readonly ThreadRuntime[]): ThreadRuntime | null {
if (readyThreads.length === 0) return null;
switch (this.policy) {
case "FCFS":
// Primeira a chegar, primeira a ser servida
return [...readyThreads].sort(
(a, b) =>
a.arrivalTick - b.arrivalTick ||
a.threadId.localeCompare(b.threadId),
)[0] ?? null;
case "RR":
// Ordem de fila (o engine mantém a ordem FIFO ao reinserir)
return readyThreads[0] ?? null;
case "PRIORITY":
// Menor currentPriority = maior prioridade; desempate por arrivalTick
return [...readyThreads].sort(
(a, b) =>
a.currentPriority - b.currentPriority ||
a.arrivalTick - b.arrivalTick ||
a.threadId.localeCompare(b.threadId),
)[0] ?? null;
}
}
/**
* Verifica se o quantum intra-processo da thread expirou.
* Apenas relevante para política RR.
*/
isQuantumExpired(thread: ThreadRuntime): boolean {
return this.policy === "RR" && thread.quantumRemaining <= 0;
}
/**
* Para política PRIORITY: indica se a thread candidata deve preemptar a atual.
*/
shouldPreemptThread(
current: ThreadRuntime,
candidate: ThreadRuntime,
): boolean {
return (
this.policy === "PRIORITY" &&
candidate.currentPriority < current.currentPriority
);
}
}
+86 -2
View File
@@ -11,7 +11,36 @@ export type SchedulingAlgorithm =
| "PRIORITY_P"
| "PRIORITY_RR"
| "MULTILEVEL"
| "HRRN";
| "HRRN"
| "MLFQ";
// --------------- Thread types --------------------------------
/**
* Modelo de mapeamento threads de usuário → threads de kernel.
*
* MANY_TO_ONE — N threads de usuário para 1 kernel thread (green threads).
* Uma thread em I/O bloqueia todo o processo.
* ONE_TO_ONE — 1 thread de usuário para 1 kernel thread (pthreads/POSIX).
* Threads bloqueiam independentemente.
* MANY_TO_MANY — N threads de usuário para M kernel threads (M ≤ N).
* Slots de kernel são liberados durante I/O e reatribuídos.
*/
export type ThreadModel =
| "MANY_TO_ONE"
| "ONE_TO_ONE"
| "MANY_TO_MANY";
/** Política de escalonamento intra-processo (entre threads da mesma processo). */
export type IntraProcessPolicy = "FCFS" | "RR" | "PRIORITY";
/** Definição estática de uma thread dentro de um processo. */
export interface Thread {
tid: string; // identificador único dentro do processo
name: string;
bursts: BurstSegment[]; // sequência própria de bursts CPU/IO
priority?: number; // 1 (mais alta) – 10 (mais baixa); default 5
}
// --------------- Static process definition ------------------
@@ -27,7 +56,13 @@ export interface Process {
arrivalTime: number; // tick of arrival (≥ 0)
priority: number; // 1 (highest) – 10 (lowest); default 5
color: string; // HEX color for Gantt display
bursts: BurstSegment[]; // must start (and end) with a CPU burst
bursts: BurstSegment[]; // must start (and end) with a CPU burst; ignored when threads defined
// ── Thread fields (optional — backward compatible) ──────
threads?: Thread[]; // se definido, bursts acima são ignorados
threadModel?: ThreadModel; // default ONE_TO_ONE quando threads definidas
threadSchedulingPolicy?: IntraProcessPolicy; // política intra-processo; default FCFS
threadQuantum?: number; // quantum para política RR intra-processo; default 2
kernelThreadCount?: number; // apenas MANY_TO_MANY: M ≤ threads.length; default ceil(N/2)
}
// --------------- Scheduler configuration --------------------
@@ -39,6 +74,10 @@ export interface MlqQueueDef {
quantum: number; // used only when algorithm === "RR"
}
export interface MlfqLevelDef {
quantum: number; // time slice for this level (ticks, >= 1)
}
export interface SchedulerConfig {
algorithm: SchedulingAlgorithm;
quantum: number; // for RR (default 2)
@@ -47,6 +86,8 @@ export interface SchedulerConfig {
agingEnabled: boolean; // prevent starvation in Priority scheduling
agingInterval: number; // ticks in Ready before priority-- (min 1)
mlqQueues?: MlqQueueDef[]; // queue definitions for MULTILEVEL algorithm
mlfqLevels?: MlfqLevelDef[]; // level definitions for MLFQ algorithm
mlfqBoostInterval?: number; // ticks in Ready before boost to level 0; 0 = disabled
}
// --------------- Runtime state ------------------------------
@@ -58,6 +99,28 @@ export type StateLabel =
| "Waiting" // executing I/O burst
| "Terminated"; // finished all bursts
/**
* Estado de runtime de uma thread individual.
* Espelha ProcessRuntime mas no nível de thread.
*/
export interface ThreadRuntime {
threadId: string;
processId: string;
state: StateLabel;
remainingBurst: number;
burstIndex: number;
arrivalTick: number; // mesmo que o processo pai
startTick: number | null; // primeiro tick em Running
finishTick: number | null;
waitingTime: number;
cpuTime: number;
ioTime: number;
responseTime: number | null;
quantumRemaining: number; // quantum intra-processo (para política RR)
currentPriority: number;
kernelWaiting?: boolean; // MANY_TO_MANY: aguardando slot de kernel thread
}
export interface ProcessRuntime {
// ---- spec fields ----
processId: string;
@@ -74,6 +137,7 @@ export interface ProcessRuntime {
// ---- engine-internal fields ----
quantumRemaining: number; // remaining quantum for RR; reset on dispatch
currentPriority: number; // mutable via aging; starts = Process.priority
mlfqLevel?: number; // current MLFQ level (0 = highest); undefined for non-MLFQ runs
}
// --------------- Tick snapshot ------------------------------
@@ -86,10 +150,30 @@ export interface SimTick {
states: Record<string, StateLabel>; // all processId → state
contextSwitching: boolean; // true during context-switch overhead
ctxSwitchForProcess: string | null; // incoming processId during ctx-switch overhead (null otherwise)
// ── Thread state (only present when at least one process has threads) ──
threadStates?: Record<string, Record<string, StateLabel>>; // processId → threadId → state
cpuThreads?: Record<string, string | null>; // processId → threadId ativo (null se nenhum)
kernelWaitingThreads?: Record<string, string[]>; // MANY_TO_MANY: threadIds em espera de slot
}
// --------------- Metrics ------------------------------------
/** Métricas por thread individual (calculadas após terminação). */
export interface ThreadMetrics {
threadId: string;
processId: string;
threadName: string;
processName?: string;
arrivalTime: number;
startTick: number;
finishTick: number;
responseTime: number;
turnaroundTime: number;
waitingTime: number;
cpuTime: number;
ioTime: number;
}
export interface ProcessMetrics {
processId: string;
arrivalTime: number;