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:
Lucas Guerra committed 2026-03-23 16:04:14 -03:00
1 parent 9c552e53fa
commit 571d8e51d2
26 files changed
+1583 -1221

No files matched your search

+74 -18
View File
@@ -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>
+135 -48
View File
@@ -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>
);
};
+1 -1
View File
@@ -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 => (
+81 -20
View File
@@ -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>