loadgen: route room-read/read-receipt/members through the batched pacer#374
Conversation
The room-read, read-receipt, and members workloads each rolled their own one-event-per-tick time.Ticker dispatch loop instead of using the shared pacedDispatch/pacer. At sub-millisecond intervals the Go runtime silently coalesces ticks, so achieved RPS plateaued at a few thousand regardless of --steps (room-read stuck ~1-2k, reported INCONCLUSIVE). Raising MAX_IN_FLIGHT could not help: the in-flight pool never filled because the emitter could not release enough events — the bottleneck was emit-side, not pool-side, and the legacy path did not even account emit underrun, so the verdict fell through to a generic "load box limited" with no actionable knob. Route all three generators through the batched pacer (MaxInFlight<=0 keeps the legacy serial path for bisection) and wire an emit-underrun counter into each: RoomReadCollector/ReadReceiptCollector gain RecordUnderrun/UnderrunCount, buildRoomReadInputs/buildReadReceiptInputs populate EmitUnderrun, and the members generator tallies an "underrun" publish-error reason while preserving its takeNext/giveBack/ErrPoolsExhausted semantics. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9bAdsThfEHmwzVNJapCyH
📝 WalkthroughWalkthroughUnderrun tracking is added to ChangesUnderrun Tracking and Paced Dispatch Refactor
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tools/loadgen/members_generator.go (1)
180-229: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoffOptional: this inline pacer loop largely duplicates
pacedDispatch. The ticker/pacer/semaphore/drain scaffolding mirrorspacedDispatch(pacer.go), differing only in the per-eventtakeNext/giveBack/ErrPoolsExhaustedsemantics. Reuse isn't trivial because this path returns an error and mutates a finite pool, so this is acceptable as-is — but ifpacedDispatchlater grows a per-event hook returning a "continue/stop" signal, consider collapsing this onto it to avoid drift between the two pacing loops.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tools/loadgen/members_generator.go` around lines 180 - 229, The pacer loop in the members_generator.go file (lines 180-229) duplicates the ticker, pacer, semaphore, and drain scaffolding from pacedDispatch in pacer.go. If pacedDispatch is later enhanced to support per-event hooks with continue/stop signals, refactor these two loops to share a common pacing implementation. Extract the shared scaffolding (ticker initialization, pacer tick logic, semaphore management, and drain function) into a reusable helper that accepts a callback function for per-event processing, allowing the members_generator loop to pass its takeNext/giveBack/error handling logic as a callback while pacedDispatch passes its own event logic, thereby eliminating drift between the two pacing patterns.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tools/loadgen/members_generator.go`:
- Around line 180-229: The pacer loop in the members_generator.go file (lines
180-229) duplicates the ticker, pacer, semaphore, and drain scaffolding from
pacedDispatch in pacer.go. If pacedDispatch is later enhanced to support
per-event hooks with continue/stop signals, refactor these two loops to share a
common pacing implementation. Extract the shared scaffolding (ticker
initialization, pacer tick logic, semaphore management, and drain function) into
a reusable helper that accepts a callback function for per-event processing,
allowing the members_generator loop to pass its takeNext/giveBack/error handling
logic as a callback while pacedDispatch passes its own event logic, thereby
eliminating drift between the two pacing patterns.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8e2ed64d-d030-4d3a-b297-fb7397e83fec
📒 Files selected for processing (12)
tools/loadgen/maxrps_readreceipt.gotools/loadgen/maxrps_readreceipt_test.gotools/loadgen/maxrps_roomread.gotools/loadgen/maxrps_roomread_test.gotools/loadgen/members_generator.gotools/loadgen/metrics.gotools/loadgen/readreceipt_collector.gotools/loadgen/readreceipt_collector_test.gotools/loadgen/readreceipt_generator.gotools/loadgen/roomread_collector.gotools/loadgen/roomread_collector_test.gotools/loadgen/roomread_generator.go
The room-read, read-receipt, and members workloads each rolled their own
one-event-per-tick time.Ticker dispatch loop instead of using the shared
pacedDispatch/pacer. At sub-millisecond intervals the Go runtime silently
coalesces ticks, so achieved RPS plateaued at a few thousand regardless of
--steps (room-read stuck ~1-2k, reported INCONCLUSIVE). Raising MAX_IN_FLIGHT
could not help: the in-flight pool never filled because the emitter could not
release enough events — the bottleneck was emit-side, not pool-side, and the
legacy path did not even account emit underrun, so the verdict fell through to
a generic "load box limited" with no actionable knob.
Route all three generators through the batched pacer (MaxInFlight<=0 keeps the
legacy serial path for bisection) and wire an emit-underrun counter into each:
RoomReadCollector/ReadReceiptCollector gain RecordUnderrun/UnderrunCount,
buildRoomReadInputs/buildReadReceiptInputs populate EmitUnderrun, and the
members generator tallies an "underrun" publish-error reason while preserving
its takeNext/giveBack/ErrPoolsExhausted semantics.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01P9bAdsThfEHmwzVNJapCyH
Summary by CodeRabbit
Release Notes
New Features
Tests
Chores