gdharness 1.1.58 → 1.1.59
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 +60 -6
- package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +31 -13
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +55 -1
- package/build/index.js +62 -5
- package/build/keeper.js +6905 -25
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -18436,7 +18436,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
18436
18436
|
summary: "what one property reads on a node, or through the objects it holds, refusing a name nothing along the way has",
|
|
18437
18437
|
requires: ["nodePath", "property"]
|
|
18438
18438
|
},
|
|
18439
|
-
metrics: {
|
|
18439
|
+
metrics: {
|
|
18440
|
+
summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game itself, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. The memory, object and render counts are read as asked",
|
|
18441
|
+
requires: []
|
|
18442
|
+
}
|
|
18440
18443
|
},
|
|
18441
18444
|
defaultOperation: "tree"
|
|
18442
18445
|
},
|
|
@@ -42305,6 +42308,13 @@ function readProfileFile(path) {
|
|
|
42305
42308
|
return null;
|
|
42306
42309
|
}
|
|
42307
42310
|
}
|
|
42311
|
+
function frameFunctionsFor(queued) {
|
|
42312
|
+
return Math.max(64, queued - QUEUE_HEADROOM);
|
|
42313
|
+
}
|
|
42314
|
+
function queuedMessagesOf(projectGodot) {
|
|
42315
|
+
const set = projectGodot === null ? undefined : parseProjectGodot(projectGodot)["network"]?.[QUEUE_SETTING];
|
|
42316
|
+
return typeof set === "number" && Number.isInteger(set) && set > 0 ? set : QUEUE_DEFAULT;
|
|
42317
|
+
}
|
|
42308
42318
|
function signatureParts(signature) {
|
|
42309
42319
|
const parts = signature.split("::");
|
|
42310
42320
|
if (parts.length < 3) {
|
|
@@ -42366,10 +42376,11 @@ function callersOf(totals, name, scripts) {
|
|
|
42366
42376
|
sites.sort((a, b) => (b.withinTotalMs ?? -1) - (a.withinTotalMs ?? -1) || a.script.localeCompare(b.script) || a.line - b.line);
|
|
42367
42377
|
return { target, sites: sites.slice(0, CALL_SITES), omitted: Math.max(0, sites.length - CALL_SITES) };
|
|
42368
42378
|
}
|
|
42369
|
-
var ms = (seconds) => Math.round(seconds * 1e6) / 1000, CALL_SITES = 40;
|
|
42379
|
+
var QUEUE_SETTING = "limits/debugger/max_queued_messages", QUEUE_DEFAULT = 2048, QUEUE_HEADROOM = 512, UNNAMED_PREFIX = "#", ms = (seconds) => Math.round(seconds * 1e6) / 1000, CALL_SITES = 40;
|
|
42370
42380
|
var init_script_profile = __esm(() => {
|
|
42371
42381
|
init_gdscript_source();
|
|
42372
42382
|
init_godot_variant();
|
|
42383
|
+
init_resources();
|
|
42373
42384
|
});
|
|
42374
42385
|
|
|
42375
42386
|
// src/server.ts
|
|
@@ -43072,6 +43083,26 @@ function profileOnStart(profiled, through, runtimeAddon) {
|
|
|
43072
43083
|
totals: `On from the game's start, through ${debuggerOf}. editor_run profile answers at any time: each frame is added as it ends, so a scene still inside a long _ready shows nothing until that returns, and the engine's own totals replace the sum when the game quits` + (runtimeAddon ? "." : ", which takes the runtime addon: without it the frames that arrived before the exit are what is kept.")
|
|
43073
43084
|
};
|
|
43074
43085
|
}
|
|
43086
|
+
function profileFrameFunctions(projectPath) {
|
|
43087
|
+
let projectGodot = null;
|
|
43088
|
+
if (projectPath !== null) {
|
|
43089
|
+
try {
|
|
43090
|
+
projectGodot = readFileSync23(join26(projectPath, "project.godot"), "utf8");
|
|
43091
|
+
} catch {}
|
|
43092
|
+
}
|
|
43093
|
+
return frameFunctionsFor(queuedMessagesOf(projectGodot));
|
|
43094
|
+
}
|
|
43095
|
+
function playedCap(held) {
|
|
43096
|
+
const frameFunctions = readPositiveNumber(held, "frameFunctions");
|
|
43097
|
+
if (frameFunctions === undefined) {
|
|
43098
|
+
return {};
|
|
43099
|
+
}
|
|
43100
|
+
return {
|
|
43101
|
+
frameFunctions,
|
|
43102
|
+
cappedFrames: readNonNegativeNumber(held, "cappedFrames") ?? 0,
|
|
43103
|
+
totalCapped: readBoolean(held, "totalCapped") === true
|
|
43104
|
+
};
|
|
43105
|
+
}
|
|
43075
43106
|
function projectSourceOf(projectPath) {
|
|
43076
43107
|
const read = new Map;
|
|
43077
43108
|
return (script) => {
|
|
@@ -43118,6 +43149,7 @@ function profileAnswer(reading, limit) {
|
|
|
43118
43149
|
} else {
|
|
43119
43150
|
notes.push(`These are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} that arrived, summed, without the engine's own totals, so what the game did after the last of them is not in it: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${addonNote}`);
|
|
43120
43151
|
}
|
|
43152
|
+
notes.push(...cappedNotes(totals));
|
|
43121
43153
|
if (totals.unreadable > 0) {
|
|
43122
43154
|
notes.push(`${totals.unreadable} message${totals.unreadable === 1 ? "" : "s"} from the game could not be read, so the totals have that many gaps.`);
|
|
43123
43155
|
}
|
|
@@ -43136,6 +43168,22 @@ function profileAnswer(reading, limit) {
|
|
|
43136
43168
|
note: notes.length > 0 ? notes.join(" ") : undefined
|
|
43137
43169
|
};
|
|
43138
43170
|
}
|
|
43171
|
+
function cappedNotes(totals) {
|
|
43172
|
+
const notes = [];
|
|
43173
|
+
const most = totals.frameFunctions;
|
|
43174
|
+
const why = `${most} is kept below the game's debugger queue (network/limits/debugger/max_queued_messages), which drops the message naming a function when one frame names more than it holds, and a function whose name is dropped is never named afterwards.`;
|
|
43175
|
+
if (most !== undefined && totals.complete && totals.totalCapped === true) {
|
|
43176
|
+
notes.push(`The engine's totals carried the most functions the profiler asks for, ${most}, so the functions with the least time are not in them: ${why}`);
|
|
43177
|
+
} else if (most !== undefined && !totals.complete && (totals.cappedFrames ?? 0) > 0) {
|
|
43178
|
+
const capped = totals.cappedFrames ?? 0;
|
|
43179
|
+
notes.push(`${capped === totals.frames ? capped === 1 ? "The one frame" : `All ${capped} frames` : `${capped} of the ${totals.frames} frames`} carried the most functions the profiler asks for in one, ${most}, so the functions with the least time in ${capped === 1 ? "it" : "them"} are not counted there: ${why}`);
|
|
43180
|
+
}
|
|
43181
|
+
const unnamed = Object.entries(totals.functions).filter(([name, sum]) => name.startsWith(UNNAMED_PREFIX) && sum.calls > 0).length;
|
|
43182
|
+
if (unnamed > 0) {
|
|
43183
|
+
notes.push(`${unnamed === 1 ? "One function" : `${unnamed} functions`} arrived without ${unnamed === 1 ? "its name" : "their names"}, shown as ${UNNAMED_PREFIX} and the number the engine gave ${unnamed === 1 ? "it" : "them"}: the engine names a function once, as it first appears, and the game's debugger queue was full when that message was sent. They are script functions of the game; which ones cannot be recovered from this profile.`);
|
|
43184
|
+
}
|
|
43185
|
+
return notes;
|
|
43186
|
+
}
|
|
43139
43187
|
function callersAnswer(totals, asked) {
|
|
43140
43188
|
const found = callersOf(totals, asked.name, asked.scripts());
|
|
43141
43189
|
return {
|
|
@@ -46082,7 +46130,10 @@ class GodotServer {
|
|
|
46082
46130
|
const breakpoints = await this.dapHoldingBreakpointsOf(projectPath).reapplyBreakpoints();
|
|
46083
46131
|
this.logDebug(`Breakpoints were sent again after ${Date.now() - sendingFrom}ms`);
|
|
46084
46132
|
if (profiled) {
|
|
46085
|
-
const armed = await this.handleViaBridge("profile_start", {
|
|
46133
|
+
const armed = await this.handleViaBridge("profile_start", {
|
|
46134
|
+
arm: true,
|
|
46135
|
+
frameFunctions: profileFrameFunctions(projectPath)
|
|
46136
|
+
});
|
|
46086
46137
|
if (armed.isError === true) {
|
|
46087
46138
|
return this.profilerRefusal(armed);
|
|
46088
46139
|
}
|
|
@@ -46865,7 +46916,7 @@ class GodotServer {
|
|
|
46865
46916
|
...callersName === undefined ? {} : { callersOf: { name: callersName, scripts: () => projectScripts(projectPath) } }
|
|
46866
46917
|
};
|
|
46867
46918
|
if (run.throughEditor) {
|
|
46868
|
-
return await this.profileOfThePlayedGame(going, fromProject, limit);
|
|
46919
|
+
return await this.profileOfThePlayedGame(going, projectPath, fromProject, limit);
|
|
46869
46920
|
}
|
|
46870
46921
|
const record = run.projectPath === null ? null : readRunRecord(run.projectPath);
|
|
46871
46922
|
const path = record !== null && record.pid === run.pid ? record.profile : undefined;
|
|
@@ -46887,7 +46938,7 @@ class GodotServer {
|
|
|
46887
46938
|
...fromProject
|
|
46888
46939
|
}, limit));
|
|
46889
46940
|
}
|
|
46890
|
-
async profileOfThePlayedGame(going, fromProject, limit) {
|
|
46941
|
+
async profileOfThePlayedGame(going, projectPath, fromProject, limit) {
|
|
46891
46942
|
const read = await this.handleViaBridge("profile_read", {});
|
|
46892
46943
|
if (read.isError === true) {
|
|
46893
46944
|
return this.profilerRefusal(read);
|
|
@@ -46899,7 +46950,9 @@ class GodotServer {
|
|
|
46899
46950
|
"editor_run start with profile: true plays it again with the profiler on from its start"
|
|
46900
46951
|
]);
|
|
46901
46952
|
}
|
|
46902
|
-
const started = await this.handleViaBridge("profile_start", {
|
|
46953
|
+
const started = await this.handleViaBridge("profile_start", {
|
|
46954
|
+
frameFunctions: profileFrameFunctions(projectPath)
|
|
46955
|
+
});
|
|
46903
46956
|
if (started.isError === true) {
|
|
46904
46957
|
return this.profilerRefusal(started);
|
|
46905
46958
|
}
|
|
@@ -46922,6 +46975,7 @@ class GodotServer {
|
|
|
46922
46975
|
frames: readNonNegativeNumber(held, "frames") ?? 0,
|
|
46923
46976
|
complete: readBoolean(held, "complete") === true,
|
|
46924
46977
|
unreadable: 0,
|
|
46978
|
+
...playedCap(held),
|
|
46925
46979
|
functions: Object.fromEntries(Object.entries(functions).map(([signature, sum]) => {
|
|
46926
46980
|
const [calls, self, total] = Array.isArray(sum) ? sum : [];
|
|
46927
46981
|
return [
|
|
@@ -12,9 +12,10 @@ extends Node
|
|
|
12
12
|
|
|
13
13
|
const Read = preload("../reading.gd")
|
|
14
14
|
|
|
15
|
-
## The
|
|
16
|
-
##
|
|
17
|
-
|
|
15
|
+
## The most functions a frame may carry when the server does not say. The engine names a function
|
|
16
|
+
## once, as it first appears, and the game's debugger queue drops those messages past its size, so
|
|
17
|
+
## the server keeps this below the queue the project sets; this is that for the engine's default.
|
|
18
|
+
const FRAME_FUNCTIONS: int = 1536
|
|
18
19
|
const DEBUGGER_CLASS: String = "ScriptEditorDebugger"
|
|
19
20
|
|
|
20
21
|
|
|
@@ -44,6 +45,10 @@ var _started_msec: int = 0
|
|
|
44
45
|
var _last_frame_msec: int = -1
|
|
45
46
|
var _frames: int = 0
|
|
46
47
|
var _complete: bool = false
|
|
48
|
+
var _frame_functions: int = FRAME_FUNCTIONS
|
|
49
|
+
## Frames that carried as many functions as were asked for, and whether the totals did.
|
|
50
|
+
var _capped_frames: int = 0
|
|
51
|
+
var _total_capped: bool = false
|
|
47
52
|
var _names: Dictionary[int, String] = {}
|
|
48
53
|
## Per signature: calls, self seconds, total seconds.
|
|
49
54
|
var _totals: Dictionary[String, PackedFloat64Array] = {}
|
|
@@ -81,6 +86,8 @@ func profile_start(args: Dictionary) -> Dictionary:
|
|
|
81
86
|
),
|
|
82
87
|
}
|
|
83
88
|
_reset()
|
|
89
|
+
var asked: int = Read.as_int(args.get("frameFunctions", FRAME_FUNCTIONS))
|
|
90
|
+
_frame_functions = asked if asked > 0 else FRAME_FUNCTIONS
|
|
84
91
|
if Read.as_bool(args.get("arm", false)):
|
|
85
92
|
_armed = true
|
|
86
93
|
return {"ok": true, "started": false, "armed": true, "session": -1}
|
|
@@ -104,6 +111,9 @@ func profile_read(_args: Dictionary) -> Dictionary:
|
|
|
104
111
|
"playing": EditorInterface.is_playing_scene(),
|
|
105
112
|
"frames": _frames,
|
|
106
113
|
"complete": _complete,
|
|
114
|
+
"frameFunctions": _frame_functions,
|
|
115
|
+
"cappedFrames": _capped_frames,
|
|
116
|
+
"totalCapped": _total_capped,
|
|
107
117
|
"coveredMs": _last_frame_msec - _started_msec if _last_frame_msec >= 0 else 0,
|
|
108
118
|
"functions": functions,
|
|
109
119
|
}
|
|
@@ -114,6 +124,8 @@ func _reset() -> void:
|
|
|
114
124
|
_session = -1
|
|
115
125
|
_frames = 0
|
|
116
126
|
_complete = false
|
|
127
|
+
_capped_frames = 0
|
|
128
|
+
_total_capped = false
|
|
117
129
|
_last_frame_msec = -1
|
|
118
130
|
_names.clear()
|
|
119
131
|
_totals.clear()
|
|
@@ -133,7 +145,7 @@ func _switch_on(session_id: int) -> void:
|
|
|
133
145
|
_session = session_id
|
|
134
146
|
_started_msec = Time.get_ticks_msec()
|
|
135
147
|
var session: EditorDebuggerSession = _sessions.get_session(session_id)
|
|
136
|
-
session.toggle_profiler("servers", true, [
|
|
148
|
+
session.toggle_profiler("servers", true, [_frame_functions, false])
|
|
137
149
|
|
|
138
150
|
|
|
139
151
|
## A play that was asked to be profiled is switched on as it starts. Any other play drops what is
|
|
@@ -170,37 +182,42 @@ func _on_debug_data(message: String, data: Array, session_id: int) -> void:
|
|
|
170
182
|
if message == "servers:function_signature" and data.size() >= 2:
|
|
171
183
|
_names[Read.as_int(data[1])] = str(data[0])
|
|
172
184
|
elif message == "servers:profile_frame":
|
|
173
|
-
|
|
185
|
+
var carried: int = _add(data, _totals)
|
|
186
|
+
if carried >= 0:
|
|
174
187
|
_frames += 1
|
|
188
|
+
_capped_frames += 1 if carried >= _frame_functions else 0
|
|
175
189
|
_last_frame_msec = Time.get_ticks_msec()
|
|
176
190
|
elif message == "servers:profile_total":
|
|
177
191
|
# The engine's own count since the profiler started, so it replaces the frames' sum.
|
|
178
192
|
var whole: Dictionary[String, PackedFloat64Array] = {}
|
|
179
|
-
|
|
193
|
+
var carried: int = _add(data, whole)
|
|
194
|
+
if carried >= 0:
|
|
180
195
|
_totals = whole
|
|
181
196
|
_complete = true
|
|
197
|
+
_total_capped = carried >= _frame_functions
|
|
182
198
|
_last_frame_msec = Time.get_ticks_msec()
|
|
183
199
|
|
|
184
200
|
|
|
185
201
|
## Adds one frame's functions into [param into], reading past the servers' section to reach them:
|
|
186
202
|
## 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]) ->
|
|
203
|
+
## numbers per function. Answers how many functions it carried, or -1 for a frame it cannot read.
|
|
204
|
+
func _add(data: Array, into: Dictionary[String, PackedFloat64Array]) -> int:
|
|
189
205
|
var at: int = 6
|
|
190
206
|
if data.size() <= at:
|
|
191
|
-
return
|
|
207
|
+
return -1
|
|
192
208
|
var servers: int = Read.as_int(data[at])
|
|
193
209
|
at += 1
|
|
194
210
|
for _server: int in servers:
|
|
195
211
|
if data.size() <= at + 1:
|
|
196
|
-
return
|
|
212
|
+
return -1
|
|
197
213
|
at += 2 + Read.as_int(data[at + 1])
|
|
198
214
|
if data.size() <= at:
|
|
199
|
-
return
|
|
215
|
+
return -1
|
|
200
216
|
var values: int = Read.as_int(data[at])
|
|
201
217
|
at += 1
|
|
202
218
|
if values % 5 != 0 or data.size() < at + values:
|
|
203
|
-
return
|
|
219
|
+
return -1
|
|
220
|
+
var carried: int = 0
|
|
204
221
|
for index: int in range(at, at + values, 5):
|
|
205
222
|
var id: int = Read.as_int(data[index])
|
|
206
223
|
var signature: String = _names[id] if _names.has(id) else "#%d" % id
|
|
@@ -211,4 +228,5 @@ func _add(data: Array, into: Dictionary[String, PackedFloat64Array]) -> bool:
|
|
|
211
228
|
sum[1] += Read.as_float(data[index + 2])
|
|
212
229
|
sum[2] += Read.as_float(data[index + 3])
|
|
213
230
|
into[signature] = sum
|
|
214
|
-
|
|
231
|
+
carried += 1
|
|
232
|
+
return carried
|
|
@@ -172,6 +172,7 @@ func _report_errors() -> void:
|
|
|
172
172
|
func _process(_delta: float) -> void:
|
|
173
173
|
if not _enabled or _server == null:
|
|
174
174
|
return
|
|
175
|
+
_queries.note_frame(Time.get_ticks_usec())
|
|
175
176
|
|
|
176
177
|
if _server.is_connection_available():
|
|
177
178
|
var client: StreamPeerTCP = _server.take_connection()
|
|
@@ -28,8 +28,14 @@ const READ_LIMIT: int = 500
|
|
|
28
28
|
## and said nothing, and a caller told to raise a number that stays put concludes the screen moved.
|
|
29
29
|
const LIMIT_CEILING: int = 5000
|
|
30
30
|
|
|
31
|
+
## How many frames the frame time is the mean of: two seconds at sixty frames a second.
|
|
32
|
+
const FRAMES_TIMED: int = 120
|
|
33
|
+
|
|
31
34
|
var _host: Node
|
|
32
35
|
var _values: Values
|
|
36
|
+
## How long each of the last frames took, oldest first, in microseconds, as the game timed them.
|
|
37
|
+
var _frame_usec: PackedInt64Array = PackedInt64Array()
|
|
38
|
+
var _last_frame_usec: int = 0
|
|
33
39
|
|
|
34
40
|
|
|
35
41
|
## The host is the autoload, which is how the tree is reached: it is not in one when the
|
|
@@ -649,12 +655,60 @@ func get_property(params: Dictionary) -> Dictionary:
|
|
|
649
655
|
}
|
|
650
656
|
|
|
651
657
|
|
|
658
|
+
## Called by the autoload once a frame, whatever else it does that frame, with the time it was called.
|
|
659
|
+
##
|
|
660
|
+
## The engine's own monitors do not follow the frame: TIME_PROCESS and the frame rate are set once a
|
|
661
|
+
## second, the first to the slowest process step of that second, so a game whose frames took 16.7ms
|
|
662
|
+
## read 45ms for a second at a time, and a change to the game showed only on the next second's
|
|
663
|
+
## worst frame. The time between two of these calls is the frame as it passed, physics and the wait
|
|
664
|
+
## for the display included.
|
|
665
|
+
func note_frame(now_usec: int) -> void:
|
|
666
|
+
if _last_frame_usec > 0:
|
|
667
|
+
var _grew: bool = _frame_usec.push_back(now_usec - _last_frame_usec)
|
|
668
|
+
if _frame_usec.size() > FRAMES_TIMED:
|
|
669
|
+
_frame_usec.remove_at(0)
|
|
670
|
+
_last_frame_usec = now_usec
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
## Whether the frame waits for the display, which sets its floor: a game with vsync on cannot
|
|
674
|
+
## finish a frame sooner than the display refreshes, however little it does.
|
|
675
|
+
static func _vsync() -> String:
|
|
676
|
+
if DisplayServer.get_name() == "headless":
|
|
677
|
+
return "none, headless"
|
|
678
|
+
match DisplayServer.window_get_vsync_mode():
|
|
679
|
+
DisplayServer.VSYNC_DISABLED:
|
|
680
|
+
return "disabled"
|
|
681
|
+
DisplayServer.VSYNC_ADAPTIVE:
|
|
682
|
+
return "adaptive"
|
|
683
|
+
DisplayServer.VSYNC_MAILBOX:
|
|
684
|
+
return "mailbox"
|
|
685
|
+
_:
|
|
686
|
+
return "enabled"
|
|
687
|
+
|
|
688
|
+
|
|
652
689
|
func get_metrics(params: Dictionary) -> Dictionary:
|
|
653
690
|
var metrics: Array = params.get("metrics", [])
|
|
691
|
+
var timed: int = _frame_usec.size()
|
|
692
|
+
var total_usec: int = 0
|
|
693
|
+
var slowest_usec: int = 0
|
|
694
|
+
for one: int in _frame_usec:
|
|
695
|
+
total_usec += one
|
|
696
|
+
slowest_usec = maxi(slowest_usec, one)
|
|
697
|
+
# Null rather than zero before a frame has been timed: no frame took no time.
|
|
698
|
+
var frame_time: Variant = null
|
|
699
|
+
var frame_time_max: Variant = null
|
|
700
|
+
if timed > 0:
|
|
701
|
+
frame_time = float(total_usec) / timed / 1000000.0
|
|
702
|
+
frame_time_max = slowest_usec / 1000000.0
|
|
654
703
|
var all: Dictionary = {
|
|
655
704
|
"fps": Engine.get_frames_per_second(),
|
|
656
|
-
"frame_time":
|
|
705
|
+
"frame_time": frame_time,
|
|
706
|
+
"frame_time_max": frame_time_max,
|
|
707
|
+
"frames_timed": timed,
|
|
708
|
+
"process_time_max": Performance.get_monitor(Performance.TIME_PROCESS),
|
|
657
709
|
"physics_time": Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS),
|
|
710
|
+
"vsync": _vsync(),
|
|
711
|
+
"max_fps": Engine.max_fps,
|
|
658
712
|
"memory_static": Performance.get_monitor(Performance.MEMORY_STATIC),
|
|
659
713
|
"memory_static_max": Performance.get_monitor(Performance.MEMORY_STATIC_MAX),
|
|
660
714
|
"object_count": Performance.get_monitor(Performance.OBJECT_COUNT),
|
package/build/index.js
CHANGED
|
@@ -33101,6 +33101,17 @@ function readProfileFile(path) {
|
|
|
33101
33101
|
return null;
|
|
33102
33102
|
}
|
|
33103
33103
|
}
|
|
33104
|
+
var QUEUE_SETTING = "limits/debugger/max_queued_messages";
|
|
33105
|
+
var QUEUE_DEFAULT = 2048;
|
|
33106
|
+
var QUEUE_HEADROOM = 512;
|
|
33107
|
+
function frameFunctionsFor(queued) {
|
|
33108
|
+
return Math.max(64, queued - QUEUE_HEADROOM);
|
|
33109
|
+
}
|
|
33110
|
+
function queuedMessagesOf(projectGodot) {
|
|
33111
|
+
const set = projectGodot === null ? undefined : parseProjectGodot(projectGodot)["network"]?.[QUEUE_SETTING];
|
|
33112
|
+
return typeof set === "number" && Number.isInteger(set) && set > 0 ? set : QUEUE_DEFAULT;
|
|
33113
|
+
}
|
|
33114
|
+
var UNNAMED_PREFIX = "#";
|
|
33104
33115
|
function signatureParts(signature) {
|
|
33105
33116
|
const parts = signature.split("::");
|
|
33106
33117
|
if (parts.length < 3) {
|
|
@@ -34406,7 +34417,10 @@ var TOOL_SPECS = [
|
|
|
34406
34417
|
summary: "what one property reads on a node, or through the objects it holds, refusing a name nothing along the way has",
|
|
34407
34418
|
requires: ["nodePath", "property"]
|
|
34408
34419
|
},
|
|
34409
|
-
metrics: {
|
|
34420
|
+
metrics: {
|
|
34421
|
+
summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game itself, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. The memory, object and render counts are read as asked",
|
|
34422
|
+
requires: []
|
|
34423
|
+
}
|
|
34410
34424
|
},
|
|
34411
34425
|
defaultOperation: "tree"
|
|
34412
34426
|
},
|
|
@@ -35562,6 +35576,26 @@ function profileOnStart(profiled, through, runtimeAddon) {
|
|
|
35562
35576
|
};
|
|
35563
35577
|
}
|
|
35564
35578
|
var PROFILER_ON_MS = 3000;
|
|
35579
|
+
function profileFrameFunctions(projectPath) {
|
|
35580
|
+
let projectGodot = null;
|
|
35581
|
+
if (projectPath !== null) {
|
|
35582
|
+
try {
|
|
35583
|
+
projectGodot = readFileSync21(join22(projectPath, "project.godot"), "utf8");
|
|
35584
|
+
} catch {}
|
|
35585
|
+
}
|
|
35586
|
+
return frameFunctionsFor(queuedMessagesOf(projectGodot));
|
|
35587
|
+
}
|
|
35588
|
+
function playedCap(held) {
|
|
35589
|
+
const frameFunctions = readPositiveNumber(held, "frameFunctions");
|
|
35590
|
+
if (frameFunctions === undefined) {
|
|
35591
|
+
return {};
|
|
35592
|
+
}
|
|
35593
|
+
return {
|
|
35594
|
+
frameFunctions,
|
|
35595
|
+
cappedFrames: readNonNegativeNumber(held, "cappedFrames") ?? 0,
|
|
35596
|
+
totalCapped: readBoolean(held, "totalCapped") === true
|
|
35597
|
+
};
|
|
35598
|
+
}
|
|
35565
35599
|
var CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too. Each place carries the profile's numbers for the function it is written in.";
|
|
35566
35600
|
function projectSourceOf(projectPath) {
|
|
35567
35601
|
const read = new Map;
|
|
@@ -35609,6 +35643,7 @@ function profileAnswer(reading, limit) {
|
|
|
35609
35643
|
} else {
|
|
35610
35644
|
notes.push(`These are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} that arrived, summed, without the engine's own totals, so what the game did after the last of them is not in it: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${addonNote}`);
|
|
35611
35645
|
}
|
|
35646
|
+
notes.push(...cappedNotes(totals));
|
|
35612
35647
|
if (totals.unreadable > 0) {
|
|
35613
35648
|
notes.push(`${totals.unreadable} message${totals.unreadable === 1 ? "" : "s"} from the game could not be read, so the totals have that many gaps.`);
|
|
35614
35649
|
}
|
|
@@ -35627,6 +35662,22 @@ function profileAnswer(reading, limit) {
|
|
|
35627
35662
|
note: notes.length > 0 ? notes.join(" ") : undefined
|
|
35628
35663
|
};
|
|
35629
35664
|
}
|
|
35665
|
+
function cappedNotes(totals) {
|
|
35666
|
+
const notes = [];
|
|
35667
|
+
const most = totals.frameFunctions;
|
|
35668
|
+
const why = `${most} is kept below the game's debugger queue (network/limits/debugger/max_queued_messages), which drops the message naming a function when one frame names more than it holds, and a function whose name is dropped is never named afterwards.`;
|
|
35669
|
+
if (most !== undefined && totals.complete && totals.totalCapped === true) {
|
|
35670
|
+
notes.push(`The engine's totals carried the most functions the profiler asks for, ${most}, so the functions with the least time are not in them: ${why}`);
|
|
35671
|
+
} else if (most !== undefined && !totals.complete && (totals.cappedFrames ?? 0) > 0) {
|
|
35672
|
+
const capped = totals.cappedFrames ?? 0;
|
|
35673
|
+
notes.push(`${capped === totals.frames ? capped === 1 ? "The one frame" : `All ${capped} frames` : `${capped} of the ${totals.frames} frames`} carried the most functions the profiler asks for in one, ${most}, so the functions with the least time in ${capped === 1 ? "it" : "them"} are not counted there: ${why}`);
|
|
35674
|
+
}
|
|
35675
|
+
const unnamed = Object.entries(totals.functions).filter(([name, sum]) => name.startsWith(UNNAMED_PREFIX) && sum.calls > 0).length;
|
|
35676
|
+
if (unnamed > 0) {
|
|
35677
|
+
notes.push(`${unnamed === 1 ? "One function" : `${unnamed} functions`} arrived without ${unnamed === 1 ? "its name" : "their names"}, shown as ${UNNAMED_PREFIX} and the number the engine gave ${unnamed === 1 ? "it" : "them"}: the engine names a function once, as it first appears, and the game's debugger queue was full when that message was sent. They are script functions of the game; which ones cannot be recovered from this profile.`);
|
|
35678
|
+
}
|
|
35679
|
+
return notes;
|
|
35680
|
+
}
|
|
35630
35681
|
function callersAnswer(totals, asked) {
|
|
35631
35682
|
const found = callersOf(totals, asked.name, asked.scripts());
|
|
35632
35683
|
return {
|
|
@@ -38576,7 +38627,10 @@ class GodotServer {
|
|
|
38576
38627
|
const breakpoints = await this.dapHoldingBreakpointsOf(projectPath).reapplyBreakpoints();
|
|
38577
38628
|
this.logDebug(`Breakpoints were sent again after ${Date.now() - sendingFrom}ms`);
|
|
38578
38629
|
if (profiled) {
|
|
38579
|
-
const armed = await this.handleViaBridge("profile_start", {
|
|
38630
|
+
const armed = await this.handleViaBridge("profile_start", {
|
|
38631
|
+
arm: true,
|
|
38632
|
+
frameFunctions: profileFrameFunctions(projectPath)
|
|
38633
|
+
});
|
|
38580
38634
|
if (armed.isError === true) {
|
|
38581
38635
|
return this.profilerRefusal(armed);
|
|
38582
38636
|
}
|
|
@@ -39359,7 +39413,7 @@ class GodotServer {
|
|
|
39359
39413
|
...callersName === undefined ? {} : { callersOf: { name: callersName, scripts: () => projectScripts(projectPath) } }
|
|
39360
39414
|
};
|
|
39361
39415
|
if (run.throughEditor) {
|
|
39362
|
-
return await this.profileOfThePlayedGame(going, fromProject, limit);
|
|
39416
|
+
return await this.profileOfThePlayedGame(going, projectPath, fromProject, limit);
|
|
39363
39417
|
}
|
|
39364
39418
|
const record = run.projectPath === null ? null : readRunRecord(run.projectPath);
|
|
39365
39419
|
const path = record !== null && record.pid === run.pid ? record.profile : undefined;
|
|
@@ -39381,7 +39435,7 @@ class GodotServer {
|
|
|
39381
39435
|
...fromProject
|
|
39382
39436
|
}, limit));
|
|
39383
39437
|
}
|
|
39384
|
-
async profileOfThePlayedGame(going, fromProject, limit) {
|
|
39438
|
+
async profileOfThePlayedGame(going, projectPath, fromProject, limit) {
|
|
39385
39439
|
const read = await this.handleViaBridge("profile_read", {});
|
|
39386
39440
|
if (read.isError === true) {
|
|
39387
39441
|
return this.profilerRefusal(read);
|
|
@@ -39393,7 +39447,9 @@ class GodotServer {
|
|
|
39393
39447
|
"editor_run start with profile: true plays it again with the profiler on from its start"
|
|
39394
39448
|
]);
|
|
39395
39449
|
}
|
|
39396
|
-
const started = await this.handleViaBridge("profile_start", {
|
|
39450
|
+
const started = await this.handleViaBridge("profile_start", {
|
|
39451
|
+
frameFunctions: profileFrameFunctions(projectPath)
|
|
39452
|
+
});
|
|
39397
39453
|
if (started.isError === true) {
|
|
39398
39454
|
return this.profilerRefusal(started);
|
|
39399
39455
|
}
|
|
@@ -39416,6 +39472,7 @@ class GodotServer {
|
|
|
39416
39472
|
frames: readNonNegativeNumber(held, "frames") ?? 0,
|
|
39417
39473
|
complete: readBoolean(held, "complete") === true,
|
|
39418
39474
|
unreadable: 0,
|
|
39475
|
+
...playedCap(held),
|
|
39419
39476
|
functions: Object.fromEntries(Object.entries(functions).map(([signature, sum]) => {
|
|
39420
39477
|
const [calls, self, total] = Array.isArray(sum) ? sum : [];
|
|
39421
39478
|
return [
|