• 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 * as m from 'mithril';
16
17import {TraceUrlSource} from '../common/state';
18import {saveTrace} from '../common/upload_utils';
19
20import {globals} from './globals';
21import {showModal} from './modal';
22import {isShareable} from './trace_attrs';
23
24// Never show more than one dialog per minute.
25const MIN_REPORT_PERIOD_MS = 60000;
26let timeLastReport = 0;
27
28// Keeps the last ERR_QUEUE_MAX_LEN errors while the dialog is throttled.
29const queuedErrors = new Array<string>();
30const ERR_QUEUE_MAX_LEN = 10;
31
32export function maybeShowErrorDialog(errLog: string) {
33  globals.logging.logError(errLog);
34  const now = performance.now();
35
36  // Here we rely on the exception message from onCannotGrowMemory function
37  if (errLog.includes('Cannot enlarge memory')) {
38    showOutOfMemoryDialog();
39    // Refresh timeLastReport to prevent a different error showing a dialog
40    timeLastReport = now;
41    return;
42  }
43
44  if (errLog.includes('Unable to claim interface.') ||
45      errLog.includes('A transfer error has occurred')) {
46    showWebUSBError();
47    timeLastReport = now;
48  }
49
50  if (errLog.includes('(ERR:fmt)')) {
51    showUnknownFileError();
52    return;
53  }
54
55  if (errLog.includes('(ERR:rpc_seq)')) {
56    showRpcSequencingError();
57    return;
58  }
59
60  if (timeLastReport > 0 && now - timeLastReport <= MIN_REPORT_PERIOD_MS) {
61    queuedErrors.unshift(errLog);
62    if (queuedErrors.length > ERR_QUEUE_MAX_LEN) queuedErrors.pop();
63    console.log('Suppressing crash dialog, last error notified too soon.');
64    return;
65  }
66  timeLastReport = now;
67
68  // Append queued errors.
69  while (queuedErrors.length > 0) {
70    const queuedErr = queuedErrors.shift();
71    errLog += `\n\n---------------------------------------\n${queuedErr}`;
72  }
73
74  const errTitle = errLog.split('\n', 1)[0].substr(0, 80);
75  const userDescription = '';
76  let checked = false;
77  const engine = Object.values(globals.state.engines)[0];
78
79  const shareTraceSection: m.Vnode[] = [];
80  if (isShareable() && !urlExists()) {
81    shareTraceSection.push(
82        m(`input[type=checkbox]`, {
83          checked,
84          oninput: (ev: InputEvent) => {
85            checked = (ev.target as HTMLInputElement).checked;
86            if (checked && engine.source.type === 'FILE') {
87              saveTrace(engine.source.file).then(url => {
88                const errMessage = createErrorMessage(errLog, checked, url);
89                renderModal(
90                    errTitle, errMessage, userDescription, shareTraceSection);
91                return;
92              });
93            }
94            const errMessage = createErrorMessage(errLog, checked);
95            renderModal(
96                errTitle, errMessage, userDescription, shareTraceSection);
97          },
98        }),
99        m('span', `Check this box to share the current trace for debugging
100     purposes.`),
101        m('div.modal-small',
102          `This will create a permalink to this trace, you may
103     leave it unchecked and attach the trace manually
104     to the bug if preferred.`));
105  }
106  renderModal(
107      errTitle,
108      createErrorMessage(errLog, checked),
109      userDescription,
110      shareTraceSection);
111}
112
113function renderModal(
114    errTitle: string,
115    errMessage: string,
116    userDescription: string,
117    shareTraceSection: m.Vnode[]) {
118  showModal({
119    title: 'Oops, something went wrong. Please file a bug.',
120    content:
121        m('div',
122          m('.modal-logs', errMessage),
123          m('span', `Please provide any additional details describing
124           how the crash occurred:`),
125          m('textarea.modal-textarea', {
126            rows: 3,
127            maxlength: 1000,
128            oninput: (ev: InputEvent) => {
129              userDescription = (ev.target as HTMLTextAreaElement).value;
130            },
131            onkeydown: (e: Event) => {
132              e.stopPropagation();
133            },
134            onkeyup: (e: Event) => {
135              e.stopPropagation();
136            },
137          }),
138          shareTraceSection),
139    buttons: [
140      {
141        text: 'File a bug (Googlers only)',
142        primary: true,
143        id: 'file_bug',
144        action: () => {
145          window.open(
146              createLink(errTitle, errMessage, userDescription), '_blank');
147        }
148      },
149    ]
150  });
151}
152
153// If there is a trace URL to share, we don't have to show the upload checkbox.
154function urlExists() {
155  const engine = Object.values(globals.state.engines)[0];
156  return engine !== undefined &&
157      (engine.source.type === 'ARRAY_BUFFER' || engine.source.type === 'URL') &&
158      engine.source.url !== undefined;
159}
160
161function createErrorMessage(errLog: string, checked: boolean, url?: string) {
162  let errMessage = '';
163  const engine = Object.values(globals.state.engines)[0];
164  if (checked && url !== undefined) {
165    errMessage += `Trace: ${url}`;
166  } else if (urlExists()) {
167    errMessage += `Trace: ${(engine.source as TraceUrlSource).url}`;
168  } else {
169    errMessage += 'To assist with debugging please attach or link to the ' +
170        'trace you were viewing.';
171  }
172  return errMessage + '\n\n' +
173      'Viewed on: ' + self.location.origin + '\n\n' + errLog;
174}
175
176function createLink(
177    errTitle: string, errMessage: string, userDescription: string): string {
178  let link = 'https://goto.google.com/perfetto-ui-bug';
179  link += '?title=' + encodeURIComponent(`UI Error: ${errTitle}`);
180  link += '&description=';
181  if (userDescription !== '') {
182    link +=
183        encodeURIComponent('User description:\n' + userDescription + '\n\n');
184  }
185  link += encodeURIComponent(errMessage);
186  // 8kb is common limit on request size so restrict links to that long:
187  return link.substr(0, 8000);
188}
189
190function showOutOfMemoryDialog() {
191  const url =
192      'https://perfetto.dev/docs/quickstart/trace-analysis#get-trace-processor';
193
194  const tpCmd = 'curl -LO https://get.perfetto.dev/trace_processor\n' +
195      'chmod +x ./trace_processor\n' +
196      'trace_processor --httpd /path/to/trace.pftrace\n' +
197      '# Reload the UI, it will prompt to use the HTTP+RPC interface';
198  showModal({
199    title: 'Oops! Your WASM trace processor ran out of memory',
200    content: m(
201        'div',
202        m('span',
203          'The in-memory representation of the trace is too big ' +
204              'for the browser memory limits (typically 2GB per tab).'),
205        m('br'),
206        m('span',
207          'You can work around this problem by using the trace_processor ' +
208              'native binary as an accelerator for the UI as follows:'),
209        m('br'),
210        m('br'),
211        m('.modal-bash', tpCmd),
212        m('br'),
213        m('span', 'For details see '),
214        m('a', {href: url, target: '_blank'}, url),
215        ),
216    buttons: []
217  });
218}
219
220function showUnknownFileError() {
221  showModal({
222    title: 'Cannot open this file',
223    content: m(
224        'div',
225        m('p',
226          'The file opened doesn\'t look like a Perfetto trace or any ' +
227              'other format recognized by the Perfetto TraceProcessor.'),
228        m('p', 'Formats supported:'),
229        m(
230            'ul',
231            m('li', 'Perfetto protobuf trace'),
232            m('li', 'chrome://tracing JSON'),
233            m('li', 'Android systrace'),
234            m('li', 'Fuchsia trace'),
235            m('li', 'Ninja build log'),
236            ),
237        ),
238    buttons: []
239  });
240}
241
242function showWebUSBError() {
243  showModal({
244    title: 'A WebUSB error occurred',
245    content: m(
246        'div',
247        m('span', `Is adb already running on the host? Run this command and
248      try again.`),
249        m('br'),
250        m('.modal-bash', '> adb kill-server'),
251        m('br'),
252        m('span', 'For details see '),
253        m('a', {href: 'http://b/159048331', target: '_blank'}, 'b/159048331'),
254        ),
255    buttons: []
256  });
257}
258
259function showRpcSequencingError() {
260  showModal({
261    title: 'A TraceProcessor RPC error occurred',
262    content: m(
263        'div',
264        m('p', 'The trace processor RPC sequence ID was broken'),
265        m('p', `This can happen when using a HTTP trace processor instance and
266either accidentally sharing this between multiple tabs or
267restarting the trace processor while still in use by UI.`),
268        m('p', `Please refresh this tab and ensure that trace processor is used
269at most one tab at a time.`),
270        ),
271    buttons: []
272  });
273}
274