Files
godot-benchmarks/manager.gd
Hugo Locurcio 86bbebd66d Implement saving results JSON file using --save-json CLI argument
- Save operating system and CPU name in results JSON.
2022-09-12 03:25:24 +02:00

274 lines
7.9 KiB
GDScript

extends Node
class Results:
var render_cpu := 0.0
var render_gpu := 0.0
var idle := 0.0
var physics := 0.0
var time := 0.0
class Test:
var name : String
var category : String
var path : String
var results : Results = null
func _init(p_name : String,p_category: String,p_path : String):
name = p_name
category = p_category
path = p_path
# List of benchmarks populated in `_ready()`.
var tests: Array[Test] = []
var frames_captured := 0
var results: Results = null
var recording := false
var begin_time := 0.0
var remaining_time := 5.0
var tests_queue = []
## Used to display the number of benchmarks that need to be run in the console output and window title.
var tests_queue_initial_size := 0
var test_time := 5.0
var return_to_scene : = ""
var skip_first := false
var run_from_cli := false
var save_json_to_path := ""
var record_render_gpu := false
var record_render_cpu := false
var record_idle := false
var record_physics := false
var time_limit := true
## Returns file paths ending with `.tscn` within a folder, recursively.
func dir_contents(path: String, contents: PackedStringArray = PackedStringArray()) -> PackedStringArray:
var dir := Directory.new()
if dir.open(path) == OK:
dir.list_dir_begin()
var file_name = dir.get_next()
while file_name != "":
if dir.current_is_dir():
dir_contents(path.path_join(file_name), contents)
elif file_name.ends_with(".tscn"):
contents.push_back(path.path_join(file_name))
file_name = dir.get_next()
else:
print("An error occurred when trying to access the path: %s" % path)
return contents
func _ready():
RenderingServer.viewport_set_measure_render_time(get_tree().root.get_viewport_rid(),true)
set_process(false)
# Register benchmarks automatically based on `.tscn` file paths in the `benchmarks/` folder.
# Scene names starting with `_` are excluded, as this denotes an instanced scene that is
# referred to in another scene.
var benchmark_paths := dir_contents("res://benchmarks/")
benchmark_paths.sort()
for benchmark_path in benchmark_paths:
var benchmark_name := benchmark_path.get_file().get_basename()
# Capitalize only after checking whether the name begins with `_`, as `capitalize()`
# removes underscores.
if not benchmark_name.begins_with("_"):
benchmark_name = benchmark_name.capitalize()
var category := benchmark_path.get_base_dir().trim_prefix("res://benchmarks/").replace("/", " > ").capitalize()
tests.push_back(Test.new(benchmark_name, category, benchmark_path))
func _process(delta: float) -> void:
if not recording:
return
if skip_first:
skip_first = false
return
if record_render_cpu:
results.render_cpu += RenderingServer.viewport_get_measured_render_time_cpu(get_tree().root.get_viewport_rid()) + RenderingServer.get_frame_setup_time_cpu()
if record_render_gpu:
results.render_gpu += RenderingServer.viewport_get_measured_render_time_gpu(get_tree().root.get_viewport_rid())
if record_idle:
results.idle += 0.0
if record_physics:
results.physics += 0.0
frames_captured += 1
if time_limit:
# Some benchmarks (such as scripting) may not have a time limit.
remaining_time -= delta
if remaining_time < 0.0:
end_test()
func get_test_count() -> int:
return tests.size()
func get_test_name(index: int) -> String:
return tests[index].name
func get_test_category(index: int) -> String:
return tests[index].category
func get_test_result(index: int) -> Results:
return tests[index].results
func get_test_path(index: int) -> String:
return tests[index].path
func benchmark(queue: Array, return_path: String) -> void:
tests_queue = queue
if tests_queue.size() == 0:
return
if tests_queue_initial_size == 0:
tests_queue_initial_size = queue.size()
# Run benchmarks for 5 seconds if they have a time limit.
test_time = 5.0
return_to_scene = return_path
begin_test()
func begin_test() -> void:
DisplayServer.window_set_title("%d/%d - Running - Godot Benchmarks" % [tests_queue_initial_size - tests_queue.size() + 1, tests_queue_initial_size])
print("Running benchmark %d of %d: %s" % [
tests_queue_initial_size - tests_queue.size() + 1,
tests_queue_initial_size,
tests[tests_queue[0]].path.trim_prefix("res://benchmarks/").trim_suffix(".tscn")]
)
results = Results.new()
set_process(true)
get_tree().change_scene_to_file(tests[tests_queue[0]].path)
recording = true
begin_time = Time.get_ticks_usec() * 0.001
remaining_time = test_time
# Wait for the scene tree to be ready (required for `benchmark_config` group to be available).
# This requires waiting for 2 frames to work consistently (1 frame is flaky).
for i in 2:
await get_tree().process_frame
var benchmark_node := get_tree().get_first_node_in_group("benchmark_config")
if benchmark_node:
record_render_cpu = benchmark_node.test_render_cpu
record_render_gpu = benchmark_node.test_render_gpu
record_idle = benchmark_node.test_idle
record_physics = benchmark_node.test_physics
time_limit = benchmark_node.time_limit
else:
record_render_cpu = true
record_render_gpu = true
record_idle = true
record_physics = true
time_limit = true
skip_first = true
frames_captured = 0
func end_test() -> void:
recording = false
results.render_cpu /= float(max(1.0, float(frames_captured)))
results.render_gpu /= float(max(1.0, float(frames_captured)))
results.idle /= float(max(1.0, float(frames_captured)))
results.physics /= float(max(1.0, float(frames_captured)))
results.time = Time.get_ticks_usec() * 0.001 - begin_time
tests[tests_queue[0]].results = results
results = null
tests_queue.pop_front()
# If more tests are still pending, go to the next test.
if tests_queue.size() > 0:
begin_test()
else:
get_tree().change_scene_to_file(return_to_scene)
return_to_scene = ""
DisplayServer.window_set_title("[DONE] %d benchmarks - Godot Benchmarks" % tests_queue_initial_size)
print_rich("[color=green][b]Done running %d benchmarks.[/b] Results JSON:[/color]\n" % tests_queue_initial_size)
print("Results JSON:")
print("----------------")
print(JSON.stringify(get_results_dict()))
print("----------------")
if not save_json_to_path.is_empty():
print("Saving JSON output to: %s" % save_json_to_path)
var file := File.new()
file.open(save_json_to_path, File.WRITE)
file.store_string(JSON.stringify(get_results_dict()))
file.close()
if run_from_cli:
# Automatically exit after running benchmarks for automation purposes.
get_tree().quit()
func get_results_dict() -> Dictionary:
var version_info := Engine.get_version_info()
var version_string: String
if version_info.patch >= 1:
version_string = "v%d.%d.%d.%s.%s" % [version_info.major, version_info.minor, version_info.patch, version_info.status, version_info.build]
else:
version_string = "v%d.%d.%s.%s" % [version_info.major, version_info.minor, version_info.status, version_info.build]
var dict := {
engine = {
version = version_string,
version_hash = version_info.hash,
build_type = "debug" if OS.is_debug_build() else "release",
},
system = {
os = OS.get_name(),
cpu_name = OS.get_processor_name(),
cpu_architecture = (
"x86_64" if OS.has_feature("x86_64")
else "arm64" if OS.has_feature("arm64")
else "arm" if OS.has_feature("arm")
else "x86" if OS.has_feature("x86")
else "unknown"
),
cpu_count = OS.get_processor_count(),
gpu_name = RenderingServer.get_video_adapter_name(),
gpu_vendor = RenderingServer.get_video_adapter_vendor(),
}
}
var benchmarks := []
for i in Manager.get_test_count():
var test := {
category = Manager.get_test_category(i),
name = Manager.get_test_name(i),
}
var result: Results = Manager.get_test_result(i)
if result:
test.results = {
render_cpu = snapped(result.render_cpu, 0.01),
render_gpu = snapped(result.render_gpu, 0.01),
idle = snapped(result.idle, 0.01),
physics = snapped(result.physics, 0.01),
time = round(result.time),
}
else:
test.results = {}
benchmarks.push_back(test)
dict.benchmarks = benchmarks
return dict