Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion docs/docs/performance-tracing.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ To help you debug performance issues, Valdi provides a cross-platform tracing AP

### Recording traces

#### Using the Valdi Debugger

With a hot reloader connected to the application, run `valdi debugger`, attach
to a native target, and open **Performance**. The **UI Performance** card can
start and stop a renderer trace or capture a bounded interval of up to 15
seconds. Enable **Renderer events** to include component `onRender()` spans and
ViewModel-change triggers. Exported captures use Chrome Trace JSON and can be
opened in [Perfetto UI](https://ui.perfetto.dev/). Native trace recording is
process-wide; the selected context is recorded as the capture target used to
reach the runtime, not as the origin of each trace event.

To keep debugger responses bounded before serialization, the native recorder
retains at most 10,000 events, 2 KiB per trace name, and 1 MiB of aggregate
trace-name data per active recording window. Capture results report how many
events were dropped by those limits. Automatically stopped and recently
completed results remain available for retry for one minute before they are
discarded.

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.

> [!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`.

Expand Down Expand Up @@ -91,4 +113,3 @@ The Valdi runtime traces some default important events that happens during the l
- `Valdi.setUserDefinedViewport`: The framework is reacting to a scroll change.
- `Valdi.updateVisibility`: The framework is resolving the viewports for the nodes. It is finding out which nodes are visible and which nodes are not visible on the screen.
- `Valdi.calculateLayout`: The framework is calculating the frames (rectangles) for the nodes. This can happen because nodes have been inserted/removed, because a layout attribute has changed (like `padding`), or because the available space for the root component has changed (for instance if the window has been resized). This can be an expensive operation. Because of this, you should avoid triggering changes in the elements that will cause a layout pass to happen when scrolling. If you need to move elements or show/hide them when scrolling, prefer using the `translationX`/`translationY` attributes or `opacity` which are not layout attributes and don't trigger layout passes when they change.

17 changes: 11 additions & 6 deletions npm_modules/cli/debugger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ the CLI package.
- `debugger-preview-html.js`: inert HTML projection of the hot-reloaded snapshot tree.
- `debugger-render.js`: header, target list, tree, preview overlay, inspector, and export rendering.
- `debugger-runtime.js`: target discovery, snapshots, runtime log streaming, heap, and copy/export helpers.
- `debugger-performance.js`: Hermes CPU profile controls.
- `debugger-performance.js`: renderer trace and Hermes CPU profile controls.
- `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.
Expand All @@ -36,15 +36,20 @@ Important routes:
- `/api/snapshot`: fetches the selected target's view tree and preview data.
- `/api/runtime-logs` and `/api/runtime-logs/stream`: read and stream target logs.
- `/api/debugger/state`, `/api/debugger/events`, and `/api/debugger/actions`: keep the browser UI and external agents in sync.
- `/api/performance/trace/*`: start, stop, capture, and export native renderer traces.
- `/api/performance/profile/*`: list Hermes contexts and capture CPU profiles.
- `/api/devtools/target`: matches the inspected Chromium page to the exact configured preview origin and path.
- `/api/devtools/snapshot`, `/api/devtools/highlight`, and `/api/devtools/evaluate`: proxy the explicit web debugger bridge contract through loopback CDP.

Renderer tracing is intentionally not part of this foundation. It requires the
separate runtime and native renderer-instrumentation stack; land that stack
before adding renderer trace routes or controls to this debugger. Hermes CPU
profiling uses the existing inspector transport and has no such prerequisite.
Target input forwarding and data/network provider tabs should likewise land
Renderer tracing uses the runtime debugger protocol and the existing native
trace recorder. Captures are process-wide: the selected context is the capture
target used to reach the runtime, not the origin assigned to every event.
One-shot captures are limited to 15 seconds so the debugger handler can retain
the result before the native recorder's independent safety timeout, and
exported JSON can be opened in Perfetto. Native bounds report dropped event
counts, and retained timeout/retry results expire after one minute.
Hermes CPU profiling uses the existing inspector transport. Target input
forwarding and data/network provider tabs should likewise land
with their runtime-side contracts and end-to-end tests rather than as inactive
browser-only surfaces.
Web-renderer inspection depends on the target page explicitly exposing
Expand Down
34 changes: 30 additions & 4 deletions npm_modules/cli/debugger/debugger-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ function setActiveSection(section) {
}

function shouldAutoRefresh() {
return !document.hidden && !state.performance.profileActive;
return (
!document.hidden &&
!state.performance.traceActive &&
!state.performance.traceCapturePending &&
!state.performance.traceResultPending &&
!state.performance.traceStateUnknown &&
!state.performance.profileActive
);
}

function shouldAutoRefreshLiveSnapshot() {
Expand Down Expand Up @@ -152,14 +159,16 @@ function detachDebuggerView() {
render();
}

function setTargetPort(port) {
async function setTargetPort(port, nextTarget) {
if (!(await preparePerformanceTraceTargetSwitch(nextTarget))) return;
elements.portSelect.value = String(port);
state.snapshot.target = {
...state.snapshot.target,
proxyPort: port,
port,
};
state.attached = false;
state.performance.traceSupported = true;
state.manualDetach = false;
state.followLatestTarget = true;
state.rootSnapshotImage = null;
Expand Down Expand Up @@ -208,7 +217,7 @@ function applyDebuggerAction(action, params = {}) {

if (action === 'setPort') {
const port = actionNumber(params, 'port');
if (port !== null) setTargetPort(port);
if (port !== null) void setTargetPort(port, { port });
return;
}

Expand Down Expand Up @@ -247,6 +256,21 @@ function applyDebuggerAction(action, params = {}) {
return;
}

if (action === 'startRendererTrace') {
void startPerformanceTrace();
return;
}

if (action === 'stopRendererTrace') {
void stopPerformanceTrace({});
return;
}

if (action === 'captureRendererTrace') {
void capturePerformanceTrace();
return;
}

if (action === 'refreshHermesContexts') {
void refreshProfileContexts({ silent: false });
return;
Expand Down Expand Up @@ -279,7 +303,7 @@ function runCommand(rawCommand) {
addLog(
'info',
'console',
'Commands: help, section <ui|performance|logs>, path, issues, select <id>, connect, refresh, reload, status, snapshot, heap, profile, auto on, auto off, clear.',
'Commands: help, section <ui|performance|logs>, path, issues, select <id>, connect, refresh, reload, status, snapshot, heap, trace, profile, auto on, auto off, clear.',
);
} else if (command.startsWith('section ')) {
void requestDebuggerAction('setActiveSection', { section: command.slice('section '.length).trim() });
Expand All @@ -306,6 +330,8 @@ function runCommand(rawCommand) {
void requestDebuggerAction('captureElementSnapshot');
} else if (command === 'heap') {
void requestDebuggerAction('dumpHeap');
} else if (command === 'trace') {
void requestDebuggerAction('captureRendererTrace');
} else if (command === 'profile') {
void requestDebuggerAction('captureCpuProfile');
} else if (command === 'auto on') {
Expand Down
14 changes: 11 additions & 3 deletions npm_modules/cli/debugger/debugger-bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,12 @@ document.addEventListener('keydown', event => {
actionable.click();
});

function selectTarget(id) {
async function selectTarget(id) {
const target = debuggerTargets().find(candidate => candidate.id === id);
if (!target) return;
if (!(await preparePerformanceTraceTargetSwitch(target))) return;
if (target.daemonPort && target.port) {
setTargetPort(target.port);
await setTargetPort(target.port, target);
state.snapshot.target = {
...state.snapshot.target,
...target,
Expand Down Expand Up @@ -166,6 +167,10 @@ elements.autoRefreshToggle.addEventListener(
'change',
() => void requestDebuggerAction('setAutoRefresh', { enabled: elements.autoRefreshToggle.checked }),
);
elements.traceStartButton.addEventListener('click', () => void requestDebuggerAction('startRendererTrace'));
elements.traceStopButton.addEventListener('click', () => void requestDebuggerAction('stopRendererTrace'));
elements.traceCaptureButton.addEventListener('click', () => void requestDebuggerAction('captureRendererTrace'));
elements.traceExportButton.addEventListener('click', exportPerformanceTrace);
elements.profileRefreshButton.addEventListener('click', () => void requestDebuggerAction('refreshHermesContexts'));
elements.profileStartButton.addEventListener('click', () => void requestDebuggerAction('startCpuProfile'));
elements.profileStopButton.addEventListener('click', () => void requestDebuggerAction('stopCpuProfile'));
Expand Down Expand Up @@ -210,4 +215,7 @@ applyDebuggerSessionDomState();
setAutoRefresh(state.autoRefresh, { silent: true });
installDebuggerSessionPersistence();
void refreshProfileContexts({ silent: true });
refreshTargets({ silent: restoredDebuggerSession, autoAttach: !state.manualDetach });
void (async () => {
await recoverPerformanceTraceState();
await refreshTargets({ silent: restoredDebuggerSession, autoAttach: !state.manualDetach });
})();
Loading
Loading