feat: implement Multilevel Queue (MLQ) scheduling algorithm

- Added MLQ scheduling logic in multilevel.ts, including queue selection, preemption rules, and quantum expiration checks.
- Updated SchedulerConfig to include mlqQueues for MLQ configuration.
- Created default MLQ queue definitions for use in presets and process store.
- Modified process store to auto-increment process IDs.
- Enhanced tests for MLQ scheduling, covering selection, preemption, quantum expiration, and integration scenarios.
- Updated existing presets to support MLQ and adjusted process definitions to include auto-incremented IDs.
This commit is contained in:
Lucas Guerra committed 2026-03-19 18:28:04 -03:00
1 parent 64bbc4ca27
commit 9c552e53fa
12 files changed
+550 -67

No files matched your search

+6 -1
View File
@@ -23,7 +23,7 @@ const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
return (
<div className="overflow-x-auto">
<table className="w-full text-xs text-slate-300 border-collapse min-w-[700px]">
<table className="w-full text-xs text-slate-300 border-collapse min-w-175">
<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>
@@ -55,6 +55,11 @@ const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
className="inline-block w-2.5 h-2.5 rounded-full shrink-0"
style={{ backgroundColor: color }}
/>
{proc?.pid !== undefined && (
<span className="text-[11px] font-mono font-semibold text-slate-400 bg-slate-700 px-1.5 py-0.5 rounded shrink-0">
#{proc.pid}
</span>
)}
<span className="font-medium text-slate-200">{name}</span>
</div>
</td>
+7 -2
View File
@@ -24,7 +24,7 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
>
{/* Color indicator */}
<div
className="mt-0.5 h-8 w-1.5 rounded-full flex-shrink-0"
className="mt-0.5 h-8 w-1.5 rounded-full shrink-0"
style={{ backgroundColor: process.color }}
aria-hidden="true"
/>
@@ -32,6 +32,11 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
{process.pid !== undefined && (
<span className="text-[11px] font-mono font-semibold text-slate-400 bg-slate-700 px-1.5 py-0.5 rounded shrink-0">
#{process.pid}
</span>
)}
<span className="font-semibold text-sm text-slate-100 truncate">{process.name}</span>
<Badge variant={boundVariant}>{bound}</Badge>
</div>
@@ -47,7 +52,7 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
</div>
{/* Actions */}
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
<IconButton
size="sm"
variant="ghost"
+5 -6
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { Plus, Minus, Check, X } from 'lucide-react';
import type { Process, BurstSegment } from '@core/types';
import type { BurstSegment, Process } from '@core/types';
import { useProcessStore } from '../../store/processStore';
import IconButton from '../shared/IconButton';
import { generateId, nextColor } from '../shared/processUtils';
@@ -32,7 +32,7 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
setArrivalTime(editingProcess.arrivalTime);
setPriority(editingProcess.priority);
setColor(editingProcess.color);
setBursts([...editingProcess.bursts.map(b => ({ ...b }))]);
setBursts([...editingProcess.bursts.map((b: BurstSegment) => ({ ...b }))]);
} else {
setName('');
setArrivalTime(0);
@@ -89,15 +89,14 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
if (isEditing && editingProcess) {
updateProcess(editingProcess.id, { name: name.trim(), arrivalTime, priority, color, bursts });
} else {
const p: Process = {
addProcess({
id: generateId(),
name: name.trim(),
arrivalTime,
priority,
color,
bursts,
};
addProcess(p);
});
}
onClose();
};
@@ -141,7 +140,7 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
type="color"
value={color}
onChange={e => setColor(e.target.value)}
className="h-[34px] w-12 rounded-md border border-slate-600 bg-slate-700 cursor-pointer"
className="h-8.5 w-12 rounded-md border border-slate-600 bg-slate-700 cursor-pointer"
aria-label="Cor do processo"
/>
</div>
@@ -10,7 +10,7 @@ const ALGORITHM_OPTIONS: { value: SchedulingAlgorithm; label: string; disabled?:
{ 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 (em breve)', disabled: true },
{ value: 'MULTILEVEL', label: 'Multilevel Queue (MLQ)' },
];
const AlgorithmSelector = () => {
@@ -1,8 +1,9 @@
import { Plus, Trash2 } from 'lucide-react';
import { useProcessStore } from '../../store/processStore';
import type { SchedulingAlgorithm } from '@core/types';
import type { MlqQueueDef, SchedulingAlgorithm } from '@core/types';
// Algorithms that require a quantum parameter
const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'PRIORITY_RR', 'MULTILEVEL']);
// Algorithms that require a global quantum parameter
const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'PRIORITY_RR']);
// Algorithms where aging (anti-starvation) is applicable
const AGING_ALGORITHMS = new Set<SchedulingAlgorithm>(['PRIORITY_NP', 'PRIORITY_P', 'PRIORITY_RR']);
@@ -44,6 +45,114 @@ const ToggleRow = ({
</div>
);
const MLQ_ALGO_OPTIONS: MlqQueueDef['algorithm'][] = ['RR', 'PRIORITY_NP', 'FCFS'];
const MlqQueuesConfig = ({
queues,
onChange,
}: {
queues: MlqQueueDef[];
onChange: (queues: MlqQueueDef[]) => void;
}) => {
const update = (idx: number, patch: Partial<MlqQueueDef>) => {
onChange(queues.map((q, i) => (i === idx ? { ...q, ...patch } : q)));
};
const addQueue = () => {
if (queues.length >= 5) return;
onChange([...queues, { priorityMin: 1, priorityMax: 10, algorithm: 'FCFS', quantum: 2 }]);
};
const removeQueue = (idx: number) => {
if (queues.length <= 1) return;
onChange(queues.filter((_, i) => i !== idx));
};
return (
<div className="space-y-2 pt-1" data-testid="mlq-queues-config">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">Filas MLQ (prioridade decrescente)</span>
<button
onClick={addQueue}
disabled={queues.length >= 5}
className="flex items-center gap-1 text-[10px] text-blue-400 hover:text-blue-300 disabled:text-slate-600 disabled:cursor-not-allowed transition-colors"
aria-label="Adicionar fila"
>
<Plus size={11} /> Fila
</button>
</div>
{queues.map((q, idx) => (
<div
key={idx}
className="rounded-md border border-slate-600 bg-slate-800/60 p-2 space-y-1.5"
data-testid={`mlq-queue-${idx}`}
>
<div className="flex items-center justify-between">
<span className="text-[10px] text-slate-400 font-semibold uppercase tracking-wide">
Fila {idx} {idx === 0 ? '(mais alta)' : idx === queues.length - 1 ? '(mais baixa)' : ''}
</span>
<button
onClick={() => removeQueue(idx)}
disabled={queues.length <= 1}
className="text-slate-500 hover:text-red-400 disabled:text-slate-700 disabled:cursor-not-allowed transition-colors"
aria-label={`Remover fila ${idx}`}
>
<Trash2 size={11} />
</button>
</div>
<div className="grid grid-cols-2 gap-1.5">
<div>
<label className="block text-[10px] text-slate-500 mb-0.5">Prio. mín</label>
<input
type="number" min={1} max={10}
value={q.priorityMin}
onChange={e => update(idx, { priorityMin: Math.min(10, Math.max(1, Number(e.target.value))) })}
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
aria-label={`Fila ${idx}: prioridade mínima`}
/>
</div>
<div>
<label className="block text-[10px] text-slate-500 mb-0.5">Prio. máx</label>
<input
type="number" min={1} max={10}
value={q.priorityMax}
onChange={e => update(idx, { priorityMax: Math.min(10, Math.max(1, Number(e.target.value))) })}
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
aria-label={`Fila ${idx}: prioridade máxima`}
/>
</div>
</div>
<div>
<label className="block text-[10px] text-slate-500 mb-0.5">Algoritmo</label>
<select
value={q.algorithm}
onChange={e => update(idx, { algorithm: e.target.value as MlqQueueDef['algorithm'] })}
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
aria-label={`Fila ${idx}: algoritmo`}
>
{MLQ_ALGO_OPTIONS.map(a => (
<option key={a} value={a}>{a}</option>
))}
</select>
</div>
{q.algorithm === 'RR' && (
<div>
<label className="block text-[10px] text-slate-500 mb-0.5">Quantum</label>
<input
type="number" min={1}
value={q.quantum}
onChange={e => update(idx, { quantum: Math.max(1, Number(e.target.value)) })}
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
aria-label={`Fila ${idx}: quantum`}
/>
</div>
)}
</div>
))}
</div>
);
};
const SchedulerConfig = () => {
const config = useProcessStore(s => s.config);
const setConfig = useProcessStore(s => s.setConfig);
@@ -133,6 +242,14 @@ const SchedulerConfig = () => {
)}
</div>
)}
{/* MLQ Queue Configuration */}
{algo === 'MULTILEVEL' && (
<MlqQueuesConfig
queues={config.mlqQueues ?? []}
onChange={queues => setConfig({ mlqQueues: queues })}
/>
)}
</div>
);
};
@@ -65,10 +65,10 @@ describe('SchedulerConfig', () => {
expect(screen.getByLabelText('Quantum do Round Robin')).toBeInTheDocument();
});
it('shows Quantum field when algorithm is MULTILEVEL', () => {
it('shows MLQ queue config panel when algorithm is MULTILEVEL', () => {
setAlgorithm('MULTILEVEL');
render(<SchedulerConfig />);
expect(screen.getByTestId('quantum-field')).toBeInTheDocument();
expect(screen.getByTestId('mlq-queues-config')).toBeInTheDocument();
});
it('does NOT show Quantum field for SJF_NP', () => {
+234
View File
@@ -0,0 +1,234 @@
import { describe, it, expect } from "vitest";
import { multilevel } from "../algorithms/multilevel";
import { SimulationEngine } from "../engine";
import type { MlqQueueDef, Process, ProcessRuntime, SchedulerConfig } from "../types";
// ----------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------
const DEFAULT_QUEUES: MlqQueueDef[] = [
{ priorityMin: 1, priorityMax: 3, algorithm: "RR", quantum: 2 },
{ priorityMin: 4, priorityMax: 7, algorithm: "PRIORITY_NP", quantum: 2 },
{ priorityMin: 8, priorityMax: 10, algorithm: "FCFS", quantum: 2 },
];
function makeRt(
id: string,
priority: number,
arrivalTick = 0,
quantumRemaining = 2,
): ProcessRuntime {
return {
processId: id,
state: "Ready",
remainingBurst: 4,
burstIndex: 0,
arrivalTick,
startTick: null,
finishTick: null,
waitingTime: 0,
cpuTime: 0,
ioTime: 0,
responseTime: null,
quantumRemaining,
currentPriority: priority,
};
}
const cfg = (overrides: Partial<SchedulerConfig> = {}): SchedulerConfig => ({
algorithm: "MULTILEVEL",
quantum: 2,
contextSwitchTime: 0,
isPreemptive: false,
agingEnabled: false,
agingInterval: 5,
mlqQueues: DEFAULT_QUEUES,
...overrides,
});
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("MULTILEVEL select()", () => {
it("returns null for empty queue", () => {
expect(multilevel.select([], cfg())).toBeNull();
});
it("selects from queue 0 (highest priority) when it has processes", () => {
const q0 = makeRt("hi", 1); // priority 1 → queue 0 (1–3)
const q1 = makeRt("mid", 5); // priority 5 → queue 1 (4–7)
const q2 = makeRt("lo", 9); // priority 9 → queue 2 (8–10)
expect(multilevel.select([q1, q2, q0], cfg())?.processId).toBe("hi");
});
it("falls through to queue 1 when queue 0 is empty", () => {
const q1a = makeRt("m1", 4, 0);
const q1b = makeRt("m2", 7, 1);
const q2 = makeRt("lo", 9, 0);
// queue 0 empty — queue 1 processes present
expect(multilevel.select([q1a, q1b, q2], cfg())?.processId).toBe("m1");
});
it("falls through to queue 2 (FCFS) when queues 0 and 1 are empty", () => {
const a = makeRt("a", 10, 5); // priority 10 → queue 2
const b = makeRt("b", 8, 0); // priority 8 → queue 2, earlier arrival
expect(multilevel.select([a, b], cfg())?.processId).toBe("b");
});
it("queue 1 (PRIORITY_NP) selects by lowest priority number", () => {
const low = makeRt("lo", 7, 0); // priority 7
const high = makeRt("hi", 4, 0); // priority 4 (highest in band)
const mid = makeRt("mid", 6, 0); // priority 6
expect(multilevel.select([low, mid, high], cfg())?.processId).toBe("hi");
});
it("queue 0 (RR) returns first in insertion order", () => {
const a = makeRt("a", 1, 0);
const b = makeRt("b", 2, 1);
const c = makeRt("c", 3, 2);
expect(multilevel.select([b, a, c], cfg())?.processId).toBe("b");
expect(multilevel.select([c, b, a], cfg())?.processId).toBe("c");
});
});
// ----------------------------------------------------------------
// Unit: shouldPreempt()
// ----------------------------------------------------------------
describe("MULTILEVEL shouldPreempt()", () => {
it("preempts when candidate is in a strictly higher-priority queue", () => {
const running = makeRt("lo", 9); // queue 2
const candidate = makeRt("hi", 1); // queue 0
expect(multilevel.shouldPreempt(running, candidate, cfg())).toBe(true);
});
it("preempts across adjacent queues", () => {
const running = makeRt("mid", 5); // queue 1
const candidate = makeRt("hi", 2); // queue 0
expect(multilevel.shouldPreempt(running, candidate, cfg())).toBe(true);
});
it("does NOT preempt for same queue", () => {
const a = makeRt("a", 1); // queue 0
const b = makeRt("b", 2); // queue 0
expect(multilevel.shouldPreempt(a, b, cfg())).toBe(false);
});
it("does NOT preempt when candidate is in a lower-priority queue", () => {
const running = makeRt("hi", 1); // queue 0
const candidate = makeRt("lo", 9); // queue 2
expect(multilevel.shouldPreempt(running, candidate, cfg())).toBe(false);
});
});
// ----------------------------------------------------------------
// Unit: isQuantumExpired()
// ----------------------------------------------------------------
describe("MULTILEVEL isQuantumExpired()", () => {
it("returns true for a RR queue process with quantumRemaining <= 0", () => {
const rt = makeRt("hi", 1, 0, 0); // queue 0 → RR
expect(multilevel.isQuantumExpired(rt, cfg())).toBe(true);
});
it("returns false for a RR queue process with quantum remaining", () => {
const rt = makeRt("hi", 2, 0, 1); // queue 0 → RR, still has quantum
expect(multilevel.isQuantumExpired(rt, cfg())).toBe(false);
});
it("returns false for a FCFS queue process even if quantumRemaining is 0", () => {
const rt = makeRt("lo", 9, 0, 0); // queue 2 → FCFS
expect(multilevel.isQuantumExpired(rt, cfg())).toBe(false);
});
it("returns false for a PRIORITY_NP queue process", () => {
const rt = makeRt("mid", 5, 0, 0); // queue 1 → PRIORITY_NP
expect(multilevel.isQuantumExpired(rt, cfg())).toBe(false);
});
});
// ----------------------------------------------------------------
// Integration
// ----------------------------------------------------------------
describe("MULTILEVEL integration — strict queue ordering", () => {
it("queue 0 processes run before queue 1 and queue 2 (CT-MLQ-01)", () => {
// P1: priority=1 (queue 0), CPU:3
// P2: priority=5 (queue 1), CPU:3
// P3: priority=9 (queue 2), CPU:3
// All arrive at 0. Queue 0 runs first, then queue 1, then queue 2.
const processes = [p("p2", 0, 3, 5), p("p3", 0, 3, 9), p("p1", 0, 3, 1)];
const engine = new SimulationEngine(processes, cfg());
const ticks = engine.runAll();
const cpu = ticks.map((t) => t.cpuProcess);
expect(cpu.slice(0, 3).every((id) => id === "p1")).toBe(true);
expect(cpu.slice(3, 6).every((id) => id === "p2")).toBe(true);
expect(cpu.slice(6, 9).every((id) => id === "p3")).toBe(true);
expect(cpu).toHaveLength(9);
});
it("queue 0 RR rotates with quantum=2 (CT-MLQ-02)", () => {
// P1 and P2 both in queue 0 (priority 1), CPU:4 each, quantum=2
const processes = [p("p1", 0, 4, 1), p("p2", 0, 4, 1)];
const engine = new SimulationEngine(processes, cfg());
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("p1");
expect(cpu[5]).toBe("p1");
expect(cpu[6]).toBe("p2");
expect(cpu[7]).toBe("p2");
expect(cpu).toHaveLength(8);
});
it("high-priority queue process preempts lower-queue running process (CT-MLQ-03)", () => {
// P1: priority=9 (queue 2), arrives at 0, CPU:6
// P2: priority=1 (queue 0), arrives at 2, CPU:3
// P1 runs ticks 0–1, P2 preempts at tick 2
const processes = [p("p1", 0, 6, 9), p("p2", 2, 3, 1)];
const engine = new SimulationEngine(processes, cfg());
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("p2");
// P1 resumes after P2 terminates
expect(cpu[5]).toBe("p1");
expect(cpu).toHaveLength(9);
});
it("all processes terminate correctly", () => {
const processes = [p("p1", 0, 3, 1), p("p2", 0, 3, 5), p("p3", 0, 3, 9)];
const engine = new SimulationEngine(processes, cfg());
engine.runAll();
const rts = engine.runtimeStates;
expect(rts.every((rt) => rt.state === "Terminated")).toBe(true);
expect(rts.find((r) => r.processId === "p1")!.cpuTime).toBe(3);
expect(rts.find((r) => r.processId === "p2")!.cpuTime).toBe(3);
expect(rts.find((r) => r.processId === "p3")!.cpuTime).toBe(3);
});
});
+98 -8
View File
@@ -1,19 +1,109 @@
import type { MlqQueueDef, ProcessRuntime } from "../types";
import type { SchedulerAlgorithm } from "./index";
import { tiebreak } from "./utils";
/**
* MULTILEVEL — Multilevel Queue (stub)
* Not yet implemented. Deferred to a later phase.
* MULTILEVEL — Multilevel Queue Scheduling
*
* Processes are permanently assigned to a queue based on their currentPriority.
* The scheduler always dispatches from the highest-priority non-empty queue.
* Each queue has its own scheduling algorithm (FCFS, RR, or PRIORITY_NP).
*
* Inter-queue behaviour:
* - A process from a higher-priority queue always preempts a process
* from a lower-priority queue.
* - No process from a lower-priority queue runs while a higher-priority
* queue has processes.
*
* Intra-queue behaviour:
* - FCFS: dispatch in arrival order; no preemption within the queue.
* - RR: round-robin using the queue's own quantum; no cross-queue switching.
* - PRIORITY_NP: dispatch the highest currentPriority process; no preemption.
*
* Queue assignment is derived from config.mlqQueues. Each queue definition
* specifies [priorityMin, priorityMax] (both inclusive). If a process's
* currentPriority falls outside all queues, it is assigned to the last queue.
*/
/** Returns the index of the queue that owns this priority value (0 = highest). */
function getQueueIndex(priority: number, queues: readonly MlqQueueDef[]): number {
for (let i = 0; i < queues.length; i++) {
const q = queues[i]!;
if (priority >= q.priorityMin && priority <= q.priorityMax) return i;
}
// Fallback: assign to last queue if priority is out of all defined ranges.
return queues.length - 1;
}
/** Select the best candidate from a sub-queue using the queue's algorithm. */
function selectWithinQueue(
candidates: readonly ProcessRuntime[],
queueDef: MlqQueueDef,
): ProcessRuntime | null {
if (candidates.length === 0) return null;
if (queueDef.algorithm === "RR") {
// RR: dispatch in insertion order (engine tail-appends on preemption).
return candidates[0]!;
}
if (queueDef.algorithm === "FCFS") {
// FCFS: dispatch earliest-arriving process (by arrivalTick, then processId).
return candidates.reduce((best, rt) =>
tiebreak(rt, best) < 0 ? rt : best,
);
}
// PRIORITY_NP: dispatch the process with the lowest currentPriority number.
let best = candidates[0]!;
for (let i = 1; i < candidates.length; i++) {
const rt = candidates[i]!;
if (
rt.currentPriority < best.currentPriority ||
(rt.currentPriority === best.currentPriority && tiebreak(rt, best) < 0)
) {
best = rt;
}
}
return best;
}
const DEFAULT_QUEUES: MlqQueueDef[] = [
{ priorityMin: 1, priorityMax: 3, algorithm: "RR", quantum: 2 },
{ priorityMin: 4, priorityMax: 7, algorithm: "PRIORITY_NP", quantum: 2 },
{ priorityMin: 8, priorityMax: 10, algorithm: "FCFS", quantum: 2 },
];
export const multilevel: SchedulerAlgorithm = {
select() {
throw new Error("MULTILEVEL scheduling is not yet implemented");
select(readyQueue, config): ProcessRuntime | null {
const queues = config.mlqQueues ?? DEFAULT_QUEUES;
if (queues.length === 0) return null;
// Iterate from highest-priority queue (index 0) to lowest.
for (let qi = 0; qi < queues.length; qi++) {
const queueDef = queues[qi]!;
const candidates = readyQueue.filter(
(rt) => getQueueIndex(rt.currentPriority, queues) === qi,
);
const selected = selectWithinQueue(candidates, queueDef);
if (selected !== null) return selected;
}
return null;
},
shouldPreempt() {
throw new Error("MULTILEVEL scheduling is not yet implemented");
shouldPreempt(current, candidate, config): boolean {
const queues = config.mlqQueues ?? DEFAULT_QUEUES;
const currentQueueIdx = getQueueIndex(current.currentPriority, queues);
const candidateQueueIdx = getQueueIndex(candidate.currentPriority, queues);
// Preempt only if the candidate belongs to a strictly higher-priority queue.
return candidateQueueIdx < currentQueueIdx;
},
isQuantumExpired() {
throw new Error("MULTILEVEL scheduling is not yet implemented");
isQuantumExpired(runtime, config): boolean {
const queues = config.mlqQueues ?? DEFAULT_QUEUES;
const queueIdx = getQueueIndex(runtime.currentPriority, queues);
const queueDef = queues[queueIdx];
if (!queueDef) return false;
return queueDef.algorithm === "RR" && runtime.quantumRemaining <= 0;
},
};
+47 -40
View File
@@ -1,4 +1,4 @@
import type { Process, SchedulerConfig } from "./types";
import type { MlqQueueDef, Process, SchedulerConfig } from "./types";
export interface Preset {
name: string;
@@ -7,6 +7,12 @@ export interface Preset {
config: SchedulerConfig;
}
const DEFAULT_MLQ_QUEUES: MlqQueueDef[] = [
{ priorityMin: 1, priorityMax: 3, algorithm: "RR", quantum: 2 },
{ priorityMin: 4, priorityMax: 7, algorithm: "PRIORITY_NP", quantum: 2 },
{ priorityMin: 8, priorityMax: 10, algorithm: "FCFS", quantum: 2 },
];
const defaultConfig = (
overrides: Partial<SchedulerConfig> = {},
): SchedulerConfig => ({
@@ -16,6 +22,7 @@ const defaultConfig = (
isPreemptive: false,
agingEnabled: false,
agingInterval: 5,
mlqQueues: DEFAULT_MLQ_QUEUES,
...overrides,
});
@@ -24,10 +31,10 @@ export const presets: Preset[] = [
name: "classic_fcfs",
description: "4 simple processes, no I/O — demonstrates FCFS order",
processes: [
{ id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 5 }] },
{ id: "p2", name: "P2", arrivalTime: 1, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p3", name: "P3", arrivalTime: 2, priority: 5, color: "#FFB74D", bursts: [{ type: "cpu", duration: 1 }] },
{ id: "p4", name: "P4", arrivalTime: 3, priority: 5, color: "#F06292", bursts: [{ type: "cpu", duration: 7 }] },
{ pid: 1, id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 5 }] },
{ pid: 2, id: "p2", name: "P2", arrivalTime: 1, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] },
{ pid: 3, id: "p3", name: "P3", arrivalTime: 2, priority: 5, color: "#FFB74D", bursts: [{ type: "cpu", duration: 1 }] },
{ pid: 4, id: "p4", name: "P4", arrivalTime: 3, priority: 5, color: "#F06292", bursts: [{ type: "cpu", duration: 7 }] },
],
config: defaultConfig({ algorithm: "FCFS" }),
},
@@ -35,11 +42,11 @@ export const presets: Preset[] = [
name: "rr_demo",
description: "5 processes with quantum 3 — Round Robin rotation",
processes: [
{ id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 6 }] },
{ id: "p2", name: "P2", arrivalTime: 0, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 4 }] },
{ id: "p3", name: "P3", arrivalTime: 1, priority: 5, color: "#FFB74D", bursts: [{ type: "cpu", duration: 5 }] },
{ id: "p4", name: "P4", arrivalTime: 2, priority: 5, color: "#F06292", bursts: [{ type: "cpu", duration: 2 }] },
{ id: "p5", name: "P5", arrivalTime: 3, priority: 5, color: "#CE93D8", bursts: [{ type: "cpu", duration: 3 }] },
{ pid: 1, id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 6 }] },
{ pid: 2, id: "p2", name: "P2", arrivalTime: 0, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 4 }] },
{ pid: 3, id: "p3", name: "P3", arrivalTime: 1, priority: 5, color: "#FFB74D", bursts: [{ type: "cpu", duration: 5 }] },
{ pid: 4, id: "p4", name: "P4", arrivalTime: 2, priority: 5, color: "#F06292", bursts: [{ type: "cpu", duration: 2 }] },
{ pid: 5, id: "p5", name: "P5", arrivalTime: 3, priority: 5, color: "#CE93D8", bursts: [{ type: "cpu", duration: 3 }] },
],
config: defaultConfig({ algorithm: "RR", quantum: 3 }),
},
@@ -47,11 +54,11 @@ export const presets: Preset[] = [
name: "starvation",
description: "High-priority processes starve P5 — demonstrates Priority NP starvation",
processes: [
{ id: "p1", name: "P1", arrivalTime: 0, priority: 1, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p2", name: "P2", arrivalTime: 1, priority: 1, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p3", name: "P3", arrivalTime: 2, priority: 1, color: "#FFB74D", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p4", name: "P4", arrivalTime: 3, priority: 1, color: "#F06292", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p5", name: "P5", arrivalTime: 0, priority: 10, color: "#CE93D8", bursts: [{ type: "cpu", duration: 5 }] },
{ pid: 1, id: "p1", name: "P1", arrivalTime: 0, priority: 1, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 3 }] },
{ pid: 2, id: "p2", name: "P2", arrivalTime: 1, priority: 1, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] },
{ pid: 3, id: "p3", name: "P3", arrivalTime: 2, priority: 1, color: "#FFB74D", bursts: [{ type: "cpu", duration: 3 }] },
{ pid: 4, id: "p4", name: "P4", arrivalTime: 3, priority: 1, color: "#F06292", bursts: [{ type: "cpu", duration: 3 }] },
{ pid: 5, id: "p5", name: "P5", arrivalTime: 0, priority: 10, color: "#CE93D8", bursts: [{ type: "cpu", duration: 5 }] },
],
config: defaultConfig({ algorithm: "PRIORITY_NP" }),
},
@@ -59,11 +66,11 @@ export const presets: Preset[] = [
name: "aging_fix",
description: "Same starvation scenario fixed with aging",
processes: [
{ id: "p1", name: "P1", arrivalTime: 0, priority: 1, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p2", name: "P2", arrivalTime: 1, priority: 1, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p3", name: "P3", arrivalTime: 2, priority: 1, color: "#FFB74D", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p4", name: "P4", arrivalTime: 3, priority: 1, color: "#F06292", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p5", name: "P5", arrivalTime: 0, priority: 10, color: "#CE93D8", bursts: [{ type: "cpu", duration: 5 }] },
{ pid: 1, id: "p1", name: "P1", arrivalTime: 0, priority: 1, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 3 }] },
{ pid: 2, id: "p2", name: "P2", arrivalTime: 1, priority: 1, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] },
{ pid: 3, id: "p3", name: "P3", arrivalTime: 2, priority: 1, color: "#FFB74D", bursts: [{ type: "cpu", duration: 3 }] },
{ pid: 4, id: "p4", name: "P4", arrivalTime: 3, priority: 1, color: "#F06292", bursts: [{ type: "cpu", duration: 3 }] },
{ pid: 5, id: "p5", name: "P5", arrivalTime: 0, priority: 10, color: "#CE93D8", bursts: [{ type: "cpu", duration: 5 }] },
],
config: defaultConfig({ algorithm: "PRIORITY_P", isPreemptive: true, agingEnabled: true, agingInterval: 3 }),
},
@@ -71,9 +78,9 @@ export const presets: Preset[] = [
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 }] },
{ pid: 1, id: "p1", name: "P1", arrivalTime: 0, priority: 2, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 4 }] },
{ pid: 2, id: "p2", name: "P2", arrivalTime: 0, priority: 2, color: "#81C784", bursts: [{ type: "cpu", duration: 4 }] },
{ pid: 3, id: "p3", name: "P3", arrivalTime: 4, priority: 1, color: "#F06292", bursts: [{ type: "cpu", duration: 2 }] },
],
config: defaultConfig({ algorithm: "PRIORITY_RR", quantum: 2 }),
},
@@ -81,9 +88,9 @@ export const presets: Preset[] = [
name: "io_heavy",
description: "Mixed CPU-bound and I/O-bound processes",
processes: [
{ id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 6 }, { type: "io", duration: 1 }, { type: "cpu", duration: 2 }] },
{ id: "p2", name: "P2", arrivalTime: 0, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 1 }, { type: "io", duration: 8 }, { type: "cpu", duration: 1 }] },
{ id: "p3", name: "P3", arrivalTime: 2, priority: 5, color: "#FFB74D", bursts: [{ type: "cpu", duration: 4 }, { type: "io", duration: 2 }, { type: "cpu", duration: 4 }] },
{ pid: 1, id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 6 }, { type: "io", duration: 1 }, { type: "cpu", duration: 2 }] },
{ pid: 2, id: "p2", name: "P2", arrivalTime: 0, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 1 }, { type: "io", duration: 8 }, { type: "cpu", duration: 1 }] },
{ pid: 3, id: "p3", name: "P3", arrivalTime: 2, priority: 5, color: "#FFB74D", bursts: [{ type: "cpu", duration: 4 }, { type: "io", duration: 2 }, { type: "cpu", duration: 4 }] },
],
config: defaultConfig({ algorithm: "SJF_P", isPreemptive: true }),
},
@@ -91,10 +98,10 @@ export const presets: Preset[] = [
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 }] },
{ pid: 1, id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#EF5350", bursts: [{ type: "cpu", duration: 12 }] },
{ pid: 2, id: "p2", name: "P2", arrivalTime: 1, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 2 }] },
{ pid: 3, id: "p3", name: "P3", arrivalTime: 2, priority: 5, color: "#FFB74D", bursts: [{ type: "cpu", duration: 1 }] },
{ pid: 4, id: "p4", name: "P4", arrivalTime: 3, priority: 5, color: "#CE93D8", bursts: [{ type: "cpu", duration: 3 }] },
],
config: defaultConfig({ algorithm: "FCFS" }),
},
@@ -102,22 +109,22 @@ export const presets: Preset[] = [
name: "context_switch_demo",
description: "3 processos FCFS com 2 ticks de overhead — veja as células vermelhas de Context Switch",
processes: [
{ id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p2", name: "P2", arrivalTime: 0, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p3", name: "P3", arrivalTime: 0, priority: 5, color: "#FFB74D", bursts: [{ type: "cpu", duration: 3 }] },
{ pid: 1, id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 3 }] },
{ pid: 2, id: "p2", name: "P2", arrivalTime: 0, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] },
{ pid: 3, id: "p3", name: "P3", arrivalTime: 0, priority: 5, color: "#FFB74D", bursts: [{ type: "cpu", duration: 3 }] },
],
config: defaultConfig({ algorithm: "FCFS", contextSwitchTime: 2 }),
},
{
name: "multilevel_demo",
description: "Priority RR with 3 priority tiers — demonstrates multilevel scheduling",
description: "MLQ: 3 priority tiers — Q0 (1–3) RR, Q1 (4–7) Priority NP, Q2 (8–10) FCFS",
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 }] },
{ pid: 1, id: "p1", name: "P1", arrivalTime: 0, priority: 1, color: "#EF5350", bursts: [{ type: "cpu", duration: 4 }] },
{ pid: 2, id: "p2", name: "P2", arrivalTime: 0, priority: 1, color: "#FF8A65", bursts: [{ type: "cpu", duration: 3 }] },
{ pid: 3, id: "p3", name: "P3", arrivalTime: 2, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 5 }] },
{ pid: 4, id: "p4", name: "P4", arrivalTime: 2, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 2 }] },
{ pid: 5, id: "p5", name: "P5", arrivalTime: 4, priority: 9, color: "#CE93D8", bursts: [{ type: "cpu", duration: 6 }] },
],
config: defaultConfig({ algorithm: "PRIORITY_RR", quantum: 2 }),
config: defaultConfig({ algorithm: "MULTILEVEL", quantum: 2, mlqQueues: DEFAULT_MLQ_QUEUES }),
},
];
+9
View File
@@ -21,6 +21,7 @@ export interface BurstSegment {
}
export interface Process {
pid?: number; // auto-incremented sequential integer (1, 2, 3…); set by store
id: string;
name: string;
arrivalTime: number; // tick of arrival (≥ 0)
@@ -31,6 +32,13 @@ export interface Process {
// --------------- Scheduler configuration --------------------
export interface MlqQueueDef {
priorityMin: number; // inclusive lower bound
priorityMax: number; // inclusive upper bound
algorithm: "FCFS" | "RR" | "PRIORITY_NP";
quantum: number; // used only when algorithm === "RR"
}
export interface SchedulerConfig {
algorithm: SchedulingAlgorithm;
quantum: number; // for RR (default 2)
@@ -38,6 +46,7 @@ export interface SchedulerConfig {
isPreemptive: boolean; // applicable to SJF and Priority
agingEnabled: boolean; // prevent starvation in Priority scheduling
agingInterval: number; // ticks in Ready before priority-- (min 1)
mlqQueues?: MlqQueueDef[]; // queue definitions for MULTILEVEL algorithm
}
// --------------- Runtime state ------------------------------
+21 -4
View File
@@ -1,11 +1,17 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import type { Process, SchedulerConfig } from "@core/types";
import type { MlqQueueDef, Process, SchedulerConfig } from "@core/types";
// ============================================================
// Defaults
// ============================================================
const DEFAULT_MLQ_QUEUES: MlqQueueDef[] = [
{ priorityMin: 1, priorityMax: 3, algorithm: "RR", quantum: 2 },
{ priorityMin: 4, priorityMax: 7, algorithm: "PRIORITY_NP", quantum: 2 },
{ priorityMin: 8, priorityMax: 10, algorithm: "FCFS", quantum: 2 },
];
const DEFAULT_CONFIG: SchedulerConfig = {
algorithm: "FCFS",
quantum: 2,
@@ -13,6 +19,7 @@ const DEFAULT_CONFIG: SchedulerConfig = {
isPreemptive: false,
agingEnabled: false,
agingInterval: 5,
mlqQueues: DEFAULT_MLQ_QUEUES,
};
// ============================================================
@@ -22,6 +29,7 @@ const DEFAULT_CONFIG: SchedulerConfig = {
interface ProcessStore {
processes: Process[];
config: SchedulerConfig;
_nextPid: number;
addProcess: (p: Process) => void;
updateProcess: (id: string, updates: Partial<Process>) => void;
@@ -36,9 +44,13 @@ export const useProcessStore = create<ProcessStore>()(
(set) => ({
processes: [],
config: DEFAULT_CONFIG,
_nextPid: 1,
addProcess: (p) =>
set((s) => ({ processes: [...s.processes, p] })),
set((s) => ({
processes: [...s.processes, { ...p, pid: s._nextPid }],
_nextPid: s._nextPid + 1,
})),
updateProcess: (id, updates) =>
set((s) => ({
@@ -50,12 +62,17 @@ export const useProcessStore = create<ProcessStore>()(
removeProcess: (id) =>
set((s) => ({ processes: s.processes.filter((p) => p.id !== id) })),
clearProcesses: () => set({ processes: [] }),
clearProcesses: () => set({ processes: [], _nextPid: 1 }),
setConfig: (updates) =>
set((s) => ({ config: { ...s.config, ...updates } })),
loadPreset: (processes, config) => set({ processes, config }),
loadPreset: (processes, config) =>
set({
processes,
config,
_nextPid: processes.reduce((max, p) => Math.max(max, p.pid ?? 0), 0) + 1,
}),
}),
{ name: "pss:session" },
),