diff --git a/dist.tar.gz b/dist.tar.gz index 2c461ab..5817118 100644 Binary files a/dist.tar.gz and b/dist.tar.gz differ diff --git a/package.json b/package.json index 33e765a..838e264 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pss", - "version": "1.3.1", + "version": "1.3.2", "description": "Process Scheduler Simulator — an interactive educational tool that runs entirely in the browser", "main": "index.js", "scripts": { diff --git a/src/core/__tests__/engine-threads.test.ts b/src/core/__tests__/engine-threads.test.ts index 7f899bd..a1c5f1f 100644 --- a/src/core/__tests__/engine-threads.test.ts +++ b/src/core/__tests__/engine-threads.test.ts @@ -412,6 +412,252 @@ describe("Engine — RR intra-processo", () => { }); }); +// ── Quantum de processo com threads ──────────────────────────────────────── +// +// Testa o comportamento correto quando o burst de uma thread termina +// exatamente no mesmo tick em que o quantum do processo expira. +// Antes do fix, o processo recebia quantum+1 ticks (bug). + +describe("Engine — quantum de processo com threads (regressão)", () => { + const rrConfig: SchedulerConfig = { + algorithm: "RR", + quantum: 2, + contextSwitchTime: 0, + isPreemptive: false, + agingEnabled: false, + agingInterval: 5, + }; + + const priorityRrConfig: SchedulerConfig = { + algorithm: "PRIORITY_RR", + quantum: 2, + contextSwitchTime: 0, + isPreemptive: false, + agingEnabled: false, + agingInterval: 5, + }; + + it("RR: segundo processo recebe CPU no tick 2 quando burst da thread = quantum", () => { + // P1: thread T1 com CPU=2 (= quantum). Ao fim do tick 1, T1 termina E + // o quantum expira ao mesmo tempo. P1 deve ceder a CPU imediatamente. + // P2 deve começar no tick 2, não no tick 3. + const p1: Process = { + pid: 1, + id: "p1", + name: "P1", + arrivalTime: 0, + priority: 5, + color: "#4FC3F7", + bursts: [], + threadModel: "ONE_TO_ONE", + threadSchedulingPolicy: "FCFS", + threads: [ + { tid: "t1", name: "T1", bursts: [{ type: "cpu", duration: 2 }] }, + { tid: "t2", name: "T2", bursts: [{ type: "cpu", duration: 2 }] }, + ], + }; + const p2: Process = { + pid: 2, + id: "p2", + name: "P2", + arrivalTime: 0, + priority: 5, + color: "#81C784", + bursts: [{ type: "cpu", duration: 3 }], + }; + + const engine = new SimulationEngine([p1, p2], rrConfig); + const ticks = engine.runAll(); + const cpu = ticks.map((t) => t.cpuProcess); + + // Tick 0 e 1: P1 roda (T1, burst=2=quantum) + expect(cpu[0]).toBe("p1"); + expect(cpu[1]).toBe("p1"); + // Tick 2: P2 deve receber CPU (não tick 3) + expect(cpu[2]).toBe("p2"); + + const rt2 = engine.runtimeStates.find((r) => r.processId === "p2")!; + expect(rt2.startTick).toBe(2); + }); + + it("RR: processo alterna exatamente a cada quantum quando bursts coincidem com quantum", () => { + // Ambos os processos têm threads com CPU=2 = quantum. + // Devem alternar estritamente: P1(0-1), P2(2-3), P1(4-5), P2(6-7)... + const makeThreaded = (id: string, pid: number, color: string): Process => ({ + pid, + id, + name: id, + arrivalTime: 0, + priority: 5, + color, + bursts: [], + threadModel: "ONE_TO_ONE" as const, + threadSchedulingPolicy: "FCFS" as const, + threads: [ + { tid: `${id}_t1`, name: "T1", bursts: [{ type: "cpu" as const, duration: 2 }] }, + { tid: `${id}_t2`, name: "T2", bursts: [{ type: "cpu" as const, duration: 2 }] }, + ], + }); + + const engine = new SimulationEngine( + [makeThreaded("p1", 1, "#4FC3F7"), makeThreaded("p2", 2, "#81C784")], + rrConfig, + ); + const ticks = engine.runAll(); + const cpu = ticks.map((t) => t.cpuProcess); + + // Cada bloco de 2 ticks deve pertencer a um processo diferente + expect(cpu[0]).toBe("p1"); + expect(cpu[1]).toBe("p1"); + expect(cpu[2]).toBe("p2"); + expect(cpu[3]).toBe("p2"); + expect(cpu[4]).toBe("p1"); + expect(cpu[5]).toBe("p1"); + expect(cpu[6]).toBe("p2"); + expect(cpu[7]).toBe("p2"); + }); + + it("PRIORITY_RR: reproduz o cenário reportado — P2.startTick deve ser 2", () => { + // Replica o config.json do usuário (versão simplificada): + // App: ONE_TO_ONE, PRIORITY, High-1(prio=1, cpu=2), High-2(prio=2, cpu=2) + // P2: ONE_TO_ONE, FCFS, T1(cpu=3) + // Algoritmo: PRIORITY_RR, quantum=2, ambos prio=1 + // Bug: App rodava ticks 0,1,2 (quantum+1). Correto: 0,1 → P2 começa no tick 2. + const app: Process = { + pid: 1, + id: "app", + name: "App", + arrivalTime: 0, + priority: 1, + color: "#F06292", + bursts: [], + threadModel: "ONE_TO_ONE", + threadSchedulingPolicy: "PRIORITY", + threads: [ + { tid: "h1", name: "High-1", priority: 1, bursts: [{ type: "cpu", duration: 2 }] }, + { tid: "h2", name: "High-2", priority: 2, bursts: [{ type: "cpu", duration: 2 }] }, + ], + }; + const p2: Process = { + pid: 2, + id: "p2", + name: "P2", + arrivalTime: 0, + priority: 1, + color: "#f472b6", + bursts: [], + threadModel: "ONE_TO_ONE", + threadSchedulingPolicy: "FCFS", + threads: [ + { tid: "p2t1", name: "T1", bursts: [{ type: "cpu", duration: 3 }] }, + ], + }; + + const engine = new SimulationEngine([app, p2], priorityRrConfig); + const ticks = engine.runAll(); + const cpu = ticks.map((t) => t.cpuProcess); + + // App roda ticks 0 e 1 (quantum=2). P2 deve começar no tick 2. + expect(cpu[0]).toBe("app"); + expect(cpu[1]).toBe("app"); + expect(cpu[2]).toBe("p2"); + + const p2Rt = engine.runtimeStates.find((r) => r.processId === "p2")!; + expect(p2Rt.startTick).toBe(2); + // responseTime de threads é calculado em _dispatchThreadForProcess + const p2ThreadRts = engine.threadRuntimeStates.filter((t) => t.processId === "p2"); + expect(p2ThreadRts[0]!.startTick).toBe(2); + expect(p2ThreadRts[0]!.responseTime).toBe(2); + }); + + it("thread com burst menor que quantum não causa preempção prematura", () => { + // T1 tem burst=1 (< quantum=2). T1 termina no tick 1 sem quantum expirado. + // T2 deve continuar rodando no tick 2 (dentro do mesmo quantum). + // P2 só recebe CPU no tick 3 (quando quantum do processo expirar com T2). + const p1: Process = { + pid: 1, + id: "p1", + name: "P1", + arrivalTime: 0, + priority: 5, + color: "#4FC3F7", + bursts: [], + threadModel: "ONE_TO_ONE", + threadSchedulingPolicy: "FCFS", + threads: [ + { tid: "t1", name: "T1", bursts: [{ type: "cpu", duration: 1 }] }, + { tid: "t2", name: "T2", bursts: [{ type: "cpu", duration: 3 }] }, + ], + }; + const p2: Process = { + pid: 2, + id: "p2", + name: "P2", + arrivalTime: 0, + priority: 5, + color: "#81C784", + bursts: [{ type: "cpu", duration: 2 }], + }; + + const engine = new SimulationEngine([p1, p2], rrConfig); + const ticks = engine.runAll(); + const cpu = ticks.map((t) => t.cpuProcess); + + // Tick 0: P1/T1 roda (burst=1, quantum=2 → quantum não expirado) + expect(cpu[0]).toBe("p1"); + // Tick 1: P1/T2 assume (quantum restante=1) + expect(cpu[1]).toBe("p1"); + // Tick 2: quantum do P1 expira → P2 recebe CPU + expect(cpu[2]).toBe("p2"); + }); + + it("processo com thread em I/O no momento do quantum cede corretamente", () => { + // T1: CPU(2) → IO(2) → CPU(1). Quantum=2. + // T1 termina burst CPU no tick 1 E quantum=0: vai para IO, processo cede. + // P2 deve receber CPU no tick 2. + const p1: Process = { + pid: 1, + id: "p1", + name: "P1", + arrivalTime: 0, + priority: 5, + color: "#4FC3F7", + bursts: [], + threadModel: "ONE_TO_ONE", + threadSchedulingPolicy: "FCFS", + threads: [ + { + tid: "t1", + name: "T1", + bursts: [ + { type: "cpu", duration: 2 }, + { type: "io", duration: 2 }, + { type: "cpu", duration: 1 }, + ], + }, + ], + }; + const p2: Process = { + pid: 2, + id: "p2", + name: "P2", + arrivalTime: 0, + priority: 5, + color: "#81C784", + bursts: [{ type: "cpu", duration: 3 }], + }; + + const engine = new SimulationEngine([p1, p2], rrConfig); + const ticks = engine.runAll(); + const cpu = ticks.map((t) => t.cpuProcess); + + expect(cpu[0]).toBe("p1"); + expect(cpu[1]).toBe("p1"); + // Tick 2: T1 foi para IO e quantum expirou → P2 recebe CPU + expect(cpu[2]).toBe("p2"); + }); +}); + // ── Política intra-processo PRIORITY ────────────────────────────────────── describe("Engine — Priority intra-processo", () => {