Skip to content

perf(events): typed event subscription to skip boxing unwanted events - #676

Merged
jlucaso1 merged 3 commits into
mainfrom
perf/typed-event-subscription
Jun 1, 2026
Merged

perf(events): typed event subscription to skip boxing unwanted events#676
jlucaso1 merged 3 commits into
mainfrom
perf/typed-event-subscription

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

What

Let event handlers declare which Event kinds they want, so the bus can skip materializing and dispatching events nobody is subscribed to.

  • New EventKind (one discriminant per Event variant) and EventInterest (a u64 bitset of kinds), plus Event::kind().
  • EventHandler gains fn interest(&self) -> EventInterest, defaulting to EventInterest::ALL. Existing handlers and the add_handler path keep receiving everything unchanged.
  • CoreEventBus::dispatch computes the event's kind and skips Arc::new(event) and handle_event for 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_event is unchanged (all kinds).
  • CoreEventBus::has_handler_for(kind) plus a history-sync gate change: retain_history_blob now 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.
  • The main and benchmark examples are migrated to on_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_event was 59 percent of all allocated bytes. The bot was invoked (and its future boxed) for every dispatched event, including the per-message Receipt it 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_for to {Message, PairingQrCode, Connected, LoggedOut}:

  • Handler-future box: 836 MB / 4 boxes per message to 418 MB / 1 box (the Receipt and other dispatches the bot ignores no longer box).
  • Total run: 1410 MB to 955 MB (about -32 percent).
  • 10000 replies sent, 0 dropped (message handling unchanged).

History sync: a bot that does not subscribe to HistorySync now 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 of ALL, so every existing EventHandler impl and every on_event registration 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 via on_event_for (or by overriding interest()).

Tests

  • New interest_filters_dispatch unit test: a Message-only handler is skipped for a Connected event while an ALL handler still receives it, and a kind no handler wants never reaches a handler.
  • cargo clippy --all-targets -- -D warnings clean (includes the migrated examples)
  • cargo test -p wacore -p whatsapp-rust (827 + 657 passing)

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.
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added on_event_for() method to selectively register event handlers for specific event types.
    • Handlers can now be configured to respond only to events they're interested in.
  • Backward Compatibility

    • The existing on_event() method remains fully functional and unchanged for all-events subscriptions.

Walkthrough

Handlers 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.

Changes

Event Interest Filtering System

Layer / File(s) Summary
EventKind and EventInterest types
wacore/src/types/events.rs
EventKind enum maps each Event variant to a discriminant; EventInterest is a bitset tracking handler subscriptions. EventHandler trait gains interest() method (defaults to all kinds).
Event::kind mapping
wacore/src/types/events.rs
Event::kind(&self) method maps every Event variant to its EventKind, allowing dispatch to determine event kind without full handler materialization.
CoreEventBus dispatch filtering
wacore/src/types/events.rs
CoreEventBus::has_handler_for(kind) and dispatch compute event kind and only call handle_event on handlers whose declared interest includes that kind. Unit test verifies uninterested handlers are skipped.
Bot RegisteredHandler and builder API
src/bot.rs
Introduces RegisteredHandler { callback, interest }; BotEventHandler stores it and reports interest(); Bot/BotBuilder fields updated; adds BotBuilder::on_event_for(&[EventKind], handler) and adapts on_event to register with interest.
Examples, main, and history-sync retention
examples/benchmark.rs, src/main.rs, src/history_sync.rs
Examples and main switch from on_event(...) to on_event_for(&[EventKind...]) and import EventKind; history-sync retains/decompresses full blob only when has_handler_for(EventKind::HistorySync) is true.

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs:

Suggested labels:
api-design

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)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding typed event subscription to avoid boxing unwanted events, which is the core performance optimization in this PR.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the what, why, results, compatibility, and tests for the event subscription feature.
Docstring Coverage ✅ Passed Docstring coverage is 96.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/typed-event-subscription

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

Benchmark Results

67 unchanged benchmark(s)
Benchmark Current Baseline Change
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message() 3,933 3,933 +0.0%
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message() 12,038 12,038 +0.0%
reporting_token_benchmark::key_derivation_group::bench_key_derivation 43,514 43,514 +0.0%
reporting_token_benchmark::token_calculation_group::bench_token_calculation 19,365 19,365 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple() 68,579 68,579 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended() 76,679 76,679 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message() 2,230 2,230 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message() 5,988 5,988 +0.0%
send_receive_benchmark::dm_send::bench_dm_send text:setup_dm_send() 182,940 183,240 -0.2%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 2,204,889 2,205,397 -0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 888,583 888,670 -0.0%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 1,027,876 1,028,069 -0.0%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 1,760,316 1,760,890 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 1,131,120 1,131,734 -0.1%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 2,133,853 2,143,707 -0.5%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 7,284,355 7,293,892 -0.1%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 12,647,625 12,682,303 -0.3%
binary_benchmark::marshal_group::bench_marshal_allocating 71,296 71,296 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_allocating 71,349 71,349 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_allocating 98,416 98,416 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer 78,796 78,796 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer 71,396 71,396 +0.0%
binary_benchmark::marshal_group::bench_marshal_long_string 7,599 7,599 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_long_string 7,642 7,642 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_long_string 9,354 9,354 +0.0%
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating 530,581 530,581 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating 530,149 530,149 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating 531,504 531,504 +0.0%
binary_benchmark::marshal_group::bench_marshal_many_children_allocating 8,506,104 8,506,104 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating 8,450,452 8,450,452 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating 19,677,941 19,677,941 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled() 2,468 2,468 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled() 33,558 33,558 +0.0%
binary_benchmark::unpack_group::bench_unpack_uncompressed 787 787 +0.0%
binary_benchmark::unpack_group::bench_unpack_compressed 526,830 526,830 +0.0%
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled() 4,990 4,990 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled() 5,315 5,315 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled() 61,874 61,874 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled() 5,347 5,347 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled() 61,942 61,942 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled() 6,734 6,734 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled() 85,585 85,585 +0.0%
binary_benchmark::child_iteration_group::bench_get_children_by_tag 477,570 477,570 +0.0%
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled() 11,563 11,563 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u32 396 396 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u32 120 120 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u64 439 439 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u64 153 153 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_i64 499 499 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_i64 162 162 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_loop_100_u64 44,624 44,624 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_loop_100_u64 16,424 16,424 +0.0%
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users() 17,260,180 17,397,968 -0.8%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 157,179 157,179 +0.0%
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message() 5,513,975 5,513,975 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session() 157,539 157,539 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender() 296,767 296,767 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution() 706,282 706,282 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message() 12,547,378 12,609,298 -0.5%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 27,635,430 27,528,646 +0.4%
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message() 3,467,011 3,467,011 +0.0%
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message() 124,594,113 126,138,683 -1.2%
libsignal_benchmark::signature_group::bench_key_generation keygen 2,830,452 2,830,452 +0.0%
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions() 46,566 46,566 +0.0%
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages() 5,197,012 5,197,012 +0.0%
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session() 360,648 360,648 +0.0%
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction() 14,255,917 14,255,917 +0.0%
No significant changes detected.

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).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread wacore/src/types/events.rs
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Harden EventKind’s 64-kind bitset invariant (otherwise dispatch can go wrong).

EventInterest builds its mask via 1u64 << (kind as u8). Once a future EventKind discriminant 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 < 64 const-assert only trips if MexNotification remains the last variant—update it whenever the last variant changes).
  • Since Event is #[non_exhaustive], consider adding #[non_exhaustive] to EventKind so 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

📥 Commits

Reviewing files that changed from the base of the PR and between 93d1833 and a35149d.

📒 Files selected for processing (2)
  • src/history_sync.rs
  • wacore/src/types/events.rs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant