perf(events): typed event subscription to skip boxing unwanted events - #676
Conversation
Add EventKind + EventInterest and EventHandler::interest() (default: all kinds, so existing handlers and on_event are unaffected). CoreEventBus::dispatch skips materializing the event and invoking handlers whose declared interest excludes the kind, so a handler subscribed to a few kinds never pays for boxing the others. BotBuilder gains on_event_for(&[EventKind], handler). dhat (pingpong 10k, bot subscribed to Message/PairingQrCode/Connected/LoggedOut): the per-event handler-future box drops from 836 MB / 4 boxes per message to 418 MB / 1 box (the Receipt and other dispatches it ignores no longer box); total run 1410 -> 955 MB (-32%). 10000 replies, 0 dropped.
📝 WalkthroughSummary by CodeRabbit
WalkthroughHandlers now declare EventInterest (bitset of EventKind). CoreEventBus computes Event::kind() and dispatches only to handlers whose interest includes that kind. BotBuilder and Bot store RegisteredHandler { callback, interest } and expose on_event_for(...) for filtered registration; examples and history-sync retention logic updated accordingly. ChangesEvent Interest Filtering System
🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs:
Suggested labels: Mark: This needs to work right — verify EventKind coverage, tests, and that history-sync avoids unnecessary allocation. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 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 |
Benchmark Results67 unchanged benchmark(s)
|
Both example handlers act on only a handful of event kinds; subscribe via on_event_for so they demonstrate and benefit from the typed event interest (the bus stops boxing the handler future for the events they ignore).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 93d1833dd2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
retain_history_blob keyed off has_handlers(), so any registered handler forced the full decompress-and-materialize path even when none wanted HistorySync. Add CoreEventBus::has_handler_for(kind) and gate on HistorySync interest, so a message-only bot takes the streaming path during history replay instead of retaining the whole payload just to drop it at dispatch.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
wacore/src/types/events.rs (1)
195-285:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHarden
EventKind’s 64-kind bitset invariant (otherwise dispatch can go wrong).
EventInterestbuilds its mask via1u64 << (kind as u8). Once a futureEventKinddiscriminant reaches>= 64, debug shifts can panic, while release shifts mask the shift amount (effectively modulo 64), silently corrupting the interest bitset and breaking filtered dispatch. We can’t leave this as “only in a comment.”
- Enforce the “at most 64 kinds” limit in code (the suggested
EventKind::MexNotification as u8 < 64const-assert only trips ifMexNotificationremains the last variant—update it whenever the last variant changes).- Since
Eventis#[non_exhaustive], consider adding#[non_exhaustive]toEventKindso downstream matches are forced to use a wildcard now, not broken later.🤖 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 `@wacore/src/types/events.rs` around lines 195 - 285, Add a compile-time guard and non-exhaustive marker to prevent silent corruption when variant discriminants reach 64+: mark EventKind with #[non_exhaustive], and add a const assertion that the largest discriminant is < 64 (e.g. a const check using the last variant, e.g. MexNotification as u8 < 64) so the build fails if variants grow beyond the 64-bit limit; also update EventInterest's bit operations (methods of, with, wants) to use kind as u64 for the shift (1u64 << (kind as u64)) or otherwise ensure the shift operand is an integer type that makes the check above meaningful, so shifts never silently wrap—update the referenced symbols EventKind, EventInterest::of, EventInterest::with, and EventInterest::wants accordingly.
🤖 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.
Outside diff comments:
In `@wacore/src/types/events.rs`:
- Around line 195-285: Add a compile-time guard and non-exhaustive marker to
prevent silent corruption when variant discriminants reach 64+: mark EventKind
with #[non_exhaustive], and add a const assertion that the largest discriminant
is < 64 (e.g. a const check using the last variant, e.g. MexNotification as u8 <
64) so the build fails if variants grow beyond the 64-bit limit; also update
EventInterest's bit operations (methods of, with, wants) to use kind as u64 for
the shift (1u64 << (kind as u64)) or otherwise ensure the shift operand is an
integer type that makes the check above meaningful, so shifts never silently
wrap—update the referenced symbols EventKind, EventInterest::of,
EventInterest::with, and EventInterest::wants accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 6e8f2fd6-7b33-4cb5-8cec-6cccf1040fb4
📒 Files selected for processing (2)
src/history_sync.rswacore/src/types/events.rs
What
Let event handlers declare which
Eventkinds they want, so the bus can skip materializing and dispatching events nobody is subscribed to.EventKind(one discriminant perEventvariant) andEventInterest(au64bitset of kinds), plusEvent::kind().EventHandlergainsfn interest(&self) -> EventInterest, defaulting toEventInterest::ALL. Existing handlers and theadd_handlerpath keep receiving everything unchanged.CoreEventBus::dispatchcomputes the event's kind and skipsArc::new(event)andhandle_eventfor handlers whose interest excludes it. If no handler wants the kind, the event is dropped before it is ever wrapped.BotBuilder::on_event_for(&[EventKind], handler)registers a narrowly-scoped handler.on_eventis unchanged (all kinds).CoreEventBus::has_handler_for(kind)plus a history-sync gate change:retain_history_blobnow keys off HistorySync interest instead of "any handler registered", so a message-only bot takes the streaming history-sync path (from perf: implement streaming decompression for history sync processing #672) during replay instead of fully decompressing and retaining a blob nobody consumes.mainandbenchmarkexamples are migrated toon_event_for, since each acts on only a handful of kinds.Why
In the post-#675 ping/pong profile, boxing the handler future in
on_eventwas 59 percent of all allocated bytes. The bot was invoked (and its future boxed) for every dispatched event, including the per-messageReceiptit ignores. There was no way to tell the bus "I only care about these kinds." The same gap defeated #672's streaming history sync:retain_history_blob = has_handlers()retained the whole payload whenever any handler existed, even a message-only one.Results
dhat on ping/pong (10000 messages), bot subscribed via
on_event_forto{Message, PairingQrCode, Connected, LoggedOut}:Receiptand other dispatches the bot ignores no longer box).History sync: a bot that does not subscribe to
HistorySyncnow keeps the streaming path during replay (peak ~the largest single conversation) instead of materializing the full blob, per the #672 measurements.Compatibility
Not a breaking change.
interest()has a default ofALL, so every existingEventHandlerimpl and everyon_eventregistration behaves exactly as before, including the history-sync gate (a default handler is interested in HistorySync, so the blob is retained as it was). The win is opt-in viaon_event_for(or by overridinginterest()).Tests
interest_filters_dispatchunit test: a Message-only handler is skipped for aConnectedevent while anALLhandler still receives it, and a kind no handler wants never reaches a handler.cargo clippy --all-targets -- -D warningsclean (includes the migrated examples)cargo test -p wacore -p whatsapp-rust(827 + 657 passing)