• 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 {assertExists} from '../base/logging';
18import {RECORDING_V2_FLAG} from '../common/feature_flags';
19import {EXTENSION_URL} from '../common/recordingV2/recording_utils';
20import {TraceUrlSource} from '../common/state';
21import {saveTrace} from '../common/upload_utils';
22
23import {globals} from './globals';
24import {showModal} from './modal';
25import {isShareable} from './trace_attrs';
26
27// Never show more than one dialog per minute.
28const MIN_REPORT_PERIOD_MS = 60000;
29let timeLastReport = 0;
30
31// Keeps the last ERR_QUEUE_MAX_LEN errors while the dialog is throttled.
32const queuedErrors = new Array<string>();
33const ERR_QUEUE_MAX_LEN = 10;
34
35export function maybeShowErrorDialog(errLog: string) {
36  globals.logging.logError(errLog);
37  const now = performance.now();
38
39  // Here we rely on the exception message from onCannotGrowMemory function
40  if (errLog.includes('Cannot enlarge memory')) {
41    showOutOfMemoryDialog();
42    // Refresh timeLastReport to prevent a different error showing a dialog
43    timeLastReport = now;
44    return;
45  }
46
47  if (!RECORDING_V2_FLAG.get()) {
48    if (errLog.includes('Unable to claim interface')) {
49      showWebUSBError();
50      timeLastReport = now;
51      return;
52    }
53
54    if (errLog.includes('A transfer error has occurred') ||
55        errLog.includes('The device was disconnected') ||
56        errLog.includes('The transfer was cancelled')) {
57      showConnectionLostError();
58      timeLastReport = now;
59      return;
60    }
61  }
62
63  if (errLog.includes('(ERR:fmt)')) {
64    showUnknownFileError();
65    return;
66  }
67
68  if (errLog.includes('(ERR:rpc_seq)')) {
69    showRpcSequencingError();
70    return;
71  }
72
73  if (timeLastReport > 0 && now - timeLastReport <= MIN_REPORT_PERIOD_MS) {
74    queuedErrors.unshift(errLog);
75    if (queuedErrors.length > ERR_QUEUE_MAX_LEN) queuedErrors.pop();
76    console.log('Suppressing crash dialog, last error notified too soon.');
77    return;
78  }
79  timeLastReport = now;
80
81  // Append queued errors.
82  while (queuedErrors.length > 0) {
83    const queuedErr = queuedErrors.shift();
84    errLog += `\n\n---------------------------------------\n${queuedErr}`;
85  }
86
87  const errTitle = errLog.split('\n', 1)[0].substr(0, 80);
88  const userDescription = '';
89  let checked = false;
90  const engine = globals.getCurrentEngine();
91
92  const shareTraceSection: m.Vnode[] = [];
93  if (isShareable() && !urlExists()) {
94    shareTraceSection.push(
95        m(`input[type=checkbox]`, {
96          checked,
97          oninput: (ev: InputEvent) => {
98            checked = (ev.target as HTMLInputElement).checked;
99            if (checked && engine && engine.source.type === 'FILE') {
100              saveTrace(engine.source.file).then((url) => {
101                const errMessage = createErrorMessage(errLog, checked, url);
102                renderModal(
103                    errTitle, errMessage, userDescription, shareTraceSection);
104                return;
105              });
106            }
107            const errMessage = createErrorMessage(errLog, checked);
108            renderModal(
109                errTitle, errMessage, userDescription, shareTraceSection);
110          },
111        }),
112        m('span', `Check this box to share the current trace for debugging
113     purposes.`),
114        m('div.modal-small',
115          `This will create a permalink to this trace, you may
116     leave it unchecked and attach the trace manually
117     to the bug if preferred.`));
118  }
119  renderModal(
120      errTitle,
121      createErrorMessage(errLog, checked),
122      userDescription,
123      shareTraceSection);
124}
125
126function renderModal(
127    errTitle: string,
128    errMessage: string,
129    userDescription: string,
130    shareTraceSection: m.Vnode[]) {
131  showModal({
132    title: 'Oops, something went wrong. Please file a bug.',
133    content:
134        m('div',
135          m('.modal-logs', errMessage),
136          m('span', `Please provide any additional details describing
137           how the crash occurred:`),
138          m('textarea.modal-textarea', {
139            rows: 3,
140            maxlength: 1000,
141            oninput: (ev: InputEvent) => {
142              userDescription = (ev.target as HTMLTextAreaElement).value;
143            },
144            onkeydown: (e: Event) => {
145              e.stopPropagation();
146            },
147            onkeyup: (e: Event) => {
148              e.stopPropagation();
149            },
150          }),
151          shareTraceSection),
152    buttons: [
153      {
154        text: 'File a bug (Googlers only)',
155        primary: true,
156        id: 'file_bug',
157        action: () => {
158          window.open(
159              createLink(errTitle, errMessage, userDescription), '_blank');
160        },
161      },
162    ],
163  });
164}
165
166// If there is a trace URL to share, we don't have to show the upload checkbox.
167function urlExists() {
168  const engine = globals.getCurrentEngine();
169  return engine !== undefined &&
170      (engine.source.type === 'ARRAY_BUFFER' || engine.source.type === 'URL') &&
171      engine.source.url !== undefined;
172}
173
174function createErrorMessage(errLog: string, checked: boolean, url?: string) {
175  let errMessage = '';
176  const engine = globals.getCurrentEngine();
177  if (checked && url !== undefined) {
178    errMessage += `Trace: ${url}`;
179  } else if (urlExists()) {
180    errMessage +=
181        `Trace: ${(assertExists(engine).source as TraceUrlSource).url}`;
182  } else {
183    errMessage += 'To assist with debugging please attach or link to the ' +
184        'trace you were viewing.';
185  }
186  return errMessage + '\n\n' +
187      'Viewed on: ' + self.location.origin + '\n\n' + errLog;
188}
189
190function createLink(
191    errTitle: string, errMessage: string, userDescription: string): string {
192  let link = 'https://goto.google.com/perfetto-ui-bug';
193  link += '?title=' + encodeURIComponent(`UI Error: ${errTitle}`);
194  link += '&description=';
195  if (userDescription !== '') {
196    link +=
197        encodeURIComponent('User description:\n' + userDescription + '\n\n');
198  }
199  link += encodeURIComponent(errMessage);
200  // 8kb is common limit on request size so restrict links to that long:
201  return link.substr(0, 8000);
202}
203
204function showOutOfMemoryDialog() {
205  const url =
206      'https://perfetto.dev/docs/quickstart/trace-analysis#get-trace-processor';
207
208  const tpCmd = 'curl -LO https://get.perfetto.dev/trace_processor\n' +
209      'chmod +x ./trace_processor\n' +
210      'trace_processor --httpd /path/to/trace.pftrace\n' +
211      '# Reload the UI, it will prompt to use the HTTP+RPC interface';
212  showModal({
213    title: 'Oops! Your WASM trace processor ran out of memory',
214    content: m(
215        'div',
216        m('span',
217          'The in-memory representation of the trace is too big ' +
218              'for the browser memory limits (typically 2GB per tab).'),
219        m('br'),
220        m('span',
221          'You can work around this problem by using the trace_processor ' +
222              'native binary as an accelerator for the UI as follows:'),
223        m('br'),
224        m('br'),
225        m('.modal-bash', tpCmd),
226        m('br'),
227        m('span', 'For details see '),
228        m('a', {href: url, target: '_blank'}, url),
229        ),
230    buttons: [],
231  });
232}
233
234function showUnknownFileError() {
235  showModal({
236    title: 'Cannot open this file',
237    content: m(
238        'div',
239        m('p',
240          'The file opened doesn\'t look like a Perfetto trace or any ' +
241              'other format recognized by the Perfetto TraceProcessor.'),
242        m('p', 'Formats supported:'),
243        m(
244            'ul',
245            m('li', 'Perfetto protobuf trace'),
246            m('li', 'chrome://tracing JSON'),
247            m('li', 'Android systrace'),
248            m('li', 'Fuchsia trace'),
249            m('li', 'Ninja build log'),
250            ),
251        ),
252    buttons: [],
253  });
254}
255
256function showWebUSBError() {
257  showModal({
258    title: 'A WebUSB error occurred',
259    content: m(
260        'div',
261        m('span', `Is adb already running on the host? Run this command and
262      try again.`),
263        m('br'),
264        m('.modal-bash', '> adb kill-server'),
265        m('br'),
266        m('span', 'For details see '),
267        m('a', {href: 'http://b/159048331', target: '_blank'}, 'b/159048331'),
268        ),
269    buttons: [],
270  });
271}
272
273export function showWebUSBErrorV2() {
274  showModal({
275    title: 'A WebUSB error occurred',
276    content: m(
277        'div',
278        m('span', `Is adb already running on the host? Run this command and
279      try again.`),
280        m('br'),
281        m('.modal-bash', '> adb kill-server'),
282        m('br'),
283        // The statement below covers the following edge case:
284        // 1. 'adb server' is running on the device.
285        // 2. The user selects the new Android target, so we try to fetch the
286        // OS version and do QSS.
287        // 3. The error modal is shown.
288        // 4. The user runs 'adb kill-server'.
289        // At this point we don't have a trigger to try fetching the OS version
290        // + QSS again. Therefore, the user will need to refresh the page.
291        m('span',
292          'If after running \'adb kill-server\', you don\'t see ' +
293              'a \'Start Recording\' button on the page and you don\'t see ' +
294              '\'Allow USB debugging\' on the device, ' +
295              'you will need to reload this page.'),
296        m('br'),
297        m('br'),
298        m('span', 'For details see '),
299        m('a', {href: 'http://b/159048331', target: '_blank'}, 'b/159048331'),
300        ),
301    buttons: [],
302  });
303}
304
305export function showConnectionLostError(): void {
306  showModal({
307    title: 'Connection with the ADB device lost',
308    content: m(
309        'div',
310        m('span', `Please connect the device again to restart the recording.`),
311        m('br')),
312    buttons: [],
313  });
314}
315
316export function showAllowUSBDebugging(): void {
317  showModal({
318    title: 'Could not connect to the device',
319    content: m(
320        'div', m('span', 'Please allow USB debugging on the device.'), m('br')),
321    buttons: [],
322  });
323}
324
325export function showNoDeviceSelected(): void {
326  showModal({
327    title: 'No device was selected for recording',
328    content:
329        m('div',
330          m('span', `If you want to connect to an ADB device,
331           please select it from the list.`),
332          m('br')),
333    buttons: [],
334  });
335}
336
337export function showExtensionNotInstalled(): void {
338  showModal({
339    title: 'Perfetto Chrome extension not installed',
340    content:
341        m('div',
342          m('.note',
343            `To trace Chrome from the Perfetto UI, you need to install our `,
344            m('a', {href: EXTENSION_URL, target: '_blank'}, 'Chrome extension'),
345            ' and then reload this page.'),
346          m('br')),
347    buttons: [],
348  });
349}
350
351export function showWebsocketConnectionIssue(message: string): void {
352  showModal({
353    title: 'Unable to connect to the device via websocket',
354    content: m('div', m('span', message), m('br')),
355    buttons: [],
356  });
357}
358
359export function showIssueParsingTheTracedResponse(message: string): void {
360  showModal({
361    title: 'A problem was encountered while connecting to' +
362        ' the Perfetto tracing service',
363    content: m('div', m('span', message), m('br')),
364    buttons: [],
365  });
366}
367
368export function showFailedToPushBinary(message: string): void {
369  showModal({
370    title: 'Failed to push a binary to the device',
371    content:
372        m('div',
373          m('span',
374            'This can happen if your Android device has an OS version lower ' +
375                'than Q. Perfetto tried to push the latest version of its ' +
376                'embedded binary but failed.'),
377          m('br'),
378          m('br'),
379          m('span', 'Error message:'),
380          m('br'),
381          m('span', message)),
382    buttons: [],
383  });
384}
385
386function showRpcSequencingError() {
387  showModal({
388    title: 'A TraceProcessor RPC error occurred',
389    content: m(
390        'div',
391        m('p', 'The trace processor RPC sequence ID was broken'),
392        m('p', `This can happen when using a HTTP trace processor instance and
393either accidentally sharing this between multiple tabs or
394restarting the trace processor while still in use by UI.`),
395        m('p', `Please refresh this tab and ensure that trace processor is used
396at most one tab at a time.`),
397        ),
398    buttons: [],
399  });
400}
401