Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 75 additions & 1 deletion ixa-bench/criterion/set_property.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
use std::cell::RefCell;
use std::hint::black_box;

use criterion::{criterion_group, criterion_main, Criterion};
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
use ixa::prelude::*;
use ixa::{impl_derived_property, impl_property};

define_entity!(Person);

define_property!(struct IndependentValue(u64), Person, default_const = IndependentValue(0));
define_property!(struct BaseValue(u64), Person, default_const = BaseValue(0));

#[derive(Debug, PartialEq, Eq, Clone, Copy, serde::Serialize, Hash)]
struct MixedBaseValue(u64);

impl_property!(MixedBaseValue, Person, default_const = MixedBaseValue(0));

define_derived_property!(
struct DerivedLowBit(bool),
Person,
Expand Down Expand Up @@ -41,6 +48,36 @@ define_derived_property!(
}
);

define_derived_property!(
struct MixedDerivedIndexed(bool),
Person,
[MixedBaseValue],
[],
|base| {
let base: MixedBaseValue = base;
MixedDerivedIndexed(base.0 & 1 == 0)
}
);

#[derive(Debug, PartialEq, Eq, Clone, Copy, serde::Serialize, Hash)]
struct MixedDerivedCounted(u8);

impl_derived_property!(MixedDerivedCounted, Person, [MixedBaseValue], [], |base| {
let base: MixedBaseValue = base;
MixedDerivedCounted((base.0 % 4) as u8)
});

define_derived_property!(
struct MixedDerivedHandled(bool),
Person,
[MixedBaseValue],
[],
|base| {
let base: MixedBaseValue = base;
MixedDerivedHandled(base.0.trailing_zeros() == 0)
}
);

pub fn criterion_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("set_property");

Expand All @@ -66,6 +103,43 @@ pub fn criterion_benchmark(c: &mut Criterion) {
});
});

group.bench_function("set_property_three_dependents_mixed", |bencher| {
let mut context = Context::new();
let person = context
.add_entity((MixedBaseValue(0), IndependentValue(0)))
.unwrap();

context.index_property::<Person, MixedDerivedIndexed>();
context
.track_periodic_value_change_counts::<Person, (MixedBaseValue, ), MixedDerivedCounted, _>(
1.0,
|_context, _counter| {},
);
context.subscribe_to_event(
|_context, _event: PropertyChangeEvent<Person, MixedDerivedHandled>| {},
);

// Set a value to trigger lazy initialization of the derived properties.
context.set_property(person, MixedBaseValue(42));
context.execute();
let context = RefCell::new(context);

// Reuse one Context so lazy initialization is amortized, but flush callbacks between
// measured chunks to avoid pathological callback queue growth.
bencher.iter_batched(
|| {
context.borrow_mut().execute();
},
|_| {
let mut context = context.borrow_mut();
for value in 0..256 {
context.set_property(black_box(person), MixedBaseValue(black_box(value)));
}
},
BatchSize::SmallInput,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What's the reason for SmallInput?

);
});

group.finish();
}

Expand Down
4 changes: 4 additions & 0 deletions src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ impl Context {
E::on_subscribe(self);
}

pub(crate) fn has_event_handlers<E: IxaEvent + 'static>(&self) -> bool {
self.event_handlers.contains_key(&TypeId::of::<E>())
}

/// Emit an event of type E to be handled by registered receivers
///
/// Receivers will handle events in the order that they have subscribed and
Expand Down
24 changes: 14 additions & 10 deletions src/entity/context_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,8 @@ impl ContextEntitiesExt for Context {
debug_assert!(!P::is_derived(), "cannot set a derived property");

// The algorithm is as follows:
// 1. Snapshot previous values for the main property and its dependents by creating
// `PartialPropertyChangeEvent` instances.
// 1. Snapshot previous values for the main property and any dependents that need change
// processing by creating `PartialPropertyChangeEvent` instances.
// 2. Set the new value of the main property in the property store.
// 3. Emit each partial event; during emission each event computes the current value,
// updates its index (remove old/add new), and emits a `PropertyChangeEvent`.
Expand Down Expand Up @@ -326,19 +326,23 @@ impl ContextEntitiesExt for Context {
let property_store = self.entity_store.get_property_store::<E>();

// Create the partial property change for this value.
dependents.push(property_store.create_partial_property_change(
P::id(),
entity_id,
self,
));
// Now create partial property change events for each dependent.
for dependent_idx in P::dependents() {
if property_store.should_create_partial_property_change(P::id(), self) {
dependents.push(property_store.create_partial_property_change(
*dependent_idx,
P::id(),
entity_id,
self,
));
}
// Now create partial property change events for each dependent.
for dependent_idx in P::dependents() {
if property_store.should_create_partial_property_change(*dependent_idx, self) {
dependents.push(property_store.create_partial_property_change(
*dependent_idx,
entity_id,
self,
));
}
}
}

// Update the value
Expand Down
13 changes: 13 additions & 0 deletions src/entity/property_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,19 @@ impl<E: Entity> PropertyStore<E> {
property_value_store.create_partial_property_change(entity_id, context)
}

/// Returns whether the property with `property_index` needs partial change-event processing.
pub(crate) fn should_create_partial_property_change(
&self,
property_index: usize,
context: &Context,
) -> bool {
let property_value_store = self.items
.get(property_index)
.unwrap_or_else(|| panic!("No registered property found with index = {property_index:?}. You must use the `define_property!` macro to create a registered property."));

property_value_store.should_create_partial_change(context)
}

/// Returns whether or not the property `P` is indexed.
///
/// This method can return `true` even if `context.index_property::<P>()` has never been called. For example,
Expand Down
12 changes: 12 additions & 0 deletions src/entity/property_value_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use log::{error, trace};

use crate::entity::events::{
PartialPropertyChangeEvent, PartialPropertyChangeEventBox, PartialPropertyChangeEventCore,
PropertyChangeEvent,
};
use crate::entity::index::{
FullIndex, IndexCountResult, IndexSetResult, PropertyIndex, PropertyIndexType, ValueCountIndex,
Expand All @@ -41,6 +42,11 @@ pub(crate) trait PropertyValueStore<E: Entity>: Any {
context: &Context,
) -> PartialPropertyChangeEventBox;

/// Returns whether a property write needs the partial change-event machinery.
///
/// This is true if the property has change-event subscribers, value change counters, or an index.
fn should_create_partial_change(&self, context: &Context) -> bool;

// Index-related methods. Anything beyond these requires the `PropertyValueStoreCore<E, P>`.

fn add_entity_to_index_with_hash(&mut self, hash: HashValueType, entity_id: EntityId<E>);
Expand Down Expand Up @@ -98,6 +104,12 @@ impl<E: Entity, P: Property<E>> PropertyValueStore<E> for PropertyValueStoreCore
))
}

fn should_create_partial_change(&self, context: &Context) -> bool {
context.has_event_handlers::<PropertyChangeEvent<E, P>>()
|| !self.value_change_counters.is_empty()
|| self.index.index_type() != PropertyIndexType::Unindexed
}

fn add_entity_to_index_with_hash(&mut self, hash: HashValueType, entity_id: EntityId<E>) {
if self.index.index_type() != PropertyIndexType::Unindexed {
self.index.add_entity_with_hash(hash, entity_id);
Expand Down
Loading