static processTrace(trace, options) {
    const {timeOriginDeterminationMethod = 'auto'} = options || {};

    // Parse the trace for our key events and sort them by timestamp. Note: sort
    // *must* be stable to keep events correctly nested.
    const keyEvents = this.filteredTraceSort(trace.traceEvents, e => {
      return e.cat.includes('blink.user_timing') ||
          e.cat.includes('loading') ||
          e.cat.includes('devtools.timeline') ||
          e.cat === '__metadata';
    });

    // Find the inspected frame
    const mainFrameInfo = this.findMainFrameIds(keyEvents);
    const rendererPidToTid = this.findMainFramePidTids(mainFrameInfo, keyEvents);

    // Subset all trace events to just our tab's process (incl threads other than main)
    // stable-sort events to keep them correctly nested.
    const processEvents = TraceProcessor
      .filteredTraceSort(trace.traceEvents, e => rendererPidToTid.has(e.pid));

    // TODO(paulirish): filter down frames (and subsequent actions) to the primary process tree & frame tree

    /** @type {Map<string, {id: string, url: string, parent?: string}>} */
    const framesById = new Map();

    // Begin collection of frame tree information with TracingStartedInBrowser,
    // which should be present even without navigations.
    const tracingStartedFrames = keyEvents
        .find(e => e.name === 'TracingStartedInBrowser')?.args?.data?.frames;
    if (tracingStartedFrames) {
      for (const frame of tracingStartedFrames) {
        framesById.set(frame.frame, {
          id: frame.frame,
          url: frame.url,
          parent: frame.parent,
        });
      }
    }

    // Update known frames if FrameCommittedInBrowser events come in, typically
    // with updated `url`, as well as pid, etc. Some traces (like timespans) may
    // not have any committed frames.
    keyEvents
      .filter(/** @return {evt is FrameCommittedEvent} */ evt => {
        return Boolean(
          evt.name === 'FrameCommittedInBrowser' &&
          evt.args.data?.frame &&
          evt.args.data.url !== undefined
        );
      }).forEach(evt => {
        framesById.set(evt.args.data.frame, {
          id: evt.args.data.frame,
          url: evt.args.data.url,
          parent: evt.args.data.parent,
        });
      });

    const frames = [...framesById.values()];
    const frameIdToRootFrameId = this.resolveRootFrames(frames);

    const inspectedTreeFrameIds = [...frameIdToRootFrameId.entries()]
      .filter(([, rootFrameId]) => rootFrameId === mainFrameInfo.frameId)
      .map(([child]) => child);

    // Filter to just events matching the main frame ID, just to make sure.
    /** @param {LH.TraceEvent} e */
    function associatedToMainFrame(e) {
      const frameId = TraceProcessor.getFrameId(e);
      return frameId === mainFrameInfo.frameId;
    }

    /** @param {LH.TraceEvent} e */
    function associatedToAllFrames(e) {
      const frameId = TraceProcessor.getFrameId(e);
      return frameId ? inspectedTreeFrameIds.includes(frameId) : false;
    }
    const frameEvents = keyEvents.filter(e => associatedToMainFrame(e));

    // Filter to just events matching the main frame ID or any child frame IDs. The subframes
    // are either in-process (same origin) or, potentially, out-of-process. (OOPIFs)
    let frameTreeEvents = [];
    if (frameIdToRootFrameId.has(mainFrameInfo.frameId)) {
      frameTreeEvents = keyEvents.filter(e => associatedToAllFrames(e));
    } else {
      // In practice, there should always be TracingStartedInBrowser/FrameCommittedInBrowser events to
      // define the frame tree. Unfortunately, many test traces do not that frame info due to minification.
      // This ensures there is always a minimal frame tree and events so those tests don't fail.
      log.warn(
        'TraceProcessor',
        'frameTreeEvents may be incomplete, make sure the trace has frame events'
      );
      frameIdToRootFrameId.set(mainFrameInfo.frameId, mainFrameInfo.frameId);
      frameTreeEvents = frameEvents;
    }

    // Compute our time origin to use for all relative timings.
    const timeOriginEvt = this.computeTimeOrigin(
      {keyEvents, frameEvents, mainFrameInfo: mainFrameInfo},
      timeOriginDeterminationMethod
    );

    const mainThreadEvents = processEvents.filter(e => e.tid === rendererPidToTid.get(e.pid));

    // Ensure our traceEnd reflects all page activity.
    const traceEnd = this.computeTraceEnd(trace.traceEvents, timeOriginEvt);

    // This could be much more concise with object spread, but the consensus is that explicitness is
    // preferred over brevity here.
    return {
      frames,
      mainThreadEvents,
      frameEvents,
      frameTreeEvents,
      processEvents,
      mainFrameInfo,
      timeOriginEvt,
      timings: {
        timeOrigin: 0,
        traceEnd: traceEnd.timing,
      },
      timestamps: {
        timeOrigin: timeOriginEvt.ts,
        traceEnd: traceEnd.timestamp,
      },
      _keyEvents: keyEvents,
      _rendererPidToTid: rendererPidToTid,
    };
  }
Finds key trace events, identifies main process/thread, and returns timings of trace events
in milliseconds since the time origin in addition to the standard microsecond monotonic timestamps.
@param {LH.Trace} trace
@param {{timeOriginDeterminationMethod?: TimeOriginDeterminationMethod}} [options]
@return {LH.Artifacts.ProcessedTrace}