xref: /aosp_15_r20/external/perfetto/ui/src/base/logging.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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