11const DataCacheManager = preload(
"res://scripts/systems/dataCacheManager/data_cache_manager.gd")
13var enabled:bool = true
14var max_frame_samples:int = 240
15var timers:Dictionary = {}
16var active_timers:Dictionary = {}
17var counters:Dictionary = {}
18var gauges:Dictionary = {}
20var frame_samples:Array = []
21var data_cache_manager = DataCacheManager.new()
24func configure(options:Dictionary = {}) -> void:
25 enabled =
bool(options.get(
"enabled", enabled))
26 max_frame_samples = max(1,
int(options.get(
"max_frame_samples", max_frame_samples)))
39func start_timer(id:String) -> void:
40 if not enabled
or id.is_empty():
42 active_timers[id] = Time.get_ticks_usec()
45func stop_timer(id:String, metadata:Dictionary = {}) -> Dictionary:
46 if not enabled
or id.is_empty():
48 if not active_timers.has(id):
50 var start_usec:int =
int(active_timers.get(id, Time.get_ticks_usec()))
51 active_timers.erase(id)
52 var duration_usec:int = max(0, Time.get_ticks_usec() - start_usec)
53 return record_duration(id, duration_usec, metadata)
56func record_duration(id:String, duration_usec:int, metadata:Dictionary = {}) -> Dictionary:
57 if not enabled
or id.is_empty():
59 var normalized_duration:int = max(0, duration_usec)
60 var aggregate:Dictionary = timers.get(id, {
63 "min_usec": normalized_duration,
64 "max_usec": normalized_duration,
68 aggregate[
"count"] =
int(aggregate.get(
"count", 0)) + 1
69 aggregate[
"total_usec"] =
int(aggregate.get(
"total_usec", 0)) + normalized_duration
70 aggregate[
"min_usec"] = min(
int(aggregate.get(
"min_usec", normalized_duration)), normalized_duration)
71 aggregate[
"max_usec"] = max(
int(aggregate.get(
"max_usec", normalized_duration)), normalized_duration)
72 aggregate[
"last_usec"] = normalized_duration
73 aggregate[
"last_metadata"] = metadata.duplicate(true)
74 timers[id] = aggregate
77 "duration_usec": normalized_duration,
78 "metadata": metadata.duplicate(true)
82func increment(id:String, amount:int = 1) -> int:
83 if not enabled
or id.is_empty():
84 return int(counters.get(id, 0))
85 var next_value:int =
int(counters.get(id, 0)) + amount
86 counters[id] = next_value
90func set_gauge(id:String, value:Variant) -> void:
91 if not enabled
or id.is_empty():
96func mark(id:String, metadata:Dictionary = {}) -> Dictionary:
97 if not enabled
or id.is_empty():
99 var entry:Dictionary = {
101 "at_usec": Time.get_ticks_usec(),
102 "metadata": metadata.duplicate(true)
108func sample_frame(delta:float) -> Dictionary:
111 var normalized_delta:float = max(0.0, delta)
113 if normalized_delta > 0.0:
114 fps = 1.0 / normalized_delta
115 var sample:Dictionary = {
116 "delta": normalized_delta,
118 "at_usec": Time.get_ticks_usec()
120 frame_samples.append(sample)
121 _trim_frame_samples()
125func capture_node_counts(root_node:Node, id:String =
"nodes") -> Dictionary:
126 if not enabled
or root_node == null:
128 var counts:Dictionary = {
135 _count_node(root_node, counts)
136 for key
in counts.keys():
137 set_gauge(
"%s.%s" % [id, str(key)], counts.get(key))
141func capture_provider_stats(id:String, provider:Object) -> Dictionary:
142 if not enabled
or id.is_empty()
or provider == null:
144 var stats:Dictionary = _provider_stats(provider)
145 for key
in stats.keys():
146 var value:Variant = stats.get(key)
147 if value
is int
or value
is float:
148 set_gauge(
"%s.%s" % [id, str(key)], value)
152func measure_callable(id:String, action:Callable, metadata:Dictionary = {}) -> Dictionary:
153 if not enabled
or id.is_empty()
or not action.is_valid():
155 var start_usec:int = Time.get_ticks_usec()
156 var result:Variant = action.call()
157 var sample:Dictionary = record_duration(id, max(0, Time.get_ticks_usec() - start_usec), metadata)
158 sample[
"result"] = result
162func capture_provider_delta(id:String, provider:Object, action:Callable, metadata:Dictionary = {}) -> Dictionary:
163 if not enabled
or id.is_empty()
or provider == null:
165 var before:Dictionary = _provider_stats(provider)
166 var timed:Dictionary = measure_callable(id, action, metadata)
167 var after:Dictionary = _provider_stats(provider)
168 var delta:Dictionary = _numeric_delta(before, after)
169 for key
in delta.keys():
170 set_gauge(
"%s.delta.%s" % [id, str(key)], delta.get(key))
171 for key
in after.keys():
172 var value:Variant = after.get(key)
173 if value
is int
or value
is float:
174 set_gauge(
"%s.after.%s" % [id, str(key)], value)
184func benchmark_scene_instantiation(id:String, packed_scene:PackedScene, parent:Node, iterations:int = 1, metadata:Dictionary = {}) -> Dictionary:
185 if not enabled
or id.is_empty()
or packed_scene == null
or parent == null:
187 var safe_iterations:int = max(1, iterations)
188 var samples:Array = []
189 var total_usec:int = 0
192 var max_nodes:int = 0
193 var before_parent_counts:Dictionary = capture_node_counts(parent,
"%s.parent_before" % id)
194 for index
in range(safe_iterations):
195 var start_usec:int = Time.get_ticks_usec()
196 var instance:Node = packed_scene.instantiate()
197 var instantiate_usec:int = max(0, Time.get_ticks_usec() - start_usec)
198 total_usec += instantiate_usec
199 max_usec = max(max_usec, instantiate_usec)
200 min_usec = instantiate_usec
if index == 0
else min(min_usec, instantiate_usec)
202 parent.add_child(instance)
203 var node_counts:Dictionary = capture_node_counts(instance,
"%s.instance_%d" % [id, index])
204 max_nodes = max(max_nodes,
int(node_counts.get(
"total", 0)))
205 parent.remove_child(instance)
209 "instantiate_usec": instantiate_usec
211 var average_usec:int =
int(round(
float(total_usec) /
float(safe_iterations)))
212 record_duration(
"%s.instantiate" % id, average_usec, metadata)
213 var after_parent_counts:Dictionary = capture_node_counts(parent,
"%s.parent_after" % id)
214 var result:Dictionary = {
216 "iterations": safe_iterations,
217 "total_usec": total_usec,
218 "avg_usec": average_usec,
219 "min_usec": min_usec,
220 "max_usec": max_usec,
221 "max_instance_nodes": max_nodes,
222 "parent_before": before_parent_counts,
223 "parent_after": after_parent_counts,
226 for key
in [
"iterations",
"total_usec",
"avg_usec",
"min_usec",
"max_usec",
"max_instance_nodes"]:
227 set_gauge(
"%s.%s" % [id, key], result.get(key))
228 mark(
"%s.scene_benchmark" % id, {
229 "iterations": safe_iterations,
230 "avg_usec": average_usec,
231 "max_instance_nodes": max_nodes
236func benchmark_scene_profiles(profiles:Array, parent:Node, options:Dictionary = {}) -> Dictionary:
237 var results:Dictionary = {}
238 var errors:Array = []
239 for profile_value
in profiles:
240 if not profile_value
is Dictionary:
242 var profile:Dictionary = profile_value
243 var id:String = str(profile.get(
"id",
""))
244 var scene_path:String = str(profile.get(
"scene",
""))
245 if id.is_empty()
or scene_path.is_empty():
246 errors.append({
"id": id,
"scene": scene_path,
"error":
"missing_id_or_scene"})
248 var loaded_scene = load(scene_path)
249 if loaded_scene == null
or not loaded_scene
is PackedScene:
250 errors.append({
"id": id,
"scene": scene_path,
"error":
"scene_not_loaded"})
252 var metadata:Dictionary = profile.duplicate(true)
253 if options.has(
"metadata")
and options[
"metadata"]
is Dictionary:
254 for key
in options[
"metadata"].keys():
255 metadata[key] = options[
"metadata"][key]
256 var iterations:int = max(1,
int(profile.get(
"iterations", options.get(
"iterations", 1))))
257 results[id] = benchmark_scene_instantiation(id, loaded_scene, parent, iterations, metadata)
261 "count": results.size()
265func load_baseline_profiles(file_path:String) -> Array:
266 var data:Dictionary = data_cache_manager.load_json(file_path)
267 if data
is Dictionary
and data.get(
"profiles", [])
is Array:
268 return data.get(
"profiles", [])
273func get_cache_stats() -> Dictionary:
274 return data_cache_manager.get_cache_stats()
277func format_summary_text() -> String:
278 return "\n".join(format_summary_lines())
281func write_summary_file(file_path:String) -> bool:
282 if file_path.is_empty():
284 var file = FileAccess.open(file_path, FileAccess.WRITE)
287 file.store_string(JSON.stringify(get_summary(),
"\t"))
291func get_summary() -> Dictionary:
293 "timers": _timer_summary(),
294 "counters": counters.duplicate(true),
295 "gauges": gauges.duplicate(true),
296 "marks": marks.duplicate(true),
297 "frames": _frame_summary()
301func get_state() -> Dictionary:
304 "max_frame_samples": max_frame_samples,
305 "timers": timers.duplicate(true),
306 "counters": counters.duplicate(true),
307 "gauges": gauges.duplicate(true),
308 "marks": marks.duplicate(true),
309 "frame_samples": frame_samples.duplicate(true)
313func apply_state(state:Dictionary) -> void:
314 enabled =
bool(state.get(
"enabled", enabled))
315 max_frame_samples = max(1,
int(state.get(
"max_frame_samples", max_frame_samples)))
316 timers = _dictionary_copy(state.get(
"timers", {}))
317 active_timers.clear()
318 counters = _dictionary_copy(state.get(
"counters", {}))
319 gauges = _dictionary_copy(state.get(
"gauges", {}))
320 marks = _array_copy(state.get(
"marks", []))
321 frame_samples = _array_copy(state.get(
"frame_samples", []))
322 _trim_frame_samples()
325func format_summary_lines() -> Array:
327 var summary:Dictionary = get_summary()
328 var timer_summary:Dictionary = summary.get(
"timers", {})
329 for timer_id
in timer_summary.keys():
330 var timer_data:Dictionary = timer_summary.get(timer_id, {})
331 lines.append(
"%s: %s calls, avg %sus" % [
333 str(timer_data.get(
"count", 0)),
334 str(timer_data.get(
"avg_usec", 0))
336 for counter_id
in counters.keys():
337 lines.append(
"%s: %s" % [str(counter_id), str(counters.get(counter_id))])
338 var frame_summary:Dictionary = summary.get(
"frames", {})
339 if int(frame_summary.get(
"sample_count", 0)) > 0:
340 lines.append(
"frames: avg %sfps, min %sfps" % [
341 str(frame_summary.get(
"avg_fps", 0.0)),
342 str(frame_summary.get(
"min_fps", 0.0))
347func _timer_summary() -> Dictionary:
348 var summary:Dictionary = {}
349 for id
in timers.keys():
350 var aggregate:Dictionary = timers.get(id, {})
351 var count:int = max(0,
int(aggregate.get(
"count", 0)))
355 var row:Dictionary = aggregate.duplicate(true)
356 row[
"avg_usec"] = avg_usec
361func _frame_summary() -> Dictionary:
362 if frame_samples.is_empty():
370 var total_delta:float = 0.0
371 var total_fps:float = 0.0
372 var min_fps:float = INF
373 var max_delta:float = 0.0
374 for sample
in frame_samples:
375 var row:Dictionary = sample
376 var delta:float =
float(row.get(
"delta", 0.0))
377 var fps:float =
float(row.get(
"fps", 0.0))
380 min_fps = min(min_fps, fps)
381 max_delta = max(max_delta, delta)
382 var sample_count:int = frame_samples.size()
384 "sample_count": sample_count,
385 "avg_delta": total_delta /
float(sample_count),
386 "avg_fps": total_fps /
float(sample_count),
388 "max_delta": max_delta
392func _count_node(node:Node, counts:Dictionary) -> void:
393 counts[
"total"] =
int(counts.get(
"total", 0)) + 1
395 counts[
"control"] =
int(counts.get(
"control", 0)) + 1
396 if node
is CanvasItem:
397 counts[
"canvas_item"] =
int(counts.get(
"canvas_item", 0)) + 1
399 counts[
"visible"] =
int(counts.get(
"visible", 0)) + 1
401 counts[
"hidden"] =
int(counts.get(
"hidden", 0)) + 1
402 for child
in node.get_children():
403 _count_node(child, counts)
406func _provider_stats(provider:Object) -> Dictionary:
409 var stats:Dictionary = {}
410 if provider.has_method(
"get_cache_stats"):
411 stats = provider.call(
"get_cache_stats")
412 elif provider.has_method(
"get_stats"):
413 stats = provider.call(
"get_stats")
417func _numeric_delta(before:Dictionary, after:Dictionary) -> Dictionary:
418 var delta:Dictionary = {}
419 for key
in after.keys():
420 var after_value:Variant = after.get(key)
421 if not (after_value
is int
or after_value
is float):
423 var before_value:Variant = before.get(key, 0)
424 if not (before_value
is int
or before_value
is float):
426 delta[key] = after_value - before_value
430func _trim_frame_samples() -> void:
431 while frame_samples.size() > max_frame_samples:
432 frame_samples.pop_front()
435func _dictionary_copy(value:Variant) -> Dictionary:
436 if value
is Dictionary:
437 return value.duplicate(true)
441func _array_copy(value:Variant) -> Array:
443 return value.duplicate(true)