Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b707e4990c | ||
|
|
2ee3eeb2b6 | ||
|
|
44ee62e2c2 | ||
|
|
5761dfe70a | ||
|
|
532c14e382 |
No files matched your search
@@ -7,6 +7,11 @@ date-released: "2026-03-23"
|
||||
license: MIT
|
||||
url: "https://pss.lucasrguerra.dev.br"
|
||||
repository-code: "https://github.com/lucasrguerra/pss"
|
||||
doi: "10.5281/zenodo.19197235"
|
||||
identifiers:
|
||||
- type: doi
|
||||
value: "10.5281/zenodo.19197235"
|
||||
description: "Zenodo archive of PSS v1.2.0"
|
||||
authors:
|
||||
- family-names: Guerra
|
||||
given-names: Lucas Rayan
|
||||
|
||||
+30
-6
@@ -64,10 +64,10 @@ docker compose up
|
||||
|
||||
## 🎮 How to use
|
||||
|
||||
1. **Create your processes**: In the left panel, add processes by defining name, arrival time, priority, and a sequence of bursts (CPU and I/O). Or use the random generator to get started quickly.
|
||||
2. **Configure the scheduler**: Choose an algorithm and adjust the parameters (quantum for RR, aging for Priority, etc.).
|
||||
1. **Create your processes**: In the left panel, add processes by defining name, arrival time, priority, and a sequence of bursts (CPU and I/O). Enable the **Use Threads** toggle to create threads within a process, choosing the model (MANY_TO_ONE, ONE_TO_ONE, or MANY_TO_MANY) and the intra-process policy (FCFS, RR, or Priority).
|
||||
2. **Configure the scheduler**: Choose an algorithm and adjust the parameters (quantum for RR, aging for Priority, quantums and boost interval for MLFQ, etc.).
|
||||
3. **Run the simulation**: Use the Play/Pause/Step controls to run the simulation at the speed you want (0.25× to 4×).
|
||||
4. **Analyze the results**: The Gantt chart shows each process's state in real time. At the end, the metrics panel displays turnaround, waiting time, throughput, and more.
|
||||
4. **Analyze the results**: The Gantt chart shows each process's state in real time (and its threads, when configured). At the end, the metrics panel displays turnaround, waiting time, throughput, and — in the Threads tab — individual per-thread metrics.
|
||||
|
||||
> 💡 **Tip:** Load one of the built-in **presets** to see classic phenomena such as the FCFS convoy effect or priority starvation.
|
||||
|
||||
@@ -83,8 +83,10 @@ docker compose up
|
||||
| **Round Robin (RR)** | ✅ | Quantum |
|
||||
| **Priority NP**: Non-Preemptive Priority | ❌ | Aging (optional) |
|
||||
| **Priority P**: Preemptive Priority | ✅ | Aging (optional) |
|
||||
| **Priority RR**: Priority with Round Robin | ✅ | Quantum + Aging (optional) |
|
||||
| **HRRN**: Highest Response Ratio Next | ❌ | None |
|
||||
| **Multilevel Queue** | ✅ | Quantum per queue |
|
||||
| **MLFQ**: Multilevel Feedback Queue | ✅ | Quantums per queue + boost interval |
|
||||
|
||||
> See the [algorithms documentation](./docs/algorithms.en.md) for details on how each one works.
|
||||
|
||||
@@ -119,20 +121,30 @@ src/
|
||||
│ │ ├── priority_rr.ts
|
||||
│ │ ├── hrrn.ts
|
||||
│ │ ├── multilevel.ts
|
||||
│ │ ├── mlfq.ts # Multilevel Feedback Queue
|
||||
│ │ └── index.ts # Dispatch by algorithm
|
||||
│ ├── engine.ts # SimulationEngine, orchestrates the ticks
|
||||
│ ├── metrics.ts # Per-process and global metrics computation
|
||||
│ ├── engine.ts # SimulationEngine, orchestrates the ticks (thread-aware)
|
||||
│ ├── thread-scheduler.ts # Intra-process scheduler (FCFS / RR / Priority)
|
||||
│ ├── metrics.ts # Per-process, global, and per-thread metrics computation
|
||||
│ ├── types.ts # All TypeScript interfaces
|
||||
│ └── presets.ts # Pre-defined scenarios
|
||||
│ └── presets.ts # Pre-defined scenarios (processes and threads)
|
||||
│
|
||||
├── store/ # Global state with Zustand
|
||||
├── components/ # React components
|
||||
│ ├── ProcessPanel/ # Process creation and editing
|
||||
│ │ ├── ThreadEditor.tsx # Thread editor (model, policy, per-thread bursts)
|
||||
│ │ └── ...
|
||||
│ ├── SchedulerPanel/ # Algorithm selection and parameters
|
||||
│ ├── ControlBar/ # Play, Pause, Step, Reset, speed
|
||||
│ ├── GanttChart/ # Interactive Gantt chart
|
||||
│ │ ├── GanttThreadRow.tsx # Thread sub-row in the Gantt chart
|
||||
│ │ └── ...
|
||||
│ ├── MetricsPanel/ # Table, charts, and global metrics
|
||||
│ │ ├── ThreadMetricsTable.tsx # Per-thread metrics table
|
||||
│ │ └── ...
|
||||
│ └── shared/ # Buttons, badges, tooltips, modals
|
||||
│ ├── threadUtils.ts # Thread color derivation (HSL)
|
||||
│ └── ...
|
||||
│
|
||||
└── hooks/ # Custom hooks (animation loop, export, etc.)
|
||||
```
|
||||
@@ -143,6 +155,8 @@ src/
|
||||
|
||||
Load classic scenarios with a single click to explore OS phenomena:
|
||||
|
||||
**Processes:**
|
||||
|
||||
| Preset | What it demonstrates |
|
||||
|--------|----------------------|
|
||||
| `classic_fcfs` | Basic execution with FCFS, no I/O |
|
||||
@@ -153,6 +167,16 @@ Load classic scenarios with a single click to explore OS phenomena:
|
||||
| `io_heavy` | Mix of CPU Bound vs. I/O Bound processes |
|
||||
| `multilevel_demo` | Multiple queues with different priorities |
|
||||
|
||||
**Threads:**
|
||||
|
||||
| Preset | What it demonstrates |
|
||||
|--------|----------------------|
|
||||
| `many_to_one` | N:1 model — any thread's I/O blocks the entire process |
|
||||
| `one_to_one` | 1:1 model — threads block independently |
|
||||
| `many_to_many` | N:M model — kernel slots limit parallelism |
|
||||
| `thread_starvation` | Intra-process starvation with Priority policy |
|
||||
| `threads_vs_processes` | Direct comparison: threaded process vs single-burst process |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Tests
|
||||
|
||||
@@ -62,10 +62,10 @@ docker compose up
|
||||
|
||||
## 🎮 Como usar
|
||||
|
||||
1. **Crie seus processos**: No painel esquerdo, adicione processos definindo nome, tempo de chegada, prioridade e sequência de bursts (CPU e I/O). Ou use o gerador aleatório para começar rápido.
|
||||
2. **Configure o escalonador**: Escolha o algoritmo e ajuste os parâmetros (quantum para RR, aging para Priority, etc.).
|
||||
1. **Crie seus processos**: No painel esquerdo, adicione processos definindo nome, tempo de chegada, prioridade e sequência de bursts (CPU e I/O). Ative a opção **Usar Threads** para criar threads dentro do processo, escolhendo o modelo (MANY_TO_ONE, ONE_TO_ONE ou MANY_TO_MANY) e a política intra-processo (FCFS, RR ou Priority).
|
||||
2. **Configure o escalonador**: Escolha o algoritmo e ajuste os parâmetros (quantum para RR, aging para Priority, quantuns e boost para MLFQ, etc.).
|
||||
3. **Execute a simulação**: Use os controles de Play/Pause/Step para rodar a simulação na velocidade que quiser (0.25× até 4×).
|
||||
4. **Analise os resultados**: O diagrama de Gantt mostra em tempo real o estado de cada processo. Ao final, o painel de métricas exibe turnaround, waiting time, throughput e mais.
|
||||
4. **Analise os resultados**: O diagrama de Gantt mostra em tempo real o estado de cada processo (e suas threads, quando configuradas). Ao final, o painel de métricas exibe turnaround, waiting time, throughput, e — na aba Threads — métricas individuais por thread.
|
||||
|
||||
> 💡 **Dica:** Carregue um dos **presets** disponíveis para ver fenômenos clássicos como o efeito de comboio do FCFS ou starvation com Priority Scheduling.
|
||||
|
||||
@@ -81,8 +81,10 @@ docker compose up
|
||||
| **Round Robin (RR)** | ✅ | Quantum |
|
||||
| **Priority NP**: Prioridade Não-Preemptiva | ❌ | Aging (opcional) |
|
||||
| **Priority P**: Prioridade Preemptiva | ✅ | Aging (opcional) |
|
||||
| **Priority RR**: Prioridade com Round Robin | ✅ | Quantum + Aging (opcional) |
|
||||
| **HRRN**: Highest Response Ratio Next | ❌ | Nenhum |
|
||||
| **Multilevel Queue** | ✅ | Quantum por fila |
|
||||
| **MLFQ**: Multilevel Feedback Queue | ✅ | Quantuns por fila + intervalo de boost |
|
||||
|
||||
> Consulte a [documentação dos algoritmos](./docs/algorithms.md) para detalhes sobre como cada um funciona.
|
||||
|
||||
@@ -117,20 +119,30 @@ src/
|
||||
│ │ ├── priority_rr.ts
|
||||
│ │ ├── hrrn.ts
|
||||
│ │ ├── multilevel.ts
|
||||
│ │ ├── mlfq.ts # Multilevel Feedback Queue
|
||||
│ │ └── index.ts # Dispatch por algoritmo
|
||||
│ ├── engine.ts # SimulationEngine, orquestra os ticks
|
||||
│ ├── metrics.ts # Cálculo de métricas por processo e globais
|
||||
│ ├── engine.ts # SimulationEngine, orquestra os ticks (suporte a threads)
|
||||
│ ├── thread-scheduler.ts # Escalonador intra-processo (FCFS / RR / Priority)
|
||||
│ ├── metrics.ts # Cálculo de métricas por processo, globais e por thread
|
||||
│ ├── types.ts # Todas as interfaces TypeScript
|
||||
│ └── presets.ts # Cenários pré-definidos
|
||||
│ └── presets.ts # Cenários pré-definidos (processos e threads)
|
||||
│
|
||||
├── store/ # Estado global com Zustand
|
||||
├── components/ # Componentes React
|
||||
│ ├── ProcessPanel/ # Criação e edição de processos
|
||||
│ │ ├── ThreadEditor.tsx # Editor de threads (modelo, política, bursts por thread)
|
||||
│ │ └── ...
|
||||
│ ├── SchedulerPanel/ # Seleção de algoritmo e parâmetros
|
||||
│ ├── ControlBar/ # Play, Pause, Step, Reset, velocidade
|
||||
│ ├── GanttChart/ # Diagrama de Gantt interativo
|
||||
│ │ ├── GanttThreadRow.tsx # Sub-linha de thread no Gantt
|
||||
│ │ └── ...
|
||||
│ ├── MetricsPanel/ # Tabela, gráficos e métricas globais
|
||||
│ │ ├── ThreadMetricsTable.tsx # Tabela de métricas por thread
|
||||
│ │ └── ...
|
||||
│ └── shared/ # Botões, badges, tooltips, modais
|
||||
│ ├── threadUtils.ts # Derivação de cores de threads (HSL)
|
||||
│ └── ...
|
||||
│
|
||||
└── hooks/ # Hooks personalizados (loop de animação, export, etc.)
|
||||
```
|
||||
@@ -141,6 +153,8 @@ src/
|
||||
|
||||
Carregue cenários clássicos com um clique para explorar fenômenos de SO:
|
||||
|
||||
**Processos:**
|
||||
|
||||
| Preset | O que demonstra |
|
||||
|--------|-----------------|
|
||||
| `classic_fcfs` | Execução básica com FCFS, sem I/O |
|
||||
@@ -151,6 +165,16 @@ Carregue cenários clássicos com um clique para explorar fenômenos de SO:
|
||||
| `io_heavy` | Mix de processos CPU Bound vs I/O Bound |
|
||||
| `multilevel_demo` | Múltiplas filas com prioridades diferentes |
|
||||
|
||||
**Threads:**
|
||||
|
||||
| Preset | O que demonstra |
|
||||
|--------|-----------------|
|
||||
| `many_to_one` | Modelo N:1 — I/O de uma thread bloqueia todo o processo |
|
||||
| `one_to_one` | Modelo 1:1 — threads bloqueiam independentemente |
|
||||
| `many_to_many` | Modelo N:M — slots de kernel limitam o paralelismo |
|
||||
| `thread_starvation` | Starvation intra-processo com política Priority |
|
||||
| `threads_vs_processes` | Comparação direta: processo com threads vs processo único |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testes
|
||||
|
||||
BIN
Binary file not shown.
+101
-1
@@ -147,7 +147,7 @@ The process with the **highest ratio** is selected. This naturally increases the
|
||||
|
||||
## Multilevel Queue
|
||||
|
||||
**Type:** Preemptive
|
||||
**Type:** Preemptive
|
||||
**Parameters:** number of queues, quantum per queue
|
||||
|
||||
### How it works
|
||||
@@ -158,6 +158,106 @@ This allows different classes of processes to be treated distinctly: for example
|
||||
|
||||
---
|
||||
|
||||
## MLFQ (Multilevel Feedback Queue)
|
||||
|
||||
**Type:** Preemptive
|
||||
**Parameters:** number of queues, quantum per queue (default: [2, 4, 8]), boost interval
|
||||
|
||||
### How it works
|
||||
|
||||
MLFQ is an extension of the Multilevel Queue that allows **processes to move between queues** based on their observed runtime behavior — eliminating the need to know burst durations in advance.
|
||||
|
||||
#### Movement rules
|
||||
|
||||
| Event | Action |
|
||||
|-------|--------|
|
||||
| Newly arrived process | Enters **queue 0** (highest priority) |
|
||||
| Process exhausts the quantum of its current queue | **Demoted** to the next queue (lower priority) |
|
||||
| Process returns from I/O | Stays in the **same queue** (cooperative behavior is rewarded) |
|
||||
| Boost tick (configurable interval) | **All** processes are moved to queue 0 |
|
||||
|
||||
#### Why is MLFQ efficient?
|
||||
|
||||
- Interactive processes (I/O Bound) naturally stay in high-priority queues because they leave the CPU before exhausting the quantum.
|
||||
- Batch processes (CPU Bound) progressively migrate to lower-priority queues where they receive larger quantums.
|
||||
- The periodic boost prevents starvation: even processes that fell to lower queues eventually return to the top.
|
||||
|
||||
#### Example with 3 queues (quantums: 2, 4, 8)
|
||||
|
||||
```
|
||||
Process A (CPU Bound, burst=12):
|
||||
Queue 0 → uses 2 ticks, demoted → Queue 1
|
||||
Queue 1 → uses 4 ticks, demoted → Queue 2
|
||||
Queue 2 → uses remaining 6 ticks (quantum=8, not exhausted)
|
||||
|
||||
Process B (I/O Bound, burst=1 CPU + I/O + burst=1 CPU):
|
||||
Queue 0 → uses 1 tick → goes to I/O → returns to Queue 0
|
||||
→ always stays in the highest-priority queue
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| Number of queues | 3 | Number of priority queues |
|
||||
| Quantum per queue | [2, 4, 8] | Time slice for each queue |
|
||||
| Boost interval | 20 ticks | Every N ticks, all processes move up to queue 0 |
|
||||
|
||||
---
|
||||
|
||||
## Thread Models
|
||||
|
||||
PSS supports **thread** simulation within processes, implementing the three classical models described in Operating Systems textbooks.
|
||||
|
||||
### General Concepts
|
||||
|
||||
- A process can have **N user threads**, each with its own burst sequence (CPU and I/O).
|
||||
- The scheduling policy among threads within the same process is configurable: **FCFS**, **Round Robin**, or **Priority**.
|
||||
- The thread model determines how user threads map to kernel threads.
|
||||
|
||||
### MANY_TO_ONE (N:1)
|
||||
|
||||
**N user threads → 1 kernel thread**
|
||||
|
||||
- Only one thread can use the CPU at a time within the process.
|
||||
- If any thread initiates I/O, **the entire process blocks** (the kernel thread is stuck).
|
||||
- No real parallelism; very simple to implement.
|
||||
- Simulated by the `many_to_one` presets.
|
||||
|
||||
### ONE_TO_ONE (1:1)
|
||||
|
||||
**Each user thread → 1 dedicated kernel thread**
|
||||
|
||||
- Threads block **independently**: if Thread A does I/O, Threads B and C remain in the Ready queue.
|
||||
- The process only terminates when **all** threads have terminated.
|
||||
- The process blocks only when all threads are simultaneously in I/O.
|
||||
- Simulated by the `one_to_one` presets.
|
||||
|
||||
### MANY_TO_MANY (N:M)
|
||||
|
||||
**N user threads → M kernel threads (M ≤ N)**
|
||||
|
||||
- The number of kernel threads (`kernelThreadCount`) is configurable.
|
||||
- If all M kernel slots are occupied, additional user threads wait in the **kernel wait queue** (`KernelWait`).
|
||||
- Combines flexibility with resource control: more active threads than N:1 but less overhead than 1:1.
|
||||
- Simulated by the `many_to_many` presets.
|
||||
|
||||
### Intra-Process Policy
|
||||
|
||||
Defines how the scheduler selects the next thread when multiple threads are Ready within the same process:
|
||||
|
||||
| Policy | Behavior |
|
||||
|--------|----------|
|
||||
| **FCFS** | The oldest thread in the Ready queue is selected |
|
||||
| **Round Robin (RR)** | Each thread receives `threadQuantum` ticks; when exhausted, it goes to the end of the queue |
|
||||
| **Priority** | The highest-priority thread is selected; immediate preemption if a higher-priority thread becomes ready |
|
||||
|
||||
### Gantt Chart with Threads
|
||||
|
||||
When a process has threads, the Gantt chart displays **sub-rows** below the process row, one per thread, with colors derived from the process color (40° hue shift per thread). The state of each thread (New, Ready, Running, Waiting, Terminated) is colored using the same CSS variables as process states.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- Silberschatz, A., Galvin, P. B., & Gagne, G., *Operating System Concepts*, 10th edition.
|
||||
|
||||
+101
-1
@@ -147,7 +147,7 @@ O processo com o **maior ratio** é selecionado. Isso faz com que processos que
|
||||
|
||||
## Multilevel Queue
|
||||
|
||||
**Tipo:** Preemptivo
|
||||
**Tipo:** Preemptivo
|
||||
**Parâmetros:** número de filas, quantum por fila
|
||||
|
||||
### Como funciona
|
||||
@@ -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
|
||||
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
Considere três processos com os seguintes bursts:
|
||||
|
||||
Generated
+17
-9
@@ -147,6 +147,7 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -519,6 +520,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
@@ -567,6 +569,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
@@ -1892,8 +1895,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
@@ -2034,6 +2036,7 @@
|
||||
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
@@ -2051,6 +2054,7 @@
|
||||
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
@@ -2062,6 +2066,7 @@
|
||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
@@ -2373,6 +2378,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -2725,8 +2731,7 @@
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
@@ -3544,7 +3549,6 @@
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
@@ -3760,6 +3764,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -3802,7 +3807,6 @@
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
@@ -3818,7 +3822,6 @@
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -3829,7 +3832,6 @@
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -3852,6 +3854,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -3864,6 +3867,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -3884,6 +3888,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
@@ -3960,7 +3965,8 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
@@ -4546,6 +4552,7 @@
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
@@ -4644,6 +4651,7 @@
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pss",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.2",
|
||||
"description": "Process Scheduler Simulator — an interactive educational tool that runs entirely in the browser",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -66,7 +66,7 @@ const ControlBar = () => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-16 flex items-center px-5 gap-3 bg-slate-800 border-b border-slate-700 shadow-sm"
|
||||
className="h-16 py-2 flex items-center px-5 gap-3 bg-slate-800 border-b border-slate-700 shadow-sm"
|
||||
role="toolbar"
|
||||
aria-label="Simulation controls"
|
||||
>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useSimulationStore } from "@store/simulationStore";
|
||||
import { useProcessStore } from "@store/processStore";
|
||||
import { useGanttScroll } from "@hooks/useGanttScroll";
|
||||
import GanttRow from "./GanttRow";
|
||||
import GanttThreadRow from "./GanttThreadRow";
|
||||
import GanttLegend from "./GanttLegend";
|
||||
import GanttTooltip, { type TooltipData } from "./GanttTooltip";
|
||||
import { CELL_H, CELL_W } from "./constants";
|
||||
@@ -13,33 +14,29 @@ const GanttChart = () => {
|
||||
const status = useSimulationStore((s) => s.status);
|
||||
const processes = useProcessStore((s) => s.processes);
|
||||
|
||||
// Scrollable container ref for auto-scroll
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const currentTickIndex = ticks.length - 1;
|
||||
useGanttScroll(scrollRef, currentTickIndex, status === "running");
|
||||
|
||||
// Tooltip state
|
||||
const [tooltip, setTooltip] = useState<TooltipData | null>(null);
|
||||
const handleMouseLeave = useCallback(() => setTooltip(null), []);
|
||||
|
||||
// Empty state
|
||||
if (ticks.length === 0) {
|
||||
return (
|
||||
<div className="border-b border-slate-800 flex items-center justify-center text-slate-600 bg-slate-900/50 text-sm select-none"
|
||||
style={{ minHeight: 80 }}>
|
||||
<div
|
||||
className="border-b border-slate-800 flex items-center justify-center text-slate-600 bg-slate-900/50 text-sm select-none"
|
||||
style={{ minHeight: 80 }}
|
||||
>
|
||||
Start the simulation to see the Gantt chart
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── ruler (tick numbers) ──────────────────────────────────
|
||||
const tickNumbers = ticks.map((t) => t.tick);
|
||||
const totalWidth = ticks.length * CELL_W;
|
||||
|
||||
return (
|
||||
<div className="border-b border-slate-800 bg-slate-950 flex flex-col">
|
||||
|
||||
{/* ── scrollable grid + cursor ── */}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="overflow-x-auto overflow-y-hidden gantt-scrollbar relative"
|
||||
@@ -48,9 +45,10 @@ const GanttChart = () => {
|
||||
|
||||
{/* Tick ruler */}
|
||||
<div className="flex sticky top-0 z-10 bg-slate-950 border-b border-slate-800">
|
||||
{/* Spacer for sticky process labels column */}
|
||||
<div className="sticky left-0 z-20 bg-slate-950 border-r border-slate-700 shrink-0" style={{ width: 48, height: 16 }} />
|
||||
|
||||
<div
|
||||
className="sticky left-0 z-20 bg-slate-950 border-r border-slate-700 shrink-0"
|
||||
style={{ width: 60, height: 16 }}
|
||||
/>
|
||||
{tickNumbers.map((t) => (
|
||||
<div
|
||||
key={t}
|
||||
@@ -62,34 +60,64 @@ const GanttChart = () => {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Process rows */}
|
||||
{/* Process rows (+ thread sub-rows) */}
|
||||
<div className="relative">
|
||||
{processes.map((process) => (
|
||||
<div key={process.id} className="flex items-center border-b border-white/5 last:border-b-0">
|
||||
{/* Process label (sticky on the left) */}
|
||||
<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>
|
||||
{processes.map((process) => {
|
||||
const hasThreads = (process.threads?.length ?? 0) > 0;
|
||||
|
||||
<GanttRow
|
||||
process={process}
|
||||
ticks={ticks}
|
||||
onCellMouseEnter={setTooltip}
|
||||
onCellMouseLeave={handleMouseLeave}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
return (
|
||||
<div key={process.id}>
|
||||
{/* ── Linha do processo ─────────────────────────── */}
|
||||
<div
|
||||
className={`flex items-center border-b border-white/5 ${hasThreads ? 'border-b-0' : ''}`}
|
||||
>
|
||||
<div
|
||||
className="sticky left-0 z-20 bg-slate-900 border-r border-slate-700 px-2 flex items-center shrink-0"
|
||||
style={{ width: 60, 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 */}
|
||||
{currentTickIndex >= 0 && (
|
||||
<div
|
||||
className="gantt-cursor absolute top-0 bottom-0 w-px bg-white/20 pointer-events-none"
|
||||
style={{ left: 48 + currentTickIndex * CELL_W + CELL_W / 2 }}
|
||||
style={{ left: 60 + currentTickIndex * CELL_W + CELL_W / 2 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -97,14 +125,7 @@ const GanttChart = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── process name labels (sticky left, overlay) ── */}
|
||||
{/* Rendered OUTSIDE the scrollable div so they stay fixed on the left */}
|
||||
{/* We achieve this with a separate absolute-positioned column */}
|
||||
|
||||
{/* ── legend ── */}
|
||||
<GanttLegend />
|
||||
|
||||
{/* ── tooltip ── */}
|
||||
{tooltip && <GanttTooltip data={tooltip} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import React from "react";
|
||||
import type { Process, SimTick, StateLabel, Thread } from "@core/types";
|
||||
import { CELL_W } from "./constants";
|
||||
import type { TooltipData } from "./GanttTooltip";
|
||||
import { deriveThreadColor } from "../shared/threadUtils";
|
||||
|
||||
const THREAD_CELL_H = 20;
|
||||
|
||||
// ── cores de estado (mesmas do processo) ────────────────────
|
||||
const STATE_CLASS: Record<StateLabel, string> = {
|
||||
New: "bg-[var(--color-state-new)]",
|
||||
Ready: "bg-[var(--color-state-ready)]",
|
||||
Running: "bg-[var(--color-state-running)]",
|
||||
Waiting: "bg-[var(--color-state-waiting)]",
|
||||
Terminated: "bg-[var(--color-state-terminated)]",
|
||||
};
|
||||
|
||||
interface GanttThreadRowProps {
|
||||
thread: Thread;
|
||||
threadIndex: number;
|
||||
process: Process;
|
||||
ticks: SimTick[];
|
||||
onCellMouseEnter: (data: TooltipData) => void;
|
||||
onCellMouseLeave: () => void;
|
||||
}
|
||||
|
||||
const GanttThreadRow = React.memo(function GanttThreadRow({
|
||||
thread,
|
||||
threadIndex,
|
||||
process,
|
||||
ticks,
|
||||
onCellMouseEnter,
|
||||
onCellMouseLeave,
|
||||
}: GanttThreadRowProps) {
|
||||
const threadColor = deriveThreadColor(process.color, threadIndex);
|
||||
|
||||
return (
|
||||
<div className="flex items-center border-b border-white/3">
|
||||
{/* Rótulo da thread (sticky) */}
|
||||
<div
|
||||
className="sticky left-0 z-20 bg-slate-900/90 border-r border-slate-700/50 px-2 flex items-center shrink-0 gap-1"
|
||||
style={{ width: 60, 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
|
||||
</div>
|
||||
<div className="overflow-y-auto">
|
||||
{presets.map((preset, i) => (
|
||||
<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="text-xs font-medium text-slate-200 group-hover:text-white">
|
||||
{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>
|
||||
))}
|
||||
{/* Classic process presets */}
|
||||
<div className="px-3 py-1 text-[9px] text-slate-500 uppercase tracking-wider font-semibold bg-slate-800/80 sticky top-0">
|
||||
Processes
|
||||
</div>
|
||||
{presets
|
||||
.filter(p => !['many_to_one','one_to_one','many_to_many','thread_starvation','threads_vs_processes'].includes(p.name))
|
||||
.map((preset) => {
|
||||
const i = presets.indexOf(preset);
|
||||
return (
|
||||
<button
|
||||
key={preset.name}
|
||||
onClick={() => handleLoadPreset(i)}
|
||||
className="w-full text-left px-3 py-2.5 hover:bg-slate-700 transition-colors group"
|
||||
>
|
||||
<div className="text-xs font-medium text-slate-200 group-hover:text-white">
|
||||
{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>
|
||||
</>
|
||||
|
||||
@@ -4,7 +4,7 @@ import MetricsPanel from '../MetricsPanel/MetricsPanel';
|
||||
|
||||
const MainArea = () => {
|
||||
return (
|
||||
<main className="flex-1 flex flex-col h-full bg-slate-900 overflow-hidden relative">
|
||||
<main className="flex-1 flex flex-col bg-slate-900 overflow-y-auto relative">
|
||||
<ControlBar />
|
||||
<GanttChart />
|
||||
<MetricsPanel />
|
||||
|
||||
@@ -4,12 +4,6 @@ import SchedulerPanel from '../SchedulerPanel/SchedulerPanel';
|
||||
const Sidebar = () => {
|
||||
return (
|
||||
<aside className="w-80 min-w-[20rem] h-full bg-slate-800 border-r border-slate-700 flex flex-col shadow-xl z-20">
|
||||
<div className="h-16 flex items-center px-6 border-b border-slate-700 bg-slate-800/80 backdrop-blur-sm">
|
||||
<h1 className="text-xl font-bold bg-linear-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent truncate">
|
||||
Process Scheduler Simulator
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden p-4 space-y-6 scrollbar-hide">
|
||||
<ProcessPanel />
|
||||
<div className="border-t border-slate-700 pt-4">
|
||||
@@ -18,7 +12,7 @@ const Sidebar = () => {
|
||||
</div>
|
||||
|
||||
<div className="h-12 border-t border-slate-700 flex items-center justify-center text-xs text-slate-500">
|
||||
PSS v1.2.0
|
||||
PSS v1.3.2
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -28,6 +28,22 @@ const TOOLTIP_STYLE = {
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper that gives ResponsiveContainer a bounded, non-growing parent.
|
||||
*
|
||||
* Without this, recharts' ResizeObserver inside a scroll container can trigger
|
||||
* a resize loop: scrollbar appears → available width shrinks → chart redraws →
|
||||
* height shifts → scrollbar disappears → repeat. The explicit height + overflow
|
||||
* hidden caps the chart's DOM footprint and breaks the loop.
|
||||
*/
|
||||
const ChartBox = ({ height, children }: { height: number; children: React.ReactNode }) => (
|
||||
<div style={{ width: '100%', height, overflow: 'hidden', flexShrink: 0 }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
{children as React.ReactElement}
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
|
||||
const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
if (metrics.length === 0) {
|
||||
return (
|
||||
@@ -80,10 +96,9 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
|
||||
// ── Radar chart data (normalized 0-100 per dimension) ─────────
|
||||
// Each "subject" is a process, each series is a dimension.
|
||||
// We invert "Response" and "Waiting" so higher = better.
|
||||
const maxT = Math.max(...metrics.map(m => m.turnaroundTime), 1);
|
||||
const maxR = Math.max(...metrics.map(m => m.responseTime), 1);
|
||||
const maxW = Math.max(...metrics.map(m => m.waitingTime), 1);
|
||||
const maxT = Math.max(...metrics.map(m => m.turnaroundTime), 1);
|
||||
const maxR = Math.max(...metrics.map(m => m.responseTime), 1);
|
||||
const maxW = Math.max(...metrics.map(m => m.waitingTime), 1);
|
||||
const maxCpu = Math.max(...metrics.map(m => m.cpuTime), 1);
|
||||
const maxIo = Math.max(...metrics.map(m => m.ioTime), 1);
|
||||
|
||||
@@ -98,11 +113,11 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
metrics.forEach(m => {
|
||||
const key = getName(m.processId);
|
||||
switch (dim) {
|
||||
case 'Turnaround': row[key] = (m.turnaroundTime / maxT) * 100; break;
|
||||
case 'Response': row[key] = (m.responseTime / maxR) * 100; break;
|
||||
case 'Waiting': row[key] = (m.waitingTime / maxW) * 100; break;
|
||||
case 'CPU Time': row[key] = (m.cpuTime / maxCpu)* 100; break;
|
||||
case 'I/O Time': row[key] = (m.ioTime / maxIo) * 100; break;
|
||||
case 'Turnaround': row[key] = (m.turnaroundTime / maxT) * 100; break;
|
||||
case 'Response': row[key] = (m.responseTime / maxR) * 100; break;
|
||||
case 'Waiting': row[key] = (m.waitingTime / maxW) * 100; break;
|
||||
case 'CPU Time': row[key] = (m.cpuTime / maxCpu) * 100; break;
|
||||
case 'I/O Time': row[key] = (m.ioTime / maxIo) * 100; break;
|
||||
}
|
||||
});
|
||||
return row;
|
||||
@@ -110,15 +125,18 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
|
||||
const showRadar = metrics.length >= 2;
|
||||
|
||||
// Charts are always stacked vertically (flex-col) so that the total height
|
||||
// exceeds the panel's viewport, making the scroll area in MetricsPanel
|
||||
// active and accessible on any screen size.
|
||||
return (
|
||||
<div className={`p-3 flex gap-3 ${showRadar ? 'flex-row' : 'flex-col'}`}>
|
||||
<div className="p-3 flex flex-col gap-6">
|
||||
{/* ── Radar chart ───────────────────────────────────────── */}
|
||||
{showRadar && (
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="w-full">
|
||||
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
||||
Comparative Profile (normalized)
|
||||
</p>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<ChartBox height={360}>
|
||||
<RadarChart data={radarData} margin={{ top: 8, right: 20, bottom: 8, left: 20 }}>
|
||||
<PolarGrid stroke="#334155" />
|
||||
<PolarAngleAxis
|
||||
@@ -142,16 +160,16 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
|
||||
/>
|
||||
</RadarChart>
|
||||
</ResponsiveContainer>
|
||||
</ChartBox>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Stacked bar chart ─────────────────────────────────── */}
|
||||
<div className={showRadar ? 'flex-1 min-w-0' : 'w-full'}>
|
||||
<div className="w-full">
|
||||
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
||||
Breakdown per Process (ticks)
|
||||
</p>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<ChartBox height={360}>
|
||||
<BarChart data={barData} margin={{ top: 8, right: 12, left: -8, bottom: 4 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
||||
<XAxis
|
||||
@@ -183,7 +201,7 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
<Bar dataKey="CPU" stackId="a" fill="#22c55e" radius={[0, 0, 0, 0]} />
|
||||
<Bar dataKey="I/O" stackId="a" fill="#06b6d4" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</ChartBox>
|
||||
<p className="text-center text-[9px] text-slate-600 mt-0.5">
|
||||
Waiting + CPU + I/O = Turnaround
|
||||
</p>
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import { useState } from 'react';
|
||||
import { Download, BarChart2, Table, Globe } from 'lucide-react';
|
||||
import { computeProcessMetrics, computeGlobalMetrics } from '@core/metrics';
|
||||
import { Download, BarChart2, Table, Globe, Layers } from 'lucide-react';
|
||||
import { computeProcessMetrics, computeGlobalMetrics, computeThreadMetrics } from '@core/metrics';
|
||||
import { useSimulationStore } from '../../store/simulationStore';
|
||||
import { useProcessStore } from '../../store/processStore';
|
||||
import { useExport } from '../../hooks/useExport';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import GlobalMetrics from './GlobalMetrics';
|
||||
import MetricsChart from './MetricsChart';
|
||||
import ThreadMetricsTable from './ThreadMetricsTable';
|
||||
import ThreadMetricsChart from './ThreadMetricsChart';
|
||||
|
||||
type Tab = 'table' | 'global' | 'charts';
|
||||
|
||||
const TAB_CONFIG: { id: Tab; label: string; icon: React.ReactNode }[] = [
|
||||
{ id: 'table', label: 'Per Process', icon: <Table size={13} /> },
|
||||
{ id: 'global', label: 'Global', icon: <Globe size={13} /> },
|
||||
{ id: 'charts', label: 'Charts', icon: <BarChart2 size={13} /> },
|
||||
];
|
||||
type Tab = 'table' | 'global' | 'charts' | 'threads' | 'thread-charts';
|
||||
|
||||
const MetricsPanel = () => {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('table');
|
||||
@@ -36,20 +32,40 @@ const MetricsPanel = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Derive metrics from engine runtime state
|
||||
// ── Métricas de processos ──────────────────────────────────────────────
|
||||
const runtimes = engine?.runtimeStates ?? [];
|
||||
const completedRuntimes = runtimes.filter(rt => rt.finishTick !== null);
|
||||
const processMetrics = completedRuntimes.map(rt => computeProcessMetrics(rt));
|
||||
const globalMetrics = computeGlobalMetrics(processMetrics, ticks.length);
|
||||
const processMap = Object.fromEntries(processes.map(p => [p.id, p]));
|
||||
|
||||
// ── Métricas de threads ────────────────────────────────────────────────
|
||||
const threadRuntimes = engine?.threadRuntimeStates ?? [];
|
||||
const completedThreadRts = threadRuntimes.filter(trt => trt.finishTick !== null);
|
||||
const threadMetrics = completedThreadRts.map(trt => {
|
||||
const process = processMap[trt.processId];
|
||||
const thread = process?.threads?.find(t => t.tid === trt.threadId);
|
||||
if (!process || !thread) return null;
|
||||
return computeThreadMetrics(trt, thread, process);
|
||||
}).filter(Boolean) as ReturnType<typeof computeThreadMetrics>[];
|
||||
|
||||
const hasThreads = processes.some(p => (p.threads?.length ?? 0) > 0);
|
||||
|
||||
const canExportCSV = status === 'finished' && processMetrics.length > 0;
|
||||
|
||||
const TAB_CONFIG: { id: Tab; label: string; icon: React.ReactNode; show: boolean }[] = [
|
||||
{ id: 'table', label: 'Per Process', icon: <Table size={13} />, show: true },
|
||||
{ id: 'global', label: 'Global', icon: <Globe size={13} />, show: true },
|
||||
{ id: 'charts', label: 'Process Charts', icon: <BarChart2 size={13} />, show: true },
|
||||
{ id: 'threads', label: 'Threads', icon: <Layers size={13} />, show: hasThreads },
|
||||
{ id: 'thread-charts', label: 'Thread Charts', icon: <BarChart2 size={13} />, show: hasThreads },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="border-t border-slate-700 bg-slate-950 flex flex-col shrink-0" style={{ height: '600px' }}>
|
||||
<div className="border-t border-slate-700 bg-slate-950 flex flex-col">
|
||||
{/* Panel header with tabs + export button */}
|
||||
<div className="flex items-center px-3 border-b border-slate-700 shrink-0 h-10 gap-1">
|
||||
{TAB_CONFIG.map(tab => (
|
||||
{TAB_CONFIG.filter(t => t.show).map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
@@ -62,12 +78,17 @@ const MetricsPanel = () => {
|
||||
>
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
{tab.id === 'threads' && threadMetrics.length > 0 && (
|
||||
<span className="text-[9px] bg-blue-500/20 text-blue-400 px-1 rounded">
|
||||
{threadMetrics.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<div className="ml-auto">
|
||||
<button
|
||||
onClick={() => exportCSV(processMetrics, processMap)}
|
||||
onClick={() => exportCSV(processMetrics, processMap, hasThreads ? threadMetrics : undefined)}
|
||||
disabled={!canExportCSV}
|
||||
title={!canExportCSV ? 'Available when simulation completes' : 'Export metrics as CSV'}
|
||||
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-slate-200 px-2.5 py-1 rounded-md hover:bg-slate-800 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
@@ -80,7 +101,7 @@ const MetricsPanel = () => {
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div>
|
||||
{activeTab === 'table' && (
|
||||
<MetricsTable metrics={processMetrics} processMap={processMap} />
|
||||
)}
|
||||
@@ -90,6 +111,12 @@ const MetricsPanel = () => {
|
||||
{activeTab === 'charts' && (
|
||||
<MetricsChart metrics={processMetrics} processMap={processMap} />
|
||||
)}
|
||||
{activeTab === 'threads' && (
|
||||
<ThreadMetricsTable metrics={threadMetrics} processMap={processMap} />
|
||||
)}
|
||||
{activeTab === 'thread-charts' && (
|
||||
<ThreadMetricsChart metrics={threadMetrics} processMap={processMap} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
CartesianGrid,
|
||||
RadarChart,
|
||||
PolarGrid,
|
||||
PolarAngleAxis,
|
||||
Radar,
|
||||
} from 'recharts';
|
||||
import type { ThreadMetrics, Process } from '@core/types';
|
||||
import { deriveThreadColor } from '../shared/threadUtils';
|
||||
|
||||
interface ThreadMetricsChartProps {
|
||||
metrics: ThreadMetrics[];
|
||||
processMap: Record<string, Process>;
|
||||
}
|
||||
|
||||
const TOOLTIP_STYLE = {
|
||||
backgroundColor: '#1e293b',
|
||||
border: '1px solid #334155',
|
||||
borderRadius: '8px',
|
||||
fontSize: '11px',
|
||||
color: '#e2e8f0',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
|
||||
};
|
||||
|
||||
const 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 ThreadMetricsChart = ({ metrics, processMap }: ThreadMetricsChartProps) => {
|
||||
if (metrics.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-48 text-slate-500 text-sm">
|
||||
No data available.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Pre-compute thread colors in order of appearance
|
||||
const threadIndexByProcess: Record<string, number> = {};
|
||||
const threadColors: Record<string, string> = {};
|
||||
metrics.forEach(m => {
|
||||
if (threadIndexByProcess[m.processId] === undefined) {
|
||||
threadIndexByProcess[m.processId] = 0;
|
||||
}
|
||||
const idx = threadIndexByProcess[m.processId]!;
|
||||
const procColor = processMap[m.processId]?.color ?? '#94a3b8';
|
||||
threadColors[`${m.processId}-${m.threadId}`] = deriveThreadColor(procColor, idx);
|
||||
threadIndexByProcess[m.processId] = idx + 1;
|
||||
});
|
||||
|
||||
const getThreadLabel = (m: ThreadMetrics) => {
|
||||
const procName = processMap[m.processId]?.name ?? m.processId;
|
||||
return `${procName}/${m.threadName}`;
|
||||
};
|
||||
|
||||
const getThreadColor = (m: ThreadMetrics) =>
|
||||
threadColors[`${m.processId}-${m.threadId}`] ?? '#94a3b8';
|
||||
|
||||
// ── Radar tooltip ──────────────────────────────────────────────
|
||||
const labelToMetric = Object.fromEntries(metrics.map(m => [getThreadLabel(m), m]));
|
||||
|
||||
const radarTooltipContent = ({ active, payload, label }: { active?: boolean; payload?: readonly any[]; label?: string | number }) => {
|
||||
if (!active || !payload?.length) return null;
|
||||
return (
|
||||
<div style={{ ...TOOLTIP_STYLE, padding: '8px 12px' }}>
|
||||
<p style={{ fontSize: '10px', color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 600, marginBottom: 4 }}>
|
||||
{label}
|
||||
</p>
|
||||
{payload.map((entry: any) => {
|
||||
const m = labelToMetric[entry.name];
|
||||
return (
|
||||
<div key={entry.name} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: '11px', marginBottom: 2 }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', backgroundColor: entry.color, flexShrink: 0, display: 'inline-block' }} />
|
||||
<span style={{ color: '#cbd5e1' }}>{m ? getThreadLabel(m) : entry.name}</span>
|
||||
<span style={{ marginLeft: 'auto', paddingLeft: 12, fontFamily: 'monospace', color: '#e2e8f0' }}>
|
||||
{typeof entry.value === 'number' ? `${entry.value.toFixed(1)}%` : '-'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ── Stacked bar data ──────────────────────────────────────────
|
||||
const barData = metrics.map(m => ({
|
||||
name: getThreadLabel(m),
|
||||
'Waiting': m.waitingTime,
|
||||
'CPU': m.cpuTime,
|
||||
'I/O': m.ioTime,
|
||||
}));
|
||||
|
||||
// ── Radar chart data (normalized 0-100 per dimension) ─────────
|
||||
const maxT = Math.max(...metrics.map(m => m.turnaroundTime), 1);
|
||||
const maxR = Math.max(...metrics.map(m => m.responseTime), 1);
|
||||
const maxW = Math.max(...metrics.map(m => m.waitingTime), 1);
|
||||
const maxCpu = Math.max(...metrics.map(m => m.cpuTime), 1);
|
||||
const maxIo = Math.max(...metrics.map(m => m.ioTime), 1);
|
||||
|
||||
const radarDimensions = ['Turnaround', 'Response', 'Waiting', 'CPU Time', 'I/O Time'] as const;
|
||||
|
||||
const radarData = radarDimensions.map(dim => {
|
||||
const row: Record<string, string | number> = { dimension: dim };
|
||||
metrics.forEach(m => {
|
||||
const key = getThreadLabel(m);
|
||||
switch (dim) {
|
||||
case 'Turnaround': row[key] = (m.turnaroundTime / maxT) * 100; break;
|
||||
case 'Response': row[key] = (m.responseTime / maxR) * 100; break;
|
||||
case 'Waiting': row[key] = (m.waitingTime / maxW) * 100; break;
|
||||
case 'CPU Time': row[key] = (m.cpuTime / maxCpu) * 100; break;
|
||||
case 'I/O Time': row[key] = (m.ioTime / maxIo) * 100; break;
|
||||
}
|
||||
});
|
||||
return row;
|
||||
});
|
||||
|
||||
const showRadar = metrics.length >= 2;
|
||||
|
||||
return (
|
||||
<div className="p-3 flex flex-col gap-6">
|
||||
{/* ── Radar chart ───────────────────────────────────────── */}
|
||||
{showRadar && (
|
||||
<div className="w-full">
|
||||
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
||||
Comparative Profile (normalized)
|
||||
</p>
|
||||
<ChartBox height={360}>
|
||||
<RadarChart data={radarData} margin={{ top: 8, right: 20, bottom: 8, left: 20 }}>
|
||||
<PolarGrid stroke="#334155" />
|
||||
<PolarAngleAxis
|
||||
dataKey="dimension"
|
||||
tick={{ fill: '#94a3b8', fontSize: 10 }}
|
||||
/>
|
||||
{metrics.map(m => (
|
||||
<Radar
|
||||
key={`${m.processId}-${m.threadId}`}
|
||||
name={getThreadLabel(m)}
|
||||
dataKey={getThreadLabel(m)}
|
||||
stroke={getThreadColor(m)}
|
||||
fill={getThreadColor(m)}
|
||||
fillOpacity={0.15}
|
||||
strokeWidth={2}
|
||||
dot={{ r: 3, fill: getThreadColor(m) }}
|
||||
/>
|
||||
))}
|
||||
<Tooltip content={radarTooltipContent} />
|
||||
<Legend
|
||||
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
|
||||
/>
|
||||
</RadarChart>
|
||||
</ChartBox>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Stacked bar chart ─────────────────────────────────── */}
|
||||
<div className="w-full">
|
||||
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
||||
Breakdown per Thread (ticks)
|
||||
</p>
|
||||
<ChartBox height={360}>
|
||||
<BarChart data={barData} margin={{ top: 8, right: 12, left: -8, bottom: 48 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tick={{ fill: '#94a3b8', fontSize: 10 }}
|
||||
axisLine={{ stroke: '#334155' }}
|
||||
tickLine={false}
|
||||
angle={-35}
|
||||
textAnchor="end"
|
||||
interval={0}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fill: '#94a3b8', fontSize: 10 }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
label={{
|
||||
value: 'ticks',
|
||||
angle: -90,
|
||||
position: 'insideLeft',
|
||||
offset: 14,
|
||||
style: { fill: '#475569', fontSize: 9 },
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={TOOLTIP_STYLE}
|
||||
cursor={{ fill: 'rgba(255,255,255,0.04)' }}
|
||||
/>
|
||||
<Legend
|
||||
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
|
||||
/>
|
||||
<Bar dataKey="Waiting" stackId="a" fill="#f59e0b" radius={[0, 0, 0, 0]} />
|
||||
<Bar dataKey="CPU" stackId="a" fill="#22c55e" radius={[0, 0, 0, 0]} />
|
||||
<Bar dataKey="I/O" stackId="a" fill="#06b6d4" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ChartBox>
|
||||
<p className="text-center text-[9px] text-slate-600 mt-0.5">
|
||||
Waiting + CPU + I/O = Turnaround
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThreadMetricsChart;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
const MODEL_LABEL: Record<string, string> = {
|
||||
MANY_TO_ONE: 'M:1',
|
||||
ONE_TO_ONE: '1:1',
|
||||
MANY_TO_MANY: 'M:M',
|
||||
};
|
||||
|
||||
const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
||||
const bound = classifyBound(process.bursts);
|
||||
const totalCpu = process.bursts.filter(b => b.type === 'cpu').reduce((a, b) => a + b.duration, 0);
|
||||
const totalIo = process.bursts.filter(b => b.type === 'io').reduce((a, b) => a + b.duration, 0);
|
||||
const hasThreads = (process.threads?.length ?? 0) > 0;
|
||||
|
||||
const bound = hasThreads ? 'Balanced' : classifyBound(process.bursts);
|
||||
const totalCpu = hasThreads
|
||||
? process.threads!.reduce(
|
||||
(sum, t) => sum + t.bursts.filter(b => b.type === 'cpu').reduce((a, b) => a + b.duration, 0),
|
||||
0,
|
||||
)
|
||||
: process.bursts.filter(b => b.type === 'cpu').reduce((a, b) => a + b.duration, 0);
|
||||
const totalIo = hasThreads
|
||||
? process.threads!.reduce(
|
||||
(sum, t) => sum + t.bursts.filter(b => b.type === 'io').reduce((a, b) => a + b.duration, 0),
|
||||
0,
|
||||
)
|
||||
: process.bursts.filter(b => b.type === 'io').reduce((a, b) => a + b.duration, 0);
|
||||
|
||||
const boundVariant = bound === 'CPU Bound' ? 'cpu' : bound === 'I/O Bound' ? 'io' : 'balanced';
|
||||
|
||||
@@ -38,7 +56,13 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
||||
</span>
|
||||
)}
|
||||
<span className="font-semibold text-sm text-slate-100 truncate">{process.name}</span>
|
||||
<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 className="flex flex-wrap gap-x-3 gap-y-0.5 text-[11px] text-slate-400">
|
||||
<span>Arrival: <span className="text-slate-300 font-medium">{process.arrivalTime}</span></span>
|
||||
@@ -47,7 +71,11 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
||||
{totalIo > 0 && (
|
||||
<span>I/O: <span className="text-cyan-400 font-medium">{totalIo}</span></span>
|
||||
)}
|
||||
<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>
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Plus, Minus, Check, X } from 'lucide-react';
|
||||
import type { BurstSegment, Process } from '@core/types';
|
||||
import type { BurstSegment, IntraProcessPolicy, Process, Thread, ThreadModel } from '@core/types';
|
||||
import { useProcessStore } from '../../store/processStore';
|
||||
import IconButton from '../shared/IconButton';
|
||||
import { generateId, nextColor } from '../shared/processUtils';
|
||||
import ThreadEditor from './ThreadEditor';
|
||||
|
||||
interface ProcessFormProps {
|
||||
/** If provided, the form edits this process; otherwise creates a new one. */
|
||||
editingProcess?: Process | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
type FormTab = 'bursts' | 'threads';
|
||||
|
||||
const DEFAULT_BURSTS: BurstSegment[] = [{ type: 'cpu', duration: 4 }];
|
||||
|
||||
const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
@@ -19,12 +21,22 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
|
||||
const isEditing = !!editingProcess;
|
||||
|
||||
// ── Campos base ─────────────────────────────────────────────────────────
|
||||
const [name, setName] = useState('');
|
||||
const [arrivalTime, setArrivalTime] = useState(0);
|
||||
const [priority, setPriority] = useState(5);
|
||||
const [color, setColor] = useState(nextColor);
|
||||
const [bursts, setBursts] = useState<BurstSegment[]>(DEFAULT_BURSTS);
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
const [, setActiveTab] = useState<FormTab>('bursts');
|
||||
|
||||
// ── Campos de threads ────────────────────────────────────────────────────
|
||||
const [hasThreads, setHasThreads] = useState(false);
|
||||
const [threads, setThreads] = useState<Thread[]>([]);
|
||||
const [threadModel, setThreadModel] = useState<ThreadModel>('ONE_TO_ONE');
|
||||
const [threadPolicy, setThreadPolicy] = useState<IntraProcessPolicy>('FCFS');
|
||||
const [threadQuantum, setThreadQuantum] = useState(2);
|
||||
const [kernelThreadCount, setKernelThreadCount] = useState(2);
|
||||
|
||||
useEffect(() => {
|
||||
if (editingProcess) {
|
||||
@@ -33,24 +45,38 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
setPriority(editingProcess.priority);
|
||||
setColor(editingProcess.color);
|
||||
setBursts([...editingProcess.bursts.map((b: BurstSegment) => ({ ...b }))]);
|
||||
|
||||
const hasT = (editingProcess.threads?.length ?? 0) > 0;
|
||||
setHasThreads(hasT);
|
||||
setThreads(editingProcess.threads ? [...editingProcess.threads] : []);
|
||||
setThreadModel(editingProcess.threadModel ?? 'ONE_TO_ONE');
|
||||
setThreadPolicy(editingProcess.threadSchedulingPolicy ?? 'FCFS');
|
||||
setThreadQuantum(editingProcess.threadQuantum ?? 2);
|
||||
setKernelThreadCount(editingProcess.kernelThreadCount ?? 2);
|
||||
setActiveTab(hasT ? 'threads' : 'bursts');
|
||||
} else {
|
||||
setName('');
|
||||
setArrivalTime(0);
|
||||
setPriority(5);
|
||||
setColor(nextColor());
|
||||
setBursts([{ type: 'cpu', duration: 4 }]);
|
||||
setHasThreads(false);
|
||||
setThreads([]);
|
||||
setThreadModel('ONE_TO_ONE');
|
||||
setThreadPolicy('FCFS');
|
||||
setThreadQuantum(2);
|
||||
setKernelThreadCount(2);
|
||||
setActiveTab('bursts');
|
||||
}
|
||||
setErrors([]);
|
||||
}, [editingProcess]);
|
||||
|
||||
// ---- Burst helpers ----
|
||||
|
||||
// ── Helpers de bursts ───────────────────────────────────────────────────
|
||||
const updateBurstDuration = (index: number, value: number) => {
|
||||
setBursts(prev => prev.map((b, i) => i === index ? { ...b, duration: Math.max(1, value) } : b));
|
||||
};
|
||||
|
||||
const addBurst = () => {
|
||||
// enforce alternating cpu/io pattern
|
||||
const lastType = bursts[bursts.length - 1]?.type ?? 'io';
|
||||
const nextType: BurstSegment['type'] = lastType === 'cpu' ? 'io' : 'cpu';
|
||||
setBursts(prev => [...prev, { type: nextType, duration: 2 }]);
|
||||
@@ -58,36 +84,90 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
|
||||
const removeBurst = (index: number) => {
|
||||
if (bursts.length <= 1) return;
|
||||
setBursts(prev => {
|
||||
const next = prev.filter((_, i) => i !== index);
|
||||
// Safe-guard: ensure it still starts (and ends) with cpu
|
||||
return next;
|
||||
});
|
||||
setBursts(prev => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
// ---- Validation ----
|
||||
const toggleThreads = (enabled: boolean) => {
|
||||
setHasThreads(enabled);
|
||||
if (enabled && threads.length === 0) {
|
||||
setThreads([
|
||||
{ tid: `t_${generateId()}`, name: 'T1', priority: 5, bursts: [{ type: 'cpu', duration: 3 }] },
|
||||
{ tid: `t_${generateId()}`, name: 'T2', priority: 5, bursts: [{ type: 'cpu', duration: 2 }] },
|
||||
]);
|
||||
}
|
||||
setActiveTab(enabled ? 'threads' : 'bursts');
|
||||
};
|
||||
|
||||
// ── Validação ───────────────────────────────────────────────────────────
|
||||
const validate = (): boolean => {
|
||||
const errs: string[] = [];
|
||||
if (!name.trim()) errs.push('Name is required');
|
||||
if (name.trim().length > 8) errs.push('Name must be at most 8 characters');
|
||||
if (arrivalTime < 0) errs.push('Arrival Time must be ≥ 0');
|
||||
if (priority < 1 || priority > 10) errs.push('Priority must be between 1 and 10');
|
||||
if (bursts.length === 0) errs.push('There must be at least 1 CPU burst');
|
||||
if (bursts[0]?.type !== 'cpu') errs.push('The first burst must be CPU');
|
||||
if (bursts.at(-1)?.type !== 'cpu') errs.push('The last burst must be CPU');
|
||||
bursts.forEach((b, i) => {
|
||||
if (b.duration < 1) errs.push(`Burst ${i + 1}: duration must be ≥ 1`);
|
||||
if (i > 0 && bursts[i - 1]?.type === b.type) errs.push(`Bursts ${i} and ${i + 1} cannot be the same type (alternate CPU/IO)`);
|
||||
});
|
||||
|
||||
if (hasThreads) {
|
||||
if (threads.length === 0) errs.push('At least one thread is required');
|
||||
threads.forEach((t, ti) => {
|
||||
if (!t.name.trim()) errs.push(`Thread ${ti + 1}: name is required`);
|
||||
if (t.bursts.length === 0) errs.push(`Thread ${ti + 1}: at least 1 burst required`);
|
||||
if (t.bursts[0]?.type !== 'cpu') errs.push(`Thread ${ti + 1}: first burst must be CPU`);
|
||||
if (t.bursts.at(-1)?.type !== 'cpu') errs.push(`Thread ${ti + 1}: last burst must be CPU`);
|
||||
t.bursts.forEach((b, bi) => {
|
||||
if (b.duration < 1) errs.push(`Thread ${ti + 1}, burst ${bi + 1}: duration must be ≥ 1`);
|
||||
if (bi > 0 && t.bursts[bi - 1]?.type === b.type)
|
||||
errs.push(`Thread ${ti + 1}: bursts must alternate CPU/IO`);
|
||||
});
|
||||
});
|
||||
if (threadModel === 'MANY_TO_MANY' && kernelThreadCount > threads.length) {
|
||||
errs.push(`Kernel threads (${kernelThreadCount}) cannot exceed user threads (${threads.length})`);
|
||||
}
|
||||
} else {
|
||||
if (bursts.length === 0) errs.push('There must be at least 1 CPU burst');
|
||||
if (bursts[0]?.type !== 'cpu') errs.push('The first burst must be CPU');
|
||||
if (bursts.at(-1)?.type !== 'cpu') errs.push('The last burst must be CPU');
|
||||
bursts.forEach((b, i) => {
|
||||
if (b.duration < 1) errs.push(`Burst ${i + 1}: duration must be ≥ 1`);
|
||||
if (i > 0 && bursts[i - 1]?.type === b.type)
|
||||
errs.push(`Bursts ${i} and ${i + 1} cannot be the same type`);
|
||||
});
|
||||
}
|
||||
|
||||
setErrors(errs);
|
||||
return errs.length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!validate()) return;
|
||||
|
||||
const processData: Partial<Process> = {
|
||||
name: name.trim(),
|
||||
arrivalTime,
|
||||
priority,
|
||||
color,
|
||||
bursts: hasThreads ? [] : bursts,
|
||||
...(hasThreads
|
||||
? {
|
||||
threads,
|
||||
threadModel,
|
||||
threadSchedulingPolicy: threadPolicy,
|
||||
threadQuantum,
|
||||
kernelThreadCount:
|
||||
threadModel === 'MANY_TO_MANY'
|
||||
? kernelThreadCount
|
||||
: undefined,
|
||||
}
|
||||
: {
|
||||
threads: undefined,
|
||||
threadModel: undefined,
|
||||
threadSchedulingPolicy: undefined,
|
||||
threadQuantum: undefined,
|
||||
kernelThreadCount: undefined,
|
||||
}),
|
||||
};
|
||||
|
||||
if (isEditing && editingProcess) {
|
||||
updateProcess(editingProcess.id, { name: name.trim(), arrivalTime, priority, color, bursts });
|
||||
updateProcess(editingProcess.id, processData);
|
||||
} else {
|
||||
addProcess({
|
||||
id: generateId(),
|
||||
@@ -95,7 +175,17 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
arrivalTime,
|
||||
priority,
|
||||
color,
|
||||
bursts,
|
||||
bursts: hasThreads ? [] : bursts,
|
||||
...(hasThreads
|
||||
? {
|
||||
threads,
|
||||
threadModel,
|
||||
threadSchedulingPolicy: threadPolicy,
|
||||
threadQuantum,
|
||||
kernelThreadCount:
|
||||
threadModel === 'MANY_TO_MANY' ? kernelThreadCount : undefined,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
@@ -118,7 +208,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
{/* Basic fields */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="col-span-2 flex gap-3">
|
||||
{/* Name */}
|
||||
<div className="flex-1">
|
||||
<label className={labelCls} htmlFor="pf-name">Name</label>
|
||||
<input
|
||||
@@ -132,7 +221,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
aria-label="Process name"
|
||||
/>
|
||||
</div>
|
||||
{/* Color */}
|
||||
<div>
|
||||
<label className={labelCls} htmlFor="pf-color">Color</label>
|
||||
<input
|
||||
@@ -146,7 +234,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Arrival */}
|
||||
<div>
|
||||
<label className={labelCls} htmlFor="pf-arrival">Arrival Time</label>
|
||||
<input
|
||||
@@ -160,7 +247,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Priority */}
|
||||
<div>
|
||||
<label className={labelCls} htmlFor="pf-priority">Priority (1–10)</label>
|
||||
<input
|
||||
@@ -176,51 +262,91 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bursts editor */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-xs text-slate-400 font-medium">Bursts (CPU → I/O → CPU…)</label>
|
||||
<IconButton size="sm" variant="ghost" label="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>
|
||||
))}
|
||||
{/* Toggle threads */}
|
||||
<div className="flex items-center justify-between border-t border-slate-700 pt-3">
|
||||
<div>
|
||||
<span className="text-xs text-slate-300 font-medium">Use Threads</span>
|
||||
<p className="text-[10px] text-slate-500">Substitui bursts por threads com execução própria</p>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={hasThreads}
|
||||
onClick={() => toggleThreads(!hasThreads)}
|
||||
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
|
||||
hasThreads ? 'bg-blue-600' : 'bg-slate-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-3.5 w-3.5 rounded-full bg-white shadow transition-transform duration-200 ${
|
||||
hasThreads ? 'translate-x-4' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs: Bursts / Threads */}
|
||||
{!hasThreads ? (
|
||||
/* ── Editor de bursts (original) ─────────────────────────── */
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-xs text-slate-400 font-medium">Bursts (CPU → I/O → CPU…)</label>
|
||||
<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 */}
|
||||
{errors.length > 0 && (
|
||||
<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 }[] = [
|
||||
{ value: 'FCFS', label: 'FCFS — First Come First Served' },
|
||||
{ value: 'SJF_NP', label: 'SJF — Shortest Job First (NP)' },
|
||||
{ value: 'SJF_P', label: 'SRTF — Shortest Remaining Time (P)' },
|
||||
{ value: 'SJF_NP', label: 'SJF — Shortest Job First (Non-preemptive)' },
|
||||
{ value: 'SJF_P', label: 'SRTF — Shortest Remaining Time (Preemptive)' },
|
||||
{ value: 'RR', label: 'RR — Round Robin' },
|
||||
{ value: 'PRIORITY_NP',label: 'Priority (Non-preemptive)' },
|
||||
{ value: 'PRIORITY_P', label: 'Priority (Preemptive)' },
|
||||
{ value: 'PRIORITY_RR',label: 'Priority Round Robin (real-time)' },
|
||||
{ value: 'HRRN', label: 'HRRN — Highest Response Ratio Next' },
|
||||
{ value: 'MULTILEVEL', label: 'Multilevel Queue (MLQ)' },
|
||||
{ value: 'MULTILEVEL', label: 'MLQ — Multilevel Queue' },
|
||||
{ value: 'MLFQ', label: 'MLFQ — Multilevel Feedback Queue' },
|
||||
];
|
||||
|
||||
const AlgorithmSelector = () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
import { useProcessStore } from '../../store/processStore';
|
||||
import type { MlqQueueDef, SchedulingAlgorithm } from '@core/types';
|
||||
import type { MlfqLevelDef, MlqQueueDef, SchedulingAlgorithm } from '@core/types';
|
||||
import { MLFQAlgorithm } from '@core/algorithms/mlfq';
|
||||
|
||||
// Algorithms that require a global quantum parameter
|
||||
const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'PRIORITY_RR']);
|
||||
@@ -9,7 +10,7 @@ const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'PRIORITY_RR']);
|
||||
const AGING_ALGORITHMS = new Set<SchedulingAlgorithm>(['PRIORITY_NP', 'PRIORITY_P', 'PRIORITY_RR']);
|
||||
|
||||
// Algorithms that are preemptive by definition (used for read-only info only)
|
||||
const PREEMPTIVE_BY_DESIGN = new Set<SchedulingAlgorithm>(['SJF_P', 'RR', 'PRIORITY_P', 'PRIORITY_RR', 'MULTILEVEL']);
|
||||
const PREEMPTIVE_BY_DESIGN = new Set<SchedulingAlgorithm>(['SJF_P', 'RR', 'PRIORITY_P', 'PRIORITY_RR', 'MULTILEVEL', 'MLFQ']);
|
||||
|
||||
const inputCls = 'w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-sm px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500';
|
||||
const labelCls = 'block text-xs text-slate-400 mb-1';
|
||||
@@ -153,6 +154,94 @@ const MlqQueuesConfig = ({
|
||||
);
|
||||
};
|
||||
|
||||
const MlfqLevelsConfig = ({
|
||||
levels,
|
||||
boostInterval,
|
||||
onLevelsChange,
|
||||
onBoostChange,
|
||||
}: {
|
||||
levels: MlfqLevelDef[];
|
||||
boostInterval: number;
|
||||
onLevelsChange: (levels: MlfqLevelDef[]) => void;
|
||||
onBoostChange: (v: number) => void;
|
||||
}) => {
|
||||
const update = (idx: number, quantum: number) => {
|
||||
onLevelsChange(levels.map((l, i) => (i === idx ? { quantum } : l)));
|
||||
};
|
||||
|
||||
const addLevel = () => {
|
||||
if (levels.length >= 5) return;
|
||||
onLevelsChange([...levels, { quantum: levels[levels.length - 1]!.quantum * 2 }]);
|
||||
};
|
||||
|
||||
const removeLevel = (idx: number) => {
|
||||
if (levels.length <= 1) return;
|
||||
onLevelsChange(levels.filter((_, i) => i !== idx));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2 pt-1" data-testid="mlfq-levels-config">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-400 font-medium">MLFQ Levels (decreasing priority)</span>
|
||||
<button
|
||||
onClick={addLevel}
|
||||
disabled={levels.length >= 5}
|
||||
className="flex items-center gap-1 text-[10px] text-blue-400 hover:text-blue-300 disabled:text-slate-600 disabled:cursor-not-allowed transition-colors"
|
||||
aria-label="Add MLFQ level"
|
||||
>
|
||||
<Plus size={11} /> Level
|
||||
</button>
|
||||
</div>
|
||||
{levels.map((l, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="rounded-md border border-slate-600 bg-slate-800/60 p-2 space-y-1.5"
|
||||
data-testid={`mlfq-level-${idx}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-slate-400 font-semibold uppercase tracking-wide">
|
||||
Level {idx} {idx === 0 ? '(highest)' : idx === levels.length - 1 ? '(lowest)' : ''}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => removeLevel(idx)}
|
||||
disabled={levels.length <= 1}
|
||||
className="text-slate-500 hover:text-red-400 disabled:text-slate-700 disabled:cursor-not-allowed transition-colors"
|
||||
aria-label={`Remove level ${idx}`}
|
||||
>
|
||||
<Trash2 size={11} />
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] text-slate-500 mb-0.5">Quantum (ticks)</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={l.quantum}
|
||||
onChange={e => update(idx, Math.max(1, Number(e.target.value)))}
|
||||
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
aria-label={`Level ${idx}: quantum`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<label className="block text-[10px] text-slate-500 mb-0.5">
|
||||
Boost Interval (ticks, 0 = disabled)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
value={boostInterval}
|
||||
onChange={e => onBoostChange(Math.max(0, Number(e.target.value)))}
|
||||
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
aria-label="MLFQ boost interval"
|
||||
data-testid="mlfq-boost-interval"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SchedulerConfig = () => {
|
||||
const config = useProcessStore(s => s.config);
|
||||
const setConfig = useProcessStore(s => s.setConfig);
|
||||
@@ -250,6 +339,16 @@ const SchedulerConfig = () => {
|
||||
onChange={queues => setConfig({ mlqQueues: queues })}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* MLFQ Level Configuration */}
|
||||
{algo === 'MLFQ' && (
|
||||
<MlfqLevelsConfig
|
||||
levels={config.mlfqLevels ?? MLFQAlgorithm.DEFAULT_LEVELS}
|
||||
boostInterval={config.mlfqBoostInterval ?? 0}
|
||||
onLevelsChange={levels => setConfig({ mlfqLevels: levels })}
|
||||
onBoostChange={v => setConfig({ mlfqBoostInterval: v })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,690 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Quantum de processo com threads ────────────────────────────────────────
|
||||
//
|
||||
// Testa o comportamento correto quando o burst de uma thread termina
|
||||
// exatamente no mesmo tick em que o quantum do processo expira.
|
||||
// Antes do fix, o processo recebia quantum+1 ticks (bug).
|
||||
|
||||
describe("Engine — quantum de processo com threads (regressão)", () => {
|
||||
const rrConfig: SchedulerConfig = {
|
||||
algorithm: "RR",
|
||||
quantum: 2,
|
||||
contextSwitchTime: 0,
|
||||
isPreemptive: false,
|
||||
agingEnabled: false,
|
||||
agingInterval: 5,
|
||||
};
|
||||
|
||||
const priorityRrConfig: SchedulerConfig = {
|
||||
algorithm: "PRIORITY_RR",
|
||||
quantum: 2,
|
||||
contextSwitchTime: 0,
|
||||
isPreemptive: false,
|
||||
agingEnabled: false,
|
||||
agingInterval: 5,
|
||||
};
|
||||
|
||||
it("RR: segundo processo recebe CPU no tick 2 quando burst da thread = quantum", () => {
|
||||
// P1: thread T1 com CPU=2 (= quantum). Ao fim do tick 1, T1 termina E
|
||||
// o quantum expira ao mesmo tempo. P1 deve ceder a CPU imediatamente.
|
||||
// P2 deve começar no tick 2, não no tick 3.
|
||||
const p1: Process = {
|
||||
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: 2 }] },
|
||||
{ tid: "t2", name: "T2", bursts: [{ type: "cpu", duration: 2 }] },
|
||||
],
|
||||
};
|
||||
const p2: Process = {
|
||||
pid: 2,
|
||||
id: "p2",
|
||||
name: "P2",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#81C784",
|
||||
bursts: [{ type: "cpu", duration: 3 }],
|
||||
};
|
||||
|
||||
const engine = new SimulationEngine([p1, p2], rrConfig);
|
||||
const ticks = engine.runAll();
|
||||
const cpu = ticks.map((t) => t.cpuProcess);
|
||||
|
||||
// Tick 0 e 1: P1 roda (T1, burst=2=quantum)
|
||||
expect(cpu[0]).toBe("p1");
|
||||
expect(cpu[1]).toBe("p1");
|
||||
// Tick 2: P2 deve receber CPU (não tick 3)
|
||||
expect(cpu[2]).toBe("p2");
|
||||
|
||||
const rt2 = engine.runtimeStates.find((r) => r.processId === "p2")!;
|
||||
expect(rt2.startTick).toBe(2);
|
||||
});
|
||||
|
||||
it("RR: processo alterna exatamente a cada quantum quando bursts coincidem com quantum", () => {
|
||||
// Ambos os processos têm threads com CPU=2 = quantum.
|
||||
// Devem alternar estritamente: P1(0-1), P2(2-3), P1(4-5), P2(6-7)...
|
||||
const makeThreaded = (id: string, pid: number, color: string): Process => ({
|
||||
pid,
|
||||
id,
|
||||
name: id,
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color,
|
||||
bursts: [],
|
||||
threadModel: "ONE_TO_ONE" as const,
|
||||
threadSchedulingPolicy: "FCFS" as const,
|
||||
threads: [
|
||||
{ tid: `${id}_t1`, name: "T1", bursts: [{ type: "cpu" as const, duration: 2 }] },
|
||||
{ tid: `${id}_t2`, name: "T2", bursts: [{ type: "cpu" as const, duration: 2 }] },
|
||||
],
|
||||
});
|
||||
|
||||
const engine = new SimulationEngine(
|
||||
[makeThreaded("p1", 1, "#4FC3F7"), makeThreaded("p2", 2, "#81C784")],
|
||||
rrConfig,
|
||||
);
|
||||
const ticks = engine.runAll();
|
||||
const cpu = ticks.map((t) => t.cpuProcess);
|
||||
|
||||
// Cada bloco de 2 ticks deve pertencer a um processo diferente
|
||||
expect(cpu[0]).toBe("p1");
|
||||
expect(cpu[1]).toBe("p1");
|
||||
expect(cpu[2]).toBe("p2");
|
||||
expect(cpu[3]).toBe("p2");
|
||||
expect(cpu[4]).toBe("p1");
|
||||
expect(cpu[5]).toBe("p1");
|
||||
expect(cpu[6]).toBe("p2");
|
||||
expect(cpu[7]).toBe("p2");
|
||||
});
|
||||
|
||||
it("PRIORITY_RR: reproduz o cenário reportado — P2.startTick deve ser 2", () => {
|
||||
// Replica o config.json do usuário (versão simplificada):
|
||||
// App: ONE_TO_ONE, PRIORITY, High-1(prio=1, cpu=2), High-2(prio=2, cpu=2)
|
||||
// P2: ONE_TO_ONE, FCFS, T1(cpu=3)
|
||||
// Algoritmo: PRIORITY_RR, quantum=2, ambos prio=1
|
||||
// Bug: App rodava ticks 0,1,2 (quantum+1). Correto: 0,1 → P2 começa no tick 2.
|
||||
const app: Process = {
|
||||
pid: 1,
|
||||
id: "app",
|
||||
name: "App",
|
||||
arrivalTime: 0,
|
||||
priority: 1,
|
||||
color: "#F06292",
|
||||
bursts: [],
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threadSchedulingPolicy: "PRIORITY",
|
||||
threads: [
|
||||
{ tid: "h1", name: "High-1", priority: 1, bursts: [{ type: "cpu", duration: 2 }] },
|
||||
{ tid: "h2", name: "High-2", priority: 2, bursts: [{ type: "cpu", duration: 2 }] },
|
||||
],
|
||||
};
|
||||
const p2: Process = {
|
||||
pid: 2,
|
||||
id: "p2",
|
||||
name: "P2",
|
||||
arrivalTime: 0,
|
||||
priority: 1,
|
||||
color: "#f472b6",
|
||||
bursts: [],
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threadSchedulingPolicy: "FCFS",
|
||||
threads: [
|
||||
{ tid: "p2t1", name: "T1", bursts: [{ type: "cpu", duration: 3 }] },
|
||||
],
|
||||
};
|
||||
|
||||
const engine = new SimulationEngine([app, p2], priorityRrConfig);
|
||||
const ticks = engine.runAll();
|
||||
const cpu = ticks.map((t) => t.cpuProcess);
|
||||
|
||||
// App roda ticks 0 e 1 (quantum=2). P2 deve começar no tick 2.
|
||||
expect(cpu[0]).toBe("app");
|
||||
expect(cpu[1]).toBe("app");
|
||||
expect(cpu[2]).toBe("p2");
|
||||
|
||||
const p2Rt = engine.runtimeStates.find((r) => r.processId === "p2")!;
|
||||
expect(p2Rt.startTick).toBe(2);
|
||||
// responseTime de threads é calculado em _dispatchThreadForProcess
|
||||
const p2ThreadRts = engine.threadRuntimeStates.filter((t) => t.processId === "p2");
|
||||
expect(p2ThreadRts[0]!.startTick).toBe(2);
|
||||
expect(p2ThreadRts[0]!.responseTime).toBe(2);
|
||||
});
|
||||
|
||||
it("thread com burst menor que quantum não causa preempção prematura", () => {
|
||||
// T1 tem burst=1 (< quantum=2). T1 termina no tick 1 sem quantum expirado.
|
||||
// T2 deve continuar rodando no tick 2 (dentro do mesmo quantum).
|
||||
// P2 só recebe CPU no tick 3 (quando quantum do processo expirar com T2).
|
||||
const p1: Process = {
|
||||
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: 1 }] },
|
||||
{ tid: "t2", name: "T2", bursts: [{ type: "cpu", duration: 3 }] },
|
||||
],
|
||||
};
|
||||
const p2: Process = {
|
||||
pid: 2,
|
||||
id: "p2",
|
||||
name: "P2",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#81C784",
|
||||
bursts: [{ type: "cpu", duration: 2 }],
|
||||
};
|
||||
|
||||
const engine = new SimulationEngine([p1, p2], rrConfig);
|
||||
const ticks = engine.runAll();
|
||||
const cpu = ticks.map((t) => t.cpuProcess);
|
||||
|
||||
// Tick 0: P1/T1 roda (burst=1, quantum=2 → quantum não expirado)
|
||||
expect(cpu[0]).toBe("p1");
|
||||
// Tick 1: P1/T2 assume (quantum restante=1)
|
||||
expect(cpu[1]).toBe("p1");
|
||||
// Tick 2: quantum do P1 expira → P2 recebe CPU
|
||||
expect(cpu[2]).toBe("p2");
|
||||
});
|
||||
|
||||
it("processo com thread em I/O no momento do quantum cede corretamente", () => {
|
||||
// T1: CPU(2) → IO(2) → CPU(1). Quantum=2.
|
||||
// T1 termina burst CPU no tick 1 E quantum=0: vai para IO, processo cede.
|
||||
// P2 deve receber CPU no tick 2.
|
||||
const p1: Process = {
|
||||
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: 2 },
|
||||
{ type: "io", duration: 2 },
|
||||
{ type: "cpu", duration: 1 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const p2: Process = {
|
||||
pid: 2,
|
||||
id: "p2",
|
||||
name: "P2",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#81C784",
|
||||
bursts: [{ type: "cpu", duration: 3 }],
|
||||
};
|
||||
|
||||
const engine = new SimulationEngine([p1, p2], rrConfig);
|
||||
const ticks = engine.runAll();
|
||||
const cpu = ticks.map((t) => t.cpuProcess);
|
||||
|
||||
expect(cpu[0]).toBe("p1");
|
||||
expect(cpu[1]).toBe("p1");
|
||||
// Tick 2: T1 foi para IO e quantum expirou → P2 recebe CPU
|
||||
expect(cpu[2]).toBe("p2");
|
||||
});
|
||||
});
|
||||
|
||||
// ── 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 {
|
||||
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 { HRRNAlgorithm } from "./hrrn";
|
||||
import { MultilevelQueueAlgorithm } from "./multilevel";
|
||||
import { MLFQAlgorithm } from "./mlfq";
|
||||
|
||||
export type { BaseAlgorithm };
|
||||
|
||||
@@ -28,4 +29,5 @@ export const algorithms: Record<SchedulingAlgorithm, BaseAlgorithm> = {
|
||||
PRIORITY_RR: new PriorityRoundRobinAlgorithm(),
|
||||
HRRN: new HRRNAlgorithm(),
|
||||
MULTILEVEL: new MultilevelQueueAlgorithm(),
|
||||
MLFQ: new MLFQAlgorithm(),
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+685
-137
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,11 @@
|
||||
import type {
|
||||
GlobalMetrics,
|
||||
Process,
|
||||
ProcessMetrics,
|
||||
ProcessRuntime,
|
||||
Thread,
|
||||
ThreadMetrics,
|
||||
ThreadRuntime,
|
||||
} from "./types";
|
||||
|
||||
// ============================================================
|
||||
@@ -83,6 +87,41 @@ export function computeGlobalMetrics(
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Thread metrics
|
||||
// ============================================================
|
||||
|
||||
export function computeThreadMetrics(
|
||||
trt: ThreadRuntime,
|
||||
thread: Thread,
|
||||
process: Process,
|
||||
): ThreadMetrics {
|
||||
if (trt.startTick === null || trt.finishTick === null) {
|
||||
throw new Error(
|
||||
`Cannot compute metrics for thread ${trt.threadId}: not yet terminated`,
|
||||
);
|
||||
}
|
||||
|
||||
const turnaroundTime = trt.finishTick - trt.arrivalTick;
|
||||
const responseTime = trt.startTick - trt.arrivalTick;
|
||||
const waitingTime = turnaroundTime - trt.cpuTime - trt.ioTime;
|
||||
|
||||
return {
|
||||
threadId: trt.threadId,
|
||||
processId: trt.processId,
|
||||
threadName: thread.name,
|
||||
processName: process.name,
|
||||
arrivalTime: trt.arrivalTick,
|
||||
startTick: trt.startTick,
|
||||
finishTick: trt.finishTick,
|
||||
responseTime,
|
||||
turnaroundTime,
|
||||
waitingTime: Math.max(0, waitingTime),
|
||||
cpuTime: trt.cpuTime,
|
||||
ioTime: trt.ioTime,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Classify process by declared burst ratios (static, pre-simulation)
|
||||
// Used in the process list preview badge.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { MlqQueueDef, Process, SchedulerConfig } from "./types";
|
||||
// Thread preset IDs are prefixed with "t_" to avoid conflicts
|
||||
|
||||
export interface Preset {
|
||||
name: string;
|
||||
@@ -127,4 +128,319 @@ export const presets: Preset[] = [
|
||||
],
|
||||
config: defaultConfig({ algorithm: "MULTILEVEL", quantum: 2, mlqQueues: DEFAULT_MLQ_QUEUES }),
|
||||
},
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// PRESETS DE THREADS — demonstram os três modelos e conceitos de concorrência
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
{
|
||||
name: "many_to_one",
|
||||
description:
|
||||
"MANY_TO_ONE: 2 processos com 3 threads cada. Uma thread em I/O bloqueia todo o processo — observe como o processo fica Waiting enquanto as outras threads ficam ociosas.",
|
||||
processes: [
|
||||
{
|
||||
pid: 1,
|
||||
id: "t_p1",
|
||||
name: "Srv-A",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#4FC3F7",
|
||||
bursts: [],
|
||||
threadModel: "MANY_TO_ONE",
|
||||
threadSchedulingPolicy: "RR",
|
||||
threadQuantum: 2,
|
||||
threads: [
|
||||
{
|
||||
tid: "t_p1_t1",
|
||||
name: "A-T1",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 3 },
|
||||
{ type: "io", duration: 4 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t2",
|
||||
name: "A-T2",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 2 },
|
||||
{ type: "cpu", duration: 3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t3",
|
||||
name: "A-T3",
|
||||
bursts: [{ type: "cpu", duration: 4 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pid: 2,
|
||||
id: "t_p2",
|
||||
name: "Srv-B",
|
||||
arrivalTime: 2,
|
||||
priority: 5,
|
||||
color: "#81C784",
|
||||
bursts: [],
|
||||
threadModel: "MANY_TO_ONE",
|
||||
threadSchedulingPolicy: "FCFS",
|
||||
threads: [
|
||||
{
|
||||
tid: "t_p2_t1",
|
||||
name: "B-T1",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 3 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p2_t2",
|
||||
name: "B-T2",
|
||||
bursts: [{ type: "cpu", duration: 5 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
config: defaultConfig({ algorithm: "RR", quantum: 4 }),
|
||||
},
|
||||
|
||||
{
|
||||
name: "one_to_one",
|
||||
description:
|
||||
"ONE_TO_ONE: mesmo cenário do Many-to-One mas com kernel threads independentes. Threads em I/O não bloqueiam o processo — compare o throughput com o preset many_to_one.",
|
||||
processes: [
|
||||
{
|
||||
pid: 1,
|
||||
id: "t_p1",
|
||||
name: "Srv-A",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#4FC3F7",
|
||||
bursts: [],
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threadSchedulingPolicy: "RR",
|
||||
threadQuantum: 2,
|
||||
threads: [
|
||||
{
|
||||
tid: "t_p1_t1",
|
||||
name: "A-T1",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 3 },
|
||||
{ type: "io", duration: 4 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t2",
|
||||
name: "A-T2",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 2 },
|
||||
{ type: "cpu", duration: 3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t3",
|
||||
name: "A-T3",
|
||||
bursts: [{ type: "cpu", duration: 4 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pid: 2,
|
||||
id: "t_p2",
|
||||
name: "Srv-B",
|
||||
arrivalTime: 2,
|
||||
priority: 5,
|
||||
color: "#81C784",
|
||||
bursts: [],
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threadSchedulingPolicy: "FCFS",
|
||||
threads: [
|
||||
{
|
||||
tid: "t_p2_t1",
|
||||
name: "B-T1",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 3 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p2_t2",
|
||||
name: "B-T2",
|
||||
bursts: [{ type: "cpu", duration: 5 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
config: defaultConfig({ algorithm: "RR", quantum: 4 }),
|
||||
},
|
||||
|
||||
{
|
||||
name: "many_to_many",
|
||||
description:
|
||||
"MANY_TO_MANY: processo com 4 threads de usuário mas apenas 2 kernel threads (M=2). Observe threads em KernelWait aguardando slot disponível.",
|
||||
processes: [
|
||||
{
|
||||
pid: 1,
|
||||
id: "t_p1",
|
||||
name: "App",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#CE93D8",
|
||||
bursts: [],
|
||||
threadModel: "MANY_TO_MANY",
|
||||
kernelThreadCount: 2,
|
||||
threadSchedulingPolicy: "PRIORITY",
|
||||
threads: [
|
||||
{
|
||||
tid: "t_p1_t1",
|
||||
name: "Worker1",
|
||||
priority: 1,
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 3 },
|
||||
{ type: "io", duration: 2 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t2",
|
||||
name: "Worker2",
|
||||
priority: 2,
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 3 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t3",
|
||||
name: "Worker3",
|
||||
priority: 3,
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 4 },
|
||||
{ type: "io", duration: 1 },
|
||||
{ type: "cpu", duration: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t4",
|
||||
name: "Worker4",
|
||||
priority: 4,
|
||||
bursts: [{ type: "cpu", duration: 3 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
config: defaultConfig({ algorithm: "FCFS" }),
|
||||
},
|
||||
|
||||
{
|
||||
name: "thread_starvation",
|
||||
description:
|
||||
"Starvation de thread: processo com 3 threads em política Priority. A thread de baixa prioridade (T3) é indefinidamente preterida pelas de alta prioridade.",
|
||||
processes: [
|
||||
{
|
||||
pid: 1,
|
||||
id: "t_p1",
|
||||
name: "App",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#F06292",
|
||||
bursts: [],
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threadSchedulingPolicy: "PRIORITY",
|
||||
threads: [
|
||||
{
|
||||
tid: "t_p1_t1",
|
||||
name: "High-1",
|
||||
priority: 1,
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 1 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 1 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t2",
|
||||
name: "High-2",
|
||||
priority: 2,
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 1 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 1 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t3",
|
||||
name: "Low",
|
||||
priority: 10,
|
||||
bursts: [{ type: "cpu", duration: 3 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
config: defaultConfig({ algorithm: "FCFS" }),
|
||||
},
|
||||
|
||||
{
|
||||
name: "threads_vs_processes",
|
||||
description:
|
||||
"Threads vs Processos: P1 usa 2 threads ONE_TO_ONE para uma tarefa. P2 faz o mesmo trabalho como processo único. Compare turnaround e throughput.",
|
||||
processes: [
|
||||
{
|
||||
pid: 1,
|
||||
id: "t_p1",
|
||||
name: "Multi-T",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#4FC3F7",
|
||||
bursts: [],
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threadSchedulingPolicy: "RR",
|
||||
threadQuantum: 3,
|
||||
threads: [
|
||||
{
|
||||
tid: "t_p1_t1",
|
||||
name: "T1",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 3 },
|
||||
{ type: "io", duration: 2 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t2",
|
||||
name: "T2",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 3 },
|
||||
{ type: "cpu", duration: 3 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pid: 2,
|
||||
id: "t_p2",
|
||||
name: "Single-P",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#FFB74D",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 5 },
|
||||
{ type: "io", duration: 3 },
|
||||
{ type: "cpu", duration: 5 },
|
||||
],
|
||||
},
|
||||
],
|
||||
config: defaultConfig({ algorithm: "RR", quantum: 4 }),
|
||||
},
|
||||
];
|
||||
@@ -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_RR"
|
||||
| "MULTILEVEL"
|
||||
| "HRRN";
|
||||
| "HRRN"
|
||||
| "MLFQ";
|
||||
|
||||
// --------------- Thread types --------------------------------
|
||||
|
||||
/**
|
||||
* Modelo de mapeamento threads de usuário → threads de kernel.
|
||||
*
|
||||
* MANY_TO_ONE — N threads de usuário para 1 kernel thread (green threads).
|
||||
* Uma thread em I/O bloqueia todo o processo.
|
||||
* ONE_TO_ONE — 1 thread de usuário para 1 kernel thread (pthreads/POSIX).
|
||||
* Threads bloqueiam independentemente.
|
||||
* MANY_TO_MANY — N threads de usuário para M kernel threads (M ≤ N).
|
||||
* Slots de kernel são liberados durante I/O e reatribuídos.
|
||||
*/
|
||||
export type ThreadModel =
|
||||
| "MANY_TO_ONE"
|
||||
| "ONE_TO_ONE"
|
||||
| "MANY_TO_MANY";
|
||||
|
||||
/** Política de escalonamento intra-processo (entre threads da mesma processo). */
|
||||
export type IntraProcessPolicy = "FCFS" | "RR" | "PRIORITY";
|
||||
|
||||
/** Definição estática de uma thread dentro de um processo. */
|
||||
export interface Thread {
|
||||
tid: string; // identificador único dentro do processo
|
||||
name: string;
|
||||
bursts: BurstSegment[]; // sequência própria de bursts CPU/IO
|
||||
priority?: number; // 1 (mais alta) – 10 (mais baixa); default 5
|
||||
}
|
||||
|
||||
// --------------- Static process definition ------------------
|
||||
|
||||
@@ -27,7 +56,13 @@ export interface Process {
|
||||
arrivalTime: number; // tick of arrival (≥ 0)
|
||||
priority: number; // 1 (highest) – 10 (lowest); default 5
|
||||
color: string; // HEX color for Gantt display
|
||||
bursts: BurstSegment[]; // must start (and end) with a CPU burst
|
||||
bursts: BurstSegment[]; // must start (and end) with a CPU burst; ignored when threads defined
|
||||
// ── Thread fields (optional — backward compatible) ──────
|
||||
threads?: Thread[]; // se definido, bursts acima são ignorados
|
||||
threadModel?: ThreadModel; // default ONE_TO_ONE quando threads definidas
|
||||
threadSchedulingPolicy?: IntraProcessPolicy; // política intra-processo; default FCFS
|
||||
threadQuantum?: number; // quantum para política RR intra-processo; default 2
|
||||
kernelThreadCount?: number; // apenas MANY_TO_MANY: M ≤ threads.length; default ceil(N/2)
|
||||
}
|
||||
|
||||
// --------------- Scheduler configuration --------------------
|
||||
@@ -39,6 +74,10 @@ export interface MlqQueueDef {
|
||||
quantum: number; // used only when algorithm === "RR"
|
||||
}
|
||||
|
||||
export interface MlfqLevelDef {
|
||||
quantum: number; // time slice for this level (ticks, >= 1)
|
||||
}
|
||||
|
||||
export interface SchedulerConfig {
|
||||
algorithm: SchedulingAlgorithm;
|
||||
quantum: number; // for RR (default 2)
|
||||
@@ -47,17 +86,41 @@ export interface SchedulerConfig {
|
||||
agingEnabled: boolean; // prevent starvation in Priority scheduling
|
||||
agingInterval: number; // ticks in Ready before priority-- (min 1)
|
||||
mlqQueues?: MlqQueueDef[]; // queue definitions for MULTILEVEL algorithm
|
||||
mlfqLevels?: MlfqLevelDef[]; // level definitions for MLFQ algorithm
|
||||
mlfqBoostInterval?: number; // ticks in Ready before boost to level 0; 0 = disabled
|
||||
}
|
||||
|
||||
// --------------- Runtime state ------------------------------
|
||||
|
||||
export type StateLabel =
|
||||
| "New" // before arrivalTime
|
||||
| "Ready" // in ready queue, waiting for CPU
|
||||
| "Running" // executing CPU burst
|
||||
| "Waiting" // executing I/O burst
|
||||
| "New" // before arrivalTime
|
||||
| "Ready" // in ready queue, waiting for CPU
|
||||
| "Running" // executing CPU burst
|
||||
| "Waiting" // executing I/O burst
|
||||
| "Terminated"; // finished all bursts
|
||||
|
||||
/**
|
||||
* Estado de runtime de uma thread individual.
|
||||
* Espelha ProcessRuntime mas no nível de thread.
|
||||
*/
|
||||
export interface ThreadRuntime {
|
||||
threadId: string;
|
||||
processId: string;
|
||||
state: StateLabel;
|
||||
remainingBurst: number;
|
||||
burstIndex: number;
|
||||
arrivalTick: number; // mesmo que o processo pai
|
||||
startTick: number | null; // primeiro tick em Running
|
||||
finishTick: number | null;
|
||||
waitingTime: number;
|
||||
cpuTime: number;
|
||||
ioTime: number;
|
||||
responseTime: number | null;
|
||||
quantumRemaining: number; // quantum intra-processo (para política RR)
|
||||
currentPriority: number;
|
||||
kernelWaiting?: boolean; // MANY_TO_MANY: aguardando slot de kernel thread
|
||||
}
|
||||
|
||||
export interface ProcessRuntime {
|
||||
// ---- spec fields ----
|
||||
processId: string;
|
||||
@@ -74,6 +137,7 @@ export interface ProcessRuntime {
|
||||
// ---- engine-internal fields ----
|
||||
quantumRemaining: number; // remaining quantum for RR; reset on dispatch
|
||||
currentPriority: number; // mutable via aging; starts = Process.priority
|
||||
mlfqLevel?: number; // current MLFQ level (0 = highest); undefined for non-MLFQ runs
|
||||
}
|
||||
|
||||
// --------------- Tick snapshot ------------------------------
|
||||
@@ -86,10 +150,30 @@ export interface SimTick {
|
||||
states: Record<string, StateLabel>; // all processId → state
|
||||
contextSwitching: boolean; // true during context-switch overhead
|
||||
ctxSwitchForProcess: string | null; // incoming processId during ctx-switch overhead (null otherwise)
|
||||
// ── Thread state (only present when at least one process has threads) ──
|
||||
threadStates?: Record<string, Record<string, StateLabel>>; // processId → threadId → state
|
||||
cpuThreads?: Record<string, string | null>; // processId → threadId ativo (null se nenhum)
|
||||
kernelWaitingThreads?: Record<string, string[]>; // MANY_TO_MANY: threadIds em espera de slot
|
||||
}
|
||||
|
||||
// --------------- Metrics ------------------------------------
|
||||
|
||||
/** Métricas por thread individual (calculadas após terminação). */
|
||||
export interface ThreadMetrics {
|
||||
threadId: string;
|
||||
processId: string;
|
||||
threadName: string;
|
||||
processName?: string;
|
||||
arrivalTime: number;
|
||||
startTick: number;
|
||||
finishTick: number;
|
||||
responseTime: number;
|
||||
turnaroundTime: number;
|
||||
waitingTime: number;
|
||||
cpuTime: number;
|
||||
ioTime: number;
|
||||
}
|
||||
|
||||
export interface ProcessMetrics {
|
||||
processId: string;
|
||||
arrivalTime: number;
|
||||
|
||||
+51
-6
@@ -1,5 +1,4 @@
|
||||
import type { ProcessMetrics } from "@core/types";
|
||||
import type { Process } from "@core/types";
|
||||
import type { ProcessMetrics, ThreadMetrics, Process } from "@core/types";
|
||||
|
||||
interface ProcessMap {
|
||||
[id: string]: Process;
|
||||
@@ -16,8 +15,13 @@ function downloadBlob(content: string, filename: string, mimeType: string) {
|
||||
}
|
||||
|
||||
export function useExport() {
|
||||
const exportCSV = (metrics: ProcessMetrics[], processMap: ProcessMap) => {
|
||||
const headers = [
|
||||
const exportCSV = (
|
||||
metrics: ProcessMetrics[],
|
||||
processMap: ProcessMap,
|
||||
threadMetrics?: ThreadMetrics[],
|
||||
) => {
|
||||
// ── Seção de processos ─────────────────────────────────────────────────
|
||||
const processHeaders = [
|
||||
"PID",
|
||||
"Process",
|
||||
"Priority",
|
||||
@@ -33,7 +37,7 @@ export function useExport() {
|
||||
"Bound",
|
||||
];
|
||||
|
||||
const rows = metrics.map((m) => {
|
||||
const processRows = metrics.map((m) => {
|
||||
const proc = processMap[m.processId];
|
||||
const pid = proc?.pid ?? "";
|
||||
const name = proc?.name ?? m.processId;
|
||||
@@ -55,7 +59,48 @@ export function useExport() {
|
||||
].join(",");
|
||||
});
|
||||
|
||||
const csv = [headers.join(","), ...rows].join("\n");
|
||||
const sections: string[] = [
|
||||
"# Processes",
|
||||
processHeaders.join(","),
|
||||
...processRows,
|
||||
];
|
||||
|
||||
// ── Seção de threads (opcional) ────────────────────────────────────────
|
||||
if (threadMetrics && threadMetrics.length > 0) {
|
||||
const threadHeaders = [
|
||||
"Thread",
|
||||
"Process",
|
||||
"Arrival",
|
||||
"Start",
|
||||
"Finish",
|
||||
"Response",
|
||||
"Turnaround",
|
||||
"Waiting",
|
||||
"CPU Time",
|
||||
"I/O Time",
|
||||
];
|
||||
|
||||
const threadRows = threadMetrics.map((t) => {
|
||||
const proc = processMap[t.processId];
|
||||
const procName = proc?.name ?? t.processId;
|
||||
return [
|
||||
t.threadName,
|
||||
procName,
|
||||
t.arrivalTime,
|
||||
t.startTick,
|
||||
t.finishTick,
|
||||
t.responseTime,
|
||||
t.turnaroundTime,
|
||||
t.waitingTime,
|
||||
t.cpuTime,
|
||||
t.ioTime,
|
||||
].join(",");
|
||||
});
|
||||
|
||||
sections.push("", "# Threads", threadHeaders.join(","), ...threadRows);
|
||||
}
|
||||
|
||||
const csv = sections.join("\n");
|
||||
downloadBlob(csv, "pss-metrics.csv", "text/csv;charset=utf-8;");
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user