@zkmake/three-meter 0.4.0 → 0.6.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 +42 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{mount-perf-hud-Rf98QNxM.js → mount-perf-hud-C5wdjgmM.js} +286 -25
- package/dist/mount-perf-hud-C5wdjgmM.js.map +1 -0
- package/dist/{mount-perf-hud-v6kLGJUX.d.ts → mount-perf-hud-CmINOH3T.d.ts} +3 -2
- package/dist/{performance-monitor-Br7x3cPt.d.ts → performance-monitor-CBN05_BC.d.ts} +22 -2
- package/dist/{performance-monitor-C8dhWJWR.js → performance-monitor-yxsxs57v.js} +43 -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 +49 -2
- package/dist/ui.d.ts +8 -3
- package/dist/ui.js +2 -2
- package/package.json +1 -1
- package/dist/mount-perf-hud-Rf98QNxM.js.map +0 -1
- package/dist/performance-monitor-C8dhWJWR.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
|
|
19
|
+
alt="The HUD in full mode, dark theme. Option rows for theme (light, system, dark), dim on leave, explain metrics and a copy-report button; FPS, CPU and GPU sparklines with live values; then one row per metric, each with an icon, its value and a checkbox choosing whether the compact card shows it (FPS, calls, CPU, GPU, 1% low, frame p99, hitches, triangles, lines, points, render passes, geometries, textures, shaders); and 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
|
|
|
@@ -143,6 +143,45 @@ monitor.getEnvironment();
|
|
|
143
143
|
WebGL's unmasked renderer string, and is `null` where the browser hides it. Chrome's WebGPU gives
|
|
144
144
|
vendor and architecture, not the model.
|
|
145
145
|
|
|
146
|
+
## Reading the panel
|
|
147
|
+
|
|
148
|
+
Every row has an icon, its value, and a checkbox on the right that puts it in the compact HUD. Hover
|
|
149
|
+
a label for a one-line explanation of the metric and what a bad reading means, or tick **explain
|
|
150
|
+
metrics** to show them all under the rows (also works on touch).
|
|
151
|
+
|
|
152
|
+
## Stutter
|
|
153
|
+
|
|
154
|
+
Average FPS hides a hitch every few seconds. Three rows in the full view catch it, and any of them
|
|
155
|
+
can be ticked into the compact HUD:
|
|
156
|
+
|
|
157
|
+
- **1% low**: mean FPS across the slowest 1% of frames.
|
|
158
|
+
- **Frame p99**: 99% of frames finish within this many ms.
|
|
159
|
+
- **Hitches**: frames over twice the median frame time.
|
|
160
|
+
|
|
161
|
+
They cover the last `frameStatsSize` frames (1000 by default, about 16 s at 60 Hz). Gaps over a
|
|
162
|
+
second, like a hidden tab, are left out. Read them with `monitor.getFrameStats()`:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
monitor.getFrameStats();
|
|
166
|
+
// { frames: 1000, lowFps: 97.6, p99Ms: 10.2, hitches: 3 }
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Bug reports
|
|
170
|
+
|
|
171
|
+
The **report** row's copy button puts a Markdown snapshot on the clipboard: versions, backend, GPU,
|
|
172
|
+
the current metrics, the stutter stats, the viewport and the user agent. Paste it into an issue.
|
|
173
|
+
`formatReport(monitor)` from `@zkmake/three-meter/ui` returns the same text.
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
three-meter v0.5.0 · three r186 · WebGL2 · Apple M4 Max
|
|
177
|
+
FPS 120 · 1% low 98 · p99 10.2 ms · hitches 3 / 1,000 frames
|
|
178
|
+
CPU 0.2 ms · GPU 0.7 ms
|
|
179
|
+
Calls 1 · passes 1 · triangles 24,000 · lines 0 · points 0
|
|
180
|
+
Geometries 1 · textures 1 · shaders 1
|
|
181
|
+
Viewport 1280×720 @2x
|
|
182
|
+
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) …
|
|
183
|
+
```
|
|
184
|
+
|
|
146
185
|
## Examples
|
|
147
186
|
|
|
148
187
|
[`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 Environment, type GpuTiming, type PerfRenderer, PerformanceMonitor, type PerformanceMonitorOptions, type RenderBackend, 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:
|
|
@@ -453,9 +453,26 @@ const dockPanel = (element, options = {}) => {
|
|
|
453
453
|
//#endregion
|
|
454
454
|
//#region src/ui/icons.ts
|
|
455
455
|
const ICON_MARKUP = {
|
|
456
|
+
activity: "<path d=\"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2\"/>",
|
|
457
|
+
box: "<path d=\"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z\"/><path d=\"m3.3 7 8.7 5 8.7-5\"/><path d=\"M12 22V12\"/>",
|
|
458
|
+
circleDot: "<circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"1\"/>",
|
|
459
|
+
cpu: "<rect width=\"16\" height=\"16\" x=\"4\" y=\"4\" rx=\"2\"/><rect width=\"6\" height=\"6\" x=\"9\" y=\"9\" rx=\"1\"/><path d=\"M15 2v2\"/><path d=\"M15 20v2\"/><path d=\"M2 15h2\"/><path d=\"M2 9h2\"/><path d=\"M20 15h2\"/><path d=\"M20 9h2\"/><path d=\"M9 2v2\"/><path d=\"M9 20v2\"/>",
|
|
460
|
+
gauge: "<path d=\"m12 14 4-4\"/><path d=\"M3.34 19a10 10 0 1 1 17.32 0\"/>",
|
|
461
|
+
image: "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" ry=\"2\"/><circle cx=\"9\" cy=\"9\" r=\"2\"/><path d=\"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21\"/>",
|
|
462
|
+
info: "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/>",
|
|
463
|
+
layers: "<path d=\"m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z\"/><path d=\"m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65\"/><path d=\"m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65\"/>",
|
|
464
|
+
palette: "<circle cx=\"13.5\" cy=\"6.5\" r=\".5\" fill=\"currentColor\"/><circle cx=\"17.5\" cy=\"10.5\" r=\".5\" fill=\"currentColor\"/><circle cx=\"8.5\" cy=\"7.5\" r=\".5\" fill=\"currentColor\"/><circle cx=\"6.5\" cy=\"12.5\" r=\".5\" fill=\"currentColor\"/><path d=\"M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z\"/>",
|
|
465
|
+
repeat: "<path d=\"m17 2 4 4-4 4\"/><path d=\"M3 11v-1a4 4 0 0 1 4-4h14\"/><path d=\"m7 22-4-4 4-4\"/><path d=\"M21 13v1a4 4 0 0 1-4 4H3\"/>",
|
|
466
|
+
spline: "<circle cx=\"19\" cy=\"5\" r=\"2\"/><circle cx=\"5\" cy=\"19\" r=\"2\"/><path d=\"M5 17A12 12 0 0 1 17 5\"/>",
|
|
467
|
+
timer: "<line x1=\"10\" x2=\"14\" y1=\"2\" y2=\"2\"/><line x1=\"12\" x2=\"15\" y1=\"14\" y2=\"11\"/><circle cx=\"12\" cy=\"14\" r=\"8\"/>",
|
|
468
|
+
trendingDown: "<polyline points=\"22 17 13.5 8.5 8.5 13.5 2 7\"/><polyline points=\"16 17 22 17 22 11\"/>",
|
|
469
|
+
triangle: "<path d=\"M13.73 4a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z\"/>",
|
|
470
|
+
zap: "<path d=\"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z\"/>",
|
|
456
471
|
blend: "<circle cx=\"9\" cy=\"9\" r=\"7\"/><circle cx=\"15\" cy=\"15\" r=\"7\"/>",
|
|
472
|
+
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\"/>",
|
|
457
473
|
contrast: "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 18a6 6 0 0 0 0-12v12z\"/>",
|
|
458
474
|
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\"/>",
|
|
475
|
+
help: "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\"/><path d=\"M12 17h.01\"/>",
|
|
459
476
|
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\"/>",
|
|
460
477
|
moon: "<path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\"/>",
|
|
461
478
|
sun: "<circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2\"/><path d=\"M12 20v2\"/><path d=\"m4.93 4.93 1.41 1.41\"/><path d=\"m17.66 17.66 1.41 1.41\"/><path d=\"M2 12h2\"/><path d=\"M20 12h2\"/><path d=\"m6.34 17.66-1.41 1.41\"/><path d=\"m19.07 4.93-1.41 1.41\"/>",
|
|
@@ -476,7 +493,42 @@ const createIcon = (name, className) => {
|
|
|
476
493
|
};
|
|
477
494
|
//#endregion
|
|
478
495
|
//#region package.json
|
|
479
|
-
var version = "0.
|
|
496
|
+
var version = "0.6.0";
|
|
497
|
+
//#endregion
|
|
498
|
+
//#region src/ui/clipboard.ts
|
|
499
|
+
/**
|
|
500
|
+
* Copy text from a click handler. The async Clipboard API needs a secure
|
|
501
|
+
* context and the `clipboard-write` permission; a dev server opened over a LAN
|
|
502
|
+
* IP has neither, and some embeds deny the permission. The fallback is the old
|
|
503
|
+
* select-a-textarea `execCommand("copy")`, still honoured inside a user gesture.
|
|
504
|
+
*/
|
|
505
|
+
const copyWithSelection = (text) => {
|
|
506
|
+
const textarea = document.createElement("textarea");
|
|
507
|
+
textarea.value = text;
|
|
508
|
+
textarea.setAttribute("readonly", "");
|
|
509
|
+
textarea.style.position = "fixed";
|
|
510
|
+
textarea.style.opacity = "0";
|
|
511
|
+
textarea.style.pointerEvents = "none";
|
|
512
|
+
const previous = document.activeElement;
|
|
513
|
+
document.body.append(textarea);
|
|
514
|
+
textarea.select();
|
|
515
|
+
try {
|
|
516
|
+
return document.execCommand("copy");
|
|
517
|
+
} catch {
|
|
518
|
+
return false;
|
|
519
|
+
} finally {
|
|
520
|
+
textarea.remove();
|
|
521
|
+
previous?.focus?.();
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
const copyText = async (text) => {
|
|
525
|
+
try {
|
|
526
|
+
await navigator.clipboard.writeText(text);
|
|
527
|
+
return true;
|
|
528
|
+
} catch {
|
|
529
|
+
return copyWithSelection(text);
|
|
530
|
+
}
|
|
531
|
+
};
|
|
480
532
|
//#endregion
|
|
481
533
|
//#region src/ui/format.ts
|
|
482
534
|
/**
|
|
@@ -493,6 +545,99 @@ const compact = new Intl.NumberFormat("en-US", {
|
|
|
493
545
|
});
|
|
494
546
|
const formatCount = (value) => Math.abs(value) < COMPACT_FROM ? exact.format(value) : compact.format(value);
|
|
495
547
|
//#endregion
|
|
548
|
+
//#region src/ui/metric-help.ts
|
|
549
|
+
/**
|
|
550
|
+
* One plain-language line per metric: what it is, and what a bad reading
|
|
551
|
+
* means. Shown as the label's tooltip and, with "explain metrics" on, under
|
|
552
|
+
* each row. Keyed like the stat rows; the graphs reuse `fps`, `cpu`, `gpu`.
|
|
553
|
+
*/
|
|
554
|
+
const METRIC_HELP = {
|
|
555
|
+
calls: "Separate draw commands sent to the GPU this frame. Fewer is better: merge or instance meshes that share a material.",
|
|
556
|
+
cpu: "JavaScript time per frame: your update code plus three.js preparing the render. It has to fit the frame budget (16.7 ms at 60 FPS).",
|
|
557
|
+
fps: "Frames drawn per second. Higher is smoother; most displays top out at 60 or 120.",
|
|
558
|
+
geometries: "Geometries held in GPU memory. Should level off; a steady climb is a leak, so dispose() what you remove.",
|
|
559
|
+
gpu: "Time the graphics card spent drawing the frame. High means shaders, overdraw or resolution are the bottleneck.",
|
|
560
|
+
hitches: "Frames that took over twice as long as usual, in the last 1,000 frames. Each one is a visible stutter.",
|
|
561
|
+
lines: "Line segments drawn this frame.",
|
|
562
|
+
low: "Frame rate during the slowest 1% of frames. Far below FPS means stutter the average hides.",
|
|
563
|
+
p99: "99% of frames finished within this many ms. Above the frame budget (16.7 ms at 60 FPS) means dropped frames.",
|
|
564
|
+
passes: "Times renderer.render() ran this frame. Shadows, post-processing and extra cameras each add passes.",
|
|
565
|
+
points: "Points drawn this frame.",
|
|
566
|
+
shaders: "Compiled shader programs. A jump mid-session usually means a stutter while a new material compiled.",
|
|
567
|
+
textures: "Textures held in GPU memory. Should level off; a steady climb is a leak, so dispose() what you remove.",
|
|
568
|
+
triangles: "Triangles drawn this frame. Mostly GPU cost: use lower-poly models or level of detail to cut it."
|
|
569
|
+
};
|
|
570
|
+
//#endregion
|
|
571
|
+
//#region src/ui/report.ts
|
|
572
|
+
/**
|
|
573
|
+
* A plain-text snapshot of the monitor for bug reports: versions, backend and
|
|
574
|
+
* GPU, the current sample, stutter stats and the page. Fenced as a `text`
|
|
575
|
+
* code block so GitHub and Slack keep the line breaks.
|
|
576
|
+
*/
|
|
577
|
+
const BACKEND_LABELS = {
|
|
578
|
+
webgl: "WebGL",
|
|
579
|
+
webgl2: "WebGL2",
|
|
580
|
+
webgpu: "WebGPU"
|
|
581
|
+
};
|
|
582
|
+
/** `WebGPU`, `WebGL2`, `WebGL2 fallback`, or `""` before the backend settles. */
|
|
583
|
+
const backendLabel = (environment) => {
|
|
584
|
+
if (!environment.backend) return "";
|
|
585
|
+
const label = BACKEND_LABELS[environment.backend];
|
|
586
|
+
return environment.fallback ? `${label} fallback` : label;
|
|
587
|
+
};
|
|
588
|
+
const ms = (value) => `${value.toFixed(1)} ms`;
|
|
589
|
+
const renderReport = ({ environment, page, sample, stats }) => {
|
|
590
|
+
const { render, resources } = sample;
|
|
591
|
+
const heading = [
|
|
592
|
+
`three-meter v${version}`,
|
|
593
|
+
environment.three ? `three r${environment.three}` : "",
|
|
594
|
+
backendLabel(environment),
|
|
595
|
+
environment.gpu ?? ""
|
|
596
|
+
].filter(Boolean);
|
|
597
|
+
const frames = stats.frames === 0 ? `FPS ${Math.round(sample.fps)}` : [
|
|
598
|
+
`FPS ${Math.round(sample.fps)}`,
|
|
599
|
+
`1% low ${Math.round(stats.lowFps)}`,
|
|
600
|
+
`p99 ${ms(stats.p99Ms)}`,
|
|
601
|
+
`hitches ${formatCount(stats.hitches)} / ${formatCount(stats.frames)} frames`
|
|
602
|
+
].join(" · ");
|
|
603
|
+
const lines = [
|
|
604
|
+
heading.join(" · "),
|
|
605
|
+
frames,
|
|
606
|
+
`CPU ${ms(sample.cpu)} · GPU ${sample.gpu.available ? ms(sample.gpu.ms) : "unavailable"}`,
|
|
607
|
+
[
|
|
608
|
+
`Calls ${formatCount(render.calls)}`,
|
|
609
|
+
`passes ${formatCount(render.passes)}`,
|
|
610
|
+
`triangles ${formatCount(render.triangles)}`,
|
|
611
|
+
`lines ${formatCount(render.lines)}`,
|
|
612
|
+
`points ${formatCount(render.points)}`
|
|
613
|
+
].join(" · "),
|
|
614
|
+
[
|
|
615
|
+
`Geometries ${formatCount(resources.geometries)}`,
|
|
616
|
+
`textures ${formatCount(resources.textures)}`,
|
|
617
|
+
`shaders ${formatCount(resources.programs)}`
|
|
618
|
+
].join(" · ")
|
|
619
|
+
];
|
|
620
|
+
if (page) lines.push(`Viewport ${page.width}×${page.height} @${page.dpr}x`, page.userAgent);
|
|
621
|
+
return [
|
|
622
|
+
"```text",
|
|
623
|
+
...lines,
|
|
624
|
+
"```"
|
|
625
|
+
].join("\n");
|
|
626
|
+
};
|
|
627
|
+
const readPage = () => typeof window === "undefined" ? null : {
|
|
628
|
+
dpr: window.devicePixelRatio || 1,
|
|
629
|
+
height: window.innerHeight,
|
|
630
|
+
userAgent: navigator.userAgent,
|
|
631
|
+
width: window.innerWidth
|
|
632
|
+
};
|
|
633
|
+
/** The report for `monitor` right now, as Markdown ready to paste into an issue. */
|
|
634
|
+
const formatReport = (monitor) => renderReport({
|
|
635
|
+
environment: monitor.getEnvironment(),
|
|
636
|
+
page: readPage(),
|
|
637
|
+
sample: monitor.getSample(),
|
|
638
|
+
stats: monitor.getFrameStats()
|
|
639
|
+
});
|
|
640
|
+
//#endregion
|
|
496
641
|
//#region src/ui/sparkline.ts
|
|
497
642
|
function drawSparkline(ctx, width, height, values, style) {
|
|
498
643
|
ctx.clearRect(0, 0, width, height);
|
|
@@ -519,6 +664,7 @@ function drawSparkline(ctx, width, height, values, style) {
|
|
|
519
664
|
const TIMINGS = [
|
|
520
665
|
{
|
|
521
666
|
format: (value) => Math.round(value).toString(),
|
|
667
|
+
icon: "gauge",
|
|
522
668
|
label: "FPS",
|
|
523
669
|
metric: "fps",
|
|
524
670
|
style: {
|
|
@@ -535,6 +681,7 @@ const TIMINGS = [
|
|
|
535
681
|
},
|
|
536
682
|
{
|
|
537
683
|
format: (value) => value.toFixed(1),
|
|
684
|
+
icon: "cpu",
|
|
538
685
|
label: "CPU",
|
|
539
686
|
metric: "cpu",
|
|
540
687
|
style: {
|
|
@@ -551,6 +698,7 @@ const TIMINGS = [
|
|
|
551
698
|
},
|
|
552
699
|
{
|
|
553
700
|
format: (value) => value.toFixed(1),
|
|
701
|
+
icon: "zap",
|
|
554
702
|
label: "GPU",
|
|
555
703
|
metric: "gpu",
|
|
556
704
|
style: {
|
|
@@ -584,16 +732,8 @@ const THEME_OPTIONS = [
|
|
|
584
732
|
}
|
|
585
733
|
];
|
|
586
734
|
const RELEASE_URL = `https://github.com/zkmake/three-meter/releases/tag/v${version}`;
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
webgl2: "WebGL2",
|
|
590
|
-
webgpu: "WebGPU"
|
|
591
|
-
};
|
|
592
|
-
const backendLabel = (environment) => {
|
|
593
|
-
if (!environment.backend) return "";
|
|
594
|
-
const label = BACKEND_LABELS[environment.backend];
|
|
595
|
-
return environment.fallback ? `${label} fallback` : label;
|
|
596
|
-
};
|
|
735
|
+
/** How long the report button says `copied` / `failed` before reading `copy` again. */
|
|
736
|
+
const COPY_FEEDBACK_MS = 1500;
|
|
597
737
|
const readTiming = (sample, metric) => {
|
|
598
738
|
switch (metric) {
|
|
599
739
|
case "fps": return sample.fps;
|
|
@@ -604,56 +744,85 @@ const readTiming = (sample, metric) => {
|
|
|
604
744
|
const isTimingAvailable = (sample, metric) => metric !== "gpu" || sample.gpu.available;
|
|
605
745
|
const NUMBERS = [
|
|
606
746
|
{
|
|
747
|
+
icon: "gauge",
|
|
607
748
|
key: "fps",
|
|
608
749
|
label: "FPS",
|
|
609
750
|
read: (sample) => Math.round(sample.fps).toString()
|
|
610
751
|
},
|
|
611
752
|
{
|
|
753
|
+
icon: "layers",
|
|
612
754
|
key: "calls",
|
|
613
755
|
label: "Calls",
|
|
614
756
|
read: (sample) => formatCount(sample.render.calls)
|
|
615
757
|
},
|
|
616
758
|
{
|
|
759
|
+
icon: "cpu",
|
|
617
760
|
key: "cpu",
|
|
618
761
|
label: "CPU",
|
|
619
762
|
read: (sample) => sample.cpu.toFixed(1)
|
|
620
763
|
},
|
|
621
764
|
{
|
|
765
|
+
icon: "zap",
|
|
622
766
|
key: "gpu",
|
|
623
767
|
label: "GPU",
|
|
624
768
|
read: (sample) => sample.gpu.available ? sample.gpu.ms.toFixed(1) : "—"
|
|
625
769
|
},
|
|
626
770
|
{
|
|
771
|
+
icon: "trendingDown",
|
|
772
|
+
key: "low",
|
|
773
|
+
label: "1% low",
|
|
774
|
+
read: (_sample, stats) => stats.frames ? Math.round(stats.lowFps).toString() : "—"
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
icon: "timer",
|
|
778
|
+
key: "p99",
|
|
779
|
+
label: "Frame p99",
|
|
780
|
+
read: (_sample, stats) => stats.frames ? stats.p99Ms.toFixed(1) : "—"
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
icon: "activity",
|
|
784
|
+
key: "hitches",
|
|
785
|
+
label: "Hitches",
|
|
786
|
+
read: (_sample, stats) => stats.frames ? formatCount(stats.hitches) : "—"
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
icon: "triangle",
|
|
627
790
|
key: "triangles",
|
|
628
791
|
label: "Triangles",
|
|
629
792
|
read: (sample) => formatCount(sample.render.triangles)
|
|
630
793
|
},
|
|
631
794
|
{
|
|
795
|
+
icon: "spline",
|
|
632
796
|
key: "lines",
|
|
633
797
|
label: "Lines",
|
|
634
798
|
read: (sample) => formatCount(sample.render.lines)
|
|
635
799
|
},
|
|
636
800
|
{
|
|
801
|
+
icon: "circleDot",
|
|
637
802
|
key: "points",
|
|
638
803
|
label: "Points",
|
|
639
804
|
read: (sample) => formatCount(sample.render.points)
|
|
640
805
|
},
|
|
641
806
|
{
|
|
807
|
+
icon: "repeat",
|
|
642
808
|
key: "passes",
|
|
643
809
|
label: "Render passes",
|
|
644
810
|
read: (sample) => formatCount(sample.render.passes)
|
|
645
811
|
},
|
|
646
812
|
{
|
|
813
|
+
icon: "box",
|
|
647
814
|
key: "geometries",
|
|
648
815
|
label: "Geometries",
|
|
649
816
|
read: (sample) => formatCount(sample.resources.geometries)
|
|
650
817
|
},
|
|
651
818
|
{
|
|
819
|
+
icon: "image",
|
|
652
820
|
key: "textures",
|
|
653
821
|
label: "Textures",
|
|
654
822
|
read: (sample) => formatCount(sample.resources.textures)
|
|
655
823
|
},
|
|
656
824
|
{
|
|
825
|
+
icon: "palette",
|
|
657
826
|
key: "shaders",
|
|
658
827
|
label: "Shaders",
|
|
659
828
|
read: (sample) => formatCount(sample.resources.programs)
|
|
@@ -760,7 +929,8 @@ var PerformanceView = class {
|
|
|
760
929
|
const graph = this.graphCanvases.get(config.metric);
|
|
761
930
|
drawSparkline(graph.ctx, graph.width, graph.height, available ? this.monitor.getHistory(config.metric) : [], config.style[this.theme.resolved]);
|
|
762
931
|
}
|
|
763
|
-
|
|
932
|
+
const stats = this.monitor.getFrameStats();
|
|
933
|
+
for (const config of NUMBERS) this.statValueEls.get(config.key).textContent = config.read(sample, stats);
|
|
764
934
|
this.renderFooter();
|
|
765
935
|
}
|
|
766
936
|
/** The monitor caches the environment once its backend settles; until then this re-reads. */
|
|
@@ -782,9 +952,10 @@ var PerformanceView = class {
|
|
|
782
952
|
this.footerHardwareEl.hidden = backend === "" && gpu === "";
|
|
783
953
|
}
|
|
784
954
|
renderHud(sample) {
|
|
955
|
+
const stats = this.hudNumberEls.size > 0 ? this.monitor.getFrameStats() : null;
|
|
785
956
|
for (const [key, element] of this.hudNumberEls) {
|
|
786
957
|
const config = NUMBERS.find((number) => number.key === key);
|
|
787
|
-
if (config) element.textContent = config.read(sample);
|
|
958
|
+
if (config && stats) element.textContent = config.read(sample, stats);
|
|
788
959
|
}
|
|
789
960
|
for (const [metric, graph] of this.hudGraphs) {
|
|
790
961
|
const config = TIMINGS.find((timing) => timing.metric === metric);
|
|
@@ -836,7 +1007,14 @@ var PerformanceView = class {
|
|
|
836
1007
|
dimLabel.className = "perf-monitor__label";
|
|
837
1008
|
dimLabel.textContent = "dim on leave";
|
|
838
1009
|
dimRow.append(createIcon("blend", "perf-monitor__icon"), dimLabel, this.dimCheckbox);
|
|
839
|
-
|
|
1010
|
+
const explainRow = document.createElement("label");
|
|
1011
|
+
explainRow.className = "perf-monitor__row";
|
|
1012
|
+
const explainCheckbox = this.buildCheckbox(false, "Explain each metric", (on) => this.element.classList.toggle("perf-monitor--explain", on));
|
|
1013
|
+
const explainLabel = document.createElement("span");
|
|
1014
|
+
explainLabel.className = "perf-monitor__label";
|
|
1015
|
+
explainLabel.textContent = "explain metrics";
|
|
1016
|
+
explainRow.append(createIcon("help", "perf-monitor__icon"), explainLabel, explainCheckbox);
|
|
1017
|
+
options.append(themeRow, dimRow, explainRow, this.buildReportRow());
|
|
840
1018
|
const graphs = document.createElement("div");
|
|
841
1019
|
graphs.className = "perf-monitor__graphs";
|
|
842
1020
|
for (const config of TIMINGS) {
|
|
@@ -844,21 +1022,25 @@ var PerformanceView = class {
|
|
|
844
1022
|
graph.className = "perf-monitor__graph";
|
|
845
1023
|
const canvas = document.createElement("canvas");
|
|
846
1024
|
canvas.className = "perf-monitor__canvas";
|
|
847
|
-
const overlay = document.createElement("
|
|
848
|
-
overlay.className = "perf-monitor__graph-overlay";
|
|
849
|
-
const head = document.createElement("
|
|
1025
|
+
const overlay = document.createElement("label");
|
|
1026
|
+
overlay.className = "perf-monitor__graph-overlay perf-monitor__graph-overlay--toggle";
|
|
1027
|
+
const head = document.createElement("span");
|
|
850
1028
|
head.className = "perf-monitor__graph-head";
|
|
851
1029
|
const checkbox = this.buildCheckbox(this.settings.isGraphEnabled(config.metric), `Show ${config.label} graph in HUD`, (on) => this.settings.setGraphEnabled(config.metric, on));
|
|
852
1030
|
this.graphCheckboxes.set(config.metric, checkbox);
|
|
853
1031
|
const label = document.createElement("span");
|
|
854
1032
|
label.className = "perf-monitor__graph-label";
|
|
855
1033
|
label.textContent = config.label;
|
|
1034
|
+
label.title = METRIC_HELP[config.metric] ?? "";
|
|
856
1035
|
const value = document.createElement("span");
|
|
857
1036
|
value.className = "perf-monitor__graph-value";
|
|
858
1037
|
value.textContent = "—";
|
|
859
1038
|
this.graphValueEls.set(config.metric, value);
|
|
860
|
-
|
|
861
|
-
|
|
1039
|
+
const tail = document.createElement("span");
|
|
1040
|
+
tail.className = "perf-monitor__graph-head";
|
|
1041
|
+
tail.append(value, checkbox);
|
|
1042
|
+
head.append(createIcon(config.icon, "perf-monitor__icon"), label);
|
|
1043
|
+
overlay.append(head, tail);
|
|
862
1044
|
graph.append(canvas, overlay);
|
|
863
1045
|
graphs.append(graph);
|
|
864
1046
|
const ctx = canvas.getContext("2d");
|
|
@@ -879,14 +1061,19 @@ var PerformanceView = class {
|
|
|
879
1061
|
rowEl.className = "perf-monitor__row";
|
|
880
1062
|
const checkbox = this.buildCheckbox(this.settings.isNumberEnabled(config.key), `Show ${config.label} in HUD`, (on) => this.settings.setNumberEnabled(config.key, on));
|
|
881
1063
|
this.statCheckboxes.set(config.key, checkbox);
|
|
1064
|
+
const help = METRIC_HELP[config.key] ?? "";
|
|
882
1065
|
const label = document.createElement("span");
|
|
883
1066
|
label.className = "perf-monitor__label";
|
|
884
1067
|
label.textContent = config.label;
|
|
1068
|
+
label.title = help;
|
|
885
1069
|
const value = document.createElement("span");
|
|
886
1070
|
value.className = "perf-monitor__value";
|
|
887
1071
|
value.textContent = "—";
|
|
888
1072
|
this.statValueEls.set(config.key, value);
|
|
889
|
-
|
|
1073
|
+
const hint = document.createElement("span");
|
|
1074
|
+
hint.className = "perf-monitor__hint";
|
|
1075
|
+
hint.textContent = help;
|
|
1076
|
+
rowEl.append(createIcon(config.icon, "perf-monitor__icon"), label, value, checkbox, hint);
|
|
890
1077
|
stats.append(rowEl);
|
|
891
1078
|
}
|
|
892
1079
|
const footer = document.createElement("div");
|
|
@@ -914,7 +1101,7 @@ var PerformanceView = class {
|
|
|
914
1101
|
this.footerGpuEl.className = "perf-monitor__footer-gpu";
|
|
915
1102
|
this.footerHardwareEl.append(this.footerBackendEl, this.footerGpuEl);
|
|
916
1103
|
footerBody.append(softwareRow, this.footerHardwareEl);
|
|
917
|
-
footer.append(this.infoCheckbox
|
|
1104
|
+
footer.append(createIcon("info", "perf-monitor__icon"), footerBody, this.infoCheckbox);
|
|
918
1105
|
this.element.append(hud, options, graphs, stats, footer);
|
|
919
1106
|
}
|
|
920
1107
|
rebuildHud() {
|
|
@@ -935,6 +1122,7 @@ var PerformanceView = class {
|
|
|
935
1122
|
const label = document.createElement("span");
|
|
936
1123
|
label.className = "perf-monitor__graph-label";
|
|
937
1124
|
label.textContent = config.label;
|
|
1125
|
+
label.title = METRIC_HELP[config.metric] ?? "";
|
|
938
1126
|
const value = document.createElement("span");
|
|
939
1127
|
value.className = "perf-monitor__graph-value";
|
|
940
1128
|
value.textContent = "—";
|
|
@@ -960,6 +1148,7 @@ var PerformanceView = class {
|
|
|
960
1148
|
const label = document.createElement("span");
|
|
961
1149
|
label.className = "perf-monitor__hud-label";
|
|
962
1150
|
label.textContent = config.label;
|
|
1151
|
+
label.title = METRIC_HELP[config.key] ?? "";
|
|
963
1152
|
const value = document.createElement("span");
|
|
964
1153
|
value.className = "perf-monitor__hud-value";
|
|
965
1154
|
value.textContent = "—";
|
|
@@ -983,6 +1172,31 @@ var PerformanceView = class {
|
|
|
983
1172
|
applyTheme(this.element, this.theme);
|
|
984
1173
|
for (const [mode, radio] of this.themeRadios) radio.setAttribute("aria-checked", String(mode === this.theme.effective));
|
|
985
1174
|
}
|
|
1175
|
+
buildReportRow() {
|
|
1176
|
+
const row = document.createElement("div");
|
|
1177
|
+
row.className = "perf-monitor__row perf-monitor__row--static";
|
|
1178
|
+
const label = document.createElement("span");
|
|
1179
|
+
label.className = "perf-monitor__label";
|
|
1180
|
+
label.textContent = "report";
|
|
1181
|
+
const button = document.createElement("button");
|
|
1182
|
+
button.type = "button";
|
|
1183
|
+
button.className = "perf-monitor__button";
|
|
1184
|
+
button.title = "Copy versions, GPU and current metrics as Markdown for a bug report";
|
|
1185
|
+
button.textContent = "copy";
|
|
1186
|
+
let resetTimer = 0;
|
|
1187
|
+
const flash = (text) => {
|
|
1188
|
+
button.textContent = text;
|
|
1189
|
+
window.clearTimeout(resetTimer);
|
|
1190
|
+
resetTimer = window.setTimeout(() => {
|
|
1191
|
+
button.textContent = "copy";
|
|
1192
|
+
}, COPY_FEEDBACK_MS);
|
|
1193
|
+
};
|
|
1194
|
+
button.addEventListener("click", () => {
|
|
1195
|
+
copyText(formatReport(this.monitor)).then((copied) => flash(copied ? "copied" : "failed"));
|
|
1196
|
+
});
|
|
1197
|
+
row.append(createIcon("clipboard", "perf-monitor__icon"), label, button);
|
|
1198
|
+
return row;
|
|
1199
|
+
}
|
|
986
1200
|
buildCheckbox(checked, title, onChange) {
|
|
987
1201
|
const checkbox = document.createElement("input");
|
|
988
1202
|
checkbox.type = "checkbox";
|
|
@@ -1109,8 +1323,9 @@ const PERF_HUD_STYLES = `
|
|
|
1109
1323
|
cursor: pointer;
|
|
1110
1324
|
}
|
|
1111
1325
|
|
|
1112
|
-
.perf-monitor__graph-overlay
|
|
1326
|
+
.perf-monitor__graph-overlay--toggle {
|
|
1113
1327
|
pointer-events: auto;
|
|
1328
|
+
cursor: pointer;
|
|
1114
1329
|
}
|
|
1115
1330
|
|
|
1116
1331
|
.perf-monitor__graph-label {
|
|
@@ -1142,6 +1357,25 @@ const PERF_HUD_STYLES = `
|
|
|
1142
1357
|
cursor: pointer;
|
|
1143
1358
|
}
|
|
1144
1359
|
|
|
1360
|
+
/* Hidden until "explain metrics" is on; then the row wraps it under the label. */
|
|
1361
|
+
.perf-monitor__hint {
|
|
1362
|
+
display: none;
|
|
1363
|
+
flex-basis: 100%;
|
|
1364
|
+
padding: 0 0 2px 18px;
|
|
1365
|
+
color: var(--perf-muted);
|
|
1366
|
+
font-size: 10px;
|
|
1367
|
+
line-height: 1.35;
|
|
1368
|
+
opacity: 0.85;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
.perf-monitor--explain .perf-monitor__hint {
|
|
1372
|
+
display: block;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
.perf-monitor--explain .perf-monitor__section .perf-monitor__row {
|
|
1376
|
+
flex-wrap: wrap;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1145
1379
|
.perf-monitor__row:nth-child(even) {
|
|
1146
1380
|
background: var(--perf-row);
|
|
1147
1381
|
}
|
|
@@ -1211,6 +1445,27 @@ const PERF_HUD_STYLES = `
|
|
|
1211
1445
|
color: inherit;
|
|
1212
1446
|
}
|
|
1213
1447
|
|
|
1448
|
+
.perf-monitor__button {
|
|
1449
|
+
flex: none;
|
|
1450
|
+
min-width: 7ch;
|
|
1451
|
+
padding: 0 6px;
|
|
1452
|
+
border: 1px solid var(--perf-border);
|
|
1453
|
+
border-radius: 3px;
|
|
1454
|
+
background: var(--perf-row);
|
|
1455
|
+
color: var(--perf-fg-dim);
|
|
1456
|
+
font: inherit;
|
|
1457
|
+
cursor: pointer;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
.perf-monitor__button:hover {
|
|
1461
|
+
color: var(--perf-fg);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
.perf-monitor__button:focus-visible {
|
|
1465
|
+
outline: 1px solid color-mix(in srgb, var(--perf-accent) 60%, transparent);
|
|
1466
|
+
outline-offset: 1px;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1214
1469
|
.perf-monitor__checkbox {
|
|
1215
1470
|
flex: none;
|
|
1216
1471
|
margin: 0;
|
|
@@ -1353,7 +1608,8 @@ const PERF_HUD_STYLES = `
|
|
|
1353
1608
|
.perf-monitor--compact .perf-monitor__graphs,
|
|
1354
1609
|
.perf-monitor--compact .perf-monitor__section,
|
|
1355
1610
|
.perf-monitor--compact:not(.perf-monitor--info) .perf-monitor__footer,
|
|
1356
|
-
.perf-monitor--compact .perf-monitor__footer > .perf-monitor__checkbox
|
|
1611
|
+
.perf-monitor--compact .perf-monitor__footer > .perf-monitor__checkbox,
|
|
1612
|
+
.perf-monitor--compact .perf-monitor__footer > .perf-monitor__icon {
|
|
1357
1613
|
display: none;
|
|
1358
1614
|
}
|
|
1359
1615
|
|
|
@@ -1384,6 +1640,11 @@ const PERF_HUD_STYLES = `
|
|
|
1384
1640
|
max-width: 100%;
|
|
1385
1641
|
}
|
|
1386
1642
|
|
|
1643
|
+
/* Room for the hints, so most fit on two or three lines. Plus padding, this fills the host's 20rem cap. */
|
|
1644
|
+
.perf-hud--full > .perf-monitor--explain {
|
|
1645
|
+
width: 19rem;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1387
1648
|
.perf-hud > .perf-monitor {
|
|
1388
1649
|
position: relative;
|
|
1389
1650
|
z-index: 1;
|
|
@@ -1605,6 +1866,6 @@ const mountPerfHud = (monitor, options = {}) => {
|
|
|
1605
1866
|
};
|
|
1606
1867
|
};
|
|
1607
1868
|
//#endregion
|
|
1608
|
-
export {
|
|
1869
|
+
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 };
|
|
1609
1870
|
|
|
1610
|
-
//# sourceMappingURL=mount-perf-hud-
|
|
1871
|
+
//# sourceMappingURL=mount-perf-hud-C5wdjgmM.js.map
|