feat: establish simulation scaling baseline
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user