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