gdharness 1.1.57 → 1.1.59

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
@@ -9155,6 +9155,44 @@ ${line.text}`;
9155
9155
  const { body } = parseBody(null, null, 0, 0, 0);
9156
9156
  return { className, body };
9157
9157
  }
9158
+ function indentOf(text) {
9159
+ return /^[ \t]*/.exec(text)?.[0].length ?? 0;
9160
+ }
9161
+ function enclosingFunction(source, line, code = codeOf(source)) {
9162
+ const lines = code.split(`
9163
+ `);
9164
+ const at = lines[line - 1];
9165
+ if (at === undefined) {
9166
+ return null;
9167
+ }
9168
+ const own = FUNC_LINE.exec(at);
9169
+ if (own?.[1] !== undefined) {
9170
+ return own[1];
9171
+ }
9172
+ let indent = indentOf(at);
9173
+ for (let index = line - 2;index >= 0 && indent > 0; index -= 1) {
9174
+ const text = lines[index] ?? "";
9175
+ if (text.trim() === "" || indentOf(text) >= indent) {
9176
+ continue;
9177
+ }
9178
+ indent = indentOf(text);
9179
+ const declared = FUNC_LINE.exec(text);
9180
+ if (declared?.[1] !== undefined) {
9181
+ return declared[1];
9182
+ }
9183
+ }
9184
+ return null;
9185
+ }
9186
+ function callSitesOf(source, name) {
9187
+ const regions = regionsOf(source);
9188
+ const code = codeOf(source, regions);
9189
+ const lines = new Lines(source);
9190
+ return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).map((one) => {
9191
+ const line = lines.positionOf(one.offset).line + 1;
9192
+ const after = code.slice(one.offset + name.length).trimStart();
9193
+ return { line, within: enclosingFunction(source, line, code), called: after.startsWith("(") };
9194
+ });
9195
+ }
9158
9196
  function collectEnumValues(text, from, into) {
9159
9197
  const close = text.indexOf("}");
9160
9198
  const inside = text.slice(1, close === -1 ? text.length : close);
@@ -9170,7 +9208,7 @@ function collectEnumValues(text, from, into) {
9170
9208
  function isIdentifier(name) {
9171
9209
  return new RegExp(`^${NAME}$`, "u").test(name) && !KEYWORDS.has(name);
9172
9210
  }
9173
- var IDENTIFIER_START, IDENTIFIER_PART, DECLARING, NAME = "[\\p{L}_][\\p{L}\\p{N}_]*", ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*", DECLARATION_LINE, CLASS_NAME_LINE, EXTENDS_LINE, EXTENDS_CLAUSE, KEYWORDS;
9211
+ var IDENTIFIER_START, IDENTIFIER_PART, DECLARING, NAME = "[\\p{L}_][\\p{L}\\p{N}_]*", ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*", DECLARATION_LINE, CLASS_NAME_LINE, EXTENDS_LINE, EXTENDS_CLAUSE, FUNC_LINE, KEYWORDS;
9174
9212
  var init_gdscript_source = __esm(() => {
9175
9213
  IDENTIFIER_START = /[\p{L}_]/u;
9176
9214
  IDENTIFIER_PART = /[\p{L}\p{N}_]/u;
@@ -9179,6 +9217,7 @@ var init_gdscript_source = __esm(() => {
9179
9217
  CLASS_NAME_LINE = new RegExp(`^${ANNOTATIONS}class_name\\s+(${NAME})`, "u");
9180
9218
  EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
9181
9219
  EXTENDS_CLAUSE = /\bextends(?=\s)/u;
9220
+ FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
9182
9221
  KEYWORDS = new Set([
9183
9222
  "and",
9184
9223
  "as",
@@ -18138,7 +18177,12 @@ var init_tool_definitions = __esm(() => {
18138
18177
  profile: {
18139
18178
  type: "boolean",
18140
18179
  ops: ["start"],
18141
- description: "start: profile the run's GDScript with the engine's own script profiler from its start, for editor_run profile to answer which functions took the time. A run this server starts dials the process that keeps it as its debugger, on loopback, with breakpoints and error breaks skipped so it never stops to wait; a game the editor plays is profiled through the editor's debugger session. Needs an engine built with debugging, which the editor binary is and a release export template is not. Default false."
18180
+ description: "start: profile the run's GDScript with the engine's own script profiler from its start, for editor_run profile to answer which functions took the time. A run this server starts dials the process that keeps it as its debugger, on loopback, with breakpoints and error breaks skipped so it never stops to wait; a game the editor plays is profiled through the editor's debugger session. Needs an engine built with debugging, which the editor binary is and a release export template is not. The start answers profile.on and, under profile.totals, how the numbers will arrive. Default false."
18181
+ },
18182
+ callersOf: {
18183
+ type: "string",
18184
+ ops: ["profile"],
18185
+ 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 and a call made through a string is not."
18142
18186
  },
18143
18187
  limit: {
18144
18188
  type: "integer",
@@ -18174,7 +18218,7 @@ var init_tool_definitions = __esm(() => {
18174
18218
  },
18175
18219
  wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
18176
18220
  profile: {
18177
- summary: "where the run spends its time by GDScript function, while it runs or after it ended: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, calls, and self and total milliseconds, sorted by self time, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a function the profiler started inside is left out, since its time would be counted from the engine's start. A run started with profile: true is profiled from its start. A game the editor plays that was not is profiled from the moment this is asked, and the answer says so; a run this server started without it is refused, since its debugger is fixed as it starts",
18221
+ 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 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",
18178
18222
  requires: []
18179
18223
  }
18180
18224
  },
@@ -18392,7 +18436,10 @@ var init_tool_definitions = __esm(() => {
18392
18436
  summary: "what one property reads on a node, or through the objects it holds, refusing a name nothing along the way has",
18393
18437
  requires: ["nodePath", "property"]
18394
18438
  },
18395
- metrics: { summary: "frame time, memory, draw calls and the rest", requires: [] }
18439
+ metrics: {
18440
+ summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game itself, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. The memory, object and render counts are read as asked",
18441
+ requires: []
18442
+ }
18396
18443
  },
18397
18444
  defaultOperation: "tree"
18398
18445
  },
@@ -42261,6 +42308,13 @@ function readProfileFile(path) {
42261
42308
  return null;
42262
42309
  }
42263
42310
  }
42311
+ function frameFunctionsFor(queued) {
42312
+ return Math.max(64, queued - QUEUE_HEADROOM);
42313
+ }
42314
+ function queuedMessagesOf(projectGodot) {
42315
+ const set = projectGodot === null ? undefined : parseProjectGodot(projectGodot)["network"]?.[QUEUE_SETTING];
42316
+ return typeof set === "number" && Number.isInteger(set) && set > 0 ? set : QUEUE_DEFAULT;
42317
+ }
42264
42318
  function signatureParts(signature) {
42265
42319
  const parts = signature.split("::");
42266
42320
  if (parts.length < 3) {
@@ -42270,8 +42324,18 @@ function signatureParts(signature) {
42270
42324
  const line = Number(parts.at(-2));
42271
42325
  return { script: parts.slice(0, -2).join("::"), line: Number.isFinite(line) ? line : 0, function: name };
42272
42326
  }
42273
- function profiledFunctions(totals, limit) {
42274
- const rows = Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
42327
+ function profiledFunctions(totals, limit, sourceOf = () => null) {
42328
+ const rows = rowsOf(totals);
42329
+ const scriptMs = ms(Object.values(totals.functions).reduce((sum, one) => sum + (one.calls > 0 ? one.selfSeconds : 0), 0));
42330
+ return {
42331
+ functions: rows.slice(0, limit).map((row) => placedLambda(row, sourceOf)),
42332
+ omitted: Math.max(0, rows.length - limit),
42333
+ scriptMs,
42334
+ counted: rows.length
42335
+ };
42336
+ }
42337
+ function rowsOf(totals) {
42338
+ return Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
42275
42339
  const parts = signatureParts(signature);
42276
42340
  return {
42277
42341
  script: parts.script,
@@ -42282,17 +42346,41 @@ function profiledFunctions(totals, limit) {
42282
42346
  totalMs: ms(sum.totalSeconds)
42283
42347
  };
42284
42348
  }).sort((a, b) => b.selfMs - a.selfMs || b.totalMs - a.totalMs || a.function.localeCompare(b.function));
42285
- const scriptMs = ms(Object.values(totals.functions).reduce((sum, one) => sum + (one.calls > 0 ? one.selfSeconds : 0), 0));
42286
- return {
42287
- functions: rows.slice(0, limit),
42288
- omitted: Math.max(0, rows.length - limit),
42289
- scriptMs,
42290
- counted: rows.length
42291
- };
42292
42349
  }
42293
- var ms = (seconds) => Math.round(seconds * 1e6) / 1000;
42350
+ function bareName(fn) {
42351
+ return fn.slice(fn.lastIndexOf(".") + 1);
42352
+ }
42353
+ function placedLambda(row, sourceOf) {
42354
+ if (!row.function.endsWith("(lambda)")) {
42355
+ return row;
42356
+ }
42357
+ const source = sourceOf(row.script);
42358
+ const within = source === null ? null : enclosingFunction(source, row.line);
42359
+ return within === null ? row : { ...row, within };
42360
+ }
42361
+ function callersOf(totals, name, scripts) {
42362
+ const rows = rowsOf(totals);
42363
+ const bare = bareName(name);
42364
+ const target = rows.filter((row) => name.includes(".") ? row.function === name : bareName(row.function) === bare);
42365
+ const sites = [];
42366
+ for (const [script, source] of scripts) {
42367
+ for (const site of callSitesOf(source, bare)) {
42368
+ const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
42369
+ sites.push({
42370
+ script,
42371
+ ...site,
42372
+ ...within === undefined ? {} : { withinCalls: within.calls, withinSelfMs: within.selfMs, withinTotalMs: within.totalMs }
42373
+ });
42374
+ }
42375
+ }
42376
+ sites.sort((a, b) => (b.withinTotalMs ?? -1) - (a.withinTotalMs ?? -1) || a.script.localeCompare(b.script) || a.line - b.line);
42377
+ return { target, sites: sites.slice(0, CALL_SITES), omitted: Math.max(0, sites.length - CALL_SITES) };
42378
+ }
42379
+ var QUEUE_SETTING = "limits/debugger/max_queued_messages", QUEUE_DEFAULT = 2048, QUEUE_HEADROOM = 512, UNNAMED_PREFIX = "#", ms = (seconds) => Math.round(seconds * 1e6) / 1000, CALL_SITES = 40;
42294
42380
  var init_script_profile = __esm(() => {
42381
+ init_gdscript_source();
42295
42382
  init_godot_variant();
42383
+ init_resources();
42296
42384
  });
42297
42385
 
42298
42386
  // src/server.ts
@@ -42425,13 +42513,19 @@ function playOf(answer) {
42425
42513
  const editorPid = readNumber(answer, "editorPid");
42426
42514
  return play === undefined || editorPid === undefined ? null : { play, editorPid };
42427
42515
  }
42428
- function runIsUp(run, editorSays, now = Date.now()) {
42516
+ function playedSince(run, playing) {
42517
+ return run.throughEditor && run.play !== undefined && playing !== null && (playing.play !== run.play.play || playing.editorPid !== run.play.editorPid);
42518
+ }
42519
+ function runIsUp(run, editorSays, editorPlay = null, now = Date.now()) {
42429
42520
  if (run?.throughEditor !== true) {
42430
42521
  return stillRunning(run);
42431
42522
  }
42432
42523
  if (typeof run.endedHere === "string") {
42433
42524
  return false;
42434
42525
  }
42526
+ if (playedSince(run, editorPlay)) {
42527
+ return false;
42528
+ }
42435
42529
  if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
42436
42530
  return false;
42437
42531
  }
@@ -42975,6 +43069,63 @@ function aboveTheRunner(entry) {
42975
43069
  ]
42976
43070
  };
42977
43071
  }
43072
+ function runtimeAddonRegistered(projectPath) {
43073
+ const registered = inspectProject(projectPath);
43074
+ return registered.runtimeAutoloadPath !== null || registered.runtimeLoaderAutoload !== null;
43075
+ }
43076
+ function profileOnStart(profiled, through, runtimeAddon) {
43077
+ if (!profiled) {
43078
+ return;
43079
+ }
43080
+ const debuggerOf = through === "editor" ? "the editor's debugger session" : "the process that keeps the run";
43081
+ return {
43082
+ on: true,
43083
+ totals: `On from the game's start, through ${debuggerOf}. editor_run profile answers at any time: each frame is added as it ends, so a scene still inside a long _ready shows nothing until that returns, and the engine's own totals replace the sum when the game quits` + (runtimeAddon ? "." : ", which takes the runtime addon: without it the frames that arrived before the exit are what is kept.")
43084
+ };
43085
+ }
43086
+ function profileFrameFunctions(projectPath) {
43087
+ let projectGodot = null;
43088
+ if (projectPath !== null) {
43089
+ try {
43090
+ projectGodot = readFileSync23(join26(projectPath, "project.godot"), "utf8");
43091
+ } catch {}
43092
+ }
43093
+ return frameFunctionsFor(queuedMessagesOf(projectGodot));
43094
+ }
43095
+ function playedCap(held) {
43096
+ const frameFunctions = readPositiveNumber(held, "frameFunctions");
43097
+ if (frameFunctions === undefined) {
43098
+ return {};
43099
+ }
43100
+ return {
43101
+ frameFunctions,
43102
+ cappedFrames: readNonNegativeNumber(held, "cappedFrames") ?? 0,
43103
+ totalCapped: readBoolean(held, "totalCapped") === true
43104
+ };
43105
+ }
43106
+ function projectSourceOf(projectPath) {
43107
+ const read = new Map;
43108
+ return (script) => {
43109
+ if (!read.has(script)) {
43110
+ const inside = resolveWithinProject(projectPath, script);
43111
+ let source = null;
43112
+ if (inside.ok) {
43113
+ try {
43114
+ source = readFileSync23(inside.absolutePath, "utf8");
43115
+ } catch {}
43116
+ }
43117
+ read.set(script, source);
43118
+ }
43119
+ return read.get(script) ?? null;
43120
+ };
43121
+ }
43122
+ function projectScripts(projectPath) {
43123
+ const scripts = [];
43124
+ eachScript(projectPath, (script, source) => {
43125
+ scripts.push([script, source]);
43126
+ });
43127
+ return scripts;
43128
+ }
42978
43129
  function profileAnswer(reading, limit) {
42979
43130
  const { totals } = reading;
42980
43131
  if (totals === null) {
@@ -42986,16 +43137,19 @@ function profileAnswer(reading, limit) {
42986
43137
  note: reading.running ? "The game has not connected to the profiler yet: it dials its debugger once, as it starts, before any script runs. Ask again in a moment." : "The game never connected to the profiler, so nothing was measured. A game dials its debugger once, as it starts, and an engine built without debugging (a release export template) refuses to: profiling needs the editor binary or a debug template."
42987
43138
  };
42988
43139
  }
42989
- const table = profiledFunctions(totals, limit);
43140
+ const table = profiledFunctions(totals, limit, reading.sourceOf);
42990
43141
  const notes = [];
42991
43142
  const addonNote = reading.runtimeAddon ? "" : " This project has no gdharness runtime addon, which switches the profiler off as the game quits so the engine sends its totals: setup installs it.";
42992
- if (totals.complete) {} else if (reading.running) {
43143
+ if (totals.complete) {} else if (reading.running && totals.frames === 0) {
43144
+ notes.push(`The profiler is on and the game has not finished its first frame yet: a frame is sent as it ends, so a scene doing its work in _ready shows nothing until that returns, and the engine's own totals arrive when the game quits.${addonNote}`);
43145
+ } else if (reading.running) {
42993
43146
  notes.push(`The run is still going, so these are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} summed so far${reading.through === "gdharness" ? ", written at most half a second behind" : ""}.`);
42994
43147
  } else if (totals.frames === 0) {
42995
43148
  notes.push(`The game connected and ended before a frame of the profile reached ${reading.through === "editor" ? "the editor" : "this server"}: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${addonNote}`);
42996
43149
  } else {
42997
43150
  notes.push(`These are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} that arrived, summed, without the engine's own totals, so what the game did after the last of them is not in it: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${addonNote}`);
42998
43151
  }
43152
+ notes.push(...cappedNotes(totals));
42999
43153
  if (totals.unreadable > 0) {
43000
43154
  notes.push(`${totals.unreadable} message${totals.unreadable === 1 ? "" : "s"} from the game could not be read, so the totals have that many gaps.`);
43001
43155
  }
@@ -43010,9 +43164,36 @@ function profileAnswer(reading, limit) {
43010
43164
  functionsCounted: table.counted,
43011
43165
  omitted: table.omitted > 0 ? table.omitted : undefined,
43012
43166
  functions: table.functions,
43167
+ callers: reading.callersOf === undefined ? undefined : callersAnswer(totals, reading.callersOf),
43013
43168
  note: notes.length > 0 ? notes.join(" ") : undefined
43014
43169
  };
43015
43170
  }
43171
+ function cappedNotes(totals) {
43172
+ const notes = [];
43173
+ const most = totals.frameFunctions;
43174
+ 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.`;
43175
+ if (most !== undefined && totals.complete && totals.totalCapped === true) {
43176
+ 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}`);
43177
+ } else if (most !== undefined && !totals.complete && (totals.cappedFrames ?? 0) > 0) {
43178
+ const capped = totals.cappedFrames ?? 0;
43179
+ 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}`);
43180
+ }
43181
+ const unnamed = Object.entries(totals.functions).filter(([name, sum]) => name.startsWith(UNNAMED_PREFIX) && sum.calls > 0).length;
43182
+ if (unnamed > 0) {
43183
+ 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.`);
43184
+ }
43185
+ return notes;
43186
+ }
43187
+ function callersAnswer(totals, asked) {
43188
+ const found = callersOf(totals, asked.name, asked.scripts());
43189
+ return {
43190
+ of: asked.name,
43191
+ rows: found.target,
43192
+ sites: found.sites,
43193
+ omitted: found.omitted > 0 ? found.omitted : undefined,
43194
+ 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
43195
+ };
43196
+ }
43016
43197
  function foldedEntriesAnswer(log) {
43017
43198
  const problems = log.selectFolded({
43018
43199
  severity: "warning",
@@ -43586,7 +43767,7 @@ class GodotServer {
43586
43767
  return this.saying(answer, {
43587
43768
  update_available: {
43588
43769
  ...notice,
43589
- what_to_do: "Tell the user a newer gdharness is out, with what changed. Run the upgrade command " + "only with their agreement: a yes now, or a standing instruction of theirs, such as " + "their CLAUDE.md, to upgrade on every release. Without either, offer it and wait, since " + "it replaces the addons in their project. Afterwards the MCP server has to be " + "reconnected and the editor restarted with editor_launch restart."
43770
+ what_to_do: "Tell the user a newer gdharness is out, with what changed. Run the upgrade command " + "only with their agreement: a yes now, or a standing instruction of theirs, such as one " + "in their AGENTS.md or CLAUDE.md, to upgrade on every release. Without either, offer it " + "and wait, since it replaces the addons in their project. Afterwards the MCP server has " + "to be reconnected and the editor restarted with editor_launch restart."
43590
43771
  }
43591
43772
  });
43592
43773
  }
@@ -45822,6 +46003,7 @@ class GodotServer {
45822
46003
  pid: started.pid,
45823
46004
  arguments: cmdArgs,
45824
46005
  transcript: started.transcript,
46006
+ profile: profileOnStart(profiled, "gdharness", runtimeAddonRegistered(project.value.path)),
45825
46007
  refreshedClasses: refreshed.value,
45826
46008
  ...scanWaitAnswer(scanned),
45827
46009
  savesNote: asked.value.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
@@ -45948,7 +46130,10 @@ class GodotServer {
45948
46130
  const breakpoints = await this.dapHoldingBreakpointsOf(projectPath).reapplyBreakpoints();
45949
46131
  this.logDebug(`Breakpoints were sent again after ${Date.now() - sendingFrom}ms`);
45950
46132
  if (profiled) {
45951
- const armed = await this.handleViaBridge("profile_start", { arm: true });
46133
+ const armed = await this.handleViaBridge("profile_start", {
46134
+ arm: true,
46135
+ frameFunctions: profileFrameFunctions(projectPath)
46136
+ });
45952
46137
  if (armed.isError === true) {
45953
46138
  return this.profilerRefusal(armed);
45954
46139
  }
@@ -45961,6 +46146,7 @@ class GodotServer {
45961
46146
  const transcript = openTranscript(startedAt);
45962
46147
  closeSync2(transcript.fd);
45963
46148
  const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
46149
+ const which = playOf(playAnswer);
45964
46150
  const played = {
45965
46151
  pid: null,
45966
46152
  log,
@@ -45973,11 +46159,11 @@ class GodotServer {
45973
46159
  throughEditor: true,
45974
46160
  brokeOn: null,
45975
46161
  announcedBefore: alreadyPlaying,
45976
- seenPlaying: readBoolean(playAnswer, "playing") === true
46162
+ seenPlaying: readBoolean(playAnswer, "playing") === true,
46163
+ ...which === null ? {} : { play: which }
45977
46164
  };
45978
46165
  this.activeProcess = played;
45979
46166
  this.watchThePlayedConsole();
45980
- const which = playOf(playAnswer);
45981
46167
  writeEditorRunNote({
45982
46168
  projectPath,
45983
46169
  transcript: transcript.path,
@@ -45989,6 +46175,7 @@ class GodotServer {
45989
46175
  started: true,
45990
46176
  through: "editor",
45991
46177
  scene: scene === null ? "the main scene" : `res://${scene}`,
46178
+ profile: profileOnStart(profiled, "editor", runtimeAddonRegistered(projectPath)),
45992
46179
  refreshedClasses,
45993
46180
  ...scanWaitAnswer(scanned),
45994
46181
  debugPort: readNumber(playAnswer, "debugPort"),
@@ -46371,9 +46558,14 @@ class GodotServer {
46371
46558
  return;
46372
46559
  }
46373
46560
  const going = this.activeProcess;
46374
- const givesWay = going !== null && !stillRunning(going) && typeof going.endedHere === "string";
46561
+ const asked = going?.throughEditor === true && going.play !== undefined ? await this.editorPlayingState() : null;
46562
+ const superseded = going !== null && asked !== null && playedSince(going, asked.play);
46563
+ if (superseded && stillRunning(going)) {
46564
+ going.endedUnwatched = true;
46565
+ }
46566
+ const givesWay = going !== null && (superseded || !stillRunning(going) && typeof going.endedHere === "string");
46375
46567
  if (going !== null && !givesWay) {
46376
- if (going.throughEditor && going.consoleLost === true && (await this.editorPlayingState())?.playing) {
46568
+ if (going.throughEditor && going.consoleLost === true && (asked ?? await this.editorPlayingState())?.playing) {
46377
46569
  try {
46378
46570
  await this.dap().connect();
46379
46571
  going.log.append("stdout", `[gdharness] the debug adapter was reconnected here
@@ -46384,7 +46576,7 @@ class GodotServer {
46384
46576
  }
46385
46577
  return;
46386
46578
  }
46387
- const playing = await this.editorPlayingState();
46579
+ const playing = asked ?? await this.editorPlayingState();
46388
46580
  if (playing?.playing !== true) {
46389
46581
  return;
46390
46582
  }
@@ -46407,7 +46599,8 @@ class GodotServer {
46407
46599
  exitSignal: null,
46408
46600
  throughEditor: true,
46409
46601
  brokeOn: null,
46410
- pickedUpPlaying: true
46602
+ pickedUpPlaying: true,
46603
+ ...playing.play === null ? {} : { play: playing.play }
46411
46604
  };
46412
46605
  this.activeProcess = picked;
46413
46606
  this.watchThePlayedConsole();
@@ -46711,11 +46904,19 @@ class GodotServer {
46711
46904
  return this.createErrorResponse(this.nothingOfOursIsRunning());
46712
46905
  }
46713
46906
  const limit = readPositiveNumber(args, "limit") ?? PROFILE_FUNCTIONS;
46907
+ const callersName = readString(args, "callersOf");
46908
+ if (callersName !== undefined && !callersName.split(".").every(isIdentifier)) {
46909
+ 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"]);
46910
+ }
46714
46911
  const going = await this.runStillGoing(run);
46715
- const registered = run.projectPath === null ? null : inspectProject(run.projectPath);
46716
- const runtimeAddon = registered !== null && (registered.runtimeAutoloadPath !== null || registered.runtimeLoaderAutoload !== null);
46912
+ const projectPath = run.projectPath;
46913
+ const fromProject = projectPath === null ? { runtimeAddon: false } : {
46914
+ runtimeAddon: runtimeAddonRegistered(projectPath),
46915
+ sourceOf: projectSourceOf(projectPath),
46916
+ ...callersName === undefined ? {} : { callersOf: { name: callersName, scripts: () => projectScripts(projectPath) } }
46917
+ };
46717
46918
  if (run.throughEditor) {
46718
- return await this.profileOfThePlayedGame(going, runtimeAddon, limit);
46919
+ return await this.profileOfThePlayedGame(going, projectPath, fromProject, limit);
46719
46920
  }
46720
46921
  const record = run.projectPath === null ? null : readRunRecord(run.projectPath);
46721
46922
  const path = record !== null && record.pid === run.pid ? record.profile : undefined;
@@ -46734,10 +46935,10 @@ class GodotServer {
46734
46935
  running: going,
46735
46936
  totals,
46736
46937
  coveredMs: totals?.lastFrameAt == null ? 0 : totals.lastFrameAt - totals.startedAt,
46737
- runtimeAddon
46938
+ ...fromProject
46738
46939
  }, limit));
46739
46940
  }
46740
- async profileOfThePlayedGame(going, runtimeAddon, limit) {
46941
+ async profileOfThePlayedGame(going, projectPath, fromProject, limit) {
46741
46942
  const read = await this.handleViaBridge("profile_read", {});
46742
46943
  if (read.isError === true) {
46743
46944
  return this.profilerRefusal(read);
@@ -46749,7 +46950,9 @@ class GodotServer {
46749
46950
  "editor_run start with profile: true plays it again with the profiler on from its start"
46750
46951
  ]);
46751
46952
  }
46752
- const started = await this.handleViaBridge("profile_start", {});
46953
+ const started = await this.handleViaBridge("profile_start", {
46954
+ frameFunctions: profileFrameFunctions(projectPath)
46955
+ });
46753
46956
  if (started.isError === true) {
46754
46957
  return this.profilerRefusal(started);
46755
46958
  }
@@ -46772,6 +46975,7 @@ class GodotServer {
46772
46975
  frames: readNonNegativeNumber(held, "frames") ?? 0,
46773
46976
  complete: readBoolean(held, "complete") === true,
46774
46977
  unreadable: 0,
46978
+ ...playedCap(held),
46775
46979
  functions: Object.fromEntries(Object.entries(functions).map(([signature, sum]) => {
46776
46980
  const [calls, self, total] = Array.isArray(sum) ? sum : [];
46777
46981
  return [
@@ -46789,7 +46993,7 @@ class GodotServer {
46789
46993
  running: going,
46790
46994
  totals,
46791
46995
  coveredMs: readNonNegativeNumber(held, "coveredMs") ?? 0,
46792
- runtimeAddon
46996
+ ...fromProject
46793
46997
  }, limit));
46794
46998
  }
46795
46999
  async firstProfiledFrame() {
@@ -46848,11 +47052,13 @@ class GodotServer {
46848
47052
  if (announced !== undefined && editorSays === undefined) {
46849
47053
  return stillRunning(run);
46850
47054
  }
46851
- const said = editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays;
46852
- if (said === true) {
47055
+ const state = editorSays === undefined ? await this.editorPlayingState() : undefined;
47056
+ const said = state === undefined ? editorSays ?? null : state?.playing ?? null;
47057
+ const play = state?.play ?? null;
47058
+ if (said === true && !playedSince(run, play)) {
46853
47059
  run.seenPlaying = true;
46854
47060
  }
46855
- return runIsUp(run, said);
47061
+ return runIsUp(run, said, play);
46856
47062
  }
46857
47063
  async untilTheExitIsRecorded(run) {
46858
47064
  const until = Date.now() + EXIT_REPORTED_WITHIN_MS;
@@ -48058,7 +48264,7 @@ async function runGodotServer() {
48058
48264
  const server = new GodotServer;
48059
48265
  await server.run();
48060
48266
  }
48061
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "';
48267
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "';
48062
48268
  var init_server3 = __esm(() => {
48063
48269
  init_mcp();
48064
48270
  init_stdio2();
@@ -48079,6 +48285,7 @@ var init_server3 = __esm(() => {
48079
48285
  init_engine_run();
48080
48286
  init_errors();
48081
48287
  init_game_log();
48288
+ init_gdscript_source();
48082
48289
  init_godot_bridge();
48083
48290
  init_godot_path();
48084
48291
  init_headless();
@@ -49189,7 +49396,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
49189
49396
  | What every label on a screen says | \`runtime_inspect tree\` with \`properties\` naming \`text\`, which reads it off every node that has one; \`includeProperties\` is every stored property of every node and ran to seventy-six thousand characters for one row |
49190
49397
  | Whether this engine has a method or property, and what it takes | \`editor_classes info\` with \`member\`, which answers that one member and which class declares it rather than the whole class |
49191
49398
  | Wait for a run to finish | \`editor_run wait\`, never a sleep or a shell loop on the pid: it answers as \`editor_output\` does, once the run is over |
49192
- | Find which GDScript makes a run slow | \`editor_run start\` with \`profile\` set, then \`editor_run profile\`: the engine's own script profiler, per function with calls and self and total milliseconds, by self time, never \`Time.get_ticks_usec()\` laps edited into the game. A scene that does its work in \`_ready\` and quits is covered whole when the runtime autoload is registered, and a game the editor is already playing is profiled from the moment it is asked |
49399
+ | Find which GDScript makes a run slow | \`editor_run start\` with \`profile\` set, then \`editor_run profile\`: the engine's own script profiler, per function with calls and self and total milliseconds, by self time, never \`Time.get_ticks_usec()\` laps edited into the game. A scene that does its work in \`_ready\` and quits is covered whole when the runtime autoload is registered, and a game the editor is already playing is profiled from the moment it is asked. \`callersOf\` names where the project's scripts call one function, each place with its function's numbers, since the engine records no callers |
49193
49400
  | End a bench and the workers it opened | \`editor_run stop\` with \`andChildren\`, never a process listing by scene name: it ends every process under the run's, to any depth, that is a game of the project, announced as one or the project's engine run with \`--path\` on it, names them under \`endedChildren\` with what identified each, and leaves and names any other |
49194
49401
  | End a game of the project that no server is holding | \`editor_run stop\` with its \`pid\` from \`editor_status\` \`runtimes\`, never a process kill by hand: it is ended only when it announced itself for the project and its command line is the project's engine run |
49195
49402
  | Play a game without touching the player's saves | \`editor_run start\` with \`savesIn\`, a directory for this run's \`user://\`: the editor plays with its own environment, so a run given one is started by the server, and the answer says under \`savesNote\` when the platform ignores it |
@@ -127,7 +127,10 @@ func stop_playing(_args: Dictionary) -> Dictionary:
127
127
  var was_playing: bool = EditorInterface.is_playing_scene()
128
128
  if was_playing:
129
129
  EditorInterface.stop_playing_scene()
130
- return {"ok": true, "wasPlaying": was_playing, "playing": EditorInterface.is_playing_scene()}
130
+ # Known over now rather than at the next frame's look: a play somebody else starts before that
131
+ # look would otherwise be read as this one still going, and given its number and its console.
132
+ _was_playing = EditorInterface.is_playing_scene()
133
+ return {"ok": true, "wasPlaying": was_playing, "playing": _was_playing}
131
134
 
132
135
 
133
136
  ## Restarts the editor, which is how a replaced addon is picked up.