refactor: Convert scheduling algorithms to class-based BaseAlgorithm structure, update engine and tests, and add new algorithm and metrics documentation.
This commit is contained in:
1 parent
9c552e53fa
commit
571d8e51d2
26 files changed
+1583
-1221
No files matched your search
@@ -1,3 +1,11 @@
|
||||
import {
|
||||
Clock,
|
||||
Timer,
|
||||
Hourglass,
|
||||
Cpu,
|
||||
Zap,
|
||||
Activity,
|
||||
} from 'lucide-react';
|
||||
import type { GlobalMetrics as GlobalMetricsType } from '@core/types';
|
||||
|
||||
interface GlobalMetricsProps {
|
||||
@@ -8,14 +16,36 @@ interface MetricCardProps {
|
||||
label: string;
|
||||
value: string;
|
||||
sub?: string;
|
||||
accent?: string;
|
||||
accentColor: string; // hex or CSS color for the bar fill
|
||||
accentClass: string; // tailwind text class for the value
|
||||
icon: React.ReactNode;
|
||||
barPct?: number; // 0–100; undefined = no bar
|
||||
}
|
||||
|
||||
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>
|
||||
const MetricCard = ({
|
||||
label,
|
||||
value,
|
||||
sub,
|
||||
accentColor,
|
||||
accentClass,
|
||||
icon,
|
||||
barPct,
|
||||
}: MetricCardProps) => (
|
||||
<div className="bg-slate-800/70 border border-slate-700/80 rounded-xl px-4 py-3 flex flex-col gap-1.5 hover:border-slate-600 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-medium">{label}</span>
|
||||
<span className="text-slate-500">{icon}</span>
|
||||
</div>
|
||||
<span className={`text-2xl font-bold font-mono leading-none ${accentClass}`}>{value}</span>
|
||||
{sub && <span className="text-[10px] text-slate-500">{sub}</span>}
|
||||
{barPct !== undefined && (
|
||||
<div className="mt-1 h-1.5 w-full bg-slate-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-500"
|
||||
style={{ width: `${Math.min(100, barPct)}%`, backgroundColor: accentColor }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -30,48 +60,74 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
|
||||
} = metrics;
|
||||
|
||||
const utilizationPct = Math.min(100, cpuUtilization);
|
||||
// Bars for time metrics are proportional to the total simulation time
|
||||
const toPct = (v: number) =>
|
||||
totalSimulationTime > 0 ? Math.min(100, (v / totalSimulationTime) * 100) : 0;
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="p-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"
|
||||
accentColor="#f59e0b"
|
||||
accentClass="text-amber-300"
|
||||
icon={<Clock size={13} />}
|
||||
barPct={toPct(avgResponseTime)}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Avg Turnaround Time"
|
||||
value={avgTurnaroundTime.toFixed(2)}
|
||||
sub="unidades de tempo"
|
||||
accent="text-blue-300"
|
||||
accentColor="#60a5fa"
|
||||
accentClass="text-blue-300"
|
||||
icon={<Timer size={13} />}
|
||||
barPct={toPct(avgTurnaroundTime)}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Avg Waiting Time"
|
||||
value={avgWaitingTime.toFixed(2)}
|
||||
sub="unidades de tempo"
|
||||
accent="text-orange-300"
|
||||
accentColor="#fb923c"
|
||||
accentClass="text-orange-300"
|
||||
icon={<Hourglass size={13} />}
|
||||
barPct={toPct(avgWaitingTime)}
|
||||
/>
|
||||
<MetricCard
|
||||
label="CPU Throughput"
|
||||
value={cpuThroughput.toFixed(3)}
|
||||
sub="processos / u.t."
|
||||
accent="text-emerald-300"
|
||||
accentColor="#34d399"
|
||||
accentClass="text-emerald-300"
|
||||
icon={<Zap size={13} />}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Total Simulation Time"
|
||||
value={String(totalSimulationTime)}
|
||||
sub="ticks"
|
||||
accent="text-slate-200"
|
||||
accentColor="#94a3b8"
|
||||
accentClass="text-slate-200"
|
||||
icon={<Activity size={13} />}
|
||||
/>
|
||||
{/* 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">
|
||||
|
||||
{/* CPU Utilization — gradient bar */}
|
||||
<div className="bg-slate-800/70 border border-slate-700/80 rounded-xl px-4 py-3 flex flex-col gap-1.5 hover:border-slate-600 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-medium">CPU Utilization</span>
|
||||
<span className="text-slate-500"><Cpu size={13} /></span>
|
||||
</div>
|
||||
<span className="text-2xl font-bold font-mono leading-none text-violet-300">
|
||||
{utilizationPct.toFixed(1)}%
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-500">do tempo total em CPU</span>
|
||||
<div className="mt-1 h-2 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}%` }}
|
||||
className="h-full rounded-full transition-all duration-500"
|
||||
style={{
|
||||
width: `${utilizationPct}%`,
|
||||
background: `linear-gradient(90deg, #f59e0b ${0}%, #34d399 ${100}%)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
CartesianGrid,
|
||||
RadarChart,
|
||||
PolarGrid,
|
||||
PolarAngleAxis,
|
||||
Radar,
|
||||
} from 'recharts';
|
||||
import type { ProcessMetrics, Process } from '@core/types';
|
||||
|
||||
@@ -15,6 +19,15 @@ interface MetricsChartProps {
|
||||
processMap: Record<string, Process>;
|
||||
}
|
||||
|
||||
const TOOLTIP_STYLE = {
|
||||
backgroundColor: '#1e293b',
|
||||
border: '1px solid #334155',
|
||||
borderRadius: '8px',
|
||||
fontSize: '11px',
|
||||
color: '#e2e8f0',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
|
||||
};
|
||||
|
||||
const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
if (metrics.length === 0) {
|
||||
return (
|
||||
@@ -24,57 +37,131 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
const getName = (id: string) => processMap[id]?.name ?? id;
|
||||
const getColor = (id: string) => processMap[id]?.color ?? '#94a3b8';
|
||||
|
||||
// ── Stacked bar data ──────────────────────────────────────────
|
||||
const barData = metrics.map((m) => ({
|
||||
name: getName(m.processId),
|
||||
'Waiting': m.waitingTime,
|
||||
'CPU': m.cpuTime,
|
||||
'I/O': m.ioTime,
|
||||
_color: getColor(m.processId),
|
||||
}));
|
||||
|
||||
// ── Radar chart data (normalized 0-100 per dimension) ─────────
|
||||
// Each "subject" is a process, each series is a dimension.
|
||||
// We invert "Response" and "Waiting" so higher = better.
|
||||
const maxT = Math.max(...metrics.map(m => m.turnaroundTime), 1);
|
||||
const maxR = Math.max(...metrics.map(m => m.responseTime), 1);
|
||||
const maxW = Math.max(...metrics.map(m => m.waitingTime), 1);
|
||||
const maxCpu = Math.max(...metrics.map(m => m.cpuTime), 1);
|
||||
const maxIo = Math.max(...metrics.map(m => m.ioTime), 1);
|
||||
|
||||
const radarDimensions = [
|
||||
'Turnaround', 'Response', 'Waiting', 'CPU Time', 'I/O Time',
|
||||
] as const;
|
||||
|
||||
// Radar: rows = dimensions, column per process → Recharts RadarChart needs
|
||||
// one row per "subject" (dimension), with one field per process.
|
||||
const radarData = radarDimensions.map(dim => {
|
||||
const row: Record<string, string | number> = { dimension: dim };
|
||||
metrics.forEach(m => {
|
||||
const key = getName(m.processId);
|
||||
switch (dim) {
|
||||
case 'Turnaround': row[key] = (m.turnaroundTime / maxT) * 100; break;
|
||||
case 'Response': row[key] = (m.responseTime / maxR) * 100; break;
|
||||
case 'Waiting': row[key] = (m.waitingTime / maxW) * 100; break;
|
||||
case 'CPU Time': row[key] = (m.cpuTime / maxCpu)* 100; break;
|
||||
case 'I/O Time': row[key] = (m.ioTime / maxIo) * 100; break;
|
||||
}
|
||||
});
|
||||
return row;
|
||||
});
|
||||
|
||||
const showRadar = metrics.length >= 2;
|
||||
|
||||
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 className={`p-3 flex gap-3 ${showRadar ? 'flex-row' : 'flex-col'}`}>
|
||||
{/* ── Radar chart ───────────────────────────────────────── */}
|
||||
{showRadar && (
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
||||
Perfil Comparativo (normalizado)
|
||||
</p>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<RadarChart data={radarData} margin={{ top: 8, right: 20, bottom: 8, left: 20 }}>
|
||||
<PolarGrid stroke="#334155" />
|
||||
<PolarAngleAxis
|
||||
dataKey="dimension"
|
||||
tick={{ fill: '#94a3b8', fontSize: 10 }}
|
||||
/>
|
||||
{metrics.map((m) => (
|
||||
<Radar
|
||||
key={m.processId}
|
||||
name={getName(m.processId)}
|
||||
dataKey={getName(m.processId)}
|
||||
stroke={getColor(m.processId)}
|
||||
fill={getColor(m.processId)}
|
||||
fillOpacity={0.15}
|
||||
strokeWidth={2}
|
||||
dot={{ r: 3, fill: getColor(m.processId) }}
|
||||
/>
|
||||
))}
|
||||
<Tooltip
|
||||
contentStyle={TOOLTIP_STYLE}
|
||||
formatter={(value) => [typeof value === 'number' ? `${value.toFixed(1)}%` : '-', '']}
|
||||
/>
|
||||
<Legend
|
||||
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
|
||||
/>
|
||||
</RadarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Stacked bar chart ─────────────────────────────────── */}
|
||||
<div className={showRadar ? 'flex-1 min-w-0' : 'w-full'}>
|
||||
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
||||
Breakdown por Processo (ticks)
|
||||
</p>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<BarChart data={barData} margin={{ top: 8, right: 12, left: -8, bottom: 4 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tick={{ fill: '#94a3b8', fontSize: 11 }}
|
||||
axisLine={{ stroke: '#334155' }}
|
||||
tickLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fill: '#94a3b8', fontSize: 10 }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
label={{
|
||||
value: 'ticks',
|
||||
angle: -90,
|
||||
position: 'insideLeft',
|
||||
offset: 14,
|
||||
style: { fill: '#475569', fontSize: 9 },
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={TOOLTIP_STYLE}
|
||||
cursor={{ fill: 'rgba(255,255,255,0.04)' }}
|
||||
/>
|
||||
<Legend
|
||||
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
|
||||
/>
|
||||
<Bar dataKey="Waiting" stackId="a" fill="#f59e0b" radius={[0, 0, 0, 0]} />
|
||||
<Bar dataKey="CPU" stackId="a" fill="#22c55e" radius={[0, 0, 0, 0]} />
|
||||
<Bar dataKey="I/O" stackId="a" fill="#06b6d4" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
<p className="text-center text-[9px] text-slate-600 mt-0.5">
|
||||
Waiting + CPU + I/O = Turnaround
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@ const MetricsPanel = () => {
|
||||
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' }}>
|
||||
<div className="border-t border-slate-700 bg-slate-950 flex flex-col shrink-0" style={{ height: '340px' }}>
|
||||
{/* 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 => (
|
||||
|
||||
@@ -12,6 +12,26 @@ function boundVariant(type: ProcessMetrics['boundType']) {
|
||||
return 'balanced';
|
||||
}
|
||||
|
||||
// Inline mini bar rendered behind the numeric value
|
||||
interface MiniBarProps {
|
||||
value: number;
|
||||
max: number;
|
||||
color: string; // tailwind bg class
|
||||
}
|
||||
|
||||
const MiniBar = ({ value, max, color }: MiniBarProps) => {
|
||||
const pct = max > 0 ? (value / max) * 100 : 0;
|
||||
return (
|
||||
<div className="relative flex items-center justify-end gap-1.5 min-w-[52px]">
|
||||
<div
|
||||
className="absolute inset-y-0 right-0 rounded-sm opacity-20 transition-all duration-300"
|
||||
style={{ width: `${pct}%`, backgroundColor: color }}
|
||||
/>
|
||||
<span className="relative z-10 font-mono text-xs">{value}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
|
||||
if (metrics.length === 0) {
|
||||
return (
|
||||
@@ -21,34 +41,42 @@ const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Pre-compute maximums for proportional bars
|
||||
const maxResponse = Math.max(...metrics.map(m => m.responseTime), 1);
|
||||
const maxTurnaround = Math.max(...metrics.map(m => m.turnaroundTime), 1);
|
||||
const maxWaiting = Math.max(...metrics.map(m => m.waitingTime), 1);
|
||||
const maxCpu = Math.max(...metrics.map(m => m.cpuTime), 1);
|
||||
const maxIo = Math.max(...metrics.map(m => m.ioTime), 1);
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs text-slate-300 border-collapse min-w-175">
|
||||
<table className="w-full text-xs text-slate-300 border-collapse min-w-[780px]">
|
||||
<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 sticky top-0 bg-slate-950 z-10">
|
||||
<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 text-amber-400">Response</th>
|
||||
<th className="text-right px-3 py-2 font-medium text-blue-400">Turnaround</th>
|
||||
<th className="text-right px-3 py-2 font-medium text-orange-400">Waiting</th>
|
||||
<th className="text-right px-3 py-2 font-medium text-emerald-400">CPU</th>
|
||||
<th className="text-right px-3 py-2 font-medium text-cyan-400">I/O</th>
|
||||
<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 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'}`}
|
||||
className={`border-b border-slate-800/80 hover:bg-slate-800/50 transition-colors ${i % 2 === 0 ? '' : 'bg-slate-900/20'}`}
|
||||
>
|
||||
{/* Process name */}
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
@@ -56,22 +84,55 @@ const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
|
||||
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">
|
||||
<span className="text-[10px] 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>
|
||||
<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>
|
||||
|
||||
{/* Plain timestamps */}
|
||||
<td className="text-right px-3 py-2 font-mono text-slate-400">{m.arrivalTime}</td>
|
||||
<td className="text-right px-3 py-2 font-mono text-slate-400">{m.startTick}</td>
|
||||
<td className="text-right px-3 py-2 font-mono text-slate-400">{m.finishTick}</td>
|
||||
|
||||
{/* Cells with inline mini-bars */}
|
||||
<td className="px-3 py-2 text-amber-300">
|
||||
<MiniBar value={m.responseTime} max={maxResponse} color="#f59e0b" />
|
||||
</td>
|
||||
<td className="px-3 py-2 text-blue-300">
|
||||
<MiniBar value={m.turnaroundTime} max={maxTurnaround} color="#60a5fa" />
|
||||
</td>
|
||||
<td className="px-3 py-2 text-orange-300">
|
||||
<MiniBar value={m.waitingTime} max={maxWaiting} color="#fb923c" />
|
||||
</td>
|
||||
<td className="px-3 py-2 text-emerald-300">
|
||||
<MiniBar value={m.cpuTime} max={maxCpu} color="#34d399" />
|
||||
</td>
|
||||
<td className="px-3 py-2 text-cyan-300">
|
||||
<MiniBar value={m.ioTime} max={maxIo} color="#22d3ee" />
|
||||
</td>
|
||||
|
||||
{/* CPU utilization with a tiny arc/bar */}
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex items-center justify-end gap-1.5">
|
||||
<div className="w-10 h-1.5 bg-slate-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.min(100, m.cpuUtilization)}%`,
|
||||
background: `linear-gradient(90deg, #a78bfa, #60a5fa)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="font-mono text-slate-300 w-9 text-right">
|
||||
{m.cpuUtilization.toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Bound badge */}
|
||||
<td className="text-center px-3 py-2">
|
||||
<Badge variant={boundVariant(m.boundType)}>{m.boundType}</Badge>
|
||||
</td>
|
||||
|
||||
@@ -193,10 +193,11 @@ describe("CT-04: SRTF Preemption", () => {
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// General engine behaviour
|
||||
// SimulationEngine — ciclo de vida
|
||||
// Tests: isFinished, step() guard, runAll vs step equivalence
|
||||
// ============================================================
|
||||
|
||||
describe("SimulationEngine general", () => {
|
||||
describe("SimulationEngine — ciclo de vida", () => {
|
||||
it("isFinished returns false mid-run, true after runAll", () => {
|
||||
const engine = new SimulationEngine(
|
||||
[p("p1", 0, [{ type: "cpu", duration: 3 }])],
|
||||
@@ -232,22 +233,14 @@ describe("SimulationEngine general", () => {
|
||||
|
||||
expect(fromRunAll).toEqual(fromStep);
|
||||
});
|
||||
});
|
||||
|
||||
it("process with I/O: transitions through Waiting state", () => {
|
||||
const engine = new SimulationEngine(
|
||||
[p("p1", 0, [{ type: "cpu", duration: 2 }, { type: "io", duration: 2 }, { type: "cpu", duration: 1 }])],
|
||||
cfg(),
|
||||
);
|
||||
const ticks = engine.runAll();
|
||||
const states = ticks.map((t) => t.states["p1"]);
|
||||
// Running for 2, Waiting for 2, Running for 1
|
||||
expect(states[0]).toBe("Running");
|
||||
expect(states[1]).toBe("Running");
|
||||
expect(states[2]).toBe("Waiting");
|
||||
expect(states[3]).toBe("Waiting");
|
||||
expect(states[4]).toBe("Running");
|
||||
});
|
||||
// ============================================================
|
||||
// SimulationEngine — chegada de processos
|
||||
// Tests: late arrivals, idle ticks while waiting for process
|
||||
// ============================================================
|
||||
|
||||
describe("SimulationEngine — chegada de processos", () => {
|
||||
it("process arriving after simulation starts is correctly scheduled", () => {
|
||||
const engine = new SimulationEngine(
|
||||
[
|
||||
@@ -261,7 +254,14 @@ describe("SimulationEngine general", () => {
|
||||
expect(ticks[4]?.cpuProcess).toBeNull(); // tick 4: idle
|
||||
expect(ticks[5]?.cpuProcess).toBe("p2");
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// SimulationEngine — troca de contexto
|
||||
// Tests: idle ticks during switch, ctxSwitchForProcess across overhead
|
||||
// ============================================================
|
||||
|
||||
describe("SimulationEngine — troca de contexto", () => {
|
||||
it("context switch inserts idle ticks between processes", () => {
|
||||
const engine = new SimulationEngine(
|
||||
[
|
||||
@@ -298,3 +298,25 @@ describe("SimulationEngine general", () => {
|
||||
expect(ticks[4]?.ctxSwitchForProcess).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// SimulationEngine — I/O
|
||||
// Tests: Waiting state, IO burst transitions
|
||||
// ============================================================
|
||||
|
||||
describe("SimulationEngine — I/O", () => {
|
||||
it("process with I/O: transitions through Waiting state", () => {
|
||||
const engine = new SimulationEngine(
|
||||
[p("p1", 0, [{ type: "cpu", duration: 2 }, { type: "io", duration: 2 }, { type: "cpu", duration: 1 }])],
|
||||
cfg(),
|
||||
);
|
||||
const ticks = engine.runAll();
|
||||
const states = ticks.map((t) => t.states["p1"]);
|
||||
// Running for 2, Waiting for 2, Running for 1
|
||||
expect(states[0]).toBe("Running");
|
||||
expect(states[1]).toBe("Running");
|
||||
expect(states[2]).toBe("Waiting");
|
||||
expect(states[3]).toBe("Waiting");
|
||||
expect(states[4]).toBe("Running");
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,9 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { fcfs } from "../algorithms/fcfs";
|
||||
import { FCFSAlgorithm } from "../algorithms/fcfs";
|
||||
import type { ProcessRuntime } from "../types";
|
||||
|
||||
const fcfs = new FCFSAlgorithm();
|
||||
|
||||
function makeRt(id: string, arrivalTick: number): ProcessRuntime {
|
||||
return {
|
||||
processId: id, state: "Ready", remainingBurst: 4, burstIndex: 0,
|
||||
@@ -11,37 +13,33 @@ function makeRt(id: string, arrivalTick: number): ProcessRuntime {
|
||||
};
|
||||
}
|
||||
|
||||
const config = { algorithm: "FCFS" as const, quantum: 2, contextSwitchTime: 0, isPreemptive: false, agingEnabled: false, agingInterval: 5 };
|
||||
|
||||
describe("FCFS select()", () => {
|
||||
it("returns null for empty queue", () => {
|
||||
expect(fcfs.select([], config)).toBeNull();
|
||||
expect(fcfs.select([])).toBeNull();
|
||||
});
|
||||
|
||||
it("returns single process", () => {
|
||||
const rt = makeRt("p1", 0);
|
||||
expect(fcfs.select([rt], config)).toBe(rt);
|
||||
expect(fcfs.select([rt])).toBe(rt);
|
||||
});
|
||||
|
||||
it("returns earliest-arriving process", () => {
|
||||
const r1 = makeRt("p1", 5);
|
||||
const r2 = makeRt("p2", 2);
|
||||
const r3 = makeRt("p3", 8);
|
||||
expect(fcfs.select([r1, r2, r3], config)?.processId).toBe("p2");
|
||||
expect(fcfs.select([r1, r2, r3])?.processId).toBe("p2");
|
||||
});
|
||||
|
||||
it("tiebreaks by processId lexicographic", () => {
|
||||
const r1 = makeRt("p2", 0);
|
||||
const r2 = makeRt("p1", 0);
|
||||
expect(fcfs.select([r1, r2], config)?.processId).toBe("p1");
|
||||
expect(fcfs.select([r1, r2])?.processId).toBe("p1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("FCFS shouldPreempt()", () => {
|
||||
it("always returns false", () => {
|
||||
const a = makeRt("p1", 0);
|
||||
const b = makeRt("p2", 0);
|
||||
expect(fcfs.shouldPreempt(a, b, config)).toBe(false);
|
||||
expect(fcfs.shouldPreempt()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,6 +47,14 @@ describe("FCFS isQuantumExpired()", () => {
|
||||
it("always returns false", () => {
|
||||
const rt = makeRt("p1", 0);
|
||||
rt.quantumRemaining = 0;
|
||||
expect(fcfs.isQuantumExpired(rt, config)).toBe(false);
|
||||
expect(fcfs.isQuantumExpired()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("FCFSAlgorithm — metadados", () => {
|
||||
it("expõe propriedades educacionais corretas", () => {
|
||||
expect(fcfs.name).toContain("First Come");
|
||||
expect(fcfs.isPreemptiveCapable).toBe(false);
|
||||
expect(fcfs.usesQuantum).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,10 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { hrrn } from "../algorithms/hrrn";
|
||||
import { HRRNAlgorithm } from "../algorithms/hrrn";
|
||||
import { SimulationEngine } from "../engine";
|
||||
import type { Process, ProcessRuntime, SchedulerConfig } from "../types";
|
||||
|
||||
const hrrn = new HRRNAlgorithm();
|
||||
|
||||
function makeRt(id: string, remainingBurst: number, waitingTime: number, arrivalTick = 0): ProcessRuntime {
|
||||
return {
|
||||
processId: id, state: "Ready", remainingBurst, burstIndex: 0,
|
||||
@@ -24,7 +26,7 @@ const p = (id: string, arrival: number, cpuDuration: number): Process => ({
|
||||
|
||||
describe("HRRN select()", () => {
|
||||
it("returns null for empty queue", () => {
|
||||
expect(hrrn.select([], config)).toBeNull();
|
||||
expect(hrrn.select([])).toBeNull();
|
||||
});
|
||||
|
||||
it("picks process with highest response ratio", () => {
|
||||
@@ -35,20 +37,18 @@ describe("HRRN select()", () => {
|
||||
const r1 = makeRt("p1", 8, 0);
|
||||
const r2 = makeRt("p2", 3, 6);
|
||||
const r3 = makeRt("p3", 5, 2);
|
||||
expect(hrrn.select([r1, r2, r3], config)?.processId).toBe("p2");
|
||||
expect(hrrn.select([r1, r2, r3])?.processId).toBe("p2");
|
||||
});
|
||||
|
||||
it("tiebreaks equal ratio by arrival then id", () => {
|
||||
// Both ratio = (4 + 4) / 4 = 2.0
|
||||
const r1 = makeRt("p2", 4, 4, 1); // arrives at 1
|
||||
const r2 = makeRt("p1", 4, 4, 2); // arrives at 2
|
||||
expect(hrrn.select([r1, r2], config)?.processId).toBe("p2");
|
||||
expect(hrrn.select([r1, r2])?.processId).toBe("p2");
|
||||
});
|
||||
|
||||
it("shouldPreempt always false", () => {
|
||||
const a = makeRt("p1", 4, 0);
|
||||
const b = makeRt("p2", 4, 10);
|
||||
expect(hrrn.shouldPreempt(a, b, config)).toBe(false);
|
||||
expect(hrrn.shouldPreempt()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,3 +81,11 @@ describe("HRRN integration", () => {
|
||||
expect(ticks[3]?.cpuProcess).toBe("p3");
|
||||
});
|
||||
});
|
||||
|
||||
describe("HRRNAlgorithm — metadados", () => {
|
||||
it("expõe propriedades educacionais corretas", () => {
|
||||
expect(hrrn.name).toContain("HRRN");
|
||||
expect(hrrn.isPreemptiveCapable).toBe(false);
|
||||
expect(hrrn.usesQuantum).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,10 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { multilevel } from "../algorithms/multilevel";
|
||||
import { MultilevelQueueAlgorithm } from "../algorithms/multilevel";
|
||||
import { SimulationEngine } from "../engine";
|
||||
import type { MlqQueueDef, Process, ProcessRuntime, SchedulerConfig } from "../types";
|
||||
|
||||
const multilevel = new MultilevelQueueAlgorithm();
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Helpers
|
||||
// ----------------------------------------------------------------
|
||||
@@ -232,3 +234,11 @@ describe("MULTILEVEL integration — strict queue ordering", () => {
|
||||
expect(rts.find((r) => r.processId === "p3")!.cpuTime).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MultilevelQueueAlgorithm — metadados", () => {
|
||||
it("expõe propriedades educacionais corretas", () => {
|
||||
expect(multilevel.name).toContain("Multilevel");
|
||||
expect(multilevel.isPreemptiveCapable).toBe(true);
|
||||
expect(multilevel.usesQuantum).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,11 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { priorityNp, priorityP } from "../algorithms/priority";
|
||||
import { PriorityNonPreemptiveAlgorithm, PriorityPreemptiveAlgorithm } from "../algorithms/priority";
|
||||
import { SimulationEngine } from "../engine";
|
||||
import type { Process, ProcessRuntime, SchedulerConfig } from "../types";
|
||||
|
||||
const priorityNp = new PriorityNonPreemptiveAlgorithm();
|
||||
const priorityP = new PriorityPreemptiveAlgorithm();
|
||||
|
||||
function makeRt(id: string, priority: number, arrivalTick = 0): ProcessRuntime {
|
||||
return {
|
||||
processId: id, state: "Ready", remainingBurst: 4, burstIndex: 0,
|
||||
@@ -16,10 +19,6 @@ const cfgNp: SchedulerConfig = {
|
||||
algorithm: "PRIORITY_NP", quantum: 2, contextSwitchTime: 0,
|
||||
isPreemptive: false, agingEnabled: false, agingInterval: 5,
|
||||
};
|
||||
const cfgP: SchedulerConfig = {
|
||||
algorithm: "PRIORITY_P", quantum: 2, contextSwitchTime: 0,
|
||||
isPreemptive: true, agingEnabled: false, agingInterval: 5,
|
||||
};
|
||||
|
||||
const p = (id: string, arrival: number, priority: number, cpuDuration: number): Process => ({
|
||||
id, name: id, arrivalTime: arrival, priority, color: "#fff",
|
||||
@@ -31,19 +30,17 @@ describe("PRIORITY_NP select()", () => {
|
||||
const r1 = makeRt("p1", 5);
|
||||
const r2 = makeRt("p2", 2);
|
||||
const r3 = makeRt("p3", 8);
|
||||
expect(priorityNp.select([r1, r2, r3], cfgNp)?.processId).toBe("p2");
|
||||
expect(priorityNp.select([r1, r2, r3])?.processId).toBe("p2");
|
||||
});
|
||||
|
||||
it("tiebreaks by arrival then id", () => {
|
||||
const r1 = makeRt("p2", 3, 2);
|
||||
const r2 = makeRt("p1", 3, 5); // p1 arrives later
|
||||
expect(priorityNp.select([r1, r2], cfgNp)?.processId).toBe("p2");
|
||||
expect(priorityNp.select([r1, r2])?.processId).toBe("p2");
|
||||
});
|
||||
|
||||
it("shouldPreempt always false", () => {
|
||||
const a = makeRt("p1", 5);
|
||||
const b = makeRt("p2", 1);
|
||||
expect(priorityNp.shouldPreempt(a, b, cfgNp)).toBe(false);
|
||||
expect(priorityNp.shouldPreempt()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,19 +48,19 @@ describe("PRIORITY_P shouldPreempt()", () => {
|
||||
it("preempts when candidate has higher priority (lower number)", () => {
|
||||
const current = makeRt("p1", 5);
|
||||
const candidate = makeRt("p2", 2);
|
||||
expect(priorityP.shouldPreempt(current, candidate, cfgP)).toBe(true);
|
||||
expect(priorityP.shouldPreempt(current, candidate)).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT preempt on equal priority", () => {
|
||||
const current = makeRt("p1", 3);
|
||||
const candidate = makeRt("p2", 3);
|
||||
expect(priorityP.shouldPreempt(current, candidate, cfgP)).toBe(false);
|
||||
expect(priorityP.shouldPreempt(current, candidate)).toBe(false);
|
||||
});
|
||||
|
||||
it("does NOT preempt when current has higher priority", () => {
|
||||
const current = makeRt("p1", 2);
|
||||
const candidate = makeRt("p2", 5);
|
||||
expect(priorityP.shouldPreempt(current, candidate, cfgP)).toBe(false);
|
||||
expect(priorityP.shouldPreempt(current, candidate)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,3 +113,14 @@ describe("Priority P + aging", () => {
|
||||
expect(rt.currentPriority).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Priority — metadados", () => {
|
||||
it("PriorityNonPreemptiveAlgorithm: isPreemptiveCapable=false", () => {
|
||||
expect(priorityNp.isPreemptiveCapable).toBe(false);
|
||||
expect(priorityNp.usesQuantum).toBe(false);
|
||||
});
|
||||
it("PriorityPreemptiveAlgorithm: isPreemptiveCapable=true", () => {
|
||||
expect(priorityP.isPreemptiveCapable).toBe(true);
|
||||
expect(priorityP.usesQuantum).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,10 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { priorityRr } from "../algorithms/priority_rr";
|
||||
import { PriorityRoundRobinAlgorithm } from "../algorithms/priority_rr";
|
||||
import { SimulationEngine } from "../engine";
|
||||
import type { Process, ProcessRuntime, SchedulerConfig } from "../types";
|
||||
|
||||
const priorityRr = new PriorityRoundRobinAlgorithm();
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Helpers
|
||||
// ----------------------------------------------------------------
|
||||
@@ -58,22 +60,22 @@ const p = (
|
||||
|
||||
describe("PRIORITY_RR select()", () => {
|
||||
it("returns null for empty queue", () => {
|
||||
expect(priorityRr.select([], cfg())).toBeNull();
|
||||
expect(priorityRr.select([])).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");
|
||||
expect(priorityRr.select([low, high])?.processId).toBe("high");
|
||||
expect(priorityRr.select([high, low])?.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");
|
||||
expect(priorityRr.select([a, b, c])?.processId).toBe("a");
|
||||
expect(priorityRr.select([b, c, a])?.processId).toBe("b");
|
||||
});
|
||||
|
||||
it("ignores lower-priority processes when a higher-priority one is present", () => {
|
||||
@@ -81,7 +83,7 @@ describe("PRIORITY_RR select()", () => {
|
||||
const normal1 = makeRt("n1", 5);
|
||||
const normal2 = makeRt("n2", 5);
|
||||
expect(
|
||||
priorityRr.select([normal1, normal2, critical], cfg())?.processId,
|
||||
priorityRr.select([normal1, normal2, critical])?.processId,
|
||||
).toBe("critical");
|
||||
});
|
||||
});
|
||||
@@ -94,19 +96,19 @@ 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);
|
||||
expect(priorityRr.shouldPreempt(running, higher)).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);
|
||||
expect(priorityRr.shouldPreempt(a, b)).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);
|
||||
expect(priorityRr.shouldPreempt(running, lower)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,13 +118,13 @@ describe("PRIORITY_RR shouldPreempt()", () => {
|
||||
|
||||
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);
|
||||
expect(priorityRr.isQuantumExpired(makeRt("p", 5, 0))).toBe(true);
|
||||
expect(priorityRr.isQuantumExpired(makeRt("p", 5, -1))).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);
|
||||
expect(priorityRr.isQuantumExpired(makeRt("p", 5, 1))).toBe(false);
|
||||
expect(priorityRr.isQuantumExpired(makeRt("p", 5, 2))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -246,3 +248,11 @@ describe("PRIORITY_RR integration — combined preemption + RR", () => {
|
||||
expect(rts.find((r) => r.processId === "p3")!.cpuTime).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PriorityRoundRobinAlgorithm — metadados", () => {
|
||||
it("expõe propriedades educacionais corretas", () => {
|
||||
expect(priorityRr.name).toContain("Priority Round Robin");
|
||||
expect(priorityRr.isPreemptiveCapable).toBe(true);
|
||||
expect(priorityRr.usesQuantum).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,10 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { rr } from "../algorithms/rr";
|
||||
import { RoundRobinAlgorithm } from "../algorithms/rr";
|
||||
import { SimulationEngine } from "../engine";
|
||||
import type { Process, ProcessRuntime, SchedulerConfig } from "../types";
|
||||
|
||||
const rr = new RoundRobinAlgorithm();
|
||||
|
||||
function makeRt(id: string, quantumRemaining = 2): ProcessRuntime {
|
||||
return {
|
||||
processId: id, state: "Running", remainingBurst: 4, burstIndex: 0,
|
||||
@@ -26,32 +28,30 @@ describe("RR select()", () => {
|
||||
it("returns front of queue (FIFO)", () => {
|
||||
const r1: ProcessRuntime = { ...makeRt("p1"), state: "Ready" };
|
||||
const r2: ProcessRuntime = { ...makeRt("p2"), state: "Ready" };
|
||||
expect(rr.select([r1, r2], config)?.processId).toBe("p1");
|
||||
expect(rr.select([r2, r1], config)?.processId).toBe("p2");
|
||||
expect(rr.select([r1, r2])?.processId).toBe("p1");
|
||||
expect(rr.select([r2, r1])?.processId).toBe("p2");
|
||||
});
|
||||
|
||||
it("returns null for empty queue", () => {
|
||||
expect(rr.select([], config)).toBeNull();
|
||||
expect(rr.select([])).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("RR isQuantumExpired()", () => {
|
||||
it("returns true when quantumRemaining <= 0", () => {
|
||||
expect(rr.isQuantumExpired(makeRt("p1", 0), config)).toBe(true);
|
||||
expect(rr.isQuantumExpired(makeRt("p1", -1), config)).toBe(true);
|
||||
expect(rr.isQuantumExpired(makeRt("p1", 0))).toBe(true);
|
||||
expect(rr.isQuantumExpired(makeRt("p1", -1))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when quantum not yet exhausted", () => {
|
||||
expect(rr.isQuantumExpired(makeRt("p1", 1), config)).toBe(false);
|
||||
expect(rr.isQuantumExpired(makeRt("p1", 2), config)).toBe(false);
|
||||
expect(rr.isQuantumExpired(makeRt("p1", 1))).toBe(false);
|
||||
expect(rr.isQuantumExpired(makeRt("p1", 2))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("RR shouldPreempt()", () => {
|
||||
it("always returns false (preemption via quantum expiry only)", () => {
|
||||
const a = makeRt("p1");
|
||||
const b = makeRt("p2");
|
||||
expect(rr.shouldPreempt(a, b, config)).toBe(false);
|
||||
expect(rr.shouldPreempt()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -114,3 +114,11 @@ describe("CT-02: Round Robin integration", () => {
|
||||
void ticks; // used for debugging if needed
|
||||
});
|
||||
});
|
||||
|
||||
describe("RoundRobinAlgorithm — metadados", () => {
|
||||
it("expõe propriedades educacionais corretas", () => {
|
||||
expect(rr.name).toContain("Round Robin");
|
||||
expect(rr.isPreemptiveCapable).toBe(false);
|
||||
expect(rr.usesQuantum).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,13 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { sjfNp, srtf } from "../algorithms/sjf";
|
||||
import { SJFNonPreemptiveAlgorithm, SRTFAlgorithm } from "../algorithms/sjf";
|
||||
import { SimulationEngine } from "../engine";
|
||||
import type { Process, ProcessRuntime, SchedulerConfig } from "../types";
|
||||
|
||||
const sjfNp = new SJFNonPreemptiveAlgorithm();
|
||||
const srtf = new SRTFAlgorithm();
|
||||
|
||||
|
||||
|
||||
function makeRt(id: string, remainingBurst: number, arrivalTick = 0): ProcessRuntime {
|
||||
return {
|
||||
processId: id, state: "Ready", remainingBurst, burstIndex: 0,
|
||||
@@ -23,27 +28,25 @@ const p = (id: string, arrival: number, bursts: { type: "cpu" | "io"; duration:
|
||||
|
||||
describe("SJF_NP select()", () => {
|
||||
it("returns null for empty queue", () => {
|
||||
expect(sjfNp.select([], config)).toBeNull();
|
||||
expect(sjfNp.select([])).toBeNull();
|
||||
});
|
||||
|
||||
it("picks shortest remaining burst", () => {
|
||||
const r1 = makeRt("p1", 8);
|
||||
const r2 = makeRt("p2", 3);
|
||||
const r3 = makeRt("p3", 5);
|
||||
expect(sjfNp.select([r1, r2, r3], config)?.processId).toBe("p2");
|
||||
expect(sjfNp.select([r1, r2, r3])?.processId).toBe("p2");
|
||||
});
|
||||
|
||||
it("tiebreaks equal burst by arrival then id", () => {
|
||||
const r1 = makeRt("p2", 4, 1);
|
||||
const r2 = makeRt("p1", 4, 2);
|
||||
// p2 arrives earlier
|
||||
expect(sjfNp.select([r1, r2], config)?.processId).toBe("p2");
|
||||
expect(sjfNp.select([r1, r2])?.processId).toBe("p2");
|
||||
});
|
||||
|
||||
it("shouldPreempt always false", () => {
|
||||
const a = makeRt("p1", 4);
|
||||
const b = makeRt("p2", 2);
|
||||
expect(sjfNp.shouldPreempt(a, b, config)).toBe(false);
|
||||
expect(sjfNp.shouldPreempt()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,19 +54,19 @@ describe("SRTF shouldPreempt()", () => {
|
||||
it("preempts when candidate has strictly smaller burst", () => {
|
||||
const current = makeRt("p1", 7);
|
||||
const candidate = makeRt("p2", 4);
|
||||
expect(srtf.shouldPreempt(current, candidate, config)).toBe(true);
|
||||
expect(srtf.shouldPreempt(current, candidate)).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT preempt on equal remaining burst", () => {
|
||||
const current = makeRt("p1", 4);
|
||||
const candidate = makeRt("p2", 4);
|
||||
expect(srtf.shouldPreempt(current, candidate, config)).toBe(false);
|
||||
expect(srtf.shouldPreempt(current, candidate)).toBe(false);
|
||||
});
|
||||
|
||||
it("does NOT preempt when current has smaller burst", () => {
|
||||
const current = makeRt("p1", 3);
|
||||
const candidate = makeRt("p2", 6);
|
||||
expect(srtf.shouldPreempt(current, candidate, config)).toBe(false);
|
||||
expect(srtf.shouldPreempt(current, candidate)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,3 +95,16 @@ describe("CT-04: SRTF Preemption (integration)", () => {
|
||||
expect(rt1.finishTick).toBe(12);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SJF/SRTF — metadados", () => {
|
||||
it("SJFNonPreemptiveAlgorithm: isPreemptiveCapable=false, usesQuantum=false", () => {
|
||||
expect(sjfNp.isPreemptiveCapable).toBe(false);
|
||||
expect(sjfNp.usesQuantum).toBe(false);
|
||||
expect(sjfNp.name).toContain("Shortest Job First");
|
||||
});
|
||||
it("SRTFAlgorithm: isPreemptiveCapable=true, usesQuantum=false", () => {
|
||||
expect(srtf.isPreemptiveCapable).toBe(true);
|
||||
expect(srtf.usesQuantum).toBe(false);
|
||||
expect(srtf.name).toContain("SRTF");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { ProcessRuntime, SchedulerConfig } from "../types";
|
||||
import { tiebreak as _tiebreak } from "./utils";
|
||||
|
||||
/**
|
||||
* BaseAlgorithm — Classe abstrata para algoritmos de escalonamento.
|
||||
*
|
||||
* Cada algoritmo concreto deve implementar:
|
||||
* - Propriedades educacionais (name, description, isPreemptiveCapable, usesQuantum)
|
||||
* - Os três métodos do contrato de escalonamento (select, shouldPreempt, isQuantumExpired)
|
||||
*
|
||||
* O método estático `tiebreak` é disponibilizado a todas as subclasses como
|
||||
* critério de desempate padrão: chegada mais cedo → ID lexicográfico menor.
|
||||
*/
|
||||
export abstract class BaseAlgorithm {
|
||||
/** Nome completo do algoritmo para exibição. */
|
||||
abstract readonly name: string;
|
||||
|
||||
/** Breve descrição educacional do comportamento do algoritmo. */
|
||||
abstract readonly description: string;
|
||||
|
||||
/** Indica se o algoritmo suporta preempção por candidato mais prioritário. */
|
||||
abstract readonly isPreemptiveCapable: boolean;
|
||||
|
||||
/** Indica se o algoritmo usa fatia de tempo (quantum). */
|
||||
abstract readonly usesQuantum: boolean;
|
||||
|
||||
// ─── Contrato de escalonamento ───────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Seleciona o próximo processo a executar a partir da fila de prontos.
|
||||
* Não deve modificar o array — o engine remove o processo selecionado.
|
||||
* Retorna null se a fila estiver vazia.
|
||||
*/
|
||||
abstract select(
|
||||
readyQueue: readonly ProcessRuntime[],
|
||||
config: SchedulerConfig,
|
||||
): ProcessRuntime | null;
|
||||
|
||||
/**
|
||||
* Para algoritmos preemptivos: indica se o processo em execução deve ser
|
||||
* interrompido em favor do candidato selecionado.
|
||||
* Chamado apenas quando há um processo rodando e um candidato disponível.
|
||||
*/
|
||||
abstract shouldPreempt(
|
||||
current: ProcessRuntime,
|
||||
candidate: ProcessRuntime,
|
||||
config: SchedulerConfig,
|
||||
): boolean;
|
||||
|
||||
/**
|
||||
* Para Round Robin: indica se o quantum do processo expirou.
|
||||
* Todos os outros algoritmos devem retornar false.
|
||||
*/
|
||||
abstract isQuantumExpired(
|
||||
runtime: ProcessRuntime,
|
||||
config: SchedulerConfig,
|
||||
): boolean;
|
||||
|
||||
// ─── Helper compartilhado ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Critério de desempate padrão: chegada mais cedo (arrivalTick ASC),
|
||||
* depois ID do processo em ordem lexicográfica.
|
||||
* Disponível a todas as subclasses como método estático herdado.
|
||||
*/
|
||||
public static tiebreak(a: ProcessRuntime, b: ProcessRuntime): number {
|
||||
return _tiebreak(a, b);
|
||||
}
|
||||
}
|
||||
+24
-13
@@ -1,21 +1,32 @@
|
||||
import type { SchedulerAlgorithm } from "./index";
|
||||
import { tiebreak } from "./utils";
|
||||
import type { ProcessRuntime } from "../types";
|
||||
import { BaseAlgorithm } from "./base";
|
||||
|
||||
/**
|
||||
* FCFS — First Come, First Served
|
||||
* Non-preemptive. Selects the process with earliest arrival; tiebreak by id.
|
||||
* FCFS — First Come, First Served (Primeiro a Chegar, Primeiro a Ser Servido)
|
||||
*
|
||||
* Algoritmo não-preemptivo mais simples: processa os jobs na ordem exata
|
||||
* de chegada. Fácil de implementar, mas pode causar o "efeito comboio"
|
||||
* (convoy effect), onde processos curtos ficam presos atrás de um longo.
|
||||
*/
|
||||
export const fcfs: SchedulerAlgorithm = {
|
||||
select(readyQueue) {
|
||||
export class FCFSAlgorithm extends BaseAlgorithm {
|
||||
readonly name = "First Come, First Served (FCFS)";
|
||||
readonly description =
|
||||
"Não-preemptivo. Despacha processos na ordem de chegada. " +
|
||||
"Simples, mas pode causar o efeito comboio: processos curtos " +
|
||||
"aguardam atrás de um processo longo.";
|
||||
readonly isPreemptiveCapable = false;
|
||||
readonly usesQuantum = false;
|
||||
|
||||
select(readyQueue: readonly ProcessRuntime[]): ProcessRuntime | null {
|
||||
if (readyQueue.length === 0) return null;
|
||||
return [...readyQueue].sort(tiebreak)[0] ?? null;
|
||||
},
|
||||
return [...readyQueue].sort(FCFSAlgorithm.tiebreak)[0] ?? null;
|
||||
}
|
||||
|
||||
shouldPreempt() {
|
||||
shouldPreempt(): boolean {
|
||||
return false;
|
||||
},
|
||||
}
|
||||
|
||||
isQuantumExpired() {
|
||||
isQuantumExpired(): boolean {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+39
-20
@@ -1,33 +1,52 @@
|
||||
import type { ProcessRuntime } from "../types";
|
||||
import type { SchedulerAlgorithm } from "./index";
|
||||
import { tiebreak } from "./utils";
|
||||
|
||||
function hrrnRatio(rt: ProcessRuntime): number {
|
||||
// ratio = (waitingTime + burstTime) / burstTime
|
||||
// remainingBurst > 0 is guaranteed for processes in the ready queue
|
||||
if (rt.remainingBurst <= 0) return Infinity;
|
||||
return (rt.waitingTime + rt.remainingBurst) / rt.remainingBurst;
|
||||
}
|
||||
import { BaseAlgorithm } from "./base";
|
||||
|
||||
/**
|
||||
* HRRN — Highest Response Ratio Next
|
||||
* Non-preemptive. Balances waiting time and burst length to prevent starvation.
|
||||
*
|
||||
* Algoritmo não-preemptivo que equilibra o tempo de burst e o tempo de espera
|
||||
* para evitar inanição. A cada decisão de escalonamento, calcula a razão de
|
||||
* resposta de cada processo na fila e despacha o de maior razão.
|
||||
*
|
||||
* Fórmula: ratio = (waitingTime + remainingBurst) / remainingBurst
|
||||
*
|
||||
* - Processos longos que esperam muito aumentam sua razão, evitando inanição.
|
||||
* - Processos curtos naturalmente têm razão alta (denominador pequeno).
|
||||
* - Oferece um balanço entre SJF (favorece curtos) e FCFS (favorece chegada).
|
||||
*/
|
||||
export const hrrn: SchedulerAlgorithm = {
|
||||
select(readyQueue) {
|
||||
export class HRRNAlgorithm extends BaseAlgorithm {
|
||||
readonly name = "Highest Response Ratio Next (HRRN)";
|
||||
readonly description =
|
||||
"Não-preemptivo. Despacha o processo com maior razão de resposta: " +
|
||||
"(espera + burst) / burst. Equilibra duração e tempo de espera, " +
|
||||
"prevenindo inanição sem precisar de aging explícito.";
|
||||
readonly isPreemptiveCapable = false;
|
||||
readonly usesQuantum = false;
|
||||
|
||||
/**
|
||||
* Calcula a razão de resposta de um processo.
|
||||
* Retorna Infinity se remainingBurst <= 0 (não deve ocorrer na fila de prontos,
|
||||
* mas garante ordenação segura em casos de borda).
|
||||
*/
|
||||
private static hrrnRatio(rt: ProcessRuntime): number {
|
||||
if (rt.remainingBurst <= 0) return Infinity;
|
||||
return (rt.waitingTime + rt.remainingBurst) / rt.remainingBurst;
|
||||
}
|
||||
|
||||
select(readyQueue: readonly ProcessRuntime[]): ProcessRuntime | null {
|
||||
if (readyQueue.length === 0) return null;
|
||||
return [...readyQueue].sort((a, b) => {
|
||||
const ratioDiff = hrrnRatio(b) - hrrnRatio(a); // descending
|
||||
const ratioDiff = HRRNAlgorithm.hrrnRatio(b) - HRRNAlgorithm.hrrnRatio(a); // descendente
|
||||
if (ratioDiff !== 0) return ratioDiff;
|
||||
return tiebreak(a, b);
|
||||
return HRRNAlgorithm.tiebreak(a, b);
|
||||
})[0] ?? null;
|
||||
},
|
||||
}
|
||||
|
||||
shouldPreempt() {
|
||||
shouldPreempt(): boolean {
|
||||
return false;
|
||||
},
|
||||
}
|
||||
|
||||
isQuantumExpired() {
|
||||
isQuantumExpired(): boolean {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,61 +1,31 @@
|
||||
import type { ProcessRuntime, SchedulerConfig, SchedulingAlgorithm } from "../types";
|
||||
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";
|
||||
import type { SchedulingAlgorithm } from "../types";
|
||||
import { BaseAlgorithm } from "./base";
|
||||
import { FCFSAlgorithm } from "./fcfs";
|
||||
import { SJFNonPreemptiveAlgorithm, SRTFAlgorithm } from "./sjf";
|
||||
import { RoundRobinAlgorithm } from "./rr";
|
||||
import { PriorityNonPreemptiveAlgorithm, PriorityPreemptiveAlgorithm } from "./priority";
|
||||
import { PriorityRoundRobinAlgorithm } from "./priority_rr";
|
||||
import { HRRNAlgorithm } from "./hrrn";
|
||||
import { MultilevelQueueAlgorithm } from "./multilevel";
|
||||
|
||||
export type { BaseAlgorithm };
|
||||
|
||||
// ============================================================
|
||||
// SchedulerAlgorithm interface
|
||||
// All implementations must be pure (no mutation of the queue).
|
||||
// Tabela de despacho — o engine busca o algoritmo pelo nome
|
||||
// ============================================================
|
||||
//
|
||||
// As instâncias são criadas uma única vez no carregamento do módulo.
|
||||
// Isso é seguro pois todas as classes são stateless: todo estado mutável
|
||||
// da simulação vive em ProcessRuntime, gerenciado pelo SimulationEngine.
|
||||
|
||||
export interface SchedulerAlgorithm {
|
||||
/**
|
||||
* Select the best process to dispatch from the ready queue.
|
||||
* Returns null if the queue is empty.
|
||||
* Must NOT mutate the array; the engine handles removal.
|
||||
*/
|
||||
select(
|
||||
readyQueue: readonly ProcessRuntime[],
|
||||
config: SchedulerConfig,
|
||||
): ProcessRuntime | null;
|
||||
|
||||
/**
|
||||
* For preemptive algorithms: should the currently-running process
|
||||
* be replaced by `candidate`?
|
||||
* Called only when cpuProcess != null and a candidate exists.
|
||||
*/
|
||||
shouldPreempt(
|
||||
current: ProcessRuntime,
|
||||
candidate: ProcessRuntime,
|
||||
config: SchedulerConfig,
|
||||
): boolean;
|
||||
|
||||
/**
|
||||
* RR only: has the process exhausted its quantum?
|
||||
* For all other algorithms this must return false.
|
||||
*/
|
||||
isQuantumExpired(
|
||||
runtime: ProcessRuntime,
|
||||
config: SchedulerConfig,
|
||||
): boolean;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Dispatch table — engine looks up algorithm by name
|
||||
// ============================================================
|
||||
|
||||
export const algorithms: Record<SchedulingAlgorithm, SchedulerAlgorithm> = {
|
||||
FCFS: fcfs,
|
||||
SJF_NP: sjfNp,
|
||||
SJF_P: srtf,
|
||||
RR: rr,
|
||||
PRIORITY_NP: priorityNp,
|
||||
PRIORITY_P: priorityP,
|
||||
PRIORITY_RR: priorityRr,
|
||||
HRRN: hrrn,
|
||||
MULTILEVEL: multilevel,
|
||||
export const algorithms: Record<SchedulingAlgorithm, BaseAlgorithm> = {
|
||||
FCFS: new FCFSAlgorithm(),
|
||||
SJF_NP: new SJFNonPreemptiveAlgorithm(),
|
||||
SJF_P: new SRTFAlgorithm(),
|
||||
RR: new RoundRobinAlgorithm(),
|
||||
PRIORITY_NP: new PriorityNonPreemptiveAlgorithm(),
|
||||
PRIORITY_P: new PriorityPreemptiveAlgorithm(),
|
||||
PRIORITY_RR: new PriorityRoundRobinAlgorithm(),
|
||||
HRRN: new HRRNAlgorithm(),
|
||||
MULTILEVEL: new MultilevelQueueAlgorithm(),
|
||||
};
|
||||
@@ -1,109 +1,132 @@
|
||||
import type { MlqQueueDef, ProcessRuntime } from "../types";
|
||||
import type { SchedulerAlgorithm } from "./index";
|
||||
import { tiebreak } from "./utils";
|
||||
import type { MlqQueueDef, ProcessRuntime, SchedulerConfig } from "../types";
|
||||
import { BaseAlgorithm } from "./base";
|
||||
|
||||
/**
|
||||
* MULTILEVEL — Multilevel Queue Scheduling
|
||||
* MLQ — Multilevel Queue (Fila Multinível)
|
||||
*
|
||||
* 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).
|
||||
* Processos são atribuídos permanentemente a filas com base em sua prioridade.
|
||||
* Cada fila tem seu próprio algoritmo interno (FCFS, RR ou 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.
|
||||
* Comportamento entre filas:
|
||||
* - O escalonador sempre despacha da fila de maior prioridade não-vazia (índice 0).
|
||||
* - Um processo de fila superior sempre preempta um processo de fila inferior.
|
||||
* - Nenhum processo de fila inferior executa enquanto houver processos em fila superior.
|
||||
*
|
||||
* 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.
|
||||
* Comportamento dentro de cada fila:
|
||||
* - FCFS: despacha na ordem de chegada; sem preempção interna.
|
||||
* - RR: rodízio usando o quantum da fila; sem preempção interna.
|
||||
* - PRIORITY_NP: despacha o de maior prioridade; sem preempção interna.
|
||||
*
|
||||
* 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.
|
||||
* A configuração das filas vem de `config.mlqQueues`; se ausente, usa as
|
||||
* filas padrão definidas em `DEFAULT_QUEUES`.
|
||||
*/
|
||||
export class MultilevelQueueAlgorithm extends BaseAlgorithm {
|
||||
readonly name = "Multilevel Queue (MLQ)";
|
||||
readonly description =
|
||||
"Filas de prioridade fixas, cada uma com seu algoritmo interno (FCFS, RR, PRIORITY_NP). " +
|
||||
"A fila de maior prioridade sempre executa primeiro. Processos são atribuídos " +
|
||||
"permanentemente a uma fila conforme sua prioridade.";
|
||||
readonly isPreemptiveCapable = true;
|
||||
readonly usesQuantum = true; // verdadeiro porque a fila RR usa quantum
|
||||
|
||||
/** 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;
|
||||
}
|
||||
/** Configuração padrão: 3 filas cobrindo prioridades 1–10. */
|
||||
private static readonly 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 },
|
||||
];
|
||||
|
||||
/** 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;
|
||||
/**
|
||||
* Retorna o índice da fila à qual pertence o valor de prioridade informado.
|
||||
* Se a prioridade não se enquadrar em nenhuma fila definida, usa a última.
|
||||
*/
|
||||
private static 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;
|
||||
}
|
||||
return queues.length - 1;
|
||||
}
|
||||
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 },
|
||||
];
|
||||
/**
|
||||
* Seleciona o melhor candidato dentro de uma sub-fila usando o algoritmo
|
||||
* definido para ela (RR, FCFS ou PRIORITY_NP).
|
||||
*/
|
||||
private static selectWithinQueue(
|
||||
candidates: readonly ProcessRuntime[],
|
||||
queueDef: MlqQueueDef,
|
||||
): ProcessRuntime | null {
|
||||
if (candidates.length === 0) return null;
|
||||
|
||||
export const multilevel: SchedulerAlgorithm = {
|
||||
select(readyQueue, config): ProcessRuntime | null {
|
||||
const queues = config.mlqQueues ?? DEFAULT_QUEUES;
|
||||
if (queueDef.algorithm === "RR") {
|
||||
// RR: despacha na ordem de inserção (engine mantém FIFO).
|
||||
return candidates[0]!;
|
||||
}
|
||||
|
||||
if (queueDef.algorithm === "FCFS") {
|
||||
// FCFS: despacha o de chegada mais antiga (arrivalTick, depois processId).
|
||||
return candidates.reduce((best, rt) =>
|
||||
BaseAlgorithm.tiebreak(rt, best) < 0 ? rt : best,
|
||||
);
|
||||
}
|
||||
|
||||
// PRIORITY_NP: despacha o de maior prioridade (menor número).
|
||||
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 &&
|
||||
BaseAlgorithm.tiebreak(rt, best) < 0)
|
||||
) {
|
||||
best = rt;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
select(
|
||||
readyQueue: readonly ProcessRuntime[],
|
||||
config: SchedulerConfig,
|
||||
): ProcessRuntime | null {
|
||||
const queues = config.mlqQueues ?? MultilevelQueueAlgorithm.DEFAULT_QUEUES;
|
||||
if (queues.length === 0) return null;
|
||||
|
||||
// Iterate from highest-priority queue (index 0) to lowest.
|
||||
// Itera da fila de maior prioridade (índice 0) para a de menor.
|
||||
for (let qi = 0; qi < queues.length; qi++) {
|
||||
const queueDef = queues[qi]!;
|
||||
const candidates = readyQueue.filter(
|
||||
(rt) => getQueueIndex(rt.currentPriority, queues) === qi,
|
||||
(rt) => MultilevelQueueAlgorithm.getQueueIndex(rt.currentPriority, queues) === qi,
|
||||
);
|
||||
const selected = selectWithinQueue(candidates, queueDef);
|
||||
const selected = MultilevelQueueAlgorithm.selectWithinQueue(candidates, queueDef);
|
||||
if (selected !== null) return selected;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
}
|
||||
|
||||
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.
|
||||
shouldPreempt(
|
||||
current: ProcessRuntime,
|
||||
candidate: ProcessRuntime,
|
||||
config: SchedulerConfig,
|
||||
): boolean {
|
||||
const queues = config.mlqQueues ?? MultilevelQueueAlgorithm.DEFAULT_QUEUES;
|
||||
const currentQueueIdx = MultilevelQueueAlgorithm.getQueueIndex(current.currentPriority, queues);
|
||||
const candidateQueueIdx = MultilevelQueueAlgorithm.getQueueIndex(candidate.currentPriority, queues);
|
||||
// Preempta apenas se o candidato pertence a uma fila de prioridade estritamente maior.
|
||||
return candidateQueueIdx < currentQueueIdx;
|
||||
},
|
||||
}
|
||||
|
||||
isQuantumExpired(runtime, config): boolean {
|
||||
const queues = config.mlqQueues ?? DEFAULT_QUEUES;
|
||||
const queueIdx = getQueueIndex(runtime.currentPriority, queues);
|
||||
isQuantumExpired(
|
||||
runtime: ProcessRuntime,
|
||||
config: SchedulerConfig,
|
||||
): boolean {
|
||||
const queues = config.mlqQueues ?? MultilevelQueueAlgorithm.DEFAULT_QUEUES;
|
||||
const queueIdx = MultilevelQueueAlgorithm.getQueueIndex(runtime.currentPriority, queues);
|
||||
const queueDef = queues[queueIdx];
|
||||
if (!queueDef) return false;
|
||||
return queueDef.algorithm === "RR" && runtime.quantumRemaining <= 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,49 +1,79 @@
|
||||
import type { ProcessRuntime } from "../types";
|
||||
import type { SchedulerAlgorithm } from "./index";
|
||||
import { tiebreak } from "./utils";
|
||||
import { BaseAlgorithm } from "./base";
|
||||
|
||||
/**
|
||||
* Comparador interno compartilhado pelas duas variantes de Prioridade.
|
||||
* Menor número = maior prioridade. Usa tiebreak padrão em caso de empate.
|
||||
*/
|
||||
function prioritySort(a: ProcessRuntime, b: ProcessRuntime): number {
|
||||
// Lower number = higher priority
|
||||
if (a.currentPriority !== b.currentPriority)
|
||||
return a.currentPriority - b.currentPriority;
|
||||
return tiebreak(a, b);
|
||||
return BaseAlgorithm.tiebreak(a, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Priority Non-Preemptive
|
||||
* Selects process with lowest priority number; does not interrupt running process.
|
||||
*/
|
||||
export const priorityNp: SchedulerAlgorithm = {
|
||||
select(readyQueue) {
|
||||
if (readyQueue.length === 0) return null;
|
||||
return [...readyQueue].sort(prioritySort)[0] ?? null;
|
||||
},
|
||||
|
||||
shouldPreempt() {
|
||||
return false;
|
||||
},
|
||||
|
||||
isQuantumExpired() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Priority Preemptive
|
||||
* Preempts running process if a higher-priority (lower number) process arrives.
|
||||
* Aging is applied by the engine — currentPriority decrements over time.
|
||||
* Priority Não-Preemptivo
|
||||
*
|
||||
* Seleciona o processo com maior prioridade (menor número). Uma vez que
|
||||
* um processo ocupa a CPU, ele roda até terminar — novos processos de alta
|
||||
* prioridade precisam esperar. Pode causar inanição (starvation) de processos
|
||||
* de baixa prioridade. O mecanismo de aging, quando ativado, incrementa
|
||||
* gradualmente a prioridade de processos em espera.
|
||||
*/
|
||||
export const priorityP: SchedulerAlgorithm = {
|
||||
select(readyQueue) {
|
||||
export class PriorityNonPreemptiveAlgorithm extends BaseAlgorithm {
|
||||
readonly name = "Escalonamento por Prioridade — Não-Preemptivo";
|
||||
readonly description =
|
||||
"Despacha o processo de maior prioridade (menor número). Não-preemptivo: " +
|
||||
"o processo atual não é interrompido por novos de alta prioridade. " +
|
||||
"Pode causar inanição; use aging para mitigar.";
|
||||
readonly isPreemptiveCapable = false;
|
||||
readonly usesQuantum = false;
|
||||
|
||||
select(readyQueue: readonly ProcessRuntime[]): ProcessRuntime | null {
|
||||
if (readyQueue.length === 0) return null;
|
||||
return [...readyQueue].sort(prioritySort)[0] ?? null;
|
||||
},
|
||||
}
|
||||
|
||||
shouldPreempt(current, candidate) {
|
||||
shouldPreempt(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isQuantumExpired(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Priority Preemptivo
|
||||
*
|
||||
* Variante preemptiva: o processo em execução é interrompido imediatamente
|
||||
* ao chegar um processo com prioridade estritamente maior (número menor).
|
||||
* Prioridade igual não causa preempção (evita context switches desnecessários).
|
||||
* O aging, quando ativado no engine, decrementa `currentPriority` a cada
|
||||
* intervalo, prevenindo inanição de processos de baixa prioridade.
|
||||
*/
|
||||
export class PriorityPreemptiveAlgorithm extends BaseAlgorithm {
|
||||
readonly name = "Escalonamento por Prioridade — Preemptivo";
|
||||
readonly description =
|
||||
"Preempta o processo em execução quando um de maior prioridade fica pronto. " +
|
||||
"Prioridade igual não gera preempção. Suporta aging para evitar inanição.";
|
||||
readonly isPreemptiveCapable = true;
|
||||
readonly usesQuantum = false;
|
||||
|
||||
select(readyQueue: readonly ProcessRuntime[]): ProcessRuntime | null {
|
||||
if (readyQueue.length === 0) return null;
|
||||
return [...readyQueue].sort(prioritySort)[0] ?? null;
|
||||
}
|
||||
|
||||
shouldPreempt(current: ProcessRuntime, candidate: ProcessRuntime): boolean {
|
||||
return candidate.currentPriority < current.currentPriority;
|
||||
},
|
||||
}
|
||||
|
||||
isQuantumExpired() {
|
||||
isQuantumExpired(): boolean {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,49 +1,59 @@
|
||||
import type { SchedulerAlgorithm } from "./index";
|
||||
import type { ProcessRuntime } from "../types";
|
||||
import { BaseAlgorithm } from "./base";
|
||||
|
||||
/**
|
||||
* Priority Round Robin (PRIORITY_RR)
|
||||
*
|
||||
* Mimics real-OS scheduling (Windows priority bands, POSIX SCHED_RR):
|
||||
* Combina escalonamento por prioridade com rodízio dentro de cada banda:
|
||||
*
|
||||
* - 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.
|
||||
* - A fila de prontos é dividida conceitualmente em bandas de prioridade.
|
||||
* - O escalonador sempre despacha da banda de maior prioridade (menor número),
|
||||
* mantendo processos de menor prioridade em espera.
|
||||
* - Dentro de uma mesma banda, os processos compartilham a CPU via round-robin
|
||||
* com o quantum configurado — garantindo justiça entre iguais.
|
||||
* - Preempção ocorre imediatamente ao chegar um processo de prioridade
|
||||
* estritamente maior; prioridade igual é tratada pelo quantum.
|
||||
* - Compatível com aging: ao diminuir `currentPriority`, processos de baixa
|
||||
* prioridade eventualmente sobem de banda, prevenindo inanição.
|
||||
*
|
||||
* Simula o comportamento de SOs reais como Windows (priority bands)
|
||||
* e POSIX SCHED_RR.
|
||||
*/
|
||||
export const priorityRr: SchedulerAlgorithm = {
|
||||
select(readyQueue) {
|
||||
export class PriorityRoundRobinAlgorithm extends BaseAlgorithm {
|
||||
readonly name = "Priority Round Robin (PRR)";
|
||||
readonly description =
|
||||
"Combina prioridade com round-robin dentro de cada banda. A banda mais " +
|
||||
"prioritária sempre executa primeiro; dentro da mesma banda, os processos " +
|
||||
"se revezam pelo quantum. Simula o escalonamento de SOs reais (Windows, POSIX).";
|
||||
readonly isPreemptiveCapable = true;
|
||||
readonly usesQuantum = true;
|
||||
|
||||
select(readyQueue: readonly ProcessRuntime[]): ProcessRuntime | null {
|
||||
if (readyQueue.length === 0) return null;
|
||||
|
||||
// Find the highest priority level (smallest numeric value) present.
|
||||
// Localiza o nível de prioridade mais alto (menor valor numérico) presente.
|
||||
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.
|
||||
// Dentro da banda, retorna o primeiro processo (ordem de inserção = FIFO).
|
||||
// O engine insere processos expirados no final da fila, garantindo a rotação
|
||||
// round-robin: o processo que esgotou o quantum vai para o fim,
|
||||
// o próximo da mesma prioridade assume.
|
||||
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) {
|
||||
// Preempta APENAS por prioridade estritamente maior.
|
||||
// Processos de mesma prioridade são tratados pelo mecanismo de quantum —
|
||||
// evita trocas de contexto desnecessárias dentro da mesma banda.
|
||||
shouldPreempt(current: ProcessRuntime, candidate: ProcessRuntime): boolean {
|
||||
return candidate.currentPriority < current.currentPriority;
|
||||
},
|
||||
}
|
||||
|
||||
// Quantum expiry drives round-robin rotation within a priority band,
|
||||
// exactly like plain Round Robin.
|
||||
isQuantumExpired(runtime) {
|
||||
// A expiração do quantum conduz a rotação round-robin dentro da banda,
|
||||
// exatamente como no Round Robin puro.
|
||||
isQuantumExpired(runtime: ProcessRuntime): boolean {
|
||||
return runtime.quantumRemaining <= 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+27
-13
@@ -1,22 +1,36 @@
|
||||
import type { SchedulerAlgorithm } from "./index";
|
||||
import type { ProcessRuntime } from "../types";
|
||||
import { BaseAlgorithm } from "./base";
|
||||
|
||||
/**
|
||||
* RR — Round Robin
|
||||
* FIFO queue with a configurable quantum.
|
||||
* select() returns the front of the queue (insertion-order maintained by engine).
|
||||
* Preemption is driven by quantum expiry, not by a better candidate.
|
||||
*
|
||||
* Mantém a fila de prontos como FIFO e concede a cada processo uma fatia
|
||||
* de tempo (quantum). Ao esgotar o quantum, o processo vai para o fim da
|
||||
* fila e o próximo assume a CPU. Garante justiça e baixo tempo de resposta,
|
||||
* ao custo de maior tempo de turnaround para processos curtos.
|
||||
*
|
||||
* A rotação é garantida pelo engine: ao expirar o quantum, o processo é
|
||||
* reinserido no final da fila — select() simplesmente retorna o primeiro.
|
||||
*/
|
||||
export const rr: SchedulerAlgorithm = {
|
||||
select(readyQueue) {
|
||||
export class RoundRobinAlgorithm extends BaseAlgorithm {
|
||||
readonly name = "Round Robin (RR)";
|
||||
readonly description =
|
||||
"Fila FIFO com quantum configurável. Cada processo recebe uma fatia " +
|
||||
"de tempo igual. Garante justiça e baixo tempo de resposta, mas pode " +
|
||||
"aumentar o turnaround de processos longos.";
|
||||
readonly isPreemptiveCapable = false; // preempção é por expiração de quantum, não por candidato
|
||||
readonly usesQuantum = true;
|
||||
|
||||
select(readyQueue: readonly ProcessRuntime[]): ProcessRuntime | null {
|
||||
return readyQueue[0] ?? null;
|
||||
},
|
||||
}
|
||||
|
||||
// RR does not preempt based on candidate priority; quantum expiry handles it.
|
||||
shouldPreempt() {
|
||||
// RR não preempta por candidato mais prioritário; a troca ocorre via quantum.
|
||||
shouldPreempt(): boolean {
|
||||
return false;
|
||||
},
|
||||
}
|
||||
|
||||
isQuantumExpired(runtime) {
|
||||
isQuantumExpired(runtime: ProcessRuntime): boolean {
|
||||
return runtime.quantumRemaining <= 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+63
-32
@@ -1,48 +1,79 @@
|
||||
import type { ProcessRuntime } from "../types";
|
||||
import type { SchedulerAlgorithm } from "./index";
|
||||
import { tiebreak } from "./utils";
|
||||
import { BaseAlgorithm } from "./base";
|
||||
|
||||
/**
|
||||
* Comparador interno compartilhado pelas duas variantes do SJF.
|
||||
* Ordena por menor burst restante; usa tiebreak padrão em caso de empate.
|
||||
*/
|
||||
function sjfSort(a: ProcessRuntime, b: ProcessRuntime): number {
|
||||
if (a.remainingBurst !== b.remainingBurst)
|
||||
return a.remainingBurst - b.remainingBurst;
|
||||
return tiebreak(a, b);
|
||||
return BaseAlgorithm.tiebreak(a, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* SJF Non-Preemptive — Shortest Job First
|
||||
* Selects the process with the smallest remaining burst; no preemption.
|
||||
*/
|
||||
export const sjfNp: SchedulerAlgorithm = {
|
||||
select(readyQueue) {
|
||||
if (readyQueue.length === 0) return null;
|
||||
return [...readyQueue].sort(sjfSort)[0] ?? null;
|
||||
},
|
||||
|
||||
shouldPreempt() {
|
||||
return false;
|
||||
},
|
||||
|
||||
isQuantumExpired() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* SRTF — Shortest Remaining Time First (SJF Preemptive)
|
||||
* At each tick, preempts if a ready process has strictly smaller remainingBurst.
|
||||
* Equal remaining burst → no preemption (avoids unnecessary context switches).
|
||||
* SJF Não-Preemptivo — Shortest Job First
|
||||
*
|
||||
* Seleciona o processo com o menor burst de CPU restante. Uma vez que um
|
||||
* processo ocupa a CPU, ele roda até terminar (não pode ser interrompido).
|
||||
* Ótimo em tempo médio de espera, mas pode causar inanição (starvation)
|
||||
* de processos longos se processos curtos chegam continuamente.
|
||||
*/
|
||||
export const srtf: SchedulerAlgorithm = {
|
||||
select(readyQueue) {
|
||||
export class SJFNonPreemptiveAlgorithm extends BaseAlgorithm {
|
||||
readonly name = "Shortest Job First — Não-Preemptivo (SJF)";
|
||||
readonly description =
|
||||
"Seleciona o processo com menor burst restante. Não-preemptivo: " +
|
||||
"o processo rodando não pode ser interrompido. Minimiza o tempo " +
|
||||
"médio de espera, mas pode causar inanição de processos longos.";
|
||||
readonly isPreemptiveCapable = false;
|
||||
readonly usesQuantum = false;
|
||||
|
||||
select(readyQueue: readonly ProcessRuntime[]): ProcessRuntime | null {
|
||||
if (readyQueue.length === 0) return null;
|
||||
return [...readyQueue].sort(sjfSort)[0] ?? null;
|
||||
},
|
||||
}
|
||||
|
||||
shouldPreempt(current, candidate) {
|
||||
shouldPreempt(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isQuantumExpired(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* SRTF — Shortest Remaining Time First (SJF Preemptivo)
|
||||
*
|
||||
* Variante preemptiva do SJF. A cada tick, se um processo recém-chegado
|
||||
* tem burst restante estritamente menor que o processo em execução, ocorre
|
||||
* preempção. Burst igual não gera troca (evita context switches desnecessários).
|
||||
* Produz o menor tempo médio de espera possível entre todos os algoritmos,
|
||||
* porém com maior overhead de trocas de contexto.
|
||||
*/
|
||||
export class SRTFAlgorithm extends BaseAlgorithm {
|
||||
readonly name = "Shortest Remaining Time First (SRTF)";
|
||||
readonly description =
|
||||
"Variante preemptiva do SJF. Preempta o processo rodando sempre que " +
|
||||
"um processo mais curto fica disponível. Otimiza o tempo médio de " +
|
||||
"espera, mas gera mais trocas de contexto.";
|
||||
readonly isPreemptiveCapable = true;
|
||||
readonly usesQuantum = false;
|
||||
|
||||
select(readyQueue: readonly ProcessRuntime[]): ProcessRuntime | null {
|
||||
if (readyQueue.length === 0) return null;
|
||||
return [...readyQueue].sort(sjfSort)[0] ?? null;
|
||||
}
|
||||
|
||||
shouldPreempt(current: ProcessRuntime, candidate: ProcessRuntime): boolean {
|
||||
return candidate.remainingBurst < current.remainingBurst;
|
||||
},
|
||||
}
|
||||
|
||||
isQuantumExpired() {
|
||||
isQuantumExpired(): boolean {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+268
-141
@@ -6,24 +6,52 @@ import type {
|
||||
StateLabel,
|
||||
} from "./types";
|
||||
import { algorithms } from "./algorithms/index";
|
||||
import type { SchedulerAlgorithm } from "./algorithms/index";
|
||||
|
||||
// ============================================================
|
||||
// SimulationEngine
|
||||
// ============================================================
|
||||
import type { BaseAlgorithm } from "./algorithms/index";
|
||||
|
||||
/**
|
||||
* SimulationEngine — Núcleo de execução da simulação de escalonamento.
|
||||
*
|
||||
* Responsável por avançar o simulador tick a tick, aplicando o algoritmo
|
||||
* de escalonamento configurado e mantendo o estado de todos os processos.
|
||||
*
|
||||
* Ciclo de vida de um único tick (método `step()`):
|
||||
* 1. Chegada — processos cujo `arrivalTime` é igual ao tick atual
|
||||
* transitam New → Ready e entram na fila de prontos.
|
||||
* 2. Preempção — o algoritmo decide se o processo em CPU deve ceder
|
||||
* lugar a um candidato mais prioritário.
|
||||
* 3. Despacho — seleciona e despacha o próximo processo (com overhead
|
||||
* de troca de contexto, se configurado).
|
||||
* 4. Snapshot — captura o estado de todos os processos neste tick.
|
||||
* 5. Passagem do tempo — decrementa bursts, acumula tempos de CPU/IO/espera.
|
||||
* 6. Aging — decrementa prioridade dos processos em espera prolongada.
|
||||
* 7. Conclusões — trata término de bursts de CPU e IO; avança para o
|
||||
* próximo burst ou termina o processo.
|
||||
*
|
||||
* Uso típico:
|
||||
* ```ts
|
||||
* const engine = new SimulationEngine(processes, config);
|
||||
* const ticks = engine.runAll(); // executa até todos os processos terminarem
|
||||
* ```
|
||||
*
|
||||
* Invariante: após `runAll()`, `isFinished` é `true` e todos os runtimes
|
||||
* têm `state === "Terminated"`.
|
||||
*/
|
||||
export class SimulationEngine {
|
||||
// ─── Campos privados ──────────────────────────────────────────────────────
|
||||
|
||||
private readonly _processes: Process[];
|
||||
private readonly _config: SchedulerConfig;
|
||||
private readonly _algorithm: SchedulerAlgorithm;
|
||||
private readonly _algorithm: BaseAlgorithm;
|
||||
|
||||
private _tick: number; // starts at -1; incremented at top of step()
|
||||
private _tick: number; // começa em -1; incrementado no início de step()
|
||||
private _runtimes: Map<string, ProcessRuntime>;
|
||||
private _cpuProcess: ProcessRuntime | null;
|
||||
private _readyQueue: ProcessRuntime[]; // ordered array (FIFO for RR)
|
||||
private _readyQueue: ProcessRuntime[]; // array ordenado (FIFO para RR)
|
||||
private _ioQueue: ProcessRuntime[];
|
||||
private _contextSwitchRemaining: number;
|
||||
private _prevCpuProcessId: string | null; // track context switch detection
|
||||
private _prevCpuProcessId: string | null; // para detectar troca de contexto
|
||||
|
||||
// ─── Construtor ───────────────────────────────────────────────────────────
|
||||
|
||||
constructor(processes: Process[], config: SchedulerConfig) {
|
||||
if (processes.length === 0) throw new Error("At least one process required");
|
||||
@@ -36,20 +64,75 @@ export class SimulationEngine {
|
||||
this._ioQueue = [];
|
||||
this._contextSwitchRemaining = 0;
|
||||
this._prevCpuProcessId = null;
|
||||
this._runtimes = new Map(processes.map((p) => [p.id, initRuntime(p, config)]));
|
||||
this._runtimes = new Map(
|
||||
processes.map((p) => [p.id, SimulationEngine._initRuntime(p, config)]),
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Public API
|
||||
// ----------------------------------------------------------
|
||||
// ─── API pública ──────────────────────────────────────────────────────────
|
||||
|
||||
/** Advance one tick; return snapshot of that tick. */
|
||||
/**
|
||||
* Avança a simulação por um tick e retorna o snapshot do estado neste tick.
|
||||
*
|
||||
* Lança um erro se a simulação já tiver terminado (`isFinished === true`).
|
||||
* O número do tick é incrementado no início; o primeiro tick válido é 0.
|
||||
*/
|
||||
step(): SimTick {
|
||||
if (this.isFinished) throw new Error("Simulation is already finished");
|
||||
|
||||
this._tick++;
|
||||
|
||||
// 1. Arrivals
|
||||
this._handleArrivals(); // 1. New → Ready para quem chega agora
|
||||
this._checkPreemption(); // 2. Preempta CPU se candidato melhor chegou
|
||||
this._dispatchNext(); // 3. Despacha próximo (ou inicia context switch)
|
||||
const snapshot = this._snapshot(); // 4. Captura estado DURANTE este tick
|
||||
this._advanceTime(); // 5. Decrementa bursts, acumula métricas
|
||||
this._applyAging(); // 6. Aging de prioridade (se habilitado)
|
||||
this._handleCpuCompletion(); // 7a. Finaliza burst/quantum de CPU
|
||||
this._handleIoCompletions(); // 7b. Finaliza bursts de I/O
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/** Executa até o fim e retorna todos os snapshots de tick. */
|
||||
runAll(): SimTick[] {
|
||||
const results: SimTick[] = [];
|
||||
while (!this.isFinished) {
|
||||
results.push(this.step());
|
||||
if (results.length > 10_000) {
|
||||
throw new Error("Simulation exceeded 10,000 ticks — check for infinite loop");
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/** Verdadeiro quando todos os processos estão no estado "Terminated". */
|
||||
get isFinished(): boolean {
|
||||
for (const rt of this._runtimes.values()) {
|
||||
if (rt.state !== "Terminated") return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Número do tick atual (−1 antes do primeiro `step()`). */
|
||||
get currentTick(): number {
|
||||
return this._tick;
|
||||
}
|
||||
|
||||
/** Array com o estado de runtime de todos os processos. */
|
||||
get runtimeStates(): ProcessRuntime[] {
|
||||
return Array.from(this._runtimes.values());
|
||||
}
|
||||
|
||||
// ─── Fases do ciclo de simulação ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Fase 1 — Chegada de processos.
|
||||
*
|
||||
* Processos cujo `arrivalTime` é igual ao tick atual transitam do estado
|
||||
* "New" para "Ready" e são inseridos na fila de prontos.
|
||||
*/
|
||||
private _handleArrivals(): void {
|
||||
for (const p of this._processes) {
|
||||
const rt = this._runtimes.get(p.id)!;
|
||||
if (p.arrivalTime === this._tick && rt.state === "New") {
|
||||
@@ -57,55 +140,79 @@ export class SimulationEngine {
|
||||
this._readyQueue.push(rt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Preemption & Quantum Check (from previous tick's state)
|
||||
if (this._cpuProcess !== null && this._readyQueue.length > 0) {
|
||||
const candidate = this._algorithm.select(this._readyQueue, this._config);
|
||||
if (
|
||||
candidate !== null &&
|
||||
this._algorithm.shouldPreempt(this._cpuProcess, candidate, this._config)
|
||||
) {
|
||||
const cpu = this._cpuProcess;
|
||||
this._transition(cpu, "Ready");
|
||||
this._readyQueue.push(cpu);
|
||||
this._cpuProcess = null;
|
||||
}
|
||||
/**
|
||||
* Fase 2 — Verificação de preempção.
|
||||
*
|
||||
* Consulta o algoritmo: se houver um candidato na fila de prontos que deva
|
||||
* preemptar o processo em CPU, o processo atual retorna à fila de prontos
|
||||
* e `_cpuProcess` é zerado. Chamado após a chegada para que processos
|
||||
* recém-chegados possam imediatamente preemptar.
|
||||
*/
|
||||
private _checkPreemption(): void {
|
||||
if (this._cpuProcess === null || this._readyQueue.length === 0) return;
|
||||
const candidate = this._algorithm.select(this._readyQueue, this._config);
|
||||
if (
|
||||
candidate !== null &&
|
||||
this._algorithm.shouldPreempt(this._cpuProcess, candidate, this._config)
|
||||
) {
|
||||
const cpu = this._cpuProcess;
|
||||
this._transition(cpu, "Ready");
|
||||
this._readyQueue.push(cpu);
|
||||
this._cpuProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Dispatch / Context Switch
|
||||
if (this._contextSwitchRemaining > 0) {
|
||||
// Doing context switch this tick
|
||||
// We don't dispatch.
|
||||
/**
|
||||
* Fase 3 — Despacho / troca de contexto.
|
||||
*
|
||||
* Se um context switch estiver em andamento, nenhum processo é despachado
|
||||
* neste tick. Caso contrário, e se a CPU estiver ociosa com processos
|
||||
* prontos, o algoritmo seleciona o próximo processo:
|
||||
*
|
||||
* - Se houver overhead de troca de contexto configurado e o processo
|
||||
* selecionado for diferente do último, o processo volta ao início da
|
||||
* fila e o contador de context-switch é iniciado.
|
||||
* - Caso contrário, o processo é despachado imediatamente para a CPU.
|
||||
*/
|
||||
private _dispatchNext(): void {
|
||||
if (this._contextSwitchRemaining > 0) return;
|
||||
if (this._cpuProcess !== null || this._readyQueue.length === 0) return;
|
||||
|
||||
const next = this._algorithm.select(this._readyQueue, this._config);
|
||||
if (next === null) return;
|
||||
|
||||
const idx = this._readyQueue.indexOf(next);
|
||||
if (idx !== -1) this._readyQueue.splice(idx, 1);
|
||||
|
||||
if (
|
||||
this._config.contextSwitchTime > 0 &&
|
||||
this._prevCpuProcessId !== null &&
|
||||
this._prevCpuProcessId !== next.processId
|
||||
) {
|
||||
this._readyQueue.unshift(next);
|
||||
this._contextSwitchRemaining = this._config.contextSwitchTime;
|
||||
this._prevCpuProcessId = next.processId;
|
||||
} else {
|
||||
if (this._cpuProcess === null && this._readyQueue.length > 0) {
|
||||
const next = this._algorithm.select(this._readyQueue, this._config);
|
||||
if (next !== null) {
|
||||
const idx = this._readyQueue.indexOf(next);
|
||||
if (idx !== -1) this._readyQueue.splice(idx, 1);
|
||||
|
||||
if (
|
||||
this._config.contextSwitchTime > 0 &&
|
||||
this._prevCpuProcessId !== null &&
|
||||
this._prevCpuProcessId !== next.processId
|
||||
) {
|
||||
this._readyQueue.unshift(next);
|
||||
this._contextSwitchRemaining = this._config.contextSwitchTime;
|
||||
this._prevCpuProcessId = next.processId;
|
||||
} else {
|
||||
if (next.startTick === null) next.startTick = this._tick;
|
||||
next.quantumRemaining = this._config.quantum;
|
||||
this._transition(next, "Running");
|
||||
this._prevCpuProcessId = next.processId;
|
||||
this._cpuProcess = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (next.startTick === null) next.startTick = this._tick;
|
||||
next.quantumRemaining = this._config.quantum;
|
||||
this._transition(next, "Running");
|
||||
this._prevCpuProcessId = next.processId;
|
||||
this._cpuProcess = next;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Take Snapshot representing what happens DURING this tick
|
||||
const snapshot = this._snapshot();
|
||||
|
||||
// 5. Passage of Time (Simulate execution for 1 tick)
|
||||
/**
|
||||
* Fase 5 — Passagem do tempo.
|
||||
*
|
||||
* Simula a execução de 1 tick:
|
||||
* - Decrementa o contador de context-switch restante (se ativo).
|
||||
* - Decrementa o burst restante do processo em CPU e acumula cpuTime/quantum.
|
||||
* - Decrementa o burst restante de cada processo em I/O e acumula ioTime.
|
||||
* - Incrementa o waitingTime de todos os processos na fila de prontos.
|
||||
*/
|
||||
private _advanceTime(): void {
|
||||
if (this._contextSwitchRemaining > 0) {
|
||||
this._contextSwitchRemaining--;
|
||||
}
|
||||
@@ -125,44 +232,80 @@ export class SimulationEngine {
|
||||
for (const rt of this._readyQueue) {
|
||||
rt.waitingTime++;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Aging (Priority)
|
||||
/**
|
||||
* Fase 6 — Aging (envelhecimento de prioridade).
|
||||
*
|
||||
* Quando o aging está habilitado, a cada `agingInterval` ticks o valor de
|
||||
* `currentPriority` de todos os processos na fila de prontos é decrementado
|
||||
* em 1 (até o mínimo de 1). Isso previne inanição em algoritmos de prioridade.
|
||||
*
|
||||
* Não é executado no tick 0 para evitar decremento prematuro.
|
||||
*/
|
||||
private _applyAging(): void {
|
||||
if (
|
||||
this._config.agingEnabled &&
|
||||
this._tick > 0 &&
|
||||
this._tick % this._config.agingInterval === 0
|
||||
!this._config.agingEnabled ||
|
||||
this._tick === 0 ||
|
||||
this._tick % this._config.agingInterval !== 0
|
||||
) {
|
||||
for (const rt of this._readyQueue) {
|
||||
rt.currentPriority = Math.max(1, rt.currentPriority - 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const rt of this._readyQueue) {
|
||||
rt.currentPriority = Math.max(1, rt.currentPriority - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Process completions / transition to next state for NEXT tick
|
||||
if (this._cpuProcess !== null) {
|
||||
const cpu = this._cpuProcess;
|
||||
if (cpu.remainingBurst === 0) {
|
||||
this._advanceBurst(cpu);
|
||||
const process = this._processes.find((p) => p.id === cpu.processId)!;
|
||||
const nextBurst = process.bursts[cpu.burstIndex];
|
||||
if (nextBurst !== undefined && nextBurst.type === "io") {
|
||||
this._transition(cpu, "Waiting");
|
||||
this._ioQueue.push(cpu);
|
||||
} else if (nextBurst !== undefined && nextBurst.type === "cpu") {
|
||||
this._transition(cpu, "Ready");
|
||||
this._readyQueue.push(cpu);
|
||||
} else {
|
||||
this._transition(cpu, "Terminated");
|
||||
cpu.finishTick = this._tick + 1;
|
||||
}
|
||||
this._cpuProcess = null;
|
||||
} else if (this._algorithm.isQuantumExpired(cpu, this._config)) {
|
||||
/**
|
||||
* Fase 7a — Conclusão de burst de CPU.
|
||||
*
|
||||
* Após a passagem do tempo, verifica se o processo em CPU terminou seu burst
|
||||
* ou esgotou o quantum:
|
||||
*
|
||||
* - Burst zerado: avança para o próximo burst do processo.
|
||||
* · Se o próximo for I/O → transita para "Waiting" e entra na ioQueue.
|
||||
* · Se o próximo for CPU → retorna à fila de prontos ("Ready").
|
||||
* · Se não houver próximo → termina o processo ("Terminated").
|
||||
*
|
||||
* - Quantum expirado (sem término de burst): retorna à fila de prontos.
|
||||
*/
|
||||
private _handleCpuCompletion(): void {
|
||||
if (this._cpuProcess === null) return;
|
||||
|
||||
const cpu = this._cpuProcess;
|
||||
|
||||
if (cpu.remainingBurst === 0) {
|
||||
this._advanceBurst(cpu);
|
||||
const process = this._processes.find((p) => p.id === cpu.processId)!;
|
||||
const nextBurst = process.bursts[cpu.burstIndex];
|
||||
if (nextBurst !== undefined && nextBurst.type === "io") {
|
||||
this._transition(cpu, "Waiting");
|
||||
this._ioQueue.push(cpu);
|
||||
} else if (nextBurst !== undefined && nextBurst.type === "cpu") {
|
||||
this._transition(cpu, "Ready");
|
||||
this._readyQueue.push(cpu);
|
||||
this._cpuProcess = null;
|
||||
} else {
|
||||
this._transition(cpu, "Terminated");
|
||||
cpu.finishTick = this._tick + 1;
|
||||
}
|
||||
this._cpuProcess = null;
|
||||
} else if (this._algorithm.isQuantumExpired(cpu, this._config)) {
|
||||
this._transition(cpu, "Ready");
|
||||
this._readyQueue.push(cpu);
|
||||
this._cpuProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
// IO completions
|
||||
/**
|
||||
* Fase 7b — Conclusão de bursts de I/O.
|
||||
*
|
||||
* Percorre a ioQueue e, para cada processo cujo burst de I/O zerou:
|
||||
* - Se houver próximo burst → transita para "Ready" e insere na readyQueue.
|
||||
* - Caso contrário → termina o processo ("Terminated").
|
||||
*
|
||||
* Processos ainda com I/O pendente permanecem na ioQueue.
|
||||
*/
|
||||
private _handleIoCompletions(): void {
|
||||
const nextIoQueue: ProcessRuntime[] = [];
|
||||
for (const rt of this._ioQueue) {
|
||||
if (rt.remainingBurst === 0) {
|
||||
@@ -180,45 +323,22 @@ export class SimulationEngine {
|
||||
}
|
||||
}
|
||||
this._ioQueue = nextIoQueue;
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/** Run to completion; return all tick snapshots. */
|
||||
runAll(): SimTick[] {
|
||||
const results: SimTick[] = [];
|
||||
while (!this.isFinished) {
|
||||
results.push(this.step());
|
||||
if (results.length > 10_000) {
|
||||
throw new Error("Simulation exceeded 10,000 ticks — check for infinite loop");
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
get isFinished(): boolean {
|
||||
for (const rt of this._runtimes.values()) {
|
||||
if (rt.state !== "Terminated") return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
get currentTick(): number {
|
||||
return this._tick;
|
||||
}
|
||||
|
||||
get runtimeStates(): ProcessRuntime[] {
|
||||
return Array.from(this._runtimes.values());
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Private helpers
|
||||
// ----------------------------------------------------------
|
||||
// ─── Helpers internos ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Aplica uma transição de estado a um runtime, atualizando `state`.
|
||||
* Centraliza todas as transições para facilitar rastreamento e depuração.
|
||||
*/
|
||||
private _transition(rt: ProcessRuntime, state: StateLabel): void {
|
||||
rt.state = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Avança o índice de burst de um runtime e atualiza `remainingBurst`
|
||||
* com a duração do próximo burst. Se não houver próximo burst, `remainingBurst` é 0.
|
||||
*/
|
||||
private _advanceBurst(rt: ProcessRuntime): void {
|
||||
rt.burstIndex++;
|
||||
const process = this._processes.find((p) => p.id === rt.processId)!;
|
||||
@@ -226,6 +346,11 @@ export class SimulationEngine {
|
||||
rt.remainingBurst = nextBurst !== undefined ? nextBurst.duration : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Captura o estado instantâneo de todos os processos no tick atual.
|
||||
* Se um context switch estiver em andamento, `cpuProcess` é null e
|
||||
* `ctxSwitchForProcess` identifica o processo prestes a executar.
|
||||
*/
|
||||
private _snapshot(): SimTick {
|
||||
const states: Record<string, StateLabel> = {};
|
||||
for (const [id, rt] of this._runtimes) {
|
||||
@@ -244,30 +369,32 @@ export class SimulationEngine {
|
||||
: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Helper: initialise a ProcessRuntime from a Process
|
||||
// ============================================================
|
||||
// ─── Inicialização ────────────────────────────────────────────────────────
|
||||
|
||||
function initRuntime(p: Process, config: SchedulerConfig): ProcessRuntime {
|
||||
const firstBurst = p.bursts[0];
|
||||
if (firstBurst === undefined || firstBurst.type !== "cpu") {
|
||||
throw new Error(`Process ${p.id}: first burst must be a CPU burst`);
|
||||
/**
|
||||
* Inicializa o estado de runtime de um processo a partir de sua definição.
|
||||
* O primeiro burst deve ser do tipo "cpu"; caso contrário, um erro é lançado.
|
||||
*/
|
||||
private static _initRuntime(p: Process, config: SchedulerConfig): ProcessRuntime {
|
||||
const firstBurst = p.bursts[0];
|
||||
if (firstBurst === undefined || firstBurst.type !== "cpu") {
|
||||
throw new Error(`Process ${p.id}: first burst must be a CPU burst`);
|
||||
}
|
||||
return {
|
||||
processId: p.id,
|
||||
state: "New",
|
||||
remainingBurst: firstBurst.duration,
|
||||
burstIndex: 0,
|
||||
arrivalTick: p.arrivalTime,
|
||||
startTick: null,
|
||||
finishTick: null,
|
||||
waitingTime: 0,
|
||||
cpuTime: 0,
|
||||
ioTime: 0,
|
||||
responseTime: null,
|
||||
quantumRemaining: config.quantum,
|
||||
currentPriority: p.priority,
|
||||
};
|
||||
}
|
||||
return {
|
||||
processId: p.id,
|
||||
state: "New",
|
||||
remainingBurst: firstBurst.duration,
|
||||
burstIndex: 0,
|
||||
arrivalTick: p.arrivalTime,
|
||||
startTick: null,
|
||||
finishTick: null,
|
||||
waitingTime: 0,
|
||||
cpuTime: 0,
|
||||
ioTime: 0,
|
||||
responseTime: null,
|
||||
quantumRemaining: config.quantum,
|
||||
currentPriority: p.priority,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user