xref: /aosp_15_r20/external/perfetto/ui/src/open_perfetto_trace/index.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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