static getThreadBreakdownTable(interactionEvent, trace, processedTrace, networkRecords) {
    // Limit to interactionEvent's thread.
    // TODO(bckenny): limit to interactionEvent's navigation.
    const threadEvents = TraceProcessor.filteredTraceSort(trace.traceEvents, evt => {
      return evt.pid === interactionEvent.pid && evt.tid === interactionEvent.tid;
    });
    const traceEndTs = threadEvents.reduce((endTs, evt) => {
      return Math.max(evt.ts + (evt.dur || 0), endTs);
    }, 0);
    // frames is only used for URL attribution, so can include all frames, even if OOPIF.
    const {frames} = processedTrace;
    const threadTasks = MainThreadTasks.getMainThreadTasks(threadEvents, frames, traceEndTs);

    const phases = WorkDuringInteraction.getPhaseTimes(interactionEvent);

    /** @type {LH.Audit.Details.TableItem[]} */
    const items = [];
    for (const [phaseName, phaseTimes] of Object.entries(phases)) {
      // Clip tasks to start and end time.
      WorkDuringInteraction.clipTasksByTs(threadTasks, phaseTimes.startTs, phaseTimes.endTs);
      const executionTimings = getExecutionTimingsByURL(threadTasks, networkRecords);

      const results = [];
      for (const [url, timingByGroupId] of executionTimings) {
        const totalExecutionTimeForURL = Object.values(timingByGroupId)
            .reduce((total, timespanMs) => total + timespanMs);

        const scriptingTotal = timingByGroupId[taskGroups.scriptEvaluation.id] || 0;
        const layoutTotal = timingByGroupId[taskGroups.styleLayout.id] || 0;
        const renderTotal = timingByGroupId[taskGroups.paintCompositeRender.id] || 0;

        results.push({
          url: url,
          total: totalExecutionTimeForURL,
          scripting: scriptingTotal,
          layout: layoutTotal,
          render: renderTotal,
        });
      }

      const filteredResults = results
        .filter(result => result.total > TASK_THRESHOLD)
        .sort((a, b) => b.total - a.total);

      items.push({
        phase: str_(UIStrings[/** @type {keyof UIStrings} */ (phaseName)]),
        total: (phaseTimes.endTs - phaseTimes.startTs) / 1000,
        subItems: {
          type: 'subitems',
          items: filteredResults,
        },
      });
    }

    /** @type {LH.Audit.Details.Table['headings']} */
    const headings = [
      /* eslint-disable max-len */
      {key: 'phase', valueType: 'text', subItemsHeading: {key: 'url', valueType: 'url'}, label: 'Phase'},
      {key: 'total', valueType: 'ms', subItemsHeading: {key: 'total', granularity: 1, valueType: 'ms'}, granularity: 1, label: 'Total time'},
      {key: null, valueType: 'ms', subItemsHeading: {key: 'scripting', granularity: 1, valueType: 'ms'}, label: 'Script evaluation'},
      {key: null, valueType: 'ms', subItemsHeading: {key: 'layout', granularity: 1, valueType: 'ms'}, label: taskGroups.styleLayout.label},
      {key: null, valueType: 'ms', subItemsHeading: {key: 'render', granularity: 1, valueType: 'ms'}, label: taskGroups.paintCompositeRender.label},
      /* eslint-enable max-len */
    ];

    return {
      table: Audit.makeTableDetails(headings, items, {sortedBy: ['total']}),
      phases,
    };
  }
@param {EventTimingEvent} interactionEvent
@param {LH.Trace} trace
@param {LH.Artifacts.ProcessedTrace} processedTrace
@param {Array<LH.Artifacts.NetworkRequest>} networkRecords
@return {{table: LH.Audit.Details.Table, phases: Record<string, {startTs: number, endTs: number}>}}