• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2019 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 {ErrorDetails} from '../base/logging';
17import {GcsUploader} from '../base/gcs_uploader';
18import {raf} from '../core/raf_scheduler';
19import {VERSION} from '../gen/perfetto_version';
20import {getCurrentModalKey, showModal} from '../widgets/modal';
21import {globals} from './globals';
22import {AppImpl} from '../core/app_impl';
23import {Router} from '../core/router';
24
25const MODAL_KEY = 'crash_modal';
26
27// Never show more than one dialog per 10s.
28const MIN_REPORT_PERIOD_MS = 10000;
29let timeLastReport = 0;
30
31export function maybeShowErrorDialog(err: ErrorDetails) {
32  const now = performance.now();
33
34  // Here we rely on the exception message from onCannotGrowMemory function
35  if (
36    err.message.includes('Cannot enlarge memory') ||
37    err.stack.some((entry) => entry.name.includes('base::AlignedAlloc')) ||
38    err.stack.some((entry) => entry.name.includes('OutOfMemoryHandler')) ||
39    err.stack.some((entry) => entry.name.includes('_emscripten_resize_heap')) ||
40    err.stack.some((entry) => entry.name.includes('sbrk')) ||
41    /^out of memory$/m.exec(err.message)
42  ) {
43    showOutOfMemoryDialog();
44    // Refresh timeLastReport to prevent a different error showing a dialog
45    timeLastReport = now;
46    return;
47  }
48
49  if (err.message.includes('Unable to claim interface')) {
50    showWebUSBError();
51    timeLastReport = now;
52    return;
53  }
54
55  if (
56    err.message.includes('A transfer error has occurred') ||
57    err.message.includes('The device was disconnected') ||
58    err.message.includes('The transfer was cancelled')
59  ) {
60    showConnectionLostError();
61    timeLastReport = now;
62    return;
63  }
64
65  if (err.message.includes('(ERR:fmt)')) {
66    showUnknownFileError();
67    return;
68  }
69
70  if (err.message.includes('(ERR:rpc_seq)')) {
71    showRpcSequencingError();
72    return;
73  }
74
75  if (err.message.includes('(ERR:ws)')) {
76    showWebsocketConnectionIssue(err.message);
77    return;
78  }
79
80  // This is only for older version of the UI and for ease of tracking across
81  // cherry-picks. Newer versions don't have this exception anymore.
82  if (err.message.includes('State hash does not match')) {
83    showNewerStateError();
84    return;
85  }
86
87  if (timeLastReport > 0 && now - timeLastReport <= MIN_REPORT_PERIOD_MS) {
88    console.log('Suppressing crash dialog, last error notified too soon.');
89    return;
90  }
91  timeLastReport = now;
92
93  // If we are already showing a crash dialog, don't overwrite it with a newer
94  // crash. Usually the first crash matters, the rest avalanching effects.
95  if (getCurrentModalKey() === MODAL_KEY) {
96    return;
97  }
98
99  showModal({
100    key: MODAL_KEY,
101    title: 'Oops, something went wrong. Please file a bug.',
102    content: () => m(ErrorDialogComponent, err),
103  });
104}
105
106class ErrorDialogComponent implements m.ClassComponent<ErrorDetails> {
107  private traceState:
108    | 'NOT_AVAILABLE'
109    | 'NOT_UPLOADED'
110    | 'UPLOADING'
111    | 'UPLOADED';
112  private traceType: string = 'No trace loaded';
113  private traceData?: ArrayBuffer | File;
114  private traceUrl?: string;
115  private attachTrace = false;
116  private uploadStatus = '';
117  private userDescription = '';
118  private errorMessage = '';
119  private uploader?: GcsUploader;
120
121  constructor() {
122    this.traceState = 'NOT_AVAILABLE';
123    const traceSource = AppImpl.instance.trace?.traceInfo.source;
124    if (traceSource === undefined) return;
125    this.traceType = traceSource.type;
126    // If the trace is either already uploaded, or comes from a postmessage+url
127    // we don't need any re-upload.
128    if ('url' in traceSource && traceSource.url !== undefined) {
129      this.traceUrl = traceSource.url;
130      this.traceState = 'UPLOADED';
131      // The trace is already uploaded, so assume the user is fine attaching to
132      // the bugreport (this make the checkbox ticked by default).
133      this.attachTrace = true;
134      return;
135    }
136
137    // If the user is not a googler, don't even offer the option to upload it.
138    if (!globals.isInternalUser) return;
139
140    if (traceSource.type === 'FILE') {
141      this.traceState = 'NOT_UPLOADED';
142      this.traceData = traceSource.file;
143      // this.traceSize = this.traceData.size;
144    } else if (traceSource.type === 'ARRAY_BUFFER') {
145      this.traceData = traceSource.buffer;
146      // this.traceSize = this.traceData.byteLength;
147    } else {
148      return; // Can't upload HTTP+RPC.
149    }
150    this.traceState = 'NOT_UPLOADED';
151  }
152
153  view(vnode: m.Vnode<ErrorDetails>) {
154    const err = vnode.attrs;
155    let msg = `UI: ${location.protocol}//${location.host}/${VERSION}\n\n`;
156
157    // Append the trace stack.
158    msg += `${err.message}\n`;
159    for (const entry of err.stack) {
160      msg += ` - ${entry.name} (${entry.location})\n`;
161    }
162    msg += '\n';
163
164    // Append the trace URL.
165    if (this.attachTrace && this.traceUrl) {
166      msg += `Trace: ${this.traceUrl}\n`;
167    } else if (this.attachTrace && this.traceState === 'UPLOADING') {
168      msg += `Trace: uploading...\n`;
169    } else {
170      msg += `Trace: not available (${this.traceType}). Provide repro steps.\n`;
171    }
172    msg += `UA: ${navigator.userAgent}\n`;
173    msg += `Referrer: ${document.referrer}\n`;
174    this.errorMessage = msg;
175
176    let shareTraceSection: m.Vnode | null = null;
177    if (this.traceState !== 'NOT_AVAILABLE') {
178      shareTraceSection = m(
179        'div',
180        m(
181          'label',
182          m(`input[type=checkbox]`, {
183            checked: this.attachTrace,
184            oninput: (ev: InputEvent) => {
185              const checked = (ev.target as HTMLInputElement).checked;
186              this.onUploadCheckboxChange(checked);
187            },
188          }),
189          this.traceState === 'UPLOADING'
190            ? `Uploading trace... ${this.uploadStatus}`
191            : 'Tick to share the current trace and help debugging',
192        ), // m('label')
193        m(
194          'div.modal-small',
195          `This will upload the trace and attach a link to the bug.
196          You may leave it unchecked and attach the trace manually to the bug
197          if preferred.`,
198        ),
199      );
200    } // if (this.traceState !== 'NOT_AVAILABLE')
201
202    return [
203      m(
204        'div',
205        m('.modal-logs', msg),
206        m(
207          'span',
208          `Please provide any additional details describing
209        how the crash occurred:`,
210        ),
211        m('textarea.modal-textarea', {
212          rows: 3,
213          maxlength: 1000,
214          oninput: (ev: InputEvent) => {
215            this.userDescription = (ev.target as HTMLTextAreaElement).value;
216          },
217          onkeydown: (e: Event) => e.stopPropagation(),
218          onkeyup: (e: Event) => e.stopPropagation(),
219        }),
220        shareTraceSection,
221      ),
222      m(
223        'footer',
224        m(
225          'button.modal-btn.modal-btn-primary',
226          {onclick: () => this.fileBug(err)},
227          'File a bug (Googlers only)',
228        ),
229      ),
230    ];
231  }
232
233  private onUploadCheckboxChange(checked: boolean) {
234    this.attachTrace = checked;
235
236    if (
237      checked &&
238      this.traceData !== undefined &&
239      this.traceState === 'NOT_UPLOADED'
240    ) {
241      this.traceState = 'UPLOADING';
242      this.uploadStatus = '';
243      const uploader = new GcsUploader(this.traceData, {
244        onProgress: () => {
245          raf.scheduleFullRedraw();
246          this.uploadStatus = uploader.getEtaString();
247          if (uploader.state === 'UPLOADED') {
248            this.traceState = 'UPLOADED';
249            this.traceUrl = uploader.uploadedUrl;
250          } else if (uploader.state === 'ERROR') {
251            this.traceState = 'NOT_UPLOADED';
252            this.uploadStatus = uploader.error;
253          }
254        },
255      });
256      this.uploader = uploader;
257    } else if (!checked && this.uploader) {
258      this.uploader.abort();
259    }
260  }
261
262  private fileBug(err: ErrorDetails) {
263    const errTitle = err.message.split('\n', 1)[0].substring(0, 80);
264    let url = 'https://goto.google.com/perfetto-ui-bug';
265    url += '?title=' + encodeURIComponent(`UI Error: ${errTitle}`);
266    url += '&description=';
267    if (this.userDescription !== '') {
268      url += encodeURIComponent(
269        'User description:\n' + this.userDescription + '\n\n',
270      );
271    }
272    url += encodeURIComponent(this.errorMessage);
273    // 8kb is common limit on request size so restrict links to that long:
274    url = url.substring(0, 8000);
275    window.open(url, '_blank');
276  }
277}
278
279function showOutOfMemoryDialog() {
280  const url =
281    'https://perfetto.dev/docs/quickstart/trace-analysis#get-trace-processor';
282
283  const tpCmd =
284    'curl -LO https://get.perfetto.dev/trace_processor\n' +
285    'chmod +x ./trace_processor\n' +
286    'trace_processor --httpd /path/to/trace.pftrace\n' +
287    '# Reload the UI, it will prompt to use the HTTP+RPC interface';
288  showModal({
289    title: 'Oops! Your WASM trace processor ran out of memory',
290    content: m(
291      'div',
292      m(
293        'span',
294        'The in-memory representation of the trace is too big ' +
295          'for the browser memory limits (typically 2GB per tab).',
296      ),
297      m('br'),
298      m(
299        'span',
300        'You can work around this problem by using the trace_processor ' +
301          'native binary as an accelerator for the UI as follows:',
302      ),
303      m('br'),
304      m('br'),
305      m('.modal-bash', tpCmd),
306      m('br'),
307      m('span', 'For details see '),
308      m('a', {href: url, target: '_blank'}, url),
309    ),
310  });
311}
312
313function showUnknownFileError() {
314  showModal({
315    title: 'Cannot open this file',
316    content: m(
317      'div',
318      m(
319        'p',
320        "The file opened doesn't look like a Perfetto trace or any " +
321          'other format recognized by the Perfetto TraceProcessor.',
322      ),
323      m('p', 'Formats supported:'),
324      m(
325        'ul',
326        m('li', 'Perfetto protobuf trace'),
327        m('li', 'chrome://tracing JSON'),
328        m('li', 'Android systrace'),
329        m('li', 'Fuchsia trace'),
330        m('li', 'Ninja build log'),
331      ),
332    ),
333  });
334}
335
336function showWebUSBError() {
337  showModal({
338    title: 'A WebUSB error occurred',
339    content: m(
340      'div',
341      m(
342        'span',
343        `Is adb already running on the host? Run this command and
344      try again.`,
345      ),
346      m('br'),
347      m('.modal-bash', '> adb kill-server'),
348      m('br'),
349      m('span', 'For details see '),
350      m('a', {href: 'http://b/159048331', target: '_blank'}, 'b/159048331'),
351    ),
352  });
353}
354
355function showRpcSequencingError() {
356  showModal({
357    title: 'A TraceProcessor RPC error occurred',
358    content: m(
359      'div',
360      m('p', 'The trace processor RPC sequence ID was broken'),
361      m(
362        'p',
363        `This can happen when using a HTTP trace processor instance and
364either accidentally sharing this between multiple tabs or
365restarting the trace processor while still in use by UI.`,
366      ),
367      m(
368        'p',
369        `Please refresh this tab and ensure that trace processor is used
370at most one tab at a time.`,
371      ),
372    ),
373  });
374}
375
376function showNewerStateError() {
377  showModal({
378    title: 'Cannot deserialize the permalink',
379    content: m(
380      'div',
381      m('p', "The state hash doesn't match."),
382      m(
383        'p',
384        'This usually happens when the permalink is generated by a version ' +
385          'the UI that is newer than the current version, e.g., when a ' +
386          'colleague created the permalink using the Canary or Autopush ' +
387          'channel and you are trying to open it using Stable channel.',
388      ),
389      m(
390        'p',
391        'Try switching to Canary or Autopush channel from the Flags page ' +
392          ' and try again.',
393      ),
394    ),
395    buttons: [
396      {
397        text: 'Take me to the flags page',
398        primary: true,
399        action: () => Router.navigate('#!/flags/releaseChannel'),
400      },
401    ],
402  });
403}
404
405function showWebsocketConnectionIssue(message: string): void {
406  showModal({
407    title: 'Unable to connect to the device via websocket',
408    content: m(
409      'div',
410      m('div', 'trace_processor_shell --httpd is unreachable or crashed.'),
411      m('pre', message),
412    ),
413  });
414}
415
416function showConnectionLostError(): void {
417  showModal({
418    title: 'Connection with the ADB device lost',
419    content: m(
420      'div',
421      m('span', `Please connect the device again to restart the recording.`),
422      m('br'),
423    ),
424  });
425}
426