diff --git a/scripts/gpu-probe.mjs b/scripts/gpu-probe.mjs index e9ee370..6b7de49 100644 --- a/scripts/gpu-probe.mjs +++ b/scripts/gpu-probe.mjs @@ -34,7 +34,7 @@ export function startProbe({ spawn, scriptPath, intervalMs, onSample, parentPid function scheduleRespawn(entry) { if (entry.respawned) return; entry.respawned = true; - if (current === entry) current = null; + current = null; setTimeout(launch, Math.max(0, lastSpawnAt + intervalMs - Date.now())).unref(); } @@ -60,7 +60,7 @@ export function startProbe({ spawn, scriptPath, intervalMs, onSample, parentPid const lines = pending.split("\n"); pending = lines.pop(); for (const line of lines) { - const sample = parseSample(line.trim()); + const sample = parseSample(line); if (sample) { lastSampleAt = Date.now(); onSample(sample); @@ -72,7 +72,7 @@ export function startProbe({ spawn, scriptPath, intervalMs, onSample, parentPid } const watchdog = setInterval(() => { - if (stopped || current === null || Date.now() - lastSampleAt <= staleIntervals * intervalMs) return; + if (current === null || Date.now() - lastSampleAt <= staleIntervals * intervalMs) return; const hung = current; scheduleRespawn(hung); hung.child.kill(); diff --git a/scripts/test-gpu-tenancy.mjs b/scripts/test-gpu-tenancy.mjs index 840c95a..7f1ac4b 100644 --- a/scripts/test-gpu-tenancy.mjs +++ b/scripts/test-gpu-tenancy.mjs @@ -270,8 +270,25 @@ test("own VRAM: the next request right after a job is admitted while the child's return "second"; }); assert.equal(second, "second"); + r.feed({ ...CLEAR, freeMiB: 5000 }); + assert.equal(r.gate.status().inUse, false, "the baseline survives back-to-back jobs: 5000 is not recovery from a 14000 start"); r.feed(CLEAR); - assert.equal(r.gate.status().inUse, false, "the baseline survives back-to-back jobs, so recovery is judged against the first"); + assert.equal(r.gate.status().inUse, false); + r.feed({ ...CLEAR, freeMiB: 5000 }); + assert.equal(r.gate.status().inUse, true, "recovered, so low VRAM is the household's again"); +}); + +test("slot: releasing twice does not restart the drain", () => { + const r = freeRig(); + const slot = r.gate.acquire(); + r.feed(OWN); + slot.release(); + r.feed(OWN); + r.feed(OWN); + r.feed(OWN); + slot.release(); // a second release is nothing: it must not give the drain a fresh cap + r.feed(OWN); + assert.equal(r.gate.status().inUse, true, "60 s after the one release, the drain cap has passed"); }); test("own VRAM: low VRAM that outlives the drain is the household's, and refuses", async () => { @@ -362,6 +379,8 @@ test("env overrides: a malformed value refuses to start and names the variable, ); } } - assert.throws(() => gpuTenancyOptionsFromEnv({ VILI_GPU_SAMPLE_MS: "0" }), /VILI_GPU_SAMPLE_MS/, "a zero interval would spin the probe"); - assert.doesNotThrow(() => gpuTenancyOptionsFromEnv({ VILI_GPU_CLEAR_HOLD_MS: "0" })); + assert.throws(() => gpuTenancyOptionsFromEnv({ VILI_GPU_SAMPLE_MS: "0" }), { message: "VILI_GPU_SAMPLE_MS must be a finite positive number" }); + assert.throws(() => gpuTenancyOptionsFromEnv({ VILI_GPU_MIN_FREE_MIB: "x" }), { message: "VILI_GPU_MIN_FREE_MIB must be a finite non-negative number" }); + assert.deepEqual(gpuTenancyOptionsFromEnv({ VILI_GPU_CLEAR_HOLD_MS: "0", VILI_GPU_MIN_FREE_MIB: "0", VILI_GPU_UTIL3D_BUSY_PCT: "0" }), + { clearHoldMs: 0, minFreeMiB: 0, util3dBusyPct: 0 }, "zero is a valid hold, minimum and threshold"); });