@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 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. 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
- and `historySize` (default 120).
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 PerformanceMonitorOptions, c as ResourceCounts, i as PerfRenderer, l as Sample, n as Environment, o as RenderBackend, r as GpuTiming, s as RenderCounts, t as PerformanceMonitor, u as TimingMetric } from "./performance-monitor-Br7x3cPt.js";
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-C8dhWJWR.js";
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.4.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
- const BACKEND_LABELS = {
588
- webgl: "WebGL",
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
- for (const config of NUMBERS) this.statValueEls.get(config.key).textContent = config.read(sample);
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
- options.append(themeRow, dimRow);
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("div");
848
- overlay.className = "perf-monitor__graph-overlay";
849
- const head = document.createElement("label");
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
- head.append(checkbox, label);
861
- overlay.append(head, value);
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
- rowEl.append(checkbox, label, value);
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, footerBody);
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 .perf-monitor__graph-head {
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 { dockPanel as a, HudTheme as c, PerformanceView as i, THEME_MODES as l, PERF_HUD_STYLES as n, DEFAULT_STORAGE_KEY as o, injectStyles as r, HudSettings as s, mountPerfHud as t, isThemeMode as u };
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-Rf98QNxM.js.map
1871
+ //# sourceMappingURL=mount-perf-hud-C5wdjgmM.js.map