summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance-new/shared/symbolication-worker.js
blob: 4f2ee02f29cc35eadb532b2c1c9d66e37ee3d7c5 (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
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* 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/. */

/* eslint-env mozilla/chrome-worker */

// FIXME: This file is currently not covered by TypeScript, there is no "@ts-check" comment.
// We should fix this once we know how to deal with the module imports below.
// (Maybe once Firefox supports worker module? Bug 1247687)

"use strict";

/* import-globals-from profiler_get_symbols.js */
importScripts(
  "resource://devtools/client/performance-new/shared/profiler_get_symbols.js"
);

/**
 * @typedef {import("../@types/perf").SymbolicationWorkerInitialMessage} SymbolicationWorkerInitialMessage
 * @typedef {import("../@types/perf").FileHandle} FileHandle
 */

// This worker uses the wasm module that was generated from https://github.com/mstange/profiler-get-symbols.
// See ProfilerGetSymbols.jsm for more information.
//
// The worker instantiates the module, reads the binary into wasm memory, runs
// the wasm code, and returns the symbol table or an error. Then it shuts down
// itself.

/* eslint camelcase: 0*/
const { getCompactSymbolTable, queryAPI } = wasm_bindgen;

// Returns a plain object that is Structured Cloneable and has name and
// message properties.
function createPlainErrorObject(e) {
  // Regular errors: just rewrap the object.
  const { name, message, fileName, lineNumber } = e;
  return { name, message, fileName, lineNumber };
}

/**
 * A FileAndPathHelper object is passed to getCompactSymbolTable, which calls
 * the methods `getCandidatePathsForBinaryOrPdb` and `readFile` on it.
 */
class FileAndPathHelper {
  constructor(libInfoMap, objdirs) {
    this._libInfoMap = libInfoMap;
    this._objdirs = objdirs;
  }

  /**
   * Enumerate all paths at which we could find files with symbol information.
   * This method is called by wasm code (via the bindings).
   *
   * @param {LibraryInfo} libraryInfo
   * @returns {Array<string>}
   */
  getCandidatePathsForDebugFile(libraryInfo) {
    const { debugName, breakpadId } = libraryInfo;
    const key = `${debugName}:${breakpadId}`;
    const lib = this._libInfoMap.get(key);
    if (!lib) {
      throw new Error(
        `Could not find the library for "${debugName}", "${breakpadId}".`
      );
    }

    const { name, path, debugPath, arch } = lib;
    const candidatePaths = [];

    // First, try to find a binary with a matching file name and breakpadId
    // in one of the manually specified objdirs.
    // This is needed if the debuggee is a build running on a remote machine that
    // was compiled by the developer on *this* machine (the "host machine"). In
    // that case, the objdir will contain the compiled binary with full symbol and
    // debug information, whereas the binary on the device may not exist in
    // uncompressed form or may have been stripped of debug information and some
    // symbol information.
    // An objdir, or "object directory", is a directory on the host machine that's
    // used to store build artifacts ("object files") from the compilation process.
    // This only works if the binary is one of the Gecko binaries and not
    // a system library.
    for (const objdirPath of this._objdirs) {
      try {
        // Binaries are usually expected to exist at objdir/dist/bin/filename.
        candidatePaths.push(PathUtils.join(objdirPath, "dist", "bin", name));

        // Also search in the "objdir" directory itself (not just in dist/bin).
        // If, for some unforeseen reason, the relevant binary is not inside the
        // objdirs dist/bin/ directory, this provides a way out because it lets the
        // user specify the actual location.
        candidatePaths.push(PathUtils.join(objdirPath, name));
      } catch (e) {
        // PathUtils.join throws if objdirPath is not an absolute path.
        // Ignore those invalid objdir paths.
      }
    }

    // Check the absolute paths of the library last.
    // We do this after the objdir search because the library's path may point
    // to a stripped binary, which will have fewer symbols than the original
    // binaries in the objdir.
    if (debugPath !== path) {
      // We're on Windows, and debugPath points to a PDB file.
      // On non-Windows, path and debugPath are always the same.

      // Check the PDB file before the binary because the PDB has the symbol
      // information. The binary is only used as a last-ditch fallback
      // for things like Windows system libraries (e.g. graphics drivers).
      candidatePaths.push(debugPath);
    }

    // The location of the binary. If the profile was obtained on this machine
    // (and not, for example, on an Android device), this file should always
    // exist.
    candidatePaths.push(path);

    // On macOS, for system libraries, add a final fallback for the dyld shared
    // cache. Starting with macOS 11, most system libraries are located in this
    // system-wide cache file and not present as individual files.
    if (arch && (path.startsWith("/usr/") || path.startsWith("/System/"))) {
      // Use the special syntax `dyldcache:<dyldcachepath>:<librarypath>`.

      // Dyld cache location used on macOS 13+:
      candidatePaths.push(
        `dyldcache:/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_${arch}:${path}`
      );
      // Dyld cache location used on macOS 11 and 12:
      candidatePaths.push(
        `dyldcache:/System/Library/dyld/dyld_shared_cache_${arch}:${path}`
      );
    }

    return candidatePaths;
  }

  /**
   * Enumerate all paths at which we could find the binary which matches the
   * given libraryInfo, in order to disassemble machine code.
   * This method is called by wasm code (via the bindings).
   *
   * @param {LibraryInfo} libraryInfo
   * @returns {Array<string>}
   */
  getCandidatePathsForBinary(libraryInfo) {
    const { debugName, breakpadId } = libraryInfo;
    const key = `${debugName}:${breakpadId}`;
    const lib = this._libInfoMap.get(key);
    if (!lib) {
      throw new Error(
        `Could not find the library for "${debugName}", "${breakpadId}".`
      );
    }

    const { name, path, arch } = lib;
    const candidatePaths = [];

    // The location of the binary. If the profile was obtained on this machine
    // (and not, for example, on an Android device), this file should always
    // exist.
    candidatePaths.push(path);

    // Fall back to searching in the manually specified objdirs.
    // This is needed if the debuggee is a build running on a remote machine that
    // was compiled by the developer on *this* machine (the "host machine"). In
    // that case, the objdir will contain the compiled binary.
    for (const objdirPath of this._objdirs) {
      try {
        // Binaries are usually expected to exist at objdir/dist/bin/filename.
        candidatePaths.push(PathUtils.join(objdirPath, "dist", "bin", name));

        // Also search in the "objdir" directory itself (not just in dist/bin).
        // If, for some unforeseen reason, the relevant binary is not inside the
        // objdirs dist/bin/ directory, this provides a way out because it lets the
        // user specify the actual location.
        candidatePaths.push(PathUtils.join(objdirPath, name));
      } catch (e) {
        // PathUtils.join throws if objdirPath is not an absolute path.
        // Ignore those invalid objdir paths.
      }
    }

    // On macOS, for system libraries, add a final fallback for the dyld shared
    // cache. Starting with macOS 11, most system libraries are located in this
    // system-wide cache file and not present as individual files.
    if (arch && (path.startsWith("/usr/") || path.startsWith("/System/"))) {
      // Use the special syntax `dyldcache:<dyldcachepath>:<librarypath>`.

      // Dyld cache location used on macOS 13+:
      candidatePaths.push(
        `dyldcache:/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_${arch}:${path}`
      );
      // Dyld cache location used on macOS 11 and 12:
      candidatePaths.push(
        `dyldcache:/System/Library/dyld/dyld_shared_cache_${arch}:${path}`
      );
    }

    return candidatePaths;
  }

  /**
   * Asynchronously prepare the file at `path` for synchronous reading.
   * This method is called by wasm code (via the bindings).
   *
   * @param {string} path
   * @returns {FileHandle}
   */
  async readFile(path) {
    const info = await IOUtils.stat(path);
    if (info.type === "directory") {
      throw new Error(`Path "${path}" is a directory.`);
    }

    return IOUtils.openFileForSyncReading(path);
  }
}

/** @param {MessageEvent<SymbolicationWorkerInitialMessage>} e */
onmessage = async e => {
  try {
    const { request, libInfoMap, objdirs, module } = e.data;

    if (!(module instanceof WebAssembly.Module)) {
      throw new Error("invalid WebAssembly module");
    }

    // Instantiate the WASM module.
    await wasm_bindgen(module);

    const helper = new FileAndPathHelper(libInfoMap, objdirs);

    switch (request.type) {
      case "GET_SYMBOL_TABLE": {
        const { debugName, breakpadId } = request;
        const result = await getCompactSymbolTable(
          debugName,
          breakpadId,
          helper
        );
        postMessage(
          { result },
          result.map(r => r.buffer)
        );
        break;
      }
      case "QUERY_SYMBOLICATION_API": {
        const { path, requestJson } = request;
        const result = await queryAPI(path, requestJson, helper);
        postMessage({ result });
        break;
      }
      default:
        throw new Error(`Unexpected request type ${request.type}`);
    }
  } catch (error) {
    postMessage({ error: createPlainErrorObject(error) });
  }
  close();
};

onunhandledrejection = e => {
  // Unhandled rejections can happen if the WASM code throws a
  // "RuntimeError: unreachable executed" exception, which can happen
  // if the Rust code panics or runs out of memory.
  // These panics currently are not propagated to the promise reject
  // callback, see https://github.com/rustwasm/wasm-bindgen/issues/2724 .
  // Ideally, the Rust code should never panic and handle all error
  // cases gracefully.
  e.preventDefault();
  postMessage({ error: createPlainErrorObject(e.reason) });
};

// Catch any other unhandled errors, just to be sure.
onerror = e => {
  postMessage({ error: createPlainErrorObject(e) });
};