@zkmake/three-meter 0.3.1 → 0.4.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.
@@ -1,4 +1,4 @@
1
- import { c as TimingMetric, t as PerformanceMonitor } from "./performance-monitor-WGLWbxOZ.js";
1
+ import { t as PerformanceMonitor, u as TimingMetric } from "./performance-monitor-Br7x3cPt.js";
2
2
  //#region src/ui/dock-panel.d.ts
3
3
  declare const EDGES: readonly ["left", "right", "top", "bottom"];
4
4
  type ScreenEdge = (typeof EDGES)[number];
@@ -82,12 +82,15 @@ declare class HudTheme {
82
82
  type HudSelection = {
83
83
  dim: boolean;
84
84
  graphs: ReadonlySet<TimingMetric>;
85
+ /** Version, three revision, backend and GPU footer in the compact HUD. */
86
+ info: boolean;
85
87
  numbers: ReadonlySet<string>;
86
88
  theme: ThemeMode | null;
87
89
  };
88
90
  type HudDefaults = {
89
91
  dim?: boolean;
90
92
  graphs?: readonly TimingMetric[];
93
+ info?: boolean;
91
94
  numbers?: readonly string[];
92
95
  };
93
96
  type HudSettingsOptions = {
@@ -106,6 +109,8 @@ declare class HudSettings {
106
109
  get selection(): HudSelection;
107
110
  get dim(): boolean;
108
111
  setDim(enabled: boolean): void;
112
+ get info(): boolean;
113
+ setInfo(enabled: boolean): void;
109
114
  /** The user's pick in the panel; `null` means none, so the consumer's option applies. */
110
115
  get theme(): ThemeMode | null;
111
116
  setTheme(mode: ThemeMode | null): void;
@@ -155,7 +160,13 @@ declare class PerformanceView {
155
160
  private readonly statValueEls;
156
161
  private readonly statCheckboxes;
157
162
  private dimCheckbox;
163
+ private infoCheckbox;
158
164
  private readonly themeRadios;
165
+ private footerThreeEl;
166
+ private footerHardwareEl;
167
+ private footerBackendEl;
168
+ private footerGpuEl;
169
+ private lastEnvironmentKey;
159
170
  private hudGraphsEl;
160
171
  private hudGridEl;
161
172
  private readonly hudGraphs;
@@ -172,6 +183,8 @@ declare class PerformanceView {
172
183
  dispose(): void;
173
184
  private frame;
174
185
  private render;
186
+ /** The monitor caches the environment once its backend settles; until then this re-reads. */
187
+ private renderFooter;
175
188
  private renderHud;
176
189
  private formatTiming;
177
190
  private build;
@@ -180,6 +193,7 @@ declare class PerformanceView {
180
193
  /** Repaint the palette and mark the radio matching what applies (pick, else consumer mode). */
181
194
  private onThemeChanged;
182
195
  private buildCheckbox;
196
+ private applyInfoClass;
183
197
  private applyModeClass;
184
198
  private resizeCanvases;
185
199
  }
@@ -230,4 +244,4 @@ type PerfHudHandle = {
230
244
  declare const mountPerfHud: (monitor: PerformanceMonitor, options?: MountPerfHudOptions) => PerfHudHandle;
231
245
  //#endregion
232
246
  export { dockPanel as C, ScreenEdge as S, DefaultPlacement as _, PerformanceViewMode as a, DockPanelOptions as b, HudDefaults as c, HudSettingsOptions as d, HudTheme as f, isThemeMode as g, ThemeMode as h, PerformanceView as i, HudSelection as l, THEME_MODES as m, PerfHudHandle as n, PerformanceViewOptions as o, ResolvedTheme as p, mountPerfHud as r, DEFAULT_STORAGE_KEY as s, MountPerfHudOptions as t, HudSettings as u, DockAlign as v, Placement as x, DockHandle as y };
233
- //# sourceMappingURL=mount-perf-hud-B3wBLrcu.d.ts.map
247
+ //# sourceMappingURL=mount-perf-hud-v6kLGJUX.d.ts.map
@@ -24,13 +24,31 @@ type ResourceCounts = {
24
24
  programs: number;
25
25
  };
26
26
  type Sample = {
27
+ /** Smoothed. Intervals over a second (hidden tab, breakpoint) are left out. */
27
28
  fps: number;
28
- /** CPU frame time in ms (JS-side): tick + render dispatch. */
29
+ /**
30
+ * JS time in ms from `begin()` to the return of the frame's last `render()`:
31
+ * your update code plus the render dispatch. Falls back to `end()` when no
32
+ * render ran.
33
+ */
29
34
  cpu: number;
30
35
  gpu: GpuTiming;
31
36
  render: RenderCounts;
32
37
  resources: ResourceCounts;
33
38
  };
39
+ /** `webgl` is WebGL 1, which three dropped in r163. */
40
+ type RenderBackend = "webgpu" | "webgl2" | "webgl";
41
+ /** What the monitor is running on, for bug reports. `null` where it can't be read. */
42
+ type Environment = {
43
+ /** `null` until `WebGPURenderer` finishes `init()`. */
44
+ backend: RenderBackend | null;
45
+ /** `WebGPURenderer` asked for WebGPU and fell back to its WebGL2 backend. */
46
+ fallback: boolean;
47
+ /** Adapter or unmasked renderer name, e.g. `Apple M3 Pro`. Browsers may hide it. */
48
+ gpu: string | null;
49
+ /** three's revision from `window.__THREE__`, e.g. `"186"`. */
50
+ three: string | null;
51
+ };
34
52
  /**
35
53
  * The slice of a three renderer the monitor needs. WebGPU's `info.render` uses
36
54
  * `drawCalls` for this frame; WebGL's uses `calls`. `programs` is a cache array
@@ -84,12 +102,18 @@ type PerformanceMonitorOptions = {
84
102
  * While attached, `renderer.info.autoReset` is off and `info` is reset in
85
103
  * {@link begin}; host code that reads `renderer.info` mid-frame sees this
86
104
  * frame's counts only. {@link dispose} restores both.
105
+ *
106
+ * CPU time runs from {@link begin} to the return of the frame's last
107
+ * `render()` call, which the `render` patch stamps. So it holds even when
108
+ * {@link end} is only reached on the next tick, as in the R3F sampler: it is
109
+ * the JS work of the frame, not the interval to the next one.
87
110
  */
88
111
  declare class PerformanceMonitor {
89
112
  private readonly renderer;
90
113
  private readonly gpuTimer;
91
114
  private readonly originalRender;
92
115
  private renderPasses;
116
+ private lastRenderEndAt;
93
117
  private readonly fpsHistory;
94
118
  private readonly cpuHistory;
95
119
  private readonly gpuHistory;
@@ -98,14 +122,21 @@ declare class PerformanceMonitor {
98
122
  private frameFps;
99
123
  private smoothedFps;
100
124
  private sample;
125
+ private environment;
101
126
  constructor(options: PerformanceMonitorOptions);
102
127
  begin(): void;
103
128
  end(): void;
104
129
  getSample(): Sample;
105
130
  getHistory(metric: TimingMetric): readonly number[];
131
+ /**
132
+ * three revision, backend and GPU name. Cached once the backend is known;
133
+ * before `WebGPURenderer.init()` settles it, `backend` is `null` and the
134
+ * next call reads again.
135
+ */
136
+ getEnvironment(): Environment;
106
137
  dispose(): void;
107
138
  private buildEmptySample;
108
139
  }
109
140
  //#endregion
110
- export { RenderCounts as a, TimingMetric as c, PerformanceMonitorOptions as i, GpuTiming as n, ResourceCounts as o, PerfRenderer as r, Sample as s, PerformanceMonitor as t };
111
- //# sourceMappingURL=performance-monitor-WGLWbxOZ.d.ts.map
141
+ export { PerformanceMonitorOptions as a, ResourceCounts as c, PerfRenderer as i, Sample as l, Environment as n, RenderBackend as o, GpuTiming as r, RenderCounts as s, PerformanceMonitor as t, TimingMetric as u };
142
+ //# sourceMappingURL=performance-monitor-Br7x3cPt.d.ts.map
@@ -1,3 +1,95 @@
1
+ //#region src/core/environment.ts
2
+ /** three sets `window.__THREE__` to its revision on import (and warns on a second copy). */
3
+ const readThreeRevision = () => {
4
+ const revision = globalThis.__THREE__;
5
+ return typeof revision === "string" && revision !== "" ? revision : null;
6
+ };
7
+ /**
8
+ * Trim a WebGL renderer string to the part a person recognises. Chrome wraps
9
+ * it in ANGLE, e.g. `ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 (0x00002786)
10
+ * Direct3D11 vs_5_0 ps_5_0, D3D11)` or `ANGLE (Apple, ANGLE Metal Renderer:
11
+ * Apple M3 Pro, Unspecified Version)`.
12
+ */
13
+ const cleanGpuName = (raw) => {
14
+ let name = raw.trim();
15
+ const angle = /^ANGLE \((.*)\)$/.exec(name);
16
+ if (angle) {
17
+ const parts = angle[1].split(", ");
18
+ name = parts[1] ?? parts[0];
19
+ }
20
+ return name.replace(/^ANGLE [\w ]+ Renderer: /, "").replace(/ \(0x[0-9a-f]+\)/i, "").replace(/ (Direct3D\d*|OpenGL|Vulkan|Metal)\b.*$/, "").trim();
21
+ };
22
+ const webglGpuName = (gl) => {
23
+ const ext = gl.getExtension("WEBGL_debug_renderer_info");
24
+ const raw = gl.getParameter(ext ? ext.UNMASKED_RENDERER_WEBGL : gl.RENDERER);
25
+ return typeof raw === "string" && raw !== "" && raw !== "WebKit WebGL" ? cleanGpuName(raw) : null;
26
+ };
27
+ /** WebGPU reports vendors lowercase (`apple`, `nvidia`). */
28
+ const VENDOR_NAMES = {
29
+ amd: "AMD",
30
+ apple: "Apple",
31
+ arm: "Arm",
32
+ intel: "Intel",
33
+ nvidia: "NVIDIA",
34
+ qualcomm: "Qualcomm"
35
+ };
36
+ /**
37
+ * `GPUDevice.adapterInfo`. Chrome leaves `description` empty to limit
38
+ * fingerprinting, so this is usually vendor + architecture: `Apple metal-3`.
39
+ */
40
+ const webgpuGpuName = (info) => {
41
+ if (!info) return null;
42
+ if (info.description) return info.description;
43
+ const name = [info.vendor ? VENDOR_NAMES[info.vendor] ?? info.vendor : "", info.architecture].filter(Boolean).join(" ");
44
+ return name === "" ? null : name;
45
+ };
46
+ const isWebglContext = (value) => typeof WebGL2RenderingContext !== "undefined" && value instanceof WebGL2RenderingContext || typeof WebGLRenderingContext !== "undefined" && value instanceof WebGLRenderingContext;
47
+ const webglBackend = (gl) => typeof WebGL2RenderingContext !== "undefined" && gl instanceof WebGL2RenderingContext ? "webgl2" : "webgl";
48
+ const readEnvironment = (renderer) => {
49
+ const three = readThreeRevision();
50
+ const backend = renderer.backend;
51
+ if (backend) {
52
+ if (backend.isWebGPUBackend) {
53
+ const device = backend.device;
54
+ return device ? {
55
+ backend: "webgpu",
56
+ fallback: false,
57
+ gpu: webgpuGpuName(device.adapterInfo),
58
+ three
59
+ } : {
60
+ backend: null,
61
+ fallback: false,
62
+ gpu: null,
63
+ three
64
+ };
65
+ }
66
+ if (backend.isWebGLBackend && isWebglContext(backend.gl)) return {
67
+ backend: webglBackend(backend.gl),
68
+ fallback: backend.parameters?.forceWebGL !== true,
69
+ gpu: webglGpuName(backend.gl),
70
+ three
71
+ };
72
+ return {
73
+ backend: null,
74
+ fallback: false,
75
+ gpu: null,
76
+ three
77
+ };
78
+ }
79
+ const context = renderer.getContext?.();
80
+ return isWebglContext(context) ? {
81
+ backend: webglBackend(context),
82
+ fallback: false,
83
+ gpu: webglGpuName(context),
84
+ three
85
+ } : {
86
+ backend: null,
87
+ fallback: false,
88
+ gpu: null,
89
+ three
90
+ };
91
+ };
92
+ //#endregion
1
93
  //#region src/core/gpu-timer.ts
2
94
  var GpuTimer = class GpuTimer {
3
95
  lastMs = 0;
@@ -130,6 +222,12 @@ const DEFAULT_GPU_POOL_SIZE = 5;
130
222
  const ATTACHED = /* @__PURE__ */ new WeakSet();
131
223
  const DEFAULT_HISTORY_SIZE = 120;
132
224
  const FPS_SMOOTHING = .1;
225
+ /**
226
+ * A frame interval past this is a stall (hidden tab, breakpoint, laptop lid),
227
+ * not a slow frame. It is left out of FPS so one gap doesn't flatten the
228
+ * graph's scale for the whole history window.
229
+ */
230
+ const STALL_MS = 1e3;
133
231
  const frameCalls = (info) => info.render.drawCalls ?? info.render.calls;
134
232
  const programCount = (info) => info.memory.programs ?? info.programs?.length ?? 0;
135
233
  /**
@@ -141,12 +239,18 @@ const programCount = (info) => info.memory.programs ?? info.programs?.length ??
141
239
  * While attached, `renderer.info.autoReset` is off and `info` is reset in
142
240
  * {@link begin}; host code that reads `renderer.info` mid-frame sees this
143
241
  * frame's counts only. {@link dispose} restores both.
242
+ *
243
+ * CPU time runs from {@link begin} to the return of the frame's last
244
+ * `render()` call, which the `render` patch stamps. So it holds even when
245
+ * {@link end} is only reached on the next tick, as in the R3F sampler: it is
246
+ * the JS work of the frame, not the interval to the next one.
144
247
  */
145
248
  var PerformanceMonitor = class {
146
249
  renderer;
147
250
  gpuTimer;
148
251
  originalRender;
149
252
  renderPasses = 0;
253
+ lastRenderEndAt = null;
150
254
  fpsHistory;
151
255
  cpuHistory;
152
256
  gpuHistory;
@@ -155,6 +259,7 @@ var PerformanceMonitor = class {
155
259
  frameFps = 0;
156
260
  smoothedFps = 0;
157
261
  sample;
262
+ environment = null;
158
263
  constructor(options) {
159
264
  const { renderer, trackGPU = true, gpuQueryPoolSize = DEFAULT_GPU_POOL_SIZE, historySize = DEFAULT_HISTORY_SIZE } = options;
160
265
  if (ATTACHED.has(renderer)) throw new Error("three-meter: a PerformanceMonitor is already attached to this renderer. dispose() it first.");
@@ -164,7 +269,11 @@ var PerformanceMonitor = class {
164
269
  this.originalRender = renderer.render;
165
270
  renderer.render = (...args) => {
166
271
  this.renderPasses += 1;
167
- return this.originalRender.apply(renderer, args);
272
+ try {
273
+ return this.originalRender.apply(renderer, args);
274
+ } finally {
275
+ this.lastRenderEndAt = performance.now();
276
+ }
168
277
  };
169
278
  this.gpuTimer = trackGPU ? GpuTimer.create(renderer, gpuQueryPoolSize) : null;
170
279
  this.fpsHistory = new RingBuffer(historySize);
@@ -176,16 +285,18 @@ var PerformanceMonitor = class {
176
285
  const now = performance.now();
177
286
  if (this.lastBeginAt !== null) {
178
287
  const frameMs = now - this.lastBeginAt;
179
- this.frameFps = frameMs > 0 ? 1e3 / frameMs : 0;
288
+ if (frameMs > 0 && frameMs <= STALL_MS) this.frameFps = 1e3 / frameMs;
180
289
  }
181
290
  this.lastBeginAt = now;
182
291
  this.cpuStart = now;
292
+ this.lastRenderEndAt = null;
183
293
  this.renderPasses = 0;
184
294
  this.renderer.info.reset();
185
295
  this.gpuTimer?.begin();
186
296
  }
187
297
  end() {
188
- const cpu = performance.now() - this.cpuStart;
298
+ const now = performance.now();
299
+ const cpu = (this.lastRenderEndAt ?? now) - this.cpuStart;
189
300
  this.gpuTimer?.end();
190
301
  this.gpuTimer?.poll();
191
302
  const gpuMs = this.gpuTimer?.getMs() ?? 0;
@@ -225,6 +336,17 @@ var PerformanceMonitor = class {
225
336
  case "gpu": return this.gpuHistory.toArray();
226
337
  }
227
338
  }
339
+ /**
340
+ * three revision, backend and GPU name. Cached once the backend is known;
341
+ * before `WebGPURenderer.init()` settles it, `backend` is `null` and the
342
+ * next call reads again.
343
+ */
344
+ getEnvironment() {
345
+ if (this.environment) return this.environment;
346
+ const environment = Object.freeze(readEnvironment(this.renderer));
347
+ if (environment.backend !== null) this.environment = environment;
348
+ return environment;
349
+ }
228
350
  dispose() {
229
351
  ATTACHED.delete(this.renderer);
230
352
  this.renderer.info.autoReset = true;
@@ -260,4 +382,4 @@ var PerformanceMonitor = class {
260
382
  //#endregion
261
383
  export { RingBuffer as n, PerformanceMonitor as t };
262
384
 
263
- //# sourceMappingURL=performance-monitor-qswCj9Nl.js.map
385
+ //# sourceMappingURL=performance-monitor-C8dhWJWR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"performance-monitor-C8dhWJWR.js","names":[],"sources":["../src/core/environment.ts","../src/core/gpu-timer.ts","../src/core/ring-buffer.ts","../src/core/performance-monitor.ts"],"sourcesContent":["/**\n * What the monitor is running on: three revision, rendering backend, GPU.\n * Read from the renderer and the page, never from an import of three.\n *\n * `WebGPURenderer` settles its backend in `init()`, so until then the backend\n * reads `null`; callers poll and the monitor caches once it is known.\n */\nimport type { Environment, PerfRenderer, RenderBackend } from \"./types.ts\";\n\ntype BackendShape = {\n device?: { adapterInfo?: AdapterInfoShape } | null;\n gl?: unknown;\n isWebGLBackend?: boolean;\n isWebGPUBackend?: boolean;\n parameters?: { forceWebGL?: boolean };\n};\n\ntype AdapterInfoShape = {\n architecture?: string;\n description?: string;\n device?: string;\n vendor?: string;\n};\n\ntype DebugRendererInfoExt = { readonly UNMASKED_RENDERER_WEBGL: number };\n\n/** three sets `window.__THREE__` to its revision on import (and warns on a second copy). */\nconst readThreeRevision = (): string | null => {\n const revision = (globalThis as { __THREE__?: unknown }).__THREE__;\n\n return typeof revision === \"string\" && revision !== \"\" ? revision : null;\n};\n\n/**\n * Trim a WebGL renderer string to the part a person recognises. Chrome wraps\n * it in ANGLE, e.g. `ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 (0x00002786)\n * Direct3D11 vs_5_0 ps_5_0, D3D11)` or `ANGLE (Apple, ANGLE Metal Renderer:\n * Apple M3 Pro, Unspecified Version)`.\n */\nconst cleanGpuName = (raw: string): string => {\n let name = raw.trim();\n const angle = /^ANGLE \\((.*)\\)$/.exec(name);\n\n if (angle) {\n const parts = angle[1]!.split(\", \");\n name = parts[1] ?? parts[0]!;\n }\n\n return name\n .replace(/^ANGLE [\\w ]+ Renderer: /, \"\")\n .replace(/ \\(0x[0-9a-f]+\\)/i, \"\")\n .replace(/ (Direct3D\\d*|OpenGL|Vulkan|Metal)\\b.*$/, \"\")\n .trim();\n};\n\nconst webglGpuName = (gl: WebGLRenderingContext | WebGL2RenderingContext): string | null => {\n // Firefox deprecates the extension but already returns the unmasked name from RENDERER.\n const ext = gl.getExtension(\"WEBGL_debug_renderer_info\") as DebugRendererInfoExt | null;\n const raw = gl.getParameter(ext ? ext.UNMASKED_RENDERER_WEBGL : gl.RENDERER) as unknown;\n\n return typeof raw === \"string\" && raw !== \"\" && raw !== \"WebKit WebGL\" ? cleanGpuName(raw) : null;\n};\n\n/** WebGPU reports vendors lowercase (`apple`, `nvidia`). */\nconst VENDOR_NAMES: Record<string, string> = {\n amd: \"AMD\",\n apple: \"Apple\",\n arm: \"Arm\",\n intel: \"Intel\",\n nvidia: \"NVIDIA\",\n qualcomm: \"Qualcomm\",\n};\n\n/**\n * `GPUDevice.adapterInfo`. Chrome leaves `description` empty to limit\n * fingerprinting, so this is usually vendor + architecture: `Apple metal-3`.\n */\nconst webgpuGpuName = (info: AdapterInfoShape | undefined): string | null => {\n if (!info) {\n return null;\n }\n\n if (info.description) {\n return info.description;\n }\n\n const vendor = info.vendor ? (VENDOR_NAMES[info.vendor] ?? info.vendor) : \"\";\n const name = [vendor, info.architecture].filter(Boolean).join(\" \");\n\n return name === \"\" ? null : name;\n};\n\nconst isWebglContext = (value: unknown): value is WebGLRenderingContext | WebGL2RenderingContext =>\n (typeof WebGL2RenderingContext !== \"undefined\" && value instanceof WebGL2RenderingContext) ||\n (typeof WebGLRenderingContext !== \"undefined\" && value instanceof WebGLRenderingContext);\n\nconst webglBackend = (gl: WebGLRenderingContext | WebGL2RenderingContext): RenderBackend =>\n typeof WebGL2RenderingContext !== \"undefined\" && gl instanceof WebGL2RenderingContext\n ? \"webgl2\"\n : \"webgl\";\n\nconst readEnvironment = (renderer: PerfRenderer): Environment => {\n const three = readThreeRevision();\n const backend = renderer.backend as BackendShape | undefined;\n\n // WebGPURenderer. Read the backend's own fields: `getContext()` before\n // `init()` is what the GPU timer learned not to touch.\n if (backend) {\n if (backend.isWebGPUBackend) {\n const device = backend.device;\n\n return device\n ? { backend: \"webgpu\", fallback: false, gpu: webgpuGpuName(device.adapterInfo), three }\n : { backend: null, fallback: false, gpu: null, three };\n }\n\n if (backend.isWebGLBackend && isWebglContext(backend.gl)) {\n return {\n backend: webglBackend(backend.gl),\n fallback: backend.parameters?.forceWebGL !== true,\n gpu: webglGpuName(backend.gl),\n three,\n };\n }\n\n return { backend: null, fallback: false, gpu: null, three };\n }\n\n // WebGLRenderer.\n const context = renderer.getContext?.();\n\n return isWebglContext(context)\n ? { backend: webglBackend(context), fallback: false, gpu: webglGpuName(context), three }\n : { backend: null, fallback: false, gpu: null, three };\n};\n\nexport { cleanGpuName, readEnvironment };\n","/**\n * Real GPU frame timing.\n *\n * WebGPU: Three's `trackTimestamp` + `resolveTimestampsAsync` (ms, a frame or\n * two late). Never calls `getExtension`: that is what remounted the canvas.\n *\n * WebGL2 fallback (Safari without WebGPU, or a forced GL backend):\n * `EXT_disjoint_timer_query_webgl2`, same pool-of-queries pattern as the\n * platform-typing monitor.\n */\nimport type { PerfRenderer } from \"./types.ts\";\n\ntype DisjointTimerQueryExt = {\n readonly GPU_DISJOINT_EXT: number;\n readonly TIME_ELAPSED_EXT: number;\n};\n\nclass GpuTimer {\n private lastMs = 0;\n private readonly webgpu: PerfRenderer | null;\n private webgpuInFlight = false;\n private readonly gl: WebGL2RenderingContext | null;\n private readonly ext: DisjointTimerQueryExt | null;\n private readonly free: WebGLQuery[] = [];\n private readonly pending: WebGLQuery[] = [];\n private active: WebGLQuery | null = null;\n\n private constructor(\n webgpu: PerfRenderer | null,\n gl: WebGL2RenderingContext | null,\n ext: DisjointTimerQueryExt | null,\n poolSize: number,\n ) {\n this.webgpu = webgpu;\n this.gl = gl;\n this.ext = ext;\n\n if (!gl) {\n return;\n }\n\n for (let index = 0; index < poolSize; index += 1) {\n const query = gl.createQuery();\n\n if (query) {\n this.free.push(query);\n }\n }\n }\n\n static create(renderer: PerfRenderer, poolSize: number): GpuTimer | null {\n const backend = renderer.backend as { trackTimestamp?: boolean } | undefined;\n\n if (backend?.trackTimestamp && renderer.resolveTimestampsAsync) {\n return new GpuTimer(renderer, null, null, poolSize);\n }\n\n const context = renderer.getContext?.();\n\n if (!(context instanceof WebGL2RenderingContext)) {\n return null;\n }\n\n const ext = context.getExtension(\n \"EXT_disjoint_timer_query_webgl2\",\n ) as DisjointTimerQueryExt | null;\n\n return ext ? new GpuTimer(null, context, ext, poolSize) : null;\n }\n\n begin() {\n if (this.webgpu || !this.gl || !this.ext) {\n return;\n }\n\n const query = this.free.pop();\n\n if (!query) {\n this.active = null;\n\n return;\n }\n\n this.active = query;\n this.gl.beginQuery(this.ext.TIME_ELAPSED_EXT, query);\n }\n\n end() {\n if (this.webgpu || !this.gl || !this.ext || !this.active) {\n return;\n }\n\n this.gl.endQuery(this.ext.TIME_ELAPSED_EXT);\n this.pending.push(this.active);\n this.active = null;\n }\n\n poll() {\n if (this.webgpu) {\n this.pollWebgpu();\n\n return;\n }\n\n this.pollWebgl();\n }\n\n getMs() {\n return this.lastMs;\n }\n\n dispose() {\n if (this.gl && this.ext && this.active) {\n this.gl.endQuery(this.ext.TIME_ELAPSED_EXT);\n this.active = null;\n }\n\n if (this.gl) {\n for (const query of this.free) {\n this.gl.deleteQuery(query);\n }\n\n for (const query of this.pending) {\n this.gl.deleteQuery(query);\n }\n }\n\n this.free.length = 0;\n this.pending.length = 0;\n }\n\n private pollWebgpu() {\n const resolve = this.webgpu?.resolveTimestampsAsync;\n\n if (!resolve || this.webgpuInFlight) {\n return;\n }\n\n this.webgpuInFlight = true;\n\n void resolve\n .call(this.webgpu, \"render\")\n .then((ms) => {\n if (typeof ms === \"number\" && Number.isFinite(ms)) {\n this.lastMs = ms;\n }\n\n this.webgpuInFlight = false;\n })\n .catch(() => {\n this.webgpuInFlight = false;\n });\n }\n\n private pollWebgl() {\n if (!this.gl || !this.ext) {\n return;\n }\n\n const disjoint = this.gl.getParameter(this.ext.GPU_DISJOINT_EXT) as boolean;\n\n if (disjoint) {\n while (this.pending.length > 0) {\n this.free.push(this.pending.shift()!);\n }\n\n return;\n }\n\n while (this.pending.length > 0) {\n const query = this.pending[0]!;\n const ready = this.gl.getQueryParameter(query, this.gl.QUERY_RESULT_AVAILABLE) as boolean;\n\n if (!ready) {\n break;\n }\n\n this.pending.shift();\n const ns = this.gl.getQueryParameter(query, this.gl.QUERY_RESULT) as number;\n this.lastMs = ns / 1e6;\n this.free.push(query);\n }\n }\n}\n\nexport { GpuTimer };\n","/**\n * Fixed-capacity numeric ring buffer. Backs the per-frame timing history the\n * view reads to draw sparklines. Push is O(1); `toArray()` returns values in\n * chronological (oldest → newest) order.\n */\nclass RingBuffer {\n private readonly data: Float32Array;\n private readonly capacity: number;\n private head = 0;\n private count = 0;\n\n constructor(capacity: number) {\n this.capacity = capacity;\n this.data = new Float32Array(capacity);\n }\n\n push(value: number) {\n this.data[this.head] = value;\n this.head = (this.head + 1) % this.capacity;\n\n if (this.count < this.capacity) {\n this.count += 1;\n }\n }\n\n /** Values oldest → newest. Length grows up to capacity as samples accumulate. */\n toArray(): number[] {\n const out = Array.from<number>({ length: this.count });\n const start = (this.head - this.count + this.capacity) % this.capacity;\n\n for (let index = 0; index < this.count; index += 1) {\n out[index] = this.data[(start + index) % this.capacity]!;\n }\n\n return out;\n }\n\n clear() {\n this.head = 0;\n this.count = 0;\n }\n}\n\nexport { RingBuffer };\n","import { readEnvironment } from \"./environment.ts\";\nimport { GpuTimer } from \"./gpu-timer.ts\";\nimport { RingBuffer } from \"./ring-buffer.ts\";\nimport type {\n Environment,\n PerfRenderer,\n PerformanceMonitorOptions,\n Sample,\n TimingMetric,\n} from \"./types.ts\";\n\nconst DEFAULT_GPU_POOL_SIZE = 5;\n/** One monitor per renderer: a second would stack the `render` patch and double-count passes. */\nconst ATTACHED = new WeakSet<PerfRenderer>();\nconst DEFAULT_HISTORY_SIZE = 120;\nconst FPS_SMOOTHING = 0.1;\n/**\n * A frame interval past this is a stall (hidden tab, breakpoint, laptop lid),\n * not a slow frame. It is left out of FPS so one gap doesn't flatten the\n * graph's scale for the whole history window.\n */\nconst STALL_MS = 1000;\n\nconst frameCalls = (info: PerfRenderer[\"info\"]) => info.render.drawCalls ?? info.render.calls;\n\nconst programCount = (info: PerfRenderer[\"info\"]) =>\n info.memory.programs ?? info.programs?.length ?? 0;\n\n/**\n * Collector of three.js frame metrics. Bracket each frame with {@link begin}/\n * {@link end}, or let {@link wrapAnimationLoop} or the R3F `PerfSampler` do it.\n * Works on WebGPURenderer (drawCalls, memory.programs, timestamp queries) and\n * on a WebGL renderer (calls, programs[], timer-query ext).\n *\n * While attached, `renderer.info.autoReset` is off and `info` is reset in\n * {@link begin}; host code that reads `renderer.info` mid-frame sees this\n * frame's counts only. {@link dispose} restores both.\n *\n * CPU time runs from {@link begin} to the return of the frame's last\n * `render()` call, which the `render` patch stamps. So it holds even when\n * {@link end} is only reached on the next tick, as in the R3F sampler: it is\n * the JS work of the frame, not the interval to the next one.\n */\nclass PerformanceMonitor {\n private readonly renderer: PerfRenderer;\n private readonly gpuTimer: GpuTimer | null;\n private readonly originalRender: PerfRenderer[\"render\"];\n private renderPasses = 0;\n private lastRenderEndAt: number | null = null;\n private readonly fpsHistory: RingBuffer;\n private readonly cpuHistory: RingBuffer;\n private readonly gpuHistory: RingBuffer;\n private cpuStart = 0;\n private lastBeginAt: number | null = null;\n private frameFps = 0;\n private smoothedFps = 0;\n private sample: Sample;\n private environment: Environment | null = null;\n\n constructor(options: PerformanceMonitorOptions) {\n const {\n renderer,\n trackGPU = true,\n gpuQueryPoolSize = DEFAULT_GPU_POOL_SIZE,\n historySize = DEFAULT_HISTORY_SIZE,\n } = options;\n\n if (ATTACHED.has(renderer)) {\n throw new Error(\n \"three-meter: a PerformanceMonitor is already attached to this renderer. dispose() it first.\",\n );\n }\n\n ATTACHED.add(renderer);\n this.renderer = renderer;\n renderer.info.autoReset = false;\n this.originalRender = renderer.render;\n\n renderer.render = (...args: never[]) => {\n this.renderPasses += 1;\n\n try {\n return this.originalRender.apply(renderer, args);\n } finally {\n this.lastRenderEndAt = performance.now();\n }\n };\n\n this.gpuTimer = trackGPU ? GpuTimer.create(renderer, gpuQueryPoolSize) : null;\n this.fpsHistory = new RingBuffer(historySize);\n this.cpuHistory = new RingBuffer(historySize);\n this.gpuHistory = new RingBuffer(historySize);\n this.sample = this.buildEmptySample();\n }\n\n begin() {\n const now = performance.now();\n\n if (this.lastBeginAt !== null) {\n const frameMs = now - this.lastBeginAt;\n\n // A stall keeps the previous reading rather than logging a near-zero frame.\n if (frameMs > 0 && frameMs <= STALL_MS) {\n this.frameFps = 1000 / frameMs;\n }\n }\n\n this.lastBeginAt = now;\n this.cpuStart = now;\n this.lastRenderEndAt = null;\n this.renderPasses = 0;\n this.renderer.info.reset();\n this.gpuTimer?.begin();\n }\n\n end() {\n const now = performance.now();\n // Without a render this frame there is nothing better than \"until end()\".\n const cpu = (this.lastRenderEndAt ?? now) - this.cpuStart;\n\n this.gpuTimer?.end();\n this.gpuTimer?.poll();\n\n const gpuMs = this.gpuTimer?.getMs() ?? 0;\n\n this.smoothedFps =\n this.smoothedFps === 0\n ? this.frameFps\n : this.smoothedFps + (this.frameFps - this.smoothedFps) * FPS_SMOOTHING;\n\n this.fpsHistory.push(this.frameFps);\n this.cpuHistory.push(cpu);\n this.gpuHistory.push(gpuMs);\n\n const info = this.renderer.info;\n\n this.sample = Object.freeze({\n cpu,\n fps: this.smoothedFps,\n gpu: { available: this.gpuTimer !== null, ms: gpuMs },\n render: {\n calls: frameCalls(info),\n lines: info.render.lines,\n passes: this.renderPasses,\n points: info.render.points,\n triangles: info.render.triangles,\n },\n resources: {\n geometries: info.memory.geometries,\n programs: programCount(info),\n textures: info.memory.textures,\n },\n });\n }\n\n getSample(): Sample {\n return this.sample;\n }\n\n getHistory(metric: TimingMetric): readonly number[] {\n switch (metric) {\n case \"fps\":\n return this.fpsHistory.toArray();\n case \"cpu\":\n return this.cpuHistory.toArray();\n case \"gpu\":\n return this.gpuHistory.toArray();\n }\n }\n\n /**\n * three revision, backend and GPU name. Cached once the backend is known;\n * before `WebGPURenderer.init()` settles it, `backend` is `null` and the\n * next call reads again.\n */\n getEnvironment(): Environment {\n if (this.environment) {\n return this.environment;\n }\n\n const environment = Object.freeze(readEnvironment(this.renderer));\n\n if (environment.backend !== null) {\n this.environment = environment;\n }\n\n return environment;\n }\n\n dispose() {\n ATTACHED.delete(this.renderer);\n this.renderer.info.autoReset = true;\n this.renderer.render = this.originalRender;\n this.gpuTimer?.dispose();\n this.fpsHistory.clear();\n this.cpuHistory.clear();\n this.gpuHistory.clear();\n }\n\n private buildEmptySample(): Sample {\n return Object.freeze({\n cpu: 0,\n fps: 0,\n gpu: { available: this.gpuTimer !== null, ms: 0 },\n render: { calls: 0, lines: 0, passes: 0, points: 0, triangles: 0 },\n resources: { geometries: 0, programs: 0, textures: 0 },\n });\n }\n}\n\nexport { PerformanceMonitor };\n"],"mappings":"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"}
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { i as PerformanceMonitorOptions, t as PerformanceMonitor } from "./performance-monitor-WGLWbxOZ.js";
2
- import { t as MountPerfHudOptions } from "./mount-perf-hud-B3wBLrcu.js";
1
+ import { a as PerformanceMonitorOptions, t as PerformanceMonitor } from "./performance-monitor-Br7x3cPt.js";
2
+ import { t as MountPerfHudOptions } from "./mount-perf-hud-v6kLGJUX.js";
3
3
  //#region src/react/monitor-store.d.ts
4
4
  type MonitorListener = (monitor: PerformanceMonitor | null) => void;
5
5
  type PerfMonitorStore = {
@@ -18,8 +18,12 @@ type PerfHudProps = Omit<MountPerfHudOptions, "settings"> & {
18
18
  /**
19
19
  * DOM overlay. Render it *outside* `<Canvas>`, or Fiber would treat its markup
20
20
  * as three objects. Mounts the dockable HUD whenever a `PerfSampler` has
21
- * published a monitor and tears it down when that sampler unmounts. `theme`
22
- * is the one prop that applies live; every other change remounts the HUD.
21
+ * published a monitor and tears it down when that sampler unmounts.
22
+ *
23
+ * `theme`, `mode` and `label` apply live through the handle. `defaultPlacement`
24
+ * is compared by value, so an inline `{{ edge: "right" }}` literal is fine.
25
+ * The remaining props (`storageKey`, `parent`, `refreshHz`, `injectStyles`,
26
+ * `store`) remount the HUD when they change.
23
27
  */
24
28
  declare function PerfHud({ defaultPlacement, injectStyles, label, mode, parent, refreshHz, storageKey, store, theme }: PerfHudProps): null;
25
29
  //#endregion
package/dist/react.js CHANGED
@@ -1,5 +1,5 @@
1
- import { t as PerformanceMonitor } from "./performance-monitor-qswCj9Nl.js";
2
- import { t as mountPerfHud } from "./mount-perf-hud-CX9DrxjA.js";
1
+ import { t as PerformanceMonitor } from "./performance-monitor-C8dhWJWR.js";
2
+ import { t as mountPerfHud } from "./mount-perf-hud-Rf98QNxM.js";
3
3
  import { useEffect, useRef } from "react";
4
4
  import { useFrame, useThree } from "@react-three/fiber";
5
5
  //#region src/react/monitor-store.ts
@@ -26,23 +26,34 @@ const defaultPerfMonitorStore = createPerfMonitorStore();
26
26
  /**
27
27
  * DOM overlay. Render it *outside* `<Canvas>`, or Fiber would treat its markup
28
28
  * as three objects. Mounts the dockable HUD whenever a `PerfSampler` has
29
- * published a monitor and tears it down when that sampler unmounts. `theme`
30
- * is the one prop that applies live; every other change remounts the HUD.
29
+ * published a monitor and tears it down when that sampler unmounts.
30
+ *
31
+ * `theme`, `mode` and `label` apply live through the handle. `defaultPlacement`
32
+ * is compared by value, so an inline `{{ edge: "right" }}` literal is fine.
33
+ * The remaining props (`storageKey`, `parent`, `refreshHz`, `injectStyles`,
34
+ * `store`) remount the HUD when they change.
31
35
  */
32
36
  function PerfHud({ defaultPlacement, injectStyles, label, mode, parent, refreshHz, storageKey, store, theme }) {
33
37
  const source = store ?? defaultPerfMonitorStore;
34
38
  const handleRef = useRef(null);
35
39
  const themeRef = useRef(theme);
40
+ const modeRef = useRef(mode);
41
+ const labelRef = useRef(label);
42
+ const placementEdge = defaultPlacement?.edge;
43
+ const placementAlign = defaultPlacement?.align;
36
44
  useEffect(() => {
37
45
  const attach = (monitor) => {
38
46
  handleRef.current?.dispose();
39
47
  handleRef.current = null;
40
48
  if (!monitor) return;
41
49
  handleRef.current = mountPerfHud(monitor, {
42
- defaultPlacement,
50
+ defaultPlacement: placementEdge ? {
51
+ align: placementAlign,
52
+ edge: placementEdge
53
+ } : void 0,
43
54
  injectStyles,
44
- label,
45
- mode,
55
+ label: labelRef.current,
56
+ mode: modeRef.current,
46
57
  parent,
47
58
  refreshHz,
48
59
  storageKey,
@@ -57,11 +68,10 @@ function PerfHud({ defaultPlacement, injectStyles, label, mode, parent, refreshH
57
68
  handleRef.current = null;
58
69
  };
59
70
  }, [
60
- defaultPlacement,
61
71
  injectStyles,
62
- label,
63
- mode,
64
72
  parent,
73
+ placementAlign,
74
+ placementEdge,
65
75
  refreshHz,
66
76
  source,
67
77
  storageKey
@@ -70,6 +80,14 @@ function PerfHud({ defaultPlacement, injectStyles, label, mode, parent, refreshH
70
80
  themeRef.current = theme;
71
81
  handleRef.current?.setTheme(theme ?? "system");
72
82
  }, [theme]);
83
+ useEffect(() => {
84
+ modeRef.current = mode;
85
+ handleRef.current?.setMode(mode ?? "compact");
86
+ }, [mode]);
87
+ useEffect(() => {
88
+ labelRef.current = label;
89
+ handleRef.current?.element.setAttribute("aria-label", label ?? "Performance");
90
+ }, [label]);
73
91
  return null;
74
92
  }
75
93
  //#endregion
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"react.js","names":[],"sources":["../src/react/monitor-store.ts","../src/react/perf-hud.tsx","../src/react/perf-sampler.tsx"],"sourcesContent":["/**\n * Hands the in-canvas monitor to the DOM-side HUD. A React context can't do\n * it: R3F's `<Canvas>` is its own reconciler root, so a provider outside it\n * isn't visible inside without a bridge. A tiny external store crosses that\n * boundary for free. One default store; pass your own to both components to\n * run two canvases on one page.\n */\nimport type { PerformanceMonitor } from \"../core/performance-monitor.ts\";\n\ntype MonitorListener = (monitor: PerformanceMonitor | null) => void;\n\ntype PerfMonitorStore = {\n get: () => PerformanceMonitor | null;\n set: (monitor: PerformanceMonitor | null) => void;\n subscribe: (listener: MonitorListener) => () => void;\n};\n\nconst createPerfMonitorStore = (): PerfMonitorStore => {\n let current: PerformanceMonitor | null = null;\n const listeners = new Set<MonitorListener>();\n\n return {\n get: () => current,\n set: (monitor) => {\n current = monitor;\n\n for (const listener of listeners) {\n listener(monitor);\n }\n },\n subscribe: (listener) => {\n listeners.add(listener);\n\n return () => {\n listeners.delete(listener);\n };\n },\n };\n};\n\nconst defaultPerfMonitorStore = createPerfMonitorStore();\n\nexport { createPerfMonitorStore, defaultPerfMonitorStore };\nexport type { PerfMonitorStore };\n","import { useEffect, useRef } from \"react\";\n\nimport {\n mountPerfHud,\n type MountPerfHudOptions,\n type PerfHudHandle,\n} from \"../ui/mount-perf-hud.ts\";\nimport { defaultPerfMonitorStore, type PerfMonitorStore } from \"./monitor-store.ts\";\n\ntype PerfHudProps = Omit<MountPerfHudOptions, \"settings\"> & {\n /** Must match the `PerfSampler`'s. Default: the shared store. */\n store?: PerfMonitorStore;\n};\n\n/**\n * DOM overlay. Render it *outside* `<Canvas>`, or Fiber would treat its markup\n * as three objects. Mounts the dockable HUD whenever a `PerfSampler` has\n * published a monitor and tears it down when that sampler unmounts. `theme`\n * is the one prop that applies live; every other change remounts the HUD.\n */\nfunction PerfHud({\n defaultPlacement,\n injectStyles,\n label,\n mode,\n parent,\n refreshHz,\n storageKey,\n store,\n theme,\n}: PerfHudProps) {\n const source = store ?? defaultPerfMonitorStore;\n const handleRef = useRef<PerfHudHandle | null>(null);\n // The mount effect reads the latest theme without depending on it, so a\n // theme change never remounts. Kept current by the theme effect below.\n const themeRef = useRef(theme);\n\n useEffect(() => {\n const attach = (monitor: ReturnType<PerfMonitorStore[\"get\"]>) => {\n handleRef.current?.dispose();\n handleRef.current = null;\n\n if (!monitor) {\n return;\n }\n\n handleRef.current = mountPerfHud(monitor, {\n defaultPlacement,\n injectStyles,\n label,\n mode,\n parent,\n refreshHz,\n storageKey,\n theme: themeRef.current,\n });\n };\n\n const unsubscribe = source.subscribe(attach);\n attach(source.get());\n\n return () => {\n unsubscribe();\n handleRef.current?.dispose();\n handleRef.current = null;\n };\n }, [defaultPlacement, injectStyles, label, mode, parent, refreshHz, source, storageKey]);\n\n useEffect(() => {\n themeRef.current = theme;\n handleRef.current?.setTheme(theme ?? \"system\");\n }, [theme]);\n\n return null;\n}\n\nexport { PerfHud };\nexport type { PerfHudProps };\n","import { useFrame, useThree } from \"@react-three/fiber\";\nimport { useEffect, useRef } from \"react\";\n\nimport { PerformanceMonitor } from \"../core/performance-monitor.ts\";\nimport type { PerformanceMonitorOptions, PerfRenderer } from \"../core/types.ts\";\nimport { defaultPerfMonitorStore, type PerfMonitorStore } from \"./monitor-store.ts\";\n\ntype PerfSamplerProps = Omit<PerformanceMonitorOptions, \"renderer\"> & {\n /** Where the monitor is published for `PerfHud`. Default: the shared store. */\n store?: PerfMonitorStore;\n};\n\n/**\n * In-canvas collector. Renders nothing. Fiber runs every `useFrame` before\n * `gl.render`, so the previous frame is closed here and the next one opens, and\n * GPU queries then span the render that just ran. The monitor is created in\n * an effect so StrictMode's double mount attaches and detaches cleanly.\n */\nfunction PerfSampler({ gpuQueryPoolSize, historySize, store, trackGPU }: PerfSamplerProps) {\n const renderer = useThree((state) => state.gl);\n const monitorRef = useRef<PerformanceMonitor | null>(null);\n const startedRef = useRef(false);\n const target = store ?? defaultPerfMonitorStore;\n\n useEffect(() => {\n const monitor = new PerformanceMonitor({\n gpuQueryPoolSize,\n historySize,\n renderer: renderer as unknown as PerfRenderer,\n trackGPU,\n });\n monitorRef.current = monitor;\n startedRef.current = false;\n target.set(monitor);\n\n return () => {\n target.set(null);\n monitorRef.current = null;\n monitor.dispose();\n };\n }, [gpuQueryPoolSize, historySize, renderer, target, trackGPU]);\n\n useFrame(() => {\n const monitor = monitorRef.current;\n\n if (!monitor) {\n return;\n }\n\n if (startedRef.current) {\n monitor.end();\n }\n\n startedRef.current = true;\n monitor.begin();\n });\n\n return null;\n}\n\nexport { PerfSampler };\nexport type { PerfSamplerProps };\n"],"mappings":";;;;;AAiBA,MAAM,+BAAiD;CACrD,IAAI,UAAqC;CACzC,MAAM,4BAAY,IAAI,IAAqB;CAE3C,OAAO;EACL,WAAW;EACX,MAAM,YAAY;GAChB,UAAU;GAEV,KAAK,MAAM,YAAY,WACrB,SAAS,OAAO;EAEpB;EACA,YAAY,aAAa;GACvB,UAAU,IAAI,QAAQ;GAEtB,aAAa;IACX,UAAU,OAAO,QAAQ;GAC3B;EACF;CACF;AACF;AAEA,MAAM,0BAA0B,uBAAuB;;;;;;;;;ACpBvD,SAAS,QAAQ,EACf,kBACA,cACA,OACA,MACA,QACA,WACA,YACA,OACA,SACe;CACf,MAAM,SAAS,SAAS;CACxB,MAAM,YAAY,OAA6B,IAAI;CAGnD,MAAM,WAAW,OAAO,KAAK;CAE7B,gBAAgB;EACd,MAAM,UAAU,YAAiD;GAC/D,UAAU,SAAS,QAAQ;GAC3B,UAAU,UAAU;GAEpB,IAAI,CAAC,SACH;GAGF,UAAU,UAAU,aAAa,SAAS;IACxC;IACA;IACA;IACA;IACA;IACA;IACA;IACA,OAAO,SAAS;GAClB,CAAC;EACH;EAEA,MAAM,cAAc,OAAO,UAAU,MAAM;EAC3C,OAAO,OAAO,IAAI,CAAC;EAEnB,aAAa;GACX,YAAY;GACZ,UAAU,SAAS,QAAQ;GAC3B,UAAU,UAAU;EACtB;CACF,GAAG;EAAC;EAAkB;EAAc;EAAO;EAAM;EAAQ;EAAW;EAAQ;CAAU,CAAC;CAEvF,gBAAgB;EACd,SAAS,UAAU;EACnB,UAAU,SAAS,SAAS,SAAS,QAAQ;CAC/C,GAAG,CAAC,KAAK,CAAC;CAEV,OAAO;AACT;;;;;;;;;ACxDA,SAAS,YAAY,EAAE,kBAAkB,aAAa,OAAO,YAA8B;CACzF,MAAM,WAAW,UAAU,UAAU,MAAM,EAAE;CAC7C,MAAM,aAAa,OAAkC,IAAI;CACzD,MAAM,aAAa,OAAO,KAAK;CAC/B,MAAM,SAAS,SAAS;CAExB,gBAAgB;EACd,MAAM,UAAU,IAAI,mBAAmB;GACrC;GACA;GACU;GACV;EACF,CAAC;EACD,WAAW,UAAU;EACrB,WAAW,UAAU;EACrB,OAAO,IAAI,OAAO;EAElB,aAAa;GACX,OAAO,IAAI,IAAI;GACf,WAAW,UAAU;GACrB,QAAQ,QAAQ;EAClB;CACF,GAAG;EAAC;EAAkB;EAAa;EAAU;EAAQ;CAAQ,CAAC;CAE9D,eAAe;EACb,MAAM,UAAU,WAAW;EAE3B,IAAI,CAAC,SACH;EAGF,IAAI,WAAW,SACb,QAAQ,IAAI;EAGd,WAAW,UAAU;EACrB,QAAQ,MAAM;CAChB,CAAC;CAED,OAAO;AACT"}
1
+ {"version":3,"file":"react.js","names":[],"sources":["../src/react/monitor-store.ts","../src/react/perf-hud.tsx","../src/react/perf-sampler.tsx"],"sourcesContent":["/**\n * Hands the in-canvas monitor to the DOM-side HUD. A React context can't do\n * it: R3F's `<Canvas>` is its own reconciler root, so a provider outside it\n * isn't visible inside without a bridge. A tiny external store crosses that\n * boundary for free. One default store; pass your own to both components to\n * run two canvases on one page.\n */\nimport type { PerformanceMonitor } from \"../core/performance-monitor.ts\";\n\ntype MonitorListener = (monitor: PerformanceMonitor | null) => void;\n\ntype PerfMonitorStore = {\n get: () => PerformanceMonitor | null;\n set: (monitor: PerformanceMonitor | null) => void;\n subscribe: (listener: MonitorListener) => () => void;\n};\n\nconst createPerfMonitorStore = (): PerfMonitorStore => {\n let current: PerformanceMonitor | null = null;\n const listeners = new Set<MonitorListener>();\n\n return {\n get: () => current,\n set: (monitor) => {\n current = monitor;\n\n for (const listener of listeners) {\n listener(monitor);\n }\n },\n subscribe: (listener) => {\n listeners.add(listener);\n\n return () => {\n listeners.delete(listener);\n };\n },\n };\n};\n\nconst defaultPerfMonitorStore = createPerfMonitorStore();\n\nexport { createPerfMonitorStore, defaultPerfMonitorStore };\nexport type { PerfMonitorStore };\n","import { useEffect, useRef } from \"react\";\n\nimport {\n mountPerfHud,\n type MountPerfHudOptions,\n type PerfHudHandle,\n} from \"../ui/mount-perf-hud.ts\";\nimport { defaultPerfMonitorStore, type PerfMonitorStore } from \"./monitor-store.ts\";\n\ntype PerfHudProps = Omit<MountPerfHudOptions, \"settings\"> & {\n /** Must match the `PerfSampler`'s. Default: the shared store. */\n store?: PerfMonitorStore;\n};\n\n/**\n * DOM overlay. Render it *outside* `<Canvas>`, or Fiber would treat its markup\n * as three objects. Mounts the dockable HUD whenever a `PerfSampler` has\n * published a monitor and tears it down when that sampler unmounts.\n *\n * `theme`, `mode` and `label` apply live through the handle. `defaultPlacement`\n * is compared by value, so an inline `{{ edge: \"right\" }}` literal is fine.\n * The remaining props (`storageKey`, `parent`, `refreshHz`, `injectStyles`,\n * `store`) remount the HUD when they change.\n */\nfunction PerfHud({\n defaultPlacement,\n injectStyles,\n label,\n mode,\n parent,\n refreshHz,\n storageKey,\n store,\n theme,\n}: PerfHudProps) {\n const source = store ?? defaultPerfMonitorStore;\n const handleRef = useRef<PerfHudHandle | null>(null);\n // The mount effect reads these through refs so changing them never remounts;\n // the live effects below keep the refs current and push changes to the handle.\n const themeRef = useRef(theme);\n const modeRef = useRef(mode);\n const labelRef = useRef(label);\n // Primitives, so an inline `{{ edge: \"right\" }}` literal doesn't remount each render.\n const placementEdge = defaultPlacement?.edge;\n const placementAlign = defaultPlacement?.align;\n\n useEffect(() => {\n const attach = (monitor: ReturnType<PerfMonitorStore[\"get\"]>) => {\n handleRef.current?.dispose();\n handleRef.current = null;\n\n if (!monitor) {\n return;\n }\n\n handleRef.current = mountPerfHud(monitor, {\n defaultPlacement: placementEdge\n ? { align: placementAlign, edge: placementEdge }\n : undefined,\n injectStyles,\n label: labelRef.current,\n mode: modeRef.current,\n parent,\n refreshHz,\n storageKey,\n theme: themeRef.current,\n });\n };\n\n const unsubscribe = source.subscribe(attach);\n attach(source.get());\n\n return () => {\n unsubscribe();\n handleRef.current?.dispose();\n handleRef.current = null;\n };\n }, [injectStyles, parent, placementAlign, placementEdge, refreshHz, source, storageKey]);\n\n useEffect(() => {\n themeRef.current = theme;\n handleRef.current?.setTheme(theme ?? \"system\");\n }, [theme]);\n\n useEffect(() => {\n modeRef.current = mode;\n handleRef.current?.setMode(mode ?? \"compact\");\n }, [mode]);\n\n useEffect(() => {\n labelRef.current = label;\n handleRef.current?.element.setAttribute(\"aria-label\", label ?? \"Performance\");\n }, [label]);\n\n return null;\n}\n\nexport { PerfHud };\nexport type { PerfHudProps };\n","import { useFrame, useThree } from \"@react-three/fiber\";\nimport { useEffect, useRef } from \"react\";\n\nimport { PerformanceMonitor } from \"../core/performance-monitor.ts\";\nimport type { PerformanceMonitorOptions, PerfRenderer } from \"../core/types.ts\";\nimport { defaultPerfMonitorStore, type PerfMonitorStore } from \"./monitor-store.ts\";\n\ntype PerfSamplerProps = Omit<PerformanceMonitorOptions, \"renderer\"> & {\n /** Where the monitor is published for `PerfHud`. Default: the shared store. */\n store?: PerfMonitorStore;\n};\n\n/**\n * In-canvas collector. Renders nothing. Fiber runs every `useFrame` before\n * `gl.render`, so the previous frame is closed here and the next one opens, and\n * GPU queries then span the render that just ran. The monitor is created in\n * an effect so StrictMode's double mount attaches and detaches cleanly.\n */\nfunction PerfSampler({ gpuQueryPoolSize, historySize, store, trackGPU }: PerfSamplerProps) {\n const renderer = useThree((state) => state.gl);\n const monitorRef = useRef<PerformanceMonitor | null>(null);\n const startedRef = useRef(false);\n const target = store ?? defaultPerfMonitorStore;\n\n useEffect(() => {\n const monitor = new PerformanceMonitor({\n gpuQueryPoolSize,\n historySize,\n renderer: renderer as unknown as PerfRenderer,\n trackGPU,\n });\n monitorRef.current = monitor;\n startedRef.current = false;\n target.set(monitor);\n\n return () => {\n target.set(null);\n monitorRef.current = null;\n monitor.dispose();\n };\n }, [gpuQueryPoolSize, historySize, renderer, target, trackGPU]);\n\n useFrame(() => {\n const monitor = monitorRef.current;\n\n if (!monitor) {\n return;\n }\n\n if (startedRef.current) {\n monitor.end();\n }\n\n startedRef.current = true;\n monitor.begin();\n });\n\n return null;\n}\n\nexport { PerfSampler };\nexport type { PerfSamplerProps };\n"],"mappings":";;;;;AAiBA,MAAM,+BAAiD;CACrD,IAAI,UAAqC;CACzC,MAAM,4BAAY,IAAI,IAAqB;CAE3C,OAAO;EACL,WAAW;EACX,MAAM,YAAY;GAChB,UAAU;GAEV,KAAK,MAAM,YAAY,WACrB,SAAS,OAAO;EAEpB;EACA,YAAY,aAAa;GACvB,UAAU,IAAI,QAAQ;GAEtB,aAAa;IACX,UAAU,OAAO,QAAQ;GAC3B;EACF;CACF;AACF;AAEA,MAAM,0BAA0B,uBAAuB;;;;;;;;;;;;;AChBvD,SAAS,QAAQ,EACf,kBACA,cACA,OACA,MACA,QACA,WACA,YACA,OACA,SACe;CACf,MAAM,SAAS,SAAS;CACxB,MAAM,YAAY,OAA6B,IAAI;CAGnD,MAAM,WAAW,OAAO,KAAK;CAC7B,MAAM,UAAU,OAAO,IAAI;CAC3B,MAAM,WAAW,OAAO,KAAK;CAE7B,MAAM,gBAAgB,kBAAkB;CACxC,MAAM,iBAAiB,kBAAkB;CAEzC,gBAAgB;EACd,MAAM,UAAU,YAAiD;GAC/D,UAAU,SAAS,QAAQ;GAC3B,UAAU,UAAU;GAEpB,IAAI,CAAC,SACH;GAGF,UAAU,UAAU,aAAa,SAAS;IACxC,kBAAkB,gBACd;KAAE,OAAO;KAAgB,MAAM;IAAc,IAC7C,KAAA;IACJ;IACA,OAAO,SAAS;IAChB,MAAM,QAAQ;IACd;IACA;IACA;IACA,OAAO,SAAS;GAClB,CAAC;EACH;EAEA,MAAM,cAAc,OAAO,UAAU,MAAM;EAC3C,OAAO,OAAO,IAAI,CAAC;EAEnB,aAAa;GACX,YAAY;GACZ,UAAU,SAAS,QAAQ;GAC3B,UAAU,UAAU;EACtB;CACF,GAAG;EAAC;EAAc;EAAQ;EAAgB;EAAe;EAAW;EAAQ;CAAU,CAAC;CAEvF,gBAAgB;EACd,SAAS,UAAU;EACnB,UAAU,SAAS,SAAS,SAAS,QAAQ;CAC/C,GAAG,CAAC,KAAK,CAAC;CAEV,gBAAgB;EACd,QAAQ,UAAU;EAClB,UAAU,SAAS,QAAQ,QAAQ,SAAS;CAC9C,GAAG,CAAC,IAAI,CAAC;CAET,gBAAgB;EACd,SAAS,UAAU;EACnB,UAAU,SAAS,QAAQ,aAAa,cAAc,SAAS,aAAa;CAC9E,GAAG,CAAC,KAAK,CAAC;CAEV,OAAO;AACT;;;;;;;;;AC7EA,SAAS,YAAY,EAAE,kBAAkB,aAAa,OAAO,YAA8B;CACzF,MAAM,WAAW,UAAU,UAAU,MAAM,EAAE;CAC7C,MAAM,aAAa,OAAkC,IAAI;CACzD,MAAM,aAAa,OAAO,KAAK;CAC/B,MAAM,SAAS,SAAS;CAExB,gBAAgB;EACd,MAAM,UAAU,IAAI,mBAAmB;GACrC;GACA;GACU;GACV;EACF,CAAC;EACD,WAAW,UAAU;EACrB,WAAW,UAAU;EACrB,OAAO,IAAI,OAAO;EAElB,aAAa;GACX,OAAO,IAAI,IAAI;GACf,WAAW,UAAU;GACrB,QAAQ,QAAQ;EAClB;CACF,GAAG;EAAC;EAAkB;EAAa;EAAU;EAAQ;CAAQ,CAAC;CAE9D,eAAe;EACb,MAAM,UAAU,WAAW;EAE3B,IAAI,CAAC,SACH;EAGF,IAAI,WAAW,SACb,QAAQ,IAAI;EAGd,WAAW,UAAU;EACrB,QAAQ,MAAM;CAChB,CAAC;CAED,OAAO;AACT"}
package/dist/styles.css CHANGED
@@ -242,15 +242,90 @@
242
242
  text-align: right;
243
243
  }
244
244
 
245
+ .perf-monitor__footer {
246
+ display: flex;
247
+ align-items: flex-start;
248
+ gap: 6px;
249
+ padding: 8px 6px 0;
250
+ border-top: 1px solid var(--perf-border);
251
+ color: var(--perf-muted);
252
+ font-size: 10px;
253
+ }
254
+
255
+ .perf-monitor__footer-body {
256
+ display: flex;
257
+ flex: 1;
258
+ flex-direction: column;
259
+ gap: 4px;
260
+ min-width: 0;
261
+ }
262
+
263
+ .perf-monitor__footer-row {
264
+ display: flex;
265
+ align-items: center;
266
+ justify-content: space-between;
267
+ gap: 8px;
268
+ min-width: 0;
269
+ }
270
+
271
+ .perf-monitor__footer-row[hidden],
272
+ .perf-monitor__badge[hidden] {
273
+ display: none;
274
+ }
275
+
276
+ .perf-monitor__footer-gpu {
277
+ min-width: 0;
278
+ overflow: hidden;
279
+ text-overflow: ellipsis;
280
+ white-space: nowrap;
281
+ color: var(--perf-fg-dim);
282
+ }
283
+
284
+ .perf-monitor__badge {
285
+ flex: none;
286
+ padding: 0 5px;
287
+ border: 1px solid var(--perf-border);
288
+ border-radius: 3px;
289
+ color: var(--perf-fg-dim);
290
+ letter-spacing: 0.04em;
291
+ }
292
+
293
+ .perf-monitor__badge.is-fallback {
294
+ border-color: #f59e0b;
295
+ color: #f59e0b;
296
+ }
297
+
298
+ .perf-monitor[data-theme="light"] .perf-monitor__badge.is-fallback {
299
+ border-color: #d97706;
300
+ color: #b45309;
301
+ }
302
+
303
+ .perf-monitor__link {
304
+ color: inherit;
305
+ text-decoration: none;
306
+ }
307
+
308
+ .perf-monitor__link:hover,
309
+ .perf-monitor__link:focus-visible {
310
+ color: var(--perf-accent);
311
+ text-decoration: underline;
312
+ }
313
+
245
314
  .perf-monitor--full .perf-monitor__hud {
246
315
  display: none;
247
316
  }
248
317
 
249
318
  .perf-monitor--compact .perf-monitor__graphs,
250
- .perf-monitor--compact .perf-monitor__section {
319
+ .perf-monitor--compact .perf-monitor__section,
320
+ .perf-monitor--compact:not(.perf-monitor--info) .perf-monitor__footer,
321
+ .perf-monitor--compact .perf-monitor__footer > .perf-monitor__checkbox {
251
322
  display: none;
252
323
  }
253
324
 
325
+ .perf-monitor--compact .perf-monitor__footer {
326
+ padding: 6px 2px 2px;
327
+ }
328
+
254
329
  .perf-monitor--compact {
255
330
  padding: 4px 8px;
256
331
  }