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