Skip to content

perf(events): snapshot handlers behind an Arc to drop the per-event Vec clone - #719

Merged
jlucaso1 merged 2 commits into
mainfrom
perf/event-bus-arc-handlers
Jun 5, 2026
Merged

perf(events): snapshot handlers behind an Arc to drop the per-event Vec clone#719
jlucaso1 merged 2 commits into
mainfrom
perf/event-bus-arc-handlers

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Problem

CoreEventBus::dispatch is on the hottest paths in the client (one call per
inbound message in message.rs, one per receipt in receipt.rs, plus every
notification, presence, group and app-state update). On each event it cloned the
entire Vec<Arc<dyn EventHandler>> out of the RwLock: that is a heap
allocation for the backing buffer plus one atomic refcount bump per registered
handler, every single time. Worse, the interest precheck ran after the clone,
so an event that no handler was interested in still paid the full clone before
being dropped.

The handler list is append-only and in practice only mutated at startup (every
register_handler / add_handler call), so paying a per-event allocation for a
list that never changes during steady state is pure overhead.

Change

Store the handlers in an immutable HandlerSnapshot held behind an Arc
(RwLock<Arc<HandlerSnapshot>>). Dispatch now clones only the outer Arc (one
refcount bump, zero Vec allocation), then drops the lock before iterating.

The snapshot also caches the OR of every handler's EventInterest as a single
aggregate bitmask. dispatch tests aggregate.wants(kind) first, so an event
no handler wants short-circuits on one bitmask check and materializes nothing.
has_handler_for and has_handlers read this cached snapshot too, dropping
their previous lock-and-scan over the handler list.

add_handler rebuilds the snapshot copy-on-write under the write lock and swaps
it in, OR-ing the new handler's interest into the aggregate. Registration order
is preserved, so observable handler ordering is unchanged.

Re-entrancy and concurrency are safe by construction: dispatch only holds the
read lock long enough to clone the outer Arc, then releases it. A concurrent
add_handler swaps in a fresh Arc, but the snapshot a dispatch already cloned
keeps it alive and unchanged for the duration of that dispatch, so an iteration
never observes a mutated list and a handler added mid-dispatch is not invoked
for the in-flight event.

Tests

All existing event tests stay green. Added:

  • aggregate_interest_and_has_handler_for: the cached aggregate is the OR of
    the registered handlers' interests and answers has_handler_for /
    has_handlers correctly, including the empty-bus case.
  • dispatch_preserves_handler_ordering: copy-on-write rebuilds keep handlers in
    registration order.
  • dispatch_is_reentrancy_safe_against_concurrent_add: a handler that registers
    another handler while it is being dispatched does not deadlock, the new
    handler is not invoked for the in-flight event, and the next dispatch sees
    both. This locks in the snapshot-outlives-swap guarantee.
  • The existing interest_filters_dispatch already proves an event no handler
    wants invokes nothing; that path now exercises the cached aggregate
    short-circuit.

Performance

Per-event cost on the hot dispatch path drops from "allocate a Vec + N atomic
bumps + post-clone interest scan" to "one atomic bump + one bitmask test", and
an ignored-kind event now costs a single bitmask test with no allocation at all.
The added cost moves to add_handler, which rebuilds the snapshot, but that runs
only at startup.

…ec clone

CoreEventBus::dispatch cloned the whole Vec<Arc<dyn EventHandler>> out of the
RwLock on every event (a heap allocation plus one atomic bump per handler), and
the interest precheck only ran after that clone, so an event no handler wanted
still paid for it. The handler list is append-only and only mutated at startup.

Store the handlers in an immutable snapshot behind an Arc so dispatch clones
just the outer Arc (one refcount bump, zero Vec allocation), and cache the OR
of every handler's interest in that snapshot so an ignored kind short-circuits
on a single bitmask test before touching anything else. add_handler rebuilds
and swaps the snapshot copy-on-write, keeping registration order. has_handlers
and has_handler_for now read the snapshot instead of locking and scanning.
@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 59b2a5da-b798-4312-a3a8-c6417b6c81fb

📥 Commits

Reviewing files that changed from the base of the PR and between 0c4999c and e3b249e.

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

📝 Walkthrough

Summary by CodeRabbit

  • Refactor
    • Event dispatch now uses immutable handler snapshots for stable, efficient iteration; registration order is preserved and dispatch is reentrancy-safe.
    • Handler presence checks now query the current snapshot and dispatch re-evaluates handler interest at runtime.
  • Tests
    • New unit tests validating snapshot semantics, ordering, interest evaluation, and reentrancy behavior.

Walkthrough

CoreEventBus now uses immutable copy-on-write HandlerSnapshot objects stored as Arc<RwLock<Arc>>. add_handler rebuilds and swaps snapshots under a write lock; dispatch clones the outer snapshot Arc, drops locks, checks a cached aggregate interest, and iterates a stable ordered handler list. Tests cover interest widening, OR semantics, ordering, and reentrancy-safe registration.

Changes

Handler Snapshot Mechanism

Layer / File(s) Summary
Handler snapshot refactoring and validation
wacore/src/types/events.rs
Private HandlerSnapshot introduced; CoreEventBus now stores Arc<RwLock<Arc<HandlerSnapshot>>>. add_handler, has_handlers, has_handler_for, and dispatch use snapshot copy-on-write semantics: add_handler rebuilds and swaps snapshots under write lock, dispatch clones snapshot Arc once and iterates without holding the lock while re-evaluating handler interest() at dispatch time. Unit tests added for interest widening, has_handler_for OR semantics, registration order preservation, and reentrancy behavior when handlers register new handlers during dispatch.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • oxidezap/whatsapp-rust#676: Modifies CoreEventBus::dispatch and has_handler_for filtering logic; directly related to the snapshot-based dispatch and interest filtering changes in this PR.

Suggested labels

api-design

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main optimization: replacing per-event Vec cloning with Arc-backed handler snapshots.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, clearly explaining the problem, solution, tests, and performance implications.
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/event-bus-arc-handlers

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.

@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: 0c4999cd87

ℹ️ 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 Outdated
@github-actions

github-actions Bot commented Jun 4, 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() 2,838 2,838 +0.0%
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message() 8,272 8,272 +0.0%
reporting_token_benchmark::key_derivation_group::bench_key_derivation 31,317 31,317 +0.0%
reporting_token_benchmark::token_calculation_group::bench_token_calculation 13,827 13,827 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple() 49,398 49,398 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended() 54,827 54,827 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message() 1,592 1,592 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message() 4,219 4,219 +0.0%
send_receive_benchmark::dm_send::bench_dm_send text:setup_dm_send() 113,083 112,838 +0.2%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 1,656,280 1,656,167 +0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 650,176 650,214 -0.0%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 874,323 874,254 +0.0%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 2,082,063 2,081,922 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 747,263 747,418 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 1,328,311 1,328,154 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 4,381,675 4,369,007 +0.3%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 515,006 513,400 +0.3%
binary_benchmark::marshal_group::bench_marshal_allocating 45,401 45,401 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_allocating 45,451 45,451 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_allocating 66,354 66,354 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer 43,512 43,512 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer 45,507 45,507 +0.0%
binary_benchmark::marshal_group::bench_marshal_long_string 4,930 4,930 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_long_string 4,961 4,961 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_long_string 6,732 6,732 +0.0%
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating 528,529 528,529 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating 528,150 528,150 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating 529,396 529,396 +0.0%
binary_benchmark::marshal_group::bench_marshal_many_children_allocating 5,417,786 5,417,786 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating 5,362,043 5,362,043 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating 13,276,336 13,276,336 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled() 1,850 1,850 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled() 29,217 29,217 +0.0%
binary_benchmark::unpack_group::bench_unpack_uncompressed 618 618 +0.0%
binary_benchmark::unpack_group::bench_unpack_compressed 672,890 672,890 +0.0%
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled() 3,736 3,736 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled() 3,840 3,840 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled() 48,283 48,283 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled() 3,866 3,866 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled() 48,344 48,344 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled() 5,206 5,206 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled() 66,668 66,668 +0.0%
binary_benchmark::child_iteration_group::bench_get_children_by_tag 310,312 310,312 +0.0%
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled() 8,286 8,286 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u32 254 254 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u32 91 91 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u64 292 292 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u64 137 137 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_i64 317 317 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_i64 145 145 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_loop_100_u64 27,425 27,425 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_loop_100_u64 10,725 10,725 +0.0%
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users() 4,144,465 4,140,145 +0.1%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 100,131 100,133 -0.0%
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message() 4,263,827 4,263,827 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session() 99,803 99,803 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender() 210,262 210,262 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution() 496,921 496,921 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message() 507,354 509,391 -0.4%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 11,978,281 11,975,618 +0.0%
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message() 2,466,138 2,466,138 +0.0%
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message() 4,922,382 4,888,952 +0.7%
libsignal_benchmark::signature_group::bench_key_generation keygen 2,043,397 2,043,397 +0.0%
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions() 37,950 37,950 +0.0%
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages() 3,616,916 3,616,956 -0.0%
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session() 223,397 223,397 +0.0%
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction() 9,980,959 9,980,959 +0.0%
No significant changes detected.

Drop the cached interest aggregate computed at add_handler: it short-circuited dispatch using a stale snapshot, so a handler whose interest() widens at runtime stopped receiving the newly-wanted kinds (the pre-change code re-evaluated interest every dispatch). Keep the Arc handler snapshot (the actual win: no per-event Vec clone) and re-evaluate interest over it in dispatch and has_handler_for. Adds a regression test for a handler that widens its interest after registration.
@jlucaso1
jlucaso1 merged commit 67df75d into main Jun 5, 2026
10 checks passed
@jlucaso1
jlucaso1 deleted the perf/event-bus-arc-handlers branch June 5, 2026 00:27
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