feat: update version to 1.3.1, enhance GanttChart and MetricsPanel, and add ThreadMetricsChart
This commit is contained in:
1 parent
5761dfe70a
commit
44ee62e2c2
8 files changed
+303
-20
No files matched your search
@@ -47,7 +47,7 @@ const GanttChart = () => {
|
||||
<div className="flex sticky top-0 z-10 bg-slate-950 border-b border-slate-800">
|
||||
<div
|
||||
className="sticky left-0 z-20 bg-slate-950 border-r border-slate-700 shrink-0"
|
||||
style={{ width: 48, height: 16 }}
|
||||
style={{ width: 60, height: 16 }}
|
||||
/>
|
||||
{tickNumbers.map((t) => (
|
||||
<div
|
||||
@@ -73,7 +73,7 @@ const GanttChart = () => {
|
||||
>
|
||||
<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 }}
|
||||
style={{ width: 60, height: CELL_H }}
|
||||
>
|
||||
<span
|
||||
className="text-[10px] font-bold truncate"
|
||||
@@ -117,7 +117,7 @@ const GanttChart = () => {
|
||||
{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 }}
|
||||
style={{ left: 60 + currentTickIndex * CELL_W + CELL_W / 2 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,7 @@ const GanttThreadRow = React.memo(function GanttThreadRow({
|
||||
{/* 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 }}
|
||||
style={{ width: 60, height: THREAD_CELL_H }}
|
||||
>
|
||||
<span
|
||||
className="w-1.5 h-1.5 rounded-full shrink-0"
|
||||
|
||||
@@ -4,7 +4,7 @@ import MetricsPanel from '../MetricsPanel/MetricsPanel';
|
||||
|
||||
const MainArea = () => {
|
||||
return (
|
||||
<main className="flex-1 flex flex-col h-full bg-slate-900 overflow-hidden relative">
|
||||
<main className="flex-1 flex flex-col bg-slate-900 overflow-y-auto relative">
|
||||
<ControlBar />
|
||||
<GanttChart />
|
||||
<MetricsPanel />
|
||||
|
||||
@@ -8,8 +8,9 @@ import MetricsTable from './MetricsTable';
|
||||
import GlobalMetrics from './GlobalMetrics';
|
||||
import MetricsChart from './MetricsChart';
|
||||
import ThreadMetricsTable from './ThreadMetricsTable';
|
||||
import ThreadMetricsChart from './ThreadMetricsChart';
|
||||
|
||||
type Tab = 'table' | 'global' | 'charts' | 'threads';
|
||||
type Tab = 'table' | 'global' | 'charts' | 'threads' | 'thread-charts';
|
||||
|
||||
const MetricsPanel = () => {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('table');
|
||||
@@ -53,14 +54,15 @@ const MetricsPanel = () => {
|
||||
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 },
|
||||
{ id: 'table', label: 'Per Process', icon: <Table size={13} />, show: true },
|
||||
{ id: 'global', label: 'Global', icon: <Globe size={13} />, show: true },
|
||||
{ id: 'charts', label: 'Process Charts', icon: <BarChart2 size={13} />, show: true },
|
||||
{ id: 'threads', label: 'Threads', icon: <Layers size={13} />, show: hasThreads },
|
||||
{ id: 'thread-charts', label: 'Thread Charts', icon: <BarChart2 size={13} />, show: hasThreads },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="border-t border-slate-700 bg-slate-950 flex flex-col overflow-hidden" style={{ height: '600px' }}>
|
||||
<div className="border-t border-slate-700 bg-slate-950 flex flex-col">
|
||||
{/* 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.filter(t => t.show).map(tab => (
|
||||
@@ -86,7 +88,7 @@ const MetricsPanel = () => {
|
||||
|
||||
<div className="ml-auto">
|
||||
<button
|
||||
onClick={() => exportCSV(processMetrics, processMap)}
|
||||
onClick={() => exportCSV(processMetrics, processMap, hasThreads ? threadMetrics : undefined)}
|
||||
disabled={!canExportCSV}
|
||||
title={!canExportCSV ? 'Available when simulation completes' : 'Export metrics as 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"
|
||||
@@ -99,7 +101,7 @@ const MetricsPanel = () => {
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
<div className="flex-1 overflow-y-auto min-h-0">
|
||||
<div>
|
||||
{activeTab === 'table' && (
|
||||
<MetricsTable metrics={processMetrics} processMap={processMap} />
|
||||
)}
|
||||
@@ -112,6 +114,9 @@ const MetricsPanel = () => {
|
||||
{activeTab === 'threads' && (
|
||||
<ThreadMetricsTable metrics={threadMetrics} processMap={processMap} />
|
||||
)}
|
||||
{activeTab === 'thread-charts' && (
|
||||
<ThreadMetricsChart metrics={threadMetrics} processMap={processMap} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
CartesianGrid,
|
||||
RadarChart,
|
||||
PolarGrid,
|
||||
PolarAngleAxis,
|
||||
Radar,
|
||||
} from 'recharts';
|
||||
import type { ThreadMetrics, Process } from '@core/types';
|
||||
import { deriveThreadColor } from '../shared/threadUtils';
|
||||
|
||||
interface ThreadMetricsChartProps {
|
||||
metrics: ThreadMetrics[];
|
||||
processMap: Record<string, Process>;
|
||||
}
|
||||
|
||||
const TOOLTIP_STYLE = {
|
||||
backgroundColor: '#1e293b',
|
||||
border: '1px solid #334155',
|
||||
borderRadius: '8px',
|
||||
fontSize: '11px',
|
||||
color: '#e2e8f0',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
|
||||
};
|
||||
|
||||
const 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 ThreadMetricsChart = ({ metrics, processMap }: ThreadMetricsChartProps) => {
|
||||
if (metrics.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-48 text-slate-500 text-sm">
|
||||
No data available.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Pre-compute thread colors in order of appearance
|
||||
const threadIndexByProcess: Record<string, number> = {};
|
||||
const threadColors: Record<string, string> = {};
|
||||
metrics.forEach(m => {
|
||||
if (threadIndexByProcess[m.processId] === undefined) {
|
||||
threadIndexByProcess[m.processId] = 0;
|
||||
}
|
||||
const idx = threadIndexByProcess[m.processId]!;
|
||||
const procColor = processMap[m.processId]?.color ?? '#94a3b8';
|
||||
threadColors[`${m.processId}-${m.threadId}`] = deriveThreadColor(procColor, idx);
|
||||
threadIndexByProcess[m.processId] = idx + 1;
|
||||
});
|
||||
|
||||
const getThreadLabel = (m: ThreadMetrics) => {
|
||||
const procName = processMap[m.processId]?.name ?? m.processId;
|
||||
return `${procName}/${m.threadName}`;
|
||||
};
|
||||
|
||||
const getThreadColor = (m: ThreadMetrics) =>
|
||||
threadColors[`${m.processId}-${m.threadId}`] ?? '#94a3b8';
|
||||
|
||||
// ── Radar tooltip ──────────────────────────────────────────────
|
||||
const labelToMetric = Object.fromEntries(metrics.map(m => [getThreadLabel(m), m]));
|
||||
|
||||
const radarTooltipContent = ({ active, payload, label }: { active?: boolean; payload?: readonly any[]; label?: string | number }) => {
|
||||
if (!active || !payload?.length) return null;
|
||||
return (
|
||||
<div style={{ ...TOOLTIP_STYLE, padding: '8px 12px' }}>
|
||||
<p style={{ fontSize: '10px', color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 600, marginBottom: 4 }}>
|
||||
{label}
|
||||
</p>
|
||||
{payload.map((entry: any) => {
|
||||
const m = labelToMetric[entry.name];
|
||||
return (
|
||||
<div key={entry.name} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: '11px', marginBottom: 2 }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', backgroundColor: entry.color, flexShrink: 0, display: 'inline-block' }} />
|
||||
<span style={{ color: '#cbd5e1' }}>{m ? getThreadLabel(m) : entry.name}</span>
|
||||
<span style={{ marginLeft: 'auto', paddingLeft: 12, fontFamily: 'monospace', color: '#e2e8f0' }}>
|
||||
{typeof entry.value === 'number' ? `${entry.value.toFixed(1)}%` : '-'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ── Stacked bar data ──────────────────────────────────────────
|
||||
const barData = metrics.map(m => ({
|
||||
name: getThreadLabel(m),
|
||||
'Waiting': m.waitingTime,
|
||||
'CPU': m.cpuTime,
|
||||
'I/O': m.ioTime,
|
||||
}));
|
||||
|
||||
// ── Radar chart data (normalized 0-100 per dimension) ─────────
|
||||
const maxT = Math.max(...metrics.map(m => m.turnaroundTime), 1);
|
||||
const maxR = Math.max(...metrics.map(m => m.responseTime), 1);
|
||||
const maxW = Math.max(...metrics.map(m => m.waitingTime), 1);
|
||||
const maxCpu = Math.max(...metrics.map(m => m.cpuTime), 1);
|
||||
const maxIo = Math.max(...metrics.map(m => m.ioTime), 1);
|
||||
|
||||
const radarDimensions = ['Turnaround', 'Response', 'Waiting', 'CPU Time', 'I/O Time'] as const;
|
||||
|
||||
const radarData = radarDimensions.map(dim => {
|
||||
const row: Record<string, string | number> = { dimension: dim };
|
||||
metrics.forEach(m => {
|
||||
const key = getThreadLabel(m);
|
||||
switch (dim) {
|
||||
case 'Turnaround': row[key] = (m.turnaroundTime / maxT) * 100; break;
|
||||
case 'Response': row[key] = (m.responseTime / maxR) * 100; break;
|
||||
case 'Waiting': row[key] = (m.waitingTime / maxW) * 100; break;
|
||||
case 'CPU Time': row[key] = (m.cpuTime / maxCpu) * 100; break;
|
||||
case 'I/O Time': row[key] = (m.ioTime / maxIo) * 100; break;
|
||||
}
|
||||
});
|
||||
return row;
|
||||
});
|
||||
|
||||
const showRadar = metrics.length >= 2;
|
||||
|
||||
return (
|
||||
<div className="p-3 flex flex-col gap-6">
|
||||
{/* ── Radar chart ───────────────────────────────────────── */}
|
||||
{showRadar && (
|
||||
<div className="w-full">
|
||||
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
||||
Comparative Profile (normalized)
|
||||
</p>
|
||||
<ChartBox height={360}>
|
||||
<RadarChart data={radarData} margin={{ top: 8, right: 20, bottom: 8, left: 20 }}>
|
||||
<PolarGrid stroke="#334155" />
|
||||
<PolarAngleAxis
|
||||
dataKey="dimension"
|
||||
tick={{ fill: '#94a3b8', fontSize: 10 }}
|
||||
/>
|
||||
{metrics.map(m => (
|
||||
<Radar
|
||||
key={`${m.processId}-${m.threadId}`}
|
||||
name={getThreadLabel(m)}
|
||||
dataKey={getThreadLabel(m)}
|
||||
stroke={getThreadColor(m)}
|
||||
fill={getThreadColor(m)}
|
||||
fillOpacity={0.15}
|
||||
strokeWidth={2}
|
||||
dot={{ r: 3, fill: getThreadColor(m) }}
|
||||
/>
|
||||
))}
|
||||
<Tooltip content={radarTooltipContent} />
|
||||
<Legend
|
||||
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
|
||||
/>
|
||||
</RadarChart>
|
||||
</ChartBox>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Stacked bar chart ─────────────────────────────────── */}
|
||||
<div className="w-full">
|
||||
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
||||
Breakdown per Thread (ticks)
|
||||
</p>
|
||||
<ChartBox height={360}>
|
||||
<BarChart data={barData} margin={{ top: 8, right: 12, left: -8, bottom: 48 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tick={{ fill: '#94a3b8', fontSize: 10 }}
|
||||
axisLine={{ stroke: '#334155' }}
|
||||
tickLine={false}
|
||||
angle={-35}
|
||||
textAnchor="end"
|
||||
interval={0}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fill: '#94a3b8', fontSize: 10 }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
label={{
|
||||
value: 'ticks',
|
||||
angle: -90,
|
||||
position: 'insideLeft',
|
||||
offset: 14,
|
||||
style: { fill: '#475569', fontSize: 9 },
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={TOOLTIP_STYLE}
|
||||
cursor={{ fill: 'rgba(255,255,255,0.04)' }}
|
||||
/>
|
||||
<Legend
|
||||
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
|
||||
/>
|
||||
<Bar dataKey="Waiting" stackId="a" fill="#f59e0b" radius={[0, 0, 0, 0]} />
|
||||
<Bar dataKey="CPU" stackId="a" fill="#22c55e" radius={[0, 0, 0, 0]} />
|
||||
<Bar dataKey="I/O" stackId="a" fill="#06b6d4" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ChartBox>
|
||||
<p className="text-center text-[9px] text-slate-600 mt-0.5">
|
||||
Waiting + CPU + I/O = Turnaround
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThreadMetricsChart;
|
||||
@@ -551,6 +551,24 @@ export class SimulationEngine {
|
||||
// ── CPU burst da thread concluído ────────────────────────────────────
|
||||
this._activeThreads.set(processId, null);
|
||||
this._handleThreadBurstComplete(cpu, activeThread, process);
|
||||
|
||||
// Se o quantum do processo também expirou neste mesmo tick, cede a CPU
|
||||
// agora (após tratar o burst) em vez de dar um tick extra ao processo.
|
||||
if (this._cpuProcess !== null && this._algorithm.isQuantumExpired(cpu, this._config)) {
|
||||
const activeAfter = this._activeThreads.get(processId) ?? null;
|
||||
if (activeAfter !== null) {
|
||||
this._transitionThread(activeAfter, "Ready");
|
||||
if (policy === "RR") {
|
||||
this._readyThreadQueues.get(processId)!.push(activeAfter);
|
||||
} else {
|
||||
this._readyThreadQueues.get(processId)!.unshift(activeAfter);
|
||||
}
|
||||
this._activeThreads.set(processId, null);
|
||||
}
|
||||
this._transition(cpu, "Ready");
|
||||
this._readyQueue.push(cpu);
|
||||
this._cpuProcess = null;
|
||||
}
|
||||
} else if (scheduler.isQuantumExpired(activeThread)) {
|
||||
// ── Quantum intra-processo expirou (RR): rotaciona thread ────────────
|
||||
this._transitionThread(activeThread, "Ready");
|
||||
|
||||
+51
-6
@@ -1,5 +1,4 @@
|
||||
import type { ProcessMetrics } from "@core/types";
|
||||
import type { Process } from "@core/types";
|
||||
import type { ProcessMetrics, ThreadMetrics, Process } from "@core/types";
|
||||
|
||||
interface ProcessMap {
|
||||
[id: string]: Process;
|
||||
@@ -16,8 +15,13 @@ function downloadBlob(content: string, filename: string, mimeType: string) {
|
||||
}
|
||||
|
||||
export function useExport() {
|
||||
const exportCSV = (metrics: ProcessMetrics[], processMap: ProcessMap) => {
|
||||
const headers = [
|
||||
const exportCSV = (
|
||||
metrics: ProcessMetrics[],
|
||||
processMap: ProcessMap,
|
||||
threadMetrics?: ThreadMetrics[],
|
||||
) => {
|
||||
// ── Seção de processos ─────────────────────────────────────────────────
|
||||
const processHeaders = [
|
||||
"PID",
|
||||
"Process",
|
||||
"Priority",
|
||||
@@ -33,7 +37,7 @@ export function useExport() {
|
||||
"Bound",
|
||||
];
|
||||
|
||||
const rows = metrics.map((m) => {
|
||||
const processRows = metrics.map((m) => {
|
||||
const proc = processMap[m.processId];
|
||||
const pid = proc?.pid ?? "";
|
||||
const name = proc?.name ?? m.processId;
|
||||
@@ -55,7 +59,48 @@ export function useExport() {
|
||||
].join(",");
|
||||
});
|
||||
|
||||
const csv = [headers.join(","), ...rows].join("\n");
|
||||
const sections: string[] = [
|
||||
"# Processes",
|
||||
processHeaders.join(","),
|
||||
...processRows,
|
||||
];
|
||||
|
||||
// ── Seção de threads (opcional) ────────────────────────────────────────
|
||||
if (threadMetrics && threadMetrics.length > 0) {
|
||||
const threadHeaders = [
|
||||
"Thread",
|
||||
"Process",
|
||||
"Arrival",
|
||||
"Start",
|
||||
"Finish",
|
||||
"Response",
|
||||
"Turnaround",
|
||||
"Waiting",
|
||||
"CPU Time",
|
||||
"I/O Time",
|
||||
];
|
||||
|
||||
const threadRows = threadMetrics.map((t) => {
|
||||
const proc = processMap[t.processId];
|
||||
const procName = proc?.name ?? t.processId;
|
||||
return [
|
||||
t.threadName,
|
||||
procName,
|
||||
t.arrivalTime,
|
||||
t.startTick,
|
||||
t.finishTick,
|
||||
t.responseTime,
|
||||
t.turnaroundTime,
|
||||
t.waitingTime,
|
||||
t.cpuTime,
|
||||
t.ioTime,
|
||||
].join(",");
|
||||
});
|
||||
|
||||
sections.push("", "# Threads", threadHeaders.join(","), ...threadRows);
|
||||
}
|
||||
|
||||
const csv = sections.join("\n");
|
||||
downloadBlob(csv, "pss-metrics.csv", "text/csv;charset=utf-8;");
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user