gdharness 1.1.57 → 1.1.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +244 -37
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +4 -1
- package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +31 -13
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +55 -1
- package/build/index.js +243 -33
- package/build/keeper.js +7078 -144
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -9155,6 +9155,44 @@ ${line.text}`;
|
|
|
9155
9155
|
const { body } = parseBody(null, null, 0, 0, 0);
|
|
9156
9156
|
return { className, body };
|
|
9157
9157
|
}
|
|
9158
|
+
function indentOf(text) {
|
|
9159
|
+
return /^[ \t]*/.exec(text)?.[0].length ?? 0;
|
|
9160
|
+
}
|
|
9161
|
+
function enclosingFunction(source, line, code = codeOf(source)) {
|
|
9162
|
+
const lines = code.split(`
|
|
9163
|
+
`);
|
|
9164
|
+
const at = lines[line - 1];
|
|
9165
|
+
if (at === undefined) {
|
|
9166
|
+
return null;
|
|
9167
|
+
}
|
|
9168
|
+
const own = FUNC_LINE.exec(at);
|
|
9169
|
+
if (own?.[1] !== undefined) {
|
|
9170
|
+
return own[1];
|
|
9171
|
+
}
|
|
9172
|
+
let indent = indentOf(at);
|
|
9173
|
+
for (let index = line - 2;index >= 0 && indent > 0; index -= 1) {
|
|
9174
|
+
const text = lines[index] ?? "";
|
|
9175
|
+
if (text.trim() === "" || indentOf(text) >= indent) {
|
|
9176
|
+
continue;
|
|
9177
|
+
}
|
|
9178
|
+
indent = indentOf(text);
|
|
9179
|
+
const declared = FUNC_LINE.exec(text);
|
|
9180
|
+
if (declared?.[1] !== undefined) {
|
|
9181
|
+
return declared[1];
|
|
9182
|
+
}
|
|
9183
|
+
}
|
|
9184
|
+
return null;
|
|
9185
|
+
}
|
|
9186
|
+
function callSitesOf(source, name) {
|
|
9187
|
+
const regions = regionsOf(source);
|
|
9188
|
+
const code = codeOf(source, regions);
|
|
9189
|
+
const lines = new Lines(source);
|
|
9190
|
+
return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).map((one) => {
|
|
9191
|
+
const line = lines.positionOf(one.offset).line + 1;
|
|
9192
|
+
const after = code.slice(one.offset + name.length).trimStart();
|
|
9193
|
+
return { line, within: enclosingFunction(source, line, code), called: after.startsWith("(") };
|
|
9194
|
+
});
|
|
9195
|
+
}
|
|
9158
9196
|
function collectEnumValues(text, from, into) {
|
|
9159
9197
|
const close = text.indexOf("}");
|
|
9160
9198
|
const inside = text.slice(1, close === -1 ? text.length : close);
|
|
@@ -9170,7 +9208,7 @@ function collectEnumValues(text, from, into) {
|
|
|
9170
9208
|
function isIdentifier(name) {
|
|
9171
9209
|
return new RegExp(`^${NAME}$`, "u").test(name) && !KEYWORDS.has(name);
|
|
9172
9210
|
}
|
|
9173
|
-
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, KEYWORDS;
|
|
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;
|
|
9174
9212
|
var init_gdscript_source = __esm(() => {
|
|
9175
9213
|
IDENTIFIER_START = /[\p{L}_]/u;
|
|
9176
9214
|
IDENTIFIER_PART = /[\p{L}\p{N}_]/u;
|
|
@@ -9179,6 +9217,7 @@ var init_gdscript_source = __esm(() => {
|
|
|
9179
9217
|
CLASS_NAME_LINE = new RegExp(`^${ANNOTATIONS}class_name\\s+(${NAME})`, "u");
|
|
9180
9218
|
EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
|
|
9181
9219
|
EXTENDS_CLAUSE = /\bextends(?=\s)/u;
|
|
9220
|
+
FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
|
|
9182
9221
|
KEYWORDS = new Set([
|
|
9183
9222
|
"and",
|
|
9184
9223
|
"as",
|
|
@@ -18138,7 +18177,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
18138
18177
|
profile: {
|
|
18139
18178
|
type: "boolean",
|
|
18140
18179
|
ops: ["start"],
|
|
18141
|
-
description: "start: profile the run's GDScript with the engine's own script profiler from its start, for editor_run profile to answer which functions took the time. A run this server starts dials the process that keeps it as its debugger, on loopback, with breakpoints and error breaks skipped so it never stops to wait; a game the editor plays is profiled through the editor's debugger session. Needs an engine built with debugging, which the editor binary is and a release export template is not. Default false."
|
|
18180
|
+
description: "start: profile the run's GDScript with the engine's own script profiler from its start, for editor_run profile to answer which functions took the time. A run this server starts dials the process that keeps it as its debugger, on loopback, with breakpoints and error breaks skipped so it never stops to wait; a game the editor plays is profiled through the editor's debugger session. Needs an engine built with debugging, which the editor binary is and a release export template is not. The start answers profile.on and, under profile.totals, how the numbers will arrive. Default false."
|
|
18181
|
+
},
|
|
18182
|
+
callersOf: {
|
|
18183
|
+
type: "string",
|
|
18184
|
+
ops: ["profile"],
|
|
18185
|
+
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 and a call made through a string is not."
|
|
18142
18186
|
},
|
|
18143
18187
|
limit: {
|
|
18144
18188
|
type: "integer",
|
|
@@ -18174,7 +18218,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18174
18218
|
},
|
|
18175
18219
|
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
|
|
18176
18220
|
profile: {
|
|
18177
|
-
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, 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. 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",
|
|
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",
|
|
18178
18222
|
requires: []
|
|
18179
18223
|
}
|
|
18180
18224
|
},
|
|
@@ -18392,7 +18436,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
18392
18436
|
summary: "what one property reads on a node, or through the objects it holds, refusing a name nothing along the way has",
|
|
18393
18437
|
requires: ["nodePath", "property"]
|
|
18394
18438
|
},
|
|
18395
|
-
metrics: {
|
|
18439
|
+
metrics: {
|
|
18440
|
+
summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game itself, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. The memory, object and render counts are read as asked",
|
|
18441
|
+
requires: []
|
|
18442
|
+
}
|
|
18396
18443
|
},
|
|
18397
18444
|
defaultOperation: "tree"
|
|
18398
18445
|
},
|
|
@@ -42261,6 +42308,13 @@ function readProfileFile(path) {
|
|
|
42261
42308
|
return null;
|
|
42262
42309
|
}
|
|
42263
42310
|
}
|
|
42311
|
+
function frameFunctionsFor(queued) {
|
|
42312
|
+
return Math.max(64, queued - QUEUE_HEADROOM);
|
|
42313
|
+
}
|
|
42314
|
+
function queuedMessagesOf(projectGodot) {
|
|
42315
|
+
const set = projectGodot === null ? undefined : parseProjectGodot(projectGodot)["network"]?.[QUEUE_SETTING];
|
|
42316
|
+
return typeof set === "number" && Number.isInteger(set) && set > 0 ? set : QUEUE_DEFAULT;
|
|
42317
|
+
}
|
|
42264
42318
|
function signatureParts(signature) {
|
|
42265
42319
|
const parts = signature.split("::");
|
|
42266
42320
|
if (parts.length < 3) {
|
|
@@ -42270,8 +42324,18 @@ function signatureParts(signature) {
|
|
|
42270
42324
|
const line = Number(parts.at(-2));
|
|
42271
42325
|
return { script: parts.slice(0, -2).join("::"), line: Number.isFinite(line) ? line : 0, function: name };
|
|
42272
42326
|
}
|
|
42273
|
-
function profiledFunctions(totals, limit) {
|
|
42274
|
-
const rows =
|
|
42327
|
+
function profiledFunctions(totals, limit, sourceOf = () => null) {
|
|
42328
|
+
const rows = rowsOf(totals);
|
|
42329
|
+
const scriptMs = ms(Object.values(totals.functions).reduce((sum, one) => sum + (one.calls > 0 ? one.selfSeconds : 0), 0));
|
|
42330
|
+
return {
|
|
42331
|
+
functions: rows.slice(0, limit).map((row) => placedLambda(row, sourceOf)),
|
|
42332
|
+
omitted: Math.max(0, rows.length - limit),
|
|
42333
|
+
scriptMs,
|
|
42334
|
+
counted: rows.length
|
|
42335
|
+
};
|
|
42336
|
+
}
|
|
42337
|
+
function rowsOf(totals) {
|
|
42338
|
+
return Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
|
|
42275
42339
|
const parts = signatureParts(signature);
|
|
42276
42340
|
return {
|
|
42277
42341
|
script: parts.script,
|
|
@@ -42282,17 +42346,41 @@ function profiledFunctions(totals, limit) {
|
|
|
42282
42346
|
totalMs: ms(sum.totalSeconds)
|
|
42283
42347
|
};
|
|
42284
42348
|
}).sort((a, b) => b.selfMs - a.selfMs || b.totalMs - a.totalMs || a.function.localeCompare(b.function));
|
|
42285
|
-
const scriptMs = ms(Object.values(totals.functions).reduce((sum, one) => sum + (one.calls > 0 ? one.selfSeconds : 0), 0));
|
|
42286
|
-
return {
|
|
42287
|
-
functions: rows.slice(0, limit),
|
|
42288
|
-
omitted: Math.max(0, rows.length - limit),
|
|
42289
|
-
scriptMs,
|
|
42290
|
-
counted: rows.length
|
|
42291
|
-
};
|
|
42292
42349
|
}
|
|
42293
|
-
|
|
42350
|
+
function bareName(fn) {
|
|
42351
|
+
return fn.slice(fn.lastIndexOf(".") + 1);
|
|
42352
|
+
}
|
|
42353
|
+
function placedLambda(row, sourceOf) {
|
|
42354
|
+
if (!row.function.endsWith("(lambda)")) {
|
|
42355
|
+
return row;
|
|
42356
|
+
}
|
|
42357
|
+
const source = sourceOf(row.script);
|
|
42358
|
+
const within = source === null ? null : enclosingFunction(source, row.line);
|
|
42359
|
+
return within === null ? row : { ...row, within };
|
|
42360
|
+
}
|
|
42361
|
+
function callersOf(totals, name, scripts) {
|
|
42362
|
+
const rows = rowsOf(totals);
|
|
42363
|
+
const bare = bareName(name);
|
|
42364
|
+
const target = rows.filter((row) => name.includes(".") ? row.function === name : bareName(row.function) === bare);
|
|
42365
|
+
const sites = [];
|
|
42366
|
+
for (const [script, source] of scripts) {
|
|
42367
|
+
for (const site of callSitesOf(source, bare)) {
|
|
42368
|
+
const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
|
|
42369
|
+
sites.push({
|
|
42370
|
+
script,
|
|
42371
|
+
...site,
|
|
42372
|
+
...within === undefined ? {} : { withinCalls: within.calls, withinSelfMs: within.selfMs, withinTotalMs: within.totalMs }
|
|
42373
|
+
});
|
|
42374
|
+
}
|
|
42375
|
+
}
|
|
42376
|
+
sites.sort((a, b) => (b.withinTotalMs ?? -1) - (a.withinTotalMs ?? -1) || a.script.localeCompare(b.script) || a.line - b.line);
|
|
42377
|
+
return { target, sites: sites.slice(0, CALL_SITES), omitted: Math.max(0, sites.length - CALL_SITES) };
|
|
42378
|
+
}
|
|
42379
|
+
var QUEUE_SETTING = "limits/debugger/max_queued_messages", QUEUE_DEFAULT = 2048, QUEUE_HEADROOM = 512, UNNAMED_PREFIX = "#", ms = (seconds) => Math.round(seconds * 1e6) / 1000, CALL_SITES = 40;
|
|
42294
42380
|
var init_script_profile = __esm(() => {
|
|
42381
|
+
init_gdscript_source();
|
|
42295
42382
|
init_godot_variant();
|
|
42383
|
+
init_resources();
|
|
42296
42384
|
});
|
|
42297
42385
|
|
|
42298
42386
|
// src/server.ts
|
|
@@ -42425,13 +42513,19 @@ function playOf(answer) {
|
|
|
42425
42513
|
const editorPid = readNumber(answer, "editorPid");
|
|
42426
42514
|
return play === undefined || editorPid === undefined ? null : { play, editorPid };
|
|
42427
42515
|
}
|
|
42428
|
-
function
|
|
42516
|
+
function playedSince(run, playing) {
|
|
42517
|
+
return run.throughEditor && run.play !== undefined && playing !== null && (playing.play !== run.play.play || playing.editorPid !== run.play.editorPid);
|
|
42518
|
+
}
|
|
42519
|
+
function runIsUp(run, editorSays, editorPlay = null, now = Date.now()) {
|
|
42429
42520
|
if (run?.throughEditor !== true) {
|
|
42430
42521
|
return stillRunning(run);
|
|
42431
42522
|
}
|
|
42432
42523
|
if (typeof run.endedHere === "string") {
|
|
42433
42524
|
return false;
|
|
42434
42525
|
}
|
|
42526
|
+
if (playedSince(run, editorPlay)) {
|
|
42527
|
+
return false;
|
|
42528
|
+
}
|
|
42435
42529
|
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
42436
42530
|
return false;
|
|
42437
42531
|
}
|
|
@@ -42975,6 +43069,63 @@ function aboveTheRunner(entry) {
|
|
|
42975
43069
|
]
|
|
42976
43070
|
};
|
|
42977
43071
|
}
|
|
43072
|
+
function runtimeAddonRegistered(projectPath) {
|
|
43073
|
+
const registered = inspectProject(projectPath);
|
|
43074
|
+
return registered.runtimeAutoloadPath !== null || registered.runtimeLoaderAutoload !== null;
|
|
43075
|
+
}
|
|
43076
|
+
function profileOnStart(profiled, through, runtimeAddon) {
|
|
43077
|
+
if (!profiled) {
|
|
43078
|
+
return;
|
|
43079
|
+
}
|
|
43080
|
+
const debuggerOf = through === "editor" ? "the editor's debugger session" : "the process that keeps the run";
|
|
43081
|
+
return {
|
|
43082
|
+
on: true,
|
|
43083
|
+
totals: `On from the game's start, through ${debuggerOf}. editor_run profile answers at any time: each frame is added as it ends, so a scene still inside a long _ready shows nothing until that returns, and the engine's own totals replace the sum when the game quits` + (runtimeAddon ? "." : ", which takes the runtime addon: without it the frames that arrived before the exit are what is kept.")
|
|
43084
|
+
};
|
|
43085
|
+
}
|
|
43086
|
+
function profileFrameFunctions(projectPath) {
|
|
43087
|
+
let projectGodot = null;
|
|
43088
|
+
if (projectPath !== null) {
|
|
43089
|
+
try {
|
|
43090
|
+
projectGodot = readFileSync23(join26(projectPath, "project.godot"), "utf8");
|
|
43091
|
+
} catch {}
|
|
43092
|
+
}
|
|
43093
|
+
return frameFunctionsFor(queuedMessagesOf(projectGodot));
|
|
43094
|
+
}
|
|
43095
|
+
function playedCap(held) {
|
|
43096
|
+
const frameFunctions = readPositiveNumber(held, "frameFunctions");
|
|
43097
|
+
if (frameFunctions === undefined) {
|
|
43098
|
+
return {};
|
|
43099
|
+
}
|
|
43100
|
+
return {
|
|
43101
|
+
frameFunctions,
|
|
43102
|
+
cappedFrames: readNonNegativeNumber(held, "cappedFrames") ?? 0,
|
|
43103
|
+
totalCapped: readBoolean(held, "totalCapped") === true
|
|
43104
|
+
};
|
|
43105
|
+
}
|
|
43106
|
+
function projectSourceOf(projectPath) {
|
|
43107
|
+
const read = new Map;
|
|
43108
|
+
return (script) => {
|
|
43109
|
+
if (!read.has(script)) {
|
|
43110
|
+
const inside = resolveWithinProject(projectPath, script);
|
|
43111
|
+
let source = null;
|
|
43112
|
+
if (inside.ok) {
|
|
43113
|
+
try {
|
|
43114
|
+
source = readFileSync23(inside.absolutePath, "utf8");
|
|
43115
|
+
} catch {}
|
|
43116
|
+
}
|
|
43117
|
+
read.set(script, source);
|
|
43118
|
+
}
|
|
43119
|
+
return read.get(script) ?? null;
|
|
43120
|
+
};
|
|
43121
|
+
}
|
|
43122
|
+
function projectScripts(projectPath) {
|
|
43123
|
+
const scripts = [];
|
|
43124
|
+
eachScript(projectPath, (script, source) => {
|
|
43125
|
+
scripts.push([script, source]);
|
|
43126
|
+
});
|
|
43127
|
+
return scripts;
|
|
43128
|
+
}
|
|
42978
43129
|
function profileAnswer(reading, limit) {
|
|
42979
43130
|
const { totals } = reading;
|
|
42980
43131
|
if (totals === null) {
|
|
@@ -42986,16 +43137,19 @@ function profileAnswer(reading, limit) {
|
|
|
42986
43137
|
note: reading.running ? "The game has not connected to the profiler yet: it dials its debugger once, as it starts, before any script runs. Ask again in a moment." : "The game never connected to the profiler, so nothing was measured. A game dials its debugger once, as it starts, and an engine built without debugging (a release export template) refuses to: profiling needs the editor binary or a debug template."
|
|
42987
43138
|
};
|
|
42988
43139
|
}
|
|
42989
|
-
const table = profiledFunctions(totals, limit);
|
|
43140
|
+
const table = profiledFunctions(totals, limit, reading.sourceOf);
|
|
42990
43141
|
const notes = [];
|
|
42991
43142
|
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.";
|
|
42992
|
-
if (totals.complete) {} else if (reading.running) {
|
|
43143
|
+
if (totals.complete) {} else if (reading.running && totals.frames === 0) {
|
|
43144
|
+
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
|
+
} else if (reading.running) {
|
|
42993
43146
|
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" : ""}.`);
|
|
42994
43147
|
} else if (totals.frames === 0) {
|
|
42995
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.${addonNote}`);
|
|
42996
43149
|
} else {
|
|
42997
43150
|
notes.push(`These are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} that arrived, summed, without the engine's own totals, so what the game did after the last of them is not in it: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${addonNote}`);
|
|
42998
43151
|
}
|
|
43152
|
+
notes.push(...cappedNotes(totals));
|
|
42999
43153
|
if (totals.unreadable > 0) {
|
|
43000
43154
|
notes.push(`${totals.unreadable} message${totals.unreadable === 1 ? "" : "s"} from the game could not be read, so the totals have that many gaps.`);
|
|
43001
43155
|
}
|
|
@@ -43010,9 +43164,36 @@ function profileAnswer(reading, limit) {
|
|
|
43010
43164
|
functionsCounted: table.counted,
|
|
43011
43165
|
omitted: table.omitted > 0 ? table.omitted : undefined,
|
|
43012
43166
|
functions: table.functions,
|
|
43167
|
+
callers: reading.callersOf === undefined ? undefined : callersAnswer(totals, reading.callersOf),
|
|
43013
43168
|
note: notes.length > 0 ? notes.join(" ") : undefined
|
|
43014
43169
|
};
|
|
43015
43170
|
}
|
|
43171
|
+
function cappedNotes(totals) {
|
|
43172
|
+
const notes = [];
|
|
43173
|
+
const most = totals.frameFunctions;
|
|
43174
|
+
const why = `${most} is kept below the game's debugger queue (network/limits/debugger/max_queued_messages), which drops the message naming a function when one frame names more than it holds, and a function whose name is dropped is never named afterwards.`;
|
|
43175
|
+
if (most !== undefined && totals.complete && totals.totalCapped === true) {
|
|
43176
|
+
notes.push(`The engine's totals carried the most functions the profiler asks for, ${most}, so the functions with the least time are not in them: ${why}`);
|
|
43177
|
+
} else if (most !== undefined && !totals.complete && (totals.cappedFrames ?? 0) > 0) {
|
|
43178
|
+
const capped = totals.cappedFrames ?? 0;
|
|
43179
|
+
notes.push(`${capped === totals.frames ? capped === 1 ? "The one frame" : `All ${capped} frames` : `${capped} of the ${totals.frames} frames`} carried the most functions the profiler asks for in one, ${most}, so the functions with the least time in ${capped === 1 ? "it" : "them"} are not counted there: ${why}`);
|
|
43180
|
+
}
|
|
43181
|
+
const unnamed = Object.entries(totals.functions).filter(([name, sum]) => name.startsWith(UNNAMED_PREFIX) && sum.calls > 0).length;
|
|
43182
|
+
if (unnamed > 0) {
|
|
43183
|
+
notes.push(`${unnamed === 1 ? "One function" : `${unnamed} functions`} arrived without ${unnamed === 1 ? "its name" : "their names"}, shown as ${UNNAMED_PREFIX} and the number the engine gave ${unnamed === 1 ? "it" : "them"}: the engine names a function once, as it first appears, and the game's debugger queue was full when that message was sent. They are script functions of the game; which ones cannot be recovered from this profile.`);
|
|
43184
|
+
}
|
|
43185
|
+
return notes;
|
|
43186
|
+
}
|
|
43187
|
+
function callersAnswer(totals, asked) {
|
|
43188
|
+
const found = callersOf(totals, asked.name, asked.scripts());
|
|
43189
|
+
return {
|
|
43190
|
+
of: asked.name,
|
|
43191
|
+
rows: found.target,
|
|
43192
|
+
sites: found.sites,
|
|
43193
|
+
omitted: found.omitted > 0 ? found.omitted : undefined,
|
|
43194
|
+
note: found.sites.length === 0 ? `No script in the project names ${asked.name} in code, outside its declaration: a call made through a string, such as call("name"), is not found by name.` : found.target.length === 0 ? `${CALLERS_FOUND_BY_NAME} The profile counts no call of ${asked.name}, so these places are where it would be called from.` : CALLERS_FOUND_BY_NAME
|
|
43195
|
+
};
|
|
43196
|
+
}
|
|
43016
43197
|
function foldedEntriesAnswer(log) {
|
|
43017
43198
|
const problems = log.selectFolded({
|
|
43018
43199
|
severity: "warning",
|
|
@@ -43586,7 +43767,7 @@ class GodotServer {
|
|
|
43586
43767
|
return this.saying(answer, {
|
|
43587
43768
|
update_available: {
|
|
43588
43769
|
...notice,
|
|
43589
|
-
what_to_do: "Tell the user a newer gdharness is out, with what changed. Run the upgrade command " + "only with their agreement: a yes now, or a standing instruction of theirs, such as " + "their CLAUDE.md, to upgrade on every release. Without either, offer it and wait, since
|
|
43770
|
+
what_to_do: "Tell the user a newer gdharness is out, with what changed. Run the upgrade command " + "only with their agreement: a yes now, or a standing instruction of theirs, such as one " + "in their AGENTS.md or CLAUDE.md, to upgrade on every release. Without either, offer it " + "and wait, since it replaces the addons in their project. Afterwards the MCP server has " + "to be reconnected and the editor restarted with editor_launch restart."
|
|
43590
43771
|
}
|
|
43591
43772
|
});
|
|
43592
43773
|
}
|
|
@@ -45822,6 +46003,7 @@ class GodotServer {
|
|
|
45822
46003
|
pid: started.pid,
|
|
45823
46004
|
arguments: cmdArgs,
|
|
45824
46005
|
transcript: started.transcript,
|
|
46006
|
+
profile: profileOnStart(profiled, "gdharness", runtimeAddonRegistered(project.value.path)),
|
|
45825
46007
|
refreshedClasses: refreshed.value,
|
|
45826
46008
|
...scanWaitAnswer(scanned),
|
|
45827
46009
|
savesNote: asked.value.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
@@ -45948,7 +46130,10 @@ class GodotServer {
|
|
|
45948
46130
|
const breakpoints = await this.dapHoldingBreakpointsOf(projectPath).reapplyBreakpoints();
|
|
45949
46131
|
this.logDebug(`Breakpoints were sent again after ${Date.now() - sendingFrom}ms`);
|
|
45950
46132
|
if (profiled) {
|
|
45951
|
-
const armed = await this.handleViaBridge("profile_start", {
|
|
46133
|
+
const armed = await this.handleViaBridge("profile_start", {
|
|
46134
|
+
arm: true,
|
|
46135
|
+
frameFunctions: profileFrameFunctions(projectPath)
|
|
46136
|
+
});
|
|
45952
46137
|
if (armed.isError === true) {
|
|
45953
46138
|
return this.profilerRefusal(armed);
|
|
45954
46139
|
}
|
|
@@ -45961,6 +46146,7 @@ class GodotServer {
|
|
|
45961
46146
|
const transcript = openTranscript(startedAt);
|
|
45962
46147
|
closeSync2(transcript.fd);
|
|
45963
46148
|
const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
46149
|
+
const which = playOf(playAnswer);
|
|
45964
46150
|
const played = {
|
|
45965
46151
|
pid: null,
|
|
45966
46152
|
log,
|
|
@@ -45973,11 +46159,11 @@ class GodotServer {
|
|
|
45973
46159
|
throughEditor: true,
|
|
45974
46160
|
brokeOn: null,
|
|
45975
46161
|
announcedBefore: alreadyPlaying,
|
|
45976
|
-
seenPlaying: readBoolean(playAnswer, "playing") === true
|
|
46162
|
+
seenPlaying: readBoolean(playAnswer, "playing") === true,
|
|
46163
|
+
...which === null ? {} : { play: which }
|
|
45977
46164
|
};
|
|
45978
46165
|
this.activeProcess = played;
|
|
45979
46166
|
this.watchThePlayedConsole();
|
|
45980
|
-
const which = playOf(playAnswer);
|
|
45981
46167
|
writeEditorRunNote({
|
|
45982
46168
|
projectPath,
|
|
45983
46169
|
transcript: transcript.path,
|
|
@@ -45989,6 +46175,7 @@ class GodotServer {
|
|
|
45989
46175
|
started: true,
|
|
45990
46176
|
through: "editor",
|
|
45991
46177
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
46178
|
+
profile: profileOnStart(profiled, "editor", runtimeAddonRegistered(projectPath)),
|
|
45992
46179
|
refreshedClasses,
|
|
45993
46180
|
...scanWaitAnswer(scanned),
|
|
45994
46181
|
debugPort: readNumber(playAnswer, "debugPort"),
|
|
@@ -46371,9 +46558,14 @@ class GodotServer {
|
|
|
46371
46558
|
return;
|
|
46372
46559
|
}
|
|
46373
46560
|
const going = this.activeProcess;
|
|
46374
|
-
const
|
|
46561
|
+
const asked = going?.throughEditor === true && going.play !== undefined ? await this.editorPlayingState() : null;
|
|
46562
|
+
const superseded = going !== null && asked !== null && playedSince(going, asked.play);
|
|
46563
|
+
if (superseded && stillRunning(going)) {
|
|
46564
|
+
going.endedUnwatched = true;
|
|
46565
|
+
}
|
|
46566
|
+
const givesWay = going !== null && (superseded || !stillRunning(going) && typeof going.endedHere === "string");
|
|
46375
46567
|
if (going !== null && !givesWay) {
|
|
46376
|
-
if (going.throughEditor && going.consoleLost === true && (await this.editorPlayingState())?.playing) {
|
|
46568
|
+
if (going.throughEditor && going.consoleLost === true && (asked ?? await this.editorPlayingState())?.playing) {
|
|
46377
46569
|
try {
|
|
46378
46570
|
await this.dap().connect();
|
|
46379
46571
|
going.log.append("stdout", `[gdharness] the debug adapter was reconnected here
|
|
@@ -46384,7 +46576,7 @@ class GodotServer {
|
|
|
46384
46576
|
}
|
|
46385
46577
|
return;
|
|
46386
46578
|
}
|
|
46387
|
-
const playing = await this.editorPlayingState();
|
|
46579
|
+
const playing = asked ?? await this.editorPlayingState();
|
|
46388
46580
|
if (playing?.playing !== true) {
|
|
46389
46581
|
return;
|
|
46390
46582
|
}
|
|
@@ -46407,7 +46599,8 @@ class GodotServer {
|
|
|
46407
46599
|
exitSignal: null,
|
|
46408
46600
|
throughEditor: true,
|
|
46409
46601
|
brokeOn: null,
|
|
46410
|
-
pickedUpPlaying: true
|
|
46602
|
+
pickedUpPlaying: true,
|
|
46603
|
+
...playing.play === null ? {} : { play: playing.play }
|
|
46411
46604
|
};
|
|
46412
46605
|
this.activeProcess = picked;
|
|
46413
46606
|
this.watchThePlayedConsole();
|
|
@@ -46711,11 +46904,19 @@ class GodotServer {
|
|
|
46711
46904
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
46712
46905
|
}
|
|
46713
46906
|
const limit = readPositiveNumber(args, "limit") ?? PROFILE_FUNCTIONS;
|
|
46907
|
+
const callersName = readString(args, "callersOf");
|
|
46908
|
+
if (callersName !== undefined && !callersName.split(".").every(isIdentifier)) {
|
|
46909
|
+
return this.createErrorResponse(`callersOf takes a function's name, or Class.name for the functions of one class, and ${JSON.stringify(callersName)} is neither.`, ["editor_run profile names each function as it answers; pass one of those"]);
|
|
46910
|
+
}
|
|
46714
46911
|
const going = await this.runStillGoing(run);
|
|
46715
|
-
const
|
|
46716
|
-
const
|
|
46912
|
+
const projectPath = run.projectPath;
|
|
46913
|
+
const fromProject = projectPath === null ? { runtimeAddon: false } : {
|
|
46914
|
+
runtimeAddon: runtimeAddonRegistered(projectPath),
|
|
46915
|
+
sourceOf: projectSourceOf(projectPath),
|
|
46916
|
+
...callersName === undefined ? {} : { callersOf: { name: callersName, scripts: () => projectScripts(projectPath) } }
|
|
46917
|
+
};
|
|
46717
46918
|
if (run.throughEditor) {
|
|
46718
|
-
return await this.profileOfThePlayedGame(going,
|
|
46919
|
+
return await this.profileOfThePlayedGame(going, projectPath, fromProject, limit);
|
|
46719
46920
|
}
|
|
46720
46921
|
const record = run.projectPath === null ? null : readRunRecord(run.projectPath);
|
|
46721
46922
|
const path = record !== null && record.pid === run.pid ? record.profile : undefined;
|
|
@@ -46734,10 +46935,10 @@ class GodotServer {
|
|
|
46734
46935
|
running: going,
|
|
46735
46936
|
totals,
|
|
46736
46937
|
coveredMs: totals?.lastFrameAt == null ? 0 : totals.lastFrameAt - totals.startedAt,
|
|
46737
|
-
|
|
46938
|
+
...fromProject
|
|
46738
46939
|
}, limit));
|
|
46739
46940
|
}
|
|
46740
|
-
async profileOfThePlayedGame(going,
|
|
46941
|
+
async profileOfThePlayedGame(going, projectPath, fromProject, limit) {
|
|
46741
46942
|
const read = await this.handleViaBridge("profile_read", {});
|
|
46742
46943
|
if (read.isError === true) {
|
|
46743
46944
|
return this.profilerRefusal(read);
|
|
@@ -46749,7 +46950,9 @@ class GodotServer {
|
|
|
46749
46950
|
"editor_run start with profile: true plays it again with the profiler on from its start"
|
|
46750
46951
|
]);
|
|
46751
46952
|
}
|
|
46752
|
-
const started = await this.handleViaBridge("profile_start", {
|
|
46953
|
+
const started = await this.handleViaBridge("profile_start", {
|
|
46954
|
+
frameFunctions: profileFrameFunctions(projectPath)
|
|
46955
|
+
});
|
|
46753
46956
|
if (started.isError === true) {
|
|
46754
46957
|
return this.profilerRefusal(started);
|
|
46755
46958
|
}
|
|
@@ -46772,6 +46975,7 @@ class GodotServer {
|
|
|
46772
46975
|
frames: readNonNegativeNumber(held, "frames") ?? 0,
|
|
46773
46976
|
complete: readBoolean(held, "complete") === true,
|
|
46774
46977
|
unreadable: 0,
|
|
46978
|
+
...playedCap(held),
|
|
46775
46979
|
functions: Object.fromEntries(Object.entries(functions).map(([signature, sum]) => {
|
|
46776
46980
|
const [calls, self, total] = Array.isArray(sum) ? sum : [];
|
|
46777
46981
|
return [
|
|
@@ -46789,7 +46993,7 @@ class GodotServer {
|
|
|
46789
46993
|
running: going,
|
|
46790
46994
|
totals,
|
|
46791
46995
|
coveredMs: readNonNegativeNumber(held, "coveredMs") ?? 0,
|
|
46792
|
-
|
|
46996
|
+
...fromProject
|
|
46793
46997
|
}, limit));
|
|
46794
46998
|
}
|
|
46795
46999
|
async firstProfiledFrame() {
|
|
@@ -46848,11 +47052,13 @@ class GodotServer {
|
|
|
46848
47052
|
if (announced !== undefined && editorSays === undefined) {
|
|
46849
47053
|
return stillRunning(run);
|
|
46850
47054
|
}
|
|
46851
|
-
const
|
|
46852
|
-
|
|
47055
|
+
const state = editorSays === undefined ? await this.editorPlayingState() : undefined;
|
|
47056
|
+
const said = state === undefined ? editorSays ?? null : state?.playing ?? null;
|
|
47057
|
+
const play = state?.play ?? null;
|
|
47058
|
+
if (said === true && !playedSince(run, play)) {
|
|
46853
47059
|
run.seenPlaying = true;
|
|
46854
47060
|
}
|
|
46855
|
-
return runIsUp(run, said);
|
|
47061
|
+
return runIsUp(run, said, play);
|
|
46856
47062
|
}
|
|
46857
47063
|
async untilTheExitIsRecorded(run) {
|
|
46858
47064
|
const until = Date.now() + EXIT_REPORTED_WITHIN_MS;
|
|
@@ -48058,7 +48264,7 @@ async function runGodotServer() {
|
|
|
48058
48264
|
const server = new GodotServer;
|
|
48059
48265
|
await server.run();
|
|
48060
48266
|
}
|
|
48061
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, 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, SCAN_WAIT_MS = 30000, 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, MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "';
|
|
48267
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, 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, SCAN_WAIT_MS = 30000, 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 "';
|
|
48062
48268
|
var init_server3 = __esm(() => {
|
|
48063
48269
|
init_mcp();
|
|
48064
48270
|
init_stdio2();
|
|
@@ -48079,6 +48285,7 @@ var init_server3 = __esm(() => {
|
|
|
48079
48285
|
init_engine_run();
|
|
48080
48286
|
init_errors();
|
|
48081
48287
|
init_game_log();
|
|
48288
|
+
init_gdscript_source();
|
|
48082
48289
|
init_godot_bridge();
|
|
48083
48290
|
init_godot_path();
|
|
48084
48291
|
init_headless();
|
|
@@ -49189,7 +49396,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
49189
49396
|
| What every label on a screen says | \`runtime_inspect tree\` with \`properties\` naming \`text\`, which reads it off every node that has one; \`includeProperties\` is every stored property of every node and ran to seventy-six thousand characters for one row |
|
|
49190
49397
|
| Whether this engine has a method or property, and what it takes | \`editor_classes info\` with \`member\`, which answers that one member and which class declares it rather than the whole class |
|
|
49191
49398
|
| Wait for a run to finish | \`editor_run wait\`, never a sleep or a shell loop on the pid: it answers as \`editor_output\` does, once the run is over |
|
|
49192
|
-
| Find which GDScript makes a run slow | \`editor_run start\` with \`profile\` set, then \`editor_run profile\`: the engine's own script profiler, per function with calls and self and total milliseconds, by self time, never \`Time.get_ticks_usec()\` laps edited into the game. A scene that does its work in \`_ready\` and quits is covered whole when the runtime autoload is registered, and a game the editor is already playing is profiled from the moment it is asked |
|
|
49399
|
+
| Find which GDScript makes a run slow | \`editor_run start\` with \`profile\` set, then \`editor_run profile\`: the engine's own script profiler, per function with calls and self and total milliseconds, by self time, never \`Time.get_ticks_usec()\` laps edited into the game. A scene that does its work in \`_ready\` and quits is covered whole when the runtime autoload is registered, and a game the editor is already playing is profiled from the moment it is asked. \`callersOf\` names where the project's scripts call one function, each place with its function's numbers, since the engine records no callers |
|
|
49193
49400
|
| End a bench and the workers it opened | \`editor_run stop\` with \`andChildren\`, never a process listing by scene name: it ends every process under the run's, to any depth, that is a game of the project, announced as one or the project's engine run with \`--path\` on it, names them under \`endedChildren\` with what identified each, and leaves and names any other |
|
|
49194
49401
|
| End a game of the project that no server is holding | \`editor_run stop\` with its \`pid\` from \`editor_status\` \`runtimes\`, never a process kill by hand: it is ended only when it announced itself for the project and its command line is the project's engine run |
|
|
49195
49402
|
| Play a game without touching the player's saves | \`editor_run start\` with \`savesIn\`, a directory for this run's \`user://\`: the editor plays with its own environment, so a run given one is started by the server, and the answer says under \`savesNote\` when the platform ignores it |
|
|
@@ -127,7 +127,10 @@ func stop_playing(_args: Dictionary) -> Dictionary:
|
|
|
127
127
|
var was_playing: bool = EditorInterface.is_playing_scene()
|
|
128
128
|
if was_playing:
|
|
129
129
|
EditorInterface.stop_playing_scene()
|
|
130
|
-
|
|
130
|
+
# Known over now rather than at the next frame's look: a play somebody else starts before that
|
|
131
|
+
# look would otherwise be read as this one still going, and given its number and its console.
|
|
132
|
+
_was_playing = EditorInterface.is_playing_scene()
|
|
133
|
+
return {"ok": true, "wasPlaying": was_playing, "playing": _was_playing}
|
|
131
134
|
|
|
132
135
|
|
|
133
136
|
## Restarts the editor, which is how a replaced addon is picked up.
|