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.
@@ -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 and how many looks it took, which is how a
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
- next_look = Time.get_ticks_usec() + (Time.get_ticks_usec() - looking) * LOOKS_APART
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