Keep a pacing stretch open across freed slots - #78
Merged
Conversation
Preventive pacing was meant to report once per throttled stretch, but the stretch ended the moment `msUntilBurstSafe()` returned zero. A saturated rolling window frees exactly one slot at a time and the loop consumes it immediately, so the wait alternates between a fraction of a second and zero from one track to the next. Ending on the first zero ended the stretch once per track, which turned "one event per track" into "one begin/end pair per track": telemetry after the deploy showed 439 stretches in a day, a median `paced_tracks` of 1, waits of 4-40ms, and 0.66 events per paced track where roughly a hundredth of one was intended. Require a streak of genuinely unimpeded tracks before leaving the paced state. Saturation cannot produce three zero-wait tracks in a row, so the stretch now stays open for as long as the throttling lasts, while a window that really has cleared - or an adaptive burst limit raise, which frees a block of slots at once - still exits within three tracks. The regression test seeds the burst window so slots free in *pairs*, which makes the alternation structural rather than a race: the first track of each pair always waits and the second always finds a slot free. An earlier version tried to reproduce it by varying the mocked reply latency around the steady-state spacing, but the send rate is capped by the window regardless of how fast Last.fm answers, so the loop simply drifted permanently ahead and both the fixed and the broken code passed. Also document the local Playwright footgun that hid this for three runs: `reuseExistingServer: true` on a fixed port will silently test whichever checkout already has a dev server up, so a fix verified against another worktree's build looks verified but is not. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f5423b1e-b316-412a-bade-c28bf90eeb0f
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
What
Preventive pacing was supposed to report once per throttled stretch. PR #76 shipped that, but the stretch ended on the first track that needed no wait — and a saturated rolling window frees exactly one slot at a time, which the loop consumes immediately. So
msUntilBurstSafe()alternates between a fraction of a second and zero from one track to the next, and the stretch ended roughly every other track.Net effect: instead of ~1 event per stretch, we got a begin/end pair per track. A 34% reduction where ~100x was intended.
Evidence from production (post-#76 deploy)
Median
paced_tracksis 1. A single user's timeline shows the shape plainly — waits of 4ms and 6ms,burst_countsitting exactly onburst_limit:Fix
PACING_EXIT_CLEAR_TRACKS = 3— require a streak of genuinely unimpeded tracks before leaving the paced state. Saturation cannot produce three zero-wait tracks in a row, so the stretch stays open for as long as the throttling lasts. A window that really has cleared, or an adaptive burst-limit raise that frees a block of slots at once, still exits within three tracks.RateLimitTrackeris deliberately untouched: one-slot-at-a-time is the right answer for throughput, it was only the wrong thing to read as "the throttling is over".Test
a throttled stretch is continuous, it does not restart on every freed slotseeds the burst window so slots free in pairs, making the alternation structural rather than a race — the first track of each pair always waits, the second always finds a slot free. It counts how often the pacing notice comes back after disappearing (counting reappearances rather than disappearances ignores the last one, which is the stretch legitimately ending as the queue drains).Verified against
git stash push -- src/components/ScrobbleStep.vue: 11 reappearances without the fix, 0 with.An earlier attempt varied the mocked reply latency around the steady-state spacing instead. That does not work: the send rate is capped by the window no matter how fast Last.fm answers, so the loop drifts permanently ahead, pacing ends once and never restarts, and both the fixed and the broken code pass.
Also
Documents a local Playwright footgun that hid this through three "verified" runs.
playwright.config.tssetsreuseExistingServer: trueon a fixed port, so avue-cli-service servealready running from another checkout or worktree gets tested instead of yours, silently. CI is unaffected — it starts from nothing.Data note
paced_tracks/scrobble_pacing_endedbetween 2026-08-04 and this deploy are flap-inflated and not comparable with later data. Recorded in AGENTS.md alongside the existing note about the 07-27 to 07-29 window.