Implement Priority Round Robin Scheduling Algorithm with Tests
- Added `priority_rr` algorithm to handle scheduling based on priority and round-robin principles. - Created unit tests for `select()`, `shouldPreempt()`, and `isQuantumExpired()` functions to ensure correct behavior. - Developed integration tests to validate the scheduling logic under various scenarios, including preemption and round-robin execution. - Introduced `useExport` hook for exporting process metrics to CSV format. - Added `useGanttScroll` hook to manage smooth scrolling of the Gantt chart during simulation.
This commit is contained in:
1 parent
083fc0027e
commit
51a8a6fa40
38 files changed
+2530
-45
No files matched your search
@@ -1,6 +1,8 @@
|
||||
import AppLayout from './components/Layout/AppLayout';
|
||||
import { useSimulation } from './hooks/useSimulation';
|
||||
|
||||
function App() {
|
||||
useSimulation();
|
||||
return (
|
||||
<AppLayout />
|
||||
);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from "react";
|
||||
import type { Process, SimTick, StateLabel } from "@core/types";
|
||||
import { CELL_H, CELL_W } from "./constants";
|
||||
|
||||
// ── colour map ──────────────────────────────────────────────
|
||||
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)]",
|
||||
};
|
||||
|
||||
// ── props ────────────────────────────────────────────────────
|
||||
interface GanttCellProps {
|
||||
tick: SimTick;
|
||||
process: Process;
|
||||
state: StateLabel;
|
||||
isCtxSwitch: boolean;
|
||||
onMouseEnter: (e: React.MouseEvent, tick: SimTick, process: Process, state: StateLabel, isCtxSwitch: boolean) => void;
|
||||
onMouseLeave: () => void;
|
||||
}
|
||||
|
||||
// ── component ───────────────────────────────────────────────
|
||||
const GanttCell = React.memo(function GanttCell({
|
||||
tick,
|
||||
process,
|
||||
state,
|
||||
isCtxSwitch,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
}: GanttCellProps) {
|
||||
const bg = isCtxSwitch
|
||||
? "bg-[var(--color-state-ctx)]"
|
||||
: STATE_CLASS[state];
|
||||
|
||||
// Running cells get a coloured left border using the process colour
|
||||
const borderStyle =
|
||||
!isCtxSwitch && state === "Running"
|
||||
? { borderLeft: `3px solid ${process.color}` }
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${bg} shrink-0 opacity-90 hover:opacity-100 transition-opacity cursor-default`}
|
||||
style={{ width: CELL_W, height: CELL_H, ...borderStyle }}
|
||||
onMouseEnter={(e) => onMouseEnter(e, tick, process, state, isCtxSwitch)}
|
||||
onMouseLeave={onMouseLeave}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default GanttCell;
|
||||
@@ -0,0 +1,113 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useSimulationStore } from "@store/simulationStore";
|
||||
import { useProcessStore } from "@store/processStore";
|
||||
import { useGanttScroll } from "@hooks/useGanttScroll";
|
||||
import GanttRow from "./GanttRow";
|
||||
import GanttLegend from "./GanttLegend";
|
||||
import GanttTooltip, { type TooltipData } from "./GanttTooltip";
|
||||
import { CELL_H, CELL_W } from "./constants";
|
||||
|
||||
// ── GanttChart ────────────────────────────────────────────────
|
||||
const GanttChart = () => {
|
||||
const ticks = useSimulationStore((s) => s.ticks);
|
||||
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 }}>
|
||||
Inicie a simulação para ver o diagrama de Gantt
|
||||
</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"
|
||||
>
|
||||
<div style={{ width: totalWidth, position: "relative" }}>
|
||||
|
||||
{/* 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 }} />
|
||||
|
||||
{tickNumbers.map((t) => (
|
||||
<div
|
||||
key={t}
|
||||
className="shrink-0 text-center text-[9px] text-slate-500 font-mono select-none"
|
||||
style={{ width: CELL_W, height: 16, lineHeight: "16px" }}
|
||||
>
|
||||
{t % 5 === 0 ? t : ""}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Process 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>
|
||||
|
||||
<GanttRow
|
||||
process={process}
|
||||
ticks={ticks}
|
||||
onCellMouseEnter={setTooltip}
|
||||
onCellMouseLeave={handleMouseLeave}
|
||||
/>
|
||||
</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 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── process name labels (sticky left, overlay) ── */}
|
||||
{/* Rendered OUTSIDE the scrollable div so they stay fixed on the left */}
|
||||
{/* We achieve this with a separate absolute-positioned column */}
|
||||
|
||||
{/* ── legend ── */}
|
||||
<GanttLegend />
|
||||
|
||||
{/* ── tooltip ── */}
|
||||
{tooltip && <GanttTooltip data={tooltip} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GanttChart;
|
||||
@@ -0,0 +1,25 @@
|
||||
// Legend items ordered as they appear in a typical lifecycle
|
||||
const ITEMS: { label: string; cssVar: string; textClass: string }[] = [
|
||||
{ label: "New", cssVar: "var(--color-state-new)", textClass: "text-white" },
|
||||
{ label: "Ready", cssVar: "var(--color-state-ready)", textClass: "text-slate-900" },
|
||||
{ label: "Running", cssVar: "var(--color-state-running)", textClass: "text-slate-900" },
|
||||
{ label: "Waiting (I/O)", cssVar: "var(--color-state-waiting)", textClass: "text-slate-900" },
|
||||
{ label: "Terminated", cssVar: "var(--color-state-terminated)", textClass: "text-slate-900" },
|
||||
{ label: "Context Switch",cssVar: "var(--color-state-ctx)", textClass: "text-white" },
|
||||
];
|
||||
|
||||
const GanttLegend = () => (
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-2 px-4 py-3 border-t border-slate-700/60">
|
||||
{ITEMS.map(({ label, cssVar, textClass }) => (
|
||||
<div key={label} className="flex items-center gap-1.5">
|
||||
<span
|
||||
className={`inline-block rounded-sm ${textClass}`}
|
||||
style={{ width: 14, height: 14, background: cssVar, flexShrink: 0 }}
|
||||
/>
|
||||
<span className="text-xs text-slate-400">{label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default GanttLegend;
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from "react";
|
||||
import type { Process, SimTick, StateLabel } from "@core/types";
|
||||
import GanttCell from "./GanttCell";
|
||||
import { CELL_H, CELL_W } from "./constants";
|
||||
import type { TooltipData } from "./GanttTooltip";
|
||||
|
||||
interface GanttRowProps {
|
||||
process: Process;
|
||||
ticks: SimTick[];
|
||||
onCellMouseEnter: (data: TooltipData) => void;
|
||||
onCellMouseLeave: () => void;
|
||||
}
|
||||
|
||||
const GanttRow = React.memo(function GanttRow({
|
||||
process,
|
||||
ticks,
|
||||
onCellMouseEnter,
|
||||
onCellMouseLeave,
|
||||
}: GanttRowProps) {
|
||||
return (
|
||||
<div className="flex" style={{ height: CELL_H }}>
|
||||
{ticks.map((tick) => {
|
||||
const state: StateLabel = tick.states[process.id] ?? "New";
|
||||
// A context-switch tick affects the process currently on CPU
|
||||
const isCtxSwitch =
|
||||
tick.contextSwitching && tick.cpuProcess === process.id;
|
||||
|
||||
return (
|
||||
<GanttCell
|
||||
key={tick.tick}
|
||||
tick={tick}
|
||||
process={process}
|
||||
state={state}
|
||||
isCtxSwitch={isCtxSwitch}
|
||||
onMouseEnter={(e, t, p, s, ctx) =>
|
||||
onCellMouseEnter({ tick: t, process: p, state: s, isCtxSwitch: ctx, x: e.clientX, y: e.clientY })
|
||||
}
|
||||
onMouseLeave={onCellMouseLeave}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Empty filler so the row is always at least CELL_W wide */}
|
||||
{ticks.length === 0 && (
|
||||
<div style={{ width: CELL_W, height: CELL_H }} className="bg-slate-800/40" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default GanttRow;
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { Process, SimTick, StateLabel } from "@core/types";
|
||||
|
||||
// ── colour labels ────────────────────────────────────────────
|
||||
const STATE_LABEL: Record<StateLabel, string> = {
|
||||
New: "New",
|
||||
Ready: "Ready",
|
||||
Running: "Running",
|
||||
Waiting: "Waiting (I/O)",
|
||||
Terminated: "Terminated",
|
||||
};
|
||||
|
||||
const STATE_BADGE: Record<StateLabel, string> = {
|
||||
New: "bg-[var(--color-state-new)] text-white",
|
||||
Ready: "bg-[var(--color-state-ready)] text-slate-900",
|
||||
Running: "bg-[var(--color-state-running)] text-slate-900",
|
||||
Waiting: "bg-[var(--color-state-waiting)] text-slate-900",
|
||||
Terminated: "bg-[var(--color-state-terminated)] text-slate-900",
|
||||
};
|
||||
|
||||
// ── types ────────────────────────────────────────────────────
|
||||
export interface TooltipData {
|
||||
tick: SimTick;
|
||||
process: Process;
|
||||
state: StateLabel;
|
||||
isCtxSwitch: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface GanttTooltipProps {
|
||||
data: TooltipData;
|
||||
}
|
||||
|
||||
// ── component ────────────────────────────────────────────────
|
||||
const GanttTooltip = ({ data }: GanttTooltipProps) => {
|
||||
const { tick, process, state, isCtxSwitch, x, y } = data;
|
||||
|
||||
// Keep the tooltip fully inside the viewport
|
||||
const left = Math.min(x + 12, window.innerWidth - 220);
|
||||
const top = Math.min(y + 12, window.innerHeight - 140);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed z-50 pointer-events-none bg-slate-800 border border-slate-600 rounded-lg shadow-2xl px-3 py-2.5 text-xs text-slate-100 w-52"
|
||||
style={{ left, top }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-semibold text-sm" style={{ color: process.color }}>
|
||||
{process.name}
|
||||
</span>
|
||||
<span className="text-slate-400 font-mono">tick {tick.tick}</span>
|
||||
</div>
|
||||
|
||||
{/* State badge */}
|
||||
<div className="mb-1.5">
|
||||
{isCtxSwitch ? (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-state-ctx text-white font-medium">
|
||||
⚠ Context Switch
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full font-medium ${STATE_BADGE[state]}`}
|
||||
>
|
||||
{STATE_LABEL[state]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Queue context */}
|
||||
{tick.readyQueue.length > 0 && (
|
||||
<p className="text-slate-400 text-[10px] mt-1">
|
||||
Ready queue: {tick.readyQueue.length} process{tick.readyQueue.length !== 1 ? "es" : ""}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GanttTooltip;
|
||||
@@ -0,0 +1,79 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, fireEvent } from "@testing-library/react";
|
||||
import GanttCell from "../GanttCell";
|
||||
import type { Process, SimTick } from "@core/types";
|
||||
|
||||
describe("GanttCell", () => {
|
||||
const mockProcess: Process = {
|
||||
id: "p1",
|
||||
name: "P1",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#ff0000",
|
||||
bursts: [{ type: "cpu", duration: 5 }],
|
||||
};
|
||||
|
||||
const mockTick: SimTick = {
|
||||
tick: 1,
|
||||
cpuProcess: "p1",
|
||||
ioProcesses: [],
|
||||
readyQueue: [],
|
||||
states: { p1: "Running" },
|
||||
contextSwitching: false,
|
||||
};
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
it("renders with the correct background color for Running state", () => {
|
||||
const { container } = render(
|
||||
<GanttCell
|
||||
tick={mockTick}
|
||||
process={mockProcess}
|
||||
state="Running"
|
||||
isCtxSwitch={false}
|
||||
onMouseEnter={noop}
|
||||
onMouseLeave={noop}
|
||||
/>
|
||||
);
|
||||
const cell = container.firstChild as HTMLElement;
|
||||
expect(cell.className).toContain("bg-[var(--color-state-running)]");
|
||||
});
|
||||
|
||||
it("renders with the context switch color when isCtxSwitch is true", () => {
|
||||
const { container } = render(
|
||||
<GanttCell
|
||||
tick={mockTick}
|
||||
process={mockProcess}
|
||||
state="Running"
|
||||
isCtxSwitch={true}
|
||||
onMouseEnter={noop}
|
||||
onMouseLeave={noop}
|
||||
/>
|
||||
);
|
||||
const cell = container.firstChild as HTMLElement;
|
||||
expect(cell.className).toContain("bg-[var(--color-state-ctx)]");
|
||||
});
|
||||
|
||||
it("calls onMouseEnter with correct arguments", () => {
|
||||
const onMouseEnter = vi.fn();
|
||||
const { container } = render(
|
||||
<GanttCell
|
||||
tick={mockTick}
|
||||
process={mockProcess}
|
||||
state="Running"
|
||||
isCtxSwitch={false}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={noop}
|
||||
/>
|
||||
);
|
||||
const cell = container.firstChild as HTMLElement;
|
||||
fireEvent.mouseEnter(cell);
|
||||
expect(onMouseEnter).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
mockTick,
|
||||
mockProcess,
|
||||
"Running",
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
/** Width of a single tick cell (px) */
|
||||
export const CELL_W = 28;
|
||||
|
||||
/** Height of a single process row (px) */
|
||||
export const CELL_H = 32;
|
||||
@@ -0,0 +1,150 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { ChevronDown, Download, Upload, BookOpen } from 'lucide-react';
|
||||
import { presets } from '@core/presets';
|
||||
import { useProcessStore } from '../../store/processStore';
|
||||
import { useSimulationStore } from '../../store/simulationStore';
|
||||
|
||||
const AppHeader = () => {
|
||||
const [presetsOpen, setPresetsOpen] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const loadPreset = useProcessStore(s => s.loadPreset);
|
||||
const processes = useProcessStore(s => s.processes);
|
||||
const config = useProcessStore(s => s.config);
|
||||
const simStatus = useSimulationStore(s => s.status);
|
||||
|
||||
const handleLoadPreset = (index: number) => {
|
||||
const p = presets[index];
|
||||
if (!p) return;
|
||||
loadPreset(p.processes, p.config);
|
||||
setPresetsOpen(false);
|
||||
};
|
||||
|
||||
const handleExportJSON = () => {
|
||||
const data = JSON.stringify({ processes, config }, null, 2);
|
||||
const blob = new Blob([data], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'pss-config.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleImportJSON = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => {
|
||||
try {
|
||||
const parsed = JSON.parse(ev.target?.result as string);
|
||||
if (parsed.processes && parsed.config) {
|
||||
loadPreset(parsed.processes, parsed.config);
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore malformed JSON
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
// Reset so the same file can be imported again
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
const isSimulationActive = simStatus !== 'idle';
|
||||
|
||||
return (
|
||||
<header className="h-12 flex items-center px-5 gap-4 bg-slate-900 border-b border-slate-700/60 shrink-0 z-30">
|
||||
{/* Title */}
|
||||
<h1 className="text-sm font-semibold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent whitespace-nowrap">
|
||||
Process Scheduler Simulator
|
||||
</h1>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Presets dropdown */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setPresetsOpen(o => !o)}
|
||||
className="flex items-center gap-1.5 text-xs text-slate-300 hover:text-white px-2.5 py-1.5 rounded-md hover:bg-slate-700 transition-colors"
|
||||
aria-label="Carregar cenário pré-definido"
|
||||
>
|
||||
<BookOpen size={13} />
|
||||
Presets
|
||||
<ChevronDown size={12} className={`transition-transform ${presetsOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
{presetsOpen && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => setPresetsOpen(false)}
|
||||
/>
|
||||
{/* Dropdown */}
|
||||
<div className="absolute right-0 top-full mt-1 w-72 bg-slate-800 border border-slate-600 rounded-lg shadow-xl z-50 overflow-hidden">
|
||||
<div className="px-3 py-2 border-b border-slate-700 text-[10px] text-slate-400 uppercase tracking-wider font-medium">
|
||||
Cenários pré-definidos
|
||||
</div>
|
||||
{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>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Import JSON */}
|
||||
<label
|
||||
htmlFor="import-json"
|
||||
title={isSimulationActive ? 'Reinicie a simulação antes de importar' : 'Importar configuração JSON'}
|
||||
className={`flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md transition-colors cursor-pointer ${
|
||||
isSimulationActive
|
||||
? 'text-slate-600 cursor-not-allowed'
|
||||
: 'text-slate-300 hover:text-white hover:bg-slate-700'
|
||||
}`}
|
||||
aria-label="Importar configuração JSON"
|
||||
>
|
||||
<Upload size={13} />
|
||||
Importar
|
||||
</label>
|
||||
<input
|
||||
id="import-json"
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".json"
|
||||
className="sr-only"
|
||||
onChange={handleImportJSON}
|
||||
disabled={isSimulationActive}
|
||||
aria-label="Selecionar arquivo JSON para importar"
|
||||
/>
|
||||
|
||||
{/* Export JSON */}
|
||||
<button
|
||||
onClick={handleExportJSON}
|
||||
disabled={processes.length === 0}
|
||||
title={processes.length === 0 ? 'Adicione processos antes de exportar' : 'Exportar configuração como JSON'}
|
||||
className="flex items-center gap-1.5 text-xs text-slate-300 hover:text-white px-2.5 py-1.5 rounded-md hover:bg-slate-700 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
aria-label="Exportar configuração JSON"
|
||||
>
|
||||
<Download size={13} />
|
||||
Exportar
|
||||
</button>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppHeader;
|
||||
@@ -1,11 +1,15 @@
|
||||
import AppHeader from './AppHeader';
|
||||
import Sidebar from './Sidebar';
|
||||
import MainArea from './MainArea';
|
||||
|
||||
const AppLayout = () => {
|
||||
return (
|
||||
<div className="flex h-screen w-full bg-slate-900 text-slate-200 overflow-hidden font-sans">
|
||||
<Sidebar />
|
||||
<MainArea />
|
||||
<div className="flex flex-col h-screen w-full bg-slate-900 text-slate-200 overflow-hidden font-sans">
|
||||
<AppHeader />
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<Sidebar />
|
||||
<MainArea />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
import ControlBar from '../ControlBar/ControlBar';
|
||||
import GanttChart from '../GanttChart/GanttChart';
|
||||
import MetricsPanel from '../MetricsPanel/MetricsPanel';
|
||||
|
||||
const MainArea = () => {
|
||||
return (
|
||||
<main className="flex-1 flex flex-col h-full bg-slate-900 overflow-hidden relative">
|
||||
<div className="flex-1 flex flex-col">
|
||||
<ControlBar />
|
||||
|
||||
{/* Placeholder for GanttChart — Phase 4 */}
|
||||
<div className="border-b border-slate-800 h-80 flex items-center justify-center text-slate-600 bg-slate-900/50 text-sm">
|
||||
[ Gantt Chart — Fase 4 ]
|
||||
</div>
|
||||
|
||||
{/* Placeholder for MetricsPanel — Phase 5 */}
|
||||
<div className="flex-1 flex items-center justify-center text-slate-600 bg-slate-950 p-6">
|
||||
<div className="w-full h-full border border-dashed border-slate-700 rounded-xl flex items-center justify-center text-sm">
|
||||
[ Metrics Panel — Fase 5 ]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ControlBar />
|
||||
<GanttChart />
|
||||
<MetricsPanel />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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) => (
|
||||
<div className="bg-slate-800 border border-slate-700 rounded-lg px-4 py-3 flex flex-col gap-1">
|
||||
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-medium">{label}</span>
|
||||
<span className={`text-xl font-bold font-mono ${accent}`}>{value}</span>
|
||||
{sub && <span className="text-[10px] text-slate-500">{sub}</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
|
||||
const {
|
||||
avgResponseTime,
|
||||
avgTurnaroundTime,
|
||||
avgWaitingTime,
|
||||
cpuThroughput,
|
||||
cpuUtilization,
|
||||
totalSimulationTime,
|
||||
} = metrics;
|
||||
|
||||
const utilizationPct = Math.min(100, cpuUtilization);
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
<MetricCard
|
||||
label="Avg Response Time"
|
||||
value={avgResponseTime.toFixed(2)}
|
||||
sub="unidades de tempo"
|
||||
accent="text-amber-300"
|
||||
/>
|
||||
<MetricCard
|
||||
label="Avg Turnaround Time"
|
||||
value={avgTurnaroundTime.toFixed(2)}
|
||||
sub="unidades de tempo"
|
||||
accent="text-blue-300"
|
||||
/>
|
||||
<MetricCard
|
||||
label="Avg Waiting Time"
|
||||
value={avgWaitingTime.toFixed(2)}
|
||||
sub="unidades de tempo"
|
||||
accent="text-orange-300"
|
||||
/>
|
||||
<MetricCard
|
||||
label="CPU Throughput"
|
||||
value={cpuThroughput.toFixed(3)}
|
||||
sub="processos / u.t."
|
||||
accent="text-emerald-300"
|
||||
/>
|
||||
<MetricCard
|
||||
label="Total Simulation Time"
|
||||
value={String(totalSimulationTime)}
|
||||
sub="ticks"
|
||||
accent="text-slate-200"
|
||||
/>
|
||||
{/* CPU Utilization card with progress bar */}
|
||||
<div className="bg-slate-800 border border-slate-700 rounded-lg px-4 py-3 flex flex-col gap-1">
|
||||
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-medium">CPU Utilization</span>
|
||||
<span className="text-xl font-bold font-mono text-violet-300">{utilizationPct.toFixed(1)}%</span>
|
||||
<div className="mt-1 h-1.5 w-full bg-slate-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-violet-500 rounded-full transition-all duration-500"
|
||||
style={{ width: `${utilizationPct}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalMetrics;
|
||||
@@ -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<string, Process>;
|
||||
}
|
||||
|
||||
const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
if (metrics.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-48 text-slate-500 text-sm">
|
||||
Nenhum dado disponível.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="p-4">
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<BarChart data={data} margin={{ top: 4, right: 16, left: 0, bottom: 4 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#334155" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tick={{ fill: '#94a3b8', fontSize: 11 }}
|
||||
axisLine={{ stroke: '#475569' }}
|
||||
tickLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fill: '#94a3b8', fontSize: 11 }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
label={{
|
||||
value: 'ticks',
|
||||
angle: -90,
|
||||
position: 'insideLeft',
|
||||
offset: 10,
|
||||
style: { fill: '#64748b', fontSize: 10 },
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#1e293b',
|
||||
border: '1px solid #334155',
|
||||
borderRadius: '6px',
|
||||
fontSize: '12px',
|
||||
color: '#e2e8f0',
|
||||
}}
|
||||
cursor={{ fill: 'rgba(255,255,255,0.05)' }}
|
||||
/>
|
||||
<Legend
|
||||
wrapperStyle={{ fontSize: '11px', paddingTop: '8px', color: '#94a3b8' }}
|
||||
/>
|
||||
<Bar dataKey="Waiting Time" stackId="a" fill="#f59e0b" radius={[0, 0, 0, 0]} />
|
||||
<Bar dataKey="CPU Time" stackId="a" fill="#22c55e" radius={[0, 0, 0, 0]} />
|
||||
<Bar dataKey="I/O Time" stackId="a" fill="#06b6d4" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
<p className="text-center text-[10px] text-slate-500 mt-1">
|
||||
Barras empilhadas = Turnaround Time (Waiting + CPU + I/O)
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricsChart;
|
||||
@@ -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: <Table size={13} /> },
|
||||
{ id: 'global', label: 'Global', icon: <Globe size={13} /> },
|
||||
{ id: 'chart', label: 'Gráfico', icon: <BarChart2 size={13} /> },
|
||||
];
|
||||
|
||||
const MetricsPanel = () => {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('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 (
|
||||
<div className="border-t border-slate-700 bg-slate-950 flex items-center justify-center h-32 text-slate-600 text-sm shrink-0">
|
||||
Execute a simulação para ver as métricas.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="border-t border-slate-700 bg-slate-950 flex flex-col shrink-0" style={{ height: '280px' }}>
|
||||
{/* 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 => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => 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}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<div className="ml-auto">
|
||||
<button
|
||||
onClick={() => 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"
|
||||
>
|
||||
<Download size={12} />
|
||||
CSV
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{activeTab === 'table' && (
|
||||
<MetricsTable metrics={processMetrics} processMap={processMap} />
|
||||
)}
|
||||
{activeTab === 'global' && (
|
||||
<GlobalMetrics metrics={globalMetrics} />
|
||||
)}
|
||||
{activeTab === 'chart' && (
|
||||
<MetricsChart metrics={processMetrics} processMap={processMap} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricsPanel;
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { ProcessMetrics, Process } from '@core/types';
|
||||
import Badge from '../shared/Badge';
|
||||
|
||||
interface MetricsTableProps {
|
||||
metrics: ProcessMetrics[];
|
||||
processMap: Record<string, Process>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex items-center justify-center h-32 text-slate-500 text-sm">
|
||||
Nenhum processo concluído ainda.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs text-slate-300 border-collapse min-w-[700px]">
|
||||
<thead>
|
||||
<tr className="text-[10px] text-slate-400 uppercase tracking-wider border-b border-slate-700">
|
||||
<th className="text-left px-3 py-2 font-medium">Processo</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Arrival</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Start</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Finish</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Response</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Turnaround</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Waiting</th>
|
||||
<th className="text-right px-3 py-2 font-medium">CPU Time</th>
|
||||
<th className="text-right px-3 py-2 font-medium">I/O Time</th>
|
||||
<th className="text-right px-3 py-2 font-medium">CPU%</th>
|
||||
<th className="text-center px-3 py-2 font-medium">Bound</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{metrics.map((m, i) => {
|
||||
const proc = processMap[m.processId];
|
||||
const name = proc?.name ?? m.processId;
|
||||
const color = proc?.color ?? '#94a3b8';
|
||||
return (
|
||||
<tr
|
||||
key={m.processId}
|
||||
className={`border-b border-slate-800 hover:bg-slate-800/60 transition-colors ${i % 2 === 0 ? '' : 'bg-slate-900/30'}`}
|
||||
>
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="inline-block w-2.5 h-2.5 rounded-full shrink-0"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<span className="font-medium text-slate-200">{name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-right px-3 py-2 font-mono">{m.arrivalTime}</td>
|
||||
<td className="text-right px-3 py-2 font-mono">{m.startTick}</td>
|
||||
<td className="text-right px-3 py-2 font-mono">{m.finishTick}</td>
|
||||
<td className="text-right px-3 py-2 font-mono text-amber-300">{m.responseTime}</td>
|
||||
<td className="text-right px-3 py-2 font-mono text-blue-300">{m.turnaroundTime}</td>
|
||||
<td className="text-right px-3 py-2 font-mono text-orange-300">{m.waitingTime}</td>
|
||||
<td className="text-right px-3 py-2 font-mono text-emerald-300">{m.cpuTime}</td>
|
||||
<td className="text-right px-3 py-2 font-mono text-cyan-300">{m.ioTime}</td>
|
||||
<td className="text-right px-3 py-2 font-mono">{m.cpuUtilization.toFixed(1)}%</td>
|
||||
<td className="text-center px-3 py-2">
|
||||
<Badge variant={boundVariant(m.boundType)}>{m.boundType}</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricsTable;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
<option key={opt.value} value={opt.value} disabled={opt.disabled}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
|
||||
@@ -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<SchedulingAlgorithm>(['RR', 'MULTILEVEL']);
|
||||
const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'PRIORITY_RR', 'MULTILEVEL']);
|
||||
|
||||
// Algorithms where aging (anti-starvation) is applicable
|
||||
const AGING_ALGORITHMS = new Set<SchedulingAlgorithm>(['PRIORITY_NP', 'PRIORITY_P']);
|
||||
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', 'MULTILEVEL']);
|
||||
const PREEMPTIVE_BY_DESIGN = new Set<SchedulingAlgorithm>(['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';
|
||||
|
||||
@@ -28,5 +28,5 @@ const PALETTE = [
|
||||
];
|
||||
let _paletteIndex = 0;
|
||||
export function nextColor(): string {
|
||||
return PALETTE[_paletteIndex++ % PALETTE.length];
|
||||
return PALETTE[_paletteIndex++ % PALETTE.length]!;
|
||||
}
|
||||
@@ -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> = {}): 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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<SchedulingAlgorithm, SchedulerAlgorithm> = {
|
||||
RR: rr,
|
||||
PRIORITY_NP: priorityNp,
|
||||
PRIORITY_P: priorityP,
|
||||
PRIORITY_RR: priorityRr,
|
||||
HRRN: hrrn,
|
||||
MULTILEVEL: multilevel,
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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 }),
|
||||
},
|
||||
];
|
||||
@@ -9,6 +9,7 @@ export type SchedulingAlgorithm =
|
||||
| "RR"
|
||||
| "PRIORITY_NP"
|
||||
| "PRIORITY_P"
|
||||
| "PRIORITY_RR"
|
||||
| "MULTILEVEL"
|
||||
| "HRRN";
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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<HTMLDivElement | null>,
|
||||
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]);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,8 @@ export const useSimulationStore = create<SimulationStore>()((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" });
|
||||
},
|
||||
}));
|
||||
Reference in new issue
Block a user