xref: /aosp_15_r20/external/perfetto/ui/src/frontend/legacy_trace_viewer.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2018 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
15import m from 'mithril';
16import {inflate} from 'pako';
17import {assertTrue} from '../base/logging';
18import {isString} from '../base/object_utils';
19import {showModal} from '../widgets/modal';
20import {utf8Decode} from '../base/string_utils';
21import {convertToJson} from './trace_converter';
22import {assetSrc} from '../base/assets';
23
24const CTRACE_HEADER = 'TRACE:\n';
25
26async function isCtrace(file: File): Promise<boolean> {
27  const fileName = file.name.toLowerCase();
28
29  if (fileName.endsWith('.ctrace')) {
30    return true;
31  }
32
33  // .ctrace files sometimes end with .txt. We can detect these via
34  // the presence of TRACE: near the top of the file.
35  if (fileName.endsWith('.txt')) {
36    const header = await readText(file.slice(0, 128));
37    if (header.includes(CTRACE_HEADER)) {
38      return true;
39    }
40  }
41
42  return false;
43}
44
45function readText(blob: Blob): Promise<string> {
46  return new Promise((resolve, reject) => {
47    const reader = new FileReader();
48    reader.onload = () => {
49      if (isString(reader.result)) {
50        return resolve(reader.result);
51      }
52    };
53    reader.onerror = (err) => {
54      reject(err);
55    };
56    reader.readAsText(blob);
57  });
58}
59
60export async function isLegacyTrace(file: File): Promise<boolean> {
61  const fileName = file.name.toLowerCase();
62  if (
63    fileName.endsWith('.json') ||
64    fileName.endsWith('.json.gz') ||
65    fileName.endsWith('.zip') ||
66    fileName.endsWith('.html')
67  ) {
68    return true;
69  }
70
71  if (await isCtrace(file)) {
72    return true;
73  }
74
75  // Sometimes systrace formatted traces end with '.trace'. This is a
76  // little generic to assume all such traces are systrace format though
77  // so we read the beginning of the file and check to see if is has the
78  // systrace header (several comment lines):
79  if (fileName.endsWith('.trace')) {
80    const header = await readText(file.slice(0, 512));
81    const lines = header.split('\n');
82    let commentCount = 0;
83    for (const line of lines) {
84      if (line.startsWith('#')) {
85        commentCount++;
86      }
87    }
88    if (commentCount > 5) {
89      return true;
90    }
91  }
92
93  return false;
94}
95
96export async function openFileWithLegacyTraceViewer(file: File) {
97  const reader = new FileReader();
98  reader.onload = () => {
99    if (reader.result instanceof ArrayBuffer) {
100      return openBufferWithLegacyTraceViewer(
101        file.name,
102        reader.result,
103        reader.result.byteLength,
104      );
105    } else {
106      const str = reader.result as string;
107      return openBufferWithLegacyTraceViewer(file.name, str, str.length);
108    }
109  };
110  reader.onerror = (err) => {
111    console.error(err);
112  };
113  if (
114    file.name.endsWith('.gz') ||
115    file.name.endsWith('.zip') ||
116    (await isCtrace(file))
117  ) {
118    reader.readAsArrayBuffer(file);
119  } else {
120    reader.readAsText(file);
121  }
122}
123
124function openBufferWithLegacyTraceViewer(
125  name: string,
126  data: ArrayBuffer | string,
127  size: number,
128) {
129  if (data instanceof ArrayBuffer) {
130    assertTrue(size <= data.byteLength);
131    if (size !== data.byteLength) {
132      data = data.slice(0, size);
133    }
134
135    // Handle .ctrace files.
136    const header = utf8Decode(data.slice(0, 128));
137    if (header.includes(CTRACE_HEADER)) {
138      const offset = header.indexOf(CTRACE_HEADER) + CTRACE_HEADER.length;
139      data = inflate(new Uint8Array(data.slice(offset)), {to: 'string'});
140    }
141  }
142
143  // The location.pathname mangling is to make this code work also when hosted
144  // in a non-root sub-directory, for the case of CI artifacts.
145  const catapultUrl = assetSrc('assets/catapult_trace_viewer.html');
146  const newWin = window.open(catapultUrl);
147  if (newWin) {
148    // Popup succeedeed.
149    newWin.addEventListener('load', (e: Event) => {
150      const doc = e.target as Document;
151      const ctl = doc.querySelector('x-profiling-view') as TraceViewerAPI;
152      ctl.setActiveTrace(name, data);
153    });
154    return;
155  }
156
157  // Popup blocker detected.
158  showModal({
159    title: 'Open trace in the legacy Catapult Trace Viewer',
160    content: m(
161      'div',
162      m('div', 'You are seeing this interstitial because popups are blocked'),
163      m('div', 'Enable popups to skip this dialog next time.'),
164    ),
165    buttons: [
166      {
167        text: 'Open legacy UI',
168        primary: true,
169        action: () => openBufferWithLegacyTraceViewer(name, data, size),
170      },
171    ],
172  });
173}
174
175export async function openInOldUIWithSizeCheck(trace: Blob): Promise<void> {
176  // Perfetto traces smaller than 50mb can be safely opened in the legacy UI.
177  if (trace.size < 1024 * 1024 * 50) {
178    return await convertToJson(trace, openBufferWithLegacyTraceViewer);
179  }
180
181  // Give the user the option to truncate larger perfetto traces.
182  const size = Math.round(trace.size / (1024 * 1024));
183
184  // If the user presses one of the buttons below, remember the promise that
185  // they trigger, so we await for it before returning.
186  let nextPromise: Promise<void> | undefined;
187  const setNextPromise = (p: Promise<void>) => (nextPromise = p);
188
189  await showModal({
190    title: 'Legacy UI may fail to open this trace',
191    content: m(
192      'div',
193      m(
194        'p',
195        `This trace is ${size}mb, opening it in the legacy UI ` + `may fail.`,
196      ),
197      m(
198        'p',
199        'More options can be found at ',
200        m(
201          'a',
202          {
203            href: 'https://goto.google.com/opening-large-traces',
204            target: '_blank',
205          },
206          'go/opening-large-traces',
207        ),
208        '.',
209      ),
210    ),
211    buttons: [
212      {
213        text: 'Open full trace (not recommended)',
214        action: () =>
215          setNextPromise(convertToJson(trace, openBufferWithLegacyTraceViewer)),
216      },
217      {
218        text: 'Open beginning of trace',
219        action: () =>
220          setNextPromise(
221            convertToJson(
222              trace,
223              openBufferWithLegacyTraceViewer,
224              /* truncate*/ 'start',
225            ),
226          ),
227      },
228      {
229        text: 'Open end of trace',
230        primary: true,
231        action: () =>
232          setNextPromise(
233            convertToJson(
234              trace,
235              openBufferWithLegacyTraceViewer,
236              /* truncate*/ 'end',
237            ),
238          ),
239      },
240    ],
241  });
242  // nextPromise is undefined if the user just dimisses the dialog with ESC.
243  if (nextPromise !== undefined) {
244    await nextPromise;
245  }
246}
247
248// TraceViewer method that we wire up to trigger the file load.
249interface TraceViewerAPI extends Element {
250  setActiveTrace(name: string, data: ArrayBuffer | string): void;
251}
252