// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as Platform from '../../../core/platform/platform.js';
import * as Helpers from '../helpers/helpers.js';
import * as Types from '../types/types.js';

import {HandlerState, EventCategory, KNOWN_EVENTS, type KnownEventName} from './types.js';

/**
 * Sorts samples in place, in order, by their start time.
 */
export function sortProfileSamples(samples: ProfileSample[]): void {
  samples.sort((a, b) => {
    const aBeginTime = a.ts;
    const bBeginTime = b.ts;
    if (aBeginTime < bBeginTime) {
      return -1;
    }
    if (aBeginTime > bBeginTime) {
      return 1;
    }
    return 0;
  });
}

const KNOWN_BOUNDARIES = new Set([
  EventCategory.Other,
  EventCategory.V8,
  EventCategory.Js,
  EventCategory.Gc,
]);

const ALLOWED_CALL_FRAME_CODE_TYPES = new Set([undefined, 'JS']);
const BANNED_CALL_FRAME_URL_REGS = [/^chrome-extension:\/\//, /^extensions::/];

const SAMPLING_INTERVAL = Types.Timing.MicroSeconds(200);

const events =
    new Map<Types.TraceEvents.ProcessID, Map<Types.TraceEvents.ThreadID, Types.TraceEvents.TraceEventComplete[]>>();
const profiles = new Map<Types.TraceEvents.ProfileID, Partial<SamplesProfile>>();
const processes = new Map<Types.TraceEvents.ProcessID, SamplesProcess>();

let handlerState = HandlerState.UNINITIALIZED;

const makeSamplesProcess = (): SamplesProcess => ({
  threads: new Map(),
});

const makeSamplesThread = (profile: SamplesProfile): SamplesThread => ({
  profile,
});

const makeEmptyProfileTree = (): ProfileTree => ({
  nodes: new Map(),
});

const makeEmptyProfileNode = (callFrame: Types.TraceEvents.TraceEventCallFrame): ProfileNode => ({
  callFrame,
  parentId: null,
  childrenIds: new Set(),
});

const makeProfileSample =
    (nodeId: Types.TraceEvents.CallFrameID, pid: Types.TraceEvents.ProcessID, tid: Types.TraceEvents.ThreadID,
     ts: Types.Timing.MicroSeconds): ProfileSample => ({
      topmostStackFrame: {nodeId},
      tid,
      pid,
      ts,
    });

const makeProfileCall = (nodeId: Types.TraceEvents.CallFrameID, sample: ProfileSample): ProfileCall => ({
  stackFrame: {nodeId},
  tid: sample.tid,
  pid: sample.pid,
  ts: sample.ts,
  dur: Types.Timing.MicroSeconds(0),
  selfDur: Types.Timing.MicroSeconds(0),
  children: [],
});

const makeEmptyProfileFunction = (nodeId: Types.TraceEvents.CallFrameID): ProfileFunction => ({
  stackFrame: {nodeId},
  calls: [],
  durPercent: 0,
  selfDurPercent: 0,
});

const getOrCreateSamplesProcess =
    (processes: Map<Types.TraceEvents.ProcessID, SamplesProcess>, pid: Types.TraceEvents.ProcessID): SamplesProcess => {
      return Platform.MapUtilities.getWithDefault(processes, pid, makeSamplesProcess);
    };

const getOrCreateSamplesThread =
    (process: SamplesProcess, tid: Types.TraceEvents.ThreadID, profile: SamplesProfile): SamplesThread => {
      return Platform.MapUtilities.getWithDefault(process.threads, tid, () => makeSamplesThread(profile));
    };

export function reset(): void {
  events.clear();
  profiles.clear();
  processes.clear();

  handlerState = HandlerState.UNINITIALIZED;
}

export function initialize(): void {
  if (handlerState !== HandlerState.UNINITIALIZED) {
    throw new Error('Samples Handler was not reset');
  }

  handlerState = HandlerState.INITIALIZED;
}

export function handleEvent(event: Types.TraceEvents.TraceEventData): void {
  if (handlerState !== HandlerState.INITIALIZED) {
    throw new Error('Samples Handler is not initialized');
  }

  if (Types.TraceEvents.isTraceEventProfile(event)) {
    const profile = Platform.MapUtilities.getWithDefault(profiles, event.id, (): Partial<SamplesProfile> => ({}));
    profile.head = event;
    return;
  }
  if (Types.TraceEvents.isTraceEventProfileChunk(event)) {
    const profile = Platform.MapUtilities.getWithDefault(profiles, event.id, (): Partial<SamplesProfile> => ({}));
    profile.chunks = profile.chunks ?? [];
    profile.chunks.push(event);
    return;
  }
  if (Types.TraceEvents.isTraceEventComplete(event)) {
    const process = Platform.MapUtilities.getWithDefault(events, event.pid, () => new Map());
    const thread = Platform.MapUtilities.getWithDefault(process, event.tid, () => []);
    thread.push(event);
    return;
  }
}

export async function finalize(): Promise<void> {
  if (handlerState !== HandlerState.INITIALIZED) {
    throw new Error('Samples Handler is not initialized');
  }
  buildProcessesAndThreads(profiles, processes);
  buildHierarchy(processes, events);

  handlerState = HandlerState.FINALIZED;
}

export function data(): SamplesHandlerData {
  if (handlerState !== HandlerState.FINALIZED) {
    throw new Error('Samples Handler is not finalized');
  }

  return {
    profiles: new Map(profiles),
    processes: new Map(processes),
  };
}

/**
 * Builds processes and threads from the accumulated profile chunks. This is
 * done during finalize instead of during event handling because profile heads
 * and chunks are sometimes retrieved out of order, or are incomplete.
 */
export function buildProcessesAndThreads(
    profiles: Map<Types.TraceEvents.ProfileID, Partial<SamplesProfile>>,
    processes: Map<Types.TraceEvents.ProcessID, SamplesProcess>): void {
  for (const [, profile] of profiles) {
    // Sometimes the trace includes empty profiles, or orphaned chunks, even
    // after going through all the trace events. Ignore.
    const {head, chunks} = profile;
    if (!head || !chunks?.length) {
      continue;
    }

    // Note: events are collected on a different thread than what's sampled.
    // The correct process and thread ids are specified by the profile.
    const pid = head.pid;
    const tid = head.tid;
    getOrCreateSamplesThread(getOrCreateSamplesProcess(processes, pid), tid, profile as SamplesProfile);
  }
}

/**
 * Converts the raw profile data into hierarchical and ordered structures from
 * the stack traces that were sampled during a recording. Each thread in each
 * process will contribute to their own individual profile.
 *
 * Our V8 profiler is a sampling profiler. This means that it probes the
 * program's call stack at regular intervals defined by the sampling frequency.
 * The raw profile data comes in as multiple events, from which a profile is
 * built.
 *
 * The generated data will be comprised of several parts:
 * 1. "tree": All the complete stack traces, represented by a tree whose roots
 *    are the bottomest stack frames of all stack traces.
 * 2. "samples": All the individual samples, as an ordered list where each item
 *    points to the topmost stack frame at a particular point in time.
 * 3. "calls": A list of profile calls, where each item represents multiple
 *    samples coalesced into a contiguous event. Each item will have a
 *    timestamp, duration, and refer to a stack frame and its child frames
 *    (all together forming multiple stack traces).
 */
export function buildHierarchy(
    processes: Map<Types.TraceEvents.ProcessID, SamplesProcess>,
    events: Map<Types.TraceEvents.ProcessID, Map<Types.TraceEvents.ThreadID, Types.TraceEvents.TraceEventComplete[]>>):
    void {
  for (const [pid, process] of processes) {
    for (const [tid, thread] of process.threads) {
      // Step 1. Massage the data.
      Helpers.Trace.sortTraceEventsInPlace(thread.profile.chunks);
      // ...and collect boundaries.
      const boundariesOptions = {filter: KNOWN_BOUNDARIES};
      const boundaries = thread.boundaries = collectBoundaries(events, pid, tid, boundariesOptions);

      // Step 2. Collect all the complete stack traces into a tree. (Extract tree structure from profile)
      const tree = thread.tree = collectStackTraces(thread.profile.chunks);

      // Step 3. Collect all the individual samples into a list.
      const {startTime} = thread.profile.head.args.data;
      const samplesOptions = {filterCodeTypes: true, filterUrls: true};
      const samples = thread.samples = collectSamples(pid, tid, startTime, tree, thread.profile.chunks, samplesOptions);

      // Step 4. Coalesce samples. (Apply tree to samples, turn them into calls, and merge where appropriate).
      const merge = mergeCalls(samples.map(sample => buildProfileCallFromSample(tree, sample)), boundaries);
      thread.calls = merge.calls;
      thread.dur = merge.dur;
    }
  }
}

/**
 * Builds an array of timestamps corresponding to the begin and end boundaries
 * of the events on the specified process and thread.
 *
 * Therefore we expect to reformulate a set of events which can be represented
 * hierarchically like:
 *
 * |=========== Task A ===============|== Task E ==|
 *   |=== Task B ===|== Task D ==|
 *     |= Task C =|
 *
 * ...into something ordered like below:
 *
 * | | |          | |                 |            |
 * |=========== Task A ===============|== Task E ==|
 * | |=== Task B ===|== Task D ==|    |            |
 * | | |= Task C =| |            |    |            |
 * | | |          | |            |    |            |
 * X X X          X X            X    X            X (boundaries)
 */
export function collectBoundaries(
    events: Map<Types.TraceEvents.ProcessID, Map<Types.TraceEvents.ThreadID, Types.TraceEvents.TraceEventComplete[]>>,
    pid: Types.TraceEvents.ProcessID, tid: Types.TraceEvents.ThreadID,
    options: {filter: {has: (category: EventCategory) => boolean}}): Types.Timing.MicroSeconds[] {
  const process = events.get(pid);
  if (!process) {
    return [];
  }

  const thread = process.get(tid);
  if (!thread) {
    return [];
  }

  const boundaries = new Set<Types.Timing.MicroSeconds>();

  for (const event of thread) {
    const category = KNOWN_EVENTS.get(event.name as KnownEventName)?.category ?? EventCategory.Other;
    if (!options.filter.has(category)) {
      continue;
    }
    boundaries.add(event.ts);
    boundaries.add(Types.Timing.MicroSeconds(event.ts + event.dur));
  }

  return [...boundaries].sort((a, b) => a < b ? -1 : 1);
}

/**
 * Builds all the complete stack traces that exist in a particular thread of a
 * particular process. They will be stored as a tree. The roots of this tree are
 * the bottomest stack frames of all individual stack traces.
 *
 * The stack traces are retrieved in partial chains, each chain as part of a
 * trace event. This method collects the data into a single tree.
 *
 * Therefore we expect to reformulate something like:
 *
 * Chain 1: [A, B <- A, C <- B]
 * Chain 2: [D <- A, E <- D]
 * Chain 3: [G]
 * Chain 4: [F <- B]
 * Chain 5: [H <- G, I <- H]
 *
 * ...into something hierarchically-arranged like below:
 *
 *     A       G
 *    / \      |
 *   B   D     H
 *  / \   \    |
 * C   F   E   I
 */
export function collectStackTraces(
    chunks: Types.TraceEvents.TraceEventProfileChunk[],
    options?: {filterCodeTypes: boolean, filterUrls: boolean}): ProfileTree {
  const tree = makeEmptyProfileTree();

  for (const chunk of chunks) {
    const cpuProfile = chunk.args.data?.cpuProfile;
    if (!cpuProfile) {
      continue;
    }
    const chain = cpuProfile.nodes;
    if (!chain) {
      continue;
    }

    for (const link of chain) {
      const nodeId = link.id;
      const parentNodeId = link.parent;
      const callFrame = link.callFrame;

      // If the current call frame should not be part of the tree, then simply proceed
      // with the next call frame.
      if (!isAllowedCallFrame(callFrame, options)) {
        continue;
      }

      const node = Platform.MapUtilities.getWithDefault(tree.nodes, nodeId, () => makeEmptyProfileNode(callFrame));

      // If the call frame has no parent, then it's the bottomest stack frame of
      // a stack trace (aka a root).
      if (parentNodeId === undefined) {
        continue;
      }

      // Otherwise, this call frame has a parent and threfore it's a stack frame
      // part of a larger stack trace. Each stack frame can only have a single
      // parent (called into by another unique stack frame), with multiple
      // children (calling into multiple unique stack frames). If a codepoint is
      // reached in multiple ways, multiple stack traces are created by V8.
      node.parentId = parentNodeId;
      tree.nodes.get(parentNodeId)?.childrenIds.add(nodeId);
    }
  }

  return tree;
}

/**
 * Collects all the individual samples that exist in a particular thread of a
 * particular process. They will be stored as an ordered list. Each entry
 * represents a sampled stack trace by pointing to the topmost stack frame at
 * that particular time.
 *
 * The samples are retrieved in buckets, each bucket as part of a trace event,
 * and each sample at a positive or negative delta cumulatively relative to the
 * profile's start time. This method collects the data into a single list.
 *
 * Therefore we expect to reformulate something like:
 *
 * Event 1 at 0µs: [A at Δ+1µs, A at Δ+2µs, B at Δ-1µs, C at Δ+2µs]
 * Event 2 at 9µs: [A at Δ+1µs, D at Δ+9µs, E at Δ-1µs]
 *
 * ...where each sample in each event points to the tompost stack frame at that
 * particular point in time (e.g. the first sample's tompost stack frame is A),
 * into something ordered like below:
 *
 * [A at 1µs, B at 2µs, A at 3µs, C at 4µs, A at 10µs, E at 18µs, D at 19µs]
 *
 * ...where each sample has an absolute timestamp, and the list is ordered.
 */
export function collectSamples(
    pid: Types.TraceEvents.ProcessID, tid: Types.TraceEvents.ThreadID, ts: Types.Timing.MicroSeconds, tree: ProfileTree,
    chunks: Types.TraceEvents.TraceEventProfileChunk[],
    options?: {filterCodeTypes: boolean, filterUrls: boolean}): ProfileSample[] {
  const samples: ProfileSample[] = [];

  for (const chunk of chunks) {
    const {timeDeltas, cpuProfile} = chunk.args.data ?? {};
    if (!timeDeltas || !cpuProfile) {
      continue;
    }

    for (let i = 0; i < timeDeltas.length; i++) {
      const timeDelta = timeDeltas[i];
      const nodeId = cpuProfile.samples[i];
      // The timestamp of each sample is at a positive or negative delta,
      // cumulatively relative to the profile's start time.
      ts = Types.Timing.MicroSeconds(ts + timeDelta);

      // The call frame may not have been added to the stack traces tree (e.g.
      // if its code type or url was banned). If there is no allowed stack frame
      // as part of a stack trace, then this sample is dropped.
      const topmostAllowedNodeId = findTopmostAllowedCallFrame(nodeId, tree, options);
      if (topmostAllowedNodeId === null) {
        continue;
      }

      // Otherwise, push the topmost allowed stack frame.
      samples.push(makeProfileSample(topmostAllowedNodeId, pid, tid, ts));
    }
  }

  sortProfileSamples(samples);
  return samples;
}

/**
 * For a list of samples in a thread in a process, merges together stack frames
 * which have been sampled consecutively and which do not cross boundaries. The
 * samples and boundaries arrays are assumed to be sorted.
 *
 * Therefore, if the previously collected stack traces tree looks like this:
 *
 *   A   E
 *  / \
 * B   D
 * |
 * C
 *
 * ...we expect to reformulate something like:
 *
 * [A, B, C, C .. C, B, A, A .. A, D, D .. D, A, A .. A, E, E .. E]
 *
 * ...where each sample points to the tompost stack frame at that particular
 * point in time (e.g. the first sample's tompost stack frame is A, the last
 * sample's topmost stack frame is E, etc.), and thus the expanded samples array
 * can be represented as:
 *
 * +------------> (sample at time)
 * |
 * |A|A|A|A|A|A|A|A|A|A|A|A|A|A|A|A|A| |E|E|E|E|E|E|
 * | |B|B|B|B|B|B| |D|D|D|D|D|D| | | | | | | | | | |
 * | | |C|C|C|C| | | | | | | | | | | | | | | | | | |
 * |
 * V (stack trace depth)
 *
 * ...into something ordered like below:
 *
 * [ Call A ][ Call E ]
 *
 * ...where the hierarchy of these calls can be represented as:
 *
 * [-------- Call A --------][ Call E ]
 *  [- Call B -][- Call D -]
 *   [ Call C ]
 *
 * ...and where each call has an absolute timestamp and a duration.
 *
 * Considerations:
 *
 * 1. Consecutive stack frames which cross boundaries may not be coalesced.
 * "Boundaries" are an array of timestamps corresponding to the begin and end
 * of certain events (such as "RunTask").
 *
 * For example, we expect to reformulate something like:
 *
 *   (boundary)                                    (boundary)
 *       |                                             |
 * |A|A|A|A|A|A|A|A|A|A|A|A|A|A|A|A|A| |E|E|E|E|E|E|   |
 *       |                                             |
 *
 * ...into something ordered like below:
 *
 * [ Call A ][ Call A ][ Call E ]
 *
 * ... where the first Call A is before the first boundary, second Call A is
 * after the first boundary, and Call E is inbetween boundaries.
 *
 * 2. Consecutive stack frames which are part of different branches (a.k.a part
 * of a different stack trace) must not be coalesced, even if at the same depth.
 *
 * For example, with something like:
 *
 * +------------> (sample at time)
 * |
 * | ... |X|X|X|Z|Z|Z| ...
 * |         |Y|Y|
 * |
 * V (stack trace depth)
 *
 * ...or:
 *
 * +------------> (sample at time)
 * |
 * | ... |X|X|X|Z|X|X|X| ...
 * |         |Y| |Y|
 * |
 * V (stack trace depth)
 *
 * ...the Y stack frames must not be merged even if they have been sampled
 * close together, and even if they do not cross any boundaries (e.g. are part
 * of the same `RunTask`). This is because they are either:
 * - part of separate stack traces (and therefore would have different IDs), or
 * - separated by a different parent frame.
 */
export function mergeCalls(calls: ProfileCall[], boundaries: Types.Timing.MicroSeconds[]): {
  calls: ProfileCall[],
  dur: Types.Timing.MicroSeconds,
} {
  const out = {calls: new Array<ProfileCall>(), dur: Types.Timing.MicroSeconds(0)};
  let boundary = 0;

  for (const call of calls) {
    // When the call crosses a boundary defined by any of the relevant trace
    // events (e.g. `RunTask`), even if the stack frame would be the same, start
    // a new merge with the current call as head, and find the next boundary.
    const isAcrossBoundary = call.ts >= boundary;
    if (isAcrossBoundary) {
      const index = Platform.ArrayUtilities.nearestIndexFromBeginning(boundaries, ts => ts > call.ts) ?? Infinity;
      boundary = boundaries[index];
      out.calls.push(call);
      continue;
    }

    // Otherwise, start a new merge if the call is a different stack frame, or
    // it was sampled too far into the future from the previous call.
    const previous = out.calls.at(-1);
    if (!previous) {
      continue;
    }
    const isSameStackFrame = call.stackFrame.nodeId === previous.stackFrame.nodeId;
    const isSampledConsecutively = call.ts - (previous.ts + previous.dur) < SAMPLING_INTERVAL;
    if (!isSameStackFrame || !isSampledConsecutively) {
      out.calls.push(call);
      continue;
    }

    previous.dur = Types.Timing.MicroSeconds(call.ts - previous.ts);
    previous.children.push(...call.children);
  }

  for (const call of out.calls) {
    const merge = mergeCalls(call.children, boundaries);
    call.children = merge.calls;
    call.selfDur = Types.Timing.MicroSeconds(call.dur - merge.dur);
    out.dur = Types.Timing.MicroSeconds(out.dur + call.dur);
  }

  return out;
}

/**
 * Checks if the call frame is allowed (i.e. it may not be part of the collected
 * stack traces tree).
 */
export function isAllowedCallFrame(
    callFrame: Types.TraceEvents.TraceEventCallFrame,
    options?: {filterCodeTypes: boolean, filterUrls: boolean}): boolean {
  if (options?.filterCodeTypes && !ALLOWED_CALL_FRAME_CODE_TYPES.has(callFrame.codeType)) {
    return false;
  }
  if (options?.filterUrls && BANNED_CALL_FRAME_URL_REGS.some(re => callFrame.url?.match(re))) {
    return false;
  }
  return true;
}

/**
 * Walks the stack traces tree from top to bottom until it finds a call frame that is allowed.
 */
export function findTopmostAllowedCallFrame(
    nodeId: Types.TraceEvents.CallFrameID|null, tree: ProfileTree,
    options?: {filterCodeTypes: boolean, filterUrls: boolean}): Types.TraceEvents.CallFrameID|null {
  if (nodeId === null) {
    return null;
  }
  const node = tree.nodes.get(nodeId);
  const callFrame = node?.callFrame;
  if (!node || !callFrame) {
    return null;
  }
  if (!isAllowedCallFrame(callFrame, options)) {
    return findTopmostAllowedCallFrame(node.parentId, tree, options);
  }
  return nodeId;
}

/**
 * Gets the stack trace associated with a sample. The topmost stack frame will
 * be the last entry of array. Aka the root stack frame will be the first.
 *
 * All the complete stack traces are stored as part of a profile tree. All the
 * samples point to the topmost stack frame. This method walks up the tree to
 * compose a stack trace.
 */
export function buildStackTraceAsCallFrameIdsFromId(
    tree: ProfileTree, nodeId: Types.TraceEvents.CallFrameID): Types.TraceEvents.CallFrameID[] {
  const out = [];

  let currentNodeId: Types.TraceEvents.CallFrameID|null = nodeId;
  let currentNode;

  while (currentNodeId !== null && (currentNode = tree.nodes.get(currentNodeId))) {
    out.push(currentNodeId);
    currentNodeId = currentNode.parentId;
  }

  return out.reverse();
}

/**
 * Just like `buildStackTrace`, but returns an array of call frames instead of ids.
 */
export function buildStackTraceAsCallFramesFromId(
    tree: ProfileTree, nodeId: Types.TraceEvents.CallFrameID): Types.TraceEvents.TraceEventCallFrame[] {
  const trace = buildStackTraceAsCallFrameIdsFromId(tree, nodeId);
  return trace.map(nodeId => {
    const callFrame = tree.nodes.get(nodeId)?.callFrame;
    if (!callFrame) {
      throw new Error();
    }
    return callFrame;
  });
}

/**
 * Just like `buildStackTrace`, but returns a `ProfileCall` instead of an array.
 */
export function buildProfileCallFromSample(tree: ProfileTree, sample: ProfileSample): ProfileCall {
  const trace = buildStackTraceAsCallFrameIdsFromId(tree, sample.topmostStackFrame.nodeId);
  const calls = trace.map(nodeId => makeProfileCall(nodeId, sample));

  for (let i = 1; i < calls.length; i++) {
    const parent = calls[i - 1];
    const current = calls[i];
    parent.children.push(current);
  }

  return calls[0];
}

/**
 * Gets all functions that have been called between the given timestamps, each
 * with additional information:
 * - the call frame id, which points to a node containing the function name etc.
 * - all individual calls for that function
 * - percentage of time taken, relative to the given timestamps
 * - percentage of self time taken relative to the given timestamps
 */
export function getAllFunctionsBetweenTimestamps(
    calls: ProfileCall[], begin: Types.Timing.MicroSeconds, end: Types.Timing.MicroSeconds,
    out: Map<Types.TraceEvents.CallFrameID, ProfileFunction> = new Map()): IterableIterator<ProfileFunction> {
  for (const call of calls) {
    if (call.ts < begin || call.ts + call.dur > end) {
      continue;
    }
    const func = Platform.MapUtilities.getWithDefault(
        out, call.stackFrame.nodeId, () => makeEmptyProfileFunction(call.stackFrame.nodeId));
    func.calls.push(call);
    func.durPercent += (call.dur / (end - begin)) * 100;
    func.selfDurPercent += (call.selfDur / (end - begin)) * 100;
    getAllFunctionsBetweenTimestamps(call.children, begin, end, out);
  }

  return out.values();
}

/**
 * Gets all the hot functions between timestamps, each with information about
 * the relevant call frame, time, self time, and percentages.
 *
 * The hot functions are sorted by self time.
 */
export function getAllHotFunctionsBetweenTimestamps(
    calls: ProfileCall[], begin: Types.Timing.MicroSeconds, end: Types.Timing.MicroSeconds,
    minSelfPercent: number): ProfileFunction[] {
  const functions = getAllFunctionsBetweenTimestamps(calls, begin, end);
  const hot = [...functions].filter(info => info.selfDurPercent >= minSelfPercent);
  return hot.sort((a, b) => a.selfDurPercent > b.selfDurPercent ? -1 : 1);
}

export interface SamplesHandlerData {
  profiles: Map<Types.TraceEvents.ProfileID, Partial<SamplesProfile>>;
  processes: Map<Types.TraceEvents.ProcessID, SamplesProcess>;
}

export interface SamplesProfile {
  head: Types.TraceEvents.TraceEventProfile;
  chunks: Types.TraceEvents.TraceEventProfileChunk[];
}

export interface SamplesProcess {
  threads: Map<Types.TraceEvents.ThreadID, SamplesThread>;
}

export interface SamplesThread {
  profile: SamplesProfile;
  boundaries?: Types.Timing.MicroSeconds[];
  tree?: ProfileTree;
  samples?: ProfileSample[];
  calls?: ProfileCall[];
  dur?: Types.Timing.MicroSeconds;
}

export interface ProfileTree {
  nodes: Map<Types.TraceEvents.CallFrameID, ProfileNode>;
}

export interface ProfileNode {
  callFrame: Types.TraceEvents.TraceEventCallFrame;
  parentId: Types.TraceEvents.CallFrameID|null;
  childrenIds: Set<Types.TraceEvents.CallFrameID>;
}

export interface ProfileSample {
  topmostStackFrame: {
    nodeId: Types.TraceEvents.CallFrameID,
  };
  pid: Types.TraceEvents.ProcessID;
  tid: Types.TraceEvents.ThreadID;
  ts: Types.Timing.MicroSeconds;
}

export interface ProfileCall {
  stackFrame: {
    nodeId: Types.TraceEvents.CallFrameID,
  };
  pid: Types.TraceEvents.ProcessID;
  tid: Types.TraceEvents.ThreadID;
  ts: Types.Timing.MicroSeconds;
  dur: Types.Timing.MicroSeconds;      // "time"
  selfDur: Types.Timing.MicroSeconds;  // "self time"
  children: ProfileCall[];
}

export interface ProfileFunction {
  stackFrame: {
    nodeId: Types.TraceEvents.CallFrameID,
  };
  calls: ProfileCall[];
  durPercent: number;      // 0 - 100
  selfDurPercent: number;  // 0 - 100
}
