|  | // Copyright (C) 2019 The Android Open Source Project | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this file except in compliance with the License. | 
|  | // You may obtain a copy of the License at | 
|  | // | 
|  | //      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | // See the License for the specific language governing permissions and | 
|  | // limitations under the License. | 
|  |  | 
|  | import m from 'mithril'; | 
|  |  | 
|  | import {ErrorDetails} from '../base/logging'; | 
|  | import {EXTENSION_URL} from '../common/recordingV2/recording_utils'; | 
|  | import {TraceGcsUploader} from '../common/upload_utils'; | 
|  | import {RECORDING_V2_FLAG} from '../core/feature_flags'; | 
|  | import {raf} from '../core/raf_scheduler'; | 
|  | import {VERSION} from '../gen/perfetto_version'; | 
|  | import {getCurrentModalKey, showModal} from '../widgets/modal'; | 
|  |  | 
|  | import {globals} from './globals'; | 
|  |  | 
|  | const MODAL_KEY = 'crash_modal'; | 
|  |  | 
|  | // Never show more than one dialog per 10s. | 
|  | const MIN_REPORT_PERIOD_MS = 10000; | 
|  | let timeLastReport = 0; | 
|  |  | 
|  | export function maybeShowErrorDialog(err: ErrorDetails) { | 
|  | const now = performance.now(); | 
|  |  | 
|  | // Here we rely on the exception message from onCannotGrowMemory function | 
|  | if (err.message.includes('Cannot enlarge memory') || | 
|  | /^out of memory$/m.exec(err.message)) { | 
|  | showOutOfMemoryDialog(); | 
|  | // Refresh timeLastReport to prevent a different error showing a dialog | 
|  | timeLastReport = now; | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (!RECORDING_V2_FLAG.get()) { | 
|  | if (err.message.includes('Unable to claim interface')) { | 
|  | showWebUSBError(); | 
|  | timeLastReport = now; | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (err.message.includes('A transfer error has occurred') || | 
|  | err.message.includes('The device was disconnected') || | 
|  | err.message.includes('The transfer was cancelled')) { | 
|  | showConnectionLostError(); | 
|  | timeLastReport = now; | 
|  | return; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (err.message.includes('(ERR:fmt)')) { | 
|  | showUnknownFileError(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (err.message.includes('(ERR:rpc_seq)')) { | 
|  | showRpcSequencingError(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (timeLastReport > 0 && now - timeLastReport <= MIN_REPORT_PERIOD_MS) { | 
|  | console.log('Suppressing crash dialog, last error notified too soon.'); | 
|  | return; | 
|  | } | 
|  | timeLastReport = now; | 
|  |  | 
|  | // If we are already showing a crash dialog, don't overwrite it with a newer | 
|  | // crash. Usually the first crash matters, the rest avalanching effects. | 
|  | if (getCurrentModalKey() === MODAL_KEY) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | showModal({ | 
|  | key: MODAL_KEY, | 
|  | title: 'Oops, something went wrong. Please file a bug.', | 
|  | content: () => m(ErrorDialogComponent, err), | 
|  | }); | 
|  | } | 
|  |  | 
|  |  | 
|  | class ErrorDialogComponent implements m.ClassComponent<ErrorDetails> { | 
|  | private traceState: 'NOT_AVAILABLE'|'NOT_UPLOADED'|'UPLOADING'|'UPLOADED'; | 
|  | private traceType: string = 'No trace loaded'; | 
|  | private traceData?: ArrayBuffer|File; | 
|  | private traceUrl?: string; | 
|  | private attachTrace = false; | 
|  | private uploadStatus = ''; | 
|  | private userDescription = ''; | 
|  | private errorMessage = ''; | 
|  | private uploader?: TraceGcsUploader; | 
|  |  | 
|  | constructor() { | 
|  | this.traceState = 'NOT_AVAILABLE'; | 
|  | const engine = globals.getCurrentEngine(); | 
|  | if (engine === undefined) return; | 
|  | this.traceType = engine.source.type; | 
|  | // If the trace is either already uploaded, or comes from a postmessage+url | 
|  | // we don't need any re-upload. | 
|  | if ('url' in engine.source && engine.source.url !== undefined) { | 
|  | this.traceUrl = engine.source.url; | 
|  | this.traceState = 'UPLOADED'; | 
|  | // The trace is already uploaded, so assume the user is fine attaching to | 
|  | // the bugreport (this make the checkbox ticked by default). | 
|  | this.attachTrace = true; | 
|  | return; | 
|  | } | 
|  |  | 
|  | // If the user is not a googler, don't even offer the option to upload it. | 
|  | if (!globals.isInternalUser) return; | 
|  |  | 
|  | if (engine.source.type === 'FILE') { | 
|  | this.traceState = 'NOT_UPLOADED'; | 
|  | this.traceData = engine.source.file; | 
|  | // this.traceSize = this.traceData.size; | 
|  | } else if (engine.source.type === 'ARRAY_BUFFER') { | 
|  | this.traceData = engine.source.buffer; | 
|  | // this.traceSize = this.traceData.byteLength; | 
|  | } else { | 
|  | return;  // Can't upload HTTP+RPC. | 
|  | } | 
|  | this.traceState = 'NOT_UPLOADED'; | 
|  | } | 
|  |  | 
|  | view(vnode: m.Vnode<ErrorDetails>) { | 
|  | const err = vnode.attrs; | 
|  | let msg = `UI: ${location.protocol}//${location.host}/${VERSION}\n\n`; | 
|  |  | 
|  | // Append the trace stack. | 
|  | msg += `${err.message}\n`; | 
|  | for (const entry of err.stack) { | 
|  | msg += ` - ${entry.name} (${entry.location})\n`; | 
|  | } | 
|  | msg += '\n'; | 
|  |  | 
|  | // Append the trace URL. | 
|  | if (this.attachTrace && this.traceUrl) { | 
|  | msg += `Trace: ${this.traceUrl}\n`; | 
|  | } else if (this.attachTrace && this.traceState === 'UPLOADING') { | 
|  | msg += `Trace: uploading...\n`; | 
|  | } else { | 
|  | msg += `Trace: not available (${this.traceType}). Provide repro steps.\n`; | 
|  | } | 
|  | msg += `UA: ${navigator.userAgent}\n`; | 
|  | msg += `Referrer: ${document.referrer}\n`; | 
|  | this.errorMessage = msg; | 
|  |  | 
|  | let shareTraceSection: m.Vnode|null = null; | 
|  | if (this.traceState !== 'NOT_AVAILABLE') { | 
|  | shareTraceSection = m( | 
|  | 'div', | 
|  | m( | 
|  | 'label', | 
|  | m(`input[type=checkbox]`, { | 
|  | checked: this.attachTrace, | 
|  | oninput: (ev: InputEvent) => { | 
|  | const checked = (ev.target as HTMLInputElement).checked; | 
|  | this.onUploadCheckboxChange(checked); | 
|  | }, | 
|  | }), | 
|  | this.traceState === 'UPLOADING' ? | 
|  | `Uploading trace... ${this.uploadStatus}` : | 
|  | 'Tick to share the current trace and help debugging', | 
|  | ),  // m('label') | 
|  | m('div.modal-small', | 
|  | `This will upload the trace and attach a link to the bug. | 
|  | You may leave it unchecked and attach the trace manually to the bug | 
|  | if preferred.`), | 
|  | ); | 
|  | }  // if (this.traceState !== 'NOT_AVAILABLE') | 
|  |  | 
|  | return [ | 
|  | m( | 
|  | 'div', | 
|  | m('.modal-logs', msg), | 
|  | m('span', `Please provide any additional details describing | 
|  | how the crash occurred:`), | 
|  | m('textarea.modal-textarea', { | 
|  | rows: 3, | 
|  | maxlength: 1000, | 
|  | oninput: (ev: InputEvent) => { | 
|  | this.userDescription = (ev.target as HTMLTextAreaElement).value; | 
|  | }, | 
|  | onkeydown: (e: Event) => e.stopPropagation(), | 
|  | onkeyup: (e: Event) => e.stopPropagation(), | 
|  | }), | 
|  | shareTraceSection, | 
|  | ), | 
|  | m('footer', | 
|  | m('button.modal-btn.modal-btn-primary', | 
|  | {onclick: () => this.fileBug(err)}, | 
|  | 'File a bug (Googlers only)')), | 
|  | ]; | 
|  | } | 
|  |  | 
|  | private onUploadCheckboxChange(checked: boolean) { | 
|  | raf.scheduleFullRedraw(); | 
|  | this.attachTrace = checked; | 
|  |  | 
|  | if (checked && this.traceData !== undefined && | 
|  | this.traceState === 'NOT_UPLOADED') { | 
|  | this.traceState = 'UPLOADING'; | 
|  | this.uploadStatus = ''; | 
|  | const uploader = new TraceGcsUploader(this.traceData, () => { | 
|  | raf.scheduleFullRedraw(); | 
|  | this.uploadStatus = uploader.getEtaString(); | 
|  | if (uploader.state === 'UPLOADED') { | 
|  | this.traceState = 'UPLOADED'; | 
|  | this.traceUrl = uploader.uploadedUrl; | 
|  | } else if (uploader.state === 'ERROR') { | 
|  | this.traceState = 'NOT_UPLOADED'; | 
|  | this.uploadStatus = uploader.error; | 
|  | } | 
|  | }); | 
|  | this.uploader = uploader; | 
|  | } else if (!checked && this.uploader) { | 
|  | this.uploader.abort(); | 
|  | } | 
|  | } | 
|  |  | 
|  | private fileBug(err: ErrorDetails) { | 
|  | const errTitle = err.message.split('\n', 1)[0].substring(0, 80); | 
|  | let url = 'https://goto.google.com/perfetto-ui-bug'; | 
|  | url += '?title=' + encodeURIComponent(`UI Error: ${errTitle}`); | 
|  | url += '&description='; | 
|  | if (this.userDescription !== '') { | 
|  | url += encodeURIComponent( | 
|  | 'User description:\n' + this.userDescription + '\n\n'); | 
|  | } | 
|  | url += encodeURIComponent(this.errorMessage); | 
|  | // 8kb is common limit on request size so restrict links to that long: | 
|  | url = url.substring(0, 8000); | 
|  | window.open(url, '_blank'); | 
|  | } | 
|  | } | 
|  |  | 
|  | function showOutOfMemoryDialog() { | 
|  | const url = | 
|  | 'https://perfetto.dev/docs/quickstart/trace-analysis#get-trace-processor'; | 
|  |  | 
|  | const tpCmd = 'curl -LO https://get.perfetto.dev/trace_processor\n' + | 
|  | 'chmod +x ./trace_processor\n' + | 
|  | 'trace_processor --httpd /path/to/trace.pftrace\n' + | 
|  | '# Reload the UI, it will prompt to use the HTTP+RPC interface'; | 
|  | showModal({ | 
|  | title: 'Oops! Your WASM trace processor ran out of memory', | 
|  | content: m( | 
|  | 'div', | 
|  | m('span', | 
|  | 'The in-memory representation of the trace is too big ' + | 
|  | 'for the browser memory limits (typically 2GB per tab).'), | 
|  | m('br'), | 
|  | m('span', | 
|  | 'You can work around this problem by using the trace_processor ' + | 
|  | 'native binary as an accelerator for the UI as follows:'), | 
|  | m('br'), | 
|  | m('br'), | 
|  | m('.modal-bash', tpCmd), | 
|  | m('br'), | 
|  | m('span', 'For details see '), | 
|  | m('a', {href: url, target: '_blank'}, url), | 
|  | ), | 
|  | }); | 
|  | } | 
|  |  | 
|  | function showUnknownFileError() { | 
|  | showModal({ | 
|  | title: 'Cannot open this file', | 
|  | content: m( | 
|  | 'div', | 
|  | m('p', | 
|  | 'The file opened doesn\'t look like a Perfetto trace or any ' + | 
|  | 'other format recognized by the Perfetto TraceProcessor.'), | 
|  | m('p', 'Formats supported:'), | 
|  | m( | 
|  | 'ul', | 
|  | m('li', 'Perfetto protobuf trace'), | 
|  | m('li', 'chrome://tracing JSON'), | 
|  | m('li', 'Android systrace'), | 
|  | m('li', 'Fuchsia trace'), | 
|  | m('li', 'Ninja build log'), | 
|  | ), | 
|  | ), | 
|  | }); | 
|  | } | 
|  |  | 
|  | function showWebUSBError() { | 
|  | showModal({ | 
|  | title: 'A WebUSB error occurred', | 
|  | content: m( | 
|  | 'div', | 
|  | m('span', `Is adb already running on the host? Run this command and | 
|  | try again.`), | 
|  | m('br'), | 
|  | m('.modal-bash', '> adb kill-server'), | 
|  | m('br'), | 
|  | m('span', 'For details see '), | 
|  | m('a', {href: 'http://b/159048331', target: '_blank'}, 'b/159048331'), | 
|  | ), | 
|  | }); | 
|  | } | 
|  |  | 
|  | export function showWebUSBErrorV2() { | 
|  | showModal({ | 
|  | title: 'A WebUSB error occurred', | 
|  | content: m( | 
|  | 'div', | 
|  | m('span', `Is adb already running on the host? Run this command and | 
|  | try again.`), | 
|  | m('br'), | 
|  | m('.modal-bash', '> adb kill-server'), | 
|  | m('br'), | 
|  | // The statement below covers the following edge case: | 
|  | // 1. 'adb server' is running on the device. | 
|  | // 2. The user selects the new Android target, so we try to fetch the | 
|  | // OS version and do QSS. | 
|  | // 3. The error modal is shown. | 
|  | // 4. The user runs 'adb kill-server'. | 
|  | // At this point we don't have a trigger to try fetching the OS version | 
|  | // + QSS again. Therefore, the user will need to refresh the page. | 
|  | m('span', | 
|  | 'If after running \'adb kill-server\', you don\'t see ' + | 
|  | 'a \'Start Recording\' button on the page and you don\'t see ' + | 
|  | '\'Allow USB debugging\' on the device, ' + | 
|  | 'you will need to reload this page.'), | 
|  | m('br'), | 
|  | m('br'), | 
|  | m('span', 'For details see '), | 
|  | m('a', {href: 'http://b/159048331', target: '_blank'}, 'b/159048331'), | 
|  | ), | 
|  | }); | 
|  | } | 
|  |  | 
|  | export function showConnectionLostError(): void { | 
|  | showModal({ | 
|  | title: 'Connection with the ADB device lost', | 
|  | content: m( | 
|  | 'div', | 
|  | m('span', `Please connect the device again to restart the recording.`), | 
|  | m('br')), | 
|  | }); | 
|  | } | 
|  |  | 
|  | export function showAllowUSBDebugging(): void { | 
|  | showModal({ | 
|  | title: 'Could not connect to the device', | 
|  | content: m( | 
|  | 'div', m('span', 'Please allow USB debugging on the device.'), m('br')), | 
|  | }); | 
|  | } | 
|  |  | 
|  | export function showNoDeviceSelected(): void { | 
|  | showModal({ | 
|  | title: 'No device was selected for recording', | 
|  | content: | 
|  | m('div', | 
|  | m('span', `If you want to connect to an ADB device, | 
|  | please select it from the list.`), | 
|  | m('br')), | 
|  | }); | 
|  | } | 
|  |  | 
|  | export function showExtensionNotInstalled(): void { | 
|  | showModal({ | 
|  | title: 'Perfetto Chrome extension not installed', | 
|  | content: | 
|  | m('div', | 
|  | m('.note', | 
|  | `To trace Chrome from the Perfetto UI, you need to install our `, | 
|  | m('a', {href: EXTENSION_URL, target: '_blank'}, 'Chrome extension'), | 
|  | ' and then reload this page.'), | 
|  | m('br')), | 
|  | }); | 
|  | } | 
|  |  | 
|  | export function showWebsocketConnectionIssue(message: string): void { | 
|  | showModal({ | 
|  | title: 'Unable to connect to the device via websocket', | 
|  | content: m('div', m('span', message), m('br')), | 
|  | }); | 
|  | } | 
|  |  | 
|  | export function showIssueParsingTheTracedResponse(message: string): void { | 
|  | showModal({ | 
|  | title: 'A problem was encountered while connecting to' + | 
|  | ' the Perfetto tracing service', | 
|  | content: m('div', m('span', message), m('br')), | 
|  | }); | 
|  | } | 
|  |  | 
|  | export function showFailedToPushBinary(message: string): void { | 
|  | showModal({ | 
|  | title: 'Failed to push a binary to the device', | 
|  | content: | 
|  | m('div', | 
|  | m('span', | 
|  | 'This can happen if your Android device has an OS version lower ' + | 
|  | 'than Q. Perfetto tried to push the latest version of its ' + | 
|  | 'embedded binary but failed.'), | 
|  | m('br'), | 
|  | m('br'), | 
|  | m('span', 'Error message:'), | 
|  | m('br'), | 
|  | m('span', message)), | 
|  | }); | 
|  | } | 
|  |  | 
|  | function showRpcSequencingError() { | 
|  | showModal({ | 
|  | title: 'A TraceProcessor RPC error occurred', | 
|  | content: m( | 
|  | 'div', | 
|  | m('p', 'The trace processor RPC sequence ID was broken'), | 
|  | m('p', `This can happen when using a HTTP trace processor instance and | 
|  | either accidentally sharing this between multiple tabs or | 
|  | restarting the trace processor while still in use by UI.`), | 
|  | m('p', `Please refresh this tab and ensure that trace processor is used | 
|  | at most one tab at a time.`), | 
|  | ), | 
|  | }); | 
|  | } |