From 7b22a2309c5f71b65dc1af123cf4d69c2d95d393 Mon Sep 17 00:00:00 2001 From: can1357 Date: Sun, 16 Aug 2026 02:05:54 +0200 Subject: [PATCH] fix(tui): handled CPU-throttled loop stalls --- packages/tui/CHANGELOG.md | 3 +++ packages/tui/src/loop-watchdog.ts | 17 +++++++++-------- packages/tui/test/loop-watchdog.test.ts | 5 +++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index a67c956de..17050627d 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -32,6 +32,9 @@ ### Fixed - Fixed inline images rendering permanently cropped on Kitty direct-placement terminals (WezTerm, Warp) when an image block straddled the viewport top during streaming: placements are now clipped to the visible slice at write time, and a placement id whose cells reached native scrollback is never re-used ([#8070](https://github.com/can1357/oh-my-pi/pull/8070) by [@voonfoo](https://github.com/voonfoo)) +### Fixed + +- Fixed long CPU-bound event-loop stalls being misclassified as system sleep and omitted from `ui.loop-blocked` diagnostics ([#8442](https://github.com/can1357/oh-my-pi/pull/8442) by [@Mustaqeem66](https://github.com/Mustaqeem66)). ## [17.2.12] - 2026-08-08 diff --git a/packages/tui/src/loop-watchdog.ts b/packages/tui/src/loop-watchdog.ts index 10f39f00b..79264374c 100644 --- a/packages/tui/src/loop-watchdog.ts +++ b/packages/tui/src/loop-watchdog.ts @@ -6,7 +6,7 @@ export interface LoopWatchdogOptions { intervalMs?: number; /** A tick later than this past its deadline counts as a block. Default 250. */ thresholdMs?: number; - /** Overshoot beyond this is suppressed only when the process also burned no CPU. Default 60_000. */ + /** Overshoot beyond this is suppressed only when the process burned negligible CPU. Default 60_000. */ sleepMs?: number; /** Monotonic clock source; injectable for tests. Default `performance.now`. */ now?: () => number; @@ -26,13 +26,13 @@ interface LoopWatchdogTimer { } /** - * Fraction of a missed interval that must show up as CPU time for the overshoot - * to count as a synchronous stall rather than system sleep. A suspended process - * resumes having burned essentially nothing; a wedged one burned the interval on - * a core. Half leaves room for a gap that is partly sleep and partly work, which - * is still worth reporting. + * Fraction of a missed interval that may be process CPU time while the gap is + * still treated as system sleep. Keep this near zero: cgroup throttling and + * scheduler contention can make a CPU-bound loop consume far less CPU than wall + * time. One percent allows a little measurement/background jitter while erring + * toward reporting a severe stall instead of hiding it. */ -const CPU_BUSY_RATIO = 0.5; +const CPU_BUSY_RATIO = 0.01; /** * Always-on event-loop lag probe. Each tick is scheduled `intervalMs` ahead of @@ -51,7 +51,8 @@ const CPU_BUSY_RATIO = 0.5; * A long overshoot is classified by CPU time rather than by duration. System * sleep and a CPU-bound wedge both produce an arbitrarily large gap, so duration * alone cannot tell them apart, and suppressing on duration discards exactly the - * worst stalls. Only a gap the process did not spend CPU on is treated as sleep. + * worst stalls. Only a gap the process spent negligible CPU on is treated as + * sleep. CPU accounting is process-wide, so worker activity errs toward logging. */ export class LoopWatchdog { #intervalMs: number; diff --git a/packages/tui/test/loop-watchdog.test.ts b/packages/tui/test/loop-watchdog.test.ts index db3333ce7..1fe07e59d 100644 --- a/packages/tui/test/loop-watchdog.test.ts +++ b/packages/tui/test/loop-watchdog.test.ts @@ -288,12 +288,13 @@ describe("LoopWatchdog long-block classification", () => { expect(warnSpy).not.toHaveBeenCalled(); }); - test("reports a long block that spent most, but not all, of the gap on CPU", () => { + test("reports a long CPU-bound block when the process is CPU throttled", () => { const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {}); const h = cpuHarness(); h.wd.start(); - h.set(120_250, 90_000); + // Ten percent CPU is still real work, not suspension. + h.set(82_641, 8_200); h.fireTick(); expect(warnSpy).toHaveBeenCalledTimes(1);