gdharness 1.2.8 → 1.2.9

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(`
@@ -9377,7 +9377,7 @@ function callSitesOf(source, name, otherClasses = new Set) {
9377
9377
  if (localAt(codeLines, index, name) !== null) {
9378
9378
  return false;
9379
9379
  }
9380
- return !bodyHolding(shape.body, one.offset).declarations.some((declared) => declared.name === name && MEMBER_KINDS.has(declared.kind));
9380
+ return !bodyHolding(shape.body, one.offset).declarations.some((declared) => declared.name === name && MEMBER_KINDS.has(declared.kind) && !(property && declared.kind === "var"));
9381
9381
  }).map((one) => {
9382
9382
  const line = lines.positionOf(one.offset).line + 1;
9383
9383
  const after = code.slice(one.offset + name.length).trimStart();
@@ -18377,7 +18377,7 @@ var init_tool_definitions = __esm(() => {
18377
18377
  callersOf: {
18378
18378
  type: "string",
18379
18379
  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."
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. 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, and the rows are both blocks'."
18381
18381
  },
18382
18382
  limit: {
18383
18383
  type: "integer",
@@ -18413,7 +18413,7 @@ var init_tool_definitions = __esm(() => {
18413
18413
  },
18414
18414
  wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
18415
18415
  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",
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. 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
18417
  requires: []
18418
18418
  }
18419
18419
  },
@@ -42603,6 +42603,9 @@ function queuedMessagesOf(projectGodot) {
42603
42603
  const set = projectGodot === null ? undefined : parseProjectGodot(projectGodot)["network"]?.[QUEUE_SETTING];
42604
42604
  return typeof set === "number" && Number.isInteger(set) && set > 0 ? set : QUEUE_DEFAULT;
42605
42605
  }
42606
+ function propertyOf(fn) {
42607
+ return /^@(.+)_(?:getter|setter)$/.exec(bareName(fn))?.[1] ?? null;
42608
+ }
42606
42609
  function signatureParts(signature) {
42607
42610
  const parts = signature.split("::");
42608
42611
  if (parts.length < 3) {
@@ -42624,7 +42627,8 @@ function profiledFunctions(totals, limit, sourceOf = () => null) {
42624
42627
  superCallers: rows.filter((row) => row.selfIncludesSuper === true).length,
42625
42628
  perCallUs,
42626
42629
  overheadDominated: rows.filter((row) => row.overheadMs !== undefined).length,
42627
- overheadNamed: rows.filter((row) => row.overheadMs !== undefined).map((row) => row.function)
42630
+ overheadNamed: rows.filter((row) => row.overheadMs !== undefined).map((row) => row.function),
42631
+ overheadReach: rows.findLastIndex((row) => row.overheadMs !== undefined) + 1
42628
42632
  };
42629
42633
  }
42630
42634
  function isCalibration(signature) {
@@ -42659,6 +42663,7 @@ function rowsOf(totals, sourceOf) {
42659
42663
  return Object.entries(totals.functions).filter(([signature, sum]) => sum.calls > 0 && !isCalibration(signature)).map(([signature, sum]) => {
42660
42664
  const parts = signatureParts(signature);
42661
42665
  const code = codeOfScript(parts.script);
42666
+ const property = propertyOf(parts.function);
42662
42667
  return {
42663
42668
  script: parts.script,
42664
42669
  function: parts.function,
@@ -42666,7 +42671,8 @@ function rowsOf(totals, sourceOf) {
42666
42671
  calls: sum.calls,
42667
42672
  selfMs: ms(sum.selfSeconds),
42668
42673
  totalMs: ms(sum.totalSeconds),
42669
- ...code !== null && bodyCallsSuper(code, parts.line, parts.function.endsWith("(lambda)")) ? { selfIncludesSuper: true } : {}
42674
+ ...code !== null && bodyCallsSuper(code, parts.line, parts.function.endsWith("(lambda)")) ? { selfIncludesSuper: true } : {},
42675
+ ...property === null ? {} : { property }
42670
42676
  };
42671
42677
  }).sort((a, b) => b.selfMs - a.selfMs || b.totalMs - a.totalMs || a.function.localeCompare(b.function));
42672
42678
  }
@@ -42690,9 +42696,11 @@ function callersOf(totals, name, scripts) {
42690
42696
  const listed = [...scripts];
42691
42697
  const sources = new Map(listed);
42692
42698
  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);
42695
42699
  const asked = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : null;
42700
+ const bare = propertyOf(name) ?? bareName(name);
42701
+ const ownerOf = (fn) => fn.includes(".") ? fn.slice(0, fn.lastIndexOf(".")) : null;
42702
+ const target = rows.filter((row) => (bareName(row.function) === bare || row.property === bare) && (asked === null || ownerOf(row.function) === asked));
42703
+ const property = propertyOf(name) !== null || target.some((row) => row.property === bare);
42696
42704
  const otherClasses = new Set;
42697
42705
  if (asked !== null) {
42698
42706
  for (const [, source] of listed) {
@@ -42705,7 +42713,7 @@ function callersOf(totals, name, scripts) {
42705
42713
  }
42706
42714
  const sites = [];
42707
42715
  for (const [script, source] of listed) {
42708
- for (const site of callSitesOf(source, bare, otherClasses)) {
42716
+ for (const site of callSitesOf(source, bare, otherClasses, property)) {
42709
42717
  const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
42710
42718
  sites.push({
42711
42719
  script,
@@ -43526,8 +43534,8 @@ function profileAnswer(reading, limit) {
43526
43534
  }
43527
43535
  const table = profiledFunctions(totals, limit, reading.sourceOf);
43528
43536
  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;
43537
+ 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.";
43538
+ 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
43539
  if (totals.complete && reading.running) {
43532
43540
  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
43541
  } else if (totals.complete) {} else if (reading.running && totals.frames === 0) {
@@ -43543,7 +43551,11 @@ function profileAnswer(reading, limit) {
43543
43551
  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
43552
  }
43545
43553
  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.`);
43554
+ const one = table.overheadDominated === 1;
43555
+ 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.`);
43556
+ }
43557
+ if (reading.coveredMs > 0 && table.scriptMs > reading.coveredMs) {
43558
+ 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
43559
  }
43548
43560
  notes.push(...cappedNotes(totals));
43549
43561
  if (totals.unreadable > 0) {
@@ -43565,6 +43577,17 @@ function profileAnswer(reading, limit) {
43565
43577
  note: notes.length > 0 ? notes.join(" ") : undefined
43566
43578
  };
43567
43579
  }
43580
+ function overheadRowsReturned(table, one) {
43581
+ const shown = table.functions.filter((row) => row.overheadMs !== undefined).length;
43582
+ const returned = table.functions.length;
43583
+ if (shown === table.overheadDominated) {
43584
+ return one ? " Its row carries that share as overheadMs." : " Their rows carry that share as overheadMs.";
43585
+ }
43586
+ if (shown === 0) {
43587
+ return one ? ` Its row, which carries that share as overheadMs, is not among the ${returned} returned; limit ${table.overheadReach} returns it.` : ` Their rows, which carry that share as overheadMs, are none of the ${returned} returned; limit ${table.overheadReach} returns them all.`;
43588
+ }
43589
+ return ` Their rows carry that share as overheadMs, and ${shown} of them ${shown === 1 ? "is" : "are"} among the ${returned} returned; limit ${table.overheadReach} returns them all.`;
43590
+ }
43568
43591
  function namedFew(names) {
43569
43592
  const shown = names.slice(0, 5);
43570
43593
  const more = names.length - shown.length;
@@ -43584,12 +43607,29 @@ function cappedNotes(totals) {
43584
43607
  const capped = totals.cappedFrames ?? 0;
43585
43608
  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
43609
  }
43587
- const unnamed = Object.entries(totals.functions).filter(([name, sum]) => name.startsWith(UNNAMED_PREFIX) && sum.calls > 0).length;
43610
+ const unnamed = unnamedCalled(totals);
43588
43611
  if (unnamed > 0) {
43589
43612
  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
43613
  }
43591
43614
  return notes;
43592
43615
  }
43616
+ function unnamedCalled(totals) {
43617
+ return Object.entries(totals.functions).filter(([name, sum]) => name.startsWith(UNNAMED_PREFIX) && sum.calls > 0).length;
43618
+ }
43619
+ function uncountedCallee(totals, name) {
43620
+ const others = [];
43621
+ const most = totals.frameFunctions;
43622
+ const capped = totals.complete ? totals.totalCapped === true : (totals.cappedFrames ?? 0) > 0;
43623
+ if (most !== undefined && capped) {
43624
+ others.push(`its calls took too little time to be among the ${most} functions ${totals.complete ? "the engine's totals" : "a frame"} carried`);
43625
+ }
43626
+ const unnamed = unnamedCalled(totals);
43627
+ if (unnamed > 0) {
43628
+ others.push(`it is ${unnamed === 1 ? "the function" : `one of the ${unnamed} functions`} whose name never arrived`);
43629
+ }
43630
+ 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";
43631
+ 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 ")}.`;
43632
+ }
43593
43633
  function callersAnswer(totals, asked) {
43594
43634
  const found = callersOf(totals, asked.name, asked.scripts());
43595
43635
  return {
@@ -43597,7 +43637,7 @@ function callersAnswer(totals, asked) {
43597
43637
  rows: found.target,
43598
43638
  sites: found.sites,
43599
43639
  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
43640
+ 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} ${uncountedCallee(totals, asked.name)}` : CALLERS_FOUND_BY_NAME
43601
43641
  };
43602
43642
  }
43603
43643
  function foldedEntriesAnswer(log) {
@@ -46940,6 +46980,7 @@ class GodotServer {
46940
46980
  const transcript = openTranscript(startedAt);
46941
46981
  closeSync2(transcript.fd);
46942
46982
  const gameEnv = profiled ? { ...env ?? process.env, [PROFILE_FROM_START]: "1" } : env;
46983
+ const profile = profiled ? profilePath(startedAt) : undefined;
46943
46984
  const launched = await launchOutsideTheTree({
46944
46985
  command: godotPath,
46945
46986
  args: cmdArgs,
@@ -46949,7 +46990,7 @@ class GodotServer {
46949
46990
  startedAt,
46950
46991
  projectPath,
46951
46992
  ...this.servedBy(),
46952
- ...profiled ? { profile: profilePath(startedAt) } : {}
46993
+ ...profile === undefined ? {} : { profile }
46953
46994
  },
46954
46995
  ...desktop === undefined ? {} : { desktop }
46955
46996
  });
@@ -46970,6 +47011,7 @@ class GodotServer {
46970
47011
  startedAt,
46971
47012
  startedBy,
46972
47013
  ...keeper === undefined ? {} : { keeper },
47014
+ ...profile === undefined ? {} : { profile },
46973
47015
  command: godotPath,
46974
47016
  exitCode: null,
46975
47017
  exitSignal: null,
@@ -47210,6 +47252,7 @@ class GodotServer {
47210
47252
  startedAt: record.startedAt,
47211
47253
  ...record.startedBy === undefined ? {} : { startedBy: record.startedBy },
47212
47254
  ...record.keeper === undefined ? {} : { keeper: record.keeper },
47255
+ ...record.profile === undefined ? {} : { profile: record.profile },
47213
47256
  ...record.command === undefined ? {} : { command: record.command },
47214
47257
  exitCode: null,
47215
47258
  exitSignal: null,
@@ -47362,12 +47405,16 @@ class GodotServer {
47362
47405
  }
47363
47406
  const limit = readPositiveNumber(args, "limit") ?? PROFILE_FUNCTIONS;
47364
47407
  const callersName = readString(args, "callersOf");
47365
- if (callersName !== undefined && !callersName.split(".").every(isIdentifier)) {
47408
+ if (callersName !== undefined && !callersName.replace(/@([^.@]+)_(?:getter|setter)$/, "$1").split(".").every(isIdentifier)) {
47366
47409
  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
47410
  }
47368
47411
  const going = await this.runStillGoing(run);
47369
47412
  const projectPath = run.projectPath;
47370
- const fromProject = projectPath === null ? { runtimeAddon: false } : {
47413
+ const projectGone = projectPath !== null && !existsSync20(join26(projectPath, "project.godot"));
47414
+ if (projectGone && callersName !== undefined) {
47415
+ 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"]);
47416
+ }
47417
+ const fromProject = projectPath === null ? { runtimeAddon: false } : projectGone ? { runtimeAddon: null } : {
47371
47418
  runtimeAddon: runtimeAddonRegistered(projectPath),
47372
47419
  sourceOf: projectSourceOf(projectPath),
47373
47420
  ...callersName === undefined ? {} : { callersOf: { name: callersName, scripts: () => projectScripts(projectPath) } }
@@ -47375,8 +47422,7 @@ class GodotServer {
47375
47422
  if (run.throughEditor) {
47376
47423
  return await this.profileOfThePlayedGame(going, projectPath, fromProject, limit);
47377
47424
  }
47378
- const record = run.projectPath === null ? null : readRunRecord(run.projectPath);
47379
- const path = record !== null && record.pid === run.pid ? record.profile : undefined;
47425
+ const path = run.profile;
47380
47426
  if (path === undefined) {
47381
47427
  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
47428
  }
package/build/index.js CHANGED
@@ -26720,7 +26720,7 @@ function bodyHolding(body, offset) {
26720
26720
  const inner = body.inner.find((one) => one.start <= offset && offset <= one.end);
26721
26721
  return inner === undefined ? body : bodyHolding(inner, offset);
26722
26722
  }
26723
- function callSitesOf(source, name, otherClasses = new Set) {
26723
+ function callSitesOf(source, name, otherClasses = new Set, property = false) {
26724
26724
  const regions = regionsOf(source);
26725
26725
  const code = codeOf(source, regions);
26726
26726
  const codeLines = code.split(`
@@ -26750,7 +26750,7 @@ function callSitesOf(source, name, otherClasses = new Set) {
26750
26750
  if (localAt(codeLines, index, name) !== null) {
26751
26751
  return false;
26752
26752
  }
26753
- return !bodyHolding(shape.body, one.offset).declarations.some((declared) => declared.name === name && MEMBER_KINDS.has(declared.kind));
26753
+ return !bodyHolding(shape.body, one.offset).declarations.some((declared) => declared.name === name && MEMBER_KINDS.has(declared.kind) && !(property && declared.kind === "var"));
26754
26754
  }).map((one) => {
26755
26755
  const line = lines.positionOf(one.offset).line + 1;
26756
26756
  const after = code.slice(one.offset + name.length).trimStart();
@@ -33394,6 +33394,9 @@ function queuedMessagesOf(projectGodot) {
33394
33394
  }
33395
33395
  var UNNAMED_PREFIX = "#";
33396
33396
  var PROFILE_FROM_START = "GDHARNESS_PROFILE_FROM_START";
33397
+ function propertyOf(fn) {
33398
+ return /^@(.+)_(?:getter|setter)$/.exec(bareName(fn))?.[1] ?? null;
33399
+ }
33397
33400
  function signatureParts(signature) {
33398
33401
  const parts = signature.split("::");
33399
33402
  if (parts.length < 3) {
@@ -33416,7 +33419,8 @@ function profiledFunctions(totals, limit, sourceOf = () => null) {
33416
33419
  superCallers: rows.filter((row) => row.selfIncludesSuper === true).length,
33417
33420
  perCallUs,
33418
33421
  overheadDominated: rows.filter((row) => row.overheadMs !== undefined).length,
33419
- overheadNamed: rows.filter((row) => row.overheadMs !== undefined).map((row) => row.function)
33422
+ overheadNamed: rows.filter((row) => row.overheadMs !== undefined).map((row) => row.function),
33423
+ overheadReach: rows.findLastIndex((row) => row.overheadMs !== undefined) + 1
33420
33424
  };
33421
33425
  }
33422
33426
  var RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd";
@@ -33454,6 +33458,7 @@ function rowsOf(totals, sourceOf) {
33454
33458
  return Object.entries(totals.functions).filter(([signature, sum]) => sum.calls > 0 && !isCalibration(signature)).map(([signature, sum]) => {
33455
33459
  const parts = signatureParts(signature);
33456
33460
  const code = codeOfScript(parts.script);
33461
+ const property = propertyOf(parts.function);
33457
33462
  return {
33458
33463
  script: parts.script,
33459
33464
  function: parts.function,
@@ -33461,7 +33466,8 @@ function rowsOf(totals, sourceOf) {
33461
33466
  calls: sum.calls,
33462
33467
  selfMs: ms(sum.selfSeconds),
33463
33468
  totalMs: ms(sum.totalSeconds),
33464
- ...code !== null && bodyCallsSuper(code, parts.line, parts.function.endsWith("(lambda)")) ? { selfIncludesSuper: true } : {}
33469
+ ...code !== null && bodyCallsSuper(code, parts.line, parts.function.endsWith("(lambda)")) ? { selfIncludesSuper: true } : {},
33470
+ ...property === null ? {} : { property }
33465
33471
  };
33466
33472
  }).sort((a, b) => b.selfMs - a.selfMs || b.totalMs - a.totalMs || a.function.localeCompare(b.function));
33467
33473
  }
@@ -33486,9 +33492,11 @@ function callersOf(totals, name, scripts) {
33486
33492
  const listed = [...scripts];
33487
33493
  const sources = new Map(listed);
33488
33494
  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);
33491
33495
  const asked = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : null;
33496
+ const bare = propertyOf(name) ?? bareName(name);
33497
+ const ownerOf = (fn) => fn.includes(".") ? fn.slice(0, fn.lastIndexOf(".")) : null;
33498
+ const target = rows.filter((row) => (bareName(row.function) === bare || row.property === bare) && (asked === null || ownerOf(row.function) === asked));
33499
+ const property = propertyOf(name) !== null || target.some((row) => row.property === bare);
33492
33500
  const otherClasses = new Set;
33493
33501
  if (asked !== null) {
33494
33502
  for (const [, source] of listed) {
@@ -33501,7 +33509,7 @@ function callersOf(totals, name, scripts) {
33501
33509
  }
33502
33510
  const sites = [];
33503
33511
  for (const [script, source] of listed) {
33504
- for (const site of callSitesOf(source, bare, otherClasses)) {
33512
+ for (const site of callSitesOf(source, bare, otherClasses, property)) {
33505
33513
  const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
33506
33514
  sites.push({
33507
33515
  script,
@@ -34506,7 +34514,7 @@ var TOOL_SPECS = [
34506
34514
  callersOf: {
34507
34515
  type: "string",
34508
34516
  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."
34517
+ 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, and the rows are both blocks'."
34510
34518
  },
34511
34519
  limit: {
34512
34520
  type: "integer",
@@ -34542,7 +34550,7 @@ var TOOL_SPECS = [
34542
34550
  },
34543
34551
  wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
34544
34552
  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",
34553
+ 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
34554
  requires: []
34547
34555
  }
34548
34556
  },
@@ -36021,8 +36029,8 @@ function profileAnswer(reading, limit) {
36021
36029
  }
36022
36030
  const table = profiledFunctions(totals, limit, reading.sourceOf);
36023
36031
  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;
36032
+ 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.";
36033
+ 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
36034
  if (totals.complete && reading.running) {
36027
36035
  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
36036
  } else if (totals.complete) {} else if (reading.running && totals.frames === 0) {
@@ -36038,7 +36046,11 @@ function profileAnswer(reading, limit) {
36038
36046
  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
36047
  }
36040
36048
  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.`);
36049
+ const one = table.overheadDominated === 1;
36050
+ 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.`);
36051
+ }
36052
+ if (reading.coveredMs > 0 && table.scriptMs > reading.coveredMs) {
36053
+ 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
36054
  }
36043
36055
  notes.push(...cappedNotes(totals));
36044
36056
  if (totals.unreadable > 0) {
@@ -36060,6 +36072,17 @@ function profileAnswer(reading, limit) {
36060
36072
  note: notes.length > 0 ? notes.join(" ") : undefined
36061
36073
  };
36062
36074
  }
36075
+ function overheadRowsReturned(table, one) {
36076
+ const shown = table.functions.filter((row) => row.overheadMs !== undefined).length;
36077
+ const returned = table.functions.length;
36078
+ if (shown === table.overheadDominated) {
36079
+ return one ? " Its row carries that share as overheadMs." : " Their rows carry that share as overheadMs.";
36080
+ }
36081
+ if (shown === 0) {
36082
+ return one ? ` Its row, which carries that share as overheadMs, is not among the ${returned} returned; limit ${table.overheadReach} returns it.` : ` Their rows, which carry that share as overheadMs, are none of the ${returned} returned; limit ${table.overheadReach} returns them all.`;
36083
+ }
36084
+ return ` Their rows carry that share as overheadMs, and ${shown} of them ${shown === 1 ? "is" : "are"} among the ${returned} returned; limit ${table.overheadReach} returns them all.`;
36085
+ }
36063
36086
  function namedFew(names) {
36064
36087
  const shown = names.slice(0, 5);
36065
36088
  const more = names.length - shown.length;
@@ -36079,12 +36102,29 @@ function cappedNotes(totals) {
36079
36102
  const capped = totals.cappedFrames ?? 0;
36080
36103
  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
36104
  }
36082
- const unnamed = Object.entries(totals.functions).filter(([name, sum]) => name.startsWith(UNNAMED_PREFIX) && sum.calls > 0).length;
36105
+ const unnamed = unnamedCalled(totals);
36083
36106
  if (unnamed > 0) {
36084
36107
  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
36108
  }
36086
36109
  return notes;
36087
36110
  }
36111
+ function unnamedCalled(totals) {
36112
+ return Object.entries(totals.functions).filter(([name, sum]) => name.startsWith(UNNAMED_PREFIX) && sum.calls > 0).length;
36113
+ }
36114
+ function uncountedCallee(totals, name) {
36115
+ const others = [];
36116
+ const most = totals.frameFunctions;
36117
+ const capped = totals.complete ? totals.totalCapped === true : (totals.cappedFrames ?? 0) > 0;
36118
+ if (most !== undefined && capped) {
36119
+ others.push(`its calls took too little time to be among the ${most} functions ${totals.complete ? "the engine's totals" : "a frame"} carried`);
36120
+ }
36121
+ const unnamed = unnamedCalled(totals);
36122
+ if (unnamed > 0) {
36123
+ others.push(`it is ${unnamed === 1 ? "the function" : `one of the ${unnamed} functions`} whose name never arrived`);
36124
+ }
36125
+ 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";
36126
+ 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 ")}.`;
36127
+ }
36088
36128
  function callersAnswer(totals, asked) {
36089
36129
  const found = callersOf(totals, asked.name, asked.scripts());
36090
36130
  return {
@@ -36092,7 +36132,7 @@ function callersAnswer(totals, asked) {
36092
36132
  rows: found.target,
36093
36133
  sites: found.sites,
36094
36134
  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
36135
+ 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} ${uncountedCallee(totals, asked.name)}` : CALLERS_FOUND_BY_NAME
36096
36136
  };
36097
36137
  }
36098
36138
  var MOST_ENGINE_ENTRIES = 200;
@@ -39438,6 +39478,7 @@ class GodotServer {
39438
39478
  const transcript = openTranscript(startedAt);
39439
39479
  closeSync2(transcript.fd);
39440
39480
  const gameEnv = profiled ? { ...env ?? process.env, [PROFILE_FROM_START]: "1" } : env;
39481
+ const profile = profiled ? profilePath(startedAt) : undefined;
39441
39482
  const launched = await launchOutsideTheTree({
39442
39483
  command: godotPath,
39443
39484
  args: cmdArgs,
@@ -39447,7 +39488,7 @@ class GodotServer {
39447
39488
  startedAt,
39448
39489
  projectPath,
39449
39490
  ...this.servedBy(),
39450
- ...profiled ? { profile: profilePath(startedAt) } : {}
39491
+ ...profile === undefined ? {} : { profile }
39451
39492
  },
39452
39493
  ...desktop === undefined ? {} : { desktop }
39453
39494
  });
@@ -39468,6 +39509,7 @@ class GodotServer {
39468
39509
  startedAt,
39469
39510
  startedBy,
39470
39511
  ...keeper === undefined ? {} : { keeper },
39512
+ ...profile === undefined ? {} : { profile },
39471
39513
  command: godotPath,
39472
39514
  exitCode: null,
39473
39515
  exitSignal: null,
@@ -39708,6 +39750,7 @@ class GodotServer {
39708
39750
  startedAt: record.startedAt,
39709
39751
  ...record.startedBy === undefined ? {} : { startedBy: record.startedBy },
39710
39752
  ...record.keeper === undefined ? {} : { keeper: record.keeper },
39753
+ ...record.profile === undefined ? {} : { profile: record.profile },
39711
39754
  ...record.command === undefined ? {} : { command: record.command },
39712
39755
  exitCode: null,
39713
39756
  exitSignal: null,
@@ -39860,12 +39903,16 @@ class GodotServer {
39860
39903
  }
39861
39904
  const limit = readPositiveNumber(args, "limit") ?? PROFILE_FUNCTIONS;
39862
39905
  const callersName = readString(args, "callersOf");
39863
- if (callersName !== undefined && !callersName.split(".").every(isIdentifier)) {
39906
+ if (callersName !== undefined && !callersName.replace(/@([^.@]+)_(?:getter|setter)$/, "$1").split(".").every(isIdentifier)) {
39864
39907
  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
39908
  }
39866
39909
  const going = await this.runStillGoing(run);
39867
39910
  const projectPath = run.projectPath;
39868
- const fromProject = projectPath === null ? { runtimeAddon: false } : {
39911
+ const projectGone = projectPath !== null && !existsSync17(join22(projectPath, "project.godot"));
39912
+ if (projectGone && callersName !== undefined) {
39913
+ 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"]);
39914
+ }
39915
+ const fromProject = projectPath === null ? { runtimeAddon: false } : projectGone ? { runtimeAddon: null } : {
39869
39916
  runtimeAddon: runtimeAddonRegistered(projectPath),
39870
39917
  sourceOf: projectSourceOf(projectPath),
39871
39918
  ...callersName === undefined ? {} : { callersOf: { name: callersName, scripts: () => projectScripts(projectPath) } }
@@ -39873,8 +39920,7 @@ class GodotServer {
39873
39920
  if (run.throughEditor) {
39874
39921
  return await this.profileOfThePlayedGame(going, projectPath, fromProject, limit);
39875
39922
  }
39876
- const record = run.projectPath === null ? null : readRunRecord(run.projectPath);
39877
- const path = record !== null && record.pid === run.pid ? record.profile : undefined;
39923
+ const path = run.profile;
39878
39924
  if (path === undefined) {
39879
39925
  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
39926
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.2.8",
3
+ "version": "1.2.9",
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",