Files
gamedev-the-steward/simulation/benchmark/LoadedResourceDiscoveryBenchmark.gd
T
2026-07-17 00:34:30 +02:00

240 lines
8.0 KiB
GDScript

class_name LoadedResourceDiscoveryBenchmark
extends RefCounted
const SCHEMA_VERSION := 1
const WORKLOAD_ID := &"loaded_resource_target_resolution"
const RESOURCE_SPACING := 20.0
const DEFAULT_QUERY_COUNT := 400
const DEFAULT_SAMPLE_COUNT := 7
const WARMUP_QUERY_COUNT := 40
const SimulationManagerScript := preload("res://simulation/SimulationManager.gd")
class LinearResourceAdapter:
extends Node
func get_resource_candidates(action_id: StringName) -> Array[Dictionary]:
var candidates: Array[Dictionary] = []
for node in ResourceNode.get_all():
if node.action_id != action_id or node.interaction_point == null:
continue
(
candidates
. append(
{
"target_id": String(node.node_id),
"position": node.interaction_point.global_position,
"resource_id": String(node.resource_id),
"safety_risk": node.safety_risk,
"comfort_distance": node.comfort_distance,
"discovery_priority": node.discovery_priority,
}
)
)
return candidates
func create_fixture(parent: Node, resource_count: int, seed_value: int) -> Dictionary:
if parent == null or resource_count <= 0:
return {}
var fixture_root := Node.new()
fixture_root.name = "LoadedResourceDiscoveryFixture"
parent.add_child(fixture_root)
var resource_root := Node3D.new()
resource_root.name = "ResourceNodes"
fixture_root.add_child(resource_root)
for resource_index in resource_count:
resource_root.add_child(_create_resource(resource_index, resource_count))
var adapter := ActiveWorldAdapter.new()
adapter.name = "ActiveWorldAdapter"
fixture_root.add_child(adapter)
var manager: Node = SimulationManagerScript.new()
manager.name = "SimulationManager"
manager.debug_logs = false
manager.simulation_seed = seed_value
manager.active_world_adapter = adapter
fixture_root.add_child(manager)
manager.set_process(false)
manager.register_loaded_resource_nodes()
var npc: SimNPC = manager.npcs[0]
npc.energy = 62.0
npc.set_task(SimulationIds.ACTION_GATHER_FOOD)
return {
"root": fixture_root,
"resource_root": resource_root,
"adapter": adapter,
"manager": manager,
"npc": npc,
"resource_count": resource_count,
}
func create_linear_adapter() -> Node:
return LinearResourceAdapter.new()
func measure_fixture(
fixture: Dictionary,
query_count: int = DEFAULT_QUERY_COUNT,
sample_count: int = DEFAULT_SAMPLE_COUNT,
include_spatial: bool = true
) -> Dictionary:
if fixture.is_empty() or query_count <= 0 or sample_count <= 0:
return {}
var adapter: ActiveWorldAdapter = fixture["adapter"]
var origins := _build_origins(int(fixture["resource_count"]), query_count)
var linear_adapter := create_linear_adapter()
(fixture["root"] as Node).add_child(linear_adapter)
_run_queries(fixture, linear_adapter, origins, mini(WARMUP_QUERY_COUNT, query_count), false)
var linear := _measure_mode(fixture, linear_adapter, origins, sample_count, false)
if linear.is_empty():
return {}
var result := {
"schema_version": SCHEMA_VERSION,
"workload_id": String(WORKLOAD_ID),
"resource_count": int(fixture["resource_count"]),
"query_count": query_count,
"sample_count": sample_count,
"linear": linear,
}
if (
include_spatial
and adapter.has_method("get_resource_candidates_in_radius")
and adapter.has_method("get_resource_query_profile")
):
_run_queries(fixture, adapter, origins, mini(WARMUP_QUERY_COUNT, query_count), false)
var spatial := _measure_mode(fixture, adapter, origins, sample_count, true)
if spatial.is_empty() or spatial["selected_checksum"] != linear["selected_checksum"]:
return {}
result["spatial"] = spatial
result["speedup"] = (
float(linear["usec_per_query_median"])
/ maxf(float(spatial["usec_per_query_median"]), 0.0001)
)
result["inspected_reduction_percent"] = (
(
1.0
- float(spatial["candidates_inspected_average"]) / float(fixture["resource_count"])
)
* 100.0
)
return result
func free_fixture(fixture: Dictionary) -> void:
if fixture.has("root") and is_instance_valid(fixture["root"]):
(fixture["root"] as Node).free()
func _measure_mode(
fixture: Dictionary,
query_adapter: Object,
origins: Array[Vector3],
sample_count: int,
spatial: bool
) -> Dictionary:
var elapsed_samples: Array[int] = []
var inspected_samples: Array[float] = []
var checksum := ""
for _sample_index in sample_count:
var sample := _run_queries(fixture, query_adapter, origins, origins.size(), spatial)
if sample.is_empty():
return {}
if checksum.is_empty():
checksum = sample["selected_checksum"]
elif checksum != sample["selected_checksum"]:
return {}
elapsed_samples.append(int(sample["elapsed_usec"]))
inspected_samples.append(float(sample["candidates_inspected_average"]))
elapsed_samples.sort()
inspected_samples.sort()
var elapsed_median := elapsed_samples[elapsed_samples.size() / 2]
return {
"elapsed_usec_samples": elapsed_samples,
"elapsed_usec_median": elapsed_median,
"usec_per_query_median": float(elapsed_median) / float(origins.size()),
"queries_per_second_median": float(origins.size()) * 1000000.0 / float(elapsed_median),
"candidates_inspected_average": inspected_samples[inspected_samples.size() / 2],
"selected_checksum": checksum,
}
func _run_queries(
fixture: Dictionary,
query_adapter: Object,
origins: Array[Vector3],
query_count: int,
spatial: bool
) -> Dictionary:
var manager: Node = fixture["manager"]
var npc: SimNPC = fixture["npc"]
var selected_ids := PackedStringArray()
var inspected_total := 0
var started_usec := Time.get_ticks_usec()
for query_index in query_count:
var result: Dictionary = manager.target_resolver.resolve(
npc, origins[query_index], manager, query_adapter
)
if result.is_empty():
return {}
var target_id := StringName(result["target_id"])
selected_ids.append(String(target_id))
manager.release_resource(target_id, npc.id)
if spatial and not manager.target_resolver.last_resource_query_stats.is_empty():
inspected_total += int(
manager.target_resolver.last_resource_query_stats.get("candidates_inspected", 0)
)
else:
inspected_total += int(fixture["resource_count"])
var elapsed_usec := maxi(Time.get_ticks_usec() - started_usec, 1)
return {
"elapsed_usec": elapsed_usec,
"candidates_inspected_average": float(inspected_total) / float(query_count),
"selected_checksum": "|".join(selected_ids).sha256_text(),
}
func _create_resource(resource_index: int, resource_count: int) -> ResourceNode:
var node := ResourceNode.new()
node.name = "Resource_%04d" % resource_index
node.node_id = StringName("benchmark_resource_%04d" % resource_index)
node.action_id = SimulationIds.ACTION_GATHER_FOOD
node.resource_id = SimulationIds.RESOURCE_FOOD
node.initial_amount = 100000.0
node.initial_enabled = resource_index % 29 != 0
node.safety_risk = float(resource_index % 6) * 0.06
node.comfort_distance = 16.0 + float(resource_index % 5) * 5.0
node.discovery_priority = float(resource_index % 9) * 0.35
node.debug_label_enabled = false
node.position = _resource_position(resource_index, resource_count)
var interaction_point := Marker3D.new()
interaction_point.name = "InteractionPoint"
node.add_child(interaction_point)
return node
func _build_origins(resource_count: int, query_count: int) -> Array[Vector3]:
var origins: Array[Vector3] = []
for query_index in query_count:
var resource_index := (query_index * 37 + 11) % resource_count
var offset := Vector3(
float((query_index * 7) % 13) - 6.0, 0.0, float((query_index * 11) % 17) - 8.0
)
origins.append(_resource_position(resource_index, resource_count) + offset)
return origins
func _resource_position(resource_index: int, resource_count: int) -> Vector3:
var columns := ceili(sqrt(float(resource_count)))
var column := resource_index % columns
var row := resource_index / columns
var centered_column := float(column) - float(columns - 1) * 0.5
var row_count := ceili(float(resource_count) / float(columns))
var centered_row := float(row) - float(row_count - 1) * 0.5
return Vector3(
centered_column * RESOURCE_SPACING,
sin(float(resource_index) * 0.37) * 2.5,
centered_row * RESOURCE_SPACING
)