@progen-labs/spectra 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +100 -2
- package/bin/spectra.js +215 -0
- package/dist/assets/index-D0Lxrmdt.js +46 -0
- package/dist/assets/index-Da3Le5Px.css +1 -0
- package/dist/index.html +14 -0
- package/index.html +13 -0
- package/package.json +76 -4
- package/server/analysis/cpu.js +369 -0
- package/server/analysis/findings.js +570 -0
- package/server/analysis/locate.js +99 -0
- package/server/analysis/report.js +775 -0
- package/server/analysis/sources.js +249 -0
- package/server/analysis/trace.js +314 -0
- package/server/api.js +292 -0
- package/server/chrome.js +81 -0
- package/server/cli.js +94 -0
- package/server/config.js +59 -0
- package/server/dashboard.js +154 -0
- package/server/explore/boot.js +150 -0
- package/server/explore/classify.js +169 -0
- package/server/explore/discover.js +162 -0
- package/server/explore/evaluate-cli.js +101 -0
- package/server/explore/evaluate.js +165 -0
- package/server/explore/paths.js +105 -0
- package/server/explore/runner.js +94 -0
- package/server/explore/test-explore.js +139 -0
- package/server/explore/test-paths.js +75 -0
- package/server/framework.js +67 -0
- package/server/git.js +59 -0
- package/server/pdf/render.js +60 -0
- package/server/pdf/template.js +419 -0
- package/server/plugin.js +19 -0
- package/server/probe.browser.js +828 -0
- package/server/recorder.js +610 -0
- package/server/signin.js +27 -0
- package/server/test-analysis.js +424 -0
- package/server/test-compare.js +473 -0
- package/server/test-sources.js +136 -0
- package/shared/compare.js +977 -0
- package/shared/snapshot.js +77 -0
- package/src/App.jsx +361 -0
- package/src/api.js +51 -0
- package/src/components/AppearanceMenu.jsx +76 -0
- package/src/components/CodeRef.jsx +48 -0
- package/src/components/ComparePanel.jsx +143 -0
- package/src/components/CompareResults.jsx +473 -0
- package/src/components/ControlBar.jsx +336 -0
- package/src/components/CulpritSphere.jsx +218 -0
- package/src/components/Culprits.jsx +120 -0
- package/src/components/CulpritsHero.jsx +109 -0
- package/src/components/EvaluationPanel.jsx +160 -0
- package/src/components/FilePanel.jsx +225 -0
- package/src/components/FindingsList.jsx +381 -0
- package/src/components/HeatTrees.jsx +222 -0
- package/src/components/Icons.jsx +58 -0
- package/src/components/LongTasks.jsx +199 -0
- package/src/components/NetworkPanel.jsx +98 -0
- package/src/components/PageMap.jsx +329 -0
- package/src/components/Popover.jsx +51 -0
- package/src/components/ReactPanel.jsx +206 -0
- package/src/components/SidePicker.jsx +207 -0
- package/src/components/SnapshotButton.jsx +140 -0
- package/src/components/SpectraCulprits.jsx +39 -0
- package/src/components/TimeBreakdown.jsx +45 -0
- package/src/components/TraceButton.jsx +76 -0
- package/src/components/TreeView.jsx +173 -0
- package/src/components/Vitals.jsx +163 -0
- package/src/culprits.js +127 -0
- package/src/download.js +18 -0
- package/src/format.js +26 -0
- package/src/heat.js +177 -0
- package/src/highlight.js +45 -0
- package/src/hooks/useCompare.js +187 -0
- package/src/hooks/useSession.js +73 -0
- package/src/layout.js +55 -0
- package/src/main.jsx +15 -0
- package/src/styles.css +2478 -0
- package/src/theme.js +75 -0
- package/src/themes.css +286 -0
- package/vite.config.js +8 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--font-ui: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", system-ui, sans-serif;--font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;--radius: 14px;--radius-sm: 9px;--btn-radius: 9px;--blur: none;--on-accent: #fff;--label-case: none;--label-tracking: 0}:root[data-theme=glass]{--radius: 22px;--radius-sm: 12px;--btn-radius: 999px;--blur: blur(28px) saturate(180%);--font-ui: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", system-ui, sans-serif}:root[data-theme=glass][data-mode=light]{color-scheme:light;--bg: radial-gradient(900px 600px at 8% -8%, #b9dcff 0%, transparent 62%), radial-gradient(800px 560px at 96% 4%, #ffd3ea 0%, transparent 58%), radial-gradient(1000px 700px at 62% 112%, #d6cfff 0%, transparent 62%), #eaeff7;--panel: rgb(255 255 255 / .5);--panel-solid: #f4f7fc;--panel-border: rgb(255 255 255 / .75);--panel-edge: inset 0 1px 0 rgb(255 255 255 / .9), inset 0 -1px 0 rgb(120 135 170 / .08);--shadow: 0 1px 2px rgb(40 60 110 / .06), 0 12px 36px rgb(40 60 110 / .12);--shadow-pop: 0 4px 12px rgb(40 60 110 / .12), 0 24px 60px rgb(40 60 110 / .22);--surface: #f6f8fc;--surface-2: rgb(255 255 255 / .62);--control: rgb(255 255 255 / .7);--control-hover: rgb(255 255 255 / .95);--border: rgb(70 85 125 / .16);--text: #0e1220;--text-2: #444c5e;--text-muted: #6d7588;--grid: rgb(70 85 125 / .1);--accent: #0a67d1;--accent-strong: #0a6fdb;--accent-soft: rgb(10 111 219 / .12);--accent-glow: 0 6px 18px rgb(10 111 219 / .35);--heat: #f2602f;--cat-1: #0a6fdb;--cat-2: #f2602f;--cat-3: #1fa971;--cat-4: #eba000;--cat-5: #e5679b;--cat-6: #5b54d6;--cat-other: #a3a9b8;--good: #1a7f37;--warn: #a35607;--poor: #c9302c;--code-string: #17754f;--code-keyword: #5b54d6;--code-number: #a35607}:root[data-theme=glass][data-mode=dark]{color-scheme:dark;--bg: radial-gradient( 900px 600px at 6% -10%, rgb(36 99 235 / .5) 0%, transparent 62% ), radial-gradient( 800px 560px at 98% 0%, rgb(168 56 200 / .38) 0%, transparent 58% ), radial-gradient( 1000px 700px at 60% 115%, rgb(20 150 170 / .3) 0%, transparent 62% ), #0a0c13;--panel: rgb(34 38 54 / .52);--panel-solid: #161924;--panel-border: rgb(255 255 255 / .14);--panel-edge: inset 0 1px 0 rgb(255 255 255 / .18), inset 0 -1px 0 rgb(255 255 255 / .03);--shadow: 0 1px 2px rgb(0 0 0 / .3), 0 16px 44px rgb(0 0 0 / .38);--shadow-pop: 0 4px 14px rgb(0 0 0 / .4), 0 28px 70px rgb(0 0 0 / .55);--surface: #12141d;--surface-2: rgb(255 255 255 / .06);--control: rgb(255 255 255 / .09);--control-hover: rgb(255 255 255 / .16);--border: rgb(255 255 255 / .13);--text: #f4f6fb;--text-2: #b8bfd0;--text-muted: #8990a3;--grid: rgb(255 255 255 / .07);--accent: #4da3ff;--accent-strong: #0a6fdb;--accent-soft: rgb(77 163 255 / .18);--accent-glow: 0 6px 22px rgb(10 111 219 / .5);--heat: #ff7a4d;--cat-1: #4da3ff;--cat-2: #ff7a4d;--cat-3: #30c98a;--cat-4: #f0b429;--cat-5: #f06ea0;--cat-6: #9a93f5;--cat-other: #6c7386;--good: #3fcf6b;--warn: #f0b04a;--poor: #ff6b6b;--code-string: #3fd49b;--code-keyword: #a29bf7;--code-number: #f0b04a}:root[data-theme=space]{--radius: 12px;--radius-sm: 8px;--btn-radius: 8px;--blur: blur(10px);--label-case: uppercase;--label-tracking: .08em;--font-ui: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif}:root[data-theme=space][data-mode=dark]{color-scheme:dark;--bg: radial-gradient(1px 1px at 22px 34px, rgb(255 255 255 / .9), transparent), radial-gradient(1px 1px at 96px 128px, rgb(255 255 255 / .6), transparent), radial-gradient( 1.5px 1.5px at 168px 62px, rgb(160 200 255 / .9), transparent ), radial-gradient(1px 1px at 58px 176px, rgb(255 255 255 / .5), transparent), radial-gradient( 1.5px 1.5px at 190px 190px, rgb(200 180 255 / .8), transparent ), radial-gradient( 900px 620px at 82% -12%, rgb(124 92 255 / .3), transparent 62% ), radial-gradient( 800px 600px at -6% 108%, rgb(0 190 255 / .18), transparent 60% ), #04050d;--bg-size: 210px 210px, 210px 210px, 210px 210px, 210px 210px, 210px 210px, auto, auto, auto;--panel: rgb(12 16 36 / .74);--panel-solid: #0b0f24;--panel-border: rgb(110 130 255 / .24);--panel-edge: inset 0 1px 0 rgb(160 180 255 / .1);--shadow: 0 0 0 1px rgb(0 0 0 / .4), 0 18px 50px rgb(0 0 0 / .55);--shadow-pop: 0 0 0 1px rgb(110 130 255 / .25), 0 0 40px rgb(100 100 255 / .18), 0 28px 70px rgb(0 0 0 / .7);--surface: #0a0d20;--surface-2: rgb(110 130 255 / .07);--control: rgb(110 130 255 / .1);--control-hover: rgb(110 130 255 / .2);--border: rgb(110 130 255 / .22);--text: #eaeefc;--text-2: #aab3d6;--text-muted: #7b85ad;--grid: rgb(110 130 255 / .1);--accent: #6f9bff;--accent-strong: #4565f0;--accent-soft: rgb(111 155 255 / .16);--accent-glow: 0 0 22px rgb(80 110 255 / .55);--accent-2: #22d3ee;--heat: #ff8a3d;--cat-1: #6f9bff;--cat-2: #ff8a3d;--cat-3: #2dd4a0;--cat-4: #fbbf24;--cat-5: #f472b6;--cat-6: #a78bfa;--cat-other: #5f6890;--good: #34d98b;--warn: #fbbf24;--poor: #ff6b81;--code-string: #34d9a0;--code-keyword: #b49bff;--code-number: #fbbf24}:root[data-theme=space][data-mode=light]{color-scheme:light;--bg: radial-gradient(1px 1px at 22px 34px, rgb(79 70 229 / .35), transparent), radial-gradient(1px 1px at 120px 130px, rgb(14 116 144 / .3), transparent), radial-gradient( 1.5px 1.5px at 170px 70px, rgb(79 70 229 / .3), transparent ), radial-gradient( 900px 600px at 84% -12%, rgb(129 140 248 / .32), transparent 62% ), radial-gradient( 800px 600px at -6% 108%, rgb(34 211 238 / .2), transparent 60% ), #eef0fa;--bg-size: 210px 210px, 210px 210px, 210px 210px, auto, auto, auto;--panel: rgb(255 255 255 / .8);--panel-solid: #f8f9ff;--panel-border: rgb(79 70 229 / .2);--panel-edge: inset 0 1px 0 rgb(255 255 255 / .9);--shadow: 0 1px 2px rgb(30 27 100 / .06), 0 14px 40px rgb(30 27 100 / .12);--shadow-pop: 0 0 0 1px rgb(79 70 229 / .15), 0 24px 60px rgb(30 27 100 / .24);--surface: #f8f9ff;--surface-2: rgb(79 70 229 / .05);--control: rgb(255 255 255 / .9);--control-hover: #fff;--border: rgb(79 70 229 / .2);--text: #0f1226;--text-2: #424a6b;--text-muted: #6a7192;--grid: rgb(79 70 229 / .1);--accent: #4338ca;--accent-strong: #4f46e5;--accent-soft: rgb(79 70 229 / .1);--accent-glow: 0 6px 20px rgb(79 70 229 / .35);--accent-2: #0e7490;--heat: #e0560c;--cat-1: #4f46e5;--cat-2: #e0560c;--cat-3: #0f9f78;--cat-4: #d99100;--cat-5: #d6458a;--cat-6: #7c3aed;--cat-other: #9aa0bd;--good: #14803f;--warn: #9a5200;--poor: #c62839;--code-string: #0f7a5a;--code-keyword: #6d3fd6;--code-number: #9a5200}:root[data-theme=glass] button.primary,:root[data-theme=glass] .tabs button[aria-selected=true]{background:linear-gradient(180deg,rgb(255 255 255 / .28),transparent 55%),var(--accent-strong)}:root[data-theme=space] button.primary{background:linear-gradient(135deg,var(--accent-strong),#8b5cf6);border-color:transparent}:root[data-theme=space] .brand-mark{background:conic-gradient(from 210deg,#22d3ee,#6f6bff,#c084fc,#22d3ee)}:root[data-theme=glass] .brand-mark{background:linear-gradient(135deg,#5ac8fa,#0a84ff 55%,#bf5af2)}*{box-sizing:border-box;scrollbar-width:thin;scrollbar-color:var(--border) transparent}html,body,#root{height:100%;margin:0}body{background:var(--bg);background-size:var(--bg-size, auto);background-attachment:fixed;color:var(--text);font:.8125rem/1.45 var(--font-ui);-webkit-font-smoothing:antialiased}button,input,select{font:inherit;color:inherit}button,select,input:not([type=checkbox]):not([type=radio]){background:var(--control);border:1px solid var(--border);border-radius:var(--radius-sm);padding:.35rem .75rem;min-height:2rem;transition:background .15s,border-color .15s,box-shadow .15s}button{border-radius:var(--btn-radius);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:.4rem;font-weight:500;white-space:nowrap}select{cursor:pointer}input:not([type=checkbox]):not([type=radio]):focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);outline:none}button:hover:not(:disabled),select:hover:not(:disabled){background:var(--control-hover);border-color:color-mix(in srgb,var(--accent) 55%,var(--border))}button:disabled,select:disabled,input:disabled{opacity:.5;cursor:default}button.primary{background:var(--accent-strong);border-color:transparent;color:var(--on-accent);box-shadow:var(--accent-glow)}button.primary:hover:not(:disabled){background:var(--accent-strong);border-color:transparent;filter:brightness(1.1)}button.danger{background:var(--poor);border-color:transparent;color:#fff}button.danger:hover:not(:disabled){background:var(--poor);border-color:transparent;filter:brightness(1.08)}button.ghost{background:transparent;border-color:transparent;color:var(--text-2)}button.ghost:hover:not(:disabled){background:var(--control);border-color:var(--border);color:var(--text)}button.ghost.destructive:hover:not(:disabled){color:var(--poor)}button.link{background:none;border:0;padding:0;min-height:0;color:var(--accent);text-align:left;display:inline;font-weight:inherit}button.link:hover:not(:disabled){background:none;text-decoration:underline}:focus-visible{outline:2px solid var(--accent);outline-offset:2px}code,.mono{font-family:var(--font-mono);font-size:12px}.muted{color:var(--text-muted);font-weight:400}.warn{color:var(--warn)}.spacer{flex:1}.pad{padding:1rem}.num{text-align:right;font-variant-numeric:tabular-nums}.empty{display:grid;place-content:center;text-align:center;gap:.4rem;padding:2rem;color:var(--text-muted);flex:1}.icon{flex:none}.app{display:flex;flex-direction:column;gap:.7rem;height:100%;padding:.75rem}.topbar,.session,.workspace{background:var(--panel);-webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur);border:1px solid var(--panel-border);border-radius:var(--radius);box-shadow:var(--shadow),var(--panel-edge)}.topbar{display:flex;align-items:center;gap:.8rem;padding:.55rem .7rem .55rem .9rem;position:relative;z-index:10}.brand{display:flex;align-items:center;gap:.7rem;min-width:0}.brand-mark{display:grid;place-items:center;width:2rem;height:2rem;border-radius:calc(var(--radius-sm) - 1px);color:#fff;box-shadow:inset 0 1px #fff6,0 4px 14px #2850dc59}.brand h1{margin:0;font-size:.95rem;line-height:1.2;font-weight:650;letter-spacing:-.01em}.brand small{display:block;color:var(--text-muted);font-size:.72rem;font-family:var(--font-mono)}.report-picker{display:inline-flex;align-items:center;gap:.3rem}.report-picker select{max-width:24rem}.error{background:color-mix(in srgb,var(--poor) 14%,var(--panel-solid));border:1px solid color-mix(in srgb,var(--poor) 55%,transparent);border-radius:var(--radius-sm);padding:.5rem .8rem;display:flex;align-items:center;gap:.8rem}.error button{margin-left:auto}.popover{position:relative;display:inline-block}.caret{font-size:.7em;opacity:.7}.popover-panel,.trace-note{background:color-mix(in srgb,var(--panel-solid) 98%,transparent);-webkit-backdrop-filter:blur(24px) saturate(160%);backdrop-filter:blur(24px) saturate(160%);border:1px solid var(--panel-border);border-radius:var(--radius);box-shadow:var(--shadow-pop),var(--panel-edge)}.popover-panel{position:absolute;top:calc(100% + 8px);z-index:40;padding:.9rem;min-width:15rem}.popover-panel.align-right{right:0}.popover-panel.align-left{left:0}.appearance,.options-list{display:grid;gap:1rem;width:17rem}.choice-group{border:0;margin:0;padding:0;min-width:0}.choice-group legend,.field-label{padding:0;margin-bottom:.4rem;font-size:.7rem;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em}.choice input{position:absolute;opacity:0;pointer-events:none}.choice:focus-within{outline:2px solid var(--accent);outline-offset:2px}.theme-cards{display:grid;grid-template-columns:1fr 1fr;gap:.5rem}.theme-cards .choice{position:relative;display:grid;gap:.15rem;padding:.45rem;border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;background:var(--surface-2);transition:border-color .15s,box-shadow .15s}.theme-cards .choice:hover{border-color:color-mix(in srgb,var(--accent) 55%,var(--border))}.theme-cards .choice.selected{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}.theme-cards .choice-label{font-weight:600;margin-top:.25rem}.theme-cards small{font-size:.7rem;line-height:1.25}.swatch{position:relative;display:block;height:2.9rem;border-radius:calc(var(--radius-sm) - 3px);overflow:hidden}.swatch i{position:absolute;left:.45rem;height:.5rem;border-radius:999px}.swatch i:first-child{top:.5rem;width:55%}.swatch i:last-child{top:1.35rem;width:35%}.swatch-glass{background:radial-gradient(circle at 20% 20%,#8ec5ff,transparent 60%),radial-gradient(circle at 85% 30%,#ffb8df,transparent 55%),linear-gradient(135deg,#c9bcff,#9fd6ff)}.swatch-glass i{background:#ffffffb3;box-shadow:inset 0 1px #fff}.swatch-space{background:radial-gradient(1px 1px at 70% 25%,#fff,transparent),radial-gradient(1px 1px at 88% 70%,#bcd4ff,transparent),radial-gradient(1px 1px at 45% 80%,#fff,transparent),radial-gradient(circle at 90% 0%,rgb(124 92 255 / .6),transparent 60%),#05071a}.swatch-space i{background:linear-gradient(90deg,#22d3ee,#6f6bff)}.segmented,.segmented-control{display:inline-flex;gap:2px;padding:3px;background:var(--surface-2);border:1px solid var(--border);border-radius:var(--btn-radius)}.segmented-control{display:flex}.segmented button,.segmented-control .choice{flex:1;background:transparent;border:0;min-height:1.7rem;padding:.15rem .8rem;border-radius:calc(var(--btn-radius) - 2px);color:var(--text-2);font-weight:500;text-align:center;cursor:pointer;position:relative}.segmented-control .choice{display:grid;place-items:center}.segmented button:hover:not(:disabled){background:var(--control);border:0}.segmented button[aria-pressed=true],.segmented-control .choice.selected{background:var(--accent-soft);color:var(--accent);font-weight:600}label.switch{display:inline-flex;align-items:center;gap:.5rem;cursor:pointer;color:var(--text-2);-webkit-user-select:none;user-select:none}.switch input{-moz-appearance:none;appearance:none;-webkit-appearance:none;position:relative;flex:none;width:2rem;height:1.15rem;margin:0;border-radius:999px;background:var(--control);border:1px solid var(--border);cursor:pointer;transition:background .15s}.switch input:after{content:"";position:absolute;top:1px;left:1px;width:calc(1.15rem - 4px);height:calc(1.15rem - 4px);border-radius:50%;background:var(--text-muted);transition:transform .15s,background .15s}.switch input:checked{background:var(--accent-strong);border-color:transparent}.switch input:checked:after{background:#fff;transform:translate(.85rem)}.switch input:disabled{cursor:default}.session{position:relative;z-index:5;padding:.7rem .8rem;display:grid;gap:.6rem}.session-row{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.session-row+.session-row{padding-top:.6rem;border-top:1px solid var(--border)}.session label,.connect label{color:var(--text-2)}.address{flex:1;min-width:14rem;font-family:var(--font-mono);font-size:12px}.btn-group{display:inline-flex;align-items:center;gap:.4rem}.connect{display:grid;gap:.7rem}.connect-head h2{margin:0;font-size:1rem;letter-spacing:-.01em}.connect-head p{margin:.15rem 0 0}.connect-main{display:flex;gap:.5rem;align-items:flex-end;flex-wrap:wrap}.field{display:grid;gap:.25rem;color:var(--text-2)}.connect-main .field{flex:1;min-width:16rem}.field input{width:100%}.field-row input{flex:1;min-width:0}.advanced{display:grid;gap:.8rem;padding-top:.7rem;border-top:1px solid var(--border)}.advanced-section{display:grid;gap:.4rem}.advanced-section+.advanced-section{padding-top:.7rem;border-top:1px dashed var(--border)}.advanced-row{display:flex;gap:.5rem;align-items:flex-end;flex-wrap:wrap}.advanced-row .field{flex:1;min-width:16rem}.pill{display:inline-flex;align-items:center;gap:.35rem;border:1px solid var(--border);border-radius:999px;padding:.1rem .65rem;background:var(--surface-2);font-size:.75rem;white-space:nowrap}.pill.ok{color:var(--good);border-color:color-mix(in srgb,var(--good) 40%,transparent)}.pill.warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent)}.rec-strip{display:flex;align-items:center;gap:.8rem;flex-wrap:wrap;flex:1}.rec-strip.recording{padding:.4rem .6rem;border-radius:var(--radius-sm);background:color-mix(in srgb,var(--poor) 10%,transparent);border:1px solid color-mix(in srgb,var(--poor) 35%,transparent)}.rec{display:inline-flex;align-items:center;gap:.5rem;font-weight:600}.rec i{width:10px;height:10px;border-radius:50%;background:var(--poor);box-shadow:0 0 0 4px color-mix(in srgb,var(--poor) 25%,transparent);animation:pulse 1s infinite}.rec i.spin{background:var(--accent);box-shadow:0 0 0 4px var(--accent-soft)}.elapsed{font-family:var(--font-mono);font-variant-numeric:tabular-nums;color:var(--text-2);font-weight:400}@keyframes pulse{50%{opacity:.3}}@media (prefers-reduced-motion: reduce){.rec i{animation:none}*{transition:none!important}}.workspace{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}.report-bar{display:flex;align-items:center;gap:.8rem;flex-wrap:wrap;padding:.6rem .9rem;border-bottom:1px solid var(--border)}.report-title{font-weight:600;font-size:.9rem;max-width:28rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.report-meta{display:flex;gap:.35rem;flex-wrap:wrap}.tabs{display:flex;gap:.2rem;overflow-x:auto}.main-tabs{align-items:center;padding:.5rem .9rem;border-bottom:1px solid var(--border)}.main-tabs .tab-list{display:flex;gap:.2rem;min-width:0;overflow-x:auto}.tabs button{background:transparent;border:0;min-height:1.9rem;padding:.25rem .8rem;color:var(--text-2)}.tabs button:hover:not(:disabled){background:var(--control);border:0;color:var(--text)}.tabs button[aria-selected=true]{background:var(--accent-strong);color:var(--on-accent);box-shadow:var(--accent-glow)}.count{margin-left:.1rem;background:var(--accent-soft);color:var(--accent);border-radius:999px;padding:0 .45rem;font-size:11px;font-weight:600}.tabs button[aria-selected=true] .count{background:#ffffff40;color:inherit}.main{flex:1;display:flex;min-height:0}.content{flex:1;min-width:0;overflow:auto;display:flex;flex-direction:column}.welcome{flex:1;overflow:auto;display:grid;align-content:center;justify-items:center;gap:1.6rem;padding:2rem 1.5rem;text-align:center}.welcome h2{margin:0;font-size:1.6rem;letter-spacing:-.02em}.welcome-lead{margin:.4rem 0 0;font-size:.95rem;color:var(--text-2)}.steps{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(3,minmax(0,17rem));gap:.9rem;text-align:left}.steps li{display:grid;gap:.35rem;align-content:start;padding:1rem;background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius);line-height:1.55}.step-num{display:grid;place-items:center;width:1.7rem;height:1.7rem;border-radius:50%;background:var(--accent-soft);color:var(--accent);font-weight:700}.steps h3{margin:.2rem 0 0;font-size:.9rem}.steps p{margin:0;color:var(--text-2)}.welcome-foot{max-width:44rem;margin:0;color:var(--text-muted)}@media (max-width: 900px){.steps{grid-template-columns:1fr}}.culprits{padding:1rem 1.2rem;display:grid;gap:1.2rem;align-content:start}.culprits h2,.network h2,.long-tasks h2{font-size:.9rem;margin:0 0 .5rem}.vital-label{color:var(--text-2);font-size:12px}.vital-value{font-size:1.25rem;font-weight:600;font-variant-numeric:tabular-nums}.vital-rating{font-size:12px}.rating-good{border-left-color:var(--good)}.rating-good .vital-rating{color:var(--good)}.rating-warn{border-left-color:var(--warn)}.rating-warn .vital-rating{color:var(--warn)}.rating-poor{border-left-color:var(--poor)}.rating-poor .vital-rating{color:var(--poor)}.rating-none{border-left-color:var(--border)}.rating-none .vital-rating{color:var(--text-muted)}.breakdown-bar{display:flex;height:16px;border-radius:4px;overflow:hidden;gap:2px;background:var(--surface)}.breakdown-bar.small{height:8px;margin:.35rem 0}.breakdown-bar span{min-width:2px}.breakdown-legend{display:flex;flex-wrap:wrap;gap:.4rem 1.2rem;list-style:none;padding:0;margin:.5rem 0 0}.breakdown-legend i{display:inline-block;width:10px;height:10px;border-radius:2px;margin-right:.35rem}.sev-critical{border-left-color:var(--poor)}.sev-warning{border-left-color:var(--warn)}.sev-info{border-left-color:var(--accent)}.badge-sev{font-weight:600;font-size:12px}.sev-critical .badge-sev{color:var(--poor)}.sev-warning .badge-sev{color:var(--warn)}.sev-info .badge-sev{color:var(--accent)}.chip{border:1px solid var(--border);border-radius:4px;padding:0 .4rem;font-size:11px;color:var(--text-2)}.chip-warn{color:var(--warn);border-color:var(--warn)}.tree-pane{flex:1;display:flex;flex-direction:column;min-height:0}.tree-toolbar{display:flex;align-items:center;gap:.8rem;padding:.4rem 1rem;border-bottom:1px solid var(--border);flex-wrap:wrap}.tree-svg{flex:1;min-height:0;width:100%;display:block;cursor:grab;background:radial-gradient(var(--grid) 1px,transparent 1px) 0 0 / 22px 22px}.tree-svg:active{cursor:grabbing}.tree-links path{fill:none;stroke:var(--border);stroke-width:1.2}.node-hit{fill:transparent}.node:hover .node-hit{fill:var(--surface-2)}.node-dot{fill:color-mix(in srgb,var(--heat) var(--heat-pct, 20%),var(--surface));stroke:var(--heat);stroke-width:1.5;cursor:pointer}.node.vendor .node-dot{fill:var(--surface);stroke:var(--text-muted);stroke-dasharray:2 2}.node.dir .node-dot{stroke-width:2}.node.selected .node-hit{fill:color-mix(in srgb,var(--accent) 22%,transparent)}.node.selected .node-label{font-weight:700}.node-glyph{font-size:10px;fill:var(--text);pointer-events:none}.node-label{fill:var(--text);font-size:12px;cursor:pointer}.node.dir .node-label{fill:var(--text-2);font-weight:600}.legend{display:flex;gap:1.2rem;align-items:center;padding:.4rem 1rem;border-top:1px solid var(--border);color:var(--text-2);flex-wrap:wrap}.ramp{display:inline-block;width:90px;height:10px;border-radius:2px;vertical-align:middle;margin-right:.4rem;background:linear-gradient(90deg,color-mix(in srgb,var(--heat) 12%,var(--surface)),var(--heat));border:1px solid var(--heat)}.ring{display:inline-block;width:10px;height:10px;border-radius:50%;border:1.5px dashed var(--text-muted);margin-right:.35rem;vertical-align:middle}.file-panel{width:34rem;max-width:45vw;flex:none;border-left:1px solid var(--border);display:flex;flex-direction:column;min-height:0}.panel-head{display:flex;align-items:center;gap:.5rem;padding:.5rem .8rem;border-bottom:1px solid var(--border)}.panel-head h2{flex:1;margin:0;font-size:.8125rem;font-family:ui-monospace,Menlo,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.metrics{display:grid;grid-template-columns:repeat(3,1fr);gap:.4rem;padding:.6rem .8rem}.metric{background:var(--surface-2);border:1px solid var(--border);border-radius:6px;padding:.3rem .5rem}.metric-value{font-weight:600;font-variant-numeric:tabular-nums}.metric.warn .metric-value{color:var(--warn)}.metric-label{font-size:11px;color:var(--text-muted)}.mini-findings{list-style:none;margin:0 .8rem .5rem;padding:0;display:grid;gap:.3rem}.mini-findings li{border-left:3px solid;padding-left:.5rem}.file-panel .tabs{padding:0 .8rem;border-bottom:1px solid var(--border)}.panel-body{flex:1;overflow:auto;display:flex;flex-direction:column;min-height:0}table.data{width:100%;border-collapse:collapse}table.data th{text-align:left;font-weight:600;color:var(--text-2);position:sticky;top:0;background:var(--panel-solid);border-bottom:1px solid var(--border);padding:.4rem .6rem}table.data th button{background:none;border:0;padding:0;font-weight:600;color:inherit}table.data td{padding:.25rem .6rem;border-bottom:1px solid var(--grid);vertical-align:top}tr.open td,tr.detail td{background:var(--surface-2)}.detail-grid{display:grid;grid-template-columns:1fr 2fr;gap:1.5rem}.detail-grid h4{margin:0 0 .3rem;font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:var(--text-2)}.detail-grid ul{margin:0;padding-left:1.1rem}.summary-row{display:flex;gap:1.4rem;flex-wrap:wrap;padding:.6rem 1rem;color:var(--text-2);border-bottom:1px solid var(--border)}.bar{height:3px;background:var(--grid);border-radius:2px;margin-top:3px}.bar span{display:block;height:100%;background:var(--heat);border-radius:2px}.react-panel,.network{overflow:auto}.network section{margin:1.2rem 0}.timeline{width:100%;height:130px;background:var(--surface-2);border-bottom:1px solid var(--border)}.timeline .task{fill:var(--cat-other)}.timeline .task.long{fill:var(--poor)}.timeline .limit{stroke:var(--warn);stroke-dasharray:4 4;vector-effect:non-scaling-stroke}.timeline .axis{fill:var(--text-muted);font-size:11px}.task-list{list-style:none;margin:0;padding:.8rem 1rem;display:grid;gap:.7rem}.task-list>li{border:1px solid var(--border);border-left-width:4px;border-radius:var(--radius-sm);padding:.6rem .9rem;background:var(--surface-2)}.task-head{display:flex;gap:.7rem;align-items:center;flex-wrap:wrap}.task-cols{display:grid;grid-template-columns:1fr 1fr;gap:1rem}.task-cols h4{margin:.3rem 0;font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:var(--text-2)}.row{display:flex;justify-content:space-between;gap:.6rem;width:100%}.row span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.code-scroll{flex:1;overflow:auto;display:flex;align-items:flex-start;font:12px/18px ui-monospace,Menlo,Consolas,monospace}.gutter{position:sticky;left:0;background:var(--surface-2);border-right:1px solid var(--border);text-align:right;flex:none}.gutter-line{height:18px;padding:0 .5rem;color:var(--text-muted);white-space:nowrap}.gutter-hot{background:color-mix(in srgb,var(--heat) 22%,transparent);color:var(--text);font-weight:600}.code{margin:0;padding:0 1rem;flex:1}.code code.hljs{background:none;padding:0;font:inherit;display:block;white-space:pre}.hljs-comment,.hljs-quote{color:var(--text-muted);font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-literal{color:var(--code-keyword)}.hljs-string,.hljs-regexp,.hljs-attr{color:var(--code-string)}.hljs-number,.hljs-built_in,.hljs-type{color:var(--code-number)}.hljs-title,.hljs-function,.hljs-name{color:var(--accent)}.culprit-box{margin:.5rem 0 .2rem;padding:.5rem .7rem;border:1px dashed var(--border);border-radius:6px;background:var(--surface)}.culprit-box h4{margin:0 0 .3rem;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--text-2)}.culprit-box ul{margin:0;padding:0;list-style:none;display:grid;gap:.25rem}code.fn{font-weight:700;color:var(--text)}.code-ref{display:inline-flex;align-items:baseline;gap:.4rem;flex-wrap:wrap}.code-ref b{font-weight:700}.editor-link{font-size:11px;color:var(--accent);text-decoration:none;border:1px solid var(--border);border-radius:4px;padding:0 .3rem}.editor-link:hover{border-color:var(--accent)}table.hot-functions td{vertical-align:middle}tr.vendor-row code.fn{font-weight:400;color:var(--text-2)}.trace-button{position:relative}.trace-note{position:absolute;right:0;top:calc(100% + 8px);z-index:40;width:30rem;max-width:80vw;padding:.8rem 2rem .8rem 1rem;line-height:1.5}.trace-note.error{border-color:var(--poor)}.trace-note ol{margin:.4rem 0 0;padding-left:1.2rem}.trace-note code{word-break:break-all}.trace-note .close{position:absolute;top:.4rem;right:.4rem;background:none;border:0;min-height:0;padding:0 .3rem;font-size:1.1rem;line-height:1}kbd{border:1px solid var(--border);border-radius:4px;padding:0 .3rem;font-size:11px}.compare-panel{padding:1rem 1.2rem 2rem;display:grid;gap:1rem;align-content:start}.compare-intro h2{margin:0 0 .3rem;font-size:1rem}.sides{display:grid;grid-template-columns:1fr auto 1fr;gap:.8rem;align-items:start}.swap{align-self:center}.swap button{font-size:1.1rem;padding:.3rem .6rem}.side-picker{border:1px solid var(--border);border-radius:var(--radius-sm);padding:.7rem .9rem;background:var(--surface-2);display:grid;gap:.5rem}.side-picker h3{margin:0;font-size:.9rem}.field{display:grid;gap:.2rem;color:var(--text-2)}.field-row{display:flex;gap:.4rem}.field input{flex:1;min-width:0}.git-facts{margin:0;display:grid;grid-template-columns:5.5rem 1fr;gap:.15rem .6rem}.git-facts dt{color:var(--text-muted)}.git-facts dd{margin:0;overflow-wrap:anywhere}.dropzone{display:flex;flex-wrap:wrap;gap:.6rem;align-items:center;border:1.5px dashed var(--border);border-radius:var(--radius-sm);padding:.6rem}.dropzone.dragging{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 10%,transparent)}.run-chips{list-style:none;margin:0;padding:0;display:grid;gap:.3rem}.run-chips li{display:flex;gap:.6rem;align-items:center;background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:.2rem .5rem}.run-chips li .mono{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.run-chips button{border:0;background:none;padding:0 .3rem;font-size:1rem}.compare-toolbar{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;padding:.5rem .7rem;border:1px solid var(--border);border-radius:var(--radius-sm);position:sticky;top:0;background:var(--surface);z-index:5}.pdf-buttons{display:inline-flex;gap:.5rem;align-items:center}.compare-results{display:grid;gap:1.2rem}.compare-results h2{font-size:.95rem;margin:0 0 .5rem}.verdict-banner{border:1.5px solid var(--c);border-left-width:6px;border-radius:var(--radius-sm);padding:.7rem 1rem}.verdict-banner .big{font-size:1.4rem;font-weight:700;color:var(--c)}.verdict-banner p{margin:.25rem 0}.verdict-banner .counts span{margin-right:1.2rem;font-weight:600}.good{--c: var(--good)}.bad{--c: var(--poor)}.warn{--c: var(--warn)}.same{--c: var(--text-2)}.na{--c: var(--text-muted)}.info{--c: var(--accent)}.verdict-banner .counts .good,.verdict-banner .counts .bad,.verdict-banner .counts .same,.verdict{color:var(--c)}.verdict{font-weight:700;white-space:nowrap}ul.icons{list-style:none;margin:0;padding:0;display:grid;gap:.25rem}ul.icons li{color:var(--text)}ul.icons li .i,ul.checks li .i{display:inline-block;width:1.4rem;font-weight:700;color:var(--c)}ul.checks{list-style:none;margin:0;padding:0;display:grid;gap:.25rem}ul.checks li{display:flex;align-items:baseline}.compare-table td,.compare-table th{white-space:nowrap}.compare-table td:first-child{white-space:normal}.compare-table tr.dim td{color:var(--text-muted)}tr.group-row td{background:var(--surface-2);font-weight:700;text-transform:uppercase;font-size:11px;letter-spacing:.05em;color:var(--text-2)}.small{font-size:11px}svg.change-bar{display:block}svg.change-bar .axis{stroke:var(--text-2);stroke-width:1}svg.change-bar rect{fill:var(--c)}.compare-results .segmented{margin-bottom:.5rem}.detail-pane{padding-top:.6rem;display:grid;gap:.6rem}.entity-group{border:1px solid var(--border);border-left:4px solid var(--c, var(--border));border-radius:6px;padding:.3rem .7rem}.entity-group summary{cursor:pointer;font-weight:600;padding:.2rem 0}.entity-name{width:40%}.entity-name>*{margin-right:.4rem}.finding-cards{list-style:none;margin:0 0 .6rem;padding:0;display:grid;gap:.5rem}.finding-card{border:1px solid var(--border);border-left:4px solid var(--c);border-radius:6px;padding:.5rem .8rem}.finding-card h4{margin:.2rem 0}.finding-card .fix{margin:.3rem 0 0}.sev{font-weight:700;color:var(--c)}.trace-note.snapshot{width:28rem;display:grid;gap:.5rem}.trace-note.snapshot p{margin:0}@media (max-width: 1100px){.sides{grid-template-columns:1fr}.swap{justify-self:center}}.page-map{display:grid;grid-template-columns:minmax(0,1fr) 20rem;gap:1rem;padding:1rem 1.2rem;align-items:start}.page-caption{margin:0 0 .6rem}.page-frame{position:relative;width:100%}.page-frame img{position:absolute;top:0;height:100%;display:block;border:1px solid var(--border);border-radius:var(--radius-sm);box-shadow:var(--shadow);background:var(--surface);box-sizing:border-box}.callout-svg{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;pointer-events:none}.callout{--pin: var(--heat)}.callout.sev-warn{--pin: var(--warn)}.callout.sev-mild{--pin: var(--accent)}.callout rect{fill:none;stroke:var(--pin);stroke-width:1;stroke-opacity:.4;vector-effect:non-scaling-stroke}.callout line{stroke:var(--pin);stroke-width:1;stroke-opacity:.5;vector-effect:non-scaling-stroke}.callout circle{fill:var(--pin);fill-opacity:.8}.callout.active rect{fill:var(--pin);fill-opacity:.16;stroke-opacity:1;stroke-width:2}.callout.active line{stroke-opacity:1;stroke-width:2}.callout.active circle{fill-opacity:1}button.marker{--pin: var(--heat);position:absolute;z-index:5;display:grid;place-items:center;width:1.7rem;height:1.7rem;min-height:0;padding:0;transform:translate(-50%,-50%);border:2px solid var(--panel-solid);border-radius:50%;background:var(--pin);color:#fff;font-size:11px;font-weight:700;box-shadow:0 1px 4px #00000059}button.marker.sev-warn{--pin: var(--warn)}button.marker.sev-mild{--pin: var(--accent-strong)}button.marker:hover:not(:disabled),button.marker.active{background:var(--pin);border-color:var(--panel-solid);transform:translate(-50%,-50%) scale(1.2);box-shadow:0 0 0 3px color-mix(in srgb,var(--pin) 40%,transparent)}.pin-num{display:inline-grid;place-items:center;min-width:1.4rem;height:1.4rem;padding:0 .3rem;border-radius:999px;font-size:11px;font-weight:700;color:#fff;background:var(--accent-strong)}.pin-card{position:absolute;z-index:20;width:20rem;max-width:90%;margin:.5rem 0;padding:.8rem .9rem;pointer-events:none;background:color-mix(in srgb,var(--panel-solid) 98%,transparent);border:1px solid var(--panel-border);border-radius:var(--radius-sm);box-shadow:var(--shadow-pop);color:var(--text)}.pin-card-title{display:flex;align-items:center;gap:.5rem;margin-bottom:.2rem}.pin-file{display:block;color:var(--text-muted);overflow-wrap:anywhere;margin-bottom:.5rem}.pin-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:.4rem;margin:0 0 .5rem}.pin-stats dt{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.pin-stats dd{margin:0;font-weight:650;font-variant-numeric:tabular-nums}.pin-card p{margin:.3rem 0 0;color:var(--text-2)}.pin-card .pin-fix{color:var(--text)}.page-list h2{margin:0 0 .5rem;font-size:.9rem}.page-list ol{list-style:none;margin:0;padding:0;display:grid;gap:.3rem}button.hotspot{width:100%;justify-content:flex-start;text-align:left;gap:.6rem;padding:.4rem .6rem;background:var(--surface-2);white-space:normal}button.hotspot.active{border-color:var(--accent);background:var(--accent-soft)}.hotspot-main{display:grid;min-width:0;flex:1}.hotspot-main .mono{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px}.hotspot-time{font-variant-numeric:tabular-nums;font-weight:600;color:var(--text-2)}@media (max-width: 1100px){.page-map{grid-template-columns:1fr}}.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.badge-sev{display:inline-flex;align-items:center;gap:.3rem;font-weight:600;font-size:12px}.culprits-hero{display:grid;grid-template-columns:minmax(18rem,34rem) minmax(0,1fr);gap:1.4rem;align-items:start}.hero-sphere h2,.hero-detail h2{margin:0 0 .3rem}.sphere{position:relative;width:100%;aspect-ratio:1;max-width:34rem;margin:0 auto;cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none;border-radius:50%}.sphere:active{cursor:grabbing}.sphere-glow{position:absolute;top:12%;right:12%;bottom:12%;left:12%;border-radius:50%;background:radial-gradient(circle at 35% 30%,color-mix(in srgb,var(--accent) 26%,transparent),color-mix(in srgb,var(--accent) 6%,transparent) 55%,transparent 72%);box-shadow:inset 0 0 40px color-mix(in srgb,var(--accent) 18%,transparent),0 0 60px color-mix(in srgb,var(--accent) 14%,transparent)}.sphere-wire{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;overflow:visible}.sphere-wire path{fill:none;stroke:var(--accent);stroke-opacity:.22;stroke-width:.006}button.orb{--orb: var(--accent);position:absolute;display:grid;gap:0;justify-items:center;min-height:0;padding:.2rem .6rem;border-radius:999px;background:color-mix(in srgb,var(--panel-solid) 78%,transparent);border:1px solid color-mix(in srgb,var(--orb) 55%,transparent);box-shadow:0 2px 10px #0000001f;line-height:1.2;will-change:transform,opacity}button.orb.sev-critical{--orb: var(--poor)}button.orb.sev-warning{--orb: var(--warn)}button.orb .orb-name{font-weight:650;color:var(--text)}button.orb .orb-ms{font-size:.78em;color:var(--orb);font-variant-numeric:tabular-nums;font-weight:600}button.orb:hover:not(:disabled),button.orb.active{background:color-mix(in srgb,var(--orb) 18%,var(--panel-solid));border-color:var(--orb);box-shadow:0 0 0 3px color-mix(in srgb,var(--orb) 30%,transparent)}.insight{display:grid;gap:.35rem;padding:.8rem 1rem;border:1px solid var(--border);border-left:4px solid var(--accent);border-radius:var(--radius-sm);background:var(--surface-2)}.insight.sev-critical{border-left-color:var(--poor)}.insight.sev-warning{border-left-color:var(--warn)}.insight.sev-critical .badge-sev{color:var(--poor)}.insight.sev-warning .badge-sev{color:var(--warn)}.insight.sev-info .badge-sev{color:var(--accent)}.insight-head{display:flex;align-items:baseline;gap:.6rem;flex-wrap:wrap}.insight-name{font-family:var(--font-mono);font-size:.95rem}.insight-ms{margin-left:auto;font-weight:700;font-variant-numeric:tabular-nums}.insight p{margin:0}.insight button{justify-self:start}.rank-list{list-style:none;margin:.8rem 0 0;padding:0;display:grid;gap:2px}button.rank-row{--orb: var(--accent);width:100%;display:grid;grid-template-columns:1.4rem minmax(0,1fr) 7rem 4.6rem;align-items:center;gap:.6rem;min-height:1.9rem;padding:.15rem .5rem;background:transparent;border-color:transparent;border-radius:var(--radius-sm);text-align:left;font-weight:400}button.rank-row.sev-critical{--orb: var(--poor)}button.rank-row.sev-warning{--orb: var(--warn)}button.rank-row.active{background:var(--accent-soft);border-color:color-mix(in srgb,var(--accent) 40%,transparent)}.rank-n{color:var(--text-muted);font-variant-numeric:tabular-nums}.rank-name{font-family:var(--font-mono);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rank-bar{height:6px;border-radius:3px;background:var(--grid);overflow:hidden}.rank-bar span{display:block;height:100%;border-radius:3px;background:var(--orb)}.rank-ms{text-align:right;font-variant-numeric:tabular-nums;color:var(--text-2)}@media (max-width: 1000px){.culprits-hero{grid-template-columns:1fr}}.vitals-block{display:grid;gap:.7rem}.vitals-hero{display:grid;grid-template-columns:repeat(auto-fit,minmax(13rem,1fr));gap:.7rem}.vital-hero{display:grid;gap:.15rem;padding:.8rem 1rem;border:1px solid var(--border);border-left-width:4px;border-radius:var(--radius-sm);background:var(--surface-2)}.vital-hero .vital-label{color:var(--text-2)}.vital-hero .vital-value{font-size:1.9rem;line-height:1.15;font-weight:650;letter-spacing:-.02em;font-variant-numeric:tabular-nums}.vital-hero .vital-rating{font-weight:600;font-size:12px}.vital-target{font-size:11px}.rating-good{--rate: var(--good);border-left-color:var(--good)}.rating-warn{--rate: var(--warn);border-left-color:var(--warn)}.rating-poor{--rate: var(--poor);border-left-color:var(--poor)}.rating-none{--rate: var(--text-muted);border-left-color:var(--border)}.vital-rating{color:var(--rate)}.gauge{position:relative;height:6px;margin:.45rem 0 .2rem;border-radius:3px;background:linear-gradient(90deg,color-mix(in srgb,var(--good) 45%,transparent) 0 var(--g),color-mix(in srgb,var(--warn) 45%,transparent) var(--g) var(--p),color-mix(in srgb,var(--poor) 45%,transparent) var(--p) 100%)}.gauge-needle{position:absolute;top:-3px;width:4px;height:12px;margin-left:-2px;border-radius:2px;background:var(--rate);box-shadow:0 0 0 2px var(--panel-solid)}.vitals-secondary{display:grid;grid-template-columns:repeat(auto-fit,minmax(11.5rem,1fr));gap:.4rem}.vital-chip{display:flex;align-items:center;gap:.5rem;padding:.35rem .7rem;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface-2);border-left-width:1px}.vital-chip-icon{display:grid;place-items:center;width:1.1rem;height:1.1rem;border-radius:50%;font-size:10px;font-weight:700;color:var(--panel-solid);background:var(--rate)}.vital-chip-label{flex:1;color:var(--text-2)}.vital-chip b{font-variant-numeric:tabular-nums}.findings-toolbar{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;margin-bottom:.6rem}.frows,.members{list-style:none;margin:0;padding:0;display:grid;gap:.4rem}.frow{--sev: var(--accent);border:1px solid var(--border);border-left:4px solid var(--sev);border-radius:var(--radius-sm);background:var(--surface-2);overflow:hidden}.frow.sev-critical{--sev: var(--poor)}.frow.sev-warning{--sev: var(--warn)}.frow.open{border-color:color-mix(in srgb,var(--sev) 45%,var(--border))}.frow.group{background:color-mix(in srgb,var(--sev) 6%,var(--surface-2))}button.frow-head{width:100%;display:grid;grid-template-columns:1.5rem minmax(0,1fr) auto 9.5rem 1rem;align-items:center;gap:.7rem;padding:.55rem .8rem;border:0;border-radius:0;background:transparent;text-align:left;font-weight:400;white-space:normal}button.frow-head:hover:not(:disabled){background:var(--control);border:0}.sev-badge{display:grid;place-items:center;width:1.4rem;height:1.4rem;border-radius:50%;font-size:11px;font-weight:700;color:var(--panel-solid);background:var(--sev)}.frow-main{display:grid;min-width:0}.frow-title{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.frow.open>.frow-head .frow-title{white-space:normal}.frow-sub{font-family:var(--font-mono);font-size:11px}.frow-chips{display:flex;gap:.25rem}.impact{display:grid;grid-template-columns:1fr auto;align-items:center;gap:.5rem}.impact-bar{height:6px;border-radius:3px;background:var(--grid);overflow:hidden}.impact-bar span{display:block;height:100%;background:var(--sev);border-radius:3px}.impact-ms{min-width:3.4rem;text-align:right;font-variant-numeric:tabular-nums;font-weight:600}.fbody{display:grid;gap:.8rem;padding:.2rem 1rem 1rem 3.1rem}.fix-callout{padding:.6rem .9rem;border-radius:var(--radius-sm);background:var(--accent-soft);border:1px solid color-mix(in srgb,var(--accent) 35%,transparent)}.fix-callout p{margin:.15rem 0 0}.fix-label{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--accent)}.fdetail{margin:0;color:var(--text-2)}.fdetail b{display:block;font-size:.7rem;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted)}.culprit-table th{position:static;background:transparent}.culprit-table td{vertical-align:middle}.members-title{margin:.2rem 0 0;font-size:.7rem;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted)}.members .frow{background:var(--surface)}@media (max-width: 900px){button.frow-head{grid-template-columns:1.5rem minmax(0,1fr) 1rem}.frow-chips,.impact{display:none}.fbody{padding-left:1rem}}.eval-panel{padding:16px 20px 28px;overflow:auto}.eval-head{display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:12px}.eval-summary{display:inline-flex;flex-wrap:wrap;gap:6px}.eval-target{font-size:13px;margin-bottom:14px;color:var(--text)}.eval-table-wrap{overflow-x:auto;border:1px solid var(--border);border-radius:var(--radius-sm)}table.eval-matrix{width:100%;border-collapse:collapse;font-size:13px}table.eval-matrix th,table.eval-matrix td{text-align:left;padding:9px 12px;border-bottom:1px solid var(--border);vertical-align:top}table.eval-matrix th{font:inherit;font-size:11px;letter-spacing:var(--label-tracking);text-transform:var(--label-case);color:var(--text-muted);background:var(--surface)}table.eval-matrix td.num,table.eval-matrix th.num{text-align:right;font-family:var(--font-mono);white-space:nowrap}table.eval-matrix tr:last-child td{border-bottom:none}table.eval-matrix tr.row-fail td{background:color-mix(in srgb,var(--poor) 8%,transparent)}.eval-path-title{font-weight:600}.eval-path-error{margin-top:3px;font-size:12px;color:var(--poor);font-family:var(--font-mono)}.badge{display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:700;letter-spacing:.04em}.badge-pass{background:color-mix(in srgb,var(--good) 20%,transparent);color:var(--good)}.badge-fail{background:color-mix(in srgb,var(--poor) 20%,transparent);color:var(--poor)}.top-finding{font-size:12px}.eval-foot{margin-top:12px;font-size:12px}
|
package/dist/index.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Performance Profiler</title>
|
|
7
|
+
<link rel="icon" href="data:," />
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-D0Lxrmdt.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Da3Le5Px.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="root"></div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
package/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Performance Profiler</title>
|
|
7
|
+
<link rel="icon" href="data:," />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,78 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@progen-labs/spectra",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Spectra — a Chrome DevTools Protocol profiler that identifies the files, functions and components causing frontend slowness, across React, Vue, Svelte, Angular and Next.js builds.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"spectra": "bin/spectra.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "server/recorder.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./server/recorder.js",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"server",
|
|
17
|
+
"shared",
|
|
18
|
+
"src",
|
|
19
|
+
"dist",
|
|
20
|
+
"index.html",
|
|
21
|
+
"vite.config.js",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"spectra": "node bin/spectra.js",
|
|
27
|
+
"dev": "vite",
|
|
28
|
+
"build": "vite build",
|
|
29
|
+
"prepack": "npm run build",
|
|
30
|
+
"demo": "vite --config fixtures/demo-app/vite.config.js",
|
|
31
|
+
"profile": "node server/cli.js",
|
|
32
|
+
"test": "node --test \"server/**/test-*.js\""
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"performance",
|
|
39
|
+
"profiler",
|
|
40
|
+
"profiling",
|
|
41
|
+
"cdp",
|
|
42
|
+
"chrome-devtools-protocol",
|
|
43
|
+
"frontend",
|
|
44
|
+
"web-vitals",
|
|
45
|
+
"react",
|
|
46
|
+
"vue",
|
|
47
|
+
"svelte",
|
|
48
|
+
"angular",
|
|
49
|
+
"nextjs",
|
|
50
|
+
"spectra",
|
|
51
|
+
"progen-labs"
|
|
52
|
+
],
|
|
53
|
+
"author": "Progen Labs (Sourav Debnath)",
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"homepage": "https://github.com/progen-labs/spectra#readme",
|
|
56
|
+
"repository": {
|
|
57
|
+
"type": "git",
|
|
58
|
+
"url": "git+https://github.com/progen-labs/spectra.git"
|
|
59
|
+
},
|
|
60
|
+
"bugs": {
|
|
61
|
+
"url": "https://github.com/progen-labs/spectra/issues"
|
|
62
|
+
},
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
65
|
+
},
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"@jridgewell/trace-mapping": "^0.3.25",
|
|
68
|
+
"d3": "^7.9.0",
|
|
69
|
+
"highlight.js": "^11.10.0",
|
|
70
|
+
"puppeteer-core": "^22.15.0",
|
|
71
|
+
"react": "^18.3.1",
|
|
72
|
+
"react-dom": "^18.3.1"
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"@vitejs/plugin-react": "^4.3.1",
|
|
76
|
+
"vite": "^5.4.0"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns V8 CPU profile samples (recorded inside the trace as Profile /
|
|
3
|
+
* ProfileChunk events) into file, function and call-tree attribution.
|
|
4
|
+
*/
|
|
5
|
+
import { isNativeFrame } from './sources.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Re-assembles the CPU profile of the main thread from trace events.
|
|
9
|
+
*
|
|
10
|
+
* A `Profile` event (emitted on the sampled thread) announces a profile; its
|
|
11
|
+
* `ProfileChunk` events carry the samples but come from V8's profiler helper
|
|
12
|
+
* thread, so they are paired by (pid, id) - not by thread. Ids restart for each
|
|
13
|
+
* profiler instance (Chrome's own WebUI in the same process has a "0x1" too), so
|
|
14
|
+
* a chunk whose node ids collide with the profile being built starts a new one.
|
|
15
|
+
*
|
|
16
|
+
* @returns {{nodes: object[], samples: number[], timeDeltas: number[], startTime: number} | null}
|
|
17
|
+
*/
|
|
18
|
+
export function extractCpuProfile(events, pid, tid) {
|
|
19
|
+
const headers = events
|
|
20
|
+
.filter(e => e.name === 'Profile')
|
|
21
|
+
.sort((a, b) => a.ts - b.ts);
|
|
22
|
+
const chunksByGroup = new Map();
|
|
23
|
+
for (const e of events) {
|
|
24
|
+
if (e.name !== 'ProfileChunk') continue;
|
|
25
|
+
const key = `${e.pid}:${e.id || e.id2?.local || '0x0'}`;
|
|
26
|
+
if (!chunksByGroup.has(key)) chunksByGroup.set(key, []);
|
|
27
|
+
chunksByGroup.get(key).push(e);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const profiles = [];
|
|
31
|
+
for (const [key, chunks] of chunksByGroup) {
|
|
32
|
+
chunks.sort((a, b) => a.ts - b.ts);
|
|
33
|
+
const groupPid = chunks[0].pid;
|
|
34
|
+
const groupHeaders = headers.filter(
|
|
35
|
+
h => `${h.pid}:${h.id || h.id2?.local || '0x0'}` === key,
|
|
36
|
+
);
|
|
37
|
+
let current = null;
|
|
38
|
+
let seen = null;
|
|
39
|
+
for (const chunk of chunks) {
|
|
40
|
+
const data = chunk.args?.data || {};
|
|
41
|
+
const cp = data.cpuProfile || {};
|
|
42
|
+
const nodes = cp.nodes || [];
|
|
43
|
+
if (!current || nodes.some(n => seen.has(n.id))) {
|
|
44
|
+
const header =
|
|
45
|
+
groupHeaders[profiles.filter(p => p.group === key).length];
|
|
46
|
+
current = {
|
|
47
|
+
group: key,
|
|
48
|
+
pid: groupPid,
|
|
49
|
+
tid: header?.tid ?? chunk.tid,
|
|
50
|
+
startTime: header?.args?.data?.startTime || header?.ts || chunk.ts,
|
|
51
|
+
nodes: [],
|
|
52
|
+
samples: [],
|
|
53
|
+
timeDeltas: [],
|
|
54
|
+
};
|
|
55
|
+
seen = new Set();
|
|
56
|
+
profiles.push(current);
|
|
57
|
+
}
|
|
58
|
+
for (const n of nodes) {
|
|
59
|
+
current.nodes.push(n);
|
|
60
|
+
seen.add(n.id);
|
|
61
|
+
}
|
|
62
|
+
for (const sample of cp.samples || []) current.samples.push(sample);
|
|
63
|
+
for (const d of data.timeDeltas || []) current.timeDeltas.push(d);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const candidates = profiles.filter(p => p.samples.length);
|
|
68
|
+
return (
|
|
69
|
+
candidates.find(p => p.pid === pid && p.tid === tid) ||
|
|
70
|
+
candidates
|
|
71
|
+
.filter(p => p.pid === pid)
|
|
72
|
+
.sort((a, b) => b.samples.length - a.samples.length)[0] ||
|
|
73
|
+
candidates.sort((a, b) => b.samples.length - a.samples.length)[0] ||
|
|
74
|
+
null
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const IDLE = '(idle)';
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {{nodes: object[], samples: number[], timeDeltas: number[], startTime: number}} profile
|
|
82
|
+
* @param {(callFrame: object) => {file: string, vendor: boolean, pkg: string|null, line: number|null, name: string|null, exact: boolean}} resolveFrame
|
|
83
|
+
*/
|
|
84
|
+
export function analyzeCpuProfile(profile, resolveFrame) {
|
|
85
|
+
const byId = new Map();
|
|
86
|
+
for (const n of profile.nodes) {
|
|
87
|
+
byId.set(n.id, {
|
|
88
|
+
id: n.id,
|
|
89
|
+
frame: n.callFrame,
|
|
90
|
+
children: [],
|
|
91
|
+
parent: n.parent ?? null,
|
|
92
|
+
self: 0,
|
|
93
|
+
total: 0,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
for (const n of byId.values()) {
|
|
97
|
+
if (n.parent !== null) byId.get(n.parent)?.children.push(n);
|
|
98
|
+
}
|
|
99
|
+
// Older profiles list children instead of parents.
|
|
100
|
+
for (const raw of profile.nodes) {
|
|
101
|
+
if (raw.children && !raw.parent) {
|
|
102
|
+
const node = byId.get(raw.id);
|
|
103
|
+
for (const c of raw.children) {
|
|
104
|
+
const child = byId.get(c);
|
|
105
|
+
if (child && child.parent === null) {
|
|
106
|
+
child.parent = node.id;
|
|
107
|
+
node.children.push(child);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const roots = [...byId.values()].filter(n => n.parent === null);
|
|
113
|
+
|
|
114
|
+
// Sample timestamps (µs) and the time each sample represents.
|
|
115
|
+
const count = profile.samples.length;
|
|
116
|
+
const sampleTs = new Float64Array(count);
|
|
117
|
+
const sampleDur = new Float64Array(count);
|
|
118
|
+
let t = profile.startTime;
|
|
119
|
+
for (let i = 0; i < count; i += 1) {
|
|
120
|
+
t += profile.timeDeltas[i] || 0;
|
|
121
|
+
sampleTs[i] = t;
|
|
122
|
+
}
|
|
123
|
+
for (let i = 0; i < count; i += 1) {
|
|
124
|
+
sampleDur[i] =
|
|
125
|
+
i + 1 < count ? sampleTs[i + 1] - sampleTs[i] : sampleDur[i - 1] || 1000;
|
|
126
|
+
}
|
|
127
|
+
const sampleNodes = profile.samples.map(id => byId.get(id));
|
|
128
|
+
sampleNodes.forEach((node, i) => {
|
|
129
|
+
if (node) node.self += sampleDur[i];
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Resolve every frame once.
|
|
133
|
+
for (const node of byId.values()) {
|
|
134
|
+
const f = node.frame;
|
|
135
|
+
node.native = isNativeFrame(f.functionName);
|
|
136
|
+
node.idle = f.functionName === IDLE;
|
|
137
|
+
node.loc = node.native
|
|
138
|
+
? {
|
|
139
|
+
file: f.functionName,
|
|
140
|
+
vendor: false,
|
|
141
|
+
pkg: null,
|
|
142
|
+
line: null,
|
|
143
|
+
name: null,
|
|
144
|
+
exact: false,
|
|
145
|
+
}
|
|
146
|
+
: resolveFrame(f);
|
|
147
|
+
node.name = node.loc.name || f.functionName || '(anonymous)';
|
|
148
|
+
node.key = `${node.loc.file}|${node.name}|${node.loc.line ?? f.lineNumber}`;
|
|
149
|
+
// Builtins without a script (Array.sort, performance.now…) are blamed on their caller.
|
|
150
|
+
node.builtin = !node.native && !f.url;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Post-order totals, iteratively (call trees can be thousands deep).
|
|
154
|
+
const order = [];
|
|
155
|
+
const stack = [...roots];
|
|
156
|
+
while (stack.length) {
|
|
157
|
+
const n = stack.pop();
|
|
158
|
+
order.push(n);
|
|
159
|
+
for (const c of n.children) stack.push(c);
|
|
160
|
+
}
|
|
161
|
+
for (const n of order) {
|
|
162
|
+
const parent = n.parent === null ? null : byId.get(n.parent);
|
|
163
|
+
n.rep = n.builtin && parent ? parent.rep : n;
|
|
164
|
+
if (n.rep !== n) {
|
|
165
|
+
n.rep.self += n.self;
|
|
166
|
+
n.self = 0;
|
|
167
|
+
}
|
|
168
|
+
// Anonymous callbacks are named after the function they sit in, so a culprit
|
|
169
|
+
// reads "(anonymous in renderRows)" instead of an unlocatable "(anonymous)".
|
|
170
|
+
const named = !n.native && !n.builtin && n.name !== '(anonymous)';
|
|
171
|
+
n.enclosing = named ? n.name : (parent?.enclosing ?? null);
|
|
172
|
+
if (
|
|
173
|
+
n.name === '(anonymous)' &&
|
|
174
|
+
!n.native &&
|
|
175
|
+
!n.builtin &&
|
|
176
|
+
parent?.enclosing
|
|
177
|
+
) {
|
|
178
|
+
n.name = `(anonymous in ${parent.enclosing})`;
|
|
179
|
+
n.key = `${n.loc.file}|${n.name}|${n.loc.line ?? n.frame.lineNumber}`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for (let i = order.length - 1; i >= 0; i -= 1) {
|
|
183
|
+
const n = order[i];
|
|
184
|
+
n.total = n.self + n.children.reduce((sum, c) => sum + c.total, 0);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Which application function drives each library's time (nearest non-library caller).
|
|
188
|
+
const vendorDrivers = new Map();
|
|
189
|
+
for (const n of order) {
|
|
190
|
+
if (!n.loc.vendor || n.self <= 0) continue;
|
|
191
|
+
let caller = n.parent === null ? null : byId.get(n.parent);
|
|
192
|
+
while (caller && (caller.native || caller.builtin || caller.loc.vendor)) {
|
|
193
|
+
caller = caller.parent === null ? null : byId.get(caller.parent);
|
|
194
|
+
}
|
|
195
|
+
if (!caller) continue;
|
|
196
|
+
const drivers = vendorDrivers.get(n.loc.pkg) || new Map();
|
|
197
|
+
const key = caller.key;
|
|
198
|
+
const entry = drivers.get(key) || {
|
|
199
|
+
file: caller.loc.file,
|
|
200
|
+
name: caller.name,
|
|
201
|
+
line: caller.loc.line,
|
|
202
|
+
exact: caller.loc.exact,
|
|
203
|
+
us: 0,
|
|
204
|
+
};
|
|
205
|
+
entry.us += n.self;
|
|
206
|
+
drivers.set(key, entry);
|
|
207
|
+
vendorDrivers.set(n.loc.pkg, drivers);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Function / file aggregation with recursion-safe inclusive time.
|
|
211
|
+
const functions = new Map();
|
|
212
|
+
const files = new Map();
|
|
213
|
+
const fnOnStack = new Map();
|
|
214
|
+
const fileOnStack = new Map();
|
|
215
|
+
const walk = [];
|
|
216
|
+
roots.forEach(r => walk.push({ node: r, phase: 0 }));
|
|
217
|
+
while (walk.length) {
|
|
218
|
+
const { node, phase } = walk.pop();
|
|
219
|
+
const fnKey = node.key;
|
|
220
|
+
const fileKey = node.loc.file;
|
|
221
|
+
if (phase === 1) {
|
|
222
|
+
fnOnStack.set(fnKey, fnOnStack.get(fnKey) - 1);
|
|
223
|
+
fileOnStack.set(fileKey, fileOnStack.get(fileKey) - 1);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (!node.idle) {
|
|
227
|
+
let fn = functions.get(fnKey);
|
|
228
|
+
if (!fn) {
|
|
229
|
+
fn = {
|
|
230
|
+
key: fnKey,
|
|
231
|
+
name: node.name,
|
|
232
|
+
file: node.loc.file,
|
|
233
|
+
line: node.loc.line,
|
|
234
|
+
exact: node.loc.exact,
|
|
235
|
+
bundleLine: node.frame.lineNumber,
|
|
236
|
+
selfUs: 0,
|
|
237
|
+
totalUs: 0,
|
|
238
|
+
};
|
|
239
|
+
functions.set(fnKey, fn);
|
|
240
|
+
}
|
|
241
|
+
fn.selfUs += node.self;
|
|
242
|
+
if (!fnOnStack.get(fnKey)) fn.totalUs += node.total;
|
|
243
|
+
|
|
244
|
+
let file = files.get(fileKey);
|
|
245
|
+
if (!file) {
|
|
246
|
+
file = {
|
|
247
|
+
file: fileKey,
|
|
248
|
+
vendor: node.loc.vendor,
|
|
249
|
+
pkg: node.loc.pkg,
|
|
250
|
+
selfUs: 0,
|
|
251
|
+
totalUs: 0,
|
|
252
|
+
};
|
|
253
|
+
files.set(fileKey, file);
|
|
254
|
+
}
|
|
255
|
+
file.selfUs += node.self;
|
|
256
|
+
if (!fileOnStack.get(fileKey)) file.totalUs += node.total;
|
|
257
|
+
}
|
|
258
|
+
fnOnStack.set(fnKey, (fnOnStack.get(fnKey) || 0) + 1);
|
|
259
|
+
fileOnStack.set(fileKey, (fileOnStack.get(fileKey) || 0) + 1);
|
|
260
|
+
walk.push({ node, phase: 1 });
|
|
261
|
+
for (const c of node.children) walk.push({ node: c, phase: 0 });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let activeUs = 0;
|
|
265
|
+
let idleUs = 0;
|
|
266
|
+
let gcUs = 0;
|
|
267
|
+
for (const n of byId.values()) {
|
|
268
|
+
if (n.idle) idleUs += n.self;
|
|
269
|
+
else activeUs += n.self;
|
|
270
|
+
if (n.frame.functionName === '(garbage collector)') gcUs += n.self;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
nodes: byId,
|
|
275
|
+
roots,
|
|
276
|
+
functions: [...functions.values()],
|
|
277
|
+
files: [...files.values()],
|
|
278
|
+
vendorDrivers,
|
|
279
|
+
totals: { activeUs, idleUs, gcUs, sampleCount: count },
|
|
280
|
+
sampleTs,
|
|
281
|
+
sampleDur,
|
|
282
|
+
sampleNodes,
|
|
283
|
+
startUs: profile.startTime,
|
|
284
|
+
endUs: count ? sampleTs[count - 1] : profile.startTime,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Self time per node within [fromUs, toUs). */
|
|
289
|
+
export function windowSelf(cpu, fromUs, toUs) {
|
|
290
|
+
const { sampleTs, sampleDur, sampleNodes } = cpu;
|
|
291
|
+
let lo = 0;
|
|
292
|
+
let hi = sampleTs.length;
|
|
293
|
+
while (lo < hi) {
|
|
294
|
+
const mid = (lo + hi) >> 1;
|
|
295
|
+
if (sampleTs[mid] < fromUs) lo = mid + 1;
|
|
296
|
+
else hi = mid;
|
|
297
|
+
}
|
|
298
|
+
const out = new Map();
|
|
299
|
+
for (let i = lo; i < sampleTs.length && sampleTs[i] < toUs; i += 1) {
|
|
300
|
+
const node = sampleNodes[i]?.rep;
|
|
301
|
+
if (!node || node.idle) continue;
|
|
302
|
+
out.set(node, (out.get(node) || 0) + sampleDur[i]);
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** The sampled node at (or just before) a timestamp: used to blame forced layouts. */
|
|
308
|
+
export function nodeAt(cpu, us) {
|
|
309
|
+
const { sampleTs, sampleNodes } = cpu;
|
|
310
|
+
let lo = 0;
|
|
311
|
+
let hi = sampleTs.length - 1;
|
|
312
|
+
let found = -1;
|
|
313
|
+
while (lo <= hi) {
|
|
314
|
+
const mid = (lo + hi) >> 1;
|
|
315
|
+
if (sampleTs[mid] <= us) {
|
|
316
|
+
found = mid;
|
|
317
|
+
lo = mid + 1;
|
|
318
|
+
} else hi = mid - 1;
|
|
319
|
+
}
|
|
320
|
+
return found === -1 ? null : sampleNodes[found].rep;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Path from the root to a node (frames only, natives dropped). */
|
|
324
|
+
export function pathTo(node, byId) {
|
|
325
|
+
const path = [];
|
|
326
|
+
for (let n = node; n; n = n.parent === null ? null : byId.get(n.parent))
|
|
327
|
+
path.push(n);
|
|
328
|
+
return path.reverse().filter(n => !n.native);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Pruned top-down call tree for the UI.
|
|
333
|
+
* @param {number} minUs drop subtrees that account for less than this
|
|
334
|
+
*/
|
|
335
|
+
export function buildCallTree(cpu, { minUs, maxDepth = 60, maxChildren = 14 }) {
|
|
336
|
+
let counter = 0;
|
|
337
|
+
const convert = (node, depth) => {
|
|
338
|
+
const out = {
|
|
339
|
+
id: `c${counter++}`,
|
|
340
|
+
name: node.native ? node.frame.functionName : node.name,
|
|
341
|
+
file: node.loc.file,
|
|
342
|
+
vendor: node.loc.vendor,
|
|
343
|
+
line: node.loc.line,
|
|
344
|
+
selfMs: node.self / 1000,
|
|
345
|
+
totalMs: node.total / 1000,
|
|
346
|
+
children: [],
|
|
347
|
+
};
|
|
348
|
+
if (depth >= maxDepth) return out;
|
|
349
|
+
const kids = node.children
|
|
350
|
+
.filter(c => !c.idle && c.total >= minUs)
|
|
351
|
+
.sort((a, b) => b.total - a.total)
|
|
352
|
+
.slice(0, maxChildren);
|
|
353
|
+
out.children = kids.map(c => convert(c, depth + 1));
|
|
354
|
+
return out;
|
|
355
|
+
};
|
|
356
|
+
const top = cpu.roots.flatMap(r => (r.children.length ? [r] : []));
|
|
357
|
+
const root = {
|
|
358
|
+
id: 'c-root',
|
|
359
|
+
name: '(all samples)',
|
|
360
|
+
file: '',
|
|
361
|
+
vendor: false,
|
|
362
|
+
line: null,
|
|
363
|
+
selfMs: 0,
|
|
364
|
+
totalMs: cpu.totals.activeUs / 1000,
|
|
365
|
+
children: [],
|
|
366
|
+
};
|
|
367
|
+
root.children = top.flatMap(r => convert(r, 0).children);
|
|
368
|
+
return root;
|
|
369
|
+
}
|