static _getTiming(artifacts) {
    const timingEntriesFromArtifacts = artifacts.Timing || [];
    const timingEntriesFromRunner = log.takeTimeEntries();
    const timingEntriesKeyValues = [
      ...timingEntriesFromArtifacts,
      ...timingEntriesFromRunner,
    ].map(entry => /** @type {[string, PerformanceEntry]} */ ([
      // As entries can share a name and start time, dedupe based on the name, startTime and duration
      `${entry.startTime}-${entry.name}-${entry.duration}`,
      entry,
    ]));
    const timingEntries = Array.from(new Map(timingEntriesKeyValues).values())
    // Truncate timestamps to hundredths of a millisecond saves ~4KB. No need for microsecond
    // resolution.
    .map(entry => {
      return {
        // Don't spread entry because browser PerformanceEntries can't be spread.
        // https://github.com/GoogleChrome/lighthouse/issues/8638
        startTime: parseFloat(entry.startTime.toFixed(2)),
        name: entry.name,
        duration: parseFloat(entry.duration.toFixed(2)),
        entryType: entry.entryType,
      };
    }).sort((a, b) => a.startTime - b.startTime);
    const gatherEntry = timingEntries.find(e => e.name === 'lh:runner:gather');
    const auditEntry = timingEntries.find(e => e.name === 'lh:runner:audit');
    const gatherTiming = gatherEntry?.duration || 0;
    const auditTiming = auditEntry?.duration || 0;
    return {entries: timingEntries, total: gatherTiming + auditTiming};
  }
This handles both the auditMode case where gatherer entries need to be merged in and
the gather/audit case where timingEntriesFromRunner contains all entries from this run,
including those also in timingEntriesFromArtifacts.
@param {LH.Artifacts} artifacts
@return {LH.Result.Timing}