gdharness 1.1.62 → 1.2.1
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 +78 -16
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +31 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +37 -5
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +93 -0
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +6 -1
- package/build/index.js +80 -15
- package/build/keeper.js +4 -2
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -18413,7 +18413,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18413
18413
|
},
|
|
18414
18414
|
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
|
|
18415
18415
|
profile: {
|
|
18416
|
-
summary: "where the run spends its time by GDScript function, while it runs or after it ended: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, sorted by self time, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. A run started with profile: true is profiled from its start. A game the editor plays that was not is profiled from the moment this is asked, and the answer says so; a run this server started without it is refused, since its debugger is fixed as it starts",
|
|
18416
|
+
summary: "where the run spends its time by GDScript function, while it runs or after it ended: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, sorted by self time, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. A run started with profile: true is profiled from its start. A game the editor plays that was not is profiled from the moment this is asked, and the answer says so; a run this server started without it is refused, since its debugger is fixed as it starts",
|
|
18417
18417
|
requires: []
|
|
18418
18418
|
}
|
|
18419
18419
|
},
|
|
@@ -18421,7 +18421,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18421
18421
|
},
|
|
18422
18422
|
{
|
|
18423
18423
|
name: "editor_output",
|
|
18424
|
-
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing.",
|
|
18424
|
+
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing. startedAt is when the run began and elapsedMs how long it has been going; for a run that is over, endedAt is when it ended and elapsedMs how long it ran, and both are left out when nobody saw the end, as for a game closed in the editor or a run picked up from a note written before ends were recorded.",
|
|
18425
18425
|
parameters: {
|
|
18426
18426
|
severity: {
|
|
18427
18427
|
type: "string",
|
|
@@ -18469,7 +18469,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18469
18469
|
},
|
|
18470
18470
|
{
|
|
18471
18471
|
name: "editor_status",
|
|
18472
|
-
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
|
|
18472
|
+
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
|
|
18473
18473
|
parameters: {},
|
|
18474
18474
|
requires: []
|
|
18475
18475
|
},
|
|
@@ -42202,7 +42202,8 @@ function recordAt(path) {
|
|
|
42202
42202
|
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
|
|
42203
42203
|
...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
|
|
42204
42204
|
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
42205
|
-
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {}
|
|
42205
|
+
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
|
|
42206
|
+
...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
|
|
42206
42207
|
};
|
|
42207
42208
|
}
|
|
42208
42209
|
async function runningAs(pid) {
|
|
@@ -42556,16 +42557,39 @@ function signatureParts(signature) {
|
|
|
42556
42557
|
return { script: parts.slice(0, -2).join("::"), line: Number.isFinite(line) ? line : 0, function: name };
|
|
42557
42558
|
}
|
|
42558
42559
|
function profiledFunctions(totals, limit, sourceOf = () => null) {
|
|
42559
|
-
const
|
|
42560
|
-
const
|
|
42560
|
+
const perCallUs = perCallCostOf(totals);
|
|
42561
|
+
const rows = rowsOf(totals, sourceOf).map((row) => withOverhead(row, perCallUs));
|
|
42562
|
+
const scriptMs = ms(Object.entries(totals.functions).reduce((sum, [signature, one]) => sum + (one.calls > 0 && !isCalibration(signature) ? one.selfSeconds : 0), 0));
|
|
42561
42563
|
return {
|
|
42562
42564
|
functions: rows.slice(0, limit).map((row) => placedLambda(row, sourceOf)),
|
|
42563
42565
|
omitted: Math.max(0, rows.length - limit),
|
|
42564
42566
|
scriptMs,
|
|
42565
42567
|
counted: rows.length,
|
|
42566
|
-
superCallers: rows.filter((row) => row.selfIncludesSuper === true).length
|
|
42568
|
+
superCallers: rows.filter((row) => row.selfIncludesSuper === true).length,
|
|
42569
|
+
perCallUs,
|
|
42570
|
+
overheadDominated: rows.filter((row) => row.overheadMs !== undefined).length
|
|
42567
42571
|
};
|
|
42568
42572
|
}
|
|
42573
|
+
function isCalibration(signature) {
|
|
42574
|
+
const parts = signatureParts(signature);
|
|
42575
|
+
return parts.script.endsWith(RUNTIME_SCRIPT) && (parts.function === PROFILER_PROBE || parts.function === PROFILER_CALIBRATOR);
|
|
42576
|
+
}
|
|
42577
|
+
function perCallCostOf(totals) {
|
|
42578
|
+
for (const [signature, sum] of Object.entries(totals.functions)) {
|
|
42579
|
+
const parts = signatureParts(signature);
|
|
42580
|
+
if (parts.script.endsWith(RUNTIME_SCRIPT) && parts.function === PROFILER_PROBE && sum.calls > 0) {
|
|
42581
|
+
return Math.round(sum.selfSeconds / sum.calls * 1e9) / 1000;
|
|
42582
|
+
}
|
|
42583
|
+
}
|
|
42584
|
+
return null;
|
|
42585
|
+
}
|
|
42586
|
+
function withOverhead(row, perCallUs) {
|
|
42587
|
+
if (perCallUs === null || row.selfMs <= 0) {
|
|
42588
|
+
return row;
|
|
42589
|
+
}
|
|
42590
|
+
const overheadMs = Math.round(row.calls * perCallUs) / 1000;
|
|
42591
|
+
return overheadMs >= row.selfMs / 2 ? { ...row, overheadMs: Math.min(overheadMs, row.selfMs) } : row;
|
|
42592
|
+
}
|
|
42569
42593
|
function rowsOf(totals, sourceOf) {
|
|
42570
42594
|
const codes = new Map;
|
|
42571
42595
|
const codeOfScript = (script) => {
|
|
@@ -42575,7 +42599,7 @@ function rowsOf(totals, sourceOf) {
|
|
|
42575
42599
|
}
|
|
42576
42600
|
return codes.get(script) ?? null;
|
|
42577
42601
|
};
|
|
42578
|
-
return Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
|
|
42602
|
+
return Object.entries(totals.functions).filter(([signature, sum]) => sum.calls > 0 && !isCalibration(signature)).map(([signature, sum]) => {
|
|
42579
42603
|
const parts = signatureParts(signature);
|
|
42580
42604
|
const code = codeOfScript(parts.script);
|
|
42581
42605
|
return {
|
|
@@ -42641,7 +42665,7 @@ function callersOf(totals, name, scripts) {
|
|
|
42641
42665
|
sites.sort((a, b) => (b.withinTotalMs ?? -1) - (a.withinTotalMs ?? -1) || a.script.localeCompare(b.script) || a.line - b.line);
|
|
42642
42666
|
return { target, sites: sites.slice(0, CALL_SITES), omitted: Math.max(0, sites.length - CALL_SITES) };
|
|
42643
42667
|
}
|
|
42644
|
-
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;
|
|
42668
|
+
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;
|
|
42645
42669
|
var init_script_profile = __esm(() => {
|
|
42646
42670
|
init_gdscript_source();
|
|
42647
42671
|
init_godot_variant();
|
|
@@ -43229,6 +43253,24 @@ function foldedScriptErrors(errors) {
|
|
|
43229
43253
|
function nonEmpty(lists) {
|
|
43230
43254
|
return Object.fromEntries(Object.entries(lists).filter(([, list]) => list.length > 0));
|
|
43231
43255
|
}
|
|
43256
|
+
function withoutUidUnder(unminted, paths) {
|
|
43257
|
+
return unminted.map((script) => `res://${script}`).filter((script) => paths.some((path) => script === path || script.startsWith(path.endsWith("/") ? path : `${path}/`)));
|
|
43258
|
+
}
|
|
43259
|
+
function withUidsMissing(answer, missing) {
|
|
43260
|
+
const content = answer.content.map((part) => ({ ...part }));
|
|
43261
|
+
const last = content.at(-1);
|
|
43262
|
+
try {
|
|
43263
|
+
const parsed = JSON.parse(last?.text ?? "");
|
|
43264
|
+
if (last !== undefined && typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
43265
|
+
last.text = answerJson({
|
|
43266
|
+
...parsed,
|
|
43267
|
+
uidsMissing: missing,
|
|
43268
|
+
uidsNote: `${missing.length === 1 ? "This script has" : "These scripts have"} no .uid sidecar yet: the engine writes one when it imports the project, which a test run does not, so a check that tracks the sidecars fails on ${missing.length === 1 ? "it" : "them"}. project_import refresh_uids writes them.`
|
|
43269
|
+
});
|
|
43270
|
+
}
|
|
43271
|
+
} catch {}
|
|
43272
|
+
return { ...answer, content };
|
|
43273
|
+
}
|
|
43232
43274
|
function withPreviousCut(answer, previous) {
|
|
43233
43275
|
const content = answer.content.map((part) => ({ ...part }));
|
|
43234
43276
|
const last = content.at(-1);
|
|
@@ -43440,6 +43482,9 @@ function profileAnswer(reading, limit) {
|
|
|
43440
43482
|
if (table.superCallers > 0) {
|
|
43441
43483
|
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.`);
|
|
43442
43484
|
}
|
|
43485
|
+
if (table.overheadDominated > 0 && table.perCallUs !== null) {
|
|
43486
|
+
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: that much of its 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.`);
|
|
43487
|
+
}
|
|
43443
43488
|
notes.push(...cappedNotes(totals));
|
|
43444
43489
|
if (totals.unreadable > 0) {
|
|
43445
43490
|
notes.push(`${totals.unreadable} message${totals.unreadable === 1 ? "" : "s"} from the game could not be read, so the totals have that many gaps.`);
|
|
@@ -43452,6 +43497,7 @@ function profileAnswer(reading, limit) {
|
|
|
43452
43497
|
frames: totals.frames,
|
|
43453
43498
|
coveredMs: reading.coveredMs,
|
|
43454
43499
|
scriptMs: table.scriptMs,
|
|
43500
|
+
perCallOverheadUs: table.perCallUs ?? undefined,
|
|
43455
43501
|
functionsCounted: table.counted,
|
|
43456
43502
|
omitted: table.omitted > 0 ? table.omitted : undefined,
|
|
43457
43503
|
functions: table.functions,
|
|
@@ -45181,11 +45227,14 @@ class GodotServer {
|
|
|
45181
45227
|
return ticker;
|
|
45182
45228
|
}
|
|
45183
45229
|
async handleRunTests(args) {
|
|
45184
|
-
const
|
|
45230
|
+
const ran = await this.runTests(args);
|
|
45185
45231
|
const project = this.project(args);
|
|
45186
45232
|
if (!project.ok || callSignal()?.aborted === true) {
|
|
45187
|
-
return
|
|
45233
|
+
return ran;
|
|
45188
45234
|
}
|
|
45235
|
+
const paths = testPathsIn(project.value.path, args["path"]);
|
|
45236
|
+
const unminted = paths.ok ? withoutUidUnder(scriptsWithoutUid(project.value.path), paths.value) : [];
|
|
45237
|
+
const answer = unminted.length === 0 ? ran : withUidsMissing(ran, unminted);
|
|
45189
45238
|
const previous = takeCutTestRun(project.value.path);
|
|
45190
45239
|
return previous === null ? answer : withPreviousCut(answer, previous);
|
|
45191
45240
|
}
|
|
@@ -45884,7 +45933,9 @@ class GodotServer {
|
|
|
45884
45933
|
return {
|
|
45885
45934
|
pid: run.pid ?? this.announcedPidOf(run) ?? run.announcedPid ?? null,
|
|
45886
45935
|
...run.throughEditor || run.projectPath === null ? {} : { project: run.projectPath },
|
|
45887
|
-
...run.pickedUp === true ? { adopted: true } : {}
|
|
45936
|
+
...run.pickedUp === true ? { adopted: true } : {},
|
|
45937
|
+
startedAt: new Date(run.startedAt).toISOString(),
|
|
45938
|
+
...run.endedAt === undefined ? {} : { endedAt: new Date(run.endedAt).toISOString() }
|
|
45888
45939
|
};
|
|
45889
45940
|
}
|
|
45890
45941
|
async handleEditorStatus() {
|
|
@@ -46657,10 +46708,11 @@ class GodotServer {
|
|
|
46657
46708
|
return "unreached";
|
|
46658
46709
|
}
|
|
46659
46710
|
running.endedThrough = "editor";
|
|
46660
|
-
|
|
46661
|
-
|
|
46711
|
+
const stopped = announced === undefined ? await this.untilTheEditorStopsPlaying() : await untilGone([announced]);
|
|
46712
|
+
if (stopped) {
|
|
46713
|
+
running.endedAt = Date.now();
|
|
46662
46714
|
}
|
|
46663
|
-
return
|
|
46715
|
+
return stopped ? "gone" : "lingering";
|
|
46664
46716
|
}
|
|
46665
46717
|
if (running.pid === null) {
|
|
46666
46718
|
return "gone";
|
|
@@ -46709,6 +46761,7 @@ class GodotServer {
|
|
|
46709
46761
|
if (landed && gone) {
|
|
46710
46762
|
running.exitCode = null;
|
|
46711
46763
|
running.exitSignal = "SIGTERM";
|
|
46764
|
+
running.endedAt ??= Date.now();
|
|
46712
46765
|
}
|
|
46713
46766
|
if (landed) {
|
|
46714
46767
|
running.endedThrough = "signal";
|
|
@@ -46733,6 +46786,7 @@ class GodotServer {
|
|
|
46733
46786
|
if (asked === "signalled" && gone) {
|
|
46734
46787
|
run.exitCode = null;
|
|
46735
46788
|
run.exitSignal = "SIGTERM";
|
|
46789
|
+
run.endedAt ??= Date.now();
|
|
46736
46790
|
}
|
|
46737
46791
|
run.log.record(gone ? "info" : "warning", !gone ? `gdharness had the keeper holding pid ${pid} end it, and it was still running ${stopWaitMs() / 1000} seconds later.` : asked === "signalled" ? `gdharness ended pid ${pid} through the keeper holding it.` : `gdharness asked the keeper holding pid ${pid} to end it, and it had already gone.`);
|
|
46738
46792
|
return gone ? "gone" : "lingering";
|
|
@@ -46780,6 +46834,7 @@ class GodotServer {
|
|
|
46780
46834
|
this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
|
|
46781
46835
|
started.exitCode = code;
|
|
46782
46836
|
started.exitSignal = code === null ? signal : null;
|
|
46837
|
+
started.endedAt = Date.now();
|
|
46783
46838
|
});
|
|
46784
46839
|
child.on("error", (err) => {
|
|
46785
46840
|
console.error("Failed to start Godot process:", err);
|
|
@@ -47094,6 +47149,9 @@ class GodotServer {
|
|
|
47094
47149
|
adopted.log.finish();
|
|
47095
47150
|
adopted.exitCode = record.exitCode ?? null;
|
|
47096
47151
|
adopted.exitSignal = record.exitSignal ?? null;
|
|
47152
|
+
if (record.endedAt !== undefined) {
|
|
47153
|
+
adopted.endedAt = record.endedAt;
|
|
47154
|
+
}
|
|
47097
47155
|
const exitMayBeComing = !alive(record.pid) && !exited(adopted);
|
|
47098
47156
|
if (!exitMayBeComing) {
|
|
47099
47157
|
adopted.endedUnwatched = !exited(adopted);
|
|
@@ -47387,6 +47445,9 @@ class GodotServer {
|
|
|
47387
47445
|
if (record !== null && record.pid === run.pid && (record.exitCode !== undefined || record.exitSignal !== undefined)) {
|
|
47388
47446
|
run.exitCode = record.exitCode ?? null;
|
|
47389
47447
|
run.exitSignal = record.exitSignal ?? null;
|
|
47448
|
+
if (record.endedAt !== undefined) {
|
|
47449
|
+
run.endedAt = record.endedAt;
|
|
47450
|
+
}
|
|
47390
47451
|
return true;
|
|
47391
47452
|
}
|
|
47392
47453
|
if (Date.now() >= until) {
|
|
@@ -47533,7 +47594,8 @@ class GodotServer {
|
|
|
47533
47594
|
project: run.throughEditor ? undefined : run.projectPath ?? undefined,
|
|
47534
47595
|
transcript: run.transcript ?? undefined,
|
|
47535
47596
|
startedAt: new Date(run.startedAt).toISOString(),
|
|
47536
|
-
|
|
47597
|
+
endedAt: stillRunning(run) || run.endedAt === undefined ? undefined : new Date(run.endedAt).toISOString(),
|
|
47598
|
+
elapsedMs: stillRunning(run) ? Date.now() - run.startedAt : run.endedAt === undefined ? undefined : run.endedAt - run.startedAt,
|
|
47537
47599
|
cpuSeconds,
|
|
47538
47600
|
note: notes.length > 0 ? notes.join(" ") : undefined,
|
|
47539
47601
|
omitted: selected.omitted,
|
|
@@ -39,6 +39,10 @@ const PROFILE_RECEIVED: String = "gdharness:profile_received"
|
|
|
39
39
|
## editor without the gdharness addon, holds every profiled exit this long.
|
|
40
40
|
const PROFILE_CONFIRM_MS: int = 3000
|
|
41
41
|
|
|
42
|
+
## How many times the empty function is called to measure what the profiler costs per call: enough
|
|
43
|
+
## that the clock's microsecond steps are lost in the sum, and a few milliseconds of one frame.
|
|
44
|
+
const PROFILER_PROBE_CALLS: int = 10000
|
|
45
|
+
|
|
42
46
|
## How many calls of `EngineDebugger.line_poll` read the debugger's messages once: it reads them
|
|
43
47
|
## on every 2048th call and only counts the others.
|
|
44
48
|
const POLLS_PER_READ: int = 2048
|
|
@@ -103,6 +107,7 @@ var _commands: Dictionary = {}
|
|
|
103
107
|
var _loaded_version: String = _read_marker(VERSION_MARKER)
|
|
104
108
|
var _loaded_digest: String = _read_marker(DIGEST_MARKER)
|
|
105
109
|
var _profile_received: bool = false
|
|
110
|
+
var _profiler_calibrated: bool = false
|
|
106
111
|
|
|
107
112
|
|
|
108
113
|
func _init() -> void:
|
|
@@ -193,6 +198,31 @@ func _send_the_profile() -> void:
|
|
|
193
198
|
)
|
|
194
199
|
|
|
195
200
|
|
|
201
|
+
## What the script profiler costs per call in this game, measured once each time it is switched on.
|
|
202
|
+
##
|
|
203
|
+
## The profiler times every call it counts, and that bookkeeping lands in the self time of the
|
|
204
|
+
## function called, so a function doing almost nothing, called six hundred thousand times, can read
|
|
205
|
+
## as a second of its own that is mostly the profiler's. So an empty function
|
|
206
|
+
## is called here [constant PROFILER_PROBE_CALLS] times while the profiler watches, under the load
|
|
207
|
+
## the game is under, and the server reads its row as the cost of one call.
|
|
208
|
+
func _calibrate_the_profiler() -> void:
|
|
209
|
+
var profiling: bool = EngineDebugger.is_active() and EngineDebugger.is_profiling("servers")
|
|
210
|
+
if not profiling:
|
|
211
|
+
_profiler_calibrated = false
|
|
212
|
+
return
|
|
213
|
+
if _profiler_calibrated:
|
|
214
|
+
return
|
|
215
|
+
_profiler_calibrated = true
|
|
216
|
+
for _call: int in PROFILER_PROBE_CALLS:
|
|
217
|
+
_profiler_probe()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
## Does nothing, so its time under the profiler is the profiler's own. Kept in step with
|
|
221
|
+
## `PROFILER_PROBE` in src/script-profile.ts.
|
|
222
|
+
func _profiler_probe() -> void:
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
|
|
196
226
|
func _on_debugger_message(message: String, _data: Array) -> bool:
|
|
197
227
|
if PROFILE_CAPTURE + ":" + message == PROFILE_RECEIVED:
|
|
198
228
|
_profile_received = true
|
|
@@ -223,6 +253,7 @@ func _process(_delta: float) -> void:
|
|
|
223
253
|
if not _enabled or _server == null:
|
|
224
254
|
return
|
|
225
255
|
_queries.note_frame(Time.get_ticks_usec())
|
|
256
|
+
_calibrate_the_profiler()
|
|
226
257
|
|
|
227
258
|
if _server.is_connection_available():
|
|
228
259
|
var client: StreamPeerTCP = _server.take_connection()
|
|
@@ -25,6 +25,15 @@ const HEADLESS_VIEWPORT: Vector2 = Vector2(64, 64)
|
|
|
25
25
|
## everything that process starts. Kept in step with `DESKTOP_VARIABLE` in src/desktop.ts.
|
|
26
26
|
const DESKTOP_VARIABLE: String = "GDHARNESS_DESKTOP"
|
|
27
27
|
|
|
28
|
+
## Node.ProcessMode as its names, for a refusal to say which one is in force.
|
|
29
|
+
const PROCESS_MODES: Dictionary[int, String] = {
|
|
30
|
+
Node.PROCESS_MODE_INHERIT: "INHERIT",
|
|
31
|
+
Node.PROCESS_MODE_PAUSABLE: "PAUSABLE",
|
|
32
|
+
Node.PROCESS_MODE_WHEN_PAUSED: "WHEN_PAUSED",
|
|
33
|
+
Node.PROCESS_MODE_ALWAYS: "ALWAYS",
|
|
34
|
+
Node.PROCESS_MODE_DISABLED: "DISABLED",
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
## Whether this run has had the pointer note in full; the game process is the run.
|
|
29
38
|
static var _pointer_explained: bool = false
|
|
30
39
|
|
|
@@ -551,20 +560,40 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
551
560
|
## that does not process while the game is paused is hovered and never handed the event.
|
|
552
561
|
func _takes_no_click(control: Control, node_path: String) -> String:
|
|
553
562
|
if not control.is_visible_in_tree():
|
|
554
|
-
return "%s is not visible, so nothing can click it" % node_path
|
|
563
|
+
return "%s is not visible%s, so nothing can click it" % [node_path, Targets.why_hidden(control)]
|
|
555
564
|
var button: BaseButton = control as BaseButton
|
|
556
565
|
if button != null and button.disabled:
|
|
557
566
|
return "%s is disabled, so clicking it presses nothing" % node_path
|
|
558
567
|
if not control.can_process():
|
|
568
|
+
var why: String = _why_it_does_not_process(control, node_path)
|
|
569
|
+
var over: Node = Targets.drawn_over(control)
|
|
570
|
+
var cover: String = "" if over == null else "; %s is drawn over it" % over.get_path()
|
|
559
571
|
if _host.get_tree().paused:
|
|
560
572
|
return (
|
|
561
|
-
"the game is paused and %s does not process while it is, so clicking it does nothing"
|
|
562
|
-
% node_path
|
|
573
|
+
"the game is paused and %s does not process while it is (%s), so clicking it does nothing%s"
|
|
574
|
+
% [node_path, why, cover]
|
|
563
575
|
)
|
|
564
|
-
return "%s
|
|
576
|
+
return "%s does not process (%s), so clicking it does nothing%s" % [node_path, why, cover]
|
|
565
577
|
return ""
|
|
566
578
|
|
|
567
579
|
|
|
580
|
+
## Which node's process_mode keeps [param control] from processing, and what it is. A node set to
|
|
581
|
+
## inherit takes its parent's, so the one deciding can be any node above it: the game turning a
|
|
582
|
+
## whole screen off while a menu shows over it reads, on the button, as a button turned off.
|
|
583
|
+
static func _why_it_does_not_process(control: Control, node_path: String) -> String:
|
|
584
|
+
var deciding: Node = control
|
|
585
|
+
while deciding.process_mode == Node.PROCESS_MODE_INHERIT and deciding.get_parent() != null:
|
|
586
|
+
deciding = deciding.get_parent()
|
|
587
|
+
# The root inherits from nothing, which processes as pausable.
|
|
588
|
+
var value: int = deciding.process_mode
|
|
589
|
+
if value == Node.PROCESS_MODE_INHERIT:
|
|
590
|
+
value = Node.PROCESS_MODE_PAUSABLE
|
|
591
|
+
var mode: String = PROCESS_MODES.get(value, str(value))
|
|
592
|
+
if deciding == control:
|
|
593
|
+
return "its process_mode is %s" % mode
|
|
594
|
+
return "%s inherits process_mode %s from %s" % [node_path, mode, deciding.get_path()]
|
|
595
|
+
|
|
596
|
+
|
|
568
597
|
## Why a click at [param control]'s centre would reach nothing, or "" when it would reach it: the
|
|
569
598
|
## centre outside the viewport it is drawn in, at [param local]; outside the viewport showing that
|
|
570
599
|
## one on the screen, which [param reached] holds with the centre there; or clipped away by a
|
|
@@ -652,7 +681,10 @@ func _press_button(viewport: Viewport, event: InputEventMouseButton) -> void:
|
|
|
652
681
|
## anything the engine can be asked, so it is not claimed.
|
|
653
682
|
func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) -> Dictionary:
|
|
654
683
|
if not item.is_visible_in_tree():
|
|
655
|
-
return {
|
|
684
|
+
return {
|
|
685
|
+
"type": "error",
|
|
686
|
+
"message": "%s is not visible%s, so nothing can click it" % [node_path, Targets.why_hidden(item)]
|
|
687
|
+
}
|
|
656
688
|
|
|
657
689
|
var found: Dictionary = Queries.in_frame(item)
|
|
658
690
|
if found.is_empty():
|
|
@@ -370,6 +370,99 @@ static func _pressed_through(control: Control) -> Control:
|
|
|
370
370
|
return control
|
|
371
371
|
|
|
372
372
|
|
|
373
|
+
## What a pointer at [param target]'s centre would reach instead of it, drawn over it, or null.
|
|
374
|
+
static func drawn_over(target: Control) -> Node:
|
|
375
|
+
var drawn: Array[Node] = _pointer_takers(target.get_tree().root)
|
|
376
|
+
return _cover_of(target, drawn, _areas_of(drawn))
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
## Why [param node], which is not visible in the tree, is not, as a parenthesis to follow its path:
|
|
380
|
+
## which nodes hide it, nearest first, and what is shown beside the nearest. Reading visible off
|
|
381
|
+
## the node itself says true when only an ancestor is hidden, so without this a caller had nothing
|
|
382
|
+
## to unhide; every hidden one is named because unhiding only the nearest leaves the rest hiding it.
|
|
383
|
+
## "" when nothing on the way up says so.
|
|
384
|
+
static func why_hidden(node: Node) -> String:
|
|
385
|
+
var hiding: Array[Node] = _hiding(node)
|
|
386
|
+
if hiding.is_empty():
|
|
387
|
+
return ""
|
|
388
|
+
var named: Array[String] = []
|
|
389
|
+
for one: Node in hiding:
|
|
390
|
+
if one == node:
|
|
391
|
+
named.append("its own visible is false")
|
|
392
|
+
else:
|
|
393
|
+
named.append("%s is hidden%s" % [one.get_path(), "" if named.is_empty() else " too"])
|
|
394
|
+
var beside: Array[String] = []
|
|
395
|
+
var parent: Node = hiding[0].get_parent()
|
|
396
|
+
if parent != null:
|
|
397
|
+
for sibling: Node in parent.get_children():
|
|
398
|
+
if sibling != hiding[0] and _shown(sibling):
|
|
399
|
+
beside.append(str(sibling.get_path()))
|
|
400
|
+
return " (%s%s)" % [", and ".join(named), _shown_beside(beside)]
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
## "; A, B and C beside it are shown" for [param beside], naming three at most and counting the rest.
|
|
404
|
+
static func _shown_beside(beside: Array[String]) -> String:
|
|
405
|
+
if beside.is_empty():
|
|
406
|
+
return ""
|
|
407
|
+
if beside.size() == 1:
|
|
408
|
+
return "; %s beside it is shown" % beside[0]
|
|
409
|
+
var listed: Array[String] = beside.slice(0, 3)
|
|
410
|
+
var rest: int = beside.size() - listed.size()
|
|
411
|
+
if rest > 0:
|
|
412
|
+
return "; %s and %d more beside it are shown" % [", ".join(listed), rest]
|
|
413
|
+
var last: String = listed.pop_back()
|
|
414
|
+
return "; %s and %s beside it are shown" % [", ".join(listed), last]
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
## The nodes whose visible is false and keeps [param node] from being drawn, nearest first. Walked
|
|
418
|
+
## the way the engine inherits visibility rather than by parent: an item under a plain Node is drawn
|
|
419
|
+
## from its canvas layer, whatever a control above that Node says, so naming that control would send
|
|
420
|
+
## a caller to unhide something that hides nothing.
|
|
421
|
+
static func _hiding(node: Node) -> Array[Node]:
|
|
422
|
+
var hiding: Array[Node] = []
|
|
423
|
+
var at: Node = node
|
|
424
|
+
while at != null:
|
|
425
|
+
var item: CanvasItem = at as CanvasItem
|
|
426
|
+
var spatial: Node3D = at as Node3D
|
|
427
|
+
if item != null:
|
|
428
|
+
if not item.visible:
|
|
429
|
+
hiding.append(item)
|
|
430
|
+
var above: CanvasItem = item.get_parent() as CanvasItem
|
|
431
|
+
if above != null:
|
|
432
|
+
at = above
|
|
433
|
+
continue
|
|
434
|
+
var layer: CanvasLayer = item.get_canvas_layer_node()
|
|
435
|
+
if layer != null and not layer.visible:
|
|
436
|
+
hiding.append(layer)
|
|
437
|
+
at = item.get_viewport()
|
|
438
|
+
elif spatial != null:
|
|
439
|
+
if not spatial.visible:
|
|
440
|
+
hiding.append(spatial)
|
|
441
|
+
at = spatial.get_parent_node_3d()
|
|
442
|
+
if at == null:
|
|
443
|
+
at = spatial.get_viewport()
|
|
444
|
+
else:
|
|
445
|
+
var window: Window = at as Window
|
|
446
|
+
if window != null and not window.visible:
|
|
447
|
+
hiding.append(window)
|
|
448
|
+
at = null
|
|
449
|
+
return hiding
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
## Whether [param node] is something drawn and on show, for naming what stands beside a hidden one.
|
|
453
|
+
static func _shown(node: Node) -> bool:
|
|
454
|
+
var item: CanvasItem = node as CanvasItem
|
|
455
|
+
if item != null:
|
|
456
|
+
return item.is_visible_in_tree()
|
|
457
|
+
var spatial: Node3D = node as Node3D
|
|
458
|
+
if spatial != null:
|
|
459
|
+
return spatial.is_visible_in_tree()
|
|
460
|
+
var layer: CanvasLayer = node as CanvasLayer
|
|
461
|
+
if layer != null:
|
|
462
|
+
return layer.visible
|
|
463
|
+
return false
|
|
464
|
+
|
|
465
|
+
|
|
373
466
|
## Everything in the tree [param root] heads that a pointer can land on: each control on screen
|
|
374
467
|
## that does not let the pointer through, and each window embedded in another.
|
|
375
468
|
static func _pointer_takers(root: Node) -> Array[Node]:
|
|
@@ -5,6 +5,7 @@ extends RefCounted
|
|
|
5
5
|
|
|
6
6
|
const Menus = preload("runtime_menus.gd")
|
|
7
7
|
const Read = preload("reading.gd")
|
|
8
|
+
const Targets = preload("runtime_targets.gd")
|
|
8
9
|
const Values = preload("runtime_values.gd")
|
|
9
10
|
const Words = preload("runtime_words.gd")
|
|
10
11
|
|
|
@@ -251,7 +252,11 @@ func choose(params: Dictionary) -> Dictionary:
|
|
|
251
252
|
if holder != null and holder.disabled:
|
|
252
253
|
return {"type": "error", "message": "%s is disabled, so its menu cannot be opened" % node_path}
|
|
253
254
|
if holder != null and not holder.is_visible_in_tree():
|
|
254
|
-
return {
|
|
255
|
+
return {
|
|
256
|
+
"type": "error",
|
|
257
|
+
"message":
|
|
258
|
+
"%s is not visible%s, so its menu cannot be opened" % [node_path, Targets.why_hidden(holder)]
|
|
259
|
+
}
|
|
255
260
|
|
|
256
261
|
# Shown first, because a menu nobody has opened has no focus to move and the press would go to
|
|
257
262
|
# whatever is behind it, and before the item is looked for, because opening is when a game fills
|
package/build/index.js
CHANGED
|
@@ -33008,7 +33008,8 @@ function recordAt(path) {
|
|
|
33008
33008
|
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
|
|
33009
33009
|
...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
|
|
33010
33010
|
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
33011
|
-
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {}
|
|
33011
|
+
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
|
|
33012
|
+
...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
|
|
33012
33013
|
};
|
|
33013
33014
|
}
|
|
33014
33015
|
async function runningAs(pid) {
|
|
@@ -33353,16 +33354,42 @@ function signatureParts(signature) {
|
|
|
33353
33354
|
}
|
|
33354
33355
|
var ms = (seconds) => Math.round(seconds * 1e6) / 1000;
|
|
33355
33356
|
function profiledFunctions(totals, limit, sourceOf = () => null) {
|
|
33356
|
-
const
|
|
33357
|
-
const
|
|
33357
|
+
const perCallUs = perCallCostOf(totals);
|
|
33358
|
+
const rows = rowsOf(totals, sourceOf).map((row) => withOverhead(row, perCallUs));
|
|
33359
|
+
const scriptMs = ms(Object.entries(totals.functions).reduce((sum, [signature, one]) => sum + (one.calls > 0 && !isCalibration(signature) ? one.selfSeconds : 0), 0));
|
|
33358
33360
|
return {
|
|
33359
33361
|
functions: rows.slice(0, limit).map((row) => placedLambda(row, sourceOf)),
|
|
33360
33362
|
omitted: Math.max(0, rows.length - limit),
|
|
33361
33363
|
scriptMs,
|
|
33362
33364
|
counted: rows.length,
|
|
33363
|
-
superCallers: rows.filter((row) => row.selfIncludesSuper === true).length
|
|
33365
|
+
superCallers: rows.filter((row) => row.selfIncludesSuper === true).length,
|
|
33366
|
+
perCallUs,
|
|
33367
|
+
overheadDominated: rows.filter((row) => row.overheadMs !== undefined).length
|
|
33364
33368
|
};
|
|
33365
33369
|
}
|
|
33370
|
+
var RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd";
|
|
33371
|
+
var PROFILER_PROBE = "_profiler_probe";
|
|
33372
|
+
var PROFILER_CALIBRATOR = "_calibrate_the_profiler";
|
|
33373
|
+
function isCalibration(signature) {
|
|
33374
|
+
const parts = signatureParts(signature);
|
|
33375
|
+
return parts.script.endsWith(RUNTIME_SCRIPT) && (parts.function === PROFILER_PROBE || parts.function === PROFILER_CALIBRATOR);
|
|
33376
|
+
}
|
|
33377
|
+
function perCallCostOf(totals) {
|
|
33378
|
+
for (const [signature, sum] of Object.entries(totals.functions)) {
|
|
33379
|
+
const parts = signatureParts(signature);
|
|
33380
|
+
if (parts.script.endsWith(RUNTIME_SCRIPT) && parts.function === PROFILER_PROBE && sum.calls > 0) {
|
|
33381
|
+
return Math.round(sum.selfSeconds / sum.calls * 1e9) / 1000;
|
|
33382
|
+
}
|
|
33383
|
+
}
|
|
33384
|
+
return null;
|
|
33385
|
+
}
|
|
33386
|
+
function withOverhead(row, perCallUs) {
|
|
33387
|
+
if (perCallUs === null || row.selfMs <= 0) {
|
|
33388
|
+
return row;
|
|
33389
|
+
}
|
|
33390
|
+
const overheadMs = Math.round(row.calls * perCallUs) / 1000;
|
|
33391
|
+
return overheadMs >= row.selfMs / 2 ? { ...row, overheadMs: Math.min(overheadMs, row.selfMs) } : row;
|
|
33392
|
+
}
|
|
33366
33393
|
function rowsOf(totals, sourceOf) {
|
|
33367
33394
|
const codes = new Map;
|
|
33368
33395
|
const codeOfScript = (script) => {
|
|
@@ -33372,7 +33399,7 @@ function rowsOf(totals, sourceOf) {
|
|
|
33372
33399
|
}
|
|
33373
33400
|
return codes.get(script) ?? null;
|
|
33374
33401
|
};
|
|
33375
|
-
return Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
|
|
33402
|
+
return Object.entries(totals.functions).filter(([signature, sum]) => sum.calls > 0 && !isCalibration(signature)).map(([signature, sum]) => {
|
|
33376
33403
|
const parts = signatureParts(signature);
|
|
33377
33404
|
const code = codeOfScript(parts.script);
|
|
33378
33405
|
return {
|
|
@@ -34463,7 +34490,7 @@ var TOOL_SPECS = [
|
|
|
34463
34490
|
},
|
|
34464
34491
|
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
|
|
34465
34492
|
profile: {
|
|
34466
|
-
summary: "where the run spends its time by GDScript function, while it runs or after it ended: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, sorted by self time, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. A run started with profile: true is profiled from its start. A game the editor plays that was not is profiled from the moment this is asked, and the answer says so; a run this server started without it is refused, since its debugger is fixed as it starts",
|
|
34493
|
+
summary: "where the run spends its time by GDScript function, while it runs or after it ended: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, sorted by self time, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. A run started with profile: true is profiled from its start. A game the editor plays that was not is profiled from the moment this is asked, and the answer says so; a run this server started without it is refused, since its debugger is fixed as it starts",
|
|
34467
34494
|
requires: []
|
|
34468
34495
|
}
|
|
34469
34496
|
},
|
|
@@ -34471,7 +34498,7 @@ var TOOL_SPECS = [
|
|
|
34471
34498
|
},
|
|
34472
34499
|
{
|
|
34473
34500
|
name: "editor_output",
|
|
34474
|
-
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing.",
|
|
34501
|
+
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing. startedAt is when the run began and elapsedMs how long it has been going; for a run that is over, endedAt is when it ended and elapsedMs how long it ran, and both are left out when nobody saw the end, as for a game closed in the editor or a run picked up from a note written before ends were recorded.",
|
|
34475
34502
|
parameters: {
|
|
34476
34503
|
severity: {
|
|
34477
34504
|
type: "string",
|
|
@@ -34519,7 +34546,7 @@ var TOOL_SPECS = [
|
|
|
34519
34546
|
},
|
|
34520
34547
|
{
|
|
34521
34548
|
name: "editor_status",
|
|
34522
|
-
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
|
|
34549
|
+
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
|
|
34523
34550
|
parameters: {},
|
|
34524
34551
|
requires: []
|
|
34525
34552
|
},
|
|
@@ -35685,6 +35712,24 @@ function foldedScriptErrors(errors) {
|
|
|
35685
35712
|
function nonEmpty(lists) {
|
|
35686
35713
|
return Object.fromEntries(Object.entries(lists).filter(([, list]) => list.length > 0));
|
|
35687
35714
|
}
|
|
35715
|
+
function withoutUidUnder(unminted, paths) {
|
|
35716
|
+
return unminted.map((script) => `res://${script}`).filter((script) => paths.some((path) => script === path || script.startsWith(path.endsWith("/") ? path : `${path}/`)));
|
|
35717
|
+
}
|
|
35718
|
+
function withUidsMissing(answer, missing) {
|
|
35719
|
+
const content = answer.content.map((part) => ({ ...part }));
|
|
35720
|
+
const last = content.at(-1);
|
|
35721
|
+
try {
|
|
35722
|
+
const parsed = JSON.parse(last?.text ?? "");
|
|
35723
|
+
if (last !== undefined && typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
35724
|
+
last.text = answerJson({
|
|
35725
|
+
...parsed,
|
|
35726
|
+
uidsMissing: missing,
|
|
35727
|
+
uidsNote: `${missing.length === 1 ? "This script has" : "These scripts have"} no .uid sidecar yet: the engine writes one when it imports the project, which a test run does not, so a check that tracks the sidecars fails on ${missing.length === 1 ? "it" : "them"}. project_import refresh_uids writes them.`
|
|
35728
|
+
});
|
|
35729
|
+
}
|
|
35730
|
+
} catch {}
|
|
35731
|
+
return { ...answer, content };
|
|
35732
|
+
}
|
|
35688
35733
|
function withPreviousCut(answer, previous) {
|
|
35689
35734
|
const content = answer.content.map((part) => ({ ...part }));
|
|
35690
35735
|
const last = content.at(-1);
|
|
@@ -35931,6 +35976,9 @@ function profileAnswer(reading, limit) {
|
|
|
35931
35976
|
if (table.superCallers > 0) {
|
|
35932
35977
|
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.`);
|
|
35933
35978
|
}
|
|
35979
|
+
if (table.overheadDominated > 0 && table.perCallUs !== null) {
|
|
35980
|
+
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: that much of its 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.`);
|
|
35981
|
+
}
|
|
35934
35982
|
notes.push(...cappedNotes(totals));
|
|
35935
35983
|
if (totals.unreadable > 0) {
|
|
35936
35984
|
notes.push(`${totals.unreadable} message${totals.unreadable === 1 ? "" : "s"} from the game could not be read, so the totals have that many gaps.`);
|
|
@@ -35943,6 +35991,7 @@ function profileAnswer(reading, limit) {
|
|
|
35943
35991
|
frames: totals.frames,
|
|
35944
35992
|
coveredMs: reading.coveredMs,
|
|
35945
35993
|
scriptMs: table.scriptMs,
|
|
35994
|
+
perCallOverheadUs: table.perCallUs ?? undefined,
|
|
35946
35995
|
functionsCounted: table.counted,
|
|
35947
35996
|
omitted: table.omitted > 0 ? table.omitted : undefined,
|
|
35948
35997
|
functions: table.functions,
|
|
@@ -37675,11 +37724,14 @@ class GodotServer {
|
|
|
37675
37724
|
return ticker;
|
|
37676
37725
|
}
|
|
37677
37726
|
async handleRunTests(args) {
|
|
37678
|
-
const
|
|
37727
|
+
const ran = await this.runTests(args);
|
|
37679
37728
|
const project = this.project(args);
|
|
37680
37729
|
if (!project.ok || callSignal()?.aborted === true) {
|
|
37681
|
-
return
|
|
37730
|
+
return ran;
|
|
37682
37731
|
}
|
|
37732
|
+
const paths = testPathsIn(project.value.path, args["path"]);
|
|
37733
|
+
const unminted = paths.ok ? withoutUidUnder(scriptsWithoutUid(project.value.path), paths.value) : [];
|
|
37734
|
+
const answer = unminted.length === 0 ? ran : withUidsMissing(ran, unminted);
|
|
37683
37735
|
const previous = takeCutTestRun(project.value.path);
|
|
37684
37736
|
return previous === null ? answer : withPreviousCut(answer, previous);
|
|
37685
37737
|
}
|
|
@@ -38378,7 +38430,9 @@ class GodotServer {
|
|
|
38378
38430
|
return {
|
|
38379
38431
|
pid: run.pid ?? this.announcedPidOf(run) ?? run.announcedPid ?? null,
|
|
38380
38432
|
...run.throughEditor || run.projectPath === null ? {} : { project: run.projectPath },
|
|
38381
|
-
...run.pickedUp === true ? { adopted: true } : {}
|
|
38433
|
+
...run.pickedUp === true ? { adopted: true } : {},
|
|
38434
|
+
startedAt: new Date(run.startedAt).toISOString(),
|
|
38435
|
+
...run.endedAt === undefined ? {} : { endedAt: new Date(run.endedAt).toISOString() }
|
|
38382
38436
|
};
|
|
38383
38437
|
}
|
|
38384
38438
|
async handleEditorStatus() {
|
|
@@ -39151,10 +39205,11 @@ class GodotServer {
|
|
|
39151
39205
|
return "unreached";
|
|
39152
39206
|
}
|
|
39153
39207
|
running.endedThrough = "editor";
|
|
39154
|
-
|
|
39155
|
-
|
|
39208
|
+
const stopped = announced === undefined ? await this.untilTheEditorStopsPlaying() : await untilGone([announced]);
|
|
39209
|
+
if (stopped) {
|
|
39210
|
+
running.endedAt = Date.now();
|
|
39156
39211
|
}
|
|
39157
|
-
return
|
|
39212
|
+
return stopped ? "gone" : "lingering";
|
|
39158
39213
|
}
|
|
39159
39214
|
if (running.pid === null) {
|
|
39160
39215
|
return "gone";
|
|
@@ -39203,6 +39258,7 @@ class GodotServer {
|
|
|
39203
39258
|
if (landed && gone) {
|
|
39204
39259
|
running.exitCode = null;
|
|
39205
39260
|
running.exitSignal = "SIGTERM";
|
|
39261
|
+
running.endedAt ??= Date.now();
|
|
39206
39262
|
}
|
|
39207
39263
|
if (landed) {
|
|
39208
39264
|
running.endedThrough = "signal";
|
|
@@ -39227,6 +39283,7 @@ class GodotServer {
|
|
|
39227
39283
|
if (asked === "signalled" && gone) {
|
|
39228
39284
|
run.exitCode = null;
|
|
39229
39285
|
run.exitSignal = "SIGTERM";
|
|
39286
|
+
run.endedAt ??= Date.now();
|
|
39230
39287
|
}
|
|
39231
39288
|
run.log.record(gone ? "info" : "warning", !gone ? `gdharness had the keeper holding pid ${pid} end it, and it was still running ${stopWaitMs() / 1000} seconds later.` : asked === "signalled" ? `gdharness ended pid ${pid} through the keeper holding it.` : `gdharness asked the keeper holding pid ${pid} to end it, and it had already gone.`);
|
|
39232
39289
|
return gone ? "gone" : "lingering";
|
|
@@ -39274,6 +39331,7 @@ class GodotServer {
|
|
|
39274
39331
|
this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
|
|
39275
39332
|
started.exitCode = code;
|
|
39276
39333
|
started.exitSignal = code === null ? signal : null;
|
|
39334
|
+
started.endedAt = Date.now();
|
|
39277
39335
|
});
|
|
39278
39336
|
child.on("error", (err) => {
|
|
39279
39337
|
console.error("Failed to start Godot process:", err);
|
|
@@ -39588,6 +39646,9 @@ class GodotServer {
|
|
|
39588
39646
|
adopted.log.finish();
|
|
39589
39647
|
adopted.exitCode = record.exitCode ?? null;
|
|
39590
39648
|
adopted.exitSignal = record.exitSignal ?? null;
|
|
39649
|
+
if (record.endedAt !== undefined) {
|
|
39650
|
+
adopted.endedAt = record.endedAt;
|
|
39651
|
+
}
|
|
39591
39652
|
const exitMayBeComing = !alive(record.pid) && !exited(adopted);
|
|
39592
39653
|
if (!exitMayBeComing) {
|
|
39593
39654
|
adopted.endedUnwatched = !exited(adopted);
|
|
@@ -39881,6 +39942,9 @@ class GodotServer {
|
|
|
39881
39942
|
if (record !== null && record.pid === run.pid && (record.exitCode !== undefined || record.exitSignal !== undefined)) {
|
|
39882
39943
|
run.exitCode = record.exitCode ?? null;
|
|
39883
39944
|
run.exitSignal = record.exitSignal ?? null;
|
|
39945
|
+
if (record.endedAt !== undefined) {
|
|
39946
|
+
run.endedAt = record.endedAt;
|
|
39947
|
+
}
|
|
39884
39948
|
return true;
|
|
39885
39949
|
}
|
|
39886
39950
|
if (Date.now() >= until) {
|
|
@@ -40027,7 +40091,8 @@ class GodotServer {
|
|
|
40027
40091
|
project: run.throughEditor ? undefined : run.projectPath ?? undefined,
|
|
40028
40092
|
transcript: run.transcript ?? undefined,
|
|
40029
40093
|
startedAt: new Date(run.startedAt).toISOString(),
|
|
40030
|
-
|
|
40094
|
+
endedAt: stillRunning(run) || run.endedAt === undefined ? undefined : new Date(run.endedAt).toISOString(),
|
|
40095
|
+
elapsedMs: stillRunning(run) ? Date.now() - run.startedAt : run.endedAt === undefined ? undefined : run.endedAt - run.startedAt,
|
|
40031
40096
|
cpuSeconds,
|
|
40032
40097
|
note: notes.length > 0 ? notes.join(" ") : undefined,
|
|
40033
40098
|
omitted: selected.omitted,
|
package/build/keeper.js
CHANGED
|
@@ -421,7 +421,8 @@ function recordRunEnded(projectPath, pid, ending) {
|
|
|
421
421
|
const written = {
|
|
422
422
|
...record,
|
|
423
423
|
...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
|
|
424
|
-
...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal }
|
|
424
|
+
...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal },
|
|
425
|
+
endedAt: Date.now()
|
|
425
426
|
};
|
|
426
427
|
try {
|
|
427
428
|
writeFileSync2(path, JSON.stringify(written, null, 2), "utf8");
|
|
@@ -457,7 +458,8 @@ function recordAt(path) {
|
|
|
457
458
|
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
|
|
458
459
|
...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
|
|
459
460
|
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
460
|
-
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {}
|
|
461
|
+
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
|
|
462
|
+
...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
|
|
461
463
|
};
|
|
462
464
|
}
|
|
463
465
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
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",
|