forked from godotengine/godot-benchmarks
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmanager.gd
323 lines (263 loc) · 10.8 KB
/
manager.gd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
extends Node
const RANDOM_SEED := 0x60d07
const CPP_CLASS_NAMES: Array[StringName] = [
&"CPPBenchmarkAlloc",
&"CPPBenchmarkArray",
&"CPPBenchmarkBinaryTrees",
&"CPPBenchmarkControl",
&"CPPBenchmarkForLoop",
&"CPPBenchmarkHelloWorld",
&"CPPBenchmarkLambdaPerformance",
&"CPPBenchmarkMandelbrotSet",
&"CPPBenchmarkMerkleTrees",
&"CPPBenchmarkNbody",
&"CPPBenchmarkSpectralNorm",
&"CPPBenchmarkStringChecksum",
&"CPPBenchmarkStringFormat",
&"CPPBenchmarkStringManipulation",
]
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 TestID:
var name : String
var category : String
var language : String
func pretty_name() -> String:
return name.capitalize()
func pretty_category() -> String:
return category.replace("/", " > ").capitalize()
func pretty() -> String:
return "%s: %s" % [pretty_category(), pretty_name()]
func _to_string() -> String:
return "%s/%s" % [category, name]
func test_ids_from_path(path: String) -> Array[TestID]:
var rv : Array[TestID] = []
# Check for runnable tests.
for extension in languages.keys():
if not path.ends_with(extension):
continue
var bench_script = load(path).new()
for method in bench_script.get_method_list():
if not method.name.begins_with(languages[extension]["test_prefix"]):
continue
# This method is a runnable test. Push it onto the result
var test_id := TestID.new()
test_id.name = method.name.trim_prefix(languages[extension]["test_prefix"])
test_id.category = path.trim_prefix("res://benchmarks/").trim_suffix(extension)
test_id.language = extension
rv.push_back(test_id)
return rv
# List of supported languages and their styles.
var languages := {".gd": {"test_prefix": "benchmark_"}, ".cpp": {"test_prefix": "benchmark_"}}
# List of benchmarks populated in `_ready()`.
var test_results := {}
var cpp_classes: Array[RefCounted] = []
var save_json_to_path := ""
var json_results_prefix := ""
var visualize := false
## Recursively walks the given directory and returns all files found
func dir_contents(path: String, contents: PackedStringArray = PackedStringArray()) -> PackedStringArray:
var dir := DirAccess.open(path)
if dir:
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)
else:
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() -> void:
RenderingServer.viewport_set_measure_render_time(get_tree().root.get_viewport_rid(),true)
set_process(false)
# Register script language compatibility
if ClassDB.class_exists(&"CSharpScript"):
languages[".cs"] = {"test_prefix": "Benchmark"}
# Register contents of `benchmarks/` folder automatically.
for benchmark_path in dir_contents("res://benchmarks/"):
for test_id in test_ids_from_path(benchmark_path):
test_results[test_id] = null
# Load GDExtension (C++) benchmarks
for cpp_class_name in CPP_CLASS_NAMES:
if not ClassDB.class_exists(cpp_class_name):
continue
var cpp_class = ClassDB.instantiate(cpp_class_name)
cpp_classes.append(cpp_class)
for method in cpp_class.get_method_list():
if not method.name.begins_with(languages[".cpp"]["test_prefix"]):
continue
var test_id := TestID.new()
test_id.name = method.name.trim_prefix(languages[".cpp"]["test_prefix"])
test_id.category = "C++/" + cpp_class.get_class().replace("CPPBenchmark", "")
test_id.language = ".cpp"
test_results[test_id] = null
func get_test_ids() -> Array[TestID]:
var rv : Array[TestID] = []
rv.assign(test_results.keys().duplicate())
var sorter = func(a, b):
return a.to_string() < b.to_string()
rv.sort_custom(sorter)
return rv
func benchmark(test_ids: Array[TestID], return_path: String) -> void:
await get_tree().process_frame
for i in range(test_ids.size()):
DisplayServer.window_set_title("%d/%d - Running %s" % [i + 1, test_ids.size(), test_ids[i].pretty()])
print("Running benchmark %d of %d: %s" % [i + 1, test_ids.size(), test_ids[i]])
seed(RANDOM_SEED)
await run_test(test_ids[i])
print("Result: %s\n" % get_result_as_string(test_ids[i]))
DisplayServer.window_set_title("[DONE] %d benchmarks - Godot Benchmarks" % test_ids.size())
print_rich("[color=green][b]Done running %d benchmarks.[/b] Results JSON:[/color]\n" % test_ids.size())
print("Results JSON:")
print("----------------")
print(JSON.stringify(get_results_dict(json_results_prefix)))
print("----------------")
if not save_json_to_path.is_empty():
print("Saving JSON output to: %s" % save_json_to_path)
print("Using prefix for results: %s" % json_results_prefix)
var file := FileAccess.open(save_json_to_path, FileAccess.WRITE)
file.store_string(JSON.stringify(get_results_dict(json_results_prefix)))
if return_path:
get_tree().change_scene_to_file(return_path)
else:
# FIXME: The line below crashes the engine. Commenting it results in a
# "ObjectDB instances leaked at exit" warning (but no crash).
#get_tree().queue_delete(get_tree())
get_tree().quit()
func run_test(test_id: TestID) -> void:
set_process(true)
var new_scene := PackedScene.new()
new_scene.pack(Node.new())
get_tree().change_scene_to_packed(new_scene)
# Wait for the scene tree to be ready
while not (get_tree().current_scene and get_tree().current_scene.get_child_count() == 0):
#print("Waiting for scene change...")
await get_tree().process_frame
# Add a dummy child so that the above check works for subsequent reloads
get_tree().current_scene.add_child(Node.new())
var language := test_id.language
var bench_script
if language != ".cpp":
bench_script = load("res://benchmarks/%s%s" % [test_id.category, language]).new()
else:
var cpp_class_name := "CPPBenchmark" + test_id.category.replace("C++", "").replace("/", "")
for cpp_class in cpp_classes:
if cpp_class_name == cpp_class.get_class():
bench_script = ClassDB.instantiate(cpp_class_name)
break
if not is_instance_valid(bench_script):
printerr("Benchmark not found!")
return
var results := Results.new()
# Call and time the function to be tested
var begin_time := Time.get_ticks_usec()
# Redundant awaits don't seem to cause a performance variation.
var bench_node = await bench_script.call(languages[test_id.language]["test_prefix"] + test_id.name)
results.time = (Time.get_ticks_usec() - begin_time) * 0.001
# Continue benchmarking if the function call has returned a node
var frames_captured := 0
if bench_node:
get_tree().current_scene.add_child(bench_node)
# TODO: Any better ways of waiting for shader compilation?
for i in 3:
await get_tree().process_frame
var time_limit: int = bench_script.get("benchmark_time")
begin_time = Time.get_ticks_usec()
while (Time.get_ticks_usec() - begin_time) < time_limit:
await get_tree().process_frame
results.render_cpu += RenderingServer.viewport_get_measured_render_time_cpu(get_tree().root.get_viewport_rid()) + RenderingServer.get_frame_setup_time_cpu()
results.render_gpu += RenderingServer.viewport_get_measured_render_time_gpu(get_tree().root.get_viewport_rid())
# Godot updates idle and physics performance monitors only once per second,
# with the value representing the average time spent processing idle/physics process in the last second.
# The value is in seconds, not milliseconds.
# Keep the highest reported value throughout the run.
results.idle = maxf(results.idle, Performance.get_monitor(Performance.TIME_PROCESS) * 1000)
results.physics = maxf(results.physics, Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS) * 1000)
frames_captured += 1
results.render_cpu /= float(max(1.0, float(frames_captured)))
results.render_gpu /= float(max(1.0, float(frames_captured)))
# Don't divide `results.idle` and `results.physics` since these are already
# metrics calculated on a per-second basis.
for metric in results.get_property_list():
if bench_script.get("test_" + metric.name) == false: # account for null
results.set(metric.name, 0.0)
test_results[test_id] = results
func get_result_as_string(test_id: TestID) -> String:
# Returns all non-zero metrics formatted as a string
var rd := get_test_result_as_dict(test_id)
for key in rd.keys():
if rd[key] == 0.0:
rd.erase(key)
return JSON.stringify(rd)
func get_test_result_as_dict(test_id: TestID, results_prefix: String = "") -> Dictionary:
var result : Results = test_results[test_id]
var rv := {}
if not results_prefix.is_empty():
# Nest the results dictionary with a prefix for easier merging of multiple unrelated runs with `jq`.
# For example, this is used on the benchmarks server to merge runs on several GPU vendors into a single JSON file.
rv = { results_prefix: {} }
if not result:
return rv
for metric in result.get_property_list():
if metric.type == TYPE_FLOAT:
var m : float = result.get(metric.name)
const sig_figs = 4
if not is_zero_approx(m):
# Only store metrics if not 0 to reduce JSON size.
if not results_prefix.is_empty():
rv[results_prefix][metric.name] = snapped(m, pow(10,floor(log(m)/log(10))-sig_figs+1))
else:
rv[metric.name] = snapped(m, pow(10,floor(log(m)/log(10))-sig_figs+1))
return rv
func get_results_dict(results_prefix: String = "") -> 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]
# Only list information that doesn't change across benchmark runs on different GPUs,
# as JSON files are merged together. Otherwise, the fields would overwrite each other
# with different information.
var dict := {
engine = {
version = version_string,
version_hash = version_info.hash,
},
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(),
}
}
var benchmarks := []
for test_id in get_test_ids():
var result_dict := get_test_result_as_dict(test_id, results_prefix)
# Only write a dictionary if a benchmark was run for it.
var should_write_dict := false
if results_prefix.is_empty():
should_write_dict = not result_dict.is_empty()
else:
should_write_dict = not result_dict[results_prefix].is_empty()
if should_write_dict:
benchmarks.push_back({
category = test_id.pretty_category(),
name = test_id.pretty_name(),
results = result_dict,
})
dict.benchmarks = benchmarks
return dict