From 44ee62e2c2028a736466c556864d9bcfa1e72387 Mon Sep 17 00:00:00 2001 From: Lucas Rayan Date: Wed, 25 Mar 2026 09:59:47 -0300 Subject: [PATCH] feat: update version to 1.3.1, enhance GanttChart and MetricsPanel, and add ThreadMetricsChart --- package.json | 2 +- src/components/GanttChart/GanttChart.tsx | 6 +- src/components/GanttChart/GanttThreadRow.tsx | 2 +- src/components/Layout/MainArea.tsx | 2 +- src/components/MetricsPanel/MetricsPanel.tsx | 21 +- .../MetricsPanel/ThreadMetricsChart.tsx | 215 ++++++++++++++++++ src/core/engine.ts | 18 ++ src/hooks/useExport.ts | 57 ++++- 8 files changed, 303 insertions(+), 20 deletions(-) create mode 100644 src/components/MetricsPanel/ThreadMetricsChart.tsx diff --git a/package.json b/package.json index 98dd77b..33e765a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pss", - "version": "1.3.0", + "version": "1.3.1", "description": "Process Scheduler Simulator — an interactive educational tool that runs entirely in the browser", "main": "index.js", "scripts": { diff --git a/src/components/GanttChart/GanttChart.tsx b/src/components/GanttChart/GanttChart.tsx index 8e28fa0..1d8f58e 100644 --- a/src/components/GanttChart/GanttChart.tsx +++ b/src/components/GanttChart/GanttChart.tsx @@ -47,7 +47,7 @@ const GanttChart = () => {
{tickNumbers.map((t) => (
{ >
{ {currentTickIndex >= 0 && (
)}
diff --git a/src/components/GanttChart/GanttThreadRow.tsx b/src/components/GanttChart/GanttThreadRow.tsx index 69c0299..cc44823 100644 --- a/src/components/GanttChart/GanttThreadRow.tsx +++ b/src/components/GanttChart/GanttThreadRow.tsx @@ -39,7 +39,7 @@ const GanttThreadRow = React.memo(function GanttThreadRow({ {/* Rótulo da thread (sticky) */}
{ return ( -
+
diff --git a/src/components/MetricsPanel/MetricsPanel.tsx b/src/components/MetricsPanel/MetricsPanel.tsx index a734833..3373fa9 100644 --- a/src/components/MetricsPanel/MetricsPanel.tsx +++ b/src/components/MetricsPanel/MetricsPanel.tsx @@ -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('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: , show: true }, - { id: 'global', label: 'Global', icon: , show: true }, - { id: 'charts', label: 'Charts', icon: , show: true }, - { id: 'threads', label: 'Threads', icon: , show: hasThreads }, + { id: 'table', label: 'Per Process', icon:
, show: true }, + { id: 'global', label: 'Global', icon: , show: true }, + { id: 'charts', label: 'Process Charts', icon: , show: true }, + { id: 'threads', label: 'Threads', icon: , show: hasThreads }, + { id: 'thread-charts', label: 'Thread Charts', icon: , show: hasThreads }, ]; return ( -
+
{/* Panel header with tabs + export button */}
{TAB_CONFIG.filter(t => t.show).map(tab => ( @@ -86,7 +88,7 @@ const MetricsPanel = () => {
{/* Tab content */} -
+
{activeTab === 'table' && ( )} @@ -112,6 +114,9 @@ const MetricsPanel = () => { {activeTab === 'threads' && ( )} + {activeTab === 'thread-charts' && ( + + )}
); diff --git a/src/components/MetricsPanel/ThreadMetricsChart.tsx b/src/components/MetricsPanel/ThreadMetricsChart.tsx new file mode 100644 index 0000000..465e4a5 --- /dev/null +++ b/src/components/MetricsPanel/ThreadMetricsChart.tsx @@ -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; +} + +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 }) => ( +
+ + {children as React.ReactElement} + +
+); + +const ThreadMetricsChart = ({ metrics, processMap }: ThreadMetricsChartProps) => { + if (metrics.length === 0) { + return ( +
+ No data available. +
+ ); + } + + // Pre-compute thread colors in order of appearance + const threadIndexByProcess: Record = {}; + const threadColors: Record = {}; + 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 ( +
+

+ {label} +

+ {payload.map((entry: any) => { + const m = labelToMetric[entry.name]; + return ( +
+ + {m ? getThreadLabel(m) : entry.name} + + {typeof entry.value === 'number' ? `${entry.value.toFixed(1)}%` : '-'} + +
+ ); + })} +
+ ); + }; + + // ── 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 = { 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 ( +
+ {/* ── Radar chart ───────────────────────────────────────── */} + {showRadar && ( +
+

+ Comparative Profile (normalized) +

+ + + + + {metrics.map(m => ( + + ))} + + + + +
+ )} + + {/* ── Stacked bar chart ─────────────────────────────────── */} +
+

+ Breakdown per Thread (ticks) +

+ + + + + + + + + + + + +

+ Waiting + CPU + I/O = Turnaround +

+
+
+ ); +}; + +export default ThreadMetricsChart; diff --git a/src/core/engine.ts b/src/core/engine.ts index 982f614..01cdc06 100644 --- a/src/core/engine.ts +++ b/src/core/engine.ts @@ -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"); diff --git a/src/hooks/useExport.ts b/src/hooks/useExport.ts index 3910b33..edd8701 100644 --- a/src/hooks/useExport.ts +++ b/src/hooks/useExport.ts @@ -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;"); };