gdharness 1.1.58 → 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
@@ -18436,7 +18436,10 @@ var init_tool_definitions = __esm(() => {
18436
18436
  summary: "what one property reads on a node, or through the objects it holds, refusing a name nothing along the way has",
18437
18437
  requires: ["nodePath", "property"]
18438
18438
  },
18439
- 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
+ }
18440
18443
  },
18441
18444
  defaultOperation: "tree"
18442
18445
  },
@@ -42305,6 +42308,13 @@ function readProfileFile(path) {
42305
42308
  return null;
42306
42309
  }
42307
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
+ }
42308
42318
  function signatureParts(signature) {
42309
42319
  const parts = signature.split("::");
42310
42320
  if (parts.length < 3) {
@@ -42366,10 +42376,11 @@ function callersOf(totals, name, scripts) {
42366
42376
  sites.sort((a, b) => (b.withinTotalMs ?? -1) - (a.withinTotalMs ?? -1) || a.script.localeCompare(b.script) || a.line - b.line);
42367
42377
  return { target, sites: sites.slice(0, CALL_SITES), omitted: Math.max(0, sites.length - CALL_SITES) };
42368
42378
  }
42369
- var ms = (seconds) => Math.round(seconds * 1e6) / 1000, CALL_SITES = 40;
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;
42370
42380
  var init_script_profile = __esm(() => {
42371
42381
  init_gdscript_source();
42372
42382
  init_godot_variant();
42383
+ init_resources();
42373
42384
  });
42374
42385
 
42375
42386
  // src/server.ts
@@ -43072,6 +43083,26 @@ function profileOnStart(profiled, through, runtimeAddon) {
43072
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.")
43073
43084
  };
43074
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
+ }
43075
43106
  function projectSourceOf(projectPath) {
43076
43107
  const read = new Map;
43077
43108
  return (script) => {
@@ -43118,6 +43149,7 @@ function profileAnswer(reading, limit) {
43118
43149
  } else {
43119
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}`);
43120
43151
  }
43152
+ notes.push(...cappedNotes(totals));
43121
43153
  if (totals.unreadable > 0) {
43122
43154
  notes.push(`${totals.unreadable} message${totals.unreadable === 1 ? "" : "s"} from the game could not be read, so the totals have that many gaps.`);
43123
43155
  }
@@ -43136,6 +43168,22 @@ function profileAnswer(reading, limit) {
43136
43168
  note: notes.length > 0 ? notes.join(" ") : undefined
43137
43169
  };
43138
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
+ }
43139
43187
  function callersAnswer(totals, asked) {
43140
43188
  const found = callersOf(totals, asked.name, asked.scripts());
43141
43189
  return {
@@ -46082,7 +46130,10 @@ class GodotServer {
46082
46130
  const breakpoints = await this.dapHoldingBreakpointsOf(projectPath).reapplyBreakpoints();
46083
46131
  this.logDebug(`Breakpoints were sent again after ${Date.now() - sendingFrom}ms`);
46084
46132
  if (profiled) {
46085
- 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
+ });
46086
46137
  if (armed.isError === true) {
46087
46138
  return this.profilerRefusal(armed);
46088
46139
  }
@@ -46865,7 +46916,7 @@ class GodotServer {
46865
46916
  ...callersName === undefined ? {} : { callersOf: { name: callersName, scripts: () => projectScripts(projectPath) } }
46866
46917
  };
46867
46918
  if (run.throughEditor) {
46868
- return await this.profileOfThePlayedGame(going, fromProject, limit);
46919
+ return await this.profileOfThePlayedGame(going, projectPath, fromProject, limit);
46869
46920
  }
46870
46921
  const record = run.projectPath === null ? null : readRunRecord(run.projectPath);
46871
46922
  const path = record !== null && record.pid === run.pid ? record.profile : undefined;
@@ -46887,7 +46938,7 @@ class GodotServer {
46887
46938
  ...fromProject
46888
46939
  }, limit));
46889
46940
  }
46890
- async profileOfThePlayedGame(going, fromProject, limit) {
46941
+ async profileOfThePlayedGame(going, projectPath, fromProject, limit) {
46891
46942
  const read = await this.handleViaBridge("profile_read", {});
46892
46943
  if (read.isError === true) {
46893
46944
  return this.profilerRefusal(read);
@@ -46899,7 +46950,9 @@ class GodotServer {
46899
46950
  "editor_run start with profile: true plays it again with the profiler on from its start"
46900
46951
  ]);
46901
46952
  }
46902
- const started = await this.handleViaBridge("profile_start", {});
46953
+ const started = await this.handleViaBridge("profile_start", {
46954
+ frameFunctions: profileFrameFunctions(projectPath)
46955
+ });
46903
46956
  if (started.isError === true) {
46904
46957
  return this.profilerRefusal(started);
46905
46958
  }
@@ -46922,6 +46975,7 @@ class GodotServer {
46922
46975
  frames: readNonNegativeNumber(held, "frames") ?? 0,
46923
46976
  complete: readBoolean(held, "complete") === true,
46924
46977
  unreadable: 0,
46978
+ ...playedCap(held),
46925
46979
  functions: Object.fromEntries(Object.entries(functions).map(([signature, sum]) => {
46926
46980
  const [calls, self, total] = Array.isArray(sum) ? sum : [];
46927
46981
  return [
@@ -12,9 +12,10 @@ extends Node
12
12
 
13
13
  const Read = preload("../reading.gd")
14
14
 
15
- ## The engine sends only the top so many functions a frame and clamps nothing on the game's side.
16
- ## Its own buffer's default, so no frame is cut short of what was called.
17
- const FRAME_FUNCTIONS: int = 16384
15
+ ## The most functions a frame may carry when the server does not say. The engine names a function
16
+ ## once, as it first appears, and the game's debugger queue drops those messages past its size, so
17
+ ## the server keeps this below the queue the project sets; this is that for the engine's default.
18
+ const FRAME_FUNCTIONS: int = 1536
18
19
  const DEBUGGER_CLASS: String = "ScriptEditorDebugger"
19
20
 
20
21
 
@@ -44,6 +45,10 @@ var _started_msec: int = 0
44
45
  var _last_frame_msec: int = -1
45
46
  var _frames: int = 0
46
47
  var _complete: bool = false
48
+ var _frame_functions: int = FRAME_FUNCTIONS
49
+ ## Frames that carried as many functions as were asked for, and whether the totals did.
50
+ var _capped_frames: int = 0
51
+ var _total_capped: bool = false
47
52
  var _names: Dictionary[int, String] = {}
48
53
  ## Per signature: calls, self seconds, total seconds.
49
54
  var _totals: Dictionary[String, PackedFloat64Array] = {}
@@ -81,6 +86,8 @@ func profile_start(args: Dictionary) -> Dictionary:
81
86
  ),
82
87
  }
83
88
  _reset()
89
+ var asked: int = Read.as_int(args.get("frameFunctions", FRAME_FUNCTIONS))
90
+ _frame_functions = asked if asked > 0 else FRAME_FUNCTIONS
84
91
  if Read.as_bool(args.get("arm", false)):
85
92
  _armed = true
86
93
  return {"ok": true, "started": false, "armed": true, "session": -1}
@@ -104,6 +111,9 @@ func profile_read(_args: Dictionary) -> Dictionary:
104
111
  "playing": EditorInterface.is_playing_scene(),
105
112
  "frames": _frames,
106
113
  "complete": _complete,
114
+ "frameFunctions": _frame_functions,
115
+ "cappedFrames": _capped_frames,
116
+ "totalCapped": _total_capped,
107
117
  "coveredMs": _last_frame_msec - _started_msec if _last_frame_msec >= 0 else 0,
108
118
  "functions": functions,
109
119
  }
@@ -114,6 +124,8 @@ func _reset() -> void:
114
124
  _session = -1
115
125
  _frames = 0
116
126
  _complete = false
127
+ _capped_frames = 0
128
+ _total_capped = false
117
129
  _last_frame_msec = -1
118
130
  _names.clear()
119
131
  _totals.clear()
@@ -133,7 +145,7 @@ func _switch_on(session_id: int) -> void:
133
145
  _session = session_id
134
146
  _started_msec = Time.get_ticks_msec()
135
147
  var session: EditorDebuggerSession = _sessions.get_session(session_id)
136
- session.toggle_profiler("servers", true, [FRAME_FUNCTIONS, false])
148
+ session.toggle_profiler("servers", true, [_frame_functions, false])
137
149
 
138
150
 
139
151
  ## A play that was asked to be profiled is switched on as it starts. Any other play drops what is
@@ -170,37 +182,42 @@ func _on_debug_data(message: String, data: Array, session_id: int) -> void:
170
182
  if message == "servers:function_signature" and data.size() >= 2:
171
183
  _names[Read.as_int(data[1])] = str(data[0])
172
184
  elif message == "servers:profile_frame":
173
- if _add(data, _totals):
185
+ var carried: int = _add(data, _totals)
186
+ if carried >= 0:
174
187
  _frames += 1
188
+ _capped_frames += 1 if carried >= _frame_functions else 0
175
189
  _last_frame_msec = Time.get_ticks_msec()
176
190
  elif message == "servers:profile_total":
177
191
  # The engine's own count since the profiler started, so it replaces the frames' sum.
178
192
  var whole: Dictionary[String, PackedFloat64Array] = {}
179
- if _add(data, whole):
193
+ var carried: int = _add(data, whole)
194
+ if carried >= 0:
180
195
  _totals = whole
181
196
  _complete = true
197
+ _total_capped = carried >= _frame_functions
182
198
  _last_frame_msec = Time.get_ticks_msec()
183
199
 
184
200
 
185
201
  ## Adds one frame's functions into [param into], reading past the servers' section to reach them:
186
202
  ## six frame times, the server count, each server's name and field count and fields, then five
187
- ## numbers per function.
188
- func _add(data: Array, into: Dictionary[String, PackedFloat64Array]) -> bool:
203
+ ## numbers per function. Answers how many functions it carried, or -1 for a frame it cannot read.
204
+ func _add(data: Array, into: Dictionary[String, PackedFloat64Array]) -> int:
189
205
  var at: int = 6
190
206
  if data.size() <= at:
191
- return false
207
+ return -1
192
208
  var servers: int = Read.as_int(data[at])
193
209
  at += 1
194
210
  for _server: int in servers:
195
211
  if data.size() <= at + 1:
196
- return false
212
+ return -1
197
213
  at += 2 + Read.as_int(data[at + 1])
198
214
  if data.size() <= at:
199
- return false
215
+ return -1
200
216
  var values: int = Read.as_int(data[at])
201
217
  at += 1
202
218
  if values % 5 != 0 or data.size() < at + values:
203
- return false
219
+ return -1
220
+ var carried: int = 0
204
221
  for index: int in range(at, at + values, 5):
205
222
  var id: int = Read.as_int(data[index])
206
223
  var signature: String = _names[id] if _names.has(id) else "#%d" % id
@@ -211,4 +228,5 @@ func _add(data: Array, into: Dictionary[String, PackedFloat64Array]) -> bool:
211
228
  sum[1] += Read.as_float(data[index + 2])
212
229
  sum[2] += Read.as_float(data[index + 3])
213
230
  into[signature] = sum
214
- return true
231
+ carried += 1
232
+ return carried
@@ -172,6 +172,7 @@ func _report_errors() -> void:
172
172
  func _process(_delta: float) -> void:
173
173
  if not _enabled or _server == null:
174
174
  return
175
+ _queries.note_frame(Time.get_ticks_usec())
175
176
 
176
177
  if _server.is_connection_available():
177
178
  var client: StreamPeerTCP = _server.take_connection()
@@ -28,8 +28,14 @@ const READ_LIMIT: int = 500
28
28
  ## and said nothing, and a caller told to raise a number that stays put concludes the screen moved.
29
29
  const LIMIT_CEILING: int = 5000
30
30
 
31
+ ## How many frames the frame time is the mean of: two seconds at sixty frames a second.
32
+ const FRAMES_TIMED: int = 120
33
+
31
34
  var _host: Node
32
35
  var _values: Values
36
+ ## How long each of the last frames took, oldest first, in microseconds, as the game timed them.
37
+ var _frame_usec: PackedInt64Array = PackedInt64Array()
38
+ var _last_frame_usec: int = 0
33
39
 
34
40
 
35
41
  ## The host is the autoload, which is how the tree is reached: it is not in one when the
@@ -649,12 +655,60 @@ func get_property(params: Dictionary) -> Dictionary:
649
655
  }
650
656
 
651
657
 
658
+ ## Called by the autoload once a frame, whatever else it does that frame, with the time it was called.
659
+ ##
660
+ ## The engine's own monitors do not follow the frame: TIME_PROCESS and the frame rate are set once a
661
+ ## second, the first to the slowest process step of that second, so a game whose frames took 16.7ms
662
+ ## read 45ms for a second at a time, and a change to the game showed only on the next second's
663
+ ## worst frame. The time between two of these calls is the frame as it passed, physics and the wait
664
+ ## for the display included.
665
+ func note_frame(now_usec: int) -> void:
666
+ if _last_frame_usec > 0:
667
+ var _grew: bool = _frame_usec.push_back(now_usec - _last_frame_usec)
668
+ if _frame_usec.size() > FRAMES_TIMED:
669
+ _frame_usec.remove_at(0)
670
+ _last_frame_usec = now_usec
671
+
672
+
673
+ ## Whether the frame waits for the display, which sets its floor: a game with vsync on cannot
674
+ ## finish a frame sooner than the display refreshes, however little it does.
675
+ static func _vsync() -> String:
676
+ if DisplayServer.get_name() == "headless":
677
+ return "none, headless"
678
+ match DisplayServer.window_get_vsync_mode():
679
+ DisplayServer.VSYNC_DISABLED:
680
+ return "disabled"
681
+ DisplayServer.VSYNC_ADAPTIVE:
682
+ return "adaptive"
683
+ DisplayServer.VSYNC_MAILBOX:
684
+ return "mailbox"
685
+ _:
686
+ return "enabled"
687
+
688
+
652
689
  func get_metrics(params: Dictionary) -> Dictionary:
653
690
  var metrics: Array = params.get("metrics", [])
691
+ var timed: int = _frame_usec.size()
692
+ var total_usec: int = 0
693
+ var slowest_usec: int = 0
694
+ for one: int in _frame_usec:
695
+ total_usec += one
696
+ slowest_usec = maxi(slowest_usec, one)
697
+ # Null rather than zero before a frame has been timed: no frame took no time.
698
+ var frame_time: Variant = null
699
+ var frame_time_max: Variant = null
700
+ if timed > 0:
701
+ frame_time = float(total_usec) / timed / 1000000.0
702
+ frame_time_max = slowest_usec / 1000000.0
654
703
  var all: Dictionary = {
655
704
  "fps": Engine.get_frames_per_second(),
656
- "frame_time": Performance.get_monitor(Performance.TIME_PROCESS),
705
+ "frame_time": frame_time,
706
+ "frame_time_max": frame_time_max,
707
+ "frames_timed": timed,
708
+ "process_time_max": Performance.get_monitor(Performance.TIME_PROCESS),
657
709
  "physics_time": Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS),
710
+ "vsync": _vsync(),
711
+ "max_fps": Engine.max_fps,
658
712
  "memory_static": Performance.get_monitor(Performance.MEMORY_STATIC),
659
713
  "memory_static_max": Performance.get_monitor(Performance.MEMORY_STATIC_MAX),
660
714
  "object_count": Performance.get_monitor(Performance.OBJECT_COUNT),
package/build/index.js CHANGED
@@ -33101,6 +33101,17 @@ function readProfileFile(path) {
33101
33101
  return null;
33102
33102
  }
33103
33103
  }
33104
+ var QUEUE_SETTING = "limits/debugger/max_queued_messages";
33105
+ var QUEUE_DEFAULT = 2048;
33106
+ var QUEUE_HEADROOM = 512;
33107
+ function frameFunctionsFor(queued) {
33108
+ return Math.max(64, queued - QUEUE_HEADROOM);
33109
+ }
33110
+ function queuedMessagesOf(projectGodot) {
33111
+ const set = projectGodot === null ? undefined : parseProjectGodot(projectGodot)["network"]?.[QUEUE_SETTING];
33112
+ return typeof set === "number" && Number.isInteger(set) && set > 0 ? set : QUEUE_DEFAULT;
33113
+ }
33114
+ var UNNAMED_PREFIX = "#";
33104
33115
  function signatureParts(signature) {
33105
33116
  const parts = signature.split("::");
33106
33117
  if (parts.length < 3) {
@@ -34406,7 +34417,10 @@ var TOOL_SPECS = [
34406
34417
  summary: "what one property reads on a node, or through the objects it holds, refusing a name nothing along the way has",
34407
34418
  requires: ["nodePath", "property"]
34408
34419
  },
34409
- metrics: { summary: "frame time, memory, draw calls and the rest", requires: [] }
34420
+ metrics: {
34421
+ 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",
34422
+ requires: []
34423
+ }
34410
34424
  },
34411
34425
  defaultOperation: "tree"
34412
34426
  },
@@ -35562,6 +35576,26 @@ function profileOnStart(profiled, through, runtimeAddon) {
35562
35576
  };
35563
35577
  }
35564
35578
  var PROFILER_ON_MS = 3000;
35579
+ function profileFrameFunctions(projectPath) {
35580
+ let projectGodot = null;
35581
+ if (projectPath !== null) {
35582
+ try {
35583
+ projectGodot = readFileSync21(join22(projectPath, "project.godot"), "utf8");
35584
+ } catch {}
35585
+ }
35586
+ return frameFunctionsFor(queuedMessagesOf(projectGodot));
35587
+ }
35588
+ function playedCap(held) {
35589
+ const frameFunctions = readPositiveNumber(held, "frameFunctions");
35590
+ if (frameFunctions === undefined) {
35591
+ return {};
35592
+ }
35593
+ return {
35594
+ frameFunctions,
35595
+ cappedFrames: readNonNegativeNumber(held, "cappedFrames") ?? 0,
35596
+ totalCapped: readBoolean(held, "totalCapped") === true
35597
+ };
35598
+ }
35565
35599
  var CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too. Each place carries the profile's numbers for the function it is written in.";
35566
35600
  function projectSourceOf(projectPath) {
35567
35601
  const read = new Map;
@@ -35609,6 +35643,7 @@ function profileAnswer(reading, limit) {
35609
35643
  } else {
35610
35644
  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}`);
35611
35645
  }
35646
+ notes.push(...cappedNotes(totals));
35612
35647
  if (totals.unreadable > 0) {
35613
35648
  notes.push(`${totals.unreadable} message${totals.unreadable === 1 ? "" : "s"} from the game could not be read, so the totals have that many gaps.`);
35614
35649
  }
@@ -35627,6 +35662,22 @@ function profileAnswer(reading, limit) {
35627
35662
  note: notes.length > 0 ? notes.join(" ") : undefined
35628
35663
  };
35629
35664
  }
35665
+ function cappedNotes(totals) {
35666
+ const notes = [];
35667
+ const most = totals.frameFunctions;
35668
+ 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.`;
35669
+ if (most !== undefined && totals.complete && totals.totalCapped === true) {
35670
+ 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}`);
35671
+ } else if (most !== undefined && !totals.complete && (totals.cappedFrames ?? 0) > 0) {
35672
+ const capped = totals.cappedFrames ?? 0;
35673
+ 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}`);
35674
+ }
35675
+ const unnamed = Object.entries(totals.functions).filter(([name, sum]) => name.startsWith(UNNAMED_PREFIX) && sum.calls > 0).length;
35676
+ if (unnamed > 0) {
35677
+ 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.`);
35678
+ }
35679
+ return notes;
35680
+ }
35630
35681
  function callersAnswer(totals, asked) {
35631
35682
  const found = callersOf(totals, asked.name, asked.scripts());
35632
35683
  return {
@@ -38576,7 +38627,10 @@ class GodotServer {
38576
38627
  const breakpoints = await this.dapHoldingBreakpointsOf(projectPath).reapplyBreakpoints();
38577
38628
  this.logDebug(`Breakpoints were sent again after ${Date.now() - sendingFrom}ms`);
38578
38629
  if (profiled) {
38579
- const armed = await this.handleViaBridge("profile_start", { arm: true });
38630
+ const armed = await this.handleViaBridge("profile_start", {
38631
+ arm: true,
38632
+ frameFunctions: profileFrameFunctions(projectPath)
38633
+ });
38580
38634
  if (armed.isError === true) {
38581
38635
  return this.profilerRefusal(armed);
38582
38636
  }
@@ -39359,7 +39413,7 @@ class GodotServer {
39359
39413
  ...callersName === undefined ? {} : { callersOf: { name: callersName, scripts: () => projectScripts(projectPath) } }
39360
39414
  };
39361
39415
  if (run.throughEditor) {
39362
- return await this.profileOfThePlayedGame(going, fromProject, limit);
39416
+ return await this.profileOfThePlayedGame(going, projectPath, fromProject, limit);
39363
39417
  }
39364
39418
  const record = run.projectPath === null ? null : readRunRecord(run.projectPath);
39365
39419
  const path = record !== null && record.pid === run.pid ? record.profile : undefined;
@@ -39381,7 +39435,7 @@ class GodotServer {
39381
39435
  ...fromProject
39382
39436
  }, limit));
39383
39437
  }
39384
- async profileOfThePlayedGame(going, fromProject, limit) {
39438
+ async profileOfThePlayedGame(going, projectPath, fromProject, limit) {
39385
39439
  const read = await this.handleViaBridge("profile_read", {});
39386
39440
  if (read.isError === true) {
39387
39441
  return this.profilerRefusal(read);
@@ -39393,7 +39447,9 @@ class GodotServer {
39393
39447
  "editor_run start with profile: true plays it again with the profiler on from its start"
39394
39448
  ]);
39395
39449
  }
39396
- const started = await this.handleViaBridge("profile_start", {});
39450
+ const started = await this.handleViaBridge("profile_start", {
39451
+ frameFunctions: profileFrameFunctions(projectPath)
39452
+ });
39397
39453
  if (started.isError === true) {
39398
39454
  return this.profilerRefusal(started);
39399
39455
  }
@@ -39416,6 +39472,7 @@ class GodotServer {
39416
39472
  frames: readNonNegativeNumber(held, "frames") ?? 0,
39417
39473
  complete: readBoolean(held, "complete") === true,
39418
39474
  unreadable: 0,
39475
+ ...playedCap(held),
39419
39476
  functions: Object.fromEntries(Object.entries(functions).map(([signature, sum]) => {
39420
39477
  const [calls, self, total] = Array.isArray(sum) ? sum : [];
39421
39478
  return [