Preserve approved pixel tower prototype as development baseline
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
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_x4": {&"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 = {}
|
||||
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]
|
||||
values.append(duration_us)
|
||||
if values.size() > sample_capacity:
|
||||
values.pop_front()
|
||||
_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 := 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_x4" if speed == 4 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 x4 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 values: Array = _samples.get(metric_id, [])
|
||||
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
|
||||
var maximum_us := 0
|
||||
for raw_value: Variant in values:
|
||||
maximum_us = maxi(maximum_us, int(raw_value))
|
||||
var p95_us := _percentile(values, 95)
|
||||
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()
|
||||
_totals.clear()
|
||||
_sample_counts.clear()
|
||||
return
|
||||
_samples.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)]
|
||||
Reference in New Issue
Block a user