gdharness 1.1.61 → 1.2.0
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 +100 -24
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +31 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +32 -3
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +6 -0
- package/build/index.js +101 -22
- package/build/keeper.js +1 -0
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -9270,17 +9270,23 @@ function parametersOf(text, from) {
|
|
|
9270
9270
|
}
|
|
9271
9271
|
return names;
|
|
9272
9272
|
}
|
|
9273
|
-
function
|
|
9273
|
+
function declaredClass(text, name) {
|
|
9274
|
+
const word = `(?<![\\p{L}\\p{N}_.])${escapedForPattern(name)}`;
|
|
9275
|
+
const found = new RegExp(`${word}\\s*:\\s*(${DOTTED})`, "u").exec(text)?.[1] ?? new RegExp(`${word}\\s*:=\\s*(${DOTTED})\\.new\\s*\\(`, "u").exec(text)?.[1] ?? new RegExp(`${word}\\s*:?=[^\\n]*?\\bas\\s+(${DOTTED})\\s*$`, "u").exec(text)?.[1];
|
|
9276
|
+
return found === undefined ? null : found.slice(found.lastIndexOf(".") + 1);
|
|
9277
|
+
}
|
|
9278
|
+
function localAt(lines, index, name) {
|
|
9274
9279
|
const word = escapedForPattern(name);
|
|
9275
9280
|
const declares = new RegExp(`(?<![\\p{L}\\p{N}_.])(?:var|const)\\s+${word}(?![\\p{L}\\p{N}_])`, "u");
|
|
9276
9281
|
const iterates = new RegExp(`^\\s*for\\s+${word}(?![\\p{L}\\p{N}_])`, "u");
|
|
9282
|
+
const local = (text) => ({ type: declaredClass(text, name) });
|
|
9277
9283
|
const own = lines[index] ?? "";
|
|
9278
9284
|
const headerOnOwnLine = FUNC_LINE.exec(own) ?? LAMBDA_HEADER.exec(own);
|
|
9279
9285
|
if (headerOnOwnLine !== null && parametersOf(own, headerOnOwnLine.index).includes(name)) {
|
|
9280
|
-
return
|
|
9286
|
+
return local(own);
|
|
9281
9287
|
}
|
|
9282
9288
|
if (FUNC_LINE.test(own)) {
|
|
9283
|
-
return
|
|
9289
|
+
return null;
|
|
9284
9290
|
}
|
|
9285
9291
|
let depth = indentOf(own);
|
|
9286
9292
|
for (let at = index - 1;at >= 0; at -= 1) {
|
|
@@ -9296,21 +9302,21 @@ function isLocalAt(lines, index, name) {
|
|
|
9296
9302
|
if (indent < depth) {
|
|
9297
9303
|
depth = indent;
|
|
9298
9304
|
if (named !== null) {
|
|
9299
|
-
return parametersOf(text, named.index).includes(name);
|
|
9305
|
+
return parametersOf(text, named.index).includes(name) ? local(text) : null;
|
|
9300
9306
|
}
|
|
9301
9307
|
const lambda = LAMBDA_HEADER.exec(text);
|
|
9302
9308
|
if (lambda !== null && parametersOf(text, lambda.index).includes(name) || iterates.test(text)) {
|
|
9303
|
-
return
|
|
9309
|
+
return local(text);
|
|
9304
9310
|
}
|
|
9305
9311
|
}
|
|
9306
9312
|
if (named !== null || depth === 0) {
|
|
9307
|
-
return
|
|
9313
|
+
return null;
|
|
9308
9314
|
}
|
|
9309
9315
|
if (declares.test(text)) {
|
|
9310
|
-
return
|
|
9316
|
+
return local(text);
|
|
9311
9317
|
}
|
|
9312
9318
|
}
|
|
9313
|
-
return
|
|
9319
|
+
return null;
|
|
9314
9320
|
}
|
|
9315
9321
|
function receiverBefore(code, offset) {
|
|
9316
9322
|
let at = offset - 1;
|
|
@@ -9328,7 +9334,14 @@ function receiverBefore(code, offset) {
|
|
|
9328
9334
|
while (start > 0 && isIdentifierPart(code[start - 1])) {
|
|
9329
9335
|
start -= 1;
|
|
9330
9336
|
}
|
|
9331
|
-
|
|
9337
|
+
if (start > at) {
|
|
9338
|
+
return null;
|
|
9339
|
+
}
|
|
9340
|
+
let before = start - 1;
|
|
9341
|
+
while (before >= 0 && /\s/.test(code[before] ?? "")) {
|
|
9342
|
+
before -= 1;
|
|
9343
|
+
}
|
|
9344
|
+
return { name: code.slice(start, at + 1), dotted: code[before] === "." };
|
|
9332
9345
|
}
|
|
9333
9346
|
function bodyHolding(body, offset) {
|
|
9334
9347
|
const inner = body.inner.find((one) => one.start <= offset && offset <= one.end);
|
|
@@ -9341,13 +9354,27 @@ function callSitesOf(source, name, otherClasses = new Set) {
|
|
|
9341
9354
|
`);
|
|
9342
9355
|
const lines = new Lines(source);
|
|
9343
9356
|
const shape = shapeOf(source);
|
|
9357
|
+
const memberClass = (member, at) => {
|
|
9358
|
+
const declared = bodyHolding(shape.body, at).declarations.find((one) => one.name === member && (one.kind === "var" || one.kind === "const"));
|
|
9359
|
+
return declared === undefined ? null : declaredClass(codeLines[lines.positionOf(declared.offset).line] ?? "", member);
|
|
9360
|
+
};
|
|
9344
9361
|
return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).filter((one) => {
|
|
9362
|
+
const index = lines.positionOf(one.offset).line;
|
|
9345
9363
|
if (one.afterDot) {
|
|
9346
9364
|
const receiver = receiverBefore(code, one.offset);
|
|
9347
|
-
|
|
9365
|
+
if (receiver === null || otherClasses.size === 0) {
|
|
9366
|
+
return true;
|
|
9367
|
+
}
|
|
9368
|
+
if (otherClasses.has(receiver.name)) {
|
|
9369
|
+
return false;
|
|
9370
|
+
}
|
|
9371
|
+
if (receiver.dotted) {
|
|
9372
|
+
return true;
|
|
9373
|
+
}
|
|
9374
|
+
const typed = localAt(codeLines, index, receiver.name)?.type ?? memberClass(receiver.name, one.offset);
|
|
9375
|
+
return typed === null || !otherClasses.has(typed);
|
|
9348
9376
|
}
|
|
9349
|
-
|
|
9350
|
-
if (isLocalAt(codeLines, index, name)) {
|
|
9377
|
+
if (localAt(codeLines, index, name) !== null) {
|
|
9351
9378
|
return false;
|
|
9352
9379
|
}
|
|
9353
9380
|
return !bodyHolding(shape.body, one.offset).declarations.some((declared) => declared.name === name && MEMBER_KINDS.has(declared.kind));
|
|
@@ -9372,7 +9399,7 @@ function collectEnumValues(text, from, into) {
|
|
|
9372
9399
|
function isIdentifier(name) {
|
|
9373
9400
|
return new RegExp(`^${NAME}$`, "u").test(name) && !KEYWORDS.has(name);
|
|
9374
9401
|
}
|
|
9375
|
-
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, MEMBER_KINDS, KEYWORDS;
|
|
9402
|
+
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, DOTTED, MEMBER_KINDS, KEYWORDS;
|
|
9376
9403
|
var init_gdscript_source = __esm(() => {
|
|
9377
9404
|
IDENTIFIER_START = /[\p{L}_]/u;
|
|
9378
9405
|
IDENTIFIER_PART = /[\p{L}\p{N}_]/u;
|
|
@@ -9384,6 +9411,7 @@ var init_gdscript_source = __esm(() => {
|
|
|
9384
9411
|
FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
|
|
9385
9412
|
SUPER_CALL = /(?<![\p{L}\p{N}_.])super\s*[.(]/u;
|
|
9386
9413
|
LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
|
|
9414
|
+
DOTTED = `${NAME}(?:\\.${NAME})*`;
|
|
9387
9415
|
MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
|
|
9388
9416
|
KEYWORDS = new Set([
|
|
9389
9417
|
"and",
|
|
@@ -18349,7 +18377,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18349
18377
|
callersOf: {
|
|
18350
18378
|
type: "string",
|
|
18351
18379
|
ops: ["profile"],
|
|
18352
|
-
description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too, except one called through another class of the project by name
|
|
18380
|
+
description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too, except, when Class.name was asked, one called through another class of the project by name or through a parameter, local or class variable declared as one, and a call made through a string is not. A parameter, local or class variable of the same name is not the function and is not listed."
|
|
18353
18381
|
},
|
|
18354
18382
|
limit: {
|
|
18355
18383
|
type: "integer",
|
|
@@ -18385,7 +18413,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18385
18413
|
},
|
|
18386
18414
|
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
|
|
18387
18415
|
profile: {
|
|
18388
|
-
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",
|
|
18389
18417
|
requires: []
|
|
18390
18418
|
}
|
|
18391
18419
|
},
|
|
@@ -18441,7 +18469,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18441
18469
|
},
|
|
18442
18470
|
{
|
|
18443
18471
|
name: "editor_status",
|
|
18444
|
-
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; null when there is
|
|
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.",
|
|
18445
18473
|
parameters: {},
|
|
18446
18474
|
requires: []
|
|
18447
18475
|
},
|
|
@@ -42528,16 +42556,39 @@ function signatureParts(signature) {
|
|
|
42528
42556
|
return { script: parts.slice(0, -2).join("::"), line: Number.isFinite(line) ? line : 0, function: name };
|
|
42529
42557
|
}
|
|
42530
42558
|
function profiledFunctions(totals, limit, sourceOf = () => null) {
|
|
42531
|
-
const
|
|
42532
|
-
const
|
|
42559
|
+
const perCallUs = perCallCostOf(totals);
|
|
42560
|
+
const rows = rowsOf(totals, sourceOf).map((row) => withOverhead(row, perCallUs));
|
|
42561
|
+
const scriptMs = ms(Object.entries(totals.functions).reduce((sum, [signature, one]) => sum + (one.calls > 0 && !isCalibration(signature) ? one.selfSeconds : 0), 0));
|
|
42533
42562
|
return {
|
|
42534
42563
|
functions: rows.slice(0, limit).map((row) => placedLambda(row, sourceOf)),
|
|
42535
42564
|
omitted: Math.max(0, rows.length - limit),
|
|
42536
42565
|
scriptMs,
|
|
42537
42566
|
counted: rows.length,
|
|
42538
|
-
superCallers: rows.filter((row) => row.selfIncludesSuper === true).length
|
|
42567
|
+
superCallers: rows.filter((row) => row.selfIncludesSuper === true).length,
|
|
42568
|
+
perCallUs,
|
|
42569
|
+
overheadDominated: rows.filter((row) => row.overheadMs !== undefined).length
|
|
42539
42570
|
};
|
|
42540
42571
|
}
|
|
42572
|
+
function isCalibration(signature) {
|
|
42573
|
+
const parts = signatureParts(signature);
|
|
42574
|
+
return parts.script.endsWith(RUNTIME_SCRIPT) && (parts.function === PROFILER_PROBE || parts.function === PROFILER_CALIBRATOR);
|
|
42575
|
+
}
|
|
42576
|
+
function perCallCostOf(totals) {
|
|
42577
|
+
for (const [signature, sum] of Object.entries(totals.functions)) {
|
|
42578
|
+
const parts = signatureParts(signature);
|
|
42579
|
+
if (parts.script.endsWith(RUNTIME_SCRIPT) && parts.function === PROFILER_PROBE && sum.calls > 0) {
|
|
42580
|
+
return Math.round(sum.selfSeconds / sum.calls * 1e9) / 1000;
|
|
42581
|
+
}
|
|
42582
|
+
}
|
|
42583
|
+
return null;
|
|
42584
|
+
}
|
|
42585
|
+
function withOverhead(row, perCallUs) {
|
|
42586
|
+
if (perCallUs === null || row.selfMs <= 0) {
|
|
42587
|
+
return row;
|
|
42588
|
+
}
|
|
42589
|
+
const overheadMs = Math.round(row.calls * perCallUs) / 1000;
|
|
42590
|
+
return overheadMs >= row.selfMs / 2 ? { ...row, overheadMs: Math.min(overheadMs, row.selfMs) } : row;
|
|
42591
|
+
}
|
|
42541
42592
|
function rowsOf(totals, sourceOf) {
|
|
42542
42593
|
const codes = new Map;
|
|
42543
42594
|
const codeOfScript = (script) => {
|
|
@@ -42547,7 +42598,7 @@ function rowsOf(totals, sourceOf) {
|
|
|
42547
42598
|
}
|
|
42548
42599
|
return codes.get(script) ?? null;
|
|
42549
42600
|
};
|
|
42550
|
-
return Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
|
|
42601
|
+
return Object.entries(totals.functions).filter(([signature, sum]) => sum.calls > 0 && !isCalibration(signature)).map(([signature, sum]) => {
|
|
42551
42602
|
const parts = signatureParts(signature);
|
|
42552
42603
|
const code = codeOfScript(parts.script);
|
|
42553
42604
|
return {
|
|
@@ -42613,7 +42664,7 @@ function callersOf(totals, name, scripts) {
|
|
|
42613
42664
|
sites.sort((a, b) => (b.withinTotalMs ?? -1) - (a.withinTotalMs ?? -1) || a.script.localeCompare(b.script) || a.line - b.line);
|
|
42614
42665
|
return { target, sites: sites.slice(0, CALL_SITES), omitted: Math.max(0, sites.length - CALL_SITES) };
|
|
42615
42666
|
}
|
|
42616
|
-
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;
|
|
42667
|
+
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;
|
|
42617
42668
|
var init_script_profile = __esm(() => {
|
|
42618
42669
|
init_gdscript_source();
|
|
42619
42670
|
init_godot_variant();
|
|
@@ -43201,6 +43252,24 @@ function foldedScriptErrors(errors) {
|
|
|
43201
43252
|
function nonEmpty(lists) {
|
|
43202
43253
|
return Object.fromEntries(Object.entries(lists).filter(([, list]) => list.length > 0));
|
|
43203
43254
|
}
|
|
43255
|
+
function withoutUidUnder(unminted, paths) {
|
|
43256
|
+
return unminted.map((script) => `res://${script}`).filter((script) => paths.some((path) => script === path || script.startsWith(path.endsWith("/") ? path : `${path}/`)));
|
|
43257
|
+
}
|
|
43258
|
+
function withUidsMissing(answer, missing) {
|
|
43259
|
+
const content = answer.content.map((part) => ({ ...part }));
|
|
43260
|
+
const last = content.at(-1);
|
|
43261
|
+
try {
|
|
43262
|
+
const parsed = JSON.parse(last?.text ?? "");
|
|
43263
|
+
if (last !== undefined && typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
43264
|
+
last.text = answerJson({
|
|
43265
|
+
...parsed,
|
|
43266
|
+
uidsMissing: missing,
|
|
43267
|
+
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.`
|
|
43268
|
+
});
|
|
43269
|
+
}
|
|
43270
|
+
} catch {}
|
|
43271
|
+
return { ...answer, content };
|
|
43272
|
+
}
|
|
43204
43273
|
function withPreviousCut(answer, previous) {
|
|
43205
43274
|
const content = answer.content.map((part) => ({ ...part }));
|
|
43206
43275
|
const last = content.at(-1);
|
|
@@ -43412,6 +43481,9 @@ function profileAnswer(reading, limit) {
|
|
|
43412
43481
|
if (table.superCallers > 0) {
|
|
43413
43482
|
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.`);
|
|
43414
43483
|
}
|
|
43484
|
+
if (table.overheadDominated > 0 && table.perCallUs !== null) {
|
|
43485
|
+
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: a function that does little and is called often looks expensive here and costs that much less once the profiler is off, so inlining it gains little.`);
|
|
43486
|
+
}
|
|
43415
43487
|
notes.push(...cappedNotes(totals));
|
|
43416
43488
|
if (totals.unreadable > 0) {
|
|
43417
43489
|
notes.push(`${totals.unreadable} message${totals.unreadable === 1 ? "" : "s"} from the game could not be read, so the totals have that many gaps.`);
|
|
@@ -43424,6 +43496,7 @@ function profileAnswer(reading, limit) {
|
|
|
43424
43496
|
frames: totals.frames,
|
|
43425
43497
|
coveredMs: reading.coveredMs,
|
|
43426
43498
|
scriptMs: table.scriptMs,
|
|
43499
|
+
perCallOverheadUs: table.perCallUs ?? undefined,
|
|
43427
43500
|
functionsCounted: table.counted,
|
|
43428
43501
|
omitted: table.omitted > 0 ? table.omitted : undefined,
|
|
43429
43502
|
functions: table.functions,
|
|
@@ -45153,11 +45226,14 @@ class GodotServer {
|
|
|
45153
45226
|
return ticker;
|
|
45154
45227
|
}
|
|
45155
45228
|
async handleRunTests(args) {
|
|
45156
|
-
const
|
|
45229
|
+
const ran = await this.runTests(args);
|
|
45157
45230
|
const project = this.project(args);
|
|
45158
45231
|
if (!project.ok || callSignal()?.aborted === true) {
|
|
45159
|
-
return
|
|
45232
|
+
return ran;
|
|
45160
45233
|
}
|
|
45234
|
+
const paths = testPathsIn(project.value.path, args["path"]);
|
|
45235
|
+
const unminted = paths.ok ? withoutUidUnder(scriptsWithoutUid(project.value.path), paths.value) : [];
|
|
45236
|
+
const answer = unminted.length === 0 ? ran : withUidsMissing(ran, unminted);
|
|
45161
45237
|
const previous = takeCutTestRun(project.value.path);
|
|
45162
45238
|
return previous === null ? answer : withPreviousCut(answer, previous);
|
|
45163
45239
|
}
|
|
@@ -48557,7 +48633,7 @@ async function runGodotServer() {
|
|
|
48557
48633
|
const server = new GodotServer;
|
|
48558
48634
|
await server.run();
|
|
48559
48635
|
}
|
|
48560
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_SERVERS_START_MS = 20000, 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, unless it is called through
|
|
48636
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_SERVERS_START_MS = 20000, 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, unless one class was asked about and it is called through another class by name or through a variable declared as one. 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 "';
|
|
48561
48637
|
var init_server3 = __esm(() => {
|
|
48562
48638
|
init_mcp();
|
|
48563
48639
|
init_stdio2();
|
|
@@ -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: a function doing almost nothing, called six hundred thousand times, read as a
|
|
205
|
+
## second of its own, and inlining it saved nothing once the profiler was off. 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
|
|
|
@@ -556,15 +565,35 @@ func _takes_no_click(control: Control, node_path: String) -> String:
|
|
|
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
|
|
@@ -370,6 +370,12 @@ 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
|
+
|
|
373
379
|
## Everything in the tree [param root] heads that a pointer can land on: each control on screen
|
|
374
380
|
## that does not let the pointer through, and each window embedded in another.
|
|
375
381
|
static func _pointer_takers(root: Node) -> Array[Node]:
|
package/build/index.js
CHANGED
|
@@ -26641,17 +26641,24 @@ function parametersOf(text, from) {
|
|
|
26641
26641
|
}
|
|
26642
26642
|
return names;
|
|
26643
26643
|
}
|
|
26644
|
-
|
|
26644
|
+
var DOTTED = `${NAME}(?:\\.${NAME})*`;
|
|
26645
|
+
function declaredClass(text, name) {
|
|
26646
|
+
const word = `(?<![\\p{L}\\p{N}_.])${escapedForPattern(name)}`;
|
|
26647
|
+
const found = new RegExp(`${word}\\s*:\\s*(${DOTTED})`, "u").exec(text)?.[1] ?? new RegExp(`${word}\\s*:=\\s*(${DOTTED})\\.new\\s*\\(`, "u").exec(text)?.[1] ?? new RegExp(`${word}\\s*:?=[^\\n]*?\\bas\\s+(${DOTTED})\\s*$`, "u").exec(text)?.[1];
|
|
26648
|
+
return found === undefined ? null : found.slice(found.lastIndexOf(".") + 1);
|
|
26649
|
+
}
|
|
26650
|
+
function localAt(lines, index, name) {
|
|
26645
26651
|
const word = escapedForPattern(name);
|
|
26646
26652
|
const declares = new RegExp(`(?<![\\p{L}\\p{N}_.])(?:var|const)\\s+${word}(?![\\p{L}\\p{N}_])`, "u");
|
|
26647
26653
|
const iterates = new RegExp(`^\\s*for\\s+${word}(?![\\p{L}\\p{N}_])`, "u");
|
|
26654
|
+
const local = (text) => ({ type: declaredClass(text, name) });
|
|
26648
26655
|
const own = lines[index] ?? "";
|
|
26649
26656
|
const headerOnOwnLine = FUNC_LINE.exec(own) ?? LAMBDA_HEADER.exec(own);
|
|
26650
26657
|
if (headerOnOwnLine !== null && parametersOf(own, headerOnOwnLine.index).includes(name)) {
|
|
26651
|
-
return
|
|
26658
|
+
return local(own);
|
|
26652
26659
|
}
|
|
26653
26660
|
if (FUNC_LINE.test(own)) {
|
|
26654
|
-
return
|
|
26661
|
+
return null;
|
|
26655
26662
|
}
|
|
26656
26663
|
let depth = indentOf(own);
|
|
26657
26664
|
for (let at = index - 1;at >= 0; at -= 1) {
|
|
@@ -26667,21 +26674,21 @@ function isLocalAt(lines, index, name) {
|
|
|
26667
26674
|
if (indent < depth) {
|
|
26668
26675
|
depth = indent;
|
|
26669
26676
|
if (named !== null) {
|
|
26670
|
-
return parametersOf(text, named.index).includes(name);
|
|
26677
|
+
return parametersOf(text, named.index).includes(name) ? local(text) : null;
|
|
26671
26678
|
}
|
|
26672
26679
|
const lambda = LAMBDA_HEADER.exec(text);
|
|
26673
26680
|
if (lambda !== null && parametersOf(text, lambda.index).includes(name) || iterates.test(text)) {
|
|
26674
|
-
return
|
|
26681
|
+
return local(text);
|
|
26675
26682
|
}
|
|
26676
26683
|
}
|
|
26677
26684
|
if (named !== null || depth === 0) {
|
|
26678
|
-
return
|
|
26685
|
+
return null;
|
|
26679
26686
|
}
|
|
26680
26687
|
if (declares.test(text)) {
|
|
26681
|
-
return
|
|
26688
|
+
return local(text);
|
|
26682
26689
|
}
|
|
26683
26690
|
}
|
|
26684
|
-
return
|
|
26691
|
+
return null;
|
|
26685
26692
|
}
|
|
26686
26693
|
function receiverBefore(code, offset) {
|
|
26687
26694
|
let at = offset - 1;
|
|
@@ -26699,7 +26706,14 @@ function receiverBefore(code, offset) {
|
|
|
26699
26706
|
while (start > 0 && isIdentifierPart(code[start - 1])) {
|
|
26700
26707
|
start -= 1;
|
|
26701
26708
|
}
|
|
26702
|
-
|
|
26709
|
+
if (start > at) {
|
|
26710
|
+
return null;
|
|
26711
|
+
}
|
|
26712
|
+
let before = start - 1;
|
|
26713
|
+
while (before >= 0 && /\s/.test(code[before] ?? "")) {
|
|
26714
|
+
before -= 1;
|
|
26715
|
+
}
|
|
26716
|
+
return { name: code.slice(start, at + 1), dotted: code[before] === "." };
|
|
26703
26717
|
}
|
|
26704
26718
|
var MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
|
|
26705
26719
|
function bodyHolding(body, offset) {
|
|
@@ -26713,13 +26727,27 @@ function callSitesOf(source, name, otherClasses = new Set) {
|
|
|
26713
26727
|
`);
|
|
26714
26728
|
const lines = new Lines(source);
|
|
26715
26729
|
const shape = shapeOf(source);
|
|
26730
|
+
const memberClass = (member, at) => {
|
|
26731
|
+
const declared = bodyHolding(shape.body, at).declarations.find((one) => one.name === member && (one.kind === "var" || one.kind === "const"));
|
|
26732
|
+
return declared === undefined ? null : declaredClass(codeLines[lines.positionOf(declared.offset).line] ?? "", member);
|
|
26733
|
+
};
|
|
26716
26734
|
return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).filter((one) => {
|
|
26735
|
+
const index = lines.positionOf(one.offset).line;
|
|
26717
26736
|
if (one.afterDot) {
|
|
26718
26737
|
const receiver = receiverBefore(code, one.offset);
|
|
26719
|
-
|
|
26738
|
+
if (receiver === null || otherClasses.size === 0) {
|
|
26739
|
+
return true;
|
|
26740
|
+
}
|
|
26741
|
+
if (otherClasses.has(receiver.name)) {
|
|
26742
|
+
return false;
|
|
26743
|
+
}
|
|
26744
|
+
if (receiver.dotted) {
|
|
26745
|
+
return true;
|
|
26746
|
+
}
|
|
26747
|
+
const typed = localAt(codeLines, index, receiver.name)?.type ?? memberClass(receiver.name, one.offset);
|
|
26748
|
+
return typed === null || !otherClasses.has(typed);
|
|
26720
26749
|
}
|
|
26721
|
-
|
|
26722
|
-
if (isLocalAt(codeLines, index, name)) {
|
|
26750
|
+
if (localAt(codeLines, index, name) !== null) {
|
|
26723
26751
|
return false;
|
|
26724
26752
|
}
|
|
26725
26753
|
return !bodyHolding(shape.body, one.offset).declarations.some((declared) => declared.name === name && MEMBER_KINDS.has(declared.kind));
|
|
@@ -33325,16 +33353,42 @@ function signatureParts(signature) {
|
|
|
33325
33353
|
}
|
|
33326
33354
|
var ms = (seconds) => Math.round(seconds * 1e6) / 1000;
|
|
33327
33355
|
function profiledFunctions(totals, limit, sourceOf = () => null) {
|
|
33328
|
-
const
|
|
33329
|
-
const
|
|
33356
|
+
const perCallUs = perCallCostOf(totals);
|
|
33357
|
+
const rows = rowsOf(totals, sourceOf).map((row) => withOverhead(row, perCallUs));
|
|
33358
|
+
const scriptMs = ms(Object.entries(totals.functions).reduce((sum, [signature, one]) => sum + (one.calls > 0 && !isCalibration(signature) ? one.selfSeconds : 0), 0));
|
|
33330
33359
|
return {
|
|
33331
33360
|
functions: rows.slice(0, limit).map((row) => placedLambda(row, sourceOf)),
|
|
33332
33361
|
omitted: Math.max(0, rows.length - limit),
|
|
33333
33362
|
scriptMs,
|
|
33334
33363
|
counted: rows.length,
|
|
33335
|
-
superCallers: rows.filter((row) => row.selfIncludesSuper === true).length
|
|
33364
|
+
superCallers: rows.filter((row) => row.selfIncludesSuper === true).length,
|
|
33365
|
+
perCallUs,
|
|
33366
|
+
overheadDominated: rows.filter((row) => row.overheadMs !== undefined).length
|
|
33336
33367
|
};
|
|
33337
33368
|
}
|
|
33369
|
+
var RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd";
|
|
33370
|
+
var PROFILER_PROBE = "_profiler_probe";
|
|
33371
|
+
var PROFILER_CALIBRATOR = "_calibrate_the_profiler";
|
|
33372
|
+
function isCalibration(signature) {
|
|
33373
|
+
const parts = signatureParts(signature);
|
|
33374
|
+
return parts.script.endsWith(RUNTIME_SCRIPT) && (parts.function === PROFILER_PROBE || parts.function === PROFILER_CALIBRATOR);
|
|
33375
|
+
}
|
|
33376
|
+
function perCallCostOf(totals) {
|
|
33377
|
+
for (const [signature, sum] of Object.entries(totals.functions)) {
|
|
33378
|
+
const parts = signatureParts(signature);
|
|
33379
|
+
if (parts.script.endsWith(RUNTIME_SCRIPT) && parts.function === PROFILER_PROBE && sum.calls > 0) {
|
|
33380
|
+
return Math.round(sum.selfSeconds / sum.calls * 1e9) / 1000;
|
|
33381
|
+
}
|
|
33382
|
+
}
|
|
33383
|
+
return null;
|
|
33384
|
+
}
|
|
33385
|
+
function withOverhead(row, perCallUs) {
|
|
33386
|
+
if (perCallUs === null || row.selfMs <= 0) {
|
|
33387
|
+
return row;
|
|
33388
|
+
}
|
|
33389
|
+
const overheadMs = Math.round(row.calls * perCallUs) / 1000;
|
|
33390
|
+
return overheadMs >= row.selfMs / 2 ? { ...row, overheadMs: Math.min(overheadMs, row.selfMs) } : row;
|
|
33391
|
+
}
|
|
33338
33392
|
function rowsOf(totals, sourceOf) {
|
|
33339
33393
|
const codes = new Map;
|
|
33340
33394
|
const codeOfScript = (script) => {
|
|
@@ -33344,7 +33398,7 @@ function rowsOf(totals, sourceOf) {
|
|
|
33344
33398
|
}
|
|
33345
33399
|
return codes.get(script) ?? null;
|
|
33346
33400
|
};
|
|
33347
|
-
return Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
|
|
33401
|
+
return Object.entries(totals.functions).filter(([signature, sum]) => sum.calls > 0 && !isCalibration(signature)).map(([signature, sum]) => {
|
|
33348
33402
|
const parts = signatureParts(signature);
|
|
33349
33403
|
const code = codeOfScript(parts.script);
|
|
33350
33404
|
return {
|
|
@@ -34399,7 +34453,7 @@ var TOOL_SPECS = [
|
|
|
34399
34453
|
callersOf: {
|
|
34400
34454
|
type: "string",
|
|
34401
34455
|
ops: ["profile"],
|
|
34402
|
-
description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too, except one called through another class of the project by name
|
|
34456
|
+
description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too, except, when Class.name was asked, one called through another class of the project by name or through a parameter, local or class variable declared as one, and a call made through a string is not. A parameter, local or class variable of the same name is not the function and is not listed."
|
|
34403
34457
|
},
|
|
34404
34458
|
limit: {
|
|
34405
34459
|
type: "integer",
|
|
@@ -34435,7 +34489,7 @@ var TOOL_SPECS = [
|
|
|
34435
34489
|
},
|
|
34436
34490
|
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
|
|
34437
34491
|
profile: {
|
|
34438
|
-
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",
|
|
34492
|
+
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",
|
|
34439
34493
|
requires: []
|
|
34440
34494
|
}
|
|
34441
34495
|
},
|
|
@@ -34491,7 +34545,7 @@ var TOOL_SPECS = [
|
|
|
34491
34545
|
},
|
|
34492
34546
|
{
|
|
34493
34547
|
name: "editor_status",
|
|
34494
|
-
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; null when there is
|
|
34548
|
+
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.",
|
|
34495
34549
|
parameters: {},
|
|
34496
34550
|
requires: []
|
|
34497
34551
|
},
|
|
@@ -35657,6 +35711,24 @@ function foldedScriptErrors(errors) {
|
|
|
35657
35711
|
function nonEmpty(lists) {
|
|
35658
35712
|
return Object.fromEntries(Object.entries(lists).filter(([, list]) => list.length > 0));
|
|
35659
35713
|
}
|
|
35714
|
+
function withoutUidUnder(unminted, paths) {
|
|
35715
|
+
return unminted.map((script) => `res://${script}`).filter((script) => paths.some((path) => script === path || script.startsWith(path.endsWith("/") ? path : `${path}/`)));
|
|
35716
|
+
}
|
|
35717
|
+
function withUidsMissing(answer, missing) {
|
|
35718
|
+
const content = answer.content.map((part) => ({ ...part }));
|
|
35719
|
+
const last = content.at(-1);
|
|
35720
|
+
try {
|
|
35721
|
+
const parsed = JSON.parse(last?.text ?? "");
|
|
35722
|
+
if (last !== undefined && typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
35723
|
+
last.text = answerJson({
|
|
35724
|
+
...parsed,
|
|
35725
|
+
uidsMissing: missing,
|
|
35726
|
+
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.`
|
|
35727
|
+
});
|
|
35728
|
+
}
|
|
35729
|
+
} catch {}
|
|
35730
|
+
return { ...answer, content };
|
|
35731
|
+
}
|
|
35660
35732
|
function withPreviousCut(answer, previous) {
|
|
35661
35733
|
const content = answer.content.map((part) => ({ ...part }));
|
|
35662
35734
|
const last = content.at(-1);
|
|
@@ -35850,7 +35922,7 @@ function playedCap(held) {
|
|
|
35850
35922
|
totalCapped: readBoolean(held, "totalCapped") === true
|
|
35851
35923
|
};
|
|
35852
35924
|
}
|
|
35853
|
-
var CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless it is called through
|
|
35925
|
+
var CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.";
|
|
35854
35926
|
function projectSourceOf(projectPath) {
|
|
35855
35927
|
const read = new Map;
|
|
35856
35928
|
return (script) => {
|
|
@@ -35903,6 +35975,9 @@ function profileAnswer(reading, limit) {
|
|
|
35903
35975
|
if (table.superCallers > 0) {
|
|
35904
35976
|
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.`);
|
|
35905
35977
|
}
|
|
35978
|
+
if (table.overheadDominated > 0 && table.perCallUs !== null) {
|
|
35979
|
+
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: a function that does little and is called often looks expensive here and costs that much less once the profiler is off, so inlining it gains little.`);
|
|
35980
|
+
}
|
|
35906
35981
|
notes.push(...cappedNotes(totals));
|
|
35907
35982
|
if (totals.unreadable > 0) {
|
|
35908
35983
|
notes.push(`${totals.unreadable} message${totals.unreadable === 1 ? "" : "s"} from the game could not be read, so the totals have that many gaps.`);
|
|
@@ -35915,6 +35990,7 @@ function profileAnswer(reading, limit) {
|
|
|
35915
35990
|
frames: totals.frames,
|
|
35916
35991
|
coveredMs: reading.coveredMs,
|
|
35917
35992
|
scriptMs: table.scriptMs,
|
|
35993
|
+
perCallOverheadUs: table.perCallUs ?? undefined,
|
|
35918
35994
|
functionsCounted: table.counted,
|
|
35919
35995
|
omitted: table.omitted > 0 ? table.omitted : undefined,
|
|
35920
35996
|
functions: table.functions,
|
|
@@ -37647,11 +37723,14 @@ class GodotServer {
|
|
|
37647
37723
|
return ticker;
|
|
37648
37724
|
}
|
|
37649
37725
|
async handleRunTests(args) {
|
|
37650
|
-
const
|
|
37726
|
+
const ran = await this.runTests(args);
|
|
37651
37727
|
const project = this.project(args);
|
|
37652
37728
|
if (!project.ok || callSignal()?.aborted === true) {
|
|
37653
|
-
return
|
|
37729
|
+
return ran;
|
|
37654
37730
|
}
|
|
37731
|
+
const paths = testPathsIn(project.value.path, args["path"]);
|
|
37732
|
+
const unminted = paths.ok ? withoutUidUnder(scriptsWithoutUid(project.value.path), paths.value) : [];
|
|
37733
|
+
const answer = unminted.length === 0 ? ran : withUidsMissing(ran, unminted);
|
|
37655
37734
|
const previous = takeCutTestRun(project.value.path);
|
|
37656
37735
|
return previous === null ? answer : withPreviousCut(answer, previous);
|
|
37657
37736
|
}
|
package/build/keeper.js
CHANGED
|
@@ -551,6 +551,7 @@ 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
553
|
var LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
|
|
554
|
+
var DOTTED = `${NAME}(?:\\.${NAME})*`;
|
|
554
555
|
var MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
|
|
555
556
|
var KEYWORDS = new Set([
|
|
556
557
|
"and",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|