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