gdharness 1.1.62 → 1.2.0
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
|
@@ -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. 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. 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
|
},
|
|
@@ -42556,16 +42556,39 @@ function signatureParts(signature) {
|
|
|
42556
42556
|
return { script: parts.slice(0, -2).join("::"), line: Number.isFinite(line) ? line : 0, function: name };
|
|
42557
42557
|
}
|
|
42558
42558
|
function profiledFunctions(totals, limit, sourceOf = () => null) {
|
|
42559
|
-
const
|
|
42560
|
-
const
|
|
42559
|
+
const perCallUs = perCallCostOf(totals);
|
|
42560
|
+
const rows = rowsOf(totals, sourceOf).map((row) => withOverhead(row, perCallUs));
|
|
42561
|
+
const scriptMs = ms(Object.entries(totals.functions).reduce((sum, [signature, one]) => sum + (one.calls > 0 && !isCalibration(signature) ? one.selfSeconds : 0), 0));
|
|
42561
42562
|
return {
|
|
42562
42563
|
functions: rows.slice(0, limit).map((row) => placedLambda(row, sourceOf)),
|
|
42563
42564
|
omitted: Math.max(0, rows.length - limit),
|
|
42564
42565
|
scriptMs,
|
|
42565
42566
|
counted: rows.length,
|
|
42566
|
-
superCallers: rows.filter((row) => row.selfIncludesSuper === true).length
|
|
42567
|
+
superCallers: rows.filter((row) => row.selfIncludesSuper === true).length,
|
|
42568
|
+
perCallUs,
|
|
42569
|
+
overheadDominated: rows.filter((row) => row.overheadMs !== undefined).length
|
|
42567
42570
|
};
|
|
42568
42571
|
}
|
|
42572
|
+
function isCalibration(signature) {
|
|
42573
|
+
const parts = signatureParts(signature);
|
|
42574
|
+
return parts.script.endsWith(RUNTIME_SCRIPT) && (parts.function === PROFILER_PROBE || parts.function === PROFILER_CALIBRATOR);
|
|
42575
|
+
}
|
|
42576
|
+
function perCallCostOf(totals) {
|
|
42577
|
+
for (const [signature, sum] of Object.entries(totals.functions)) {
|
|
42578
|
+
const parts = signatureParts(signature);
|
|
42579
|
+
if (parts.script.endsWith(RUNTIME_SCRIPT) && parts.function === PROFILER_PROBE && sum.calls > 0) {
|
|
42580
|
+
return Math.round(sum.selfSeconds / sum.calls * 1e9) / 1000;
|
|
42581
|
+
}
|
|
42582
|
+
}
|
|
42583
|
+
return null;
|
|
42584
|
+
}
|
|
42585
|
+
function withOverhead(row, perCallUs) {
|
|
42586
|
+
if (perCallUs === null || row.selfMs <= 0) {
|
|
42587
|
+
return row;
|
|
42588
|
+
}
|
|
42589
|
+
const overheadMs = Math.round(row.calls * perCallUs) / 1000;
|
|
42590
|
+
return overheadMs >= row.selfMs / 2 ? { ...row, overheadMs: Math.min(overheadMs, row.selfMs) } : row;
|
|
42591
|
+
}
|
|
42569
42592
|
function rowsOf(totals, sourceOf) {
|
|
42570
42593
|
const codes = new Map;
|
|
42571
42594
|
const codeOfScript = (script) => {
|
|
@@ -42575,7 +42598,7 @@ function rowsOf(totals, sourceOf) {
|
|
|
42575
42598
|
}
|
|
42576
42599
|
return codes.get(script) ?? null;
|
|
42577
42600
|
};
|
|
42578
|
-
return Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
|
|
42601
|
+
return Object.entries(totals.functions).filter(([signature, sum]) => sum.calls > 0 && !isCalibration(signature)).map(([signature, sum]) => {
|
|
42579
42602
|
const parts = signatureParts(signature);
|
|
42580
42603
|
const code = codeOfScript(parts.script);
|
|
42581
42604
|
return {
|
|
@@ -42641,7 +42664,7 @@ function callersOf(totals, name, scripts) {
|
|
|
42641
42664
|
sites.sort((a, b) => (b.withinTotalMs ?? -1) - (a.withinTotalMs ?? -1) || a.script.localeCompare(b.script) || a.line - b.line);
|
|
42642
42665
|
return { target, sites: sites.slice(0, CALL_SITES), omitted: Math.max(0, sites.length - CALL_SITES) };
|
|
42643
42666
|
}
|
|
42644
|
-
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;
|
|
42667
|
+
var QUEUE_SETTING = "limits/debugger/max_queued_messages", QUEUE_DEFAULT = 2048, QUEUE_HEADROOM = 512, UNNAMED_PREFIX = "#", ms = (seconds) => Math.round(seconds * 1e6) / 1000, RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd", PROFILER_PROBE = "_profiler_probe", PROFILER_CALIBRATOR = "_calibrate_the_profiler", CALL_SITES = 40;
|
|
42645
42668
|
var init_script_profile = __esm(() => {
|
|
42646
42669
|
init_gdscript_source();
|
|
42647
42670
|
init_godot_variant();
|
|
@@ -43229,6 +43252,24 @@ function foldedScriptErrors(errors) {
|
|
|
43229
43252
|
function nonEmpty(lists) {
|
|
43230
43253
|
return Object.fromEntries(Object.entries(lists).filter(([, list]) => list.length > 0));
|
|
43231
43254
|
}
|
|
43255
|
+
function withoutUidUnder(unminted, paths) {
|
|
43256
|
+
return unminted.map((script) => `res://${script}`).filter((script) => paths.some((path) => script === path || script.startsWith(path.endsWith("/") ? path : `${path}/`)));
|
|
43257
|
+
}
|
|
43258
|
+
function withUidsMissing(answer, missing) {
|
|
43259
|
+
const content = answer.content.map((part) => ({ ...part }));
|
|
43260
|
+
const last = content.at(-1);
|
|
43261
|
+
try {
|
|
43262
|
+
const parsed = JSON.parse(last?.text ?? "");
|
|
43263
|
+
if (last !== undefined && typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
43264
|
+
last.text = answerJson({
|
|
43265
|
+
...parsed,
|
|
43266
|
+
uidsMissing: missing,
|
|
43267
|
+
uidsNote: `${missing.length === 1 ? "This script has" : "These scripts have"} no .uid sidecar yet: the engine writes one when it imports the project, which a test run does not, so a check that tracks the sidecars fails on ${missing.length === 1 ? "it" : "them"}. project_import refresh_uids writes them.`
|
|
43268
|
+
});
|
|
43269
|
+
}
|
|
43270
|
+
} catch {}
|
|
43271
|
+
return { ...answer, content };
|
|
43272
|
+
}
|
|
43232
43273
|
function withPreviousCut(answer, previous) {
|
|
43233
43274
|
const content = answer.content.map((part) => ({ ...part }));
|
|
43234
43275
|
const last = content.at(-1);
|
|
@@ -43440,6 +43481,9 @@ function profileAnswer(reading, limit) {
|
|
|
43440
43481
|
if (table.superCallers > 0) {
|
|
43441
43482
|
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.`);
|
|
43442
43483
|
}
|
|
43484
|
+
if (table.overheadDominated > 0 && table.perCallUs !== null) {
|
|
43485
|
+
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: a function that does little and is called often looks expensive here and costs that much less once the profiler is off, so inlining it gains little.`);
|
|
43486
|
+
}
|
|
43443
43487
|
notes.push(...cappedNotes(totals));
|
|
43444
43488
|
if (totals.unreadable > 0) {
|
|
43445
43489
|
notes.push(`${totals.unreadable} message${totals.unreadable === 1 ? "" : "s"} from the game could not be read, so the totals have that many gaps.`);
|
|
@@ -43452,6 +43496,7 @@ function profileAnswer(reading, limit) {
|
|
|
43452
43496
|
frames: totals.frames,
|
|
43453
43497
|
coveredMs: reading.coveredMs,
|
|
43454
43498
|
scriptMs: table.scriptMs,
|
|
43499
|
+
perCallOverheadUs: table.perCallUs ?? undefined,
|
|
43455
43500
|
functionsCounted: table.counted,
|
|
43456
43501
|
omitted: table.omitted > 0 ? table.omitted : undefined,
|
|
43457
43502
|
functions: table.functions,
|
|
@@ -45181,11 +45226,14 @@ class GodotServer {
|
|
|
45181
45226
|
return ticker;
|
|
45182
45227
|
}
|
|
45183
45228
|
async handleRunTests(args) {
|
|
45184
|
-
const
|
|
45229
|
+
const ran = await this.runTests(args);
|
|
45185
45230
|
const project = this.project(args);
|
|
45186
45231
|
if (!project.ok || callSignal()?.aborted === true) {
|
|
45187
|
-
return
|
|
45232
|
+
return ran;
|
|
45188
45233
|
}
|
|
45234
|
+
const paths = testPathsIn(project.value.path, args["path"]);
|
|
45235
|
+
const unminted = paths.ok ? withoutUidUnder(scriptsWithoutUid(project.value.path), paths.value) : [];
|
|
45236
|
+
const answer = unminted.length === 0 ? ran : withUidsMissing(ran, unminted);
|
|
45189
45237
|
const previous = takeCutTestRun(project.value.path);
|
|
45190
45238
|
return previous === null ? answer : withPreviousCut(answer, previous);
|
|
45191
45239
|
}
|
|
@@ -39,6 +39,10 @@ const PROFILE_RECEIVED: String = "gdharness:profile_received"
|
|
|
39
39
|
## editor without the gdharness addon, holds every profiled exit this long.
|
|
40
40
|
const PROFILE_CONFIRM_MS: int = 3000
|
|
41
41
|
|
|
42
|
+
## How many times the empty function is called to measure what the profiler costs per call: enough
|
|
43
|
+
## that the clock's microsecond steps are lost in the sum, and a few milliseconds of one frame.
|
|
44
|
+
const PROFILER_PROBE_CALLS: int = 10000
|
|
45
|
+
|
|
42
46
|
## How many calls of `EngineDebugger.line_poll` read the debugger's messages once: it reads them
|
|
43
47
|
## on every 2048th call and only counts the others.
|
|
44
48
|
const POLLS_PER_READ: int = 2048
|
|
@@ -103,6 +107,7 @@ var _commands: Dictionary = {}
|
|
|
103
107
|
var _loaded_version: String = _read_marker(VERSION_MARKER)
|
|
104
108
|
var _loaded_digest: String = _read_marker(DIGEST_MARKER)
|
|
105
109
|
var _profile_received: bool = false
|
|
110
|
+
var _profiler_calibrated: bool = false
|
|
106
111
|
|
|
107
112
|
|
|
108
113
|
func _init() -> void:
|
|
@@ -193,6 +198,31 @@ func _send_the_profile() -> void:
|
|
|
193
198
|
)
|
|
194
199
|
|
|
195
200
|
|
|
201
|
+
## What the script profiler costs per call in this game, measured once each time it is switched on.
|
|
202
|
+
##
|
|
203
|
+
## The profiler times every call it counts, and that bookkeeping lands in the self time of the
|
|
204
|
+
## function called: a function doing almost nothing, called six hundred thousand times, read as a
|
|
205
|
+
## second of its own, and inlining it saved nothing once the profiler was off. So an empty function
|
|
206
|
+
## is called here [constant PROFILER_PROBE_CALLS] times while the profiler watches, under the load
|
|
207
|
+
## the game is under, and the server reads its row as the cost of one call.
|
|
208
|
+
func _calibrate_the_profiler() -> void:
|
|
209
|
+
var profiling: bool = EngineDebugger.is_active() and EngineDebugger.is_profiling("servers")
|
|
210
|
+
if not profiling:
|
|
211
|
+
_profiler_calibrated = false
|
|
212
|
+
return
|
|
213
|
+
if _profiler_calibrated:
|
|
214
|
+
return
|
|
215
|
+
_profiler_calibrated = true
|
|
216
|
+
for _call: int in PROFILER_PROBE_CALLS:
|
|
217
|
+
_profiler_probe()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
## Does nothing, so its time under the profiler is the profiler's own. Kept in step with
|
|
221
|
+
## `PROFILER_PROBE` in src/script-profile.ts.
|
|
222
|
+
func _profiler_probe() -> void:
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
|
|
196
226
|
func _on_debugger_message(message: String, _data: Array) -> bool:
|
|
197
227
|
if PROFILE_CAPTURE + ":" + message == PROFILE_RECEIVED:
|
|
198
228
|
_profile_received = true
|
|
@@ -223,6 +253,7 @@ func _process(_delta: float) -> void:
|
|
|
223
253
|
if not _enabled or _server == null:
|
|
224
254
|
return
|
|
225
255
|
_queries.note_frame(Time.get_ticks_usec())
|
|
256
|
+
_calibrate_the_profiler()
|
|
226
257
|
|
|
227
258
|
if _server.is_connection_available():
|
|
228
259
|
var client: StreamPeerTCP = _server.take_connection()
|
|
@@ -25,6 +25,15 @@ const HEADLESS_VIEWPORT: Vector2 = Vector2(64, 64)
|
|
|
25
25
|
## everything that process starts. Kept in step with `DESKTOP_VARIABLE` in src/desktop.ts.
|
|
26
26
|
const DESKTOP_VARIABLE: String = "GDHARNESS_DESKTOP"
|
|
27
27
|
|
|
28
|
+
## Node.ProcessMode as its names, for a refusal to say which one is in force.
|
|
29
|
+
const PROCESS_MODES: Dictionary[int, String] = {
|
|
30
|
+
Node.PROCESS_MODE_INHERIT: "INHERIT",
|
|
31
|
+
Node.PROCESS_MODE_PAUSABLE: "PAUSABLE",
|
|
32
|
+
Node.PROCESS_MODE_WHEN_PAUSED: "WHEN_PAUSED",
|
|
33
|
+
Node.PROCESS_MODE_ALWAYS: "ALWAYS",
|
|
34
|
+
Node.PROCESS_MODE_DISABLED: "DISABLED",
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
## Whether this run has had the pointer note in full; the game process is the run.
|
|
29
38
|
static var _pointer_explained: bool = false
|
|
30
39
|
|
|
@@ -556,15 +565,35 @@ func _takes_no_click(control: Control, node_path: String) -> String:
|
|
|
556
565
|
if button != null and button.disabled:
|
|
557
566
|
return "%s is disabled, so clicking it presses nothing" % node_path
|
|
558
567
|
if not control.can_process():
|
|
568
|
+
var why: String = _why_it_does_not_process(control, node_path)
|
|
569
|
+
var over: Node = Targets.drawn_over(control)
|
|
570
|
+
var cover: String = "" if over == null else "; %s is drawn over it" % over.get_path()
|
|
559
571
|
if _host.get_tree().paused:
|
|
560
572
|
return (
|
|
561
|
-
"the game is paused and %s does not process while it is, so clicking it does nothing"
|
|
562
|
-
% node_path
|
|
573
|
+
"the game is paused and %s does not process while it is (%s), so clicking it does nothing%s"
|
|
574
|
+
% [node_path, why, cover]
|
|
563
575
|
)
|
|
564
|
-
return "%s
|
|
576
|
+
return "%s does not process (%s), so clicking it does nothing%s" % [node_path, why, cover]
|
|
565
577
|
return ""
|
|
566
578
|
|
|
567
579
|
|
|
580
|
+
## Which node's process_mode keeps [param control] from processing, and what it is. A node set to
|
|
581
|
+
## inherit takes its parent's, so the one deciding can be any node above it: the game turning a
|
|
582
|
+
## whole screen off while a menu shows over it reads, on the button, as a button turned off.
|
|
583
|
+
static func _why_it_does_not_process(control: Control, node_path: String) -> String:
|
|
584
|
+
var deciding: Node = control
|
|
585
|
+
while deciding.process_mode == Node.PROCESS_MODE_INHERIT and deciding.get_parent() != null:
|
|
586
|
+
deciding = deciding.get_parent()
|
|
587
|
+
# The root inherits from nothing, which processes as pausable.
|
|
588
|
+
var value: int = deciding.process_mode
|
|
589
|
+
if value == Node.PROCESS_MODE_INHERIT:
|
|
590
|
+
value = Node.PROCESS_MODE_PAUSABLE
|
|
591
|
+
var mode: String = PROCESS_MODES.get(value, str(value))
|
|
592
|
+
if deciding == control:
|
|
593
|
+
return "its process_mode is %s" % mode
|
|
594
|
+
return "%s inherits process_mode %s from %s" % [node_path, mode, deciding.get_path()]
|
|
595
|
+
|
|
596
|
+
|
|
568
597
|
## Why a click at [param control]'s centre would reach nothing, or "" when it would reach it: the
|
|
569
598
|
## centre outside the viewport it is drawn in, at [param local]; outside the viewport showing that
|
|
570
599
|
## one on the screen, which [param reached] holds with the centre there; or clipped away by a
|
|
@@ -370,6 +370,12 @@ static func _pressed_through(control: Control) -> Control:
|
|
|
370
370
|
return control
|
|
371
371
|
|
|
372
372
|
|
|
373
|
+
## What a pointer at [param target]'s centre would reach instead of it, drawn over it, or null.
|
|
374
|
+
static func drawn_over(target: Control) -> Node:
|
|
375
|
+
var drawn: Array[Node] = _pointer_takers(target.get_tree().root)
|
|
376
|
+
return _cover_of(target, drawn, _areas_of(drawn))
|
|
377
|
+
|
|
378
|
+
|
|
373
379
|
## Everything in the tree [param root] heads that a pointer can land on: each control on screen
|
|
374
380
|
## that does not let the pointer through, and each window embedded in another.
|
|
375
381
|
static func _pointer_takers(root: Node) -> Array[Node]:
|
package/build/index.js
CHANGED
|
@@ -33353,16 +33353,42 @@ function signatureParts(signature) {
|
|
|
33353
33353
|
}
|
|
33354
33354
|
var ms = (seconds) => Math.round(seconds * 1e6) / 1000;
|
|
33355
33355
|
function profiledFunctions(totals, limit, sourceOf = () => null) {
|
|
33356
|
-
const
|
|
33357
|
-
const
|
|
33356
|
+
const perCallUs = perCallCostOf(totals);
|
|
33357
|
+
const rows = rowsOf(totals, sourceOf).map((row) => withOverhead(row, perCallUs));
|
|
33358
|
+
const scriptMs = ms(Object.entries(totals.functions).reduce((sum, [signature, one]) => sum + (one.calls > 0 && !isCalibration(signature) ? one.selfSeconds : 0), 0));
|
|
33358
33359
|
return {
|
|
33359
33360
|
functions: rows.slice(0, limit).map((row) => placedLambda(row, sourceOf)),
|
|
33360
33361
|
omitted: Math.max(0, rows.length - limit),
|
|
33361
33362
|
scriptMs,
|
|
33362
33363
|
counted: rows.length,
|
|
33363
|
-
superCallers: rows.filter((row) => row.selfIncludesSuper === true).length
|
|
33364
|
+
superCallers: rows.filter((row) => row.selfIncludesSuper === true).length,
|
|
33365
|
+
perCallUs,
|
|
33366
|
+
overheadDominated: rows.filter((row) => row.overheadMs !== undefined).length
|
|
33364
33367
|
};
|
|
33365
33368
|
}
|
|
33369
|
+
var RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd";
|
|
33370
|
+
var PROFILER_PROBE = "_profiler_probe";
|
|
33371
|
+
var PROFILER_CALIBRATOR = "_calibrate_the_profiler";
|
|
33372
|
+
function isCalibration(signature) {
|
|
33373
|
+
const parts = signatureParts(signature);
|
|
33374
|
+
return parts.script.endsWith(RUNTIME_SCRIPT) && (parts.function === PROFILER_PROBE || parts.function === PROFILER_CALIBRATOR);
|
|
33375
|
+
}
|
|
33376
|
+
function perCallCostOf(totals) {
|
|
33377
|
+
for (const [signature, sum] of Object.entries(totals.functions)) {
|
|
33378
|
+
const parts = signatureParts(signature);
|
|
33379
|
+
if (parts.script.endsWith(RUNTIME_SCRIPT) && parts.function === PROFILER_PROBE && sum.calls > 0) {
|
|
33380
|
+
return Math.round(sum.selfSeconds / sum.calls * 1e9) / 1000;
|
|
33381
|
+
}
|
|
33382
|
+
}
|
|
33383
|
+
return null;
|
|
33384
|
+
}
|
|
33385
|
+
function withOverhead(row, perCallUs) {
|
|
33386
|
+
if (perCallUs === null || row.selfMs <= 0) {
|
|
33387
|
+
return row;
|
|
33388
|
+
}
|
|
33389
|
+
const overheadMs = Math.round(row.calls * perCallUs) / 1000;
|
|
33390
|
+
return overheadMs >= row.selfMs / 2 ? { ...row, overheadMs: Math.min(overheadMs, row.selfMs) } : row;
|
|
33391
|
+
}
|
|
33366
33392
|
function rowsOf(totals, sourceOf) {
|
|
33367
33393
|
const codes = new Map;
|
|
33368
33394
|
const codeOfScript = (script) => {
|
|
@@ -33372,7 +33398,7 @@ function rowsOf(totals, sourceOf) {
|
|
|
33372
33398
|
}
|
|
33373
33399
|
return codes.get(script) ?? null;
|
|
33374
33400
|
};
|
|
33375
|
-
return Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
|
|
33401
|
+
return Object.entries(totals.functions).filter(([signature, sum]) => sum.calls > 0 && !isCalibration(signature)).map(([signature, sum]) => {
|
|
33376
33402
|
const parts = signatureParts(signature);
|
|
33377
33403
|
const code = codeOfScript(parts.script);
|
|
33378
33404
|
return {
|
|
@@ -34463,7 +34489,7 @@ var TOOL_SPECS = [
|
|
|
34463
34489
|
},
|
|
34464
34490
|
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
|
|
34465
34491
|
profile: {
|
|
34466
|
-
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. 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",
|
|
34492
|
+
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",
|
|
34467
34493
|
requires: []
|
|
34468
34494
|
}
|
|
34469
34495
|
},
|
|
@@ -35685,6 +35711,24 @@ function foldedScriptErrors(errors) {
|
|
|
35685
35711
|
function nonEmpty(lists) {
|
|
35686
35712
|
return Object.fromEntries(Object.entries(lists).filter(([, list]) => list.length > 0));
|
|
35687
35713
|
}
|
|
35714
|
+
function withoutUidUnder(unminted, paths) {
|
|
35715
|
+
return unminted.map((script) => `res://${script}`).filter((script) => paths.some((path) => script === path || script.startsWith(path.endsWith("/") ? path : `${path}/`)));
|
|
35716
|
+
}
|
|
35717
|
+
function withUidsMissing(answer, missing) {
|
|
35718
|
+
const content = answer.content.map((part) => ({ ...part }));
|
|
35719
|
+
const last = content.at(-1);
|
|
35720
|
+
try {
|
|
35721
|
+
const parsed = JSON.parse(last?.text ?? "");
|
|
35722
|
+
if (last !== undefined && typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
35723
|
+
last.text = answerJson({
|
|
35724
|
+
...parsed,
|
|
35725
|
+
uidsMissing: missing,
|
|
35726
|
+
uidsNote: `${missing.length === 1 ? "This script has" : "These scripts have"} no .uid sidecar yet: the engine writes one when it imports the project, which a test run does not, so a check that tracks the sidecars fails on ${missing.length === 1 ? "it" : "them"}. project_import refresh_uids writes them.`
|
|
35727
|
+
});
|
|
35728
|
+
}
|
|
35729
|
+
} catch {}
|
|
35730
|
+
return { ...answer, content };
|
|
35731
|
+
}
|
|
35688
35732
|
function withPreviousCut(answer, previous) {
|
|
35689
35733
|
const content = answer.content.map((part) => ({ ...part }));
|
|
35690
35734
|
const last = content.at(-1);
|
|
@@ -35931,6 +35975,9 @@ function profileAnswer(reading, limit) {
|
|
|
35931
35975
|
if (table.superCallers > 0) {
|
|
35932
35976
|
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.`);
|
|
35933
35977
|
}
|
|
35978
|
+
if (table.overheadDominated > 0 && table.perCallUs !== null) {
|
|
35979
|
+
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: a function that does little and is called often looks expensive here and costs that much less once the profiler is off, so inlining it gains little.`);
|
|
35980
|
+
}
|
|
35934
35981
|
notes.push(...cappedNotes(totals));
|
|
35935
35982
|
if (totals.unreadable > 0) {
|
|
35936
35983
|
notes.push(`${totals.unreadable} message${totals.unreadable === 1 ? "" : "s"} from the game could not be read, so the totals have that many gaps.`);
|
|
@@ -35943,6 +35990,7 @@ function profileAnswer(reading, limit) {
|
|
|
35943
35990
|
frames: totals.frames,
|
|
35944
35991
|
coveredMs: reading.coveredMs,
|
|
35945
35992
|
scriptMs: table.scriptMs,
|
|
35993
|
+
perCallOverheadUs: table.perCallUs ?? undefined,
|
|
35946
35994
|
functionsCounted: table.counted,
|
|
35947
35995
|
omitted: table.omitted > 0 ? table.omitted : undefined,
|
|
35948
35996
|
functions: table.functions,
|
|
@@ -37675,11 +37723,14 @@ class GodotServer {
|
|
|
37675
37723
|
return ticker;
|
|
37676
37724
|
}
|
|
37677
37725
|
async handleRunTests(args) {
|
|
37678
|
-
const
|
|
37726
|
+
const ran = await this.runTests(args);
|
|
37679
37727
|
const project = this.project(args);
|
|
37680
37728
|
if (!project.ok || callSignal()?.aborted === true) {
|
|
37681
|
-
return
|
|
37729
|
+
return ran;
|
|
37682
37730
|
}
|
|
37731
|
+
const paths = testPathsIn(project.value.path, args["path"]);
|
|
37732
|
+
const unminted = paths.ok ? withoutUidUnder(scriptsWithoutUid(project.value.path), paths.value) : [];
|
|
37733
|
+
const answer = unminted.length === 0 ? ran : withUidsMissing(ran, unminted);
|
|
37683
37734
|
const previous = takeCutTestRun(project.value.path);
|
|
37684
37735
|
return previous === null ? answer : withPreviousCut(answer, previous);
|
|
37685
37736
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|