summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance-new/shared/symbolication.sys.mjs
blob: da73b1210732e2bb25b634e413750e48c76bfb7e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// @ts-check

/** @type {any} */
const lazy = {};

/**
 * @typedef {import("../@types/perf").Library} Library
 * @typedef {import("../@types/perf").PerfFront} PerfFront
 * @typedef {import("../@types/perf").SymbolTableAsTuple} SymbolTableAsTuple
 * @typedef {import("../@types/perf").SymbolicationService} SymbolicationService
 * @typedef {import("../@types/perf").SymbolicationWorkerInitialMessage} SymbolicationWorkerInitialMessage
 */

/**
 * @template R
 * @typedef {import("../@types/perf").SymbolicationWorkerReplyData<R>} SymbolicationWorkerReplyData<R>
 */

ChromeUtils.defineESModuleGetters(lazy, {
  clearTimeout: "resource://gre/modules/Timer.sys.mjs",
  setTimeout: "resource://gre/modules/Timer.sys.mjs",
});

/** @type {any} */
const global = globalThis;

// This module obtains symbol tables for binaries.
// It does so with the help of a WASM module which gets pulled in from the
// internet on demand. We're doing this purely for the purposes of saving on
// code size. The contents of the WASM module are expected to be static, they
// are checked against the hash specified below.
// The WASM code is run on a ChromeWorker thread. It takes the raw byte
// contents of the to-be-dumped binary (and of an additional optional pdb file
// on Windows) as its input, and returns a set of typed arrays which make up
// the symbol table.

// Don't let the strange looking URLs and strings below scare you.
// The hash check ensures that the contents of the wasm module are what we
// expect them to be.
// The source code is at https://github.com/mstange/profiler-get-symbols/ .
// Documentation is at https://docs.rs/samply-api/ .
// The sha384 sum can be computed with the following command (tested on macOS):
// shasum -b -a 384 profiler_get_symbols_wasm_bg.wasm | awk '{ print $1 }' | xxd -r -p | base64

// Generated from https://github.com/mstange/profiler-get-symbols/commit/390b8c4be82c720dd3977ff205fb34bd7d0e00ba
const WASM_MODULE_URL =
  "https://storage.googleapis.com/firefox-profiler-get-symbols/390b8c4be82c720dd3977ff205fb34bd7d0e00ba.wasm";
const WASM_MODULE_INTEGRITY =
  "sha384-P8j6U9jY+M4zSfJKXb1ECjsTPkzQ0hAvgb4zv3gHvlg+THRtVpOrDSywHJBhin00";

const EXPIRY_TIME_IN_MS = 5 * 60 * 1000; // 5 minutes

/** @type {Promise<WebAssembly.Module> | null} */
let gCachedWASMModulePromise = null;
let gCachedWASMModuleExpiryTimer = 0;

// Keep active workers alive (see bug 1592227).
const gActiveWorkers = new Set();

function clearCachedWASMModule() {
  gCachedWASMModulePromise = null;
  gCachedWASMModuleExpiryTimer = 0;
}

function getWASMProfilerGetSymbolsModule() {
  if (!gCachedWASMModulePromise) {
    gCachedWASMModulePromise = (async function () {
      const request = new Request(WASM_MODULE_URL, {
        integrity: WASM_MODULE_INTEGRITY,
        credentials: "omit",
      });
      return WebAssembly.compileStreaming(fetch(request));
    })();
  }

  // Reset expiry timer.
  lazy.clearTimeout(gCachedWASMModuleExpiryTimer);
  gCachedWASMModuleExpiryTimer = lazy.setTimeout(
    clearCachedWASMModule,
    EXPIRY_TIME_IN_MS
  );

  return gCachedWASMModulePromise;
}

/**
 * Handle the entire life cycle of a worker, and report its result.
 * This method creates a new worker, sends the initial message to it, handles
 * any errors, and accepts the result.
 * Returns a promise that resolves with the contents of the (singular) result
 * message or rejects with an error.
 *
 * @template M
 * @template R
 * @param {string} workerURL
 * @param {M} initialMessageToWorker
 * @returns {Promise<R>}
 */
async function getResultFromWorker(workerURL, initialMessageToWorker) {
  return new Promise((resolve, reject) => {
    const worker = new ChromeWorker(workerURL);
    gActiveWorkers.add(worker);

    /** @param {MessageEvent<SymbolicationWorkerReplyData<R>>} msg */
    worker.onmessage = msg => {
      gActiveWorkers.delete(worker);
      if ("error" in msg.data) {
        const error = msg.data.error;
        if (error.name) {
          // Turn the JSON error object into a real Error object.
          const { name, message, fileName, lineNumber } = error;
          const ErrorObjConstructor =
            name in global && Error.isPrototypeOf(global[name])
              ? global[name]
              : Error;
          const e = new ErrorObjConstructor(message, fileName, lineNumber);
          e.name = name;
          reject(e);
        } else {
          reject(error);
        }
        return;
      }
      resolve(msg.data.result);
    };

    // Handle uncaught errors from the worker script. onerror is called if
    // there's a syntax error in the worker script, for example, or when an
    // unhandled exception is thrown, but not for unhandled promise
    // rejections. Without this handler, mistakes during development such as
    // syntax errors can be hard to track down.
    worker.onerror = errorEvent => {
      gActiveWorkers.delete(worker);
      worker.terminate();
      if (ErrorEvent.isInstance(errorEvent)) {
        const { message, filename, lineno } = errorEvent;
        const error = new Error(`${message} at ${filename}:${lineno}`);
        error.name = "WorkerError";
        reject(error);
      } else {
        reject(new Error("Error in worker"));
      }
    };

    // Handle errors from messages that cannot be deserialized. I'm not sure
    // how to get into such a state, but having this handler seems like a good
    // idea.
    worker.onmessageerror = () => {
      gActiveWorkers.delete(worker);
      worker.terminate();
      reject(new Error("Error in worker"));
    };

    worker.postMessage(initialMessageToWorker);
  });
}

/**
 * @param {PerfFront} perfFront
 * @param {string} path
 * @param {string} breakpadId
 * @returns {Promise<SymbolTableAsTuple>}
 */
async function getSymbolTableFromDebuggee(perfFront, path, breakpadId) {
  const [addresses, index, buffer] = await perfFront.getSymbolTable(
    path,
    breakpadId
  );
  // The protocol transmits these arrays as plain JavaScript arrays of
  // numbers, but we want to pass them on as typed arrays. Convert them now.
  return [
    new Uint32Array(addresses),
    new Uint32Array(index),
    new Uint8Array(buffer),
  ];
}

/**
 * Profiling through the DevTools remote debugging protocol supports multiple
 * different modes. This class is specialized to handle various profiling
 * modes such as:
 *
 *   1) Profiling the same browser on the same machine.
 *   2) Profiling a remote browser on the same machine.
 *   3) Profiling a remote browser on a different device.
 *
 * It's also built to handle symbolication requests for both Gecko libraries and
 * system libraries. However, it only handles cases where symbol information
 * can be found in a local file on this machine. There is one case that is not
 * covered by that restriction: Android system libraries. That case requires
 * the help of the perf actor and is implemented in
 * LocalSymbolicationServiceWithRemoteSymbolTableFallback.
 */
class LocalSymbolicationService {
  /**
   * @param {Library[]} sharedLibraries - Information about the shared libraries.
   *   This allows mapping (debugName, breakpadId) pairs to the absolute path of
   *   the binary and/or PDB file, and it ensures that these absolute paths come
   *   from a trusted source and not from the profiler UI.
   * @param {string[]} objdirs - An array of objdir paths
   *   on the host machine that should be searched for relevant build artifacts.
   */
  constructor(sharedLibraries, objdirs) {
    this._libInfoMap = new Map(
      sharedLibraries.map(lib => {
        const { debugName, breakpadId } = lib;
        const key = `${debugName}:${breakpadId}`;
        return [key, lib];
      })
    );
    this._objdirs = objdirs;
  }

  /**
   * @param {string} debugName
   * @param {string} breakpadId
   * @returns {Promise<SymbolTableAsTuple>}
   */
  async getSymbolTable(debugName, breakpadId) {
    const module = await getWASMProfilerGetSymbolsModule();
    /** @type {SymbolicationWorkerInitialMessage} */
    const initialMessage = {
      request: {
        type: "GET_SYMBOL_TABLE",
        debugName,
        breakpadId,
      },
      libInfoMap: this._libInfoMap,
      objdirs: this._objdirs,
      module,
    };
    return getResultFromWorker(
      "resource://devtools/client/performance-new/shared/symbolication.worker.js",
      initialMessage
    );
  }

  /**
   * @param {string} path
   * @param {string} requestJson
   * @returns {Promise<string>}
   */
  async querySymbolicationApi(path, requestJson) {
    const module = await getWASMProfilerGetSymbolsModule();
    /** @type {SymbolicationWorkerInitialMessage} */
    const initialMessage = {
      request: {
        type: "QUERY_SYMBOLICATION_API",
        path,
        requestJson,
      },
      libInfoMap: this._libInfoMap,
      objdirs: this._objdirs,
      module,
    };
    return getResultFromWorker(
      "resource://devtools/client/performance-new/shared/symbolication.worker.js",
      initialMessage
    );
  }
}

/**
 * An implementation of the SymbolicationService interface which also
 * covers the Android system library case.
 * We first try to get symbols from the wrapped SymbolicationService.
 * If that fails, we try to get the symbol table through the perf actor.
 */
class LocalSymbolicationServiceWithRemoteSymbolTableFallback {
  /**
   * @param {SymbolicationService} symbolicationService - The regular symbolication service.
   * @param {Library[]} sharedLibraries - Information about the shared libraries
   * @param {PerfFront} perfFront - A perf actor, to obtain symbol
   *   tables from remote targets
   */
  constructor(symbolicationService, sharedLibraries, perfFront) {
    this._symbolicationService = symbolicationService;
    this._libs = sharedLibraries;
    this._perfFront = perfFront;
  }

  /**
   * @param {string} debugName
   * @param {string} breakpadId
   * @returns {Promise<SymbolTableAsTuple>}
   */
  async getSymbolTable(debugName, breakpadId) {
    try {
      return await this._symbolicationService.getSymbolTable(
        debugName,
        breakpadId
      );
    } catch (errorFromLocalFiles) {
      // Try to obtain the symbol table on the debuggee. We get into this
      // branch in the following cases:
      //  - Android system libraries
      //  - Firefox binaries that have no matching equivalent on the host
      //    machine, for example because the user didn't point us at the
      //    corresponding objdir, or if the build was compiled somewhere
      //    else, or if the build on the device is outdated.
      // For now, the "debuggee" is never a Windows machine, which is why we don't
      // need to pass the library's debugPath. (path and debugPath are always the
      // same on non-Windows.)
      const lib = this._libs.find(
        l => l.debugName === debugName && l.breakpadId === breakpadId
      );
      if (!lib) {
        let errorMessage;
        if (errorFromLocalFiles instanceof Error) {
          errorMessage = errorFromLocalFiles.message;
        } else {
          errorMessage = `${errorFromLocalFiles}`;
        }

        throw new Error(
          `Could not find the library for "${debugName}", "${breakpadId}" after falling ` +
            `back to remote symbol table querying because regular getSymbolTable failed ` +
            `with error: ${errorMessage}.`
        );
      }
      return getSymbolTableFromDebuggee(this._perfFront, lib.path, breakpadId);
    }
  }

  /**
   * @param {string} path
   * @param {string} requestJson
   * @returns {Promise<string>}
   */
  async querySymbolicationApi(path, requestJson) {
    return this._symbolicationService.querySymbolicationApi(path, requestJson);
  }
}

/**
 * Return an object that implements the SymbolicationService interface.
 *
 * @param {Library[]} sharedLibraries - Information about the shared libraries
 * @param {string[]} objdirs - An array of objdir paths
 *   on the host machine that should be searched for relevant build artifacts.
 * @param {PerfFront} [perfFront] - An optional perf actor, to obtain symbol
 *   tables from remote targets
 * @return {SymbolicationService}
 */
export function createLocalSymbolicationService(
  sharedLibraries,
  objdirs,
  perfFront
) {
  const service = new LocalSymbolicationService(sharedLibraries, objdirs);
  if (perfFront) {
    return new LocalSymbolicationServiceWithRemoteSymbolTableFallback(
      service,
      sharedLibraries,
      perfFront
    );
  }
  return service;
}