+
+ {presetsOpen && (
+ <>
+ {/* Backdrop */}
+
+
+ );
+};
+
+export default AppHeader;
diff --git a/src/components/Layout/AppLayout.tsx b/src/components/Layout/AppLayout.tsx
index 20ec488..19ff2a6 100644
--- a/src/components/Layout/AppLayout.tsx
+++ b/src/components/Layout/AppLayout.tsx
@@ -1,11 +1,15 @@
+import AppHeader from './AppHeader';
import Sidebar from './Sidebar';
import MainArea from './MainArea';
const AppLayout = () => {
return (
-
-
-
+
);
};
diff --git a/src/components/Layout/MainArea.tsx b/src/components/Layout/MainArea.tsx
index 4b041f5..cd543f0 100644
--- a/src/components/Layout/MainArea.tsx
+++ b/src/components/Layout/MainArea.tsx
@@ -1,23 +1,13 @@
import ControlBar from '../ControlBar/ControlBar';
+import GanttChart from '../GanttChart/GanttChart';
+import MetricsPanel from '../MetricsPanel/MetricsPanel';
const MainArea = () => {
return (
-
-
-
- {/* Placeholder for GanttChart — Phase 4 */}
-
- [ Gantt Chart — Fase 4 ]
-
-
- {/* Placeholder for MetricsPanel — Phase 5 */}
-
-
- [ Metrics Panel — Fase 5 ]
-
-
-
+
+
+
);
};
diff --git a/src/components/MetricsPanel/GlobalMetrics.tsx b/src/components/MetricsPanel/GlobalMetrics.tsx
new file mode 100644
index 0000000..7d39215
--- /dev/null
+++ b/src/components/MetricsPanel/GlobalMetrics.tsx
@@ -0,0 +1,83 @@
+import type { GlobalMetrics as GlobalMetricsType } from '@core/types';
+
+interface GlobalMetricsProps {
+ metrics: GlobalMetricsType;
+}
+
+interface MetricCardProps {
+ label: string;
+ value: string;
+ sub?: string;
+ accent?: string;
+}
+
+const MetricCard = ({ label, value, sub, accent = 'text-blue-300' }: MetricCardProps) => (
+
+ {label}
+ {value}
+ {sub && {sub} }
+
+);
+
+const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
+ const {
+ avgResponseTime,
+ avgTurnaroundTime,
+ avgWaitingTime,
+ cpuThroughput,
+ cpuUtilization,
+ totalSimulationTime,
+ } = metrics;
+
+ const utilizationPct = Math.min(100, cpuUtilization);
+
+ return (
+
+
+
+
+
+
+
+ {/* CPU Utilization card with progress bar */}
+
+
CPU Utilization
+
{utilizationPct.toFixed(1)}%
+
+
+
+
+ );
+};
+
+export default GlobalMetrics;
diff --git a/src/components/MetricsPanel/MetricsChart.tsx b/src/components/MetricsPanel/MetricsChart.tsx
new file mode 100644
index 0000000..27bf118
--- /dev/null
+++ b/src/components/MetricsPanel/MetricsChart.tsx
@@ -0,0 +1,82 @@
+import {
+ BarChart,
+ Bar,
+ XAxis,
+ YAxis,
+ Tooltip,
+ Legend,
+ ResponsiveContainer,
+ CartesianGrid,
+} from 'recharts';
+import type { ProcessMetrics, Process } from '@core/types';
+
+interface MetricsChartProps {
+ metrics: ProcessMetrics[];
+ processMap: Record
;
+}
+
+const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
+ if (metrics.length === 0) {
+ return (
+
+ Nenhum dado disponível.
+
+ );
+ }
+
+ const data = metrics.map((m) => ({
+ name: processMap[m.processId]?.name ?? m.processId,
+ 'Waiting Time': m.waitingTime,
+ 'CPU Time': m.cpuTime,
+ 'I/O Time': m.ioTime,
+ }));
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Barras empilhadas = Turnaround Time (Waiting + CPU + I/O)
+
+
+ );
+};
+
+export default MetricsChart;
diff --git a/src/components/MetricsPanel/MetricsPanel.tsx b/src/components/MetricsPanel/MetricsPanel.tsx
new file mode 100644
index 0000000..8a6c7fb
--- /dev/null
+++ b/src/components/MetricsPanel/MetricsPanel.tsx
@@ -0,0 +1,98 @@
+import { useState } from 'react';
+import { Download, BarChart2, Table, Globe } from 'lucide-react';
+import { computeProcessMetrics, computeGlobalMetrics } 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';
+
+type Tab = 'table' | 'global' | 'chart';
+
+const TAB_CONFIG: { id: Tab; label: string; icon: React.ReactNode }[] = [
+ { id: 'table', label: 'Por Processo', icon: },
+ { id: 'global', label: 'Global', icon: },
+ { id: 'chart', label: 'Gráfico', icon: },
+];
+
+const MetricsPanel = () => {
+ const [activeTab, setActiveTab] = useState('table');
+
+ const engine = useSimulationStore(s => s.engine);
+ const ticks = useSimulationStore(s => s.ticks);
+ const status = useSimulationStore(s => s.status);
+ const processes = useProcessStore(s => s.processes);
+
+ const { exportCSV } = useExport();
+
+ const hasData = ticks.length > 0;
+
+ if (!hasData) {
+ return (
+
+ Execute a simulação para ver as métricas.
+
+ );
+ }
+
+ // Derive metrics from engine runtime state
+ 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]));
+
+ const canExportCSV = status === 'finished' && processMetrics.length > 0;
+
+ return (
+
+ {/* Panel header with tabs + export button */}
+
+ {TAB_CONFIG.map(tab => (
+
setActiveTab(tab.id)}
+ className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md transition-colors ${
+ activeTab === tab.id
+ ? 'bg-slate-700 text-slate-100 font-medium'
+ : 'text-slate-400 hover:text-slate-200 hover:bg-slate-800'
+ }`}
+ aria-pressed={activeTab === tab.id}
+ >
+ {tab.icon}
+ {tab.label}
+
+ ))}
+
+
+ exportCSV(processMetrics, processMap)}
+ disabled={!canExportCSV}
+ title={!canExportCSV ? 'Disponível ao concluir a simulação' : 'Exportar métricas como 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"
+ aria-label="Exportar métricas CSV"
+ >
+
+ CSV
+
+
+
+
+ {/* Tab content */}
+
+ {activeTab === 'table' && (
+
+ )}
+ {activeTab === 'global' && (
+
+ )}
+ {activeTab === 'chart' && (
+
+ )}
+
+
+ );
+};
+
+export default MetricsPanel;
diff --git a/src/components/MetricsPanel/MetricsTable.tsx b/src/components/MetricsPanel/MetricsTable.tsx
new file mode 100644
index 0000000..7602d84
--- /dev/null
+++ b/src/components/MetricsPanel/MetricsTable.tsx
@@ -0,0 +1,82 @@
+import type { ProcessMetrics, Process } from '@core/types';
+import Badge from '../shared/Badge';
+
+interface MetricsTableProps {
+ metrics: ProcessMetrics[];
+ processMap: Record;
+}
+
+function boundVariant(type: ProcessMetrics['boundType']) {
+ if (type === 'CPU Bound') return 'cpu';
+ if (type === 'I/O Bound') return 'io';
+ return 'balanced';
+}
+
+const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
+ if (metrics.length === 0) {
+ return (
+
+ Nenhum processo concluído ainda.
+
+ );
+ }
+
+ return (
+
+
+
+
+ Processo
+ Arrival
+ Start
+ Finish
+ Response
+ Turnaround
+ Waiting
+ CPU Time
+ I/O Time
+ CPU%
+ Bound
+
+
+
+ {metrics.map((m, i) => {
+ const proc = processMap[m.processId];
+ const name = proc?.name ?? m.processId;
+ const color = proc?.color ?? '#94a3b8';
+ return (
+
+
+
+
+ {name}
+
+
+ {m.arrivalTime}
+ {m.startTick}
+ {m.finishTick}
+ {m.responseTime}
+ {m.turnaroundTime}
+ {m.waitingTime}
+ {m.cpuTime}
+ {m.ioTime}
+ {m.cpuUtilization.toFixed(1)}%
+
+ {m.boundType}
+
+
+ );
+ })}
+
+
+
+ );
+};
+
+export default MetricsTable;
diff --git a/src/components/ProcessPanel/ProcessForm.tsx b/src/components/ProcessPanel/ProcessForm.tsx
index a33ab59..ade9638 100644
--- a/src/components/ProcessPanel/ProcessForm.tsx
+++ b/src/components/ProcessPanel/ProcessForm.tsx
@@ -78,7 +78,7 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
if (bursts.at(-1)?.type !== 'cpu') errs.push('O último burst deve ser CPU');
bursts.forEach((b, i) => {
if (b.duration < 1) errs.push(`Burst ${i + 1}: duração deve ser ≥ 1`);
- if (i > 0 && bursts[i - 1].type === b.type) errs.push(`Bursts ${i} e ${i + 1} não podem ser do mesmo tipo (alternar CPU/IO)`);
+ if (i > 0 && bursts[i - 1]?.type === b.type) errs.push(`Bursts ${i} e ${i + 1} não podem ser do mesmo tipo (alternar CPU/IO)`);
});
setErrors(errs);
return errs.length === 0;
diff --git a/src/components/SchedulerPanel/AlgorithmSelector.tsx b/src/components/SchedulerPanel/AlgorithmSelector.tsx
index 3dbb718..a4c8d5a 100644
--- a/src/components/SchedulerPanel/AlgorithmSelector.tsx
+++ b/src/components/SchedulerPanel/AlgorithmSelector.tsx
@@ -1,15 +1,16 @@
import type { SchedulingAlgorithm } from '@core/types';
import { useProcessStore } from '../../store/processStore';
-const ALGORITHM_OPTIONS: { value: SchedulingAlgorithm; label: string }[] = [
+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: 'RR', label: 'RR — Round Robin' },
{ value: 'PRIORITY_NP',label: 'Priority (Não-preemptivo)' },
{ value: 'PRIORITY_P', label: 'Priority (Preemptivo)' },
+ { value: 'PRIORITY_RR',label: 'Priority Round Robin (tempo real)' },
{ value: 'HRRN', label: 'HRRN — Highest Response Ratio Next' },
- { value: 'MULTILEVEL', label: 'Multilevel Queue' },
+ { value: 'MULTILEVEL', label: 'Multilevel Queue (em breve)', disabled: true },
];
const AlgorithmSelector = () => {
@@ -32,7 +33,7 @@ const AlgorithmSelector = () => {
aria-label="Selecionar algoritmo de escalonamento"
>
{ALGORITHM_OPTIONS.map(opt => (
-
+
{opt.label}
))}
diff --git a/src/components/SchedulerPanel/SchedulerConfig.tsx b/src/components/SchedulerPanel/SchedulerConfig.tsx
index 1e874a4..9515eb4 100644
--- a/src/components/SchedulerPanel/SchedulerConfig.tsx
+++ b/src/components/SchedulerPanel/SchedulerConfig.tsx
@@ -2,13 +2,13 @@ import { useProcessStore } from '../../store/processStore';
import type { SchedulingAlgorithm } from '@core/types';
// Algorithms that require a quantum parameter
-const QUANTUM_ALGORITHMS = new Set(['RR', 'MULTILEVEL']);
+const QUANTUM_ALGORITHMS = new Set(['RR', 'PRIORITY_RR', 'MULTILEVEL']);
// Algorithms where aging (anti-starvation) is applicable
-const AGING_ALGORITHMS = new Set(['PRIORITY_NP', 'PRIORITY_P']);
+const AGING_ALGORITHMS = new Set(['PRIORITY_NP', 'PRIORITY_P', 'PRIORITY_RR']);
// Algorithms that are preemptive by definition (used for read-only info only)
-const PREEMPTIVE_BY_DESIGN = new Set(['SJF_P', 'RR', 'PRIORITY_P', 'MULTILEVEL']);
+const PREEMPTIVE_BY_DESIGN = new Set(['SJF_P', 'RR', 'PRIORITY_P', 'PRIORITY_RR', 'MULTILEVEL']);
const inputCls = 'w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-sm px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500';
const labelCls = 'block text-xs text-slate-400 mb-1';
diff --git a/src/components/shared/processUtils.ts b/src/components/shared/processUtils.ts
index 24ec26d..b7e6ebd 100644
--- a/src/components/shared/processUtils.ts
+++ b/src/components/shared/processUtils.ts
@@ -28,5 +28,5 @@ const PALETTE = [
];
let _paletteIndex = 0;
export function nextColor(): string {
- return PALETTE[_paletteIndex++ % PALETTE.length];
+ return PALETTE[_paletteIndex++ % PALETTE.length]!;
}
diff --git a/src/core/__tests__/integration.test.ts b/src/core/__tests__/integration.test.ts
new file mode 100644
index 0000000..ab00f85
--- /dev/null
+++ b/src/core/__tests__/integration.test.ts
@@ -0,0 +1,716 @@
+/**
+ * Integration Tests — Process Scheduler Simulator
+ *
+ * These tests validate end-to-end simulation correctness: processes go through
+ * the full lifecycle (New → Ready → Running → Waiting → Terminated), metrics
+ * are consistent, and each algorithm produces the expected scheduling behavior.
+ *
+ * Test IDs follow the spec (CT-05 onward, CT-01..CT-04 are in engine.test.ts).
+ */
+
+import { describe, it, expect } from "vitest";
+import { SimulationEngine } from "../engine";
+import { computeProcessMetrics, computeGlobalMetrics } from "../metrics";
+import { presets } from "../presets";
+import type { Process, SchedulerConfig } from "../types";
+
+// ─── Helpers ──────────────────────────────────────────────────────────────────
+
+const cfg = (overrides: Partial = {}): SchedulerConfig => ({
+ algorithm: "FCFS",
+ quantum: 2,
+ contextSwitchTime: 0,
+ isPreemptive: false,
+ agingEnabled: false,
+ agingInterval: 5,
+ ...overrides,
+});
+
+const p = (
+ id: string,
+ arrivalTime: number,
+ bursts: { type: "cpu" | "io"; duration: number }[],
+ priority = 5,
+): Process => ({
+ id,
+ name: id.toUpperCase(),
+ arrivalTime,
+ priority,
+ color: "#fff",
+ bursts,
+});
+
+/** Runs the engine and returns per-process metrics keyed by processId */
+function runAndGetMetrics(processes: Process[], config: SchedulerConfig) {
+ const engine = new SimulationEngine(processes, config);
+ engine.runAll();
+ const rts = engine.runtimeStates;
+ const metricsMap = Object.fromEntries(
+ rts.map((rt) => [rt.processId, computeProcessMetrics(rt)]),
+ );
+ const globalMetrics = computeGlobalMetrics(
+ Object.values(metricsMap),
+ engine.currentTick + 1,
+ );
+ return { engine, metricsMap, globalMetrics };
+}
+
+// ─── CT-05: Convoy Effect (FCFS) ──────────────────────────────────────────────
+
+describe("CT-05: FCFS Convoy Effect", () => {
+ /**
+ * Large process P1 arrives first and blocks short processes P2, P3, P4.
+ * P1: arrival=0, CPU:12
+ * P2: arrival=1, CPU:2
+ * P3: arrival=2, CPU:1
+ * P4: arrival=3, CPU:3
+ *
+ * Expected Gantt: P1(0-11), P2(12-13), P3(14), P4(15-17)
+ */
+ const processes = [
+ p("p1", 0, [{ type: "cpu", duration: 12 }]),
+ p("p2", 1, [{ type: "cpu", duration: 2 }]),
+ p("p3", 2, [{ type: "cpu", duration: 1 }]),
+ p("p4", 3, [{ type: "cpu", duration: 3 }]),
+ ];
+
+ it("P1 (long process) runs uninterrupted for 12 ticks", () => {
+ const engine = new SimulationEngine(processes, cfg({ algorithm: "FCFS" }));
+ const ticks = engine.runAll();
+ for (let i = 0; i <= 11; i++) {
+ expect(ticks[i]?.cpuProcess).toBe("p1");
+ }
+ });
+
+ it("short processes execute in arrival order after P1 finishes", () => {
+ const engine = new SimulationEngine(processes, cfg({ algorithm: "FCFS" }));
+ const ticks = engine.runAll();
+ expect(ticks[12]?.cpuProcess).toBe("p2");
+ expect(ticks[13]?.cpuProcess).toBe("p2");
+ expect(ticks[14]?.cpuProcess).toBe("p3");
+ expect(ticks[15]?.cpuProcess).toBe("p4");
+ });
+
+ it("P2, P3, P4 suffer high waiting times (convoy effect)", () => {
+ const { metricsMap } = runAndGetMetrics(processes, cfg({ algorithm: "FCFS" }));
+ // P2: waited ticks 1-11 = 11 ticks
+ expect(metricsMap["p2"]!.waitingTime).toBe(11);
+ // P3: waited ticks 2-13 = 12 ticks
+ expect(metricsMap["p3"]!.waitingTime).toBe(12);
+ // P4: waited ticks 3-14 = 12 ticks
+ expect(metricsMap["p4"]!.waitingTime).toBe(12);
+ });
+
+ it("SJF-NP dramatically reduces average waiting time for the same input", () => {
+ const { globalMetrics: fcfsGlobal } = runAndGetMetrics(processes, cfg({ algorithm: "FCFS" }));
+ const { globalMetrics: sjfGlobal } = runAndGetMetrics(processes, cfg({ algorithm: "SJF_NP" }));
+
+ // SJF should yield much lower average waiting time than FCFS (convoy effect)
+ expect(sjfGlobal.avgWaitingTime).toBeLessThan(fcfsGlobal.avgWaitingTime);
+ });
+});
+
+// ─── CT-06: SJF Non-Preemptive ordering ───────────────────────────────────────
+
+describe("CT-06: SJF Non-Preemptive ordering", () => {
+ /**
+ * All processes arrive at tick 0. SJF picks shortest first.
+ * P1: CPU:5, P2: CPU:2, P3: CPU:1, P4: CPU:3
+ * Expected order: P3(CPU:1) → P2(CPU:2) → P4(CPU:3) → P1(CPU:5)
+ * Gantt: P3(0-0), P2(1-2), P4(3-5), P1(6-10)
+ */
+ const processes = [
+ p("p1", 0, [{ type: "cpu", duration: 5 }]),
+ p("p2", 0, [{ type: "cpu", duration: 2 }]),
+ p("p3", 0, [{ type: "cpu", duration: 1 }]),
+ p("p4", 0, [{ type: "cpu", duration: 3 }]),
+ ];
+
+ it("selects processes in ascending burst duration order", () => {
+ const engine = new SimulationEngine(processes, cfg({ algorithm: "SJF_NP" }));
+ const ticks = engine.runAll();
+
+ // P3 (CPU:1) first
+ expect(ticks[0]?.cpuProcess).toBe("p3");
+ // P2 (CPU:2) second
+ expect(ticks[1]?.cpuProcess).toBe("p2");
+ expect(ticks[2]?.cpuProcess).toBe("p2");
+ // P4 (CPU:3) third
+ expect(ticks[3]?.cpuProcess).toBe("p4");
+ expect(ticks[4]?.cpuProcess).toBe("p4");
+ expect(ticks[5]?.cpuProcess).toBe("p4");
+ // P1 (CPU:5) last
+ expect(ticks[6]?.cpuProcess).toBe("p1");
+ });
+
+ it("P3 has zero waiting time (runs first)", () => {
+ const { metricsMap } = runAndGetMetrics(processes, cfg({ algorithm: "SJF_NP" }));
+ expect(metricsMap["p3"]!.waitingTime).toBe(0);
+ expect(metricsMap["p3"]!.responseTime).toBe(0);
+ });
+
+ it("produces optimal average waiting time (SJF is optimal for non-preemptive)", () => {
+ const { globalMetrics } = runAndGetMetrics(processes, cfg({ algorithm: "SJF_NP" }));
+ // Avg waiting: (0 + 1 + 3 + 6) / 4 = 10/4 = 2.5
+ expect(globalMetrics.avgWaitingTime).toBeCloseTo(2.5, 5);
+ });
+});
+
+// ─── CT-07: Priority NP Starvation ───────────────────────────────────────────
+
+describe("CT-07: Priority NP — P5 starves until all high-priority processes finish", () => {
+ /**
+ * P1-P4 have priority 1 (highest). P5 has priority 10 (lowest).
+ * P5 arrives first but never gets CPU until all P1-P4 finish.
+ */
+ const processes = [
+ p("p1", 0, [{ type: "cpu", duration: 3 }], 1),
+ p("p2", 1, [{ type: "cpu", duration: 3 }], 1),
+ p("p3", 2, [{ type: "cpu", duration: 3 }], 1),
+ p("p4", 3, [{ type: "cpu", duration: 3 }], 1),
+ p("p5", 0, [{ type: "cpu", duration: 5 }], 10),
+ ];
+
+ it("P5 does not run while any high-priority process is in the ready queue", () => {
+ const engine = new SimulationEngine(processes, cfg({ algorithm: "PRIORITY_NP" }));
+ const ticks = engine.runAll();
+ // Ticks 0-11: only P1-P4 should be on CPU
+ for (let i = 0; i <= 11; i++) {
+ expect(ticks[i]?.cpuProcess).not.toBe("p5");
+ }
+ });
+
+ it("P5 starts running only after all high-priority processes finish", () => {
+ const engine = new SimulationEngine(processes, cfg({ algorithm: "PRIORITY_NP" }));
+ const ticks = engine.runAll();
+ // P4 finishes at tick 12 (runs 9-11); P5 starts at 12
+ expect(ticks[12]?.cpuProcess).toBe("p5");
+ });
+
+ it("P5 has the highest waiting time", () => {
+ const { metricsMap } = runAndGetMetrics(processes, cfg({ algorithm: "PRIORITY_NP" }));
+ const p5Waiting = metricsMap["p5"]!.waitingTime;
+ // P5 waited from tick 0 until tick 12 → waiting = 12
+ expect(p5Waiting).toBe(12);
+ // P5 has more waiting time than all others combined
+ const others = ["p1", "p2", "p3", "p4"].map((id) => metricsMap[id]!.waitingTime);
+ expect(p5Waiting).toBeGreaterThan(Math.max(...others));
+ });
+});
+
+// ─── CT-08: Aging prevents starvation ────────────────────────────────────────
+
+describe("CT-08: Priority Preemptive with aging — P5 eventually runs earlier", () => {
+ /**
+ * Same scenario as CT-07 but with aging enabled (agingInterval=3).
+ * P5 should have a lower waiting time than in CT-07 because aging
+ * eventually promotes it to run before all high-priority processes finish.
+ */
+ const processes = [
+ p("p1", 0, [{ type: "cpu", duration: 3 }], 1),
+ p("p2", 1, [{ type: "cpu", duration: 3 }], 1),
+ p("p3", 2, [{ type: "cpu", duration: 3 }], 1),
+ p("p4", 3, [{ type: "cpu", duration: 3 }], 1),
+ p("p5", 0, [{ type: "cpu", duration: 5 }], 10),
+ ];
+
+ const agingConfig = cfg({
+ algorithm: "PRIORITY_P",
+ isPreemptive: true,
+ agingEnabled: true,
+ agingInterval: 3,
+ });
+
+ const noAgingConfig = cfg({ algorithm: "PRIORITY_NP" });
+
+ it("all processes eventually terminate with aging enabled", () => {
+ const engine = new SimulationEngine(processes, agingConfig);
+ engine.runAll();
+ expect(engine.isFinished).toBe(true);
+ const rts = engine.runtimeStates;
+ for (const rt of rts) {
+ expect(rt.state).toBe("Terminated");
+ }
+ });
+
+ it("P5 waiting time with aging is less than or equal to without aging", () => {
+ const { metricsMap: withAging } = runAndGetMetrics(processes, agingConfig);
+ const { metricsMap: withoutAging } = runAndGetMetrics(processes, noAgingConfig);
+ expect(withAging["p5"]!.waitingTime).toBeLessThanOrEqual(withoutAging["p5"]!.waitingTime);
+ });
+});
+
+// ─── CT-09: Multiple I/O Cycles ──────────────────────────────────────────────
+
+describe("CT-09: Process with multiple CPU/I/O cycles", () => {
+ /**
+ * P1 alternates: CPU:2 → IO:3 → CPU:1 → IO:2 → CPU:3
+ * This validates state machine across multiple burst transitions.
+ */
+ const processes = [
+ p("p1", 0, [
+ { type: "cpu", duration: 2 },
+ { type: "io", duration: 3 },
+ { type: "cpu", duration: 1 },
+ { type: "io", duration: 2 },
+ { type: "cpu", duration: 3 },
+ ]),
+ ];
+
+ it("traverses all states in correct order", () => {
+ const engine = new SimulationEngine(processes, cfg());
+ const ticks = engine.runAll();
+ const states = ticks.map((t) => t.states["p1"]);
+
+ expect(states[0]).toBe("Running"); // CPU:2 ticks 0-1
+ expect(states[1]).toBe("Running");
+ expect(states[2]).toBe("Waiting"); // IO:3 ticks 2-4
+ expect(states[3]).toBe("Waiting");
+ expect(states[4]).toBe("Waiting");
+ expect(states[5]).toBe("Running"); // CPU:1 tick 5
+ expect(states[6]).toBe("Waiting"); // IO:2 ticks 6-7
+ expect(states[7]).toBe("Waiting");
+ expect(states[8]).toBe("Running"); // CPU:3 ticks 8-10
+ expect(states[9]).toBe("Running");
+ expect(states[10]).toBe("Running");
+ });
+
+ it("finishes at the correct tick", () => {
+ const engine = new SimulationEngine(processes, cfg());
+ engine.runAll();
+ const rt = engine.runtimeStates[0]!;
+ // Total duration: 2+3+1+2+3 = 11 ticks → finishTick = 11
+ expect(rt.finishTick).toBe(11);
+ });
+
+ it("accumulates correct cpu and io times", () => {
+ const engine = new SimulationEngine(processes, cfg());
+ engine.runAll();
+ const rt = engine.runtimeStates[0]!;
+ expect(rt.cpuTime).toBe(6); // 2+1+3
+ expect(rt.ioTime).toBe(5); // 3+2
+ });
+
+ it("metrics: turnaround = cpuTime + ioTime + waitingTime", () => {
+ const { metricsMap } = runAndGetMetrics(processes, cfg());
+ const m = metricsMap["p1"]!;
+ // For a single process, waiting time should be 0 (never in Ready queue)
+ expect(m.waitingTime).toBe(0);
+ expect(m.turnaroundTime).toBe(m.cpuTime + m.ioTime + m.waitingTime);
+ });
+});
+
+// ─── CT-10: Simultaneous arrivals — tie-breaking ──────────────────────────────
+
+describe("CT-10: Simultaneous arrivals — tie-breaking by ID", () => {
+ /**
+ * All 4 processes arrive at tick 0 with equal priority and burst duration.
+ * FCFS tie-break: by processId lexicographic order → p1, p2, p3, p4.
+ */
+ const processes = [
+ p("p4", 0, [{ type: "cpu", duration: 2 }]),
+ p("p2", 0, [{ type: "cpu", duration: 2 }]),
+ p("p1", 0, [{ type: "cpu", duration: 2 }]),
+ p("p3", 0, [{ type: "cpu", duration: 2 }]),
+ ];
+
+ it("executes processes in lexicographic ID order on tie", () => {
+ const engine = new SimulationEngine(processes, cfg({ algorithm: "FCFS" }));
+ const ticks = engine.runAll();
+
+ expect(ticks[0]?.cpuProcess).toBe("p1");
+ expect(ticks[1]?.cpuProcess).toBe("p1");
+ expect(ticks[2]?.cpuProcess).toBe("p2");
+ expect(ticks[3]?.cpuProcess).toBe("p2");
+ expect(ticks[4]?.cpuProcess).toBe("p3");
+ expect(ticks[5]?.cpuProcess).toBe("p3");
+ expect(ticks[6]?.cpuProcess).toBe("p4");
+ expect(ticks[7]?.cpuProcess).toBe("p4");
+ });
+
+ it("first process (p1) has response time 0 and zero waiting time", () => {
+ const { metricsMap } = runAndGetMetrics(processes, cfg({ algorithm: "FCFS" }));
+ expect(metricsMap["p1"]!.responseTime).toBe(0);
+ expect(metricsMap["p1"]!.waitingTime).toBe(0);
+ });
+});
+
+// ─── CT-11: CPU idle gap between arrivals ─────────────────────────────────────
+
+describe("CT-11: CPU idle gap between process batches", () => {
+ /**
+ * P1 finishes early; CPU becomes idle until P2 arrives later.
+ * P1: arrival=0, CPU:2
+ * P2: arrival=6, CPU:2
+ * Gap: ticks 2-5 (CPU idle for 4 ticks)
+ */
+ const processes = [
+ p("p1", 0, [{ type: "cpu", duration: 2 }]),
+ p("p2", 6, [{ type: "cpu", duration: 2 }]),
+ ];
+
+ it("CPU is idle during the gap (no cpuProcess)", () => {
+ const engine = new SimulationEngine(processes, cfg());
+ const ticks = engine.runAll();
+ // ticks 2,3,4,5: CPU should be idle
+ for (let i = 2; i <= 5; i++) {
+ expect(ticks[i]?.cpuProcess).toBeNull();
+ }
+ });
+
+ it("P2 starts immediately on arrival", () => {
+ const engine = new SimulationEngine(processes, cfg());
+ const ticks = engine.runAll();
+ expect(ticks[6]?.cpuProcess).toBe("p2");
+ });
+
+ it("P2 has zero waiting time (no competition)", () => {
+ const { metricsMap } = runAndGetMetrics(processes, cfg());
+ expect(metricsMap["p2"]!.waitingTime).toBe(0);
+ expect(metricsMap["p2"]!.responseTime).toBe(0);
+ });
+
+ it("global CPU utilization reflects idle period", () => {
+ const { globalMetrics } = runAndGetMetrics(processes, cfg());
+ // Total CPU used: 4 ticks out of 8 ticks = 50%
+ expect(globalMetrics.cpuUtilization).toBeCloseTo(50, 5);
+ });
+});
+
+// ─── CT-12: Round Robin with quantum=1 ───────────────────────────────────────
+
+describe("CT-12: Round Robin quantum=1 — strict rotation", () => {
+ /**
+ * P1(CPU:3), P2(CPU:2), P3(CPU:1) all arrive at tick 0, quantum=1.
+ * Each tick a different process runs in round-robin order.
+ * Expected: P1(0), P2(1), P3(2), P1(3), P2(4), P1(5)
+ */
+ const processes = [
+ p("p1", 0, [{ type: "cpu", duration: 3 }]),
+ p("p2", 0, [{ type: "cpu", duration: 2 }]),
+ p("p3", 0, [{ type: "cpu", duration: 1 }]),
+ ];
+
+ it("rotates one tick per process", () => {
+ const engine = new SimulationEngine(processes, cfg({ algorithm: "RR", quantum: 1 }));
+ const ticks = engine.runAll();
+
+ expect(ticks[0]?.cpuProcess).toBe("p1");
+ expect(ticks[1]?.cpuProcess).toBe("p2");
+ expect(ticks[2]?.cpuProcess).toBe("p3");
+ expect(ticks[3]?.cpuProcess).toBe("p1");
+ expect(ticks[4]?.cpuProcess).toBe("p2");
+ expect(ticks[5]?.cpuProcess).toBe("p1");
+ });
+
+ it("all processes finish at expected ticks", () => {
+ const engine = new SimulationEngine(processes, cfg({ algorithm: "RR", quantum: 1 }));
+ engine.runAll();
+ const rts = engine.runtimeStates;
+ const rt1 = rts.find((r) => r.processId === "p1")!;
+ const rt2 = rts.find((r) => r.processId === "p2")!;
+ const rt3 = rts.find((r) => r.processId === "p3")!;
+
+ expect(rt3.finishTick).toBe(3); // P3 finishes after 1 run at tick 2
+ expect(rt2.finishTick).toBe(5); // P2 finishes after 2 runs
+ expect(rt1.finishTick).toBe(6); // P1 finishes after 3 runs
+ });
+});
+
+// ─── CT-13: Context switch overhead ──────────────────────────────────────────
+
+describe("CT-13: Context switch overhead with multiple algorithms", () => {
+ /**
+ * Each process switch incurs 1 idle tick (contextSwitchTime=1).
+ * Tests that context switch overhead is correctly accounted for
+ * in multiple algorithm contexts.
+ */
+ const processes = [
+ p("p1", 0, [{ type: "cpu", duration: 2 }]),
+ p("p2", 0, [{ type: "cpu", duration: 2 }]),
+ p("p3", 0, [{ type: "cpu", duration: 2 }]),
+ ];
+
+ it("FCFS: inserts 2 context switch ticks for 3 processes", () => {
+ const engine = new SimulationEngine(
+ processes,
+ cfg({ algorithm: "FCFS", contextSwitchTime: 1 }),
+ );
+ const ticks = engine.runAll();
+
+ // P1: 0-1, ctx: 2, P2: 3-4, ctx: 5, P3: 6-7
+ expect(ticks[2]?.contextSwitching).toBe(true);
+ expect(ticks[2]?.cpuProcess).toBeNull();
+ expect(ticks[5]?.contextSwitching).toBe(true);
+ expect(ticks[5]?.cpuProcess).toBeNull();
+ expect(ticks.length).toBe(8);
+ });
+
+ it("total simulation time is longer with context switch", () => {
+ const withCtx = new SimulationEngine(processes, cfg({ algorithm: "FCFS", contextSwitchTime: 1 }));
+ const withoutCtx = new SimulationEngine(processes, cfg({ algorithm: "FCFS", contextSwitchTime: 0 }));
+ withCtx.runAll();
+ withoutCtx.runAll();
+
+ // With 2 context switches of 1 tick each: +2 ticks total
+ expect(withCtx.currentTick).toBe(withoutCtx.currentTick + 2);
+ });
+
+ it("context switch ticks are not counted as cpu time for any process", () => {
+ const engine = new SimulationEngine(
+ processes,
+ cfg({ algorithm: "FCFS", contextSwitchTime: 1 }),
+ );
+ engine.runAll();
+ const totalCpuTime = engine.runtimeStates.reduce((s, rt) => s + rt.cpuTime, 0);
+ // 3 processes × 2 cpu ticks each = 6
+ expect(totalCpuTime).toBe(6);
+ });
+});
+
+// ─── CT-14: HRRN fairness — waited process wins over fresh arrivals ───────────
+
+describe("CT-14: HRRN — long wait compensates for longer burst", () => {
+ /**
+ * P1(CPU:1) runs first (only one ready at tick 0).
+ * When P1 finishes at tick 1, P2(CPU:6, waited=1) and P3(CPU:2, waited=0) are both ready.
+ *
+ * HRRN ratios at tick 1:
+ * P2: (1 + 6) / 6 = 1.167
+ * P3: (0 + 2) / 2 = 1.0
+ *
+ * P2 wins because it has been waiting.
+ */
+ const processes = [
+ p("p1", 0, [{ type: "cpu", duration: 1 }]),
+ p("p2", 0, [{ type: "cpu", duration: 6 }]),
+ p("p3", 1, [{ type: "cpu", duration: 2 }]),
+ ];
+
+ it("P2 wins over fresh P3 due to higher response ratio", () => {
+ const engine = new SimulationEngine(processes, cfg({ algorithm: "HRRN" }));
+ const ticks = engine.runAll();
+ // tick 1: P2 starts (ratio 1.167 > P3 ratio 1.0)
+ expect(ticks[1]?.cpuProcess).toBe("p2");
+ });
+
+ it("P3 runs only after P2 finishes", () => {
+ const engine = new SimulationEngine(processes, cfg({ algorithm: "HRRN" }));
+ const ticks = engine.runAll();
+ // P2 runs ticks 1-6; P3 starts at tick 7
+ expect(ticks[7]?.cpuProcess).toBe("p3");
+ });
+});
+
+// ─── CT-15: SRTF — preemption on arrival with complex I/O ────────────────────
+
+describe("CT-15: SRTF preemption with I/O processes", () => {
+ /**
+ * P1(CPU:5,IO:2,CPU:2) starts at 0.
+ * P2(CPU:2) arrives at tick 2 — preempts P1 (remaining=3 > 2).
+ * While P2 runs, P1 goes to ready queue.
+ * P2 finishes at tick 4; P1 resumes its CPU burst.
+ */
+ const processes = [
+ p("p1", 0, [{ type: "cpu", duration: 5 }, { type: "io", duration: 2 }, { type: "cpu", duration: 2 }]),
+ p("p2", 2, [{ type: "cpu", duration: 2 }]),
+ ];
+
+ it("P2 preempts P1 at tick 2", () => {
+ const engine = new SimulationEngine(
+ processes,
+ cfg({ algorithm: "SJF_P", isPreemptive: true }),
+ );
+ const ticks = engine.runAll();
+ expect(ticks[0]?.cpuProcess).toBe("p1");
+ expect(ticks[1]?.cpuProcess).toBe("p1");
+ expect(ticks[2]?.cpuProcess).toBe("p2"); // preemption here
+ expect(ticks[3]?.cpuProcess).toBe("p2");
+ });
+
+ it("P1 eventually completes all burst phases and terminates", () => {
+ const engine = new SimulationEngine(
+ processes,
+ cfg({ algorithm: "SJF_P", isPreemptive: true }),
+ );
+ engine.runAll();
+ expect(engine.isFinished).toBe(true);
+ const p1rt = engine.runtimeStates.find((r) => r.processId === "p1")!;
+ expect(p1rt.state).toBe("Terminated");
+ expect(p1rt.ioTime).toBe(2);
+ });
+});
+
+// ─── CT-16: Metrics invariant across all algorithms ──────────────────────────
+
+describe("CT-16: Metrics invariant — turnaround = cpuTime + ioTime + waitingTime", () => {
+ /**
+ * For any completed process:
+ * turnaround = cpuTime + ioTime + waitingTime
+ *
+ * This invariant must hold for every algorithm.
+ */
+ const processes = [
+ p("p1", 0, [{ type: "cpu", duration: 4 }, { type: "io", duration: 2 }, { type: "cpu", duration: 2 }]),
+ p("p2", 1, [{ type: "cpu", duration: 3 }]),
+ p("p3", 2, [{ type: "cpu", duration: 1 }, { type: "io", duration: 3 }, { type: "cpu", duration: 2 }]),
+ ];
+
+ const algorithms: SchedulerConfig["algorithm"][] = [
+ "FCFS", "SJF_NP", "SJF_P", "RR", "PRIORITY_NP", "PRIORITY_P", "HRRN",
+ ];
+
+ for (const algorithm of algorithms) {
+ it(`holds for ${algorithm}`, () => {
+ const isPreemptive = algorithm === "SJF_P" || algorithm === "PRIORITY_P";
+ const { metricsMap } = runAndGetMetrics(
+ processes,
+ cfg({ algorithm, isPreemptive, quantum: 2 }),
+ );
+ for (const [id, m] of Object.entries(metricsMap)) {
+ expect(m.turnaroundTime, `${algorithm}: ${id} turnaround invariant`).toBeCloseTo(
+ m.cpuTime + m.ioTime + m.waitingTime,
+ 5,
+ );
+ }
+ });
+ }
+});
+
+// ─── CT-17: All processes always terminate ────────────────────────────────────
+
+describe("CT-17: All processes eventually terminate (no infinite loops)", () => {
+ /**
+ * For every supported algorithm, a simulation must terminate.
+ * This validates the engine has no deadlocks or infinite scheduling loops.
+ */
+ const processes = [
+ p("p1", 0, [{ type: "cpu", duration: 3 }, { type: "io", duration: 2 }, { type: "cpu", duration: 2 }]),
+ p("p2", 0, [{ type: "cpu", duration: 5 }]),
+ p("p3", 2, [{ type: "cpu", duration: 1 }]),
+ p("p4", 3, [{ type: "cpu", duration: 4 }, { type: "io", duration: 1 }, { type: "cpu", duration: 1 }]),
+ ];
+
+ const algorithmsWithConfig: [string, SchedulerConfig][] = [
+ ["FCFS", cfg({ algorithm: "FCFS" })],
+ ["SJF_NP", cfg({ algorithm: "SJF_NP" })],
+ ["SJF_P", cfg({ algorithm: "SJF_P", isPreemptive: true })],
+ ["RR q=2", cfg({ algorithm: "RR", quantum: 2 })],
+ ["RR q=1", cfg({ algorithm: "RR", quantum: 1 })],
+ ["PRIORITY_NP", cfg({ algorithm: "PRIORITY_NP" })],
+ ["PRIORITY_P", cfg({ algorithm: "PRIORITY_P", isPreemptive: true })],
+ ["PRIORITY_P+aging", cfg({ algorithm: "PRIORITY_P", isPreemptive: true, agingEnabled: true, agingInterval: 3 })],
+ ["HRRN", cfg({ algorithm: "HRRN" })],
+ ];
+
+ for (const [name, config] of algorithmsWithConfig) {
+ it(`${name}: all processes reach Terminated state`, () => {
+ const engine = new SimulationEngine(processes, config);
+ engine.runAll();
+ expect(engine.isFinished).toBe(true);
+ for (const rt of engine.runtimeStates) {
+ expect(rt.state, `${name}: process ${rt.processId} should be Terminated`).toBe("Terminated");
+ }
+ });
+ }
+});
+
+// ─── CT-18: Preset validation ─────────────────────────────────────────────────
+
+describe("CT-18: All presets complete successfully", () => {
+ /**
+ * Each preset must run to completion with all processes terminated,
+ * and produce valid (non-negative) metrics.
+ */
+ for (const preset of presets) {
+ it(`preset "${preset.name}" terminates and yields valid metrics`, () => {
+ const engine = new SimulationEngine(preset.processes, preset.config);
+ engine.runAll();
+
+ expect(engine.isFinished).toBe(true);
+
+ for (const rt of engine.runtimeStates) {
+ expect(rt.state).toBe("Terminated");
+ expect(rt.finishTick).not.toBeNull();
+ expect(rt.startTick).not.toBeNull();
+
+ const m = computeProcessMetrics(rt);
+ expect(m.turnaroundTime).toBeGreaterThan(0);
+ expect(m.waitingTime).toBeGreaterThanOrEqual(0);
+ expect(m.responseTime).toBeGreaterThanOrEqual(0);
+ expect(m.cpuTime).toBeGreaterThan(0);
+ expect(m.ioTime).toBeGreaterThanOrEqual(0);
+ // Invariant: turnaround = cpuTime + ioTime + waitingTime
+ expect(m.turnaroundTime).toBeCloseTo(m.cpuTime + m.ioTime + m.waitingTime, 5);
+ }
+ });
+ }
+});
+
+// ─── CT-19: RR with I/O — quantum resets correctly after I/O return ───────────
+
+describe("CT-19: RR with I/O — process returning from I/O gets fresh quantum", () => {
+ /**
+ * P1(CPU:4, IO:2, CPU:2) with quantum=2.
+ * P2(CPU:6) with quantum=2.
+ *
+ * P1 uses 2 CPU ticks (quantum exhausted) → goes to ready.
+ * P2 uses 2 ticks → goes to ready.
+ * P1 uses last 2 CPU ticks → goes to I/O.
+ * While P1 does I/O, P2 runs.
+ * P1 returns from I/O and gets a fresh quantum for its final 2 CPU ticks.
+ */
+ const processes = [
+ p("p1", 0, [{ type: "cpu", duration: 4 }, { type: "io", duration: 2 }, { type: "cpu", duration: 2 }]),
+ p("p2", 0, [{ type: "cpu", duration: 6 }]),
+ ];
+
+ it("P1 and P2 both terminate", () => {
+ const engine = new SimulationEngine(processes, cfg({ algorithm: "RR", quantum: 2 }));
+ engine.runAll();
+ expect(engine.isFinished).toBe(true);
+ for (const rt of engine.runtimeStates) {
+ expect(rt.state).toBe("Terminated");
+ }
+ });
+
+ it("P1 accumulates correct cpu and io times", () => {
+ const engine = new SimulationEngine(processes, cfg({ algorithm: "RR", quantum: 2 }));
+ engine.runAll();
+ const p1rt = engine.runtimeStates.find((r) => r.processId === "p1")!;
+ expect(p1rt.cpuTime).toBe(6); // 4 + 2
+ expect(p1rt.ioTime).toBe(2);
+ });
+
+ it("metrics invariant holds for both processes", () => {
+ const { metricsMap } = runAndGetMetrics(processes, cfg({ algorithm: "RR", quantum: 2 }));
+ for (const m of Object.values(metricsMap)) {
+ expect(m.turnaroundTime).toBeCloseTo(m.cpuTime + m.ioTime + m.waitingTime, 5);
+ }
+ });
+});
+
+// ─── CT-20: Single process — no competition edge case ────────────────────────
+
+describe("CT-20: Single process — baseline edge case", () => {
+ /**
+ * With only one process, there is no scheduling decision to make.
+ * The process should run uninterrupted and have zero waiting time,
+ * regardless of algorithm.
+ */
+ const process = p("p1", 0, [
+ { type: "cpu", duration: 3 },
+ { type: "io", duration: 2 },
+ { type: "cpu", duration: 2 },
+ ]);
+
+ const algorithms: SchedulerConfig["algorithm"][] = ["FCFS", "SJF_NP", "RR", "PRIORITY_NP", "HRRN"];
+
+ for (const algorithm of algorithms) {
+ it(`${algorithm}: single process has zero waiting time`, () => {
+ const { metricsMap } = runAndGetMetrics([process], cfg({ algorithm, quantum: 2 }));
+ expect(metricsMap["p1"]!.waitingTime).toBe(0);
+ expect(metricsMap["p1"]!.responseTime).toBe(0);
+ });
+ }
+});
diff --git a/src/core/__tests__/priority_rr.test.ts b/src/core/__tests__/priority_rr.test.ts
new file mode 100644
index 0000000..3f4f2fb
--- /dev/null
+++ b/src/core/__tests__/priority_rr.test.ts
@@ -0,0 +1,248 @@
+import { describe, it, expect } from "vitest";
+import { priorityRr } from "../algorithms/priority_rr";
+import { SimulationEngine } from "../engine";
+import type { Process, ProcessRuntime, SchedulerConfig } from "../types";
+
+// ----------------------------------------------------------------
+// Helpers
+// ----------------------------------------------------------------
+
+function makeRt(
+ id: string,
+ priority: number,
+ quantumRemaining = 2,
+): ProcessRuntime {
+ return {
+ processId: id,
+ state: "Ready",
+ remainingBurst: 4,
+ burstIndex: 0,
+ arrivalTick: 0,
+ startTick: null,
+ finishTick: null,
+ waitingTime: 0,
+ cpuTime: 0,
+ ioTime: 0,
+ responseTime: null,
+ quantumRemaining,
+ currentPriority: priority,
+ };
+}
+
+const cfg = (quantum = 2): SchedulerConfig => ({
+ algorithm: "PRIORITY_RR",
+ quantum,
+ contextSwitchTime: 0,
+ isPreemptive: true,
+ agingEnabled: false,
+ agingInterval: 5,
+});
+
+const p = (
+ id: string,
+ arrival: number,
+ cpuDuration: number,
+ priority = 5,
+): Process => ({
+ id,
+ name: id,
+ arrivalTime: arrival,
+ priority,
+ color: "#fff",
+ bursts: [{ type: "cpu", duration: cpuDuration }],
+});
+
+// ----------------------------------------------------------------
+// Unit: select()
+// ----------------------------------------------------------------
+
+describe("PRIORITY_RR select()", () => {
+ it("returns null for empty queue", () => {
+ expect(priorityRr.select([], cfg())).toBeNull();
+ });
+
+ it("picks the process with the lowest priority number (highest priority)", () => {
+ const high = makeRt("high", 1);
+ const low = makeRt("low", 5);
+ expect(priorityRr.select([low, high], cfg())?.processId).toBe("high");
+ expect(priorityRr.select([high, low], cfg())?.processId).toBe("high");
+ });
+
+ it("picks the FIRST in insertion order when priorities are equal (FIFO = RR)", () => {
+ const a = makeRt("a", 3);
+ const b = makeRt("b", 3);
+ const c = makeRt("c", 3);
+ expect(priorityRr.select([a, b, c], cfg())?.processId).toBe("a");
+ expect(priorityRr.select([b, c, a], cfg())?.processId).toBe("b");
+ });
+
+ it("ignores lower-priority processes when a higher-priority one is present", () => {
+ const critical = makeRt("critical", 1);
+ const normal1 = makeRt("n1", 5);
+ const normal2 = makeRt("n2", 5);
+ expect(
+ priorityRr.select([normal1, normal2, critical], cfg())?.processId,
+ ).toBe("critical");
+ });
+});
+
+// ----------------------------------------------------------------
+// Unit: shouldPreempt()
+// ----------------------------------------------------------------
+
+describe("PRIORITY_RR shouldPreempt()", () => {
+ it("preempts when candidate has strictly higher priority", () => {
+ const running = makeRt("current", 5);
+ const higher = makeRt("higher", 1);
+ expect(priorityRr.shouldPreempt(running, higher, cfg())).toBe(true);
+ });
+
+ it("does NOT preempt for same priority (quantum handles rotation)", () => {
+ const a = makeRt("a", 3);
+ const b = makeRt("b", 3);
+ expect(priorityRr.shouldPreempt(a, b, cfg())).toBe(false);
+ });
+
+ it("does NOT preempt when candidate has lower priority", () => {
+ const running = makeRt("current", 1);
+ const lower = makeRt("lower", 5);
+ expect(priorityRr.shouldPreempt(running, lower, cfg())).toBe(false);
+ });
+});
+
+// ----------------------------------------------------------------
+// Unit: isQuantumExpired()
+// ----------------------------------------------------------------
+
+describe("PRIORITY_RR isQuantumExpired()", () => {
+ it("returns true when quantumRemaining <= 0", () => {
+ expect(priorityRr.isQuantumExpired(makeRt("p", 5, 0), cfg())).toBe(true);
+ expect(priorityRr.isQuantumExpired(makeRt("p", 5, -1), cfg())).toBe(true);
+ });
+
+ it("returns false when quantum is not exhausted", () => {
+ expect(priorityRr.isQuantumExpired(makeRt("p", 5, 1), cfg())).toBe(false);
+ expect(priorityRr.isQuantumExpired(makeRt("p", 5, 2), cfg())).toBe(false);
+ });
+});
+
+// ----------------------------------------------------------------
+// Integration: Round-robin within the same priority band
+// ----------------------------------------------------------------
+
+describe("PRIORITY_RR integration — RR within same priority", () => {
+ it("two equal-priority processes rotate via quantum (CT-PRIORITY_RR-01)", () => {
+ // P1 and P2 both priority=2, CPU:4 each, quantum=2
+ // Expected Gantt: P1(0-2), P2(2-4), P1(4-6), P2(6-8)
+ const processes = [p("p1", 0, 4, 2), p("p2", 0, 4, 2)];
+ const engine = new SimulationEngine(processes, cfg(2));
+ const ticks = engine.runAll();
+ const cpu = ticks.map((t) => t.cpuProcess);
+
+ expect(cpu).toHaveLength(8);
+ 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("three equal-priority processes rotate in arrival order", () => {
+ // P1(CPU:2), P2(CPU:2), P3(CPU:2), quantum=2 — each finishes in first quantum
+ const processes = [p("p1", 0, 2, 3), p("p2", 0, 2, 3), p("p3", 0, 2, 3)];
+ const engine = new SimulationEngine(processes, cfg(2));
+ const ticks = engine.runAll();
+ const cpu = ticks.map((t) => t.cpuProcess);
+
+ expect(cpu).toHaveLength(6);
+ expect(cpu[0]).toBe("p1");
+ expect(cpu[1]).toBe("p1");
+ expect(cpu[2]).toBe("p2");
+ expect(cpu[3]).toBe("p2");
+ expect(cpu[4]).toBe("p3");
+ expect(cpu[5]).toBe("p3");
+ });
+});
+
+// ----------------------------------------------------------------
+// Integration: Priority preemption
+// ----------------------------------------------------------------
+
+describe("PRIORITY_RR integration — priority preemption", () => {
+ it("high-priority process arriving mid-run immediately preempts (CT-PRIORITY_RR-02)", () => {
+ // P1: priority=3, arrival=0, CPU:6 (large quantum so it won't expire)
+ // P2: priority=1, arrival=2, CPU:2
+ // Expected: P1(0-1), P2 preempts at tick 2, P2(2-3), P1 resumes(4-7)
+ const processes = [p("p1", 0, 6, 3), p("p2", 2, 2, 1)];
+ const engine = new SimulationEngine(processes, cfg(10));
+ const ticks = engine.runAll();
+ const cpu = ticks.map((t) => t.cpuProcess);
+
+ // P1 runs ticks 0, 1; P2 preempts at tick 2; P1 resumes after P2 finishes
+ expect(cpu[0]).toBe("p1");
+ expect(cpu[1]).toBe("p1");
+ expect(cpu[2]).toBe("p2"); // preempted here
+ expect(cpu[3]).toBe("p2");
+ // After P2 terminates, P1 resumes
+ expect(cpu[4]).toBe("p1");
+ expect(cpu[5]).toBe("p1");
+ expect(cpu[6]).toBe("p1");
+ expect(cpu[7]).toBe("p1");
+ expect(cpu).toHaveLength(8);
+ });
+
+ it("lower-priority process cannot preempt a higher-priority running process", () => {
+ const processes = [p("p1", 0, 4, 1), p("p2", 1, 4, 5)];
+ const engine = new SimulationEngine(processes, cfg(10));
+ const ticks = engine.runAll();
+ const cpu = ticks.map((t) => t.cpuProcess);
+
+ // P1 (priority=1) runs entirely first; P2 waits
+ expect(cpu.slice(0, 4).every((id) => id === "p1")).toBe(true);
+ expect(cpu.slice(4).every((id) => id === "p2")).toBe(true);
+ });
+});
+
+// ----------------------------------------------------------------
+// Integration: Combined — preemption + RR (preset scenario)
+// ----------------------------------------------------------------
+
+describe("PRIORITY_RR integration — combined preemption + RR", () => {
+ it("preset scenario: P1 & P2 (prio=2) RR, P3 (prio=1) preempts at arrival", () => {
+ // P1: prio=2, arrival=0, CPU:4
+ // P2: prio=2, arrival=0, CPU:4
+ // P3: prio=1, arrival=4, CPU:2 — arrives just as P2's first quantum ends
+ // quantum=2
+ // Expected Gantt: P1(0-2), P2(2-4), P3(4-6), P1(6-8), P2(8-10)
+ const processes = [p("p1", 0, 4, 2), p("p2", 0, 4, 2), p("p3", 4, 2, 1)];
+ const engine = new SimulationEngine(processes, cfg(2));
+ const ticks = engine.runAll();
+ const cpu = ticks.map((t) => t.cpuProcess);
+
+ expect(cpu[0]).toBe("p1");
+ expect(cpu[1]).toBe("p1");
+ expect(cpu[2]).toBe("p2");
+ expect(cpu[3]).toBe("p2");
+ expect(cpu[4]).toBe("p3"); // P3 preempts (or takes CPU when it's free)
+ expect(cpu[5]).toBe("p3");
+ expect(cpu[6]).toBe("p1");
+ expect(cpu[7]).toBe("p1");
+ expect(cpu[8]).toBe("p2");
+ expect(cpu[9]).toBe("p2");
+ expect(cpu).toHaveLength(10);
+ });
+
+ it("all processes terminate correctly", () => {
+ const processes = [p("p1", 0, 4, 2), p("p2", 0, 4, 2), p("p3", 4, 2, 1)];
+ const engine = new SimulationEngine(processes, cfg(2));
+ engine.runAll();
+ const rts = engine.runtimeStates;
+ expect(rts.every((rt) => rt.state === "Terminated")).toBe(true);
+ expect(rts.find((r) => r.processId === "p1")!.cpuTime).toBe(4);
+ expect(rts.find((r) => r.processId === "p2")!.cpuTime).toBe(4);
+ expect(rts.find((r) => r.processId === "p3")!.cpuTime).toBe(2);
+ });
+});
diff --git a/src/core/algorithms/index.ts b/src/core/algorithms/index.ts
index a596f29..ab7b801 100644
--- a/src/core/algorithms/index.ts
+++ b/src/core/algorithms/index.ts
@@ -3,6 +3,7 @@ import { fcfs } from "./fcfs";
import { sjfNp, srtf } from "./sjf";
import { rr } from "./rr";
import { priorityNp, priorityP } from "./priority";
+import { priorityRr } from "./priority_rr";
import { hrrn } from "./hrrn";
import { multilevel } from "./multilevel";
@@ -54,6 +55,7 @@ export const algorithms: Record = {
RR: rr,
PRIORITY_NP: priorityNp,
PRIORITY_P: priorityP,
+ PRIORITY_RR: priorityRr,
HRRN: hrrn,
MULTILEVEL: multilevel,
};
diff --git a/src/core/algorithms/priority_rr.ts b/src/core/algorithms/priority_rr.ts
new file mode 100644
index 0000000..82b42a7
--- /dev/null
+++ b/src/core/algorithms/priority_rr.ts
@@ -0,0 +1,49 @@
+import type { SchedulerAlgorithm } from "./index";
+
+/**
+ * Priority Round Robin (PRIORITY_RR)
+ *
+ * Mimics real-OS scheduling (Windows priority bands, POSIX SCHED_RR):
+ *
+ * - The ready queue is conceptually divided into priority bands.
+ * - The scheduler always dispatches from the highest-priority band
+ * (lowest numeric value), leaving lower-priority processes waiting.
+ * - Within the same priority band, processes share the CPU via round-robin
+ * with the configured quantum — fairness among equals.
+ * - A running process is preempted immediately when a STRICTLY higher-priority
+ * process becomes ready (arrival or I/O completion).
+ * - Quantum expiry only rotates within the same priority band; it does not
+ * allow a lower-priority process to run while a same-priority one is waiting.
+ * - Compatible with the aging mechanism: as low-priority processes age their
+ * currentPriority decreases, eventually entering a higher-priority band.
+ */
+export const priorityRr: SchedulerAlgorithm = {
+ select(readyQueue) {
+ if (readyQueue.length === 0) return null;
+
+ // Find the highest priority level (smallest numeric value) present.
+ let bestPriority = Infinity;
+ for (const rt of readyQueue) {
+ if (rt.currentPriority < bestPriority) bestPriority = rt.currentPriority;
+ }
+
+ // Within that band, return the first process (FIFO insertion order).
+ // The engine maintains insertion order, so this gives round-robin rotation
+ // after quantum expiry: the expired process goes to the tail, the next
+ // same-priority process is now at the head.
+ return readyQueue.find(rt => rt.currentPriority === bestPriority) ?? null;
+ },
+
+ // Preempt ONLY for strictly higher priority.
+ // Same-priority candidates are handled by quantum expiry — no unnecessary
+ // context switches within a priority band.
+ shouldPreempt(current, candidate) {
+ return candidate.currentPriority < current.currentPriority;
+ },
+
+ // Quantum expiry drives round-robin rotation within a priority band,
+ // exactly like plain Round Robin.
+ isQuantumExpired(runtime) {
+ return runtime.quantumRemaining <= 0;
+ },
+};
diff --git a/src/core/presets.ts b/src/core/presets.ts
index 5e52c81..bb3df64 100644
--- a/src/core/presets.ts
+++ b/src/core/presets.ts
@@ -67,6 +67,16 @@ export const presets: Preset[] = [
],
config: defaultConfig({ algorithm: "PRIORITY_P", isPreemptive: true, agingEnabled: true, agingInterval: 3 }),
},
+ {
+ name: "priority_rr_demo",
+ description: "Priority RR: P3 (priority 1) preempts mid-run; P1 & P2 (priority 2) share CPU via RR",
+ processes: [
+ { id: "p1", name: "P1", arrivalTime: 0, priority: 2, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 4 }] },
+ { id: "p2", name: "P2", arrivalTime: 0, priority: 2, color: "#81C784", bursts: [{ type: "cpu", duration: 4 }] },
+ { id: "p3", name: "P3", arrivalTime: 4, priority: 1, color: "#F06292", bursts: [{ type: "cpu", duration: 2 }] },
+ ],
+ config: defaultConfig({ algorithm: "PRIORITY_RR", quantum: 2 }),
+ },
{
name: "io_heavy",
description: "Mixed CPU-bound and I/O-bound processes",
@@ -77,4 +87,27 @@ export const presets: Preset[] = [
],
config: defaultConfig({ algorithm: "SJF_P", isPreemptive: true }),
},
+ {
+ name: "convoy_effect",
+ description: "One long process blocks shorter ones — FCFS convoy vs SJF comparison",
+ processes: [
+ { id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#EF5350", bursts: [{ type: "cpu", duration: 12 }] },
+ { id: "p2", name: "P2", arrivalTime: 1, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 2 }] },
+ { id: "p3", name: "P3", arrivalTime: 2, priority: 5, color: "#FFB74D", bursts: [{ type: "cpu", duration: 1 }] },
+ { id: "p4", name: "P4", arrivalTime: 3, priority: 5, color: "#CE93D8", bursts: [{ type: "cpu", duration: 3 }] },
+ ],
+ config: defaultConfig({ algorithm: "FCFS" }),
+ },
+ {
+ name: "multilevel_demo",
+ description: "Priority RR with 3 priority tiers — demonstrates multilevel scheduling",
+ processes: [
+ { id: "p1", name: "P1", arrivalTime: 0, priority: 1, color: "#EF5350", bursts: [{ type: "cpu", duration: 4 }] },
+ { id: "p2", name: "P2", arrivalTime: 0, priority: 1, color: "#FF8A65", bursts: [{ type: "cpu", duration: 3 }] },
+ { id: "p3", name: "P3", arrivalTime: 2, priority: 2, color: "#81C784", bursts: [{ type: "cpu", duration: 5 }] },
+ { id: "p4", name: "P4", arrivalTime: 2, priority: 2, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 2 }] },
+ { id: "p5", name: "P5", arrivalTime: 4, priority: 3, color: "#CE93D8", bursts: [{ type: "cpu", duration: 6 }] },
+ ],
+ config: defaultConfig({ algorithm: "PRIORITY_RR", quantum: 2 }),
+ },
];
diff --git a/src/core/types.ts b/src/core/types.ts
index f948320..b42eb21 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -9,6 +9,7 @@ export type SchedulingAlgorithm =
| "RR"
| "PRIORITY_NP"
| "PRIORITY_P"
+ | "PRIORITY_RR"
| "MULTILEVEL"
| "HRRN";
diff --git a/src/hooks/useExport.ts b/src/hooks/useExport.ts
new file mode 100644
index 0000000..a795788
--- /dev/null
+++ b/src/hooks/useExport.ts
@@ -0,0 +1,56 @@
+import type { ProcessMetrics } from "@core/types";
+import type { Process } from "@core/types";
+
+interface ProcessMap {
+ [id: string]: Process;
+}
+
+function downloadBlob(content: string, filename: string, mimeType: string) {
+ const blob = new Blob([content], { type: mimeType });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = filename;
+ a.click();
+ URL.revokeObjectURL(url);
+}
+
+export function useExport() {
+ const exportCSV = (metrics: ProcessMetrics[], processMap: ProcessMap) => {
+ const headers = [
+ "Processo",
+ "Arrival",
+ "Start",
+ "Finish",
+ "Response",
+ "Turnaround",
+ "Waiting",
+ "CPU Time",
+ "I/O Time",
+ "CPU%",
+ "Bound",
+ ];
+
+ const rows = metrics.map((m) => {
+ const name = processMap[m.processId]?.name ?? m.processId;
+ return [
+ name,
+ m.arrivalTime,
+ m.startTick,
+ m.finishTick,
+ m.responseTime,
+ m.turnaroundTime,
+ m.waitingTime,
+ m.cpuTime,
+ m.ioTime,
+ m.cpuUtilization.toFixed(1),
+ m.boundType,
+ ].join(",");
+ });
+
+ const csv = [headers.join(","), ...rows].join("\n");
+ downloadBlob(csv, "pss-metrics.csv", "text/csv;charset=utf-8;");
+ };
+
+ return { exportCSV };
+}
diff --git a/src/hooks/useGanttScroll.ts b/src/hooks/useGanttScroll.ts
new file mode 100644
index 0000000..ecefb51
--- /dev/null
+++ b/src/hooks/useGanttScroll.ts
@@ -0,0 +1,22 @@
+import { useEffect, type RefObject } from "react";
+import { CELL_W } from "../components/GanttChart/constants";
+
+/**
+ * Smoothly scrolls `containerRef` so the current tick stays horizontally
+ * centred in the viewport. Only triggers when the simulation is actively
+ * running (to avoid stealing the user's manual scroll while paused).
+ */
+export function useGanttScroll(
+ containerRef: RefObject,
+ currentTickIndex: number,
+ isRunning: boolean,
+) {
+ useEffect(() => {
+ if (!isRunning) return;
+ const el = containerRef.current;
+ if (!el) return;
+
+ const targetLeft = currentTickIndex * CELL_W - el.clientWidth / 2 + CELL_W / 2;
+ el.scrollTo({ left: Math.max(0, targetLeft), behavior: "smooth" });
+ }, [containerRef, currentTickIndex, isRunning]);
+}
diff --git a/src/index.css b/src/index.css
index c426e83..68161dd 100644
--- a/src/index.css
+++ b/src/index.css
@@ -17,4 +17,28 @@
-ms-overflow-style: none;
scrollbar-width: none;
}
+
+ /* Custom scrollbar for Gantt */
+ .gantt-scrollbar::-webkit-scrollbar {
+ height: 6px;
+ }
+ .gantt-scrollbar::-webkit-scrollbar-track {
+ background: #0f172a; /* slate-900 */
+ }
+ .gantt-scrollbar::-webkit-scrollbar-thumb {
+ background: #334155; /* slate-700 */
+ border-radius: 10px;
+ }
+ .gantt-scrollbar::-webkit-scrollbar-thumb:hover {
+ background: #475569; /* slate-600 */
+ }
+
+ @keyframes gantt-cursor-pulse {
+ 0%, 100% { opacity: 0.3; }
+ 50% { opacity: 1; }
+ }
+
+ .gantt-cursor {
+ animation: gantt-cursor-pulse 2s ease-in-out infinite;
+ }
}
diff --git a/src/store/simulationStore.ts b/src/store/simulationStore.ts
index 049336b..670b293 100644
--- a/src/store/simulationStore.ts
+++ b/src/store/simulationStore.ts
@@ -64,6 +64,8 @@ export const useSimulationStore = create()((set, get) => ({
const { _processes, _config } = get();
if (_processes.length === 0 || _config === null) return;
const engine = new SimulationEngine(_processes, _config);
- set({ engine, ticks: [], status: "paused" });
+ // Return to "idle" so the next Play/Step re-runs init() with the
+ // current processStore config (picks up algorithm changes, etc.)
+ set({ engine, ticks: [], status: "idle" });
},
}));
diff --git a/tsconfig.app.json b/tsconfig.app.json
index ca97cdd..33c5528 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -19,7 +19,8 @@
"paths": {
"@core/*": ["./src/core/*"],
"@components/*": ["./src/components/*"],
- "@store/*": ["./src/store/*"]
+ "@store/*": ["./src/store/*"],
+ "@hooks/*": ["./src/hooks/*"]
}
},
"include": ["src"]
diff --git a/vite.config.ts b/vite.config.ts
index 93bb192..7f8d7e8 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -20,6 +20,7 @@ export default defineConfig({
"@core": path.resolve(__dirname, "./src/core"),
"@components": path.resolve(__dirname, "./src/components"),
"@store": path.resolve(__dirname, "./src/store"),
+ "@hooks": path.resolve(__dirname, "./src/hooks"),
},
},
});