-
Notifications
You must be signed in to change notification settings - Fork 0
perf(plugin): yield the sweep on a time budget, and measure loop lag per worker #125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
harper-joseph
wants to merge
1
commit into
main
Choose a base branch
from
perf/yield-budget
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| /** | ||
| * PER-WORKER EVENT-LOOP DELAY, so a stall can be attributed instead of guessed at. | ||
| * | ||
| * WHY THIS EXISTS. The bot-facing `duration` (`path: 'p'`) metric showed, on the production fleet, | ||
| * a median and p95 identical across every worker (1.6ms / ~2.7ms) while five workers carried a p99 | ||
| * of 13-53ms against 3.6-7.4ms elsewhere. Median flat, p95 flat, p99 blown out by 10-30x is the | ||
| * signature of intermittent event-loop blocking: it only touches the small share of requests that | ||
| * land inside a stall. The arithmetic said the ready-set sweep must cause ~11ms slices (200 rows at | ||
| * ~55us/row, before `queue.ready.yieldBudget` replaced the row count) — but the tail was broader | ||
| * than one worker, and the sweep self-gates to `workerIndex === 0`. So the shape was CONSISTENT with | ||
| * the sweep and could not be pinned on it, and the other candidates (queue-status sync, reconciler, | ||
| * sitemap refresh, GC) were indistinguishable from it in that data. | ||
| * | ||
| * Lag measured per worker separates them. Whatever only worker 0 does shows up only on worker 0. | ||
| * | ||
| * WHY `monitorEventLoopDelay` AND NOT A TIMER-DRIFT LOOP. The libuv-side histogram samples in C++ | ||
| * at a fixed interval and costs nothing measurable, where the usual `setTimeout`-drift trick both | ||
| * competes for the loop it is measuring and misses any stall shorter than its own interval. | ||
| */ | ||
| import { monitorEventLoopDelay } from 'node:perf_hooks'; | ||
| import { config, onConfigApplied } from '../config.js'; | ||
| import { metrics } from '../metrics.js'; | ||
|
|
||
| const NS_PER_MS = 1e6; | ||
|
|
||
| /** Node's ceiling for `setInterval`; past it the delay overflows and fires after 1ms. */ | ||
| const MAX_TIMER_MS = 2147483647; | ||
|
|
||
| /** | ||
| * The two statistics for one window, in ms, with unusable readings dropped. | ||
| * | ||
| * EXTRACTED SO IT CAN BE TESTED, because the failure it guards is silent and fleet-wide. An empty | ||
| * histogram — a window in which libuv took no sample — returns `Infinity` from `percentile()` and | ||
| * `0`/`Infinity` from `max`. Emitting `Infinity` does not just add a bad row: `recordAnalytics` | ||
| * aggregates by mean, so one `Infinity` makes the mean of the merged row `Infinity` for that whole | ||
| * period, across every worker. The series would read as catastrophic while nothing was wrong. | ||
| * | ||
| * READ BEFORE RESET is the caller's job and equally load-bearing: the histogram is cumulative, so a | ||
| * window that never resets pins the p99 at the worst stall since boot and never recovers. | ||
| */ | ||
| export const readLag = (histogram) => { | ||
| const out = {}; | ||
| const p99 = histogram.percentile(99) / NS_PER_MS; | ||
| const max = histogram.max / NS_PER_MS; | ||
| if (Number.isFinite(p99)) out.p99 = p99; | ||
| if (Number.isFinite(max)) out.max = max; | ||
| return out; | ||
| }; | ||
|
|
||
| let started = false; | ||
|
|
||
| /** | ||
| * Sample this worker's loop delay on an interval and report it. | ||
| * | ||
| * NOT gated to one worker, unlike every other periodic task here — see the module comment. Idempotent, | ||
| * and it follows `management.eventLoopLagInterval` without a restart. | ||
| */ | ||
| export function startEventLoopLagMonitor() { | ||
| if (started) return; | ||
| const interval = () => Math.max(0, config.management.eventLoopLagInterval | 0); | ||
| if (interval() <= 0) return; | ||
| started = true; | ||
|
|
||
| // `resolution` is how often libuv samples. 20ms is coarse enough to cost nothing and fine enough | ||
| // to catch a slice of the size this exists to look for; a stall shorter than one sample is, by | ||
| // construction, shorter than the thing being investigated. | ||
| const histogram = monitorEventLoopDelay({ resolution: 20 }); | ||
| histogram.enable(); | ||
|
|
||
| const report = () => { | ||
| // Read BEFORE reset — see `readLag`. Both halves matter and neither is obvious from the call. | ||
| const lag = readLag(histogram); | ||
| histogram.reset(); | ||
| if (lag.p99 !== undefined) metrics.eventLoopLag(lag.p99, 'p99', server.workerIndex); | ||
| if (lag.max !== undefined) metrics.eventLoopLag(lag.max, 'max', server.workerIndex); | ||
| }; | ||
|
|
||
| let armed = interval(); | ||
| let timer = setInterval(report, Math.min(MAX_TIMER_MS, armed)); | ||
| timer.unref?.(); | ||
|
|
||
| onConfigApplied(() => { | ||
| if (interval() === armed) return; | ||
| clearInterval(timer); | ||
| armed = interval(); | ||
| timer = armed > 0 ? setInterval(report, Math.min(MAX_TIMER_MS, armed)) : null; | ||
| timer?.unref?.(); | ||
| }); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are three issues with the current implementation of
startEventLoopLagMonitor:config.management.eventLoopLagIntervalis0(disabled) at startup, the function returns early on line 61. This preventsonConfigAppliedfrom being registered, meaning the monitor can never be enabled at runtime without a full process restart, violating the live-reload contract.| 0onconfig.management.eventLoopLagIntervalcan wrap large values (greater than 2^31 - 1) to negative numbers, whichMath.max(0, ...)then clamps to0, disabling the timer entirely instead of using the configured interval. We should avoid bitwise coercion and useMath.floorinstead.We can resolve these issues by registering
onConfigAppliedunconditionally, usingMath.floorinstead of bitwise coercion, and flushing the remaining histogram data when disabling the monitor.References
setIntervalorsetTimeoutto not exceed2147483647to avoid unexpected hot loops, and avoid using bitwise coercion (e.g.,| 0) for large numbers.