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 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
- return { line, within: enclosingFunction(source, line, code), called: after.startsWith("(") };
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 boot of this project took when that is more, up to 60000: the server notes how long each game took to announce, so a project that boots slowly is waited for from its second start on without being asked to. 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."
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 readBootNote(projectPath) {
34447
+ function readScenes(projectPath) {
34423
34448
  try {
34424
- const noted = readNumber(asParams(JSON.parse(readFileSync12(bootNotePath(projectPath), "utf8"))), "announcedAfterMs");
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 null;
34451
+ return {};
34428
34452
  }
34429
34453
  }
34430
- function writeBootNote(projectPath, announcedAfterMs) {
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
- writeFileSync8(path, JSON.stringify({ announcedAfterMs: Math.round(announcedAfterMs), at: new Date().toISOString() }, null, 2), "utf8");
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 rows = rowsOf(totals, (script) => sources.get(script) ?? null);
42693
- const bare = bareName(name);
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 { target, sites: sites.slice(0, CALL_SITES), omitted: Math.max(0, sites.length - CALL_SITES) };
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 game took ${after.sizedToMs}ms to announce,` : `, half as long again as the ${after.sizedToMs}ms its last game took,`;
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
- 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. ${table.overheadDominated === 1 ? "One function has" : `${table.overheadDominated} functions have`} at least half ${table.overheadDominated === 1 ? "its" : "their"} selfMs in it, under overheadMs: ${namedFew(table.overheadNamed)}. That much of ${table.overheadDominated === 1 ? "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.`);
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 = Object.entries(totals.functions).filter(([name, sum]) => name.startsWith(UNNAMED_PREFIX) && sum.calls > 0).length;
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.target.length === 0 ? `${CALLERS_FOUND_BY_NAME} The profile counts no call of ${asked.name}, so these places are where it would be called from.` : CALLERS_FOUND_BY_NAME
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
- ...profiled ? { profile: profilePath(startedAt) } : {}
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 fromProject = projectPath === null ? { runtimeAddon: false } : {
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 record = run.projectPath === null ? null : readRunRecord(run.projectPath);
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
- function readBootNote(projectPath) {
26039
+ var MAIN_SCENE = "";
26040
+ function readScenes(projectPath) {
26040
26041
  try {
26041
- const noted = readNumber(asParams(JSON.parse(readFileSync4(bootNotePath(projectPath), "utf8"))), "announcedAfterMs");
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 null;
26044
+ return {};
26045
26045
  }
26046
26046
  }
26047
- function writeBootNote(projectPath, announcedAfterMs) {
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
- writeFileSync2(path, JSON.stringify({ announcedAfterMs: Math.round(announcedAfterMs), at: new Date().toISOString() }, null, 2), "utf8");
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
- return { line, within: enclosingFunction(source, line, code), called: after.startsWith("(") };
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 rows = rowsOf(totals, (script) => sources.get(script) ?? null);
33489
- const bare = bareName(name);
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 { target, sites: sites.slice(0, CALL_SITES), omitted: Math.max(0, sites.length - CALL_SITES) };
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 boot of this project took when that is more, up to 60000: the server notes how long each game took to announce, so a project that boots slowly is waited for from its second start on without being asked to. 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."
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 game took ${after.sizedToMs}ms to announce,` : `, half as long again as the ${after.sizedToMs}ms its last game took,`;
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
- 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. ${table.overheadDominated === 1 ? "One function has" : `${table.overheadDominated} functions have`} at least half ${table.overheadDominated === 1 ? "its" : "their"} selfMs in it, under overheadMs: ${namedFew(table.overheadNamed)}. That much of ${table.overheadDominated === 1 ? "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.`);
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 = Object.entries(totals.functions).filter(([name, sum]) => name.startsWith(UNNAMED_PREFIX) && sum.calls > 0).length;
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.target.length === 0 ? `${CALLERS_FOUND_BY_NAME} The profile counts no call of ${asked.name}, so these places are where it would be called from.` : CALLERS_FOUND_BY_NAME
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
- ...profiled ? { profile: profilePath(startedAt) } : {}
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 fromProject = projectPath === null ? { runtimeAddon: false } : {
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 record = run.projectPath === null ? null : readRunRecord(run.projectPath);
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.8",
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",