@zkmake/three-meter 0.6.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.
package/README.md CHANGED
@@ -89,6 +89,7 @@ Canvas is its own React root. To run two canvases on one page, pass the same `st
89
89
  | `parent` | `document.body` | Where the host element is appended. |
90
90
  | `injectStyles` | `true` | Append the stylesheet once per document. |
91
91
  | `refreshHz` | `10` | Repaint rate. |
92
+ | `budgets` | timing defaults | Limits past which a value turns amber. See [Budgets](#budgets). |
92
93
 
93
94
  `PerformanceMonitor` and `PerfSampler` take `trackGPU` (default true), `gpuQueryPoolSize` (default 5),
94
95
  `historySize` (default 120) and `frameStatsSize` (default 1000).
@@ -118,7 +119,7 @@ hud.theme.subscribe(() => syncMyPageWith(hud.theme.resolved));
118
119
 
119
120
  The resolved theme lands on `data-theme` of `.perf-hud` and `.perf-monitor`. To restyle either
120
121
  palette, override the `--perf-*` custom properties (`bg`, `fg`, `fg-dim`, `muted`, `row`,
121
- `border`, `accent`, `shadow`) on those selectors.
122
+ `border`, `accent`, `warn`, `shadow`) on those selectors. `warn` is the over-budget amber.
122
123
 
123
124
  ## GPU timing
124
125
 
@@ -149,6 +150,23 @@ Every row has an icon, its value, and a checkbox on the right that puts it in th
149
150
  a label for a one-line explanation of the metric and what a bad reading means, or tick **explain
150
151
  metrics** to show them all under the rows (also works on touch).
151
152
 
153
+ ## Budgets
154
+
155
+ A value past its budget turns amber in the full view and the compact HUD, and its tooltip names the
156
+ budget. The FPS, CPU and GPU graphs draw the budget as a dashed line once the series reaches it; the
157
+ scale never stretches to fit it, so a quiet graph keeps its detail.
158
+
159
+ Timing budgets come from `targetFps` (60 by default): FPS at least 95% of it, 1% low at least half,
160
+ CPU and GPU within one frame (16.7 ms), frame p99 within one and a half. Counts have no default,
161
+ since what's reasonable depends on the scene. Set your own:
162
+
163
+ ```ts
164
+ mountPerfHud(monitor, { budgets: { targetFps: 120, calls: 500, triangles: 2_000_000 } });
165
+ hud.setBudgets({ gpu: null }); // null drops a default; false drops them all
166
+ ```
167
+
168
+ `PerfHud` takes the same `budgets` prop and applies changes live.
169
+
152
170
  ## Stutter
153
171
 
154
172
  Average FPS hides a hitch every few seconds. Three rows in the full view catch it, and any of them
@@ -1,4 +1,28 @@
1
1
  import { d as TimingMetric, t as PerformanceMonitor } from "./performance-monitor-CBN05_BC.js";
2
+ //#region src/ui/budgets.d.ts
3
+ /**
4
+ * Budgets: the value each metric should stay within. A reading past its
5
+ * budget turns amber in the card, and the FPS / CPU / GPU graphs draw the
6
+ * line once the series reaches it. `fps` and `low` are floors (worse below);
7
+ * everything else is a ceiling (worse above).
8
+ *
9
+ * Timing budgets default from `targetFps` (60): FPS at least 95% of it, 1%
10
+ * low at least half, CPU and GPU within one frame, p99 within one and a half.
11
+ * Counts have no default, since what's reasonable depends on the scene. Pass
12
+ * `null` for a key to drop its default, or `false` for no budgets at all.
13
+ */
14
+ type BudgetKey = "calls" | "cpu" | "fps" | "geometries" | "gpu" | "hitches" | "lines" | "low" | "p99" | "passes" | "points" | "shaders" | "textures" | "triangles";
15
+ type Budgets = Partial<Record<BudgetKey, number | null>> & {
16
+ /** The frame rate the defaults are derived from. Default 60. */
17
+ targetFps?: number;
18
+ };
19
+ type ResolvedBudgets = {
20
+ /** Frame budget in ms, `1000 / targetFps`. */
21
+ frameMs: number;
22
+ limits: Partial<Record<BudgetKey, number>>;
23
+ };
24
+ declare const resolveBudgets: (budgets: Budgets | false | undefined) => ResolvedBudgets;
25
+ //#endregion
2
26
  //#region src/ui/dock-panel.d.ts
3
27
  declare const EDGES: readonly ["left", "right", "top", "bottom"];
4
28
  type ScreenEdge = (typeof EDGES)[number];
@@ -131,6 +155,11 @@ type PerformanceViewOptions = {
131
155
  mode?: PerformanceViewMode;
132
156
  /** Repaint rate. Default 10. */
133
157
  refreshHz?: number;
158
+ /**
159
+ * Limits past which a value turns amber. Timing budgets default from
160
+ * `targetFps` (60); `false` turns them all off. See {@link Budgets}.
161
+ */
162
+ budgets?: Budgets | false;
134
163
  /** Share one between views to keep them in sync; a fresh instance otherwise. */
135
164
  settings?: HudSettings;
136
165
  /**
@@ -171,6 +200,7 @@ declare class PerformanceView {
171
200
  private hudGridEl;
172
201
  private readonly hudGraphs;
173
202
  private readonly hudNumberEls;
203
+ private budgets;
174
204
  private rafId;
175
205
  private lastPaintAt;
176
206
  private readonly resizeObserver;
@@ -180,12 +210,19 @@ declare class PerformanceView {
180
210
  stop(): void;
181
211
  setMode(mode: PerformanceViewMode): void;
182
212
  getMode(): PerformanceViewMode;
213
+ /** Replace the budgets; `false` turns them all off. Applies on the next paint. */
214
+ setBudgets(budgets: Budgets | false | undefined): void;
183
215
  dispose(): void;
184
216
  private frame;
185
217
  private render;
186
218
  /** The monitor caches the environment once its backend settles; until then this re-reads. */
187
219
  private renderFooter;
188
220
  private renderHud;
221
+ private paintNumber;
222
+ /** Value and sparkline, with the budget line drawn once the series reaches it. */
223
+ private paintGraph;
224
+ /** Each value's tooltip names its budget, so amber explains itself. */
225
+ private applyBudgetTitles;
189
226
  private formatTiming;
190
227
  private build;
191
228
  private rebuildHud;
@@ -221,6 +258,12 @@ type MountPerfHudOptions = {
221
258
  injectStyles?: boolean;
222
259
  /** Repaint rate. Default 10. */
223
260
  refreshHz?: number;
261
+ /**
262
+ * Limits past which a value turns amber, e.g. `{ calls: 500, targetFps: 120 }`.
263
+ * Timing budgets default from `targetFps` (60); `false` turns them all off.
264
+ * Change later with the handle's `setBudgets`.
265
+ */
266
+ budgets?: Budgets | false;
224
267
  /** Accessible name of the panel. Default `Performance`. */
225
268
  label?: string;
226
269
  };
@@ -240,9 +283,11 @@ type PerfHudHandle = {
240
283
  getTheme: () => ThemeMode;
241
284
  /** Sets the consumer layer; a pick made in the panel still wins until `settings.setTheme(null)`. */
242
285
  setTheme: (mode: ThemeMode) => void;
286
+ /** Replace the budgets; `false` turns them all off. */
287
+ setBudgets: (budgets: Budgets | false | undefined) => void;
243
288
  dispose: () => void;
244
289
  };
245
290
  declare const mountPerfHud: (monitor: PerformanceMonitor, options?: MountPerfHudOptions) => PerfHudHandle;
246
291
  //#endregion
247
- export { dockPanel as C, ScreenEdge as S, DefaultPlacement as _, PerformanceViewMode as a, DockPanelOptions as b, HudDefaults as c, HudSettingsOptions as d, HudTheme as f, isThemeMode as g, ThemeMode as h, PerformanceView as i, HudSelection as l, THEME_MODES as m, PerfHudHandle as n, PerformanceViewOptions as o, ResolvedTheme as p, mountPerfHud as r, DEFAULT_STORAGE_KEY as s, MountPerfHudOptions as t, HudSettings as u, DockAlign as v, Placement as x, DockHandle as y };
248
- //# sourceMappingURL=mount-perf-hud-CmINOH3T.d.ts.map
292
+ export { dockPanel as C, resolveBudgets as D, ResolvedBudgets as E, ScreenEdge as S, Budgets as T, DefaultPlacement as _, PerformanceViewMode as a, DockPanelOptions as b, HudDefaults as c, HudSettingsOptions as d, HudTheme as f, isThemeMode as g, ThemeMode as h, PerformanceView as i, HudSelection as l, THEME_MODES as m, PerfHudHandle as n, PerformanceViewOptions as o, ResolvedTheme as p, mountPerfHud as r, DEFAULT_STORAGE_KEY as s, MountPerfHudOptions as t, HudSettings as u, DockAlign as v, BudgetKey as w, Placement as x, DockHandle as y };
293
+ //# sourceMappingURL=mount-perf-hud-6LeJ93Jg.d.ts.map
@@ -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",
@@ -493,7 +526,7 @@ const createIcon = (name, className) => {
493
526
  };
494
527
  //#endregion
495
528
  //#region package.json
496
- var version = "0.6.0";
529
+ var version = "0.7.0";
497
530
  //#endregion
498
531
  //#region src/ui/clipboard.ts
499
532
  /**
@@ -639,7 +672,7 @@ const formatReport = (monitor) => renderReport({
639
672
  });
640
673
  //#endregion
641
674
  //#region src/ui/sparkline.ts
642
- function drawSparkline(ctx, width, height, values, style) {
675
+ function drawSparkline(ctx, width, height, values, style, guide) {
643
676
  ctx.clearRect(0, 0, width, height);
644
677
  if (values.length < 2) return;
645
678
  let max = 0;
@@ -658,6 +691,17 @@ function drawSparkline(ctx, width, height, values, style) {
658
691
  ctx.closePath();
659
692
  ctx.fillStyle = style.fill;
660
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
+ }
661
705
  }
662
706
  //#endregion
663
707
  //#region src/ui/performance-view.ts
@@ -742,92 +786,130 @@ const readTiming = (sample, metric) => {
742
786
  }
743
787
  };
744
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);
745
791
  const NUMBERS = [
746
792
  {
793
+ format: whole,
747
794
  icon: "gauge",
748
795
  key: "fps",
749
796
  label: "FPS",
750
- read: (sample) => Math.round(sample.fps).toString()
797
+ value: (sample) => sample.fps || null
751
798
  },
752
799
  {
800
+ format: formatCount,
753
801
  icon: "layers",
754
802
  key: "calls",
755
803
  label: "Calls",
756
- read: (sample) => formatCount(sample.render.calls)
804
+ value: (sample) => sample.render.calls
757
805
  },
758
806
  {
807
+ format: tenths,
759
808
  icon: "cpu",
760
809
  key: "cpu",
761
810
  label: "CPU",
762
- read: (sample) => sample.cpu.toFixed(1)
811
+ unit: "ms",
812
+ value: (sample) => sample.cpu
763
813
  },
764
814
  {
815
+ format: tenths,
765
816
  icon: "zap",
766
817
  key: "gpu",
767
818
  label: "GPU",
768
- read: (sample) => sample.gpu.available ? sample.gpu.ms.toFixed(1) : "—"
819
+ unit: "ms",
820
+ value: (sample) => sample.gpu.available ? sample.gpu.ms : null
769
821
  },
770
822
  {
823
+ format: whole,
771
824
  icon: "trendingDown",
772
825
  key: "low",
773
826
  label: "1% low",
774
- read: (_sample, stats) => stats.frames ? Math.round(stats.lowFps).toString() : "—"
827
+ value: (_sample, stats) => stats.frames ? stats.lowFps : null
775
828
  },
776
829
  {
830
+ format: tenths,
777
831
  icon: "timer",
778
832
  key: "p99",
779
833
  label: "Frame p99",
780
- read: (_sample, stats) => stats.frames ? stats.p99Ms.toFixed(1) : "—"
834
+ unit: "ms",
835
+ value: (_sample, stats) => stats.frames ? stats.p99Ms : null
781
836
  },
782
837
  {
838
+ format: formatCount,
783
839
  icon: "activity",
784
840
  key: "hitches",
785
841
  label: "Hitches",
786
- read: (_sample, stats) => stats.frames ? formatCount(stats.hitches) : "—"
842
+ value: (_sample, stats) => stats.frames ? stats.hitches : null
787
843
  },
788
844
  {
845
+ format: formatCount,
789
846
  icon: "triangle",
790
847
  key: "triangles",
791
848
  label: "Triangles",
792
- read: (sample) => formatCount(sample.render.triangles)
849
+ value: (sample) => sample.render.triangles
793
850
  },
794
851
  {
852
+ format: formatCount,
795
853
  icon: "spline",
796
854
  key: "lines",
797
855
  label: "Lines",
798
- read: (sample) => formatCount(sample.render.lines)
856
+ value: (sample) => sample.render.lines
799
857
  },
800
858
  {
859
+ format: formatCount,
801
860
  icon: "circleDot",
802
861
  key: "points",
803
862
  label: "Points",
804
- read: (sample) => formatCount(sample.render.points)
863
+ value: (sample) => sample.render.points
805
864
  },
806
865
  {
866
+ format: formatCount,
807
867
  icon: "repeat",
808
868
  key: "passes",
809
869
  label: "Render passes",
810
- read: (sample) => formatCount(sample.render.passes)
870
+ value: (sample) => sample.render.passes
811
871
  },
812
872
  {
873
+ format: formatCount,
813
874
  icon: "box",
814
875
  key: "geometries",
815
876
  label: "Geometries",
816
- read: (sample) => formatCount(sample.resources.geometries)
877
+ value: (sample) => sample.resources.geometries
817
878
  },
818
879
  {
880
+ format: formatCount,
819
881
  icon: "image",
820
882
  key: "textures",
821
883
  label: "Textures",
822
- read: (sample) => formatCount(sample.resources.textures)
884
+ value: (sample) => sample.resources.textures
823
885
  },
824
886
  {
887
+ format: formatCount,
825
888
  icon: "palette",
826
889
  key: "shaders",
827
890
  label: "Shaders",
828
- read: (sample) => formatCount(sample.resources.programs)
891
+ value: (sample) => sample.resources.programs
829
892
  }
830
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
+ };
831
913
  /**
832
914
  * Host-agnostic widget that paints {@link Sample}s. Mount {@link element}
833
915
  * anywhere; the view drives its own throttled repaint. Compact is the floating
@@ -860,6 +942,7 @@ var PerformanceView = class {
860
942
  hudGridEl;
861
943
  hudGraphs = /* @__PURE__ */ new Map();
862
944
  hudNumberEls = /* @__PURE__ */ new Map();
945
+ budgets;
863
946
  rafId = null;
864
947
  lastPaintAt = 0;
865
948
  resizeObserver;
@@ -872,6 +955,7 @@ var PerformanceView = class {
872
955
  this.theme.setOverride(this.settings.theme);
873
956
  this.mode = options.mode ?? "full";
874
957
  this.minIntervalMs = 1e3 / (options.refreshHz ?? 10);
958
+ this.budgets = resolveBudgets(options.budgets);
875
959
  this.element = document.createElement("div");
876
960
  this.element.className = "perf-monitor";
877
961
  this.applyModeClass();
@@ -901,6 +985,11 @@ var PerformanceView = class {
901
985
  getMode() {
902
986
  return this.mode;
903
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
+ }
904
993
  dispose() {
905
994
  this.stop();
906
995
  this.unsubscribe();
@@ -922,15 +1011,9 @@ var PerformanceView = class {
922
1011
  if (this.settings.info) this.renderFooter();
923
1012
  return;
924
1013
  }
925
- for (const config of TIMINGS) {
926
- const available = isTimingAvailable(sample, config.metric);
927
- const valueEl = this.graphValueEls.get(config.metric);
928
- valueEl.textContent = available ? this.formatTiming(config, readTiming(sample, config.metric)) : "unavailable";
929
- const graph = this.graphCanvases.get(config.metric);
930
- drawSparkline(graph.ctx, graph.width, graph.height, available ? this.monitor.getHistory(config.metric) : [], config.style[this.theme.resolved]);
931
- }
1014
+ for (const config of TIMINGS) this.paintGraph(config, sample, this.graphValueEls.get(config.metric), this.graphCanvases.get(config.metric), "unavailable");
932
1015
  const stats = this.monitor.getFrameStats();
933
- 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);
934
1017
  this.renderFooter();
935
1018
  }
936
1019
  /** The monitor caches the environment once its backend settles; until then this re-reads. */
@@ -954,16 +1037,38 @@ var PerformanceView = class {
954
1037
  renderHud(sample) {
955
1038
  const stats = this.hudNumberEls.size > 0 ? this.monitor.getFrameStats() : null;
956
1039
  for (const [key, element] of this.hudNumberEls) {
957
- const config = NUMBERS.find((number) => number.key === key);
958
- 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);
959
1042
  }
960
1043
  for (const [metric, graph] of this.hudGraphs) {
961
1044
  const config = TIMINGS.find((timing) => timing.metric === metric);
962
- const available = isTimingAvailable(sample, metric);
963
- graph.valueEl.textContent = available ? this.formatTiming(config, readTiming(sample, metric)) : "—";
964
- 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, "—");
965
1046
  }
966
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
+ }
967
1072
  formatTiming(config, value) {
968
1073
  return `${config.format(value)}${config.unit ? ` ${config.unit}` : ""}`;
969
1074
  }
@@ -1156,6 +1261,7 @@ var PerformanceView = class {
1156
1261
  this.hudGridEl.append(item);
1157
1262
  this.hudNumberEls.set(config.key, value);
1158
1263
  }
1264
+ this.applyBudgetTitles();
1159
1265
  this.resizeCanvases();
1160
1266
  }
1161
1267
  onSelectionChanged() {
@@ -1256,6 +1362,7 @@ const PERF_HUD_STYLES = `
1256
1362
  --perf-row: rgba(255, 255, 255, 0.04);
1257
1363
  --perf-border: rgba(255, 255, 255, 0.12);
1258
1364
  --perf-accent: #60a5fa;
1365
+ --perf-warn: #f59e0b;
1259
1366
  --perf-shadow: 0 8px 24px rgba(0, 0, 0, 0.32);
1260
1367
 
1261
1368
  color-scheme: dark;
@@ -1270,6 +1377,7 @@ const PERF_HUD_STYLES = `
1270
1377
  --perf-row: rgba(0, 0, 0, 0.05);
1271
1378
  --perf-border: rgba(0, 0, 0, 0.12);
1272
1379
  --perf-accent: #2563eb;
1380
+ --perf-warn: #b45309;
1273
1381
  --perf-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
1274
1382
 
1275
1383
  color-scheme: light;
@@ -1581,13 +1689,15 @@ const PERF_HUD_STYLES = `
1581
1689
  }
1582
1690
 
1583
1691
  .perf-monitor__badge.is-fallback {
1584
- border-color: #f59e0b;
1585
- color: #f59e0b;
1692
+ border-color: var(--perf-warn);
1693
+ color: var(--perf-warn);
1586
1694
  }
1587
1695
 
1588
- .perf-monitor[data-theme="light"] .perf-monitor__badge.is-fallback {
1589
- border-color: #d97706;
1590
- 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);
1591
1701
  }
1592
1702
 
1593
1703
  .perf-monitor__link {
@@ -1782,6 +1892,7 @@ const mountPerfHud = (monitor, options = {}) => {
1782
1892
  const toggle = disc("Toggle full performance metrics", "", "sliders");
1783
1893
  tools.append(grip, toggle);
1784
1894
  const view = new PerformanceView({
1895
+ budgets: options.budgets,
1785
1896
  mode: options.mode ?? "compact",
1786
1897
  monitor,
1787
1898
  refreshHz: options.refreshHz,
@@ -1857,6 +1968,7 @@ const mountPerfHud = (monitor, options = {}) => {
1857
1968
  },
1858
1969
  element: host,
1859
1970
  getMode: () => view.getMode(),
1971
+ setBudgets: (budgets) => view.setBudgets(budgets),
1860
1972
  setMode,
1861
1973
  getTheme: () => theme.mode,
1862
1974
  setTheme: (mode) => theme.setMode(mode),
@@ -1866,6 +1978,6 @@ const mountPerfHud = (monitor, options = {}) => {
1866
1978
  };
1867
1979
  };
1868
1980
  //#endregion
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 };
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 };
1870
1982
 
1871
- //# sourceMappingURL=mount-perf-hud-C5wdjgmM.js.map
1983
+ //# sourceMappingURL=mount-perf-hud-DqY_tP6s.js.map