@zkmake/three-meter 0.3.2 → 0.5.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/README.md +52 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{mount-perf-hud-CX9DrxjA.js → mount-perf-hud-BLs1jkwM.js} +327 -7
- package/dist/mount-perf-hud-BLs1jkwM.js.map +1 -0
- package/dist/{mount-perf-hud-FmunHhel.d.ts → mount-perf-hud-CmINOH3T.d.ts} +17 -2
- package/dist/{performance-monitor-CdaU8Dsv.d.ts → performance-monitor-CBN05_BC.d.ts} +42 -2
- package/dist/{performance-monitor-D_v7W312.js → performance-monitor-yxsxs57v.js} +147 -3
- package/dist/performance-monitor-yxsxs57v.js.map +1 -0
- package/dist/react.d.ts +3 -3
- package/dist/react.js +5 -3
- package/dist/react.js.map +1 -1
- package/dist/styles.css +97 -1
- package/dist/ui.d.ts +8 -3
- package/dist/ui.js +2 -2
- package/package.json +1 -1
- package/dist/mount-perf-hud-CX9DrxjA.js.map +0 -1
- package/dist/performance-monitor-D_v7W312.js.map +0 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ load the scene up.
|
|
|
16
16
|
<img
|
|
17
17
|
src="https://raw.githubusercontent.com/zkmake/three-meter/main/docs/hud-full.png"
|
|
18
18
|
width="320"
|
|
19
|
-
alt="The HUD in full mode, dark theme: a theme row (light, system, dark), a dim-on-leave toggle, FPS, CPU and GPU sparklines with live values, and a checkbox list of counters (FPS, calls, CPU, GPU, triangles, lines, points, render passes, geometries, textures, shaders) choosing what the compact card shows. Two discs beside it: a drag grip and the compact/full toggle."
|
|
19
|
+
alt="The HUD in full mode, dark theme: a theme row (light, system, dark), a dim-on-leave toggle, FPS, CPU and GPU sparklines with live values, and a checkbox list of counters (FPS, calls, CPU, GPU, triangles, lines, points, render passes, geometries, textures, shaders) choosing what the compact card shows, then a footer with the three-meter version, the three revision, a WebGL2 backend badge and the GPU name. Two discs beside it: a drag grip and the compact/full toggle."
|
|
20
20
|
/>
|
|
21
21
|
</p>
|
|
22
22
|
|
|
@@ -90,8 +90,8 @@ Canvas is its own React root. To run two canvases on one page, pass the same `st
|
|
|
90
90
|
| `injectStyles` | `true` | Append the stylesheet once per document. |
|
|
91
91
|
| `refreshHz` | `10` | Repaint rate. |
|
|
92
92
|
|
|
93
|
-
`PerformanceMonitor` and `PerfSampler` take `trackGPU` (default true), `gpuQueryPoolSize` (default 5)
|
|
94
|
-
|
|
93
|
+
`PerformanceMonitor` and `PerfSampler` take `trackGPU` (default true), `gpuQueryPoolSize` (default 5),
|
|
94
|
+
`historySize` (default 120) and `frameStatsSize` (default 1000).
|
|
95
95
|
|
|
96
96
|
## Theme
|
|
97
97
|
|
|
@@ -127,6 +127,55 @@ and Firefox don't, so the GPU row shows `—` and `sample.gpu.available` is `fal
|
|
|
127
127
|
|
|
128
128
|
On WebGPU, construct the renderer with `trackTimestamp: true`. Timings resolve a frame or two late.
|
|
129
129
|
|
|
130
|
+
## Environment
|
|
131
|
+
|
|
132
|
+
The full view's footer has two rows: the three-meter version (linked to its release notes) and the
|
|
133
|
+
three revision, then the backend and the GPU name. An amber `WebGL2 fallback` badge means
|
|
134
|
+
`WebGPURenderer` couldn't get WebGPU. Its checkbox (off by default) shows the footer in the compact
|
|
135
|
+
HUD too. The same data is on the monitor:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
monitor.getEnvironment();
|
|
139
|
+
// { three: "186", backend: "webgpu", fallback: false, gpu: "Apple metal-3" }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`backend` is `null` until `WebGPURenderer.init()` settles. `gpu` comes from WebGPU's adapter info or
|
|
143
|
+
WebGL's unmasked renderer string, and is `null` where the browser hides it. Chrome's WebGPU gives
|
|
144
|
+
vendor and architecture, not the model.
|
|
145
|
+
|
|
146
|
+
## Stutter
|
|
147
|
+
|
|
148
|
+
Average FPS hides a hitch every few seconds. Three rows in the full view catch it, and any of them
|
|
149
|
+
can be ticked into the compact HUD:
|
|
150
|
+
|
|
151
|
+
- **1% low**: mean FPS across the slowest 1% of frames.
|
|
152
|
+
- **Frame p99**: 99% of frames finish within this many ms.
|
|
153
|
+
- **Hitches**: frames over twice the median frame time.
|
|
154
|
+
|
|
155
|
+
They cover the last `frameStatsSize` frames (1000 by default, about 16 s at 60 Hz). Gaps over a
|
|
156
|
+
second, like a hidden tab, are left out. Read them with `monitor.getFrameStats()`:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
monitor.getFrameStats();
|
|
160
|
+
// { frames: 1000, lowFps: 97.6, p99Ms: 10.2, hitches: 3 }
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Bug reports
|
|
164
|
+
|
|
165
|
+
The **report** row's copy button puts a Markdown snapshot on the clipboard: versions, backend, GPU,
|
|
166
|
+
the current metrics, the stutter stats, the viewport and the user agent. Paste it into an issue.
|
|
167
|
+
`formatReport(monitor)` from `@zkmake/three-meter/ui` returns the same text.
|
|
168
|
+
|
|
169
|
+
```text
|
|
170
|
+
three-meter v0.5.0 · three r186 · WebGL2 · Apple M4 Max
|
|
171
|
+
FPS 120 · 1% low 98 · p99 10.2 ms · hitches 3 / 1,000 frames
|
|
172
|
+
CPU 0.2 ms · GPU 0.7 ms
|
|
173
|
+
Calls 1 · passes 1 · triangles 24,000 · lines 0 · points 0
|
|
174
|
+
Geometries 1 · textures 1 · shaders 1
|
|
175
|
+
Viewport 1280×720 @2x
|
|
176
|
+
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) …
|
|
177
|
+
```
|
|
178
|
+
|
|
130
179
|
## Examples
|
|
131
180
|
|
|
132
181
|
[`examples/site`](examples/site) is what runs at [three-meter.pages.dev](https://three-meter.pages.dev/):
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as PerfRenderer, c as RenderCounts, d as TimingMetric, i as GpuTiming, l as ResourceCounts, n as Environment, o as PerformanceMonitorOptions, r as FrameStats, s as RenderBackend, t as PerformanceMonitor, u as Sample } from "./performance-monitor-CBN05_BC.js";
|
|
2
2
|
//#region src/core/ring-buffer.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Fixed-capacity numeric ring buffer. Backs the per-frame timing history the
|
|
@@ -32,5 +32,5 @@ type AnimationLoop<Args extends unknown[]> = (...args: Args) => void;
|
|
|
32
32
|
*/
|
|
33
33
|
declare function wrapAnimationLoop<Args extends unknown[]>(monitor: PerformanceMonitor, loop: AnimationLoop<Args>): AnimationLoop<Args>;
|
|
34
34
|
//#endregion
|
|
35
|
-
export { type GpuTiming, type PerfRenderer, PerformanceMonitor, type PerformanceMonitorOptions, type RenderCounts, type ResourceCounts, RingBuffer, type Sample, type TimingMetric, wrapAnimationLoop };
|
|
35
|
+
export { type Environment, type FrameStats, type GpuTiming, type PerfRenderer, PerformanceMonitor, type PerformanceMonitorOptions, type RenderBackend, type RenderCounts, type ResourceCounts, RingBuffer, type Sample, type TimingMetric, wrapAnimationLoop };
|
|
36
36
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as RingBuffer, t as PerformanceMonitor } from "./performance-monitor-
|
|
1
|
+
import { n as RingBuffer, t as PerformanceMonitor } from "./performance-monitor-yxsxs57v.js";
|
|
2
2
|
//#region src/core/wrap-animation-loop.ts
|
|
3
3
|
/**
|
|
4
4
|
* Wrap a `renderer.setAnimationLoop` callback so each tick is one frame:
|
|
@@ -88,6 +88,7 @@ const applyTheme = (element, theme) => {
|
|
|
88
88
|
//#region src/ui/hud-settings.ts
|
|
89
89
|
const DEFAULT_STORAGE_KEY = "three-meter";
|
|
90
90
|
const DEFAULT_DIM = false;
|
|
91
|
+
const DEFAULT_INFO = false;
|
|
91
92
|
const DEFAULT_NUMBERS = [
|
|
92
93
|
"fps",
|
|
93
94
|
"calls",
|
|
@@ -111,6 +112,7 @@ var HudSettings = class {
|
|
|
111
112
|
this.defaults = {
|
|
112
113
|
dim: options.defaults?.dim ?? DEFAULT_DIM,
|
|
113
114
|
graphs: options.defaults?.graphs ?? DEFAULT_GRAPHS,
|
|
115
|
+
info: options.defaults?.info ?? DEFAULT_INFO,
|
|
114
116
|
numbers: options.defaults?.numbers ?? DEFAULT_NUMBERS
|
|
115
117
|
};
|
|
116
118
|
this.state = this.load();
|
|
@@ -125,6 +127,13 @@ var HudSettings = class {
|
|
|
125
127
|
this.state.dim = enabled;
|
|
126
128
|
this.persistAndNotify();
|
|
127
129
|
}
|
|
130
|
+
get info() {
|
|
131
|
+
return this.state.info;
|
|
132
|
+
}
|
|
133
|
+
setInfo(enabled) {
|
|
134
|
+
this.state.info = enabled;
|
|
135
|
+
this.persistAndNotify();
|
|
136
|
+
}
|
|
128
137
|
/** The user's pick in the panel; `null` means none, so the consumer's option applies. */
|
|
129
138
|
get theme() {
|
|
130
139
|
return this.state.theme;
|
|
@@ -159,6 +168,7 @@ var HudSettings = class {
|
|
|
159
168
|
const fallback = () => ({
|
|
160
169
|
dim: this.defaults.dim,
|
|
161
170
|
graphs: new Set(this.defaults.graphs),
|
|
171
|
+
info: this.defaults.info,
|
|
162
172
|
numbers: new Set(this.defaults.numbers),
|
|
163
173
|
theme: null
|
|
164
174
|
});
|
|
@@ -170,6 +180,7 @@ var HudSettings = class {
|
|
|
170
180
|
return {
|
|
171
181
|
dim: parsed.dim ?? this.defaults.dim,
|
|
172
182
|
graphs: new Set((parsed.graphs ?? this.defaults.graphs).filter(isTimingMetric)),
|
|
183
|
+
info: parsed.info ?? this.defaults.info,
|
|
173
184
|
numbers: new Set(parsed.numbers ?? this.defaults.numbers),
|
|
174
185
|
theme: isThemeMode(parsed.theme) ? parsed.theme : null
|
|
175
186
|
};
|
|
@@ -182,6 +193,7 @@ var HudSettings = class {
|
|
|
182
193
|
localStorage.setItem(this.storageKey, JSON.stringify({
|
|
183
194
|
dim: this.state.dim,
|
|
184
195
|
graphs: [...this.state.graphs],
|
|
196
|
+
info: this.state.info,
|
|
185
197
|
numbers: [...this.state.numbers],
|
|
186
198
|
theme: this.state.theme ?? void 0
|
|
187
199
|
}));
|
|
@@ -442,6 +454,7 @@ const dockPanel = (element, options = {}) => {
|
|
|
442
454
|
//#region src/ui/icons.ts
|
|
443
455
|
const ICON_MARKUP = {
|
|
444
456
|
blend: "<circle cx=\"9\" cy=\"9\" r=\"7\"/><circle cx=\"15\" cy=\"15\" r=\"7\"/>",
|
|
457
|
+
clipboard: "<rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\" ry=\"1\"/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/>",
|
|
445
458
|
contrast: "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 18a6 6 0 0 0 0-12v12z\"/>",
|
|
446
459
|
grip: "<circle cx=\"9\" cy=\"12\" r=\"1\"/><circle cx=\"9\" cy=\"5\" r=\"1\"/><circle cx=\"9\" cy=\"19\" r=\"1\"/><circle cx=\"15\" cy=\"12\" r=\"1\"/><circle cx=\"15\" cy=\"5\" r=\"1\"/><circle cx=\"15\" cy=\"19\" r=\"1\"/>",
|
|
447
460
|
monitor: "<rect width=\"20\" height=\"14\" x=\"2\" y=\"3\" rx=\"2\"/><line x1=\"8\" x2=\"16\" y1=\"21\" y2=\"21\"/><line x1=\"12\" x2=\"12\" y1=\"17\" y2=\"21\"/>",
|
|
@@ -463,6 +476,44 @@ const createIcon = (name, className) => {
|
|
|
463
476
|
return svg;
|
|
464
477
|
};
|
|
465
478
|
//#endregion
|
|
479
|
+
//#region package.json
|
|
480
|
+
var version = "0.5.0";
|
|
481
|
+
//#endregion
|
|
482
|
+
//#region src/ui/clipboard.ts
|
|
483
|
+
/**
|
|
484
|
+
* Copy text from a click handler. The async Clipboard API needs a secure
|
|
485
|
+
* context and the `clipboard-write` permission; a dev server opened over a LAN
|
|
486
|
+
* IP has neither, and some embeds deny the permission. The fallback is the old
|
|
487
|
+
* select-a-textarea `execCommand("copy")`, still honoured inside a user gesture.
|
|
488
|
+
*/
|
|
489
|
+
const copyWithSelection = (text) => {
|
|
490
|
+
const textarea = document.createElement("textarea");
|
|
491
|
+
textarea.value = text;
|
|
492
|
+
textarea.setAttribute("readonly", "");
|
|
493
|
+
textarea.style.position = "fixed";
|
|
494
|
+
textarea.style.opacity = "0";
|
|
495
|
+
textarea.style.pointerEvents = "none";
|
|
496
|
+
const previous = document.activeElement;
|
|
497
|
+
document.body.append(textarea);
|
|
498
|
+
textarea.select();
|
|
499
|
+
try {
|
|
500
|
+
return document.execCommand("copy");
|
|
501
|
+
} catch {
|
|
502
|
+
return false;
|
|
503
|
+
} finally {
|
|
504
|
+
textarea.remove();
|
|
505
|
+
previous?.focus?.();
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
const copyText = async (text) => {
|
|
509
|
+
try {
|
|
510
|
+
await navigator.clipboard.writeText(text);
|
|
511
|
+
return true;
|
|
512
|
+
} catch {
|
|
513
|
+
return copyWithSelection(text);
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
//#endregion
|
|
466
517
|
//#region src/ui/format.ts
|
|
467
518
|
/**
|
|
468
519
|
* Number formatting for the card. Counts stay exact with thousands separators
|
|
@@ -478,6 +529,76 @@ const compact = new Intl.NumberFormat("en-US", {
|
|
|
478
529
|
});
|
|
479
530
|
const formatCount = (value) => Math.abs(value) < COMPACT_FROM ? exact.format(value) : compact.format(value);
|
|
480
531
|
//#endregion
|
|
532
|
+
//#region src/ui/report.ts
|
|
533
|
+
/**
|
|
534
|
+
* A plain-text snapshot of the monitor for bug reports: versions, backend and
|
|
535
|
+
* GPU, the current sample, stutter stats and the page. Fenced as a `text`
|
|
536
|
+
* code block so GitHub and Slack keep the line breaks.
|
|
537
|
+
*/
|
|
538
|
+
const BACKEND_LABELS = {
|
|
539
|
+
webgl: "WebGL",
|
|
540
|
+
webgl2: "WebGL2",
|
|
541
|
+
webgpu: "WebGPU"
|
|
542
|
+
};
|
|
543
|
+
/** `WebGPU`, `WebGL2`, `WebGL2 fallback`, or `""` before the backend settles. */
|
|
544
|
+
const backendLabel = (environment) => {
|
|
545
|
+
if (!environment.backend) return "";
|
|
546
|
+
const label = BACKEND_LABELS[environment.backend];
|
|
547
|
+
return environment.fallback ? `${label} fallback` : label;
|
|
548
|
+
};
|
|
549
|
+
const ms = (value) => `${value.toFixed(1)} ms`;
|
|
550
|
+
const renderReport = ({ environment, page, sample, stats }) => {
|
|
551
|
+
const { render, resources } = sample;
|
|
552
|
+
const heading = [
|
|
553
|
+
`three-meter v${version}`,
|
|
554
|
+
environment.three ? `three r${environment.three}` : "",
|
|
555
|
+
backendLabel(environment),
|
|
556
|
+
environment.gpu ?? ""
|
|
557
|
+
].filter(Boolean);
|
|
558
|
+
const frames = stats.frames === 0 ? `FPS ${Math.round(sample.fps)}` : [
|
|
559
|
+
`FPS ${Math.round(sample.fps)}`,
|
|
560
|
+
`1% low ${Math.round(stats.lowFps)}`,
|
|
561
|
+
`p99 ${ms(stats.p99Ms)}`,
|
|
562
|
+
`hitches ${formatCount(stats.hitches)} / ${formatCount(stats.frames)} frames`
|
|
563
|
+
].join(" · ");
|
|
564
|
+
const lines = [
|
|
565
|
+
heading.join(" · "),
|
|
566
|
+
frames,
|
|
567
|
+
`CPU ${ms(sample.cpu)} · GPU ${sample.gpu.available ? ms(sample.gpu.ms) : "unavailable"}`,
|
|
568
|
+
[
|
|
569
|
+
`Calls ${formatCount(render.calls)}`,
|
|
570
|
+
`passes ${formatCount(render.passes)}`,
|
|
571
|
+
`triangles ${formatCount(render.triangles)}`,
|
|
572
|
+
`lines ${formatCount(render.lines)}`,
|
|
573
|
+
`points ${formatCount(render.points)}`
|
|
574
|
+
].join(" · "),
|
|
575
|
+
[
|
|
576
|
+
`Geometries ${formatCount(resources.geometries)}`,
|
|
577
|
+
`textures ${formatCount(resources.textures)}`,
|
|
578
|
+
`shaders ${formatCount(resources.programs)}`
|
|
579
|
+
].join(" · ")
|
|
580
|
+
];
|
|
581
|
+
if (page) lines.push(`Viewport ${page.width}×${page.height} @${page.dpr}x`, page.userAgent);
|
|
582
|
+
return [
|
|
583
|
+
"```text",
|
|
584
|
+
...lines,
|
|
585
|
+
"```"
|
|
586
|
+
].join("\n");
|
|
587
|
+
};
|
|
588
|
+
const readPage = () => typeof window === "undefined" ? null : {
|
|
589
|
+
dpr: window.devicePixelRatio || 1,
|
|
590
|
+
height: window.innerHeight,
|
|
591
|
+
userAgent: navigator.userAgent,
|
|
592
|
+
width: window.innerWidth
|
|
593
|
+
};
|
|
594
|
+
/** The report for `monitor` right now, as Markdown ready to paste into an issue. */
|
|
595
|
+
const formatReport = (monitor) => renderReport({
|
|
596
|
+
environment: monitor.getEnvironment(),
|
|
597
|
+
page: readPage(),
|
|
598
|
+
sample: monitor.getSample(),
|
|
599
|
+
stats: monitor.getFrameStats()
|
|
600
|
+
});
|
|
601
|
+
//#endregion
|
|
481
602
|
//#region src/ui/sparkline.ts
|
|
482
603
|
function drawSparkline(ctx, width, height, values, style) {
|
|
483
604
|
ctx.clearRect(0, 0, width, height);
|
|
@@ -568,6 +689,9 @@ const THEME_OPTIONS = [
|
|
|
568
689
|
mode: "dark"
|
|
569
690
|
}
|
|
570
691
|
];
|
|
692
|
+
const RELEASE_URL = `https://github.com/zkmake/three-meter/releases/tag/v${version}`;
|
|
693
|
+
/** How long the report button says `copied` / `failed` before reading `copy` again. */
|
|
694
|
+
const COPY_FEEDBACK_MS = 1500;
|
|
571
695
|
const readTiming = (sample, metric) => {
|
|
572
696
|
switch (metric) {
|
|
573
697
|
case "fps": return sample.fps;
|
|
@@ -597,6 +721,21 @@ const NUMBERS = [
|
|
|
597
721
|
label: "GPU",
|
|
598
722
|
read: (sample) => sample.gpu.available ? sample.gpu.ms.toFixed(1) : "—"
|
|
599
723
|
},
|
|
724
|
+
{
|
|
725
|
+
key: "low",
|
|
726
|
+
label: "1% low",
|
|
727
|
+
read: (_sample, stats) => stats.frames ? Math.round(stats.lowFps).toString() : "—"
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
key: "p99",
|
|
731
|
+
label: "Frame p99",
|
|
732
|
+
read: (_sample, stats) => stats.frames ? stats.p99Ms.toFixed(1) : "—"
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
key: "hitches",
|
|
736
|
+
label: "Hitches",
|
|
737
|
+
read: (_sample, stats) => stats.frames ? formatCount(stats.hitches) : "—"
|
|
738
|
+
},
|
|
600
739
|
{
|
|
601
740
|
key: "triangles",
|
|
602
741
|
label: "Triangles",
|
|
@@ -654,7 +793,13 @@ var PerformanceView = class {
|
|
|
654
793
|
statValueEls = /* @__PURE__ */ new Map();
|
|
655
794
|
statCheckboxes = /* @__PURE__ */ new Map();
|
|
656
795
|
dimCheckbox;
|
|
796
|
+
infoCheckbox;
|
|
657
797
|
themeRadios = /* @__PURE__ */ new Map();
|
|
798
|
+
footerThreeEl;
|
|
799
|
+
footerHardwareEl;
|
|
800
|
+
footerBackendEl;
|
|
801
|
+
footerGpuEl;
|
|
802
|
+
lastEnvironmentKey = "";
|
|
658
803
|
hudGraphsEl;
|
|
659
804
|
hudGridEl;
|
|
660
805
|
hudGraphs = /* @__PURE__ */ new Map();
|
|
@@ -676,6 +821,7 @@ var PerformanceView = class {
|
|
|
676
821
|
this.applyModeClass();
|
|
677
822
|
this.resizeObserver = new ResizeObserver(() => this.resizeCanvases());
|
|
678
823
|
this.build();
|
|
824
|
+
this.applyInfoClass();
|
|
679
825
|
this.rebuildHud();
|
|
680
826
|
this.onThemeChanged();
|
|
681
827
|
this.unsubscribe = this.settings.subscribe(() => this.onSelectionChanged());
|
|
@@ -717,6 +863,7 @@ var PerformanceView = class {
|
|
|
717
863
|
const sample = this.monitor.getSample();
|
|
718
864
|
if (this.mode === "compact") {
|
|
719
865
|
this.renderHud(sample);
|
|
866
|
+
if (this.settings.info) this.renderFooter();
|
|
720
867
|
return;
|
|
721
868
|
}
|
|
722
869
|
for (const config of TIMINGS) {
|
|
@@ -726,12 +873,33 @@ var PerformanceView = class {
|
|
|
726
873
|
const graph = this.graphCanvases.get(config.metric);
|
|
727
874
|
drawSparkline(graph.ctx, graph.width, graph.height, available ? this.monitor.getHistory(config.metric) : [], config.style[this.theme.resolved]);
|
|
728
875
|
}
|
|
729
|
-
|
|
876
|
+
const stats = this.monitor.getFrameStats();
|
|
877
|
+
for (const config of NUMBERS) this.statValueEls.get(config.key).textContent = config.read(sample, stats);
|
|
878
|
+
this.renderFooter();
|
|
879
|
+
}
|
|
880
|
+
/** The monitor caches the environment once its backend settles; until then this re-reads. */
|
|
881
|
+
renderFooter() {
|
|
882
|
+
const environment = this.monitor.getEnvironment();
|
|
883
|
+
const three = environment.three ? `three r${environment.three}` : "";
|
|
884
|
+
const backend = backendLabel(environment);
|
|
885
|
+
const gpu = environment.gpu ?? "";
|
|
886
|
+
const key = `${three}\n${backend}\n${gpu}`;
|
|
887
|
+
if (key === this.lastEnvironmentKey) return;
|
|
888
|
+
this.lastEnvironmentKey = key;
|
|
889
|
+
this.footerThreeEl.textContent = three;
|
|
890
|
+
this.footerBackendEl.textContent = backend;
|
|
891
|
+
this.footerBackendEl.hidden = backend === "";
|
|
892
|
+
this.footerBackendEl.title = environment.fallback ? "WebGPURenderer couldn't get WebGPU and fell back to WebGL2" : "";
|
|
893
|
+
this.footerBackendEl.classList.toggle("is-fallback", environment.fallback);
|
|
894
|
+
this.footerGpuEl.textContent = gpu;
|
|
895
|
+
this.footerGpuEl.title = gpu;
|
|
896
|
+
this.footerHardwareEl.hidden = backend === "" && gpu === "";
|
|
730
897
|
}
|
|
731
898
|
renderHud(sample) {
|
|
899
|
+
const stats = this.hudNumberEls.size > 0 ? this.monitor.getFrameStats() : null;
|
|
732
900
|
for (const [key, element] of this.hudNumberEls) {
|
|
733
901
|
const config = NUMBERS.find((number) => number.key === key);
|
|
734
|
-
if (config) element.textContent = config.read(sample);
|
|
902
|
+
if (config && stats) element.textContent = config.read(sample, stats);
|
|
735
903
|
}
|
|
736
904
|
for (const [metric, graph] of this.hudGraphs) {
|
|
737
905
|
const config = TIMINGS.find((timing) => timing.metric === metric);
|
|
@@ -783,7 +951,7 @@ var PerformanceView = class {
|
|
|
783
951
|
dimLabel.className = "perf-monitor__label";
|
|
784
952
|
dimLabel.textContent = "dim on leave";
|
|
785
953
|
dimRow.append(createIcon("blend", "perf-monitor__icon"), dimLabel, this.dimCheckbox);
|
|
786
|
-
options.append(themeRow, dimRow);
|
|
954
|
+
options.append(themeRow, dimRow, this.buildReportRow());
|
|
787
955
|
const graphs = document.createElement("div");
|
|
788
956
|
graphs.className = "perf-monitor__graphs";
|
|
789
957
|
for (const config of TIMINGS) {
|
|
@@ -836,7 +1004,33 @@ var PerformanceView = class {
|
|
|
836
1004
|
rowEl.append(checkbox, label, value);
|
|
837
1005
|
stats.append(rowEl);
|
|
838
1006
|
}
|
|
839
|
-
|
|
1007
|
+
const footer = document.createElement("div");
|
|
1008
|
+
footer.className = "perf-monitor__footer";
|
|
1009
|
+
this.infoCheckbox = this.buildCheckbox(this.settings.info, "Show version and environment in HUD", (on) => this.settings.setInfo(on));
|
|
1010
|
+
const footerBody = document.createElement("div");
|
|
1011
|
+
footerBody.className = "perf-monitor__footer-body";
|
|
1012
|
+
const softwareRow = document.createElement("div");
|
|
1013
|
+
softwareRow.className = "perf-monitor__footer-row";
|
|
1014
|
+
const release = document.createElement("a");
|
|
1015
|
+
release.className = "perf-monitor__link";
|
|
1016
|
+
release.href = RELEASE_URL;
|
|
1017
|
+
release.target = "_blank";
|
|
1018
|
+
release.rel = "noopener noreferrer";
|
|
1019
|
+
release.title = `Release notes for v${version}`;
|
|
1020
|
+
release.textContent = `three-meter v${version}`;
|
|
1021
|
+
this.footerThreeEl = document.createElement("span");
|
|
1022
|
+
softwareRow.append(release, this.footerThreeEl);
|
|
1023
|
+
this.footerHardwareEl = document.createElement("div");
|
|
1024
|
+
this.footerHardwareEl.className = "perf-monitor__footer-row";
|
|
1025
|
+
this.footerHardwareEl.hidden = true;
|
|
1026
|
+
this.footerBackendEl = document.createElement("span");
|
|
1027
|
+
this.footerBackendEl.className = "perf-monitor__badge";
|
|
1028
|
+
this.footerGpuEl = document.createElement("span");
|
|
1029
|
+
this.footerGpuEl.className = "perf-monitor__footer-gpu";
|
|
1030
|
+
this.footerHardwareEl.append(this.footerBackendEl, this.footerGpuEl);
|
|
1031
|
+
footerBody.append(softwareRow, this.footerHardwareEl);
|
|
1032
|
+
footer.append(this.infoCheckbox, footerBody);
|
|
1033
|
+
this.element.append(hud, options, graphs, stats, footer);
|
|
840
1034
|
}
|
|
841
1035
|
rebuildHud() {
|
|
842
1036
|
const { selection } = this.settings;
|
|
@@ -894,6 +1088,8 @@ var PerformanceView = class {
|
|
|
894
1088
|
for (const [metric, checkbox] of this.graphCheckboxes) checkbox.checked = this.settings.isGraphEnabled(metric);
|
|
895
1089
|
for (const [key, checkbox] of this.statCheckboxes) checkbox.checked = this.settings.isNumberEnabled(key);
|
|
896
1090
|
this.dimCheckbox.checked = this.settings.dim;
|
|
1091
|
+
this.infoCheckbox.checked = this.settings.info;
|
|
1092
|
+
this.applyInfoClass();
|
|
897
1093
|
this.theme.setOverride(this.settings.theme);
|
|
898
1094
|
this.rebuildHud();
|
|
899
1095
|
}
|
|
@@ -902,6 +1098,31 @@ var PerformanceView = class {
|
|
|
902
1098
|
applyTheme(this.element, this.theme);
|
|
903
1099
|
for (const [mode, radio] of this.themeRadios) radio.setAttribute("aria-checked", String(mode === this.theme.effective));
|
|
904
1100
|
}
|
|
1101
|
+
buildReportRow() {
|
|
1102
|
+
const row = document.createElement("div");
|
|
1103
|
+
row.className = "perf-monitor__row perf-monitor__row--static";
|
|
1104
|
+
const label = document.createElement("span");
|
|
1105
|
+
label.className = "perf-monitor__label";
|
|
1106
|
+
label.textContent = "report";
|
|
1107
|
+
const button = document.createElement("button");
|
|
1108
|
+
button.type = "button";
|
|
1109
|
+
button.className = "perf-monitor__button";
|
|
1110
|
+
button.title = "Copy versions, GPU and current metrics as Markdown for a bug report";
|
|
1111
|
+
button.textContent = "copy";
|
|
1112
|
+
let resetTimer = 0;
|
|
1113
|
+
const flash = (text) => {
|
|
1114
|
+
button.textContent = text;
|
|
1115
|
+
window.clearTimeout(resetTimer);
|
|
1116
|
+
resetTimer = window.setTimeout(() => {
|
|
1117
|
+
button.textContent = "copy";
|
|
1118
|
+
}, COPY_FEEDBACK_MS);
|
|
1119
|
+
};
|
|
1120
|
+
button.addEventListener("click", () => {
|
|
1121
|
+
copyText(formatReport(this.monitor)).then((copied) => flash(copied ? "copied" : "failed"));
|
|
1122
|
+
});
|
|
1123
|
+
row.append(createIcon("clipboard", "perf-monitor__icon"), label, button);
|
|
1124
|
+
return row;
|
|
1125
|
+
}
|
|
905
1126
|
buildCheckbox(checked, title, onChange) {
|
|
906
1127
|
const checkbox = document.createElement("input");
|
|
907
1128
|
checkbox.type = "checkbox";
|
|
@@ -912,6 +1133,9 @@ var PerformanceView = class {
|
|
|
912
1133
|
checkbox.addEventListener("change", () => onChange(checkbox.checked));
|
|
913
1134
|
return checkbox;
|
|
914
1135
|
}
|
|
1136
|
+
applyInfoClass() {
|
|
1137
|
+
this.element.classList.toggle("perf-monitor--info", this.settings.info);
|
|
1138
|
+
}
|
|
915
1139
|
applyModeClass() {
|
|
916
1140
|
this.element.classList.toggle("perf-monitor--compact", this.mode === "compact");
|
|
917
1141
|
this.element.classList.toggle("perf-monitor--full", this.mode === "full");
|
|
@@ -1127,6 +1351,27 @@ const PERF_HUD_STYLES = `
|
|
|
1127
1351
|
color: inherit;
|
|
1128
1352
|
}
|
|
1129
1353
|
|
|
1354
|
+
.perf-monitor__button {
|
|
1355
|
+
flex: none;
|
|
1356
|
+
min-width: 7ch;
|
|
1357
|
+
padding: 0 6px;
|
|
1358
|
+
border: 1px solid var(--perf-border);
|
|
1359
|
+
border-radius: 3px;
|
|
1360
|
+
background: var(--perf-row);
|
|
1361
|
+
color: var(--perf-fg-dim);
|
|
1362
|
+
font: inherit;
|
|
1363
|
+
cursor: pointer;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
.perf-monitor__button:hover {
|
|
1367
|
+
color: var(--perf-fg);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
.perf-monitor__button:focus-visible {
|
|
1371
|
+
outline: 1px solid color-mix(in srgb, var(--perf-accent) 60%, transparent);
|
|
1372
|
+
outline-offset: 1px;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1130
1375
|
.perf-monitor__checkbox {
|
|
1131
1376
|
flex: none;
|
|
1132
1377
|
margin: 0;
|
|
@@ -1193,15 +1438,90 @@ const PERF_HUD_STYLES = `
|
|
|
1193
1438
|
text-align: right;
|
|
1194
1439
|
}
|
|
1195
1440
|
|
|
1441
|
+
.perf-monitor__footer {
|
|
1442
|
+
display: flex;
|
|
1443
|
+
align-items: flex-start;
|
|
1444
|
+
gap: 6px;
|
|
1445
|
+
padding: 8px 6px 0;
|
|
1446
|
+
border-top: 1px solid var(--perf-border);
|
|
1447
|
+
color: var(--perf-muted);
|
|
1448
|
+
font-size: 10px;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
.perf-monitor__footer-body {
|
|
1452
|
+
display: flex;
|
|
1453
|
+
flex: 1;
|
|
1454
|
+
flex-direction: column;
|
|
1455
|
+
gap: 4px;
|
|
1456
|
+
min-width: 0;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
.perf-monitor__footer-row {
|
|
1460
|
+
display: flex;
|
|
1461
|
+
align-items: center;
|
|
1462
|
+
justify-content: space-between;
|
|
1463
|
+
gap: 8px;
|
|
1464
|
+
min-width: 0;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
.perf-monitor__footer-row[hidden],
|
|
1468
|
+
.perf-monitor__badge[hidden] {
|
|
1469
|
+
display: none;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
.perf-monitor__footer-gpu {
|
|
1473
|
+
min-width: 0;
|
|
1474
|
+
overflow: hidden;
|
|
1475
|
+
text-overflow: ellipsis;
|
|
1476
|
+
white-space: nowrap;
|
|
1477
|
+
color: var(--perf-fg-dim);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
.perf-monitor__badge {
|
|
1481
|
+
flex: none;
|
|
1482
|
+
padding: 0 5px;
|
|
1483
|
+
border: 1px solid var(--perf-border);
|
|
1484
|
+
border-radius: 3px;
|
|
1485
|
+
color: var(--perf-fg-dim);
|
|
1486
|
+
letter-spacing: 0.04em;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
.perf-monitor__badge.is-fallback {
|
|
1490
|
+
border-color: #f59e0b;
|
|
1491
|
+
color: #f59e0b;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
.perf-monitor[data-theme="light"] .perf-monitor__badge.is-fallback {
|
|
1495
|
+
border-color: #d97706;
|
|
1496
|
+
color: #b45309;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
.perf-monitor__link {
|
|
1500
|
+
color: inherit;
|
|
1501
|
+
text-decoration: none;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
.perf-monitor__link:hover,
|
|
1505
|
+
.perf-monitor__link:focus-visible {
|
|
1506
|
+
color: var(--perf-accent);
|
|
1507
|
+
text-decoration: underline;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1196
1510
|
.perf-monitor--full .perf-monitor__hud {
|
|
1197
1511
|
display: none;
|
|
1198
1512
|
}
|
|
1199
1513
|
|
|
1200
1514
|
.perf-monitor--compact .perf-monitor__graphs,
|
|
1201
|
-
.perf-monitor--compact .perf-monitor__section
|
|
1515
|
+
.perf-monitor--compact .perf-monitor__section,
|
|
1516
|
+
.perf-monitor--compact:not(.perf-monitor--info) .perf-monitor__footer,
|
|
1517
|
+
.perf-monitor--compact .perf-monitor__footer > .perf-monitor__checkbox {
|
|
1202
1518
|
display: none;
|
|
1203
1519
|
}
|
|
1204
1520
|
|
|
1521
|
+
.perf-monitor--compact .perf-monitor__footer {
|
|
1522
|
+
padding: 6px 2px 2px;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1205
1525
|
.perf-monitor--compact {
|
|
1206
1526
|
padding: 4px 8px;
|
|
1207
1527
|
}
|
|
@@ -1446,6 +1766,6 @@ const mountPerfHud = (monitor, options = {}) => {
|
|
|
1446
1766
|
};
|
|
1447
1767
|
};
|
|
1448
1768
|
//#endregion
|
|
1449
|
-
export {
|
|
1769
|
+
export { formatReport as a, HudSettings as c, isThemeMode as d, PerformanceView as i, HudTheme as l, PERF_HUD_STYLES as n, dockPanel as o, injectStyles as r, DEFAULT_STORAGE_KEY as s, mountPerfHud as t, THEME_MODES as u };
|
|
1450
1770
|
|
|
1451
|
-
//# sourceMappingURL=mount-perf-hud-
|
|
1771
|
+
//# sourceMappingURL=mount-perf-hud-BLs1jkwM.js.map
|