Skip to content

Commit f0895f1

Browse files
authored
perf(lambda-rs): Optimize event dispatch (#192)
## Summary Optimizes `ApplicationRuntime` event dispatch by precomputing per-category listener buckets so each event is delivered only to interested components instead of scanning the full component stack. This reduces dispatch from `O(C)` per event to `O(k)` per event after a one-time `O(C)` startup index build, where `C` is total components and `k` is listeners for the event category. ## Related Issues ## Changes - Added per-category event listener indexing in `crates/lambda-rs/src/runtimes/application.rs` - Updated dispatch to iterate only listeners for the current event category - Added unit tests for bucket mapping, empty-mask rejection, and listener index construction ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] Feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation (updates to docs, specs, tutorials, or comments) - [ ] Refactor (code change that neither fixes a bug nor adds a feature) - [x] Performance (change that improves performance) - [x] Test (adding or updating tests) - [ ] Build/CI (changes to build process or CI configuration) ## Affected Crates - [x] `lambda-rs` - [ ] `lambda-rs-platform` - [ ] `lambda-rs-args` - [ ] `lambda-rs-logging` - [ ] Other: ## Checklist - [ ] Code follows the repository style guidelines (`cargo +nightly fmt --all`) - [ ] Code passes clippy (`cargo clippy --workspace --all-targets -- -D warnings`) - [ ] Tests pass (`cargo test --workspace`) - [x] New code includes appropriate documentation - [ ] Public API changes are documented - [ ] Breaking changes are noted in this PR description ## Testing **Commands run:** ```bash cargo test -p lambda-rs runtimes::application ``` **Manual verification steps (if applicable):** ## Screenshots/Recordings N/A ## Platform Testing - [x] macOS - [ ] Windows - [ ] Linux ## Additional Notes
2 parents d00056e + 2c38017 commit f0895f1

File tree

1 file changed

+145
-1
lines changed

1 file changed

+145
-1
lines changed

crates/lambda-rs/src/runtimes/application.rs

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,71 @@ fn dispatch_event_to_component(
186186
}
187187
}
188188

189+
const EVENT_CATEGORY_COUNT: usize = 5;
190+
191+
/// Maps a single event-category bit to the corresponding listener bucket.
192+
///
193+
/// This helper accepts only one of the concrete event category masks produced
194+
/// by [`Events::mask`]. It returns an error for `EventMask::NONE` or any mask
195+
/// outside the supported categories so the runtime can surface the invariant
196+
/// violation without panicking.
197+
fn event_listener_bucket(event_mask: EventMask) -> Result<usize, String> {
198+
if event_mask == EventMask::WINDOW {
199+
return Ok(0);
200+
}
201+
if event_mask == EventMask::KEYBOARD {
202+
return Ok(1);
203+
}
204+
if event_mask == EventMask::MOUSE {
205+
return Ok(2);
206+
}
207+
if event_mask == EventMask::RUNTIME {
208+
return Ok(3);
209+
}
210+
if event_mask == EventMask::COMPONENT {
211+
return Ok(4);
212+
}
213+
214+
return Err(format!(
215+
"Unsupported event mask for listener bucket: {:?}",
216+
event_mask
217+
));
218+
}
219+
220+
/// Builds a per-category index of component listeners for event dispatch.
221+
///
222+
/// Each component is inspected once during runtime startup and its position in
223+
/// the component stack is recorded in every bucket named by its [`EventMask`].
224+
/// This front-loads an `O(C)` setup cost so dispatch can visit only matching
225+
/// listeners for an event category instead of scanning all `C` components on
226+
/// every event.
227+
fn build_event_listener_index(
228+
components: &[Box<dyn Component<ComponentResult, String>>],
229+
) -> [Vec<usize>; EVENT_CATEGORY_COUNT] {
230+
let mut listeners = std::array::from_fn(|_| Vec::new());
231+
232+
for (index, component) in components.iter().enumerate() {
233+
let mask = component.event_mask();
234+
if mask.contains(EventMask::WINDOW) {
235+
listeners[0].push(index);
236+
}
237+
if mask.contains(EventMask::KEYBOARD) {
238+
listeners[1].push(index);
239+
}
240+
if mask.contains(EventMask::MOUSE) {
241+
listeners[2].push(index);
242+
}
243+
if mask.contains(EventMask::RUNTIME) {
244+
listeners[3].push(index);
245+
}
246+
if mask.contains(EventMask::COMPONENT) {
247+
listeners[4].push(index);
248+
}
249+
}
250+
251+
return listeners;
252+
}
253+
189254
const MAX_TARGET_FPS: u32 = 1000;
190255

191256
fn div_ceil_u64(numerator: u64, denominator: u64) -> u64 {
@@ -222,6 +287,7 @@ impl Runtime<(), String> for ApplicationRuntime {
222287
let mut event_loop = LoopBuilder::new().build();
223288
let window = self.window_builder.build(&mut event_loop);
224289
let mut component_stack = self.component_stack;
290+
let listener_index = build_event_listener_index(&component_stack);
225291
let render_context = match self.render_context_builder.build(&window) {
226292
Ok(ctx) => ctx,
227293
Err(err) => {
@@ -493,7 +559,34 @@ impl Runtime<(), String> for ApplicationRuntime {
493559
logging::trace!("Sending event: {:?} to all components", event);
494560

495561
let event_mask = event.mask();
496-
for component in &mut component_stack {
562+
let bucket = match event_listener_bucket(event_mask) {
563+
Ok(bucket) => bucket,
564+
Err(error) => {
565+
logging::error!("{}", error);
566+
publisher.publish_event(Events::Runtime {
567+
event: RuntimeEvent::ComponentPanic { message: error },
568+
issued_at: Instant::now(),
569+
});
570+
return;
571+
}
572+
};
573+
let listeners = &listener_index[bucket];
574+
for component_index in listeners {
575+
let component = match component_stack.get_mut(*component_index) {
576+
Some(component) => component,
577+
None => {
578+
let error = format!(
579+
"Listener index {} is out of bounds for component stack.",
580+
component_index
581+
);
582+
logging::error!("{}", error);
583+
publisher.publish_event(Events::Runtime {
584+
event: RuntimeEvent::ComponentPanic { message: error },
585+
issued_at: Instant::now(),
586+
});
587+
return;
588+
}
589+
};
497590
let event_result = dispatch_event_to_component(
498591
&event,
499592
event_mask,
@@ -723,4 +816,55 @@ mod tests {
723816
assert!(error.contains("A component has panicked while handling an event."));
724817
assert!(error.contains("window failure"));
725818
}
819+
820+
#[test]
821+
fn event_listener_bucket_maps_each_category() {
822+
assert_eq!(event_listener_bucket(EventMask::WINDOW), Ok(0));
823+
assert_eq!(event_listener_bucket(EventMask::KEYBOARD), Ok(1));
824+
assert_eq!(event_listener_bucket(EventMask::MOUSE), Ok(2));
825+
assert_eq!(event_listener_bucket(EventMask::RUNTIME), Ok(3));
826+
assert_eq!(event_listener_bucket(EventMask::COMPONENT), Ok(4));
827+
}
828+
829+
#[test]
830+
fn event_listener_bucket_rejects_empty_mask() {
831+
let error = event_listener_bucket(EventMask::NONE).unwrap_err();
832+
assert!(error.contains("Unsupported event mask"));
833+
}
834+
835+
#[test]
836+
fn event_listener_bucket_rejects_multi_bit_masks() {
837+
let error = event_listener_bucket(EventMask::WINDOW | EventMask::KEYBOARD)
838+
.unwrap_err();
839+
assert!(error.contains("Unsupported event mask"));
840+
}
841+
842+
#[test]
843+
fn build_event_listener_index_registers_only_matching_components() {
844+
let components: Vec<Box<dyn Component<ComponentResult, String>>> = vec![
845+
Box::new(RecordingComponent {
846+
mask: EventMask::WINDOW | EventMask::KEYBOARD,
847+
..Default::default()
848+
}),
849+
Box::new(RecordingComponent {
850+
mask: EventMask::RUNTIME,
851+
..Default::default()
852+
}),
853+
Box::new(RecordingComponent {
854+
mask: EventMask::NONE,
855+
..Default::default()
856+
}),
857+
Box::new(RecordingComponent {
858+
mask: EventMask::MOUSE | EventMask::COMPONENT,
859+
..Default::default()
860+
}),
861+
];
862+
863+
let listeners = build_event_listener_index(&components);
864+
assert_eq!(listeners[0], vec![0]);
865+
assert_eq!(listeners[1], vec![0]);
866+
assert_eq!(listeners[2], vec![3]);
867+
assert_eq!(listeners[3], vec![1]);
868+
assert_eq!(listeners[4], vec![3]);
869+
}
726870
}

0 commit comments

Comments
 (0)