fix(process): report no remaining time after a provider deadline fires

Node timers can fire while performance.now() is still a fraction of a
millisecond short of the monotonic deadline. ProviderStartupDeadline then
reported interruption "timeout" and an aborted signal while remainingMs()
returned ceil(0.x) = 1, so polling loops guarded by remainingMs() > 0
(Hopper and Ghidra startup) could run past an elapsed deadline, and the
real-timer test "aborts a bounded wait when its real monotonic deadline
expires" failed intermittently in CI (expected 1 to be 0).

Once the deadline's own timer has classified a timeout, report zero
remaining time. A deterministic regression test fires the timer with the
monotonic clock 0.4 ms short of the deadline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822
2026-10-13 08:52:29 +00:00
parent c02fb0ea80
commit dfdaa0ca74
2 changed files with 20 additions and 0 deletions
+17
View File
@@ -33,3 +33,20 @@ it("aborts a bounded wait when its real monotonic deadline expires", async () =>
deadline.dispose();
}
});
it("reports no remaining time once its timer fires before the monotonic deadline", () => {
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
const now = vi.spyOn(performance, "now").mockReturnValue(1_000);
const deadline = new ProviderStartupDeadline(40);
try {
// Timers may fire while the monotonic clock is still just short of the deadline.
now.mockReturnValue(1_039.6);
vi.advanceTimersByTime(40);
expect(deadline.signal.aborted).toBe(true);
expect(deadline.interruption).toBe("timeout");
expect(deadline.remainingMs()).toBe(0);
} finally {
deadline.dispose();
vi.useRealTimers();
}
});
+3
View File
@@ -85,6 +85,9 @@ export class ProviderStartupDeadline {
/** Milliseconds still available to the next startup phase. */
remainingMs(): number {
// Node timers can fire a fraction of a millisecond before the monotonic
// deadline, so an elapsed timer must not report time left to spend.
if (this.#interruption === "timeout") return 0;
return Math.max(0, Math.ceil(this.#deadlineAt - performance.now()));
}