240 lines
8.0 KiB
GDScript
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
|
|
)
|