class_name SimulationPerformanceBudget extends RefCounted ## Lightweight runtime profiler with explicit integer-microsecond samples. ## It is diagnostic state only and deliberately stays outside Save snapshots. signal budget_exceeded(metric_id: StringName, report: Dictionary) enum Aggregation { AVERAGE, P95, MAXIMUM, } const DEFAULT_SAMPLE_CAPACITY := 512 const DEFAULT_BUDGETS := { &"simulation_x1": {&"aggregation": Aggregation.AVERAGE, &"limit_us": 4_000}, &"simulation_x3": {&"aggregation": Aggregation.P95, &"limit_us": 12_000}, &"path_query": {&"aggregation": Aggregation.P95, &"limit_us": 2_000}, &"main_thread_stall": {&"aggregation": Aggregation.MAXIMUM, &"limit_us": 50_000}, &"save": {&"aggregation": Aggregation.MAXIMUM, &"limit_us": 5_000_000}, &"load": {&"aggregation": Aggregation.MAXIMUM, &"limit_us": 8_000_000}, } var sample_capacity := DEFAULT_SAMPLE_CAPACITY var _budgets: Dictionary = {} var _samples: Dictionary = {} # Derived diagnostic index; never part of game Save snapshots. var _ordered_samples: Dictionary = {} var _ordered_sample_bytes: Dictionary = {} var _totals: Dictionary = {} var _sample_counts: Dictionary = {} func _init(capacity := DEFAULT_SAMPLE_CAPACITY) -> void: sample_capacity = maxi(int(capacity), 1) for raw_metric_id: Variant in DEFAULT_BUDGETS: _budgets[StringName(raw_metric_id)] = ( DEFAULT_BUDGETS[raw_metric_id] as Dictionary ).duplicate(true) func set_budget( metric_id: StringName, limit_us: int, aggregation: Aggregation = Aggregation.P95 ) -> bool: if metric_id.is_empty() or limit_us <= 0 \ or aggregation < Aggregation.AVERAGE or aggregation > Aggregation.MAXIMUM: return false _budgets[metric_id] = { &"aggregation": aggregation, &"limit_us": limit_us, } return true func record_sample(metric_id: StringName, duration_us: int) -> Dictionary: if metric_id.is_empty() or duration_us < 0: return {&"ok": false, &"error": "Metric ID and non-negative duration are required."} if not _samples.has(metric_id): _samples[metric_id] = [] var values: Array = _samples[metric_id] var use_index: bool = get_script().resource_path == "res://scripts/core/performance_budget.gd" and _integer_window(values) var ordered: Array[int] = [] if use_index: ordered = _validated_sample_order(metric_id, values) values.append(duration_us) if use_index: ordered.insert(ordered.bsearch(duration_us), duration_us) if values.size() > sample_capacity: var removed: Variant = values.pop_front() if use_index: ordered.remove_at(ordered.bsearch(int(removed))) if use_index: _ordered_sample_bytes[metric_id] = var_to_bytes(values) else: _ordered_samples.erase(metric_id) _ordered_sample_bytes.erase(metric_id) _totals[metric_id] = int(_totals.get(metric_id, 0)) + duration_us _sample_counts[metric_id] = int(_sample_counts.get(metric_id, 0)) + 1 var report: Dictionary = _indexed_report(metric_id, values, ordered) if use_index else get_report(metric_id) if bool(report.get(&"budget_exceeded", false)): budget_exceeded.emit(metric_id, report.duplicate(true)) return {&"ok": true, &"report": report} func record_simulation_tick(duration_us: int, speed: int) -> Dictionary: var metric_id := &"simulation_x3" if speed == 3 else &"simulation_x1" return record_sample(metric_id, duration_us) ## Records the full main-thread cost of one rendered/process frame, including ## all game-minute ticks consumed at accelerated speed. This keeps x3 from ## accidentally reporting four individually-fast ticks while the frame stalls. func record_simulation_frame(duration_us: int, speed: int, ticks_run: int) -> Dictionary: if ticks_run <= 0: return {&"ok": false, &"error": "At least one completed tick is required."} var simulation_result := record_simulation_tick(duration_us, speed) var stall_result := record_sample(&"main_thread_stall", duration_us) return { &"ok": bool(simulation_result[&"ok"]) and bool(stall_result[&"ok"]), &"ticks_run": ticks_run, &"simulation": simulation_result, &"main_thread": stall_result, } func get_report(metric_id: StringName) -> Dictionary: var raw: Array = _samples.get(metric_id, []) if get_script().resource_path != "res://scripts/core/performance_budget.gd" or not _integer_window(raw): return SimulationPerformanceBudget._legacy_report(self, metric_id) var ordered: Array[int] = [] return _indexed_report(metric_id, raw, ordered, true) func _indexed_report(metric_id: StringName, values: Array, ordered: Array[int], validate_order: bool = false) -> Dictionary: var count := values.size() var lifetime_count := int(_sample_counts.get(metric_id, 0)) var lifetime_total := int(_totals.get(metric_id, 0)) var average_us := lifetime_total / lifetime_count if lifetime_count > 0 else 0 if validate_order: ordered = _validated_sample_order(metric_id, values) var maximum_us := maxi(0, ordered[-1]) if not ordered.is_empty() else 0 var p95_us := 0 if not ordered.is_empty(): var rank_numerator := 95 * ordered.size() var index := maxi((rank_numerator + 99) / 100 - 1, 0) p95_us = ordered[mini(index, ordered.size() - 1)] var budget: Dictionary = _budgets.get(metric_id, {}) var aggregation := int(budget.get(&"aggregation", Aggregation.P95)) var measured_us := _aggregated_value(aggregation, average_us, p95_us, maximum_us) var limit_us := int(budget.get(&"limit_us", 0)) return { &"metric_id": metric_id, &"window_sample_count": count, &"lifetime_sample_count": lifetime_count, &"average_us": average_us, &"p95_us": p95_us, &"maximum_us": maximum_us, &"aggregation": aggregation, &"measured_us": measured_us, &"limit_us": limit_us, &"budget_exceeded": limit_us > 0 and measured_us > limit_us, } func get_all_reports() -> Array[Dictionary]: var metric_ids: Array[StringName] = [] for raw_metric_id: Variant in _budgets: metric_ids.append(StringName(raw_metric_id)) for raw_metric_id: Variant in _samples: var metric_id := StringName(raw_metric_id) if not metric_ids.has(metric_id): metric_ids.append(metric_id) metric_ids.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b) ) var reports: Array[Dictionary] = [] for metric_id: StringName in metric_ids: reports.append(get_report(metric_id)) return reports func clear(metric_id: StringName = &"") -> void: if metric_id.is_empty(): _samples.clear() _ordered_samples.clear() _ordered_sample_bytes.clear() _totals.clear() _sample_counts.clear() return _samples.erase(metric_id) _ordered_samples.erase(metric_id) _ordered_sample_bytes.erase(metric_id) _totals.erase(metric_id) _sample_counts.erase(metric_id) func _aggregated_value( aggregation: int, average_us: int, p95_us: int, maximum_us: int ) -> int: match aggregation: Aggregation.AVERAGE: return average_us Aggregation.MAXIMUM: return maximum_us _: return p95_us func _percentile(raw_values: Array, percentile: int) -> int: if raw_values.is_empty(): return 0 var values: Array[int] = [] for raw_value: Variant in raw_values: values.append(int(raw_value)) values.sort() var rank_numerator := clampi(percentile, 0, 100) * values.size() var index := maxi((rank_numerator + 99) / 100 - 1, 0) return values[mini(index, values.size() - 1)] static func _integer_window(values: Array) -> bool: for raw_value: Variant in values: if typeof(raw_value) != TYPE_INT: return false return true func _validated_sample_order(metric_id: StringName, values: Array) -> Array[int]: # No retained state for unknown metrics; raw dictionary removal also drops its index. if not _samples.has(metric_id): _ordered_samples.erase(metric_id) _ordered_sample_bytes.erase(metric_id) return [] # Complete immutable raw bytes detect in-place/replaced windows, not just length. var current_bytes := var_to_bytes(values) if not _ordered_samples.has(metric_id) or _ordered_sample_bytes.get(metric_id, PackedByteArray()) != current_bytes: var ordered: Array[int] = [] for raw_value: Variant in values: ordered.append(int(raw_value)) ordered.sort() _ordered_samples[metric_id] = ordered _ordered_sample_bytes[metric_id] = current_bytes return _ordered_samples[metric_id] static func _legacy_report(owner: SimulationPerformanceBudget, metric_id: StringName) -> Dictionary: var values: Array = owner._samples.get(metric_id, []) var count := values.size() var lifetime_count := int(owner._sample_counts.get(metric_id, 0)) var lifetime_total := int(owner._totals.get(metric_id, 0)) var average_us := lifetime_total / lifetime_count if lifetime_count > 0 else 0 var maximum_us := 0 for raw_value: Variant in values: maximum_us = maxi(maximum_us, int(raw_value)) var p95_us := owner._percentile(values, 95) var budget: Dictionary = owner._budgets.get(metric_id, {}) var aggregation := int(budget.get(&"aggregation", Aggregation.P95)) var measured_us := owner._aggregated_value(aggregation, average_us, p95_us, maximum_us) var limit_us := int(budget.get(&"limit_us", 0)) return { &"metric_id": metric_id, &"window_sample_count": count, &"lifetime_sample_count": lifetime_count, &"average_us": average_us, &"p95_us": p95_us, &"maximum_us": maximum_us, &"aggregation": aggregation, &"measured_us": measured_us, &"limit_us": limit_us, &"budget_exceeded": limit_us > 0 and measured_us > limit_us, }