@zkmake/three-meter 0.6.0 → 0.8.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 +120 -64
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{mount-perf-hud-C5wdjgmM.js → mount-perf-hud-B_cmTG2M.js} +390 -39
- package/dist/mount-perf-hud-B_cmTG2M.js.map +1 -0
- package/dist/{mount-perf-hud-CmINOH3T.d.ts → mount-perf-hud-DkSNfZmB.d.ts} +60 -3
- package/dist/{performance-monitor-CBN05_BC.d.ts → performance-monitor-DgHxeQ2X.d.ts} +42 -2
- package/dist/{performance-monitor-yxsxs57v.js → performance-monitor-DsBp13ts.js} +204 -1
- package/dist/performance-monitor-DsBp13ts.js.map +1 -0
- package/dist/react.d.ts +6 -5
- package/dist/react.js +14 -5
- package/dist/react.js.map +1 -1
- package/dist/styles.css +99 -5
- package/dist/ui.d.ts +4 -4
- package/dist/ui.js +2 -2
- package/package.json +1 -1
- package/dist/mount-perf-hud-C5wdjgmM.js.map +0 -1
- package/dist/performance-monitor-yxsxs57v.js.map +0 -1
|
@@ -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,6 +486,7 @@ const dockPanel = (element, options = {}) => {
|
|
|
453
486
|
//#endregion
|
|
454
487
|
//#region src/ui/icons.ts
|
|
455
488
|
const ICON_MARKUP = {
|
|
489
|
+
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\"/>",
|
|
456
490
|
activity: "<path d=\"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2\"/>",
|
|
457
491
|
box: "<path d=\"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z\"/><path d=\"m3.3 7 8.7 5 8.7-5\"/><path d=\"M12 22V12\"/>",
|
|
458
492
|
circleDot: "<circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"1\"/>",
|
|
@@ -493,7 +527,7 @@ const createIcon = (name, className) => {
|
|
|
493
527
|
};
|
|
494
528
|
//#endregion
|
|
495
529
|
//#region package.json
|
|
496
|
-
var version = "0.
|
|
530
|
+
var version = "0.8.0";
|
|
497
531
|
//#endregion
|
|
498
532
|
//#region src/ui/clipboard.ts
|
|
499
533
|
/**
|
|
@@ -571,7 +605,7 @@ const METRIC_HELP = {
|
|
|
571
605
|
//#region src/ui/report.ts
|
|
572
606
|
/**
|
|
573
607
|
* A plain-text snapshot of the monitor for bug reports: versions, backend and
|
|
574
|
-
* GPU, the current sample, stutter stats and the page. Fenced as a `text`
|
|
608
|
+
* GPU, the current sample, stutter stats, the scene's top meshes and the page. Fenced as a `text`
|
|
575
609
|
* code block so GitHub and Slack keep the line breaks.
|
|
576
610
|
*/
|
|
577
611
|
const BACKEND_LABELS = {
|
|
@@ -586,7 +620,13 @@ const backendLabel = (environment) => {
|
|
|
586
620
|
return environment.fallback ? `${label} fallback` : label;
|
|
587
621
|
};
|
|
588
622
|
const ms = (value) => `${value.toFixed(1)} ms`;
|
|
589
|
-
|
|
623
|
+
/** Top meshes by draw calls, three at most: `Top: cubes ×2,000 (1 call, 24K tris) · …`. */
|
|
624
|
+
const topMeshes = (cost) => `Top: ${cost.meshes.slice(0, 3).map((entry) => {
|
|
625
|
+
const copies = entry.instances > 1 ? ` ×${formatCount(entry.instances)}` : "";
|
|
626
|
+
const calls = `${formatCount(entry.calls)} ${entry.calls === 1 ? "call" : "calls"}`;
|
|
627
|
+
return `${entry.label}${copies} (${calls}, ${formatCount(entry.triangles)} tris)`;
|
|
628
|
+
}).join(" · ")}`;
|
|
629
|
+
const renderReport = ({ cost, environment, page, sample, stats }) => {
|
|
590
630
|
const { render, resources } = sample;
|
|
591
631
|
const heading = [
|
|
592
632
|
`three-meter v${version}`,
|
|
@@ -617,6 +657,7 @@ const renderReport = ({ environment, page, sample, stats }) => {
|
|
|
617
657
|
`shaders ${formatCount(resources.programs)}`
|
|
618
658
|
].join(" · ")
|
|
619
659
|
];
|
|
660
|
+
if (cost && cost.meshes.length > 0) lines.push(topMeshes(cost));
|
|
620
661
|
if (page) lines.push(`Viewport ${page.width}×${page.height} @${page.dpr}x`, page.userAgent);
|
|
621
662
|
return [
|
|
622
663
|
"```text",
|
|
@@ -632,6 +673,7 @@ const readPage = () => typeof window === "undefined" ? null : {
|
|
|
632
673
|
};
|
|
633
674
|
/** The report for `monitor` right now, as Markdown ready to paste into an issue. */
|
|
634
675
|
const formatReport = (monitor) => renderReport({
|
|
676
|
+
cost: monitor.getSceneCost(),
|
|
635
677
|
environment: monitor.getEnvironment(),
|
|
636
678
|
page: readPage(),
|
|
637
679
|
sample: monitor.getSample(),
|
|
@@ -639,7 +681,7 @@ const formatReport = (monitor) => renderReport({
|
|
|
639
681
|
});
|
|
640
682
|
//#endregion
|
|
641
683
|
//#region src/ui/sparkline.ts
|
|
642
|
-
function drawSparkline(ctx, width, height, values, style) {
|
|
684
|
+
function drawSparkline(ctx, width, height, values, style, guide) {
|
|
643
685
|
ctx.clearRect(0, 0, width, height);
|
|
644
686
|
if (values.length < 2) return;
|
|
645
687
|
let max = 0;
|
|
@@ -658,6 +700,17 @@ function drawSparkline(ctx, width, height, values, style) {
|
|
|
658
700
|
ctx.closePath();
|
|
659
701
|
ctx.fillStyle = style.fill;
|
|
660
702
|
ctx.fill();
|
|
703
|
+
if (guide && guide.value > 0 && guide.value <= range) {
|
|
704
|
+
const y = Math.round(toY(guide.value)) + .5;
|
|
705
|
+
ctx.save();
|
|
706
|
+
ctx.setLineDash([3, 3]);
|
|
707
|
+
ctx.strokeStyle = guide.color;
|
|
708
|
+
ctx.beginPath();
|
|
709
|
+
ctx.moveTo(0, y);
|
|
710
|
+
ctx.lineTo(width, y);
|
|
711
|
+
ctx.stroke();
|
|
712
|
+
ctx.restore();
|
|
713
|
+
}
|
|
661
714
|
}
|
|
662
715
|
//#endregion
|
|
663
716
|
//#region src/ui/performance-view.ts
|
|
@@ -734,6 +787,24 @@ const THEME_OPTIONS = [
|
|
|
734
787
|
const RELEASE_URL = `https://github.com/zkmake/three-meter/releases/tag/v${version}`;
|
|
735
788
|
/** How long the report button says `copied` / `failed` before reading `copy` again. */
|
|
736
789
|
const COPY_FEEDBACK_MS = 1500;
|
|
790
|
+
/** Walking the scene graph isn't free; the top-costs list refreshes at 2 Hz while open. */
|
|
791
|
+
const COSTS_INTERVAL_MS = 500;
|
|
792
|
+
const COSTS_ROWS = 5;
|
|
793
|
+
const COST_GROUPS = [{
|
|
794
|
+
group: "meshes",
|
|
795
|
+
label: "meshes"
|
|
796
|
+
}, {
|
|
797
|
+
group: "materials",
|
|
798
|
+
label: "materials"
|
|
799
|
+
}];
|
|
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.";
|
|
801
|
+
/** `×2,000` for instances or copies; nothing for a single object. */
|
|
802
|
+
const copiesOf = (entry) => entry.instances > 1 ? `×${formatCount(entry.instances)}` : "";
|
|
803
|
+
const costTitle = (entry) => {
|
|
804
|
+
const objects = entry.objects.length;
|
|
805
|
+
const shape = entry.instances > objects ? `${formatCount(entry.instances)} instances in ${objects === 1 ? "one object" : `${objects} objects`}` : `${formatCount(objects)} ${objects === 1 ? "object" : "separate objects"}`;
|
|
806
|
+
return `${entry.label}: ${shape}, ${formatCount(entry.calls)} draw ${entry.calls === 1 ? "call" : "calls"}, ${formatCount(entry.triangles)} triangles. Click to log to the console.`;
|
|
807
|
+
};
|
|
737
808
|
const readTiming = (sample, metric) => {
|
|
738
809
|
switch (metric) {
|
|
739
810
|
case "fps": return sample.fps;
|
|
@@ -742,92 +813,130 @@ const readTiming = (sample, metric) => {
|
|
|
742
813
|
}
|
|
743
814
|
};
|
|
744
815
|
const isTimingAvailable = (sample, metric) => metric !== "gpu" || sample.gpu.available;
|
|
816
|
+
const whole = (value) => Math.round(value).toString();
|
|
817
|
+
const tenths = (value) => value.toFixed(1);
|
|
745
818
|
const NUMBERS = [
|
|
746
819
|
{
|
|
820
|
+
format: whole,
|
|
747
821
|
icon: "gauge",
|
|
748
822
|
key: "fps",
|
|
749
823
|
label: "FPS",
|
|
750
|
-
|
|
824
|
+
value: (sample) => sample.fps || null
|
|
751
825
|
},
|
|
752
826
|
{
|
|
827
|
+
format: formatCount,
|
|
753
828
|
icon: "layers",
|
|
754
829
|
key: "calls",
|
|
755
830
|
label: "Calls",
|
|
756
|
-
|
|
831
|
+
value: (sample) => sample.render.calls
|
|
757
832
|
},
|
|
758
833
|
{
|
|
834
|
+
format: tenths,
|
|
759
835
|
icon: "cpu",
|
|
760
836
|
key: "cpu",
|
|
761
837
|
label: "CPU",
|
|
762
|
-
|
|
838
|
+
unit: "ms",
|
|
839
|
+
value: (sample) => sample.cpu
|
|
763
840
|
},
|
|
764
841
|
{
|
|
842
|
+
format: tenths,
|
|
765
843
|
icon: "zap",
|
|
766
844
|
key: "gpu",
|
|
767
845
|
label: "GPU",
|
|
768
|
-
|
|
846
|
+
unit: "ms",
|
|
847
|
+
value: (sample) => sample.gpu.available ? sample.gpu.ms : null
|
|
769
848
|
},
|
|
770
849
|
{
|
|
850
|
+
format: whole,
|
|
771
851
|
icon: "trendingDown",
|
|
772
852
|
key: "low",
|
|
773
853
|
label: "1% low",
|
|
774
|
-
|
|
854
|
+
value: (_sample, stats) => stats.frames ? stats.lowFps : null
|
|
775
855
|
},
|
|
776
856
|
{
|
|
857
|
+
format: tenths,
|
|
777
858
|
icon: "timer",
|
|
778
859
|
key: "p99",
|
|
779
860
|
label: "Frame p99",
|
|
780
|
-
|
|
861
|
+
unit: "ms",
|
|
862
|
+
value: (_sample, stats) => stats.frames ? stats.p99Ms : null
|
|
781
863
|
},
|
|
782
864
|
{
|
|
865
|
+
format: formatCount,
|
|
783
866
|
icon: "activity",
|
|
784
867
|
key: "hitches",
|
|
785
868
|
label: "Hitches",
|
|
786
|
-
|
|
869
|
+
value: (_sample, stats) => stats.frames ? stats.hitches : null
|
|
787
870
|
},
|
|
788
871
|
{
|
|
872
|
+
format: formatCount,
|
|
789
873
|
icon: "triangle",
|
|
790
874
|
key: "triangles",
|
|
791
875
|
label: "Triangles",
|
|
792
|
-
|
|
876
|
+
value: (sample) => sample.render.triangles
|
|
793
877
|
},
|
|
794
878
|
{
|
|
879
|
+
format: formatCount,
|
|
795
880
|
icon: "spline",
|
|
796
881
|
key: "lines",
|
|
797
882
|
label: "Lines",
|
|
798
|
-
|
|
883
|
+
value: (sample) => sample.render.lines
|
|
799
884
|
},
|
|
800
885
|
{
|
|
886
|
+
format: formatCount,
|
|
801
887
|
icon: "circleDot",
|
|
802
888
|
key: "points",
|
|
803
889
|
label: "Points",
|
|
804
|
-
|
|
890
|
+
value: (sample) => sample.render.points
|
|
805
891
|
},
|
|
806
892
|
{
|
|
893
|
+
format: formatCount,
|
|
807
894
|
icon: "repeat",
|
|
808
895
|
key: "passes",
|
|
809
896
|
label: "Render passes",
|
|
810
|
-
|
|
897
|
+
value: (sample) => sample.render.passes
|
|
811
898
|
},
|
|
812
899
|
{
|
|
900
|
+
format: formatCount,
|
|
813
901
|
icon: "box",
|
|
814
902
|
key: "geometries",
|
|
815
903
|
label: "Geometries",
|
|
816
|
-
|
|
904
|
+
value: (sample) => sample.resources.geometries
|
|
817
905
|
},
|
|
818
906
|
{
|
|
907
|
+
format: formatCount,
|
|
819
908
|
icon: "image",
|
|
820
909
|
key: "textures",
|
|
821
910
|
label: "Textures",
|
|
822
|
-
|
|
911
|
+
value: (sample) => sample.resources.textures
|
|
823
912
|
},
|
|
824
913
|
{
|
|
914
|
+
format: formatCount,
|
|
825
915
|
icon: "palette",
|
|
826
916
|
key: "shaders",
|
|
827
917
|
label: "Shaders",
|
|
828
|
-
|
|
918
|
+
value: (sample) => sample.resources.programs
|
|
829
919
|
}
|
|
830
920
|
];
|
|
921
|
+
const NUMBER_BY_KEY = new Map(NUMBERS.map((config) => [config.key, config]));
|
|
922
|
+
/** Canvas can't read CSS custom properties; the budget line's amber per theme. */
|
|
923
|
+
const GUIDE_COLOR = {
|
|
924
|
+
dark: "rgba(245, 158, 11, 0.75)",
|
|
925
|
+
light: "rgba(180, 83, 9, 0.7)"
|
|
926
|
+
};
|
|
927
|
+
/** `Budget: at most 16.7 ms` / `at least 57`, or `""` without one. */
|
|
928
|
+
const budgetTitle = (budgets, key) => {
|
|
929
|
+
const limit = budgets.limits[key];
|
|
930
|
+
const config = NUMBER_BY_KEY.get(key);
|
|
931
|
+
if (limit === void 0 || !config) return "";
|
|
932
|
+
return `Budget: ${isFloor(key) ? "at least" : "at most"} ${config.format(limit)}${config.unit ? ` ${config.unit}` : ""}`;
|
|
933
|
+
};
|
|
934
|
+
/** The sample value a timing graph shows, `null` when not known. */
|
|
935
|
+
const timingValue = (sample, metric) => {
|
|
936
|
+
if (!isTimingAvailable(sample, metric)) return null;
|
|
937
|
+
const value = readTiming(sample, metric);
|
|
938
|
+
return metric === "fps" && value === 0 ? null : value;
|
|
939
|
+
};
|
|
831
940
|
/**
|
|
832
941
|
* Host-agnostic widget that paints {@link Sample}s. Mount {@link element}
|
|
833
942
|
* anywhere; the view drives its own throttled repaint. Compact is the floating
|
|
@@ -860,10 +969,19 @@ var PerformanceView = class {
|
|
|
860
969
|
hudGridEl;
|
|
861
970
|
hudGraphs = /* @__PURE__ */ new Map();
|
|
862
971
|
hudNumberEls = /* @__PURE__ */ new Map();
|
|
972
|
+
budgets;
|
|
863
973
|
rafId = null;
|
|
864
974
|
lastPaintAt = 0;
|
|
865
975
|
resizeObserver;
|
|
866
976
|
unsubscribe;
|
|
977
|
+
costsOpen = false;
|
|
978
|
+
costGroup = "meshes";
|
|
979
|
+
costSort = "calls";
|
|
980
|
+
lastCostsAt = -Infinity;
|
|
981
|
+
costsBodyEl;
|
|
982
|
+
costsListEl;
|
|
983
|
+
costGroupRadios = /* @__PURE__ */ new Map();
|
|
984
|
+
costSortButtons = /* @__PURE__ */ new Map();
|
|
867
985
|
constructor(options) {
|
|
868
986
|
this.monitor = options.monitor;
|
|
869
987
|
this.settings = options.settings ?? new HudSettings();
|
|
@@ -872,6 +990,7 @@ var PerformanceView = class {
|
|
|
872
990
|
this.theme.setOverride(this.settings.theme);
|
|
873
991
|
this.mode = options.mode ?? "full";
|
|
874
992
|
this.minIntervalMs = 1e3 / (options.refreshHz ?? 10);
|
|
993
|
+
this.budgets = resolveBudgets(options.budgets);
|
|
875
994
|
this.element = document.createElement("div");
|
|
876
995
|
this.element.className = "perf-monitor";
|
|
877
996
|
this.applyModeClass();
|
|
@@ -901,6 +1020,11 @@ var PerformanceView = class {
|
|
|
901
1020
|
getMode() {
|
|
902
1021
|
return this.mode;
|
|
903
1022
|
}
|
|
1023
|
+
/** Replace the budgets; `false` turns them all off. Applies on the next paint. */
|
|
1024
|
+
setBudgets(budgets) {
|
|
1025
|
+
this.budgets = resolveBudgets(budgets);
|
|
1026
|
+
this.applyBudgetTitles();
|
|
1027
|
+
}
|
|
904
1028
|
dispose() {
|
|
905
1029
|
this.stop();
|
|
906
1030
|
this.unsubscribe();
|
|
@@ -922,17 +1046,60 @@ var PerformanceView = class {
|
|
|
922
1046
|
if (this.settings.info) this.renderFooter();
|
|
923
1047
|
return;
|
|
924
1048
|
}
|
|
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
|
-
}
|
|
1049
|
+
for (const config of TIMINGS) this.paintGraph(config, sample, this.graphValueEls.get(config.metric), this.graphCanvases.get(config.metric), "unavailable");
|
|
932
1050
|
const stats = this.monitor.getFrameStats();
|
|
933
|
-
for (const config of NUMBERS) this.statValueEls.get(config.key)
|
|
1051
|
+
for (const config of NUMBERS) this.paintNumber(config, this.statValueEls.get(config.key), sample, stats);
|
|
1052
|
+
if (this.costsOpen && performance.now() - this.lastCostsAt >= COSTS_INTERVAL_MS) this.renderCosts();
|
|
934
1053
|
this.renderFooter();
|
|
935
1054
|
}
|
|
1055
|
+
renderCosts() {
|
|
1056
|
+
this.lastCostsAt = performance.now();
|
|
1057
|
+
const cost = this.monitor.getSceneCost();
|
|
1058
|
+
if (!cost) {
|
|
1059
|
+
this.costsListEl.replaceChildren(this.costsNote("No scene rendered yet."));
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
const entries = [...this.costGroup === "meshes" ? cost.meshes : cost.materials];
|
|
1063
|
+
if (this.costSort === "triangles") entries.sort((a, b) => b.triangles - a.triangles || b.calls - a.calls);
|
|
1064
|
+
if (entries.length === 0) {
|
|
1065
|
+
this.costsListEl.replaceChildren(this.costsNote("Nothing drawn."));
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
const rows = entries.slice(0, COSTS_ROWS).map((entry) => {
|
|
1069
|
+
const row = document.createElement("button");
|
|
1070
|
+
row.type = "button";
|
|
1071
|
+
row.className = "perf-monitor__cost";
|
|
1072
|
+
row.title = costTitle(entry);
|
|
1073
|
+
const name = document.createElement("span");
|
|
1074
|
+
name.className = "perf-monitor__cost-name";
|
|
1075
|
+
name.textContent = entry.label;
|
|
1076
|
+
const copies = copiesOf(entry);
|
|
1077
|
+
if (copies) {
|
|
1078
|
+
const suffix = document.createElement("span");
|
|
1079
|
+
suffix.className = "perf-monitor__cost-copies";
|
|
1080
|
+
suffix.textContent = ` ${copies}`;
|
|
1081
|
+
name.append(suffix);
|
|
1082
|
+
}
|
|
1083
|
+
const calls = document.createElement("span");
|
|
1084
|
+
calls.className = "perf-monitor__cost-value";
|
|
1085
|
+
calls.textContent = formatCount(entry.calls);
|
|
1086
|
+
const triangles = document.createElement("span");
|
|
1087
|
+
triangles.className = "perf-monitor__cost-value";
|
|
1088
|
+
triangles.textContent = formatCount(entry.triangles);
|
|
1089
|
+
row.append(name, calls, triangles);
|
|
1090
|
+
row.addEventListener("click", () => {
|
|
1091
|
+
console.log(`three-meter: ${entry.label}`, entry.objects);
|
|
1092
|
+
});
|
|
1093
|
+
return row;
|
|
1094
|
+
});
|
|
1095
|
+
this.costsListEl.replaceChildren(...rows);
|
|
1096
|
+
}
|
|
1097
|
+
costsNote(text) {
|
|
1098
|
+
const note = document.createElement("div");
|
|
1099
|
+
note.className = "perf-monitor__costs-note";
|
|
1100
|
+
note.textContent = text;
|
|
1101
|
+
return note;
|
|
1102
|
+
}
|
|
936
1103
|
/** The monitor caches the environment once its backend settles; until then this re-reads. */
|
|
937
1104
|
renderFooter() {
|
|
938
1105
|
const environment = this.monitor.getEnvironment();
|
|
@@ -954,16 +1121,38 @@ var PerformanceView = class {
|
|
|
954
1121
|
renderHud(sample) {
|
|
955
1122
|
const stats = this.hudNumberEls.size > 0 ? this.monitor.getFrameStats() : null;
|
|
956
1123
|
for (const [key, element] of this.hudNumberEls) {
|
|
957
|
-
const config =
|
|
958
|
-
if (config && stats)
|
|
1124
|
+
const config = NUMBER_BY_KEY.get(key);
|
|
1125
|
+
if (config && stats) this.paintNumber(config, element, sample, stats);
|
|
959
1126
|
}
|
|
960
1127
|
for (const [metric, graph] of this.hudGraphs) {
|
|
961
1128
|
const config = TIMINGS.find((timing) => timing.metric === metric);
|
|
962
|
-
|
|
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]);
|
|
1129
|
+
this.paintGraph(config, sample, graph.valueEl, graph, "—");
|
|
965
1130
|
}
|
|
966
1131
|
}
|
|
1132
|
+
paintNumber(config, element, sample, stats) {
|
|
1133
|
+
const value = config.value(sample, stats);
|
|
1134
|
+
element.textContent = value === null ? "—" : config.format(value);
|
|
1135
|
+
element.classList.toggle("is-over", isOverBudget(this.budgets, config.key, value));
|
|
1136
|
+
}
|
|
1137
|
+
/** Value and sparkline, with the budget line drawn once the series reaches it. */
|
|
1138
|
+
paintGraph(config, sample, valueEl, graph, missing) {
|
|
1139
|
+
const value = timingValue(sample, config.metric);
|
|
1140
|
+
const available = isTimingAvailable(sample, config.metric);
|
|
1141
|
+
const limit = this.budgets.limits[config.metric];
|
|
1142
|
+
valueEl.textContent = available ? this.formatTiming(config, readTiming(sample, config.metric)) : missing;
|
|
1143
|
+
valueEl.classList.toggle("is-over", isOverBudget(this.budgets, config.metric, value));
|
|
1144
|
+
drawSparkline(graph.ctx, graph.width, graph.height, available ? this.monitor.getHistory(config.metric) : [], config.style[this.theme.resolved], limit === void 0 ? void 0 : {
|
|
1145
|
+
color: GUIDE_COLOR[this.theme.resolved],
|
|
1146
|
+
value: limit
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
/** Each value's tooltip names its budget, so amber explains itself. */
|
|
1150
|
+
applyBudgetTitles() {
|
|
1151
|
+
for (const [key, element] of this.statValueEls) element.title = budgetTitle(this.budgets, key);
|
|
1152
|
+
for (const [key, element] of this.hudNumberEls) element.title = budgetTitle(this.budgets, key);
|
|
1153
|
+
for (const [metric, element] of this.graphValueEls) element.title = budgetTitle(this.budgets, metric);
|
|
1154
|
+
for (const [metric, graph] of this.hudGraphs) graph.valueEl.title = budgetTitle(this.budgets, metric);
|
|
1155
|
+
}
|
|
967
1156
|
formatTiming(config, value) {
|
|
968
1157
|
return `${config.format(value)}${config.unit ? ` ${config.unit}` : ""}`;
|
|
969
1158
|
}
|
|
@@ -1102,7 +1291,7 @@ var PerformanceView = class {
|
|
|
1102
1291
|
this.footerHardwareEl.append(this.footerBackendEl, this.footerGpuEl);
|
|
1103
1292
|
footerBody.append(softwareRow, this.footerHardwareEl);
|
|
1104
1293
|
footer.append(createIcon("info", "perf-monitor__icon"), footerBody, this.infoCheckbox);
|
|
1105
|
-
this.element.append(hud, options, graphs, stats, footer);
|
|
1294
|
+
this.element.append(hud, options, graphs, stats, this.buildCosts(), footer);
|
|
1106
1295
|
}
|
|
1107
1296
|
rebuildHud() {
|
|
1108
1297
|
const { selection } = this.settings;
|
|
@@ -1156,6 +1345,7 @@ var PerformanceView = class {
|
|
|
1156
1345
|
this.hudGridEl.append(item);
|
|
1157
1346
|
this.hudNumberEls.set(config.key, value);
|
|
1158
1347
|
}
|
|
1348
|
+
this.applyBudgetTitles();
|
|
1159
1349
|
this.resizeCanvases();
|
|
1160
1350
|
}
|
|
1161
1351
|
onSelectionChanged() {
|
|
@@ -1172,6 +1362,71 @@ var PerformanceView = class {
|
|
|
1172
1362
|
applyTheme(this.element, this.theme);
|
|
1173
1363
|
for (const [mode, radio] of this.themeRadios) radio.setAttribute("aria-checked", String(mode === this.theme.effective));
|
|
1174
1364
|
}
|
|
1365
|
+
/** "top costs": a toggle row, then meshes / materials and a sortable top five. */
|
|
1366
|
+
buildCosts() {
|
|
1367
|
+
const section = document.createElement("div");
|
|
1368
|
+
section.className = "perf-monitor__section perf-monitor__costs";
|
|
1369
|
+
const toggleRow = document.createElement("label");
|
|
1370
|
+
toggleRow.className = "perf-monitor__row";
|
|
1371
|
+
toggleRow.title = COSTS_HELP;
|
|
1372
|
+
const label = document.createElement("span");
|
|
1373
|
+
label.className = "perf-monitor__label";
|
|
1374
|
+
label.textContent = "top costs";
|
|
1375
|
+
const checkbox = this.buildCheckbox(false, "Show the scene's biggest draw costs", (on) => {
|
|
1376
|
+
this.costsOpen = on;
|
|
1377
|
+
this.costsBodyEl.hidden = !on;
|
|
1378
|
+
if (on) this.renderCosts();
|
|
1379
|
+
});
|
|
1380
|
+
toggleRow.append(createIcon("flame", "perf-monitor__icon"), label, checkbox);
|
|
1381
|
+
this.costsBodyEl = document.createElement("div");
|
|
1382
|
+
this.costsBodyEl.className = "perf-monitor__costs-body";
|
|
1383
|
+
this.costsBodyEl.hidden = true;
|
|
1384
|
+
const head = document.createElement("div");
|
|
1385
|
+
head.className = "perf-monitor__cost perf-monitor__cost--head";
|
|
1386
|
+
const segment = document.createElement("div");
|
|
1387
|
+
segment.className = "perf-monitor__segment";
|
|
1388
|
+
segment.setAttribute("role", "radiogroup");
|
|
1389
|
+
segment.setAttribute("aria-label", "Group costs by");
|
|
1390
|
+
for (const option of COST_GROUPS) {
|
|
1391
|
+
const radio = document.createElement("button");
|
|
1392
|
+
radio.type = "button";
|
|
1393
|
+
radio.className = "perf-monitor__segment-option perf-monitor__segment-option--text";
|
|
1394
|
+
radio.setAttribute("role", "radio");
|
|
1395
|
+
radio.setAttribute("aria-checked", String(option.group === this.costGroup));
|
|
1396
|
+
radio.textContent = option.label;
|
|
1397
|
+
radio.addEventListener("click", () => {
|
|
1398
|
+
this.costGroup = option.group;
|
|
1399
|
+
for (const [group, button] of this.costGroupRadios) button.setAttribute("aria-checked", String(group === option.group));
|
|
1400
|
+
this.renderCosts();
|
|
1401
|
+
});
|
|
1402
|
+
this.costGroupRadios.set(option.group, radio);
|
|
1403
|
+
segment.append(radio);
|
|
1404
|
+
}
|
|
1405
|
+
head.append(segment);
|
|
1406
|
+
for (const sort of ["calls", "triangles"]) {
|
|
1407
|
+
const button = document.createElement("button");
|
|
1408
|
+
button.type = "button";
|
|
1409
|
+
button.className = "perf-monitor__cost-sort";
|
|
1410
|
+
button.textContent = sort === "calls" ? "calls" : "tris";
|
|
1411
|
+
button.title = `Sort by ${sort === "calls" ? "draw calls" : "triangles"}`;
|
|
1412
|
+
button.setAttribute("aria-pressed", String(sort === this.costSort));
|
|
1413
|
+
button.addEventListener("click", () => {
|
|
1414
|
+
this.costSort = sort;
|
|
1415
|
+
for (const [key, other] of this.costSortButtons) other.setAttribute("aria-pressed", String(key === sort));
|
|
1416
|
+
this.renderCosts();
|
|
1417
|
+
});
|
|
1418
|
+
this.costSortButtons.set(sort, button);
|
|
1419
|
+
head.append(button);
|
|
1420
|
+
}
|
|
1421
|
+
this.costsListEl = document.createElement("div");
|
|
1422
|
+
this.costsListEl.className = "perf-monitor__costs-list";
|
|
1423
|
+
const hint = document.createElement("span");
|
|
1424
|
+
hint.className = "perf-monitor__hint";
|
|
1425
|
+
hint.textContent = COSTS_HELP;
|
|
1426
|
+
this.costsBodyEl.append(head, this.costsListEl, hint);
|
|
1427
|
+
section.append(toggleRow, this.costsBodyEl);
|
|
1428
|
+
return section;
|
|
1429
|
+
}
|
|
1175
1430
|
buildReportRow() {
|
|
1176
1431
|
const row = document.createElement("div");
|
|
1177
1432
|
row.className = "perf-monitor__row perf-monitor__row--static";
|
|
@@ -1256,6 +1511,7 @@ const PERF_HUD_STYLES = `
|
|
|
1256
1511
|
--perf-row: rgba(255, 255, 255, 0.04);
|
|
1257
1512
|
--perf-border: rgba(255, 255, 255, 0.12);
|
|
1258
1513
|
--perf-accent: #60a5fa;
|
|
1514
|
+
--perf-warn: #f59e0b;
|
|
1259
1515
|
--perf-shadow: 0 8px 24px rgba(0, 0, 0, 0.32);
|
|
1260
1516
|
|
|
1261
1517
|
color-scheme: dark;
|
|
@@ -1270,6 +1526,7 @@ const PERF_HUD_STYLES = `
|
|
|
1270
1526
|
--perf-row: rgba(0, 0, 0, 0.05);
|
|
1271
1527
|
--perf-border: rgba(0, 0, 0, 0.12);
|
|
1272
1528
|
--perf-accent: #2563eb;
|
|
1529
|
+
--perf-warn: #b45309;
|
|
1273
1530
|
--perf-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
|
|
1274
1531
|
|
|
1275
1532
|
color-scheme: light;
|
|
@@ -1466,6 +1723,96 @@ const PERF_HUD_STYLES = `
|
|
|
1466
1723
|
outline-offset: 1px;
|
|
1467
1724
|
}
|
|
1468
1725
|
|
|
1726
|
+
/* Top costs: name, calls, triangles. Rows are buttons that log their objects. */
|
|
1727
|
+
.perf-monitor__costs-body {
|
|
1728
|
+
display: flex;
|
|
1729
|
+
flex-direction: column;
|
|
1730
|
+
gap: 1px;
|
|
1731
|
+
padding-top: 2px;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
.perf-monitor__costs-body[hidden] {
|
|
1735
|
+
display: none;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
.perf-monitor__cost {
|
|
1739
|
+
/* Rows are buttons, which shrink to their content without a width. */
|
|
1740
|
+
box-sizing: border-box;
|
|
1741
|
+
width: 100%;
|
|
1742
|
+
display: grid;
|
|
1743
|
+
grid-template-columns: minmax(0, 1fr) 5ch 6ch;
|
|
1744
|
+
gap: 6px;
|
|
1745
|
+
align-items: baseline;
|
|
1746
|
+
padding: 2px 6px;
|
|
1747
|
+
border: 0;
|
|
1748
|
+
border-radius: 3px;
|
|
1749
|
+
background: transparent;
|
|
1750
|
+
color: inherit;
|
|
1751
|
+
font: inherit;
|
|
1752
|
+
text-align: left;
|
|
1753
|
+
cursor: pointer;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
.perf-monitor__cost:not(.perf-monitor__cost--head):hover {
|
|
1757
|
+
background: var(--perf-row);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
.perf-monitor__cost:focus-visible {
|
|
1761
|
+
outline: 1px solid color-mix(in srgb, var(--perf-accent) 60%, transparent);
|
|
1762
|
+
outline-offset: -1px;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
.perf-monitor__cost--head {
|
|
1766
|
+
align-items: center;
|
|
1767
|
+
cursor: default;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
.perf-monitor__cost-name {
|
|
1771
|
+
overflow: hidden;
|
|
1772
|
+
text-overflow: ellipsis;
|
|
1773
|
+
white-space: nowrap;
|
|
1774
|
+
color: var(--perf-fg-dim);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
.perf-monitor__cost-copies {
|
|
1778
|
+
color: var(--perf-muted);
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
.perf-monitor__cost-value {
|
|
1782
|
+
font-variant-numeric: tabular-nums lining-nums;
|
|
1783
|
+
text-align: right;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
.perf-monitor__cost-sort {
|
|
1787
|
+
padding: 0;
|
|
1788
|
+
border: 0;
|
|
1789
|
+
background: transparent;
|
|
1790
|
+
color: var(--perf-muted);
|
|
1791
|
+
font: inherit;
|
|
1792
|
+
font-size: 10px;
|
|
1793
|
+
text-align: right;
|
|
1794
|
+
cursor: pointer;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
.perf-monitor__cost-sort[aria-pressed="true"] {
|
|
1798
|
+
color: var(--perf-fg);
|
|
1799
|
+
text-decoration: underline;
|
|
1800
|
+
text-underline-offset: 2px;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
.perf-monitor__segment-option--text {
|
|
1804
|
+
width: auto;
|
|
1805
|
+
padding: 0 5px;
|
|
1806
|
+
font: inherit;
|
|
1807
|
+
font-size: 10px;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
.perf-monitor__costs-note {
|
|
1811
|
+
padding: 2px 6px;
|
|
1812
|
+
color: var(--perf-muted);
|
|
1813
|
+
font-size: 10px;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1469
1816
|
.perf-monitor__checkbox {
|
|
1470
1817
|
flex: none;
|
|
1471
1818
|
margin: 0;
|
|
@@ -1581,13 +1928,15 @@ const PERF_HUD_STYLES = `
|
|
|
1581
1928
|
}
|
|
1582
1929
|
|
|
1583
1930
|
.perf-monitor__badge.is-fallback {
|
|
1584
|
-
border-color:
|
|
1585
|
-
color:
|
|
1931
|
+
border-color: var(--perf-warn);
|
|
1932
|
+
color: var(--perf-warn);
|
|
1586
1933
|
}
|
|
1587
1934
|
|
|
1588
|
-
.
|
|
1589
|
-
|
|
1590
|
-
|
|
1935
|
+
/* Past its budget. The value's tooltip names the budget. */
|
|
1936
|
+
.perf-monitor__value.is-over,
|
|
1937
|
+
.perf-monitor__hud-value.is-over,
|
|
1938
|
+
.perf-monitor__graph-value.is-over {
|
|
1939
|
+
color: var(--perf-warn);
|
|
1591
1940
|
}
|
|
1592
1941
|
|
|
1593
1942
|
.perf-monitor__link {
|
|
@@ -1782,6 +2131,7 @@ const mountPerfHud = (monitor, options = {}) => {
|
|
|
1782
2131
|
const toggle = disc("Toggle full performance metrics", "", "sliders");
|
|
1783
2132
|
tools.append(grip, toggle);
|
|
1784
2133
|
const view = new PerformanceView({
|
|
2134
|
+
budgets: options.budgets,
|
|
1785
2135
|
mode: options.mode ?? "compact",
|
|
1786
2136
|
monitor,
|
|
1787
2137
|
refreshHz: options.refreshHz,
|
|
@@ -1857,6 +2207,7 @@ const mountPerfHud = (monitor, options = {}) => {
|
|
|
1857
2207
|
},
|
|
1858
2208
|
element: host,
|
|
1859
2209
|
getMode: () => view.getMode(),
|
|
2210
|
+
setBudgets: (budgets) => view.setBudgets(budgets),
|
|
1860
2211
|
setMode,
|
|
1861
2212
|
getTheme: () => theme.mode,
|
|
1862
2213
|
setTheme: (mode) => theme.setMode(mode),
|
|
@@ -1866,6 +2217,6 @@ const mountPerfHud = (monitor, options = {}) => {
|
|
|
1866
2217
|
};
|
|
1867
2218
|
};
|
|
1868
2219
|
//#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 };
|
|
2220
|
+
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
2221
|
|
|
1871
|
-
//# sourceMappingURL=mount-perf-hud-
|
|
2222
|
+
//# sourceMappingURL=mount-perf-hud-B_cmTG2M.js.map
|