249 lines
9.3 KiB
GDScript
249 lines
9.3 KiB
GDScript
class_name SimulationScalingBenchmark
|
|
extends RefCounted
|
|
|
|
const SCHEMA_VERSION := 3
|
|
const WORKLOAD_ID := &"full_fidelity_emergent_headless_arrival_v3"
|
|
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)
|
|
var no_situations: Array[SituationStateRecord] = []
|
|
var no_journal_entries: Array[QuestJournalEntryStateRecord] = []
|
|
var no_commitments: Array[CommitmentStateRecord] = []
|
|
manager.situation_system.restore(no_situations, 0)
|
|
manager.quest_journal_system.restore(no_journal_entries, 0)
|
|
manager.commitment_system.restore(no_commitments, 0)
|
|
manager.conversation_history.clear()
|
|
manager._next_conversation_act_id = 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)
|
|
manager.refresh_population_index()
|
|
var no_combatants: Array[CombatantStateRecord] = []
|
|
var no_factions: Array[FactionStateRecord] = []
|
|
manager.conflict_system.restore_state_records(no_combatants, no_factions, manager.tick_count)
|
|
manager.conflict_system.register_npc_combatants(manager.npcs)
|
|
if not _get_combatant_coverage(manager)["valid"]:
|
|
return false
|
|
|
|
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
|
|
or not _get_combatant_coverage(manager)["valid"]
|
|
):
|
|
return {}
|
|
var warmup_arrivals := 0
|
|
for _tick in warmup_ticks:
|
|
warmup_arrivals += _advance_headless_tick(manager)
|
|
var warmup_coverage := _get_combatant_coverage(manager)
|
|
if not warmup_coverage["valid"] or warmup_coverage["npc_combatant_count"] != population:
|
|
return {}
|
|
|
|
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_situation_count: int = manager.situation_system.get_all_sorted().size()
|
|
var start_commitment_count: int = manager.commitment_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)
|
|
var combatant_coverage := _get_combatant_coverage(manager)
|
|
if not combatant_coverage["valid"] or combatant_coverage["npc_combatant_count"] != population:
|
|
return {}
|
|
return {
|
|
"schema_version": SCHEMA_VERSION,
|
|
"simulation_state_schema_version": SimulationStateRecord.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,
|
|
"npc_combatant_count": combatant_coverage["npc_combatant_count"],
|
|
"npc_combatant_coverage_valid": combatant_coverage["valid"],
|
|
"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(),
|
|
"start_situation_count": start_situation_count,
|
|
"end_situation_count": manager.situation_system.get_all_sorted().size(),
|
|
"start_commitment_count": start_commitment_count,
|
|
"end_commitment_count": manager.commitment_system.get_all_sorted().size(),
|
|
"conversation_act_count": manager.conversation_history.size(),
|
|
"final_checksum": end_json.sha256_text(),
|
|
}
|
|
|
|
|
|
func _get_combatant_coverage(manager: Node) -> Dictionary:
|
|
var expected_npcs := {}
|
|
for npc in manager.npcs:
|
|
expected_npcs[npc.id] = npc
|
|
var covered_npc_ids := {}
|
|
var npc_combatant_count := 0
|
|
var valid: bool = expected_npcs.size() == manager.npcs.size()
|
|
for combatant in manager.conflict_system.get_all_combatants():
|
|
if combatant.get_kind() != SimulationIds.COMBATANT_KIND_NPC:
|
|
continue
|
|
npc_combatant_count += 1
|
|
var npc_id: int = combatant.get_npc_id()
|
|
var npc := expected_npcs.get(npc_id) as SimNPC
|
|
if (
|
|
npc == null
|
|
or covered_npc_ids.has(npc_id)
|
|
or combatant.get_combatant_id() != SimulationIds.npc_combatant_id(npc_id)
|
|
or combatant.get_display_name() != npc.npc_name
|
|
or not combatant.get_position().is_equal_approx(npc.position)
|
|
):
|
|
valid = false
|
|
covered_npc_ids[npc_id] = true
|
|
valid = (
|
|
valid
|
|
and npc_combatant_count == expected_npcs.size()
|
|
and covered_npc_ids.size() == expected_npcs.size()
|
|
)
|
|
return {"valid": valid, "npc_combatant_count": npc_combatant_count}
|
|
|
|
|
|
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)
|