1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2018 The Android Open Source Project 2*6dbdd20aSAndroid Build Coastguard Worker// 3*6dbdd20aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License"); 4*6dbdd20aSAndroid Build Coastguard Worker// you may not use this file except in compliance with the License. 5*6dbdd20aSAndroid Build Coastguard Worker// You may obtain a copy of the License at 6*6dbdd20aSAndroid Build Coastguard Worker// 7*6dbdd20aSAndroid Build Coastguard Worker// http://www.apache.org/licenses/LICENSE-2.0 8*6dbdd20aSAndroid Build Coastguard Worker// 9*6dbdd20aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software 10*6dbdd20aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS, 11*6dbdd20aSAndroid Build Coastguard Worker// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*6dbdd20aSAndroid Build Coastguard Worker// See the License for the specific language governing permissions and 13*6dbdd20aSAndroid Build Coastguard Worker// limitations under the License. 14*6dbdd20aSAndroid Build Coastguard Worker 15*6dbdd20aSAndroid Build Coastguard Workerimport {VERSION} from '../gen/perfetto_version'; 16*6dbdd20aSAndroid Build Coastguard Workerimport {exists} from './utils'; 17*6dbdd20aSAndroid Build Coastguard Worker 18*6dbdd20aSAndroid Build Coastguard Workerexport type ErrorType = 'ERROR' | 'PROMISE_REJ' | 'OTHER'; 19*6dbdd20aSAndroid Build Coastguard Workerexport interface ErrorStackEntry { 20*6dbdd20aSAndroid Build Coastguard Worker name: string; // e.g. renderCanvas 21*6dbdd20aSAndroid Build Coastguard Worker location: string; // e.g. frontend_bundle.js:12:3 22*6dbdd20aSAndroid Build Coastguard Worker} 23*6dbdd20aSAndroid Build Coastguard Workerexport interface ErrorDetails { 24*6dbdd20aSAndroid Build Coastguard Worker errType: ErrorType; 25*6dbdd20aSAndroid Build Coastguard Worker message: string; // Uncaught StoreError: No such subtree: tracks,1374,state 26*6dbdd20aSAndroid Build Coastguard Worker stack: ErrorStackEntry[]; 27*6dbdd20aSAndroid Build Coastguard Worker} 28*6dbdd20aSAndroid Build Coastguard Worker 29*6dbdd20aSAndroid Build Coastguard Workerexport type ErrorHandler = (err: ErrorDetails) => void; 30*6dbdd20aSAndroid Build Coastguard Workerconst errorHandlers: ErrorHandler[] = []; 31*6dbdd20aSAndroid Build Coastguard Worker 32*6dbdd20aSAndroid Build Coastguard Workerexport function assertExists<A>(value: A | null | undefined): A { 33*6dbdd20aSAndroid Build Coastguard Worker if (value === null || value === undefined) { 34*6dbdd20aSAndroid Build Coastguard Worker throw new Error("Value doesn't exist"); 35*6dbdd20aSAndroid Build Coastguard Worker } 36*6dbdd20aSAndroid Build Coastguard Worker return value; 37*6dbdd20aSAndroid Build Coastguard Worker} 38*6dbdd20aSAndroid Build Coastguard Worker 39*6dbdd20aSAndroid Build Coastguard Workerexport function assertIsInstance<T>(value: unknown, clazz: Function): T { 40*6dbdd20aSAndroid Build Coastguard Worker assertTrue(value instanceof clazz); 41*6dbdd20aSAndroid Build Coastguard Worker return value as T; 42*6dbdd20aSAndroid Build Coastguard Worker} 43*6dbdd20aSAndroid Build Coastguard Worker 44*6dbdd20aSAndroid Build Coastguard Workerexport function assertTrue(value: boolean, optMsg?: string) { 45*6dbdd20aSAndroid Build Coastguard Worker if (!value) { 46*6dbdd20aSAndroid Build Coastguard Worker throw new Error(optMsg ?? 'Failed assertion'); 47*6dbdd20aSAndroid Build Coastguard Worker } 48*6dbdd20aSAndroid Build Coastguard Worker} 49*6dbdd20aSAndroid Build Coastguard Worker 50*6dbdd20aSAndroid Build Coastguard Workerexport function assertFalse(value: boolean, optMsg?: string) { 51*6dbdd20aSAndroid Build Coastguard Worker assertTrue(!value, optMsg); 52*6dbdd20aSAndroid Build Coastguard Worker} 53*6dbdd20aSAndroid Build Coastguard Worker 54*6dbdd20aSAndroid Build Coastguard Workerexport function addErrorHandler(handler: ErrorHandler) { 55*6dbdd20aSAndroid Build Coastguard Worker if (!errorHandlers.includes(handler)) { 56*6dbdd20aSAndroid Build Coastguard Worker errorHandlers.push(handler); 57*6dbdd20aSAndroid Build Coastguard Worker } 58*6dbdd20aSAndroid Build Coastguard Worker} 59*6dbdd20aSAndroid Build Coastguard Worker 60*6dbdd20aSAndroid Build Coastguard Workerexport function reportError(err: ErrorEvent | PromiseRejectionEvent | {}) { 61*6dbdd20aSAndroid Build Coastguard Worker let errorObj = undefined; 62*6dbdd20aSAndroid Build Coastguard Worker let errMsg = ''; 63*6dbdd20aSAndroid Build Coastguard Worker let errType: ErrorType; 64*6dbdd20aSAndroid Build Coastguard Worker const stack: ErrorStackEntry[] = []; 65*6dbdd20aSAndroid Build Coastguard Worker const baseUrl = `${location.protocol}//${location.host}`; 66*6dbdd20aSAndroid Build Coastguard Worker 67*6dbdd20aSAndroid Build Coastguard Worker if (err instanceof ErrorEvent) { 68*6dbdd20aSAndroid Build Coastguard Worker errType = 'ERROR'; 69*6dbdd20aSAndroid Build Coastguard Worker // In nominal cases the error is set in err.error{message,stack} and 70*6dbdd20aSAndroid Build Coastguard Worker // a toString() of the error object returns a meaningful one-line 71*6dbdd20aSAndroid Build Coastguard Worker // description. However, in the case of wasm errors, emscripten seems to 72*6dbdd20aSAndroid Build Coastguard Worker // wrap the error in an unusual way: err.error is null but err.message 73*6dbdd20aSAndroid Build Coastguard Worker // contains the whole one-line + stack trace. 74*6dbdd20aSAndroid Build Coastguard Worker if (err.error === null || err.error === undefined) { 75*6dbdd20aSAndroid Build Coastguard Worker // Wasm case. 76*6dbdd20aSAndroid Build Coastguard Worker const errLines = `${err.message}`.split('\n'); 77*6dbdd20aSAndroid Build Coastguard Worker errMsg = errLines[0]; 78*6dbdd20aSAndroid Build Coastguard Worker errorObj = {stack: errLines.slice(1).join('\n')}; 79*6dbdd20aSAndroid Build Coastguard Worker } else { 80*6dbdd20aSAndroid Build Coastguard Worker // Standard JS case. 81*6dbdd20aSAndroid Build Coastguard Worker errMsg = `${err.error}`; 82*6dbdd20aSAndroid Build Coastguard Worker errorObj = err.error; 83*6dbdd20aSAndroid Build Coastguard Worker } 84*6dbdd20aSAndroid Build Coastguard Worker } else if (err instanceof PromiseRejectionEvent) { 85*6dbdd20aSAndroid Build Coastguard Worker errType = 'PROMISE_REJ'; 86*6dbdd20aSAndroid Build Coastguard Worker errMsg = `${err.reason}`; 87*6dbdd20aSAndroid Build Coastguard Worker errorObj = err.reason; 88*6dbdd20aSAndroid Build Coastguard Worker } else { 89*6dbdd20aSAndroid Build Coastguard Worker errType = 'OTHER'; 90*6dbdd20aSAndroid Build Coastguard Worker errMsg = `${err}`; 91*6dbdd20aSAndroid Build Coastguard Worker } 92*6dbdd20aSAndroid Build Coastguard Worker 93*6dbdd20aSAndroid Build Coastguard Worker // Remove useless "Uncaught Error:" or "Error:" prefixes which just create 94*6dbdd20aSAndroid Build Coastguard Worker // noise in the bug tracker without adding any meaningful value. 95*6dbdd20aSAndroid Build Coastguard Worker errMsg = errMsg.replace(/^Uncaught Error:/, ''); 96*6dbdd20aSAndroid Build Coastguard Worker errMsg = errMsg.replace(/^Error:/, ''); 97*6dbdd20aSAndroid Build Coastguard Worker errMsg = errMsg.trim(); 98*6dbdd20aSAndroid Build Coastguard Worker 99*6dbdd20aSAndroid Build Coastguard Worker if (errorObj !== undefined && errorObj !== null) { 100*6dbdd20aSAndroid Build Coastguard Worker const maybeStack = (errorObj as {stack?: string}).stack; 101*6dbdd20aSAndroid Build Coastguard Worker let errStack = maybeStack !== undefined ? `${maybeStack}` : ''; 102*6dbdd20aSAndroid Build Coastguard Worker errStack = errStack.replaceAll(/\r/g, ''); // Strip Windows CR. 103*6dbdd20aSAndroid Build Coastguard Worker for (let line of errStack.split('\n')) { 104*6dbdd20aSAndroid Build Coastguard Worker if (errMsg.includes(line)) continue; 105*6dbdd20aSAndroid Build Coastguard Worker // Chrome, Firefox and safari don't agree on the stack format: 106*6dbdd20aSAndroid Build Coastguard Worker // Chrome: prefixes entries with a ' at ' and uses the format 107*6dbdd20aSAndroid Build Coastguard Worker // function(https://url:line:col), e.g. 108*6dbdd20aSAndroid Build Coastguard Worker // ' at FooBar (https://.../frontend_bundle.js:2073:15)' 109*6dbdd20aSAndroid Build Coastguard Worker // however, if the function name is not known, it prints just: 110*6dbdd20aSAndroid Build Coastguard Worker // ' at https://.../frontend_bundle.js:2073:15' 111*6dbdd20aSAndroid Build Coastguard Worker // or also: 112*6dbdd20aSAndroid Build Coastguard Worker // ' at <anonymous>:5:11' 113*6dbdd20aSAndroid Build Coastguard Worker // Firefox and Safari: don't have any prefix and use @ as a separator: 114*6dbdd20aSAndroid Build Coastguard Worker // redrawCanvas@https://.../frontend_bundle.js:468814:26 115*6dbdd20aSAndroid Build Coastguard Worker // @debugger eval code:1:32 116*6dbdd20aSAndroid Build Coastguard Worker 117*6dbdd20aSAndroid Build Coastguard Worker // Here we first normalize Chrome into the Firefox/Safari format by 118*6dbdd20aSAndroid Build Coastguard Worker // removing the ' at ' prefix and replacing (xxx)$ into @xxx. 119*6dbdd20aSAndroid Build Coastguard Worker line = line.replace(/^\s*at\s*/, ''); 120*6dbdd20aSAndroid Build Coastguard Worker line = line.replace(/\s*\(([^)]+)\)$/, '@$1'); 121*6dbdd20aSAndroid Build Coastguard Worker 122*6dbdd20aSAndroid Build Coastguard Worker // This leaves us still with two possible options here: 123*6dbdd20aSAndroid Build Coastguard Worker // 1. FooBar@https://ui.perfetto.dev/v123/frontend_bundle.js:2073:15 124*6dbdd20aSAndroid Build Coastguard Worker // 2. https://ui.perfetto.dev/v123/frontend_bundle.js:2073:15 125*6dbdd20aSAndroid Build Coastguard Worker const lastAt = line.lastIndexOf('@'); 126*6dbdd20aSAndroid Build Coastguard Worker let entryName = ''; 127*6dbdd20aSAndroid Build Coastguard Worker let entryLocation = ''; 128*6dbdd20aSAndroid Build Coastguard Worker if (lastAt >= 0) { 129*6dbdd20aSAndroid Build Coastguard Worker entryLocation = line.substring(lastAt + 1); 130*6dbdd20aSAndroid Build Coastguard Worker entryName = line.substring(0, lastAt); 131*6dbdd20aSAndroid Build Coastguard Worker } else { 132*6dbdd20aSAndroid Build Coastguard Worker entryLocation = line; 133*6dbdd20aSAndroid Build Coastguard Worker } 134*6dbdd20aSAndroid Build Coastguard Worker 135*6dbdd20aSAndroid Build Coastguard Worker // Remove redundant https://ui.perfetto.dev/v38.0-d6ed090ee/ as we have 136*6dbdd20aSAndroid Build Coastguard Worker // that information already and don't need to repeat it on each line. 137*6dbdd20aSAndroid Build Coastguard Worker if (entryLocation.includes(baseUrl)) { 138*6dbdd20aSAndroid Build Coastguard Worker entryLocation = entryLocation.replace(baseUrl, ''); 139*6dbdd20aSAndroid Build Coastguard Worker entryLocation = entryLocation.replace(`/${VERSION}/`, ''); 140*6dbdd20aSAndroid Build Coastguard Worker } 141*6dbdd20aSAndroid Build Coastguard Worker stack.push({name: entryName, location: entryLocation}); 142*6dbdd20aSAndroid Build Coastguard Worker } // for (line in stack) 143*6dbdd20aSAndroid Build Coastguard Worker 144*6dbdd20aSAndroid Build Coastguard Worker // Beautify the Wasm error message if possible. Most Wasm errors are of the 145*6dbdd20aSAndroid Build Coastguard Worker // form RuntimeError: unreachable or RuntimeError: abort. Those lead to bug 146*6dbdd20aSAndroid Build Coastguard Worker // titles that are undistinguishable from each other. Instead try using the 147*6dbdd20aSAndroid Build Coastguard Worker // first entry of the stack that contains a perfetto:: function name. 148*6dbdd20aSAndroid Build Coastguard Worker const wasmFunc = stack.find((e) => e.name.includes('perfetto::'))?.name; 149*6dbdd20aSAndroid Build Coastguard Worker if (errMsg.includes('RuntimeError') && exists(wasmFunc)) { 150*6dbdd20aSAndroid Build Coastguard Worker errMsg += ` @ ${wasmFunc.trim()}`; 151*6dbdd20aSAndroid Build Coastguard Worker } 152*6dbdd20aSAndroid Build Coastguard Worker } 153*6dbdd20aSAndroid Build Coastguard Worker // Invoke all the handlers registered through addErrorHandler. 154*6dbdd20aSAndroid Build Coastguard Worker // There are usually two handlers registered, one for the UI (error_dialog.ts) 155*6dbdd20aSAndroid Build Coastguard Worker // and one for Analytics (analytics.ts). 156*6dbdd20aSAndroid Build Coastguard Worker for (const handler of errorHandlers) { 157*6dbdd20aSAndroid Build Coastguard Worker handler({ 158*6dbdd20aSAndroid Build Coastguard Worker errType, 159*6dbdd20aSAndroid Build Coastguard Worker message: errMsg, 160*6dbdd20aSAndroid Build Coastguard Worker stack, 161*6dbdd20aSAndroid Build Coastguard Worker } as ErrorDetails); 162*6dbdd20aSAndroid Build Coastguard Worker } 163*6dbdd20aSAndroid Build Coastguard Worker} 164*6dbdd20aSAndroid Build Coastguard Worker 165*6dbdd20aSAndroid Build Coastguard Worker// This function serves two purposes. 166*6dbdd20aSAndroid Build Coastguard Worker// 1) A runtime check - if we are ever called, we throw an exception. 167*6dbdd20aSAndroid Build Coastguard Worker// This is useful for checking that code we suspect should never be reached is 168*6dbdd20aSAndroid Build Coastguard Worker// actually never reached. 169*6dbdd20aSAndroid Build Coastguard Worker// 2) A compile time check where typescript asserts that the value passed can be 170*6dbdd20aSAndroid Build Coastguard Worker// cast to the "never" type. 171*6dbdd20aSAndroid Build Coastguard Worker// This is useful for ensuring we exhastively check union types. 172*6dbdd20aSAndroid Build Coastguard Workerexport function assertUnreachable(value: never): never { 173*6dbdd20aSAndroid Build Coastguard Worker throw new Error(`This code should not be reachable ${value as unknown}`); 174*6dbdd20aSAndroid Build Coastguard Worker} 175