gdharness 1.2.8 → 1.2.10
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 +159 -42
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +11 -0
- package/build/index.js +160 -39
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -9347,7 +9347,7 @@ function bodyHolding(body, offset) {
|
|
|
9347
9347
|
const inner = body.inner.find((one) => one.start <= offset && offset <= one.end);
|
|
9348
9348
|
return inner === undefined ? body : bodyHolding(inner, offset);
|
|
9349
9349
|
}
|
|
9350
|
-
function callSitesOf(source, name, otherClasses = new Set) {
|
|
9350
|
+
function callSitesOf(source, name, otherClasses = new Set, property = false) {
|
|
9351
9351
|
const regions = regionsOf(source);
|
|
9352
9352
|
const code = codeOf(source, regions);
|
|
9353
9353
|
const codeLines = code.split(`
|
|
@@ -9358,8 +9358,31 @@ function callSitesOf(source, name, otherClasses = new Set) {
|
|
|
9358
9358
|
const declared = bodyHolding(shape.body, at).declarations.find((one) => one.name === member && (one.kind === "var" || one.kind === "const"));
|
|
9359
9359
|
return declared === undefined ? null : declaredClass(codeLines[lines.positionOf(declared.offset).line] ?? "", member);
|
|
9360
9360
|
};
|
|
9361
|
+
const blockUnder = (declared) => {
|
|
9362
|
+
const indent = (codeLines[declared] ?? "").search(/\S|$/);
|
|
9363
|
+
let last = declared;
|
|
9364
|
+
for (let index = declared + 1;index < codeLines.length; index += 1) {
|
|
9365
|
+
const text = codeLines[index] ?? "";
|
|
9366
|
+
if (text.trim() === "") {
|
|
9367
|
+
continue;
|
|
9368
|
+
}
|
|
9369
|
+
if (text.search(/\S/) <= indent) {
|
|
9370
|
+
break;
|
|
9371
|
+
}
|
|
9372
|
+
last = index;
|
|
9373
|
+
}
|
|
9374
|
+
return [declared, last];
|
|
9375
|
+
};
|
|
9376
|
+
const accessorsIn = (body) => [
|
|
9377
|
+
...body.declarations.filter((one) => one.kind === "var" && one.name === name).map((one) => blockUnder(lines.positionOf(one.offset).line)),
|
|
9378
|
+
...body.inner.flatMap(accessorsIn)
|
|
9379
|
+
];
|
|
9380
|
+
const accessors = property ? accessorsIn(shape.body) : [];
|
|
9361
9381
|
return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).filter((one) => {
|
|
9362
9382
|
const index = lines.positionOf(one.offset).line;
|
|
9383
|
+
if (!one.afterDot && accessors.some(([declared, last]) => index > declared && index <= last)) {
|
|
9384
|
+
return false;
|
|
9385
|
+
}
|
|
9363
9386
|
if (one.afterDot) {
|
|
9364
9387
|
const receiver = receiverBefore(code, one.offset);
|
|
9365
9388
|
if (receiver === null || otherClasses.size === 0) {
|
|
@@ -9377,11 +9400,12 @@ function callSitesOf(source, name, otherClasses = new Set) {
|
|
|
9377
9400
|
if (localAt(codeLines, index, name) !== null) {
|
|
9378
9401
|
return false;
|
|
9379
9402
|
}
|
|
9380
|
-
return !bodyHolding(shape.body, one.offset).declarations.some((declared) => declared.name === name && MEMBER_KINDS.has(declared.kind));
|
|
9403
|
+
return !bodyHolding(shape.body, one.offset).declarations.some((declared) => declared.name === name && MEMBER_KINDS.has(declared.kind) && !(property && declared.kind === "var"));
|
|
9381
9404
|
}).map((one) => {
|
|
9382
9405
|
const line = lines.positionOf(one.offset).line + 1;
|
|
9383
9406
|
const after = code.slice(one.offset + name.length).trimStart();
|
|
9384
|
-
|
|
9407
|
+
const within = enclosingFunction(source, line, code);
|
|
9408
|
+
return property ? { line, within, access: ASSIGNED.test(after) ? "write" : "read" } : { line, within, called: after.startsWith("(") };
|
|
9385
9409
|
});
|
|
9386
9410
|
}
|
|
9387
9411
|
function collectEnumValues(text, from, into) {
|
|
@@ -9399,7 +9423,7 @@ function collectEnumValues(text, from, into) {
|
|
|
9399
9423
|
function isIdentifier(name) {
|
|
9400
9424
|
return new RegExp(`^${NAME}$`, "u").test(name) && !KEYWORDS.has(name);
|
|
9401
9425
|
}
|
|
9402
|
-
var IDENTIFIER_START, IDENTIFIER_PART, DECLARING, NAME = "[\\p{L}_][\\p{L}\\p{N}_]*", ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*", DECLARATION_LINE, CLASS_NAME_LINE, EXTENDS_LINE, EXTENDS_CLAUSE, FUNC_LINE, SUPER_CALL, LAMBDA_HEADER, DOTTED, MEMBER_KINDS, KEYWORDS;
|
|
9426
|
+
var IDENTIFIER_START, IDENTIFIER_PART, DECLARING, NAME = "[\\p{L}_][\\p{L}\\p{N}_]*", ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*", DECLARATION_LINE, CLASS_NAME_LINE, EXTENDS_LINE, EXTENDS_CLAUSE, FUNC_LINE, SUPER_CALL, LAMBDA_HEADER, ASSIGNED, DOTTED, MEMBER_KINDS, KEYWORDS;
|
|
9403
9427
|
var init_gdscript_source = __esm(() => {
|
|
9404
9428
|
IDENTIFIER_START = /[\p{L}_]/u;
|
|
9405
9429
|
IDENTIFIER_PART = /[\p{L}\p{N}_]/u;
|
|
@@ -9411,6 +9435,7 @@ var init_gdscript_source = __esm(() => {
|
|
|
9411
9435
|
FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
|
|
9412
9436
|
SUPER_CALL = /(?<![\p{L}\p{N}_.])super\s*[.(]/u;
|
|
9413
9437
|
LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
|
|
9438
|
+
ASSIGNED = /^(?:\*\*|<<|>>|[-+*/%&|^])?=(?!=)/;
|
|
9414
9439
|
DOTTED = `${NAME}(?:\\.${NAME})*`;
|
|
9415
9440
|
MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
|
|
9416
9441
|
KEYWORDS = new Set([
|
|
@@ -18355,7 +18380,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18355
18380
|
type: "integer",
|
|
18356
18381
|
minimum: 0,
|
|
18357
18382
|
ops: ["start"],
|
|
18358
|
-
description: "start: how long to wait for the game to announce its runtime before answering. Default 5000, or half as long again as the last
|
|
18383
|
+
description: "start: how long to wait for the game to announce its runtime before answering. Default 5000, or half as long again as the slowest of the last five boots of the same scene when that is more, up to 60000: the server notes how long each game took to announce, per scene and for the main scene when none is named, since a boot moves with the load on the machine, so a scene that boots slowly is waited for from its second start on without being asked to, and a harness scene that announces late sets nothing for the main scene. What it waits for is the first frame, so everything the game does before drawing one is inside it, including work the args just asked for: a flag that simulates six years of game time before anything is drawn makes the announcement that late, on a project that announces promptly without it. Raise it for such a run rather than reading runtime listening false as a fault. 0 does not wait at all, which is the one to pass for a scene that announces nothing by construction, such as a bench that prints and quits."
|
|
18359
18384
|
},
|
|
18360
18385
|
frames: {
|
|
18361
18386
|
type: "integer",
|
|
@@ -18377,7 +18402,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18377
18402
|
callersOf: {
|
|
18378
18403
|
type: "string",
|
|
18379
18404
|
ops: ["profile"],
|
|
18380
|
-
description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too, except, when Class.name was asked, one called through another class of the project by name or through a parameter, local or class variable declared as one, and a call made through a string is not. A parameter, local or class variable of the same name is not the function and is not listed."
|
|
18405
|
+
description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too, except, when Class.name was asked, one called through another class of the project by name or through a parameter, local or class variable declared as one, and a call made through a string is not. A parameter, local or class variable of the same name is not the function and is not listed. A property's get or set block, which the profile names Class.@name_getter or Class.@name_setter, is asked about by that name or by the property's: the places are where the property is named, each saying instead of called whether it writes the property (access write), which runs the set block, or reads it (access read), which runs the get block, and the rows are both blocks'. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not places."
|
|
18381
18406
|
},
|
|
18382
18407
|
limit: {
|
|
18383
18408
|
type: "integer",
|
|
@@ -18413,7 +18438,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18413
18438
|
},
|
|
18414
18439
|
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
|
|
18415
18440
|
profile: {
|
|
18416
|
-
summary: "where the run spends its time by GDScript function, while it runs or after it ended: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, sorted by self time, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. A run started with profile: true is profiled from its start. A game the editor plays that was not is profiled from the moment this is asked, and the answer says so; a run this server started without it is refused, since its debugger is fixed as it starts",
|
|
18441
|
+
summary: "where the run spends its time by GDScript function, while it runs or after it ended: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, sorted by self time, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. 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",
|
|
18417
18442
|
requires: []
|
|
18418
18443
|
}
|
|
18419
18444
|
},
|
|
@@ -34419,19 +34444,33 @@ import { dirname as dirname9, join as join14 } from "node:path";
|
|
|
34419
34444
|
function bootNotePath(projectPath) {
|
|
34420
34445
|
return join14(projectPath, ".godot", "gdharness-boot.json");
|
|
34421
34446
|
}
|
|
34422
|
-
function
|
|
34447
|
+
function readScenes(projectPath) {
|
|
34423
34448
|
try {
|
|
34424
|
-
|
|
34425
|
-
return noted !== undefined && Number.isFinite(noted) && noted > 0 ? noted : null;
|
|
34449
|
+
return asParams(asParams(JSON.parse(readFileSync12(bootNotePath(projectPath), "utf8")))["scenes"]);
|
|
34426
34450
|
} catch {
|
|
34427
|
-
return
|
|
34451
|
+
return {};
|
|
34428
34452
|
}
|
|
34429
34453
|
}
|
|
34430
|
-
function
|
|
34454
|
+
function bootsOf(projectPath, scene) {
|
|
34455
|
+
const kept = asParams(readScenes(projectPath)[scene])["boots"];
|
|
34456
|
+
return Array.isArray(kept) ? kept.filter((one) => typeof one === "number" && Number.isFinite(one) && one > 0) : [];
|
|
34457
|
+
}
|
|
34458
|
+
function readBootNote(projectPath, scene) {
|
|
34459
|
+
const boots = bootsOf(projectPath, scene);
|
|
34460
|
+
return boots.length === 0 ? null : Math.max(...boots);
|
|
34461
|
+
}
|
|
34462
|
+
function writeBootNote(projectPath, scene, announcedAfterMs) {
|
|
34431
34463
|
try {
|
|
34432
34464
|
const path = bootNotePath(projectPath);
|
|
34433
34465
|
mkdirSync6(dirname9(path), { recursive: true });
|
|
34434
|
-
|
|
34466
|
+
const scenes = {
|
|
34467
|
+
...readScenes(projectPath),
|
|
34468
|
+
[scene]: {
|
|
34469
|
+
boots: [...bootsOf(projectPath, scene), Math.round(announcedAfterMs)].slice(-BOOTS_KEPT),
|
|
34470
|
+
at: new Date().toISOString()
|
|
34471
|
+
}
|
|
34472
|
+
};
|
|
34473
|
+
writeFileSync8(path, JSON.stringify({ scenes }, null, 2), "utf8");
|
|
34435
34474
|
} catch {}
|
|
34436
34475
|
}
|
|
34437
34476
|
function halfAsLongAgain(lastBootMs) {
|
|
@@ -34443,7 +34482,7 @@ function waitSizedTo(lastBootMs, usual = ANNOUNCE_BUDGET_MS) {
|
|
|
34443
34482
|
}
|
|
34444
34483
|
return Math.min(LONGEST_SIZED_WAIT_MS, Math.max(usual, halfAsLongAgain(lastBootMs)));
|
|
34445
34484
|
}
|
|
34446
|
-
var LONGEST_SIZED_WAIT_MS = 60000;
|
|
34485
|
+
var MAIN_SCENE = "", BOOTS_KEPT = 5, LONGEST_SIZED_WAIT_MS = 60000;
|
|
34447
34486
|
var init_boot_note = __esm(() => {
|
|
34448
34487
|
init_runtime_client();
|
|
34449
34488
|
});
|
|
@@ -42603,6 +42642,9 @@ function queuedMessagesOf(projectGodot) {
|
|
|
42603
42642
|
const set = projectGodot === null ? undefined : parseProjectGodot(projectGodot)["network"]?.[QUEUE_SETTING];
|
|
42604
42643
|
return typeof set === "number" && Number.isInteger(set) && set > 0 ? set : QUEUE_DEFAULT;
|
|
42605
42644
|
}
|
|
42645
|
+
function propertyOf(fn) {
|
|
42646
|
+
return /^@(.+)_(?:getter|setter)$/.exec(bareName(fn))?.[1] ?? null;
|
|
42647
|
+
}
|
|
42606
42648
|
function signatureParts(signature) {
|
|
42607
42649
|
const parts = signature.split("::");
|
|
42608
42650
|
if (parts.length < 3) {
|
|
@@ -42624,7 +42666,8 @@ function profiledFunctions(totals, limit, sourceOf = () => null) {
|
|
|
42624
42666
|
superCallers: rows.filter((row) => row.selfIncludesSuper === true).length,
|
|
42625
42667
|
perCallUs,
|
|
42626
42668
|
overheadDominated: rows.filter((row) => row.overheadMs !== undefined).length,
|
|
42627
|
-
overheadNamed: rows.filter((row) => row.overheadMs !== undefined).map((row) => row.function)
|
|
42669
|
+
overheadNamed: rows.filter((row) => row.overheadMs !== undefined).map((row) => row.function),
|
|
42670
|
+
overheadReach: rows.findLastIndex((row) => row.overheadMs !== undefined) + 1
|
|
42628
42671
|
};
|
|
42629
42672
|
}
|
|
42630
42673
|
function isCalibration(signature) {
|
|
@@ -42659,6 +42702,7 @@ function rowsOf(totals, sourceOf) {
|
|
|
42659
42702
|
return Object.entries(totals.functions).filter(([signature, sum]) => sum.calls > 0 && !isCalibration(signature)).map(([signature, sum]) => {
|
|
42660
42703
|
const parts = signatureParts(signature);
|
|
42661
42704
|
const code = codeOfScript(parts.script);
|
|
42705
|
+
const property = propertyOf(parts.function);
|
|
42662
42706
|
return {
|
|
42663
42707
|
script: parts.script,
|
|
42664
42708
|
function: parts.function,
|
|
@@ -42666,7 +42710,8 @@ function rowsOf(totals, sourceOf) {
|
|
|
42666
42710
|
calls: sum.calls,
|
|
42667
42711
|
selfMs: ms(sum.selfSeconds),
|
|
42668
42712
|
totalMs: ms(sum.totalSeconds),
|
|
42669
|
-
...code !== null && bodyCallsSuper(code, parts.line, parts.function.endsWith("(lambda)")) ? { selfIncludesSuper: true } : {}
|
|
42713
|
+
...code !== null && bodyCallsSuper(code, parts.line, parts.function.endsWith("(lambda)")) ? { selfIncludesSuper: true } : {},
|
|
42714
|
+
...property === null ? {} : { property }
|
|
42670
42715
|
};
|
|
42671
42716
|
}).sort((a, b) => b.selfMs - a.selfMs || b.totalMs - a.totalMs || a.function.localeCompare(b.function));
|
|
42672
42717
|
}
|
|
@@ -42689,10 +42734,13 @@ function classNamesOf(source) {
|
|
|
42689
42734
|
function callersOf(totals, name, scripts) {
|
|
42690
42735
|
const listed = [...scripts];
|
|
42691
42736
|
const sources = new Map(listed);
|
|
42692
|
-
const
|
|
42693
|
-
const
|
|
42694
|
-
const target = rows.filter((row) => name.includes(".") ? row.function === name : bareName(row.function) === bare);
|
|
42737
|
+
const perCallUs = perCallCostOf(totals);
|
|
42738
|
+
const rows = rowsOf(totals, (script) => sources.get(script) ?? null).map((row) => withOverhead(row, perCallUs));
|
|
42695
42739
|
const asked = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : null;
|
|
42740
|
+
const bare = propertyOf(name) ?? bareName(name);
|
|
42741
|
+
const ownerOf = (fn) => fn.includes(".") ? fn.slice(0, fn.lastIndexOf(".")) : null;
|
|
42742
|
+
const target = rows.filter((row) => (bareName(row.function) === bare || row.property === bare) && (asked === null || ownerOf(row.function) === asked));
|
|
42743
|
+
const property = propertyOf(name) !== null || target.some((row) => row.property === bare);
|
|
42696
42744
|
const otherClasses = new Set;
|
|
42697
42745
|
if (asked !== null) {
|
|
42698
42746
|
for (const [, source] of listed) {
|
|
@@ -42705,7 +42753,7 @@ function callersOf(totals, name, scripts) {
|
|
|
42705
42753
|
}
|
|
42706
42754
|
const sites = [];
|
|
42707
42755
|
for (const [script, source] of listed) {
|
|
42708
|
-
for (const site of callSitesOf(source, bare, otherClasses)) {
|
|
42756
|
+
for (const site of callSitesOf(source, bare, otherClasses, property)) {
|
|
42709
42757
|
const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
|
|
42710
42758
|
sites.push({
|
|
42711
42759
|
script,
|
|
@@ -42720,7 +42768,12 @@ function callersOf(totals, name, scripts) {
|
|
|
42720
42768
|
}
|
|
42721
42769
|
}
|
|
42722
42770
|
sites.sort((a, b) => (b.withinTotalMs ?? -1) - (a.withinTotalMs ?? -1) || a.script.localeCompare(b.script) || a.line - b.line);
|
|
42723
|
-
return {
|
|
42771
|
+
return {
|
|
42772
|
+
target,
|
|
42773
|
+
sites: sites.slice(0, CALL_SITES),
|
|
42774
|
+
omitted: Math.max(0, sites.length - CALL_SITES),
|
|
42775
|
+
property
|
|
42776
|
+
};
|
|
42724
42777
|
}
|
|
42725
42778
|
var QUEUE_SETTING = "limits/debugger/max_queued_messages", QUEUE_DEFAULT = 2048, QUEUE_HEADROOM = 512, UNNAMED_PREFIX = "#", PROFILE_FROM_START = "GDHARNESS_PROFILE_FROM_START", ms = (seconds) => Math.round(seconds * 1e6) / 1000, RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd", PROFILER_PROBE = "_profiler_probe", PROFILER_CALIBRATOR = "_calibrate_the_profiler", CALL_SITES = 40;
|
|
42726
42779
|
var init_script_profile = __esm(() => {
|
|
@@ -43100,7 +43153,7 @@ function runtimeVerdict(endpoint, after) {
|
|
|
43100
43153
|
};
|
|
43101
43154
|
}
|
|
43102
43155
|
const state = after.playingSeen === false ? "the editor has not yet reported the game as playing, which a restarted editor does not until its scan is over" : "the game is still running";
|
|
43103
|
-
const sized = after.sizedToMs === undefined ? "" : halfAsLongAgain(after.sizedToMs) > after.budgetMs ? `, the longest a start waits unasked though its last
|
|
43156
|
+
const sized = after.sizedToMs === undefined ? "" : halfAsLongAgain(after.sizedToMs) > after.budgetMs ? `, the longest a start waits unasked though the slowest of its last boots took ${after.sizedToMs}ms to announce,` : `, half as long again as the ${after.sizedToMs}ms the slowest of its last boots took,`;
|
|
43104
43157
|
return {
|
|
43105
43158
|
listening: false,
|
|
43106
43159
|
mayYetAnnounce: after.running,
|
|
@@ -43526,8 +43579,8 @@ function profileAnswer(reading, limit) {
|
|
|
43526
43579
|
}
|
|
43527
43580
|
const table = profiledFunctions(totals, limit, reading.sourceOf);
|
|
43528
43581
|
const notes = [];
|
|
43529
|
-
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.";
|
|
43530
|
-
const totalsWithheld = reading.runtimeAddon ? " The runtime addon switches the profiler off as the game quits, and did not here: either it was no longer running in this game by then, which a project can arrange for some scenes; or the game ended without taking its tree down, as a crash or a kill does; or the totals did not reach the debugger before the game exited, which the game then says in its output." : addonNote;
|
|
43582
|
+
const addonNote = reading.runtimeAddon === true ? "" : reading.runtimeAddon === null ? " The run's project is no longer at its path, so whether it had the gdharness runtime addon, which switches the profiler off as the game quits so the engine sends its totals, cannot be read." : " 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.";
|
|
43583
|
+
const totalsWithheld = reading.runtimeAddon === true ? " The runtime addon switches the profiler off as the game quits, and did not here: either it was no longer running in this game by then, which a project can arrange for some scenes; or the game ended without taking its tree down, as a crash or a kill does; or the totals did not reach the debugger before the game exited, which the game then says in its output." : addonNote;
|
|
43531
43584
|
if (totals.complete && reading.running) {
|
|
43532
43585
|
notes.push(`The engine's totals arrived while the game is still running, which it sends only when the profiler is switched off, so ${reading.through === "editor" ? "the editor's Profiler tab or the game itself, through EngineDebugger," : "the game itself, through EngineDebugger,"} switched it off before the game ended. They cover the run up to then and nothing after.`);
|
|
43533
43586
|
} else if (totals.complete) {} else if (reading.running && totals.frames === 0) {
|
|
@@ -43543,7 +43596,11 @@ function profileAnswer(reading, limit) {
|
|
|
43543
43596
|
notes.push(`${table.superCallers === 1 ? "One function calls" : `${table.superCallers} functions call`} through super, marked selfIncludesSuper: the engine takes every other call out of the caller's self time, and not a super call, so ${table.superCallers === 1 ? "its" : "their"} selfMs holds the overridden function's time, which that function's own row counts again. Self time therefore cannot be added along an override chain, and scriptMs counts the time under a super call once more for each level above it.`);
|
|
43544
43597
|
}
|
|
43545
43598
|
if (table.overheadDominated > 0 && table.perCallUs !== null) {
|
|
43546
|
-
|
|
43599
|
+
const one = table.overheadDominated === 1;
|
|
43600
|
+
notes.push(`The profiler costs about ${table.perCallUs}µs a call in this run, measured on an empty function the runtime addon calls as it goes on, and that time lands in the self time of the function called. ${one ? "One function has" : `${table.overheadDominated} functions have`} at least half ${one ? "its" : "their"} selfMs in it: ${namedFew(table.overheadNamed)}.${overheadRowsReturned(table, one)} That much of ${one ? "its" : "their"} time is the profiler's and goes once the profiler is off, whether the function is inlined or not. What inlining saves is the call itself, which costs something without the profiler too, not the work inside it.`);
|
|
43601
|
+
}
|
|
43602
|
+
if (reading.coveredMs > 0 && table.scriptMs > reading.coveredMs) {
|
|
43603
|
+
notes.push(`scriptMs, ${table.scriptMs}ms, is more than the ${reading.coveredMs}ms the profile covers.${table.superCallers > 0 ? " The super calls above, counted twice, can account for some or all of that; whatever they do not is" : " One thread cannot spend more time than passes, so that is"} functions running on several threads at once, through WorkerThreadPool or Thread. The engine adds every thread's time to the same function and does not say which thread a call ran on, so a function's selfMs is summed over every thread it ran on rather than taken out of frames, and a function that waits for the others, through wait_for_group_task_completion or wait_to_finish, has the wait in its own selfMs as well.`);
|
|
43547
43604
|
}
|
|
43548
43605
|
notes.push(...cappedNotes(totals));
|
|
43549
43606
|
if (totals.unreadable > 0) {
|
|
@@ -43565,6 +43622,19 @@ function profileAnswer(reading, limit) {
|
|
|
43565
43622
|
note: notes.length > 0 ? notes.join(" ") : undefined
|
|
43566
43623
|
};
|
|
43567
43624
|
}
|
|
43625
|
+
function overheadRowsReturned(table, one) {
|
|
43626
|
+
const shown = table.functions.filter((row) => row.overheadMs !== undefined).length;
|
|
43627
|
+
const returned = table.functions.length;
|
|
43628
|
+
if (shown === table.overheadDominated) {
|
|
43629
|
+
return one ? " Its row carries that share as overheadMs." : " Their rows carry that share as overheadMs.";
|
|
43630
|
+
}
|
|
43631
|
+
const theReturned = returned === 1 ? "the one row returned" : `the ${returned} rows returned`;
|
|
43632
|
+
if (shown === 0) {
|
|
43633
|
+
return one ? ` Its row, which carries that share as overheadMs, comes after ${theReturned}; limit ${table.overheadReach} returns it.` : ` Their rows, which carry that share as overheadMs, come after ${theReturned}; limit ${table.overheadReach} returns them all.`;
|
|
43634
|
+
}
|
|
43635
|
+
const among = returned === 1 ? "the one row returned is one of them" : `${shown} of them ${shown === 1 ? "is" : "are"} among ${theReturned}`;
|
|
43636
|
+
return ` Their rows carry that share as overheadMs, and ${among}; limit ${table.overheadReach} returns them all.`;
|
|
43637
|
+
}
|
|
43568
43638
|
function namedFew(names) {
|
|
43569
43639
|
const shown = names.slice(0, 5);
|
|
43570
43640
|
const more = names.length - shown.length;
|
|
@@ -43584,20 +43654,38 @@ function cappedNotes(totals) {
|
|
|
43584
43654
|
const capped = totals.cappedFrames ?? 0;
|
|
43585
43655
|
notes.push(`${capped === totals.frames ? capped === 1 ? "The one frame" : `All ${capped} frames` : `${capped} of the ${totals.frames} frames`} carried the most functions the profiler asks for in one, ${most}, so the functions with the least time in ${capped === 1 ? "it" : "them"} are not counted there: ${why}`);
|
|
43586
43656
|
}
|
|
43587
|
-
const unnamed =
|
|
43657
|
+
const unnamed = unnamedCalled(totals);
|
|
43588
43658
|
if (unnamed > 0) {
|
|
43589
43659
|
notes.push(`${unnamed === 1 ? "One function" : `${unnamed} functions`} arrived without ${unnamed === 1 ? "its name" : "their names"}, shown as ${UNNAMED_PREFIX} and the number the engine gave ${unnamed === 1 ? "it" : "them"}: the engine names a function once, as it first appears, and the game's debugger queue was full when that message was sent. They are script functions of the game; which ones cannot be recovered from this profile.`);
|
|
43590
43660
|
}
|
|
43591
43661
|
return notes;
|
|
43592
43662
|
}
|
|
43663
|
+
function unnamedCalled(totals) {
|
|
43664
|
+
return Object.entries(totals.functions).filter(([name, sum]) => name.startsWith(UNNAMED_PREFIX) && sum.calls > 0).length;
|
|
43665
|
+
}
|
|
43666
|
+
function uncountedCallee(totals, name) {
|
|
43667
|
+
const others = [];
|
|
43668
|
+
const most = totals.frameFunctions;
|
|
43669
|
+
const capped = totals.complete ? totals.totalCapped === true : (totals.cappedFrames ?? 0) > 0;
|
|
43670
|
+
if (most !== undefined && capped) {
|
|
43671
|
+
others.push(`its calls took too little time to be among the ${most} functions ${totals.complete ? "the engine's totals" : "a frame"} carried`);
|
|
43672
|
+
}
|
|
43673
|
+
const unnamed = unnamedCalled(totals);
|
|
43674
|
+
if (unnamed > 0) {
|
|
43675
|
+
others.push(`it is ${unnamed === 1 ? "the function" : `one of the ${unnamed} functions`} whose name never arrived`);
|
|
43676
|
+
}
|
|
43677
|
+
const ranNot = "none of these places ran it while the profiler was on: withinCalls says the function a place is in ran, not that the line did";
|
|
43678
|
+
return others.length === 0 ? `The profile counts no call of ${name}, so ${ranNot}.` : `The profile counts no call of ${name}: either ${ranNot}, or ${others.join(", or ")}.`;
|
|
43679
|
+
}
|
|
43593
43680
|
function callersAnswer(totals, asked) {
|
|
43594
43681
|
const found = callersOf(totals, asked.name, asked.scripts());
|
|
43682
|
+
const byName = found.target.length === 0 ? `${CALLERS_FOUND_BY_NAME} ${uncountedCallee(totals, asked.name)}` : CALLERS_FOUND_BY_NAME;
|
|
43595
43683
|
return {
|
|
43596
43684
|
of: asked.name,
|
|
43597
43685
|
rows: found.target,
|
|
43598
43686
|
sites: found.sites,
|
|
43599
43687
|
omitted: found.omitted > 0 ? found.omitted : undefined,
|
|
43600
|
-
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.
|
|
43688
|
+
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.property ? `${byName} ${PROPERTY_PLACES}` : byName
|
|
43601
43689
|
};
|
|
43602
43690
|
}
|
|
43603
43691
|
function foldedEntriesAnswer(log) {
|
|
@@ -46408,7 +46496,7 @@ class GodotServer {
|
|
|
46408
46496
|
platform: process.platform,
|
|
46409
46497
|
variables: process.env
|
|
46410
46498
|
});
|
|
46411
|
-
const runtimeWaitMs = this.announceBudget(args, project.value.path);
|
|
46499
|
+
const runtimeWaitMs = this.announceBudget(args, project.value.path, sceneArgument);
|
|
46412
46500
|
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
46413
46501
|
const silent = resolveSilent(args["silent"], process.env) && !headless;
|
|
46414
46502
|
const profiled = readBoolean(args, "profile") === true;
|
|
@@ -46430,6 +46518,7 @@ class GodotServer {
|
|
|
46430
46518
|
]);
|
|
46431
46519
|
}
|
|
46432
46520
|
started.announcedBefore = alreadyPlaying;
|
|
46521
|
+
started.bootScene = runtimeWaitMs.scene;
|
|
46433
46522
|
this.activeProcess = started;
|
|
46434
46523
|
const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}: the debug_* tools answer only for a game the editor is playing, and silent: false plays it through the editor.` : "";
|
|
46435
46524
|
const endedForThis = endedToStartThis(ended);
|
|
@@ -46519,17 +46608,18 @@ class GodotServer {
|
|
|
46519
46608
|
...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {}
|
|
46520
46609
|
});
|
|
46521
46610
|
}
|
|
46522
|
-
announceBudget(args, projectPath) {
|
|
46611
|
+
announceBudget(args, projectPath, scene) {
|
|
46612
|
+
const key = scene ?? MAIN_SCENE;
|
|
46523
46613
|
const asked = readNonNegativeNumber(args, "runtimeWaitMs");
|
|
46524
46614
|
if (asked !== undefined) {
|
|
46525
|
-
return { ms: asked };
|
|
46615
|
+
return { ms: asked, scene: key };
|
|
46526
46616
|
}
|
|
46527
|
-
const lastBoot = readBootNote(projectPath);
|
|
46617
|
+
const lastBoot = readBootNote(projectPath, key);
|
|
46528
46618
|
const ms = waitSizedTo(lastBoot);
|
|
46529
|
-
return lastBoot !== null && ms > ANNOUNCE_BUDGET_MS ? { ms, sizedToMs: lastBoot } : { ms };
|
|
46619
|
+
return lastBoot !== null && ms > ANNOUNCE_BUDGET_MS ? { ms, sizedToMs: lastBoot, scene: key } : { ms, scene: key };
|
|
46530
46620
|
}
|
|
46531
46621
|
noteBootOf(run, endpoint) {
|
|
46532
|
-
if (run.projectPath === null) {
|
|
46622
|
+
if (run.projectPath === null || run.bootScene === undefined) {
|
|
46533
46623
|
return;
|
|
46534
46624
|
}
|
|
46535
46625
|
let announcedAt;
|
|
@@ -46541,7 +46631,7 @@ class GodotServer {
|
|
|
46541
46631
|
const took = announcedAt - run.startedAt;
|
|
46542
46632
|
this.logDebug(`boot of pid ${endpoint.pid} announced ${Math.round(took)}ms after the start of ${run.projectPath}`);
|
|
46543
46633
|
if (took > 0 && took <= LONGEST_BOOT_NOTED_MS) {
|
|
46544
|
-
writeBootNote(run.projectPath, took);
|
|
46634
|
+
writeBootNote(run.projectPath, run.bootScene, took);
|
|
46545
46635
|
}
|
|
46546
46636
|
}
|
|
46547
46637
|
async playThroughEditor(scene, refreshedClasses, scanned, projectPath, alreadyPlaying, runtimeWaitMs, ended, profiled) {
|
|
@@ -46600,7 +46690,8 @@ class GodotServer {
|
|
|
46600
46690
|
brokeOn: null,
|
|
46601
46691
|
announcedBefore: alreadyPlaying,
|
|
46602
46692
|
seenPlaying: readBoolean(playAnswer, "playing") === true,
|
|
46603
|
-
...which === null ? {} : { play: which }
|
|
46693
|
+
...which === null ? {} : { play: which },
|
|
46694
|
+
bootScene: runtimeWaitMs.scene
|
|
46604
46695
|
};
|
|
46605
46696
|
this.activeProcess = played;
|
|
46606
46697
|
this.noteTheLatestRun({ projectPath, pid: null, startedAt });
|
|
@@ -46770,6 +46861,7 @@ class GodotServer {
|
|
|
46770
46861
|
}
|
|
46771
46862
|
running.endedHere = reason;
|
|
46772
46863
|
const announced = this.announcedPidOf(running);
|
|
46864
|
+
await this.sendTheProfileFirst(running, announced);
|
|
46773
46865
|
if (running.throughEditor) {
|
|
46774
46866
|
const asked = await this.handleViaBridge("stop_playing", {});
|
|
46775
46867
|
if (asked.isError === true) {
|
|
@@ -46840,6 +46932,25 @@ class GodotServer {
|
|
|
46840
46932
|
running.log.record(gone ? "info" : "warning", declined !== null ? `gdharness could not signal pid ${running.pid}: the operating system refused it (${declined}). It had described that process as ${described}.` : !gone ? `gdharness signalled pid ${running.pid}, which the operating system described as ${described}, and it was still running ${stopWaitMs() / 1000} seconds later.` : landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
|
|
46841
46933
|
return gone ? "gone" : "lingering";
|
|
46842
46934
|
}
|
|
46935
|
+
async sendTheProfileFirst(run, announced) {
|
|
46936
|
+
if (announced === undefined) {
|
|
46937
|
+
return;
|
|
46938
|
+
}
|
|
46939
|
+
const profiled = run.throughEditor ? await this.theEditorIsProfiling() : run.profile !== undefined;
|
|
46940
|
+
const endpoint = profiled ? runtimesAnnounced().running.find((one) => one.pid === announced) : undefined;
|
|
46941
|
+
if (endpoint === undefined) {
|
|
46942
|
+
return;
|
|
46943
|
+
}
|
|
46944
|
+
await runtimeRequest(endpoint, "send_profile", {}, PROFILE_SEND_MS);
|
|
46945
|
+
}
|
|
46946
|
+
async theEditorIsProfiling() {
|
|
46947
|
+
const read = await this.handleViaBridge("profile_read", {});
|
|
46948
|
+
if (read.isError === true) {
|
|
46949
|
+
return false;
|
|
46950
|
+
}
|
|
46951
|
+
const held = asParams(JSON.parse(read.content[0]?.text ?? "{}"));
|
|
46952
|
+
return readBoolean(held, "profiled") === true && readBoolean(held, "complete") !== true;
|
|
46953
|
+
}
|
|
46843
46954
|
async endThroughTheKeeper(run, pid, keeper) {
|
|
46844
46955
|
const asked = await askTheKeeperToStop(keeper, KEEPER_ANSWER_MS);
|
|
46845
46956
|
if (typeof asked !== "string") {
|
|
@@ -46940,6 +47051,7 @@ class GodotServer {
|
|
|
46940
47051
|
const transcript = openTranscript(startedAt);
|
|
46941
47052
|
closeSync2(transcript.fd);
|
|
46942
47053
|
const gameEnv = profiled ? { ...env ?? process.env, [PROFILE_FROM_START]: "1" } : env;
|
|
47054
|
+
const profile = profiled ? profilePath(startedAt) : undefined;
|
|
46943
47055
|
const launched = await launchOutsideTheTree({
|
|
46944
47056
|
command: godotPath,
|
|
46945
47057
|
args: cmdArgs,
|
|
@@ -46949,7 +47061,7 @@ class GodotServer {
|
|
|
46949
47061
|
startedAt,
|
|
46950
47062
|
projectPath,
|
|
46951
47063
|
...this.servedBy(),
|
|
46952
|
-
...
|
|
47064
|
+
...profile === undefined ? {} : { profile }
|
|
46953
47065
|
},
|
|
46954
47066
|
...desktop === undefined ? {} : { desktop }
|
|
46955
47067
|
});
|
|
@@ -46970,6 +47082,7 @@ class GodotServer {
|
|
|
46970
47082
|
startedAt,
|
|
46971
47083
|
startedBy,
|
|
46972
47084
|
...keeper === undefined ? {} : { keeper },
|
|
47085
|
+
...profile === undefined ? {} : { profile },
|
|
46973
47086
|
command: godotPath,
|
|
46974
47087
|
exitCode: null,
|
|
46975
47088
|
exitSignal: null,
|
|
@@ -47210,6 +47323,7 @@ class GodotServer {
|
|
|
47210
47323
|
startedAt: record.startedAt,
|
|
47211
47324
|
...record.startedBy === undefined ? {} : { startedBy: record.startedBy },
|
|
47212
47325
|
...record.keeper === undefined ? {} : { keeper: record.keeper },
|
|
47326
|
+
...record.profile === undefined ? {} : { profile: record.profile },
|
|
47213
47327
|
...record.command === undefined ? {} : { command: record.command },
|
|
47214
47328
|
exitCode: null,
|
|
47215
47329
|
exitSignal: null,
|
|
@@ -47362,12 +47476,16 @@ class GodotServer {
|
|
|
47362
47476
|
}
|
|
47363
47477
|
const limit = readPositiveNumber(args, "limit") ?? PROFILE_FUNCTIONS;
|
|
47364
47478
|
const callersName = readString(args, "callersOf");
|
|
47365
|
-
if (callersName !== undefined && !callersName.split(".").every(isIdentifier)) {
|
|
47479
|
+
if (callersName !== undefined && !callersName.replace(/@([^.@]+)_(?:getter|setter)$/, "$1").split(".").every(isIdentifier)) {
|
|
47366
47480
|
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"]);
|
|
47367
47481
|
}
|
|
47368
47482
|
const going = await this.runStillGoing(run);
|
|
47369
47483
|
const projectPath = run.projectPath;
|
|
47370
|
-
const
|
|
47484
|
+
const projectGone = projectPath !== null && !existsSync20(join26(projectPath, "project.godot"));
|
|
47485
|
+
if (projectGone && callersName !== undefined) {
|
|
47486
|
+
return this.createErrorResponse(`callersOf reads the project's scripts, and the run's project at ${projectPath} has no project.godot any more.`, ["editor_run profile without callersOf answers with the profile itself"]);
|
|
47487
|
+
}
|
|
47488
|
+
const fromProject = projectPath === null ? { runtimeAddon: false } : projectGone ? { runtimeAddon: null } : {
|
|
47371
47489
|
runtimeAddon: runtimeAddonRegistered(projectPath),
|
|
47372
47490
|
sourceOf: projectSourceOf(projectPath),
|
|
47373
47491
|
...callersName === undefined ? {} : { callersOf: { name: callersName, scripts: () => projectScripts(projectPath) } }
|
|
@@ -47375,8 +47493,7 @@ class GodotServer {
|
|
|
47375
47493
|
if (run.throughEditor) {
|
|
47376
47494
|
return await this.profileOfThePlayedGame(going, projectPath, fromProject, limit);
|
|
47377
47495
|
}
|
|
47378
|
-
const
|
|
47379
|
-
const path = record !== null && record.pid === run.pid ? record.profile : undefined;
|
|
47496
|
+
const path = run.profile;
|
|
47380
47497
|
if (path === undefined) {
|
|
47381
47498
|
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"]);
|
|
47382
47499
|
}
|
|
@@ -48509,7 +48626,7 @@ class GodotServer {
|
|
|
48509
48626
|
return null;
|
|
48510
48627
|
}
|
|
48511
48628
|
const which = run.throughEditor ? "the game the editor is playing for it" : `pid ${run.pid ?? "unknown"}`;
|
|
48512
|
-
const comingUpUntil = run.startedAt + waitSizedTo(readBootNote(project)) + budgetMs;
|
|
48629
|
+
const comingUpUntil = run.startedAt + waitSizedTo(readBootNote(project, run.bootScene ?? MAIN_SCENE)) + budgetMs;
|
|
48513
48630
|
const remaining = comingUpUntil - Date.now();
|
|
48514
48631
|
if (remaining <= 0) {
|
|
48515
48632
|
const seconds = Math.round((Date.now() - run.startedAt) / 1000);
|
|
@@ -48727,7 +48844,7 @@ async function runGodotServer() {
|
|
|
48727
48844
|
const server = new GodotServer;
|
|
48728
48845
|
await server.run();
|
|
48729
48846
|
}
|
|
48730
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "';
|
|
48847
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "';
|
|
48731
48848
|
var init_server3 = __esm(() => {
|
|
48732
48849
|
init_mcp();
|
|
48733
48850
|
init_stdio2();
|
|
@@ -140,6 +140,7 @@ func _init() -> void:
|
|
|
140
140
|
"wait_frames": _waits.wait_frames,
|
|
141
141
|
"wait_signal": _waits.wait_signal,
|
|
142
142
|
"wait_until": _waits.wait_until,
|
|
143
|
+
"send_profile": _send_profile_now,
|
|
143
144
|
}
|
|
144
145
|
_wait_for_the_profiler()
|
|
145
146
|
|
|
@@ -236,6 +237,16 @@ func _send_the_profile() -> void:
|
|
|
236
237
|
)
|
|
237
238
|
|
|
238
239
|
|
|
240
|
+
## The totals sent now, for a stop that is about to end the game without letting it quit: ended by
|
|
241
|
+
## its process, a game takes the totals with it, and every stopped profile came back as the frames
|
|
242
|
+
## summed. Nothing of the game's own runs, which quitting the tree would have done.
|
|
243
|
+
func _send_profile_now(_params: Dictionary) -> Dictionary:
|
|
244
|
+
if not (EngineDebugger.is_active() and EngineDebugger.is_profiling("servers")):
|
|
245
|
+
return {"sent": false}
|
|
246
|
+
_send_the_profile()
|
|
247
|
+
return {"sent": true, "confirmed": _profile_received}
|
|
248
|
+
|
|
249
|
+
|
|
239
250
|
## What the script profiler costs per call in this game, measured once each time it is switched on.
|
|
240
251
|
##
|
|
241
252
|
## The profiler times every call it counts, and that bookkeeping lands in the self time of the
|
package/build/index.js
CHANGED
|
@@ -26036,19 +26036,35 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
|
|
|
26036
26036
|
function bootNotePath(projectPath) {
|
|
26037
26037
|
return join3(projectPath, ".godot", "gdharness-boot.json");
|
|
26038
26038
|
}
|
|
26039
|
-
|
|
26039
|
+
var MAIN_SCENE = "";
|
|
26040
|
+
function readScenes(projectPath) {
|
|
26040
26041
|
try {
|
|
26041
|
-
|
|
26042
|
-
return noted !== undefined && Number.isFinite(noted) && noted > 0 ? noted : null;
|
|
26042
|
+
return asParams(asParams(JSON.parse(readFileSync4(bootNotePath(projectPath), "utf8")))["scenes"]);
|
|
26043
26043
|
} catch {
|
|
26044
|
-
return
|
|
26044
|
+
return {};
|
|
26045
26045
|
}
|
|
26046
26046
|
}
|
|
26047
|
-
|
|
26047
|
+
var BOOTS_KEPT = 5;
|
|
26048
|
+
function bootsOf(projectPath, scene) {
|
|
26049
|
+
const kept = asParams(readScenes(projectPath)[scene])["boots"];
|
|
26050
|
+
return Array.isArray(kept) ? kept.filter((one) => typeof one === "number" && Number.isFinite(one) && one > 0) : [];
|
|
26051
|
+
}
|
|
26052
|
+
function readBootNote(projectPath, scene) {
|
|
26053
|
+
const boots = bootsOf(projectPath, scene);
|
|
26054
|
+
return boots.length === 0 ? null : Math.max(...boots);
|
|
26055
|
+
}
|
|
26056
|
+
function writeBootNote(projectPath, scene, announcedAfterMs) {
|
|
26048
26057
|
try {
|
|
26049
26058
|
const path = bootNotePath(projectPath);
|
|
26050
26059
|
mkdirSync2(dirname2(path), { recursive: true });
|
|
26051
|
-
|
|
26060
|
+
const scenes = {
|
|
26061
|
+
...readScenes(projectPath),
|
|
26062
|
+
[scene]: {
|
|
26063
|
+
boots: [...bootsOf(projectPath, scene), Math.round(announcedAfterMs)].slice(-BOOTS_KEPT),
|
|
26064
|
+
at: new Date().toISOString()
|
|
26065
|
+
}
|
|
26066
|
+
};
|
|
26067
|
+
writeFileSync2(path, JSON.stringify({ scenes }, null, 2), "utf8");
|
|
26052
26068
|
} catch {}
|
|
26053
26069
|
}
|
|
26054
26070
|
var LONGEST_SIZED_WAIT_MS = 60000;
|
|
@@ -26613,6 +26629,7 @@ function bodyCallsSuper(code, line, lambda) {
|
|
|
26613
26629
|
}
|
|
26614
26630
|
return false;
|
|
26615
26631
|
}
|
|
26632
|
+
var ASSIGNED = /^(?:\*\*|<<|>>|[-+*/%&|^])?=(?!=)/;
|
|
26616
26633
|
function parametersOf(text, from) {
|
|
26617
26634
|
const open = text.indexOf("(", from);
|
|
26618
26635
|
if (open === -1) {
|
|
@@ -26720,7 +26737,7 @@ function bodyHolding(body, offset) {
|
|
|
26720
26737
|
const inner = body.inner.find((one) => one.start <= offset && offset <= one.end);
|
|
26721
26738
|
return inner === undefined ? body : bodyHolding(inner, offset);
|
|
26722
26739
|
}
|
|
26723
|
-
function callSitesOf(source, name, otherClasses = new Set) {
|
|
26740
|
+
function callSitesOf(source, name, otherClasses = new Set, property = false) {
|
|
26724
26741
|
const regions = regionsOf(source);
|
|
26725
26742
|
const code = codeOf(source, regions);
|
|
26726
26743
|
const codeLines = code.split(`
|
|
@@ -26731,8 +26748,31 @@ function callSitesOf(source, name, otherClasses = new Set) {
|
|
|
26731
26748
|
const declared = bodyHolding(shape.body, at).declarations.find((one) => one.name === member && (one.kind === "var" || one.kind === "const"));
|
|
26732
26749
|
return declared === undefined ? null : declaredClass(codeLines[lines.positionOf(declared.offset).line] ?? "", member);
|
|
26733
26750
|
};
|
|
26751
|
+
const blockUnder = (declared) => {
|
|
26752
|
+
const indent = (codeLines[declared] ?? "").search(/\S|$/);
|
|
26753
|
+
let last = declared;
|
|
26754
|
+
for (let index = declared + 1;index < codeLines.length; index += 1) {
|
|
26755
|
+
const text = codeLines[index] ?? "";
|
|
26756
|
+
if (text.trim() === "") {
|
|
26757
|
+
continue;
|
|
26758
|
+
}
|
|
26759
|
+
if (text.search(/\S/) <= indent) {
|
|
26760
|
+
break;
|
|
26761
|
+
}
|
|
26762
|
+
last = index;
|
|
26763
|
+
}
|
|
26764
|
+
return [declared, last];
|
|
26765
|
+
};
|
|
26766
|
+
const accessorsIn = (body) => [
|
|
26767
|
+
...body.declarations.filter((one) => one.kind === "var" && one.name === name).map((one) => blockUnder(lines.positionOf(one.offset).line)),
|
|
26768
|
+
...body.inner.flatMap(accessorsIn)
|
|
26769
|
+
];
|
|
26770
|
+
const accessors = property ? accessorsIn(shape.body) : [];
|
|
26734
26771
|
return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).filter((one) => {
|
|
26735
26772
|
const index = lines.positionOf(one.offset).line;
|
|
26773
|
+
if (!one.afterDot && accessors.some(([declared, last]) => index > declared && index <= last)) {
|
|
26774
|
+
return false;
|
|
26775
|
+
}
|
|
26736
26776
|
if (one.afterDot) {
|
|
26737
26777
|
const receiver = receiverBefore(code, one.offset);
|
|
26738
26778
|
if (receiver === null || otherClasses.size === 0) {
|
|
@@ -26750,11 +26790,12 @@ function callSitesOf(source, name, otherClasses = new Set) {
|
|
|
26750
26790
|
if (localAt(codeLines, index, name) !== null) {
|
|
26751
26791
|
return false;
|
|
26752
26792
|
}
|
|
26753
|
-
return !bodyHolding(shape.body, one.offset).declarations.some((declared) => declared.name === name && MEMBER_KINDS.has(declared.kind));
|
|
26793
|
+
return !bodyHolding(shape.body, one.offset).declarations.some((declared) => declared.name === name && MEMBER_KINDS.has(declared.kind) && !(property && declared.kind === "var"));
|
|
26754
26794
|
}).map((one) => {
|
|
26755
26795
|
const line = lines.positionOf(one.offset).line + 1;
|
|
26756
26796
|
const after = code.slice(one.offset + name.length).trimStart();
|
|
26757
|
-
|
|
26797
|
+
const within = enclosingFunction(source, line, code);
|
|
26798
|
+
return property ? { line, within, access: ASSIGNED.test(after) ? "write" : "read" } : { line, within, called: after.startsWith("(") };
|
|
26758
26799
|
});
|
|
26759
26800
|
}
|
|
26760
26801
|
function collectEnumValues(text, from, into) {
|
|
@@ -33394,6 +33435,9 @@ function queuedMessagesOf(projectGodot) {
|
|
|
33394
33435
|
}
|
|
33395
33436
|
var UNNAMED_PREFIX = "#";
|
|
33396
33437
|
var PROFILE_FROM_START = "GDHARNESS_PROFILE_FROM_START";
|
|
33438
|
+
function propertyOf(fn) {
|
|
33439
|
+
return /^@(.+)_(?:getter|setter)$/.exec(bareName(fn))?.[1] ?? null;
|
|
33440
|
+
}
|
|
33397
33441
|
function signatureParts(signature) {
|
|
33398
33442
|
const parts = signature.split("::");
|
|
33399
33443
|
if (parts.length < 3) {
|
|
@@ -33416,7 +33460,8 @@ function profiledFunctions(totals, limit, sourceOf = () => null) {
|
|
|
33416
33460
|
superCallers: rows.filter((row) => row.selfIncludesSuper === true).length,
|
|
33417
33461
|
perCallUs,
|
|
33418
33462
|
overheadDominated: rows.filter((row) => row.overheadMs !== undefined).length,
|
|
33419
|
-
overheadNamed: rows.filter((row) => row.overheadMs !== undefined).map((row) => row.function)
|
|
33463
|
+
overheadNamed: rows.filter((row) => row.overheadMs !== undefined).map((row) => row.function),
|
|
33464
|
+
overheadReach: rows.findLastIndex((row) => row.overheadMs !== undefined) + 1
|
|
33420
33465
|
};
|
|
33421
33466
|
}
|
|
33422
33467
|
var RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd";
|
|
@@ -33454,6 +33499,7 @@ function rowsOf(totals, sourceOf) {
|
|
|
33454
33499
|
return Object.entries(totals.functions).filter(([signature, sum]) => sum.calls > 0 && !isCalibration(signature)).map(([signature, sum]) => {
|
|
33455
33500
|
const parts = signatureParts(signature);
|
|
33456
33501
|
const code = codeOfScript(parts.script);
|
|
33502
|
+
const property = propertyOf(parts.function);
|
|
33457
33503
|
return {
|
|
33458
33504
|
script: parts.script,
|
|
33459
33505
|
function: parts.function,
|
|
@@ -33461,7 +33507,8 @@ function rowsOf(totals, sourceOf) {
|
|
|
33461
33507
|
calls: sum.calls,
|
|
33462
33508
|
selfMs: ms(sum.selfSeconds),
|
|
33463
33509
|
totalMs: ms(sum.totalSeconds),
|
|
33464
|
-
...code !== null && bodyCallsSuper(code, parts.line, parts.function.endsWith("(lambda)")) ? { selfIncludesSuper: true } : {}
|
|
33510
|
+
...code !== null && bodyCallsSuper(code, parts.line, parts.function.endsWith("(lambda)")) ? { selfIncludesSuper: true } : {},
|
|
33511
|
+
...property === null ? {} : { property }
|
|
33465
33512
|
};
|
|
33466
33513
|
}).sort((a, b) => b.selfMs - a.selfMs || b.totalMs - a.totalMs || a.function.localeCompare(b.function));
|
|
33467
33514
|
}
|
|
@@ -33485,10 +33532,13 @@ var CALL_SITES = 40;
|
|
|
33485
33532
|
function callersOf(totals, name, scripts) {
|
|
33486
33533
|
const listed = [...scripts];
|
|
33487
33534
|
const sources = new Map(listed);
|
|
33488
|
-
const
|
|
33489
|
-
const
|
|
33490
|
-
const target = rows.filter((row) => name.includes(".") ? row.function === name : bareName(row.function) === bare);
|
|
33535
|
+
const perCallUs = perCallCostOf(totals);
|
|
33536
|
+
const rows = rowsOf(totals, (script) => sources.get(script) ?? null).map((row) => withOverhead(row, perCallUs));
|
|
33491
33537
|
const asked = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : null;
|
|
33538
|
+
const bare = propertyOf(name) ?? bareName(name);
|
|
33539
|
+
const ownerOf = (fn) => fn.includes(".") ? fn.slice(0, fn.lastIndexOf(".")) : null;
|
|
33540
|
+
const target = rows.filter((row) => (bareName(row.function) === bare || row.property === bare) && (asked === null || ownerOf(row.function) === asked));
|
|
33541
|
+
const property = propertyOf(name) !== null || target.some((row) => row.property === bare);
|
|
33492
33542
|
const otherClasses = new Set;
|
|
33493
33543
|
if (asked !== null) {
|
|
33494
33544
|
for (const [, source] of listed) {
|
|
@@ -33501,7 +33551,7 @@ function callersOf(totals, name, scripts) {
|
|
|
33501
33551
|
}
|
|
33502
33552
|
const sites = [];
|
|
33503
33553
|
for (const [script, source] of listed) {
|
|
33504
|
-
for (const site of callSitesOf(source, bare, otherClasses)) {
|
|
33554
|
+
for (const site of callSitesOf(source, bare, otherClasses, property)) {
|
|
33505
33555
|
const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
|
|
33506
33556
|
sites.push({
|
|
33507
33557
|
script,
|
|
@@ -33516,7 +33566,12 @@ function callersOf(totals, name, scripts) {
|
|
|
33516
33566
|
}
|
|
33517
33567
|
}
|
|
33518
33568
|
sites.sort((a, b) => (b.withinTotalMs ?? -1) - (a.withinTotalMs ?? -1) || a.script.localeCompare(b.script) || a.line - b.line);
|
|
33519
|
-
return {
|
|
33569
|
+
return {
|
|
33570
|
+
target,
|
|
33571
|
+
sites: sites.slice(0, CALL_SITES),
|
|
33572
|
+
omitted: Math.max(0, sites.length - CALL_SITES),
|
|
33573
|
+
property
|
|
33574
|
+
};
|
|
33520
33575
|
}
|
|
33521
33576
|
|
|
33522
33577
|
// src/setup.ts
|
|
@@ -34484,7 +34539,7 @@ var TOOL_SPECS = [
|
|
|
34484
34539
|
type: "integer",
|
|
34485
34540
|
minimum: 0,
|
|
34486
34541
|
ops: ["start"],
|
|
34487
|
-
description: "start: how long to wait for the game to announce its runtime before answering. Default 5000, or half as long again as the last
|
|
34542
|
+
description: "start: how long to wait for the game to announce its runtime before answering. Default 5000, or half as long again as the slowest of the last five boots of the same scene when that is more, up to 60000: the server notes how long each game took to announce, per scene and for the main scene when none is named, since a boot moves with the load on the machine, so a scene that boots slowly is waited for from its second start on without being asked to, and a harness scene that announces late sets nothing for the main scene. What it waits for is the first frame, so everything the game does before drawing one is inside it, including work the args just asked for: a flag that simulates six years of game time before anything is drawn makes the announcement that late, on a project that announces promptly without it. Raise it for such a run rather than reading runtime listening false as a fault. 0 does not wait at all, which is the one to pass for a scene that announces nothing by construction, such as a bench that prints and quits."
|
|
34488
34543
|
},
|
|
34489
34544
|
frames: {
|
|
34490
34545
|
type: "integer",
|
|
@@ -34506,7 +34561,7 @@ var TOOL_SPECS = [
|
|
|
34506
34561
|
callersOf: {
|
|
34507
34562
|
type: "string",
|
|
34508
34563
|
ops: ["profile"],
|
|
34509
|
-
description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too, except, when Class.name was asked, one called through another class of the project by name or through a parameter, local or class variable declared as one, and a call made through a string is not. A parameter, local or class variable of the same name is not the function and is not listed."
|
|
34564
|
+
description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too, except, when Class.name was asked, one called through another class of the project by name or through a parameter, local or class variable declared as one, and a call made through a string is not. A parameter, local or class variable of the same name is not the function and is not listed. A property's get or set block, which the profile names Class.@name_getter or Class.@name_setter, is asked about by that name or by the property's: the places are where the property is named, each saying instead of called whether it writes the property (access write), which runs the set block, or reads it (access read), which runs the get block, and the rows are both blocks'. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not places."
|
|
34510
34565
|
},
|
|
34511
34566
|
limit: {
|
|
34512
34567
|
type: "integer",
|
|
@@ -34542,7 +34597,7 @@ var TOOL_SPECS = [
|
|
|
34542
34597
|
},
|
|
34543
34598
|
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
|
|
34544
34599
|
profile: {
|
|
34545
|
-
summary: "where the run spends its time by GDScript function, while it runs or after it ended: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, sorted by self time, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. A run started with profile: true is profiled from its start. A game the editor plays that was not is profiled from the moment this is asked, and the answer says so; a run this server started without it is refused, since its debugger is fixed as it starts",
|
|
34600
|
+
summary: "where the run spends its time by GDScript function, while it runs or after it ended: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, sorted by self time, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. 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",
|
|
34546
34601
|
requires: []
|
|
34547
34602
|
}
|
|
34548
34603
|
},
|
|
@@ -35513,6 +35568,7 @@ function endedToStartThis(ended) {
|
|
|
35513
35568
|
return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
|
|
35514
35569
|
}
|
|
35515
35570
|
var LONGEST_BOOT_NOTED_MS = 5 * 60000;
|
|
35571
|
+
var PROFILE_SEND_MS = 5000;
|
|
35516
35572
|
var MOST_GROUPS = 20;
|
|
35517
35573
|
function runtimeVerdict(endpoint, after) {
|
|
35518
35574
|
if (endpoint !== null) {
|
|
@@ -35534,7 +35590,7 @@ function runtimeVerdict(endpoint, after) {
|
|
|
35534
35590
|
};
|
|
35535
35591
|
}
|
|
35536
35592
|
const state = after.playingSeen === false ? "the editor has not yet reported the game as playing, which a restarted editor does not until its scan is over" : "the game is still running";
|
|
35537
|
-
const sized = after.sizedToMs === undefined ? "" : halfAsLongAgain(after.sizedToMs) > after.budgetMs ? `, the longest a start waits unasked though its last
|
|
35593
|
+
const sized = after.sizedToMs === undefined ? "" : halfAsLongAgain(after.sizedToMs) > after.budgetMs ? `, the longest a start waits unasked though the slowest of its last boots took ${after.sizedToMs}ms to announce,` : `, half as long again as the ${after.sizedToMs}ms the slowest of its last boots took,`;
|
|
35538
35594
|
return {
|
|
35539
35595
|
listening: false,
|
|
35540
35596
|
mayYetAnnounce: after.running,
|
|
@@ -35984,6 +36040,7 @@ function playedCap(held) {
|
|
|
35984
36040
|
totalCapped: readBoolean(held, "totalCapped") === true
|
|
35985
36041
|
};
|
|
35986
36042
|
}
|
|
36043
|
+
var PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.";
|
|
35987
36044
|
var CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.";
|
|
35988
36045
|
function projectSourceOf(projectPath) {
|
|
35989
36046
|
const read = new Map;
|
|
@@ -36021,8 +36078,8 @@ function profileAnswer(reading, limit) {
|
|
|
36021
36078
|
}
|
|
36022
36079
|
const table = profiledFunctions(totals, limit, reading.sourceOf);
|
|
36023
36080
|
const notes = [];
|
|
36024
|
-
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.";
|
|
36025
|
-
const totalsWithheld = reading.runtimeAddon ? " The runtime addon switches the profiler off as the game quits, and did not here: either it was no longer running in this game by then, which a project can arrange for some scenes; or the game ended without taking its tree down, as a crash or a kill does; or the totals did not reach the debugger before the game exited, which the game then says in its output." : addonNote;
|
|
36081
|
+
const addonNote = reading.runtimeAddon === true ? "" : reading.runtimeAddon === null ? " The run's project is no longer at its path, so whether it had the gdharness runtime addon, which switches the profiler off as the game quits so the engine sends its totals, cannot be read." : " 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.";
|
|
36082
|
+
const totalsWithheld = reading.runtimeAddon === true ? " The runtime addon switches the profiler off as the game quits, and did not here: either it was no longer running in this game by then, which a project can arrange for some scenes; or the game ended without taking its tree down, as a crash or a kill does; or the totals did not reach the debugger before the game exited, which the game then says in its output." : addonNote;
|
|
36026
36083
|
if (totals.complete && reading.running) {
|
|
36027
36084
|
notes.push(`The engine's totals arrived while the game is still running, which it sends only when the profiler is switched off, so ${reading.through === "editor" ? "the editor's Profiler tab or the game itself, through EngineDebugger," : "the game itself, through EngineDebugger,"} switched it off before the game ended. They cover the run up to then and nothing after.`);
|
|
36028
36085
|
} else if (totals.complete) {} else if (reading.running && totals.frames === 0) {
|
|
@@ -36038,7 +36095,11 @@ function profileAnswer(reading, limit) {
|
|
|
36038
36095
|
notes.push(`${table.superCallers === 1 ? "One function calls" : `${table.superCallers} functions call`} through super, marked selfIncludesSuper: the engine takes every other call out of the caller's self time, and not a super call, so ${table.superCallers === 1 ? "its" : "their"} selfMs holds the overridden function's time, which that function's own row counts again. Self time therefore cannot be added along an override chain, and scriptMs counts the time under a super call once more for each level above it.`);
|
|
36039
36096
|
}
|
|
36040
36097
|
if (table.overheadDominated > 0 && table.perCallUs !== null) {
|
|
36041
|
-
|
|
36098
|
+
const one = table.overheadDominated === 1;
|
|
36099
|
+
notes.push(`The profiler costs about ${table.perCallUs}µs a call in this run, measured on an empty function the runtime addon calls as it goes on, and that time lands in the self time of the function called. ${one ? "One function has" : `${table.overheadDominated} functions have`} at least half ${one ? "its" : "their"} selfMs in it: ${namedFew(table.overheadNamed)}.${overheadRowsReturned(table, one)} That much of ${one ? "its" : "their"} time is the profiler's and goes once the profiler is off, whether the function is inlined or not. What inlining saves is the call itself, which costs something without the profiler too, not the work inside it.`);
|
|
36100
|
+
}
|
|
36101
|
+
if (reading.coveredMs > 0 && table.scriptMs > reading.coveredMs) {
|
|
36102
|
+
notes.push(`scriptMs, ${table.scriptMs}ms, is more than the ${reading.coveredMs}ms the profile covers.${table.superCallers > 0 ? " The super calls above, counted twice, can account for some or all of that; whatever they do not is" : " One thread cannot spend more time than passes, so that is"} functions running on several threads at once, through WorkerThreadPool or Thread. The engine adds every thread's time to the same function and does not say which thread a call ran on, so a function's selfMs is summed over every thread it ran on rather than taken out of frames, and a function that waits for the others, through wait_for_group_task_completion or wait_to_finish, has the wait in its own selfMs as well.`);
|
|
36042
36103
|
}
|
|
36043
36104
|
notes.push(...cappedNotes(totals));
|
|
36044
36105
|
if (totals.unreadable > 0) {
|
|
@@ -36060,6 +36121,19 @@ function profileAnswer(reading, limit) {
|
|
|
36060
36121
|
note: notes.length > 0 ? notes.join(" ") : undefined
|
|
36061
36122
|
};
|
|
36062
36123
|
}
|
|
36124
|
+
function overheadRowsReturned(table, one) {
|
|
36125
|
+
const shown = table.functions.filter((row) => row.overheadMs !== undefined).length;
|
|
36126
|
+
const returned = table.functions.length;
|
|
36127
|
+
if (shown === table.overheadDominated) {
|
|
36128
|
+
return one ? " Its row carries that share as overheadMs." : " Their rows carry that share as overheadMs.";
|
|
36129
|
+
}
|
|
36130
|
+
const theReturned = returned === 1 ? "the one row returned" : `the ${returned} rows returned`;
|
|
36131
|
+
if (shown === 0) {
|
|
36132
|
+
return one ? ` Its row, which carries that share as overheadMs, comes after ${theReturned}; limit ${table.overheadReach} returns it.` : ` Their rows, which carry that share as overheadMs, come after ${theReturned}; limit ${table.overheadReach} returns them all.`;
|
|
36133
|
+
}
|
|
36134
|
+
const among = returned === 1 ? "the one row returned is one of them" : `${shown} of them ${shown === 1 ? "is" : "are"} among ${theReturned}`;
|
|
36135
|
+
return ` Their rows carry that share as overheadMs, and ${among}; limit ${table.overheadReach} returns them all.`;
|
|
36136
|
+
}
|
|
36063
36137
|
function namedFew(names) {
|
|
36064
36138
|
const shown = names.slice(0, 5);
|
|
36065
36139
|
const more = names.length - shown.length;
|
|
@@ -36079,20 +36153,38 @@ function cappedNotes(totals) {
|
|
|
36079
36153
|
const capped = totals.cappedFrames ?? 0;
|
|
36080
36154
|
notes.push(`${capped === totals.frames ? capped === 1 ? "The one frame" : `All ${capped} frames` : `${capped} of the ${totals.frames} frames`} carried the most functions the profiler asks for in one, ${most}, so the functions with the least time in ${capped === 1 ? "it" : "them"} are not counted there: ${why}`);
|
|
36081
36155
|
}
|
|
36082
|
-
const unnamed =
|
|
36156
|
+
const unnamed = unnamedCalled(totals);
|
|
36083
36157
|
if (unnamed > 0) {
|
|
36084
36158
|
notes.push(`${unnamed === 1 ? "One function" : `${unnamed} functions`} arrived without ${unnamed === 1 ? "its name" : "their names"}, shown as ${UNNAMED_PREFIX} and the number the engine gave ${unnamed === 1 ? "it" : "them"}: the engine names a function once, as it first appears, and the game's debugger queue was full when that message was sent. They are script functions of the game; which ones cannot be recovered from this profile.`);
|
|
36085
36159
|
}
|
|
36086
36160
|
return notes;
|
|
36087
36161
|
}
|
|
36162
|
+
function unnamedCalled(totals) {
|
|
36163
|
+
return Object.entries(totals.functions).filter(([name, sum]) => name.startsWith(UNNAMED_PREFIX) && sum.calls > 0).length;
|
|
36164
|
+
}
|
|
36165
|
+
function uncountedCallee(totals, name) {
|
|
36166
|
+
const others = [];
|
|
36167
|
+
const most = totals.frameFunctions;
|
|
36168
|
+
const capped = totals.complete ? totals.totalCapped === true : (totals.cappedFrames ?? 0) > 0;
|
|
36169
|
+
if (most !== undefined && capped) {
|
|
36170
|
+
others.push(`its calls took too little time to be among the ${most} functions ${totals.complete ? "the engine's totals" : "a frame"} carried`);
|
|
36171
|
+
}
|
|
36172
|
+
const unnamed = unnamedCalled(totals);
|
|
36173
|
+
if (unnamed > 0) {
|
|
36174
|
+
others.push(`it is ${unnamed === 1 ? "the function" : `one of the ${unnamed} functions`} whose name never arrived`);
|
|
36175
|
+
}
|
|
36176
|
+
const ranNot = "none of these places ran it while the profiler was on: withinCalls says the function a place is in ran, not that the line did";
|
|
36177
|
+
return others.length === 0 ? `The profile counts no call of ${name}, so ${ranNot}.` : `The profile counts no call of ${name}: either ${ranNot}, or ${others.join(", or ")}.`;
|
|
36178
|
+
}
|
|
36088
36179
|
function callersAnswer(totals, asked) {
|
|
36089
36180
|
const found = callersOf(totals, asked.name, asked.scripts());
|
|
36181
|
+
const byName = found.target.length === 0 ? `${CALLERS_FOUND_BY_NAME} ${uncountedCallee(totals, asked.name)}` : CALLERS_FOUND_BY_NAME;
|
|
36090
36182
|
return {
|
|
36091
36183
|
of: asked.name,
|
|
36092
36184
|
rows: found.target,
|
|
36093
36185
|
sites: found.sites,
|
|
36094
36186
|
omitted: found.omitted > 0 ? found.omitted : undefined,
|
|
36095
|
-
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.
|
|
36187
|
+
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.property ? `${byName} ${PROPERTY_PLACES}` : byName
|
|
36096
36188
|
};
|
|
36097
36189
|
}
|
|
36098
36190
|
var MOST_ENGINE_ENTRIES = 200;
|
|
@@ -38906,7 +38998,7 @@ class GodotServer {
|
|
|
38906
38998
|
platform: process.platform,
|
|
38907
38999
|
variables: process.env
|
|
38908
39000
|
});
|
|
38909
|
-
const runtimeWaitMs = this.announceBudget(args, project.value.path);
|
|
39001
|
+
const runtimeWaitMs = this.announceBudget(args, project.value.path, sceneArgument);
|
|
38910
39002
|
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
38911
39003
|
const silent = resolveSilent(args["silent"], process.env) && !headless;
|
|
38912
39004
|
const profiled = readBoolean(args, "profile") === true;
|
|
@@ -38928,6 +39020,7 @@ class GodotServer {
|
|
|
38928
39020
|
]);
|
|
38929
39021
|
}
|
|
38930
39022
|
started.announcedBefore = alreadyPlaying;
|
|
39023
|
+
started.bootScene = runtimeWaitMs.scene;
|
|
38931
39024
|
this.activeProcess = started;
|
|
38932
39025
|
const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}: the debug_* tools answer only for a game the editor is playing, and silent: false plays it through the editor.` : "";
|
|
38933
39026
|
const endedForThis = endedToStartThis(ended);
|
|
@@ -39017,17 +39110,18 @@ class GodotServer {
|
|
|
39017
39110
|
...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {}
|
|
39018
39111
|
});
|
|
39019
39112
|
}
|
|
39020
|
-
announceBudget(args, projectPath) {
|
|
39113
|
+
announceBudget(args, projectPath, scene) {
|
|
39114
|
+
const key = scene ?? MAIN_SCENE;
|
|
39021
39115
|
const asked = readNonNegativeNumber(args, "runtimeWaitMs");
|
|
39022
39116
|
if (asked !== undefined) {
|
|
39023
|
-
return { ms: asked };
|
|
39117
|
+
return { ms: asked, scene: key };
|
|
39024
39118
|
}
|
|
39025
|
-
const lastBoot = readBootNote(projectPath);
|
|
39119
|
+
const lastBoot = readBootNote(projectPath, key);
|
|
39026
39120
|
const ms = waitSizedTo(lastBoot);
|
|
39027
|
-
return lastBoot !== null && ms > ANNOUNCE_BUDGET_MS ? { ms, sizedToMs: lastBoot } : { ms };
|
|
39121
|
+
return lastBoot !== null && ms > ANNOUNCE_BUDGET_MS ? { ms, sizedToMs: lastBoot, scene: key } : { ms, scene: key };
|
|
39028
39122
|
}
|
|
39029
39123
|
noteBootOf(run, endpoint) {
|
|
39030
|
-
if (run.projectPath === null) {
|
|
39124
|
+
if (run.projectPath === null || run.bootScene === undefined) {
|
|
39031
39125
|
return;
|
|
39032
39126
|
}
|
|
39033
39127
|
let announcedAt;
|
|
@@ -39039,7 +39133,7 @@ class GodotServer {
|
|
|
39039
39133
|
const took = announcedAt - run.startedAt;
|
|
39040
39134
|
this.logDebug(`boot of pid ${endpoint.pid} announced ${Math.round(took)}ms after the start of ${run.projectPath}`);
|
|
39041
39135
|
if (took > 0 && took <= LONGEST_BOOT_NOTED_MS) {
|
|
39042
|
-
writeBootNote(run.projectPath, took);
|
|
39136
|
+
writeBootNote(run.projectPath, run.bootScene, took);
|
|
39043
39137
|
}
|
|
39044
39138
|
}
|
|
39045
39139
|
async playThroughEditor(scene, refreshedClasses, scanned, projectPath, alreadyPlaying, runtimeWaitMs, ended, profiled) {
|
|
@@ -39098,7 +39192,8 @@ class GodotServer {
|
|
|
39098
39192
|
brokeOn: null,
|
|
39099
39193
|
announcedBefore: alreadyPlaying,
|
|
39100
39194
|
seenPlaying: readBoolean(playAnswer, "playing") === true,
|
|
39101
|
-
...which === null ? {} : { play: which }
|
|
39195
|
+
...which === null ? {} : { play: which },
|
|
39196
|
+
bootScene: runtimeWaitMs.scene
|
|
39102
39197
|
};
|
|
39103
39198
|
this.activeProcess = played;
|
|
39104
39199
|
this.noteTheLatestRun({ projectPath, pid: null, startedAt });
|
|
@@ -39268,6 +39363,7 @@ class GodotServer {
|
|
|
39268
39363
|
}
|
|
39269
39364
|
running.endedHere = reason;
|
|
39270
39365
|
const announced = this.announcedPidOf(running);
|
|
39366
|
+
await this.sendTheProfileFirst(running, announced);
|
|
39271
39367
|
if (running.throughEditor) {
|
|
39272
39368
|
const asked = await this.handleViaBridge("stop_playing", {});
|
|
39273
39369
|
if (asked.isError === true) {
|
|
@@ -39338,6 +39434,25 @@ class GodotServer {
|
|
|
39338
39434
|
running.log.record(gone ? "info" : "warning", declined !== null ? `gdharness could not signal pid ${running.pid}: the operating system refused it (${declined}). It had described that process as ${described}.` : !gone ? `gdharness signalled pid ${running.pid}, which the operating system described as ${described}, and it was still running ${stopWaitMs() / 1000} seconds later.` : landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
|
|
39339
39435
|
return gone ? "gone" : "lingering";
|
|
39340
39436
|
}
|
|
39437
|
+
async sendTheProfileFirst(run, announced) {
|
|
39438
|
+
if (announced === undefined) {
|
|
39439
|
+
return;
|
|
39440
|
+
}
|
|
39441
|
+
const profiled = run.throughEditor ? await this.theEditorIsProfiling() : run.profile !== undefined;
|
|
39442
|
+
const endpoint = profiled ? runtimesAnnounced().running.find((one) => one.pid === announced) : undefined;
|
|
39443
|
+
if (endpoint === undefined) {
|
|
39444
|
+
return;
|
|
39445
|
+
}
|
|
39446
|
+
await runtimeRequest(endpoint, "send_profile", {}, PROFILE_SEND_MS);
|
|
39447
|
+
}
|
|
39448
|
+
async theEditorIsProfiling() {
|
|
39449
|
+
const read = await this.handleViaBridge("profile_read", {});
|
|
39450
|
+
if (read.isError === true) {
|
|
39451
|
+
return false;
|
|
39452
|
+
}
|
|
39453
|
+
const held = asParams(JSON.parse(read.content[0]?.text ?? "{}"));
|
|
39454
|
+
return readBoolean(held, "profiled") === true && readBoolean(held, "complete") !== true;
|
|
39455
|
+
}
|
|
39341
39456
|
async endThroughTheKeeper(run, pid, keeper) {
|
|
39342
39457
|
const asked = await askTheKeeperToStop(keeper, KEEPER_ANSWER_MS);
|
|
39343
39458
|
if (typeof asked !== "string") {
|
|
@@ -39438,6 +39553,7 @@ class GodotServer {
|
|
|
39438
39553
|
const transcript = openTranscript(startedAt);
|
|
39439
39554
|
closeSync2(transcript.fd);
|
|
39440
39555
|
const gameEnv = profiled ? { ...env ?? process.env, [PROFILE_FROM_START]: "1" } : env;
|
|
39556
|
+
const profile = profiled ? profilePath(startedAt) : undefined;
|
|
39441
39557
|
const launched = await launchOutsideTheTree({
|
|
39442
39558
|
command: godotPath,
|
|
39443
39559
|
args: cmdArgs,
|
|
@@ -39447,7 +39563,7 @@ class GodotServer {
|
|
|
39447
39563
|
startedAt,
|
|
39448
39564
|
projectPath,
|
|
39449
39565
|
...this.servedBy(),
|
|
39450
|
-
...
|
|
39566
|
+
...profile === undefined ? {} : { profile }
|
|
39451
39567
|
},
|
|
39452
39568
|
...desktop === undefined ? {} : { desktop }
|
|
39453
39569
|
});
|
|
@@ -39468,6 +39584,7 @@ class GodotServer {
|
|
|
39468
39584
|
startedAt,
|
|
39469
39585
|
startedBy,
|
|
39470
39586
|
...keeper === undefined ? {} : { keeper },
|
|
39587
|
+
...profile === undefined ? {} : { profile },
|
|
39471
39588
|
command: godotPath,
|
|
39472
39589
|
exitCode: null,
|
|
39473
39590
|
exitSignal: null,
|
|
@@ -39708,6 +39825,7 @@ class GodotServer {
|
|
|
39708
39825
|
startedAt: record.startedAt,
|
|
39709
39826
|
...record.startedBy === undefined ? {} : { startedBy: record.startedBy },
|
|
39710
39827
|
...record.keeper === undefined ? {} : { keeper: record.keeper },
|
|
39828
|
+
...record.profile === undefined ? {} : { profile: record.profile },
|
|
39711
39829
|
...record.command === undefined ? {} : { command: record.command },
|
|
39712
39830
|
exitCode: null,
|
|
39713
39831
|
exitSignal: null,
|
|
@@ -39860,12 +39978,16 @@ class GodotServer {
|
|
|
39860
39978
|
}
|
|
39861
39979
|
const limit = readPositiveNumber(args, "limit") ?? PROFILE_FUNCTIONS;
|
|
39862
39980
|
const callersName = readString(args, "callersOf");
|
|
39863
|
-
if (callersName !== undefined && !callersName.split(".").every(isIdentifier)) {
|
|
39981
|
+
if (callersName !== undefined && !callersName.replace(/@([^.@]+)_(?:getter|setter)$/, "$1").split(".").every(isIdentifier)) {
|
|
39864
39982
|
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"]);
|
|
39865
39983
|
}
|
|
39866
39984
|
const going = await this.runStillGoing(run);
|
|
39867
39985
|
const projectPath = run.projectPath;
|
|
39868
|
-
const
|
|
39986
|
+
const projectGone = projectPath !== null && !existsSync17(join22(projectPath, "project.godot"));
|
|
39987
|
+
if (projectGone && callersName !== undefined) {
|
|
39988
|
+
return this.createErrorResponse(`callersOf reads the project's scripts, and the run's project at ${projectPath} has no project.godot any more.`, ["editor_run profile without callersOf answers with the profile itself"]);
|
|
39989
|
+
}
|
|
39990
|
+
const fromProject = projectPath === null ? { runtimeAddon: false } : projectGone ? { runtimeAddon: null } : {
|
|
39869
39991
|
runtimeAddon: runtimeAddonRegistered(projectPath),
|
|
39870
39992
|
sourceOf: projectSourceOf(projectPath),
|
|
39871
39993
|
...callersName === undefined ? {} : { callersOf: { name: callersName, scripts: () => projectScripts(projectPath) } }
|
|
@@ -39873,8 +39995,7 @@ class GodotServer {
|
|
|
39873
39995
|
if (run.throughEditor) {
|
|
39874
39996
|
return await this.profileOfThePlayedGame(going, projectPath, fromProject, limit);
|
|
39875
39997
|
}
|
|
39876
|
-
const
|
|
39877
|
-
const path = record !== null && record.pid === run.pid ? record.profile : undefined;
|
|
39998
|
+
const path = run.profile;
|
|
39878
39999
|
if (path === undefined) {
|
|
39879
40000
|
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"]);
|
|
39880
40001
|
}
|
|
@@ -41007,7 +41128,7 @@ class GodotServer {
|
|
|
41007
41128
|
return null;
|
|
41008
41129
|
}
|
|
41009
41130
|
const which = run.throughEditor ? "the game the editor is playing for it" : `pid ${run.pid ?? "unknown"}`;
|
|
41010
|
-
const comingUpUntil = run.startedAt + waitSizedTo(readBootNote(project)) + budgetMs;
|
|
41131
|
+
const comingUpUntil = run.startedAt + waitSizedTo(readBootNote(project, run.bootScene ?? MAIN_SCENE)) + budgetMs;
|
|
41011
41132
|
const remaining = comingUpUntil - Date.now();
|
|
41012
41133
|
if (remaining <= 0) {
|
|
41013
41134
|
const seconds = Math.round((Date.now() - run.startedAt) / 1000);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.10",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|