@zkmake/three-meter 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,36 @@
1
+ //#region src/ui/budgets.ts
2
+ const DEFAULT_TARGET_FPS = 60;
3
+ const FLOORS = /* @__PURE__ */ new Set(["fps", "low"]);
4
+ const resolveBudgets = (budgets) => {
5
+ const targetFps = budgets && budgets.targetFps || DEFAULT_TARGET_FPS;
6
+ const frameMs = 1e3 / targetFps;
7
+ if (budgets === false) return {
8
+ frameMs,
9
+ limits: {}
10
+ };
11
+ const merged = {
12
+ cpu: frameMs,
13
+ fps: targetFps * .95,
14
+ gpu: frameMs,
15
+ low: targetFps / 2,
16
+ p99: frameMs * 1.5,
17
+ ...budgets
18
+ };
19
+ const limits = {};
20
+ for (const [key, limit] of Object.entries(merged)) if (key !== "targetFps" && typeof limit === "number" && Number.isFinite(limit)) limits[key] = limit;
21
+ return {
22
+ frameMs,
23
+ limits
24
+ };
25
+ };
26
+ const isFloor = (key) => FLOORS.has(key);
27
+ /** `null` means the value isn't known yet (no frames, no GPU timer), which is never over. */
28
+ const isOverBudget = (resolved, key, value) => {
29
+ const limit = resolved.limits[key];
30
+ if (limit === void 0 || value === null) return false;
31
+ return isFloor(key) ? value < limit : value > limit;
32
+ };
33
+ //#endregion
1
34
  //#region src/ui/theme.ts
2
35
  const THEME_MODES = [
3
36
  "dark",
@@ -453,10 +486,26 @@ const dockPanel = (element, options = {}) => {
453
486
  //#endregion
454
487
  //#region src/ui/icons.ts
455
488
  const ICON_MARKUP = {
489
+ 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\"/>",
490
+ 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\"/>",
491
+ circleDot: "<circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"1\"/>",
492
+ 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\"/>",
493
+ gauge: "<path d=\"m12 14 4-4\"/><path d=\"M3.34 19a10 10 0 1 1 17.32 0\"/>",
494
+ 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\"/>",
495
+ info: "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/>",
496
+ 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\"/>",
497
+ 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\"/>",
498
+ 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\"/>",
499
+ 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\"/>",
500
+ 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\"/>",
501
+ trendingDown: "<polyline points=\"22 17 13.5 8.5 8.5 13.5 2 7\"/><polyline points=\"16 17 22 17 22 11\"/>",
502
+ 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\"/>",
503
+ 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
504
  blend: "<circle cx=\"9\" cy=\"9\" r=\"7\"/><circle cx=\"15\" cy=\"15\" r=\"7\"/>",
457
505
  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\"/>",
458
506
  contrast: "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 18a6 6 0 0 0 0-12v12z\"/>",
459
507
  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\"/>",
508
+ 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\"/>",
460
509
  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\"/>",
461
510
  moon: "<path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\"/>",
462
511
  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\"/>",
@@ -477,7 +526,7 @@ const createIcon = (name, className) => {
477
526
  };
478
527
  //#endregion
479
528
  //#region package.json
480
- var version = "0.5.0";
529
+ var version = "0.7.0";
481
530
  //#endregion
482
531
  //#region src/ui/clipboard.ts
483
532
  /**
@@ -529,6 +578,29 @@ const compact = new Intl.NumberFormat("en-US", {
529
578
  });
530
579
  const formatCount = (value) => Math.abs(value) < COMPACT_FROM ? exact.format(value) : compact.format(value);
531
580
  //#endregion
581
+ //#region src/ui/metric-help.ts
582
+ /**
583
+ * One plain-language line per metric: what it is, and what a bad reading
584
+ * means. Shown as the label's tooltip and, with "explain metrics" on, under
585
+ * each row. Keyed like the stat rows; the graphs reuse `fps`, `cpu`, `gpu`.
586
+ */
587
+ const METRIC_HELP = {
588
+ calls: "Separate draw commands sent to the GPU this frame. Fewer is better: merge or instance meshes that share a material.",
589
+ 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).",
590
+ fps: "Frames drawn per second. Higher is smoother; most displays top out at 60 or 120.",
591
+ geometries: "Geometries held in GPU memory. Should level off; a steady climb is a leak, so dispose() what you remove.",
592
+ gpu: "Time the graphics card spent drawing the frame. High means shaders, overdraw or resolution are the bottleneck.",
593
+ hitches: "Frames that took over twice as long as usual, in the last 1,000 frames. Each one is a visible stutter.",
594
+ lines: "Line segments drawn this frame.",
595
+ low: "Frame rate during the slowest 1% of frames. Far below FPS means stutter the average hides.",
596
+ p99: "99% of frames finished within this many ms. Above the frame budget (16.7 ms at 60 FPS) means dropped frames.",
597
+ passes: "Times renderer.render() ran this frame. Shadows, post-processing and extra cameras each add passes.",
598
+ points: "Points drawn this frame.",
599
+ shaders: "Compiled shader programs. A jump mid-session usually means a stutter while a new material compiled.",
600
+ textures: "Textures held in GPU memory. Should level off; a steady climb is a leak, so dispose() what you remove.",
601
+ triangles: "Triangles drawn this frame. Mostly GPU cost: use lower-poly models or level of detail to cut it."
602
+ };
603
+ //#endregion
532
604
  //#region src/ui/report.ts
533
605
  /**
534
606
  * A plain-text snapshot of the monitor for bug reports: versions, backend and
@@ -600,7 +672,7 @@ const formatReport = (monitor) => renderReport({
600
672
  });
601
673
  //#endregion
602
674
  //#region src/ui/sparkline.ts
603
- function drawSparkline(ctx, width, height, values, style) {
675
+ function drawSparkline(ctx, width, height, values, style, guide) {
604
676
  ctx.clearRect(0, 0, width, height);
605
677
  if (values.length < 2) return;
606
678
  let max = 0;
@@ -619,12 +691,24 @@ function drawSparkline(ctx, width, height, values, style) {
619
691
  ctx.closePath();
620
692
  ctx.fillStyle = style.fill;
621
693
  ctx.fill();
694
+ if (guide && guide.value > 0 && guide.value <= range) {
695
+ const y = Math.round(toY(guide.value)) + .5;
696
+ ctx.save();
697
+ ctx.setLineDash([3, 3]);
698
+ ctx.strokeStyle = guide.color;
699
+ ctx.beginPath();
700
+ ctx.moveTo(0, y);
701
+ ctx.lineTo(width, y);
702
+ ctx.stroke();
703
+ ctx.restore();
704
+ }
622
705
  }
623
706
  //#endregion
624
707
  //#region src/ui/performance-view.ts
625
708
  const TIMINGS = [
626
709
  {
627
710
  format: (value) => Math.round(value).toString(),
711
+ icon: "gauge",
628
712
  label: "FPS",
629
713
  metric: "fps",
630
714
  style: {
@@ -641,6 +725,7 @@ const TIMINGS = [
641
725
  },
642
726
  {
643
727
  format: (value) => value.toFixed(1),
728
+ icon: "cpu",
644
729
  label: "CPU",
645
730
  metric: "cpu",
646
731
  style: {
@@ -657,6 +742,7 @@ const TIMINGS = [
657
742
  },
658
743
  {
659
744
  format: (value) => value.toFixed(1),
745
+ icon: "zap",
660
746
  label: "GPU",
661
747
  metric: "gpu",
662
748
  style: {
@@ -700,78 +786,130 @@ const readTiming = (sample, metric) => {
700
786
  }
701
787
  };
702
788
  const isTimingAvailable = (sample, metric) => metric !== "gpu" || sample.gpu.available;
789
+ const whole = (value) => Math.round(value).toString();
790
+ const tenths = (value) => value.toFixed(1);
703
791
  const NUMBERS = [
704
792
  {
793
+ format: whole,
794
+ icon: "gauge",
705
795
  key: "fps",
706
796
  label: "FPS",
707
- read: (sample) => Math.round(sample.fps).toString()
797
+ value: (sample) => sample.fps || null
708
798
  },
709
799
  {
800
+ format: formatCount,
801
+ icon: "layers",
710
802
  key: "calls",
711
803
  label: "Calls",
712
- read: (sample) => formatCount(sample.render.calls)
804
+ value: (sample) => sample.render.calls
713
805
  },
714
806
  {
807
+ format: tenths,
808
+ icon: "cpu",
715
809
  key: "cpu",
716
810
  label: "CPU",
717
- read: (sample) => sample.cpu.toFixed(1)
811
+ unit: "ms",
812
+ value: (sample) => sample.cpu
718
813
  },
719
814
  {
815
+ format: tenths,
816
+ icon: "zap",
720
817
  key: "gpu",
721
818
  label: "GPU",
722
- read: (sample) => sample.gpu.available ? sample.gpu.ms.toFixed(1) : "—"
819
+ unit: "ms",
820
+ value: (sample) => sample.gpu.available ? sample.gpu.ms : null
723
821
  },
724
822
  {
823
+ format: whole,
824
+ icon: "trendingDown",
725
825
  key: "low",
726
826
  label: "1% low",
727
- read: (_sample, stats) => stats.frames ? Math.round(stats.lowFps).toString() : "—"
827
+ value: (_sample, stats) => stats.frames ? stats.lowFps : null
728
828
  },
729
829
  {
830
+ format: tenths,
831
+ icon: "timer",
730
832
  key: "p99",
731
833
  label: "Frame p99",
732
- read: (_sample, stats) => stats.frames ? stats.p99Ms.toFixed(1) : "—"
834
+ unit: "ms",
835
+ value: (_sample, stats) => stats.frames ? stats.p99Ms : null
733
836
  },
734
837
  {
838
+ format: formatCount,
839
+ icon: "activity",
735
840
  key: "hitches",
736
841
  label: "Hitches",
737
- read: (_sample, stats) => stats.frames ? formatCount(stats.hitches) : "—"
842
+ value: (_sample, stats) => stats.frames ? stats.hitches : null
738
843
  },
739
844
  {
845
+ format: formatCount,
846
+ icon: "triangle",
740
847
  key: "triangles",
741
848
  label: "Triangles",
742
- read: (sample) => formatCount(sample.render.triangles)
849
+ value: (sample) => sample.render.triangles
743
850
  },
744
851
  {
852
+ format: formatCount,
853
+ icon: "spline",
745
854
  key: "lines",
746
855
  label: "Lines",
747
- read: (sample) => formatCount(sample.render.lines)
856
+ value: (sample) => sample.render.lines
748
857
  },
749
858
  {
859
+ format: formatCount,
860
+ icon: "circleDot",
750
861
  key: "points",
751
862
  label: "Points",
752
- read: (sample) => formatCount(sample.render.points)
863
+ value: (sample) => sample.render.points
753
864
  },
754
865
  {
866
+ format: formatCount,
867
+ icon: "repeat",
755
868
  key: "passes",
756
869
  label: "Render passes",
757
- read: (sample) => formatCount(sample.render.passes)
870
+ value: (sample) => sample.render.passes
758
871
  },
759
872
  {
873
+ format: formatCount,
874
+ icon: "box",
760
875
  key: "geometries",
761
876
  label: "Geometries",
762
- read: (sample) => formatCount(sample.resources.geometries)
877
+ value: (sample) => sample.resources.geometries
763
878
  },
764
879
  {
880
+ format: formatCount,
881
+ icon: "image",
765
882
  key: "textures",
766
883
  label: "Textures",
767
- read: (sample) => formatCount(sample.resources.textures)
884
+ value: (sample) => sample.resources.textures
768
885
  },
769
886
  {
887
+ format: formatCount,
888
+ icon: "palette",
770
889
  key: "shaders",
771
890
  label: "Shaders",
772
- read: (sample) => formatCount(sample.resources.programs)
891
+ value: (sample) => sample.resources.programs
773
892
  }
774
893
  ];
894
+ const NUMBER_BY_KEY = new Map(NUMBERS.map((config) => [config.key, config]));
895
+ /** Canvas can't read CSS custom properties; the budget line's amber per theme. */
896
+ const GUIDE_COLOR = {
897
+ dark: "rgba(245, 158, 11, 0.75)",
898
+ light: "rgba(180, 83, 9, 0.7)"
899
+ };
900
+ /** `Budget: at most 16.7 ms` / `at least 57`, or `""` without one. */
901
+ const budgetTitle = (budgets, key) => {
902
+ const limit = budgets.limits[key];
903
+ const config = NUMBER_BY_KEY.get(key);
904
+ if (limit === void 0 || !config) return "";
905
+ return `Budget: ${isFloor(key) ? "at least" : "at most"} ${config.format(limit)}${config.unit ? ` ${config.unit}` : ""}`;
906
+ };
907
+ /** The sample value a timing graph shows, `null` when not known. */
908
+ const timingValue = (sample, metric) => {
909
+ if (!isTimingAvailable(sample, metric)) return null;
910
+ const value = readTiming(sample, metric);
911
+ return metric === "fps" && value === 0 ? null : value;
912
+ };
775
913
  /**
776
914
  * Host-agnostic widget that paints {@link Sample}s. Mount {@link element}
777
915
  * anywhere; the view drives its own throttled repaint. Compact is the floating
@@ -804,6 +942,7 @@ var PerformanceView = class {
804
942
  hudGridEl;
805
943
  hudGraphs = /* @__PURE__ */ new Map();
806
944
  hudNumberEls = /* @__PURE__ */ new Map();
945
+ budgets;
807
946
  rafId = null;
808
947
  lastPaintAt = 0;
809
948
  resizeObserver;
@@ -816,6 +955,7 @@ var PerformanceView = class {
816
955
  this.theme.setOverride(this.settings.theme);
817
956
  this.mode = options.mode ?? "full";
818
957
  this.minIntervalMs = 1e3 / (options.refreshHz ?? 10);
958
+ this.budgets = resolveBudgets(options.budgets);
819
959
  this.element = document.createElement("div");
820
960
  this.element.className = "perf-monitor";
821
961
  this.applyModeClass();
@@ -845,6 +985,11 @@ var PerformanceView = class {
845
985
  getMode() {
846
986
  return this.mode;
847
987
  }
988
+ /** Replace the budgets; `false` turns them all off. Applies on the next paint. */
989
+ setBudgets(budgets) {
990
+ this.budgets = resolveBudgets(budgets);
991
+ this.applyBudgetTitles();
992
+ }
848
993
  dispose() {
849
994
  this.stop();
850
995
  this.unsubscribe();
@@ -866,15 +1011,9 @@ var PerformanceView = class {
866
1011
  if (this.settings.info) this.renderFooter();
867
1012
  return;
868
1013
  }
869
- for (const config of TIMINGS) {
870
- const available = isTimingAvailable(sample, config.metric);
871
- const valueEl = this.graphValueEls.get(config.metric);
872
- valueEl.textContent = available ? this.formatTiming(config, readTiming(sample, config.metric)) : "unavailable";
873
- const graph = this.graphCanvases.get(config.metric);
874
- drawSparkline(graph.ctx, graph.width, graph.height, available ? this.monitor.getHistory(config.metric) : [], config.style[this.theme.resolved]);
875
- }
1014
+ for (const config of TIMINGS) this.paintGraph(config, sample, this.graphValueEls.get(config.metric), this.graphCanvases.get(config.metric), "unavailable");
876
1015
  const stats = this.monitor.getFrameStats();
877
- for (const config of NUMBERS) this.statValueEls.get(config.key).textContent = config.read(sample, stats);
1016
+ for (const config of NUMBERS) this.paintNumber(config, this.statValueEls.get(config.key), sample, stats);
878
1017
  this.renderFooter();
879
1018
  }
880
1019
  /** The monitor caches the environment once its backend settles; until then this re-reads. */
@@ -898,16 +1037,38 @@ var PerformanceView = class {
898
1037
  renderHud(sample) {
899
1038
  const stats = this.hudNumberEls.size > 0 ? this.monitor.getFrameStats() : null;
900
1039
  for (const [key, element] of this.hudNumberEls) {
901
- const config = NUMBERS.find((number) => number.key === key);
902
- if (config && stats) element.textContent = config.read(sample, stats);
1040
+ const config = NUMBER_BY_KEY.get(key);
1041
+ if (config && stats) this.paintNumber(config, element, sample, stats);
903
1042
  }
904
1043
  for (const [metric, graph] of this.hudGraphs) {
905
1044
  const config = TIMINGS.find((timing) => timing.metric === metric);
906
- const available = isTimingAvailable(sample, metric);
907
- graph.valueEl.textContent = available ? this.formatTiming(config, readTiming(sample, metric)) : "—";
908
- drawSparkline(graph.ctx, graph.width, graph.height, available ? this.monitor.getHistory(metric) : [], config.style[this.theme.resolved]);
1045
+ this.paintGraph(config, sample, graph.valueEl, graph, "—");
909
1046
  }
910
1047
  }
1048
+ paintNumber(config, element, sample, stats) {
1049
+ const value = config.value(sample, stats);
1050
+ element.textContent = value === null ? "—" : config.format(value);
1051
+ element.classList.toggle("is-over", isOverBudget(this.budgets, config.key, value));
1052
+ }
1053
+ /** Value and sparkline, with the budget line drawn once the series reaches it. */
1054
+ paintGraph(config, sample, valueEl, graph, missing) {
1055
+ const value = timingValue(sample, config.metric);
1056
+ const available = isTimingAvailable(sample, config.metric);
1057
+ const limit = this.budgets.limits[config.metric];
1058
+ valueEl.textContent = available ? this.formatTiming(config, readTiming(sample, config.metric)) : missing;
1059
+ valueEl.classList.toggle("is-over", isOverBudget(this.budgets, config.metric, value));
1060
+ drawSparkline(graph.ctx, graph.width, graph.height, available ? this.monitor.getHistory(config.metric) : [], config.style[this.theme.resolved], limit === void 0 ? void 0 : {
1061
+ color: GUIDE_COLOR[this.theme.resolved],
1062
+ value: limit
1063
+ });
1064
+ }
1065
+ /** Each value's tooltip names its budget, so amber explains itself. */
1066
+ applyBudgetTitles() {
1067
+ for (const [key, element] of this.statValueEls) element.title = budgetTitle(this.budgets, key);
1068
+ for (const [key, element] of this.hudNumberEls) element.title = budgetTitle(this.budgets, key);
1069
+ for (const [metric, element] of this.graphValueEls) element.title = budgetTitle(this.budgets, metric);
1070
+ for (const [metric, graph] of this.hudGraphs) graph.valueEl.title = budgetTitle(this.budgets, metric);
1071
+ }
911
1072
  formatTiming(config, value) {
912
1073
  return `${config.format(value)}${config.unit ? ` ${config.unit}` : ""}`;
913
1074
  }
@@ -951,7 +1112,14 @@ var PerformanceView = class {
951
1112
  dimLabel.className = "perf-monitor__label";
952
1113
  dimLabel.textContent = "dim on leave";
953
1114
  dimRow.append(createIcon("blend", "perf-monitor__icon"), dimLabel, this.dimCheckbox);
954
- options.append(themeRow, dimRow, this.buildReportRow());
1115
+ const explainRow = document.createElement("label");
1116
+ explainRow.className = "perf-monitor__row";
1117
+ const explainCheckbox = this.buildCheckbox(false, "Explain each metric", (on) => this.element.classList.toggle("perf-monitor--explain", on));
1118
+ const explainLabel = document.createElement("span");
1119
+ explainLabel.className = "perf-monitor__label";
1120
+ explainLabel.textContent = "explain metrics";
1121
+ explainRow.append(createIcon("help", "perf-monitor__icon"), explainLabel, explainCheckbox);
1122
+ options.append(themeRow, dimRow, explainRow, this.buildReportRow());
955
1123
  const graphs = document.createElement("div");
956
1124
  graphs.className = "perf-monitor__graphs";
957
1125
  for (const config of TIMINGS) {
@@ -959,21 +1127,25 @@ var PerformanceView = class {
959
1127
  graph.className = "perf-monitor__graph";
960
1128
  const canvas = document.createElement("canvas");
961
1129
  canvas.className = "perf-monitor__canvas";
962
- const overlay = document.createElement("div");
963
- overlay.className = "perf-monitor__graph-overlay";
964
- const head = document.createElement("label");
1130
+ const overlay = document.createElement("label");
1131
+ overlay.className = "perf-monitor__graph-overlay perf-monitor__graph-overlay--toggle";
1132
+ const head = document.createElement("span");
965
1133
  head.className = "perf-monitor__graph-head";
966
1134
  const checkbox = this.buildCheckbox(this.settings.isGraphEnabled(config.metric), `Show ${config.label} graph in HUD`, (on) => this.settings.setGraphEnabled(config.metric, on));
967
1135
  this.graphCheckboxes.set(config.metric, checkbox);
968
1136
  const label = document.createElement("span");
969
1137
  label.className = "perf-monitor__graph-label";
970
1138
  label.textContent = config.label;
1139
+ label.title = METRIC_HELP[config.metric] ?? "";
971
1140
  const value = document.createElement("span");
972
1141
  value.className = "perf-monitor__graph-value";
973
1142
  value.textContent = "—";
974
1143
  this.graphValueEls.set(config.metric, value);
975
- head.append(checkbox, label);
976
- overlay.append(head, value);
1144
+ const tail = document.createElement("span");
1145
+ tail.className = "perf-monitor__graph-head";
1146
+ tail.append(value, checkbox);
1147
+ head.append(createIcon(config.icon, "perf-monitor__icon"), label);
1148
+ overlay.append(head, tail);
977
1149
  graph.append(canvas, overlay);
978
1150
  graphs.append(graph);
979
1151
  const ctx = canvas.getContext("2d");
@@ -994,14 +1166,19 @@ var PerformanceView = class {
994
1166
  rowEl.className = "perf-monitor__row";
995
1167
  const checkbox = this.buildCheckbox(this.settings.isNumberEnabled(config.key), `Show ${config.label} in HUD`, (on) => this.settings.setNumberEnabled(config.key, on));
996
1168
  this.statCheckboxes.set(config.key, checkbox);
1169
+ const help = METRIC_HELP[config.key] ?? "";
997
1170
  const label = document.createElement("span");
998
1171
  label.className = "perf-monitor__label";
999
1172
  label.textContent = config.label;
1173
+ label.title = help;
1000
1174
  const value = document.createElement("span");
1001
1175
  value.className = "perf-monitor__value";
1002
1176
  value.textContent = "—";
1003
1177
  this.statValueEls.set(config.key, value);
1004
- rowEl.append(checkbox, label, value);
1178
+ const hint = document.createElement("span");
1179
+ hint.className = "perf-monitor__hint";
1180
+ hint.textContent = help;
1181
+ rowEl.append(createIcon(config.icon, "perf-monitor__icon"), label, value, checkbox, hint);
1005
1182
  stats.append(rowEl);
1006
1183
  }
1007
1184
  const footer = document.createElement("div");
@@ -1029,7 +1206,7 @@ var PerformanceView = class {
1029
1206
  this.footerGpuEl.className = "perf-monitor__footer-gpu";
1030
1207
  this.footerHardwareEl.append(this.footerBackendEl, this.footerGpuEl);
1031
1208
  footerBody.append(softwareRow, this.footerHardwareEl);
1032
- footer.append(this.infoCheckbox, footerBody);
1209
+ footer.append(createIcon("info", "perf-monitor__icon"), footerBody, this.infoCheckbox);
1033
1210
  this.element.append(hud, options, graphs, stats, footer);
1034
1211
  }
1035
1212
  rebuildHud() {
@@ -1050,6 +1227,7 @@ var PerformanceView = class {
1050
1227
  const label = document.createElement("span");
1051
1228
  label.className = "perf-monitor__graph-label";
1052
1229
  label.textContent = config.label;
1230
+ label.title = METRIC_HELP[config.metric] ?? "";
1053
1231
  const value = document.createElement("span");
1054
1232
  value.className = "perf-monitor__graph-value";
1055
1233
  value.textContent = "—";
@@ -1075,6 +1253,7 @@ var PerformanceView = class {
1075
1253
  const label = document.createElement("span");
1076
1254
  label.className = "perf-monitor__hud-label";
1077
1255
  label.textContent = config.label;
1256
+ label.title = METRIC_HELP[config.key] ?? "";
1078
1257
  const value = document.createElement("span");
1079
1258
  value.className = "perf-monitor__hud-value";
1080
1259
  value.textContent = "—";
@@ -1082,6 +1261,7 @@ var PerformanceView = class {
1082
1261
  this.hudGridEl.append(item);
1083
1262
  this.hudNumberEls.set(config.key, value);
1084
1263
  }
1264
+ this.applyBudgetTitles();
1085
1265
  this.resizeCanvases();
1086
1266
  }
1087
1267
  onSelectionChanged() {
@@ -1182,6 +1362,7 @@ const PERF_HUD_STYLES = `
1182
1362
  --perf-row: rgba(255, 255, 255, 0.04);
1183
1363
  --perf-border: rgba(255, 255, 255, 0.12);
1184
1364
  --perf-accent: #60a5fa;
1365
+ --perf-warn: #f59e0b;
1185
1366
  --perf-shadow: 0 8px 24px rgba(0, 0, 0, 0.32);
1186
1367
 
1187
1368
  color-scheme: dark;
@@ -1196,6 +1377,7 @@ const PERF_HUD_STYLES = `
1196
1377
  --perf-row: rgba(0, 0, 0, 0.05);
1197
1378
  --perf-border: rgba(0, 0, 0, 0.12);
1198
1379
  --perf-accent: #2563eb;
1380
+ --perf-warn: #b45309;
1199
1381
  --perf-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
1200
1382
 
1201
1383
  color-scheme: light;
@@ -1249,8 +1431,9 @@ const PERF_HUD_STYLES = `
1249
1431
  cursor: pointer;
1250
1432
  }
1251
1433
 
1252
- .perf-monitor__graph-overlay .perf-monitor__graph-head {
1434
+ .perf-monitor__graph-overlay--toggle {
1253
1435
  pointer-events: auto;
1436
+ cursor: pointer;
1254
1437
  }
1255
1438
 
1256
1439
  .perf-monitor__graph-label {
@@ -1282,6 +1465,25 @@ const PERF_HUD_STYLES = `
1282
1465
  cursor: pointer;
1283
1466
  }
1284
1467
 
1468
+ /* Hidden until "explain metrics" is on; then the row wraps it under the label. */
1469
+ .perf-monitor__hint {
1470
+ display: none;
1471
+ flex-basis: 100%;
1472
+ padding: 0 0 2px 18px;
1473
+ color: var(--perf-muted);
1474
+ font-size: 10px;
1475
+ line-height: 1.35;
1476
+ opacity: 0.85;
1477
+ }
1478
+
1479
+ .perf-monitor--explain .perf-monitor__hint {
1480
+ display: block;
1481
+ }
1482
+
1483
+ .perf-monitor--explain .perf-monitor__section .perf-monitor__row {
1484
+ flex-wrap: wrap;
1485
+ }
1486
+
1285
1487
  .perf-monitor__row:nth-child(even) {
1286
1488
  background: var(--perf-row);
1287
1489
  }
@@ -1487,13 +1689,15 @@ const PERF_HUD_STYLES = `
1487
1689
  }
1488
1690
 
1489
1691
  .perf-monitor__badge.is-fallback {
1490
- border-color: #f59e0b;
1491
- color: #f59e0b;
1692
+ border-color: var(--perf-warn);
1693
+ color: var(--perf-warn);
1492
1694
  }
1493
1695
 
1494
- .perf-monitor[data-theme="light"] .perf-monitor__badge.is-fallback {
1495
- border-color: #d97706;
1496
- color: #b45309;
1696
+ /* Past its budget. The value's tooltip names the budget. */
1697
+ .perf-monitor__value.is-over,
1698
+ .perf-monitor__hud-value.is-over,
1699
+ .perf-monitor__graph-value.is-over {
1700
+ color: var(--perf-warn);
1497
1701
  }
1498
1702
 
1499
1703
  .perf-monitor__link {
@@ -1514,7 +1718,8 @@ const PERF_HUD_STYLES = `
1514
1718
  .perf-monitor--compact .perf-monitor__graphs,
1515
1719
  .perf-monitor--compact .perf-monitor__section,
1516
1720
  .perf-monitor--compact:not(.perf-monitor--info) .perf-monitor__footer,
1517
- .perf-monitor--compact .perf-monitor__footer > .perf-monitor__checkbox {
1721
+ .perf-monitor--compact .perf-monitor__footer > .perf-monitor__checkbox,
1722
+ .perf-monitor--compact .perf-monitor__footer > .perf-monitor__icon {
1518
1723
  display: none;
1519
1724
  }
1520
1725
 
@@ -1545,6 +1750,11 @@ const PERF_HUD_STYLES = `
1545
1750
  max-width: 100%;
1546
1751
  }
1547
1752
 
1753
+ /* Room for the hints, so most fit on two or three lines. Plus padding, this fills the host's 20rem cap. */
1754
+ .perf-hud--full > .perf-monitor--explain {
1755
+ width: 19rem;
1756
+ }
1757
+
1548
1758
  .perf-hud > .perf-monitor {
1549
1759
  position: relative;
1550
1760
  z-index: 1;
@@ -1682,6 +1892,7 @@ const mountPerfHud = (monitor, options = {}) => {
1682
1892
  const toggle = disc("Toggle full performance metrics", "", "sliders");
1683
1893
  tools.append(grip, toggle);
1684
1894
  const view = new PerformanceView({
1895
+ budgets: options.budgets,
1685
1896
  mode: options.mode ?? "compact",
1686
1897
  monitor,
1687
1898
  refreshHz: options.refreshHz,
@@ -1757,6 +1968,7 @@ const mountPerfHud = (monitor, options = {}) => {
1757
1968
  },
1758
1969
  element: host,
1759
1970
  getMode: () => view.getMode(),
1971
+ setBudgets: (budgets) => view.setBudgets(budgets),
1760
1972
  setMode,
1761
1973
  getTheme: () => theme.mode,
1762
1974
  setTheme: (mode) => theme.setMode(mode),
@@ -1766,6 +1978,6 @@ const mountPerfHud = (monitor, options = {}) => {
1766
1978
  };
1767
1979
  };
1768
1980
  //#endregion
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 };
1981
+ export { formatReport as a, HudSettings as c, isThemeMode as d, resolveBudgets as f, 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 };
1770
1982
 
1771
- //# sourceMappingURL=mount-perf-hud-BLs1jkwM.js.map
1983
+ //# sourceMappingURL=mount-perf-hud-DqY_tP6s.js.map