gdharness 1.1.57 → 1.1.58
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 +187 -34
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +4 -1
- package/build/index.js +183 -30
- package/build/keeper.js +54 -0
- 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
|
},
|
|
@@ -42270,8 +42314,18 @@ function signatureParts(signature) {
|
|
|
42270
42314
|
const line = Number(parts.at(-2));
|
|
42271
42315
|
return { script: parts.slice(0, -2).join("::"), line: Number.isFinite(line) ? line : 0, function: name };
|
|
42272
42316
|
}
|
|
42273
|
-
function profiledFunctions(totals, limit) {
|
|
42274
|
-
const rows =
|
|
42317
|
+
function profiledFunctions(totals, limit, sourceOf = () => null) {
|
|
42318
|
+
const rows = rowsOf(totals);
|
|
42319
|
+
const scriptMs = ms(Object.values(totals.functions).reduce((sum, one) => sum + (one.calls > 0 ? one.selfSeconds : 0), 0));
|
|
42320
|
+
return {
|
|
42321
|
+
functions: rows.slice(0, limit).map((row) => placedLambda(row, sourceOf)),
|
|
42322
|
+
omitted: Math.max(0, rows.length - limit),
|
|
42323
|
+
scriptMs,
|
|
42324
|
+
counted: rows.length
|
|
42325
|
+
};
|
|
42326
|
+
}
|
|
42327
|
+
function rowsOf(totals) {
|
|
42328
|
+
return Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
|
|
42275
42329
|
const parts = signatureParts(signature);
|
|
42276
42330
|
return {
|
|
42277
42331
|
script: parts.script,
|
|
@@ -42282,16 +42336,39 @@ function profiledFunctions(totals, limit) {
|
|
|
42282
42336
|
totalMs: ms(sum.totalSeconds)
|
|
42283
42337
|
};
|
|
42284
42338
|
}).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
42339
|
}
|
|
42293
|
-
|
|
42340
|
+
function bareName(fn) {
|
|
42341
|
+
return fn.slice(fn.lastIndexOf(".") + 1);
|
|
42342
|
+
}
|
|
42343
|
+
function placedLambda(row, sourceOf) {
|
|
42344
|
+
if (!row.function.endsWith("(lambda)")) {
|
|
42345
|
+
return row;
|
|
42346
|
+
}
|
|
42347
|
+
const source = sourceOf(row.script);
|
|
42348
|
+
const within = source === null ? null : enclosingFunction(source, row.line);
|
|
42349
|
+
return within === null ? row : { ...row, within };
|
|
42350
|
+
}
|
|
42351
|
+
function callersOf(totals, name, scripts) {
|
|
42352
|
+
const rows = rowsOf(totals);
|
|
42353
|
+
const bare = bareName(name);
|
|
42354
|
+
const target = rows.filter((row) => name.includes(".") ? row.function === name : bareName(row.function) === bare);
|
|
42355
|
+
const sites = [];
|
|
42356
|
+
for (const [script, source] of scripts) {
|
|
42357
|
+
for (const site of callSitesOf(source, bare)) {
|
|
42358
|
+
const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
|
|
42359
|
+
sites.push({
|
|
42360
|
+
script,
|
|
42361
|
+
...site,
|
|
42362
|
+
...within === undefined ? {} : { withinCalls: within.calls, withinSelfMs: within.selfMs, withinTotalMs: within.totalMs }
|
|
42363
|
+
});
|
|
42364
|
+
}
|
|
42365
|
+
}
|
|
42366
|
+
sites.sort((a, b) => (b.withinTotalMs ?? -1) - (a.withinTotalMs ?? -1) || a.script.localeCompare(b.script) || a.line - b.line);
|
|
42367
|
+
return { target, sites: sites.slice(0, CALL_SITES), omitted: Math.max(0, sites.length - CALL_SITES) };
|
|
42368
|
+
}
|
|
42369
|
+
var ms = (seconds) => Math.round(seconds * 1e6) / 1000, CALL_SITES = 40;
|
|
42294
42370
|
var init_script_profile = __esm(() => {
|
|
42371
|
+
init_gdscript_source();
|
|
42295
42372
|
init_godot_variant();
|
|
42296
42373
|
});
|
|
42297
42374
|
|
|
@@ -42425,13 +42502,19 @@ function playOf(answer) {
|
|
|
42425
42502
|
const editorPid = readNumber(answer, "editorPid");
|
|
42426
42503
|
return play === undefined || editorPid === undefined ? null : { play, editorPid };
|
|
42427
42504
|
}
|
|
42428
|
-
function
|
|
42505
|
+
function playedSince(run, playing) {
|
|
42506
|
+
return run.throughEditor && run.play !== undefined && playing !== null && (playing.play !== run.play.play || playing.editorPid !== run.play.editorPid);
|
|
42507
|
+
}
|
|
42508
|
+
function runIsUp(run, editorSays, editorPlay = null, now = Date.now()) {
|
|
42429
42509
|
if (run?.throughEditor !== true) {
|
|
42430
42510
|
return stillRunning(run);
|
|
42431
42511
|
}
|
|
42432
42512
|
if (typeof run.endedHere === "string") {
|
|
42433
42513
|
return false;
|
|
42434
42514
|
}
|
|
42515
|
+
if (playedSince(run, editorPlay)) {
|
|
42516
|
+
return false;
|
|
42517
|
+
}
|
|
42435
42518
|
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
42436
42519
|
return false;
|
|
42437
42520
|
}
|
|
@@ -42975,6 +43058,43 @@ function aboveTheRunner(entry) {
|
|
|
42975
43058
|
]
|
|
42976
43059
|
};
|
|
42977
43060
|
}
|
|
43061
|
+
function runtimeAddonRegistered(projectPath) {
|
|
43062
|
+
const registered = inspectProject(projectPath);
|
|
43063
|
+
return registered.runtimeAutoloadPath !== null || registered.runtimeLoaderAutoload !== null;
|
|
43064
|
+
}
|
|
43065
|
+
function profileOnStart(profiled, through, runtimeAddon) {
|
|
43066
|
+
if (!profiled) {
|
|
43067
|
+
return;
|
|
43068
|
+
}
|
|
43069
|
+
const debuggerOf = through === "editor" ? "the editor's debugger session" : "the process that keeps the run";
|
|
43070
|
+
return {
|
|
43071
|
+
on: true,
|
|
43072
|
+
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.")
|
|
43073
|
+
};
|
|
43074
|
+
}
|
|
43075
|
+
function projectSourceOf(projectPath) {
|
|
43076
|
+
const read = new Map;
|
|
43077
|
+
return (script) => {
|
|
43078
|
+
if (!read.has(script)) {
|
|
43079
|
+
const inside = resolveWithinProject(projectPath, script);
|
|
43080
|
+
let source = null;
|
|
43081
|
+
if (inside.ok) {
|
|
43082
|
+
try {
|
|
43083
|
+
source = readFileSync23(inside.absolutePath, "utf8");
|
|
43084
|
+
} catch {}
|
|
43085
|
+
}
|
|
43086
|
+
read.set(script, source);
|
|
43087
|
+
}
|
|
43088
|
+
return read.get(script) ?? null;
|
|
43089
|
+
};
|
|
43090
|
+
}
|
|
43091
|
+
function projectScripts(projectPath) {
|
|
43092
|
+
const scripts = [];
|
|
43093
|
+
eachScript(projectPath, (script, source) => {
|
|
43094
|
+
scripts.push([script, source]);
|
|
43095
|
+
});
|
|
43096
|
+
return scripts;
|
|
43097
|
+
}
|
|
42978
43098
|
function profileAnswer(reading, limit) {
|
|
42979
43099
|
const { totals } = reading;
|
|
42980
43100
|
if (totals === null) {
|
|
@@ -42986,10 +43106,12 @@ function profileAnswer(reading, limit) {
|
|
|
42986
43106
|
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
43107
|
};
|
|
42988
43108
|
}
|
|
42989
|
-
const table = profiledFunctions(totals, limit);
|
|
43109
|
+
const table = profiledFunctions(totals, limit, reading.sourceOf);
|
|
42990
43110
|
const notes = [];
|
|
42991
43111
|
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) {
|
|
43112
|
+
if (totals.complete) {} else if (reading.running && totals.frames === 0) {
|
|
43113
|
+
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}`);
|
|
43114
|
+
} else if (reading.running) {
|
|
42993
43115
|
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
43116
|
} else if (totals.frames === 0) {
|
|
42995
43117
|
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}`);
|
|
@@ -43010,9 +43132,20 @@ function profileAnswer(reading, limit) {
|
|
|
43010
43132
|
functionsCounted: table.counted,
|
|
43011
43133
|
omitted: table.omitted > 0 ? table.omitted : undefined,
|
|
43012
43134
|
functions: table.functions,
|
|
43135
|
+
callers: reading.callersOf === undefined ? undefined : callersAnswer(totals, reading.callersOf),
|
|
43013
43136
|
note: notes.length > 0 ? notes.join(" ") : undefined
|
|
43014
43137
|
};
|
|
43015
43138
|
}
|
|
43139
|
+
function callersAnswer(totals, asked) {
|
|
43140
|
+
const found = callersOf(totals, asked.name, asked.scripts());
|
|
43141
|
+
return {
|
|
43142
|
+
of: asked.name,
|
|
43143
|
+
rows: found.target,
|
|
43144
|
+
sites: found.sites,
|
|
43145
|
+
omitted: found.omitted > 0 ? found.omitted : undefined,
|
|
43146
|
+
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
|
|
43147
|
+
};
|
|
43148
|
+
}
|
|
43016
43149
|
function foldedEntriesAnswer(log) {
|
|
43017
43150
|
const problems = log.selectFolded({
|
|
43018
43151
|
severity: "warning",
|
|
@@ -43586,7 +43719,7 @@ class GodotServer {
|
|
|
43586
43719
|
return this.saying(answer, {
|
|
43587
43720
|
update_available: {
|
|
43588
43721
|
...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
|
|
43722
|
+
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
43723
|
}
|
|
43591
43724
|
});
|
|
43592
43725
|
}
|
|
@@ -45822,6 +45955,7 @@ class GodotServer {
|
|
|
45822
45955
|
pid: started.pid,
|
|
45823
45956
|
arguments: cmdArgs,
|
|
45824
45957
|
transcript: started.transcript,
|
|
45958
|
+
profile: profileOnStart(profiled, "gdharness", runtimeAddonRegistered(project.value.path)),
|
|
45825
45959
|
refreshedClasses: refreshed.value,
|
|
45826
45960
|
...scanWaitAnswer(scanned),
|
|
45827
45961
|
savesNote: asked.value.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
@@ -45961,6 +46095,7 @@ class GodotServer {
|
|
|
45961
46095
|
const transcript = openTranscript(startedAt);
|
|
45962
46096
|
closeSync2(transcript.fd);
|
|
45963
46097
|
const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
46098
|
+
const which = playOf(playAnswer);
|
|
45964
46099
|
const played = {
|
|
45965
46100
|
pid: null,
|
|
45966
46101
|
log,
|
|
@@ -45973,11 +46108,11 @@ class GodotServer {
|
|
|
45973
46108
|
throughEditor: true,
|
|
45974
46109
|
brokeOn: null,
|
|
45975
46110
|
announcedBefore: alreadyPlaying,
|
|
45976
|
-
seenPlaying: readBoolean(playAnswer, "playing") === true
|
|
46111
|
+
seenPlaying: readBoolean(playAnswer, "playing") === true,
|
|
46112
|
+
...which === null ? {} : { play: which }
|
|
45977
46113
|
};
|
|
45978
46114
|
this.activeProcess = played;
|
|
45979
46115
|
this.watchThePlayedConsole();
|
|
45980
|
-
const which = playOf(playAnswer);
|
|
45981
46116
|
writeEditorRunNote({
|
|
45982
46117
|
projectPath,
|
|
45983
46118
|
transcript: transcript.path,
|
|
@@ -45989,6 +46124,7 @@ class GodotServer {
|
|
|
45989
46124
|
started: true,
|
|
45990
46125
|
through: "editor",
|
|
45991
46126
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
46127
|
+
profile: profileOnStart(profiled, "editor", runtimeAddonRegistered(projectPath)),
|
|
45992
46128
|
refreshedClasses,
|
|
45993
46129
|
...scanWaitAnswer(scanned),
|
|
45994
46130
|
debugPort: readNumber(playAnswer, "debugPort"),
|
|
@@ -46371,9 +46507,14 @@ class GodotServer {
|
|
|
46371
46507
|
return;
|
|
46372
46508
|
}
|
|
46373
46509
|
const going = this.activeProcess;
|
|
46374
|
-
const
|
|
46510
|
+
const asked = going?.throughEditor === true && going.play !== undefined ? await this.editorPlayingState() : null;
|
|
46511
|
+
const superseded = going !== null && asked !== null && playedSince(going, asked.play);
|
|
46512
|
+
if (superseded && stillRunning(going)) {
|
|
46513
|
+
going.endedUnwatched = true;
|
|
46514
|
+
}
|
|
46515
|
+
const givesWay = going !== null && (superseded || !stillRunning(going) && typeof going.endedHere === "string");
|
|
46375
46516
|
if (going !== null && !givesWay) {
|
|
46376
|
-
if (going.throughEditor && going.consoleLost === true && (await this.editorPlayingState())?.playing) {
|
|
46517
|
+
if (going.throughEditor && going.consoleLost === true && (asked ?? await this.editorPlayingState())?.playing) {
|
|
46377
46518
|
try {
|
|
46378
46519
|
await this.dap().connect();
|
|
46379
46520
|
going.log.append("stdout", `[gdharness] the debug adapter was reconnected here
|
|
@@ -46384,7 +46525,7 @@ class GodotServer {
|
|
|
46384
46525
|
}
|
|
46385
46526
|
return;
|
|
46386
46527
|
}
|
|
46387
|
-
const playing = await this.editorPlayingState();
|
|
46528
|
+
const playing = asked ?? await this.editorPlayingState();
|
|
46388
46529
|
if (playing?.playing !== true) {
|
|
46389
46530
|
return;
|
|
46390
46531
|
}
|
|
@@ -46407,7 +46548,8 @@ class GodotServer {
|
|
|
46407
46548
|
exitSignal: null,
|
|
46408
46549
|
throughEditor: true,
|
|
46409
46550
|
brokeOn: null,
|
|
46410
|
-
pickedUpPlaying: true
|
|
46551
|
+
pickedUpPlaying: true,
|
|
46552
|
+
...playing.play === null ? {} : { play: playing.play }
|
|
46411
46553
|
};
|
|
46412
46554
|
this.activeProcess = picked;
|
|
46413
46555
|
this.watchThePlayedConsole();
|
|
@@ -46711,11 +46853,19 @@ class GodotServer {
|
|
|
46711
46853
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
46712
46854
|
}
|
|
46713
46855
|
const limit = readPositiveNumber(args, "limit") ?? PROFILE_FUNCTIONS;
|
|
46856
|
+
const callersName = readString(args, "callersOf");
|
|
46857
|
+
if (callersName !== undefined && !callersName.split(".").every(isIdentifier)) {
|
|
46858
|
+
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"]);
|
|
46859
|
+
}
|
|
46714
46860
|
const going = await this.runStillGoing(run);
|
|
46715
|
-
const
|
|
46716
|
-
const
|
|
46861
|
+
const projectPath = run.projectPath;
|
|
46862
|
+
const fromProject = projectPath === null ? { runtimeAddon: false } : {
|
|
46863
|
+
runtimeAddon: runtimeAddonRegistered(projectPath),
|
|
46864
|
+
sourceOf: projectSourceOf(projectPath),
|
|
46865
|
+
...callersName === undefined ? {} : { callersOf: { name: callersName, scripts: () => projectScripts(projectPath) } }
|
|
46866
|
+
};
|
|
46717
46867
|
if (run.throughEditor) {
|
|
46718
|
-
return await this.profileOfThePlayedGame(going,
|
|
46868
|
+
return await this.profileOfThePlayedGame(going, fromProject, limit);
|
|
46719
46869
|
}
|
|
46720
46870
|
const record = run.projectPath === null ? null : readRunRecord(run.projectPath);
|
|
46721
46871
|
const path = record !== null && record.pid === run.pid ? record.profile : undefined;
|
|
@@ -46734,10 +46884,10 @@ class GodotServer {
|
|
|
46734
46884
|
running: going,
|
|
46735
46885
|
totals,
|
|
46736
46886
|
coveredMs: totals?.lastFrameAt == null ? 0 : totals.lastFrameAt - totals.startedAt,
|
|
46737
|
-
|
|
46887
|
+
...fromProject
|
|
46738
46888
|
}, limit));
|
|
46739
46889
|
}
|
|
46740
|
-
async profileOfThePlayedGame(going,
|
|
46890
|
+
async profileOfThePlayedGame(going, fromProject, limit) {
|
|
46741
46891
|
const read = await this.handleViaBridge("profile_read", {});
|
|
46742
46892
|
if (read.isError === true) {
|
|
46743
46893
|
return this.profilerRefusal(read);
|
|
@@ -46789,7 +46939,7 @@ class GodotServer {
|
|
|
46789
46939
|
running: going,
|
|
46790
46940
|
totals,
|
|
46791
46941
|
coveredMs: readNonNegativeNumber(held, "coveredMs") ?? 0,
|
|
46792
|
-
|
|
46942
|
+
...fromProject
|
|
46793
46943
|
}, limit));
|
|
46794
46944
|
}
|
|
46795
46945
|
async firstProfiledFrame() {
|
|
@@ -46848,11 +46998,13 @@ class GodotServer {
|
|
|
46848
46998
|
if (announced !== undefined && editorSays === undefined) {
|
|
46849
46999
|
return stillRunning(run);
|
|
46850
47000
|
}
|
|
46851
|
-
const
|
|
46852
|
-
|
|
47001
|
+
const state = editorSays === undefined ? await this.editorPlayingState() : undefined;
|
|
47002
|
+
const said = state === undefined ? editorSays ?? null : state?.playing ?? null;
|
|
47003
|
+
const play = state?.play ?? null;
|
|
47004
|
+
if (said === true && !playedSince(run, play)) {
|
|
46853
47005
|
run.seenPlaying = true;
|
|
46854
47006
|
}
|
|
46855
|
-
return runIsUp(run, said);
|
|
47007
|
+
return runIsUp(run, said, play);
|
|
46856
47008
|
}
|
|
46857
47009
|
async untilTheExitIsRecorded(run) {
|
|
46858
47010
|
const until = Date.now() + EXIT_REPORTED_WITHIN_MS;
|
|
@@ -48058,7 +48210,7 @@ async function runGodotServer() {
|
|
|
48058
48210
|
const server = new GodotServer;
|
|
48059
48211
|
await server.run();
|
|
48060
48212
|
}
|
|
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 "';
|
|
48213
|
+
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
48214
|
var init_server3 = __esm(() => {
|
|
48063
48215
|
init_mcp();
|
|
48064
48216
|
init_stdio2();
|
|
@@ -48079,6 +48231,7 @@ var init_server3 = __esm(() => {
|
|
|
48079
48231
|
init_engine_run();
|
|
48080
48232
|
init_errors();
|
|
48081
48233
|
init_game_log();
|
|
48234
|
+
init_gdscript_source();
|
|
48082
48235
|
init_godot_bridge();
|
|
48083
48236
|
init_godot_path();
|
|
48084
48237
|
init_headless();
|
|
@@ -49189,7 +49342,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
49189
49342
|
| 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
49343
|
| 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
49344
|
| 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 |
|
|
49345
|
+
| 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
49346
|
| 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
49347
|
| 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
49348
|
| 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.
|
package/build/index.js
CHANGED
|
@@ -26523,6 +26523,45 @@ ${line.text}`;
|
|
|
26523
26523
|
const { body } = parseBody(null, null, 0, 0, 0);
|
|
26524
26524
|
return { className, body };
|
|
26525
26525
|
}
|
|
26526
|
+
var FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
|
|
26527
|
+
function indentOf(text) {
|
|
26528
|
+
return /^[ \t]*/.exec(text)?.[0].length ?? 0;
|
|
26529
|
+
}
|
|
26530
|
+
function enclosingFunction(source, line, code = codeOf(source)) {
|
|
26531
|
+
const lines = code.split(`
|
|
26532
|
+
`);
|
|
26533
|
+
const at = lines[line - 1];
|
|
26534
|
+
if (at === undefined) {
|
|
26535
|
+
return null;
|
|
26536
|
+
}
|
|
26537
|
+
const own = FUNC_LINE.exec(at);
|
|
26538
|
+
if (own?.[1] !== undefined) {
|
|
26539
|
+
return own[1];
|
|
26540
|
+
}
|
|
26541
|
+
let indent = indentOf(at);
|
|
26542
|
+
for (let index = line - 2;index >= 0 && indent > 0; index -= 1) {
|
|
26543
|
+
const text = lines[index] ?? "";
|
|
26544
|
+
if (text.trim() === "" || indentOf(text) >= indent) {
|
|
26545
|
+
continue;
|
|
26546
|
+
}
|
|
26547
|
+
indent = indentOf(text);
|
|
26548
|
+
const declared = FUNC_LINE.exec(text);
|
|
26549
|
+
if (declared?.[1] !== undefined) {
|
|
26550
|
+
return declared[1];
|
|
26551
|
+
}
|
|
26552
|
+
}
|
|
26553
|
+
return null;
|
|
26554
|
+
}
|
|
26555
|
+
function callSitesOf(source, name) {
|
|
26556
|
+
const regions = regionsOf(source);
|
|
26557
|
+
const code = codeOf(source, regions);
|
|
26558
|
+
const lines = new Lines(source);
|
|
26559
|
+
return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).map((one) => {
|
|
26560
|
+
const line = lines.positionOf(one.offset).line + 1;
|
|
26561
|
+
const after = code.slice(one.offset + name.length).trimStart();
|
|
26562
|
+
return { line, within: enclosingFunction(source, line, code), called: after.startsWith("(") };
|
|
26563
|
+
});
|
|
26564
|
+
}
|
|
26526
26565
|
function collectEnumValues(text, from, into) {
|
|
26527
26566
|
const close = text.indexOf("}");
|
|
26528
26567
|
const inside = text.slice(1, close === -1 ? text.length : close);
|
|
@@ -33072,8 +33111,18 @@ function signatureParts(signature) {
|
|
|
33072
33111
|
return { script: parts.slice(0, -2).join("::"), line: Number.isFinite(line) ? line : 0, function: name };
|
|
33073
33112
|
}
|
|
33074
33113
|
var ms = (seconds) => Math.round(seconds * 1e6) / 1000;
|
|
33075
|
-
function profiledFunctions(totals, limit) {
|
|
33076
|
-
const rows =
|
|
33114
|
+
function profiledFunctions(totals, limit, sourceOf = () => null) {
|
|
33115
|
+
const rows = rowsOf(totals);
|
|
33116
|
+
const scriptMs = ms(Object.values(totals.functions).reduce((sum, one) => sum + (one.calls > 0 ? one.selfSeconds : 0), 0));
|
|
33117
|
+
return {
|
|
33118
|
+
functions: rows.slice(0, limit).map((row) => placedLambda(row, sourceOf)),
|
|
33119
|
+
omitted: Math.max(0, rows.length - limit),
|
|
33120
|
+
scriptMs,
|
|
33121
|
+
counted: rows.length
|
|
33122
|
+
};
|
|
33123
|
+
}
|
|
33124
|
+
function rowsOf(totals) {
|
|
33125
|
+
return Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
|
|
33077
33126
|
const parts = signatureParts(signature);
|
|
33078
33127
|
return {
|
|
33079
33128
|
script: parts.script,
|
|
@@ -33084,13 +33133,36 @@ function profiledFunctions(totals, limit) {
|
|
|
33084
33133
|
totalMs: ms(sum.totalSeconds)
|
|
33085
33134
|
};
|
|
33086
33135
|
}).sort((a, b) => b.selfMs - a.selfMs || b.totalMs - a.totalMs || a.function.localeCompare(b.function));
|
|
33087
|
-
|
|
33088
|
-
|
|
33089
|
-
|
|
33090
|
-
|
|
33091
|
-
|
|
33092
|
-
|
|
33093
|
-
|
|
33136
|
+
}
|
|
33137
|
+
function bareName(fn) {
|
|
33138
|
+
return fn.slice(fn.lastIndexOf(".") + 1);
|
|
33139
|
+
}
|
|
33140
|
+
function placedLambda(row, sourceOf) {
|
|
33141
|
+
if (!row.function.endsWith("(lambda)")) {
|
|
33142
|
+
return row;
|
|
33143
|
+
}
|
|
33144
|
+
const source = sourceOf(row.script);
|
|
33145
|
+
const within = source === null ? null : enclosingFunction(source, row.line);
|
|
33146
|
+
return within === null ? row : { ...row, within };
|
|
33147
|
+
}
|
|
33148
|
+
var CALL_SITES = 40;
|
|
33149
|
+
function callersOf(totals, name, scripts) {
|
|
33150
|
+
const rows = rowsOf(totals);
|
|
33151
|
+
const bare = bareName(name);
|
|
33152
|
+
const target = rows.filter((row) => name.includes(".") ? row.function === name : bareName(row.function) === bare);
|
|
33153
|
+
const sites = [];
|
|
33154
|
+
for (const [script, source] of scripts) {
|
|
33155
|
+
for (const site of callSitesOf(source, bare)) {
|
|
33156
|
+
const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
|
|
33157
|
+
sites.push({
|
|
33158
|
+
script,
|
|
33159
|
+
...site,
|
|
33160
|
+
...within === undefined ? {} : { withinCalls: within.calls, withinSelfMs: within.selfMs, withinTotalMs: within.totalMs }
|
|
33161
|
+
});
|
|
33162
|
+
}
|
|
33163
|
+
}
|
|
33164
|
+
sites.sort((a, b) => (b.withinTotalMs ?? -1) - (a.withinTotalMs ?? -1) || a.script.localeCompare(b.script) || a.line - b.line);
|
|
33165
|
+
return { target, sites: sites.slice(0, CALL_SITES), omitted: Math.max(0, sites.length - CALL_SITES) };
|
|
33094
33166
|
}
|
|
33095
33167
|
|
|
33096
33168
|
// src/setup.ts
|
|
@@ -34075,7 +34147,12 @@ var TOOL_SPECS = [
|
|
|
34075
34147
|
profile: {
|
|
34076
34148
|
type: "boolean",
|
|
34077
34149
|
ops: ["start"],
|
|
34078
|
-
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."
|
|
34150
|
+
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."
|
|
34151
|
+
},
|
|
34152
|
+
callersOf: {
|
|
34153
|
+
type: "string",
|
|
34154
|
+
ops: ["profile"],
|
|
34155
|
+
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."
|
|
34079
34156
|
},
|
|
34080
34157
|
limit: {
|
|
34081
34158
|
type: "integer",
|
|
@@ -34111,7 +34188,7 @@ var TOOL_SPECS = [
|
|
|
34111
34188
|
},
|
|
34112
34189
|
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
|
|
34113
34190
|
profile: {
|
|
34114
|
-
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",
|
|
34191
|
+
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",
|
|
34115
34192
|
requires: []
|
|
34116
34193
|
}
|
|
34117
34194
|
},
|
|
@@ -34848,13 +34925,19 @@ function playOf(answer) {
|
|
|
34848
34925
|
const editorPid = readNumber(answer, "editorPid");
|
|
34849
34926
|
return play === undefined || editorPid === undefined ? null : { play, editorPid };
|
|
34850
34927
|
}
|
|
34851
|
-
function
|
|
34928
|
+
function playedSince(run, playing) {
|
|
34929
|
+
return run.throughEditor && run.play !== undefined && playing !== null && (playing.play !== run.play.play || playing.editorPid !== run.play.editorPid);
|
|
34930
|
+
}
|
|
34931
|
+
function runIsUp(run, editorSays, editorPlay = null, now = Date.now()) {
|
|
34852
34932
|
if (run?.throughEditor !== true) {
|
|
34853
34933
|
return stillRunning(run);
|
|
34854
34934
|
}
|
|
34855
34935
|
if (typeof run.endedHere === "string") {
|
|
34856
34936
|
return false;
|
|
34857
34937
|
}
|
|
34938
|
+
if (playedSince(run, editorPlay)) {
|
|
34939
|
+
return false;
|
|
34940
|
+
}
|
|
34858
34941
|
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
34859
34942
|
return false;
|
|
34860
34943
|
}
|
|
@@ -35464,7 +35547,45 @@ function aboveTheRunner(entry) {
|
|
|
35464
35547
|
};
|
|
35465
35548
|
}
|
|
35466
35549
|
var PROFILE_FUNCTIONS = 30;
|
|
35550
|
+
function runtimeAddonRegistered(projectPath) {
|
|
35551
|
+
const registered = inspectProject(projectPath);
|
|
35552
|
+
return registered.runtimeAutoloadPath !== null || registered.runtimeLoaderAutoload !== null;
|
|
35553
|
+
}
|
|
35554
|
+
function profileOnStart(profiled, through, runtimeAddon) {
|
|
35555
|
+
if (!profiled) {
|
|
35556
|
+
return;
|
|
35557
|
+
}
|
|
35558
|
+
const debuggerOf = through === "editor" ? "the editor's debugger session" : "the process that keeps the run";
|
|
35559
|
+
return {
|
|
35560
|
+
on: true,
|
|
35561
|
+
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.")
|
|
35562
|
+
};
|
|
35563
|
+
}
|
|
35467
35564
|
var PROFILER_ON_MS = 3000;
|
|
35565
|
+
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. Each place carries the profile's numbers for the function it is written in.";
|
|
35566
|
+
function projectSourceOf(projectPath) {
|
|
35567
|
+
const read = new Map;
|
|
35568
|
+
return (script) => {
|
|
35569
|
+
if (!read.has(script)) {
|
|
35570
|
+
const inside = resolveWithinProject(projectPath, script);
|
|
35571
|
+
let source = null;
|
|
35572
|
+
if (inside.ok) {
|
|
35573
|
+
try {
|
|
35574
|
+
source = readFileSync21(inside.absolutePath, "utf8");
|
|
35575
|
+
} catch {}
|
|
35576
|
+
}
|
|
35577
|
+
read.set(script, source);
|
|
35578
|
+
}
|
|
35579
|
+
return read.get(script) ?? null;
|
|
35580
|
+
};
|
|
35581
|
+
}
|
|
35582
|
+
function projectScripts(projectPath) {
|
|
35583
|
+
const scripts = [];
|
|
35584
|
+
eachScript(projectPath, (script, source) => {
|
|
35585
|
+
scripts.push([script, source]);
|
|
35586
|
+
});
|
|
35587
|
+
return scripts;
|
|
35588
|
+
}
|
|
35468
35589
|
function profileAnswer(reading, limit) {
|
|
35469
35590
|
const { totals } = reading;
|
|
35470
35591
|
if (totals === null) {
|
|
@@ -35476,10 +35597,12 @@ function profileAnswer(reading, limit) {
|
|
|
35476
35597
|
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."
|
|
35477
35598
|
};
|
|
35478
35599
|
}
|
|
35479
|
-
const table = profiledFunctions(totals, limit);
|
|
35600
|
+
const table = profiledFunctions(totals, limit, reading.sourceOf);
|
|
35480
35601
|
const notes = [];
|
|
35481
35602
|
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.";
|
|
35482
|
-
if (totals.complete) {} else if (reading.running) {
|
|
35603
|
+
if (totals.complete) {} else if (reading.running && totals.frames === 0) {
|
|
35604
|
+
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}`);
|
|
35605
|
+
} else if (reading.running) {
|
|
35483
35606
|
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" : ""}.`);
|
|
35484
35607
|
} else if (totals.frames === 0) {
|
|
35485
35608
|
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}`);
|
|
@@ -35500,9 +35623,20 @@ function profileAnswer(reading, limit) {
|
|
|
35500
35623
|
functionsCounted: table.counted,
|
|
35501
35624
|
omitted: table.omitted > 0 ? table.omitted : undefined,
|
|
35502
35625
|
functions: table.functions,
|
|
35626
|
+
callers: reading.callersOf === undefined ? undefined : callersAnswer(totals, reading.callersOf),
|
|
35503
35627
|
note: notes.length > 0 ? notes.join(" ") : undefined
|
|
35504
35628
|
};
|
|
35505
35629
|
}
|
|
35630
|
+
function callersAnswer(totals, asked) {
|
|
35631
|
+
const found = callersOf(totals, asked.name, asked.scripts());
|
|
35632
|
+
return {
|
|
35633
|
+
of: asked.name,
|
|
35634
|
+
rows: found.target,
|
|
35635
|
+
sites: found.sites,
|
|
35636
|
+
omitted: found.omitted > 0 ? found.omitted : undefined,
|
|
35637
|
+
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
|
|
35638
|
+
};
|
|
35639
|
+
}
|
|
35506
35640
|
var MOST_ENGINE_ENTRIES = 200;
|
|
35507
35641
|
function foldedEntriesAnswer(log) {
|
|
35508
35642
|
const problems = log.selectFolded({
|
|
@@ -36079,7 +36213,7 @@ class GodotServer {
|
|
|
36079
36213
|
return this.saying(answer, {
|
|
36080
36214
|
update_available: {
|
|
36081
36215
|
...notice,
|
|
36082
|
-
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
|
|
36216
|
+
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."
|
|
36083
36217
|
}
|
|
36084
36218
|
});
|
|
36085
36219
|
}
|
|
@@ -38315,6 +38449,7 @@ class GodotServer {
|
|
|
38315
38449
|
pid: started.pid,
|
|
38316
38450
|
arguments: cmdArgs,
|
|
38317
38451
|
transcript: started.transcript,
|
|
38452
|
+
profile: profileOnStart(profiled, "gdharness", runtimeAddonRegistered(project.value.path)),
|
|
38318
38453
|
refreshedClasses: refreshed.value,
|
|
38319
38454
|
...scanWaitAnswer(scanned),
|
|
38320
38455
|
savesNote: asked.value.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
@@ -38454,6 +38589,7 @@ class GodotServer {
|
|
|
38454
38589
|
const transcript = openTranscript(startedAt);
|
|
38455
38590
|
closeSync2(transcript.fd);
|
|
38456
38591
|
const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
38592
|
+
const which = playOf(playAnswer);
|
|
38457
38593
|
const played = {
|
|
38458
38594
|
pid: null,
|
|
38459
38595
|
log,
|
|
@@ -38466,11 +38602,11 @@ class GodotServer {
|
|
|
38466
38602
|
throughEditor: true,
|
|
38467
38603
|
brokeOn: null,
|
|
38468
38604
|
announcedBefore: alreadyPlaying,
|
|
38469
|
-
seenPlaying: readBoolean(playAnswer, "playing") === true
|
|
38605
|
+
seenPlaying: readBoolean(playAnswer, "playing") === true,
|
|
38606
|
+
...which === null ? {} : { play: which }
|
|
38470
38607
|
};
|
|
38471
38608
|
this.activeProcess = played;
|
|
38472
38609
|
this.watchThePlayedConsole();
|
|
38473
|
-
const which = playOf(playAnswer);
|
|
38474
38610
|
writeEditorRunNote({
|
|
38475
38611
|
projectPath,
|
|
38476
38612
|
transcript: transcript.path,
|
|
@@ -38482,6 +38618,7 @@ class GodotServer {
|
|
|
38482
38618
|
started: true,
|
|
38483
38619
|
through: "editor",
|
|
38484
38620
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
38621
|
+
profile: profileOnStart(profiled, "editor", runtimeAddonRegistered(projectPath)),
|
|
38485
38622
|
refreshedClasses,
|
|
38486
38623
|
...scanWaitAnswer(scanned),
|
|
38487
38624
|
debugPort: readNumber(playAnswer, "debugPort"),
|
|
@@ -38864,9 +39001,14 @@ class GodotServer {
|
|
|
38864
39001
|
return;
|
|
38865
39002
|
}
|
|
38866
39003
|
const going = this.activeProcess;
|
|
38867
|
-
const
|
|
39004
|
+
const asked = going?.throughEditor === true && going.play !== undefined ? await this.editorPlayingState() : null;
|
|
39005
|
+
const superseded = going !== null && asked !== null && playedSince(going, asked.play);
|
|
39006
|
+
if (superseded && stillRunning(going)) {
|
|
39007
|
+
going.endedUnwatched = true;
|
|
39008
|
+
}
|
|
39009
|
+
const givesWay = going !== null && (superseded || !stillRunning(going) && typeof going.endedHere === "string");
|
|
38868
39010
|
if (going !== null && !givesWay) {
|
|
38869
|
-
if (going.throughEditor && going.consoleLost === true && (await this.editorPlayingState())?.playing) {
|
|
39011
|
+
if (going.throughEditor && going.consoleLost === true && (asked ?? await this.editorPlayingState())?.playing) {
|
|
38870
39012
|
try {
|
|
38871
39013
|
await this.dap().connect();
|
|
38872
39014
|
going.log.append("stdout", `[gdharness] the debug adapter was reconnected here
|
|
@@ -38877,7 +39019,7 @@ class GodotServer {
|
|
|
38877
39019
|
}
|
|
38878
39020
|
return;
|
|
38879
39021
|
}
|
|
38880
|
-
const playing = await this.editorPlayingState();
|
|
39022
|
+
const playing = asked ?? await this.editorPlayingState();
|
|
38881
39023
|
if (playing?.playing !== true) {
|
|
38882
39024
|
return;
|
|
38883
39025
|
}
|
|
@@ -38900,7 +39042,8 @@ class GodotServer {
|
|
|
38900
39042
|
exitSignal: null,
|
|
38901
39043
|
throughEditor: true,
|
|
38902
39044
|
brokeOn: null,
|
|
38903
|
-
pickedUpPlaying: true
|
|
39045
|
+
pickedUpPlaying: true,
|
|
39046
|
+
...playing.play === null ? {} : { play: playing.play }
|
|
38904
39047
|
};
|
|
38905
39048
|
this.activeProcess = picked;
|
|
38906
39049
|
this.watchThePlayedConsole();
|
|
@@ -39204,11 +39347,19 @@ class GodotServer {
|
|
|
39204
39347
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
39205
39348
|
}
|
|
39206
39349
|
const limit = readPositiveNumber(args, "limit") ?? PROFILE_FUNCTIONS;
|
|
39350
|
+
const callersName = readString(args, "callersOf");
|
|
39351
|
+
if (callersName !== undefined && !callersName.split(".").every(isIdentifier)) {
|
|
39352
|
+
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"]);
|
|
39353
|
+
}
|
|
39207
39354
|
const going = await this.runStillGoing(run);
|
|
39208
|
-
const
|
|
39209
|
-
const
|
|
39355
|
+
const projectPath = run.projectPath;
|
|
39356
|
+
const fromProject = projectPath === null ? { runtimeAddon: false } : {
|
|
39357
|
+
runtimeAddon: runtimeAddonRegistered(projectPath),
|
|
39358
|
+
sourceOf: projectSourceOf(projectPath),
|
|
39359
|
+
...callersName === undefined ? {} : { callersOf: { name: callersName, scripts: () => projectScripts(projectPath) } }
|
|
39360
|
+
};
|
|
39210
39361
|
if (run.throughEditor) {
|
|
39211
|
-
return await this.profileOfThePlayedGame(going,
|
|
39362
|
+
return await this.profileOfThePlayedGame(going, fromProject, limit);
|
|
39212
39363
|
}
|
|
39213
39364
|
const record = run.projectPath === null ? null : readRunRecord(run.projectPath);
|
|
39214
39365
|
const path = record !== null && record.pid === run.pid ? record.profile : undefined;
|
|
@@ -39227,10 +39378,10 @@ class GodotServer {
|
|
|
39227
39378
|
running: going,
|
|
39228
39379
|
totals,
|
|
39229
39380
|
coveredMs: totals?.lastFrameAt == null ? 0 : totals.lastFrameAt - totals.startedAt,
|
|
39230
|
-
|
|
39381
|
+
...fromProject
|
|
39231
39382
|
}, limit));
|
|
39232
39383
|
}
|
|
39233
|
-
async profileOfThePlayedGame(going,
|
|
39384
|
+
async profileOfThePlayedGame(going, fromProject, limit) {
|
|
39234
39385
|
const read = await this.handleViaBridge("profile_read", {});
|
|
39235
39386
|
if (read.isError === true) {
|
|
39236
39387
|
return this.profilerRefusal(read);
|
|
@@ -39282,7 +39433,7 @@ class GodotServer {
|
|
|
39282
39433
|
running: going,
|
|
39283
39434
|
totals,
|
|
39284
39435
|
coveredMs: readNonNegativeNumber(held, "coveredMs") ?? 0,
|
|
39285
|
-
|
|
39436
|
+
...fromProject
|
|
39286
39437
|
}, limit));
|
|
39287
39438
|
}
|
|
39288
39439
|
async firstProfiledFrame() {
|
|
@@ -39341,11 +39492,13 @@ class GodotServer {
|
|
|
39341
39492
|
if (announced !== undefined && editorSays === undefined) {
|
|
39342
39493
|
return stillRunning(run);
|
|
39343
39494
|
}
|
|
39344
|
-
const
|
|
39345
|
-
|
|
39495
|
+
const state = editorSays === undefined ? await this.editorPlayingState() : undefined;
|
|
39496
|
+
const said = state === undefined ? editorSays ?? null : state?.playing ?? null;
|
|
39497
|
+
const play = state?.play ?? null;
|
|
39498
|
+
if (said === true && !playedSince(run, play)) {
|
|
39346
39499
|
run.seenPlaying = true;
|
|
39347
39500
|
}
|
|
39348
|
-
return runIsUp(run, said);
|
|
39501
|
+
return runIsUp(run, said, play);
|
|
39349
39502
|
}
|
|
39350
39503
|
async untilTheExitIsRecorded(run) {
|
|
39351
39504
|
const until = Date.now() + EXIT_REPORTED_WITHIN_MS;
|
package/build/keeper.js
CHANGED
|
@@ -542,6 +542,60 @@ function lastTryOnExit() {
|
|
|
542
542
|
import { readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
543
543
|
import { createServer as createServer2 } from "node:net";
|
|
544
544
|
|
|
545
|
+
// src/gdscript-source.ts
|
|
546
|
+
var DECLARING = new Set(["var", "const", "func", "signal", "enum", "class", "for", "class_name"]);
|
|
547
|
+
var NAME = "[\\p{L}_][\\p{L}\\p{N}_]*";
|
|
548
|
+
var ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*";
|
|
549
|
+
var DECLARATION_LINE = new RegExp(`^(${ANNOTATIONS})(static\\s+)?(func|var|const|signal|enum|class)\\s+(${NAME})`, "u");
|
|
550
|
+
var CLASS_NAME_LINE = new RegExp(`^${ANNOTATIONS}class_name\\s+(${NAME})`, "u");
|
|
551
|
+
var EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
|
|
552
|
+
var FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
|
|
553
|
+
var KEYWORDS = new Set([
|
|
554
|
+
"and",
|
|
555
|
+
"as",
|
|
556
|
+
"assert",
|
|
557
|
+
"await",
|
|
558
|
+
"break",
|
|
559
|
+
"breakpoint",
|
|
560
|
+
"class",
|
|
561
|
+
"class_name",
|
|
562
|
+
"const",
|
|
563
|
+
"continue",
|
|
564
|
+
"elif",
|
|
565
|
+
"else",
|
|
566
|
+
"enum",
|
|
567
|
+
"extends",
|
|
568
|
+
"false",
|
|
569
|
+
"for",
|
|
570
|
+
"func",
|
|
571
|
+
"if",
|
|
572
|
+
"in",
|
|
573
|
+
"is",
|
|
574
|
+
"match",
|
|
575
|
+
"namespace",
|
|
576
|
+
"not",
|
|
577
|
+
"null",
|
|
578
|
+
"or",
|
|
579
|
+
"pass",
|
|
580
|
+
"preload",
|
|
581
|
+
"return",
|
|
582
|
+
"self",
|
|
583
|
+
"signal",
|
|
584
|
+
"static",
|
|
585
|
+
"super",
|
|
586
|
+
"trait",
|
|
587
|
+
"true",
|
|
588
|
+
"var",
|
|
589
|
+
"void",
|
|
590
|
+
"when",
|
|
591
|
+
"while",
|
|
592
|
+
"yield",
|
|
593
|
+
"INF",
|
|
594
|
+
"NAN",
|
|
595
|
+
"PI",
|
|
596
|
+
"TAU"
|
|
597
|
+
]);
|
|
598
|
+
|
|
545
599
|
// src/godot-variant.ts
|
|
546
600
|
var NIL = 0;
|
|
547
601
|
var BOOL = 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.58",
|
|
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",
|