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
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pss",
|
"name": "pss",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"description": "Process Scheduler Simulator — an interactive educational tool that runs entirely in the browser",
|
"description": "Process Scheduler Simulator — an interactive educational tool that runs entirely in the browser",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const GanttChart = () => {
|
|||||||
<div className="flex sticky top-0 z-10 bg-slate-950 border-b border-slate-800">
|
<div className="flex sticky top-0 z-10 bg-slate-950 border-b border-slate-800">
|
||||||
<div
|
<div
|
||||||
className="sticky left-0 z-20 bg-slate-950 border-r border-slate-700 shrink-0"
|
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) => (
|
{tickNumbers.map((t) => (
|
||||||
<div
|
<div
|
||||||
@@ -73,7 +73,7 @@ const GanttChart = () => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="sticky left-0 z-20 bg-slate-900 border-r border-slate-700 px-2 flex items-center shrink-0"
|
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
|
<span
|
||||||
className="text-[10px] font-bold truncate"
|
className="text-[10px] font-bold truncate"
|
||||||
@@ -117,7 +117,7 @@ const GanttChart = () => {
|
|||||||
{currentTickIndex >= 0 && (
|
{currentTickIndex >= 0 && (
|
||||||
<div
|
<div
|
||||||
className="gantt-cursor absolute top-0 bottom-0 w-px bg-white/20 pointer-events-none"
|
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>
|
</div>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const GanttThreadRow = React.memo(function GanttThreadRow({
|
|||||||
{/* Rótulo da thread (sticky) */}
|
{/* Rótulo da thread (sticky) */}
|
||||||
<div
|
<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"
|
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
|
<span
|
||||||
className="w-1.5 h-1.5 rounded-full shrink-0"
|
className="w-1.5 h-1.5 rounded-full shrink-0"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import MetricsPanel from '../MetricsPanel/MetricsPanel';
|
|||||||
|
|
||||||
const MainArea = () => {
|
const MainArea = () => {
|
||||||
return (
|
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 />
|
<ControlBar />
|
||||||
<GanttChart />
|
<GanttChart />
|
||||||
<MetricsPanel />
|
<MetricsPanel />
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import MetricsTable from './MetricsTable';
|
|||||||
import GlobalMetrics from './GlobalMetrics';
|
import GlobalMetrics from './GlobalMetrics';
|
||||||
import MetricsChart from './MetricsChart';
|
import MetricsChart from './MetricsChart';
|
||||||
import ThreadMetricsTable from './ThreadMetricsTable';
|
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 MetricsPanel = () => {
|
||||||
const [activeTab, setActiveTab] = useState<Tab>('table');
|
const [activeTab, setActiveTab] = useState<Tab>('table');
|
||||||
@@ -53,14 +54,15 @@ const MetricsPanel = () => {
|
|||||||
const canExportCSV = status === 'finished' && processMetrics.length > 0;
|
const canExportCSV = status === 'finished' && processMetrics.length > 0;
|
||||||
|
|
||||||
const TAB_CONFIG: { id: Tab; label: string; icon: React.ReactNode; show: boolean }[] = [
|
const TAB_CONFIG: { id: Tab; label: string; icon: React.ReactNode; show: boolean }[] = [
|
||||||
{ id: 'table', label: 'Per Process', icon: <Table size={13} />, show: true },
|
{ id: 'table', label: 'Per Process', icon: <Table size={13} />, show: true },
|
||||||
{ id: 'global', label: 'Global', icon: <Globe 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: 'charts', label: 'Process Charts', icon: <BarChart2 size={13} />, show: true },
|
||||||
{ id: 'threads', label: 'Threads', icon: <Layers size={13} />, show: hasThreads },
|
{ id: 'threads', label: 'Threads', icon: <Layers size={13} />, show: hasThreads },
|
||||||
|
{ id: 'thread-charts', label: 'Thread Charts', icon: <BarChart2 size={13} />, show: hasThreads },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* Panel header with tabs + export button */}
|
||||||
<div className="flex items-center px-3 border-b border-slate-700 shrink-0 h-10 gap-1">
|
<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 => (
|
{TAB_CONFIG.filter(t => t.show).map(tab => (
|
||||||
@@ -86,7 +88,7 @@ const MetricsPanel = () => {
|
|||||||
|
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
<button
|
<button
|
||||||
onClick={() => exportCSV(processMetrics, processMap)}
|
onClick={() => exportCSV(processMetrics, processMap, hasThreads ? threadMetrics : undefined)}
|
||||||
disabled={!canExportCSV}
|
disabled={!canExportCSV}
|
||||||
title={!canExportCSV ? 'Available when simulation completes' : 'Export metrics as CSV'}
|
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"
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Tab content */}
|
{/* Tab content */}
|
||||||
<div className="flex-1 overflow-y-auto min-h-0">
|
<div>
|
||||||
{activeTab === 'table' && (
|
{activeTab === 'table' && (
|
||||||
<MetricsTable metrics={processMetrics} processMap={processMap} />
|
<MetricsTable metrics={processMetrics} processMap={processMap} />
|
||||||
)}
|
)}
|
||||||
@@ -112,6 +114,9 @@ const MetricsPanel = () => {
|
|||||||
{activeTab === 'threads' && (
|
{activeTab === 'threads' && (
|
||||||
<ThreadMetricsTable metrics={threadMetrics} processMap={processMap} />
|
<ThreadMetricsTable metrics={threadMetrics} processMap={processMap} />
|
||||||
)}
|
)}
|
||||||
|
{activeTab === 'thread-charts' && (
|
||||||
|
<ThreadMetricsChart metrics={threadMetrics} processMap={processMap} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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 ────────────────────────────────────
|
// ── CPU burst da thread concluído ────────────────────────────────────
|
||||||
this._activeThreads.set(processId, null);
|
this._activeThreads.set(processId, null);
|
||||||
this._handleThreadBurstComplete(cpu, activeThread, process);
|
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)) {
|
} else if (scheduler.isQuantumExpired(activeThread)) {
|
||||||
// ── Quantum intra-processo expirou (RR): rotaciona thread ────────────
|
// ── Quantum intra-processo expirou (RR): rotaciona thread ────────────
|
||||||
this._transitionThread(activeThread, "Ready");
|
this._transitionThread(activeThread, "Ready");
|
||||||
|
|||||||
+51
-6
@@ -1,5 +1,4 @@
|
|||||||
import type { ProcessMetrics } from "@core/types";
|
import type { ProcessMetrics, ThreadMetrics, Process } from "@core/types";
|
||||||
import type { Process } from "@core/types";
|
|
||||||
|
|
||||||
interface ProcessMap {
|
interface ProcessMap {
|
||||||
[id: string]: Process;
|
[id: string]: Process;
|
||||||
@@ -16,8 +15,13 @@ function downloadBlob(content: string, filename: string, mimeType: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useExport() {
|
export function useExport() {
|
||||||
const exportCSV = (metrics: ProcessMetrics[], processMap: ProcessMap) => {
|
const exportCSV = (
|
||||||
const headers = [
|
metrics: ProcessMetrics[],
|
||||||
|
processMap: ProcessMap,
|
||||||
|
threadMetrics?: ThreadMetrics[],
|
||||||
|
) => {
|
||||||
|
// ── Seção de processos ─────────────────────────────────────────────────
|
||||||
|
const processHeaders = [
|
||||||
"PID",
|
"PID",
|
||||||
"Process",
|
"Process",
|
||||||
"Priority",
|
"Priority",
|
||||||
@@ -33,7 +37,7 @@ export function useExport() {
|
|||||||
"Bound",
|
"Bound",
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = metrics.map((m) => {
|
const processRows = metrics.map((m) => {
|
||||||
const proc = processMap[m.processId];
|
const proc = processMap[m.processId];
|
||||||
const pid = proc?.pid ?? "";
|
const pid = proc?.pid ?? "";
|
||||||
const name = proc?.name ?? m.processId;
|
const name = proc?.name ?? m.processId;
|
||||||
@@ -55,7 +59,48 @@ export function useExport() {
|
|||||||
].join(",");
|
].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;");
|
downloadBlob(csv, "pss-metrics.csv", "text/csv;charset=utf-8;");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user