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