gdharness 1.1.59 → 1.1.60
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 +150 -44
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +9 -0
- package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +16 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +60 -10
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +8 -1
- package/build/index.js +148 -45
- package/build/keeper.js +6 -0
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -9183,6 +9183,65 @@ function enclosingFunction(source, line, code = codeOf(source)) {
|
|
|
9183
9183
|
}
|
|
9184
9184
|
return null;
|
|
9185
9185
|
}
|
|
9186
|
+
function afterHeader(text, from) {
|
|
9187
|
+
let depth = 0;
|
|
9188
|
+
for (let at = text.indexOf("(", from);at !== -1 && at < text.length; at += 1) {
|
|
9189
|
+
if (text[at] === "(") {
|
|
9190
|
+
depth += 1;
|
|
9191
|
+
} else if (text[at] === ")") {
|
|
9192
|
+
depth -= 1;
|
|
9193
|
+
if (depth === 0) {
|
|
9194
|
+
const colon = text.indexOf(":", at);
|
|
9195
|
+
return colon === -1 ? -1 : colon + 1;
|
|
9196
|
+
}
|
|
9197
|
+
}
|
|
9198
|
+
}
|
|
9199
|
+
return -1;
|
|
9200
|
+
}
|
|
9201
|
+
function bodyCallsSuper(code, line, lambda) {
|
|
9202
|
+
const lines = code.split(`
|
|
9203
|
+
`);
|
|
9204
|
+
const first = lines[line - 1];
|
|
9205
|
+
if (first === undefined) {
|
|
9206
|
+
return false;
|
|
9207
|
+
}
|
|
9208
|
+
const header = lambda ? LAMBDA_HEADER.exec(first) : FUNC_LINE.exec(first);
|
|
9209
|
+
if (header !== null) {
|
|
9210
|
+
const body = afterHeader(first, header.index);
|
|
9211
|
+
if (body !== -1 && first.slice(body).trim() !== "") {
|
|
9212
|
+
return SUPER_CALL.test(first.slice(body));
|
|
9213
|
+
}
|
|
9214
|
+
}
|
|
9215
|
+
const indent = indentOf(first);
|
|
9216
|
+
let lambdaIndent = null;
|
|
9217
|
+
for (let index = line - 1;index < lines.length; index += 1) {
|
|
9218
|
+
const text = lines[index] ?? "";
|
|
9219
|
+
if (text.trim() === "") {
|
|
9220
|
+
continue;
|
|
9221
|
+
}
|
|
9222
|
+
const at = indentOf(text);
|
|
9223
|
+
if (at < indent) {
|
|
9224
|
+
break;
|
|
9225
|
+
}
|
|
9226
|
+
if (lambdaIndent !== null) {
|
|
9227
|
+
if (at > lambdaIndent) {
|
|
9228
|
+
continue;
|
|
9229
|
+
}
|
|
9230
|
+
lambdaIndent = null;
|
|
9231
|
+
}
|
|
9232
|
+
const inner = LAMBDA_HEADER.exec(text);
|
|
9233
|
+
if (SUPER_CALL.test(inner === null ? text : text.slice(0, inner.index))) {
|
|
9234
|
+
return true;
|
|
9235
|
+
}
|
|
9236
|
+
if (inner !== null) {
|
|
9237
|
+
const body = afterHeader(text, inner.index);
|
|
9238
|
+
if (body !== -1 && text.slice(body).trim() === "") {
|
|
9239
|
+
lambdaIndent = at;
|
|
9240
|
+
}
|
|
9241
|
+
}
|
|
9242
|
+
}
|
|
9243
|
+
return false;
|
|
9244
|
+
}
|
|
9186
9245
|
function callSitesOf(source, name) {
|
|
9187
9246
|
const regions = regionsOf(source);
|
|
9188
9247
|
const code = codeOf(source, regions);
|
|
@@ -9208,7 +9267,7 @@ function collectEnumValues(text, from, into) {
|
|
|
9208
9267
|
function isIdentifier(name) {
|
|
9209
9268
|
return new RegExp(`^${NAME}$`, "u").test(name) && !KEYWORDS.has(name);
|
|
9210
9269
|
}
|
|
9211
|
-
var IDENTIFIER_START, IDENTIFIER_PART, DECLARING, NAME = "[\\p{L}_][\\p{L}\\p{N}_]*", ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*", DECLARATION_LINE, CLASS_NAME_LINE, EXTENDS_LINE, EXTENDS_CLAUSE, FUNC_LINE, KEYWORDS;
|
|
9270
|
+
var IDENTIFIER_START, IDENTIFIER_PART, DECLARING, NAME = "[\\p{L}_][\\p{L}\\p{N}_]*", ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*", DECLARATION_LINE, CLASS_NAME_LINE, EXTENDS_LINE, EXTENDS_CLAUSE, FUNC_LINE, SUPER_CALL, LAMBDA_HEADER, KEYWORDS;
|
|
9212
9271
|
var init_gdscript_source = __esm(() => {
|
|
9213
9272
|
IDENTIFIER_START = /[\p{L}_]/u;
|
|
9214
9273
|
IDENTIFIER_PART = /[\p{L}\p{N}_]/u;
|
|
@@ -9218,6 +9277,8 @@ var init_gdscript_source = __esm(() => {
|
|
|
9218
9277
|
EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
|
|
9219
9278
|
EXTENDS_CLAUSE = /\bextends(?=\s)/u;
|
|
9220
9279
|
FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
|
|
9280
|
+
SUPER_CALL = /(?<![\p{L}\p{N}_.])super\s*[.(]/u;
|
|
9281
|
+
LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
|
|
9221
9282
|
KEYWORDS = new Set([
|
|
9222
9283
|
"and",
|
|
9223
9284
|
"as",
|
|
@@ -18218,7 +18279,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18218
18279
|
},
|
|
18219
18280
|
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
|
|
18220
18281
|
profile: {
|
|
18221
|
-
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 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",
|
|
18282
|
+
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",
|
|
18222
18283
|
requires: []
|
|
18223
18284
|
}
|
|
18224
18285
|
},
|
|
@@ -34131,7 +34192,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
|
|
|
34131
34192
|
ok: false,
|
|
34132
34193
|
reason: "busy",
|
|
34133
34194
|
requestId: id,
|
|
34134
|
-
message: `${still} Nothing was cancelled: a call that takes longer than the wait goes on and does everything it was asked, so read the state back rather than repeating it. runtime_invoke op result with requestId ${id} answers with its reply once it comes
|
|
34195
|
+
message: `${still} Nothing was cancelled: a call that takes longer than the wait goes on and does everything it was asked, so read the state back rather than repeating it. runtime_invoke op result with requestId ${id} answers with its reply once it comes. A game held at a breakpoint instead is one debug_state stack answers about.`
|
|
34135
34196
|
});
|
|
34136
34197
|
}, timeoutMs);
|
|
34137
34198
|
socket.setEncoding("utf8");
|
|
@@ -42325,25 +42386,36 @@ function signatureParts(signature) {
|
|
|
42325
42386
|
return { script: parts.slice(0, -2).join("::"), line: Number.isFinite(line) ? line : 0, function: name };
|
|
42326
42387
|
}
|
|
42327
42388
|
function profiledFunctions(totals, limit, sourceOf = () => null) {
|
|
42328
|
-
const rows = rowsOf(totals);
|
|
42389
|
+
const rows = rowsOf(totals, sourceOf);
|
|
42329
42390
|
const scriptMs = ms(Object.values(totals.functions).reduce((sum, one) => sum + (one.calls > 0 ? one.selfSeconds : 0), 0));
|
|
42330
42391
|
return {
|
|
42331
42392
|
functions: rows.slice(0, limit).map((row) => placedLambda(row, sourceOf)),
|
|
42332
42393
|
omitted: Math.max(0, rows.length - limit),
|
|
42333
42394
|
scriptMs,
|
|
42334
|
-
counted: rows.length
|
|
42395
|
+
counted: rows.length,
|
|
42396
|
+
superCallers: rows.filter((row) => row.selfIncludesSuper === true).length
|
|
42335
42397
|
};
|
|
42336
42398
|
}
|
|
42337
|
-
function rowsOf(totals) {
|
|
42399
|
+
function rowsOf(totals, sourceOf) {
|
|
42400
|
+
const codes = new Map;
|
|
42401
|
+
const codeOfScript = (script) => {
|
|
42402
|
+
if (!codes.has(script)) {
|
|
42403
|
+
const source = sourceOf(script);
|
|
42404
|
+
codes.set(script, source === null ? null : codeOf(source));
|
|
42405
|
+
}
|
|
42406
|
+
return codes.get(script) ?? null;
|
|
42407
|
+
};
|
|
42338
42408
|
return Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
|
|
42339
42409
|
const parts = signatureParts(signature);
|
|
42410
|
+
const code = codeOfScript(parts.script);
|
|
42340
42411
|
return {
|
|
42341
42412
|
script: parts.script,
|
|
42342
42413
|
function: parts.function,
|
|
42343
42414
|
line: parts.line,
|
|
42344
42415
|
calls: sum.calls,
|
|
42345
42416
|
selfMs: ms(sum.selfSeconds),
|
|
42346
|
-
totalMs: ms(sum.totalSeconds)
|
|
42417
|
+
totalMs: ms(sum.totalSeconds),
|
|
42418
|
+
...code !== null && bodyCallsSuper(code, parts.line, parts.function.endsWith("(lambda)")) ? { selfIncludesSuper: true } : {}
|
|
42347
42419
|
};
|
|
42348
42420
|
}).sort((a, b) => b.selfMs - a.selfMs || b.totalMs - a.totalMs || a.function.localeCompare(b.function));
|
|
42349
42421
|
}
|
|
@@ -42359,17 +42431,24 @@ function placedLambda(row, sourceOf) {
|
|
|
42359
42431
|
return within === null ? row : { ...row, within };
|
|
42360
42432
|
}
|
|
42361
42433
|
function callersOf(totals, name, scripts) {
|
|
42362
|
-
const
|
|
42434
|
+
const listed = [...scripts];
|
|
42435
|
+
const sources = new Map(listed);
|
|
42436
|
+
const rows = rowsOf(totals, (script) => sources.get(script) ?? null);
|
|
42363
42437
|
const bare = bareName(name);
|
|
42364
42438
|
const target = rows.filter((row) => name.includes(".") ? row.function === name : bareName(row.function) === bare);
|
|
42365
42439
|
const sites = [];
|
|
42366
|
-
for (const [script, source] of
|
|
42440
|
+
for (const [script, source] of listed) {
|
|
42367
42441
|
for (const site of callSitesOf(source, bare)) {
|
|
42368
42442
|
const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
|
|
42369
42443
|
sites.push({
|
|
42370
42444
|
script,
|
|
42371
42445
|
...site,
|
|
42372
|
-
...within === undefined ? {} : {
|
|
42446
|
+
...within === undefined ? {} : {
|
|
42447
|
+
withinCalls: within.calls,
|
|
42448
|
+
withinSelfMs: within.selfMs,
|
|
42449
|
+
withinTotalMs: within.totalMs,
|
|
42450
|
+
...within.selfIncludesSuper === true ? { withinSelfIncludesSuper: true } : {}
|
|
42451
|
+
}
|
|
42373
42452
|
});
|
|
42374
42453
|
}
|
|
42375
42454
|
}
|
|
@@ -42406,8 +42485,14 @@ import { setTimeout as delay4 } from "node:timers/promises";
|
|
|
42406
42485
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
42407
42486
|
import { promisify as promisify5 } from "node:util";
|
|
42408
42487
|
function editorRestartTimeoutMs() {
|
|
42409
|
-
|
|
42410
|
-
|
|
42488
|
+
return waitFromEnv("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS", 90000);
|
|
42489
|
+
}
|
|
42490
|
+
function waitFromEnv(variable, fallback) {
|
|
42491
|
+
const override = Number.parseInt(envValue(variable) ?? "", 10);
|
|
42492
|
+
return Number.isInteger(override) && override > 0 ? override : fallback;
|
|
42493
|
+
}
|
|
42494
|
+
function greetingWaitMs() {
|
|
42495
|
+
return waitFromEnv("GDHARNESS_GREETING_WAIT_MS", 1e4);
|
|
42411
42496
|
}
|
|
42412
42497
|
function restartUnderway(note, now = Date.now()) {
|
|
42413
42498
|
return alive(note.byPid) && now - Date.parse(note.quitAt) < editorRestartTimeoutMs() + 30000;
|
|
@@ -42458,6 +42543,14 @@ function captureDetail(payload) {
|
|
|
42458
42543
|
function patienceForFrames(frames, atLeast) {
|
|
42459
42544
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
42460
42545
|
}
|
|
42546
|
+
function longerWaitNote(tool, op, waitedMs) {
|
|
42547
|
+
const spec = TOOL_SPECS.find((one) => one.name === tool);
|
|
42548
|
+
if (spec !== undefined && argumentsOf(spec, op).includes("timeoutMs")) {
|
|
42549
|
+
return "timeoutMs waits longer next time.";
|
|
42550
|
+
}
|
|
42551
|
+
const how = tool === "runtime_wait" && op === "frames" ? `the wait is counted from the number of frames, at ${SLOWEST_FRAME_RATE} a second on top of GDHARNESS_RUNTIME_TIMEOUT_MS, and this game fell below that rate, so ask for fewer frames at a time` : "the wait is GDHARNESS_RUNTIME_TIMEOUT_MS, which is set in the environment the server is started with";
|
|
42552
|
+
return `${tool} ${op} takes no timeoutMs: it was given ${waitedMs}ms, and ${how}.`;
|
|
42553
|
+
}
|
|
42461
42554
|
function projectSpelling(projectPath, absolutePath) {
|
|
42462
42555
|
return `res://${relative5(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
|
|
42463
42556
|
}
|
|
@@ -42537,7 +42630,10 @@ function runIsUp(run, editorSays, editorPlay = null, now = Date.now()) {
|
|
|
42537
42630
|
function noCodeWillCome(run) {
|
|
42538
42631
|
return !exited(run);
|
|
42539
42632
|
}
|
|
42540
|
-
|
|
42633
|
+
function stopWaitMs() {
|
|
42634
|
+
return waitFromEnv("GDHARNESS_STOP_WAIT_MS", 1e4);
|
|
42635
|
+
}
|
|
42636
|
+
async function untilGone(pids, withinMs = stopWaitMs()) {
|
|
42541
42637
|
const gone = () => pids.every((pid) => !alive(pid));
|
|
42542
42638
|
const deadline = Date.now() + withinMs;
|
|
42543
42639
|
while (!gone() && Date.now() < deadline) {
|
|
@@ -42592,7 +42688,7 @@ function stopVerdict(stop) {
|
|
|
42592
42688
|
return {
|
|
42593
42689
|
stopped: false,
|
|
42594
42690
|
stillGoing: stop.endedPid,
|
|
42595
|
-
note: `${told} ${
|
|
42691
|
+
note: `${told} ${stopWaitMs() / 1000} seconds later, so it has not stopped. editor_status says when it has gone, and editor_run stop asks again.`,
|
|
42596
42692
|
withChildren: true
|
|
42597
42693
|
};
|
|
42598
42694
|
}
|
|
@@ -42710,7 +42806,7 @@ function endedToStartThis(ended) {
|
|
|
42710
42806
|
return ` ${which} was not ended here: the editor could not be asked to stop it. The editor stops a play before starting the next, so it is not running beside this one if this start reached the editor.`;
|
|
42711
42807
|
}
|
|
42712
42808
|
if (ended.ending === "lingering") {
|
|
42713
|
-
return ` ${which} was told to end to start this one and was still running ${
|
|
42809
|
+
return ` ${which} was told to end to start this one and was still running ${stopWaitMs() / 1000} seconds later, so it is running beside this one: editor_status says when it has gone.`;
|
|
42714
42810
|
}
|
|
42715
42811
|
return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
|
|
42716
42812
|
}
|
|
@@ -42827,6 +42923,9 @@ function outsideTheirRanges(spec, args) {
|
|
|
42827
42923
|
}
|
|
42828
42924
|
return complaints;
|
|
42829
42925
|
}
|
|
42926
|
+
function scanWaitMs() {
|
|
42927
|
+
return waitFromEnv("GDHARNESS_SCAN_WAIT_MS", 30000);
|
|
42928
|
+
}
|
|
42830
42929
|
function scanWaitIn(payload) {
|
|
42831
42930
|
const waited = readNumber(payload, "waitedForEditorScanMs");
|
|
42832
42931
|
const note = readString(payload, "scanNote");
|
|
@@ -42885,7 +42984,7 @@ function scanWaitAnswer(scanned, started = "game") {
|
|
|
42885
42984
|
if (scanned.waitedMs === 0) {
|
|
42886
42985
|
return {};
|
|
42887
42986
|
}
|
|
42888
|
-
const still = `The editor was still scanning the project after ${
|
|
42987
|
+
const still = `The editor was still scanning the project after ${scanWaitMs() / 1000} seconds, so`;
|
|
42889
42988
|
return {
|
|
42890
42989
|
waitedForEditorScanMs: scanned.waitedMs,
|
|
42891
42990
|
scanNote: !scanned.stillScanning ? undefined : started === "game" ? `${still} this game started during the scan and may have read a class cache being rewritten. If ` + 'editor_output shows "Could not find type" for a class that is declared, editor_rescan ' + "waits for the scan to finish, and a start after it reads the finished cache." : `${still} the engine answering this started during the scan and may have read a class cache being ` + "rewritten, and a class cache it wrote can be written over when the scan finishes. If " + 'engine_messages shows "Could not find type" for a class that is declared, editor_rescan waits ' + "for the scan to finish, and the same call after it reads the finished cache."
|
|
@@ -43140,14 +43239,20 @@ function profileAnswer(reading, limit) {
|
|
|
43140
43239
|
const table = profiledFunctions(totals, limit, reading.sourceOf);
|
|
43141
43240
|
const notes = [];
|
|
43142
43241
|
const addonNote = reading.runtimeAddon ? "" : " This project has no gdharness runtime addon, which switches the profiler off as the game quits so the engine sends its totals: setup installs it.";
|
|
43143
|
-
|
|
43242
|
+
const totalsWithheld = reading.runtimeAddon ? " The runtime addon switches the profiler off as the game quits, and did not here: either it was no longer running in this game by then, which a project can arrange for some scenes; or the game ended without taking its tree down, as a crash or a kill does; or the totals did not reach the debugger before the game exited, which the game then says in its output." : addonNote;
|
|
43243
|
+
if (totals.complete && reading.running) {
|
|
43244
|
+
notes.push(`The engine's totals arrived while the game is still running, which it sends only when the profiler is switched off, so ${reading.through === "editor" ? "the editor's Profiler tab or the game itself, through EngineDebugger," : "the game itself, through EngineDebugger,"} switched it off before the game ended. They cover the run up to then and nothing after.`);
|
|
43245
|
+
} else if (totals.complete) {} else if (reading.running && totals.frames === 0) {
|
|
43144
43246
|
notes.push(`The profiler is on and the game has not finished its first frame yet: a frame is sent as it ends, so a scene doing its work in _ready shows nothing until that returns, and the engine's own totals arrive when the game quits.${addonNote}`);
|
|
43145
43247
|
} else if (reading.running) {
|
|
43146
43248
|
notes.push(`The run is still going, so these are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} summed so far${reading.through === "gdharness" ? ", written at most half a second behind" : ""}.`);
|
|
43147
43249
|
} else if (totals.frames === 0) {
|
|
43148
|
-
notes.push(`The game connected and ended before a frame of the profile reached ${reading.through === "editor" ? "the editor" : "this server"}: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${
|
|
43250
|
+
notes.push(`The game connected and ended before a frame of the profile reached ${reading.through === "editor" ? "the editor" : "this server"}: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${totalsWithheld}`);
|
|
43149
43251
|
} else {
|
|
43150
|
-
notes.push(`These are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} that arrived, summed, without the engine's own totals,
|
|
43252
|
+
notes.push(`These are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} that arrived, summed, without the engine's own totals, which it sends only when the profiler is switched off.${totalsWithheld} Not in them is any script time after the last frame sent: a frame still queued as the game exited, since the engine stops sending without emptying its queue, and what scripts did as it quit, such as in _exit_tree. That can be nothing: a scene that does its work in _ready and quits has all of it in its first frame.`);
|
|
43253
|
+
}
|
|
43254
|
+
if (table.superCallers > 0) {
|
|
43255
|
+
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.`);
|
|
43151
43256
|
}
|
|
43152
43257
|
notes.push(...cappedNotes(totals));
|
|
43153
43258
|
if (totals.unreadable > 0) {
|
|
@@ -43884,7 +43989,7 @@ class GodotServer {
|
|
|
43884
43989
|
}
|
|
43885
43990
|
async dispatch(tool, op, args) {
|
|
43886
43991
|
if (tool !== "editor_status" && this.godotBridge.isArriving()) {
|
|
43887
|
-
await this.waitForBridge(() => !this.godotBridge.isArriving(), Date.now() +
|
|
43992
|
+
await this.waitForBridge(() => !this.godotBridge.isArriving(), Date.now() + greetingWaitMs());
|
|
43888
43993
|
}
|
|
43889
43994
|
if (ENGINE_PASSES[tool]?.[op] !== undefined) {
|
|
43890
43995
|
return op === "reimport" ? await this.handleReimport(args) : await this.handleRefreshUids(args);
|
|
@@ -44044,7 +44149,7 @@ class GodotServer {
|
|
|
44044
44149
|
depth: depth ?? 3,
|
|
44045
44150
|
include_properties: readBoolean(args, "includeProperties") ?? false,
|
|
44046
44151
|
properties: readArray(args, "properties") ?? []
|
|
44047
|
-
}, patience);
|
|
44152
|
+
}, { tool: "runtime_inspect", op, patience });
|
|
44048
44153
|
}
|
|
44049
44154
|
case "find":
|
|
44050
44155
|
return await this.handleFindRuntimeNodes(args, patience);
|
|
@@ -44054,17 +44159,17 @@ class GodotServer {
|
|
|
44054
44159
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
44055
44160
|
include_hidden: readBoolean(args, "includeHidden") ?? false,
|
|
44056
44161
|
limit: readPositiveNumber(args, "limit") ?? 500
|
|
44057
|
-
}, patience);
|
|
44162
|
+
}, { tool: "runtime_inspect", op, patience });
|
|
44058
44163
|
case "rect":
|
|
44059
|
-
return await this.handleRuntimeCommand("get_rect", { ...whichGame(args), path: readNonEmptyString(args, "nodePath") ?? "" }, patience);
|
|
44164
|
+
return await this.handleRuntimeCommand("get_rect", { ...whichGame(args), path: readNonEmptyString(args, "nodePath") ?? "" }, { tool: "runtime_inspect", op, patience });
|
|
44060
44165
|
case "property":
|
|
44061
44166
|
return await this.handleRuntimeCommand("get_property", {
|
|
44062
44167
|
...whichGame(args),
|
|
44063
44168
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
44064
44169
|
property: readNonEmptyString(args, "property") ?? ""
|
|
44065
|
-
}, patience);
|
|
44170
|
+
}, { tool: "runtime_inspect", op, patience });
|
|
44066
44171
|
default:
|
|
44067
|
-
return await this.handleRuntimeCommand("get_metrics", { ...whichGame(args), metrics: readArray(args, "metrics") ?? [] }, patience);
|
|
44172
|
+
return await this.handleRuntimeCommand("get_metrics", { ...whichGame(args), metrics: readArray(args, "metrics") ?? [] }, { tool: "runtime_inspect", op, patience });
|
|
44068
44173
|
}
|
|
44069
44174
|
}
|
|
44070
44175
|
case "runtime_invoke": {
|
|
@@ -44077,16 +44182,16 @@ class GodotServer {
|
|
|
44077
44182
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
44078
44183
|
property: readString(args, "property") ?? "",
|
|
44079
44184
|
value: args["value"]
|
|
44080
|
-
}, patience) : await this.handleRuntimeCommand("call_method", {
|
|
44185
|
+
}, { tool: "runtime_invoke", op, patience }) : await this.handleRuntimeCommand("call_method", {
|
|
44081
44186
|
...whichGame(args),
|
|
44082
44187
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
44083
44188
|
method: readString(args, "method") ?? "",
|
|
44084
44189
|
args: readArray(args, "args") ?? [],
|
|
44085
44190
|
...args["properties"] === undefined ? {} : { properties: args["properties"] }
|
|
44086
|
-
}, patience);
|
|
44191
|
+
}, { tool: "runtime_invoke", op, patience });
|
|
44087
44192
|
}
|
|
44088
44193
|
case "runtime_capture":
|
|
44089
|
-
return await this.handleRuntimeCommand(op === "screenshot" ? "capture_screenshot" : "capture_viewport", args);
|
|
44194
|
+
return await this.handleRuntimeCommand(op === "screenshot" ? "capture_screenshot" : "capture_viewport", args, { tool: "runtime_capture", op });
|
|
44090
44195
|
case "runtime_input":
|
|
44091
44196
|
if (op === "click") {
|
|
44092
44197
|
if (readNonEmptyString(args, "nodePath") === undefined && readNonEmptyString(args, "says") === undefined) {
|
|
@@ -44099,7 +44204,7 @@ class GodotServer {
|
|
|
44099
44204
|
double: readBoolean(args, "doubleClick") ?? false,
|
|
44100
44205
|
...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
|
|
44101
44206
|
...args["index"] === undefined ? {} : { index: args["index"] }
|
|
44102
|
-
});
|
|
44207
|
+
}, { tool: "runtime_input", op });
|
|
44103
44208
|
}
|
|
44104
44209
|
if (op === "choose") {
|
|
44105
44210
|
return await this.handleRuntimeCommand("choose", {
|
|
@@ -44107,9 +44212,9 @@ class GodotServer {
|
|
|
44107
44212
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
44108
44213
|
text: readString(args, "text") ?? "",
|
|
44109
44214
|
...args["index"] === undefined ? {} : { index: args["index"] }
|
|
44110
|
-
});
|
|
44215
|
+
}, { tool: "runtime_input", op });
|
|
44111
44216
|
}
|
|
44112
|
-
return await this.handleRuntimeCommand(`inject_${op}`, args);
|
|
44217
|
+
return await this.handleRuntimeCommand(`inject_${op}`, args, { tool: "runtime_input", op });
|
|
44113
44218
|
case "runtime_wait":
|
|
44114
44219
|
return await this.handleRuntimeWait(op, args);
|
|
44115
44220
|
case "debug_breakpoint": {
|
|
@@ -44338,7 +44443,7 @@ class GodotServer {
|
|
|
44338
44443
|
const answered = Date.now();
|
|
44339
44444
|
if (status["scanning"] === true || status["importing"] === true || status["pending"] === true) {
|
|
44340
44445
|
sawAScan = true;
|
|
44341
|
-
if (Date.now() - started >=
|
|
44446
|
+
if (Date.now() - started >= scanWaitMs()) {
|
|
44342
44447
|
return { waitedMs: Date.now() - started, stillScanning: true };
|
|
44343
44448
|
}
|
|
44344
44449
|
await new Promise((settle) => setTimeout(settle, 100));
|
|
@@ -45685,7 +45790,7 @@ class GodotServer {
|
|
|
45685
45790
|
]);
|
|
45686
45791
|
}
|
|
45687
45792
|
if (!hasSaidWhoItIs(before)) {
|
|
45688
|
-
return this.createErrorResponse(`The editor has connected and has not said who it is within ${
|
|
45793
|
+
return this.createErrorResponse(`The editor has connected and has not said who it is within ${greetingWaitMs() / 1000}s, so whether this server or the editor itself restarts it cannot be told.`, ["editor_status says when it has, under greeting while it has not", "Then ask for the restart again"]);
|
|
45689
45794
|
}
|
|
45690
45795
|
const mine = before.openedByAServer === true && before.projectPath !== undefined;
|
|
45691
45796
|
if (!mine && args["hidden"] === true) {
|
|
@@ -46300,7 +46405,7 @@ class GodotServer {
|
|
|
46300
46405
|
game.brokeOn = halt.text;
|
|
46301
46406
|
game.log.record("error", halt.text);
|
|
46302
46407
|
}
|
|
46303
|
-
async untilTheEditorStopsPlaying(withinMs =
|
|
46408
|
+
async untilTheEditorStopsPlaying(withinMs = stopWaitMs()) {
|
|
46304
46409
|
const deadline = Date.now() + withinMs;
|
|
46305
46410
|
for (;; ) {
|
|
46306
46411
|
if ((await this.editorPlayingState())?.playing === false) {
|
|
@@ -46394,7 +46499,7 @@ class GodotServer {
|
|
|
46394
46499
|
running.endedThrough = "signal";
|
|
46395
46500
|
}
|
|
46396
46501
|
clearTheRecord();
|
|
46397
|
-
running.log.record(gone ? "info" : "warning", declined !== null ? `gdharness could not signal pid ${running.pid}: the operating system refused it (${declined}). It had described that process as ${described}.` : !gone ? `gdharness signalled pid ${running.pid}, which the operating system described as ${described}, and it was still running ${
|
|
46502
|
+
running.log.record(gone ? "info" : "warning", declined !== null ? `gdharness could not signal pid ${running.pid}: the operating system refused it (${declined}). It had described that process as ${described}.` : !gone ? `gdharness signalled pid ${running.pid}, which the operating system described as ${described}, and it was still running ${stopWaitMs() / 1000} seconds later.` : landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
|
|
46398
46503
|
return gone ? "gone" : "lingering";
|
|
46399
46504
|
}
|
|
46400
46505
|
async endThroughTheKeeper(run, pid, keeper) {
|
|
@@ -46414,7 +46519,7 @@ class GodotServer {
|
|
|
46414
46519
|
run.exitCode = null;
|
|
46415
46520
|
run.exitSignal = "SIGTERM";
|
|
46416
46521
|
}
|
|
46417
|
-
run.log.record(gone ? "info" : "warning", !gone ? `gdharness had the keeper holding pid ${pid} end it, and it was still running ${
|
|
46522
|
+
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.`);
|
|
46418
46523
|
return gone ? "gone" : "lingering";
|
|
46419
46524
|
}
|
|
46420
46525
|
spawnGame(godotPath, cmdArgs, env) {
|
|
@@ -47339,7 +47444,7 @@ class GodotServer {
|
|
|
47339
47444
|
...landed ? { endedThrough: "signal" } : {},
|
|
47340
47445
|
...childrenFields(ended),
|
|
47341
47446
|
exitedBeforeStop: !landed,
|
|
47342
|
-
note: `${!gone ? `pid ${pid} was told to end and was still running ${
|
|
47447
|
+
note: `${!gone ? `pid ${pid} was told to end and was still running ${stopWaitMs() / 1000} seconds later, so it has not stopped: a game of this project that no server here was holding,` : landed ? `pid ${pid} was ended: a game of this project that no server here was holding,` : `pid ${pid} had already gone when it was signalled: a game of this project that no server here was holding,`} identified by its announcement and by its command line, ${read.executable} run with --path ${read.projectPath ?? ""}. Nothing here read what it printed or holds its exit code, so neither is in this answer.${gone ? "" : " editor_status says when it has gone."} ${aboutTheChildren(ended, false)}`
|
|
47343
47448
|
});
|
|
47344
47449
|
}
|
|
47345
47450
|
async whatTheRunStarted(run) {
|
|
@@ -48075,7 +48180,8 @@ class GodotServer {
|
|
|
48075
48180
|
}
|
|
48076
48181
|
return `The game this server started (${which}) is still coming up and has not announced its runtime after a wait of ${Math.min(budgetMs, remaining)}ms, so there is nothing to talk to yet. It is still running: ask again in a moment rather than starting another.`;
|
|
48077
48182
|
}
|
|
48078
|
-
async handleRuntimeCommand(command, args,
|
|
48183
|
+
async handleRuntimeCommand(command, args, asked) {
|
|
48184
|
+
const timeoutMs = asked.patience ?? this.runtimeTimeoutMs();
|
|
48079
48185
|
const { op: _op, projectPath, pid, outputPath, ...params } = asParams(args);
|
|
48080
48186
|
const waited = typeof pid === "number" ? null : await this.ownRunOnItsWay(typeof projectPath === "string" ? projectPath : this.ownProject, timeoutMs);
|
|
48081
48187
|
if (waited !== null) {
|
|
@@ -48128,7 +48234,7 @@ ${staleNote}`);
|
|
|
48128
48234
|
requestId: reply.requestId,
|
|
48129
48235
|
command,
|
|
48130
48236
|
waitedMs: timeoutMs,
|
|
48131
|
-
note: reply.message,
|
|
48237
|
+
note: `${reply.message} ${longerWaitNote(asked.tool, asked.op, timeoutMs)}`,
|
|
48132
48238
|
...answeredBy,
|
|
48133
48239
|
...stale
|
|
48134
48240
|
});
|
|
@@ -48228,13 +48334,13 @@ ${staleNote}`}`
|
|
|
48228
48334
|
limit: readPositiveNumber(args, "limit") ?? 100,
|
|
48229
48335
|
...property === undefined ? {} : { property },
|
|
48230
48336
|
...includeHidden === undefined ? {} : { include_hidden: includeHidden }
|
|
48231
|
-
}, patience);
|
|
48337
|
+
}, { tool: "runtime_inspect", op: "find", patience });
|
|
48232
48338
|
}
|
|
48233
48339
|
async handleRuntimeWait(op, args) {
|
|
48234
48340
|
if (op === "frames") {
|
|
48235
48341
|
const frames = readPositiveNumber(args, "frames") ?? 1;
|
|
48236
48342
|
const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
|
|
48237
|
-
return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, waited);
|
|
48343
|
+
return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, { tool: "runtime_wait", op, patience: waited });
|
|
48238
48344
|
}
|
|
48239
48345
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
|
|
48240
48346
|
const asked = readNonEmptyString(args, "nodePath");
|
|
@@ -48248,7 +48354,7 @@ ${staleNote}`}`
|
|
|
48248
48354
|
path: nodePath,
|
|
48249
48355
|
signal: readString(args, "signal") ?? "",
|
|
48250
48356
|
timeout_ms: timeoutMs
|
|
48251
|
-
}, patience) : await this.handleRuntimeCommand("wait_until", {
|
|
48357
|
+
}, { tool: "runtime_wait", op, patience }) : await this.handleRuntimeCommand("wait_until", {
|
|
48252
48358
|
...whichGame(args),
|
|
48253
48359
|
path: nodePath,
|
|
48254
48360
|
property: readString(args, "property") ?? "",
|
|
@@ -48256,7 +48362,7 @@ ${staleNote}`}`
|
|
|
48256
48362
|
...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
|
|
48257
48363
|
...readBoolean(args, "includeHidden") === undefined ? {} : { include_hidden: readBoolean(args, "includeHidden") },
|
|
48258
48364
|
timeout_ms: timeoutMs
|
|
48259
|
-
}, patience);
|
|
48365
|
+
}, { tool: "runtime_wait", op, patience });
|
|
48260
48366
|
}
|
|
48261
48367
|
}
|
|
48262
48368
|
async function runGodotServer() {
|
|
@@ -48264,7 +48370,7 @@ async function runGodotServer() {
|
|
|
48264
48370
|
const server = new GodotServer;
|
|
48265
48371
|
await server.run();
|
|
48266
48372
|
}
|
|
48267
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2,
|
|
48373
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "';
|
|
48268
48374
|
var init_server3 = __esm(() => {
|
|
48269
48375
|
init_mcp();
|
|
48270
48376
|
init_stdio2();
|
|
@@ -34,6 +34,14 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
func _process(_delta: float) -> void:
|
|
37
|
+
_count_a_play_begun()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## Counts a play the editor has begun since the last look, whoever began it. Looked at every frame
|
|
41
|
+
## and before the count is read: read alone, a play begun in this frame went uncounted until this
|
|
42
|
+
## node's turn came round, and a server asking before then was told the earlier play's number with
|
|
43
|
+
## "playing", which it took for the earlier play still going.
|
|
44
|
+
func _count_a_play_begun() -> void:
|
|
37
45
|
var playing: bool = EditorInterface.is_playing_scene()
|
|
38
46
|
if playing and not _was_playing:
|
|
39
47
|
if _awaited:
|
|
@@ -231,6 +239,7 @@ static func _leave() -> void:
|
|
|
231
239
|
|
|
232
240
|
|
|
233
241
|
func playing_status(_args: Dictionary) -> Dictionary:
|
|
242
|
+
_count_a_play_begun()
|
|
234
243
|
var playing: bool = EditorInterface.is_playing_scene()
|
|
235
244
|
# The debugger's port with it, read now rather than remembered: it is taken again before every
|
|
236
245
|
# play, so the one from when this editor greeted the server is a number it has moved off.
|
|
@@ -17,6 +17,11 @@ const Read = preload("../reading.gd")
|
|
|
17
17
|
## the server keeps this below the queue the project sets; this is that for the engine's default.
|
|
18
18
|
const FRAME_FUNCTIONS: int = 1536
|
|
19
19
|
const DEBUGGER_CLASS: String = "ScriptEditorDebugger"
|
|
20
|
+
## What a quitting game sends after its profile's totals, and the answer it waits for. Kept in step
|
|
21
|
+
## with the runtime addon's runtime_autoload.gd.
|
|
22
|
+
const PROFILE_CAPTURE: String = "gdharness"
|
|
23
|
+
const PROFILE_SENT: String = "gdharness:profile_sent"
|
|
24
|
+
const PROFILE_RECEIVED: String = "gdharness:profile_received"
|
|
20
25
|
|
|
21
26
|
|
|
22
27
|
## The editor's debugger sessions, numbered as the editor numbers its debugger tabs.
|
|
@@ -32,6 +37,17 @@ class Sessions:
|
|
|
32
37
|
if joined != OK:
|
|
33
38
|
push_error("gdharness could not watch debugger session %d start: %d" % [session_id, joined])
|
|
34
39
|
|
|
40
|
+
func _has_capture(capture: String) -> bool:
|
|
41
|
+
return capture == PROFILE_CAPTURE
|
|
42
|
+
|
|
43
|
+
## Answers a quitting game that its profile's totals arrived, which it waits for before it exits.
|
|
44
|
+
## The editor reads a session's messages in order, so the totals sent before this have been read.
|
|
45
|
+
func _capture(message: String, _data: Array, session_id: int) -> bool:
|
|
46
|
+
if message != PROFILE_SENT:
|
|
47
|
+
return false
|
|
48
|
+
get_session(session_id).send_message(PROFILE_RECEIVED, [])
|
|
49
|
+
return true
|
|
50
|
+
|
|
35
51
|
|
|
36
52
|
var _editor_plugin: EditorPlugin = null
|
|
37
53
|
var _sessions: Sessions = null
|
|
@@ -27,9 +27,21 @@ const BIND_ADDRESS_SETTING: String = "gdharness/runtime/bind_address"
|
|
|
27
27
|
## expects. A fixed port is for a client that cannot read the announcement.
|
|
28
28
|
const PORT_SETTING: String = "gdharness/runtime/port"
|
|
29
29
|
|
|
30
|
-
##
|
|
31
|
-
##
|
|
32
|
-
|
|
30
|
+
## The debugger messages that confirm a profile's totals arrived: the game sends the first after
|
|
31
|
+
## them, and the debugger answers with the second once it has read that far. Kept in step with
|
|
32
|
+
## `PROFILE_SENT` and `PROFILE_RECEIVED` in src/script-profile.ts and the editor addon's
|
|
33
|
+
## profile_tools.gd.
|
|
34
|
+
const PROFILE_CAPTURE: String = "gdharness"
|
|
35
|
+
const PROFILE_SENT: String = "gdharness:profile_sent"
|
|
36
|
+
const PROFILE_RECEIVED: String = "gdharness:profile_received"
|
|
37
|
+
|
|
38
|
+
## The longest a quitting game waits for that answer. A debugger that never gives one, such as an
|
|
39
|
+
## editor without the gdharness addon, holds every profiled exit this long.
|
|
40
|
+
const PROFILE_CONFIRM_MS: int = 3000
|
|
41
|
+
|
|
42
|
+
## How many calls of `EngineDebugger.line_poll` read the debugger's messages once: it reads them
|
|
43
|
+
## on every 2048th call and only counts the others.
|
|
44
|
+
const POLLS_PER_READ: int = 2048
|
|
33
45
|
|
|
34
46
|
## How the engine is told to run a script instead of the game: `godot -s thing.gd`.
|
|
35
47
|
const SCRIPT_FLAGS: PackedStringArray = ["-s", "--script"]
|
|
@@ -90,6 +102,7 @@ var _commands: Dictionary = {}
|
|
|
90
102
|
## Empty when the markers cannot be read, as in a copy made by hand.
|
|
91
103
|
var _loaded_version: String = _read_marker(VERSION_MARKER)
|
|
92
104
|
var _loaded_digest: String = _read_marker(DIGEST_MARKER)
|
|
105
|
+
var _profile_received: bool = false
|
|
93
106
|
|
|
94
107
|
|
|
95
108
|
func _init() -> void:
|
|
@@ -125,12 +138,14 @@ func _ready() -> void:
|
|
|
125
138
|
# even be un-paused over the socket. A debug server has to stay responsive while the game
|
|
126
139
|
# is frozen, to inspect, capture, inject or resume it.
|
|
127
140
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
|
141
|
+
var watched: int = get_tree().root.tree_exiting.connect(_send_the_profile)
|
|
142
|
+
if watched != OK:
|
|
143
|
+
push_error("gdharness could not watch for the game quitting: %d" % watched)
|
|
128
144
|
_report_errors()
|
|
129
145
|
_start_server()
|
|
130
146
|
|
|
131
147
|
|
|
132
148
|
func _exit_tree() -> void:
|
|
133
|
-
_send_the_profile()
|
|
134
149
|
if _errors != null:
|
|
135
150
|
OS.remove_logger(_errors)
|
|
136
151
|
_errors.close()
|
|
@@ -138,16 +153,51 @@ func _exit_tree() -> void:
|
|
|
138
153
|
_cleanup()
|
|
139
154
|
|
|
140
155
|
|
|
141
|
-
## A profiled run's totals, sent as the game
|
|
156
|
+
## A profiled run's totals, sent as the game quits.
|
|
142
157
|
##
|
|
143
158
|
## The engine sends its script profiler's totals only when the profiler is switched off, and at
|
|
144
159
|
## exit it is torn down without being switched off, so a scene that did its work in _ready and
|
|
145
|
-
## quit took its whole profile with it, last frame included.
|
|
146
|
-
##
|
|
160
|
+
## quit took its whole profile with it, last frame included.
|
|
161
|
+
##
|
|
162
|
+
## The totals go out on the debugger's own thread, which stops without emptying its queue, so the
|
|
163
|
+
## game waits until the debugger says it has read them. A fixed wait of 100 ms lost the totals of
|
|
164
|
+
## games quitting under load, whose debugger thread did not get its turn in time.
|
|
165
|
+
##
|
|
166
|
+
## On the root leaving rather than this node: the root leaves only as the game quits, and after
|
|
167
|
+
## every other node's _exit_tree, so what those do is counted. A project that sent this node away
|
|
168
|
+
## mid-run had the profiler switched off then, and its run answered as complete with none of the
|
|
169
|
+
## frames after it.
|
|
147
170
|
func _send_the_profile() -> void:
|
|
148
|
-
if EngineDebugger.is_active() and EngineDebugger.is_profiling("servers"):
|
|
149
|
-
|
|
150
|
-
|
|
171
|
+
if not (EngineDebugger.is_active() and EngineDebugger.is_profiling("servers")):
|
|
172
|
+
return
|
|
173
|
+
_profile_received = false
|
|
174
|
+
if not EngineDebugger.has_capture(PROFILE_CAPTURE):
|
|
175
|
+
EngineDebugger.register_message_capture(PROFILE_CAPTURE, _on_debugger_message)
|
|
176
|
+
EngineDebugger.profiler_enable("servers", false)
|
|
177
|
+
EngineDebugger.send_message(PROFILE_SENT, [])
|
|
178
|
+
var deadline: int = Time.get_ticks_msec() + PROFILE_CONFIRM_MS
|
|
179
|
+
while not _profile_received and Time.get_ticks_msec() < deadline:
|
|
180
|
+
for _poll: int in POLLS_PER_READ:
|
|
181
|
+
EngineDebugger.line_poll()
|
|
182
|
+
OS.delay_msec(5)
|
|
183
|
+
EngineDebugger.unregister_message_capture(PROFILE_CAPTURE)
|
|
184
|
+
if not _profile_received:
|
|
185
|
+
print(
|
|
186
|
+
(
|
|
187
|
+
(
|
|
188
|
+
"[gdharness] the debugger did not confirm the profiler's totals within %d ms, so the"
|
|
189
|
+
+ " profile may be the frames that arrived without them"
|
|
190
|
+
)
|
|
191
|
+
% PROFILE_CONFIRM_MS
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
func _on_debugger_message(message: String, _data: Array) -> bool:
|
|
197
|
+
if PROFILE_CAPTURE + ":" + message == PROFILE_RECEIVED:
|
|
198
|
+
_profile_received = true
|
|
199
|
+
return true
|
|
200
|
+
return false
|
|
151
201
|
|
|
152
202
|
|
|
153
203
|
## The engine's error reports, written to a file for the server, for a game the editor plays, and
|
|
@@ -331,6 +331,7 @@ func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_
|
|
|
331
331
|
var looks: int = 0
|
|
332
332
|
var looked_usec: int = 0
|
|
333
333
|
var next_look: int = 0
|
|
334
|
+
var looked_at_frame: int = 0
|
|
334
335
|
var found: bool = false
|
|
335
336
|
while true:
|
|
336
337
|
if Time.get_ticks_usec() >= next_look:
|
|
@@ -340,13 +341,19 @@ func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_
|
|
|
340
341
|
var took: int = Time.get_ticks_usec() - looking
|
|
341
342
|
looked_usec += took
|
|
342
343
|
next_look = Time.get_ticks_usec() + took * LOOKS_APART
|
|
344
|
+
looked_at_frame = frames
|
|
343
345
|
if found or Time.get_ticks_msec() - started >= timeout_ms:
|
|
344
346
|
break
|
|
345
347
|
await _host.get_tree().process_frame
|
|
346
348
|
frames += 1
|
|
347
349
|
# One more look at the end, so words that arrived between two paced looks are not answered as
|
|
348
|
-
# never having come.
|
|
350
|
+
# never having come. Taken once a whole frame has run since the last look, which is two frame
|
|
351
|
+
# starts: a first look that outlasted the timeout otherwise left none, so the wait answered with
|
|
352
|
+
# the game not having run at all, and a command sent during that look not yet read.
|
|
349
353
|
if not found:
|
|
354
|
+
while frames - looked_at_frame < 2:
|
|
355
|
+
await _host.get_tree().process_frame
|
|
356
|
+
frames += 1
|
|
350
357
|
var last: int = Time.get_ticks_usec()
|
|
351
358
|
found = _anything_says(node_path, said, include_hidden)
|
|
352
359
|
looks += 1
|
package/build/index.js
CHANGED
|
@@ -25964,7 +25964,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
|
|
|
25964
25964
|
ok: false,
|
|
25965
25965
|
reason: "busy",
|
|
25966
25966
|
requestId: id,
|
|
25967
|
-
message: `${still} Nothing was cancelled: a call that takes longer than the wait goes on and does everything it was asked, so read the state back rather than repeating it. runtime_invoke op result with requestId ${id} answers with its reply once it comes
|
|
25967
|
+
message: `${still} Nothing was cancelled: a call that takes longer than the wait goes on and does everything it was asked, so read the state back rather than repeating it. runtime_invoke op result with requestId ${id} answers with its reply once it comes. A game held at a breakpoint instead is one debug_state stack answers about.`
|
|
25968
25968
|
});
|
|
25969
25969
|
}, timeoutMs);
|
|
25970
25970
|
socket.setEncoding("utf8");
|
|
@@ -26552,6 +26552,67 @@ function enclosingFunction(source, line, code = codeOf(source)) {
|
|
|
26552
26552
|
}
|
|
26553
26553
|
return null;
|
|
26554
26554
|
}
|
|
26555
|
+
var SUPER_CALL = /(?<![\p{L}\p{N}_.])super\s*[.(]/u;
|
|
26556
|
+
var LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
|
|
26557
|
+
function afterHeader(text, from) {
|
|
26558
|
+
let depth = 0;
|
|
26559
|
+
for (let at = text.indexOf("(", from);at !== -1 && at < text.length; at += 1) {
|
|
26560
|
+
if (text[at] === "(") {
|
|
26561
|
+
depth += 1;
|
|
26562
|
+
} else if (text[at] === ")") {
|
|
26563
|
+
depth -= 1;
|
|
26564
|
+
if (depth === 0) {
|
|
26565
|
+
const colon = text.indexOf(":", at);
|
|
26566
|
+
return colon === -1 ? -1 : colon + 1;
|
|
26567
|
+
}
|
|
26568
|
+
}
|
|
26569
|
+
}
|
|
26570
|
+
return -1;
|
|
26571
|
+
}
|
|
26572
|
+
function bodyCallsSuper(code, line, lambda) {
|
|
26573
|
+
const lines = code.split(`
|
|
26574
|
+
`);
|
|
26575
|
+
const first = lines[line - 1];
|
|
26576
|
+
if (first === undefined) {
|
|
26577
|
+
return false;
|
|
26578
|
+
}
|
|
26579
|
+
const header = lambda ? LAMBDA_HEADER.exec(first) : FUNC_LINE.exec(first);
|
|
26580
|
+
if (header !== null) {
|
|
26581
|
+
const body = afterHeader(first, header.index);
|
|
26582
|
+
if (body !== -1 && first.slice(body).trim() !== "") {
|
|
26583
|
+
return SUPER_CALL.test(first.slice(body));
|
|
26584
|
+
}
|
|
26585
|
+
}
|
|
26586
|
+
const indent = indentOf(first);
|
|
26587
|
+
let lambdaIndent = null;
|
|
26588
|
+
for (let index = line - 1;index < lines.length; index += 1) {
|
|
26589
|
+
const text = lines[index] ?? "";
|
|
26590
|
+
if (text.trim() === "") {
|
|
26591
|
+
continue;
|
|
26592
|
+
}
|
|
26593
|
+
const at = indentOf(text);
|
|
26594
|
+
if (at < indent) {
|
|
26595
|
+
break;
|
|
26596
|
+
}
|
|
26597
|
+
if (lambdaIndent !== null) {
|
|
26598
|
+
if (at > lambdaIndent) {
|
|
26599
|
+
continue;
|
|
26600
|
+
}
|
|
26601
|
+
lambdaIndent = null;
|
|
26602
|
+
}
|
|
26603
|
+
const inner = LAMBDA_HEADER.exec(text);
|
|
26604
|
+
if (SUPER_CALL.test(inner === null ? text : text.slice(0, inner.index))) {
|
|
26605
|
+
return true;
|
|
26606
|
+
}
|
|
26607
|
+
if (inner !== null) {
|
|
26608
|
+
const body = afterHeader(text, inner.index);
|
|
26609
|
+
if (body !== -1 && text.slice(body).trim() === "") {
|
|
26610
|
+
lambdaIndent = at;
|
|
26611
|
+
}
|
|
26612
|
+
}
|
|
26613
|
+
}
|
|
26614
|
+
return false;
|
|
26615
|
+
}
|
|
26555
26616
|
function callSitesOf(source, name) {
|
|
26556
26617
|
const regions = regionsOf(source);
|
|
26557
26618
|
const code = codeOf(source, regions);
|
|
@@ -33123,25 +33184,36 @@ function signatureParts(signature) {
|
|
|
33123
33184
|
}
|
|
33124
33185
|
var ms = (seconds) => Math.round(seconds * 1e6) / 1000;
|
|
33125
33186
|
function profiledFunctions(totals, limit, sourceOf = () => null) {
|
|
33126
|
-
const rows = rowsOf(totals);
|
|
33187
|
+
const rows = rowsOf(totals, sourceOf);
|
|
33127
33188
|
const scriptMs = ms(Object.values(totals.functions).reduce((sum, one) => sum + (one.calls > 0 ? one.selfSeconds : 0), 0));
|
|
33128
33189
|
return {
|
|
33129
33190
|
functions: rows.slice(0, limit).map((row) => placedLambda(row, sourceOf)),
|
|
33130
33191
|
omitted: Math.max(0, rows.length - limit),
|
|
33131
33192
|
scriptMs,
|
|
33132
|
-
counted: rows.length
|
|
33193
|
+
counted: rows.length,
|
|
33194
|
+
superCallers: rows.filter((row) => row.selfIncludesSuper === true).length
|
|
33133
33195
|
};
|
|
33134
33196
|
}
|
|
33135
|
-
function rowsOf(totals) {
|
|
33197
|
+
function rowsOf(totals, sourceOf) {
|
|
33198
|
+
const codes = new Map;
|
|
33199
|
+
const codeOfScript = (script) => {
|
|
33200
|
+
if (!codes.has(script)) {
|
|
33201
|
+
const source = sourceOf(script);
|
|
33202
|
+
codes.set(script, source === null ? null : codeOf(source));
|
|
33203
|
+
}
|
|
33204
|
+
return codes.get(script) ?? null;
|
|
33205
|
+
};
|
|
33136
33206
|
return Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
|
|
33137
33207
|
const parts = signatureParts(signature);
|
|
33208
|
+
const code = codeOfScript(parts.script);
|
|
33138
33209
|
return {
|
|
33139
33210
|
script: parts.script,
|
|
33140
33211
|
function: parts.function,
|
|
33141
33212
|
line: parts.line,
|
|
33142
33213
|
calls: sum.calls,
|
|
33143
33214
|
selfMs: ms(sum.selfSeconds),
|
|
33144
|
-
totalMs: ms(sum.totalSeconds)
|
|
33215
|
+
totalMs: ms(sum.totalSeconds),
|
|
33216
|
+
...code !== null && bodyCallsSuper(code, parts.line, parts.function.endsWith("(lambda)")) ? { selfIncludesSuper: true } : {}
|
|
33145
33217
|
};
|
|
33146
33218
|
}).sort((a, b) => b.selfMs - a.selfMs || b.totalMs - a.totalMs || a.function.localeCompare(b.function));
|
|
33147
33219
|
}
|
|
@@ -33158,17 +33230,24 @@ function placedLambda(row, sourceOf) {
|
|
|
33158
33230
|
}
|
|
33159
33231
|
var CALL_SITES = 40;
|
|
33160
33232
|
function callersOf(totals, name, scripts) {
|
|
33161
|
-
const
|
|
33233
|
+
const listed = [...scripts];
|
|
33234
|
+
const sources = new Map(listed);
|
|
33235
|
+
const rows = rowsOf(totals, (script) => sources.get(script) ?? null);
|
|
33162
33236
|
const bare = bareName(name);
|
|
33163
33237
|
const target = rows.filter((row) => name.includes(".") ? row.function === name : bareName(row.function) === bare);
|
|
33164
33238
|
const sites = [];
|
|
33165
|
-
for (const [script, source] of
|
|
33239
|
+
for (const [script, source] of listed) {
|
|
33166
33240
|
for (const site of callSitesOf(source, bare)) {
|
|
33167
33241
|
const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
|
|
33168
33242
|
sites.push({
|
|
33169
33243
|
script,
|
|
33170
33244
|
...site,
|
|
33171
|
-
...within === undefined ? {} : {
|
|
33245
|
+
...within === undefined ? {} : {
|
|
33246
|
+
withinCalls: within.calls,
|
|
33247
|
+
withinSelfMs: within.selfMs,
|
|
33248
|
+
withinTotalMs: within.totalMs,
|
|
33249
|
+
...within.selfIncludesSuper === true ? { withinSelfIncludesSuper: true } : {}
|
|
33250
|
+
}
|
|
33172
33251
|
});
|
|
33173
33252
|
}
|
|
33174
33253
|
}
|
|
@@ -34199,7 +34278,7 @@ var TOOL_SPECS = [
|
|
|
34199
34278
|
},
|
|
34200
34279
|
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
|
|
34201
34280
|
profile: {
|
|
34202
|
-
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 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",
|
|
34281
|
+
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",
|
|
34203
34282
|
requires: []
|
|
34204
34283
|
}
|
|
34205
34284
|
},
|
|
@@ -34828,10 +34907,15 @@ var FEEDBACK_NOTICE_EVERY = 250;
|
|
|
34828
34907
|
var run4 = promisify5(execFile5);
|
|
34829
34908
|
var __dirname2 = dirname15(fileURLToPath4(import.meta.url));
|
|
34830
34909
|
function editorRestartTimeoutMs() {
|
|
34831
|
-
|
|
34832
|
-
|
|
34910
|
+
return waitFromEnv("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS", 90000);
|
|
34911
|
+
}
|
|
34912
|
+
function waitFromEnv(variable, fallback) {
|
|
34913
|
+
const override = Number.parseInt(envValue(variable) ?? "", 10);
|
|
34914
|
+
return Number.isInteger(override) && override > 0 ? override : fallback;
|
|
34915
|
+
}
|
|
34916
|
+
function greetingWaitMs() {
|
|
34917
|
+
return waitFromEnv("GDHARNESS_GREETING_WAIT_MS", 1e4);
|
|
34833
34918
|
}
|
|
34834
|
-
var GREETING_WAIT_MS = 1e4;
|
|
34835
34919
|
function restartUnderway(note, now = Date.now()) {
|
|
34836
34920
|
return alive(note.byPid) && now - Date.parse(note.quitAt) < editorRestartTimeoutMs() + 30000;
|
|
34837
34921
|
}
|
|
@@ -34883,6 +34967,14 @@ function captureDetail(payload) {
|
|
|
34883
34967
|
function patienceForFrames(frames, atLeast) {
|
|
34884
34968
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
34885
34969
|
}
|
|
34970
|
+
function longerWaitNote(tool, op, waitedMs) {
|
|
34971
|
+
const spec = TOOL_SPECS.find((one) => one.name === tool);
|
|
34972
|
+
if (spec !== undefined && argumentsOf(spec, op).includes("timeoutMs")) {
|
|
34973
|
+
return "timeoutMs waits longer next time.";
|
|
34974
|
+
}
|
|
34975
|
+
const how = tool === "runtime_wait" && op === "frames" ? `the wait is counted from the number of frames, at ${SLOWEST_FRAME_RATE} a second on top of GDHARNESS_RUNTIME_TIMEOUT_MS, and this game fell below that rate, so ask for fewer frames at a time` : "the wait is GDHARNESS_RUNTIME_TIMEOUT_MS, which is set in the environment the server is started with";
|
|
34976
|
+
return `${tool} ${op} takes no timeoutMs: it was given ${waitedMs}ms, and ${how}.`;
|
|
34977
|
+
}
|
|
34886
34978
|
function projectSpelling(projectPath, absolutePath) {
|
|
34887
34979
|
return `res://${relative4(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
|
|
34888
34980
|
}
|
|
@@ -34963,10 +35055,12 @@ function runIsUp(run, editorSays, editorPlay = null, now = Date.now()) {
|
|
|
34963
35055
|
function noCodeWillCome(run) {
|
|
34964
35056
|
return !exited(run);
|
|
34965
35057
|
}
|
|
34966
|
-
|
|
35058
|
+
function stopWaitMs() {
|
|
35059
|
+
return waitFromEnv("GDHARNESS_STOP_WAIT_MS", 1e4);
|
|
35060
|
+
}
|
|
34967
35061
|
var KEEPER_ANSWER_MS = 15000;
|
|
34968
35062
|
var EXIT_REPORTED_WITHIN_MS = 2000;
|
|
34969
|
-
async function untilGone(pids, withinMs =
|
|
35063
|
+
async function untilGone(pids, withinMs = stopWaitMs()) {
|
|
34970
35064
|
const gone = () => pids.every((pid) => !alive(pid));
|
|
34971
35065
|
const deadline = Date.now() + withinMs;
|
|
34972
35066
|
while (!gone() && Date.now() < deadline) {
|
|
@@ -35021,7 +35115,7 @@ function stopVerdict(stop) {
|
|
|
35021
35115
|
return {
|
|
35022
35116
|
stopped: false,
|
|
35023
35117
|
stillGoing: stop.endedPid,
|
|
35024
|
-
note: `${told} ${
|
|
35118
|
+
note: `${told} ${stopWaitMs() / 1000} seconds later, so it has not stopped. editor_status says when it has gone, and editor_run stop asks again.`,
|
|
35025
35119
|
withChildren: true
|
|
35026
35120
|
};
|
|
35027
35121
|
}
|
|
@@ -35140,7 +35234,7 @@ function endedToStartThis(ended) {
|
|
|
35140
35234
|
return ` ${which} was not ended here: the editor could not be asked to stop it. The editor stops a play before starting the next, so it is not running beside this one if this start reached the editor.`;
|
|
35141
35235
|
}
|
|
35142
35236
|
if (ended.ending === "lingering") {
|
|
35143
|
-
return ` ${which} was told to end to start this one and was still running ${
|
|
35237
|
+
return ` ${which} was told to end to start this one and was still running ${stopWaitMs() / 1000} seconds later, so it is running beside this one: editor_status says when it has gone.`;
|
|
35144
35238
|
}
|
|
35145
35239
|
return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
|
|
35146
35240
|
}
|
|
@@ -35272,7 +35366,9 @@ var BOUNDARY_WAIT_MS = 2000;
|
|
|
35272
35366
|
var HANDOVER_MS = SUCCESSOR_CHECK_MS + 5000;
|
|
35273
35367
|
var HANDOVER_POLL_MS = 250;
|
|
35274
35368
|
var CACHE_WRITE_MS = 2000;
|
|
35275
|
-
|
|
35369
|
+
function scanWaitMs() {
|
|
35370
|
+
return waitFromEnv("GDHARNESS_SCAN_WAIT_MS", 30000);
|
|
35371
|
+
}
|
|
35276
35372
|
var STREAMS_GRACE_MS = 2000;
|
|
35277
35373
|
function scanWaitIn(payload) {
|
|
35278
35374
|
const waited = readNumber(payload, "waitedForEditorScanMs");
|
|
@@ -35332,7 +35428,7 @@ function scanWaitAnswer(scanned, started = "game") {
|
|
|
35332
35428
|
if (scanned.waitedMs === 0) {
|
|
35333
35429
|
return {};
|
|
35334
35430
|
}
|
|
35335
|
-
const still = `The editor was still scanning the project after ${
|
|
35431
|
+
const still = `The editor was still scanning the project after ${scanWaitMs() / 1000} seconds, so`;
|
|
35336
35432
|
return {
|
|
35337
35433
|
waitedForEditorScanMs: scanned.waitedMs,
|
|
35338
35434
|
scanNote: !scanned.stillScanning ? undefined : started === "game" ? `${still} this game started during the scan and may have read a class cache being rewritten. If ` + 'editor_output shows "Could not find type" for a class that is declared, editor_rescan ' + "waits for the scan to finish, and a start after it reads the finished cache." : `${still} the engine answering this started during the scan and may have read a class cache being ` + "rewritten, and a class cache it wrote can be written over when the scan finishes. If " + 'engine_messages shows "Could not find type" for a class that is declared, editor_rescan waits ' + "for the scan to finish, and the same call after it reads the finished cache."
|
|
@@ -35634,14 +35730,20 @@ function profileAnswer(reading, limit) {
|
|
|
35634
35730
|
const table = profiledFunctions(totals, limit, reading.sourceOf);
|
|
35635
35731
|
const notes = [];
|
|
35636
35732
|
const addonNote = reading.runtimeAddon ? "" : " This project has no gdharness runtime addon, which switches the profiler off as the game quits so the engine sends its totals: setup installs it.";
|
|
35637
|
-
|
|
35733
|
+
const totalsWithheld = reading.runtimeAddon ? " The runtime addon switches the profiler off as the game quits, and did not here: either it was no longer running in this game by then, which a project can arrange for some scenes; or the game ended without taking its tree down, as a crash or a kill does; or the totals did not reach the debugger before the game exited, which the game then says in its output." : addonNote;
|
|
35734
|
+
if (totals.complete && reading.running) {
|
|
35735
|
+
notes.push(`The engine's totals arrived while the game is still running, which it sends only when the profiler is switched off, so ${reading.through === "editor" ? "the editor's Profiler tab or the game itself, through EngineDebugger," : "the game itself, through EngineDebugger,"} switched it off before the game ended. They cover the run up to then and nothing after.`);
|
|
35736
|
+
} else if (totals.complete) {} else if (reading.running && totals.frames === 0) {
|
|
35638
35737
|
notes.push(`The profiler is on and the game has not finished its first frame yet: a frame is sent as it ends, so a scene doing its work in _ready shows nothing until that returns, and the engine's own totals arrive when the game quits.${addonNote}`);
|
|
35639
35738
|
} else if (reading.running) {
|
|
35640
35739
|
notes.push(`The run is still going, so these are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} summed so far${reading.through === "gdharness" ? ", written at most half a second behind" : ""}.`);
|
|
35641
35740
|
} else if (totals.frames === 0) {
|
|
35642
|
-
notes.push(`The game connected and ended before a frame of the profile reached ${reading.through === "editor" ? "the editor" : "this server"}: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${
|
|
35741
|
+
notes.push(`The game connected and ended before a frame of the profile reached ${reading.through === "editor" ? "the editor" : "this server"}: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${totalsWithheld}`);
|
|
35643
35742
|
} else {
|
|
35644
|
-
notes.push(`These are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} that arrived, summed, without the engine's own totals,
|
|
35743
|
+
notes.push(`These are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} that arrived, summed, without the engine's own totals, which it sends only when the profiler is switched off.${totalsWithheld} Not in them is any script time after the last frame sent: a frame still queued as the game exited, since the engine stops sending without emptying its queue, and what scripts did as it quit, such as in _exit_tree. That can be nothing: a scene that does its work in _ready and quits has all of it in its first frame.`);
|
|
35744
|
+
}
|
|
35745
|
+
if (table.superCallers > 0) {
|
|
35746
|
+
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.`);
|
|
35645
35747
|
}
|
|
35646
35748
|
notes.push(...cappedNotes(totals));
|
|
35647
35749
|
if (totals.unreadable > 0) {
|
|
@@ -36381,7 +36483,7 @@ class GodotServer {
|
|
|
36381
36483
|
}
|
|
36382
36484
|
async dispatch(tool, op, args) {
|
|
36383
36485
|
if (tool !== "editor_status" && this.godotBridge.isArriving()) {
|
|
36384
|
-
await this.waitForBridge(() => !this.godotBridge.isArriving(), Date.now() +
|
|
36486
|
+
await this.waitForBridge(() => !this.godotBridge.isArriving(), Date.now() + greetingWaitMs());
|
|
36385
36487
|
}
|
|
36386
36488
|
if (ENGINE_PASSES[tool]?.[op] !== undefined) {
|
|
36387
36489
|
return op === "reimport" ? await this.handleReimport(args) : await this.handleRefreshUids(args);
|
|
@@ -36541,7 +36643,7 @@ class GodotServer {
|
|
|
36541
36643
|
depth: depth ?? 3,
|
|
36542
36644
|
include_properties: readBoolean(args, "includeProperties") ?? false,
|
|
36543
36645
|
properties: readArray(args, "properties") ?? []
|
|
36544
|
-
}, patience);
|
|
36646
|
+
}, { tool: "runtime_inspect", op, patience });
|
|
36545
36647
|
}
|
|
36546
36648
|
case "find":
|
|
36547
36649
|
return await this.handleFindRuntimeNodes(args, patience);
|
|
@@ -36551,17 +36653,17 @@ class GodotServer {
|
|
|
36551
36653
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
36552
36654
|
include_hidden: readBoolean(args, "includeHidden") ?? false,
|
|
36553
36655
|
limit: readPositiveNumber(args, "limit") ?? 500
|
|
36554
|
-
}, patience);
|
|
36656
|
+
}, { tool: "runtime_inspect", op, patience });
|
|
36555
36657
|
case "rect":
|
|
36556
|
-
return await this.handleRuntimeCommand("get_rect", { ...whichGame(args), path: readNonEmptyString(args, "nodePath") ?? "" }, patience);
|
|
36658
|
+
return await this.handleRuntimeCommand("get_rect", { ...whichGame(args), path: readNonEmptyString(args, "nodePath") ?? "" }, { tool: "runtime_inspect", op, patience });
|
|
36557
36659
|
case "property":
|
|
36558
36660
|
return await this.handleRuntimeCommand("get_property", {
|
|
36559
36661
|
...whichGame(args),
|
|
36560
36662
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
36561
36663
|
property: readNonEmptyString(args, "property") ?? ""
|
|
36562
|
-
}, patience);
|
|
36664
|
+
}, { tool: "runtime_inspect", op, patience });
|
|
36563
36665
|
default:
|
|
36564
|
-
return await this.handleRuntimeCommand("get_metrics", { ...whichGame(args), metrics: readArray(args, "metrics") ?? [] }, patience);
|
|
36666
|
+
return await this.handleRuntimeCommand("get_metrics", { ...whichGame(args), metrics: readArray(args, "metrics") ?? [] }, { tool: "runtime_inspect", op, patience });
|
|
36565
36667
|
}
|
|
36566
36668
|
}
|
|
36567
36669
|
case "runtime_invoke": {
|
|
@@ -36574,16 +36676,16 @@ class GodotServer {
|
|
|
36574
36676
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
36575
36677
|
property: readString(args, "property") ?? "",
|
|
36576
36678
|
value: args["value"]
|
|
36577
|
-
}, patience) : await this.handleRuntimeCommand("call_method", {
|
|
36679
|
+
}, { tool: "runtime_invoke", op, patience }) : await this.handleRuntimeCommand("call_method", {
|
|
36578
36680
|
...whichGame(args),
|
|
36579
36681
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
36580
36682
|
method: readString(args, "method") ?? "",
|
|
36581
36683
|
args: readArray(args, "args") ?? [],
|
|
36582
36684
|
...args["properties"] === undefined ? {} : { properties: args["properties"] }
|
|
36583
|
-
}, patience);
|
|
36685
|
+
}, { tool: "runtime_invoke", op, patience });
|
|
36584
36686
|
}
|
|
36585
36687
|
case "runtime_capture":
|
|
36586
|
-
return await this.handleRuntimeCommand(op === "screenshot" ? "capture_screenshot" : "capture_viewport", args);
|
|
36688
|
+
return await this.handleRuntimeCommand(op === "screenshot" ? "capture_screenshot" : "capture_viewport", args, { tool: "runtime_capture", op });
|
|
36587
36689
|
case "runtime_input":
|
|
36588
36690
|
if (op === "click") {
|
|
36589
36691
|
if (readNonEmptyString(args, "nodePath") === undefined && readNonEmptyString(args, "says") === undefined) {
|
|
@@ -36596,7 +36698,7 @@ class GodotServer {
|
|
|
36596
36698
|
double: readBoolean(args, "doubleClick") ?? false,
|
|
36597
36699
|
...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
|
|
36598
36700
|
...args["index"] === undefined ? {} : { index: args["index"] }
|
|
36599
|
-
});
|
|
36701
|
+
}, { tool: "runtime_input", op });
|
|
36600
36702
|
}
|
|
36601
36703
|
if (op === "choose") {
|
|
36602
36704
|
return await this.handleRuntimeCommand("choose", {
|
|
@@ -36604,9 +36706,9 @@ class GodotServer {
|
|
|
36604
36706
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
36605
36707
|
text: readString(args, "text") ?? "",
|
|
36606
36708
|
...args["index"] === undefined ? {} : { index: args["index"] }
|
|
36607
|
-
});
|
|
36709
|
+
}, { tool: "runtime_input", op });
|
|
36608
36710
|
}
|
|
36609
|
-
return await this.handleRuntimeCommand(`inject_${op}`, args);
|
|
36711
|
+
return await this.handleRuntimeCommand(`inject_${op}`, args, { tool: "runtime_input", op });
|
|
36610
36712
|
case "runtime_wait":
|
|
36611
36713
|
return await this.handleRuntimeWait(op, args);
|
|
36612
36714
|
case "debug_breakpoint": {
|
|
@@ -36835,7 +36937,7 @@ class GodotServer {
|
|
|
36835
36937
|
const answered = Date.now();
|
|
36836
36938
|
if (status["scanning"] === true || status["importing"] === true || status["pending"] === true) {
|
|
36837
36939
|
sawAScan = true;
|
|
36838
|
-
if (Date.now() - started >=
|
|
36940
|
+
if (Date.now() - started >= scanWaitMs()) {
|
|
36839
36941
|
return { waitedMs: Date.now() - started, stillScanning: true };
|
|
36840
36942
|
}
|
|
36841
36943
|
await new Promise((settle) => setTimeout(settle, 100));
|
|
@@ -38182,7 +38284,7 @@ class GodotServer {
|
|
|
38182
38284
|
]);
|
|
38183
38285
|
}
|
|
38184
38286
|
if (!hasSaidWhoItIs(before)) {
|
|
38185
|
-
return this.createErrorResponse(`The editor has connected and has not said who it is within ${
|
|
38287
|
+
return this.createErrorResponse(`The editor has connected and has not said who it is within ${greetingWaitMs() / 1000}s, so whether this server or the editor itself restarts it cannot be told.`, ["editor_status says when it has, under greeting while it has not", "Then ask for the restart again"]);
|
|
38186
38288
|
}
|
|
38187
38289
|
const mine = before.openedByAServer === true && before.projectPath !== undefined;
|
|
38188
38290
|
if (!mine && args["hidden"] === true) {
|
|
@@ -38797,7 +38899,7 @@ class GodotServer {
|
|
|
38797
38899
|
game.brokeOn = halt.text;
|
|
38798
38900
|
game.log.record("error", halt.text);
|
|
38799
38901
|
}
|
|
38800
|
-
async untilTheEditorStopsPlaying(withinMs =
|
|
38902
|
+
async untilTheEditorStopsPlaying(withinMs = stopWaitMs()) {
|
|
38801
38903
|
const deadline = Date.now() + withinMs;
|
|
38802
38904
|
for (;; ) {
|
|
38803
38905
|
if ((await this.editorPlayingState())?.playing === false) {
|
|
@@ -38891,7 +38993,7 @@ class GodotServer {
|
|
|
38891
38993
|
running.endedThrough = "signal";
|
|
38892
38994
|
}
|
|
38893
38995
|
clearTheRecord();
|
|
38894
|
-
running.log.record(gone ? "info" : "warning", declined !== null ? `gdharness could not signal pid ${running.pid}: the operating system refused it (${declined}). It had described that process as ${described}.` : !gone ? `gdharness signalled pid ${running.pid}, which the operating system described as ${described}, and it was still running ${
|
|
38996
|
+
running.log.record(gone ? "info" : "warning", declined !== null ? `gdharness could not signal pid ${running.pid}: the operating system refused it (${declined}). It had described that process as ${described}.` : !gone ? `gdharness signalled pid ${running.pid}, which the operating system described as ${described}, and it was still running ${stopWaitMs() / 1000} seconds later.` : landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
|
|
38895
38997
|
return gone ? "gone" : "lingering";
|
|
38896
38998
|
}
|
|
38897
38999
|
async endThroughTheKeeper(run, pid, keeper) {
|
|
@@ -38911,7 +39013,7 @@ class GodotServer {
|
|
|
38911
39013
|
run.exitCode = null;
|
|
38912
39014
|
run.exitSignal = "SIGTERM";
|
|
38913
39015
|
}
|
|
38914
|
-
run.log.record(gone ? "info" : "warning", !gone ? `gdharness had the keeper holding pid ${pid} end it, and it was still running ${
|
|
39016
|
+
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.`);
|
|
38915
39017
|
return gone ? "gone" : "lingering";
|
|
38916
39018
|
}
|
|
38917
39019
|
spawnGame(godotPath, cmdArgs, env) {
|
|
@@ -39836,7 +39938,7 @@ class GodotServer {
|
|
|
39836
39938
|
...landed ? { endedThrough: "signal" } : {},
|
|
39837
39939
|
...childrenFields(ended),
|
|
39838
39940
|
exitedBeforeStop: !landed,
|
|
39839
|
-
note: `${!gone ? `pid ${pid} was told to end and was still running ${
|
|
39941
|
+
note: `${!gone ? `pid ${pid} was told to end and was still running ${stopWaitMs() / 1000} seconds later, so it has not stopped: a game of this project that no server here was holding,` : landed ? `pid ${pid} was ended: a game of this project that no server here was holding,` : `pid ${pid} had already gone when it was signalled: a game of this project that no server here was holding,`} identified by its announcement and by its command line, ${read.executable} run with --path ${read.projectPath ?? ""}. Nothing here read what it printed or holds its exit code, so neither is in this answer.${gone ? "" : " editor_status says when it has gone."} ${aboutTheChildren(ended, false)}`
|
|
39840
39942
|
});
|
|
39841
39943
|
}
|
|
39842
39944
|
async whatTheRunStarted(run) {
|
|
@@ -40572,7 +40674,8 @@ class GodotServer {
|
|
|
40572
40674
|
}
|
|
40573
40675
|
return `The game this server started (${which}) is still coming up and has not announced its runtime after a wait of ${Math.min(budgetMs, remaining)}ms, so there is nothing to talk to yet. It is still running: ask again in a moment rather than starting another.`;
|
|
40574
40676
|
}
|
|
40575
|
-
async handleRuntimeCommand(command, args,
|
|
40677
|
+
async handleRuntimeCommand(command, args, asked) {
|
|
40678
|
+
const timeoutMs = asked.patience ?? this.runtimeTimeoutMs();
|
|
40576
40679
|
const { op: _op, projectPath, pid, outputPath, ...params } = asParams(args);
|
|
40577
40680
|
const waited = typeof pid === "number" ? null : await this.ownRunOnItsWay(typeof projectPath === "string" ? projectPath : this.ownProject, timeoutMs);
|
|
40578
40681
|
if (waited !== null) {
|
|
@@ -40625,7 +40728,7 @@ ${staleNote}`);
|
|
|
40625
40728
|
requestId: reply.requestId,
|
|
40626
40729
|
command,
|
|
40627
40730
|
waitedMs: timeoutMs,
|
|
40628
|
-
note: reply.message,
|
|
40731
|
+
note: `${reply.message} ${longerWaitNote(asked.tool, asked.op, timeoutMs)}`,
|
|
40629
40732
|
...answeredBy,
|
|
40630
40733
|
...stale
|
|
40631
40734
|
});
|
|
@@ -40725,13 +40828,13 @@ ${staleNote}`}`
|
|
|
40725
40828
|
limit: readPositiveNumber(args, "limit") ?? 100,
|
|
40726
40829
|
...property === undefined ? {} : { property },
|
|
40727
40830
|
...includeHidden === undefined ? {} : { include_hidden: includeHidden }
|
|
40728
|
-
}, patience);
|
|
40831
|
+
}, { tool: "runtime_inspect", op: "find", patience });
|
|
40729
40832
|
}
|
|
40730
40833
|
async handleRuntimeWait(op, args) {
|
|
40731
40834
|
if (op === "frames") {
|
|
40732
40835
|
const frames = readPositiveNumber(args, "frames") ?? 1;
|
|
40733
40836
|
const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
|
|
40734
|
-
return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, waited);
|
|
40837
|
+
return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, { tool: "runtime_wait", op, patience: waited });
|
|
40735
40838
|
}
|
|
40736
40839
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
|
|
40737
40840
|
const asked = readNonEmptyString(args, "nodePath");
|
|
@@ -40745,7 +40848,7 @@ ${staleNote}`}`
|
|
|
40745
40848
|
path: nodePath,
|
|
40746
40849
|
signal: readString(args, "signal") ?? "",
|
|
40747
40850
|
timeout_ms: timeoutMs
|
|
40748
|
-
}, patience) : await this.handleRuntimeCommand("wait_until", {
|
|
40851
|
+
}, { tool: "runtime_wait", op, patience }) : await this.handleRuntimeCommand("wait_until", {
|
|
40749
40852
|
...whichGame(args),
|
|
40750
40853
|
path: nodePath,
|
|
40751
40854
|
property: readString(args, "property") ?? "",
|
|
@@ -40753,7 +40856,7 @@ ${staleNote}`}`
|
|
|
40753
40856
|
...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
|
|
40754
40857
|
...readBoolean(args, "includeHidden") === undefined ? {} : { include_hidden: readBoolean(args, "includeHidden") },
|
|
40755
40858
|
timeout_ms: timeoutMs
|
|
40756
|
-
}, patience);
|
|
40859
|
+
}, { tool: "runtime_wait", op, patience });
|
|
40757
40860
|
}
|
|
40758
40861
|
}
|
|
40759
40862
|
async function runGodotServer() {
|
package/build/keeper.js
CHANGED
|
@@ -550,6 +550,7 @@ var DECLARATION_LINE = new RegExp(`^(${ANNOTATIONS})(static\\s+)?(func|var|const
|
|
|
550
550
|
var CLASS_NAME_LINE = new RegExp(`^${ANNOTATIONS}class_name\\s+(${NAME})`, "u");
|
|
551
551
|
var EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
|
|
552
552
|
var FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
|
|
553
|
+
var LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
|
|
553
554
|
var KEYWORDS = new Set([
|
|
554
555
|
"and",
|
|
555
556
|
"as",
|
|
@@ -7767,6 +7768,8 @@ function queuedMessagesOf(projectGodot) {
|
|
|
7767
7768
|
const set = projectGodot === null ? undefined : parseProjectGodot(projectGodot)["network"]?.[QUEUE_SETTING];
|
|
7768
7769
|
return typeof set === "number" && Number.isInteger(set) && set > 0 ? set : QUEUE_DEFAULT;
|
|
7769
7770
|
}
|
|
7771
|
+
var PROFILE_SENT = "gdharness:profile_sent";
|
|
7772
|
+
var PROFILE_RECEIVED = "gdharness:profile_received";
|
|
7770
7773
|
var UNNAMED_PREFIX = "#";
|
|
7771
7774
|
function profilerOn(frameFunctions) {
|
|
7772
7775
|
return framedCommand("profiler:servers", [true, [frameFunctions, false]]);
|
|
@@ -7931,6 +7934,9 @@ class ProfileListener {
|
|
|
7931
7934
|
if ("message" in received && received.message === "debug_enter") {
|
|
7932
7935
|
socket.write(framedCommand("continue", []));
|
|
7933
7936
|
}
|
|
7937
|
+
if ("message" in received && received.message === PROFILE_SENT) {
|
|
7938
|
+
socket.write(framedCommand(PROFILE_RECEIVED, []));
|
|
7939
|
+
}
|
|
7934
7940
|
aggregate.take(received, Date.now());
|
|
7935
7941
|
frames ||= "message" in received && (received.message === "servers:profile_frame" || received.message === "servers:profile_total");
|
|
7936
7942
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.60",
|
|
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",
|