@pix3/vite-plugin 0.0.0-stage → 2.0.0-alpha.1

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.
Files changed (101) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +39 -2
  3. package/dist/build/bundle-hooks.d.ts +23 -0
  4. package/dist/build/bundle-hooks.js +79 -0
  5. package/dist/build/classic-script.d.ts +11 -0
  6. package/dist/build/classic-script.js +46 -0
  7. package/dist/build/compress.d.ts +29 -0
  8. package/dist/build/compress.js +63 -0
  9. package/dist/build/dependency-imports.d.ts +53 -0
  10. package/dist/build/dependency-imports.js +239 -0
  11. package/dist/build/dev-manifest.d.ts +10 -0
  12. package/dist/build/dev-manifest.js +25 -0
  13. package/dist/build/editor-flush.d.ts +42 -0
  14. package/dist/build/editor-flush.js +91 -0
  15. package/dist/build/finish.d.ts +11 -0
  16. package/dist/build/finish.js +126 -0
  17. package/dist/build/output-plugins.d.ts +26 -0
  18. package/dist/build/output-plugins.js +88 -0
  19. package/dist/build/plan.d.ts +75 -0
  20. package/dist/build/plan.js +162 -0
  21. package/dist/build/player-modules.d.ts +123 -0
  22. package/dist/build/player-modules.js +254 -0
  23. package/dist/build/project-manifest.d.ts +57 -0
  24. package/dist/build/project-manifest.js +169 -0
  25. package/dist/build/record.d.ts +25 -0
  26. package/dist/build/record.js +14 -0
  27. package/dist/build/report.d.ts +114 -0
  28. package/dist/build/report.js +102 -0
  29. package/dist/build/scan.d.ts +84 -0
  30. package/dist/build/scan.js +375 -0
  31. package/dist/build/strip-decision.d.ts +27 -0
  32. package/dist/build/strip-decision.js +82 -0
  33. package/dist/build/strippable-runtime-modules.d.ts +85 -0
  34. package/dist/build/strippable-runtime-modules.js +309 -0
  35. package/dist/build/zip.d.ts +6 -0
  36. package/dist/build/zip.js +28 -0
  37. package/dist/client/host-files.d.ts +96 -0
  38. package/dist/client/host-files.js +169 -0
  39. package/dist/client/index.d.ts +218 -0
  40. package/dist/client/index.js +381 -0
  41. package/dist/dev-info.d.ts +55 -0
  42. package/dist/dev-info.js +116 -0
  43. package/dist/dev-server.d.ts +28 -0
  44. package/dist/dev-server.js +139 -0
  45. package/dist/editor-page.d.ts +31 -0
  46. package/dist/editor-page.js +74 -0
  47. package/dist/files/changeset.d.ts +84 -0
  48. package/dist/files/changeset.js +321 -0
  49. package/dist/files/content-type.d.ts +1 -0
  50. package/dist/files/content-type.js +56 -0
  51. package/dist/files/history.d.ts +67 -0
  52. package/dist/files/history.js +162 -0
  53. package/dist/files/paths.d.ts +41 -0
  54. package/dist/files/paths.js +144 -0
  55. package/dist/files/project-files.d.ts +208 -0
  56. package/dist/files/project-files.js +1062 -0
  57. package/dist/files/scan.d.ts +71 -0
  58. package/dist/files/scan.js +252 -0
  59. package/dist/index.d.ts +56 -0
  60. package/dist/index.js +290 -0
  61. package/dist/module-paths.d.ts +13 -0
  62. package/dist/module-paths.js +32 -0
  63. package/dist/player/debug/context.d.ts +9 -0
  64. package/dist/player/debug/context.js +1 -0
  65. package/dist/player/debug/device-info.d.ts +15 -0
  66. package/dist/player/debug/device-info.js +36 -0
  67. package/dist/player/debug/frame-stats.d.ts +79 -0
  68. package/dist/player/debug/frame-stats.js +159 -0
  69. package/dist/player/debug/index.d.ts +10 -0
  70. package/dist/player/debug/index.js +20 -0
  71. package/dist/player/debug/long-frames.d.ts +46 -0
  72. package/dist/player/debug/long-frames.js +79 -0
  73. package/dist/player/debug/overlay.d.ts +25 -0
  74. package/dist/player/debug/overlay.js +320 -0
  75. package/dist/player/debug/session.d.ts +92 -0
  76. package/dist/player/debug/session.js +156 -0
  77. package/dist/player/dev/editor-link.d.ts +16 -0
  78. package/dist/player/dev/editor-link.js +125 -0
  79. package/dist/player/index.d.ts +38 -0
  80. package/dist/player/index.js +119 -0
  81. package/dist/server/editor-socket.d.ts +53 -0
  82. package/dist/server/editor-socket.js +134 -0
  83. package/dist/server/guard.d.ts +61 -0
  84. package/dist/server/guard.js +165 -0
  85. package/dist/server/http.d.ts +24 -0
  86. package/dist/server/http.js +70 -0
  87. package/dist/server/image-keys.d.ts +46 -0
  88. package/dist/server/image-keys.js +118 -0
  89. package/dist/server/image-proxy.d.ts +32 -0
  90. package/dist/server/image-proxy.js +124 -0
  91. package/dist/server/router.d.ts +54 -0
  92. package/dist/server/router.js +147 -0
  93. package/dist/sync/barrier.d.ts +69 -0
  94. package/dist/sync/barrier.js +240 -0
  95. package/dist/sync/hot-update.d.ts +16 -0
  96. package/dist/sync/hot-update.js +58 -0
  97. package/dist/sync/script-graph.d.ts +79 -0
  98. package/dist/sync/script-graph.js +214 -0
  99. package/dist/vite-config.d.ts +12 -0
  100. package/dist/vite-config.js +65 -0
  101. package/package.json +58 -4
@@ -0,0 +1,32 @@
1
+ import { realpathSync } from 'node:fs';
2
+ import { isAbsolute, relative, sep } from 'node:path';
3
+ /** A module id without its query (`/a/b.ts?v=1` → `/a/b.ts`). */
4
+ export const fileOf = (id) => id.split('?')[0];
5
+ /** A platform path with `/` separators. */
6
+ export const posixPath = (path) => path.split(sep).join('/');
7
+ /** `file` relative to `dir`, `/`-separated, when it lies strictly inside `dir`; else null. */
8
+ export const relativeInside = (dir, file) => {
9
+ const rel = relative(dir, file);
10
+ return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel) ? posixPath(rel) : null;
11
+ };
12
+ export const isUnder = (file, dir) => dir !== null && relativeInside(dir, file) !== null;
13
+ const realPaths = new Map();
14
+ /**
15
+ * The real path of a module id (query dropped; the id itself when it is not a file), cached. The
16
+ * runtime's `src/` is a real path, and module ids are not always (`resolve.preserveSymlinks`, a
17
+ * linked workspace): comparing the two as written would silently strip nothing.
18
+ */
19
+ export const realFileOf = (id) => {
20
+ const file = fileOf(id);
21
+ let real = realPaths.get(file);
22
+ if (real === undefined) {
23
+ try {
24
+ real = realpathSync.native(file);
25
+ }
26
+ catch {
27
+ real = file;
28
+ }
29
+ realPaths.set(file, real);
30
+ }
31
+ return real;
32
+ };
@@ -0,0 +1,9 @@
1
+ import type { AudioService, RuntimeRenderer, SceneRunner } from '@pix3/runtime';
2
+ import type { Pix3PlayerState } from '../index';
3
+ /** What the player hands the debug overlay (`virtual:pix3/dev`'s `installDevTools`). */
4
+ export interface Pix3DebugContext {
5
+ readonly runner: SceneRunner;
6
+ readonly renderer: RuntimeRenderer;
7
+ readonly audio: AudioService;
8
+ readonly state: Pix3PlayerState;
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ /** What the debug overlay shows about the device (the 1.x remote preview's `device-info`). */
2
+ export interface DeviceInfo {
3
+ readonly userAgent: string;
4
+ /** `UNMASKED_RENDERER_WEBGL` when the browser exposes it, else `RENDERER`. */
5
+ readonly gpu: string | null;
6
+ readonly devicePixelRatio: number;
7
+ readonly screen: string;
8
+ readonly viewport: string;
9
+ /** `navigator.deviceMemory` (Chromium, coarse and capped at 8). */
10
+ readonly memoryGb: number | null;
11
+ readonly cores: number | null;
12
+ }
13
+ export declare const readDeviceInfo: () => DeviceInfo;
14
+ /** Used JS heap in MiB (`performance.memory`, Chromium only). */
15
+ export declare const readHeapMb: () => number | null;
@@ -0,0 +1,36 @@
1
+ /** A throwaway GL context: the game's own context is not touched. */
2
+ const readGpu = () => {
3
+ try {
4
+ const canvas = document.createElement('canvas');
5
+ const gl = canvas.getContext('webgl2') ?? canvas.getContext('webgl');
6
+ if (!gl)
7
+ return null;
8
+ const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
9
+ const renderer = gl.getParameter(debugInfo ? debugInfo.UNMASKED_RENDERER_WEBGL : gl.RENDERER);
10
+ gl.getExtension('WEBGL_lose_context')?.loseContext();
11
+ return typeof renderer === 'string' && renderer.length > 0 ? renderer : null;
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ };
17
+ let gpu;
18
+ export const readDeviceInfo = () => {
19
+ if (gpu === undefined)
20
+ gpu = readGpu();
21
+ const nav = navigator;
22
+ return {
23
+ userAgent: navigator.userAgent,
24
+ gpu,
25
+ devicePixelRatio: window.devicePixelRatio || 1,
26
+ screen: `${window.screen?.width ?? 0}×${window.screen?.height ?? 0}`,
27
+ viewport: `${window.innerWidth}×${window.innerHeight}`,
28
+ memoryGb: typeof nav.deviceMemory === 'number' ? nav.deviceMemory : null,
29
+ cores: typeof navigator.hardwareConcurrency === 'number' ? navigator.hardwareConcurrency : null,
30
+ };
31
+ };
32
+ /** Used JS heap in MiB (`performance.memory`, Chromium only). */
33
+ export const readHeapMb = () => {
34
+ const used = performance.memory?.usedJSHeapSize;
35
+ return typeof used === 'number' && Number.isFinite(used) ? used / (1024 * 1024) : null;
36
+ };
@@ -0,0 +1,79 @@
1
+ import type { SceneRunnerFrameSample } from '@pix3/runtime';
2
+ /**
3
+ * The debug overlay's frame bookkeeping, DOM-free: a fixed-bucket interval histogram for the
4
+ * session's percentiles, a ring of recent frames for the chart, and a rolling window of averages
5
+ * (frame split, FPS, `FrameProfiler` activities) that the overlay swaps at its text cadence.
6
+ * Everything here is O(1) per frame and allocates nothing per frame.
7
+ */
8
+ /** Frames past this count as hitches: ~2 missed vsyncs at 60 Hz. */
9
+ export declare const LONG_FRAME_MS = 33.4;
10
+ export interface FrameStability {
11
+ readonly sampleCount: number;
12
+ readonly p50Ms: number | null;
13
+ readonly p95Ms: number | null;
14
+ readonly p99Ms: number | null;
15
+ readonly worstMs: number | null;
16
+ /** Frames longer than {@link LONG_FRAME_MS}. */
17
+ readonly longCount: number;
18
+ readonly longPercent: number | null;
19
+ }
20
+ /**
21
+ * 1 ms buckets up to 100 ms plus an overflow bucket (the 1.x Profiler's `FrameIntervalHistogram`):
22
+ * a session can run for hours, so nothing keeps every frame. Percentiles are nearest-rank and
23
+ * report the bucket's upper edge clamped to the worst frame, so they are never optimistic.
24
+ */
25
+ export declare class FrameIntervalHistogram {
26
+ private readonly buckets;
27
+ private count;
28
+ private long;
29
+ private worst;
30
+ reset(): void;
31
+ record(intervalMs: number): void;
32
+ snapshot(): FrameStability;
33
+ private percentile;
34
+ }
35
+ /** The last {@link capacity} frames, split the way the chart stacks them. */
36
+ export declare class FrameRing {
37
+ readonly capacity: number;
38
+ readonly logicMs: Float32Array;
39
+ readonly renderMs: Float32Array;
40
+ /** Frame duration (`dt`), the bar's full height; the rest above logic + render is "not us". */
41
+ readonly frameMs: Float32Array;
42
+ private next;
43
+ private filled;
44
+ constructor(capacity?: number);
45
+ push(logicMs: number, renderMs: number, frameMs: number): void;
46
+ get length(): number;
47
+ /** Ring index of the `i`-th oldest retained frame (`0` = oldest). */
48
+ indexOf(i: number): number;
49
+ clear(): void;
50
+ }
51
+ export interface FrameWindow {
52
+ readonly frames: number;
53
+ /** Frames per second of wall clock in the window (`frames / Σdt`), null when empty. */
54
+ readonly fps: number | null;
55
+ readonly frameMs: number | null;
56
+ readonly logicMs: number | null;
57
+ readonly renderMs: number | null;
58
+ readonly unaccountedMs: number | null;
59
+ readonly rafLatenessMs: number | null;
60
+ /** `FrameProfiler` activities, mean self time per frame, largest first. */
61
+ readonly activities: readonly {
62
+ readonly label: string;
63
+ readonly ms: number;
64
+ }[];
65
+ }
66
+ export declare const EMPTY_WINDOW: FrameWindow;
67
+ /** Sums since the last {@link close}; `close` turns them into a {@link FrameWindow}. */
68
+ export declare class FrameWindowAccumulator {
69
+ private frames;
70
+ private dtSec;
71
+ private logic;
72
+ private render;
73
+ private unaccounted;
74
+ private lateness;
75
+ private readonly activitySums;
76
+ add(sample: SceneRunnerFrameSample): void;
77
+ close(): FrameWindow;
78
+ private addActivities;
79
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * The debug overlay's frame bookkeeping, DOM-free: a fixed-bucket interval histogram for the
3
+ * session's percentiles, a ring of recent frames for the chart, and a rolling window of averages
4
+ * (frame split, FPS, `FrameProfiler` activities) that the overlay swaps at its text cadence.
5
+ * Everything here is O(1) per frame and allocates nothing per frame.
6
+ */
7
+ /** Frames past this count as hitches: ~2 missed vsyncs at 60 Hz. */
8
+ export const LONG_FRAME_MS = 33.4;
9
+ const HISTOGRAM_MAX_MS = 100;
10
+ /**
11
+ * 1 ms buckets up to 100 ms plus an overflow bucket (the 1.x Profiler's `FrameIntervalHistogram`):
12
+ * a session can run for hours, so nothing keeps every frame. Percentiles are nearest-rank and
13
+ * report the bucket's upper edge clamped to the worst frame, so they are never optimistic.
14
+ */
15
+ export class FrameIntervalHistogram {
16
+ buckets = new Uint32Array(HISTOGRAM_MAX_MS + 1);
17
+ count = 0;
18
+ long = 0;
19
+ worst = 0;
20
+ reset() {
21
+ this.buckets.fill(0);
22
+ this.count = 0;
23
+ this.long = 0;
24
+ this.worst = 0;
25
+ }
26
+ record(intervalMs) {
27
+ if (!Number.isFinite(intervalMs) || intervalMs < 0)
28
+ return;
29
+ this.buckets[Math.min(Math.floor(intervalMs), HISTOGRAM_MAX_MS)] += 1;
30
+ this.count += 1;
31
+ if (intervalMs > this.worst)
32
+ this.worst = intervalMs;
33
+ if (intervalMs > LONG_FRAME_MS)
34
+ this.long += 1;
35
+ }
36
+ snapshot() {
37
+ const empty = this.count === 0;
38
+ return {
39
+ sampleCount: this.count,
40
+ p50Ms: this.percentile(50),
41
+ p95Ms: this.percentile(95),
42
+ p99Ms: this.percentile(99),
43
+ worstMs: empty ? null : this.worst,
44
+ longCount: this.long,
45
+ longPercent: empty ? null : (this.long / this.count) * 100,
46
+ };
47
+ }
48
+ percentile(percent) {
49
+ if (this.count === 0)
50
+ return null;
51
+ const rank = Math.max(1, Math.ceil((percent / 100) * this.count));
52
+ let cumulative = 0;
53
+ for (let bucket = 0; bucket < this.buckets.length; bucket += 1) {
54
+ cumulative += this.buckets[bucket];
55
+ if (cumulative >= rank) {
56
+ // The overflow bucket has no upper edge: the worst frame is the only honest answer.
57
+ return bucket === HISTOGRAM_MAX_MS ? this.worst : Math.min(bucket + 1, this.worst);
58
+ }
59
+ }
60
+ return this.worst;
61
+ }
62
+ }
63
+ /** The last {@link capacity} frames, split the way the chart stacks them. */
64
+ export class FrameRing {
65
+ capacity;
66
+ logicMs;
67
+ renderMs;
68
+ /** Frame duration (`dt`), the bar's full height; the rest above logic + render is "not us". */
69
+ frameMs;
70
+ next = 0;
71
+ filled = 0;
72
+ constructor(capacity = 240) {
73
+ this.capacity = capacity;
74
+ this.logicMs = new Float32Array(capacity);
75
+ this.renderMs = new Float32Array(capacity);
76
+ this.frameMs = new Float32Array(capacity);
77
+ }
78
+ push(logicMs, renderMs, frameMs) {
79
+ this.logicMs[this.next] = logicMs;
80
+ this.renderMs[this.next] = renderMs;
81
+ this.frameMs[this.next] = frameMs;
82
+ this.next = (this.next + 1) % this.capacity;
83
+ this.filled = Math.min(this.filled + 1, this.capacity);
84
+ }
85
+ get length() {
86
+ return this.filled;
87
+ }
88
+ /** Ring index of the `i`-th oldest retained frame (`0` = oldest). */
89
+ indexOf(i) {
90
+ return (this.next - this.filled + i + this.capacity) % this.capacity;
91
+ }
92
+ clear() {
93
+ this.next = 0;
94
+ this.filled = 0;
95
+ }
96
+ }
97
+ export const EMPTY_WINDOW = Object.freeze({
98
+ frames: 0,
99
+ fps: null,
100
+ frameMs: null,
101
+ logicMs: null,
102
+ renderMs: null,
103
+ unaccountedMs: null,
104
+ rafLatenessMs: null,
105
+ activities: Object.freeze([]),
106
+ });
107
+ /** Sums since the last {@link close}; `close` turns them into a {@link FrameWindow}. */
108
+ export class FrameWindowAccumulator {
109
+ frames = 0;
110
+ dtSec = 0;
111
+ logic = 0;
112
+ render = 0;
113
+ unaccounted = 0;
114
+ lateness = 0;
115
+ activitySums = new Map();
116
+ add(sample) {
117
+ this.frames += 1;
118
+ this.dtSec += sample.dt;
119
+ this.logic += sample.logicMs;
120
+ this.render += sample.renderMs;
121
+ this.unaccounted += sample.unaccountedMs;
122
+ this.lateness += sample.rafLatenessMs;
123
+ if (sample.profilerActivities)
124
+ this.addActivities(sample.profilerActivities);
125
+ }
126
+ close() {
127
+ const n = this.frames;
128
+ if (n === 0)
129
+ return EMPTY_WINDOW;
130
+ const activities = [...this.activitySums]
131
+ .map(([label, sum]) => ({ label, ms: sum / n }))
132
+ .sort((a, b) => b.ms - a.ms);
133
+ const window = {
134
+ frames: n,
135
+ fps: this.dtSec > 0 ? n / this.dtSec : null,
136
+ frameMs: (this.dtSec * 1000) / n,
137
+ logicMs: this.logic / n,
138
+ renderMs: this.render / n,
139
+ unaccountedMs: this.unaccounted / n,
140
+ rafLatenessMs: this.lateness / n,
141
+ activities,
142
+ };
143
+ this.frames = 0;
144
+ this.dtSec = 0;
145
+ this.logic = 0;
146
+ this.render = 0;
147
+ this.unaccounted = 0;
148
+ this.lateness = 0;
149
+ this.activitySums.clear();
150
+ return window;
151
+ }
152
+ addActivities(activities) {
153
+ for (const activity of activities) {
154
+ if (!Number.isFinite(activity.selfTimeMs))
155
+ continue;
156
+ this.activitySums.set(activity.label, (this.activitySums.get(activity.label) ?? 0) + activity.selfTimeMs);
157
+ }
158
+ }
159
+ }
@@ -0,0 +1,10 @@
1
+ import type { Pix3DebugContext } from './context';
2
+ /**
3
+ * Debug mode (`/?debug` on the dev server; `virtual:pix3/dev` imports this only then, and a
4
+ * build never). Mounts the overlay over the running game and exposes
5
+ * `__PIX3_PLAYER__.debug.snapshot()` for an agent. `editorUrl` (null with `editor: false`) adds
6
+ * the header's Editor button. Returns the teardown.
7
+ */
8
+ export declare function mountDebugOverlay(context: Pix3DebugContext, options?: {
9
+ readonly editorUrl?: string | null;
10
+ }): () => void;
@@ -0,0 +1,20 @@
1
+ import { DebugOverlay } from './overlay';
2
+ import { DebugSession } from './session';
3
+ /**
4
+ * Debug mode (`/?debug` on the dev server; `virtual:pix3/dev` imports this only then, and a
5
+ * build never). Mounts the overlay over the running game and exposes
6
+ * `__PIX3_PLAYER__.debug.snapshot()` for an agent. `editorUrl` (null with `editor: false`) adds
7
+ * the header's Editor button. Returns the teardown.
8
+ */
9
+ export function mountDebugOverlay(context, options = {}) {
10
+ const session = new DebugSession(context);
11
+ session.start();
12
+ const overlay = new DebugOverlay(session, options.editorUrl ?? null);
13
+ overlay.mount();
14
+ context.state.debug = { snapshot: () => session.snapshot() };
15
+ return () => {
16
+ overlay.dispose();
17
+ session.dispose();
18
+ delete context.state.debug;
19
+ };
20
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The browser's **Long Animation Frame** entries (LoAF, Chromium), a port of the 1.x Profiler's
3
+ * `LongAnimationFrameObserver`. The runner's own stopwatch only sees its logic and paint; LoAF is
4
+ * the browser's view of the same frame and the only source that says what else ran. LoAF
5
+ * attributes only scripts that ran longer than ~5 ms, so a long frame with no script is a finding
6
+ * (late delivery, compositing, GC), not a gap: it is kept as `script: null`.
7
+ */
8
+ /** One `scripts[]` entry of a LoAF timing (TS's DOM lib has no LoAF types yet). */
9
+ interface LoafScriptLike {
10
+ readonly sourceURL?: string;
11
+ readonly sourceFunctionName?: string;
12
+ readonly duration?: number;
13
+ readonly invoker?: string;
14
+ }
15
+ interface LoafEntryLike {
16
+ readonly duration?: number;
17
+ readonly blockingDuration?: number;
18
+ readonly scripts?: readonly LoafScriptLike[];
19
+ }
20
+ export interface LongFrameRecord {
21
+ readonly durationMs: number;
22
+ readonly blockingMs: number;
23
+ /** `basename.js · function` of the longest attributed script, or null: none was attributed. */
24
+ readonly script: string | null;
25
+ }
26
+ export interface LongFrameStats {
27
+ /** False where the browser has no `long-animation-frame` entries (Firefox, Safari). */
28
+ readonly supported: boolean;
29
+ readonly count: number;
30
+ /** The worst few of the session, longest first. */
31
+ readonly worst: readonly LongFrameRecord[];
32
+ }
33
+ export declare class LongFrameObserver {
34
+ readonly supported: boolean;
35
+ private observer;
36
+ private count;
37
+ private worst;
38
+ constructor();
39
+ start(): void;
40
+ stop(): void;
41
+ reset(): void;
42
+ stats(): LongFrameStats;
43
+ /** One entry, as the observer's callback hands it over (public for the spec). */
44
+ record(entry: LoafEntryLike): void;
45
+ }
46
+ export {};
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The browser's **Long Animation Frame** entries (LoAF, Chromium), a port of the 1.x Profiler's
3
+ * `LongAnimationFrameObserver`. The runner's own stopwatch only sees its logic and paint; LoAF is
4
+ * the browser's view of the same frame and the only source that says what else ran. LoAF
5
+ * attributes only scripts that ran longer than ~5 ms, so a long frame with no script is a finding
6
+ * (late delivery, compositing, GC), not a gap: it is kept as `script: null`.
7
+ */
8
+ const KEEP_WORST = 4;
9
+ export class LongFrameObserver {
10
+ supported;
11
+ observer = null;
12
+ count = 0;
13
+ worst = [];
14
+ constructor() {
15
+ const types = typeof PerformanceObserver === 'undefined'
16
+ ? undefined
17
+ : PerformanceObserver.supportedEntryTypes;
18
+ this.supported = Array.isArray(types) && types.includes('long-animation-frame');
19
+ }
20
+ start() {
21
+ if (!this.supported || this.observer)
22
+ return;
23
+ this.observer = new PerformanceObserver(list => {
24
+ for (const entry of list.getEntries())
25
+ this.record(entry);
26
+ });
27
+ this.observer.observe({ type: 'long-animation-frame' });
28
+ }
29
+ stop() {
30
+ this.observer?.disconnect();
31
+ this.observer = null;
32
+ }
33
+ reset() {
34
+ this.count = 0;
35
+ this.worst = [];
36
+ }
37
+ stats() {
38
+ return { supported: this.supported, count: this.count, worst: [...this.worst] };
39
+ }
40
+ /** One entry, as the observer's callback hands it over (public for the spec). */
41
+ record(entry) {
42
+ const durationMs = typeof entry.duration === 'number' ? entry.duration : 0;
43
+ if (!Number.isFinite(durationMs) || durationMs <= 0)
44
+ return;
45
+ this.count += 1;
46
+ const record = {
47
+ durationMs,
48
+ blockingMs: Number.isFinite(entry.blockingDuration) ? entry.blockingDuration : 0,
49
+ script: describeScript(longestScript(entry.scripts)),
50
+ };
51
+ const at = this.worst.findIndex(existing => durationMs > existing.durationMs);
52
+ if (at === -1) {
53
+ if (this.worst.length < KEEP_WORST)
54
+ this.worst.push(record);
55
+ return;
56
+ }
57
+ this.worst.splice(at, 0, record);
58
+ if (this.worst.length > KEEP_WORST)
59
+ this.worst.length = KEEP_WORST;
60
+ }
61
+ }
62
+ const longestScript = (scripts) => {
63
+ let longest = null;
64
+ for (const script of scripts ?? []) {
65
+ if (!longest || (script.duration ?? 0) > (longest.duration ?? 0))
66
+ longest = script;
67
+ }
68
+ return longest;
69
+ };
70
+ const describeScript = (script) => {
71
+ if (!script)
72
+ return null;
73
+ const url = script.sourceURL?.trim().split(/[?#]/)[0];
74
+ const basename = url ? url.slice(url.lastIndexOf('/') + 1) || url : null;
75
+ const fn = script.sourceFunctionName?.trim();
76
+ if (basename && fn)
77
+ return `${basename} · ${fn}`;
78
+ return basename || fn || script.invoker?.trim() || null;
79
+ };
@@ -0,0 +1,25 @@
1
+ import type { DebugSession } from './session';
2
+ export declare class DebugOverlay {
3
+ private readonly session;
4
+ private readonly editorUrl;
5
+ private readonly host;
6
+ private readonly root;
7
+ private readonly panel;
8
+ private readonly canvas;
9
+ private readonly fps;
10
+ private readonly sections;
11
+ private readonly pauseButton;
12
+ private readonly stepButton;
13
+ private readonly collapseButton;
14
+ private timer;
15
+ private ticks;
16
+ constructor(session: DebugSession, editorUrl?: string | null);
17
+ mount(): void;
18
+ dispose(): void;
19
+ /** The timer: the chart every tick; every {@link TEXT_EVERY}th, a new window and the text. */
20
+ private tick;
21
+ private render;
22
+ private renderHeader;
23
+ private readonly onClick;
24
+ private setCollapsed;
25
+ }