gdharness 1.1.55 → 1.1.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +419 -53
- package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -0
- package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +214 -0
- package/build/godot/addons/gdharness_editor/tools/profile_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +10 -3
- package/build/index.js +424 -59
- package/build/keeper.js +527 -6
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ const PlayTools = preload("tools/play_tools.gd")
|
|
|
12
12
|
const ClassTools = preload("tools/class_tools.gd")
|
|
13
13
|
const SettingsTools = preload("tools/settings_tools.gd")
|
|
14
14
|
const FilesystemTools = preload("tools/filesystem_tools.gd")
|
|
15
|
+
const ProfileTools = preload("tools/profile_tools.gd")
|
|
15
16
|
|
|
16
17
|
var _editor_plugin: EditorPlugin = null
|
|
17
18
|
|
|
@@ -22,6 +23,7 @@ var _play_tools: PlayTools = null
|
|
|
22
23
|
var _class_tools: ClassTools = null
|
|
23
24
|
var _settings_tools: SettingsTools = null
|
|
24
25
|
var _filesystem_tools: FilesystemTools = null
|
|
26
|
+
var _profile_tools: ProfileTools = null
|
|
25
27
|
|
|
26
28
|
var _tool_map: Dictionary = {}
|
|
27
29
|
var _initialized: bool = false
|
|
@@ -36,6 +38,7 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
|
36
38
|
_play_tools.set_editor_plugin(plugin)
|
|
37
39
|
_class_tools.set_editor_plugin(plugin)
|
|
38
40
|
_filesystem_tools.set_editor_plugin(plugin)
|
|
41
|
+
_profile_tools.set_editor_plugin(plugin)
|
|
39
42
|
|
|
40
43
|
|
|
41
44
|
func _init_tools() -> void:
|
|
@@ -71,6 +74,10 @@ func _init_tools() -> void:
|
|
|
71
74
|
_filesystem_tools.name = "FilesystemTools"
|
|
72
75
|
add_child(_filesystem_tools)
|
|
73
76
|
|
|
77
|
+
_profile_tools = ProfileTools.new()
|
|
78
|
+
_profile_tools.name = "ProfileTools"
|
|
79
|
+
add_child(_profile_tools)
|
|
80
|
+
|
|
74
81
|
_tool_map = {
|
|
75
82
|
# Scene tools
|
|
76
83
|
"create_scene": [_scene_tools, "create_scene"],
|
|
@@ -108,6 +115,9 @@ func _init_tools() -> void:
|
|
|
108
115
|
"quit_editor": [_play_tools, "quit_editor"],
|
|
109
116
|
"stop_playing": [_play_tools, "stop_playing"],
|
|
110
117
|
"playing_status": [_play_tools, "playing_status"],
|
|
118
|
+
# The script profiler on a game the editor plays
|
|
119
|
+
"profile_start": [_profile_tools, "profile_start"],
|
|
120
|
+
"profile_read": [_profile_tools, "profile_read"],
|
|
111
121
|
"create_animation": [_animation_tools, "create_animation"],
|
|
112
122
|
"add_animation_track": [_animation_tools, "add_animation_track"],
|
|
113
123
|
"add_animation_state": [_animation_tools, "add_animation_state"],
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends Node
|
|
3
|
+
|
|
4
|
+
## The engine's script profiler on a game the editor plays, summed here for the server.
|
|
5
|
+
##
|
|
6
|
+
## The editor is that game's debugger, so the profiler is switched on through the editor's own
|
|
7
|
+
## session, and what the game sends back is read where the editor reads it. The editor's handlers
|
|
8
|
+
## take the `servers:*` messages before any EditorDebuggerPlugin is asked (script_editor_debugger
|
|
9
|
+
## .cpp at 4.7.2), so a plugin's capture never sees them. The one place they pass through is the
|
|
10
|
+
## `debug_data` signal every ScriptEditorDebugger emits for every message, on a node Godot does not
|
|
11
|
+
## expose to scripts: found by its class in the editor's tree and connected to by name.
|
|
12
|
+
|
|
13
|
+
const Read = preload("../reading.gd")
|
|
14
|
+
|
|
15
|
+
## The engine sends only the top so many functions a frame and clamps nothing on the game's side.
|
|
16
|
+
## Its own buffer's default, so no frame is cut short of what was called.
|
|
17
|
+
const FRAME_FUNCTIONS: int = 16384
|
|
18
|
+
const DEBUGGER_CLASS: String = "ScriptEditorDebugger"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## The editor's debugger sessions, numbered as the editor numbers its debugger tabs.
|
|
22
|
+
class Sessions:
|
|
23
|
+
extends EditorDebuggerPlugin
|
|
24
|
+
|
|
25
|
+
signal session_started(session_id: int)
|
|
26
|
+
|
|
27
|
+
func _setup_session(session_id: int) -> void:
|
|
28
|
+
var session: EditorDebuggerSession = get_session(session_id)
|
|
29
|
+
# int rather than Error: Signal.connect answers with a plain int.
|
|
30
|
+
var joined: int = session.started.connect(func() -> void: session_started.emit(session_id))
|
|
31
|
+
if joined != OK:
|
|
32
|
+
push_error("gdharness could not watch debugger session %d start: %d" % [session_id, joined])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
var _editor_plugin: EditorPlugin = null
|
|
36
|
+
var _sessions: Sessions = null
|
|
37
|
+
## The debugger tabs connected to, by instance id, so none is connected twice.
|
|
38
|
+
var _hooked: Dictionary[int, bool] = {}
|
|
39
|
+
|
|
40
|
+
## A play asked to be profiled before its session started.
|
|
41
|
+
var _armed: bool = false
|
|
42
|
+
var _session: int = -1
|
|
43
|
+
var _started_msec: int = 0
|
|
44
|
+
var _last_frame_msec: int = -1
|
|
45
|
+
var _frames: int = 0
|
|
46
|
+
var _complete: bool = false
|
|
47
|
+
var _names: Dictionary[int, String] = {}
|
|
48
|
+
## Per signature: calls, self seconds, total seconds.
|
|
49
|
+
var _totals: Dictionary[String, PackedFloat64Array] = {}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
53
|
+
_editor_plugin = plugin
|
|
54
|
+
_sessions = Sessions.new()
|
|
55
|
+
var joined: int = _sessions.session_started.connect(_on_session_started)
|
|
56
|
+
if joined != OK:
|
|
57
|
+
push_error("gdharness could not watch debugger sessions start: %d" % joined)
|
|
58
|
+
plugin.add_debugger_plugin(_sessions)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
func _exit_tree() -> void:
|
|
62
|
+
if _editor_plugin != null and _sessions != null:
|
|
63
|
+
_editor_plugin.remove_debugger_plugin(_sessions)
|
|
64
|
+
_sessions = null
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
## Switches the profiler on for the game being played, or with [code]arm[/code] for the next one
|
|
68
|
+
## to start and never the one playing: a start ends the game before it, which can still be going.
|
|
69
|
+
## Either way what was summed before is dropped.
|
|
70
|
+
func profile_start(args: Dictionary) -> Dictionary:
|
|
71
|
+
if _hook() == 0:
|
|
72
|
+
return {
|
|
73
|
+
"ok": false,
|
|
74
|
+
"error":
|
|
75
|
+
(
|
|
76
|
+
(
|
|
77
|
+
"This editor's debugger has no %s node to read the profiler from, which is where Godot"
|
|
78
|
+
+ " 4.7 keeps it: a Godot that moved it has to be met by a gdharness that knows where."
|
|
79
|
+
)
|
|
80
|
+
% DEBUGGER_CLASS
|
|
81
|
+
),
|
|
82
|
+
}
|
|
83
|
+
_reset()
|
|
84
|
+
if Read.as_bool(args.get("arm", false)):
|
|
85
|
+
_armed = true
|
|
86
|
+
return {"ok": true, "started": false, "armed": true, "session": -1}
|
|
87
|
+
var active: int = _active_session()
|
|
88
|
+
if active < 0:
|
|
89
|
+
return {"ok": true, "started": false, "armed": false, "session": -1}
|
|
90
|
+
_switch_on(active)
|
|
91
|
+
return {"ok": true, "started": true, "armed": false, "session": active}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
## What has been summed, for the server to put into an answer.
|
|
95
|
+
func profile_read(_args: Dictionary) -> Dictionary:
|
|
96
|
+
var functions: Dictionary = {}
|
|
97
|
+
for signature: String in _totals:
|
|
98
|
+
functions[signature] = Array(_totals[signature])
|
|
99
|
+
return {
|
|
100
|
+
"ok": true,
|
|
101
|
+
"profiled": _session >= 0,
|
|
102
|
+
"armed": _armed,
|
|
103
|
+
"session": _session,
|
|
104
|
+
"playing": EditorInterface.is_playing_scene(),
|
|
105
|
+
"frames": _frames,
|
|
106
|
+
"complete": _complete,
|
|
107
|
+
"coveredMs": _last_frame_msec - _started_msec if _last_frame_msec >= 0 else 0,
|
|
108
|
+
"functions": functions,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
func _reset() -> void:
|
|
113
|
+
_armed = false
|
|
114
|
+
_session = -1
|
|
115
|
+
_frames = 0
|
|
116
|
+
_complete = false
|
|
117
|
+
_last_frame_msec = -1
|
|
118
|
+
_names.clear()
|
|
119
|
+
_totals.clear()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
func _active_session() -> int:
|
|
123
|
+
if _sessions == null:
|
|
124
|
+
return -1
|
|
125
|
+
var all: Array[EditorDebuggerSession] = _sessions.get_sessions()
|
|
126
|
+
for index: int in all.size():
|
|
127
|
+
if all[index].is_active():
|
|
128
|
+
return index
|
|
129
|
+
return -1
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
func _switch_on(session_id: int) -> void:
|
|
133
|
+
_session = session_id
|
|
134
|
+
_started_msec = Time.get_ticks_msec()
|
|
135
|
+
var session: EditorDebuggerSession = _sessions.get_session(session_id)
|
|
136
|
+
session.toggle_profiler("servers", true, [FRAME_FUNCTIONS, false])
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
## A play that was asked to be profiled is switched on as it starts. Any other play drops what is
|
|
140
|
+
## held: the editor plays the next game in the same session, and a profile kept past it was read as
|
|
141
|
+
## the new game's.
|
|
142
|
+
func _on_session_started(session_id: int) -> void:
|
|
143
|
+
if not _armed:
|
|
144
|
+
_reset()
|
|
145
|
+
return
|
|
146
|
+
_armed = false
|
|
147
|
+
if _hook() > 0:
|
|
148
|
+
_switch_on(session_id)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
## Connects to every debugger tab not connected yet, in tab order, which is session order.
|
|
152
|
+
func _hook() -> int:
|
|
153
|
+
var found: Array[Node] = EditorInterface.get_base_control().find_children(
|
|
154
|
+
"*", DEBUGGER_CLASS, true, false
|
|
155
|
+
)
|
|
156
|
+
for index: int in found.size():
|
|
157
|
+
var tab: Node = found[index]
|
|
158
|
+
var id: int = tab.get_instance_id()
|
|
159
|
+
if not _hooked.has(id) and tab.has_signal("debug_data"):
|
|
160
|
+
var joined: Error = tab.connect("debug_data", _on_debug_data.bind(index))
|
|
161
|
+
if joined != OK:
|
|
162
|
+
push_error("gdharness could not read debugger tab %d: %d" % [index, joined])
|
|
163
|
+
_hooked[id] = true
|
|
164
|
+
return found.size()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
func _on_debug_data(message: String, data: Array, session_id: int) -> void:
|
|
168
|
+
if session_id != _session:
|
|
169
|
+
return
|
|
170
|
+
if message == "servers:function_signature" and data.size() >= 2:
|
|
171
|
+
_names[Read.as_int(data[1])] = str(data[0])
|
|
172
|
+
elif message == "servers:profile_frame":
|
|
173
|
+
if _add(data, _totals):
|
|
174
|
+
_frames += 1
|
|
175
|
+
_last_frame_msec = Time.get_ticks_msec()
|
|
176
|
+
elif message == "servers:profile_total":
|
|
177
|
+
# The engine's own count since the profiler started, so it replaces the frames' sum.
|
|
178
|
+
var whole: Dictionary[String, PackedFloat64Array] = {}
|
|
179
|
+
if _add(data, whole):
|
|
180
|
+
_totals = whole
|
|
181
|
+
_complete = true
|
|
182
|
+
_last_frame_msec = Time.get_ticks_msec()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
## Adds one frame's functions into [param into], reading past the servers' section to reach them:
|
|
186
|
+
## six frame times, the server count, each server's name and field count and fields, then five
|
|
187
|
+
## numbers per function.
|
|
188
|
+
func _add(data: Array, into: Dictionary[String, PackedFloat64Array]) -> bool:
|
|
189
|
+
var at: int = 6
|
|
190
|
+
if data.size() <= at:
|
|
191
|
+
return false
|
|
192
|
+
var servers: int = Read.as_int(data[at])
|
|
193
|
+
at += 1
|
|
194
|
+
for _server: int in servers:
|
|
195
|
+
if data.size() <= at + 1:
|
|
196
|
+
return false
|
|
197
|
+
at += 2 + Read.as_int(data[at + 1])
|
|
198
|
+
if data.size() <= at:
|
|
199
|
+
return false
|
|
200
|
+
var values: int = Read.as_int(data[at])
|
|
201
|
+
at += 1
|
|
202
|
+
if values % 5 != 0 or data.size() < at + values:
|
|
203
|
+
return false
|
|
204
|
+
for index: int in range(at, at + values, 5):
|
|
205
|
+
var id: int = Read.as_int(data[index])
|
|
206
|
+
var signature: String = _names[id] if _names.has(id) else "#%d" % id
|
|
207
|
+
var sum: PackedFloat64Array = (
|
|
208
|
+
into[signature] if into.has(signature) else PackedFloat64Array([0, 0, 0])
|
|
209
|
+
)
|
|
210
|
+
sum[0] += Read.as_float(data[index + 1])
|
|
211
|
+
sum[1] += Read.as_float(data[index + 2])
|
|
212
|
+
sum[2] += Read.as_float(data[index + 3])
|
|
213
|
+
into[signature] = sum
|
|
214
|
+
return true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://bxqtu4010skdi
|
|
@@ -27,6 +27,10 @@ const BIND_ADDRESS_SETTING: String = "gdharness/runtime/bind_address"
|
|
|
27
27
|
## expects. A fixed port is for a client that cannot read the announcement.
|
|
28
28
|
const PORT_SETTING: String = "gdharness/runtime/port"
|
|
29
29
|
|
|
30
|
+
## How long a profiled game waits as it leaves for its profile to be sent: the debugger's thread
|
|
31
|
+
## sends every 6.9 ms, so this is many of its turns.
|
|
32
|
+
const PROFILE_SEND_MS: int = 100
|
|
33
|
+
|
|
30
34
|
## How the engine is told to run a script instead of the game: `godot -s thing.gd`.
|
|
31
35
|
const SCRIPT_FLAGS: PackedStringArray = ["-s", "--script"]
|
|
32
36
|
|
|
@@ -126,6 +130,7 @@ func _ready() -> void:
|
|
|
126
130
|
|
|
127
131
|
|
|
128
132
|
func _exit_tree() -> void:
|
|
133
|
+
_send_the_profile()
|
|
129
134
|
if _errors != null:
|
|
130
135
|
OS.remove_logger(_errors)
|
|
131
136
|
_errors.close()
|
|
@@ -133,6 +138,18 @@ func _exit_tree() -> void:
|
|
|
133
138
|
_cleanup()
|
|
134
139
|
|
|
135
140
|
|
|
141
|
+
## A profiled run's totals, sent as the game leaves the tree.
|
|
142
|
+
##
|
|
143
|
+
## The engine sends its script profiler's totals only when the profiler is switched off, and at
|
|
144
|
+
## exit it is torn down without being switched off, so a scene that did its work in _ready and
|
|
145
|
+
## quit took its whole profile with it, last frame included. The wait is for the debugger's own
|
|
146
|
+
## thread, which sends what is queued every few milliseconds and stops without emptying the queue.
|
|
147
|
+
func _send_the_profile() -> void:
|
|
148
|
+
if EngineDebugger.is_active() and EngineDebugger.is_profiling("servers"):
|
|
149
|
+
EngineDebugger.profiler_enable("servers", false)
|
|
150
|
+
OS.delay_msec(PROFILE_SEND_MS)
|
|
151
|
+
|
|
152
|
+
|
|
136
153
|
## The engine's error reports, written to a file for the server, for a game the editor plays, and
|
|
137
154
|
## its prints as well from the line announcing the runtime on.
|
|
138
155
|
##
|
|
@@ -322,12 +322,14 @@ static func _declared_type(holder: Variant, named: String) -> int:
|
|
|
322
322
|
## looking takes about a tenth of the time. Every frame on a hall of 3,500 nodes held the game at a
|
|
323
323
|
## sixth of its frame rate, and its clock, which advances by the frame's delta, lost three quarters
|
|
324
324
|
## of its time, so a wait for a date timed out on a clock that turned a day every thirty seconds.
|
|
325
|
-
## The answer says how many frames the wait spanned
|
|
326
|
-
## caller sees what it cost
|
|
325
|
+
## The answer says how many frames the wait spanned, how many looks it took and how long they took
|
|
326
|
+
## between them, which is how a caller sees what it cost and whether the game lost its speed to the
|
|
327
|
+
## looks or to something else.
|
|
327
328
|
func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_hidden: bool) -> Dictionary:
|
|
328
329
|
var started: int = Time.get_ticks_msec()
|
|
329
330
|
var frames: int = 0
|
|
330
331
|
var looks: int = 0
|
|
332
|
+
var looked_usec: int = 0
|
|
331
333
|
var next_look: int = 0
|
|
332
334
|
var found: bool = false
|
|
333
335
|
while true:
|
|
@@ -335,7 +337,9 @@ func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_
|
|
|
335
337
|
var looking: int = Time.get_ticks_usec()
|
|
336
338
|
found = _anything_says(node_path, said, include_hidden)
|
|
337
339
|
looks += 1
|
|
338
|
-
|
|
340
|
+
var took: int = Time.get_ticks_usec() - looking
|
|
341
|
+
looked_usec += took
|
|
342
|
+
next_look = Time.get_ticks_usec() + took * LOOKS_APART
|
|
339
343
|
if found or Time.get_ticks_msec() - started >= timeout_ms:
|
|
340
344
|
break
|
|
341
345
|
await _host.get_tree().process_frame
|
|
@@ -343,8 +347,10 @@ func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_
|
|
|
343
347
|
# One more look at the end, so words that arrived between two paced looks are not answered as
|
|
344
348
|
# never having come.
|
|
345
349
|
if not found:
|
|
350
|
+
var last: int = Time.get_ticks_usec()
|
|
346
351
|
found = _anything_says(node_path, said, include_hidden)
|
|
347
352
|
looks += 1
|
|
353
|
+
looked_usec += Time.get_ticks_usec() - last
|
|
348
354
|
|
|
349
355
|
return {
|
|
350
356
|
"type": "condition",
|
|
@@ -355,6 +361,7 @@ func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_
|
|
|
355
361
|
"elapsed_ms": Time.get_ticks_msec() - started,
|
|
356
362
|
"frames": frames,
|
|
357
363
|
"looks": looks,
|
|
364
|
+
"looking_ms": looked_usec / 1000.0,
|
|
358
365
|
}
|
|
359
366
|
|
|
360
367
|
|