fix(tui): handled CPU-throttled loop stalls
This commit is contained in:
@@ -32,6 +32,9 @@
|
|||||||
### Fixed
|
### 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 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
|
## [17.2.12] - 2026-08-08
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export interface LoopWatchdogOptions {
|
|||||||
intervalMs?: number;
|
intervalMs?: number;
|
||||||
/** A tick later than this past its deadline counts as a block. Default 250. */
|
/** A tick later than this past its deadline counts as a block. Default 250. */
|
||||||
thresholdMs?: number;
|
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;
|
sleepMs?: number;
|
||||||
/** Monotonic clock source; injectable for tests. Default `performance.now`. */
|
/** Monotonic clock source; injectable for tests. Default `performance.now`. */
|
||||||
now?: () => number;
|
now?: () => number;
|
||||||
@@ -26,13 +26,13 @@ interface LoopWatchdogTimer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fraction of a missed interval that must show up as CPU time for the overshoot
|
* Fraction of a missed interval that may be process CPU time while the gap is
|
||||||
* to count as a synchronous stall rather than system sleep. A suspended process
|
* still treated as system sleep. Keep this near zero: cgroup throttling and
|
||||||
* resumes having burned essentially nothing; a wedged one burned the interval on
|
* scheduler contention can make a CPU-bound loop consume far less CPU than wall
|
||||||
* a core. Half leaves room for a gap that is partly sleep and partly work, which
|
* time. One percent allows a little measurement/background jitter while erring
|
||||||
* is still worth reporting.
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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 {
|
export class LoopWatchdog {
|
||||||
#intervalMs: number;
|
#intervalMs: number;
|
||||||
|
|||||||
@@ -288,12 +288,13 @@ describe("LoopWatchdog long-block classification", () => {
|
|||||||
expect(warnSpy).not.toHaveBeenCalled();
|
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 warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {});
|
||||||
const h = cpuHarness();
|
const h = cpuHarness();
|
||||||
|
|
||||||
h.wd.start();
|
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();
|
h.fireTick();
|
||||||
|
|
||||||
expect(warnSpy).toHaveBeenCalledTimes(1);
|
expect(warnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user