// Copyright (C) 2024 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // open-perfetto-trace is a standalone JS/TS library that can be used in other // projects to facilitate the deep linking into perfetto. It allows opening // trace files or URL with ui.perfetto.dev, handling all the handshake with it. const PERFETTO_UI_URL = 'https://ui.perfetto.dev'; interface OpenTraceOptions { // If true (default) shows a popup dialog with a progress bar that informs // about the status of the fetch. This is only relevant when the trace source // is a url. statusDialog?: boolean; // Opens the trace in a new tab. newTab?: boolean; // Override the referrer. Useful for scripts such as // record_android_trace to record where the trace is coming from. referrer?: string; // For the 'mode' of the UI. For example when the mode is 'embedded' // some features are disabled. mode: 'embedded' | undefined; // Hides the sidebar in the opened perfetto UI. hideSidebar?: boolean; ts?: string; dur?: string; tid?: string; pid?: string; query?: string; visStart?: string; visEnd?: string; // Used to override ui.perfetto.dev with a custom hosted URL. // Useful for testing. uiUrl?: string; } // Opens a trace in the Perfetto UI. // `source` can be either: // - A blob (e.g. a File). // - A URL. export default function openPerfettoTrace( source: Blob | string, opts?: OpenTraceOptions, ) { if (source instanceof Blob) { return openTraceBlob(source, opts); } else if (typeof source === 'string') { return fetchAndOpenTrace(source, opts); } } function openTraceBlob(blob: Blob, opts?: OpenTraceOptions) { const form = document.createElement('form'); form.method = 'POST'; form.style.visibility = 'hidden'; form.enctype = 'multipart/form-data'; const uiUrl = opts?.uiUrl ?? PERFETTO_UI_URL; form.action = `${uiUrl}/_open_trace/${Date.now()}`; if (opts?.newTab === true) { form.target = '_blank'; } const fileInput = document.createElement('input'); fileInput.name = 'trace'; fileInput.type = 'file'; const dataTransfer = new DataTransfer(); dataTransfer.items.add(new File([blob], 'trace.file')); fileInput.files = dataTransfer.files; form.appendChild(fileInput); for (const [key, value] of Object.entries(opts ?? {})) { const varInput = document.createElement('input'); varInput.type = 'hidden'; varInput.name = key; varInput.value = value; form.appendChild(varInput); } document.body.appendChild(form); form.submit(); } function fetchAndOpenTrace(url: string, opts?: OpenTraceOptions) { updateProgressDiv({status: 'Fetching trace'}, opts); const xhr = new XMLHttpRequest(); xhr.addEventListener('progress', (event) => { if (event.lengthComputable) { updateProgressDiv( { status: `Fetching trace (${Math.round(event.loaded / 1000)} KB)`, progress: event.loaded / event.total, }, opts, ); } }); xhr.addEventListener('loadend', () => { if (xhr.readyState === 4 && xhr.status === 200) { const blob = xhr.response as Blob; updateProgressDiv({status: 'Opening trace'}, opts); openTraceBlob(blob, opts); updateProgressDiv({close: true}, opts); } }); xhr.addEventListener('error', () => { updateProgressDiv({status: 'Failed to fetch trace'}, opts); }); xhr.responseType = 'blob'; xhr.overrideMimeType('application/octet-stream'); xhr.open('GET', url); xhr.send(); } interface ProgressDivOpts { status?: string; progress?: number; close?: boolean; } function updateProgressDiv(progress: ProgressDivOpts, opts?: OpenTraceOptions) { if (opts?.statusDialog === false) return; const kDivId = 'open_perfetto_trace'; let div = document.getElementById(kDivId); if (!div) { div = document.createElement('div'); div.id = kDivId; div.style.all = 'initial'; div.style.position = 'fixed'; div.style.bottom = '10px'; div.style.left = '0'; div.style.right = '0'; div.style.width = 'fit-content'; div.style.height = '20px'; div.style.padding = '10px'; div.style.zIndex = '99'; div.style.margin = 'auto'; div.style.backgroundColor = '#fff'; div.style.color = '#333'; div.style.fontFamily = 'monospace'; div.style.fontSize = '12px'; div.style.border = '1px solid #eee'; div.style.boxShadow = '0 0 20px #aaa'; div.style.display = 'flex'; div.style.flexDirection = 'column'; const title = document.createElement('div'); title.className = 'perfetto-open-title'; title.innerText = 'Opening perfetto trace'; title.style.fontWeight = '12px'; title.style.textAlign = 'center'; div.appendChild(title); const progressbar = document.createElement('progress'); progressbar.className = 'perfetto-open-progress'; progressbar.style.width = '200px'; progressbar.value = 0; div.appendChild(progressbar); document.body.appendChild(div); } const title = div.querySelector('.perfetto-open-title') as HTMLElement; if (progress.status !== undefined) { title.innerText = progress.status; } const bar = div.querySelector('.perfetto-open-progress') as HTMLInputElement; if (progress.progress === undefined) { bar.style.visibility = 'hidden'; } else { bar.style.visibility = 'visible'; bar.value = `${progress.progress}`; } if (progress.close === true) { div.remove(); } }