fix(tui): handled CPU-throttled loop stalls

This commit is contained in:
can1357
2026-08-16 02:05:54 +02:00
parent 62965c03a5
commit 7b22a2309c
3 changed files with 15 additions and 10 deletions
+3
View File
@@ -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
+9 -8
View File
@@ -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;
+3 -2
View File
@@ -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);