Files
2026-01-15 15:27:48 +01:00

329 lines
11 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 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)