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