feat: add MLFQ scheduling algorithm and thread scheduler
- Implemented MLFQ (Multilevel Feedback Queue) algorithm with dynamic priority levels and quantum management. - Created unit tests for MLFQ algorithm covering initialization, demotion, selection, priority boost, preemption, and quantum expiration. - Developed a ThreadScheduler class supporting FCFS, RR, and PRIORITY scheduling policies for intra-process thread management. - Added unit tests for ThreadScheduler to validate thread selection and quantum expiration logic.
This commit is contained in:
1 parent
532c14e382
commit
5761dfe70a
34 files changed
+3789
-334
No files matched your search
@@ -66,7 +66,7 @@ const ControlBar = () => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-16 flex items-center px-5 gap-3 bg-slate-800 border-b border-slate-700 shadow-sm"
|
||||
className="h-16 py-2 flex items-center px-5 gap-3 bg-slate-800 border-b border-slate-700 shadow-sm"
|
||||
role="toolbar"
|
||||
aria-label="Simulation controls"
|
||||
>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useSimulationStore } from "@store/simulationStore";
|
||||
import { useProcessStore } from "@store/processStore";
|
||||
import { useGanttScroll } from "@hooks/useGanttScroll";
|
||||
import GanttRow from "./GanttRow";
|
||||
import GanttThreadRow from "./GanttThreadRow";
|
||||
import GanttLegend from "./GanttLegend";
|
||||
import GanttTooltip, { type TooltipData } from "./GanttTooltip";
|
||||
import { CELL_H, CELL_W } from "./constants";
|
||||
@@ -13,33 +14,29 @@ const GanttChart = () => {
|
||||
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 }}>
|
||||
<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 }}
|
||||
>
|
||||
Start the simulation to see the Gantt chart
|
||||
</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"
|
||||
@@ -48,9 +45,10 @@ const GanttChart = () => {
|
||||
|
||||
{/* 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 }} />
|
||||
|
||||
<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}
|
||||
@@ -62,28 +60,58 @@ const GanttChart = () => {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Process rows */}
|
||||
{/* Process rows (+ thread sub-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>
|
||||
{processes.map((process) => {
|
||||
const hasThreads = (process.threads?.length ?? 0) > 0;
|
||||
|
||||
<GanttRow
|
||||
process={process}
|
||||
ticks={ticks}
|
||||
onCellMouseEnter={setTooltip}
|
||||
onCellMouseLeave={handleMouseLeave}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
return (
|
||||
<div key={process.id}>
|
||||
{/* ── Linha do processo ─────────────────────────── */}
|
||||
<div
|
||||
className={`flex items-center border-b border-white/5 ${hasThreads ? 'border-b-0' : ''}`}
|
||||
>
|
||||
<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>
|
||||
|
||||
{/* ── Sub-linhas de threads ─────────────────────── */}
|
||||
{hasThreads &&
|
||||
process.threads!.map((thread, threadIdx) => (
|
||||
<GanttThreadRow
|
||||
key={thread.tid}
|
||||
thread={thread}
|
||||
threadIndex={threadIdx}
|
||||
process={process}
|
||||
ticks={ticks}
|
||||
onCellMouseEnter={setTooltip}
|
||||
onCellMouseLeave={handleMouseLeave}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Separador após o bloco de threads */}
|
||||
{hasThreads && (
|
||||
<div className="border-b border-white/5" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Current-tick cursor line */}
|
||||
{currentTickIndex >= 0 && (
|
||||
@@ -97,14 +125,7 @@ const GanttChart = () => {
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import React from "react";
|
||||
import type { Process, SimTick, StateLabel, Thread } from "@core/types";
|
||||
import { CELL_W } from "./constants";
|
||||
import type { TooltipData } from "./GanttTooltip";
|
||||
import { deriveThreadColor } from "../shared/threadUtils";
|
||||
|
||||
const THREAD_CELL_H = 20;
|
||||
|
||||
// ── cores de estado (mesmas do processo) ────────────────────
|
||||
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)]",
|
||||
};
|
||||
|
||||
interface GanttThreadRowProps {
|
||||
thread: Thread;
|
||||
threadIndex: number;
|
||||
process: Process;
|
||||
ticks: SimTick[];
|
||||
onCellMouseEnter: (data: TooltipData) => void;
|
||||
onCellMouseLeave: () => void;
|
||||
}
|
||||
|
||||
const GanttThreadRow = React.memo(function GanttThreadRow({
|
||||
thread,
|
||||
threadIndex,
|
||||
process,
|
||||
ticks,
|
||||
onCellMouseEnter,
|
||||
onCellMouseLeave,
|
||||
}: GanttThreadRowProps) {
|
||||
const threadColor = deriveThreadColor(process.color, threadIndex);
|
||||
|
||||
return (
|
||||
<div className="flex items-center border-b border-white/3">
|
||||
{/* Rótulo da thread (sticky) */}
|
||||
<div
|
||||
className="sticky left-0 z-20 bg-slate-900/90 border-r border-slate-700/50 px-2 flex items-center shrink-0 gap-1"
|
||||
style={{ width: 48, height: THREAD_CELL_H }}
|
||||
>
|
||||
<span
|
||||
className="w-1.5 h-1.5 rounded-full shrink-0"
|
||||
style={{ backgroundColor: threadColor }}
|
||||
/>
|
||||
<span
|
||||
className="text-[8px] font-medium truncate"
|
||||
style={{ color: threadColor }}
|
||||
>
|
||||
{thread.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Células da thread */}
|
||||
{ticks.map((tick) => {
|
||||
const threadState: StateLabel =
|
||||
tick.threadStates?.[process.id]?.[thread.tid] ?? "New";
|
||||
|
||||
const isActive = tick.cpuThreads?.[process.id] === thread.tid;
|
||||
const isKernelWait =
|
||||
tick.kernelWaitingThreads?.[process.id]?.includes(thread.tid) ?? false;
|
||||
|
||||
const bg = STATE_CLASS[threadState];
|
||||
|
||||
// Borda colorida para thread ativa (Running)
|
||||
const borderStyle =
|
||||
isActive && threadState === "Running"
|
||||
? { borderLeft: `2px solid ${threadColor}` }
|
||||
: undefined;
|
||||
|
||||
// Padrão diagonal para kernel wait (MANY_TO_MANY)
|
||||
const extraClass = isKernelWait
|
||||
? "opacity-50"
|
||||
: "opacity-80 hover:opacity-100";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={tick.tick}
|
||||
className={`${bg} ${extraClass} shrink-0 transition-opacity cursor-default`}
|
||||
style={{ width: CELL_W, height: THREAD_CELL_H, ...borderStyle }}
|
||||
title={`${thread.name} — tick ${tick.tick}: ${threadState}${isKernelWait ? " (KernelWait)" : ""}`}
|
||||
onMouseEnter={(e) =>
|
||||
onCellMouseEnter({
|
||||
tick,
|
||||
process,
|
||||
state: threadState,
|
||||
isCtxSwitch: false,
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
})
|
||||
}
|
||||
onMouseLeave={onCellMouseLeave}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{ticks.length === 0 && (
|
||||
<div style={{ width: CELL_W, height: THREAD_CELL_H }} className="bg-slate-800/40" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default GanttThreadRow;
|
||||
@@ -86,23 +86,61 @@ const AppHeader = () => {
|
||||
Preset Scenarios
|
||||
</div>
|
||||
<div className="overflow-y-auto">
|
||||
{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>
|
||||
))}
|
||||
{/* Classic process presets */}
|
||||
<div className="px-3 py-1 text-[9px] text-slate-500 uppercase tracking-wider font-semibold bg-slate-800/80 sticky top-0">
|
||||
Processes
|
||||
</div>
|
||||
{presets
|
||||
.filter(p => !['many_to_one','one_to_one','many_to_many','thread_starvation','threads_vs_processes'].includes(p.name))
|
||||
.map((preset) => {
|
||||
const i = presets.indexOf(preset);
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Thread presets */}
|
||||
<div className="px-3 py-1 text-[9px] text-slate-500 uppercase tracking-wider font-semibold bg-slate-800/80 sticky top-0 border-t border-slate-700 mt-1">
|
||||
Threads
|
||||
</div>
|
||||
{presets
|
||||
.filter(p => ['many_to_one','one_to_one','many_to_many','thread_starvation','threads_vs_processes'].includes(p.name))
|
||||
.map((preset) => {
|
||||
const i = presets.indexOf(preset);
|
||||
return (
|
||||
<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="flex items-center gap-1.5">
|
||||
<div className="text-xs font-medium text-slate-200 group-hover:text-white">
|
||||
{preset.name}
|
||||
</div>
|
||||
<span className="text-[8px] px-1 py-0.5 rounded bg-blue-500/20 text-blue-400 border border-blue-500/30 shrink-0">
|
||||
THREADS
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-[11px] text-slate-400 mt-0.5 leading-snug">
|
||||
{preset.description}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -4,12 +4,6 @@ import SchedulerPanel from '../SchedulerPanel/SchedulerPanel';
|
||||
const Sidebar = () => {
|
||||
return (
|
||||
<aside className="w-80 min-w-[20rem] h-full bg-slate-800 border-r border-slate-700 flex flex-col shadow-xl z-20">
|
||||
<div className="h-16 flex items-center px-6 border-b border-slate-700 bg-slate-800/80 backdrop-blur-sm">
|
||||
<h1 className="text-xl font-bold bg-linear-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent truncate">
|
||||
Process Scheduler Simulator
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden p-4 space-y-6 scrollbar-hide">
|
||||
<ProcessPanel />
|
||||
<div className="border-t border-slate-700 pt-4">
|
||||
|
||||
@@ -28,6 +28,22 @@ const TOOLTIP_STYLE = {
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper that gives ResponsiveContainer a bounded, non-growing parent.
|
||||
*
|
||||
* Without this, recharts' ResizeObserver inside a scroll container can trigger
|
||||
* a resize loop: scrollbar appears → available width shrinks → chart redraws →
|
||||
* height shifts → scrollbar disappears → repeat. The explicit height + overflow
|
||||
* hidden caps the chart's DOM footprint and breaks the loop.
|
||||
*/
|
||||
const ChartBox = ({ height, children }: { height: number; children: React.ReactNode }) => (
|
||||
<div style={{ width: '100%', height, overflow: 'hidden', flexShrink: 0 }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
{children as React.ReactElement}
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
|
||||
const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
if (metrics.length === 0) {
|
||||
return (
|
||||
@@ -80,10 +96,9 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
|
||||
// ── 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 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);
|
||||
|
||||
@@ -98,11 +113,11 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
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;
|
||||
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;
|
||||
@@ -110,15 +125,18 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
|
||||
const showRadar = metrics.length >= 2;
|
||||
|
||||
// Charts are always stacked vertically (flex-col) so that the total height
|
||||
// exceeds the panel's viewport, making the scroll area in MetricsPanel
|
||||
// active and accessible on any screen size.
|
||||
return (
|
||||
<div className={`p-3 flex gap-3 ${showRadar ? 'flex-row' : 'flex-col'}`}>
|
||||
<div className="p-3 flex flex-col gap-6">
|
||||
{/* ── Radar chart ───────────────────────────────────────── */}
|
||||
{showRadar && (
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="w-full">
|
||||
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
||||
Comparative Profile (normalized)
|
||||
</p>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<ChartBox height={360}>
|
||||
<RadarChart data={radarData} margin={{ top: 8, right: 20, bottom: 8, left: 20 }}>
|
||||
<PolarGrid stroke="#334155" />
|
||||
<PolarAngleAxis
|
||||
@@ -142,16 +160,16 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
|
||||
/>
|
||||
</RadarChart>
|
||||
</ResponsiveContainer>
|
||||
</ChartBox>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Stacked bar chart ─────────────────────────────────── */}
|
||||
<div className={showRadar ? 'flex-1 min-w-0' : 'w-full'}>
|
||||
<div className="w-full">
|
||||
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
||||
Breakdown per Process (ticks)
|
||||
</p>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<ChartBox height={360}>
|
||||
<BarChart data={barData} margin={{ top: 8, right: 12, left: -8, bottom: 4 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
||||
<XAxis
|
||||
@@ -183,7 +201,7 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
<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>
|
||||
</ChartBox>
|
||||
<p className="text-center text-[9px] text-slate-600 mt-0.5">
|
||||
Waiting + CPU + I/O = Turnaround
|
||||
</p>
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import { useState } from 'react';
|
||||
import { Download, BarChart2, Table, Globe } from 'lucide-react';
|
||||
import { computeProcessMetrics, computeGlobalMetrics } from '@core/metrics';
|
||||
import { Download, BarChart2, Table, Globe, Layers } from 'lucide-react';
|
||||
import { computeProcessMetrics, computeGlobalMetrics, computeThreadMetrics } 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';
|
||||
import ThreadMetricsTable from './ThreadMetricsTable';
|
||||
|
||||
type Tab = 'table' | 'global' | 'charts';
|
||||
|
||||
const TAB_CONFIG: { id: Tab; label: string; icon: React.ReactNode }[] = [
|
||||
{ id: 'table', label: 'Per Process', icon: <Table size={13} /> },
|
||||
{ id: 'global', label: 'Global', icon: <Globe size={13} /> },
|
||||
{ id: 'charts', label: 'Charts', icon: <BarChart2 size={13} /> },
|
||||
];
|
||||
type Tab = 'table' | 'global' | 'charts' | 'threads';
|
||||
|
||||
const MetricsPanel = () => {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('table');
|
||||
@@ -36,20 +31,39 @@ const MetricsPanel = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Derive metrics from engine runtime state
|
||||
// ── Métricas de processos ──────────────────────────────────────────────
|
||||
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]));
|
||||
|
||||
// ── Métricas de threads ────────────────────────────────────────────────
|
||||
const threadRuntimes = engine?.threadRuntimeStates ?? [];
|
||||
const completedThreadRts = threadRuntimes.filter(trt => trt.finishTick !== null);
|
||||
const threadMetrics = completedThreadRts.map(trt => {
|
||||
const process = processMap[trt.processId];
|
||||
const thread = process?.threads?.find(t => t.tid === trt.threadId);
|
||||
if (!process || !thread) return null;
|
||||
return computeThreadMetrics(trt, thread, process);
|
||||
}).filter(Boolean) as ReturnType<typeof computeThreadMetrics>[];
|
||||
|
||||
const hasThreads = processes.some(p => (p.threads?.length ?? 0) > 0);
|
||||
|
||||
const canExportCSV = status === 'finished' && processMetrics.length > 0;
|
||||
|
||||
const TAB_CONFIG: { id: Tab; label: string; icon: React.ReactNode; show: boolean }[] = [
|
||||
{ id: 'table', label: 'Per Process', icon: <Table size={13} />, show: true },
|
||||
{ id: 'global', label: 'Global', icon: <Globe size={13} />, show: true },
|
||||
{ id: 'charts', label: 'Charts', icon: <BarChart2 size={13} />, show: true },
|
||||
{ id: 'threads', label: 'Threads', icon: <Layers size={13} />, show: hasThreads },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="border-t border-slate-700 bg-slate-950 flex flex-col shrink-0" style={{ height: '600px' }}>
|
||||
<div className="border-t border-slate-700 bg-slate-950 flex flex-col overflow-hidden" style={{ height: '600px' }}>
|
||||
{/* 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 => (
|
||||
{TAB_CONFIG.filter(t => t.show).map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
@@ -62,6 +76,11 @@ const MetricsPanel = () => {
|
||||
>
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
{tab.id === 'threads' && threadMetrics.length > 0 && (
|
||||
<span className="text-[9px] bg-blue-500/20 text-blue-400 px-1 rounded">
|
||||
{threadMetrics.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -80,7 +99,7 @@ const MetricsPanel = () => {
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="flex-1 overflow-y-auto min-h-0">
|
||||
{activeTab === 'table' && (
|
||||
<MetricsTable metrics={processMetrics} processMap={processMap} />
|
||||
)}
|
||||
@@ -90,6 +109,9 @@ const MetricsPanel = () => {
|
||||
{activeTab === 'charts' && (
|
||||
<MetricsChart metrics={processMetrics} processMap={processMap} />
|
||||
)}
|
||||
{activeTab === 'threads' && (
|
||||
<ThreadMetricsTable metrics={threadMetrics} processMap={processMap} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import type { Process, ThreadMetrics } from "@core/types";
|
||||
import { deriveThreadColor } from "../shared/threadUtils";
|
||||
|
||||
interface ThreadMetricsTableProps {
|
||||
metrics: ThreadMetrics[];
|
||||
processMap: Record<string, Process>;
|
||||
}
|
||||
|
||||
const MiniBar = ({
|
||||
value,
|
||||
max,
|
||||
color,
|
||||
}: {
|
||||
value: number;
|
||||
max: number;
|
||||
color: string;
|
||||
}) => {
|
||||
const pct = max > 0 ? (value / max) * 100 : 0;
|
||||
return (
|
||||
<div className="relative flex items-center justify-end gap-1.5 min-w-13">
|
||||
<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 ThreadMetricsTable = ({ metrics, processMap }: ThreadMetricsTableProps) => {
|
||||
if (metrics.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-32 text-slate-500 text-sm">
|
||||
No completed threads yet.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Agrupa por processo para mostrar o índice de thread e cor correta
|
||||
const threadIndexByProcess: Record<string, number> = {};
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs text-slate-300 border-collapse min-w-195">
|
||||
<thead>
|
||||
<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">Thread</th>
|
||||
<th className="text-left px-3 py-2 font-medium">Process</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 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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{metrics.map((m, i) => {
|
||||
const proc = processMap[m.processId];
|
||||
const procColor = proc?.color ?? "#94a3b8";
|
||||
|
||||
// Obtém o índice da thread dentro do processo para cor
|
||||
if (threadIndexByProcess[m.processId] === undefined) {
|
||||
threadIndexByProcess[m.processId] = 0;
|
||||
}
|
||||
const threadIdx = threadIndexByProcess[m.processId]!;
|
||||
threadIndexByProcess[m.processId] = threadIdx + 1;
|
||||
|
||||
const threadColor = deriveThreadColor(procColor, threadIdx);
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={`${m.processId}-${m.threadId}`}
|
||||
className={`border-b border-slate-800/80 hover:bg-slate-800/50 transition-colors ${
|
||||
i % 2 === 0 ? "" : "bg-slate-900/20"
|
||||
}`}
|
||||
>
|
||||
{/* Thread name */}
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="inline-block w-2 h-2 rounded-full shrink-0"
|
||||
style={{ backgroundColor: threadColor }}
|
||||
/>
|
||||
<span className="font-medium text-slate-200">{m.threadName}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Process name */}
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="inline-block w-2 h-2 rounded-full shrink-0"
|
||||
style={{ backgroundColor: procColor }}
|
||||
/>
|
||||
<span className="text-slate-400 text-[10px]">
|
||||
{proc?.name ?? m.processId}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThreadMetricsTable;
|
||||
@@ -10,10 +10,28 @@ interface ProcessCardProps {
|
||||
onRemove: (id: string) => void;
|
||||
}
|
||||
|
||||
const MODEL_LABEL: Record<string, string> = {
|
||||
MANY_TO_ONE: 'M:1',
|
||||
ONE_TO_ONE: '1:1',
|
||||
MANY_TO_MANY: 'M:M',
|
||||
};
|
||||
|
||||
const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
||||
const bound = classifyBound(process.bursts);
|
||||
const totalCpu = process.bursts.filter(b => b.type === 'cpu').reduce((a, b) => a + b.duration, 0);
|
||||
const totalIo = process.bursts.filter(b => b.type === 'io').reduce((a, b) => a + b.duration, 0);
|
||||
const hasThreads = (process.threads?.length ?? 0) > 0;
|
||||
|
||||
const bound = hasThreads ? 'Balanced' : classifyBound(process.bursts);
|
||||
const totalCpu = hasThreads
|
||||
? process.threads!.reduce(
|
||||
(sum, t) => sum + t.bursts.filter(b => b.type === 'cpu').reduce((a, b) => a + b.duration, 0),
|
||||
0,
|
||||
)
|
||||
: process.bursts.filter(b => b.type === 'cpu').reduce((a, b) => a + b.duration, 0);
|
||||
const totalIo = hasThreads
|
||||
? process.threads!.reduce(
|
||||
(sum, t) => sum + t.bursts.filter(b => b.type === 'io').reduce((a, b) => a + b.duration, 0),
|
||||
0,
|
||||
)
|
||||
: process.bursts.filter(b => b.type === 'io').reduce((a, b) => a + b.duration, 0);
|
||||
|
||||
const boundVariant = bound === 'CPU Bound' ? 'cpu' : bound === 'I/O Bound' ? 'io' : 'balanced';
|
||||
|
||||
@@ -38,7 +56,13 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
||||
</span>
|
||||
)}
|
||||
<span className="font-semibold text-sm text-slate-100 truncate">{process.name}</span>
|
||||
<Badge variant={boundVariant}>{bound}</Badge>
|
||||
{hasThreads ? (
|
||||
<span className="text-[9px] font-semibold px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-400 border border-blue-500/30 shrink-0">
|
||||
{MODEL_LABEL[process.threadModel ?? 'ONE_TO_ONE']} · {process.threads!.length}T
|
||||
</span>
|
||||
) : (
|
||||
<Badge variant={boundVariant}>{bound}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-3 gap-y-0.5 text-[11px] text-slate-400">
|
||||
<span>Arrival: <span className="text-slate-300 font-medium">{process.arrivalTime}</span></span>
|
||||
@@ -47,7 +71,11 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
||||
{totalIo > 0 && (
|
||||
<span>I/O: <span className="text-cyan-400 font-medium">{totalIo}</span></span>
|
||||
)}
|
||||
<span>Bursts: <span className="text-slate-300 font-medium">{process.bursts.length}</span></span>
|
||||
{hasThreads ? (
|
||||
<span>Threads: <span className="text-slate-300 font-medium">{process.threads!.length}</span></span>
|
||||
) : (
|
||||
<span>Bursts: <span className="text-slate-300 font-medium">{process.bursts.length}</span></span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Plus, Minus, Check, X } from 'lucide-react';
|
||||
import type { BurstSegment, Process } from '@core/types';
|
||||
import type { BurstSegment, IntraProcessPolicy, Process, Thread, ThreadModel } from '@core/types';
|
||||
import { useProcessStore } from '../../store/processStore';
|
||||
import IconButton from '../shared/IconButton';
|
||||
import { generateId, nextColor } from '../shared/processUtils';
|
||||
import ThreadEditor from './ThreadEditor';
|
||||
|
||||
interface ProcessFormProps {
|
||||
/** If provided, the form edits this process; otherwise creates a new one. */
|
||||
editingProcess?: Process | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
type FormTab = 'bursts' | 'threads';
|
||||
|
||||
const DEFAULT_BURSTS: BurstSegment[] = [{ type: 'cpu', duration: 4 }];
|
||||
|
||||
const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
@@ -19,12 +21,22 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
|
||||
const isEditing = !!editingProcess;
|
||||
|
||||
// ── Campos base ─────────────────────────────────────────────────────────
|
||||
const [name, setName] = useState('');
|
||||
const [arrivalTime, setArrivalTime] = useState(0);
|
||||
const [priority, setPriority] = useState(5);
|
||||
const [color, setColor] = useState(nextColor);
|
||||
const [bursts, setBursts] = useState<BurstSegment[]>(DEFAULT_BURSTS);
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
const [, setActiveTab] = useState<FormTab>('bursts');
|
||||
|
||||
// ── Campos de threads ────────────────────────────────────────────────────
|
||||
const [hasThreads, setHasThreads] = useState(false);
|
||||
const [threads, setThreads] = useState<Thread[]>([]);
|
||||
const [threadModel, setThreadModel] = useState<ThreadModel>('ONE_TO_ONE');
|
||||
const [threadPolicy, setThreadPolicy] = useState<IntraProcessPolicy>('FCFS');
|
||||
const [threadQuantum, setThreadQuantum] = useState(2);
|
||||
const [kernelThreadCount, setKernelThreadCount] = useState(2);
|
||||
|
||||
useEffect(() => {
|
||||
if (editingProcess) {
|
||||
@@ -33,24 +45,38 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
setPriority(editingProcess.priority);
|
||||
setColor(editingProcess.color);
|
||||
setBursts([...editingProcess.bursts.map((b: BurstSegment) => ({ ...b }))]);
|
||||
|
||||
const hasT = (editingProcess.threads?.length ?? 0) > 0;
|
||||
setHasThreads(hasT);
|
||||
setThreads(editingProcess.threads ? [...editingProcess.threads] : []);
|
||||
setThreadModel(editingProcess.threadModel ?? 'ONE_TO_ONE');
|
||||
setThreadPolicy(editingProcess.threadSchedulingPolicy ?? 'FCFS');
|
||||
setThreadQuantum(editingProcess.threadQuantum ?? 2);
|
||||
setKernelThreadCount(editingProcess.kernelThreadCount ?? 2);
|
||||
setActiveTab(hasT ? 'threads' : 'bursts');
|
||||
} else {
|
||||
setName('');
|
||||
setArrivalTime(0);
|
||||
setPriority(5);
|
||||
setColor(nextColor());
|
||||
setBursts([{ type: 'cpu', duration: 4 }]);
|
||||
setHasThreads(false);
|
||||
setThreads([]);
|
||||
setThreadModel('ONE_TO_ONE');
|
||||
setThreadPolicy('FCFS');
|
||||
setThreadQuantum(2);
|
||||
setKernelThreadCount(2);
|
||||
setActiveTab('bursts');
|
||||
}
|
||||
setErrors([]);
|
||||
}, [editingProcess]);
|
||||
|
||||
// ---- Burst helpers ----
|
||||
|
||||
// ── Helpers de bursts ───────────────────────────────────────────────────
|
||||
const updateBurstDuration = (index: number, value: number) => {
|
||||
setBursts(prev => prev.map((b, i) => i === index ? { ...b, duration: Math.max(1, value) } : b));
|
||||
};
|
||||
|
||||
const addBurst = () => {
|
||||
// enforce alternating cpu/io pattern
|
||||
const lastType = bursts[bursts.length - 1]?.type ?? 'io';
|
||||
const nextType: BurstSegment['type'] = lastType === 'cpu' ? 'io' : 'cpu';
|
||||
setBursts(prev => [...prev, { type: nextType, duration: 2 }]);
|
||||
@@ -58,36 +84,90 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
|
||||
const removeBurst = (index: number) => {
|
||||
if (bursts.length <= 1) return;
|
||||
setBursts(prev => {
|
||||
const next = prev.filter((_, i) => i !== index);
|
||||
// Safe-guard: ensure it still starts (and ends) with cpu
|
||||
return next;
|
||||
});
|
||||
setBursts(prev => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
// ---- Validation ----
|
||||
const toggleThreads = (enabled: boolean) => {
|
||||
setHasThreads(enabled);
|
||||
if (enabled && threads.length === 0) {
|
||||
setThreads([
|
||||
{ tid: `t_${generateId()}`, name: 'T1', priority: 5, bursts: [{ type: 'cpu', duration: 3 }] },
|
||||
{ tid: `t_${generateId()}`, name: 'T2', priority: 5, bursts: [{ type: 'cpu', duration: 2 }] },
|
||||
]);
|
||||
}
|
||||
setActiveTab(enabled ? 'threads' : 'bursts');
|
||||
};
|
||||
|
||||
// ── Validação ───────────────────────────────────────────────────────────
|
||||
const validate = (): boolean => {
|
||||
const errs: string[] = [];
|
||||
if (!name.trim()) errs.push('Name is required');
|
||||
if (name.trim().length > 8) errs.push('Name must be at most 8 characters');
|
||||
if (arrivalTime < 0) errs.push('Arrival Time must be ≥ 0');
|
||||
if (priority < 1 || priority > 10) errs.push('Priority must be between 1 and 10');
|
||||
if (bursts.length === 0) errs.push('There must be at least 1 CPU burst');
|
||||
if (bursts[0]?.type !== 'cpu') errs.push('The first burst must be CPU');
|
||||
if (bursts.at(-1)?.type !== 'cpu') errs.push('The last burst must be CPU');
|
||||
bursts.forEach((b, i) => {
|
||||
if (b.duration < 1) errs.push(`Burst ${i + 1}: duration must be ≥ 1`);
|
||||
if (i > 0 && bursts[i - 1]?.type === b.type) errs.push(`Bursts ${i} and ${i + 1} cannot be the same type (alternate CPU/IO)`);
|
||||
});
|
||||
|
||||
if (hasThreads) {
|
||||
if (threads.length === 0) errs.push('At least one thread is required');
|
||||
threads.forEach((t, ti) => {
|
||||
if (!t.name.trim()) errs.push(`Thread ${ti + 1}: name is required`);
|
||||
if (t.bursts.length === 0) errs.push(`Thread ${ti + 1}: at least 1 burst required`);
|
||||
if (t.bursts[0]?.type !== 'cpu') errs.push(`Thread ${ti + 1}: first burst must be CPU`);
|
||||
if (t.bursts.at(-1)?.type !== 'cpu') errs.push(`Thread ${ti + 1}: last burst must be CPU`);
|
||||
t.bursts.forEach((b, bi) => {
|
||||
if (b.duration < 1) errs.push(`Thread ${ti + 1}, burst ${bi + 1}: duration must be ≥ 1`);
|
||||
if (bi > 0 && t.bursts[bi - 1]?.type === b.type)
|
||||
errs.push(`Thread ${ti + 1}: bursts must alternate CPU/IO`);
|
||||
});
|
||||
});
|
||||
if (threadModel === 'MANY_TO_MANY' && kernelThreadCount > threads.length) {
|
||||
errs.push(`Kernel threads (${kernelThreadCount}) cannot exceed user threads (${threads.length})`);
|
||||
}
|
||||
} else {
|
||||
if (bursts.length === 0) errs.push('There must be at least 1 CPU burst');
|
||||
if (bursts[0]?.type !== 'cpu') errs.push('The first burst must be CPU');
|
||||
if (bursts.at(-1)?.type !== 'cpu') errs.push('The last burst must be CPU');
|
||||
bursts.forEach((b, i) => {
|
||||
if (b.duration < 1) errs.push(`Burst ${i + 1}: duration must be ≥ 1`);
|
||||
if (i > 0 && bursts[i - 1]?.type === b.type)
|
||||
errs.push(`Bursts ${i} and ${i + 1} cannot be the same type`);
|
||||
});
|
||||
}
|
||||
|
||||
setErrors(errs);
|
||||
return errs.length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!validate()) return;
|
||||
|
||||
const processData: Partial<Process> = {
|
||||
name: name.trim(),
|
||||
arrivalTime,
|
||||
priority,
|
||||
color,
|
||||
bursts: hasThreads ? [] : bursts,
|
||||
...(hasThreads
|
||||
? {
|
||||
threads,
|
||||
threadModel,
|
||||
threadSchedulingPolicy: threadPolicy,
|
||||
threadQuantum,
|
||||
kernelThreadCount:
|
||||
threadModel === 'MANY_TO_MANY'
|
||||
? kernelThreadCount
|
||||
: undefined,
|
||||
}
|
||||
: {
|
||||
threads: undefined,
|
||||
threadModel: undefined,
|
||||
threadSchedulingPolicy: undefined,
|
||||
threadQuantum: undefined,
|
||||
kernelThreadCount: undefined,
|
||||
}),
|
||||
};
|
||||
|
||||
if (isEditing && editingProcess) {
|
||||
updateProcess(editingProcess.id, { name: name.trim(), arrivalTime, priority, color, bursts });
|
||||
updateProcess(editingProcess.id, processData);
|
||||
} else {
|
||||
addProcess({
|
||||
id: generateId(),
|
||||
@@ -95,7 +175,17 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
arrivalTime,
|
||||
priority,
|
||||
color,
|
||||
bursts,
|
||||
bursts: hasThreads ? [] : bursts,
|
||||
...(hasThreads
|
||||
? {
|
||||
threads,
|
||||
threadModel,
|
||||
threadSchedulingPolicy: threadPolicy,
|
||||
threadQuantum,
|
||||
kernelThreadCount:
|
||||
threadModel === 'MANY_TO_MANY' ? kernelThreadCount : undefined,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
@@ -118,7 +208,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
{/* Basic fields */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="col-span-2 flex gap-3">
|
||||
{/* Name */}
|
||||
<div className="flex-1">
|
||||
<label className={labelCls} htmlFor="pf-name">Name</label>
|
||||
<input
|
||||
@@ -132,7 +221,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
aria-label="Process name"
|
||||
/>
|
||||
</div>
|
||||
{/* Color */}
|
||||
<div>
|
||||
<label className={labelCls} htmlFor="pf-color">Color</label>
|
||||
<input
|
||||
@@ -146,7 +234,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Arrival */}
|
||||
<div>
|
||||
<label className={labelCls} htmlFor="pf-arrival">Arrival Time</label>
|
||||
<input
|
||||
@@ -160,7 +247,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Priority */}
|
||||
<div>
|
||||
<label className={labelCls} htmlFor="pf-priority">Priority (1–10)</label>
|
||||
<input
|
||||
@@ -176,51 +262,91 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bursts editor */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-xs text-slate-400 font-medium">Bursts (CPU → I/O → CPU…)</label>
|
||||
<IconButton size="sm" variant="ghost" label="Add burst" onClick={addBurst}>
|
||||
<Plus size={13} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className="space-y-1.5" data-testid="burst-list">
|
||||
{bursts.map((burst, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`flex items-center gap-2 px-2 py-1.5 rounded-md border ${
|
||||
burst.type === 'cpu'
|
||||
? 'border-emerald-700/50 bg-emerald-900/20'
|
||||
: 'border-cyan-700/50 bg-cyan-900/20'
|
||||
}`}
|
||||
>
|
||||
<span className={`text-[10px] font-bold uppercase w-7 tracking-wider ${burst.type === 'cpu' ? 'text-emerald-400' : 'text-cyan-400'}`}>
|
||||
{burst.type}
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={burst.duration}
|
||||
onChange={e => updateBurstDuration(idx, Number(e.target.value))}
|
||||
className="w-full rounded bg-slate-700/60 border border-slate-600/50 text-slate-200 text-xs px-2 py-0.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
aria-label={`Burst ${idx + 1} duration`}
|
||||
/>
|
||||
<span className="text-[10px] text-slate-500">ticks</span>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
label={`Remove burst ${idx + 1}`}
|
||||
onClick={() => removeBurst(idx)}
|
||||
disabled={bursts.length <= 1}
|
||||
className="hover:text-red-400"
|
||||
>
|
||||
<Minus size={12} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
{/* Toggle threads */}
|
||||
<div className="flex items-center justify-between border-t border-slate-700 pt-3">
|
||||
<div>
|
||||
<span className="text-xs text-slate-300 font-medium">Use Threads</span>
|
||||
<p className="text-[10px] text-slate-500">Substitui bursts por threads com execução própria</p>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={hasThreads}
|
||||
onClick={() => toggleThreads(!hasThreads)}
|
||||
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
|
||||
hasThreads ? 'bg-blue-600' : 'bg-slate-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-3.5 w-3.5 rounded-full bg-white shadow transition-transform duration-200 ${
|
||||
hasThreads ? 'translate-x-4' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs: Bursts / Threads */}
|
||||
{!hasThreads ? (
|
||||
/* ── Editor de bursts (original) ─────────────────────────── */
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-xs text-slate-400 font-medium">Bursts (CPU → I/O → CPU…)</label>
|
||||
<IconButton size="sm" variant="ghost" label="Add burst" onClick={addBurst}>
|
||||
<Plus size={13} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className="space-y-1.5" data-testid="burst-list">
|
||||
{bursts.map((burst, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`flex items-center gap-2 px-2 py-1.5 rounded-md border ${
|
||||
burst.type === 'cpu'
|
||||
? 'border-emerald-700/50 bg-emerald-900/20'
|
||||
: 'border-cyan-700/50 bg-cyan-900/20'
|
||||
}`}
|
||||
>
|
||||
<span className={`text-[10px] font-bold uppercase w-7 tracking-wider ${burst.type === 'cpu' ? 'text-emerald-400' : 'text-cyan-400'}`}>
|
||||
{burst.type}
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={burst.duration}
|
||||
onChange={e => updateBurstDuration(idx, Number(e.target.value))}
|
||||
className="w-full rounded bg-slate-700/60 border border-slate-600/50 text-slate-200 text-xs px-2 py-0.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
aria-label={`Burst ${idx + 1} duration`}
|
||||
/>
|
||||
<span className="text-[10px] text-slate-500">ticks</span>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
label={`Remove burst ${idx + 1}`}
|
||||
onClick={() => removeBurst(idx)}
|
||||
disabled={bursts.length <= 1}
|
||||
className="hover:text-red-400"
|
||||
>
|
||||
<Minus size={12} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* ── Editor de threads ────────────────────────────────────── */
|
||||
<ThreadEditor
|
||||
threads={threads}
|
||||
threadModel={threadModel}
|
||||
threadSchedulingPolicy={threadPolicy}
|
||||
threadQuantum={threadQuantum}
|
||||
kernelThreadCount={kernelThreadCount}
|
||||
processColor={color}
|
||||
onChange={setThreads}
|
||||
onModelChange={setThreadModel}
|
||||
onPolicyChange={setThreadPolicy}
|
||||
onQuantumChange={setThreadQuantum}
|
||||
onKernelCountChange={setKernelThreadCount}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Validation errors */}
|
||||
{errors.length > 0 && (
|
||||
<ul className="space-y-0.5" role="alert" aria-live="polite">
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
import { useState } from 'react';
|
||||
import { Plus, Minus, Trash2, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import type { BurstSegment, IntraProcessPolicy, Thread, ThreadModel } from '@core/types';
|
||||
import { generateId } from '../shared/processUtils';
|
||||
import { deriveThreadColor } from '../shared/threadUtils';
|
||||
import IconButton from '../shared/IconButton';
|
||||
|
||||
interface ThreadEditorProps {
|
||||
threads: Thread[];
|
||||
threadModel: ThreadModel;
|
||||
threadSchedulingPolicy: IntraProcessPolicy;
|
||||
threadQuantum: number;
|
||||
kernelThreadCount: number;
|
||||
processColor: string;
|
||||
onChange: (threads: Thread[]) => void;
|
||||
onModelChange: (model: ThreadModel) => void;
|
||||
onPolicyChange: (policy: IntraProcessPolicy) => void;
|
||||
onQuantumChange: (q: number) => void;
|
||||
onKernelCountChange: (k: number) => void;
|
||||
}
|
||||
|
||||
const MODEL_OPTIONS: { value: ThreadModel; label: string; desc: string }[] = [
|
||||
{
|
||||
value: 'MANY_TO_ONE',
|
||||
label: 'Many-to-One',
|
||||
desc: 'N threads → 1 kernel. I/O bloqueia todo o processo (green threads).',
|
||||
},
|
||||
{
|
||||
value: 'ONE_TO_ONE',
|
||||
label: 'One-to-One',
|
||||
desc: 'N threads → N kernel threads. Bloqueios são independentes (pthreads).',
|
||||
},
|
||||
{
|
||||
value: 'MANY_TO_MANY',
|
||||
label: 'Many-to-Many',
|
||||
desc: 'N threads → M kernel threads (M ≤ N). Slots liberados em I/O.',
|
||||
},
|
||||
];
|
||||
|
||||
const POLICY_OPTIONS: { value: IntraProcessPolicy; label: string }[] = [
|
||||
{ value: 'FCFS', label: 'FCFS' },
|
||||
{ value: 'RR', label: 'Round Robin' },
|
||||
{ value: 'PRIORITY', label: 'Priority' },
|
||||
];
|
||||
|
||||
const inputCls = 'w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500';
|
||||
const labelCls = 'block text-[10px] text-slate-400 mb-0.5';
|
||||
|
||||
// ── Burst editor interna de cada thread ───────────────────────────────────
|
||||
const ThreadBurstEditor = ({
|
||||
bursts,
|
||||
onChange,
|
||||
}: {
|
||||
bursts: BurstSegment[];
|
||||
onChange: (b: BurstSegment[]) => void;
|
||||
}) => {
|
||||
const addBurst = () => {
|
||||
const lastType = bursts[bursts.length - 1]?.type ?? 'io';
|
||||
const nextType: BurstSegment['type'] = lastType === 'cpu' ? 'io' : 'cpu';
|
||||
onChange([...bursts, { type: nextType, duration: 2 }]);
|
||||
};
|
||||
|
||||
const removeBurst = (idx: number) => {
|
||||
if (bursts.length <= 1) return;
|
||||
onChange(bursts.filter((_, i) => i !== idx));
|
||||
};
|
||||
|
||||
const updateDuration = (idx: number, val: number) => {
|
||||
onChange(bursts.map((b, i) => (i === idx ? { ...b, duration: Math.max(1, val) } : b)));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-1 mt-1.5">
|
||||
{bursts.map((b, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`flex items-center gap-1.5 px-2 py-1 rounded border ${
|
||||
b.type === 'cpu'
|
||||
? 'border-emerald-700/40 bg-emerald-900/15'
|
||||
: 'border-cyan-700/40 bg-cyan-900/15'
|
||||
}`}
|
||||
>
|
||||
<span className={`text-[9px] font-bold uppercase w-6 tracking-wide shrink-0 ${b.type === 'cpu' ? 'text-emerald-400' : 'text-cyan-400'}`}>
|
||||
{b.type}
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={b.duration}
|
||||
onChange={e => updateDuration(idx, Number(e.target.value))}
|
||||
className="w-14 rounded bg-slate-700/60 border border-slate-600/50 text-slate-200 text-[10px] px-1.5 py-0.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-[9px] text-slate-500">ticks</span>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
label="Remove burst"
|
||||
onClick={() => removeBurst(idx)}
|
||||
disabled={bursts.length <= 1}
|
||||
className="ml-auto hover:text-red-400"
|
||||
>
|
||||
<Minus size={10} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={addBurst}
|
||||
className="flex items-center gap-1 text-[10px] text-blue-400 hover:text-blue-300 transition-colors"
|
||||
>
|
||||
<Plus size={10} /> burst
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ── Card de thread individual ─────────────────────────────────────────────
|
||||
const ThreadCard = ({
|
||||
thread,
|
||||
index,
|
||||
processColor,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
canRemove,
|
||||
}: {
|
||||
thread: Thread;
|
||||
index: number;
|
||||
processColor: string;
|
||||
onUpdate: (t: Thread) => void;
|
||||
onRemove: () => void;
|
||||
canRemove: boolean;
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const color = deriveThreadColor(processColor, index);
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-slate-600 bg-slate-800/50 overflow-hidden">
|
||||
<div className="flex items-center gap-2 px-2.5 py-1.5">
|
||||
<span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: color }} />
|
||||
<input
|
||||
type="text"
|
||||
maxLength={8}
|
||||
value={thread.name}
|
||||
onChange={e => onUpdate({ ...thread, name: e.target.value })}
|
||||
className="flex-1 bg-transparent text-slate-200 text-xs font-medium focus:outline-none focus:ring-1 focus:ring-blue-500 rounded px-1"
|
||||
aria-label={`Thread ${index + 1} name`}
|
||||
/>
|
||||
<span className="text-[9px] text-slate-500 font-mono shrink-0">T{index + 1}</span>
|
||||
|
||||
{/* Priority */}
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
title="Priority (1=high, 10=low)"
|
||||
value={thread.priority ?? 5}
|
||||
onChange={e => onUpdate({ ...thread, priority: Math.min(10, Math.max(1, Number(e.target.value))) })}
|
||||
className="w-10 rounded bg-slate-700 border border-slate-600 text-slate-200 text-[10px] px-1 py-0.5 focus:outline-none focus:ring-1 focus:ring-blue-500 text-center"
|
||||
aria-label={`Thread ${index + 1} priority`}
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={() => setExpanded(v => !v)}
|
||||
className="text-slate-500 hover:text-slate-300 transition-colors"
|
||||
aria-label={expanded ? 'Collapse' : 'Expand'}
|
||||
>
|
||||
{expanded ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
|
||||
</button>
|
||||
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
label="Remove thread"
|
||||
onClick={onRemove}
|
||||
disabled={!canRemove}
|
||||
className="hover:text-red-400"
|
||||
>
|
||||
<Trash2 size={11} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className="px-2.5 pb-2 border-t border-slate-700/50">
|
||||
<ThreadBurstEditor
|
||||
bursts={thread.bursts}
|
||||
onChange={bursts => onUpdate({ ...thread, bursts })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ── ThreadEditor principal ────────────────────────────────────────────────
|
||||
const ThreadEditor = ({
|
||||
threads,
|
||||
threadModel,
|
||||
threadSchedulingPolicy,
|
||||
threadQuantum,
|
||||
kernelThreadCount,
|
||||
processColor,
|
||||
onChange,
|
||||
onModelChange,
|
||||
onPolicyChange,
|
||||
onQuantumChange,
|
||||
onKernelCountChange,
|
||||
}: ThreadEditorProps) => {
|
||||
const addThread = () => {
|
||||
const newThread: Thread = {
|
||||
tid: `t_${generateId()}`,
|
||||
name: `T${threads.length + 1}`,
|
||||
priority: 5,
|
||||
bursts: [{ type: 'cpu', duration: 3 }],
|
||||
};
|
||||
onChange([...threads, newThread]);
|
||||
};
|
||||
|
||||
const updateThread = (idx: number, updated: Thread) => {
|
||||
onChange(threads.map((t, i) => (i === idx ? updated : t)));
|
||||
};
|
||||
|
||||
const removeThread = (idx: number) => {
|
||||
onChange(threads.filter((_, i) => i !== idx));
|
||||
};
|
||||
|
||||
const selectedModel = MODEL_OPTIONS.find(m => m.value === threadModel);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{/* Modelo de threads */}
|
||||
<div>
|
||||
<label className={labelCls}>Thread Model</label>
|
||||
<select
|
||||
value={threadModel}
|
||||
onChange={e => onModelChange(e.target.value as ThreadModel)}
|
||||
className="w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
|
||||
aria-label="Thread model"
|
||||
>
|
||||
{MODEL_OPTIONS.map(m => (
|
||||
<option key={m.value} value={m.value}>{m.label}</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedModel && (
|
||||
<p className="text-[10px] text-slate-500 mt-1">{selectedModel.desc}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Kernel thread count — apenas MANY_TO_MANY */}
|
||||
{threadModel === 'MANY_TO_MANY' && (
|
||||
<div>
|
||||
<label className={labelCls}>
|
||||
Kernel Threads (M ≤ {threads.length || '?'})
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={Math.max(1, threads.length)}
|
||||
value={kernelThreadCount}
|
||||
onChange={e =>
|
||||
onKernelCountChange(Math.min(threads.length || 1, Math.max(1, Number(e.target.value))))
|
||||
}
|
||||
className={inputCls}
|
||||
aria-label="Kernel thread count"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Política intra-processo */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className={labelCls}>Intra-process Policy</label>
|
||||
<select
|
||||
value={threadSchedulingPolicy}
|
||||
onChange={e => onPolicyChange(e.target.value as IntraProcessPolicy)}
|
||||
className="w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
|
||||
aria-label="Thread scheduling policy"
|
||||
>
|
||||
{POLICY_OPTIONS.map(p => (
|
||||
<option key={p.value} value={p.value}>{p.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{threadSchedulingPolicy === 'RR' && (
|
||||
<div>
|
||||
<label className={labelCls}>Thread Quantum</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={threadQuantum}
|
||||
onChange={e => onQuantumChange(Math.max(1, Number(e.target.value)))}
|
||||
className={inputCls}
|
||||
aria-label="Thread quantum"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Lista de threads */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<label className="text-[10px] text-slate-400 font-medium">
|
||||
Threads ({threads.length})
|
||||
</label>
|
||||
<button
|
||||
onClick={addThread}
|
||||
disabled={threads.length >= 8}
|
||||
className="flex items-center gap-1 text-[10px] text-blue-400 hover:text-blue-300 disabled:text-slate-600 disabled:cursor-not-allowed transition-colors"
|
||||
aria-label="Add thread"
|
||||
>
|
||||
<Plus size={11} /> Thread
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
{threads.map((thread, idx) => (
|
||||
<ThreadCard
|
||||
key={thread.tid}
|
||||
thread={thread}
|
||||
index={idx}
|
||||
processColor={processColor}
|
||||
onUpdate={updated => updateThread(idx, updated)}
|
||||
onRemove={() => removeThread(idx)}
|
||||
canRemove={threads.length > 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{threads.length === 0 && (
|
||||
<div className="text-center py-4 text-[10px] text-slate-500">
|
||||
Nenhuma thread. Clique em "+ Thread" para adicionar.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThreadEditor;
|
||||
@@ -3,14 +3,15 @@ import { useProcessStore } from '../../store/processStore';
|
||||
|
||||
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: 'SJF_NP', label: 'SJF — Shortest Job First (Non-preemptive)' },
|
||||
{ value: 'SJF_P', label: 'SRTF — Shortest Remaining Time (Preemptive)' },
|
||||
{ value: 'RR', label: 'RR — Round Robin' },
|
||||
{ value: 'PRIORITY_NP',label: 'Priority (Non-preemptive)' },
|
||||
{ value: 'PRIORITY_P', label: 'Priority (Preemptive)' },
|
||||
{ value: 'PRIORITY_RR',label: 'Priority Round Robin (real-time)' },
|
||||
{ value: 'HRRN', label: 'HRRN — Highest Response Ratio Next' },
|
||||
{ value: 'MULTILEVEL', label: 'Multilevel Queue (MLQ)' },
|
||||
{ value: 'MULTILEVEL', label: 'MLQ — Multilevel Queue' },
|
||||
{ value: 'MLFQ', label: 'MLFQ — Multilevel Feedback Queue' },
|
||||
];
|
||||
|
||||
const AlgorithmSelector = () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
import { useProcessStore } from '../../store/processStore';
|
||||
import type { MlqQueueDef, SchedulingAlgorithm } from '@core/types';
|
||||
import type { MlfqLevelDef, MlqQueueDef, SchedulingAlgorithm } from '@core/types';
|
||||
import { MLFQAlgorithm } from '@core/algorithms/mlfq';
|
||||
|
||||
// Algorithms that require a global quantum parameter
|
||||
const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'PRIORITY_RR']);
|
||||
@@ -9,7 +10,7 @@ const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'PRIORITY_RR']);
|
||||
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', 'PRIORITY_RR', 'MULTILEVEL']);
|
||||
const PREEMPTIVE_BY_DESIGN = new Set<SchedulingAlgorithm>(['SJF_P', 'RR', 'PRIORITY_P', 'PRIORITY_RR', 'MULTILEVEL', 'MLFQ']);
|
||||
|
||||
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';
|
||||
@@ -153,6 +154,94 @@ const MlqQueuesConfig = ({
|
||||
);
|
||||
};
|
||||
|
||||
const MlfqLevelsConfig = ({
|
||||
levels,
|
||||
boostInterval,
|
||||
onLevelsChange,
|
||||
onBoostChange,
|
||||
}: {
|
||||
levels: MlfqLevelDef[];
|
||||
boostInterval: number;
|
||||
onLevelsChange: (levels: MlfqLevelDef[]) => void;
|
||||
onBoostChange: (v: number) => void;
|
||||
}) => {
|
||||
const update = (idx: number, quantum: number) => {
|
||||
onLevelsChange(levels.map((l, i) => (i === idx ? { quantum } : l)));
|
||||
};
|
||||
|
||||
const addLevel = () => {
|
||||
if (levels.length >= 5) return;
|
||||
onLevelsChange([...levels, { quantum: levels[levels.length - 1]!.quantum * 2 }]);
|
||||
};
|
||||
|
||||
const removeLevel = (idx: number) => {
|
||||
if (levels.length <= 1) return;
|
||||
onLevelsChange(levels.filter((_, i) => i !== idx));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2 pt-1" data-testid="mlfq-levels-config">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-400 font-medium">MLFQ Levels (decreasing priority)</span>
|
||||
<button
|
||||
onClick={addLevel}
|
||||
disabled={levels.length >= 5}
|
||||
className="flex items-center gap-1 text-[10px] text-blue-400 hover:text-blue-300 disabled:text-slate-600 disabled:cursor-not-allowed transition-colors"
|
||||
aria-label="Add MLFQ level"
|
||||
>
|
||||
<Plus size={11} /> Level
|
||||
</button>
|
||||
</div>
|
||||
{levels.map((l, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="rounded-md border border-slate-600 bg-slate-800/60 p-2 space-y-1.5"
|
||||
data-testid={`mlfq-level-${idx}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-slate-400 font-semibold uppercase tracking-wide">
|
||||
Level {idx} {idx === 0 ? '(highest)' : idx === levels.length - 1 ? '(lowest)' : ''}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => removeLevel(idx)}
|
||||
disabled={levels.length <= 1}
|
||||
className="text-slate-500 hover:text-red-400 disabled:text-slate-700 disabled:cursor-not-allowed transition-colors"
|
||||
aria-label={`Remove level ${idx}`}
|
||||
>
|
||||
<Trash2 size={11} />
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] text-slate-500 mb-0.5">Quantum (ticks)</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={l.quantum}
|
||||
onChange={e => update(idx, Math.max(1, Number(e.target.value)))}
|
||||
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
aria-label={`Level ${idx}: quantum`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<label className="block text-[10px] text-slate-500 mb-0.5">
|
||||
Boost Interval (ticks, 0 = disabled)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
value={boostInterval}
|
||||
onChange={e => onBoostChange(Math.max(0, Number(e.target.value)))}
|
||||
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
aria-label="MLFQ boost interval"
|
||||
data-testid="mlfq-boost-interval"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SchedulerConfig = () => {
|
||||
const config = useProcessStore(s => s.config);
|
||||
const setConfig = useProcessStore(s => s.setConfig);
|
||||
@@ -250,6 +339,16 @@ const SchedulerConfig = () => {
|
||||
onChange={queues => setConfig({ mlqQueues: queues })}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* MLFQ Level Configuration */}
|
||||
{algo === 'MLFQ' && (
|
||||
<MlfqLevelsConfig
|
||||
levels={config.mlfqLevels ?? MLFQAlgorithm.DEFAULT_LEVELS}
|
||||
boostInterval={config.mlfqBoostInterval ?? 0}
|
||||
onLevelsChange={levels => setConfig({ mlfqLevels: levels })}
|
||||
onBoostChange={v => setConfig({ mlfqBoostInterval: v })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Deriva uma cor para uma thread a partir da cor base do processo.
|
||||
* Desloca o matiz (hue) em 35° por índice de thread para garantir
|
||||
* cores visualmente distintas dentro do mesmo processo.
|
||||
*/
|
||||
export function deriveThreadColor(processColor: string, threadIndex: number): string {
|
||||
const [h, s, l] = hexToHsl(processColor);
|
||||
const newH = (h + threadIndex * 40) % 360;
|
||||
const newL = Math.min(80, Math.max(35, l + (threadIndex % 2 === 0 ? 8 : -8)));
|
||||
return hslToHex(newH, Math.min(100, s + 10), newL);
|
||||
}
|
||||
|
||||
function hexToHsl(hex: string): [number, number, number] {
|
||||
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
||||
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
||||
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
let h = 0;
|
||||
let s = 0;
|
||||
const l = (max + min) / 2;
|
||||
|
||||
if (max !== min) {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
||||
case g: h = ((b - r) / d + 2) / 6; break;
|
||||
case b: h = ((r - g) / d + 4) / 6; break;
|
||||
}
|
||||
}
|
||||
|
||||
return [h * 360, s * 100, l * 100];
|
||||
}
|
||||
|
||||
function hslToHex(h: number, s: number, l: number): string {
|
||||
h /= 360; s /= 100; l /= 100;
|
||||
let r: number, g: number, b: number;
|
||||
|
||||
if (s === 0) {
|
||||
r = g = b = l;
|
||||
} else {
|
||||
const hue2rgb = (p: number, q: number, t: number) => {
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||
if (t < 1 / 2) return q;
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||
return p;
|
||||
};
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
const p = 2 * l - q;
|
||||
r = hue2rgb(p, q, h + 1 / 3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1 / 3);
|
||||
}
|
||||
|
||||
const toHex = (x: number) =>
|
||||
Math.round(x * 255).toString(16).padStart(2, "0");
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
}
|
||||
Reference in new issue
Block a user