From dfdaa0ca74cc84a45dcb186422a0f51aa61bc427 Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:52:29 +0000 Subject: [PATCH] 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 --- src/process/ProviderDeadline.test.ts | 17 +++++++++++++++++ src/process/ProviderDeadline.ts | 3 +++ 2 files changed, 20 insertions(+) diff --git a/src/process/ProviderDeadline.test.ts b/src/process/ProviderDeadline.test.ts index b43ce66542..0a94481503 100644 --- a/src/process/ProviderDeadline.test.ts +++ b/src/process/ProviderDeadline.test.ts @@ -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(); + } +}); diff --git a/src/process/ProviderDeadline.ts b/src/process/ProviderDeadline.ts index 33cb3a8d00..a6f10779af 100644 --- a/src/process/ProviderDeadline.ts +++ b/src/process/ProviderDeadline.ts @@ -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())); }