329 lines
11 KiB
GDScript
329 lines
11 KiB
GDScript
## Observer Performance Tests
|
||
## Compares observers vs traditional systems for different use cases
|
||
extends GdUnitTestSuite
|
||
|
||
var runner: GdUnitSceneRunner
|
||
var world: World
|
||
|
||
|
||
func before():
|
||
runner = scene_runner("res://addons/gecs/tests/test_scene.tscn")
|
||
world = runner.get_property("world")
|
||
ECS.world = world
|
||
|
||
|
||
func after_test():
|
||
if world:
|
||
world.purge(false)
|
||
|
||
|
||
## Setup entities with position and velocity for movement tests
|
||
func setup_velocity_entities(count: int) -> void:
|
||
for i in count:
|
||
var entity = Entity.new()
|
||
entity.name = "VelocityEntity_%d" % i
|
||
entity.add_component(C_TestPosition.new(Vector3(i, 0, 0)))
|
||
entity.add_component(C_TestVelocity.new(Vector3(randf() * 10, randf() * 10, randf() * 10)))
|
||
world.add_entity(entity, null, false)
|
||
|
||
|
||
## Setup entities for observer add/remove tests
|
||
func setup_observer_test_entities(count: int) -> void:
|
||
for i in count:
|
||
var entity = Entity.new()
|
||
entity.name = "ObserverTestEntity_%d" % i
|
||
entity.add_component(C_ObserverTest.new(i))
|
||
world.add_entity(entity, null, false)
|
||
|
||
|
||
## Test traditional system approach for continuous processing (like velocity)
|
||
## This is the IDEAL use case for systems - they excel at continuous per-frame processing
|
||
func test_system_continuous_processing(scale: int, test_parameters := [[100], [1000], [10000]]):
|
||
setup_velocity_entities(scale)
|
||
|
||
var system = S_VelocitySystem.new()
|
||
world.add_system(system)
|
||
|
||
var time_ms = PerfHelpers.time_it(func():
|
||
# Simulate 60 frames of processing
|
||
for i in range(60):
|
||
world.process(0.016)
|
||
)
|
||
|
||
PerfHelpers.record_result("system_continuous_velocity", scale, time_ms)
|
||
prints("System processed %d entities across 60 frames" % system.process_count)
|
||
world.purge(false)
|
||
|
||
|
||
## Test observer detecting component additions
|
||
## This is an IDEAL use case for observers - they excel at reacting to state changes
|
||
func test_observer_component_additions(scale: int, test_parameters := [[100], [1000], [10000]]):
|
||
var observer = O_PerformanceTest.new()
|
||
world.add_observer(observer)
|
||
|
||
var time_ms = PerfHelpers.time_it(func():
|
||
# Add components to entities (observers react to additions)
|
||
for i in range(scale):
|
||
var entity = Entity.new()
|
||
entity.add_component(C_ObserverTest.new(i))
|
||
world.add_entity(entity, null, false)
|
||
)
|
||
|
||
PerfHelpers.record_result("observer_component_additions", scale, time_ms)
|
||
prints("Observer detected %d additions" % observer.added_count)
|
||
assert_int(observer.added_count).is_equal(scale)
|
||
world.purge(false)
|
||
|
||
|
||
## Test observer detecting component removals
|
||
## Another IDEAL use case for observers - reacting to cleanup/removal events
|
||
func test_observer_component_removals(scale: int, test_parameters := [[100], [1000], [10000]]):
|
||
setup_observer_test_entities(scale)
|
||
|
||
var observer = O_PerformanceTest.new()
|
||
world.add_observer(observer)
|
||
|
||
var entities = world.query.with_all([C_ObserverTest]).execute()
|
||
|
||
var time_ms = PerfHelpers.time_it(func():
|
||
# Remove components (observers react to removals)
|
||
for entity in entities:
|
||
entity.remove_component(C_ObserverTest)
|
||
)
|
||
|
||
PerfHelpers.record_result("observer_component_removals", scale, time_ms)
|
||
prints("Observer detected %d removals" % observer.removed_count)
|
||
assert_int(observer.removed_count).is_equal(scale)
|
||
world.purge(false)
|
||
|
||
|
||
## Test observer detecting property changes
|
||
## Good use case for observers - reacting to specific property changes
|
||
func test_observer_property_changes(scale: int, test_parameters := [[100], [1000], [10000]]):
|
||
setup_observer_test_entities(scale)
|
||
|
||
var observer = O_PerformanceTest.new()
|
||
world.add_observer(observer)
|
||
observer.reset_counts()
|
||
|
||
var entities = world.query.with_all([C_ObserverTest]).execute()
|
||
|
||
var time_ms = PerfHelpers.time_it(func():
|
||
# Change properties (observers react to changes)
|
||
for entity in entities:
|
||
var comp = entity.get_component(C_ObserverTest)
|
||
comp.value = comp.value + 1 # Triggers property_changed signal
|
||
)
|
||
|
||
PerfHelpers.record_result("observer_property_changes", scale, time_ms)
|
||
prints("Observer detected %d property changes" % observer.changed_count)
|
||
assert_int(observer.changed_count).is_equal(scale)
|
||
world.purge(false)
|
||
|
||
|
||
## Test system approach for batch property reads
|
||
## Systems are better for batch operations without individual reactions
|
||
func test_system_batch_property_reads(scale: int, test_parameters := [[100], [1000], [10000]]):
|
||
setup_observer_test_entities(scale)
|
||
|
||
var system = PerformanceTestSystem.new()
|
||
world.add_system(system)
|
||
|
||
var time_ms = PerfHelpers.time_it(func():
|
||
# Single process call reads all entities
|
||
world.process(0.016)
|
||
)
|
||
|
||
PerfHelpers.record_result("system_batch_property_reads", scale, time_ms)
|
||
prints("System processed %d entities in batch" % system.process_count)
|
||
world.purge(false)
|
||
|
||
|
||
## Test observer overhead with multiple property changes per entity
|
||
## Shows cost of observers when entities change frequently
|
||
func test_observer_frequent_changes(scale: int, test_parameters := [[100], [1000], [10000]]):
|
||
setup_observer_test_entities(scale)
|
||
|
||
var observer = O_PerformanceTest.new()
|
||
world.add_observer(observer)
|
||
observer.reset_counts()
|
||
|
||
var entities = world.query.with_all([C_ObserverTest]).execute()
|
||
|
||
var time_ms = PerfHelpers.time_it(func():
|
||
# Each entity changes multiple times
|
||
for entity in entities:
|
||
var comp = entity.get_component(C_ObserverTest)
|
||
for j in range(10): # 10 changes per entity
|
||
comp.value = comp.value + 1
|
||
)
|
||
|
||
PerfHelpers.record_result("observer_frequent_changes", scale, time_ms)
|
||
prints("Observer detected %d property changes (%d entities × 10 changes)" % [observer.changed_count, scale])
|
||
assert_int(observer.changed_count).is_equal(scale * 10)
|
||
world.purge(false)
|
||
|
||
|
||
## Test system processing the same frequent changes scenario
|
||
## Compares continuous polling vs reactive observation
|
||
func test_system_simulating_frequent_changes(scale: int, test_parameters := [[100], [1000], [10000]]):
|
||
setup_observer_test_entities(scale)
|
||
|
||
var system = PerformanceTestSystem.new()
|
||
world.add_system(system)
|
||
|
||
var entities = world.query.with_all([C_ObserverTest]).execute()
|
||
|
||
var time_ms = PerfHelpers.time_it(func():
|
||
# Make the changes
|
||
for entity in entities:
|
||
var comp = entity.get_component(C_ObserverTest)
|
||
for j in range(10):
|
||
# Direct property change without signal
|
||
comp.value = comp.value + 1
|
||
|
||
# System processes once (doesn't know about individual changes)
|
||
world.process(0.016)
|
||
)
|
||
|
||
PerfHelpers.record_result("system_simulating_frequent_changes", scale, time_ms)
|
||
prints("System processed %d entities once after changes" % system.process_count)
|
||
world.purge(false)
|
||
|
||
|
||
## Test multiple observers watching the same component
|
||
## Shows overhead of multiple reactive systems
|
||
func test_multiple_observers_same_component(scale: int, test_parameters := [[100], [1000], [10000]]):
|
||
setup_observer_test_entities(scale)
|
||
|
||
var observer1 = O_PerformanceTest.new()
|
||
var observer2 = O_PerformanceTest.new()
|
||
var observer3 = O_PerformanceTest.new()
|
||
world.add_observers([observer1, observer2, observer3])
|
||
|
||
observer1.reset_counts()
|
||
observer2.reset_counts()
|
||
observer3.reset_counts()
|
||
|
||
var entities = world.query.with_all([C_ObserverTest]).execute()
|
||
|
||
var time_ms = PerfHelpers.time_it(func():
|
||
# Change properties (all 3 observers react)
|
||
for entity in entities:
|
||
var comp = entity.get_component(C_ObserverTest)
|
||
comp.value = comp.value + 1
|
||
)
|
||
|
||
PerfHelpers.record_result("multiple_observers_same_component", scale, time_ms)
|
||
prints("3 observers each detected %d changes" % observer1.changed_count)
|
||
assert_int(observer1.changed_count).is_equal(scale)
|
||
assert_int(observer2.changed_count).is_equal(scale)
|
||
assert_int(observer3.changed_count).is_equal(scale)
|
||
world.purge(false)
|
||
|
||
|
||
## Test observer query filtering performance
|
||
## Shows cost of query evaluation for observers
|
||
func test_observer_with_complex_query(scale: int, test_parameters := [[100], [1000], [10000]]):
|
||
# Create entities with varying component combinations
|
||
for i in range(scale):
|
||
var entity = Entity.new()
|
||
entity.add_component(C_ObserverTest.new(i))
|
||
if i % 2 == 0:
|
||
entity.add_component(C_ObserverHealth.new())
|
||
world.add_entity(entity, null, false)
|
||
|
||
# Observer with complex query (needs both components)
|
||
var observer = O_HealthObserver.new()
|
||
world.add_observer(observer)
|
||
observer.reset()
|
||
|
||
var entities_matching = world.query.with_all([C_ObserverTest, C_ObserverHealth]).execute()
|
||
|
||
var time_ms = PerfHelpers.time_it(func():
|
||
# Change health on matching entities
|
||
for entity in entities_matching:
|
||
var health = entity.get_component(C_ObserverHealth)
|
||
health.health = health.health - 1
|
||
)
|
||
|
||
PerfHelpers.record_result("observer_complex_query", scale, time_ms)
|
||
prints("Observer with complex query detected %d changes (out of %d total entities)" % [observer.health_changed_count, scale])
|
||
world.purge(false)
|
||
|
||
|
||
## Test baseline: Empty observer overhead
|
||
## Measures the cost of just having observers in the system
|
||
func test_observer_baseline_overhead(scale: int, test_parameters := [[100], [1000], [10000]]):
|
||
setup_observer_test_entities(scale)
|
||
|
||
# Add observer but don't trigger it
|
||
var observer = O_PerformanceTest.new()
|
||
world.add_observer(observer)
|
||
|
||
var entities = world.query.with_all([C_ObserverTest]).execute()
|
||
|
||
var time_ms = PerfHelpers.time_it(func():
|
||
# Make changes WITHOUT triggering property_changed signals
|
||
for entity in entities:
|
||
var comp = entity.get_component(C_ObserverTest)
|
||
# Direct property access without signal emission
|
||
comp.value = comp.value + 1
|
||
)
|
||
|
||
PerfHelpers.record_result("observer_baseline_overhead", scale, time_ms)
|
||
prints("Made %d changes without triggering observer" % scale)
|
||
assert_int(observer.changed_count).is_equal(scale) # Observer should have triggered
|
||
world.purge(false)
|
||
|
||
|
||
## Test comparison: Observer vs System for sporadic changes
|
||
## Real-world scenario: only 10% of entities change per frame
|
||
func test_observer_vs_system_sporadic_changes(scale: int, test_parameters := [[100], [1000], [10000]]):
|
||
setup_observer_test_entities(scale)
|
||
|
||
var observer = O_PerformanceTest.new()
|
||
world.add_observer(observer)
|
||
observer.reset_counts()
|
||
|
||
var entities = world.query.with_all([C_ObserverTest]).execute()
|
||
var changes_per_frame = max(1, scale / 10) # 10% of entities change
|
||
|
||
var time_ms_observer = PerfHelpers.time_it(func():
|
||
# Simulate 60 frames where only 10% of entities change per frame
|
||
for frame in range(60):
|
||
for i in range(changes_per_frame):
|
||
var entity = entities[i % scale]
|
||
var comp = entity.get_component(C_ObserverTest)
|
||
comp.value = comp.value + 1 # Triggers observer
|
||
)
|
||
|
||
PerfHelpers.record_result("observer_sporadic_changes", scale, time_ms_observer)
|
||
prints("Observer detected %d sporadic changes over 60 frames" % observer.changed_count)
|
||
|
||
# Now test with system approach
|
||
world.purge(false)
|
||
setup_observer_test_entities(scale)
|
||
|
||
var system = PerformanceTestSystem.new()
|
||
world.add_system(system)
|
||
|
||
entities = world.query.with_all([C_ObserverTest]).execute()
|
||
|
||
var time_ms_system = PerfHelpers.time_it(func():
|
||
# Same scenario but system processes ALL entities every frame
|
||
for frame in range(60):
|
||
# Make the same changes
|
||
for i in range(changes_per_frame):
|
||
var entity = entities[i % scale]
|
||
var comp = entity.get_component(C_ObserverTest)
|
||
comp.value = comp.value + 1
|
||
|
||
# System processes ALL entities every frame
|
||
world.process(0.016)
|
||
)
|
||
|
||
PerfHelpers.record_result("system_sporadic_changes", scale, time_ms_system)
|
||
prints("System processed %d total entities over 60 frames (even though only 10%% changed)" % system.process_count)
|
||
world.purge(false)
|