perf(dispatch): index Client event bus and Dispatcher by event discriminant#57
Open
devin-ai-integration[bot] wants to merge 2 commits intomainfrom
Open
perf(dispatch): index Client event bus and Dispatcher by event discriminant#57devin-ai-integration[bot] wants to merge 2 commits intomainfrom
devin-ai-integration[bot] wants to merge 2 commits intomainfrom
Conversation
Dispatch used to linearly scan every registered subscription (on the Client event bus) and every handler (on the high-level Dispatcher) just to filter by event kind, which becomes O(total_subs) per event regardless of how few handlers actually care about a given discriminant. This change indexes both stores by mem::Discriminant<Event>: * EventBus (client/bus/subscription.rs): subscriptions are still owned in a single insertion-order Vec, but two secondary indexes (by_event: HashMap<Discriminant<Event>, Vec<usize>> plus any_indices: Vec<usize>) are maintained on subscribe, unsubscribe, unsubscribe_group, and clear. Dispatch merges the specific + wildcard index slices in ascending index order, so the visible firing order is identical to the previous implementation. Subscriptions only store the discriminant (not the full Event value) because that is all the filter check ever used. * Dispatcher (dispatch/dispatcher.rs): same two-index pattern, built up as handlers are added via add_handler / add_handler_any. The inner HandlerEntry no longer carries its own event filter — the entry's bucket placement encodes it. Behaviour is preserved: * every handler/subscription that would have fired before still fires; * cross-bucket firing order is preserved because both index lists stay in insertion (and therefore ascending) order and are merged; * wildcards continue to see every event; * unsubscribe/unsubscribe_group/clear rebuild the index so index integrity is restored without introducing tombstones. A new mock-only helper Client::mock_dispatch_bus_for_tests lets integration tests exercise the bus path synchronously without spinning the full poll loop. tests/indexed_dispatch_tests.rs (9 new tests) pins down the invariants: specific-only firing, wildcard fan-out, insertion-order merging, unrelated-event skip, Stop flow propagation, and EventBus unsubscribe / unsubscribe_event_group / clear_event_subscriptions behaviour across buckets.
Contributor
Author
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
…NTS.md
Devin Review flagged a two-line inline comment in `Subscription::matches`
explaining why the discriminant filter check was removed. AGENTS.md
("Coding Style & Naming Conventions") requires no inline comments in
library code; the commit drops the comment without changing behaviour.
3 tasks
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.
Summary
Both the
Clientsubscription bus (Client::on_event/Client::on_any) and the high-levelDispatcher(teamtalk::dispatch::Dispatcher) used to linearly scan every registered subscription/handler on every event, even though the filter check was justmem::discriminant(expected) == mem::discriminant(event). Dispatch cost was therefore O(total_subs) per event, not O(subscribers-actually-watching-this-event).This PR indexes both stores by
mem::Discriminant<Event>so dispatch only walks handlers that can possibly match the current event, while preserving the visible firing order.Changes:
crates/teamtalk/src/client/bus/subscription.rs—EventBusnow maintains two secondary indexes on top of the single insertion-orderVec<Subscription>:by_event: HashMap<Discriminant<Event>, Vec<usize>>— indices of event-specific subscribers;any_indices: Vec<usize>— indices of wildcard subscribers.subscribe,unsubscribe,unsubscribe_group, andclearkeep the indexes consistent (rebuild on removal, rather than tombstones).dispatchmerges the specific + wildcard index slices in ascending order, so cross-bucket firing order is identical to the pre-change linear scan.Subscriptiondrops itsevent: Option<Event>field; the discriminant is now implicit in bucket placement and an explicitevent_filter: Option<Discriminant<Event>>is used for index rebuilds after unsubscribe.crates/teamtalk/src/dispatch/dispatcher.rs/source.rs— same two-index pattern applied toDispatcher::handlers. The per-entry filter field is dropped fromHandlerEntry(bucket placement is the source of truth), andprocess_eventmerges the specific + wildcard slices.crates/teamtalk/src/client/core/mod.rs— new mock-only helperClient::mock_dispatch_bus_for_tests(event, source)so integration tests can exercise the bus path synchronously without spinning the full poll loop. Gated by#[cfg(feature = "mock")]alongside the other existingmock_*_for_testshelpers.crates/teamtalk/tests/indexed_dispatch_tests.rs(new, 9 tests):DispatchFlow::Stopfrom a wildcard slot surfaces as the step result;Client::on_event/on_anyregister through both buckets and dispatch merges in insertion order;unsubscribe_eventrebuilds the index and later dispatches skip the removed sub;unsubscribe_event_groupremoves from specific and wildcard buckets simultaneously;clear_event_subscriptionsresets both buckets.Total test count locally: 305 passed, 0 failed (296 pre-existing + 9 new),
cargo fmt/cargo clippy --all-features --all-targets -- -D warningsclean.Complexity
Before:
O(total_subs)per event (every sub checked for discriminant + filter match).After:
O(specific_for_this_event + wildcards)per event. The per-subscription filter-match check (user id, channel id, text type, predicate, etc.) is unchanged — only the discriminant filter moved into the bucket lookup.Not changed
bot/router/dispatch.rsis intentionally left linear: route ordering is semantically load-bearing there (thematched_command/matched_command_path/ auto-help fallback logic depends on walking routes in registration order, interleavingAny/Event/Commandmatchers). Indexing routes by event discriminant would change dispatch semantics; that belongs to a separate behavioural PR if wanted.Review & Testing Checklist for Human
EventBus::dispatchandDispatcher::process_eventpreserves strict insertion order across thespecific+anyindex lists when a handler is registered as wildcard between two specific handlers for the current event. Testinsertion_order_is_preserved_across_specific_and_wildcardandclient_bus_specific_and_wildcard_dispatch_in_insertion_orderpin this down, but a second pair of eyes on thewhile si < … || ai < …merge is worth it.unsubscribe/unsubscribe_group— they rebuild indexes viaretain+rebuild_indices. Ensure there's no subtle index-invalidation path I missed (the rebuild is deliberately O(n) on removal rather than tombstones to keep dispatch zero-cost).Client::mock_dispatch_bus_for_testsbecoming part of the publicmock-feature test surface (follows the same pattern as existingmock_apply_event_for_tests/mock_set_connection_state_for_tests).Notes
Client::on_event,on_any,unsubscribe_event,event_subscription_count,Dispatcher::on_event,Dispatcher::on_any, etc.). Handler closure signatures, return types, and visibility are identical tomain.mem::Discriminant<T>isHash + Eqsince Rust 1.73, so no nightly / no extra dependency is needed.semverCI gate will be red on this PR too — same pre-existing red as on feat(types): add StreamTypes newtype for stream-kind bit masks #52–feat(types): introduce SecretString and use it for LoginParams password #56, release-plz resolves it at release time.Link to Devin session: https://app.devin.ai/sessions/71fdd6196cb74723a2e277bb81993a9c
Requested by: @BlindMaster24