Implement Priority Round Robin Scheduling Algorithm with Tests

- Added `priority_rr` algorithm to handle scheduling based on priority and round-robin principles.
- Created unit tests for `select()`, `shouldPreempt()`, and `isQuantumExpired()` functions to ensure correct behavior.
- Developed integration tests to validate the scheduling logic under various scenarios, including preemption and round-robin execution.
- Introduced `useExport` hook for exporting process metrics to CSV format.
- Added `useGanttScroll` hook to manage smooth scrolling of the Gantt chart during simulation.
This commit is contained in:
Lucas Rayan committed 2026-03-19 16:45:05 -03:00
1 parent 083fc0027e
commit 51a8a6fa40
38 files changed
+2530 -45

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
import React from "react";
import type { Process, SimTick, StateLabel } from "@core/types";
import { CELL_H, CELL_W } from "./constants";
// ── colour map ──────────────────────────────────────────────
const STATE_CLASS: Record<StateLabel, string> = {
New: "bg-[var(--color-state-new)]",
Ready: "bg-[var(--color-state-ready)]",
Running: "bg-[var(--color-state-running)]",
Waiting: "bg-[var(--color-state-waiting)]",
Terminated: "bg-[var(--color-state-terminated)]",
};
// ── props ────────────────────────────────────────────────────
interface GanttCellProps {
tick: SimTick;
process: Process;
state: StateLabel;
isCtxSwitch: boolean;
onMouseEnter: (e: React.MouseEvent, tick: SimTick, process: Process, state: StateLabel, isCtxSwitch: boolean) => void;
onMouseLeave: () => void;
}
// ── component ───────────────────────────────────────────────
const GanttCell = React.memo(function GanttCell({
tick,
process,
state,
isCtxSwitch,
onMouseEnter,
onMouseLeave,
}: GanttCellProps) {
const bg = isCtxSwitch
? "bg-[var(--color-state-ctx)]"
: STATE_CLASS[state];
// Running cells get a coloured left border using the process colour
const borderStyle =
!isCtxSwitch && state === "Running"
? { borderLeft: `3px solid ${process.color}` }
: undefined;
return (
<div
className={`${bg} shrink-0 opacity-90 hover:opacity-100 transition-opacity cursor-default`}
style={{ width: CELL_W, height: CELL_H, ...borderStyle }}
onMouseEnter={(e) => onMouseEnter(e, tick, process, state, isCtxSwitch)}
onMouseLeave={onMouseLeave}
/>
);
});
export default GanttCell;
+113
View File
@@ -0,0 +1,113 @@
import { useCallback, useRef, useState } from "react";
import { useSimulationStore } from "@store/simulationStore";
import { useProcessStore } from "@store/processStore";
import { useGanttScroll } from "@hooks/useGanttScroll";
import GanttRow from "./GanttRow";
import GanttLegend from "./GanttLegend";
import GanttTooltip, { type TooltipData } from "./GanttTooltip";
import { CELL_H, CELL_W } from "./constants";
// ── GanttChart ────────────────────────────────────────────────
const GanttChart = () => {
const ticks = useSimulationStore((s) => s.ticks);
const status = useSimulationStore((s) => s.status);
const processes = useProcessStore((s) => s.processes);
// Scrollable container ref for auto-scroll
const scrollRef = useRef<HTMLDivElement>(null);
const currentTickIndex = ticks.length - 1;
useGanttScroll(scrollRef, currentTickIndex, status === "running");
// Tooltip state
const [tooltip, setTooltip] = useState<TooltipData | null>(null);
const handleMouseLeave = useCallback(() => setTooltip(null), []);
// Empty state
if (ticks.length === 0) {
return (
<div className="border-b border-slate-800 flex items-center justify-center text-slate-600 bg-slate-900/50 text-sm select-none"
style={{ minHeight: 80 }}>
Inicie a simulação para ver o diagrama de Gantt
</div>
);
}
// ── ruler (tick numbers) ──────────────────────────────────
const tickNumbers = ticks.map((t) => t.tick);
const totalWidth = ticks.length * CELL_W;
return (
<div className="border-b border-slate-800 bg-slate-950 flex flex-col">
{/* ── scrollable grid + cursor ── */}
<div
ref={scrollRef}
className="overflow-x-auto overflow-y-hidden gantt-scrollbar relative"
>
<div style={{ width: totalWidth, position: "relative" }}>
{/* Tick ruler */}
<div className="flex sticky top-0 z-10 bg-slate-950 border-b border-slate-800">
{/* Spacer for sticky process labels column */}
<div className="sticky left-0 z-20 bg-slate-950 border-r border-slate-700 shrink-0" style={{ width: 48, height: 16 }} />
{tickNumbers.map((t) => (
<div
key={t}
className="shrink-0 text-center text-[9px] text-slate-500 font-mono select-none"
style={{ width: CELL_W, height: 16, lineHeight: "16px" }}
>
{t % 5 === 0 ? t : ""}
</div>
))}
</div>
{/* Process rows */}
<div className="relative">
{processes.map((process) => (
<div key={process.id} className="flex items-center border-b border-white/5 last:border-b-0">
{/* Process label (sticky on the left) */}
<div
className="sticky left-0 z-20 bg-slate-900 border-r border-slate-700 px-2 flex items-center shrink-0"
style={{ width: 48, height: CELL_H }}
>
<span className="text-[10px] font-bold truncate" style={{ color: process.color }}>
{process.name}
</span>
</div>
<GanttRow
process={process}
ticks={ticks}
onCellMouseEnter={setTooltip}
onCellMouseLeave={handleMouseLeave}
/>
</div>
))}
{/* Current-tick cursor line */}
{currentTickIndex >= 0 && (
<div
className="gantt-cursor absolute top-0 bottom-0 w-px bg-white/20 pointer-events-none"
style={{ left: 48 + currentTickIndex * CELL_W + CELL_W / 2 }}
/>
)}
</div>
</div>
</div>
{/* ── process name labels (sticky left, overlay) ── */}
{/* Rendered OUTSIDE the scrollable div so they stay fixed on the left */}
{/* We achieve this with a separate absolute-positioned column */}
{/* ── legend ── */}
<GanttLegend />
{/* ── tooltip ── */}
{tooltip && <GanttTooltip data={tooltip} />}
</div>
);
};
export default GanttChart;
+25
View File
@@ -0,0 +1,25 @@
// Legend items ordered as they appear in a typical lifecycle
const ITEMS: { label: string; cssVar: string; textClass: string }[] = [
{ label: "New", cssVar: "var(--color-state-new)", textClass: "text-white" },
{ label: "Ready", cssVar: "var(--color-state-ready)", textClass: "text-slate-900" },
{ label: "Running", cssVar: "var(--color-state-running)", textClass: "text-slate-900" },
{ label: "Waiting (I/O)", cssVar: "var(--color-state-waiting)", textClass: "text-slate-900" },
{ label: "Terminated", cssVar: "var(--color-state-terminated)", textClass: "text-slate-900" },
{ label: "Context Switch",cssVar: "var(--color-state-ctx)", textClass: "text-white" },
];
const GanttLegend = () => (
<div className="flex flex-wrap gap-x-4 gap-y-2 px-4 py-3 border-t border-slate-700/60">
{ITEMS.map(({ label, cssVar, textClass }) => (
<div key={label} className="flex items-center gap-1.5">
<span
className={`inline-block rounded-sm ${textClass}`}
style={{ width: 14, height: 14, background: cssVar, flexShrink: 0 }}
/>
<span className="text-xs text-slate-400">{label}</span>
</div>
))}
</div>
);
export default GanttLegend;
+51
View File
@@ -0,0 +1,51 @@
import React from "react";
import type { Process, SimTick, StateLabel } from "@core/types";
import GanttCell from "./GanttCell";
import { CELL_H, CELL_W } from "./constants";
import type { TooltipData } from "./GanttTooltip";
interface GanttRowProps {
process: Process;
ticks: SimTick[];
onCellMouseEnter: (data: TooltipData) => void;
onCellMouseLeave: () => void;
}
const GanttRow = React.memo(function GanttRow({
process,
ticks,
onCellMouseEnter,
onCellMouseLeave,
}: GanttRowProps) {
return (
<div className="flex" style={{ height: CELL_H }}>
{ticks.map((tick) => {
const state: StateLabel = tick.states[process.id] ?? "New";
// A context-switch tick affects the process currently on CPU
const isCtxSwitch =
tick.contextSwitching && tick.cpuProcess === process.id;
return (
<GanttCell
key={tick.tick}
tick={tick}
process={process}
state={state}
isCtxSwitch={isCtxSwitch}
onMouseEnter={(e, t, p, s, ctx) =>
onCellMouseEnter({ tick: t, process: p, state: s, isCtxSwitch: ctx, x: e.clientX, y: e.clientY })
}
onMouseLeave={onCellMouseLeave}
/>
);
})}
{/* Empty filler so the row is always at least CELL_W wide */}
{ticks.length === 0 && (
<div style={{ width: CELL_W, height: CELL_H }} className="bg-slate-800/40" />
)}
</div>
);
});
export default GanttRow;
@@ -0,0 +1,80 @@
import type { Process, SimTick, StateLabel } from "@core/types";
// ── colour labels ────────────────────────────────────────────
const STATE_LABEL: Record<StateLabel, string> = {
New: "New",
Ready: "Ready",
Running: "Running",
Waiting: "Waiting (I/O)",
Terminated: "Terminated",
};
const STATE_BADGE: Record<StateLabel, string> = {
New: "bg-[var(--color-state-new)] text-white",
Ready: "bg-[var(--color-state-ready)] text-slate-900",
Running: "bg-[var(--color-state-running)] text-slate-900",
Waiting: "bg-[var(--color-state-waiting)] text-slate-900",
Terminated: "bg-[var(--color-state-terminated)] text-slate-900",
};
// ── types ────────────────────────────────────────────────────
export interface TooltipData {
tick: SimTick;
process: Process;
state: StateLabel;
isCtxSwitch: boolean;
x: number;
y: number;
}
interface GanttTooltipProps {
data: TooltipData;
}
// ── component ────────────────────────────────────────────────
const GanttTooltip = ({ data }: GanttTooltipProps) => {
const { tick, process, state, isCtxSwitch, x, y } = data;
// Keep the tooltip fully inside the viewport
const left = Math.min(x + 12, window.innerWidth - 220);
const top = Math.min(y + 12, window.innerHeight - 140);
return (
<div
className="fixed z-50 pointer-events-none bg-slate-800 border border-slate-600 rounded-lg shadow-2xl px-3 py-2.5 text-xs text-slate-100 w-52"
style={{ left, top }}
>
{/* Header */}
<div className="flex items-center justify-between mb-2">
<span className="font-semibold text-sm" style={{ color: process.color }}>
{process.name}
</span>
<span className="text-slate-400 font-mono">tick {tick.tick}</span>
</div>
{/* State badge */}
<div className="mb-1.5">
{isCtxSwitch ? (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-state-ctx text-white font-medium">
⚠ Context Switch
</span>
) : (
<span
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full font-medium ${STATE_BADGE[state]}`}
>
{STATE_LABEL[state]}
</span>
)}
</div>
{/* Queue context */}
{tick.readyQueue.length > 0 && (
<p className="text-slate-400 text-[10px] mt-1">
Ready queue: {tick.readyQueue.length} process{tick.readyQueue.length !== 1 ? "es" : ""}
</p>
)}
</div>
);
};
export default GanttTooltip;
@@ -0,0 +1,79 @@
import { describe, it, expect, vi } from "vitest";
import { render, fireEvent } from "@testing-library/react";
import GanttCell from "../GanttCell";
import type { Process, SimTick } from "@core/types";
describe("GanttCell", () => {
const mockProcess: Process = {
id: "p1",
name: "P1",
arrivalTime: 0,
priority: 5,
color: "#ff0000",
bursts: [{ type: "cpu", duration: 5 }],
};
const mockTick: SimTick = {
tick: 1,
cpuProcess: "p1",
ioProcesses: [],
readyQueue: [],
states: { p1: "Running" },
contextSwitching: false,
};
const noop = () => {};
it("renders with the correct background color for Running state", () => {
const { container } = render(
<GanttCell
tick={mockTick}
process={mockProcess}
state="Running"
isCtxSwitch={false}
onMouseEnter={noop}
onMouseLeave={noop}
/>
);
const cell = container.firstChild as HTMLElement;
expect(cell.className).toContain("bg-[var(--color-state-running)]");
});
it("renders with the context switch color when isCtxSwitch is true", () => {
const { container } = render(
<GanttCell
tick={mockTick}
process={mockProcess}
state="Running"
isCtxSwitch={true}
onMouseEnter={noop}
onMouseLeave={noop}
/>
);
const cell = container.firstChild as HTMLElement;
expect(cell.className).toContain("bg-[var(--color-state-ctx)]");
});
it("calls onMouseEnter with correct arguments", () => {
const onMouseEnter = vi.fn();
const { container } = render(
<GanttCell
tick={mockTick}
process={mockProcess}
state="Running"
isCtxSwitch={false}
onMouseEnter={onMouseEnter}
onMouseLeave={noop}
/>
);
const cell = container.firstChild as HTMLElement;
fireEvent.mouseEnter(cell);
expect(onMouseEnter).toHaveBeenCalledWith(
expect.anything(),
mockTick,
mockProcess,
"Running",
false
);
});
});
+5
View File
@@ -0,0 +1,5 @@
/** Width of a single tick cell (px) */
export const CELL_W = 28;
/** Height of a single process row (px) */
export const CELL_H = 32;
+150
View File
@@ -0,0 +1,150 @@
import { useState, useRef } from 'react';
import { ChevronDown, Download, Upload, BookOpen } from 'lucide-react';
import { presets } from '@core/presets';
import { useProcessStore } from '../../store/processStore';
import { useSimulationStore } from '../../store/simulationStore';
const AppHeader = () => {
const [presetsOpen, setPresetsOpen] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const loadPreset = useProcessStore(s => s.loadPreset);
const processes = useProcessStore(s => s.processes);
const config = useProcessStore(s => s.config);
const simStatus = useSimulationStore(s => s.status);
const handleLoadPreset = (index: number) => {
const p = presets[index];
if (!p) return;
loadPreset(p.processes, p.config);
setPresetsOpen(false);
};
const handleExportJSON = () => {
const data = JSON.stringify({ processes, config }, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'pss-config.json';
a.click();
URL.revokeObjectURL(url);
};
const handleImportJSON = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
try {
const parsed = JSON.parse(ev.target?.result as string);
if (parsed.processes && parsed.config) {
loadPreset(parsed.processes, parsed.config);
}
} catch {
// Silently ignore malformed JSON
}
};
reader.readAsText(file);
// Reset so the same file can be imported again
e.target.value = '';
};
const isSimulationActive = simStatus !== 'idle';
return (
<header className="h-12 flex items-center px-5 gap-4 bg-slate-900 border-b border-slate-700/60 shrink-0 z-30">
{/* Title */}
<h1 className="text-sm font-semibold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent whitespace-nowrap">
Process Scheduler Simulator
</h1>
<div className="flex-1" />
{/* Presets dropdown */}
<div className="relative">
<button
onClick={() => setPresetsOpen(o => !o)}
className="flex items-center gap-1.5 text-xs text-slate-300 hover:text-white px-2.5 py-1.5 rounded-md hover:bg-slate-700 transition-colors"
aria-label="Carregar cenário pré-definido"
>
<BookOpen size={13} />
Presets
<ChevronDown size={12} className={`transition-transform ${presetsOpen ? 'rotate-180' : ''}`} />
</button>
{presetsOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-40"
onClick={() => setPresetsOpen(false)}
/>
{/* Dropdown */}
<div className="absolute right-0 top-full mt-1 w-72 bg-slate-800 border border-slate-600 rounded-lg shadow-xl z-50 overflow-hidden">
<div className="px-3 py-2 border-b border-slate-700 text-[10px] text-slate-400 uppercase tracking-wider font-medium">
Cenários pré-definidos
</div>
{presets.map((preset, i) => (
<button
key={preset.name}
onClick={() => handleLoadPreset(i)}
className="w-full text-left px-3 py-2.5 hover:bg-slate-700 transition-colors group"
>
<div className="text-xs font-medium text-slate-200 group-hover:text-white">
{preset.name}
</div>
<div className="text-[11px] text-slate-400 mt-0.5 leading-snug">
{preset.description}
</div>
<div className="text-[10px] text-blue-400 mt-0.5">
{preset.config.algorithm}
</div>
</button>
))}
</div>
</>
)}
</div>
{/* Import JSON */}
<label
htmlFor="import-json"
title={isSimulationActive ? 'Reinicie a simulação antes de importar' : 'Importar configuração JSON'}
className={`flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md transition-colors cursor-pointer ${
isSimulationActive
? 'text-slate-600 cursor-not-allowed'
: 'text-slate-300 hover:text-white hover:bg-slate-700'
}`}
aria-label="Importar configuração JSON"
>
<Upload size={13} />
Importar
</label>
<input
id="import-json"
ref={fileInputRef}
type="file"
accept=".json"
className="sr-only"
onChange={handleImportJSON}
disabled={isSimulationActive}
aria-label="Selecionar arquivo JSON para importar"
/>
{/* Export JSON */}
<button
onClick={handleExportJSON}
disabled={processes.length === 0}
title={processes.length === 0 ? 'Adicione processos antes de exportar' : 'Exportar configuração como JSON'}
className="flex items-center gap-1.5 text-xs text-slate-300 hover:text-white px-2.5 py-1.5 rounded-md hover:bg-slate-700 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
aria-label="Exportar configuração JSON"
>
<Download size={13} />
Exportar
</button>
</header>
);
};
export default AppHeader;
+7 -3
View File
@@ -1,11 +1,15 @@
import AppHeader from './AppHeader';
import Sidebar from './Sidebar';
import MainArea from './MainArea';
const AppLayout = () => {
return (
<div className="flex h-screen w-full bg-slate-900 text-slate-200 overflow-hidden font-sans">
<Sidebar />
<MainArea />
<div className="flex flex-col h-screen w-full bg-slate-900 text-slate-200 overflow-hidden font-sans">
<AppHeader />
<div className="flex flex-1 overflow-hidden">
<Sidebar />
<MainArea />
</div>
</div>
);
};
+5 -15
View File
@@ -1,23 +1,13 @@
import ControlBar from '../ControlBar/ControlBar';
import GanttChart from '../GanttChart/GanttChart';
import MetricsPanel from '../MetricsPanel/MetricsPanel';
const MainArea = () => {
return (
<main className="flex-1 flex flex-col h-full bg-slate-900 overflow-hidden relative">
<div className="flex-1 flex flex-col">
<ControlBar />
{/* Placeholder for GanttChart — Phase 4 */}
<div className="border-b border-slate-800 h-80 flex items-center justify-center text-slate-600 bg-slate-900/50 text-sm">
[ Gantt Chart — Fase 4 ]
</div>
{/* Placeholder for MetricsPanel — Phase 5 */}
<div className="flex-1 flex items-center justify-center text-slate-600 bg-slate-950 p-6">
<div className="w-full h-full border border-dashed border-slate-700 rounded-xl flex items-center justify-center text-sm">
[ Metrics Panel — Fase 5 ]
</div>
</div>
</div>
<ControlBar />
<GanttChart />
<MetricsPanel />
</main>
);
};
@@ -0,0 +1,83 @@
import type { GlobalMetrics as GlobalMetricsType } from '@core/types';
interface GlobalMetricsProps {
metrics: GlobalMetricsType;
}
interface MetricCardProps {
label: string;
value: string;
sub?: string;
accent?: string;
}
const MetricCard = ({ label, value, sub, accent = 'text-blue-300' }: MetricCardProps) => (
<div className="bg-slate-800 border border-slate-700 rounded-lg px-4 py-3 flex flex-col gap-1">
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-medium">{label}</span>
<span className={`text-xl font-bold font-mono ${accent}`}>{value}</span>
{sub && <span className="text-[10px] text-slate-500">{sub}</span>}
</div>
);
const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
const {
avgResponseTime,
avgTurnaroundTime,
avgWaitingTime,
cpuThroughput,
cpuUtilization,
totalSimulationTime,
} = metrics;
const utilizationPct = Math.min(100, cpuUtilization);
return (
<div className="p-4 space-y-4">
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
<MetricCard
label="Avg Response Time"
value={avgResponseTime.toFixed(2)}
sub="unidades de tempo"
accent="text-amber-300"
/>
<MetricCard
label="Avg Turnaround Time"
value={avgTurnaroundTime.toFixed(2)}
sub="unidades de tempo"
accent="text-blue-300"
/>
<MetricCard
label="Avg Waiting Time"
value={avgWaitingTime.toFixed(2)}
sub="unidades de tempo"
accent="text-orange-300"
/>
<MetricCard
label="CPU Throughput"
value={cpuThroughput.toFixed(3)}
sub="processos / u.t."
accent="text-emerald-300"
/>
<MetricCard
label="Total Simulation Time"
value={String(totalSimulationTime)}
sub="ticks"
accent="text-slate-200"
/>
{/* CPU Utilization card with progress bar */}
<div className="bg-slate-800 border border-slate-700 rounded-lg px-4 py-3 flex flex-col gap-1">
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-medium">CPU Utilization</span>
<span className="text-xl font-bold font-mono text-violet-300">{utilizationPct.toFixed(1)}%</span>
<div className="mt-1 h-1.5 w-full bg-slate-700 rounded-full overflow-hidden">
<div
className="h-full bg-violet-500 rounded-full transition-all duration-500"
style={{ width: `${utilizationPct}%` }}
/>
</div>
</div>
</div>
</div>
);
};
export default GlobalMetrics;
@@ -0,0 +1,82 @@
import {
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
Legend,
ResponsiveContainer,
CartesianGrid,
} from 'recharts';
import type { ProcessMetrics, Process } from '@core/types';
interface MetricsChartProps {
metrics: ProcessMetrics[];
processMap: Record<string, Process>;
}
const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
if (metrics.length === 0) {
return (
<div className="flex items-center justify-center h-48 text-slate-500 text-sm">
Nenhum dado disponível.
</div>
);
}
const data = metrics.map((m) => ({
name: processMap[m.processId]?.name ?? m.processId,
'Waiting Time': m.waitingTime,
'CPU Time': m.cpuTime,
'I/O Time': m.ioTime,
}));
return (
<div className="p-4">
<ResponsiveContainer width="100%" height={220}>
<BarChart data={data} margin={{ top: 4, right: 16, left: 0, bottom: 4 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" vertical={false} />
<XAxis
dataKey="name"
tick={{ fill: '#94a3b8', fontSize: 11 }}
axisLine={{ stroke: '#475569' }}
tickLine={false}
/>
<YAxis
tick={{ fill: '#94a3b8', fontSize: 11 }}
axisLine={false}
tickLine={false}
label={{
value: 'ticks',
angle: -90,
position: 'insideLeft',
offset: 10,
style: { fill: '#64748b', fontSize: 10 },
}}
/>
<Tooltip
contentStyle={{
backgroundColor: '#1e293b',
border: '1px solid #334155',
borderRadius: '6px',
fontSize: '12px',
color: '#e2e8f0',
}}
cursor={{ fill: 'rgba(255,255,255,0.05)' }}
/>
<Legend
wrapperStyle={{ fontSize: '11px', paddingTop: '8px', color: '#94a3b8' }}
/>
<Bar dataKey="Waiting Time" stackId="a" fill="#f59e0b" radius={[0, 0, 0, 0]} />
<Bar dataKey="CPU Time" stackId="a" fill="#22c55e" radius={[0, 0, 0, 0]} />
<Bar dataKey="I/O Time" stackId="a" fill="#06b6d4" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
<p className="text-center text-[10px] text-slate-500 mt-1">
Barras empilhadas = Turnaround Time (Waiting + CPU + I/O)
</p>
</div>
);
};
export default MetricsChart;
@@ -0,0 +1,98 @@
import { useState } from 'react';
import { Download, BarChart2, Table, Globe } from 'lucide-react';
import { computeProcessMetrics, computeGlobalMetrics } from '@core/metrics';
import { useSimulationStore } from '../../store/simulationStore';
import { useProcessStore } from '../../store/processStore';
import { useExport } from '../../hooks/useExport';
import MetricsTable from './MetricsTable';
import GlobalMetrics from './GlobalMetrics';
import MetricsChart from './MetricsChart';
type Tab = 'table' | 'global' | 'chart';
const TAB_CONFIG: { id: Tab; label: string; icon: React.ReactNode }[] = [
{ id: 'table', label: 'Por Processo', icon: <Table size={13} /> },
{ id: 'global', label: 'Global', icon: <Globe size={13} /> },
{ id: 'chart', label: 'Gráfico', icon: <BarChart2 size={13} /> },
];
const MetricsPanel = () => {
const [activeTab, setActiveTab] = useState<Tab>('table');
const engine = useSimulationStore(s => s.engine);
const ticks = useSimulationStore(s => s.ticks);
const status = useSimulationStore(s => s.status);
const processes = useProcessStore(s => s.processes);
const { exportCSV } = useExport();
const hasData = ticks.length > 0;
if (!hasData) {
return (
<div className="border-t border-slate-700 bg-slate-950 flex items-center justify-center h-32 text-slate-600 text-sm shrink-0">
Execute a simulação para ver as métricas.
</div>
);
}
// Derive metrics from engine runtime state
const runtimes = engine?.runtimeStates ?? [];
const completedRuntimes = runtimes.filter(rt => rt.finishTick !== null);
const processMetrics = completedRuntimes.map(rt => computeProcessMetrics(rt));
const globalMetrics = computeGlobalMetrics(processMetrics, ticks.length);
const processMap = Object.fromEntries(processes.map(p => [p.id, p]));
const canExportCSV = status === 'finished' && processMetrics.length > 0;
return (
<div className="border-t border-slate-700 bg-slate-950 flex flex-col shrink-0" style={{ height: '280px' }}>
{/* Panel header with tabs + export button */}
<div className="flex items-center px-3 border-b border-slate-700 shrink-0 h-10 gap-1">
{TAB_CONFIG.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md transition-colors ${
activeTab === tab.id
? 'bg-slate-700 text-slate-100 font-medium'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800'
}`}
aria-pressed={activeTab === tab.id}
>
{tab.icon}
{tab.label}
</button>
))}
<div className="ml-auto">
<button
onClick={() => exportCSV(processMetrics, processMap)}
disabled={!canExportCSV}
title={!canExportCSV ? 'Disponível ao concluir a simulação' : 'Exportar métricas como CSV'}
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-slate-200 px-2.5 py-1 rounded-md hover:bg-slate-800 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
aria-label="Exportar métricas CSV"
>
<Download size={12} />
CSV
</button>
</div>
</div>
{/* Tab content */}
<div className="flex-1 overflow-y-auto">
{activeTab === 'table' && (
<MetricsTable metrics={processMetrics} processMap={processMap} />
)}
{activeTab === 'global' && (
<GlobalMetrics metrics={globalMetrics} />
)}
{activeTab === 'chart' && (
<MetricsChart metrics={processMetrics} processMap={processMap} />
)}
</div>
</div>
);
};
export default MetricsPanel;
@@ -0,0 +1,82 @@
import type { ProcessMetrics, Process } from '@core/types';
import Badge from '../shared/Badge';
interface MetricsTableProps {
metrics: ProcessMetrics[];
processMap: Record<string, Process>;
}
function boundVariant(type: ProcessMetrics['boundType']) {
if (type === 'CPU Bound') return 'cpu';
if (type === 'I/O Bound') return 'io';
return 'balanced';
}
const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
if (metrics.length === 0) {
return (
<div className="flex items-center justify-center h-32 text-slate-500 text-sm">
Nenhum processo concluído ainda.
</div>
);
}
return (
<div className="overflow-x-auto">
<table className="w-full text-xs text-slate-300 border-collapse min-w-[700px]">
<thead>
<tr className="text-[10px] text-slate-400 uppercase tracking-wider border-b border-slate-700">
<th className="text-left px-3 py-2 font-medium">Processo</th>
<th className="text-right px-3 py-2 font-medium">Arrival</th>
<th className="text-right px-3 py-2 font-medium">Start</th>
<th className="text-right px-3 py-2 font-medium">Finish</th>
<th className="text-right px-3 py-2 font-medium">Response</th>
<th className="text-right px-3 py-2 font-medium">Turnaround</th>
<th className="text-right px-3 py-2 font-medium">Waiting</th>
<th className="text-right px-3 py-2 font-medium">CPU Time</th>
<th className="text-right px-3 py-2 font-medium">I/O Time</th>
<th className="text-right px-3 py-2 font-medium">CPU%</th>
<th className="text-center px-3 py-2 font-medium">Bound</th>
</tr>
</thead>
<tbody>
{metrics.map((m, i) => {
const proc = processMap[m.processId];
const name = proc?.name ?? m.processId;
const color = proc?.color ?? '#94a3b8';
return (
<tr
key={m.processId}
className={`border-b border-slate-800 hover:bg-slate-800/60 transition-colors ${i % 2 === 0 ? '' : 'bg-slate-900/30'}`}
>
<td className="px-3 py-2">
<div className="flex items-center gap-2">
<span
className="inline-block w-2.5 h-2.5 rounded-full shrink-0"
style={{ backgroundColor: color }}
/>
<span className="font-medium text-slate-200">{name}</span>
</div>
</td>
<td className="text-right px-3 py-2 font-mono">{m.arrivalTime}</td>
<td className="text-right px-3 py-2 font-mono">{m.startTick}</td>
<td className="text-right px-3 py-2 font-mono">{m.finishTick}</td>
<td className="text-right px-3 py-2 font-mono text-amber-300">{m.responseTime}</td>
<td className="text-right px-3 py-2 font-mono text-blue-300">{m.turnaroundTime}</td>
<td className="text-right px-3 py-2 font-mono text-orange-300">{m.waitingTime}</td>
<td className="text-right px-3 py-2 font-mono text-emerald-300">{m.cpuTime}</td>
<td className="text-right px-3 py-2 font-mono text-cyan-300">{m.ioTime}</td>
<td className="text-right px-3 py-2 font-mono">{m.cpuUtilization.toFixed(1)}%</td>
<td className="text-center px-3 py-2">
<Badge variant={boundVariant(m.boundType)}>{m.boundType}</Badge>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
};
export default MetricsTable;
+1 -1
View File
@@ -78,7 +78,7 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
if (bursts.at(-1)?.type !== 'cpu') errs.push('O último burst deve ser CPU');
bursts.forEach((b, i) => {
if (b.duration < 1) errs.push(`Burst ${i + 1}: duração deve ser ≥ 1`);
if (i > 0 && bursts[i - 1].type === b.type) errs.push(`Bursts ${i} e ${i + 1} não podem ser do mesmo tipo (alternar CPU/IO)`);
if (i > 0 && bursts[i - 1]?.type === b.type) errs.push(`Bursts ${i} e ${i + 1} não podem ser do mesmo tipo (alternar CPU/IO)`);
});
setErrors(errs);
return errs.length === 0;
@@ -1,15 +1,16 @@
import type { SchedulingAlgorithm } from '@core/types';
import { useProcessStore } from '../../store/processStore';
const ALGORITHM_OPTIONS: { value: SchedulingAlgorithm; label: string }[] = [
const ALGORITHM_OPTIONS: { value: SchedulingAlgorithm; label: string; disabled?: boolean }[] = [
{ value: 'FCFS', label: 'FCFS — First Come First Served' },
{ value: 'SJF_NP', label: 'SJF — Shortest Job First (NP)' },
{ value: 'SJF_P', label: 'SRTF — Shortest Remaining Time (P)' },
{ value: 'RR', label: 'RR — Round Robin' },
{ value: 'PRIORITY_NP',label: 'Priority (Não-preemptivo)' },
{ value: 'PRIORITY_P', label: 'Priority (Preemptivo)' },
{ value: 'PRIORITY_RR',label: 'Priority Round Robin (tempo real)' },
{ value: 'HRRN', label: 'HRRN — Highest Response Ratio Next' },
{ value: 'MULTILEVEL', label: 'Multilevel Queue' },
{ value: 'MULTILEVEL', label: 'Multilevel Queue (em breve)', disabled: true },
];
const AlgorithmSelector = () => {
@@ -32,7 +33,7 @@ const AlgorithmSelector = () => {
aria-label="Selecionar algoritmo de escalonamento"
>
{ALGORITHM_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>
<option key={opt.value} value={opt.value} disabled={opt.disabled}>
{opt.label}
</option>
))}
@@ -2,13 +2,13 @@ import { useProcessStore } from '../../store/processStore';
import type { SchedulingAlgorithm } from '@core/types';
// Algorithms that require a quantum parameter
const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'MULTILEVEL']);
const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'PRIORITY_RR', 'MULTILEVEL']);
// Algorithms where aging (anti-starvation) is applicable
const AGING_ALGORITHMS = new Set<SchedulingAlgorithm>(['PRIORITY_NP', 'PRIORITY_P']);
const AGING_ALGORITHMS = new Set<SchedulingAlgorithm>(['PRIORITY_NP', 'PRIORITY_P', 'PRIORITY_RR']);
// Algorithms that are preemptive by definition (used for read-only info only)
const PREEMPTIVE_BY_DESIGN = new Set<SchedulingAlgorithm>(['SJF_P', 'RR', 'PRIORITY_P', 'MULTILEVEL']);
const PREEMPTIVE_BY_DESIGN = new Set<SchedulingAlgorithm>(['SJF_P', 'RR', 'PRIORITY_P', 'PRIORITY_RR', 'MULTILEVEL']);
const inputCls = 'w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-sm px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500';
const labelCls = 'block text-xs text-slate-400 mb-1';
+1 -1
View File
@@ -28,5 +28,5 @@ const PALETTE = [
];
let _paletteIndex = 0;
export function nextColor(): string {
return PALETTE[_paletteIndex++ % PALETTE.length];
return PALETTE[_paletteIndex++ % PALETTE.length]!;
}