1// Copyright (C) 2024 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// open-perfetto-trace is a standalone JS/TS library that can be used in other 16// projects to facilitate the deep linking into perfetto. It allows opening 17// trace files or URL with ui.perfetto.dev, handling all the handshake with it. 18 19const PERFETTO_UI_URL = 'https://ui.perfetto.dev'; 20 21interface OpenTraceOptions { 22 // If true (default) shows a popup dialog with a progress bar that informs 23 // about the status of the fetch. This is only relevant when the trace source 24 // is a url. 25 statusDialog?: boolean; 26 27 // Opens the trace in a new tab. 28 newTab?: boolean; 29 30 // Override the referrer. Useful for scripts such as 31 // record_android_trace to record where the trace is coming from. 32 referrer?: string; 33 34 // For the 'mode' of the UI. For example when the mode is 'embedded' 35 // some features are disabled. 36 mode: 'embedded' | undefined; 37 38 // Hides the sidebar in the opened perfetto UI. 39 hideSidebar?: boolean; 40 41 ts?: string; 42 43 dur?: string; 44 tid?: string; 45 pid?: string; 46 query?: string; 47 visStart?: string; 48 visEnd?: string; 49 50 // Used to override ui.perfetto.dev with a custom hosted URL. 51 // Useful for testing. 52 uiUrl?: string; 53} 54 55// Opens a trace in the Perfetto UI. 56// `source` can be either: 57// - A blob (e.g. a File). 58// - A URL. 59export default function openPerfettoTrace( 60 source: Blob | string, 61 opts?: OpenTraceOptions, 62) { 63 if (source instanceof Blob) { 64 return openTraceBlob(source, opts); 65 } else if (typeof source === 'string') { 66 return fetchAndOpenTrace(source, opts); 67 } 68} 69 70function openTraceBlob(blob: Blob, opts?: OpenTraceOptions) { 71 const form = document.createElement('form'); 72 form.method = 'POST'; 73 form.style.visibility = 'hidden'; 74 form.enctype = 'multipart/form-data'; 75 const uiUrl = opts?.uiUrl ?? PERFETTO_UI_URL; 76 form.action = `${uiUrl}/_open_trace/${Date.now()}`; 77 if (opts?.newTab === true) { 78 form.target = '_blank'; 79 } 80 const fileInput = document.createElement('input'); 81 fileInput.name = 'trace'; 82 fileInput.type = 'file'; 83 const dataTransfer = new DataTransfer(); 84 dataTransfer.items.add(new File([blob], 'trace.file')); 85 fileInput.files = dataTransfer.files; 86 form.appendChild(fileInput); 87 for (const [key, value] of Object.entries(opts ?? {})) { 88 const varInput = document.createElement('input'); 89 varInput.type = 'hidden'; 90 varInput.name = key; 91 varInput.value = value; 92 form.appendChild(varInput); 93 } 94 document.body.appendChild(form); 95 form.submit(); 96} 97 98function fetchAndOpenTrace(url: string, opts?: OpenTraceOptions) { 99 updateProgressDiv({status: 'Fetching trace'}, opts); 100 const xhr = new XMLHttpRequest(); 101 xhr.addEventListener('progress', (event) => { 102 if (event.lengthComputable) { 103 updateProgressDiv( 104 { 105 status: `Fetching trace (${Math.round(event.loaded / 1000)} KB)`, 106 progress: event.loaded / event.total, 107 }, 108 opts, 109 ); 110 } 111 }); 112 xhr.addEventListener('loadend', () => { 113 if (xhr.readyState === 4 && xhr.status === 200) { 114 const blob = xhr.response as Blob; 115 updateProgressDiv({status: 'Opening trace'}, opts); 116 openTraceBlob(blob, opts); 117 updateProgressDiv({close: true}, opts); 118 } 119 }); 120 xhr.addEventListener('error', () => { 121 updateProgressDiv({status: 'Failed to fetch trace'}, opts); 122 }); 123 xhr.responseType = 'blob'; 124 xhr.overrideMimeType('application/octet-stream'); 125 xhr.open('GET', url); 126 xhr.send(); 127} 128 129interface ProgressDivOpts { 130 status?: string; 131 progress?: number; 132 close?: boolean; 133} 134 135function updateProgressDiv(progress: ProgressDivOpts, opts?: OpenTraceOptions) { 136 if (opts?.statusDialog === false) return; 137 138 const kDivId = 'open_perfetto_trace'; 139 let div = document.getElementById(kDivId); 140 if (!div) { 141 div = document.createElement('div'); 142 div.id = kDivId; 143 div.style.all = 'initial'; 144 div.style.position = 'fixed'; 145 div.style.bottom = '10px'; 146 div.style.left = '0'; 147 div.style.right = '0'; 148 div.style.width = 'fit-content'; 149 div.style.height = '20px'; 150 div.style.padding = '10px'; 151 div.style.zIndex = '99'; 152 div.style.margin = 'auto'; 153 div.style.backgroundColor = '#fff'; 154 div.style.color = '#333'; 155 div.style.fontFamily = 'monospace'; 156 div.style.fontSize = '12px'; 157 div.style.border = '1px solid #eee'; 158 div.style.boxShadow = '0 0 20px #aaa'; 159 div.style.display = 'flex'; 160 div.style.flexDirection = 'column'; 161 162 const title = document.createElement('div'); 163 title.className = 'perfetto-open-title'; 164 title.innerText = 'Opening perfetto trace'; 165 title.style.fontWeight = '12px'; 166 title.style.textAlign = 'center'; 167 div.appendChild(title); 168 169 const progressbar = document.createElement('progress'); 170 progressbar.className = 'perfetto-open-progress'; 171 progressbar.style.width = '200px'; 172 progressbar.value = 0; 173 div.appendChild(progressbar); 174 175 document.body.appendChild(div); 176 } 177 const title = div.querySelector('.perfetto-open-title') as HTMLElement; 178 if (progress.status !== undefined) { 179 title.innerText = progress.status; 180 } 181 const bar = div.querySelector('.perfetto-open-progress') as HTMLInputElement; 182 if (progress.progress === undefined) { 183 bar.style.visibility = 'hidden'; 184 } else { 185 bar.style.visibility = 'visible'; 186 bar.value = `${progress.progress}`; 187 } 188 if (progress.close === true) { 189 div.remove(); 190 } 191} 192