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:
Lucas Guerra committed 2026-03-24 23:09:16 -03:00
1 parent 532c14e382
commit 5761dfe70a
34 files changed
+3789 -334

No files matched your search

+33 -5
View File
@@ -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>
+192 -66
View File
@@ -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;