gdharness 1.1.56 → 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 +485 -56
- package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +4 -1
- package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +214 -0
- package/build/godot/addons/gdharness_editor/tools/profile_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +10 -3
- package/build/index.js +487 -61
- package/build/keeper.js +581 -6
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -7851,30 +7851,66 @@ function reported(entry) {
|
|
|
7851
7851
|
}
|
|
7852
7852
|
function framesCollapsed(detail) {
|
|
7853
7853
|
const out = [];
|
|
7854
|
-
let
|
|
7855
|
-
|
|
7856
|
-
const
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
pending.last = String(number);
|
|
7854
|
+
let at = 0;
|
|
7855
|
+
while (at < detail.length) {
|
|
7856
|
+
const frames = consecutiveFrames(detail, at);
|
|
7857
|
+
if (frames.length === 0) {
|
|
7858
|
+
out.push(detail[at] ?? "");
|
|
7859
|
+
at += 1;
|
|
7861
7860
|
continue;
|
|
7862
7861
|
}
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7862
|
+
let index = 0;
|
|
7863
|
+
while (index < frames.length) {
|
|
7864
|
+
const cycle = repeatedCycle(frames, index);
|
|
7865
|
+
const first = frames[index];
|
|
7866
|
+
if (cycle === null || first === undefined) {
|
|
7867
|
+
out.push(`[${first?.number ?? ""}] ${first?.frame ?? ""}`);
|
|
7868
|
+
index += 1;
|
|
7869
|
+
continue;
|
|
7870
|
+
}
|
|
7871
|
+
const last = frames[index + cycle.period * cycle.times - 1];
|
|
7872
|
+
const span = `[${first.number}-${last?.number ?? first.number}]`;
|
|
7873
|
+
if (cycle.period === 1) {
|
|
7874
|
+
out.push(`${span} ${first.frame}`);
|
|
7875
|
+
} else {
|
|
7876
|
+
out.push(`${span} these ${cycle.period} frames, ${cycle.times} times over:`);
|
|
7877
|
+
for (const frame of frames.slice(index, index + cycle.period)) {
|
|
7878
|
+
out.push(` ${frame.frame}`);
|
|
7879
|
+
}
|
|
7880
|
+
}
|
|
7881
|
+
index += cycle.period * cycle.times;
|
|
7869
7882
|
}
|
|
7870
|
-
|
|
7871
|
-
if (pending !== undefined) {
|
|
7872
|
-
out.push(writtenRun(pending));
|
|
7883
|
+
at += frames.length;
|
|
7873
7884
|
}
|
|
7874
7885
|
return out;
|
|
7875
7886
|
}
|
|
7876
|
-
function
|
|
7877
|
-
|
|
7887
|
+
function consecutiveFrames(detail, at) {
|
|
7888
|
+
const frames = [];
|
|
7889
|
+
for (const line of detail.slice(at)) {
|
|
7890
|
+
const match = FRAME.exec(line);
|
|
7891
|
+
const number = Number(match?.[1]);
|
|
7892
|
+
const frame = match?.[2];
|
|
7893
|
+
const before = frames.at(-1);
|
|
7894
|
+
if (frame === undefined || !Number.isInteger(number) || before !== undefined && number !== before.number + 1) {
|
|
7895
|
+
break;
|
|
7896
|
+
}
|
|
7897
|
+
frames.push({ number, frame });
|
|
7898
|
+
}
|
|
7899
|
+
return frames;
|
|
7900
|
+
}
|
|
7901
|
+
function repeatedCycle(frames, from) {
|
|
7902
|
+
let best = null;
|
|
7903
|
+
for (let period = 1;period <= LONGEST_CYCLE && from + 2 * period <= frames.length; period += 1) {
|
|
7904
|
+
let length = period;
|
|
7905
|
+
while (from + length < frames.length && frames[from + length]?.frame === frames[from + length - period]?.frame) {
|
|
7906
|
+
length += 1;
|
|
7907
|
+
}
|
|
7908
|
+
const times = Math.floor(length / period);
|
|
7909
|
+
if (times >= 2 && (best === null || period * times > best.period * best.times)) {
|
|
7910
|
+
best = { period, times };
|
|
7911
|
+
}
|
|
7912
|
+
}
|
|
7913
|
+
return best;
|
|
7878
7914
|
}
|
|
7879
7915
|
function foldedRepeats(entries) {
|
|
7880
7916
|
const first = new Map;
|
|
@@ -7998,7 +8034,7 @@ function answersTo(entry, severity, contains) {
|
|
|
7998
8034
|
const needle = contains.toLowerCase();
|
|
7999
8035
|
return [entry.text, ...entry.detail].some((line) => line.toLowerCase().includes(needle));
|
|
8000
8036
|
}
|
|
8001
|
-
var HEADLINE, COLOUR_CODE, STREAMS, ADMITTED, FRAME;
|
|
8037
|
+
var HEADLINE, COLOUR_CODE, STREAMS, ADMITTED, FRAME, LONGEST_CYCLE = 64;
|
|
8002
8038
|
var init_game_log = __esm(() => {
|
|
8003
8039
|
HEADLINE = /^(USER )?(SCRIPT ERROR|SHADER ERROR|ERROR|WARNING):\s?(.*)$/;
|
|
8004
8040
|
COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
@@ -9119,6 +9155,44 @@ ${line.text}`;
|
|
|
9119
9155
|
const { body } = parseBody(null, null, 0, 0, 0);
|
|
9120
9156
|
return { className, body };
|
|
9121
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
|
+
}
|
|
9122
9196
|
function collectEnumValues(text, from, into) {
|
|
9123
9197
|
const close = text.indexOf("}");
|
|
9124
9198
|
const inside = text.slice(1, close === -1 ? text.length : close);
|
|
@@ -9134,7 +9208,7 @@ function collectEnumValues(text, from, into) {
|
|
|
9134
9208
|
function isIdentifier(name) {
|
|
9135
9209
|
return new RegExp(`^${NAME}$`, "u").test(name) && !KEYWORDS.has(name);
|
|
9136
9210
|
}
|
|
9137
|
-
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;
|
|
9138
9212
|
var init_gdscript_source = __esm(() => {
|
|
9139
9213
|
IDENTIFIER_START = /[\p{L}_]/u;
|
|
9140
9214
|
IDENTIFIER_PART = /[\p{L}\p{N}_]/u;
|
|
@@ -9143,6 +9217,7 @@ var init_gdscript_source = __esm(() => {
|
|
|
9143
9217
|
CLASS_NAME_LINE = new RegExp(`^${ANNOTATIONS}class_name\\s+(${NAME})`, "u");
|
|
9144
9218
|
EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
|
|
9145
9219
|
EXTENDS_CLAUSE = /\bextends(?=\s)/u;
|
|
9220
|
+
FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
|
|
9146
9221
|
KEYWORDS = new Set([
|
|
9147
9222
|
"and",
|
|
9148
9223
|
"as",
|
|
@@ -17707,7 +17782,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17707
17782
|
},
|
|
17708
17783
|
{
|
|
17709
17784
|
name: "project_test",
|
|
17710
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
|
|
17785
|
+
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
|
|
17711
17786
|
parameters: {
|
|
17712
17787
|
projectPath: PROJECT_PATH,
|
|
17713
17788
|
path: {
|
|
@@ -18099,6 +18174,22 @@ var init_tool_definitions = __esm(() => {
|
|
|
18099
18174
|
ops: ["check", "wait"],
|
|
18100
18175
|
description: "check: how long to give the boot before it is called hung. Default 60000. wait: how long to wait for the run to end before answering anyway. Default 600000."
|
|
18101
18176
|
},
|
|
18177
|
+
profile: {
|
|
18178
|
+
type: "boolean",
|
|
18179
|
+
ops: ["start"],
|
|
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."
|
|
18186
|
+
},
|
|
18187
|
+
limit: {
|
|
18188
|
+
type: "integer",
|
|
18189
|
+
minimum: 1,
|
|
18190
|
+
ops: ["profile"],
|
|
18191
|
+
description: "profile: how many functions to answer with, those with the most self time. Default 30."
|
|
18192
|
+
},
|
|
18102
18193
|
andChildren: {
|
|
18103
18194
|
type: "boolean",
|
|
18104
18195
|
ops: ["stop"],
|
|
@@ -18125,7 +18216,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
18125
18216
|
summary: "boot headless, quit after a few frames, and report the verdict",
|
|
18126
18217
|
requires: ["projectPath"]
|
|
18127
18218
|
},
|
|
18128
|
-
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] }
|
|
18219
|
+
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
|
|
18220
|
+
profile: {
|
|
18221
|
+
summary: "where the run spends its time by GDScript function, while it runs or after it ended: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, sorted by self time, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. A run started with profile: true is profiled from its start. A game the editor plays that was not is profiled from the moment this is asked, and the answer says so; a run this server started without it is refused, since its debugger is fixed as it starts",
|
|
18222
|
+
requires: []
|
|
18223
|
+
}
|
|
18129
18224
|
},
|
|
18130
18225
|
defaultOperation: "start"
|
|
18131
18226
|
},
|
|
@@ -18608,7 +18703,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18608
18703
|
says: {
|
|
18609
18704
|
type: "string",
|
|
18610
18705
|
ops: ["until"],
|
|
18611
|
-
description: 'until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Matched the way runtime_inspect find matches says: case-insensitive, a plain word anywhere in the text, a pattern with * or ? a glob over the whole of it, and alternatives separated by | met by whichever appears first ("*won*|*lost*"); on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property. It looks every frame while a look is cheap and less often on a screen large enough for a look to cost a real share of a frame, so the game keeps its speed; the answer counts the frames the wait spanned and the looks it took.'
|
|
18706
|
+
description: 'until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Matched the way runtime_inspect find matches says: case-insensitive, a plain word anywhere in the text, a pattern with * or ? a glob over the whole of it, and alternatives separated by | met by whichever appears first ("*won*|*lost*"); on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property. It looks every frame while a look is cheap and less often on a screen large enough for a look to cost a real share of a frame, so the game keeps its speed; the answer counts the frames the wait spanned and the looks it took, and says under looking_ms how long the looks took between them.'
|
|
18612
18707
|
},
|
|
18613
18708
|
includeHidden: {
|
|
18614
18709
|
type: "boolean",
|
|
@@ -41767,6 +41862,9 @@ function openTranscript(startedAt) {
|
|
|
41767
41862
|
const path = join24(runsDirectory(), `run-${startedAt}.log`);
|
|
41768
41863
|
return { path, fd: openSync2(path, "a") };
|
|
41769
41864
|
}
|
|
41865
|
+
function profilePath(startedAt) {
|
|
41866
|
+
return join24(runsDirectory(), `run-${startedAt}.profile.json`);
|
|
41867
|
+
}
|
|
41770
41868
|
function noteIsAbout(note, playing) {
|
|
41771
41869
|
return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
|
|
41772
41870
|
}
|
|
@@ -41866,6 +41964,7 @@ function recordAt(path) {
|
|
|
41866
41964
|
projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
|
|
41867
41965
|
...typeof fields["servedBy"] === "string" && fields["servedBy"] !== "" ? { servedBy: fields["servedBy"] } : {},
|
|
41868
41966
|
...typeof fields["keeper"] === "string" && fields["keeper"] !== "" ? { keeper: fields["keeper"] } : {},
|
|
41967
|
+
...typeof fields["profile"] === "string" && fields["profile"] !== "" ? { profile: fields["profile"] } : {},
|
|
41869
41968
|
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
|
|
41870
41969
|
...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
|
|
41871
41970
|
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
@@ -42058,7 +42157,10 @@ function ageMs(path, now) {
|
|
|
42058
42157
|
}
|
|
42059
42158
|
function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
42060
42159
|
const directory = runsDirectory();
|
|
42061
|
-
const kept = new Set(everyRunRecord().
|
|
42160
|
+
const kept = new Set(everyRunRecord().flatMap((record) => [
|
|
42161
|
+
record.transcript,
|
|
42162
|
+
...record.profile === undefined ? [] : [record.profile]
|
|
42163
|
+
]));
|
|
42062
42164
|
let entries;
|
|
42063
42165
|
try {
|
|
42064
42166
|
entries = readdirSync9(directory);
|
|
@@ -42066,7 +42168,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
42066
42168
|
return;
|
|
42067
42169
|
}
|
|
42068
42170
|
for (const entry of entries) {
|
|
42069
|
-
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
42171
|
+
if (!entry.startsWith("run-") || !(entry.endsWith(".log") || entry.endsWith(".profile.json"))) {
|
|
42070
42172
|
continue;
|
|
42071
42173
|
}
|
|
42072
42174
|
const path = join24(directory, entry);
|
|
@@ -42186,6 +42288,90 @@ var init_script_origins = __esm(() => {
|
|
|
42186
42288
|
RELOADED_AT = /GDScript::reload \((res:\/\/.+):(\d+)\)$/;
|
|
42187
42289
|
});
|
|
42188
42290
|
|
|
42291
|
+
// src/godot-variant.ts
|
|
42292
|
+
var FLAG_64, MOST_MESSAGE_BYTES;
|
|
42293
|
+
var init_godot_variant = __esm(() => {
|
|
42294
|
+
FLAG_64 = 1 << 16;
|
|
42295
|
+
MOST_MESSAGE_BYTES = (8 << 20) + 4;
|
|
42296
|
+
});
|
|
42297
|
+
|
|
42298
|
+
// src/script-profile.ts
|
|
42299
|
+
import { readFileSync as readFileSync22, renameSync as renameSync2, writeFileSync as writeFileSync17 } from "node:fs";
|
|
42300
|
+
function readProfileFile(path) {
|
|
42301
|
+
try {
|
|
42302
|
+
const parsed = JSON.parse(readFileSync22(path, "utf8"));
|
|
42303
|
+
return typeof parsed === "object" && typeof parsed.connected === "boolean" ? parsed : null;
|
|
42304
|
+
} catch {
|
|
42305
|
+
return null;
|
|
42306
|
+
}
|
|
42307
|
+
}
|
|
42308
|
+
function signatureParts(signature) {
|
|
42309
|
+
const parts = signature.split("::");
|
|
42310
|
+
if (parts.length < 3) {
|
|
42311
|
+
return { script: "", line: 0, function: signature };
|
|
42312
|
+
}
|
|
42313
|
+
const name = parts.at(-1) ?? "";
|
|
42314
|
+
const line = Number(parts.at(-2));
|
|
42315
|
+
return { script: parts.slice(0, -2).join("::"), line: Number.isFinite(line) ? line : 0, function: name };
|
|
42316
|
+
}
|
|
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]) => {
|
|
42329
|
+
const parts = signatureParts(signature);
|
|
42330
|
+
return {
|
|
42331
|
+
script: parts.script,
|
|
42332
|
+
function: parts.function,
|
|
42333
|
+
line: parts.line,
|
|
42334
|
+
calls: sum.calls,
|
|
42335
|
+
selfMs: ms(sum.selfSeconds),
|
|
42336
|
+
totalMs: ms(sum.totalSeconds)
|
|
42337
|
+
};
|
|
42338
|
+
}).sort((a, b) => b.selfMs - a.selfMs || b.totalMs - a.totalMs || a.function.localeCompare(b.function));
|
|
42339
|
+
}
|
|
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;
|
|
42370
|
+
var init_script_profile = __esm(() => {
|
|
42371
|
+
init_gdscript_source();
|
|
42372
|
+
init_godot_variant();
|
|
42373
|
+
});
|
|
42374
|
+
|
|
42189
42375
|
// src/server.ts
|
|
42190
42376
|
import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
|
|
42191
42377
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -42199,7 +42385,7 @@ import {
|
|
|
42199
42385
|
mkdirSync as mkdirSync15,
|
|
42200
42386
|
openSync as openSync3,
|
|
42201
42387
|
readdirSync as readdirSync10,
|
|
42202
|
-
readFileSync as
|
|
42388
|
+
readFileSync as readFileSync23,
|
|
42203
42389
|
readSync as readSync2,
|
|
42204
42390
|
rmdirSync as rmdirSync2,
|
|
42205
42391
|
statSync as statSync9
|
|
@@ -42316,13 +42502,19 @@ function playOf(answer) {
|
|
|
42316
42502
|
const editorPid = readNumber(answer, "editorPid");
|
|
42317
42503
|
return play === undefined || editorPid === undefined ? null : { play, editorPid };
|
|
42318
42504
|
}
|
|
42319
|
-
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()) {
|
|
42320
42509
|
if (run?.throughEditor !== true) {
|
|
42321
42510
|
return stillRunning(run);
|
|
42322
42511
|
}
|
|
42323
42512
|
if (typeof run.endedHere === "string") {
|
|
42324
42513
|
return false;
|
|
42325
42514
|
}
|
|
42515
|
+
if (playedSince(run, editorPlay)) {
|
|
42516
|
+
return false;
|
|
42517
|
+
}
|
|
42326
42518
|
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
42327
42519
|
return false;
|
|
42328
42520
|
}
|
|
@@ -42866,6 +43058,94 @@ function aboveTheRunner(entry) {
|
|
|
42866
43058
|
]
|
|
42867
43059
|
};
|
|
42868
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
|
+
}
|
|
43098
|
+
function profileAnswer(reading, limit) {
|
|
43099
|
+
const { totals } = reading;
|
|
43100
|
+
if (totals === null) {
|
|
43101
|
+
return {
|
|
43102
|
+
through: reading.through,
|
|
43103
|
+
running: reading.running,
|
|
43104
|
+
connected: false,
|
|
43105
|
+
functions: [],
|
|
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."
|
|
43107
|
+
};
|
|
43108
|
+
}
|
|
43109
|
+
const table = profiledFunctions(totals, limit, reading.sourceOf);
|
|
43110
|
+
const notes = [];
|
|
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.";
|
|
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) {
|
|
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" : ""}.`);
|
|
43116
|
+
} else if (totals.frames === 0) {
|
|
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}`);
|
|
43118
|
+
} else {
|
|
43119
|
+
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}`);
|
|
43120
|
+
}
|
|
43121
|
+
if (totals.unreadable > 0) {
|
|
43122
|
+
notes.push(`${totals.unreadable} message${totals.unreadable === 1 ? "" : "s"} from the game could not be read, so the totals have that many gaps.`);
|
|
43123
|
+
}
|
|
43124
|
+
return {
|
|
43125
|
+
through: reading.through,
|
|
43126
|
+
running: reading.running,
|
|
43127
|
+
connected: true,
|
|
43128
|
+
complete: totals.complete,
|
|
43129
|
+
frames: totals.frames,
|
|
43130
|
+
coveredMs: reading.coveredMs,
|
|
43131
|
+
scriptMs: table.scriptMs,
|
|
43132
|
+
functionsCounted: table.counted,
|
|
43133
|
+
omitted: table.omitted > 0 ? table.omitted : undefined,
|
|
43134
|
+
functions: table.functions,
|
|
43135
|
+
callers: reading.callersOf === undefined ? undefined : callersAnswer(totals, reading.callersOf),
|
|
43136
|
+
note: notes.length > 0 ? notes.join(" ") : undefined
|
|
43137
|
+
};
|
|
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
|
+
}
|
|
42869
43149
|
function foldedEntriesAnswer(log) {
|
|
42870
43150
|
const problems = log.selectFolded({
|
|
42871
43151
|
severity: "warning",
|
|
@@ -42879,7 +43159,7 @@ function foldedEntriesAnswer(log) {
|
|
|
42879
43159
|
}
|
|
42880
43160
|
function gdUnitFailsOnScriptErrors(projectPath) {
|
|
42881
43161
|
try {
|
|
42882
|
-
return settingKeys(
|
|
43162
|
+
return settingKeys(readFileSync23(join26(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
|
|
42883
43163
|
} catch {
|
|
42884
43164
|
return true;
|
|
42885
43165
|
}
|
|
@@ -43008,7 +43288,7 @@ function scriptsUnder(directory, prefix) {
|
|
|
43008
43288
|
return found;
|
|
43009
43289
|
}
|
|
43010
43290
|
function hasMainScene(projectFile) {
|
|
43011
|
-
const scene = parseProjectGodot(
|
|
43291
|
+
const scene = parseProjectGodot(readFileSync23(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
43012
43292
|
return typeof scene === "string" && scene !== "";
|
|
43013
43293
|
}
|
|
43014
43294
|
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
@@ -43016,13 +43296,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
43016
43296
|
return false;
|
|
43017
43297
|
}
|
|
43018
43298
|
try {
|
|
43019
|
-
return
|
|
43299
|
+
return readFileSync23(join26(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
43020
43300
|
} catch {
|
|
43021
43301
|
return false;
|
|
43022
43302
|
}
|
|
43023
43303
|
}
|
|
43024
43304
|
function editorPlaysHeadless(projectFile) {
|
|
43025
|
-
const runArgs = parseProjectGodot(
|
|
43305
|
+
const runArgs = parseProjectGodot(readFileSync23(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
43026
43306
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
43027
43307
|
}
|
|
43028
43308
|
function camelCased(params) {
|
|
@@ -43439,7 +43719,7 @@ class GodotServer {
|
|
|
43439
43719
|
return this.saying(answer, {
|
|
43440
43720
|
update_available: {
|
|
43441
43721
|
...notice,
|
|
43442
|
-
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."
|
|
43443
43723
|
}
|
|
43444
43724
|
});
|
|
43445
43725
|
}
|
|
@@ -43695,6 +43975,9 @@ class GodotServer {
|
|
|
43695
43975
|
if (op === "wait") {
|
|
43696
43976
|
return await this.handleWaitForRun(args);
|
|
43697
43977
|
}
|
|
43978
|
+
if (op === "profile") {
|
|
43979
|
+
return await this.handleRunProfile(args);
|
|
43980
|
+
}
|
|
43698
43981
|
return await this.handleRunProject(args, op);
|
|
43699
43982
|
case "editor_output":
|
|
43700
43983
|
return await this.handleGetDebugOutput(args);
|
|
@@ -44448,7 +44731,7 @@ class GodotServer {
|
|
|
44448
44731
|
if (!engine.ok) {
|
|
44449
44732
|
return engine.response;
|
|
44450
44733
|
}
|
|
44451
|
-
const application = parseProjectGodot(
|
|
44734
|
+
const application = parseProjectGodot(readFileSync23(project.value.file, "utf8"))["application"] ?? {};
|
|
44452
44735
|
const name = application["config/name"];
|
|
44453
44736
|
const mainScene = application["run/main_scene"];
|
|
44454
44737
|
const info = {
|
|
@@ -44666,7 +44949,7 @@ class GodotServer {
|
|
|
44666
44949
|
const written = existsSync20(reportsDir) ? readdirSync10(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
44667
44950
|
const newest = written.at(-1);
|
|
44668
44951
|
if (newest !== undefined) {
|
|
44669
|
-
report = parseJUnit(
|
|
44952
|
+
report = parseJUnit(readFileSync23(join26(reportsDir, newest, "results.xml"), "utf8"));
|
|
44670
44953
|
}
|
|
44671
44954
|
} catch (error) {
|
|
44672
44955
|
reportProblem = errorMessage(error);
|
|
@@ -44785,6 +45068,8 @@ class GodotServer {
|
|
|
44785
45068
|
];
|
|
44786
45069
|
});
|
|
44787
45070
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
45071
|
+
const uncaughtFolded = foldedRepeats(uncaught.map(aboveTheRunner));
|
|
45072
|
+
const listedAsUncaught = new Set(uncaughtFolded.map((entry) => entry.index));
|
|
44788
45073
|
return this.jsonTextResponse({
|
|
44789
45074
|
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && missing.length === 0,
|
|
44790
45075
|
verdict,
|
|
@@ -44799,7 +45084,7 @@ class GodotServer {
|
|
|
44799
45084
|
time: report.time,
|
|
44800
45085
|
failed,
|
|
44801
45086
|
hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
|
|
44802
|
-
uncaughtScriptErrors:
|
|
45087
|
+
uncaughtScriptErrors: uncaughtFolded.length > 0 ? uncaughtFolded : undefined,
|
|
44803
45088
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
44804
45089
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
44805
45090
|
notRun: notRun > 0 ? notRun : undefined,
|
|
@@ -44826,7 +45111,7 @@ class GodotServer {
|
|
|
44826
45111
|
suitesPassed: report.suites.length - unclean.length,
|
|
44827
45112
|
engineErrors: run.log.count("error"),
|
|
44828
45113
|
engineWarnings: run.log.count("warning"),
|
|
44829
|
-
engineEntries: engineEntriesShown,
|
|
45114
|
+
engineEntries: engineEntriesShown.map((entry) => listedAsUncaught.has(entry.index) ? { ...entry, detail: undefined, detailUnder: "uncaughtScriptErrors" } : entry),
|
|
44830
45115
|
engineEntriesOmitted: engineEntries.length > engineEntriesShown.length ? engineEntries.length - engineEntriesShown.length : undefined,
|
|
44831
45116
|
classes: withAddonClassesCounted(classes.payload, cachedClasses(project.value.path) ?? new Map)
|
|
44832
45117
|
});
|
|
@@ -44950,7 +45235,7 @@ class GodotServer {
|
|
|
44950
45235
|
return null;
|
|
44951
45236
|
}
|
|
44952
45237
|
try {
|
|
44953
|
-
return
|
|
45238
|
+
return readFileSync23(contained.absolutePath, "utf8");
|
|
44954
45239
|
} catch {
|
|
44955
45240
|
return null;
|
|
44956
45241
|
}
|
|
@@ -45019,7 +45304,7 @@ class GodotServer {
|
|
|
45019
45304
|
return null;
|
|
45020
45305
|
}
|
|
45021
45306
|
try {
|
|
45022
|
-
return
|
|
45307
|
+
return readFileSync23(contained.absolutePath, "utf8");
|
|
45023
45308
|
} catch {
|
|
45024
45309
|
return null;
|
|
45025
45310
|
}
|
|
@@ -45338,7 +45623,7 @@ class GodotServer {
|
|
|
45338
45623
|
return null;
|
|
45339
45624
|
}
|
|
45340
45625
|
try {
|
|
45341
|
-
return settingKeys(
|
|
45626
|
+
return settingKeys(readFileSync23(join26(projectPath, "project.godot"), "utf8"));
|
|
45342
45627
|
} catch {
|
|
45343
45628
|
return null;
|
|
45344
45629
|
}
|
|
@@ -45639,8 +45924,9 @@ class GodotServer {
|
|
|
45639
45924
|
const runtimeWaitMs = this.announceBudget(args, project.value.path);
|
|
45640
45925
|
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
45641
45926
|
const silent = resolveSilent(args["silent"], process.env) && !headless;
|
|
45927
|
+
const profiled = readBoolean(args, "profile") === true;
|
|
45642
45928
|
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
|
|
45643
|
-
return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
|
|
45929
|
+
return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended, profiled);
|
|
45644
45930
|
}
|
|
45645
45931
|
const cmdArgs = runArguments({
|
|
45646
45932
|
projectPath: project.value.path,
|
|
@@ -45650,7 +45936,7 @@ class GodotServer {
|
|
|
45650
45936
|
silent
|
|
45651
45937
|
});
|
|
45652
45938
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
45653
|
-
const started = await this.launchKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined, headless || args["visible"] === true ? undefined : HIDDEN_DESKTOP);
|
|
45939
|
+
const started = await this.launchKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined, headless || args["visible"] === true ? undefined : HIDDEN_DESKTOP, profiled);
|
|
45654
45940
|
if ("error" in started) {
|
|
45655
45941
|
return this.createErrorResponse(`The game could not be started: ${started.error}`, [
|
|
45656
45942
|
"editor_status says which Godot this server runs and whether it answers"
|
|
@@ -45669,6 +45955,7 @@ class GodotServer {
|
|
|
45669
45955
|
pid: started.pid,
|
|
45670
45956
|
arguments: cmdArgs,
|
|
45671
45957
|
transcript: started.transcript,
|
|
45958
|
+
profile: profileOnStart(profiled, "gdharness", runtimeAddonRegistered(project.value.path)),
|
|
45672
45959
|
refreshedClasses: refreshed.value,
|
|
45673
45960
|
...scanWaitAnswer(scanned),
|
|
45674
45961
|
savesNote: asked.value.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
@@ -45770,7 +46057,7 @@ class GodotServer {
|
|
|
45770
46057
|
writeBootNote(run.projectPath, took);
|
|
45771
46058
|
}
|
|
45772
46059
|
}
|
|
45773
|
-
async playThroughEditor(scene, refreshedClasses, scanned, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
|
|
46060
|
+
async playThroughEditor(scene, refreshedClasses, scanned, projectPath, alreadyPlaying, runtimeWaitMs, ended, profiled) {
|
|
45774
46061
|
const log = new GameLog;
|
|
45775
46062
|
const connectingFrom = Date.now();
|
|
45776
46063
|
try {
|
|
@@ -45794,6 +46081,12 @@ class GodotServer {
|
|
|
45794
46081
|
const sendingFrom = Date.now();
|
|
45795
46082
|
const breakpoints = await this.dapHoldingBreakpointsOf(projectPath).reapplyBreakpoints();
|
|
45796
46083
|
this.logDebug(`Breakpoints were sent again after ${Date.now() - sendingFrom}ms`);
|
|
46084
|
+
if (profiled) {
|
|
46085
|
+
const armed = await this.handleViaBridge("profile_start", { arm: true });
|
|
46086
|
+
if (armed.isError === true) {
|
|
46087
|
+
return this.profilerRefusal(armed);
|
|
46088
|
+
}
|
|
46089
|
+
}
|
|
45797
46090
|
const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
|
|
45798
46091
|
if (answer.isError === true) {
|
|
45799
46092
|
return answer;
|
|
@@ -45802,6 +46095,7 @@ class GodotServer {
|
|
|
45802
46095
|
const transcript = openTranscript(startedAt);
|
|
45803
46096
|
closeSync2(transcript.fd);
|
|
45804
46097
|
const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
46098
|
+
const which = playOf(playAnswer);
|
|
45805
46099
|
const played = {
|
|
45806
46100
|
pid: null,
|
|
45807
46101
|
log,
|
|
@@ -45814,11 +46108,11 @@ class GodotServer {
|
|
|
45814
46108
|
throughEditor: true,
|
|
45815
46109
|
brokeOn: null,
|
|
45816
46110
|
announcedBefore: alreadyPlaying,
|
|
45817
|
-
seenPlaying: readBoolean(playAnswer, "playing") === true
|
|
46111
|
+
seenPlaying: readBoolean(playAnswer, "playing") === true,
|
|
46112
|
+
...which === null ? {} : { play: which }
|
|
45818
46113
|
};
|
|
45819
46114
|
this.activeProcess = played;
|
|
45820
46115
|
this.watchThePlayedConsole();
|
|
45821
|
-
const which = playOf(playAnswer);
|
|
45822
46116
|
writeEditorRunNote({
|
|
45823
46117
|
projectPath,
|
|
45824
46118
|
transcript: transcript.path,
|
|
@@ -45830,6 +46124,7 @@ class GodotServer {
|
|
|
45830
46124
|
started: true,
|
|
45831
46125
|
through: "editor",
|
|
45832
46126
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
46127
|
+
profile: profileOnStart(profiled, "editor", runtimeAddonRegistered(projectPath)),
|
|
45833
46128
|
refreshedClasses,
|
|
45834
46129
|
...scanWaitAnswer(scanned),
|
|
45835
46130
|
debugPort: readNumber(playAnswer, "debugPort"),
|
|
@@ -45940,7 +46235,7 @@ class GodotServer {
|
|
|
45940
46235
|
return 0;
|
|
45941
46236
|
}
|
|
45942
46237
|
try {
|
|
45943
|
-
return reportAlreadyInTranscript(
|
|
46238
|
+
return reportAlreadyInTranscript(readFileSync23(game.transcript), readFileSync23(report));
|
|
45944
46239
|
} catch (error) {
|
|
45945
46240
|
this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
|
|
45946
46241
|
return 0;
|
|
@@ -46144,7 +46439,7 @@ class GodotServer {
|
|
|
46144
46439
|
});
|
|
46145
46440
|
return Object.assign(started, { settled });
|
|
46146
46441
|
}
|
|
46147
|
-
async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop) {
|
|
46442
|
+
async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop, profiled = false) {
|
|
46148
46443
|
const startedAt = Date.now();
|
|
46149
46444
|
const transcript = openTranscript(startedAt);
|
|
46150
46445
|
closeSync2(transcript.fd);
|
|
@@ -46152,7 +46447,13 @@ class GodotServer {
|
|
|
46152
46447
|
command: godotPath,
|
|
46153
46448
|
args: cmdArgs,
|
|
46154
46449
|
...env === undefined ? {} : { env },
|
|
46155
|
-
run: {
|
|
46450
|
+
run: {
|
|
46451
|
+
transcript: transcript.path,
|
|
46452
|
+
startedAt,
|
|
46453
|
+
projectPath,
|
|
46454
|
+
...this.servedBy(),
|
|
46455
|
+
...profiled ? { profile: profilePath(startedAt) } : {}
|
|
46456
|
+
},
|
|
46156
46457
|
...desktop === undefined ? {} : { desktop }
|
|
46157
46458
|
});
|
|
46158
46459
|
if ("error" in launched) {
|
|
@@ -46206,9 +46507,14 @@ class GodotServer {
|
|
|
46206
46507
|
return;
|
|
46207
46508
|
}
|
|
46208
46509
|
const going = this.activeProcess;
|
|
46209
|
-
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");
|
|
46210
46516
|
if (going !== null && !givesWay) {
|
|
46211
|
-
if (going.throughEditor && going.consoleLost === true && (await this.editorPlayingState())?.playing) {
|
|
46517
|
+
if (going.throughEditor && going.consoleLost === true && (asked ?? await this.editorPlayingState())?.playing) {
|
|
46212
46518
|
try {
|
|
46213
46519
|
await this.dap().connect();
|
|
46214
46520
|
going.log.append("stdout", `[gdharness] the debug adapter was reconnected here
|
|
@@ -46219,7 +46525,7 @@ class GodotServer {
|
|
|
46219
46525
|
}
|
|
46220
46526
|
return;
|
|
46221
46527
|
}
|
|
46222
|
-
const playing = await this.editorPlayingState();
|
|
46528
|
+
const playing = asked ?? await this.editorPlayingState();
|
|
46223
46529
|
if (playing?.playing !== true) {
|
|
46224
46530
|
return;
|
|
46225
46531
|
}
|
|
@@ -46242,7 +46548,8 @@ class GodotServer {
|
|
|
46242
46548
|
exitSignal: null,
|
|
46243
46549
|
throughEditor: true,
|
|
46244
46550
|
brokeOn: null,
|
|
46245
|
-
pickedUpPlaying: true
|
|
46551
|
+
pickedUpPlaying: true,
|
|
46552
|
+
...playing.play === null ? {} : { play: playing.play }
|
|
46246
46553
|
};
|
|
46247
46554
|
this.activeProcess = picked;
|
|
46248
46555
|
this.watchThePlayedConsole();
|
|
@@ -46539,6 +46846,123 @@ class GodotServer {
|
|
|
46539
46846
|
...foldedEntriesAnswer(boot.log)
|
|
46540
46847
|
});
|
|
46541
46848
|
}
|
|
46849
|
+
async handleRunProfile(args) {
|
|
46850
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
46851
|
+
const run = await this.currentRun();
|
|
46852
|
+
if (!run) {
|
|
46853
|
+
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
46854
|
+
}
|
|
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
|
+
}
|
|
46860
|
+
const going = await this.runStillGoing(run);
|
|
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
|
+
};
|
|
46867
|
+
if (run.throughEditor) {
|
|
46868
|
+
return await this.profileOfThePlayedGame(going, fromProject, limit);
|
|
46869
|
+
}
|
|
46870
|
+
const record = run.projectPath === null ? null : readRunRecord(run.projectPath);
|
|
46871
|
+
const path = record !== null && record.pid === run.pid ? record.profile : undefined;
|
|
46872
|
+
if (path === undefined) {
|
|
46873
|
+
return this.createErrorResponse("This run was started without profile: true, and a game this server starts can be profiled only from its start: its debugger is whatever it dials as it starts, once.", ["editor_run start with profile: true starts the run again with the profiler on"]);
|
|
46874
|
+
}
|
|
46875
|
+
const file = readProfileFile(path);
|
|
46876
|
+
if (file === null) {
|
|
46877
|
+
return this.createErrorResponse(`The run's profile at ${path} could not be read.`, [
|
|
46878
|
+
"editor_run start with profile: true starts the run again with the profiler on"
|
|
46879
|
+
]);
|
|
46880
|
+
}
|
|
46881
|
+
const totals = file.connected ? file : null;
|
|
46882
|
+
return this.jsonTextResponse(profileAnswer({
|
|
46883
|
+
through: "gdharness",
|
|
46884
|
+
running: going,
|
|
46885
|
+
totals,
|
|
46886
|
+
coveredMs: totals?.lastFrameAt == null ? 0 : totals.lastFrameAt - totals.startedAt,
|
|
46887
|
+
...fromProject
|
|
46888
|
+
}, limit));
|
|
46889
|
+
}
|
|
46890
|
+
async profileOfThePlayedGame(going, fromProject, limit) {
|
|
46891
|
+
const read = await this.handleViaBridge("profile_read", {});
|
|
46892
|
+
if (read.isError === true) {
|
|
46893
|
+
return this.profilerRefusal(read);
|
|
46894
|
+
}
|
|
46895
|
+
const held = asParams(JSON.parse(read.content[0]?.text ?? "{}"));
|
|
46896
|
+
if (readBoolean(held, "profiled") !== true && readBoolean(held, "armed") !== true) {
|
|
46897
|
+
if (!going) {
|
|
46898
|
+
return this.createErrorResponse("The game the editor played was not profiled, and it is over.", [
|
|
46899
|
+
"editor_run start with profile: true plays it again with the profiler on from its start"
|
|
46900
|
+
]);
|
|
46901
|
+
}
|
|
46902
|
+
const started = await this.handleViaBridge("profile_start", {});
|
|
46903
|
+
if (started.isError === true) {
|
|
46904
|
+
return this.profilerRefusal(started);
|
|
46905
|
+
}
|
|
46906
|
+
if (readBoolean(asParams(JSON.parse(started.content[0]?.text ?? "{}")), "started") !== true) {
|
|
46907
|
+
return this.createErrorResponse("The editor says it is playing and has no debugger session for the game, so there is nothing to switch the profiler on in yet.", ["Ask again in a moment, once the game is up"]);
|
|
46908
|
+
}
|
|
46909
|
+
const on = await this.firstProfiledFrame();
|
|
46910
|
+
return this.jsonTextResponse({
|
|
46911
|
+
through: "editor",
|
|
46912
|
+
running: true,
|
|
46913
|
+
started: true,
|
|
46914
|
+
functions: [],
|
|
46915
|
+
note: on ? "The profiler was switched on now, for the game the editor is playing, and its first frame has arrived, so it covers what the game does from here on. Ask again for the functions; editor_run start with profile: true covers a game from its start." : `The profiler was switched on for the game the editor is playing, and no frame of it had arrived ${PROFILER_ON_MS}ms later: what the game does before the first one is not counted. Ask again for the functions; editor_run start with profile: true covers a game from its start.`
|
|
46916
|
+
});
|
|
46917
|
+
}
|
|
46918
|
+
const functions = asParams(held["functions"]);
|
|
46919
|
+
const totals = readBoolean(held, "profiled") === true ? {
|
|
46920
|
+
startedAt: 0,
|
|
46921
|
+
lastFrameAt: null,
|
|
46922
|
+
frames: readNonNegativeNumber(held, "frames") ?? 0,
|
|
46923
|
+
complete: readBoolean(held, "complete") === true,
|
|
46924
|
+
unreadable: 0,
|
|
46925
|
+
functions: Object.fromEntries(Object.entries(functions).map(([signature, sum]) => {
|
|
46926
|
+
const [calls, self, total] = Array.isArray(sum) ? sum : [];
|
|
46927
|
+
return [
|
|
46928
|
+
signature,
|
|
46929
|
+
{
|
|
46930
|
+
calls: Number(calls ?? 0),
|
|
46931
|
+
selfSeconds: Number(self ?? 0),
|
|
46932
|
+
totalSeconds: Number(total ?? 0)
|
|
46933
|
+
}
|
|
46934
|
+
];
|
|
46935
|
+
}))
|
|
46936
|
+
} : null;
|
|
46937
|
+
return this.jsonTextResponse(profileAnswer({
|
|
46938
|
+
through: "editor",
|
|
46939
|
+
running: going,
|
|
46940
|
+
totals,
|
|
46941
|
+
coveredMs: readNonNegativeNumber(held, "coveredMs") ?? 0,
|
|
46942
|
+
...fromProject
|
|
46943
|
+
}, limit));
|
|
46944
|
+
}
|
|
46945
|
+
async firstProfiledFrame() {
|
|
46946
|
+
const deadline = Date.now() + PROFILER_ON_MS;
|
|
46947
|
+
while (Date.now() < deadline) {
|
|
46948
|
+
const read = await this.handleViaBridge("profile_read", {});
|
|
46949
|
+
if (read.isError !== true) {
|
|
46950
|
+
const held = asParams(JSON.parse(read.content[0]?.text ?? "{}"));
|
|
46951
|
+
if ((readNonNegativeNumber(held, "frames") ?? 0) > 0) {
|
|
46952
|
+
return true;
|
|
46953
|
+
}
|
|
46954
|
+
}
|
|
46955
|
+
await delay4(100);
|
|
46956
|
+
}
|
|
46957
|
+
return false;
|
|
46958
|
+
}
|
|
46959
|
+
profilerRefusal(refused) {
|
|
46960
|
+
const said = refused.content[0]?.text ?? "";
|
|
46961
|
+
return said.includes("Unknown tool: profile_") ? this.createErrorResponse("The editor's gdharness addon is older than the profiler, so it cannot profile the game it plays.", [
|
|
46962
|
+
"editor_launch restart loads the addon this server installed",
|
|
46963
|
+
"editor_status says which addon the editor holds"
|
|
46964
|
+
]) : refused;
|
|
46965
|
+
}
|
|
46542
46966
|
async handleWaitForRun(args) {
|
|
46543
46967
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
46544
46968
|
const run = await this.currentRun();
|
|
@@ -46574,11 +46998,13 @@ class GodotServer {
|
|
|
46574
46998
|
if (announced !== undefined && editorSays === undefined) {
|
|
46575
46999
|
return stillRunning(run);
|
|
46576
47000
|
}
|
|
46577
|
-
const
|
|
46578
|
-
|
|
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)) {
|
|
46579
47005
|
run.seenPlaying = true;
|
|
46580
47006
|
}
|
|
46581
|
-
return runIsUp(run, said);
|
|
47007
|
+
return runIsUp(run, said, play);
|
|
46582
47008
|
}
|
|
46583
47009
|
async untilTheExitIsRecorded(run) {
|
|
46584
47010
|
const until = Date.now() + EXIT_REPORTED_WITHIN_MS;
|
|
@@ -47683,7 +48109,7 @@ ${staleNote}`);
|
|
|
47683
48109
|
|
|
47684
48110
|
${staleNote}`}`
|
|
47685
48111
|
},
|
|
47686
|
-
{ type: "image", data:
|
|
48112
|
+
{ type: "image", data: readFileSync23(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
47687
48113
|
]
|
|
47688
48114
|
};
|
|
47689
48115
|
} finally {
|
|
@@ -47784,7 +48210,7 @@ async function runGodotServer() {
|
|
|
47784
48210
|
const server = new GodotServer;
|
|
47785
48211
|
await server.run();
|
|
47786
48212
|
}
|
|
47787
|
-
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/", 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 "';
|
|
47788
48214
|
var init_server3 = __esm(() => {
|
|
47789
48215
|
init_mcp();
|
|
47790
48216
|
init_stdio2();
|
|
@@ -47805,6 +48231,7 @@ var init_server3 = __esm(() => {
|
|
|
47805
48231
|
init_engine_run();
|
|
47806
48232
|
init_errors();
|
|
47807
48233
|
init_game_log();
|
|
48234
|
+
init_gdscript_source();
|
|
47808
48235
|
init_godot_bridge();
|
|
47809
48236
|
init_godot_path();
|
|
47810
48237
|
init_headless();
|
|
@@ -47827,6 +48254,7 @@ var init_server3 = __esm(() => {
|
|
|
47827
48254
|
init_runtime_client();
|
|
47828
48255
|
init_scratch();
|
|
47829
48256
|
init_script_origins();
|
|
48257
|
+
init_script_profile();
|
|
47830
48258
|
init_server_version();
|
|
47831
48259
|
init_setup();
|
|
47832
48260
|
init_tool_definitions();
|
|
@@ -48914,6 +49342,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
48914
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 |
|
|
48915
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 |
|
|
48916
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 |
|
|
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 |
|
|
48917
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 |
|
|
48918
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 |
|
|
48919
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 |
|