gdharness 1.3.24 → 1.3.25
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 +28 -10
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +154 -67
- package/build/godot/addons/gdharness_runtime/runtime_frames.gd +46 -0
- package/build/godot/addons/gdharness_runtime/runtime_frames.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +9 -38
- package/build/keeper.js +20 -8
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -12155,7 +12155,16 @@ function announced(line) {
|
|
|
12155
12155
|
};
|
|
12156
12156
|
}
|
|
12157
12157
|
function reported(entry) {
|
|
12158
|
-
return entry.detail.length === 0 ? { index: entry.index, severity: entry.severity, source: entry.source, text: entry.text } : { ...entry, detail: framesCollapsed(entry.detail) };
|
|
12158
|
+
return entry.detail.length === 0 ? { index: entry.index, severity: entry.severity, source: entry.source, text: entry.text } : { ...entry, ...scriptRaising(entry.detail), detail: framesCollapsed(entry.detail) };
|
|
12159
|
+
}
|
|
12160
|
+
function scriptRaising(detail) {
|
|
12161
|
+
for (const line of detail) {
|
|
12162
|
+
const match = LOCATION2.exec(line);
|
|
12163
|
+
if (match?.[1] !== undefined && match[2] !== undefined && match[3] !== undefined) {
|
|
12164
|
+
return { script: match[2], line: Number(match[3]), function: match[1] };
|
|
12165
|
+
}
|
|
12166
|
+
}
|
|
12167
|
+
return {};
|
|
12159
12168
|
}
|
|
12160
12169
|
function framesCollapsed(detail) {
|
|
12161
12170
|
const out = [];
|
|
@@ -12370,7 +12379,7 @@ function answersTo(entry, severity, contains) {
|
|
|
12370
12379
|
const needle = contains.toLowerCase();
|
|
12371
12380
|
return [entry.text, ...entry.detail].some((line) => line.toLowerCase().includes(needle));
|
|
12372
12381
|
}
|
|
12373
|
-
var HEADLINE, CRASH, CRASH_FILLER, COLOUR_CODE, STREAMS, ADMITTED, FRAME, LONGEST_CYCLE = 64;
|
|
12382
|
+
var HEADLINE, CRASH, CRASH_FILLER, COLOUR_CODE, STREAMS, ADMITTED, LOCATION2, FRAME, LONGEST_CYCLE = 64;
|
|
12374
12383
|
var init_game_log = __esm(() => {
|
|
12375
12384
|
HEADLINE = /^(USER )?(SCRIPT ERROR|SHADER ERROR|ERROR|WARNING):\s?(.*)$/;
|
|
12376
12385
|
CRASH = /^(?:CrashHandlerException|handle_crash): Program crashed with signal \d+/;
|
|
@@ -12382,6 +12391,7 @@ var init_game_log = __esm(() => {
|
|
|
12382
12391
|
warning: new Set(["error", "warning"]),
|
|
12383
12392
|
info: new Set(["error", "warning", "info"])
|
|
12384
12393
|
};
|
|
12394
|
+
LOCATION2 = /^(?:at: |\[\d+\] )(.+) \((res:\/\/.+):(\d+)\)$/;
|
|
12385
12395
|
FRAME = /^\[(\d+)\] (.+)$/;
|
|
12386
12396
|
});
|
|
12387
12397
|
|
|
@@ -16478,7 +16488,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16478
16488
|
name: "project_test",
|
|
16479
16489
|
title: "Run tests",
|
|
16480
16490
|
effect: "additive",
|
|
16481
|
-
description: "Runs the project's gdUnit4 tests and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted, not listed, and an engine message keeps the frames above gdUnit4, not the twenty inside it; identical engine messages come back once under engineEntries, with times saying how often they were printed. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes. What changed on disk since it was imported is then imported before any suite runs, and named under importedFirst. On Windows and Linux the run gets a user:// of its own, away from a player's saves; on macOS the engine reads user:// off HOME, so it writes where the player keeps theirs, and savesNote says so. The run is headless unless a suite in it needs a window as gdharness test reads it, and then has one, on a desktop of its own on Windows. A run that found nothing to run is never called a pass, and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. An autoload that does not compile stops the run at once, its errors listed the same way. A client sending a progress token is told as each suite finishes, and at least every twenty seconds. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but orphan nodes were left comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and is not listed there, and none are when the project turns off gdUnit4's report/godot/script_error setting; a suite declares them all the same. Needs gdUnit4 under addons/gdUnit4.",
|
|
16491
|
+
description: "Runs the project's gdUnit4 tests and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted, not listed, and an engine message keeps the frames above gdUnit4, not the twenty inside it; identical engine messages come back once under engineEntries, with times saying how often they were printed, and with script, line and function for the project script one points at, as editor_output's entries have. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes. What changed on disk since it was imported is then imported before any suite runs, and named under importedFirst. On Windows and Linux the run gets a user:// of its own, away from a player's saves; on macOS the engine reads user:// off HOME, so it writes where the player keeps theirs, and savesNote says so. The run is headless unless a suite in it needs a window as gdharness test reads it, and then has one, on a desktop of its own on Windows. A run that found nothing to run is never called a pass, and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. An autoload that does not compile stops the run at once, its errors listed the same way. A client sending a progress token is told as each suite finishes, and at least every twenty seconds. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but orphan nodes were left comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and is not listed there, and none are when the project turns off gdUnit4's report/godot/script_error setting; a suite declares them all the same. Needs gdUnit4 under addons/gdUnit4.",
|
|
16482
16492
|
answer: "passed is the verdict to read. exitCode is the engine's own exit, gdUnit4's code for what it saw, and is 0 beside passed false when the run is failed for something gdUnit4 does not judge, such as an engine error no suite declared. time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for, and tookMsBy splits it into the class rebuild (classes), the import after it (import) and the engine running the suites (engine), each with any wait for an engine slot or the editor's scan in it. The run is judged as gdharness test judges it: undeclaredErrors names each engine error that no suite declared in its GDHARNESS_EXPECTED_ERRORS constant, and each engine warning a suite printed and did not declare in its GDHARNESS_EXPECTED_WARNINGS, marked warning true, with the suite and the case that printed it, and declaredNotPrinted each declared error or warning its case did not print. A declared text must be the whole line after ERROR: or WARNING: and its space, so a longer one its case printed holding the text is named under printedInstead. Either fails the run, and each suite holding one is listed with its count. leakedAtExit holds the engine's lines saying it exited still holding objects, resources, memory pages or a server's RIDs, which fail the run too, and the note names any script of the project Godot itself never frees. engineErrors counts every engine error the run printed but those a suite declared. orphans totals the orphan nodes gdUnit4 counted, and each suite that left some has its count, and a warning saying how to free them, under warnings, with left naming each case that left some (before or after for one of the suite's hooks, or the one value \"before or after\" where no case began and the two cannot be told apart) and the nodes, by class, name and script. suites lists only the suites with something to say (a case that failed, erred or was skipped, a hook that failed, orphans, or an error undeclared or declared and not printed), and suitesPassed counts the rest. Under classes, the classes each addon declares are counted under addedInAddons and changedInAddons rather than named, and a class the rebuild puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. A suite needs a window when it declares const GDHARNESS_NEEDS_WINDOW: bool = true, or calls scene_runner and declares nothing; a run holding one has a window, at the size project.godot gives its viewport and with no sound, and windowed is true and windowNote names those suites and any saying it runs headless that shared the window. On a machine with no display such a run is headless, windowed is false, and windowNote says so. " + IMPORTED_FIRST,
|
|
16483
16493
|
parameters: {
|
|
16484
16494
|
projectPath: PROJECT_PATH,
|
|
@@ -17322,14 +17332,14 @@ var init_tool_definitions = __esm(() => {
|
|
|
17322
17332
|
requires: []
|
|
17323
17333
|
}
|
|
17324
17334
|
},
|
|
17325
|
-
answer: "start: through is gdharness for a game this server started itself, and editor for one the editor plays. scene is the scene the game started in, the project's main scene when none was given. runtime carries addonIsStale and staleNote when the game announced on a runtime addon other than this server's, which no runtime_* call is then sent to, and staleNote says what brings it into step. refreshedClasses names the class_name declarations on disk that the engine's class cache was missing, which were added to it before the game started so the game could resolve them; it is empty when the cache was current. stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: close (the game was asked to close and exited, so exitCode is its own), keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). exitedBeforeStop is true when the run was already over when the stop came, so nothing was ended and exitCode, when anything collected it, is the one it ended with. A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is, and durationMs runs from that engine's start to the check's answer, after its exit, so it holds no import that came first. start and check: " + IMPORTED_FIRST + " An exported build carries its own imports and is not asked. waitedForEditorScanMs and scanNote are left out when no editor scan was waited for. A silent windowed run has --audio-driver Dummy in its arguments; a headless run is silent without it, and one the editor plays with sound has no arguments of its own. profile: 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, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The
|
|
17335
|
+
answer: "start: through is gdharness for a game this server started itself, and editor for one the editor plays. scene is the scene the game started in, the project's main scene when none was given. runtime carries addonIsStale and staleNote when the game announced on a runtime addon other than this server's, which no runtime_* call is then sent to, and staleNote says what brings it into step. refreshedClasses names the class_name declarations on disk that the engine's class cache was missing, which were added to it before the game started so the game could resolve them; it is empty when the cache was current. stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: close (the game was asked to close and exited, so exitCode is its own), keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). exitedBeforeStop is true when the run was already over when the stop came, so nothing was ended and exitCode, when anything collected it, is the one it ended with. A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is, and durationMs runs from that engine's start to the check's answer, after its exit, so it holds no import that came first. start and check: " + IMPORTED_FIRST + " An exported build carries its own imports and is not asked. waitedForEditorScanMs and scanNote are left out when no editor scan was waited for. A silent windowed run has --audio-driver Dummy in its arguments; a headless run is silent without it, and one the editor plays with sound has no arguments of its own. profile: 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, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The functions of gdharness's own runtime are not among them: the runtime addon's, which answer its tools inside the game, and those of res://gdharness/loader.gd, which starts it. Their self time is runtimeAddonMs, over runtimeAddonFunctions functions. 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. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
|
|
17326
17336
|
defaultOperation: "start"
|
|
17327
17337
|
},
|
|
17328
17338
|
{
|
|
17329
17339
|
name: "editor_output",
|
|
17330
17340
|
title: "Game and editor output",
|
|
17331
17341
|
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. A run answers with counts and a verdict: clean is true when no error was reported, and warnings, counted beside it, do not make it false. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one reported a game as playing fifteen seconds after its process was ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, where the output worth reading usually is, since the class cache, the plugins and the language server speak before any harness connects. It answers counts with no clean, since an editor neither passes nor fails, the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing. startedAt is when the run began and elapsedMs how long it has been going; for a run that is over, endedAt is when it ended and elapsedMs how long it ran, and both are left out when nobody saw the end, as for a game closed in the editor or a run picked up from a note written before ends were recorded.",
|
|
17332
|
-
answer: "Each entry's source says where it was read: transcript for a line of the run's transcript file, which holds both its streams and is how every run with one is read; stdout or stderr for a line of that stream of a run with no transcript, such as an editor-played run picked up after a reconnect whose first server left no note; debugger for what the editor's debug adapter reported and the game never printed, such as the script error a game stopped on. A printerr line is info: it prints to stderr, and the engine reports no error for it. For op editor with no editor connected, connected is false, editorRunning says whether the editor whose console it is still runs, and editorNote whether this server closed it or it went without a close, as a crash does; the next editor gdharness opens on the project starts a new console.",
|
|
17342
|
+
answer: "An entry with lines under its headline has them as detail, and, when one of them names a project script, script, line and function say where: the at: line when it names one, otherwise the innermost frame of the backtrace that does. A push_error or push_warning has the engine's own push_error as its at: line, so its script and line are the game's call that raised it. Each entry's source says where it was read: transcript for a line of the run's transcript file, which holds both its streams and is how every run with one is read; stdout or stderr for a line of that stream of a run with no transcript, such as an editor-played run picked up after a reconnect whose first server left no note; debugger for what the editor's debug adapter reported and the game never printed, such as the script error a game stopped on. A printerr line is info: it prints to stderr, and the engine reports no error for it. For op editor with no editor connected, connected is false, editorRunning says whether the editor whose console it is still runs, and editorNote whether this server closed it or it went without a close, as a crash does; the next editor gdharness opens on the project starts a new console.",
|
|
17333
17343
|
parameters: {
|
|
17334
17344
|
projectPath: {
|
|
17335
17345
|
type: "string",
|
|
@@ -48238,7 +48248,8 @@ function profiledFunctions(totals, limit, sourceOf = () => null) {
|
|
|
48238
48248
|
};
|
|
48239
48249
|
}
|
|
48240
48250
|
function isRuntimeAddon(signature) {
|
|
48241
|
-
|
|
48251
|
+
const { script } = signatureParts(signature);
|
|
48252
|
+
return script.startsWith(RUNTIME_ADDON2) || script === RUNTIME_LOADER;
|
|
48242
48253
|
}
|
|
48243
48254
|
function isCalibration(signature) {
|
|
48244
48255
|
const parts = signatureParts(signature);
|
|
@@ -48279,12 +48290,15 @@ function totalsSince(now, mark) {
|
|
|
48279
48290
|
}
|
|
48280
48291
|
}
|
|
48281
48292
|
shrunk += Object.keys(mark.functions).filter((signature) => now.functions[signature] === undefined && (mark.functions[signature]?.calls ?? 0) > 0).length;
|
|
48293
|
+
const { dropped: droppedInAll, ...kept } = now;
|
|
48294
|
+
const dropped = (droppedInAll ?? 0) - (mark.dropped ?? 0);
|
|
48282
48295
|
return {
|
|
48283
48296
|
totals: {
|
|
48284
|
-
...
|
|
48297
|
+
...kept,
|
|
48285
48298
|
frames: now.frames - mark.frames,
|
|
48286
48299
|
unreadable: Math.max(0, now.unreadable - mark.unreadable),
|
|
48287
48300
|
...now.cappedFrames === undefined ? {} : { cappedFrames: Math.max(0, now.cappedFrames - (mark.cappedFrames ?? 0)) },
|
|
48301
|
+
...dropped > 0 ? { dropped } : {},
|
|
48288
48302
|
functions
|
|
48289
48303
|
},
|
|
48290
48304
|
shrunk
|
|
@@ -48391,13 +48405,15 @@ function callersOf(totals, name, scripts) {
|
|
|
48391
48405
|
property
|
|
48392
48406
|
};
|
|
48393
48407
|
}
|
|
48394
|
-
var QUEUE_SETTING = "limits/debugger/max_queued_messages", QUEUE_DEFAULT = 2048, QUEUE_HEADROOM = 512, UNNAMED_PREFIX = "#", PROFILE_FROM_START = "GDHARNESS_PROFILE_FROM_START", ms = (seconds) => Math.round(seconds * 1e6) / 1000, RUNTIME_SCRIPT2 = "gdharness_runtime/runtime_autoload.gd", PROFILER_PROBE = "_profiler_probe", PROFILER_CALIBRATOR = "_calibrate_the_profiler", CALL_SITES = 40;
|
|
48408
|
+
var QUEUE_SETTING = "limits/debugger/max_queued_messages", QUEUE_DEFAULT = 2048, QUEUE_HEADROOM = 512, UNNAMED_PREFIX = "#", PROFILE_FROM_START = "GDHARNESS_PROFILE_FROM_START", ms = (seconds) => Math.round(seconds * 1e6) / 1000, RUNTIME_SCRIPT2 = "gdharness_runtime/runtime_autoload.gd", PROFILER_PROBE = "_profiler_probe", PROFILER_CALIBRATOR = "_calibrate_the_profiler", RUNTIME_LOADER, CALL_SITES = 40;
|
|
48395
48409
|
var init_script_profile = __esm(() => {
|
|
48396
48410
|
init_game_debugger();
|
|
48397
48411
|
init_gdscript_source();
|
|
48398
48412
|
init_godot_variant();
|
|
48399
48413
|
init_resources();
|
|
48414
|
+
init_setup();
|
|
48400
48415
|
init_whole_file();
|
|
48416
|
+
RUNTIME_LOADER = `res://${RUNTIME_AUTOLOAD.path}`;
|
|
48401
48417
|
});
|
|
48402
48418
|
|
|
48403
48419
|
// src/server-instructions.ts
|
|
@@ -49745,7 +49761,9 @@ function profileAnswer(reading, limit) {
|
|
|
49745
49761
|
const table = profiledFunctions(totals, limit, reading.sourceOf);
|
|
49746
49762
|
const notes = [];
|
|
49747
49763
|
const addonNote = reading.runtimeAddon === true ? "" : reading.runtimeAddon === null ? " The run's project is no longer at its path, so whether it had the gdharness runtime addon, which switches the profiler off as the game quits so the engine sends its totals, cannot be read." : " This project has no gdharness runtime addon, which switches the profiler off as the game quits so the engine sends its totals: setup installs it.";
|
|
49748
|
-
const
|
|
49764
|
+
const dropped = totals.dropped ?? 0;
|
|
49765
|
+
const droppedNote = dropped > 0 ? ` The game's debugger queue dropped ${dropped === 1 ? "one message" : `${dropped} messages`}, the engine reported, and those can be a frame or the totals: the queue holds as many as the project's network/limits/debugger/max_queued_messages, and raising it makes room.` : "";
|
|
49766
|
+
const totalsWithheld = dropped > 0 ? droppedNote : reading.runtimeAddon === true ? " The runtime addon switches the profiler off as the game quits, and did not here: either it was no longer running in this game by then, which a project can arrange for some scenes; or the game ended without taking its tree down, as a crash or a kill does; or the totals did not reach the debugger before the game exited, which the game then says in its output." : addonNote;
|
|
49749
49767
|
if (totals.complete && reading.running) {
|
|
49750
49768
|
notes.push(`The engine's totals arrived while the game is still running, which it sends only when the profiler is switched off, so ${reading.through === "editor" ? "the editor's Profiler tab or the game itself, through EngineDebugger," : "the game itself, through EngineDebugger,"} switched it off before the game ended. They cover the run up to then and nothing after.`);
|
|
49751
49769
|
} else if (totals.complete) {} else if (reading.running && totals.frames === 0) {
|
|
@@ -49768,7 +49786,7 @@ function profileAnswer(reading, limit) {
|
|
|
49768
49786
|
notes.push(`scriptMs, ${table.scriptMs}ms, is more than the ${reading.coveredMs}ms the profile covers.${table.superCallers > 0 ? " The super calls above, counted twice, can account for some or all of that; whatever they do not is" : " One thread cannot spend more time than passes, so that is"} functions running on several threads at once, through WorkerThreadPool or Thread. The engine adds every thread's time to the same function and does not say which thread a call ran on, so a function's selfMs is summed over every thread it ran on rather than taken out of frames, and a function that waits for the others, through wait_for_group_task_completion or wait_to_finish, has the wait in its own selfMs as well.`);
|
|
49769
49787
|
}
|
|
49770
49788
|
if (table.addonFunctions > 0) {
|
|
49771
|
-
notes.push(`The gdharness runtime
|
|
49789
|
+
notes.push(`The gdharness runtime's own functions, its addon's and those of the loader that starts it, took ${table.addonMs}ms of self time over ${table.addonFunctions === 1 ? "one function" : `${table.addonFunctions} functions`}, starting the runtime as the game starts, answering tools such as runtime_invoke and sending the profile. They are left out of functions, functionsCounted and scriptMs and summed under runtimeAddonMs, since they are not the game's work, though that time was spent in the game's process.`);
|
|
49772
49790
|
}
|
|
49773
49791
|
notes.push(...cappedNotes(totals));
|
|
49774
49792
|
if (totals.unreadable > 0) {
|
|
@@ -10,18 +10,60 @@ extends Node
|
|
|
10
10
|
## wanted. Both directions of the socket carry one JSON object per line. Every request names an
|
|
11
11
|
## id and the reply carries it back.
|
|
12
12
|
|
|
13
|
-
const GdhAccessibility = preload("runtime_accessibility.gd")
|
|
14
|
-
const GdhCapture = preload("runtime_capture.gd")
|
|
15
|
-
const GdhChanges = preload("runtime_changes.gd")
|
|
16
13
|
const GdhErrorReport = preload("error_report.gd")
|
|
17
|
-
const
|
|
18
|
-
const GdhPad = preload("runtime_pad.gd")
|
|
19
|
-
const GdhQueries = preload("runtime_queries.gd")
|
|
14
|
+
const GdhFrames = preload("runtime_frames.gd")
|
|
20
15
|
const GdhRead = preload("reading.gd")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
16
|
+
|
|
17
|
+
## Where the modules are loaded from. Kept in step with the markers below.
|
|
18
|
+
const ADDON_DIR: String = "res://addons/gdharness_runtime/"
|
|
19
|
+
|
|
20
|
+
## The modules that answer commands, loaded at the first command one answers rather than preloaded.
|
|
21
|
+
## A preload compiles as this script parses, and the modules are twelve thousand lines: compiling
|
|
22
|
+
## them took most of a second of every debug start, in games nobody asks anything, a bench's quiet
|
|
23
|
+
## workers and script runs among them.
|
|
24
|
+
const QUERIES: String = "runtime_queries.gd"
|
|
25
|
+
const CHANGES: String = "runtime_changes.gd"
|
|
26
|
+
const INPUT: String = "runtime_input.gd"
|
|
27
|
+
const TYPING: String = "runtime_typing.gd"
|
|
28
|
+
const PAD: String = "runtime_pad.gd"
|
|
29
|
+
const CAPTURE: String = "runtime_capture.gd"
|
|
30
|
+
const WAITS: String = "runtime_waits.gd"
|
|
31
|
+
const ACCESSIBILITY: String = "runtime_accessibility.gd"
|
|
32
|
+
const VALUES: String = "runtime_values.gd"
|
|
33
|
+
|
|
34
|
+
## Every module command, by the name a request uses, as the module and the method that answers it.
|
|
35
|
+
const MODULE_COMMANDS: Dictionary = {
|
|
36
|
+
"get_tree": [QUERIES, "get_tree"],
|
|
37
|
+
"find_nodes": [QUERIES, "find_nodes"],
|
|
38
|
+
"read_text": [QUERIES, "read_text"],
|
|
39
|
+
"get_rect": [QUERIES, "get_rect"],
|
|
40
|
+
"get_property": [QUERIES, "get_property"],
|
|
41
|
+
"set_property": [CHANGES, "set_property"],
|
|
42
|
+
"call_method": [CHANGES, "call_method"],
|
|
43
|
+
"get_metrics": [QUERIES, "get_metrics"],
|
|
44
|
+
"get_accessibility": [ACCESSIBILITY, "get_accessibility"],
|
|
45
|
+
"capture_screenshot": [CAPTURE, "capture_screenshot"],
|
|
46
|
+
"capture_viewport": [CAPTURE, "capture_viewport"],
|
|
47
|
+
"inject_action": [INPUT, "inject_action"],
|
|
48
|
+
"inject_key": [INPUT, "inject_key"],
|
|
49
|
+
"inject_text": [TYPING, "inject_text"],
|
|
50
|
+
"inject_mouse_click": [INPUT, "inject_mouse_click"],
|
|
51
|
+
"inject_mouse_motion": [INPUT, "inject_mouse_motion"],
|
|
52
|
+
"inject_mouse_drag": [INPUT, "inject_mouse_drag"],
|
|
53
|
+
"inject_joypad_button": [PAD, "inject_joypad_button"],
|
|
54
|
+
"inject_joypad_motion": [PAD, "inject_joypad_motion"],
|
|
55
|
+
"click": [INPUT, "click"],
|
|
56
|
+
"choose": [TYPING, "choose"],
|
|
57
|
+
"wait_frames": [WAITS, "wait_frames"],
|
|
58
|
+
"wait_signal": [WAITS, "wait_signal"],
|
|
59
|
+
"watch_signal": [WAITS, "watch_signal"],
|
|
60
|
+
"watch_recorded": [WAITS, "recorded"],
|
|
61
|
+
"wait_until": [WAITS, "wait_until"],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
## The size the engine gives a headless game's root viewport. Kept in step with
|
|
65
|
+
## `HEADLESS_VIEWPORT` in runtime_targets.gd, which this script does not load as it starts.
|
|
66
|
+
const HEADLESS_VIEWPORT: Vector2 = Vector2(64, 64)
|
|
25
67
|
|
|
26
68
|
const PROTOCOL: int = 3
|
|
27
69
|
const DEFAULT_BIND_ADDRESS: String = "127.0.0.1"
|
|
@@ -38,10 +80,14 @@ const PROFILE_CAPTURE: String = "gdharness"
|
|
|
38
80
|
const PROFILE_SENT: String = "gdharness:profile_sent"
|
|
39
81
|
const PROFILE_RECEIVED: String = "gdharness:profile_received"
|
|
40
82
|
|
|
41
|
-
## The longest a quitting game waits for that answer
|
|
42
|
-
## editor without the gdharness addon, holds every profiled exit this long.
|
|
83
|
+
## The longest a quitting game waits for that answer from a debugger that may not give one, such as
|
|
84
|
+
## an editor without the gdharness addon, which holds every profiled exit this long.
|
|
43
85
|
const PROFILE_CONFIRM_MS: int = 3000
|
|
44
86
|
|
|
87
|
+
## The longest it waits for a debugger gdharness gave it, which answers once it has read that far:
|
|
88
|
+
## long enough for one far behind, and an end for one that has hung.
|
|
89
|
+
const PROFILE_ANSWERED_MS: int = 60000
|
|
90
|
+
|
|
45
91
|
## How many times the empty function is called to measure what the profiler costs per call: enough
|
|
46
92
|
## that the clock's microsecond steps are lost in the sum, and a few milliseconds of one frame.
|
|
47
93
|
const PROFILER_PROBE_CALLS: int = 10000
|
|
@@ -100,19 +146,20 @@ const ERROR_REPORT_NAME: String = "runtime-%d.log"
|
|
|
100
146
|
const VERSION_MARKER: String = "res://addons/gdharness_runtime/.gdharness-version"
|
|
101
147
|
const DIGEST_MARKER: String = "res://addons/gdharness_runtime/.gdharness-digest"
|
|
102
148
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
var
|
|
112
|
-
var
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
149
|
+
## The values module every other module reads and writes values with, one for them all, loaded with
|
|
150
|
+
## the first of them.
|
|
151
|
+
var values: RefCounted:
|
|
152
|
+
get:
|
|
153
|
+
if _values == null:
|
|
154
|
+
_values = _loaded(VALUES).new()
|
|
155
|
+
return _values
|
|
156
|
+
|
|
157
|
+
var _values: RefCounted
|
|
158
|
+
var _frames: GdhFrames = GdhFrames.new()
|
|
159
|
+
## Each module loaded so far, by its file. Held here because a Callable holds its object by id
|
|
160
|
+
## rather than by reference, and a module referenced only by the Callable answering one command
|
|
161
|
+
## would be freed once it answered.
|
|
162
|
+
var _modules: Dictionary = {}
|
|
116
163
|
|
|
117
164
|
var _server: TCPServer
|
|
118
165
|
var _clients: Array[StreamPeerTCP] = []
|
|
@@ -132,7 +179,7 @@ var _announcement: String = ""
|
|
|
132
179
|
## Held for as long as the game runs: a Logger the engine has is kept by reference, and one
|
|
133
180
|
## that goes away mid-run takes the report with it.
|
|
134
181
|
var _errors: GdhErrorReport
|
|
135
|
-
##
|
|
182
|
+
## The commands this script answers itself, by the name a request uses.
|
|
136
183
|
var _commands: Dictionary = {}
|
|
137
184
|
## The addon this game loaded, read once at start: an upgrade rewrites the markers under a running
|
|
138
185
|
## game, whose code stays what it loaded, and reading them later would call that code current.
|
|
@@ -151,38 +198,53 @@ var _profiler_calibrated: bool = false
|
|
|
151
198
|
func _init() -> void:
|
|
152
199
|
_commands = {
|
|
153
200
|
"ping": _ping,
|
|
154
|
-
"get_tree": _queries.get_tree,
|
|
155
|
-
"find_nodes": _queries.find_nodes,
|
|
156
|
-
"read_text": _queries.read_text,
|
|
157
|
-
"get_rect": _queries.get_rect,
|
|
158
|
-
"get_property": _queries.get_property,
|
|
159
|
-
"set_property": _changes.set_property,
|
|
160
|
-
"call_method": _changes.call_method,
|
|
161
|
-
"get_metrics": _queries.get_metrics,
|
|
162
|
-
"get_accessibility": _accessibility.get_accessibility,
|
|
163
|
-
"capture_screenshot": _capture.capture_screenshot,
|
|
164
|
-
"capture_viewport": _capture.capture_viewport,
|
|
165
|
-
"inject_action": _input.inject_action,
|
|
166
|
-
"inject_key": _input.inject_key,
|
|
167
|
-
"inject_text": _typing.inject_text,
|
|
168
|
-
"inject_mouse_click": _input.inject_mouse_click,
|
|
169
|
-
"inject_mouse_motion": _input.inject_mouse_motion,
|
|
170
|
-
"inject_mouse_drag": _input.inject_mouse_drag,
|
|
171
|
-
"inject_joypad_button": _pad.inject_joypad_button,
|
|
172
|
-
"inject_joypad_motion": _pad.inject_joypad_motion,
|
|
173
|
-
"click": _input.click,
|
|
174
|
-
"choose": _typing.choose,
|
|
175
|
-
"wait_frames": _waits.wait_frames,
|
|
176
|
-
"wait_signal": _waits.wait_signal,
|
|
177
|
-
"watch_signal": _waits.watch_signal,
|
|
178
|
-
"watch_recorded": _waits.recorded,
|
|
179
|
-
"wait_until": _waits.wait_until,
|
|
180
201
|
"send_profile": _send_profile_now,
|
|
181
202
|
"close_game": _close_game,
|
|
182
203
|
}
|
|
183
204
|
_wait_for_the_profiler()
|
|
184
205
|
|
|
185
206
|
|
|
207
|
+
## The script of the module in [param file], compiled at the first call that needs it.
|
|
208
|
+
static func _loaded(file: String) -> GDScript:
|
|
209
|
+
return load(ADDON_DIR + file) as GDScript
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
## The module in [param file], built at its first command with what it is built with.
|
|
213
|
+
func _module(file: String) -> Object:
|
|
214
|
+
var made: Object = _modules.get(file)
|
|
215
|
+
if made != null:
|
|
216
|
+
return made
|
|
217
|
+
var script: GDScript = _loaded(file)
|
|
218
|
+
match file:
|
|
219
|
+
QUERIES:
|
|
220
|
+
made = script.new(self, values, _frames)
|
|
221
|
+
TYPING, PAD, ACCESSIBILITY:
|
|
222
|
+
made = script.new(self)
|
|
223
|
+
_:
|
|
224
|
+
made = script.new(self, values)
|
|
225
|
+
_modules[file] = made
|
|
226
|
+
_frames.skip_this_frame()
|
|
227
|
+
return made
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
## What answers [param command], loading its module if no command has needed it yet, or an invalid
|
|
231
|
+
## Callable for a command nothing answers.
|
|
232
|
+
func _handler(command: String) -> Callable:
|
|
233
|
+
if _commands.has(command):
|
|
234
|
+
return _commands[command]
|
|
235
|
+
var answered_by: Array = MODULE_COMMANDS.get(command, [])
|
|
236
|
+
if answered_by.is_empty():
|
|
237
|
+
return Callable()
|
|
238
|
+
var file: String = answered_by[0]
|
|
239
|
+
var method: String = answered_by[1]
|
|
240
|
+
return Callable(_module(file), method)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
## Every command the runtime answers, its own and the modules', loaded or not.
|
|
244
|
+
func _command_names() -> Array:
|
|
245
|
+
return _commands.keys() + MODULE_COMMANDS.keys()
|
|
246
|
+
|
|
247
|
+
|
|
186
248
|
## Gives a game with no window the size the project opens its window at. Measured: a
|
|
187
249
|
## headless engine's root viewport is 64 by 64 whatever the project settings or --resolution say, so
|
|
188
250
|
## a headless game laid its interface out in 64 pixels, and a button every windowed run shows was
|
|
@@ -195,7 +257,7 @@ func _size_a_windowless_game() -> void:
|
|
|
195
257
|
if DisplayServer.get_name() != "headless" or _script_run() or not OS.is_debug_build():
|
|
196
258
|
return
|
|
197
259
|
var root: Window = get_tree().root
|
|
198
|
-
if Vector2(root.size) !=
|
|
260
|
+
if Vector2(root.size) != HEADLESS_VIEWPORT:
|
|
199
261
|
return
|
|
200
262
|
root.size = Vector2i(_window_dimension("width", 1152), _window_dimension("height", 648))
|
|
201
263
|
|
|
@@ -305,32 +367,54 @@ func _exit_tree() -> void:
|
|
|
305
367
|
## every other node's _exit_tree, so what those do is counted. A project that sent this node away
|
|
306
368
|
## mid-run had the profiler switched off then, and its run answered as complete with none of the
|
|
307
369
|
## frames after it.
|
|
370
|
+
##
|
|
371
|
+
## The debugger reads in order, so its answer comes once it has read everything the game queued
|
|
372
|
+
## before the totals, and a debugger that has fallen behind answers late rather than not at all. A
|
|
373
|
+
## debugger gdharness gave this game, the keeper of a run the server started or an editor with the
|
|
374
|
+
## gdharness addon, is waited for until it answers; a fixed three seconds lost the totals of a run
|
|
375
|
+
## whose first frame named 1536 functions, on a loaded runner that read them slower than that. Any
|
|
376
|
+
## other debugger may never answer, and holds the exit for the short wait only.
|
|
377
|
+
##
|
|
378
|
+
## The debugger is asked once before the profiler goes off as well, so the totals go into an empty
|
|
379
|
+
## queue. They name each function no frame did, one message apiece, and with the names of a large
|
|
380
|
+
## first frame still queued for a debugger behind on its reading, they did not fit and the engine
|
|
381
|
+
## dropped them. One that does not answer that is not asked again, as it would not answer the second.
|
|
308
382
|
func _send_the_profile() -> void:
|
|
309
383
|
if not (EngineDebugger.is_active() and EngineDebugger.is_profiling("servers")):
|
|
310
384
|
return
|
|
311
|
-
_profile_received = false
|
|
312
385
|
if not EngineDebugger.has_capture(PROFILE_CAPTURE):
|
|
313
386
|
EngineDebugger.register_message_capture(PROFILE_CAPTURE, _on_debugger_message)
|
|
387
|
+
var answers: bool = _profiled_from_start or OS.get_environment(EDITOR_PID_VARIABLE).is_valid_int()
|
|
388
|
+
var waits: int = PROFILE_ANSWERED_MS if answers else PROFILE_CONFIRM_MS
|
|
389
|
+
var drained: bool = _debugger_answered(waits)
|
|
314
390
|
EngineDebugger.profiler_enable("servers", false)
|
|
315
|
-
|
|
316
|
-
var deadline: int = Time.get_ticks_msec() + PROFILE_CONFIRM_MS
|
|
317
|
-
while not _profile_received and Time.get_ticks_msec() < deadline:
|
|
318
|
-
for _poll: int in POLLS_PER_READ:
|
|
319
|
-
EngineDebugger.line_poll()
|
|
320
|
-
OS.delay_msec(5)
|
|
391
|
+
var confirmed: bool = drained and _debugger_answered(waits)
|
|
321
392
|
EngineDebugger.unregister_message_capture(PROFILE_CAPTURE)
|
|
322
|
-
if not
|
|
393
|
+
if not confirmed:
|
|
323
394
|
print(
|
|
324
395
|
(
|
|
325
396
|
(
|
|
326
397
|
"[gdharness] the debugger did not confirm the profiler's totals within %d ms, so the"
|
|
327
398
|
+ " profile may be the frames that arrived without them"
|
|
328
399
|
)
|
|
329
|
-
%
|
|
400
|
+
% waits
|
|
330
401
|
)
|
|
331
402
|
)
|
|
332
403
|
|
|
333
404
|
|
|
405
|
+
## Sends the debugger the message it answers once it has read everything sent before it, and waits
|
|
406
|
+
## up to [param waits] ms for the answer, polling for it since the game's loop is over.
|
|
407
|
+
func _debugger_answered(waits: int) -> bool:
|
|
408
|
+
_profile_received = false
|
|
409
|
+
EngineDebugger.send_message(PROFILE_SENT, [])
|
|
410
|
+
var deadline: int = Time.get_ticks_msec() + waits
|
|
411
|
+
while not _profile_received and Time.get_ticks_msec() < deadline:
|
|
412
|
+
for _poll: int in POLLS_PER_READ:
|
|
413
|
+
EngineDebugger.line_poll()
|
|
414
|
+
OS.delay_msec(5)
|
|
415
|
+
return _profile_received
|
|
416
|
+
|
|
417
|
+
|
|
334
418
|
## The totals sent now, for a stop that is about to end the game without letting it quit: ended by
|
|
335
419
|
## its process, a game takes the totals with it, and every stopped profile came back as the frames
|
|
336
420
|
## summed. Nothing of the game's own runs, which quitting the tree would have done.
|
|
@@ -395,7 +479,7 @@ func _report_errors() -> void:
|
|
|
395
479
|
func _process(_delta: float) -> void:
|
|
396
480
|
if not _enabled or _server == null:
|
|
397
481
|
return
|
|
398
|
-
|
|
482
|
+
_frames.note_frame(Time.get_ticks_usec())
|
|
399
483
|
_calibrate_the_profiler()
|
|
400
484
|
|
|
401
485
|
# Every connection waiting rather than one a frame: the engine queues eight a game has not
|
|
@@ -678,7 +762,7 @@ func _send_welcome(client: StreamPeerTCP) -> void:
|
|
|
678
762
|
str(ProjectSettings.get_setting(BIND_ADDRESS_SETTING, DEFAULT_BIND_ADDRESS))
|
|
679
763
|
)
|
|
680
764
|
welcome["type"] = "welcome"
|
|
681
|
-
welcome["commands"] =
|
|
765
|
+
welcome["commands"] = _command_names()
|
|
682
766
|
_send_response(client, welcome)
|
|
683
767
|
|
|
684
768
|
|
|
@@ -716,13 +800,16 @@ func _handle_message(client: StreamPeerTCP, line: String) -> void:
|
|
|
716
800
|
|
|
717
801
|
|
|
718
802
|
func _execute_command(command: String, params: Dictionary) -> Dictionary:
|
|
719
|
-
var handler: Callable =
|
|
803
|
+
var handler: Callable = _handler(command)
|
|
720
804
|
if not handler.is_valid():
|
|
721
805
|
return {
|
|
722
806
|
"type": "error",
|
|
723
|
-
"message": "Unknown command: %s. Commands: %s" % [command, ", ".join(
|
|
807
|
+
"message": "Unknown command: %s. Commands: %s" % [command, ", ".join(_command_names())]
|
|
724
808
|
}
|
|
725
|
-
|
|
809
|
+
# Only a waits module already loaded can have a trace running to record the call in.
|
|
810
|
+
var waits: Object = _modules.get(WAITS)
|
|
811
|
+
if waits != null:
|
|
812
|
+
var _seen: Variant = waits.call(&"saw_call", command, params)
|
|
726
813
|
var answer: Variant = await handler.call(params)
|
|
727
814
|
return answer
|
|
728
815
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## How long the game's last frames took, timed from the runtime's first frame. Apart from the
|
|
4
|
+
## queries that report it, because those are loaded at the first request and the frames before it
|
|
5
|
+
## count too.
|
|
6
|
+
|
|
7
|
+
## How many frames the frame time is the mean of: two seconds at sixty frames a second.
|
|
8
|
+
const FRAMES_TIMED: int = 120
|
|
9
|
+
|
|
10
|
+
## How long each of the last frames took, oldest first, in microseconds, as the game timed them.
|
|
11
|
+
var usec: PackedInt64Array = PackedInt64Array()
|
|
12
|
+
## How many physics steps ran in each of those frames, in the same order.
|
|
13
|
+
var physics: PackedInt64Array = PackedInt64Array()
|
|
14
|
+
var _last_frame_usec: int = 0
|
|
15
|
+
var _last_physics_frames: int = 0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## Called by the autoload once a frame, whatever else it does that frame, with the time it was called.
|
|
19
|
+
##
|
|
20
|
+
## The engine's own monitors do not follow the frame: TIME_PROCESS and the frame rate are set once a
|
|
21
|
+
## second, the first to the slowest process step of that second, so a game whose frames took 16.7ms
|
|
22
|
+
## read 45ms for a second at a time, and a change to the game showed only on the next second's
|
|
23
|
+
## worst frame. The time between two of these calls is the frame as it passed, physics and the wait
|
|
24
|
+
## for the display included.
|
|
25
|
+
##
|
|
26
|
+
## The physics steps are counted beside it because the engine steps physics from the wall clock and
|
|
27
|
+
## nothing else reports how many it took: a game drawing sixty frames a second whose brew advanced
|
|
28
|
+
## by physics steps moved a tenth as far as the frames promised, and every reading available said
|
|
29
|
+
## the game was running well.
|
|
30
|
+
func note_frame(now_usec: int) -> void:
|
|
31
|
+
var stepped: int = Engine.get_physics_frames()
|
|
32
|
+
if _last_frame_usec > 0:
|
|
33
|
+
var _grew: bool = usec.push_back(now_usec - _last_frame_usec)
|
|
34
|
+
var _counted: bool = physics.push_back(stepped - _last_physics_frames)
|
|
35
|
+
if usec.size() > FRAMES_TIMED:
|
|
36
|
+
usec.remove_at(0)
|
|
37
|
+
physics.remove_at(0)
|
|
38
|
+
_last_frame_usec = now_usec
|
|
39
|
+
_last_physics_frames = stepped
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## Leaves the frame in progress out of the timing, for one the runtime itself slowed: compiling a
|
|
43
|
+
## module at its first command takes up to half a second, which is gdharness's and not the game's,
|
|
44
|
+
## and would stand as the slowest frame for the next two seconds of readings.
|
|
45
|
+
func skip_this_frame() -> void:
|
|
46
|
+
_last_frame_usec = 0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://dd8t2ghi83ntg
|
|
@@ -4,6 +4,7 @@ extends RefCounted
|
|
|
4
4
|
## question, where one is on screen, a property's value, the metrics. What it changes, a property
|
|
5
5
|
## written or a method called, is in runtime_changes.gd.
|
|
6
6
|
|
|
7
|
+
const GdhFrames = preload("runtime_frames.gd")
|
|
7
8
|
const GdhPaths = preload("runtime_paths.gd")
|
|
8
9
|
const GdhRead = preload("reading.gd")
|
|
9
10
|
const GdhSays = preload("runtime_says.gd")
|
|
@@ -28,24 +29,18 @@ const READ_LIMIT: int = 500
|
|
|
28
29
|
## and said nothing, and a caller told to raise a number that stays put concludes the screen moved.
|
|
29
30
|
const LIMIT_CEILING: int = 5000
|
|
30
31
|
|
|
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
|
-
|
|
34
32
|
var _host: Node
|
|
35
33
|
var _values: GdhValues
|
|
36
|
-
|
|
37
|
-
var _frame_usec: PackedInt64Array = PackedInt64Array()
|
|
38
|
-
## How many physics steps ran in each of those frames, in the same order.
|
|
39
|
-
var _frame_physics: PackedInt64Array = PackedInt64Array()
|
|
40
|
-
var _last_frame_usec: int = 0
|
|
41
|
-
var _last_physics_frames: int = 0
|
|
34
|
+
var _frames: GdhFrames
|
|
42
35
|
|
|
43
36
|
|
|
44
37
|
## The host is the autoload, which is how the tree is reached: it is not in one when the
|
|
45
|
-
## modules are built, and a fixture may put it in one later.
|
|
46
|
-
|
|
38
|
+
## modules are built, and a fixture may put it in one later. [param frames] is the autoload's,
|
|
39
|
+
## timing the frames since the runtime's first.
|
|
40
|
+
func _init(host: Node, values: GdhValues, frames: GdhFrames) -> void:
|
|
47
41
|
_host = host
|
|
48
42
|
_values = values
|
|
43
|
+
_frames = frames
|
|
49
44
|
|
|
50
45
|
|
|
51
46
|
## The tree under a node, to a depth, with every stored property on each node when asked for all
|
|
@@ -801,30 +796,6 @@ func get_property(params: Dictionary) -> Dictionary:
|
|
|
801
796
|
}
|
|
802
797
|
|
|
803
798
|
|
|
804
|
-
## Called by the autoload once a frame, whatever else it does that frame, with the time it was called.
|
|
805
|
-
##
|
|
806
|
-
## The engine's own monitors do not follow the frame: TIME_PROCESS and the frame rate are set once a
|
|
807
|
-
## second, the first to the slowest process step of that second, so a game whose frames took 16.7ms
|
|
808
|
-
## read 45ms for a second at a time, and a change to the game showed only on the next second's
|
|
809
|
-
## worst frame. The time between two of these calls is the frame as it passed, physics and the wait
|
|
810
|
-
## for the display included.
|
|
811
|
-
##
|
|
812
|
-
## The physics steps are counted beside it because the engine steps physics from the wall clock and
|
|
813
|
-
## nothing else reports how many it took: a game drawing sixty frames a second whose brew advanced
|
|
814
|
-
## by physics steps moved a tenth as far as the frames promised, and every reading available said
|
|
815
|
-
## the game was running well.
|
|
816
|
-
func note_frame(now_usec: int) -> void:
|
|
817
|
-
var physics: int = Engine.get_physics_frames()
|
|
818
|
-
if _last_frame_usec > 0:
|
|
819
|
-
var _grew: bool = _frame_usec.push_back(now_usec - _last_frame_usec)
|
|
820
|
-
var _stepped: bool = _frame_physics.push_back(physics - _last_physics_frames)
|
|
821
|
-
if _frame_usec.size() > FRAMES_TIMED:
|
|
822
|
-
_frame_usec.remove_at(0)
|
|
823
|
-
_frame_physics.remove_at(0)
|
|
824
|
-
_last_frame_usec = now_usec
|
|
825
|
-
_last_physics_frames = physics
|
|
826
|
-
|
|
827
|
-
|
|
828
799
|
## Whether the frame waits for the display, which sets its floor: a game with vsync on cannot
|
|
829
800
|
## finish a frame sooner than the display refreshes, however little it does.
|
|
830
801
|
static func _vsync() -> String:
|
|
@@ -843,15 +814,15 @@ static func _vsync() -> String:
|
|
|
843
814
|
|
|
844
815
|
func get_metrics(params: Dictionary) -> Dictionary:
|
|
845
816
|
var metrics: Array = params.get("metrics", [])
|
|
846
|
-
var timed: int =
|
|
817
|
+
var timed: int = _frames.usec.size()
|
|
847
818
|
var total_usec: int = 0
|
|
848
819
|
var slowest_usec: int = 0
|
|
849
|
-
for one: int in
|
|
820
|
+
for one: int in _frames.usec:
|
|
850
821
|
total_usec += one
|
|
851
822
|
slowest_usec = maxi(slowest_usec, one)
|
|
852
823
|
# Null rather than zero before a frame has been timed: no frame took no time.
|
|
853
824
|
var steps: int = 0
|
|
854
|
-
for one: int in
|
|
825
|
+
for one: int in _frames.physics:
|
|
855
826
|
steps += one
|
|
856
827
|
var frame_time: Variant = null
|
|
857
828
|
var frame_time_max: Variant = null
|
package/build/keeper.js
CHANGED
|
@@ -8802,6 +8802,10 @@ var SERVER_VERSION = (() => {
|
|
|
8802
8802
|
})();
|
|
8803
8803
|
|
|
8804
8804
|
// src/setup.ts
|
|
8805
|
+
var RUNTIME_AUTOLOAD = {
|
|
8806
|
+
name: "GdharnessRuntime",
|
|
8807
|
+
path: "gdharness/loader.gd"
|
|
8808
|
+
};
|
|
8805
8809
|
function shippedAddonsDirectory() {
|
|
8806
8810
|
return join5(dirname3(fileURLToPath(import.meta.url)), "godot", "addons");
|
|
8807
8811
|
}
|
|
@@ -8812,7 +8816,7 @@ var STEP_MS = 1e4;
|
|
|
8812
8816
|
var ADDON_FRAMES = 5;
|
|
8813
8817
|
var ADDON_LINES = 200;
|
|
8814
8818
|
var RUNTIME_ADDON = "res://addons/gdharness_runtime/";
|
|
8815
|
-
var
|
|
8819
|
+
var RUNTIME_AUTOLOAD2 = `${RUNTIME_ADDON}runtime_autoload.gd`;
|
|
8816
8820
|
var frameEnd = null;
|
|
8817
8821
|
function frameEndLine() {
|
|
8818
8822
|
if (frameEnd === null) {
|
|
@@ -8820,7 +8824,7 @@ function frameEndLine() {
|
|
|
8820
8824
|
`);
|
|
8821
8825
|
const declared = lines.findIndex((line) => line.startsWith("func _notification("));
|
|
8822
8826
|
if (declared === -1) {
|
|
8823
|
-
throw new Error(`The shipped ${
|
|
8827
|
+
throw new Error(`The shipped ${RUNTIME_AUTOLOAD2} declares no _notification.`);
|
|
8824
8828
|
}
|
|
8825
8829
|
frameEnd = declared + 2;
|
|
8826
8830
|
}
|
|
@@ -9253,7 +9257,7 @@ class GameDebugger {
|
|
|
9253
9257
|
if (until !== undefined && Date.now() >= until) {
|
|
9254
9258
|
return { frames, gaveUp: "time" };
|
|
9255
9259
|
}
|
|
9256
|
-
if (frames.some((frame) => frame.script ===
|
|
9260
|
+
if (frames.some((frame) => frame.script === RUNTIME_AUTOLOAD2 && frame.name === "_process")) {
|
|
9257
9261
|
if (until === undefined && framesLeft === 0) {
|
|
9258
9262
|
return { frames, gaveUp: "frames" };
|
|
9259
9263
|
}
|
|
@@ -9284,15 +9288,15 @@ class GameDebugger {
|
|
|
9284
9288
|
return null;
|
|
9285
9289
|
}
|
|
9286
9290
|
const line = frameEndLine();
|
|
9287
|
-
const theirs = this.breakpointLines.has(`${
|
|
9291
|
+
const theirs = this.breakpointLines.has(`${RUNTIME_AUTOLOAD2}:${String(line)}`);
|
|
9288
9292
|
if (!theirs) {
|
|
9289
|
-
this.send("breakpoint", [
|
|
9293
|
+
this.send("breakpoint", [RUNTIME_AUTOLOAD2, line, true], held.thread);
|
|
9290
9294
|
}
|
|
9291
9295
|
this.forget("debug_enter");
|
|
9292
9296
|
this.send("continue", [], held.thread);
|
|
9293
9297
|
const stopped = await this.next(GameDebugger.named("debug_enter"), STEP_MS);
|
|
9294
9298
|
if (!theirs) {
|
|
9295
|
-
this.send("breakpoint", [
|
|
9299
|
+
this.send("breakpoint", [RUNTIME_AUTOLOAD2, line, false], this.held?.thread ?? MAIN_THREAD);
|
|
9296
9300
|
}
|
|
9297
9301
|
return stopped === null ? "lost" : await this.stack();
|
|
9298
9302
|
}
|
|
@@ -9782,6 +9786,8 @@ function withEngineArguments(args, extra) {
|
|
|
9782
9786
|
const user = args.indexOf("--");
|
|
9783
9787
|
return user < 0 ? [...args, ...extra] : [...args.slice(0, user), ...extra, ...args.slice(user)];
|
|
9784
9788
|
}
|
|
9789
|
+
var QUEUE_OVERFLOW = "TOO_MANY_MESSAGES";
|
|
9790
|
+
var DROPPED_COUNT = /(\d+) messages were dropped/;
|
|
9785
9791
|
|
|
9786
9792
|
class ProfileAggregate {
|
|
9787
9793
|
names = new Map;
|
|
@@ -9792,6 +9798,7 @@ class ProfileAggregate {
|
|
|
9792
9798
|
unreadable = 0;
|
|
9793
9799
|
cappedFrames = 0;
|
|
9794
9800
|
totalCapped = false;
|
|
9801
|
+
dropped = 0;
|
|
9795
9802
|
startedAt;
|
|
9796
9803
|
frameFunctions;
|
|
9797
9804
|
constructor(startedAt, frameFunctions) {
|
|
@@ -9803,7 +9810,10 @@ class ProfileAggregate {
|
|
|
9803
9810
|
this.unreadable += 1;
|
|
9804
9811
|
return;
|
|
9805
9812
|
}
|
|
9806
|
-
if (received.message === "
|
|
9813
|
+
if (received.message === "error" && received.data[7] === QUEUE_OVERFLOW) {
|
|
9814
|
+
const said = typeof received.data[8] === "string" ? DROPPED_COUNT.exec(received.data[8]) : null;
|
|
9815
|
+
this.dropped += said === null ? 1 : Number(said[1]);
|
|
9816
|
+
} else if (received.message === "servers:function_signature") {
|
|
9807
9817
|
const [name, id] = received.data;
|
|
9808
9818
|
if (typeof name === "string" && typeof id === "number") {
|
|
9809
9819
|
this.names.set(id, name);
|
|
@@ -9873,10 +9883,12 @@ class ProfileAggregate {
|
|
|
9873
9883
|
unreadable: this.unreadable,
|
|
9874
9884
|
frameFunctions: this.frameFunctions,
|
|
9875
9885
|
cappedFrames: this.cappedFrames,
|
|
9876
|
-
totalCapped: this.totalCapped
|
|
9886
|
+
totalCapped: this.totalCapped,
|
|
9887
|
+
...this.dropped > 0 ? { dropped: this.dropped } : {}
|
|
9877
9888
|
};
|
|
9878
9889
|
}
|
|
9879
9890
|
}
|
|
9891
|
+
var RUNTIME_LOADER = `res://${RUNTIME_AUTOLOAD.path}`;
|
|
9880
9892
|
class ProfileObserver {
|
|
9881
9893
|
aggregate = null;
|
|
9882
9894
|
changed;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.25",
|
|
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",
|