@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 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
- 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
 
@@ -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 RenderCounts, c as TimingMetric, i as PerformanceMonitorOptions, n as GpuTiming, o as ResourceCounts, r as PerfRenderer, s as Sample, t as PerformanceMonitor } from "./performance-monitor-CdaU8Dsv.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 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-D_v7W312.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:
@@ -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
- for (const config of NUMBERS) this.statValueEls.get(config.key).textContent = config.read(sample);
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
- this.element.append(hud, options, graphs, stats);
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 { 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 };
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-CX9DrxjA.js.map
1771
+ //# sourceMappingURL=mount-perf-hud-BLs1jkwM.js.map