_refineTraceEventsWithTasks(knownTasks, syntheticTasks, syntheticEvents) {
    /** @type {Array<SynthethicEvent>} */
    const refinedEvents = [];

    /** @type {Map<number, {naive: Array<SynthethicEvent>, refined: Array<SynthethicEvent>}>} */
    const syntheticEventsByTs = new Map();
    for (const event of syntheticEvents) {
      const group = syntheticEventsByTs.get(event.ts) || {naive: [], refined: []};
      group.naive.push(event);
      syntheticEventsByTs.set(event.ts, group);
    }

    /** @type {Map<SynthethicEvent, SynthethicTaskNode>} */
    const syntheticTasksByEvent = new Map();
    for (const task of syntheticTasks) {
      syntheticTasksByEvent.set(task.event, task);
      syntheticTasksByEvent.set(task.endEvent, task);
    }

    const knownTasksByStartTime = knownTasks.slice().sort((a, b) => a.startTime - b.startTime);
    const knownTasksByEndTime = knownTasks.slice().sort((a, b) => a.endTime - b.endTime);

    let knownTaskStartTimeIndex = 0;
    let knownTaskEndTimeIndex = 0;

    for (const event of syntheticEvents) {
      const syntheticTask = syntheticTasksByEvent.get(event);
      if (!syntheticTask) throw new Error('Impossible - all events have a task');
      const allEventsAtTs = syntheticEventsByTs.get(event.ts);
      if (!allEventsAtTs) throw new Error('Impossible - we just mapped every event');

      const effectiveTimestampData = CpuProfileModel._findEffectiveTimestamp({
        eventType: event.ph === 'B' ? 'start' : 'end',
        syntheticTask,
        allEventsAtTs,
        knownTaskStartTimeIndex,
        knownTaskEndTimeIndex,
        knownTasksByStartTime,
        knownTasksByEndTime,
      });

      knownTaskStartTimeIndex = effectiveTimestampData.lastStartTimeIndex;
      knownTaskEndTimeIndex = effectiveTimestampData.lastEndTimeIndex;

      const refinedEvent = {...event, ts: effectiveTimestampData.timestamp};
      refinedEvents.push(refinedEvent);
      allEventsAtTs.refined.push(refinedEvent);
    }

    return refinedEvents;
  }
Creates a copy of B/E-style trace events with refined timestamps using knowledge from the
tasks that have definitive timestamps.

With the sampling profiler we know that a function started/ended _sometime between_ two points,
but not exactly when. Using the information from other tasks gives us more information to be
more precise with timings and allows us to create a valid task tree later on.

@param {Array<{startTime: number, endTime: number}>} knownTasks
@param {Array<SynthethicTaskNode>} syntheticTasks
@param {Array<SynthethicEvent>} syntheticEvents
@return {Array<SynthethicEvent>}