@zkmake/three-meter 0.9.1 → 0.10.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,17 +1,24 @@
1
1
  //#region src/ui/budgets.ts
2
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;
3
+ const FLOORS = /* @__PURE__ */ new Set([
4
+ "fps",
5
+ "headroom",
6
+ "low"
7
+ ]);
8
+ /** `detectedHz` is the monitor's refresh reading; an explicit `targetFps` wins over it. */
9
+ const resolveBudgets = (budgets, detectedHz = null) => {
10
+ const targetFps = budgets && budgets.targetFps || detectedHz || DEFAULT_TARGET_FPS;
6
11
  const frameMs = 1e3 / targetFps;
7
12
  if (budgets === false) return {
8
13
  frameMs,
9
- limits: {}
14
+ limits: {},
15
+ targetFps
10
16
  };
11
17
  const merged = {
12
18
  cpu: frameMs,
13
19
  fps: targetFps * .95,
14
20
  gpu: frameMs,
21
+ headroom: 0,
15
22
  low: targetFps / 2,
16
23
  p99: frameMs * 1.5,
17
24
  ...budgets
@@ -20,7 +27,8 @@ const resolveBudgets = (budgets) => {
20
27
  for (const [key, limit] of Object.entries(merged)) if (key !== "targetFps" && typeof limit === "number" && Number.isFinite(limit)) limits[key] = limit;
21
28
  return {
22
29
  frameMs,
23
- limits
30
+ limits,
31
+ targetFps
24
32
  };
25
33
  };
26
34
  const isFloor = (key) => FLOORS.has(key);
@@ -486,6 +494,9 @@ const dockPanel = (element, options = {}) => {
486
494
  //#endregion
487
495
  //#region src/ui/icons.ts
488
496
  const ICON_MARKUP = {
497
+ eye: "<path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>",
498
+ eyeOff: "<path d=\"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49\"/><path d=\"M14.084 14.158a3 3 0 0 1-4.242-4.242\"/><path d=\"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143\"/><path d=\"m2 2 20 20\"/>",
499
+ hourglass: "<path d=\"M5 22h14\"/><path d=\"M5 2h14\"/><path d=\"M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22\"/><path d=\"M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2\"/>",
489
500
  flame: "<path d=\"M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z\"/>",
490
501
  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\"/>",
491
502
  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\"/>",
@@ -527,7 +538,8 @@ const createIcon = (name, className) => {
527
538
  };
528
539
  //#endregion
529
540
  //#region package.json
530
- var version = "0.9.1";
541
+ var name = "@zkmake/three-meter";
542
+ var version = "0.10.0";
531
543
  //#endregion
532
544
  //#region src/ui/clipboard.ts
533
545
  /**
@@ -579,6 +591,45 @@ const compact = new Intl.NumberFormat("en-US", {
579
591
  });
580
592
  const formatCount = (value) => Math.abs(value) < COMPACT_FROM ? exact.format(value) : compact.format(value);
581
593
  //#endregion
594
+ //#region src/ui/hider.ts
595
+ var Hider = class {
596
+ groups = /* @__PURE__ */ new Map();
597
+ /** Each hidden object's `visible` from before the first group hid it. */
598
+ original = /* @__PURE__ */ new Map();
599
+ hide(key, objects) {
600
+ if (this.groups.has(key)) return;
601
+ const hideable = objects.filter((object) => typeof object === "object" && object !== null);
602
+ this.groups.set(key, hideable);
603
+ for (const object of hideable) {
604
+ if (!this.original.has(object)) this.original.set(object, object.visible !== false);
605
+ object.visible = false;
606
+ }
607
+ }
608
+ show(key) {
609
+ const objects = this.groups.get(key);
610
+ if (!objects) return;
611
+ this.groups.delete(key);
612
+ for (const object of objects) if (!this.heldElsewhere(object)) {
613
+ object.visible = this.original.get(object) ?? true;
614
+ this.original.delete(object);
615
+ }
616
+ }
617
+ isHidden(key) {
618
+ return this.groups.has(key);
619
+ }
620
+ /** Hidden group keys, in the order they were hidden. */
621
+ keys() {
622
+ return [...this.groups.keys()];
623
+ }
624
+ restoreAll() {
625
+ for (const key of this.keys()) this.show(key);
626
+ }
627
+ heldElsewhere(object) {
628
+ for (const objects of this.groups.values()) if (objects.includes(object)) return true;
629
+ return false;
630
+ }
631
+ };
632
+ //#endregion
582
633
  //#region src/ui/metric-help.ts
583
634
  /**
584
635
  * One plain-language line per metric: what it is, and what a bad reading
@@ -590,6 +641,8 @@ const METRIC_HELP = {
590
641
  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).",
591
642
  fps: "Frames drawn per second. Higher is smoother; most displays top out at 60 or 120.",
592
643
  geometries: "Geometries held in GPU memory. Should level off; a steady climb is a leak, so dispose() what you remove.",
644
+ headroom: "Spare time left in each frame: the frame budget minus whichever of CPU and GPU took longer. Below zero, frames miss the display's refresh.",
645
+ refresh: "The rate the loop runs at when it keeps up: the display's refresh rate, or the app's own cap. Budgets use it unless you set targetFps.",
593
646
  gpu: "Time the graphics card spent drawing the frame. High means shaders, overdraw or resolution are the bottleneck.",
594
647
  hitches: "Frames that took over twice as long as usual, in the last 1,000 frames. Each one is a visible stutter.",
595
648
  lines: "Line segments drawn this frame.",
@@ -640,10 +693,15 @@ const renderReport = ({ cost, environment, page, sample, stats }) => {
640
693
  `p99 ${ms(stats.p99Ms)}`,
641
694
  `hitches ${formatCount(stats.hitches)} / ${formatCount(stats.frames)} frames`
642
695
  ].join(" · ");
696
+ const timing = [`CPU ${ms(sample.cpu)}`, `GPU ${sample.gpu.available ? ms(sample.gpu.ms) : "unavailable"}`];
697
+ if (stats.refreshHz) {
698
+ const busiest = Math.max(sample.cpu, sample.gpu.available ? sample.gpu.ms : 0);
699
+ timing.push(`refresh ${stats.refreshHz} Hz`, `headroom ${ms(1e3 / stats.refreshHz - busiest)}`);
700
+ }
643
701
  const lines = [
644
702
  heading.join(" · "),
645
703
  frames,
646
- `CPU ${ms(sample.cpu)} · GPU ${sample.gpu.available ? ms(sample.gpu.ms) : "unavailable"}`,
704
+ timing.join(" · "),
647
705
  [
648
706
  `Calls ${formatCount(render.calls)}`,
649
707
  `passes ${formatCount(render.passes)}`,
@@ -784,7 +842,8 @@ const THEME_OPTIONS = [
784
842
  mode: "dark"
785
843
  }
786
844
  ];
787
- const RELEASE_URL = `https://github.com/zkmake/three-meter/releases/tag/v${version}`;
845
+ /** The monorepo tags each package's releases `@zkmake/three-meter@0.9.2`. */
846
+ const RELEASE_URL = `https://github.com/zkmake/three-kit/releases/tag/${encodeURIComponent(`${name}@${version}`)}`;
788
847
  /** How long the report button says `copied` / `failed` before reading `copy` again. */
789
848
  const COPY_FEEDBACK_MS = 1500;
790
849
  /** Walking the scene graph isn't free; the top-costs list refreshes at 2 Hz while open. */
@@ -797,7 +856,15 @@ const COST_GROUPS = [{
797
856
  group: "materials",
798
857
  label: "materials"
799
858
  }];
800
- const COSTS_HELP = "The scene's biggest draw costs in the main render pass, estimated from the scene graph. Hidden and off-screen objects are left out; shadow and post-processing passes aren't counted. Click a row to log its objects to the console.";
859
+ const COSTS_HELP = "The scene's biggest draw costs in the main render pass, estimated from the scene graph. Hidden and off-screen objects are left out; shadow and post-processing passes aren't counted. Click a row to log its objects to the console; use the eye to hide them and measure the time they cost.";
860
+ /** Frames averaged on each side of hiding a row. */
861
+ const MEASURE_FRAMES = 30;
862
+ /** How long after hiding before the saving is read, so the average holds only frames without the row. */
863
+ const MEASURE_SETTLE_MS = 700;
864
+ const meanOfLast = (values, count) => {
865
+ const tail = values.slice(-count);
866
+ return tail.length === 0 ? 0 : tail.reduce((sum, value) => sum + value, 0) / tail.length;
867
+ };
801
868
  /** `×2,000` for instances or copies; nothing for a single object. */
802
869
  const copiesOf = (entry) => entry.instances > 1 ? `×${formatCount(entry.instances)}` : "";
803
870
  const costTitle = (entry) => {
@@ -846,6 +913,22 @@ const NUMBERS = [
846
913
  unit: "ms",
847
914
  value: (sample) => sample.gpu.available ? sample.gpu.ms : null
848
915
  },
916
+ {
917
+ format: tenths,
918
+ icon: "hourglass",
919
+ key: "headroom",
920
+ label: "Headroom",
921
+ unit: "ms",
922
+ value: (sample, _stats, frameMs) => sample.fps ? frameMs - Math.max(sample.cpu, sample.gpu.available ? sample.gpu.ms : 0) : null
923
+ },
924
+ {
925
+ format: (value) => `${Math.round(value)} Hz`,
926
+ icon: "monitor",
927
+ key: "refresh",
928
+ label: "Refresh",
929
+ unit: "Hz",
930
+ value: (_sample, stats) => stats.refreshHz
931
+ },
849
932
  {
850
933
  format: whole,
851
934
  icon: "trendingDown",
@@ -969,6 +1052,10 @@ var PerformanceView = class {
969
1052
  hudGraphs = /* @__PURE__ */ new Map();
970
1053
  hudNumberEls = /* @__PURE__ */ new Map();
971
1054
  budgets;
1055
+ budgetsInput;
1056
+ detectedHz = null;
1057
+ hider = new Hider();
1058
+ hiddenRows = /* @__PURE__ */ new Map();
972
1059
  rafId = null;
973
1060
  lastPaintAt = 0;
974
1061
  resizeObserver;
@@ -989,6 +1076,7 @@ var PerformanceView = class {
989
1076
  this.theme.setOverride(this.settings.theme);
990
1077
  this.mode = options.mode ?? "full";
991
1078
  this.minIntervalMs = 1e3 / (options.refreshHz ?? 10);
1079
+ this.budgetsInput = options.budgets;
992
1080
  this.budgets = resolveBudgets(options.budgets);
993
1081
  this.element = document.createElement("div");
994
1082
  this.element.className = "perf-monitor";
@@ -1021,11 +1109,13 @@ var PerformanceView = class {
1021
1109
  }
1022
1110
  /** Replace the budgets; `false` turns them all off. Applies on the next paint. */
1023
1111
  setBudgets(budgets) {
1024
- this.budgets = resolveBudgets(budgets);
1112
+ this.budgetsInput = budgets;
1113
+ this.budgets = resolveBudgets(budgets, this.detectedHz);
1025
1114
  this.applyBudgetTitles();
1026
1115
  }
1027
1116
  dispose() {
1028
1117
  this.stop();
1118
+ this.showAllHidden();
1029
1119
  this.unsubscribe();
1030
1120
  this.unsubscribeTheme();
1031
1121
  if (this.ownsTheme) this.theme.dispose();
@@ -1040,13 +1130,14 @@ var PerformanceView = class {
1040
1130
  };
1041
1131
  render() {
1042
1132
  const sample = this.monitor.getSample();
1133
+ const stats = this.monitor.getFrameStats();
1134
+ this.followRefresh(stats.refreshHz);
1043
1135
  if (this.mode === "compact") {
1044
- this.renderHud(sample);
1136
+ this.renderHud(sample, stats);
1045
1137
  if (this.settings.info) this.renderFooter();
1046
1138
  return;
1047
1139
  }
1048
1140
  for (const config of TIMINGS) this.paintGraph(config, sample, this.graphValueEls.get(config.metric), this.graphCanvases.get(config.metric), "unavailable");
1049
- const stats = this.monitor.getFrameStats();
1050
1141
  for (const config of NUMBERS) this.paintNumber(config, this.statValueEls.get(config.key), sample, stats);
1051
1142
  if (this.costsOpen && performance.now() - this.lastCostsAt >= COSTS_INTERVAL_MS) this.renderCosts();
1052
1143
  this.renderFooter();
@@ -1054,44 +1145,102 @@ var PerformanceView = class {
1054
1145
  renderCosts() {
1055
1146
  this.lastCostsAt = performance.now();
1056
1147
  const cost = this.monitor.getSceneCost();
1148
+ const hidden = [...this.hiddenRows.values()].map((row) => this.hiddenCostRow(row));
1057
1149
  if (!cost) {
1058
- this.costsListEl.replaceChildren(this.costsNote("No scene rendered yet."));
1150
+ this.costsListEl.replaceChildren(...hidden, this.costsNote("No scene rendered yet."));
1059
1151
  return;
1060
1152
  }
1061
- const entries = [...this.costGroup === "meshes" ? cost.meshes : cost.materials];
1153
+ const entries = (this.costGroup === "meshes" ? cost.meshes : cost.materials).filter((entry) => !this.hiddenRows.has(entry.key));
1062
1154
  if (this.costSort === "triangles") entries.sort((a, b) => b.triangles - a.triangles || b.calls - a.calls);
1063
- if (entries.length === 0) {
1155
+ if (entries.length === 0 && hidden.length === 0) {
1064
1156
  this.costsListEl.replaceChildren(this.costsNote("Nothing drawn."));
1065
1157
  return;
1066
1158
  }
1067
1159
  const rows = entries.slice(0, COSTS_ROWS).map((entry) => {
1068
- const row = document.createElement("button");
1069
- row.type = "button";
1070
- row.className = "perf-monitor__cost";
1071
- row.title = costTitle(entry);
1072
- const name = document.createElement("span");
1073
- name.className = "perf-monitor__cost-name";
1074
- name.textContent = entry.label;
1075
- const copies = copiesOf(entry);
1076
- if (copies) {
1077
- const suffix = document.createElement("span");
1078
- suffix.className = "perf-monitor__cost-copies";
1079
- suffix.textContent = ` ${copies}`;
1080
- name.append(suffix);
1081
- }
1160
+ const row = this.costRowShell(entry);
1082
1161
  const calls = document.createElement("span");
1083
1162
  calls.className = "perf-monitor__cost-value";
1084
1163
  calls.textContent = formatCount(entry.calls);
1085
1164
  const triangles = document.createElement("span");
1086
1165
  triangles.className = "perf-monitor__cost-value";
1087
1166
  triangles.textContent = formatCount(entry.triangles);
1088
- row.append(name, calls, triangles);
1089
- row.addEventListener("click", () => {
1090
- console.log(`three-meter: ${entry.label}`, entry.objects);
1091
- });
1167
+ row.append(calls, triangles, this.eyeButton(entry, false));
1092
1168
  return row;
1093
1169
  });
1094
- this.costsListEl.replaceChildren(...rows);
1170
+ this.costsListEl.replaceChildren(...hidden, ...rows);
1171
+ }
1172
+ /** Name button (logs the objects) in a row; the caller appends the value cells and the eye. */
1173
+ costRowShell(entry) {
1174
+ const row = document.createElement("div");
1175
+ row.className = "perf-monitor__cost";
1176
+ const name = document.createElement("button");
1177
+ name.type = "button";
1178
+ name.className = "perf-monitor__cost-name";
1179
+ name.title = costTitle(entry);
1180
+ name.textContent = entry.label;
1181
+ const copies = copiesOf(entry);
1182
+ if (copies) {
1183
+ const suffix = document.createElement("span");
1184
+ suffix.className = "perf-monitor__cost-copies";
1185
+ suffix.textContent = ` ${copies}`;
1186
+ name.append(suffix);
1187
+ }
1188
+ name.addEventListener("click", () => {
1189
+ console.log(`three-meter: ${entry.label}`, entry.objects);
1190
+ });
1191
+ row.append(name);
1192
+ return row;
1193
+ }
1194
+ /** A hidden row: what hiding it saved, measured from the timing history. */
1195
+ hiddenCostRow(hidden) {
1196
+ const row = this.costRowShell(hidden.entry);
1197
+ row.classList.add("is-hidden");
1198
+ const saved = document.createElement("span");
1199
+ saved.className = "perf-monitor__cost-saved";
1200
+ const label = hidden.metric.toUpperCase();
1201
+ if (performance.now() - hidden.hiddenAt < MEASURE_SETTLE_MS) saved.textContent = "measuring…";
1202
+ else {
1203
+ const now = meanOfLast(this.monitor.getHistory(hidden.metric), MEASURE_FRAMES);
1204
+ const delta = hidden.baseline - now;
1205
+ saved.textContent = `${delta >= 0 ? "−" : "+"}${Math.abs(delta).toFixed(1)} ms ${label}`;
1206
+ saved.title = `${label} time went from ${hidden.baseline.toFixed(1)} ms to ${now.toFixed(1)} ms with this hidden, averaged over ${MEASURE_FRAMES} frames each side.`;
1207
+ }
1208
+ row.append(saved, this.eyeButton(hidden.entry, true));
1209
+ return row;
1210
+ }
1211
+ eyeButton(entry, hidden) {
1212
+ const eye = document.createElement("button");
1213
+ eye.type = "button";
1214
+ eye.className = "perf-monitor__cost-eye";
1215
+ eye.setAttribute("aria-pressed", String(hidden));
1216
+ const label = hidden ? `Show ${entry.label} again` : `Hide ${entry.label} to measure its cost`;
1217
+ eye.title = label;
1218
+ eye.setAttribute("aria-label", label);
1219
+ eye.append(createIcon(hidden ? "eyeOff" : "eye", "perf-monitor__icon"));
1220
+ eye.addEventListener("click", () => hidden ? this.showRow(entry.key) : this.hideRow(entry));
1221
+ return eye;
1222
+ }
1223
+ hideRow(entry) {
1224
+ const metric = this.monitor.getSample().gpu.available ? "gpu" : "cpu";
1225
+ this.hiddenRows.set(entry.key, {
1226
+ baseline: meanOfLast(this.monitor.getHistory(metric), MEASURE_FRAMES),
1227
+ entry,
1228
+ hiddenAt: performance.now(),
1229
+ metric
1230
+ });
1231
+ const all = this.monitor.getObjectsForKey(entry.key);
1232
+ this.hider.hide(entry.key, all.length > 0 ? all : entry.objects);
1233
+ this.renderCosts();
1234
+ }
1235
+ showRow(key) {
1236
+ this.hider.show(key);
1237
+ this.hiddenRows.delete(key);
1238
+ this.renderCosts();
1239
+ }
1240
+ /** Everything hidden goes back: on closing top costs and on dispose. */
1241
+ showAllHidden() {
1242
+ this.hider.restoreAll();
1243
+ this.hiddenRows.clear();
1095
1244
  }
1096
1245
  costsNote(text) {
1097
1246
  const note = document.createElement("div");
@@ -1117,11 +1266,17 @@ var PerformanceView = class {
1117
1266
  this.footerGpuEl.title = gpu;
1118
1267
  this.footerHardwareEl.hidden = backend === "" && gpu === "";
1119
1268
  }
1120
- renderHud(sample) {
1121
- const stats = this.hudNumberEls.size > 0 ? this.monitor.getFrameStats() : null;
1269
+ /** Budgets follow the detected refresh rate unless `targetFps` was given. */
1270
+ followRefresh(hz) {
1271
+ if (hz === this.detectedHz) return;
1272
+ this.detectedHz = hz;
1273
+ this.budgets = resolveBudgets(this.budgetsInput, hz);
1274
+ this.applyBudgetTitles();
1275
+ }
1276
+ renderHud(sample, stats) {
1122
1277
  for (const [key, element] of this.hudNumberEls) {
1123
1278
  const config = NUMBER_BY_KEY.get(key);
1124
- if (config && stats) this.paintNumber(config, element, sample, stats);
1279
+ if (config) this.paintNumber(config, element, sample, stats);
1125
1280
  }
1126
1281
  for (const [metric, graph] of this.hudGraphs) {
1127
1282
  const config = TIMINGS.find((timing) => timing.metric === metric);
@@ -1129,7 +1284,7 @@ var PerformanceView = class {
1129
1284
  }
1130
1285
  }
1131
1286
  paintNumber(config, element, sample, stats) {
1132
- const value = config.value(sample, stats);
1287
+ const value = config.value(sample, stats, this.budgets.frameMs);
1133
1288
  element.textContent = value === null ? "—" : config.format(value);
1134
1289
  element.classList.toggle("is-over", isOverBudget(this.budgets, config.key, value));
1135
1290
  }
@@ -1369,6 +1524,7 @@ var PerformanceView = class {
1369
1524
  const checkbox = this.buildCheckbox(false, "Show the scene's biggest draw costs", (on) => {
1370
1525
  this.costsOpen = on;
1371
1526
  this.costsBodyEl.hidden = !on;
1527
+ if (!on) this.showAllHidden();
1372
1528
  if (on) this.renderCosts();
1373
1529
  });
1374
1530
  toggleRow.append(createIcon("flame", "perf-monitor__icon"), label, checkbox);
@@ -1412,6 +1568,7 @@ var PerformanceView = class {
1412
1568
  this.costSortButtons.set(sort, button);
1413
1569
  head.append(button);
1414
1570
  }
1571
+ head.append(document.createElement("span"));
1415
1572
  this.costsListEl = document.createElement("div");
1416
1573
  this.costsListEl.className = "perf-monitor__costs-list";
1417
1574
  const hint = document.createElement("span");
@@ -1730,11 +1887,10 @@ const PERF_HUD_STYLES = `
1730
1887
  }
1731
1888
 
1732
1889
  .perf-monitor__cost {
1733
- /* Rows are buttons, which shrink to their content without a width. */
1734
1890
  box-sizing: border-box;
1735
1891
  width: 100%;
1736
1892
  display: grid;
1737
- grid-template-columns: minmax(0, 1fr) 5ch 6ch;
1893
+ grid-template-columns: minmax(0, 1fr) 5ch 6ch 14px;
1738
1894
  gap: 6px;
1739
1895
  align-items: baseline;
1740
1896
  padding: 2px 6px;
@@ -1744,13 +1900,58 @@ const PERF_HUD_STYLES = `
1744
1900
  color: inherit;
1745
1901
  font: inherit;
1746
1902
  text-align: left;
1747
- cursor: pointer;
1903
+ cursor: default;
1748
1904
  }
1749
1905
 
1750
1906
  .perf-monitor__cost:not(.perf-monitor__cost--head):hover {
1751
1907
  background: var(--perf-row);
1752
1908
  }
1753
1909
 
1910
+ .perf-monitor__cost.is-hidden .perf-monitor__cost-name {
1911
+ opacity: 0.6;
1912
+ text-decoration: line-through;
1913
+ }
1914
+
1915
+ /* What hiding saved, across the calls and tris columns. */
1916
+ .perf-monitor__cost-saved {
1917
+ grid-column: 2 / span 2;
1918
+ color: var(--perf-accent);
1919
+ font-variant-numeric: tabular-nums lining-nums;
1920
+ text-align: right;
1921
+ white-space: nowrap;
1922
+ }
1923
+
1924
+ .perf-monitor__cost-eye {
1925
+ display: flex;
1926
+ align-items: center;
1927
+ justify-content: center;
1928
+ width: 14px;
1929
+ height: 14px;
1930
+ padding: 0;
1931
+ border: 0;
1932
+ border-radius: 3px;
1933
+ background: transparent;
1934
+ color: var(--perf-muted);
1935
+ cursor: pointer;
1936
+ align-self: center;
1937
+ }
1938
+
1939
+ .perf-monitor__cost-eye .perf-monitor__icon {
1940
+ width: 12px;
1941
+ height: 12px;
1942
+ color: inherit;
1943
+ }
1944
+
1945
+ .perf-monitor__cost-eye:hover,
1946
+ .perf-monitor__cost-eye[aria-pressed="true"] {
1947
+ color: var(--perf-fg);
1948
+ }
1949
+
1950
+ .perf-monitor__cost-eye:focus-visible {
1951
+ outline: 1px solid color-mix(in srgb, var(--perf-accent) 60%, transparent);
1952
+ outline-offset: 1px;
1953
+ }
1954
+
1754
1955
  .perf-monitor__cost:focus-visible {
1755
1956
  outline: 1px solid color-mix(in srgb, var(--perf-accent) 60%, transparent);
1756
1957
  outline-offset: -1px;
@@ -1761,13 +1962,30 @@ const PERF_HUD_STYLES = `
1761
1962
  cursor: default;
1762
1963
  }
1763
1964
 
1965
+ /* The name is a button: a click logs the row's objects. */
1764
1966
  .perf-monitor__cost-name {
1967
+ min-width: 0;
1968
+ padding: 0;
1969
+ border: 0;
1970
+ background: transparent;
1971
+ font: inherit;
1972
+ text-align: left;
1973
+ cursor: pointer;
1765
1974
  overflow: hidden;
1766
1975
  text-overflow: ellipsis;
1767
1976
  white-space: nowrap;
1768
1977
  color: var(--perf-fg-dim);
1769
1978
  }
1770
1979
 
1980
+ .perf-monitor__cost-name:hover {
1981
+ color: var(--perf-fg);
1982
+ }
1983
+
1984
+ .perf-monitor__cost-name:focus-visible {
1985
+ outline: 1px solid color-mix(in srgb, var(--perf-accent) 60%, transparent);
1986
+ outline-offset: 1px;
1987
+ }
1988
+
1771
1989
  .perf-monitor__cost-copies {
1772
1990
  color: var(--perf-muted);
1773
1991
  }
@@ -2234,4 +2452,4 @@ const mountPerfHud = (monitor, options = {}) => {
2234
2452
  //#endregion
2235
2453
  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 };
2236
2454
 
2237
- //# sourceMappingURL=mount-perf-hud-CebnfB8t.js.map
2455
+ //# sourceMappingURL=mount-perf-hud-U_Xlj0Od.js.map