gdharness 1.2.7 → 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) {
@@ -42623,7 +42626,9 @@ function profiledFunctions(totals, limit, sourceOf = () => null) {
42623
42626
  counted: rows.length,
42624
42627
  superCallers: rows.filter((row) => row.selfIncludesSuper === true).length,
42625
42628
  perCallUs,
42626
- overheadDominated: rows.filter((row) => row.overheadMs !== undefined).length
42629
+ overheadDominated: rows.filter((row) => row.overheadMs !== undefined).length,
42630
+ overheadNamed: rows.filter((row) => row.overheadMs !== undefined).map((row) => row.function),
42631
+ overheadReach: rows.findLastIndex((row) => row.overheadMs !== undefined) + 1
42627
42632
  };
42628
42633
  }
42629
42634
  function isCalibration(signature) {
@@ -42658,6 +42663,7 @@ function rowsOf(totals, sourceOf) {
42658
42663
  return Object.entries(totals.functions).filter(([signature, sum]) => sum.calls > 0 && !isCalibration(signature)).map(([signature, sum]) => {
42659
42664
  const parts = signatureParts(signature);
42660
42665
  const code = codeOfScript(parts.script);
42666
+ const property = propertyOf(parts.function);
42661
42667
  return {
42662
42668
  script: parts.script,
42663
42669
  function: parts.function,
@@ -42665,7 +42671,8 @@ function rowsOf(totals, sourceOf) {
42665
42671
  calls: sum.calls,
42666
42672
  selfMs: ms(sum.selfSeconds),
42667
42673
  totalMs: ms(sum.totalSeconds),
42668
- ...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 }
42669
42676
  };
42670
42677
  }).sort((a, b) => b.selfMs - a.selfMs || b.totalMs - a.totalMs || a.function.localeCompare(b.function));
42671
42678
  }
@@ -42689,9 +42696,11 @@ function callersOf(totals, name, scripts) {
42689
42696
  const listed = [...scripts];
42690
42697
  const sources = new Map(listed);
42691
42698
  const rows = rowsOf(totals, (script) => sources.get(script) ?? null);
42692
- const bare = bareName(name);
42693
- const target = rows.filter((row) => name.includes(".") ? row.function === name : bareName(row.function) === bare);
42694
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);
42695
42704
  const otherClasses = new Set;
42696
42705
  if (asked !== null) {
42697
42706
  for (const [, source] of listed) {
@@ -42704,7 +42713,7 @@ function callersOf(totals, name, scripts) {
42704
42713
  }
42705
42714
  const sites = [];
42706
42715
  for (const [script, source] of listed) {
42707
- for (const site of callSitesOf(source, bare, otherClasses)) {
42716
+ for (const site of callSitesOf(source, bare, otherClasses, property)) {
42708
42717
  const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
42709
42718
  sites.push({
42710
42719
  script,
@@ -43525,8 +43534,8 @@ function profileAnswer(reading, limit) {
43525
43534
  }
43526
43535
  const table = profiledFunctions(totals, limit, reading.sourceOf);
43527
43536
  const notes = [];
43528
- 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.";
43529
- 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;
43530
43539
  if (totals.complete && reading.running) {
43531
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.`);
43532
43541
  } else if (totals.complete) {} else if (reading.running && totals.frames === 0) {
@@ -43542,7 +43551,11 @@ function profileAnswer(reading, limit) {
43542
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.`);
43543
43552
  }
43544
43553
  if (table.overheadDominated > 0 && table.perCallUs !== null) {
43545
- 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: that much of its 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.`);
43546
43559
  }
43547
43560
  notes.push(...cappedNotes(totals));
43548
43561
  if (totals.unreadable > 0) {
@@ -43564,22 +43577,59 @@ function profileAnswer(reading, limit) {
43564
43577
  note: notes.length > 0 ? notes.join(" ") : undefined
43565
43578
  };
43566
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
+ }
43591
+ function namedFew(names) {
43592
+ const shown = names.slice(0, 5);
43593
+ const more = names.length - shown.length;
43594
+ if (more > 0) {
43595
+ return `${shown.join(", ")} and ${more} more`;
43596
+ }
43597
+ return shown.length > 1 ? `${shown.slice(0, -1).join(", ")} and ${shown.at(-1) ?? ""}` : shown[0] ?? "";
43598
+ }
43567
43599
  function cappedNotes(totals) {
43568
43600
  const notes = [];
43569
43601
  const most = totals.frameFunctions;
43570
43602
  const why = `${most} is kept below the game's debugger queue (network/limits/debugger/max_queued_messages), which drops the message naming a function when one frame names more than it holds, and a function whose name is dropped is never named afterwards.`;
43571
43603
  if (most !== undefined && totals.complete && totals.totalCapped === true) {
43572
- notes.push(`The engine's totals carried the most functions the profiler asks for, ${most}, so the functions with the least time are not in them: ${why}`);
43604
+ const leastKept = Math.min(...Object.values(totals.functions).map((one) => one.totalSeconds));
43605
+ notes.push(leastKept > 0 ? `The engine's totals carried the most functions the profiler asks for, ${most}, so the functions with the least time are not in them: ${why}` : `The engine's totals carried the most functions the profiler asks for, ${most}, and the last of them took no measurable time, so neither did any left out: the time is all here, and only a function too quick for the clock can be missing, with its calls. The engine lists every function it has compiled, run or not, and sends them busiest first. ${why}`);
43573
43606
  } else if (most !== undefined && !totals.complete && (totals.cappedFrames ?? 0) > 0) {
43574
43607
  const capped = totals.cappedFrames ?? 0;
43575
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}`);
43576
43609
  }
43577
- const unnamed = Object.entries(totals.functions).filter(([name, sum]) => name.startsWith(UNNAMED_PREFIX) && sum.calls > 0).length;
43610
+ const unnamed = unnamedCalled(totals);
43578
43611
  if (unnamed > 0) {
43579
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.`);
43580
43613
  }
43581
43614
  return notes;
43582
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
+ }
43583
43633
  function callersAnswer(totals, asked) {
43584
43634
  const found = callersOf(totals, asked.name, asked.scripts());
43585
43635
  return {
@@ -43587,7 +43637,7 @@ function callersAnswer(totals, asked) {
43587
43637
  rows: found.target,
43588
43638
  sites: found.sites,
43589
43639
  omitted: found.omitted > 0 ? found.omitted : undefined,
43590
- 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
43591
43641
  };
43592
43642
  }
43593
43643
  function foldedEntriesAnswer(log) {
@@ -46930,6 +46980,7 @@ class GodotServer {
46930
46980
  const transcript = openTranscript(startedAt);
46931
46981
  closeSync2(transcript.fd);
46932
46982
  const gameEnv = profiled ? { ...env ?? process.env, [PROFILE_FROM_START]: "1" } : env;
46983
+ const profile = profiled ? profilePath(startedAt) : undefined;
46933
46984
  const launched = await launchOutsideTheTree({
46934
46985
  command: godotPath,
46935
46986
  args: cmdArgs,
@@ -46939,7 +46990,7 @@ class GodotServer {
46939
46990
  startedAt,
46940
46991
  projectPath,
46941
46992
  ...this.servedBy(),
46942
- ...profiled ? { profile: profilePath(startedAt) } : {}
46993
+ ...profile === undefined ? {} : { profile }
46943
46994
  },
46944
46995
  ...desktop === undefined ? {} : { desktop }
46945
46996
  });
@@ -46960,6 +47011,7 @@ class GodotServer {
46960
47011
  startedAt,
46961
47012
  startedBy,
46962
47013
  ...keeper === undefined ? {} : { keeper },
47014
+ ...profile === undefined ? {} : { profile },
46963
47015
  command: godotPath,
46964
47016
  exitCode: null,
46965
47017
  exitSignal: null,
@@ -47200,6 +47252,7 @@ class GodotServer {
47200
47252
  startedAt: record.startedAt,
47201
47253
  ...record.startedBy === undefined ? {} : { startedBy: record.startedBy },
47202
47254
  ...record.keeper === undefined ? {} : { keeper: record.keeper },
47255
+ ...record.profile === undefined ? {} : { profile: record.profile },
47203
47256
  ...record.command === undefined ? {} : { command: record.command },
47204
47257
  exitCode: null,
47205
47258
  exitSignal: null,
@@ -47352,12 +47405,16 @@ class GodotServer {
47352
47405
  }
47353
47406
  const limit = readPositiveNumber(args, "limit") ?? PROFILE_FUNCTIONS;
47354
47407
  const callersName = readString(args, "callersOf");
47355
- if (callersName !== undefined && !callersName.split(".").every(isIdentifier)) {
47408
+ if (callersName !== undefined && !callersName.replace(/@([^.@]+)_(?:getter|setter)$/, "$1").split(".").every(isIdentifier)) {
47356
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"]);
47357
47410
  }
47358
47411
  const going = await this.runStillGoing(run);
47359
47412
  const projectPath = run.projectPath;
47360
- 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 } : {
47361
47418
  runtimeAddon: runtimeAddonRegistered(projectPath),
47362
47419
  sourceOf: projectSourceOf(projectPath),
47363
47420
  ...callersName === undefined ? {} : { callersOf: { name: callersName, scripts: () => projectScripts(projectPath) } }
@@ -47365,8 +47422,7 @@ class GodotServer {
47365
47422
  if (run.throughEditor) {
47366
47423
  return await this.profileOfThePlayedGame(going, projectPath, fromProject, limit);
47367
47424
  }
47368
- const record = run.projectPath === null ? null : readRunRecord(run.projectPath);
47369
- const path = record !== null && record.pid === run.pid ? record.profile : undefined;
47425
+ const path = run.profile;
47370
47426
  if (path === undefined) {
47371
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"]);
47372
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) {
@@ -33415,7 +33418,9 @@ function profiledFunctions(totals, limit, sourceOf = () => null) {
33415
33418
  counted: rows.length,
33416
33419
  superCallers: rows.filter((row) => row.selfIncludesSuper === true).length,
33417
33420
  perCallUs,
33418
- overheadDominated: rows.filter((row) => row.overheadMs !== undefined).length
33421
+ overheadDominated: rows.filter((row) => row.overheadMs !== undefined).length,
33422
+ overheadNamed: rows.filter((row) => row.overheadMs !== undefined).map((row) => row.function),
33423
+ overheadReach: rows.findLastIndex((row) => row.overheadMs !== undefined) + 1
33419
33424
  };
33420
33425
  }
33421
33426
  var RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd";
@@ -33453,6 +33458,7 @@ function rowsOf(totals, sourceOf) {
33453
33458
  return Object.entries(totals.functions).filter(([signature, sum]) => sum.calls > 0 && !isCalibration(signature)).map(([signature, sum]) => {
33454
33459
  const parts = signatureParts(signature);
33455
33460
  const code = codeOfScript(parts.script);
33461
+ const property = propertyOf(parts.function);
33456
33462
  return {
33457
33463
  script: parts.script,
33458
33464
  function: parts.function,
@@ -33460,7 +33466,8 @@ function rowsOf(totals, sourceOf) {
33460
33466
  calls: sum.calls,
33461
33467
  selfMs: ms(sum.selfSeconds),
33462
33468
  totalMs: ms(sum.totalSeconds),
33463
- ...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 }
33464
33471
  };
33465
33472
  }).sort((a, b) => b.selfMs - a.selfMs || b.totalMs - a.totalMs || a.function.localeCompare(b.function));
33466
33473
  }
@@ -33485,9 +33492,11 @@ function callersOf(totals, name, scripts) {
33485
33492
  const listed = [...scripts];
33486
33493
  const sources = new Map(listed);
33487
33494
  const rows = rowsOf(totals, (script) => sources.get(script) ?? null);
33488
- const bare = bareName(name);
33489
- const target = rows.filter((row) => name.includes(".") ? row.function === name : bareName(row.function) === bare);
33490
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);
33491
33500
  const otherClasses = new Set;
33492
33501
  if (asked !== null) {
33493
33502
  for (const [, source] of listed) {
@@ -33500,7 +33509,7 @@ function callersOf(totals, name, scripts) {
33500
33509
  }
33501
33510
  const sites = [];
33502
33511
  for (const [script, source] of listed) {
33503
- for (const site of callSitesOf(source, bare, otherClasses)) {
33512
+ for (const site of callSitesOf(source, bare, otherClasses, property)) {
33504
33513
  const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
33505
33514
  sites.push({
33506
33515
  script,
@@ -34505,7 +34514,7 @@ var TOOL_SPECS = [
34505
34514
  callersOf: {
34506
34515
  type: "string",
34507
34516
  ops: ["profile"],
34508
- 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'."
34509
34518
  },
34510
34519
  limit: {
34511
34520
  type: "integer",
@@ -34541,7 +34550,7 @@ var TOOL_SPECS = [
34541
34550
  },
34542
34551
  wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
34543
34552
  profile: {
34544
- 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",
34545
34554
  requires: []
34546
34555
  }
34547
34556
  },
@@ -36020,8 +36029,8 @@ function profileAnswer(reading, limit) {
36020
36029
  }
36021
36030
  const table = profiledFunctions(totals, limit, reading.sourceOf);
36022
36031
  const notes = [];
36023
- 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.";
36024
- 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;
36025
36034
  if (totals.complete && reading.running) {
36026
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.`);
36027
36036
  } else if (totals.complete) {} else if (reading.running && totals.frames === 0) {
@@ -36037,7 +36046,11 @@ function profileAnswer(reading, limit) {
36037
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.`);
36038
36047
  }
36039
36048
  if (table.overheadDominated > 0 && table.perCallUs !== null) {
36040
- 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: that much of its 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.`);
36041
36054
  }
36042
36055
  notes.push(...cappedNotes(totals));
36043
36056
  if (totals.unreadable > 0) {
@@ -36059,22 +36072,59 @@ function profileAnswer(reading, limit) {
36059
36072
  note: notes.length > 0 ? notes.join(" ") : undefined
36060
36073
  };
36061
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
+ }
36086
+ function namedFew(names) {
36087
+ const shown = names.slice(0, 5);
36088
+ const more = names.length - shown.length;
36089
+ if (more > 0) {
36090
+ return `${shown.join(", ")} and ${more} more`;
36091
+ }
36092
+ return shown.length > 1 ? `${shown.slice(0, -1).join(", ")} and ${shown.at(-1) ?? ""}` : shown[0] ?? "";
36093
+ }
36062
36094
  function cappedNotes(totals) {
36063
36095
  const notes = [];
36064
36096
  const most = totals.frameFunctions;
36065
36097
  const why = `${most} is kept below the game's debugger queue (network/limits/debugger/max_queued_messages), which drops the message naming a function when one frame names more than it holds, and a function whose name is dropped is never named afterwards.`;
36066
36098
  if (most !== undefined && totals.complete && totals.totalCapped === true) {
36067
- notes.push(`The engine's totals carried the most functions the profiler asks for, ${most}, so the functions with the least time are not in them: ${why}`);
36099
+ const leastKept = Math.min(...Object.values(totals.functions).map((one) => one.totalSeconds));
36100
+ notes.push(leastKept > 0 ? `The engine's totals carried the most functions the profiler asks for, ${most}, so the functions with the least time are not in them: ${why}` : `The engine's totals carried the most functions the profiler asks for, ${most}, and the last of them took no measurable time, so neither did any left out: the time is all here, and only a function too quick for the clock can be missing, with its calls. The engine lists every function it has compiled, run or not, and sends them busiest first. ${why}`);
36068
36101
  } else if (most !== undefined && !totals.complete && (totals.cappedFrames ?? 0) > 0) {
36069
36102
  const capped = totals.cappedFrames ?? 0;
36070
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}`);
36071
36104
  }
36072
- const unnamed = Object.entries(totals.functions).filter(([name, sum]) => name.startsWith(UNNAMED_PREFIX) && sum.calls > 0).length;
36105
+ const unnamed = unnamedCalled(totals);
36073
36106
  if (unnamed > 0) {
36074
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.`);
36075
36108
  }
36076
36109
  return notes;
36077
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
+ }
36078
36128
  function callersAnswer(totals, asked) {
36079
36129
  const found = callersOf(totals, asked.name, asked.scripts());
36080
36130
  return {
@@ -36082,7 +36132,7 @@ function callersAnswer(totals, asked) {
36082
36132
  rows: found.target,
36083
36133
  sites: found.sites,
36084
36134
  omitted: found.omitted > 0 ? found.omitted : undefined,
36085
- 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
36086
36136
  };
36087
36137
  }
36088
36138
  var MOST_ENGINE_ENTRIES = 200;
@@ -39428,6 +39478,7 @@ class GodotServer {
39428
39478
  const transcript = openTranscript(startedAt);
39429
39479
  closeSync2(transcript.fd);
39430
39480
  const gameEnv = profiled ? { ...env ?? process.env, [PROFILE_FROM_START]: "1" } : env;
39481
+ const profile = profiled ? profilePath(startedAt) : undefined;
39431
39482
  const launched = await launchOutsideTheTree({
39432
39483
  command: godotPath,
39433
39484
  args: cmdArgs,
@@ -39437,7 +39488,7 @@ class GodotServer {
39437
39488
  startedAt,
39438
39489
  projectPath,
39439
39490
  ...this.servedBy(),
39440
- ...profiled ? { profile: profilePath(startedAt) } : {}
39491
+ ...profile === undefined ? {} : { profile }
39441
39492
  },
39442
39493
  ...desktop === undefined ? {} : { desktop }
39443
39494
  });
@@ -39458,6 +39509,7 @@ class GodotServer {
39458
39509
  startedAt,
39459
39510
  startedBy,
39460
39511
  ...keeper === undefined ? {} : { keeper },
39512
+ ...profile === undefined ? {} : { profile },
39461
39513
  command: godotPath,
39462
39514
  exitCode: null,
39463
39515
  exitSignal: null,
@@ -39698,6 +39750,7 @@ class GodotServer {
39698
39750
  startedAt: record.startedAt,
39699
39751
  ...record.startedBy === undefined ? {} : { startedBy: record.startedBy },
39700
39752
  ...record.keeper === undefined ? {} : { keeper: record.keeper },
39753
+ ...record.profile === undefined ? {} : { profile: record.profile },
39701
39754
  ...record.command === undefined ? {} : { command: record.command },
39702
39755
  exitCode: null,
39703
39756
  exitSignal: null,
@@ -39850,12 +39903,16 @@ class GodotServer {
39850
39903
  }
39851
39904
  const limit = readPositiveNumber(args, "limit") ?? PROFILE_FUNCTIONS;
39852
39905
  const callersName = readString(args, "callersOf");
39853
- if (callersName !== undefined && !callersName.split(".").every(isIdentifier)) {
39906
+ if (callersName !== undefined && !callersName.replace(/@([^.@]+)_(?:getter|setter)$/, "$1").split(".").every(isIdentifier)) {
39854
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"]);
39855
39908
  }
39856
39909
  const going = await this.runStillGoing(run);
39857
39910
  const projectPath = run.projectPath;
39858
- 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 } : {
39859
39916
  runtimeAddon: runtimeAddonRegistered(projectPath),
39860
39917
  sourceOf: projectSourceOf(projectPath),
39861
39918
  ...callersName === undefined ? {} : { callersOf: { name: callersName, scripts: () => projectScripts(projectPath) } }
@@ -39863,8 +39920,7 @@ class GodotServer {
39863
39920
  if (run.throughEditor) {
39864
39921
  return await this.profileOfThePlayedGame(going, projectPath, fromProject, limit);
39865
39922
  }
39866
- const record = run.projectPath === null ? null : readRunRecord(run.projectPath);
39867
- const path = record !== null && record.pid === run.pid ? record.profile : undefined;
39923
+ const path = run.profile;
39868
39924
  if (path === undefined) {
39869
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"]);
39870
39926
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.2.7",
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",