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:
1 parent
083fc0027e
commit
51a8a6fa40
38 files changed
+2530
-45
No files matched your search
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
@@ -28,5 +28,5 @@ const PALETTE = [
|
||||
];
|
||||
let _paletteIndex = 0;
|
||||
export function nextColor(): string {
|
||||
return PALETTE[_paletteIndex++ % PALETTE.length];
|
||||
return PALETTE[_paletteIndex++ % PALETTE.length]!;
|
||||
}
|
||||
Reference in new issue
Block a user