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:
1 parent
532c14e382
commit
5761dfe70a
34 files changed
+3787
-332
No files matched your search
+30
-6
@@ -64,10 +64,10 @@ docker compose up
|
|||||||
|
|
||||||
## 🎮 How to use
|
## 🎮 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.
|
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, etc.).
|
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×).
|
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.
|
> 💡 **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 |
|
| **Round Robin (RR)** | ✅ | Quantum |
|
||||||
| **Priority NP**: Non-Preemptive Priority | ❌ | Aging (optional) |
|
| **Priority NP**: Non-Preemptive Priority | ❌ | Aging (optional) |
|
||||||
| **Priority P**: 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 |
|
| **HRRN**: Highest Response Ratio Next | ❌ | None |
|
||||||
| **Multilevel Queue** | ✅ | Quantum per queue |
|
| **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.
|
> See the [algorithms documentation](./docs/algorithms.en.md) for details on how each one works.
|
||||||
|
|
||||||
@@ -119,20 +121,30 @@ src/
|
|||||||
│ │ ├── priority_rr.ts
|
│ │ ├── priority_rr.ts
|
||||||
│ │ ├── hrrn.ts
|
│ │ ├── hrrn.ts
|
||||||
│ │ ├── multilevel.ts
|
│ │ ├── multilevel.ts
|
||||||
|
│ │ ├── mlfq.ts # Multilevel Feedback Queue
|
||||||
│ │ └── index.ts # Dispatch by algorithm
|
│ │ └── index.ts # Dispatch by algorithm
|
||||||
│ ├── engine.ts # SimulationEngine, orchestrates the ticks
|
│ ├── engine.ts # SimulationEngine, orchestrates the ticks (thread-aware)
|
||||||
│ ├── metrics.ts # Per-process and global metrics computation
|
│ ├── thread-scheduler.ts # Intra-process scheduler (FCFS / RR / Priority)
|
||||||
|
│ ├── metrics.ts # Per-process, global, and per-thread metrics computation
|
||||||
│ ├── types.ts # All TypeScript interfaces
|
│ ├── types.ts # All TypeScript interfaces
|
||||||
│ └── presets.ts # Pre-defined scenarios
|
│ └── presets.ts # Pre-defined scenarios (processes and threads)
|
||||||
│
|
│
|
||||||
├── store/ # Global state with Zustand
|
├── store/ # Global state with Zustand
|
||||||
├── components/ # React components
|
├── components/ # React components
|
||||||
│ ├── ProcessPanel/ # Process creation and editing
|
│ ├── ProcessPanel/ # Process creation and editing
|
||||||
|
│ │ ├── ThreadEditor.tsx # Thread editor (model, policy, per-thread bursts)
|
||||||
|
│ │ └── ...
|
||||||
│ ├── SchedulerPanel/ # Algorithm selection and parameters
|
│ ├── SchedulerPanel/ # Algorithm selection and parameters
|
||||||
│ ├── ControlBar/ # Play, Pause, Step, Reset, speed
|
│ ├── ControlBar/ # Play, Pause, Step, Reset, speed
|
||||||
│ ├── GanttChart/ # Interactive Gantt chart
|
│ ├── GanttChart/ # Interactive Gantt chart
|
||||||
|
│ │ ├── GanttThreadRow.tsx # Thread sub-row in the Gantt chart
|
||||||
|
│ │ └── ...
|
||||||
│ ├── MetricsPanel/ # Table, charts, and global metrics
|
│ ├── MetricsPanel/ # Table, charts, and global metrics
|
||||||
|
│ │ ├── ThreadMetricsTable.tsx # Per-thread metrics table
|
||||||
|
│ │ └── ...
|
||||||
│ └── shared/ # Buttons, badges, tooltips, modals
|
│ └── shared/ # Buttons, badges, tooltips, modals
|
||||||
|
│ ├── threadUtils.ts # Thread color derivation (HSL)
|
||||||
|
│ └── ...
|
||||||
│
|
│
|
||||||
└── hooks/ # Custom hooks (animation loop, export, etc.)
|
└── hooks/ # Custom hooks (animation loop, export, etc.)
|
||||||
```
|
```
|
||||||
@@ -143,6 +155,8 @@ src/
|
|||||||
|
|
||||||
Load classic scenarios with a single click to explore OS phenomena:
|
Load classic scenarios with a single click to explore OS phenomena:
|
||||||
|
|
||||||
|
**Processes:**
|
||||||
|
|
||||||
| Preset | What it demonstrates |
|
| Preset | What it demonstrates |
|
||||||
|--------|----------------------|
|
|--------|----------------------|
|
||||||
| `classic_fcfs` | Basic execution with FCFS, no I/O |
|
| `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 |
|
| `io_heavy` | Mix of CPU Bound vs. I/O Bound processes |
|
||||||
| `multilevel_demo` | Multiple queues with different priorities |
|
| `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
|
## 🧪 Tests
|
||||||
|
|||||||
@@ -62,10 +62,10 @@ docker compose up
|
|||||||
|
|
||||||
## 🎮 Como usar
|
## 🎮 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.
|
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, etc.).
|
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×).
|
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.
|
> 💡 **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 |
|
| **Round Robin (RR)** | ✅ | Quantum |
|
||||||
| **Priority NP**: Prioridade Não-Preemptiva | ❌ | Aging (opcional) |
|
| **Priority NP**: Prioridade Não-Preemptiva | ❌ | Aging (opcional) |
|
||||||
| **Priority P**: Prioridade Preemptiva | ✅ | Aging (opcional) |
|
| **Priority P**: Prioridade Preemptiva | ✅ | Aging (opcional) |
|
||||||
|
| **Priority RR**: Prioridade com Round Robin | ✅ | Quantum + Aging (opcional) |
|
||||||
| **HRRN**: Highest Response Ratio Next | ❌ | Nenhum |
|
| **HRRN**: Highest Response Ratio Next | ❌ | Nenhum |
|
||||||
| **Multilevel Queue** | ✅ | Quantum por fila |
|
| **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.
|
> Consulte a [documentação dos algoritmos](./docs/algorithms.md) para detalhes sobre como cada um funciona.
|
||||||
|
|
||||||
@@ -117,20 +119,30 @@ src/
|
|||||||
│ │ ├── priority_rr.ts
|
│ │ ├── priority_rr.ts
|
||||||
│ │ ├── hrrn.ts
|
│ │ ├── hrrn.ts
|
||||||
│ │ ├── multilevel.ts
|
│ │ ├── multilevel.ts
|
||||||
|
│ │ ├── mlfq.ts # Multilevel Feedback Queue
|
||||||
│ │ └── index.ts # Dispatch por algoritmo
|
│ │ └── index.ts # Dispatch por algoritmo
|
||||||
│ ├── engine.ts # SimulationEngine, orquestra os ticks
|
│ ├── engine.ts # SimulationEngine, orquestra os ticks (suporte a threads)
|
||||||
│ ├── metrics.ts # Cálculo de métricas por processo e globais
|
│ ├── 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
|
│ ├── 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
|
├── store/ # Estado global com Zustand
|
||||||
├── components/ # Componentes React
|
├── components/ # Componentes React
|
||||||
│ ├── ProcessPanel/ # Criação e edição de processos
|
│ ├── 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
|
│ ├── SchedulerPanel/ # Seleção de algoritmo e parâmetros
|
||||||
│ ├── ControlBar/ # Play, Pause, Step, Reset, velocidade
|
│ ├── ControlBar/ # Play, Pause, Step, Reset, velocidade
|
||||||
│ ├── GanttChart/ # Diagrama de Gantt interativo
|
│ ├── GanttChart/ # Diagrama de Gantt interativo
|
||||||
|
│ │ ├── GanttThreadRow.tsx # Sub-linha de thread no Gantt
|
||||||
|
│ │ └── ...
|
||||||
│ ├── MetricsPanel/ # Tabela, gráficos e métricas globais
|
│ ├── MetricsPanel/ # Tabela, gráficos e métricas globais
|
||||||
|
│ │ ├── ThreadMetricsTable.tsx # Tabela de métricas por thread
|
||||||
|
│ │ └── ...
|
||||||
│ └── shared/ # Botões, badges, tooltips, modais
|
│ └── shared/ # Botões, badges, tooltips, modais
|
||||||
|
│ ├── threadUtils.ts # Derivação de cores de threads (HSL)
|
||||||
|
│ └── ...
|
||||||
│
|
│
|
||||||
└── hooks/ # Hooks personalizados (loop de animação, export, etc.)
|
└── 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:
|
Carregue cenários clássicos com um clique para explorar fenômenos de SO:
|
||||||
|
|
||||||
|
**Processos:**
|
||||||
|
|
||||||
| Preset | O que demonstra |
|
| Preset | O que demonstra |
|
||||||
|--------|-----------------|
|
|--------|-----------------|
|
||||||
| `classic_fcfs` | Execução básica com FCFS, sem I/O |
|
| `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 |
|
| `io_heavy` | Mix de processos CPU Bound vs I/O Bound |
|
||||||
| `multilevel_demo` | Múltiplas filas com prioridades diferentes |
|
| `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
|
## 🧪 Testes
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -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
|
## References
|
||||||
|
|
||||||
- Silberschatz, A., Galvin, P. B., & Gagne, G., *Operating System Concepts*, 10th edition.
|
- Silberschatz, A., Galvin, P. B., & Gagne, G., *Operating System Concepts*, 10th edition.
|
||||||
|
|||||||
@@ -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 eventualmente 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
|
## Referências
|
||||||
|
|
||||||
- Silberschatz, A., Galvin, P. B., & Gagne, G., *Operating System Concepts*, 10ª edição.
|
- Silberschatz, A., Galvin, P. B., & Gagne, G., *Operating System Concepts*, 10ª edição.
|
||||||
|
|||||||
@@ -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
|
## Example
|
||||||
|
|
||||||
Consider three processes with the following bursts:
|
Consider three processes with the following bursts:
|
||||||
|
|||||||
@@ -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
|
## Exemplo
|
||||||
|
|
||||||
Considere três processos com os seguintes bursts:
|
Considere três processos com os seguintes bursts:
|
||||||
|
|||||||
Generated
+17
-9
@@ -147,6 +147,7 @@
|
|||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -519,6 +520,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19.0"
|
"node": ">=20.19.0"
|
||||||
},
|
},
|
||||||
@@ -567,6 +569,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19.0"
|
"node": ">=20.19.0"
|
||||||
}
|
}
|
||||||
@@ -1892,8 +1895,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
@@ -2034,6 +2036,7 @@
|
|||||||
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
|
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.18.0"
|
"undici-types": "~7.18.0"
|
||||||
}
|
}
|
||||||
@@ -2051,6 +2054,7 @@
|
|||||||
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
|
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
@@ -2062,6 +2066,7 @@
|
|||||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^18.0.0"
|
"@types/react": "^18.0.0"
|
||||||
}
|
}
|
||||||
@@ -2373,6 +2378,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -2725,8 +2731,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/eastasianwidth": {
|
"node_modules/eastasianwidth": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
@@ -3544,7 +3549,6 @@
|
|||||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"lz-string": "bin/bin.js"
|
"lz-string": "bin/bin.js"
|
||||||
}
|
}
|
||||||
@@ -3760,6 +3764,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -3802,7 +3807,6 @@
|
|||||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1",
|
"ansi-regex": "^5.0.1",
|
||||||
"ansi-styles": "^5.0.0",
|
"ansi-styles": "^5.0.0",
|
||||||
@@ -3818,7 +3822,6 @@
|
|||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -3829,7 +3832,6 @@
|
|||||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
@@ -3852,6 +3854,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
@@ -3864,6 +3867,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
@@ -3884,6 +3888,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/use-sync-external-store": "^0.0.6",
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
"use-sync-external-store": "^1.4.0"
|
"use-sync-external-store": "^1.4.0"
|
||||||
@@ -3960,7 +3965,8 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/redux-thunk": {
|
"node_modules/redux-thunk": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
@@ -4546,6 +4552,7 @@
|
|||||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
@@ -4644,6 +4651,7 @@
|
|||||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/expect": "3.2.4",
|
"@vitest/expect": "3.2.4",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pss",
|
"name": "pss",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"description": "Process Scheduler Simulator — an interactive educational tool that runs entirely in the browser",
|
"description": "Process Scheduler Simulator — an interactive educational tool that runs entirely in the browser",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ const ControlBar = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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"
|
role="toolbar"
|
||||||
aria-label="Simulation controls"
|
aria-label="Simulation controls"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useSimulationStore } from "@store/simulationStore";
|
|||||||
import { useProcessStore } from "@store/processStore";
|
import { useProcessStore } from "@store/processStore";
|
||||||
import { useGanttScroll } from "@hooks/useGanttScroll";
|
import { useGanttScroll } from "@hooks/useGanttScroll";
|
||||||
import GanttRow from "./GanttRow";
|
import GanttRow from "./GanttRow";
|
||||||
|
import GanttThreadRow from "./GanttThreadRow";
|
||||||
import GanttLegend from "./GanttLegend";
|
import GanttLegend from "./GanttLegend";
|
||||||
import GanttTooltip, { type TooltipData } from "./GanttTooltip";
|
import GanttTooltip, { type TooltipData } from "./GanttTooltip";
|
||||||
import { CELL_H, CELL_W } from "./constants";
|
import { CELL_H, CELL_W } from "./constants";
|
||||||
@@ -13,33 +14,29 @@ const GanttChart = () => {
|
|||||||
const status = useSimulationStore((s) => s.status);
|
const status = useSimulationStore((s) => s.status);
|
||||||
const processes = useProcessStore((s) => s.processes);
|
const processes = useProcessStore((s) => s.processes);
|
||||||
|
|
||||||
// Scrollable container ref for auto-scroll
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const currentTickIndex = ticks.length - 1;
|
const currentTickIndex = ticks.length - 1;
|
||||||
useGanttScroll(scrollRef, currentTickIndex, status === "running");
|
useGanttScroll(scrollRef, currentTickIndex, status === "running");
|
||||||
|
|
||||||
// Tooltip state
|
|
||||||
const [tooltip, setTooltip] = useState<TooltipData | null>(null);
|
const [tooltip, setTooltip] = useState<TooltipData | null>(null);
|
||||||
const handleMouseLeave = useCallback(() => setTooltip(null), []);
|
const handleMouseLeave = useCallback(() => setTooltip(null), []);
|
||||||
|
|
||||||
// Empty state
|
|
||||||
if (ticks.length === 0) {
|
if (ticks.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-slate-800 flex items-center justify-center text-slate-600 bg-slate-900/50 text-sm select-none"
|
<div
|
||||||
style={{ minHeight: 80 }}>
|
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
|
Start the simulation to see the Gantt chart
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── ruler (tick numbers) ──────────────────────────────────
|
|
||||||
const tickNumbers = ticks.map((t) => t.tick);
|
const tickNumbers = ticks.map((t) => t.tick);
|
||||||
const totalWidth = ticks.length * CELL_W;
|
const totalWidth = ticks.length * CELL_W;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-slate-800 bg-slate-950 flex flex-col">
|
<div className="border-b border-slate-800 bg-slate-950 flex flex-col">
|
||||||
|
|
||||||
{/* ── scrollable grid + cursor ── */}
|
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
className="overflow-x-auto overflow-y-hidden gantt-scrollbar relative"
|
className="overflow-x-auto overflow-y-hidden gantt-scrollbar relative"
|
||||||
@@ -48,9 +45,10 @@ const GanttChart = () => {
|
|||||||
|
|
||||||
{/* Tick ruler */}
|
{/* Tick ruler */}
|
||||||
<div className="flex sticky top-0 z-10 bg-slate-950 border-b border-slate-800">
|
<div className="flex sticky top-0 z-10 bg-slate-950 border-b border-slate-800">
|
||||||
{/* Spacer for sticky process labels column */}
|
<div
|
||||||
<div className="sticky left-0 z-20 bg-slate-950 border-r border-slate-700 shrink-0" style={{ width: 48, height: 16 }} />
|
className="sticky left-0 z-20 bg-slate-950 border-r border-slate-700 shrink-0"
|
||||||
|
style={{ width: 48, height: 16 }}
|
||||||
|
/>
|
||||||
{tickNumbers.map((t) => (
|
{tickNumbers.map((t) => (
|
||||||
<div
|
<div
|
||||||
key={t}
|
key={t}
|
||||||
@@ -62,28 +60,58 @@ const GanttChart = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Process rows */}
|
{/* Process rows (+ thread sub-rows) */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{processes.map((process) => (
|
{processes.map((process) => {
|
||||||
<div key={process.id} className="flex items-center border-b border-white/5 last:border-b-0">
|
const hasThreads = (process.threads?.length ?? 0) > 0;
|
||||||
{/* Process label (sticky on the left) */}
|
|
||||||
<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 }}>
|
|
||||||
{process.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<GanttRow
|
return (
|
||||||
process={process}
|
<div key={process.id}>
|
||||||
ticks={ticks}
|
{/* ── Linha do processo ─────────────────────────── */}
|
||||||
onCellMouseEnter={setTooltip}
|
<div
|
||||||
onCellMouseLeave={handleMouseLeave}
|
className={`flex items-center border-b border-white/5 ${hasThreads ? 'border-b-0' : ''}`}
|
||||||
/>
|
>
|
||||||
</div>
|
<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 }}
|
||||||
|
>
|
||||||
|
{process.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<GanttRow
|
||||||
|
process={process}
|
||||||
|
ticks={ticks}
|
||||||
|
onCellMouseEnter={setTooltip}
|
||||||
|
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 */}
|
{/* Current-tick cursor line */}
|
||||||
{currentTickIndex >= 0 && (
|
{currentTickIndex >= 0 && (
|
||||||
@@ -97,14 +125,7 @@ const GanttChart = () => {
|
|||||||
</div>
|
</div>
|
||||||
</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 />
|
<GanttLegend />
|
||||||
|
|
||||||
{/* ── tooltip ── */}
|
|
||||||
{tooltip && <GanttTooltip data={tooltip} />}
|
{tooltip && <GanttTooltip data={tooltip} />}
|
||||||
</div>
|
</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;
|
||||||
@@ -86,23 +86,61 @@ const AppHeader = () => {
|
|||||||
Preset Scenarios
|
Preset Scenarios
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-y-auto">
|
<div className="overflow-y-auto">
|
||||||
{presets.map((preset, i) => (
|
{/* Classic process presets */}
|
||||||
<button
|
<div className="px-3 py-1 text-[9px] text-slate-500 uppercase tracking-wider font-semibold bg-slate-800/80 sticky top-0">
|
||||||
key={preset.name}
|
Processes
|
||||||
onClick={() => handleLoadPreset(i)}
|
</div>
|
||||||
className="w-full text-left px-3 py-2.5 hover:bg-slate-700 transition-colors group"
|
{presets
|
||||||
>
|
.filter(p => !['many_to_one','one_to_one','many_to_many','thread_starvation','threads_vs_processes'].includes(p.name))
|
||||||
<div className="text-xs font-medium text-slate-200 group-hover:text-white">
|
.map((preset) => {
|
||||||
{preset.name}
|
const i = presets.indexOf(preset);
|
||||||
</div>
|
return (
|
||||||
<div className="text-[11px] text-slate-400 mt-0.5 leading-snug">
|
<button
|
||||||
{preset.description}
|
key={preset.name}
|
||||||
</div>
|
onClick={() => handleLoadPreset(i)}
|
||||||
<div className="text-[10px] text-blue-400 mt-0.5">
|
className="w-full text-left px-3 py-2.5 hover:bg-slate-700 transition-colors group"
|
||||||
{preset.config.algorithm}
|
>
|
||||||
</div>
|
<div className="text-xs font-medium text-slate-200 group-hover:text-white">
|
||||||
</button>
|
{preset.name}
|
||||||
))}
|
</div>
|
||||||
|
<div className="text-[11px] text-slate-400 mt-0.5 leading-snug">
|
||||||
|
{preset.description}
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-blue-400 mt-0.5">
|
||||||
|
{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>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -4,12 +4,6 @@ import SchedulerPanel from '../SchedulerPanel/SchedulerPanel';
|
|||||||
const Sidebar = () => {
|
const Sidebar = () => {
|
||||||
return (
|
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">
|
<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">
|
<div className="flex-1 overflow-y-auto overflow-x-hidden p-4 space-y-6 scrollbar-hide">
|
||||||
<ProcessPanel />
|
<ProcessPanel />
|
||||||
<div className="border-t border-slate-700 pt-4">
|
<div className="border-t border-slate-700 pt-4">
|
||||||
|
|||||||
@@ -28,6 +28,22 @@ const TOOLTIP_STYLE = {
|
|||||||
boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
|
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) => {
|
const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||||
if (metrics.length === 0) {
|
if (metrics.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -80,10 +96,9 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
|||||||
|
|
||||||
// ── Radar chart data (normalized 0-100 per dimension) ─────────
|
// ── Radar chart data (normalized 0-100 per dimension) ─────────
|
||||||
// Each "subject" is a process, each series is a 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 maxT = Math.max(...metrics.map(m => m.turnaroundTime), 1);
|
const maxR = Math.max(...metrics.map(m => m.responseTime), 1);
|
||||||
const maxR = Math.max(...metrics.map(m => m.responseTime), 1);
|
const maxW = Math.max(...metrics.map(m => m.waitingTime), 1);
|
||||||
const maxW = Math.max(...metrics.map(m => m.waitingTime), 1);
|
|
||||||
const maxCpu = Math.max(...metrics.map(m => m.cpuTime), 1);
|
const maxCpu = Math.max(...metrics.map(m => m.cpuTime), 1);
|
||||||
const maxIo = Math.max(...metrics.map(m => m.ioTime), 1);
|
const maxIo = Math.max(...metrics.map(m => m.ioTime), 1);
|
||||||
|
|
||||||
@@ -98,11 +113,11 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
|||||||
metrics.forEach(m => {
|
metrics.forEach(m => {
|
||||||
const key = getName(m.processId);
|
const key = getName(m.processId);
|
||||||
switch (dim) {
|
switch (dim) {
|
||||||
case 'Turnaround': row[key] = (m.turnaroundTime / maxT) * 100; break;
|
case 'Turnaround': row[key] = (m.turnaroundTime / maxT) * 100; break;
|
||||||
case 'Response': row[key] = (m.responseTime / maxR) * 100; break;
|
case 'Response': row[key] = (m.responseTime / maxR) * 100; break;
|
||||||
case 'Waiting': row[key] = (m.waitingTime / maxW) * 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;
|
case 'I/O Time': row[key] = (m.ioTime / maxIo) * 100; break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return row;
|
return row;
|
||||||
@@ -110,15 +125,18 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
|||||||
|
|
||||||
const showRadar = metrics.length >= 2;
|
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 (
|
return (
|
||||||
<div className={`p-3 flex gap-3 ${showRadar ? 'flex-row' : 'flex-col'}`}>
|
<div className="p-3 flex flex-col gap-6">
|
||||||
{/* ── Radar chart ───────────────────────────────────────── */}
|
{/* ── Radar chart ───────────────────────────────────────── */}
|
||||||
{showRadar && (
|
{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">
|
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
||||||
Comparative Profile (normalized)
|
Comparative Profile (normalized)
|
||||||
</p>
|
</p>
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
<ChartBox height={360}>
|
||||||
<RadarChart data={radarData} margin={{ top: 8, right: 20, bottom: 8, left: 20 }}>
|
<RadarChart data={radarData} margin={{ top: 8, right: 20, bottom: 8, left: 20 }}>
|
||||||
<PolarGrid stroke="#334155" />
|
<PolarGrid stroke="#334155" />
|
||||||
<PolarAngleAxis
|
<PolarAngleAxis
|
||||||
@@ -142,16 +160,16 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
|||||||
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
|
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
|
||||||
/>
|
/>
|
||||||
</RadarChart>
|
</RadarChart>
|
||||||
</ResponsiveContainer>
|
</ChartBox>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Stacked bar chart ─────────────────────────────────── */}
|
{/* ── 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">
|
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
||||||
Breakdown per Process (ticks)
|
Breakdown per Process (ticks)
|
||||||
</p>
|
</p>
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
<ChartBox height={360}>
|
||||||
<BarChart data={barData} margin={{ top: 8, right: 12, left: -8, bottom: 4 }}>
|
<BarChart data={barData} margin={{ top: 8, right: 12, left: -8, bottom: 4 }}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
||||||
<XAxis
|
<XAxis
|
||||||
@@ -183,7 +201,7 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
|||||||
<Bar dataKey="CPU" stackId="a" fill="#22c55e" radius={[0, 0, 0, 0]} />
|
<Bar dataKey="CPU" stackId="a" fill="#22c55e" radius={[0, 0, 0, 0]} />
|
||||||
<Bar dataKey="I/O" stackId="a" fill="#06b6d4" radius={[4, 4, 0, 0]} />
|
<Bar dataKey="I/O" stackId="a" fill="#06b6d4" radius={[4, 4, 0, 0]} />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ChartBox>
|
||||||
<p className="text-center text-[9px] text-slate-600 mt-0.5">
|
<p className="text-center text-[9px] text-slate-600 mt-0.5">
|
||||||
Waiting + CPU + I/O = Turnaround
|
Waiting + CPU + I/O = Turnaround
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Download, BarChart2, Table, Globe } from 'lucide-react';
|
import { Download, BarChart2, Table, Globe, Layers } from 'lucide-react';
|
||||||
import { computeProcessMetrics, computeGlobalMetrics } from '@core/metrics';
|
import { computeProcessMetrics, computeGlobalMetrics, computeThreadMetrics } from '@core/metrics';
|
||||||
import { useSimulationStore } from '../../store/simulationStore';
|
import { useSimulationStore } from '../../store/simulationStore';
|
||||||
import { useProcessStore } from '../../store/processStore';
|
import { useProcessStore } from '../../store/processStore';
|
||||||
import { useExport } from '../../hooks/useExport';
|
import { useExport } from '../../hooks/useExport';
|
||||||
import MetricsTable from './MetricsTable';
|
import MetricsTable from './MetricsTable';
|
||||||
import GlobalMetrics from './GlobalMetrics';
|
import GlobalMetrics from './GlobalMetrics';
|
||||||
import MetricsChart from './MetricsChart';
|
import MetricsChart from './MetricsChart';
|
||||||
|
import ThreadMetricsTable from './ThreadMetricsTable';
|
||||||
|
|
||||||
type Tab = 'table' | 'global' | 'charts';
|
type Tab = 'table' | 'global' | 'charts' | 'threads';
|
||||||
|
|
||||||
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} /> },
|
|
||||||
];
|
|
||||||
|
|
||||||
const MetricsPanel = () => {
|
const MetricsPanel = () => {
|
||||||
const [activeTab, setActiveTab] = useState<Tab>('table');
|
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 runtimes = engine?.runtimeStates ?? [];
|
||||||
const completedRuntimes = runtimes.filter(rt => rt.finishTick !== null);
|
const completedRuntimes = runtimes.filter(rt => rt.finishTick !== null);
|
||||||
const processMetrics = completedRuntimes.map(rt => computeProcessMetrics(rt));
|
const processMetrics = completedRuntimes.map(rt => computeProcessMetrics(rt));
|
||||||
const globalMetrics = computeGlobalMetrics(processMetrics, ticks.length);
|
const globalMetrics = computeGlobalMetrics(processMetrics, ticks.length);
|
||||||
const processMap = Object.fromEntries(processes.map(p => [p.id, p]));
|
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 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 (
|
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 */}
|
{/* Panel header with tabs + export button */}
|
||||||
<div className="flex items-center px-3 border-b border-slate-700 shrink-0 h-10 gap-1">
|
<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
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
onClick={() => setActiveTab(tab.id)}
|
onClick={() => setActiveTab(tab.id)}
|
||||||
@@ -62,6 +76,11 @@ const MetricsPanel = () => {
|
|||||||
>
|
>
|
||||||
{tab.icon}
|
{tab.icon}
|
||||||
{tab.label}
|
{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>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -80,7 +99,7 @@ const MetricsPanel = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab content */}
|
{/* Tab content */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto min-h-0">
|
||||||
{activeTab === 'table' && (
|
{activeTab === 'table' && (
|
||||||
<MetricsTable metrics={processMetrics} processMap={processMap} />
|
<MetricsTable metrics={processMetrics} processMap={processMap} />
|
||||||
)}
|
)}
|
||||||
@@ -90,6 +109,9 @@ const MetricsPanel = () => {
|
|||||||
{activeTab === 'charts' && (
|
{activeTab === 'charts' && (
|
||||||
<MetricsChart metrics={processMetrics} processMap={processMap} />
|
<MetricsChart metrics={processMetrics} processMap={processMap} />
|
||||||
)}
|
)}
|
||||||
|
{activeTab === 'threads' && (
|
||||||
|
<ThreadMetricsTable metrics={threadMetrics} processMap={processMap} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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;
|
||||||
@@ -10,10 +10,28 @@ interface ProcessCardProps {
|
|||||||
onRemove: (id: string) => void;
|
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 ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
||||||
const bound = classifyBound(process.bursts);
|
const hasThreads = (process.threads?.length ?? 0) > 0;
|
||||||
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 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';
|
const boundVariant = bound === 'CPU Bound' ? 'cpu' : bound === 'I/O Bound' ? 'io' : 'balanced';
|
||||||
|
|
||||||
@@ -38,7 +56,13 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="font-semibold text-sm text-slate-100 truncate">{process.name}</span>
|
<span className="font-semibold text-sm text-slate-100 truncate">{process.name}</span>
|
||||||
<Badge variant={boundVariant}>{bound}</Badge>
|
{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>
|
||||||
<div className="flex flex-wrap gap-x-3 gap-y-0.5 text-[11px] text-slate-400">
|
<div className="flex flex-wrap gap-x-3 gap-y-0.5 text-[11px] text-slate-400">
|
||||||
<span>Arrival: <span className="text-slate-300 font-medium">{process.arrivalTime}</span></span>
|
<span>Arrival: <span className="text-slate-300 font-medium">{process.arrivalTime}</span></span>
|
||||||
@@ -47,7 +71,11 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
|||||||
{totalIo > 0 && (
|
{totalIo > 0 && (
|
||||||
<span>I/O: <span className="text-cyan-400 font-medium">{totalIo}</span></span>
|
<span>I/O: <span className="text-cyan-400 font-medium">{totalIo}</span></span>
|
||||||
)}
|
)}
|
||||||
<span>Bursts: <span className="text-slate-300 font-medium">{process.bursts.length}</span></span>
|
{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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Plus, Minus, Check, X } from 'lucide-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 { useProcessStore } from '../../store/processStore';
|
||||||
import IconButton from '../shared/IconButton';
|
import IconButton from '../shared/IconButton';
|
||||||
import { generateId, nextColor } from '../shared/processUtils';
|
import { generateId, nextColor } from '../shared/processUtils';
|
||||||
|
import ThreadEditor from './ThreadEditor';
|
||||||
|
|
||||||
interface ProcessFormProps {
|
interface ProcessFormProps {
|
||||||
/** If provided, the form edits this process; otherwise creates a new one. */
|
|
||||||
editingProcess?: Process | null;
|
editingProcess?: Process | null;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FormTab = 'bursts' | 'threads';
|
||||||
|
|
||||||
const DEFAULT_BURSTS: BurstSegment[] = [{ type: 'cpu', duration: 4 }];
|
const DEFAULT_BURSTS: BurstSegment[] = [{ type: 'cpu', duration: 4 }];
|
||||||
|
|
||||||
const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||||
@@ -19,12 +21,22 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
|
|
||||||
const isEditing = !!editingProcess;
|
const isEditing = !!editingProcess;
|
||||||
|
|
||||||
|
// ── Campos base ─────────────────────────────────────────────────────────
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [arrivalTime, setArrivalTime] = useState(0);
|
const [arrivalTime, setArrivalTime] = useState(0);
|
||||||
const [priority, setPriority] = useState(5);
|
const [priority, setPriority] = useState(5);
|
||||||
const [color, setColor] = useState(nextColor);
|
const [color, setColor] = useState(nextColor);
|
||||||
const [bursts, setBursts] = useState<BurstSegment[]>(DEFAULT_BURSTS);
|
const [bursts, setBursts] = useState<BurstSegment[]>(DEFAULT_BURSTS);
|
||||||
const [errors, setErrors] = useState<string[]>([]);
|
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(() => {
|
useEffect(() => {
|
||||||
if (editingProcess) {
|
if (editingProcess) {
|
||||||
@@ -33,24 +45,38 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
setPriority(editingProcess.priority);
|
setPriority(editingProcess.priority);
|
||||||
setColor(editingProcess.color);
|
setColor(editingProcess.color);
|
||||||
setBursts([...editingProcess.bursts.map((b: BurstSegment) => ({ ...b }))]);
|
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 {
|
} else {
|
||||||
setName('');
|
setName('');
|
||||||
setArrivalTime(0);
|
setArrivalTime(0);
|
||||||
setPriority(5);
|
setPriority(5);
|
||||||
setColor(nextColor());
|
setColor(nextColor());
|
||||||
setBursts([{ type: 'cpu', duration: 4 }]);
|
setBursts([{ type: 'cpu', duration: 4 }]);
|
||||||
|
setHasThreads(false);
|
||||||
|
setThreads([]);
|
||||||
|
setThreadModel('ONE_TO_ONE');
|
||||||
|
setThreadPolicy('FCFS');
|
||||||
|
setThreadQuantum(2);
|
||||||
|
setKernelThreadCount(2);
|
||||||
|
setActiveTab('bursts');
|
||||||
}
|
}
|
||||||
setErrors([]);
|
setErrors([]);
|
||||||
}, [editingProcess]);
|
}, [editingProcess]);
|
||||||
|
|
||||||
// ---- Burst helpers ----
|
// ── Helpers de bursts ───────────────────────────────────────────────────
|
||||||
|
|
||||||
const updateBurstDuration = (index: number, value: number) => {
|
const updateBurstDuration = (index: number, value: number) => {
|
||||||
setBursts(prev => prev.map((b, i) => i === index ? { ...b, duration: Math.max(1, value) } : b));
|
setBursts(prev => prev.map((b, i) => i === index ? { ...b, duration: Math.max(1, value) } : b));
|
||||||
};
|
};
|
||||||
|
|
||||||
const addBurst = () => {
|
const addBurst = () => {
|
||||||
// enforce alternating cpu/io pattern
|
|
||||||
const lastType = bursts[bursts.length - 1]?.type ?? 'io';
|
const lastType = bursts[bursts.length - 1]?.type ?? 'io';
|
||||||
const nextType: BurstSegment['type'] = lastType === 'cpu' ? 'io' : 'cpu';
|
const nextType: BurstSegment['type'] = lastType === 'cpu' ? 'io' : 'cpu';
|
||||||
setBursts(prev => [...prev, { type: nextType, duration: 2 }]);
|
setBursts(prev => [...prev, { type: nextType, duration: 2 }]);
|
||||||
@@ -58,36 +84,90 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
|
|
||||||
const removeBurst = (index: number) => {
|
const removeBurst = (index: number) => {
|
||||||
if (bursts.length <= 1) return;
|
if (bursts.length <= 1) return;
|
||||||
setBursts(prev => {
|
setBursts(prev => prev.filter((_, i) => i !== index));
|
||||||
const next = prev.filter((_, i) => i !== index);
|
|
||||||
// Safe-guard: ensure it still starts (and ends) with cpu
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- 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 validate = (): boolean => {
|
||||||
const errs: string[] = [];
|
const errs: string[] = [];
|
||||||
if (!name.trim()) errs.push('Name is required');
|
if (!name.trim()) errs.push('Name is required');
|
||||||
if (name.trim().length > 8) errs.push('Name must be at most 8 characters');
|
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 (arrivalTime < 0) errs.push('Arrival Time must be ≥ 0');
|
||||||
if (priority < 1 || priority > 10) errs.push('Priority must be between 1 and 10');
|
if (priority < 1 || priority > 10) errs.push('Priority must be between 1 and 10');
|
||||||
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 (hasThreads) {
|
||||||
if (bursts.at(-1)?.type !== 'cpu') errs.push('The last burst must be CPU');
|
if (threads.length === 0) errs.push('At least one thread is required');
|
||||||
bursts.forEach((b, i) => {
|
threads.forEach((t, ti) => {
|
||||||
if (b.duration < 1) errs.push(`Burst ${i + 1}: duration must be ≥ 1`);
|
if (!t.name.trim()) errs.push(`Thread ${ti + 1}: name is required`);
|
||||||
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 (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`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setErrors(errs);
|
setErrors(errs);
|
||||||
return errs.length === 0;
|
return errs.length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (!validate()) return;
|
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) {
|
if (isEditing && editingProcess) {
|
||||||
updateProcess(editingProcess.id, { name: name.trim(), arrivalTime, priority, color, bursts });
|
updateProcess(editingProcess.id, processData);
|
||||||
} else {
|
} else {
|
||||||
addProcess({
|
addProcess({
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
@@ -95,7 +175,17 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
arrivalTime,
|
arrivalTime,
|
||||||
priority,
|
priority,
|
||||||
color,
|
color,
|
||||||
bursts,
|
bursts: hasThreads ? [] : bursts,
|
||||||
|
...(hasThreads
|
||||||
|
? {
|
||||||
|
threads,
|
||||||
|
threadModel,
|
||||||
|
threadSchedulingPolicy: threadPolicy,
|
||||||
|
threadQuantum,
|
||||||
|
kernelThreadCount:
|
||||||
|
threadModel === 'MANY_TO_MANY' ? kernelThreadCount : undefined,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
@@ -118,7 +208,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
{/* Basic fields */}
|
{/* Basic fields */}
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="col-span-2 flex gap-3">
|
<div className="col-span-2 flex gap-3">
|
||||||
{/* Name */}
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<label className={labelCls} htmlFor="pf-name">Name</label>
|
<label className={labelCls} htmlFor="pf-name">Name</label>
|
||||||
<input
|
<input
|
||||||
@@ -132,7 +221,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
aria-label="Process name"
|
aria-label="Process name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Color */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className={labelCls} htmlFor="pf-color">Color</label>
|
<label className={labelCls} htmlFor="pf-color">Color</label>
|
||||||
<input
|
<input
|
||||||
@@ -146,7 +234,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Arrival */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className={labelCls} htmlFor="pf-arrival">Arrival Time</label>
|
<label className={labelCls} htmlFor="pf-arrival">Arrival Time</label>
|
||||||
<input
|
<input
|
||||||
@@ -160,7 +247,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Priority */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className={labelCls} htmlFor="pf-priority">Priority (1–10)</label>
|
<label className={labelCls} htmlFor="pf-priority">Priority (1–10)</label>
|
||||||
<input
|
<input
|
||||||
@@ -176,51 +262,91 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bursts editor */}
|
{/* Toggle threads */}
|
||||||
<div>
|
<div className="flex items-center justify-between border-t border-slate-700 pt-3">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div>
|
||||||
<label className="text-xs text-slate-400 font-medium">Bursts (CPU → I/O → CPU…)</label>
|
<span className="text-xs text-slate-300 font-medium">Use Threads</span>
|
||||||
<IconButton size="sm" variant="ghost" label="Add burst" onClick={addBurst}>
|
<p className="text-[10px] text-slate-500">Substitui bursts por threads com execução própria</p>
|
||||||
<Plus size={13} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1.5" data-testid="burst-list">
|
|
||||||
{bursts.map((burst, idx) => (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
className={`flex items-center gap-2 px-2 py-1.5 rounded-md border ${
|
|
||||||
burst.type === 'cpu'
|
|
||||||
? 'border-emerald-700/50 bg-emerald-900/20'
|
|
||||||
: 'border-cyan-700/50 bg-cyan-900/20'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className={`text-[10px] font-bold uppercase w-7 tracking-wider ${burst.type === 'cpu' ? 'text-emerald-400' : 'text-cyan-400'}`}>
|
|
||||||
{burst.type}
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
value={burst.duration}
|
|
||||||
onChange={e => updateBurstDuration(idx, Number(e.target.value))}
|
|
||||||
className="w-full rounded bg-slate-700/60 border border-slate-600/50 text-slate-200 text-xs px-2 py-0.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
||||||
aria-label={`Burst ${idx + 1} duration`}
|
|
||||||
/>
|
|
||||||
<span className="text-[10px] text-slate-500">ticks</span>
|
|
||||||
<IconButton
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
label={`Remove burst ${idx + 1}`}
|
|
||||||
onClick={() => removeBurst(idx)}
|
|
||||||
disabled={bursts.length <= 1}
|
|
||||||
className="hover:text-red-400"
|
|
||||||
>
|
|
||||||
<Minus size={12} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</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>
|
</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>
|
||||||
|
<IconButton size="sm" variant="ghost" label="Add burst" onClick={addBurst}>
|
||||||
|
<Plus size={13} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5" data-testid="burst-list">
|
||||||
|
{bursts.map((burst, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={`flex items-center gap-2 px-2 py-1.5 rounded-md border ${
|
||||||
|
burst.type === 'cpu'
|
||||||
|
? 'border-emerald-700/50 bg-emerald-900/20'
|
||||||
|
: 'border-cyan-700/50 bg-cyan-900/20'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className={`text-[10px] font-bold uppercase w-7 tracking-wider ${burst.type === 'cpu' ? 'text-emerald-400' : 'text-cyan-400'}`}>
|
||||||
|
{burst.type}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={burst.duration}
|
||||||
|
onChange={e => updateBurstDuration(idx, Number(e.target.value))}
|
||||||
|
className="w-full rounded bg-slate-700/60 border border-slate-600/50 text-slate-200 text-xs px-2 py-0.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||||
|
aria-label={`Burst ${idx + 1} duration`}
|
||||||
|
/>
|
||||||
|
<span className="text-[10px] text-slate-500">ticks</span>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
label={`Remove burst ${idx + 1}`}
|
||||||
|
onClick={() => removeBurst(idx)}
|
||||||
|
disabled={bursts.length <= 1}
|
||||||
|
className="hover:text-red-400"
|
||||||
|
>
|
||||||
|
<Minus size={12} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</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 */}
|
{/* Validation errors */}
|
||||||
{errors.length > 0 && (
|
{errors.length > 0 && (
|
||||||
<ul className="space-y-0.5" role="alert" aria-live="polite">
|
<ul className="space-y-0.5" role="alert" aria-live="polite">
|
||||||
|
|||||||
@@ -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 }[] = [
|
const ALGORITHM_OPTIONS: { value: SchedulingAlgorithm; label: string; disabled?: boolean }[] = [
|
||||||
{ value: 'FCFS', label: 'FCFS — First Come First Served' },
|
{ value: 'FCFS', label: 'FCFS — First Come First Served' },
|
||||||
{ value: 'SJF_NP', label: 'SJF — Shortest Job First (NP)' },
|
{ value: 'SJF_NP', label: 'SJF — Shortest Job First (Non-preemptive)' },
|
||||||
{ value: 'SJF_P', label: 'SRTF — Shortest Remaining Time (P)' },
|
{ value: 'SJF_P', label: 'SRTF — Shortest Remaining Time (Preemptive)' },
|
||||||
{ value: 'RR', label: 'RR — Round Robin' },
|
{ value: 'RR', label: 'RR — Round Robin' },
|
||||||
{ value: 'PRIORITY_NP',label: 'Priority (Non-preemptive)' },
|
{ value: 'PRIORITY_NP',label: 'Priority (Non-preemptive)' },
|
||||||
{ value: 'PRIORITY_P', label: 'Priority (Preemptive)' },
|
{ value: 'PRIORITY_P', label: 'Priority (Preemptive)' },
|
||||||
{ value: 'PRIORITY_RR',label: 'Priority Round Robin (real-time)' },
|
{ value: 'PRIORITY_RR',label: 'Priority Round Robin (real-time)' },
|
||||||
{ value: 'HRRN', label: 'HRRN — Highest Response Ratio Next' },
|
{ 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 = () => {
|
const AlgorithmSelector = () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Plus, Trash2 } from 'lucide-react';
|
import { Plus, Trash2 } from 'lucide-react';
|
||||||
import { useProcessStore } from '../../store/processStore';
|
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
|
// Algorithms that require a global quantum parameter
|
||||||
const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'PRIORITY_RR']);
|
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']);
|
const AGING_ALGORITHMS = new Set<SchedulingAlgorithm>(['PRIORITY_NP', 'PRIORITY_P', 'PRIORITY_RR']);
|
||||||
|
|
||||||
// Algorithms that are preemptive by definition (used for read-only info only)
|
// 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 inputCls = 'w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-sm px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500';
|
||||||
const labelCls = 'block text-xs text-slate-400 mb-1';
|
const 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 SchedulerConfig = () => {
|
||||||
const config = useProcessStore(s => s.config);
|
const config = useProcessStore(s => s.config);
|
||||||
const setConfig = useProcessStore(s => s.setConfig);
|
const setConfig = useProcessStore(s => s.setConfig);
|
||||||
@@ -250,6 +339,16 @@ const SchedulerConfig = () => {
|
|||||||
onChange={queues => setConfig({ mlqQueues: queues })}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)}`;
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -66,4 +66,16 @@ export abstract class BaseAlgorithm {
|
|||||||
public static tiebreak(a: ProcessRuntime, b: ProcessRuntime): number {
|
public static tiebreak(a: ProcessRuntime, b: ProcessRuntime): number {
|
||||||
return _tiebreak(a, b);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import { PriorityNonPreemptiveAlgorithm, PriorityPreemptiveAlgorithm } from "./p
|
|||||||
import { PriorityRoundRobinAlgorithm } from "./priority_rr";
|
import { PriorityRoundRobinAlgorithm } from "./priority_rr";
|
||||||
import { HRRNAlgorithm } from "./hrrn";
|
import { HRRNAlgorithm } from "./hrrn";
|
||||||
import { MultilevelQueueAlgorithm } from "./multilevel";
|
import { MultilevelQueueAlgorithm } from "./multilevel";
|
||||||
|
import { MLFQAlgorithm } from "./mlfq";
|
||||||
|
|
||||||
export type { BaseAlgorithm };
|
export type { BaseAlgorithm };
|
||||||
|
|
||||||
@@ -28,4 +29,5 @@ export const algorithms: Record<SchedulingAlgorithm, BaseAlgorithm> = {
|
|||||||
PRIORITY_RR: new PriorityRoundRobinAlgorithm(),
|
PRIORITY_RR: new PriorityRoundRobinAlgorithm(),
|
||||||
HRRN: new HRRNAlgorithm(),
|
HRRN: new HRRNAlgorithm(),
|
||||||
MULTILEVEL: new MultilevelQueueAlgorithm(),
|
MULTILEVEL: new MultilevelQueueAlgorithm(),
|
||||||
|
MLFQ: new MLFQAlgorithm(),
|
||||||
};
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+667
-137
@@ -4,9 +4,12 @@ import type {
|
|||||||
SchedulerConfig,
|
SchedulerConfig,
|
||||||
SimTick,
|
SimTick,
|
||||||
StateLabel,
|
StateLabel,
|
||||||
|
Thread,
|
||||||
|
ThreadRuntime,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { algorithms } from "./algorithms/index";
|
import { algorithms } from "./algorithms/index";
|
||||||
import type { BaseAlgorithm } 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.
|
* 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
|
* Responsável por avançar o simulador tick a tick, aplicando o algoritmo
|
||||||
* de escalonamento configurado e mantendo o estado de todos os processos.
|
* 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()`):
|
* Ciclo de vida de um único tick (método `step()`):
|
||||||
* 1. Chegada — processos cujo `arrivalTime` é igual ao tick atual
|
* 1. Chegada — New → Ready para processos/threads que chegam neste tick.
|
||||||
* transitam New → Ready e entram na fila de prontos.
|
* 2. Preempção — Algoritmo decide se processo em CPU deve ceder lugar.
|
||||||
* 2. Preempção — o algoritmo decide se o processo em CPU deve ceder
|
* 3. Despacho — Seleciona e despacha próximo processo (e thread).
|
||||||
* lugar a um candidato mais prioritário.
|
* 4. Snapshot — Captura estado completo (processos + threads).
|
||||||
* 3. Despacho — seleciona e despacha o próximo processo (com overhead
|
* 5. Passagem do tempo — Decrementa bursts, acumula tempos.
|
||||||
* de troca de contexto, se configurado).
|
* 6. Aging — Envelhecimento de prioridade anti-starvation.
|
||||||
* 4. Snapshot — captura o estado de todos os processos neste tick.
|
* 7. Conclusões — Trata término de bursts de CPU/IO (processo e threads).
|
||||||
* 5. Passagem do tempo — decrementa bursts, acumula tempos de CPU/IO/espera.
|
|
||||||
* 6. Aging — decrementa prioridade dos processos em espera prolongada.
|
|
||||||
* 7. Conclusões — trata término de bursts de CPU e IO; avança para o
|
|
||||||
* próximo burst ou termina o processo.
|
|
||||||
*
|
|
||||||
* Uso típico:
|
|
||||||
* ```ts
|
|
||||||
* const engine = new SimulationEngine(processes, config);
|
|
||||||
* const ticks = engine.runAll(); // executa até todos os processos terminarem
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Invariante: após `runAll()`, `isFinished` é `true` e todos os runtimes
|
|
||||||
* têm `state === "Terminated"`.
|
|
||||||
*/
|
*/
|
||||||
export class SimulationEngine {
|
export class SimulationEngine {
|
||||||
// ─── Campos privados ──────────────────────────────────────────────────────
|
// ─── Campos privados — processos ──────────────────────────────────────────
|
||||||
|
|
||||||
private readonly _processes: Process[];
|
private readonly _processes: Process[];
|
||||||
private readonly _config: SchedulerConfig;
|
private readonly _config: SchedulerConfig;
|
||||||
private readonly _algorithm: BaseAlgorithm;
|
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 _runtimes: Map<string, ProcessRuntime>;
|
||||||
private _cpuProcess: ProcessRuntime | null;
|
private _cpuProcess: ProcessRuntime | null;
|
||||||
private _readyQueue: ProcessRuntime[]; // array ordenado (FIFO para RR)
|
private _readyQueue: ProcessRuntime[];
|
||||||
private _ioQueue: ProcessRuntime[];
|
private _ioQueue: ProcessRuntime[];
|
||||||
private _contextSwitchRemaining: number;
|
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 ───────────────────────────────────────────────────────────
|
// ─── Construtor ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -64,37 +91,47 @@ export class SimulationEngine {
|
|||||||
this._ioQueue = [];
|
this._ioQueue = [];
|
||||||
this._contextSwitchRemaining = 0;
|
this._contextSwitchRemaining = 0;
|
||||||
this._prevCpuProcessId = null;
|
this._prevCpuProcessId = null;
|
||||||
|
|
||||||
|
// Inicializa runtimes de processo
|
||||||
this._runtimes = new Map(
|
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 ──────────────────────────────────────────────────────────
|
// ─── 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 {
|
step(): SimTick {
|
||||||
if (this.isFinished) throw new Error("Simulation is already finished");
|
if (this.isFinished) throw new Error("Simulation is already finished");
|
||||||
|
|
||||||
this._tick++;
|
this._tick++;
|
||||||
|
|
||||||
this._handleArrivals(); // 1. New → Ready para quem chega agora
|
this._handleArrivals();
|
||||||
this._checkPreemption(); // 2. Preempta CPU se candidato melhor chegou
|
this._checkPreemption();
|
||||||
this._dispatchNext(); // 3. Despacha próximo (ou inicia context switch)
|
this._dispatchNext();
|
||||||
const snapshot = this._snapshot(); // 4. Captura estado DURANTE este tick
|
const snapshot = this._snapshot();
|
||||||
this._advanceTime(); // 5. Decrementa bursts, acumula métricas
|
this._advanceTime();
|
||||||
this._applyAging(); // 6. Aging de prioridade (se habilitado)
|
this._applyAging();
|
||||||
this._handleCpuCompletion(); // 7a. Finaliza burst/quantum de CPU
|
this._handleCpuCompletion();
|
||||||
this._handleIoCompletions(); // 7b. Finaliza bursts de I/O
|
this._handleIoCompletions();
|
||||||
|
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Executa até o fim e retorna todos os snapshots de tick. */
|
|
||||||
runAll(): SimTick[] {
|
runAll(): SimTick[] {
|
||||||
const results: SimTick[] = [];
|
const results: SimTick[] = [];
|
||||||
while (!this.isFinished) {
|
while (!this.isFinished) {
|
||||||
@@ -106,7 +143,6 @@ export class SimulationEngine {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Verdadeiro quando todos os processos estão no estado "Terminated". */
|
|
||||||
get isFinished(): boolean {
|
get isFinished(): boolean {
|
||||||
for (const rt of this._runtimes.values()) {
|
for (const rt of this._runtimes.values()) {
|
||||||
if (rt.state !== "Terminated") return false;
|
if (rt.state !== "Terminated") return false;
|
||||||
@@ -114,23 +150,34 @@ export class SimulationEngine {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Número do tick atual (−1 antes do primeiro `step()`). */
|
|
||||||
get currentTick(): number {
|
get currentTick(): number {
|
||||||
return this._tick;
|
return this._tick;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Array com o estado de runtime de todos os processos. */
|
|
||||||
get runtimeStates(): ProcessRuntime[] {
|
get runtimeStates(): ProcessRuntime[] {
|
||||||
return Array.from(this._runtimes.values());
|
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 ──────────────────────────────────────────
|
// ─── Fases do ciclo de simulação ──────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fase 1 — Chegada de processos.
|
* Fase 1 — Chegada de processos (e inicialização de threads).
|
||||||
*
|
|
||||||
* Processos cujo `arrivalTime` é igual ao tick atual transitam do estado
|
|
||||||
* "New" para "Ready" e são inseridos na fila de prontos.
|
|
||||||
*/
|
*/
|
||||||
private _handleArrivals(): void {
|
private _handleArrivals(): void {
|
||||||
for (const p of this._processes) {
|
for (const p of this._processes) {
|
||||||
@@ -138,17 +185,40 @@ export class SimulationEngine {
|
|||||||
if (p.arrivalTime === this._tick && rt.state === "New") {
|
if (p.arrivalTime === this._tick && rt.state === "New") {
|
||||||
this._transition(rt, "Ready");
|
this._transition(rt, "Ready");
|
||||||
this._readyQueue.push(rt);
|
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.
|
* Fase 2 — Verificação de preempção no nível de processo.
|
||||||
*
|
|
||||||
* Consulta o algoritmo: se houver um candidato na fila de prontos que deva
|
|
||||||
* preemptar o processo em CPU, o processo atual retorna à fila de prontos
|
|
||||||
* e `_cpuProcess` é zerado. Chamado após a chegada para que processos
|
|
||||||
* recém-chegados possam imediatamente preemptar.
|
|
||||||
*/
|
*/
|
||||||
private _checkPreemption(): void {
|
private _checkPreemption(): void {
|
||||||
if (this._cpuProcess === null || this._readyQueue.length === 0) return;
|
if (this._cpuProcess === null || this._readyQueue.length === 0) return;
|
||||||
@@ -158,6 +228,17 @@ export class SimulationEngine {
|
|||||||
this._algorithm.shouldPreempt(this._cpuProcess, candidate, this._config)
|
this._algorithm.shouldPreempt(this._cpuProcess, candidate, this._config)
|
||||||
) {
|
) {
|
||||||
const cpu = this._cpuProcess;
|
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._transition(cpu, "Ready");
|
||||||
this._readyQueue.push(cpu);
|
this._readyQueue.push(cpu);
|
||||||
this._cpuProcess = null;
|
this._cpuProcess = null;
|
||||||
@@ -165,16 +246,7 @@ export class SimulationEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fase 3 — Despacho / troca de contexto.
|
* Fase 3 — Despacho de processo (e thread).
|
||||||
*
|
|
||||||
* Se um context switch estiver em andamento, nenhum processo é despachado
|
|
||||||
* neste tick. Caso contrário, e se a CPU estiver ociosa com processos
|
|
||||||
* prontos, o algoritmo seleciona o próximo processo:
|
|
||||||
*
|
|
||||||
* - Se houver overhead de troca de contexto configurado e o processo
|
|
||||||
* selecionado for diferente do último, o processo volta ao início da
|
|
||||||
* fila e o contador de context-switch é iniciado.
|
|
||||||
* - Caso contrário, o processo é despachado imediatamente para a CPU.
|
|
||||||
*/
|
*/
|
||||||
private _dispatchNext(): void {
|
private _dispatchNext(): void {
|
||||||
if (this._contextSwitchRemaining > 0) return;
|
if (this._contextSwitchRemaining > 0) return;
|
||||||
@@ -196,21 +268,71 @@ export class SimulationEngine {
|
|||||||
this._prevCpuProcessId = next.processId;
|
this._prevCpuProcessId = next.processId;
|
||||||
} else {
|
} else {
|
||||||
if (next.startTick === null) next.startTick = this._tick;
|
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._transition(next, "Running");
|
||||||
this._prevCpuProcessId = next.processId;
|
this._prevCpuProcessId = next.processId;
|
||||||
this._cpuProcess = next;
|
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.
|
* 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 {
|
private _advanceTime(): void {
|
||||||
if (this._contextSwitchRemaining > 0) {
|
if (this._contextSwitchRemaining > 0) {
|
||||||
@@ -219,29 +341,67 @@ export class SimulationEngine {
|
|||||||
|
|
||||||
if (this._cpuProcess !== null) {
|
if (this._cpuProcess !== null) {
|
||||||
const cpu = this._cpuProcess;
|
const cpu = this._cpuProcess;
|
||||||
cpu.remainingBurst--;
|
|
||||||
cpu.cpuTime++;
|
if (this._hasThreads(cpu.processId)) {
|
||||||
cpu.quantumRemaining--;
|
// 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) {
|
for (const rt of this._ioQueue) {
|
||||||
rt.remainingBurst--;
|
if (!this._hasThreads(rt.processId)) {
|
||||||
rt.ioTime++;
|
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) {
|
for (const rt of this._readyQueue) {
|
||||||
rt.waitingTime++;
|
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).
|
* Fase 6 — Aging de prioridade.
|
||||||
*
|
|
||||||
* Quando o aging está habilitado, a cada `agingInterval` ticks o valor de
|
|
||||||
* `currentPriority` de todos os processos na fila de prontos é decrementado
|
|
||||||
* em 1 (até o mínimo de 1). Isso previne inanição em algoritmos de prioridade.
|
|
||||||
*
|
|
||||||
* Não é executado no tick 0 para evitar decremento prematuro.
|
|
||||||
*/
|
*/
|
||||||
private _applyAging(): void {
|
private _applyAging(): void {
|
||||||
if (
|
if (
|
||||||
@@ -258,24 +418,20 @@ export class SimulationEngine {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Fase 7a — Conclusão de burst de CPU.
|
* 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 {
|
private _handleCpuCompletion(): void {
|
||||||
if (this._cpuProcess === null) return;
|
if (this._cpuProcess === null) return;
|
||||||
|
|
||||||
const cpu = this._cpuProcess;
|
const cpu = this._cpuProcess;
|
||||||
|
|
||||||
|
if (this._hasThreads(cpu.processId)) {
|
||||||
|
this._handleThreadedCpuCompletion();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Processo sem threads (comportamento original) ──────────────────────
|
||||||
if (cpu.remainingBurst === 0) {
|
if (cpu.remainingBurst === 0) {
|
||||||
this._advanceBurst(cpu);
|
this._advanceBurst(cpu);
|
||||||
|
cpu.quantumRemaining = 1;
|
||||||
const process = this._processes.find((p) => p.id === cpu.processId)!;
|
const process = this._processes.find((p) => p.id === cpu.processId)!;
|
||||||
const nextBurst = process.bursts[cpu.burstIndex];
|
const nextBurst = process.bursts[cpu.burstIndex];
|
||||||
if (nextBurst !== undefined && nextBurst.type === "io") {
|
if (nextBurst !== undefined && nextBurst.type === "io") {
|
||||||
@@ -298,16 +454,16 @@ export class SimulationEngine {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Fase 7b — Conclusão de bursts de I/O.
|
* 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 {
|
private _handleIoCompletions(): void {
|
||||||
|
// ── Processos sem threads ──────────────────────────────────────────────
|
||||||
const nextIoQueue: ProcessRuntime[] = [];
|
const nextIoQueue: ProcessRuntime[] = [];
|
||||||
for (const rt of this._ioQueue) {
|
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) {
|
if (rt.remainingBurst === 0) {
|
||||||
this._advanceBurst(rt);
|
this._advanceBurst(rt);
|
||||||
const process = this._processes.find((p) => p.id === rt.processId)!;
|
const process = this._processes.find((p) => p.id === rt.processId)!;
|
||||||
@@ -323,22 +479,341 @@ export class SimulationEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._ioQueue = nextIoQueue;
|
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 ─────────────────────────────────────────────────────
|
// ─── 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 {
|
private _transition(rt: ProcessRuntime, state: StateLabel): void {
|
||||||
rt.state = state;
|
rt.state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private _transitionThread(trt: ThreadRuntime, state: StateLabel): void {
|
||||||
* Avança o índice de burst de um runtime e atualiza `remainingBurst`
|
trt.state = state;
|
||||||
* com a duração do próximo burst. Se não houver próximo burst, `remainingBurst` é 0.
|
}
|
||||||
*/
|
|
||||||
private _advanceBurst(rt: ProcessRuntime): void {
|
private _advanceBurst(rt: ProcessRuntime): void {
|
||||||
rt.burstIndex++;
|
rt.burstIndex++;
|
||||||
const process = this._processes.find((p) => p.id === rt.processId)!;
|
const process = this._processes.find((p) => p.id === rt.processId)!;
|
||||||
@@ -346,45 +821,36 @@ export class SimulationEngine {
|
|||||||
rt.remainingBurst = nextBurst !== undefined ? nextBurst.duration : 0;
|
rt.remainingBurst = nextBurst !== undefined ? nextBurst.duration : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private _hasThreads(processId: string): boolean {
|
||||||
* Captura o estado instantâneo de todos os processos no tick atual.
|
const p = this._processes.find((proc) => proc.id === processId);
|
||||||
* Se um context switch estiver em andamento, `cpuProcess` é null e
|
return (p?.threads?.length ?? 0) > 0;
|
||||||
* `ctxSwitchForProcess` identifica o processo prestes a executar.
|
}
|
||||||
*/
|
|
||||||
private _snapshot(): SimTick {
|
private _allThreadsTerminated(processId: string): boolean {
|
||||||
const states: Record<string, StateLabel> = {};
|
const threadMap = this._threadRuntimes.get(processId);
|
||||||
for (const [id, rt] of this._runtimes) {
|
if (!threadMap) return true;
|
||||||
states[id] = rt.state;
|
for (const trt of threadMap.values()) {
|
||||||
|
if (trt.state !== "Terminated") return false;
|
||||||
}
|
}
|
||||||
const contextSwitching = this._contextSwitchRemaining > 0;
|
return true;
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Inicialização ────────────────────────────────────────────────────────
|
// ─── Inicialização ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
private static _initProcessRuntime(p: Process, config: SchedulerConfig): ProcessRuntime {
|
||||||
* Inicializa o estado de runtime de um processo a partir de sua definição.
|
const isThreaded = (p.threads?.length ?? 0) > 0;
|
||||||
* O primeiro burst deve ser do tipo "cpu"; caso contrário, um erro é lançado.
|
|
||||||
*/
|
if (!isThreaded) {
|
||||||
private static _initRuntime(p: Process, config: SchedulerConfig): ProcessRuntime {
|
const firstBurst = p.bursts[0];
|
||||||
const firstBurst = p.bursts[0];
|
if (firstBurst === undefined || firstBurst.type !== "cpu") {
|
||||||
if (firstBurst === undefined || firstBurst.type !== "cpu") {
|
throw new Error(`Process ${p.id}: first burst must be a CPU burst`);
|
||||||
throw new Error(`Process ${p.id}: first burst must be a CPU burst`);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
processId: p.id,
|
processId: p.id,
|
||||||
state: "New",
|
state: "New",
|
||||||
remainingBurst: firstBurst.duration,
|
remainingBurst: isThreaded ? 1 : (p.bursts[0]?.duration ?? 0),
|
||||||
burstIndex: 0,
|
burstIndex: 0,
|
||||||
arrivalTick: p.arrivalTime,
|
arrivalTick: p.arrivalTime,
|
||||||
startTick: null,
|
startTick: null,
|
||||||
@@ -397,4 +863,68 @@ export class SimulationEngine {
|
|||||||
currentPriority: p.priority,
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import type {
|
import type {
|
||||||
GlobalMetrics,
|
GlobalMetrics,
|
||||||
|
Process,
|
||||||
ProcessMetrics,
|
ProcessMetrics,
|
||||||
ProcessRuntime,
|
ProcessRuntime,
|
||||||
|
Thread,
|
||||||
|
ThreadMetrics,
|
||||||
|
ThreadRuntime,
|
||||||
} from "./types";
|
} 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)
|
// Classify process by declared burst ratios (static, pre-simulation)
|
||||||
// Used in the process list preview badge.
|
// Used in the process list preview badge.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { MlqQueueDef, Process, SchedulerConfig } from "./types";
|
import type { MlqQueueDef, Process, SchedulerConfig } from "./types";
|
||||||
|
// Thread preset IDs are prefixed with "t_" to avoid conflicts
|
||||||
|
|
||||||
export interface Preset {
|
export interface Preset {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -127,4 +128,319 @@ export const presets: Preset[] = [
|
|||||||
],
|
],
|
||||||
config: defaultConfig({ algorithm: "MULTILEVEL", quantum: 2, mlqQueues: DEFAULT_MLQ_QUEUES }),
|
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 }),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+90
-6
@@ -11,7 +11,36 @@ export type SchedulingAlgorithm =
|
|||||||
| "PRIORITY_P"
|
| "PRIORITY_P"
|
||||||
| "PRIORITY_RR"
|
| "PRIORITY_RR"
|
||||||
| "MULTILEVEL"
|
| "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 ------------------
|
// --------------- Static process definition ------------------
|
||||||
|
|
||||||
@@ -27,7 +56,13 @@ export interface Process {
|
|||||||
arrivalTime: number; // tick of arrival (≥ 0)
|
arrivalTime: number; // tick of arrival (≥ 0)
|
||||||
priority: number; // 1 (highest) – 10 (lowest); default 5
|
priority: number; // 1 (highest) – 10 (lowest); default 5
|
||||||
color: string; // HEX color for Gantt display
|
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 --------------------
|
// --------------- Scheduler configuration --------------------
|
||||||
@@ -39,6 +74,10 @@ export interface MlqQueueDef {
|
|||||||
quantum: number; // used only when algorithm === "RR"
|
quantum: number; // used only when algorithm === "RR"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MlfqLevelDef {
|
||||||
|
quantum: number; // time slice for this level (ticks, >= 1)
|
||||||
|
}
|
||||||
|
|
||||||
export interface SchedulerConfig {
|
export interface SchedulerConfig {
|
||||||
algorithm: SchedulingAlgorithm;
|
algorithm: SchedulingAlgorithm;
|
||||||
quantum: number; // for RR (default 2)
|
quantum: number; // for RR (default 2)
|
||||||
@@ -47,17 +86,41 @@ export interface SchedulerConfig {
|
|||||||
agingEnabled: boolean; // prevent starvation in Priority scheduling
|
agingEnabled: boolean; // prevent starvation in Priority scheduling
|
||||||
agingInterval: number; // ticks in Ready before priority-- (min 1)
|
agingInterval: number; // ticks in Ready before priority-- (min 1)
|
||||||
mlqQueues?: MlqQueueDef[]; // queue definitions for MULTILEVEL algorithm
|
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 ------------------------------
|
// --------------- Runtime state ------------------------------
|
||||||
|
|
||||||
export type StateLabel =
|
export type StateLabel =
|
||||||
| "New" // before arrivalTime
|
| "New" // before arrivalTime
|
||||||
| "Ready" // in ready queue, waiting for CPU
|
| "Ready" // in ready queue, waiting for CPU
|
||||||
| "Running" // executing CPU burst
|
| "Running" // executing CPU burst
|
||||||
| "Waiting" // executing I/O burst
|
| "Waiting" // executing I/O burst
|
||||||
| "Terminated"; // finished all bursts
|
| "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 {
|
export interface ProcessRuntime {
|
||||||
// ---- spec fields ----
|
// ---- spec fields ----
|
||||||
processId: string;
|
processId: string;
|
||||||
@@ -74,6 +137,7 @@ export interface ProcessRuntime {
|
|||||||
// ---- engine-internal fields ----
|
// ---- engine-internal fields ----
|
||||||
quantumRemaining: number; // remaining quantum for RR; reset on dispatch
|
quantumRemaining: number; // remaining quantum for RR; reset on dispatch
|
||||||
currentPriority: number; // mutable via aging; starts = Process.priority
|
currentPriority: number; // mutable via aging; starts = Process.priority
|
||||||
|
mlfqLevel?: number; // current MLFQ level (0 = highest); undefined for non-MLFQ runs
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------- Tick snapshot ------------------------------
|
// --------------- Tick snapshot ------------------------------
|
||||||
@@ -86,10 +150,30 @@ export interface SimTick {
|
|||||||
states: Record<string, StateLabel>; // all processId → state
|
states: Record<string, StateLabel>; // all processId → state
|
||||||
contextSwitching: boolean; // true during context-switch overhead
|
contextSwitching: boolean; // true during context-switch overhead
|
||||||
ctxSwitchForProcess: string | null; // incoming processId during ctx-switch overhead (null otherwise)
|
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 ------------------------------------
|
// --------------- 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 {
|
export interface ProcessMetrics {
|
||||||
processId: string;
|
processId: string;
|
||||||
arrivalTime: number;
|
arrivalTime: number;
|
||||||
|
|||||||
Reference in new issue
Block a user