gdharness 1.2.6 → 1.2.8
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
CHANGED
|
@@ -42623,7 +42623,8 @@ function profiledFunctions(totals, limit, sourceOf = () => null) {
|
|
|
42623
42623
|
counted: rows.length,
|
|
42624
42624
|
superCallers: rows.filter((row) => row.selfIncludesSuper === true).length,
|
|
42625
42625
|
perCallUs,
|
|
42626
|
-
overheadDominated: rows.filter((row) => row.overheadMs !== undefined).length
|
|
42626
|
+
overheadDominated: rows.filter((row) => row.overheadMs !== undefined).length,
|
|
42627
|
+
overheadNamed: rows.filter((row) => row.overheadMs !== undefined).map((row) => row.function)
|
|
42627
42628
|
};
|
|
42628
42629
|
}
|
|
42629
42630
|
function isCalibration(signature) {
|
|
@@ -42721,7 +42722,7 @@ function callersOf(totals, name, scripts) {
|
|
|
42721
42722
|
sites.sort((a, b) => (b.withinTotalMs ?? -1) - (a.withinTotalMs ?? -1) || a.script.localeCompare(b.script) || a.line - b.line);
|
|
42722
42723
|
return { target, sites: sites.slice(0, CALL_SITES), omitted: Math.max(0, sites.length - CALL_SITES) };
|
|
42723
42724
|
}
|
|
42724
|
-
var QUEUE_SETTING = "limits/debugger/max_queued_messages", QUEUE_DEFAULT = 2048, QUEUE_HEADROOM = 512, UNNAMED_PREFIX = "#", ms = (seconds) => Math.round(seconds * 1e6) / 1000, RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd", PROFILER_PROBE = "_profiler_probe", PROFILER_CALIBRATOR = "_calibrate_the_profiler", CALL_SITES = 40;
|
|
42725
|
+
var QUEUE_SETTING = "limits/debugger/max_queued_messages", QUEUE_DEFAULT = 2048, QUEUE_HEADROOM = 512, UNNAMED_PREFIX = "#", PROFILE_FROM_START = "GDHARNESS_PROFILE_FROM_START", ms = (seconds) => Math.round(seconds * 1e6) / 1000, RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd", PROFILER_PROBE = "_profiler_probe", PROFILER_CALIBRATOR = "_calibrate_the_profiler", CALL_SITES = 40;
|
|
42725
42726
|
var init_script_profile = __esm(() => {
|
|
42726
42727
|
init_gdscript_source();
|
|
42727
42728
|
init_godot_variant();
|
|
@@ -43542,7 +43543,7 @@ function profileAnswer(reading, limit) {
|
|
|
43542
43543
|
notes.push(`${table.superCallers === 1 ? "One function calls" : `${table.superCallers} functions call`} through super, marked selfIncludesSuper: the engine takes every other call out of the caller's self time, and not a super call, so ${table.superCallers === 1 ? "its" : "their"} selfMs holds the overridden function's time, which that function's own row counts again. Self time therefore cannot be added along an override chain, and scriptMs counts the time under a super call once more for each level above it.`);
|
|
43543
43544
|
}
|
|
43544
43545
|
if (table.overheadDominated > 0 && table.perCallUs !== null) {
|
|
43545
|
-
notes.push(`The profiler costs about ${table.perCallUs}µs a call in this run, measured on an empty function the runtime addon calls as it goes on, and that time lands in the self time of the function called. ${table.overheadDominated === 1 ? "One function has" : `${table.overheadDominated} functions have`} at least half ${table.overheadDominated === 1 ? "its" : "their"} selfMs in it, under overheadMs:
|
|
43546
|
+
notes.push(`The profiler costs about ${table.perCallUs}µs a call in this run, measured on an empty function the runtime addon calls as it goes on, and that time lands in the self time of the function called. ${table.overheadDominated === 1 ? "One function has" : `${table.overheadDominated} functions have`} at least half ${table.overheadDominated === 1 ? "its" : "their"} selfMs in it, under overheadMs: ${namedFew(table.overheadNamed)}. That much of ${table.overheadDominated === 1 ? "its" : "their"} time is the profiler's and goes once the profiler is off, whether the function is inlined or not. What inlining saves is the call itself, which costs something without the profiler too, not the work inside it.`);
|
|
43546
43547
|
}
|
|
43547
43548
|
notes.push(...cappedNotes(totals));
|
|
43548
43549
|
if (totals.unreadable > 0) {
|
|
@@ -43564,12 +43565,21 @@ function profileAnswer(reading, limit) {
|
|
|
43564
43565
|
note: notes.length > 0 ? notes.join(" ") : undefined
|
|
43565
43566
|
};
|
|
43566
43567
|
}
|
|
43568
|
+
function namedFew(names) {
|
|
43569
|
+
const shown = names.slice(0, 5);
|
|
43570
|
+
const more = names.length - shown.length;
|
|
43571
|
+
if (more > 0) {
|
|
43572
|
+
return `${shown.join(", ")} and ${more} more`;
|
|
43573
|
+
}
|
|
43574
|
+
return shown.length > 1 ? `${shown.slice(0, -1).join(", ")} and ${shown.at(-1) ?? ""}` : shown[0] ?? "";
|
|
43575
|
+
}
|
|
43567
43576
|
function cappedNotes(totals) {
|
|
43568
43577
|
const notes = [];
|
|
43569
43578
|
const most = totals.frameFunctions;
|
|
43570
43579
|
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.`;
|
|
43571
43580
|
if (most !== undefined && totals.complete && totals.totalCapped === true) {
|
|
43572
|
-
|
|
43581
|
+
const leastKept = Math.min(...Object.values(totals.functions).map((one) => one.totalSeconds));
|
|
43582
|
+
notes.push(leastKept > 0 ? `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}` : `The engine's totals carried the most functions the profiler asks for, ${most}, and the last of them took no measurable time, so neither did any left out: the time is all here, and only a function too quick for the clock can be missing, with its calls. The engine lists every function it has compiled, run or not, and sends them busiest first. ${why}`);
|
|
43573
43583
|
} else if (most !== undefined && !totals.complete && (totals.cappedFrames ?? 0) > 0) {
|
|
43574
43584
|
const capped = totals.cappedFrames ?? 0;
|
|
43575
43585
|
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}`);
|
|
@@ -46929,10 +46939,11 @@ class GodotServer {
|
|
|
46929
46939
|
const startedAt = Date.now();
|
|
46930
46940
|
const transcript = openTranscript(startedAt);
|
|
46931
46941
|
closeSync2(transcript.fd);
|
|
46942
|
+
const gameEnv = profiled ? { ...env ?? process.env, [PROFILE_FROM_START]: "1" } : env;
|
|
46932
46943
|
const launched = await launchOutsideTheTree({
|
|
46933
46944
|
command: godotPath,
|
|
46934
46945
|
args: cmdArgs,
|
|
46935
|
-
...
|
|
46946
|
+
...gameEnv === undefined ? {} : { env: gameEnv },
|
|
46936
46947
|
run: {
|
|
46937
46948
|
transcript: transcript.path,
|
|
46938
46949
|
startedAt,
|
|
@@ -22,6 +22,9 @@ const DEBUGGER_CLASS: String = "ScriptEditorDebugger"
|
|
|
22
22
|
const PROFILE_CAPTURE: String = "gdharness"
|
|
23
23
|
const PROFILE_SENT: String = "gdharness:profile_sent"
|
|
24
24
|
const PROFILE_RECEIVED: String = "gdharness:profile_received"
|
|
25
|
+
## Set in the environment a played game inherits, when the play is to be profiled from its start.
|
|
26
|
+
## Kept in step with the runtime addon's runtime_autoload.gd.
|
|
27
|
+
const PROFILE_FROM_START: String = "GDHARNESS_PROFILE_FROM_START"
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
## The editor's debugger sessions, numbered as the editor numbers its debugger tabs.
|
|
@@ -106,6 +109,10 @@ func profile_start(args: Dictionary) -> Dictionary:
|
|
|
106
109
|
_frame_functions = asked if asked > 0 else FRAME_FUNCTIONS
|
|
107
110
|
if Read.as_bool(args.get("arm", false)):
|
|
108
111
|
_armed = true
|
|
112
|
+
# The game the editor starts next inherits the editor's environment, and this tells its
|
|
113
|
+
# runtime addon to hold its start until the profiler is on, which the session's start
|
|
114
|
+
# switches on with a message the game reads only on a later poll.
|
|
115
|
+
OS.set_environment(PROFILE_FROM_START, "1")
|
|
109
116
|
return {"ok": true, "started": false, "armed": true, "session": -1}
|
|
110
117
|
var active: int = _active_session()
|
|
111
118
|
if active < 0:
|
|
@@ -137,6 +144,7 @@ func profile_read(_args: Dictionary) -> Dictionary:
|
|
|
137
144
|
|
|
138
145
|
func _reset() -> void:
|
|
139
146
|
_armed = false
|
|
147
|
+
OS.unset_environment(PROFILE_FROM_START)
|
|
140
148
|
_session = -1
|
|
141
149
|
_frames = 0
|
|
142
150
|
_complete = false
|
|
@@ -172,6 +180,8 @@ func _on_session_started(session_id: int) -> void:
|
|
|
172
180
|
_reset()
|
|
173
181
|
return
|
|
174
182
|
_armed = false
|
|
183
|
+
# The game has started with it by now, and a later play must not.
|
|
184
|
+
OS.unset_environment(PROFILE_FROM_START)
|
|
175
185
|
if _hook() > 0:
|
|
176
186
|
_switch_on(session_id)
|
|
177
187
|
|
|
@@ -47,6 +47,13 @@ const PROFILER_PROBE_CALLS: int = 10000
|
|
|
47
47
|
## on every 2048th call and only counts the others.
|
|
48
48
|
const POLLS_PER_READ: int = 2048
|
|
49
49
|
|
|
50
|
+
## Set in a game's environment by whatever starts it to be profiled from its start: the server for
|
|
51
|
+
## a run it spawns, the editor for a play. Kept in step with `PROFILE_FROM_START` in the server.
|
|
52
|
+
const PROFILE_FROM_START: String = "GDHARNESS_PROFILE_FROM_START"
|
|
53
|
+
|
|
54
|
+
## How long such a game waits for its debugger to switch the profiler on before carrying on.
|
|
55
|
+
const PROFILER_WAIT_MS: int = 5000
|
|
56
|
+
|
|
50
57
|
## How the engine is told to run a script instead of the game: `godot -s thing.gd`.
|
|
51
58
|
const SCRIPT_FLAGS: PackedStringArray = ["-s", "--script"]
|
|
52
59
|
|
|
@@ -134,6 +141,37 @@ func _init() -> void:
|
|
|
134
141
|
"wait_signal": _waits.wait_signal,
|
|
135
142
|
"wait_until": _waits.wait_until,
|
|
136
143
|
}
|
|
144
|
+
_wait_for_the_profiler()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
## Holds the game's start until the profiler is on, when whatever started the game asked for a
|
|
148
|
+
## profile from its start. The debugger switches the profiler on with a message the game reads only
|
|
149
|
+
## on a poll, which comes every [constant POLLS_PER_READ] lines of script, so the main scene's _ready
|
|
150
|
+
## could run first: a game played on a macOS runner counted 25 calls of a function called 26 times,
|
|
151
|
+
## the first of them from _ready. Run at the autoload's construction, before the main scene loads.
|
|
152
|
+
func _wait_for_the_profiler() -> void:
|
|
153
|
+
if OS.get_environment(PROFILE_FROM_START).is_empty():
|
|
154
|
+
return
|
|
155
|
+
# Read once: a process this game starts inherits its environment and must not wait as well.
|
|
156
|
+
OS.unset_environment(PROFILE_FROM_START)
|
|
157
|
+
if not EngineDebugger.is_active():
|
|
158
|
+
return
|
|
159
|
+
var deadline: int = Time.get_ticks_msec() + PROFILER_WAIT_MS
|
|
160
|
+
while not EngineDebugger.is_profiling("servers"):
|
|
161
|
+
if Time.get_ticks_msec() >= deadline:
|
|
162
|
+
push_warning(
|
|
163
|
+
(
|
|
164
|
+
(
|
|
165
|
+
"gdharness: this game was started to be profiled from its start, and the profiler"
|
|
166
|
+
+ " was not on %d ms later, so what ran before it is not counted"
|
|
167
|
+
)
|
|
168
|
+
% PROFILER_WAIT_MS
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
return
|
|
172
|
+
for _poll: int in POLLS_PER_READ:
|
|
173
|
+
EngineDebugger.line_poll()
|
|
174
|
+
OS.delay_msec(1)
|
|
137
175
|
|
|
138
176
|
|
|
139
177
|
func _ready() -> void:
|
package/build/index.js
CHANGED
|
@@ -33393,6 +33393,7 @@ function queuedMessagesOf(projectGodot) {
|
|
|
33393
33393
|
return typeof set === "number" && Number.isInteger(set) && set > 0 ? set : QUEUE_DEFAULT;
|
|
33394
33394
|
}
|
|
33395
33395
|
var UNNAMED_PREFIX = "#";
|
|
33396
|
+
var PROFILE_FROM_START = "GDHARNESS_PROFILE_FROM_START";
|
|
33396
33397
|
function signatureParts(signature) {
|
|
33397
33398
|
const parts = signature.split("::");
|
|
33398
33399
|
if (parts.length < 3) {
|
|
@@ -33414,7 +33415,8 @@ function profiledFunctions(totals, limit, sourceOf = () => null) {
|
|
|
33414
33415
|
counted: rows.length,
|
|
33415
33416
|
superCallers: rows.filter((row) => row.selfIncludesSuper === true).length,
|
|
33416
33417
|
perCallUs,
|
|
33417
|
-
overheadDominated: rows.filter((row) => row.overheadMs !== undefined).length
|
|
33418
|
+
overheadDominated: rows.filter((row) => row.overheadMs !== undefined).length,
|
|
33419
|
+
overheadNamed: rows.filter((row) => row.overheadMs !== undefined).map((row) => row.function)
|
|
33418
33420
|
};
|
|
33419
33421
|
}
|
|
33420
33422
|
var RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd";
|
|
@@ -36036,7 +36038,7 @@ function profileAnswer(reading, limit) {
|
|
|
36036
36038
|
notes.push(`${table.superCallers === 1 ? "One function calls" : `${table.superCallers} functions call`} through super, marked selfIncludesSuper: the engine takes every other call out of the caller's self time, and not a super call, so ${table.superCallers === 1 ? "its" : "their"} selfMs holds the overridden function's time, which that function's own row counts again. Self time therefore cannot be added along an override chain, and scriptMs counts the time under a super call once more for each level above it.`);
|
|
36037
36039
|
}
|
|
36038
36040
|
if (table.overheadDominated > 0 && table.perCallUs !== null) {
|
|
36039
|
-
notes.push(`The profiler costs about ${table.perCallUs}µs a call in this run, measured on an empty function the runtime addon calls as it goes on, and that time lands in the self time of the function called. ${table.overheadDominated === 1 ? "One function has" : `${table.overheadDominated} functions have`} at least half ${table.overheadDominated === 1 ? "its" : "their"} selfMs in it, under overheadMs:
|
|
36041
|
+
notes.push(`The profiler costs about ${table.perCallUs}µs a call in this run, measured on an empty function the runtime addon calls as it goes on, and that time lands in the self time of the function called. ${table.overheadDominated === 1 ? "One function has" : `${table.overheadDominated} functions have`} at least half ${table.overheadDominated === 1 ? "its" : "their"} selfMs in it, under overheadMs: ${namedFew(table.overheadNamed)}. That much of ${table.overheadDominated === 1 ? "its" : "their"} time is the profiler's and goes once the profiler is off, whether the function is inlined or not. What inlining saves is the call itself, which costs something without the profiler too, not the work inside it.`);
|
|
36040
36042
|
}
|
|
36041
36043
|
notes.push(...cappedNotes(totals));
|
|
36042
36044
|
if (totals.unreadable > 0) {
|
|
@@ -36058,12 +36060,21 @@ function profileAnswer(reading, limit) {
|
|
|
36058
36060
|
note: notes.length > 0 ? notes.join(" ") : undefined
|
|
36059
36061
|
};
|
|
36060
36062
|
}
|
|
36063
|
+
function namedFew(names) {
|
|
36064
|
+
const shown = names.slice(0, 5);
|
|
36065
|
+
const more = names.length - shown.length;
|
|
36066
|
+
if (more > 0) {
|
|
36067
|
+
return `${shown.join(", ")} and ${more} more`;
|
|
36068
|
+
}
|
|
36069
|
+
return shown.length > 1 ? `${shown.slice(0, -1).join(", ")} and ${shown.at(-1) ?? ""}` : shown[0] ?? "";
|
|
36070
|
+
}
|
|
36061
36071
|
function cappedNotes(totals) {
|
|
36062
36072
|
const notes = [];
|
|
36063
36073
|
const most = totals.frameFunctions;
|
|
36064
36074
|
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.`;
|
|
36065
36075
|
if (most !== undefined && totals.complete && totals.totalCapped === true) {
|
|
36066
|
-
|
|
36076
|
+
const leastKept = Math.min(...Object.values(totals.functions).map((one) => one.totalSeconds));
|
|
36077
|
+
notes.push(leastKept > 0 ? `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}` : `The engine's totals carried the most functions the profiler asks for, ${most}, and the last of them took no measurable time, so neither did any left out: the time is all here, and only a function too quick for the clock can be missing, with its calls. The engine lists every function it has compiled, run or not, and sends them busiest first. ${why}`);
|
|
36067
36078
|
} else if (most !== undefined && !totals.complete && (totals.cappedFrames ?? 0) > 0) {
|
|
36068
36079
|
const capped = totals.cappedFrames ?? 0;
|
|
36069
36080
|
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}`);
|
|
@@ -39426,10 +39437,11 @@ class GodotServer {
|
|
|
39426
39437
|
const startedAt = Date.now();
|
|
39427
39438
|
const transcript = openTranscript(startedAt);
|
|
39428
39439
|
closeSync2(transcript.fd);
|
|
39440
|
+
const gameEnv = profiled ? { ...env ?? process.env, [PROFILE_FROM_START]: "1" } : env;
|
|
39429
39441
|
const launched = await launchOutsideTheTree({
|
|
39430
39442
|
command: godotPath,
|
|
39431
39443
|
args: cmdArgs,
|
|
39432
|
-
...
|
|
39444
|
+
...gameEnv === undefined ? {} : { env: gameEnv },
|
|
39433
39445
|
run: {
|
|
39434
39446
|
transcript: transcript.path,
|
|
39435
39447
|
startedAt,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|