feat: establish simulation scaling baseline

This commit is contained in:
Rijad Zuzo
2026-07-16 13:07:01 +02:00
parent 4f88d15e12
commit 4be72639d8
14 changed files with 1004 additions and 15 deletions
@@ -0,0 +1,186 @@
class_name SimulationScalingBenchmark
extends RefCounted
const SCHEMA_VERSION := 1
const WORKLOAD_ID := &"full_fidelity_headless_arrival"
const RECENT_FACTS_PER_NPC := 3
const STARTING_CLOCK_TICK := 50
func prepare_manager(
manager: Node, population: int, history_event_count: int, seed_value: int
) -> bool:
if manager == null or manager.clock == null or population <= 0 or history_event_count < 0:
return false
manager.set_process(false)
manager.simulation_seed = seed_value
manager.tick_count = history_event_count
manager.clock.accumulator = 0.0
manager.clock.elapsed_ticks = STARTING_CLOCK_TICK
manager.npcs.clear()
manager.wander_random_sources.clear()
manager.latest_decisions.clear()
manager.resource_states.clear()
var no_relationships: Array[RelationshipStateRecord] = []
var no_opportunities: Array[OpportunityStateRecord] = []
manager.relationship_system.restore(no_relationships)
manager.opportunity_system.restore(no_opportunities, 0)
for npc_id in population:
var npc_random := _create_random_source(seed_value, npc_id, 0)
var npc := SimNPC.new(
npc_id,
"Benchmark %04d" % npc_id,
SimulationIds.PROFESSION_FARMER,
npc_random.randf_range(1.0, 10.0),
npc_random.randf_range(1.0, 10.0),
npc_random
)
npc.debug_logs = false
npc.hunger = 35.0 + float(npc_id % 10) * 0.25
npc.energy = 90.0
npc.position = _grid_position(npc_id)
npc.home_position = npc.position
manager.npcs.append(npc)
manager.wander_random_sources[npc_id] = _create_random_source(seed_value, npc_id, 1)
var pantry: StorageStateRecord = manager.get_pantry()
pantry.withdraw(SimulationIds.RESOURCE_FOOD, pantry.get_amount(SimulationIds.RESOURCE_FOOD))
manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
manager.village.update_modifiers()
manager.village.update_priorities()
var history := _build_history(population, history_event_count)
manager.event_log.restore(history["events"], history_event_count)
manager.event_knowledge_system.restore(history["knowledge"])
return true
func measure_manager(
manager: Node, population: int, history_event_count: int, warmup_ticks: int, measured_ticks: int
) -> Dictionary:
if (
manager == null
or manager.npcs.size() != population
or warmup_ticks < 0
or measured_ticks <= 0
):
return {}
var warmup_arrivals := 0
for _tick in warmup_ticks:
warmup_arrivals += _advance_headless_tick(manager)
var start_json: String = manager.serialize_state()
var start_event_count: int = manager.economic_events.size()
var start_known_count: int = manager.event_knowledge_system.get_all_sorted().size()
var start_tick: int = manager.tick_count
var measured_arrivals := 0
var simulation_usec := 0
var arrival_usec := 0
var started_usec := Time.get_ticks_usec()
for _tick in measured_ticks:
var phase_started_usec := Time.get_ticks_usec()
manager.simulate_tick()
simulation_usec += Time.get_ticks_usec() - phase_started_usec
phase_started_usec = Time.get_ticks_usec()
measured_arrivals += _complete_headless_arrivals(manager)
arrival_usec += Time.get_ticks_usec() - phase_started_usec
var elapsed_usec := maxi(Time.get_ticks_usec() - started_usec, 1)
var end_json: String = manager.serialize_state()
var start_state_bytes := start_json.to_utf8_buffer().size()
var end_state_bytes := end_json.to_utf8_buffer().size()
var ticks_per_second := float(measured_ticks) * 1000000.0 / float(elapsed_usec)
return {
"schema_version": SCHEMA_VERSION,
"workload_id": String(WORKLOAD_ID),
"seed": manager.simulation_seed,
"population": population,
"history_seed_events": history_event_count,
"warmup_ticks": warmup_ticks,
"measured_ticks": measured_ticks,
"npc_updates": population * measured_ticks,
"warmup_arrivals": warmup_arrivals,
"arrivals_processed": measured_arrivals,
"elapsed_usec": elapsed_usec,
"simulation_usec": maxi(simulation_usec, 1),
"arrival_usec": maxi(arrival_usec, 1),
"usec_per_tick": float(elapsed_usec) / float(measured_ticks),
"ticks_per_second": ticks_per_second,
"tick_interval": manager.tick_interval,
"realtime_factor": ticks_per_second * manager.tick_interval,
"start_tick": start_tick,
"end_tick": manager.tick_count,
"start_state_bytes": start_state_bytes,
"end_state_bytes": end_state_bytes,
"state_growth_bytes": end_state_bytes - start_state_bytes,
"start_event_count": start_event_count,
"end_event_count": manager.economic_events.size(),
"events_recorded": manager.economic_events.size() - start_event_count,
"start_known_reference_count": start_known_count,
"end_known_reference_count": manager.event_knowledge_system.get_all_sorted().size(),
"final_checksum": end_json.sha256_text(),
}
func _advance_headless_tick(manager: Node) -> int:
manager.simulate_tick()
return _complete_headless_arrivals(manager)
func _complete_headless_arrivals(manager: Node) -> int:
var arrival_ids: Array[int] = []
for npc in manager.npcs:
if npc.task_state == SimNPC.TASK_STATE_TRAVELING:
arrival_ids.append(npc.id)
for npc_id in arrival_ids:
manager.notify_npc_arrived(npc_id)
return arrival_ids.size()
func _build_history(population: int, history_event_count: int) -> Dictionary:
var events: Array[EconomicEventRecord] = []
for event_id in history_event_count:
var actor_id := event_id % population
events.append(
EconomicEventRecord.create(
event_id,
SimulationIds.EVENT_STORAGE_DEPOSITED,
event_id,
actor_id,
SimulationIds.npc_inventory_id(actor_id),
SimulationIds.STORAGE_VILLAGE_PANTRY,
SimulationIds.RESOURCE_FOOD,
1.0
)
)
var knowledge: Array[KnownEventStateRecord] = []
var retained_by_actor := {}
for event_id in range(history_event_count - 1, -1, -1):
var actor_id := event_id % population
var retained_count := int(retained_by_actor.get(actor_id, 0))
if retained_count >= RECENT_FACTS_PER_NPC:
continue
knowledge.append(
KnownEventStateRecord.create(
actor_id,
event_id,
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED,
KnownEventStateRecord.NO_SOURCE_NPC_ID,
event_id
)
)
retained_by_actor[actor_id] = retained_count + 1
if knowledge.size() >= mini(history_event_count, population * RECENT_FACTS_PER_NPC):
break
return {"events": events, "knowledge": knowledge}
func _create_random_source(seed_value: int, npc_id: int, stream_id: int) -> RandomNumberGenerator:
var source := RandomNumberGenerator.new()
source.seed = seed_value + (npc_id + 1) * 1000003 + stream_id * 7919
return source
func _grid_position(npc_id: int) -> Vector3:
var row := npc_id / 32
var column := npc_id % 32
return Vector3(float(column) * 12.0, 0.0, float(row) * 12.0)