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:
1 parent
64bbc4ca27
commit
9c552e53fa
12 files changed
+550
-67
No files matched your search
BIN
Binary file not shown.
@@ -23,7 +23,7 @@ const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<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>
|
<thead>
|
||||||
<tr className="text-[10px] text-slate-400 uppercase tracking-wider border-b border-slate-700">
|
<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-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"
|
className="inline-block w-2.5 h-2.5 rounded-full shrink-0"
|
||||||
style={{ backgroundColor: color }}
|
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>
|
<span className="font-medium text-slate-200">{name}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
|||||||
>
|
>
|
||||||
{/* Color indicator */}
|
{/* Color indicator */}
|
||||||
<div
|
<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 }}
|
style={{ backgroundColor: process.color }}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
@@ -32,6 +32,11 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
|||||||
{/* Info */}
|
{/* Info */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<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>
|
<span className="font-semibold text-sm text-slate-100 truncate">{process.name}</span>
|
||||||
<Badge variant={boundVariant}>{bound}</Badge>
|
<Badge variant={boundVariant}>{bound}</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,7 +52,7 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* 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
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Plus, Minus, Check, X } from 'lucide-react';
|
import { Plus, Minus, Check, X } from 'lucide-react';
|
||||||
import type { Process, BurstSegment } from '@core/types';
|
import type { BurstSegment, Process } from '@core/types';
|
||||||
import { useProcessStore } from '../../store/processStore';
|
import { useProcessStore } from '../../store/processStore';
|
||||||
import IconButton from '../shared/IconButton';
|
import IconButton from '../shared/IconButton';
|
||||||
import { generateId, nextColor } from '../shared/processUtils';
|
import { generateId, nextColor } from '../shared/processUtils';
|
||||||
@@ -32,7 +32,7 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
setArrivalTime(editingProcess.arrivalTime);
|
setArrivalTime(editingProcess.arrivalTime);
|
||||||
setPriority(editingProcess.priority);
|
setPriority(editingProcess.priority);
|
||||||
setColor(editingProcess.color);
|
setColor(editingProcess.color);
|
||||||
setBursts([...editingProcess.bursts.map(b => ({ ...b }))]);
|
setBursts([...editingProcess.bursts.map((b: BurstSegment) => ({ ...b }))]);
|
||||||
} else {
|
} else {
|
||||||
setName('');
|
setName('');
|
||||||
setArrivalTime(0);
|
setArrivalTime(0);
|
||||||
@@ -89,15 +89,14 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
if (isEditing && editingProcess) {
|
if (isEditing && editingProcess) {
|
||||||
updateProcess(editingProcess.id, { name: name.trim(), arrivalTime, priority, color, bursts });
|
updateProcess(editingProcess.id, { name: name.trim(), arrivalTime, priority, color, bursts });
|
||||||
} else {
|
} else {
|
||||||
const p: Process = {
|
addProcess({
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
arrivalTime,
|
arrivalTime,
|
||||||
priority,
|
priority,
|
||||||
color,
|
color,
|
||||||
bursts,
|
bursts,
|
||||||
};
|
});
|
||||||
addProcess(p);
|
|
||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
@@ -141,7 +140,7 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
type="color"
|
type="color"
|
||||||
value={color}
|
value={color}
|
||||||
onChange={e => setColor(e.target.value)}
|
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"
|
aria-label="Cor do processo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const ALGORITHM_OPTIONS: { value: SchedulingAlgorithm; label: string; disabled?:
|
|||||||
{ value: 'PRIORITY_P', label: 'Priority (Preemptivo)' },
|
{ value: 'PRIORITY_P', label: 'Priority (Preemptivo)' },
|
||||||
{ value: 'PRIORITY_RR',label: 'Priority Round Robin (tempo real)' },
|
{ value: 'PRIORITY_RR',label: 'Priority Round Robin (tempo real)' },
|
||||||
{ value: 'HRRN', label: 'HRRN — Highest Response Ratio Next' },
|
{ 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 = () => {
|
const AlgorithmSelector = () => {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { Plus, Trash2 } from 'lucide-react';
|
||||||
import { useProcessStore } from '../../store/processStore';
|
import { useProcessStore } from '../../store/processStore';
|
||||||
import type { SchedulingAlgorithm } from '@core/types';
|
import type { MlqQueueDef, SchedulingAlgorithm } from '@core/types';
|
||||||
|
|
||||||
// Algorithms that require a quantum parameter
|
// Algorithms that require a global quantum parameter
|
||||||
const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'PRIORITY_RR', 'MULTILEVEL']);
|
const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'PRIORITY_RR']);
|
||||||
|
|
||||||
// Algorithms where aging (anti-starvation) is applicable
|
// Algorithms where aging (anti-starvation) is applicable
|
||||||
const AGING_ALGORITHMS = new Set<SchedulingAlgorithm>(['PRIORITY_NP', 'PRIORITY_P', 'PRIORITY_RR']);
|
const AGING_ALGORITHMS = new Set<SchedulingAlgorithm>(['PRIORITY_NP', 'PRIORITY_P', 'PRIORITY_RR']);
|
||||||
@@ -44,6 +45,114 @@ const ToggleRow = ({
|
|||||||
</div>
|
</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 SchedulerConfig = () => {
|
||||||
const config = useProcessStore(s => s.config);
|
const config = useProcessStore(s => s.config);
|
||||||
const setConfig = useProcessStore(s => s.setConfig);
|
const setConfig = useProcessStore(s => s.setConfig);
|
||||||
@@ -133,6 +242,14 @@ const SchedulerConfig = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* MLQ Queue Configuration */}
|
||||||
|
{algo === 'MULTILEVEL' && (
|
||||||
|
<MlqQueuesConfig
|
||||||
|
queues={config.mlqQueues ?? []}
|
||||||
|
onChange={queues => setConfig({ mlqQueues: queues })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -65,10 +65,10 @@ describe('SchedulerConfig', () => {
|
|||||||
expect(screen.getByLabelText('Quantum do Round Robin')).toBeInTheDocument();
|
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');
|
setAlgorithm('MULTILEVEL');
|
||||||
render(<SchedulerConfig />);
|
render(<SchedulerConfig />);
|
||||||
expect(screen.getByTestId('quantum-field')).toBeInTheDocument();
|
expect(screen.getByTestId('mlq-queues-config')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does NOT show Quantum field for SJF_NP', () => {
|
it('does NOT show Quantum field for SJF_NP', () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,19 +1,109 @@
|
|||||||
|
import type { MlqQueueDef, ProcessRuntime } from "../types";
|
||||||
import type { SchedulerAlgorithm } from "./index";
|
import type { SchedulerAlgorithm } from "./index";
|
||||||
|
import { tiebreak } from "./utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MULTILEVEL — Multilevel Queue (stub)
|
* MULTILEVEL — Multilevel Queue Scheduling
|
||||||
* Not yet implemented. Deferred to a later phase.
|
*
|
||||||
|
* 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 = {
|
export const multilevel: SchedulerAlgorithm = {
|
||||||
select() {
|
select(readyQueue, config): ProcessRuntime | null {
|
||||||
throw new Error("MULTILEVEL scheduling is not yet implemented");
|
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() {
|
shouldPreempt(current, candidate, config): boolean {
|
||||||
throw new Error("MULTILEVEL scheduling is not yet implemented");
|
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() {
|
isQuantumExpired(runtime, config): boolean {
|
||||||
throw new Error("MULTILEVEL scheduling is not yet implemented");
|
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
@@ -1,4 +1,4 @@
|
|||||||
import type { Process, SchedulerConfig } from "./types";
|
import type { MlqQueueDef, Process, SchedulerConfig } from "./types";
|
||||||
|
|
||||||
export interface Preset {
|
export interface Preset {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -7,6 +7,12 @@ export interface Preset {
|
|||||||
config: SchedulerConfig;
|
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 = (
|
const defaultConfig = (
|
||||||
overrides: Partial<SchedulerConfig> = {},
|
overrides: Partial<SchedulerConfig> = {},
|
||||||
): SchedulerConfig => ({
|
): SchedulerConfig => ({
|
||||||
@@ -16,6 +22,7 @@ const defaultConfig = (
|
|||||||
isPreemptive: false,
|
isPreemptive: false,
|
||||||
agingEnabled: false,
|
agingEnabled: false,
|
||||||
agingInterval: 5,
|
agingInterval: 5,
|
||||||
|
mlqQueues: DEFAULT_MLQ_QUEUES,
|
||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -24,10 +31,10 @@ export const presets: Preset[] = [
|
|||||||
name: "classic_fcfs",
|
name: "classic_fcfs",
|
||||||
description: "4 simple processes, no I/O — demonstrates FCFS order",
|
description: "4 simple processes, no I/O — demonstrates FCFS order",
|
||||||
processes: [
|
processes: [
|
||||||
{ id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 5 }] },
|
{ pid: 1, 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 }] },
|
{ pid: 2, 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 }] },
|
{ pid: 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: 4, id: "p4", name: "P4", arrivalTime: 3, priority: 5, color: "#F06292", bursts: [{ type: "cpu", duration: 7 }] },
|
||||||
],
|
],
|
||||||
config: defaultConfig({ algorithm: "FCFS" }),
|
config: defaultConfig({ algorithm: "FCFS" }),
|
||||||
},
|
},
|
||||||
@@ -35,11 +42,11 @@ export const presets: Preset[] = [
|
|||||||
name: "rr_demo",
|
name: "rr_demo",
|
||||||
description: "5 processes with quantum 3 — Round Robin rotation",
|
description: "5 processes with quantum 3 — Round Robin rotation",
|
||||||
processes: [
|
processes: [
|
||||||
{ id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 6 }] },
|
{ pid: 1, 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 }] },
|
{ pid: 2, 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 }] },
|
{ pid: 3, 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 }] },
|
{ pid: 4, 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: 5, id: "p5", name: "P5", arrivalTime: 3, priority: 5, color: "#CE93D8", bursts: [{ type: "cpu", duration: 3 }] },
|
||||||
],
|
],
|
||||||
config: defaultConfig({ algorithm: "RR", quantum: 3 }),
|
config: defaultConfig({ algorithm: "RR", quantum: 3 }),
|
||||||
},
|
},
|
||||||
@@ -47,11 +54,11 @@ export const presets: Preset[] = [
|
|||||||
name: "starvation",
|
name: "starvation",
|
||||||
description: "High-priority processes starve P5 — demonstrates Priority NP starvation",
|
description: "High-priority processes starve P5 — demonstrates Priority NP starvation",
|
||||||
processes: [
|
processes: [
|
||||||
{ id: "p1", name: "P1", arrivalTime: 0, priority: 1, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 3 }] },
|
{ pid: 1, 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 }] },
|
{ pid: 2, 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 }] },
|
{ pid: 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 }] },
|
{ pid: 4, 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: 5, id: "p5", name: "P5", arrivalTime: 0, priority: 10, color: "#CE93D8", bursts: [{ type: "cpu", duration: 5 }] },
|
||||||
],
|
],
|
||||||
config: defaultConfig({ algorithm: "PRIORITY_NP" }),
|
config: defaultConfig({ algorithm: "PRIORITY_NP" }),
|
||||||
},
|
},
|
||||||
@@ -59,11 +66,11 @@ export const presets: Preset[] = [
|
|||||||
name: "aging_fix",
|
name: "aging_fix",
|
||||||
description: "Same starvation scenario fixed with aging",
|
description: "Same starvation scenario fixed with aging",
|
||||||
processes: [
|
processes: [
|
||||||
{ id: "p1", name: "P1", arrivalTime: 0, priority: 1, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 3 }] },
|
{ pid: 1, 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 }] },
|
{ pid: 2, 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 }] },
|
{ pid: 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 }] },
|
{ pid: 4, 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: 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 }),
|
config: defaultConfig({ algorithm: "PRIORITY_P", isPreemptive: true, agingEnabled: true, agingInterval: 3 }),
|
||||||
},
|
},
|
||||||
@@ -71,9 +78,9 @@ export const presets: Preset[] = [
|
|||||||
name: "priority_rr_demo",
|
name: "priority_rr_demo",
|
||||||
description: "Priority RR: P3 (priority 1) preempts mid-run; P1 & P2 (priority 2) share CPU via RR",
|
description: "Priority RR: P3 (priority 1) preempts mid-run; P1 & P2 (priority 2) share CPU via RR",
|
||||||
processes: [
|
processes: [
|
||||||
{ id: "p1", name: "P1", arrivalTime: 0, priority: 2, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 4 }] },
|
{ pid: 1, 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 }] },
|
{ pid: 2, 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: 3, id: "p3", name: "P3", arrivalTime: 4, priority: 1, color: "#F06292", bursts: [{ type: "cpu", duration: 2 }] },
|
||||||
],
|
],
|
||||||
config: defaultConfig({ algorithm: "PRIORITY_RR", quantum: 2 }),
|
config: defaultConfig({ algorithm: "PRIORITY_RR", quantum: 2 }),
|
||||||
},
|
},
|
||||||
@@ -81,9 +88,9 @@ export const presets: Preset[] = [
|
|||||||
name: "io_heavy",
|
name: "io_heavy",
|
||||||
description: "Mixed CPU-bound and I/O-bound processes",
|
description: "Mixed CPU-bound and I/O-bound processes",
|
||||||
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 }] },
|
{ 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 }] },
|
||||||
{ id: "p2", name: "P2", arrivalTime: 0, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 1 }, { type: "io", duration: 8 }, { type: "cpu", duration: 1 }] },
|
{ 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 }] },
|
||||||
{ id: "p3", name: "P3", arrivalTime: 2, priority: 5, color: "#FFB74D", bursts: [{ type: "cpu", duration: 4 }, { type: "io", duration: 2 }, { type: "cpu", duration: 4 }] },
|
{ 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 }),
|
config: defaultConfig({ algorithm: "SJF_P", isPreemptive: true }),
|
||||||
},
|
},
|
||||||
@@ -91,10 +98,10 @@ export const presets: Preset[] = [
|
|||||||
name: "convoy_effect",
|
name: "convoy_effect",
|
||||||
description: "One long process blocks shorter ones — FCFS convoy vs SJF comparison",
|
description: "One long process blocks shorter ones — FCFS convoy vs SJF comparison",
|
||||||
processes: [
|
processes: [
|
||||||
{ id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#EF5350", bursts: [{ type: "cpu", duration: 12 }] },
|
{ pid: 1, 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 }] },
|
{ pid: 2, 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 }] },
|
{ pid: 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: "#CE93D8", bursts: [{ type: "cpu", duration: 3 }] },
|
{ pid: 4, id: "p4", name: "P4", arrivalTime: 3, priority: 5, color: "#CE93D8", bursts: [{ type: "cpu", duration: 3 }] },
|
||||||
],
|
],
|
||||||
config: defaultConfig({ algorithm: "FCFS" }),
|
config: defaultConfig({ algorithm: "FCFS" }),
|
||||||
},
|
},
|
||||||
@@ -102,22 +109,22 @@ export const presets: Preset[] = [
|
|||||||
name: "context_switch_demo",
|
name: "context_switch_demo",
|
||||||
description: "3 processos FCFS com 2 ticks de overhead — veja as células vermelhas de Context Switch",
|
description: "3 processos FCFS com 2 ticks de overhead — veja as células vermelhas de Context Switch",
|
||||||
processes: [
|
processes: [
|
||||||
{ id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 3 }] },
|
{ pid: 1, 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 }] },
|
{ pid: 2, 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: 3, id: "p3", name: "P3", arrivalTime: 0, priority: 5, color: "#FFB74D", bursts: [{ type: "cpu", duration: 3 }] },
|
||||||
],
|
],
|
||||||
config: defaultConfig({ algorithm: "FCFS", contextSwitchTime: 2 }),
|
config: defaultConfig({ algorithm: "FCFS", contextSwitchTime: 2 }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multilevel_demo",
|
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: [
|
processes: [
|
||||||
{ id: "p1", name: "P1", arrivalTime: 0, priority: 1, color: "#EF5350", bursts: [{ type: "cpu", duration: 4 }] },
|
{ pid: 1, 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 }] },
|
{ pid: 2, 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 }] },
|
{ pid: 3, id: "p3", name: "P3", arrivalTime: 2, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 5 }] },
|
||||||
{ id: "p4", name: "P4", arrivalTime: 2, priority: 2, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 2 }] },
|
{ pid: 4, id: "p4", name: "P4", arrivalTime: 2, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 2 }] },
|
||||||
{ id: "p5", name: "P5", arrivalTime: 4, priority: 3, color: "#CE93D8", bursts: [{ type: "cpu", duration: 6 }] },
|
{ 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 }),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -21,6 +21,7 @@ export interface BurstSegment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Process {
|
export interface Process {
|
||||||
|
pid?: number; // auto-incremented sequential integer (1, 2, 3…); set by store
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
arrivalTime: number; // tick of arrival (≥ 0)
|
arrivalTime: number; // tick of arrival (≥ 0)
|
||||||
@@ -31,6 +32,13 @@ export interface Process {
|
|||||||
|
|
||||||
// --------------- Scheduler configuration --------------------
|
// --------------- 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 {
|
export interface SchedulerConfig {
|
||||||
algorithm: SchedulingAlgorithm;
|
algorithm: SchedulingAlgorithm;
|
||||||
quantum: number; // for RR (default 2)
|
quantum: number; // for RR (default 2)
|
||||||
@@ -38,6 +46,7 @@ export interface SchedulerConfig {
|
|||||||
isPreemptive: boolean; // applicable to SJF and Priority
|
isPreemptive: boolean; // applicable to SJF and Priority
|
||||||
agingEnabled: boolean; // prevent starvation in Priority scheduling
|
agingEnabled: boolean; // prevent starvation in Priority scheduling
|
||||||
agingInterval: number; // ticks in Ready before priority-- (min 1)
|
agingInterval: number; // ticks in Ready before priority-- (min 1)
|
||||||
|
mlqQueues?: MlqQueueDef[]; // queue definitions for MULTILEVEL algorithm
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------- Runtime state ------------------------------
|
// --------------- Runtime state ------------------------------
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
import type { Process, SchedulerConfig } from "@core/types";
|
import type { MlqQueueDef, Process, SchedulerConfig } from "@core/types";
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Defaults
|
// 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 = {
|
const DEFAULT_CONFIG: SchedulerConfig = {
|
||||||
algorithm: "FCFS",
|
algorithm: "FCFS",
|
||||||
quantum: 2,
|
quantum: 2,
|
||||||
@@ -13,6 +19,7 @@ const DEFAULT_CONFIG: SchedulerConfig = {
|
|||||||
isPreemptive: false,
|
isPreemptive: false,
|
||||||
agingEnabled: false,
|
agingEnabled: false,
|
||||||
agingInterval: 5,
|
agingInterval: 5,
|
||||||
|
mlqQueues: DEFAULT_MLQ_QUEUES,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -22,6 +29,7 @@ const DEFAULT_CONFIG: SchedulerConfig = {
|
|||||||
interface ProcessStore {
|
interface ProcessStore {
|
||||||
processes: Process[];
|
processes: Process[];
|
||||||
config: SchedulerConfig;
|
config: SchedulerConfig;
|
||||||
|
_nextPid: number;
|
||||||
|
|
||||||
addProcess: (p: Process) => void;
|
addProcess: (p: Process) => void;
|
||||||
updateProcess: (id: string, updates: Partial<Process>) => void;
|
updateProcess: (id: string, updates: Partial<Process>) => void;
|
||||||
@@ -36,9 +44,13 @@ export const useProcessStore = create<ProcessStore>()(
|
|||||||
(set) => ({
|
(set) => ({
|
||||||
processes: [],
|
processes: [],
|
||||||
config: DEFAULT_CONFIG,
|
config: DEFAULT_CONFIG,
|
||||||
|
_nextPid: 1,
|
||||||
|
|
||||||
addProcess: (p) =>
|
addProcess: (p) =>
|
||||||
set((s) => ({ processes: [...s.processes, p] })),
|
set((s) => ({
|
||||||
|
processes: [...s.processes, { ...p, pid: s._nextPid }],
|
||||||
|
_nextPid: s._nextPid + 1,
|
||||||
|
})),
|
||||||
|
|
||||||
updateProcess: (id, updates) =>
|
updateProcess: (id, updates) =>
|
||||||
set((s) => ({
|
set((s) => ({
|
||||||
@@ -50,12 +62,17 @@ export const useProcessStore = create<ProcessStore>()(
|
|||||||
removeProcess: (id) =>
|
removeProcess: (id) =>
|
||||||
set((s) => ({ processes: s.processes.filter((p) => p.id !== id) })),
|
set((s) => ({ processes: s.processes.filter((p) => p.id !== id) })),
|
||||||
|
|
||||||
clearProcesses: () => set({ processes: [] }),
|
clearProcesses: () => set({ processes: [], _nextPid: 1 }),
|
||||||
|
|
||||||
setConfig: (updates) =>
|
setConfig: (updates) =>
|
||||||
set((s) => ({ config: { ...s.config, ...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" },
|
{ name: "pss:session" },
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in new issue
Block a user