docs/visualization/embedding-the-ui.md
This guide shows you how to embed the Perfetto trace viewer inside your own
tool or dashboard via an <iframe> and feed it traces programmatically. This is
the right approach when you want the trace view to live within your app's
chrome, as real tools like Dart DevTools and various profiler frontends do. If
instead you just want to launch the full Perfetto UI in a new browser tab (the
window.open() flow), see Deep linking to the Perfetto UI;
that page also covers sharing URLs and appStateHash, which this guide does not
duplicate.
http(s), not file://. The embedding protocol
relies on postMessage between windows, which browsers disable for
file:// origins.Cross-Origin-Opener-Policy: same-origin header. It breaks the
parent/iframe relationship the UI depends on.localhost / 127.0.0.1. These origins
are trusted by the UI, so traces open with no consent prompt (see
Trust prompts and going to production).Embed the UI with mode=embedded in the URL. This fully disables the sidebar
(not just hides it), which is what you want for an embedded view. The route is
hash-based:
<iframe
id="perfetto"
src="https://ui.perfetto.dev/#!/?mode=embedded"
width="100%"
height="600"
></iframe>
In embedded mode the file-drop handler is also not installed, so the iframe only loads traces you post to it.
The postMessage channel into the iframe is not buffered: if you post a trace
before the UI has registered its message listener, the message is silently
dropped. To avoid this race, repeatedly post the string 'PING' until the UI
replies with 'PONG'. The UI only sends 'PONG' once its listener is
registered and document.readyState === 'complete'.
const iframe = document.getElementById('perfetto');
function waitForReady() {
return new Promise((resolve) => {
const interval = setInterval(() => {
iframe.contentWindow.postMessage('PING', '*');
}, 100);
window.addEventListener('message', function onMsg(evt) {
if (evt.source === iframe.contentWindow && evt.data === 'PONG') {
clearInterval(interval);
window.removeEventListener('message', onMsg);
resolve();
}
});
});
}
Once the handshake completes, post an object with a single perfetto key to the
iframe's contentWindow. Only buffer (an ArrayBuffer of raw trace bytes) and
title are required:
async function openTrace() {
await waitForReady();
const resp = await fetch(
'https://storage.googleapis.com/perfetto-misc/example_android_trace_15s',
);
const buffer = await resp.arrayBuffer();
iframe.contentWindow.postMessage(
{
perfetto: {
buffer: buffer,
title: 'My embedded trace',
},
},
'*',
);
}
The full set of fields on the perfetto object:
buffer (required): ArrayBuffer of raw trace bytes.title (required): string shown as the trace title.fileName (optional): suggested name if the user downloads the trace.url (optional): sharing URL. See Deep linking
for how url and appStateHash enable sharing.appStateHash (optional): 40-char hex hash restoring saved UI state.localOnly (optional): defaults to true for posted traces, which disables
download/share. Set false to re-enable them.keepApiOpen (optional): if true, the listener stays active so you can post
more traces later. If omitted, the handler removes its listener after the
first trace.pluginArgs (optional): { [pluginId]: { [key]: unknown } }, passed to
plugins' onTraceLoad().NOTE: If you want to swap traces in the same iframe without reloading it, set
keepApiOpen: true on the first post. Otherwise the UI stops listening after
the first trace.
TIP: A bare ArrayBuffer is also accepted (the UI treats it as a trace titled
"External trace"), but posting the { perfetto: { buffer, title } } object is
preferred so you control the title.
You can steer the embedded view in two ways.
To configure the UI as the trace opens, add startupCommands to the iframe
src as a URL-encoded JSON array of commands. For example, to pin the CPU
tracks:
const commands = [
{id: 'dev.perfetto.PinTracksByRegex', args: ['.*CPU [0-3].*']},
];
const src =
'https://ui.perfetto.dev/#!/?mode=embedded&startupCommands=' +
encodeURIComponent(JSON.stringify(commands));
To scroll and zoom to a time range after the trace is loaded, post a second
message. timeStart and timeEnd are absolute trace time in seconds, not
relative to the trace start (most traces do not start at 0); a range outside the
trace is clamped to its bounds. viewPercentage is optional and is a fraction
in the range (0, 1] (e.g. 0.5 fills half the viewport, 1 fills it exactly);
out-of-range values are ignored and fall back to 0.5:
// e.g. zoom to the first 2 seconds of a trace that starts at 261187s.
iframe.contentWindow.postMessage(
{perfetto: {timeStart: 261187.0, timeEnd: 261189.0, viewPercentage: 1}},
'*',
);
The UI retries this internally until the trace is ready, so you can post it shortly after the trace without your own wait loop.
Paste this into a file (e.g. index.html), serve it over http(s) from
localhost, and open it in a browser:
<!doctype html>
<html>
<body>
<iframe
id="perfetto"
src="https://ui.perfetto.dev/#!/?mode=embedded"
width="100%"
height="600"
></iframe>
<script>
const iframe = document.getElementById('perfetto');
const SAMPLE =
'https://storage.googleapis.com/perfetto-misc/example_android_trace_15s';
function waitForReady() {
return new Promise((resolve) => {
const interval = setInterval(() => {
iframe.contentWindow.postMessage('PING', '*');
}, 100);
window.addEventListener('message', function onMsg(evt) {
if (evt.source === iframe.contentWindow && evt.data === 'PONG') {
clearInterval(interval);
window.removeEventListener('message', onMsg);
resolve();
}
});
});
}
(async () => {
await waitForReady();
const buffer = await (await fetch(SAMPLE)).arrayBuffer();
iframe.contentWindow.postMessage(
{perfetto: {buffer, title: 'My embedded trace'}},
'*',
);
})();
</script>
</body>
</html>
The UI guards which origins may push traces:
localhost, 127.0.0.1, [::1], same-origin, and a few hardcoded Google
origins are trusted. Traces from these open immediately with no prompt, so
local development just works.localStorage, so each of your users sees the prompt at most once.To avoid the consent modal entirely in production, self-host the Perfetto UI build on your own domain. A same-origin host page is trusted, so no prompt appears.
NOTE: ui.perfetto.dev follows the latest release, so the embedding protocol
described here is stable, though UI details may change over time. If you need a
fixed version, self-host the UI build to pin it. Self-hosting also gives the
same-origin trust benefit described above.
NOTE: The UI is client-only. Posted traces stay in browser memory and are never uploaded anywhere.
The companion perfetto-embed
repository is a runnable end-to-end example: npm start serves a "devtool" host
page whose control panel embeds the UI and drives it (load traces, zoom, pin
tracks, run queries). It ships a small framework-agnostic PerfettoEmbed
wrapper you can copy into your own tool, plus a React variant.
window.open() (new tab) flow, plus sharing URLs and appStateHash.