gdharness 1.1.55 → 1.1.57
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 +419 -53
- package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -0
- 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 +424 -59
- package/build/keeper.js +527 -6
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -7840,10 +7840,90 @@ function announced(line) {
|
|
|
7840
7840
|
if (!match) {
|
|
7841
7841
|
return null;
|
|
7842
7842
|
}
|
|
7843
|
-
return {
|
|
7843
|
+
return {
|
|
7844
|
+
severity: match[2] === "WARNING" ? "warning" : "error",
|
|
7845
|
+
text: match[3] ?? "",
|
|
7846
|
+
byScript: match[2] === "SCRIPT ERROR"
|
|
7847
|
+
};
|
|
7844
7848
|
}
|
|
7845
|
-
function
|
|
7846
|
-
return
|
|
7849
|
+
function reported(entry) {
|
|
7850
|
+
return entry.detail.length === 0 ? { index: entry.index, severity: entry.severity, source: entry.source, text: entry.text } : { ...entry, detail: framesCollapsed(entry.detail) };
|
|
7851
|
+
}
|
|
7852
|
+
function framesCollapsed(detail) {
|
|
7853
|
+
const out = [];
|
|
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;
|
|
7860
|
+
continue;
|
|
7861
|
+
}
|
|
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;
|
|
7882
|
+
}
|
|
7883
|
+
at += frames.length;
|
|
7884
|
+
}
|
|
7885
|
+
return out;
|
|
7886
|
+
}
|
|
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;
|
|
7914
|
+
}
|
|
7915
|
+
function foldedRepeats(entries) {
|
|
7916
|
+
const first = new Map;
|
|
7917
|
+
for (const entry of entries) {
|
|
7918
|
+
const key = JSON.stringify([entry.severity, entry.source, entry.text, entry.detail]);
|
|
7919
|
+
const seen = first.get(key);
|
|
7920
|
+
if (seen === undefined) {
|
|
7921
|
+
first.set(key, { entry, times: 1 });
|
|
7922
|
+
} else {
|
|
7923
|
+
seen.times += 1;
|
|
7924
|
+
}
|
|
7925
|
+
}
|
|
7926
|
+
return [...first.values()].map(({ entry, times }) => times === 1 ? reported(entry) : { ...reported(entry), times });
|
|
7847
7927
|
}
|
|
7848
7928
|
|
|
7849
7929
|
class GameLog {
|
|
@@ -7854,8 +7934,13 @@ class GameLog {
|
|
|
7854
7934
|
transcript: new StringDecoder("utf8")
|
|
7855
7935
|
};
|
|
7856
7936
|
partial = { stdout: "", stderr: "", transcript: "" };
|
|
7857
|
-
lastHeadline =
|
|
7937
|
+
lastHeadline = {
|
|
7938
|
+
stdout: null,
|
|
7939
|
+
stderr: null,
|
|
7940
|
+
transcript: null
|
|
7941
|
+
};
|
|
7858
7942
|
seen = { error: 0, warning: 0, info: 0 };
|
|
7943
|
+
byScript = new Set;
|
|
7859
7944
|
append(source, chunk) {
|
|
7860
7945
|
const decoded = typeof chunk === "string" ? chunk : this.decoders[source].write(chunk);
|
|
7861
7946
|
const lines = (this.partial[source] + decoded).split(`
|
|
@@ -7879,8 +7964,9 @@ class GameLog {
|
|
|
7879
7964
|
if (line.trim() === "") {
|
|
7880
7965
|
return;
|
|
7881
7966
|
}
|
|
7882
|
-
|
|
7883
|
-
|
|
7967
|
+
const owner = this.lastHeadline[source];
|
|
7968
|
+
if (owner !== null && /^\s/.test(line)) {
|
|
7969
|
+
owner.detail.push(line.trim());
|
|
7884
7970
|
return;
|
|
7885
7971
|
}
|
|
7886
7972
|
const headline = announced(line);
|
|
@@ -7893,7 +7979,13 @@ class GameLog {
|
|
|
7893
7979
|
detail
|
|
7894
7980
|
};
|
|
7895
7981
|
this.entries.push(entry);
|
|
7896
|
-
|
|
7982
|
+
if (headline?.byScript === true) {
|
|
7983
|
+
this.byScript.add(entry.index);
|
|
7984
|
+
}
|
|
7985
|
+
this.lastHeadline[source] = headline ? { detail } : null;
|
|
7986
|
+
}
|
|
7987
|
+
raisedByScript(entry) {
|
|
7988
|
+
return this.byScript.has(entry.index);
|
|
7897
7989
|
}
|
|
7898
7990
|
record(severity, text) {
|
|
7899
7991
|
this.entries.push({
|
|
@@ -7903,7 +7995,6 @@ class GameLog {
|
|
|
7903
7995
|
text,
|
|
7904
7996
|
detail: []
|
|
7905
7997
|
});
|
|
7906
|
-
this.lastHeadline = null;
|
|
7907
7998
|
}
|
|
7908
7999
|
get all() {
|
|
7909
8000
|
return this.entries;
|
|
@@ -7927,6 +8018,11 @@ class GameLog {
|
|
|
7927
8018
|
const omitted = Math.max(0, matching.length - options.limit);
|
|
7928
8019
|
return { entries: matching.slice(omitted), omitted };
|
|
7929
8020
|
}
|
|
8021
|
+
selectFolded(options) {
|
|
8022
|
+
const folded = foldedRepeats(this.select({ ...options, limit: Number.POSITIVE_INFINITY }).entries);
|
|
8023
|
+
const omitted = Math.max(0, folded.length - options.limit);
|
|
8024
|
+
return { entries: folded.slice(omitted), omitted };
|
|
8025
|
+
}
|
|
7930
8026
|
}
|
|
7931
8027
|
function answersTo(entry, severity, contains) {
|
|
7932
8028
|
if (!ADMITTED[severity].has(entry.severity)) {
|
|
@@ -7938,7 +8034,7 @@ function answersTo(entry, severity, contains) {
|
|
|
7938
8034
|
const needle = contains.toLowerCase();
|
|
7939
8035
|
return [entry.text, ...entry.detail].some((line) => line.toLowerCase().includes(needle));
|
|
7940
8036
|
}
|
|
7941
|
-
var HEADLINE, COLOUR_CODE, STREAMS, ADMITTED;
|
|
8037
|
+
var HEADLINE, COLOUR_CODE, STREAMS, ADMITTED, FRAME, LONGEST_CYCLE = 64;
|
|
7942
8038
|
var init_game_log = __esm(() => {
|
|
7943
8039
|
HEADLINE = /^(USER )?(SCRIPT ERROR|SHADER ERROR|ERROR|WARNING):\s?(.*)$/;
|
|
7944
8040
|
COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
@@ -7948,6 +8044,7 @@ var init_game_log = __esm(() => {
|
|
|
7948
8044
|
warning: new Set(["error", "warning"]),
|
|
7949
8045
|
info: new Set(["error", "warning", "info"])
|
|
7950
8046
|
};
|
|
8047
|
+
FRAME = /^\[(\d+)\] (.+)$/;
|
|
7951
8048
|
});
|
|
7952
8049
|
|
|
7953
8050
|
// src/engine-run.ts
|
|
@@ -8166,7 +8263,7 @@ function problemsIn(log) {
|
|
|
8166
8263
|
return [];
|
|
8167
8264
|
});
|
|
8168
8265
|
}
|
|
8169
|
-
function
|
|
8266
|
+
function reported2(problems) {
|
|
8170
8267
|
const omitted = Math.max(0, problems.length - MESSAGES_KEPT);
|
|
8171
8268
|
return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
|
|
8172
8269
|
}
|
|
@@ -8251,12 +8348,12 @@ async function importOnce(godotPath, projectPath) {
|
|
|
8251
8348
|
}
|
|
8252
8349
|
const problems = problemsIn(ran.log);
|
|
8253
8350
|
if (ran.exitCode === 0 && ran.failure === null) {
|
|
8254
|
-
return { ok: true, ...
|
|
8351
|
+
return { ok: true, ...reported2(problems) };
|
|
8255
8352
|
}
|
|
8256
8353
|
return {
|
|
8257
8354
|
ok: false,
|
|
8258
8355
|
message: `the import pass failed (${howItEnded(ran)}): ${reason(ran.log, problems)}`,
|
|
8259
|
-
...
|
|
8356
|
+
...reported2(problems)
|
|
8260
8357
|
};
|
|
8261
8358
|
}
|
|
8262
8359
|
async function runOperation(engine, operation, params, projectPath) {
|
|
@@ -8290,7 +8387,7 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
8290
8387
|
return {
|
|
8291
8388
|
ok: false,
|
|
8292
8389
|
message: `${operation} wrote an answer that could not be read (${answer.why})`,
|
|
8293
|
-
...
|
|
8390
|
+
...reported2(problems)
|
|
8294
8391
|
};
|
|
8295
8392
|
}
|
|
8296
8393
|
if (answer.kind === "none") {
|
|
@@ -8298,7 +8395,7 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
8298
8395
|
return {
|
|
8299
8396
|
ok: false,
|
|
8300
8397
|
message: `${operation} ${ending}: ${reason(ran.log, problems)}`,
|
|
8301
|
-
...
|
|
8398
|
+
...reported2(problems)
|
|
8302
8399
|
};
|
|
8303
8400
|
}
|
|
8304
8401
|
const fault = scriptFault(problems, engine.script);
|
|
@@ -8306,14 +8403,14 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
8306
8403
|
return {
|
|
8307
8404
|
ok: false,
|
|
8308
8405
|
message: `${operation} hit an error in the operations script, so its answer is left out: part of it was ` + `never computed. ${fault.text} (${fault.detail[0]?.slice("at: ".length) ?? "no location given"})`,
|
|
8309
|
-
...
|
|
8406
|
+
...reported2(problems)
|
|
8310
8407
|
};
|
|
8311
8408
|
}
|
|
8312
8409
|
const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under engine_messages.`;
|
|
8313
8410
|
return {
|
|
8314
8411
|
ok: true,
|
|
8315
8412
|
payload: answer.payload,
|
|
8316
|
-
...
|
|
8413
|
+
...reported2(problems),
|
|
8317
8414
|
...afterAnswer === undefined ? {} : { afterAnswer }
|
|
8318
8415
|
};
|
|
8319
8416
|
}
|
|
@@ -17646,7 +17743,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17646
17743
|
},
|
|
17647
17744
|
{
|
|
17648
17745
|
name: "project_test",
|
|
17649
|
-
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. 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. Needs gdUnit4 under addons/gdUnit4.",
|
|
17746
|
+
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.",
|
|
17650
17747
|
parameters: {
|
|
17651
17748
|
projectPath: PROJECT_PATH,
|
|
17652
17749
|
path: {
|
|
@@ -18038,6 +18135,17 @@ var init_tool_definitions = __esm(() => {
|
|
|
18038
18135
|
ops: ["check", "wait"],
|
|
18039
18136
|
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."
|
|
18040
18137
|
},
|
|
18138
|
+
profile: {
|
|
18139
|
+
type: "boolean",
|
|
18140
|
+
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."
|
|
18142
|
+
},
|
|
18143
|
+
limit: {
|
|
18144
|
+
type: "integer",
|
|
18145
|
+
minimum: 1,
|
|
18146
|
+
ops: ["profile"],
|
|
18147
|
+
description: "profile: how many functions to answer with, those with the most self time. Default 30."
|
|
18148
|
+
},
|
|
18041
18149
|
andChildren: {
|
|
18042
18150
|
type: "boolean",
|
|
18043
18151
|
ops: ["stop"],
|
|
@@ -18064,7 +18172,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
18064
18172
|
summary: "boot headless, quit after a few frames, and report the verdict",
|
|
18065
18173
|
requires: ["projectPath"]
|
|
18066
18174
|
},
|
|
18067
|
-
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] }
|
|
18175
|
+
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
|
|
18176
|
+
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",
|
|
18178
|
+
requires: []
|
|
18179
|
+
}
|
|
18068
18180
|
},
|
|
18069
18181
|
defaultOperation: "start"
|
|
18070
18182
|
},
|
|
@@ -18094,7 +18206,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18094
18206
|
limit: {
|
|
18095
18207
|
type: "integer",
|
|
18096
18208
|
minimum: 1,
|
|
18097
|
-
description: "The most entries to answer with, newest kept. Default 200, and omitted says how many matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
|
|
18209
|
+
description: "The most entries to answer with, newest kept. Identical entries come back as one, at the first of them, with times saying how many were printed, so the limit counts distinct entries. Default 200, and omitted says how many distinct matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
|
|
18098
18210
|
},
|
|
18099
18211
|
before: {
|
|
18100
18212
|
ops: ["editor"],
|
|
@@ -18547,7 +18659,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18547
18659
|
says: {
|
|
18548
18660
|
type: "string",
|
|
18549
18661
|
ops: ["until"],
|
|
18550
|
-
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.'
|
|
18662
|
+
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.'
|
|
18551
18663
|
},
|
|
18552
18664
|
includeHidden: {
|
|
18553
18665
|
type: "boolean",
|
|
@@ -41706,6 +41818,9 @@ function openTranscript(startedAt) {
|
|
|
41706
41818
|
const path = join24(runsDirectory(), `run-${startedAt}.log`);
|
|
41707
41819
|
return { path, fd: openSync2(path, "a") };
|
|
41708
41820
|
}
|
|
41821
|
+
function profilePath(startedAt) {
|
|
41822
|
+
return join24(runsDirectory(), `run-${startedAt}.profile.json`);
|
|
41823
|
+
}
|
|
41709
41824
|
function noteIsAbout(note, playing) {
|
|
41710
41825
|
return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
|
|
41711
41826
|
}
|
|
@@ -41805,6 +41920,7 @@ function recordAt(path) {
|
|
|
41805
41920
|
projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
|
|
41806
41921
|
...typeof fields["servedBy"] === "string" && fields["servedBy"] !== "" ? { servedBy: fields["servedBy"] } : {},
|
|
41807
41922
|
...typeof fields["keeper"] === "string" && fields["keeper"] !== "" ? { keeper: fields["keeper"] } : {},
|
|
41923
|
+
...typeof fields["profile"] === "string" && fields["profile"] !== "" ? { profile: fields["profile"] } : {},
|
|
41808
41924
|
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
|
|
41809
41925
|
...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
|
|
41810
41926
|
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
@@ -41997,7 +42113,10 @@ function ageMs(path, now) {
|
|
|
41997
42113
|
}
|
|
41998
42114
|
function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
41999
42115
|
const directory = runsDirectory();
|
|
42000
|
-
const kept = new Set(everyRunRecord().
|
|
42116
|
+
const kept = new Set(everyRunRecord().flatMap((record) => [
|
|
42117
|
+
record.transcript,
|
|
42118
|
+
...record.profile === undefined ? [] : [record.profile]
|
|
42119
|
+
]));
|
|
42001
42120
|
let entries;
|
|
42002
42121
|
try {
|
|
42003
42122
|
entries = readdirSync9(directory);
|
|
@@ -42005,7 +42124,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
42005
42124
|
return;
|
|
42006
42125
|
}
|
|
42007
42126
|
for (const entry of entries) {
|
|
42008
|
-
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
42127
|
+
if (!entry.startsWith("run-") || !(entry.endsWith(".log") || entry.endsWith(".profile.json"))) {
|
|
42009
42128
|
continue;
|
|
42010
42129
|
}
|
|
42011
42130
|
const path = join24(directory, entry);
|
|
@@ -42125,6 +42244,57 @@ var init_script_origins = __esm(() => {
|
|
|
42125
42244
|
RELOADED_AT = /GDScript::reload \((res:\/\/.+):(\d+)\)$/;
|
|
42126
42245
|
});
|
|
42127
42246
|
|
|
42247
|
+
// src/godot-variant.ts
|
|
42248
|
+
var FLAG_64, MOST_MESSAGE_BYTES;
|
|
42249
|
+
var init_godot_variant = __esm(() => {
|
|
42250
|
+
FLAG_64 = 1 << 16;
|
|
42251
|
+
MOST_MESSAGE_BYTES = (8 << 20) + 4;
|
|
42252
|
+
});
|
|
42253
|
+
|
|
42254
|
+
// src/script-profile.ts
|
|
42255
|
+
import { readFileSync as readFileSync22, renameSync as renameSync2, writeFileSync as writeFileSync17 } from "node:fs";
|
|
42256
|
+
function readProfileFile(path) {
|
|
42257
|
+
try {
|
|
42258
|
+
const parsed = JSON.parse(readFileSync22(path, "utf8"));
|
|
42259
|
+
return typeof parsed === "object" && typeof parsed.connected === "boolean" ? parsed : null;
|
|
42260
|
+
} catch {
|
|
42261
|
+
return null;
|
|
42262
|
+
}
|
|
42263
|
+
}
|
|
42264
|
+
function signatureParts(signature) {
|
|
42265
|
+
const parts = signature.split("::");
|
|
42266
|
+
if (parts.length < 3) {
|
|
42267
|
+
return { script: "", line: 0, function: signature };
|
|
42268
|
+
}
|
|
42269
|
+
const name = parts.at(-1) ?? "";
|
|
42270
|
+
const line = Number(parts.at(-2));
|
|
42271
|
+
return { script: parts.slice(0, -2).join("::"), line: Number.isFinite(line) ? line : 0, function: name };
|
|
42272
|
+
}
|
|
42273
|
+
function profiledFunctions(totals, limit) {
|
|
42274
|
+
const rows = Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
|
|
42275
|
+
const parts = signatureParts(signature);
|
|
42276
|
+
return {
|
|
42277
|
+
script: parts.script,
|
|
42278
|
+
function: parts.function,
|
|
42279
|
+
line: parts.line,
|
|
42280
|
+
calls: sum.calls,
|
|
42281
|
+
selfMs: ms(sum.selfSeconds),
|
|
42282
|
+
totalMs: ms(sum.totalSeconds)
|
|
42283
|
+
};
|
|
42284
|
+
}).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
|
+
}
|
|
42293
|
+
var ms = (seconds) => Math.round(seconds * 1e6) / 1000;
|
|
42294
|
+
var init_script_profile = __esm(() => {
|
|
42295
|
+
init_godot_variant();
|
|
42296
|
+
});
|
|
42297
|
+
|
|
42128
42298
|
// src/server.ts
|
|
42129
42299
|
import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
|
|
42130
42300
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -42138,7 +42308,7 @@ import {
|
|
|
42138
42308
|
mkdirSync as mkdirSync15,
|
|
42139
42309
|
openSync as openSync3,
|
|
42140
42310
|
readdirSync as readdirSync10,
|
|
42141
|
-
readFileSync as
|
|
42311
|
+
readFileSync as readFileSync23,
|
|
42142
42312
|
readSync as readSync2,
|
|
42143
42313
|
rmdirSync as rmdirSync2,
|
|
42144
42314
|
statSync as statSync9
|
|
@@ -42591,7 +42761,6 @@ function notRunNote(notRun, failFast) {
|
|
|
42591
42761
|
function exportAnswer(ending, asked, before, after) {
|
|
42592
42762
|
const { log } = ending;
|
|
42593
42763
|
const written = after !== null && after !== before;
|
|
42594
|
-
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
42595
42764
|
const verdict = {
|
|
42596
42765
|
exported: ending.exitCode === 0 && ending.failure === null && log.count("error") === 0 && written,
|
|
42597
42766
|
...asked,
|
|
@@ -42600,8 +42769,7 @@ function exportAnswer(ending, asked, before, after) {
|
|
|
42600
42769
|
failure: ending.failure ?? undefined,
|
|
42601
42770
|
errors: log.count("error"),
|
|
42602
42771
|
warnings: log.count("warning"),
|
|
42603
|
-
|
|
42604
|
-
...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
|
|
42772
|
+
...foldedEntriesAnswer(log)
|
|
42605
42773
|
};
|
|
42606
42774
|
if (verdict.exported) {
|
|
42607
42775
|
return { content: [{ type: "text", text: answerJson(verdict) }] };
|
|
@@ -42807,6 +42975,70 @@ function aboveTheRunner(entry) {
|
|
|
42807
42975
|
]
|
|
42808
42976
|
};
|
|
42809
42977
|
}
|
|
42978
|
+
function profileAnswer(reading, limit) {
|
|
42979
|
+
const { totals } = reading;
|
|
42980
|
+
if (totals === null) {
|
|
42981
|
+
return {
|
|
42982
|
+
through: reading.through,
|
|
42983
|
+
running: reading.running,
|
|
42984
|
+
connected: false,
|
|
42985
|
+
functions: [],
|
|
42986
|
+
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
|
+
};
|
|
42988
|
+
}
|
|
42989
|
+
const table = profiledFunctions(totals, limit);
|
|
42990
|
+
const notes = [];
|
|
42991
|
+
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) {
|
|
42993
|
+
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
|
+
} else if (totals.frames === 0) {
|
|
42995
|
+
notes.push(`The game connected and ended before a frame of the profile reached ${reading.through === "editor" ? "the editor" : "this server"}: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${addonNote}`);
|
|
42996
|
+
} else {
|
|
42997
|
+
notes.push(`These are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} that arrived, summed, without the engine's own totals, so what the game did after the last of them is not in it: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${addonNote}`);
|
|
42998
|
+
}
|
|
42999
|
+
if (totals.unreadable > 0) {
|
|
43000
|
+
notes.push(`${totals.unreadable} message${totals.unreadable === 1 ? "" : "s"} from the game could not be read, so the totals have that many gaps.`);
|
|
43001
|
+
}
|
|
43002
|
+
return {
|
|
43003
|
+
through: reading.through,
|
|
43004
|
+
running: reading.running,
|
|
43005
|
+
connected: true,
|
|
43006
|
+
complete: totals.complete,
|
|
43007
|
+
frames: totals.frames,
|
|
43008
|
+
coveredMs: reading.coveredMs,
|
|
43009
|
+
scriptMs: table.scriptMs,
|
|
43010
|
+
functionsCounted: table.counted,
|
|
43011
|
+
omitted: table.omitted > 0 ? table.omitted : undefined,
|
|
43012
|
+
functions: table.functions,
|
|
43013
|
+
note: notes.length > 0 ? notes.join(" ") : undefined
|
|
43014
|
+
};
|
|
43015
|
+
}
|
|
43016
|
+
function foldedEntriesAnswer(log) {
|
|
43017
|
+
const problems = log.selectFolded({
|
|
43018
|
+
severity: "warning",
|
|
43019
|
+
sinceLastCall: false,
|
|
43020
|
+
limit: MOST_ENGINE_ENTRIES
|
|
43021
|
+
});
|
|
43022
|
+
return {
|
|
43023
|
+
entries: problems.entries,
|
|
43024
|
+
...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
|
|
43025
|
+
};
|
|
43026
|
+
}
|
|
43027
|
+
function gdUnitFailsOnScriptErrors(projectPath) {
|
|
43028
|
+
try {
|
|
43029
|
+
return settingKeys(readFileSync23(join26(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
|
|
43030
|
+
} catch {
|
|
43031
|
+
return true;
|
|
43032
|
+
}
|
|
43033
|
+
}
|
|
43034
|
+
function uncaughtNote(uncaught) {
|
|
43035
|
+
const first = uncaught[0];
|
|
43036
|
+
if (first === undefined) {
|
|
43037
|
+
return;
|
|
43038
|
+
}
|
|
43039
|
+
const count = uncaught.length === 1 ? "one script error" : `${uncaught.length} script errors`;
|
|
43040
|
+
return `Every case passed, but the run printed ${count} that no case failed on, the first "${first.text}"; ` + "they are under uncaughtScriptErrors. gdUnit4 fails a case on the script errors its logger sees, and " + "it did not see these, so the cases passed on whatever the faulted calls returned. A stack overflow is " + "one it cannot see: it is raised deeper than the logger can be called.";
|
|
43041
|
+
}
|
|
42810
43042
|
function elsewhereIn(projectPath, asked) {
|
|
42811
43043
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
42812
43044
|
if (wanted === "" || existsSync20(join26(projectPath, wanted))) {
|
|
@@ -42923,7 +43155,7 @@ function scriptsUnder(directory, prefix) {
|
|
|
42923
43155
|
return found;
|
|
42924
43156
|
}
|
|
42925
43157
|
function hasMainScene(projectFile) {
|
|
42926
|
-
const scene = parseProjectGodot(
|
|
43158
|
+
const scene = parseProjectGodot(readFileSync23(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
42927
43159
|
return typeof scene === "string" && scene !== "";
|
|
42928
43160
|
}
|
|
42929
43161
|
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
@@ -42931,13 +43163,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
42931
43163
|
return false;
|
|
42932
43164
|
}
|
|
42933
43165
|
try {
|
|
42934
|
-
return
|
|
43166
|
+
return readFileSync23(join26(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
42935
43167
|
} catch {
|
|
42936
43168
|
return false;
|
|
42937
43169
|
}
|
|
42938
43170
|
}
|
|
42939
43171
|
function editorPlaysHeadless(projectFile) {
|
|
42940
|
-
const runArgs = parseProjectGodot(
|
|
43172
|
+
const runArgs = parseProjectGodot(readFileSync23(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
42941
43173
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
42942
43174
|
}
|
|
42943
43175
|
function camelCased(params) {
|
|
@@ -43610,6 +43842,9 @@ class GodotServer {
|
|
|
43610
43842
|
if (op === "wait") {
|
|
43611
43843
|
return await this.handleWaitForRun(args);
|
|
43612
43844
|
}
|
|
43845
|
+
if (op === "profile") {
|
|
43846
|
+
return await this.handleRunProfile(args);
|
|
43847
|
+
}
|
|
43613
43848
|
return await this.handleRunProject(args, op);
|
|
43614
43849
|
case "editor_output":
|
|
43615
43850
|
return await this.handleGetDebugOutput(args);
|
|
@@ -44363,7 +44598,7 @@ class GodotServer {
|
|
|
44363
44598
|
if (!engine.ok) {
|
|
44364
44599
|
return engine.response;
|
|
44365
44600
|
}
|
|
44366
|
-
const application = parseProjectGodot(
|
|
44601
|
+
const application = parseProjectGodot(readFileSync23(project.value.file, "utf8"))["application"] ?? {};
|
|
44367
44602
|
const name = application["config/name"];
|
|
44368
44603
|
const mainScene = application["run/main_scene"];
|
|
44369
44604
|
const info = {
|
|
@@ -44581,7 +44816,7 @@ class GodotServer {
|
|
|
44581
44816
|
const written = existsSync20(reportsDir) ? readdirSync10(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
44582
44817
|
const newest = written.at(-1);
|
|
44583
44818
|
if (newest !== undefined) {
|
|
44584
|
-
report = parseJUnit(
|
|
44819
|
+
report = parseJUnit(readFileSync23(join26(reportsDir, newest, "results.xml"), "utf8"));
|
|
44585
44820
|
}
|
|
44586
44821
|
} catch (error) {
|
|
44587
44822
|
reportProblem = errorMessage(error);
|
|
@@ -44591,7 +44826,8 @@ class GodotServer {
|
|
|
44591
44826
|
rmdirSync2(dirname19(reportsDir));
|
|
44592
44827
|
} catch {}
|
|
44593
44828
|
}
|
|
44594
|
-
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit:
|
|
44829
|
+
const engineEntries = foldedRepeats(run.log.select({ severity: "warning", sinceLastCall: false, limit: Number.POSITIVE_INFINITY }).entries.map(aboveTheRunner));
|
|
44830
|
+
const engineEntriesShown = engineEntries.slice(-MOST_ENGINE_ENTRIES);
|
|
44595
44831
|
const verdicts = {
|
|
44596
44832
|
0: "passed",
|
|
44597
44833
|
100: "failures",
|
|
@@ -44601,6 +44837,7 @@ class GodotServer {
|
|
|
44601
44837
|
105: "script errors"
|
|
44602
44838
|
};
|
|
44603
44839
|
const exitCode = run.exitCode;
|
|
44840
|
+
const uncaught = report !== null && exitCode === 0 && !timedOut && gdUnitFailsOnScriptErrors(project.value.path) ? run.log.all.filter((entry) => run.log.raisedByScript(entry) && !entry.detail.some((line) => line.includes(ERROR_ASSERT))) : [];
|
|
44604
44841
|
const printed = run.log.select({
|
|
44605
44842
|
severity: "info",
|
|
44606
44843
|
sinceLastCall: false,
|
|
@@ -44614,7 +44851,7 @@ class GodotServer {
|
|
|
44614
44851
|
const declaring = printedErrors.some((error) => unresolvedClassIn(error.message) !== null) ? await originsOf(printedErrors, declaredClasses(project.value.path), checkWithEngine(engine.value, project.value.path)) : { origins: [], unconfirmed: [] };
|
|
44615
44852
|
const scriptErrors = [...declaring.origins, ...printedErrors];
|
|
44616
44853
|
const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
|
|
44617
|
-
const ranAs = cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
|
|
44854
|
+
const ranAs = cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (uncaught.length > 0 ? `${uncaught.length === 1 ? "one script error" : `${uncaught.length} script errors`} that failed no case` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
|
|
44618
44855
|
const nothingAt = missing.join(" and ");
|
|
44619
44856
|
const verdict = missing.length === 0 ? ranAs : `${ranAs}; nothing at ${nothingAt}`;
|
|
44620
44857
|
const missingNote = missing.length === 0 ? undefined : `Nothing at ${nothingAt}, so ${missing.length === 1 ? "it was" : "they were"} left out and only ${asked.join(", ")} ran; a run of fewer paths than were asked for is not called a pass.${elsewhereIn(project.value.path, missing[0] ?? "")}`;
|
|
@@ -44664,7 +44901,7 @@ class GodotServer {
|
|
|
44664
44901
|
...nonEmpty(foldedScriptErrors(scriptErrors)),
|
|
44665
44902
|
...atTheEnd,
|
|
44666
44903
|
arguments: cmdArgs,
|
|
44667
|
-
entries:
|
|
44904
|
+
entries: foldedRepeats(withoutListed(printed, scriptErrors).map(aboveTheRunner)).slice(0, 60),
|
|
44668
44905
|
...printedLines,
|
|
44669
44906
|
savesNote,
|
|
44670
44907
|
...scanned
|
|
@@ -44698,8 +44935,10 @@ class GodotServer {
|
|
|
44698
44935
|
];
|
|
44699
44936
|
});
|
|
44700
44937
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
44938
|
+
const uncaughtFolded = foldedRepeats(uncaught.map(aboveTheRunner));
|
|
44939
|
+
const listedAsUncaught = new Set(uncaughtFolded.map((entry) => entry.index));
|
|
44701
44940
|
return this.jsonTextResponse({
|
|
44702
|
-
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && missing.length === 0,
|
|
44941
|
+
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && missing.length === 0,
|
|
44703
44942
|
verdict,
|
|
44704
44943
|
...missing.length > 0 ? { missing } : {},
|
|
44705
44944
|
...timedOut ? { timedOut, hung, silentForMs } : {},
|
|
@@ -44712,6 +44951,7 @@ class GodotServer {
|
|
|
44712
44951
|
time: report.time,
|
|
44713
44952
|
failed,
|
|
44714
44953
|
hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
|
|
44954
|
+
uncaughtScriptErrors: uncaughtFolded.length > 0 ? uncaughtFolded : undefined,
|
|
44715
44955
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
44716
44956
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
44717
44957
|
notRun: notRun > 0 ? notRun : undefined,
|
|
@@ -44720,6 +44960,7 @@ class GodotServer {
|
|
|
44720
44960
|
...scanned,
|
|
44721
44961
|
note: [
|
|
44722
44962
|
missingNote,
|
|
44963
|
+
uncaughtNote(uncaught),
|
|
44723
44964
|
ranAs.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true)
|
|
44724
44965
|
].filter((part) => part !== undefined).join(" ") || undefined,
|
|
44725
44966
|
suites: unclean.map((suite) => ({
|
|
@@ -44737,7 +44978,8 @@ class GodotServer {
|
|
|
44737
44978
|
suitesPassed: report.suites.length - unclean.length,
|
|
44738
44979
|
engineErrors: run.log.count("error"),
|
|
44739
44980
|
engineWarnings: run.log.count("warning"),
|
|
44740
|
-
engineEntries,
|
|
44981
|
+
engineEntries: engineEntriesShown.map((entry) => listedAsUncaught.has(entry.index) ? { ...entry, detail: undefined, detailUnder: "uncaughtScriptErrors" } : entry),
|
|
44982
|
+
engineEntriesOmitted: engineEntries.length > engineEntriesShown.length ? engineEntries.length - engineEntriesShown.length : undefined,
|
|
44741
44983
|
classes: withAddonClassesCounted(classes.payload, cachedClasses(project.value.path) ?? new Map)
|
|
44742
44984
|
});
|
|
44743
44985
|
}
|
|
@@ -44860,7 +45102,7 @@ class GodotServer {
|
|
|
44860
45102
|
return null;
|
|
44861
45103
|
}
|
|
44862
45104
|
try {
|
|
44863
|
-
return
|
|
45105
|
+
return readFileSync23(contained.absolutePath, "utf8");
|
|
44864
45106
|
} catch {
|
|
44865
45107
|
return null;
|
|
44866
45108
|
}
|
|
@@ -44929,7 +45171,7 @@ class GodotServer {
|
|
|
44929
45171
|
return null;
|
|
44930
45172
|
}
|
|
44931
45173
|
try {
|
|
44932
|
-
return
|
|
45174
|
+
return readFileSync23(contained.absolutePath, "utf8");
|
|
44933
45175
|
} catch {
|
|
44934
45176
|
return null;
|
|
44935
45177
|
}
|
|
@@ -45248,7 +45490,7 @@ class GodotServer {
|
|
|
45248
45490
|
return null;
|
|
45249
45491
|
}
|
|
45250
45492
|
try {
|
|
45251
|
-
return settingKeys(
|
|
45493
|
+
return settingKeys(readFileSync23(join26(projectPath, "project.godot"), "utf8"));
|
|
45252
45494
|
} catch {
|
|
45253
45495
|
return null;
|
|
45254
45496
|
}
|
|
@@ -45549,8 +45791,9 @@ class GodotServer {
|
|
|
45549
45791
|
const runtimeWaitMs = this.announceBudget(args, project.value.path);
|
|
45550
45792
|
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
45551
45793
|
const silent = resolveSilent(args["silent"], process.env) && !headless;
|
|
45794
|
+
const profiled = readBoolean(args, "profile") === true;
|
|
45552
45795
|
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
|
|
45553
|
-
return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
|
|
45796
|
+
return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended, profiled);
|
|
45554
45797
|
}
|
|
45555
45798
|
const cmdArgs = runArguments({
|
|
45556
45799
|
projectPath: project.value.path,
|
|
@@ -45560,7 +45803,7 @@ class GodotServer {
|
|
|
45560
45803
|
silent
|
|
45561
45804
|
});
|
|
45562
45805
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
45563
|
-
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);
|
|
45806
|
+
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);
|
|
45564
45807
|
if ("error" in started) {
|
|
45565
45808
|
return this.createErrorResponse(`The game could not be started: ${started.error}`, [
|
|
45566
45809
|
"editor_status says which Godot this server runs and whether it answers"
|
|
@@ -45680,7 +45923,7 @@ class GodotServer {
|
|
|
45680
45923
|
writeBootNote(run.projectPath, took);
|
|
45681
45924
|
}
|
|
45682
45925
|
}
|
|
45683
|
-
async playThroughEditor(scene, refreshedClasses, scanned, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
|
|
45926
|
+
async playThroughEditor(scene, refreshedClasses, scanned, projectPath, alreadyPlaying, runtimeWaitMs, ended, profiled) {
|
|
45684
45927
|
const log = new GameLog;
|
|
45685
45928
|
const connectingFrom = Date.now();
|
|
45686
45929
|
try {
|
|
@@ -45704,6 +45947,12 @@ class GodotServer {
|
|
|
45704
45947
|
const sendingFrom = Date.now();
|
|
45705
45948
|
const breakpoints = await this.dapHoldingBreakpointsOf(projectPath).reapplyBreakpoints();
|
|
45706
45949
|
this.logDebug(`Breakpoints were sent again after ${Date.now() - sendingFrom}ms`);
|
|
45950
|
+
if (profiled) {
|
|
45951
|
+
const armed = await this.handleViaBridge("profile_start", { arm: true });
|
|
45952
|
+
if (armed.isError === true) {
|
|
45953
|
+
return this.profilerRefusal(armed);
|
|
45954
|
+
}
|
|
45955
|
+
}
|
|
45707
45956
|
const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
|
|
45708
45957
|
if (answer.isError === true) {
|
|
45709
45958
|
return answer;
|
|
@@ -45850,7 +46099,7 @@ class GodotServer {
|
|
|
45850
46099
|
return 0;
|
|
45851
46100
|
}
|
|
45852
46101
|
try {
|
|
45853
|
-
return reportAlreadyInTranscript(
|
|
46102
|
+
return reportAlreadyInTranscript(readFileSync23(game.transcript), readFileSync23(report));
|
|
45854
46103
|
} catch (error) {
|
|
45855
46104
|
this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
|
|
45856
46105
|
return 0;
|
|
@@ -46054,7 +46303,7 @@ class GodotServer {
|
|
|
46054
46303
|
});
|
|
46055
46304
|
return Object.assign(started, { settled });
|
|
46056
46305
|
}
|
|
46057
|
-
async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop) {
|
|
46306
|
+
async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop, profiled = false) {
|
|
46058
46307
|
const startedAt = Date.now();
|
|
46059
46308
|
const transcript = openTranscript(startedAt);
|
|
46060
46309
|
closeSync2(transcript.fd);
|
|
@@ -46062,7 +46311,13 @@ class GodotServer {
|
|
|
46062
46311
|
command: godotPath,
|
|
46063
46312
|
args: cmdArgs,
|
|
46064
46313
|
...env === undefined ? {} : { env },
|
|
46065
|
-
run: {
|
|
46314
|
+
run: {
|
|
46315
|
+
transcript: transcript.path,
|
|
46316
|
+
startedAt,
|
|
46317
|
+
projectPath,
|
|
46318
|
+
...this.servedBy(),
|
|
46319
|
+
...profiled ? { profile: profilePath(startedAt) } : {}
|
|
46320
|
+
},
|
|
46066
46321
|
...desktop === undefined ? {} : { desktop }
|
|
46067
46322
|
});
|
|
46068
46323
|
if ("error" in launched) {
|
|
@@ -46446,9 +46701,118 @@ class GodotServer {
|
|
|
46446
46701
|
warnings,
|
|
46447
46702
|
...scanWaitAnswer(scanned),
|
|
46448
46703
|
savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
46449
|
-
|
|
46704
|
+
...foldedEntriesAnswer(boot.log)
|
|
46450
46705
|
});
|
|
46451
46706
|
}
|
|
46707
|
+
async handleRunProfile(args) {
|
|
46708
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
46709
|
+
const run = await this.currentRun();
|
|
46710
|
+
if (!run) {
|
|
46711
|
+
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
46712
|
+
}
|
|
46713
|
+
const limit = readPositiveNumber(args, "limit") ?? PROFILE_FUNCTIONS;
|
|
46714
|
+
const going = await this.runStillGoing(run);
|
|
46715
|
+
const registered = run.projectPath === null ? null : inspectProject(run.projectPath);
|
|
46716
|
+
const runtimeAddon = registered !== null && (registered.runtimeAutoloadPath !== null || registered.runtimeLoaderAutoload !== null);
|
|
46717
|
+
if (run.throughEditor) {
|
|
46718
|
+
return await this.profileOfThePlayedGame(going, runtimeAddon, limit);
|
|
46719
|
+
}
|
|
46720
|
+
const record = run.projectPath === null ? null : readRunRecord(run.projectPath);
|
|
46721
|
+
const path = record !== null && record.pid === run.pid ? record.profile : undefined;
|
|
46722
|
+
if (path === undefined) {
|
|
46723
|
+
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"]);
|
|
46724
|
+
}
|
|
46725
|
+
const file = readProfileFile(path);
|
|
46726
|
+
if (file === null) {
|
|
46727
|
+
return this.createErrorResponse(`The run's profile at ${path} could not be read.`, [
|
|
46728
|
+
"editor_run start with profile: true starts the run again with the profiler on"
|
|
46729
|
+
]);
|
|
46730
|
+
}
|
|
46731
|
+
const totals = file.connected ? file : null;
|
|
46732
|
+
return this.jsonTextResponse(profileAnswer({
|
|
46733
|
+
through: "gdharness",
|
|
46734
|
+
running: going,
|
|
46735
|
+
totals,
|
|
46736
|
+
coveredMs: totals?.lastFrameAt == null ? 0 : totals.lastFrameAt - totals.startedAt,
|
|
46737
|
+
runtimeAddon
|
|
46738
|
+
}, limit));
|
|
46739
|
+
}
|
|
46740
|
+
async profileOfThePlayedGame(going, runtimeAddon, limit) {
|
|
46741
|
+
const read = await this.handleViaBridge("profile_read", {});
|
|
46742
|
+
if (read.isError === true) {
|
|
46743
|
+
return this.profilerRefusal(read);
|
|
46744
|
+
}
|
|
46745
|
+
const held = asParams(JSON.parse(read.content[0]?.text ?? "{}"));
|
|
46746
|
+
if (readBoolean(held, "profiled") !== true && readBoolean(held, "armed") !== true) {
|
|
46747
|
+
if (!going) {
|
|
46748
|
+
return this.createErrorResponse("The game the editor played was not profiled, and it is over.", [
|
|
46749
|
+
"editor_run start with profile: true plays it again with the profiler on from its start"
|
|
46750
|
+
]);
|
|
46751
|
+
}
|
|
46752
|
+
const started = await this.handleViaBridge("profile_start", {});
|
|
46753
|
+
if (started.isError === true) {
|
|
46754
|
+
return this.profilerRefusal(started);
|
|
46755
|
+
}
|
|
46756
|
+
if (readBoolean(asParams(JSON.parse(started.content[0]?.text ?? "{}")), "started") !== true) {
|
|
46757
|
+
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"]);
|
|
46758
|
+
}
|
|
46759
|
+
const on = await this.firstProfiledFrame();
|
|
46760
|
+
return this.jsonTextResponse({
|
|
46761
|
+
through: "editor",
|
|
46762
|
+
running: true,
|
|
46763
|
+
started: true,
|
|
46764
|
+
functions: [],
|
|
46765
|
+
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.`
|
|
46766
|
+
});
|
|
46767
|
+
}
|
|
46768
|
+
const functions = asParams(held["functions"]);
|
|
46769
|
+
const totals = readBoolean(held, "profiled") === true ? {
|
|
46770
|
+
startedAt: 0,
|
|
46771
|
+
lastFrameAt: null,
|
|
46772
|
+
frames: readNonNegativeNumber(held, "frames") ?? 0,
|
|
46773
|
+
complete: readBoolean(held, "complete") === true,
|
|
46774
|
+
unreadable: 0,
|
|
46775
|
+
functions: Object.fromEntries(Object.entries(functions).map(([signature, sum]) => {
|
|
46776
|
+
const [calls, self, total] = Array.isArray(sum) ? sum : [];
|
|
46777
|
+
return [
|
|
46778
|
+
signature,
|
|
46779
|
+
{
|
|
46780
|
+
calls: Number(calls ?? 0),
|
|
46781
|
+
selfSeconds: Number(self ?? 0),
|
|
46782
|
+
totalSeconds: Number(total ?? 0)
|
|
46783
|
+
}
|
|
46784
|
+
];
|
|
46785
|
+
}))
|
|
46786
|
+
} : null;
|
|
46787
|
+
return this.jsonTextResponse(profileAnswer({
|
|
46788
|
+
through: "editor",
|
|
46789
|
+
running: going,
|
|
46790
|
+
totals,
|
|
46791
|
+
coveredMs: readNonNegativeNumber(held, "coveredMs") ?? 0,
|
|
46792
|
+
runtimeAddon
|
|
46793
|
+
}, limit));
|
|
46794
|
+
}
|
|
46795
|
+
async firstProfiledFrame() {
|
|
46796
|
+
const deadline = Date.now() + PROFILER_ON_MS;
|
|
46797
|
+
while (Date.now() < deadline) {
|
|
46798
|
+
const read = await this.handleViaBridge("profile_read", {});
|
|
46799
|
+
if (read.isError !== true) {
|
|
46800
|
+
const held = asParams(JSON.parse(read.content[0]?.text ?? "{}"));
|
|
46801
|
+
if ((readNonNegativeNumber(held, "frames") ?? 0) > 0) {
|
|
46802
|
+
return true;
|
|
46803
|
+
}
|
|
46804
|
+
}
|
|
46805
|
+
await delay4(100);
|
|
46806
|
+
}
|
|
46807
|
+
return false;
|
|
46808
|
+
}
|
|
46809
|
+
profilerRefusal(refused) {
|
|
46810
|
+
const said = refused.content[0]?.text ?? "";
|
|
46811
|
+
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.", [
|
|
46812
|
+
"editor_launch restart loads the addon this server installed",
|
|
46813
|
+
"editor_status says which addon the editor holds"
|
|
46814
|
+
]) : refused;
|
|
46815
|
+
}
|
|
46452
46816
|
async handleWaitForRun(args) {
|
|
46453
46817
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
46454
46818
|
const run = await this.currentRun();
|
|
@@ -46530,7 +46894,7 @@ class GodotServer {
|
|
|
46530
46894
|
const asked = readString(args, "severity");
|
|
46531
46895
|
const severity = asked === "error" || asked === "warning" ? asked : "info";
|
|
46532
46896
|
const contains = readNonEmptyString(args, "contains");
|
|
46533
|
-
const selected = console2.log.
|
|
46897
|
+
const selected = console2.log.selectFolded({
|
|
46534
46898
|
severity,
|
|
46535
46899
|
sinceLastCall: false,
|
|
46536
46900
|
contains,
|
|
@@ -46559,7 +46923,7 @@ class GodotServer {
|
|
|
46559
46923
|
},
|
|
46560
46924
|
repeated: shown,
|
|
46561
46925
|
moreShapes: grouped.length > shown.length ? grouped.length - shown.length : undefined,
|
|
46562
|
-
entries:
|
|
46926
|
+
entries: selected.entries,
|
|
46563
46927
|
omitted: selected.omitted === 0 ? undefined : selected.omitted
|
|
46564
46928
|
});
|
|
46565
46929
|
}
|
|
@@ -46581,7 +46945,7 @@ class GodotServer {
|
|
|
46581
46945
|
run.endedUnwatched = noCodeWillCome(run);
|
|
46582
46946
|
}
|
|
46583
46947
|
const severity = readString(args, "severity");
|
|
46584
|
-
const selected = run.log.
|
|
46948
|
+
const selected = run.log.selectFolded({
|
|
46585
46949
|
severity: severity === "error" || severity === "warning" ? severity : "info",
|
|
46586
46950
|
sinceLastCall: readBoolean(args, "sinceLastCall") ?? false,
|
|
46587
46951
|
contains: readNonEmptyString(args, "contains"),
|
|
@@ -46647,7 +47011,7 @@ class GodotServer {
|
|
|
46647
47011
|
cpuSeconds,
|
|
46648
47012
|
note: notes.length > 0 ? notes.join(" ") : undefined,
|
|
46649
47013
|
omitted: selected.omitted,
|
|
46650
|
-
entries:
|
|
47014
|
+
entries: selected.entries
|
|
46651
47015
|
});
|
|
46652
47016
|
}
|
|
46653
47017
|
anotherProjectsRun(runsProject, args, act) {
|
|
@@ -46714,7 +47078,7 @@ class GodotServer {
|
|
|
46714
47078
|
clean: verdict.clean,
|
|
46715
47079
|
errorsUnread: errorsUnread(stopped) ? true : undefined,
|
|
46716
47080
|
note: verdict.withChildren ? `${verdict.note} ${aboutTheChildren(ended, stopped.throughEditor)}` : verdict.note,
|
|
46717
|
-
|
|
47081
|
+
...foldedEntriesAnswer(stopped.log)
|
|
46718
47082
|
});
|
|
46719
47083
|
}
|
|
46720
47084
|
async stopAnnouncedGame(pid, andChildren, args) {
|
|
@@ -47593,7 +47957,7 @@ ${staleNote}`);
|
|
|
47593
47957
|
|
|
47594
47958
|
${staleNote}`}`
|
|
47595
47959
|
},
|
|
47596
|
-
{ type: "image", data:
|
|
47960
|
+
{ type: "image", data: readFileSync23(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
47597
47961
|
]
|
|
47598
47962
|
};
|
|
47599
47963
|
} finally {
|
|
@@ -47694,7 +48058,7 @@ async function runGodotServer() {
|
|
|
47694
48058
|
const server = new GodotServer;
|
|
47695
48059
|
await server.run();
|
|
47696
48060
|
}
|
|
47697
|
-
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/", EXTERNAL_MEMBER = 'Could not resolve external class member "';
|
|
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 "';
|
|
47698
48062
|
var init_server3 = __esm(() => {
|
|
47699
48063
|
init_mcp();
|
|
47700
48064
|
init_stdio2();
|
|
@@ -47737,6 +48101,7 @@ var init_server3 = __esm(() => {
|
|
|
47737
48101
|
init_runtime_client();
|
|
47738
48102
|
init_scratch();
|
|
47739
48103
|
init_script_origins();
|
|
48104
|
+
init_script_profile();
|
|
47740
48105
|
init_server_version();
|
|
47741
48106
|
init_setup();
|
|
47742
48107
|
init_tool_definitions();
|
|
@@ -48824,6 +49189,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
48824
49189
|
| 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 |
|
|
48825
49190
|
| 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 |
|
|
48826
49191
|
| 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 |
|
|
48827
49193
|
| 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 |
|
|
48828
49194
|
| 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 |
|
|
48829
49195
|
| 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 |
|