/* 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("perf").Library} Library
 * @typedef {import("perf").PerfFront} PerfFront
 * @typedef {import("perf").SymbolTableAsTuple} SymbolTableAsTuple
 * @typedef {import("perf").SymbolicationService} SymbolicationService
 * @typedef {import("perf").SymbolicationWorkerInitialMessage} SymbolicationWorkerInitialMessage
 */

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

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

/** @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;

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);

    /** @param {MessageEvent<SymbolicationWorkerReplyData<R>>} msg */
    worker.onmessage = msg => {
      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 => {
      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 " + String(errorEvent)));
      }
    };

    // 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 = () => {
      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/shared/performance-new/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/shared/performance-new/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;
}

// This file also exports a named object containing other exports to play well
// with defineESModuleGetters.
export const Symbolication = {
  createLocalSymbolicationService,
};
