diff --git a/docs/docs/performance-tracing.md b/docs/docs/performance-tracing.md index c71922a8..2954be4f 100644 --- a/docs/docs/performance-tracing.md +++ b/docs/docs/performance-tracing.md @@ -28,6 +28,32 @@ The debugger proxies these operations through `/api/performance/trace/status`, `start`, `stop`, and `capture`; the application keeps ownership of the recorder, so captures continue to work across the loopback debugger HTTP connection. +#### Web preview traces + +When `valdi debugger` is attached to an exact loopback web preview, the +integrated DevTools **Performance** panel can sample navigation, resource, heap, +and main-thread metrics. It can also record one global Chromium trace at a +time. The timeline filters are **Valdi**, **Browser**, and **All**. Enabling +Valdi renderer events reloads the inspected page with the explicit tracing +query parameters; browser events remain available without that reload. + +Web capture operations use the separate +`/api/devtools/performance/trace/status`, `start`, `stop`, `capture`, and +`enable` routes. They do not replace the daemon-backed `/api/performance/*` +routes described above. Every web request is bound to the exact inspected +`sessionId`, `inspectedUrl`, and per-tab `targetNonce`; a changed or incomplete +identity fails closed. + +One-shot web captures run for their requested duration from 100 milliseconds +through 15 seconds. Manually started recordings automatically stop after a +15-second watchdog. Normalization happens as CDP events arrive, retaining at +most 10,000 events with trace names no larger than +2 KiB. The complete JSON response is limited to 4 MiB, and an automatically +completed result remains retrievable for one minute. Responses contain the +bounded normalized trace list and export metadata, not a second raw or +Perfetto event list. The panel constructs Chrome Trace JSON only when you +choose **Export trace**. + > [!NOTE] > Please make sure to add `//src/valdi_modules/src/valdi/benchmarking` to the `deps` attribute of the `valdi_module()` call in your module's `BUILD.bazel`. diff --git a/npm_modules/cli/debugger/README.md b/npm_modules/cli/debugger/README.md index a14f0527..508a0b5c 100644 --- a/npm_modules/cli/debugger/README.md +++ b/npm_modules/cli/debugger/README.md @@ -21,7 +21,7 @@ the CLI package. - `debugger-actions.js`: UI actions, command prompt handling, auto-refresh, and externally driven debugger actions. - `debugger-session.js`: `sessionStorage` restore/persist for reload-friendly debugger state. - `debugger-bootstrap.js`: DOM event wiring and boot sequence. -- `devtools-panel.html`, `devtools-panel.css`, and `devtools-panel.js`: the focused Chromium Elements and Console panel embedded by the generated extension. +- `devtools-panel.html`, `devtools-panel.css`, and `devtools-panel.js`: the focused Chromium Elements, Console, and bounded Performance panel embedded by the generated extension. Scripts are loaded as classic browser scripts in the order listed in `index.html`. There is no module loader or bundler for this frontend; shared @@ -71,7 +71,9 @@ requested duration from 100 milliseconds through 15 seconds; manually started recordings have a 15-second watchdog. An undelivered completed result is kept for one minute. The complete response is limited to 4 MiB and contains one normalized trace list plus export metadata—never a -duplicate raw or Perfetto event list. +duplicate raw or Perfetto event list. The panel shows at most 120 graph samples, +120 timeline rows, and 12 grouped summary rows. Its only trace filters are +Valdi, Browser, and All; Chrome Trace JSON is assembled only when exported. The Data section discovers target-owned providers through a generic custom message contract. The persistence module registers its bounded web snapshot as diff --git a/npm_modules/cli/debugger/devtools-panel.css b/npm_modules/cli/debugger/devtools-panel.css index f0552afc..9d4fee45 100644 --- a/npm_modules/cli/debugger/devtools-panel.css +++ b/npm_modules/cli/debugger/devtools-panel.css @@ -550,6 +550,408 @@ button { color: var(--accent); } +.utility-section { + min-height: 0; + grid-template-rows: 32px minmax(0, 1fr); +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 10px; + border-bottom: 1px solid var(--border); + background: var(--surface); + font-weight: 500; +} + +.text-button { + padding: 4px 7px; + border: 0; + background: transparent; + color: var(--accent); +} + +.utility-content { + min-height: 0; + padding: 12px; + overflow: auto; +} + +.metric-grid { + display: grid; + margin-bottom: 15px; + gap: 9px; + grid-template-columns: repeat(auto-fit, minmax(125px, 1fr)); +} + +.metric-card { + min-width: 0; + padding: 10px; + border: 1px solid var(--border); + border-radius: 3px; +} + +.metric-label { + color: var(--muted); +} + +.metric-value { + margin-top: 6px; + font-family: var(--mono); + font-size: 18px; +} + +.metric-card-chart { + padding-bottom: 7px; +} + +.performance-sparkline { + display: block; + width: 100%; + height: 30px; + margin-top: 7px; + --graph-color: var(--accent); +} + +.performance-sparkline.main-thread { + --graph-color: var(--warning); +} + +.performance-sparkline.resources { + --graph-color: var(--tag); +} + +.performance-sparkline-baseline { + fill: none; + stroke: var(--border-soft); + vector-effect: non-scaling-stroke; +} + +.performance-sparkline-area { + fill: color-mix(in srgb, var(--graph-color) 14%, transparent); +} + +.performance-sparkline-line { + fill: none; + stroke: var(--graph-color); + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 1.5; + vector-effect: non-scaling-stroke; +} + +.performance-recorder-card { + margin: 0 0 12px; + padding: 12px; + border: 1px solid var(--border); + border-radius: 4px; + background: var(--background); +} + +.performance-recorder-heading, +.performance-recorder-controls, +.performance-legend { + display: flex; + align-items: center; + gap: 8px; +} + +.performance-recorder-heading { + justify-content: space-between; +} + +.performance-recorder-heading > div { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 7px; +} + +.performance-recorder-controls { + flex-wrap: wrap; + margin: 10px 0; +} + +.performance-status, +.performance-tracing-badge { + padding: 2px 7px; + border-radius: 10px; + background: var(--surface); + color: var(--muted); + font-size: 10px; +} + +.performance-status.recording { + background: color-mix(in srgb, var(--error) 12%, var(--background)); + color: var(--error); +} + +.performance-tracing-badge.enabled { + background: color-mix(in srgb, var(--success) 12%, var(--background)); + color: var(--success); +} + +.performance-tracing-badge.disabled { + background: color-mix(in srgb, var(--warning) 13%, var(--background)); + color: var(--warning); +} + +.performance-button { + padding: 5px 8px; + border: 1px solid var(--border); + border-radius: 3px; + background: var(--surface); +} + +.performance-button:hover:not(:disabled) { + background: var(--surface-hover); +} + +.performance-button.primary { + border-color: var(--accent); + background: var(--accent); + color: var(--background); +} + +.performance-button:disabled { + cursor: not-allowed; + opacity: 0.45; +} + +.performance-duration { + display: inline-flex; + align-items: center; + color: var(--muted); + gap: 4px; +} + +.performance-duration input { + width: 55px; + padding: 3px 4px; + border: 1px solid var(--border); + border-radius: 3px; + background: var(--background); + color: var(--text); +} + +.performance-notice, +.performance-error { + margin: 9px 0; + padding: 8px; + border: 1px solid color-mix(in srgb, var(--warning) 45%, var(--border)); + border-radius: 3px; + background: color-mix(in srgb, var(--warning) 7%, var(--background)); + line-height: 1.5; +} + +.performance-notice .performance-button { + margin-left: 6px; +} + +.performance-error { + border-color: color-mix(in srgb, var(--error) 45%, var(--border)); + background: color-mix(in srgb, var(--error) 7%, var(--background)); + color: var(--error); +} + +.performance-caption { + margin: 8px 0; + color: var(--muted); + font-size: 11px; +} + +.performance-trace-filters { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + margin: 11px 0 6px; + gap: 8px; +} + +.performance-scope-group { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.performance-scope-button { + padding: 4px 7px; + border: 1px solid var(--border); + border-radius: 3px; + background: var(--surface); + color: var(--text); + font-size: 10px; +} + +.performance-scope-button.selected { + border-color: var(--accent); + background: color-mix(in srgb, var(--accent) 12%, var(--background)); + color: var(--accent); +} + +.performance-scope-button span { + margin-left: 3px; + color: var(--muted); +} + +.performance-trace-search { + min-width: 175px; + flex: 1 1 175px; + padding: 4px 6px; + border: 1px solid var(--border); + border-radius: 3px; + background: var(--background); + color: var(--text); + font-size: 10px; +} + +.performance-legend { + flex-wrap: wrap; + margin: 11px 0 7px; +} + +.performance-legend-item { + display: inline-flex; + align-items: center; + color: var(--muted); + font-size: 10px; + gap: 4px; +} + +.performance-legend-item::before { + width: 8px; + height: 8px; + border-radius: 2px; + background: var(--lane-color); + content: ''; +} + +.performance-event-bar.valdi, +.performance-legend-item.valdi { + --lane-color: var(--accent); +} + +.performance-event-bar.script, +.performance-legend-item.script { + --lane-color: var(--warning); +} + +.performance-event-bar.layout, +.performance-legend-item.layout { + --lane-color: var(--tag); +} + +.performance-event-bar.paint, +.performance-legend-item.paint { + --lane-color: var(--success); +} + +.performance-event-bar.frames, +.performance-legend-item.frames { + --lane-color: var(--attribute); +} + +.performance-event-bar.gc, +.performance-legend-item.gc { + --lane-color: var(--error); +} + +.performance-timeline { + max-height: 350px; + overflow: auto; + border: 1px solid var(--border-soft); + border-radius: 3px; +} + +.performance-timeline-row { + display: grid; + min-height: 21px; + align-items: center; + border-bottom: 1px solid var(--border-soft); + font-family: var(--mono); + font-size: 10px; + gap: 6px; + grid-template-columns: minmax(120px, 30%) minmax(80px, 1fr) 62px; +} + +.performance-timeline-row:last-child { + border-bottom: 0; +} + +.performance-event-name, +.performance-event-duration { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.performance-event-name { + padding-left: 6px; +} + +.performance-event-duration { + padding-right: 5px; + color: var(--muted); + text-align: right; +} + +.performance-event-track { + position: relative; + height: 15px; + background: color-mix(in srgb, var(--surface) 70%, transparent); +} + +.performance-event-bar { + position: absolute; + top: 3px; + bottom: 3px; + min-width: 2px; + border-radius: 2px; + background: var(--lane-color); +} + +.utility-heading { + margin: 13px 0 7px; + color: var(--muted); + font-size: 11px; + font-weight: 500; +} + +.performance-results-table { + width: 100%; + border-collapse: collapse; + font-family: var(--mono); + font-size: 10px; +} + +.performance-results-table th, +.performance-results-table td { + padding: 5px 6px; + border-bottom: 1px solid var(--border-soft); + text-align: left; +} + +.performance-results-table th { + color: var(--muted); + font-weight: 500; +} + +.performance-results-table th:not(:first-child), +.performance-results-table td:not(:first-child) { + text-align: right; +} + +.performance-navigation { + margin: 10px 0; + color: var(--muted); +} + +.performance-navigation summary { + cursor: pointer; +} + .console-section { min-height: 0; grid-template-rows: minmax(0, 1fr) 32px; diff --git a/npm_modules/cli/debugger/devtools-panel.html b/npm_modules/cli/debugger/devtools-panel.html index 7cf08381..c0e1c3c1 100644 --- a/npm_modules/cli/debugger/devtools-panel.html +++ b/npm_modules/cli/debugger/devtools-panel.html @@ -10,8 +10,39 @@