@flighthq/debug 0.1.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/dist/debug.d.ts +7 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +131 -0
- package/dist/debug.js.map +1 -0
- package/dist/debugTiming.d.ts +6 -0
- package/dist/debugTiming.d.ts.map +1 -0
- package/dist/debugTiming.js +42 -0
- package/dist/debugTiming.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
- package/src/debug.test.ts +197 -0
- package/src/debugTiming.test.ts +125 -0
package/dist/debug.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DebugOptions, DebugSubsystemHooks, DebugSubsystemName } from '@flighthq/types';
|
|
2
|
+
export declare function disableDebug(): void;
|
|
3
|
+
export declare function enableDebug(options?: Readonly<DebugOptions>): void;
|
|
4
|
+
export declare function isDebugEnabled(): boolean;
|
|
5
|
+
export declare function registerDebugSubsystem(name: DebugSubsystemName, hooks: Readonly<DebugSubsystemHooks>): void;
|
|
6
|
+
export declare function unregisterDebugSubsystem(name: DebugSubsystemName): boolean;
|
|
7
|
+
//# sourceMappingURL=debug.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"debug.d.ts","sourceRoot":"","sources":["../src/debug.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,kBAAkB,EAAqB,MAAM,iBAAiB,CAAC;AAOhH,wBAAgB,YAAY,IAAI,IAAI,CAOnC;AASD,wBAAgB,WAAW,CAAC,OAAO,GAAE,QAAQ,CAAC,YAAY,CAAM,GAAG,IAAI,CActE;AAGD,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAOD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,kBAAkB,EAAE,KAAK,EAAE,QAAQ,CAAC,mBAAmB,CAAC,GAAG,IAAI,CAE3G;AAID,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAE1E"}
|
package/dist/debug.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { addLogSink, clearLogChannelLevels, createTextLogFormatter, getLogLevel, removeLogSink, setLogChannelLevel, setLogLevel, } from '@flighthq/log';
|
|
2
|
+
import { LogLevel } from '@flighthq/types';
|
|
3
|
+
// The one call that returns Flight to the zero-debug baseline: removes the sink enableDebug
|
|
4
|
+
// installed, restores the log levels it raised, and runs the disable-guards binding of every
|
|
5
|
+
// subsystem it switched on. No-op when debug is not enabled (idempotent, sentinel — not a throw).
|
|
6
|
+
// After this, importing @flighthq/debug costs nothing and no debug output is produced.
|
|
7
|
+
export function disableDebug() {
|
|
8
|
+
if (!_enabled)
|
|
9
|
+
return;
|
|
10
|
+
for (const hooks of _enabledSubsystems)
|
|
11
|
+
hooks.disableGuards?.();
|
|
12
|
+
_enabledSubsystems.length = 0;
|
|
13
|
+
_removeDebugSink();
|
|
14
|
+
_restoreDebugLevels();
|
|
15
|
+
_enabled = false;
|
|
16
|
+
}
|
|
17
|
+
// The friendly entry point to Flight's diagnostics. Installs a dev console LogSink (a text-formatted
|
|
18
|
+
// one by default; override with `options.sink`), raises the global log level (default
|
|
19
|
+
// LogLevel.Debug) and the per-channel levels of the selected subsystems plus any explicit
|
|
20
|
+
// `options.channels`, and runs each selected subsystem's registered guard-enabler. `options.subsystems`
|
|
21
|
+
// selects which registered subsystems to switch on; omit it to switch on every registered subsystem.
|
|
22
|
+
// Idempotent: a second call while already enabled is a no-op (disableDebug first to re-enable with
|
|
23
|
+
// different options).
|
|
24
|
+
export function enableDebug(options = {}) {
|
|
25
|
+
if (_enabled)
|
|
26
|
+
return;
|
|
27
|
+
const level = options.level ?? LogLevel.Debug;
|
|
28
|
+
const subsystems = _resolveDebugSubsystems(options.subsystems);
|
|
29
|
+
const channels = _collectDebugChannels(subsystems, options.channels);
|
|
30
|
+
_savedGlobalLevel = getLogLevel();
|
|
31
|
+
_applyDebugLevels(level, channels);
|
|
32
|
+
_installDebugSink(options.sink ?? _createDefaultDebugSink());
|
|
33
|
+
for (const hooks of subsystems) {
|
|
34
|
+
hooks.enableGuards?.();
|
|
35
|
+
_enabledSubsystems.push(hooks);
|
|
36
|
+
}
|
|
37
|
+
_enabled = true;
|
|
38
|
+
}
|
|
39
|
+
// Whether enableDebug is currently in effect (a sink is installed and levels/guards are raised).
|
|
40
|
+
export function isDebugEnabled() {
|
|
41
|
+
return _enabled;
|
|
42
|
+
}
|
|
43
|
+
// Registers a diagnostic subsystem's wiring — the log channels whose verbosity enableDebug should
|
|
44
|
+
// raise and the guard-enabler/-disabler bindings it should run for `name`. A package's thin debug
|
|
45
|
+
// adapter (or the app) calls this so enableDebug(['render']) can drive the subsystem without
|
|
46
|
+
// @flighthq/debug importing it. Last-write-wins; vendor-prefix custom subsystem names ('acme.Foo')
|
|
47
|
+
// to avoid colliding with built-ins.
|
|
48
|
+
export function registerDebugSubsystem(name, hooks) {
|
|
49
|
+
_subsystems.set(name, hooks);
|
|
50
|
+
}
|
|
51
|
+
// Removes a subsystem registration. Returns false when nothing was registered under `name`
|
|
52
|
+
// (sentinel, not a throw). Does not affect an already-active debug session.
|
|
53
|
+
export function unregisterDebugSubsystem(name) {
|
|
54
|
+
return _subsystems.delete(name);
|
|
55
|
+
}
|
|
56
|
+
const _subsystems = new Map();
|
|
57
|
+
const _enabledSubsystems = [];
|
|
58
|
+
let _enabled = false;
|
|
59
|
+
let _installedSink = null;
|
|
60
|
+
let _savedGlobalLevel = LogLevel.Verbose;
|
|
61
|
+
// Raises the global level and every selected channel's level to `level` for the session.
|
|
62
|
+
function _applyDebugLevels(level, channels) {
|
|
63
|
+
setLogLevel(level);
|
|
64
|
+
for (const channel of channels)
|
|
65
|
+
setLogChannelLevel(channel, level);
|
|
66
|
+
}
|
|
67
|
+
// Gathers the channels to raise: every selected subsystem's channels plus any explicit extras.
|
|
68
|
+
function _collectDebugChannels(subsystems, extra) {
|
|
69
|
+
const channels = [];
|
|
70
|
+
for (const hooks of subsystems) {
|
|
71
|
+
if (hooks.channels !== undefined)
|
|
72
|
+
channels.push(...hooks.channels);
|
|
73
|
+
}
|
|
74
|
+
if (extra !== undefined)
|
|
75
|
+
channels.push(...extra);
|
|
76
|
+
return channels;
|
|
77
|
+
}
|
|
78
|
+
// The default dev sink: formats each entry as a human-readable line and writes it to the matching
|
|
79
|
+
// console method. Dev-only — this is the one place @flighthq/debug touches the console, and the
|
|
80
|
+
// package is never in a shipping bundle.
|
|
81
|
+
function _createDefaultDebugSink() {
|
|
82
|
+
const formatter = createTextLogFormatter({ levelPrefix: true });
|
|
83
|
+
return (entry) => {
|
|
84
|
+
if (typeof console === 'undefined')
|
|
85
|
+
return;
|
|
86
|
+
const method = _consoleMethods[entry.level] ?? 'log';
|
|
87
|
+
// eslint-disable-next-line no-console -- the dev debug sink; writing to the console is its job.
|
|
88
|
+
console[method](formatter(entry));
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// Installs the debug sink and records it so disableDebug can remove exactly this one.
|
|
92
|
+
function _installDebugSink(sink) {
|
|
93
|
+
_installedSink = sink;
|
|
94
|
+
addLogSink(sink);
|
|
95
|
+
}
|
|
96
|
+
// Removes the installed debug sink, if any.
|
|
97
|
+
function _removeDebugSink() {
|
|
98
|
+
if (_installedSink === null)
|
|
99
|
+
return;
|
|
100
|
+
removeLogSink(_installedSink);
|
|
101
|
+
_installedSink = null;
|
|
102
|
+
}
|
|
103
|
+
// Resolves the subsystem hooks to drive: the named ones when `names` is given (unregistered names
|
|
104
|
+
// are skipped), otherwise every registered subsystem.
|
|
105
|
+
function _resolveDebugSubsystems(names) {
|
|
106
|
+
if (names === undefined)
|
|
107
|
+
return [..._subsystems.values()];
|
|
108
|
+
const resolved = [];
|
|
109
|
+
for (const name of names) {
|
|
110
|
+
const hooks = _subsystems.get(name);
|
|
111
|
+
if (hooks !== undefined)
|
|
112
|
+
resolved.push(hooks);
|
|
113
|
+
}
|
|
114
|
+
return resolved;
|
|
115
|
+
}
|
|
116
|
+
// Restores the global level enableDebug saved and clears the per-channel overrides it raised. Debug
|
|
117
|
+
// owns per-channel verbosity for the duration of a session, so disabling wipes those overrides back
|
|
118
|
+
// to inheriting the global level.
|
|
119
|
+
function _restoreDebugLevels() {
|
|
120
|
+
setLogLevel(_savedGlobalLevel);
|
|
121
|
+
clearLogChannelLevels();
|
|
122
|
+
}
|
|
123
|
+
const _consoleMethods = {
|
|
124
|
+
[LogLevel.None]: 'log',
|
|
125
|
+
[LogLevel.Error]: 'error',
|
|
126
|
+
[LogLevel.Warn]: 'warn',
|
|
127
|
+
[LogLevel.Info]: 'info',
|
|
128
|
+
[LogLevel.Debug]: 'debug',
|
|
129
|
+
[LogLevel.Verbose]: 'log',
|
|
130
|
+
};
|
|
131
|
+
//# sourceMappingURL=debug.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"debug.js","sourceRoot":"","sources":["../src/debug.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { LogTimer } from '@flighthq/types';
|
|
2
|
+
export declare function beginDebugSpan(name: string, channel?: string | null): LogTimer | null;
|
|
3
|
+
export declare function endDebugSpan(timer: Readonly<LogTimer> | null): number;
|
|
4
|
+
export declare function markDebugFrame(label?: string, channel?: string | null): void;
|
|
5
|
+
export declare function measureDebugSpan<T>(name: string, fn: () => T, channel?: string | null): T;
|
|
6
|
+
//# sourceMappingURL=debugTiming.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"debugTiming.d.ts","sourceRoot":"","sources":["../src/debugTiming.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAShD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,MAAM,GAAG,IAAW,GAAG,QAAQ,GAAG,IAAI,CAE3F;AAKD,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,GAAG,IAAI,GAAG,MAAM,CAErE;AAMD,wBAAgB,cAAc,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,GAAE,MAAM,GAAG,IAAW,GAAG,IAAI,CAGlF;AAMD,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,OAAO,GAAE,MAAM,GAAG,IAAW,GAAG,CAAC,CAO/F"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { endLogTimer, logDebug, startLogTimer } from '@flighthq/log';
|
|
2
|
+
import { isDebugEnabled } from './debug';
|
|
3
|
+
// Opens a named timing span, but only while a debug session is active: returns a LogTimer to hand to
|
|
4
|
+
// endDebugSpan, or null when debug is disabled (production, or before enableDebug). Gating here is the
|
|
5
|
+
// point — with debug off, opening a span is one boolean check and allocates nothing, so span
|
|
6
|
+
// instrumentation left in shipping code costs effectively nothing. Pair every non-null result with
|
|
7
|
+
// endDebugSpan (measureDebugSpan brackets both for a single synchronous call).
|
|
8
|
+
export function beginDebugSpan(name, channel = null) {
|
|
9
|
+
return isDebugEnabled() ? startLogTimer(name, channel) : null;
|
|
10
|
+
}
|
|
11
|
+
// Closes a span opened by beginDebugSpan, emitting its elapsed-time Debug entry through the active
|
|
12
|
+
// debug sink and returning the elapsed milliseconds. A null timer — debug was disabled when the span
|
|
13
|
+
// opened — is a no-op returning -1, the sentinel for "not measured".
|
|
14
|
+
export function endDebugSpan(timer) {
|
|
15
|
+
return timer === null ? -1 : endLogTimer(timer);
|
|
16
|
+
}
|
|
17
|
+
// Emits a frame-boundary marker while a debug session is active, so the per-frame span entries between
|
|
18
|
+
// two markers can be read against the frame they fell in. `label` names the frame; omit it to tag the
|
|
19
|
+
// marker with a process-monotonic frame counter. No-op (and no counter advance) when debug is disabled,
|
|
20
|
+
// so a per-frame call in the app loop costs a single boolean check in production.
|
|
21
|
+
export function markDebugFrame(label, channel = null) {
|
|
22
|
+
if (!isDebugEnabled())
|
|
23
|
+
return;
|
|
24
|
+
logDebug({ frame: label ?? ++_debugFrameNumber }, channel);
|
|
25
|
+
}
|
|
26
|
+
// Brackets a synchronous function as a timing span: times `fn` while a debug session is active,
|
|
27
|
+
// emitting one elapsed-time entry, and returns `fn`'s result. When debug is disabled `fn` still runs —
|
|
28
|
+
// only the timing is skipped — so wrapping a call never changes behavior, only observability. The span
|
|
29
|
+
// is closed even if `fn` throws.
|
|
30
|
+
export function measureDebugSpan(name, fn, channel = null) {
|
|
31
|
+
const timer = beginDebugSpan(name, channel);
|
|
32
|
+
try {
|
|
33
|
+
return fn();
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
endDebugSpan(timer);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Monotonic id handed to unlabeled frame markers. Not reset between sessions — a marker's job is to
|
|
40
|
+
// correlate span entries with a frame boundary, for which a unique ascending id suffices.
|
|
41
|
+
let _debugFrameNumber = 0;
|
|
42
|
+
//# sourceMappingURL=debugTiming.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"debugTiming.js","sourceRoot":"","sources":["../src/debugTiming.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGrE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEzC,qGAAqG;AACrG,uGAAuG;AACvG,6FAA6F;AAC7F,mGAAmG;AACnG,+EAA+E;AAC/E,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,UAAyB,IAAI;IACxE,OAAO,cAAc,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAChE,CAAC;AAED,mGAAmG;AACnG,qGAAqG;AACrG,qEAAqE;AACrE,MAAM,UAAU,YAAY,CAAC,KAAgC;IAC3D,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAClD,CAAC;AAED,uGAAuG;AACvG,sGAAsG;AACtG,wGAAwG;AACxG,kFAAkF;AAClF,MAAM,UAAU,cAAc,CAAC,KAAc,EAAE,UAAyB,IAAI;IAC1E,IAAI,CAAC,cAAc,EAAE;QAAE,OAAO;IAC9B,QAAQ,CAAC,EAAE,KAAK,EAAE,KAAK,IAAI,EAAE,iBAAiB,EAAE,EAAE,OAAO,CAAC,CAAC;AAC7D,CAAC;AAED,gGAAgG;AAChG,uGAAuG;AACvG,uGAAuG;AACvG,iCAAiC;AACjC,MAAM,UAAU,gBAAgB,CAAI,IAAY,EAAE,EAAW,EAAE,UAAyB,IAAI;IAC1F,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC5C,IAAI,CAAC;QACH,OAAO,EAAE,EAAE,CAAC;IACd,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC;AAED,oGAAoG;AACpG,0FAA0F;AAC1F,IAAI,iBAAiB,GAAG,CAAC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/debug",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/log": "0.1.0",
|
|
31
|
+
"@flighthq/types": "0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^5.3.0"
|
|
35
|
+
},
|
|
36
|
+
"description": "Opt-in diagnostics umbrella over @flighthq/log — an open subsystem registry driving per-channel log levels, guard-enablers, and a dev console sink",
|
|
37
|
+
"sideEffects": false
|
|
38
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearLogChannelLevels,
|
|
3
|
+
clearLogSinks,
|
|
4
|
+
createMemoryLogSink,
|
|
5
|
+
getLogChannelLevel,
|
|
6
|
+
getLogLevel,
|
|
7
|
+
getMemoryLogSinkEntries,
|
|
8
|
+
log,
|
|
9
|
+
setLogLevel,
|
|
10
|
+
} from '@flighthq/log';
|
|
11
|
+
import type { MemoryLogSink } from '@flighthq/log';
|
|
12
|
+
import { LogLevel } from '@flighthq/types';
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
14
|
+
|
|
15
|
+
import { disableDebug, enableDebug, isDebugEnabled, registerDebugSubsystem, unregisterDebugSubsystem } from './debug';
|
|
16
|
+
|
|
17
|
+
// Subsystem names touched by the tests, unregistered between each so the module-global registry
|
|
18
|
+
// never leaks state across cases.
|
|
19
|
+
const testSubsystemNames = ['render', 'input', 'audio', 'acme.custom'];
|
|
20
|
+
|
|
21
|
+
function readMemory(sink: MemoryLogSink): readonly unknown[] {
|
|
22
|
+
return getMemoryLogSinkEntries(sink);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
if (isDebugEnabled()) disableDebug();
|
|
27
|
+
for (const name of testSubsystemNames) unregisterDebugSubsystem(name);
|
|
28
|
+
clearLogSinks();
|
|
29
|
+
clearLogChannelLevels();
|
|
30
|
+
setLogLevel(LogLevel.Verbose);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
if (isDebugEnabled()) disableDebug();
|
|
35
|
+
for (const name of testSubsystemNames) unregisterDebugSubsystem(name);
|
|
36
|
+
clearLogSinks();
|
|
37
|
+
clearLogChannelLevels();
|
|
38
|
+
setLogLevel(LogLevel.Verbose);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('disableDebug', () => {
|
|
42
|
+
it('removes the installed sink so a later log does not reach it', () => {
|
|
43
|
+
const memory = createMemoryLogSink(16);
|
|
44
|
+
registerDebugSubsystem('render', { channels: ['render'] });
|
|
45
|
+
enableDebug({ subsystems: ['render'], sink: memory.sink });
|
|
46
|
+
|
|
47
|
+
log(LogLevel.Debug, 'before', 'render');
|
|
48
|
+
expect(readMemory(memory)).toHaveLength(1);
|
|
49
|
+
|
|
50
|
+
disableDebug();
|
|
51
|
+
log(LogLevel.Debug, 'after', 'render');
|
|
52
|
+
expect(readMemory(memory)).toHaveLength(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('runs each enabled subsystem disableGuards binding', () => {
|
|
56
|
+
const disableGuards = vi.fn();
|
|
57
|
+
registerDebugSubsystem('render', { disableGuards });
|
|
58
|
+
enableDebug({ subsystems: ['render'], sink: createMemoryLogSink(1).sink });
|
|
59
|
+
|
|
60
|
+
expect(disableGuards).not.toHaveBeenCalled();
|
|
61
|
+
disableDebug();
|
|
62
|
+
expect(disableGuards).toHaveBeenCalledTimes(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('restores the global level and clears the channel overrides it raised', () => {
|
|
66
|
+
setLogLevel(LogLevel.Warn);
|
|
67
|
+
registerDebugSubsystem('render', { channels: ['render'] });
|
|
68
|
+
enableDebug({ subsystems: ['render'], level: LogLevel.Verbose, sink: createMemoryLogSink(1).sink });
|
|
69
|
+
|
|
70
|
+
disableDebug();
|
|
71
|
+
expect(getLogLevel()).toBe(LogLevel.Warn);
|
|
72
|
+
expect(getLogChannelLevel('render')).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('is a no-op when debug is not enabled', () => {
|
|
76
|
+
expect(isDebugEnabled()).toBe(false);
|
|
77
|
+
expect(() => disableDebug()).not.toThrow();
|
|
78
|
+
expect(isDebugEnabled()).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('enableDebug', () => {
|
|
83
|
+
it('runs a selected subsystem enableGuards and raises its channel levels', () => {
|
|
84
|
+
const enableGuards = vi.fn();
|
|
85
|
+
registerDebugSubsystem('render', { channels: ['render', 'render.batch'], enableGuards });
|
|
86
|
+
|
|
87
|
+
enableDebug({ subsystems: ['render'], sink: createMemoryLogSink(1).sink });
|
|
88
|
+
|
|
89
|
+
expect(enableGuards).toHaveBeenCalledTimes(1);
|
|
90
|
+
expect(getLogChannelLevel('render')).toBe(LogLevel.Debug);
|
|
91
|
+
expect(getLogChannelLevel('render.batch')).toBe(LogLevel.Debug);
|
|
92
|
+
expect(isDebugEnabled()).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('routes a log on a raised channel to the installed sink', () => {
|
|
96
|
+
const memory = createMemoryLogSink(16);
|
|
97
|
+
registerDebugSubsystem('render', { channels: ['render'] });
|
|
98
|
+
enableDebug({ subsystems: ['render'], sink: memory.sink });
|
|
99
|
+
|
|
100
|
+
log(LogLevel.Debug, { msg: 'draw' }, 'render');
|
|
101
|
+
expect(readMemory(memory)).toHaveLength(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('honors an explicit level and extra channels', () => {
|
|
105
|
+
registerDebugSubsystem('render', { channels: ['render'] });
|
|
106
|
+
enableDebug({
|
|
107
|
+
subsystems: ['render'],
|
|
108
|
+
level: LogLevel.Verbose,
|
|
109
|
+
channels: ['app'],
|
|
110
|
+
sink: createMemoryLogSink(1).sink,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(getLogLevel()).toBe(LogLevel.Verbose);
|
|
114
|
+
expect(getLogChannelLevel('render')).toBe(LogLevel.Verbose);
|
|
115
|
+
expect(getLogChannelLevel('app')).toBe(LogLevel.Verbose);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('enables every registered subsystem when none are named', () => {
|
|
119
|
+
const renderGuards = vi.fn();
|
|
120
|
+
const inputGuards = vi.fn();
|
|
121
|
+
registerDebugSubsystem('render', { enableGuards: renderGuards });
|
|
122
|
+
registerDebugSubsystem('input', { enableGuards: inputGuards });
|
|
123
|
+
|
|
124
|
+
enableDebug({ sink: createMemoryLogSink(1).sink });
|
|
125
|
+
|
|
126
|
+
expect(renderGuards).toHaveBeenCalledTimes(1);
|
|
127
|
+
expect(inputGuards).toHaveBeenCalledTimes(1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('skips an unregistered subsystem name without throwing', () => {
|
|
131
|
+
const memory = createMemoryLogSink(1);
|
|
132
|
+
expect(() => enableDebug({ subsystems: ['audio'], sink: memory.sink })).not.toThrow();
|
|
133
|
+
expect(isDebugEnabled()).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('is idempotent — a second enable while active does not re-run guards or re-install', () => {
|
|
137
|
+
const enableGuards = vi.fn();
|
|
138
|
+
const memory = createMemoryLogSink(16);
|
|
139
|
+
registerDebugSubsystem('render', { channels: ['render'], enableGuards });
|
|
140
|
+
|
|
141
|
+
enableDebug({ subsystems: ['render'], sink: memory.sink });
|
|
142
|
+
enableDebug({ subsystems: ['render'], sink: memory.sink });
|
|
143
|
+
|
|
144
|
+
expect(enableGuards).toHaveBeenCalledTimes(1);
|
|
145
|
+
log(LogLevel.Debug, 'once', 'render');
|
|
146
|
+
expect(readMemory(memory)).toHaveLength(1);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('isDebugEnabled', () => {
|
|
151
|
+
it('reports the enable/disable lifecycle', () => {
|
|
152
|
+
expect(isDebugEnabled()).toBe(false);
|
|
153
|
+
enableDebug({ sink: createMemoryLogSink(1).sink });
|
|
154
|
+
expect(isDebugEnabled()).toBe(true);
|
|
155
|
+
disableDebug();
|
|
156
|
+
expect(isDebugEnabled()).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('registerDebugSubsystem', () => {
|
|
161
|
+
it('drives a newly registered subsystem through enableDebug', () => {
|
|
162
|
+
const enableGuards = vi.fn();
|
|
163
|
+
registerDebugSubsystem('acme.custom', { channels: ['acme.custom'], enableGuards });
|
|
164
|
+
|
|
165
|
+
enableDebug({ subsystems: ['acme.custom'], sink: createMemoryLogSink(1).sink });
|
|
166
|
+
|
|
167
|
+
expect(enableGuards).toHaveBeenCalledTimes(1);
|
|
168
|
+
expect(getLogChannelLevel('acme.custom')).toBe(LogLevel.Debug);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('is last-write-wins for a repeated name', () => {
|
|
172
|
+
const first = vi.fn();
|
|
173
|
+
const second = vi.fn();
|
|
174
|
+
registerDebugSubsystem('render', { enableGuards: first });
|
|
175
|
+
registerDebugSubsystem('render', { enableGuards: second });
|
|
176
|
+
|
|
177
|
+
enableDebug({ subsystems: ['render'], sink: createMemoryLogSink(1).sink });
|
|
178
|
+
|
|
179
|
+
expect(first).not.toHaveBeenCalled();
|
|
180
|
+
expect(second).toHaveBeenCalledTimes(1);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('unregisterDebugSubsystem', () => {
|
|
185
|
+
it('returns true and stops the subsystem from being driven', () => {
|
|
186
|
+
const enableGuards = vi.fn();
|
|
187
|
+
registerDebugSubsystem('render', { enableGuards });
|
|
188
|
+
|
|
189
|
+
expect(unregisterDebugSubsystem('render')).toBe(true);
|
|
190
|
+
enableDebug({ subsystems: ['render'], sink: createMemoryLogSink(1).sink });
|
|
191
|
+
expect(enableGuards).not.toHaveBeenCalled();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('returns false for a name that was never registered', () => {
|
|
195
|
+
expect(unregisterDebugSubsystem('audio')).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addLogSink,
|
|
3
|
+
clearLogChannelLevels,
|
|
4
|
+
clearLogSinks,
|
|
5
|
+
createMemoryLogSink,
|
|
6
|
+
getMemoryLogSinkEntries,
|
|
7
|
+
setLogLevel,
|
|
8
|
+
} from '@flighthq/log';
|
|
9
|
+
import type { MemoryLogSink } from '@flighthq/log';
|
|
10
|
+
import { LogLevel } from '@flighthq/types';
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
12
|
+
|
|
13
|
+
import { disableDebug, enableDebug, isDebugEnabled } from './debug';
|
|
14
|
+
import { beginDebugSpan, endDebugSpan, markDebugFrame, measureDebugSpan } from './debugTiming';
|
|
15
|
+
|
|
16
|
+
function entryData(sink: MemoryLogSink): Readonly<Record<string, unknown>>[] {
|
|
17
|
+
return getMemoryLogSinkEntries(sink).map((entry) => (entry as { data: Record<string, unknown> }).data);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
if (isDebugEnabled()) disableDebug();
|
|
22
|
+
clearLogSinks();
|
|
23
|
+
clearLogChannelLevels();
|
|
24
|
+
setLogLevel(LogLevel.Verbose);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
if (isDebugEnabled()) disableDebug();
|
|
29
|
+
clearLogSinks();
|
|
30
|
+
clearLogChannelLevels();
|
|
31
|
+
setLogLevel(LogLevel.Verbose);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('beginDebugSpan', () => {
|
|
35
|
+
it('returns null when debug is disabled', () => {
|
|
36
|
+
expect(beginDebugSpan('draw')).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns a running timer when debug is enabled', () => {
|
|
40
|
+
enableDebug({ sink: createMemoryLogSink(1).sink });
|
|
41
|
+
const timer = beginDebugSpan('draw');
|
|
42
|
+
expect(timer).not.toBeNull();
|
|
43
|
+
expect(timer!.label).toBe('draw');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('endDebugSpan', () => {
|
|
48
|
+
it('is a no-op returning -1 for a null timer', () => {
|
|
49
|
+
expect(endDebugSpan(null)).toBe(-1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('emits an elapsed-time entry and returns the elapsed milliseconds', () => {
|
|
53
|
+
const memory = createMemoryLogSink(16);
|
|
54
|
+
enableDebug({ sink: memory.sink });
|
|
55
|
+
const elapsed = endDebugSpan(beginDebugSpan('draw'));
|
|
56
|
+
expect(elapsed).toBeGreaterThanOrEqual(0);
|
|
57
|
+
const data = entryData(memory);
|
|
58
|
+
expect(data).toHaveLength(1);
|
|
59
|
+
expect(data[0].label).toBe('draw');
|
|
60
|
+
expect(typeof data[0].elapsedMs).toBe('number');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('markDebugFrame', () => {
|
|
65
|
+
it('is a no-op when debug is disabled', () => {
|
|
66
|
+
const memory = createMemoryLogSink(4);
|
|
67
|
+
addLogSink(memory.sink);
|
|
68
|
+
markDebugFrame();
|
|
69
|
+
expect(getMemoryLogSinkEntries(memory)).toHaveLength(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('emits a labeled frame marker when debug is enabled', () => {
|
|
73
|
+
const memory = createMemoryLogSink(16);
|
|
74
|
+
enableDebug({ sink: memory.sink });
|
|
75
|
+
markDebugFrame('level-load');
|
|
76
|
+
expect(entryData(memory)[0].frame).toBe('level-load');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('tags an unlabeled marker with an ascending counter', () => {
|
|
80
|
+
const memory = createMemoryLogSink(16);
|
|
81
|
+
enableDebug({ sink: memory.sink });
|
|
82
|
+
markDebugFrame();
|
|
83
|
+
markDebugFrame();
|
|
84
|
+
const [a, b] = entryData(memory);
|
|
85
|
+
expect(typeof a.frame).toBe('number');
|
|
86
|
+
expect(b.frame as number).toBeGreaterThan(a.frame as number);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('measureDebugSpan', () => {
|
|
91
|
+
it('returns the wrapped function result', () => {
|
|
92
|
+
expect(measureDebugSpan('compute', () => 42)).toBe(42);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('runs the function but emits nothing when debug is disabled', () => {
|
|
96
|
+
const memory = createMemoryLogSink(4);
|
|
97
|
+
addLogSink(memory.sink);
|
|
98
|
+
let ran = false;
|
|
99
|
+
measureDebugSpan('compute', () => {
|
|
100
|
+
ran = true;
|
|
101
|
+
});
|
|
102
|
+
expect(ran).toBe(true);
|
|
103
|
+
expect(getMemoryLogSinkEntries(memory)).toHaveLength(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('emits one timing entry named for the span when debug is enabled', () => {
|
|
107
|
+
const memory = createMemoryLogSink(16);
|
|
108
|
+
enableDebug({ sink: memory.sink });
|
|
109
|
+
measureDebugSpan('compute', () => 1);
|
|
110
|
+
const data = entryData(memory);
|
|
111
|
+
expect(data).toHaveLength(1);
|
|
112
|
+
expect(data[0].label).toBe('compute');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('closes the span even when the function throws', () => {
|
|
116
|
+
const memory = createMemoryLogSink(16);
|
|
117
|
+
enableDebug({ sink: memory.sink });
|
|
118
|
+
expect(() =>
|
|
119
|
+
measureDebugSpan('boom', () => {
|
|
120
|
+
throw new Error('nope');
|
|
121
|
+
}),
|
|
122
|
+
).toThrow('nope');
|
|
123
|
+
expect(entryData(memory)).toHaveLength(1);
|
|
124
|
+
});
|
|
125
|
+
});
|