// 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 { data as metaHandlerData } from './MetaHandler.js';
import { data as samplesHandlerData } from './SamplesHandler.js';
import { KNOWN_EVENTS } from './types.js';
import * as Types from '../types/types.js';
/**
 * This handler builds the hierarchy of trace events and profile calls
 * on each thread on each process.
 *
 * Throughout the code, trace events and profile calls are referred to
 * as "entries", but note they are different types of data. Trace events
 * come directly from the backend and it's the type the engine commonly
 * refers to. Profile calls on the other hand are built in the frontend,
 * and, for compatibility purposes, typed as an extension to the trace
 * event type.
 */
const processes = new Map();
// We track the compositor tile worker thread name events so that at the end we
// can return these keyed by the process ID. These are used in the frontend to
// show the user the rasterization thread(s) on the main frame as tracks.
const compositorTileWorkers = Array();
const entryToNode = new Map();
const allRendererEvents = [];
let nodeIdCount = 0;
const makeRendererEntrytNodeId = () => (++nodeIdCount);
const completeEventStack = [];
let handlerState = 1 /* HandlerState.UNINITIALIZED */;
const makeRendererProcess = () => ({
    url: null,
    isOnMainFrame: false,
    threads: new Map(),
});
const makeRendererThread = () => ({
    name: null,
    entries: [],
});
const makeEmptyRendererTree = () => ({
    nodes: new Map(),
    roots: new Set(),
    maxDepth: 0,
});
const makeEmptyRendererEventNode = (entry, id) => ({
    entry,
    id,
    parentId: null,
    childrenIds: new Set(),
    depth: 0,
});
const getOrCreateRendererProcess = (processes, pid) => {
    return Platform.MapUtilities.getWithDefault(processes, pid, makeRendererProcess);
};
const getOrCreateRendererThread = (process, tid) => {
    return Platform.MapUtilities.getWithDefault(process.threads, tid, makeRendererThread);
};
export function reset() {
    processes.clear();
    entryToNode.clear();
    allRendererEvents.length = 0;
    completeEventStack.length = 0;
    compositorTileWorkers.length = 0;
    nodeIdCount = -1;
    handlerState = 1 /* HandlerState.UNINITIALIZED */;
}
export function initialize() {
    if (handlerState !== 1 /* HandlerState.UNINITIALIZED */) {
        throw new Error('Renderer Handler was not reset');
    }
    handlerState = 2 /* HandlerState.INITIALIZED */;
}
export function handleEvent(event) {
    if (handlerState !== 2 /* HandlerState.INITIALIZED */) {
        throw new Error('Renderer Handler is not initialized');
    }
    if (Types.TraceEvents.isThreadName(event) && event.args.name?.startsWith('CompositorTileWorker')) {
        compositorTileWorkers.push({
            pid: event.pid,
            tid: event.tid,
        });
    }
    if (Types.TraceEvents.isTraceEventBegin(event) || Types.TraceEvents.isTraceEventEnd(event)) {
        const process = getOrCreateRendererProcess(processes, event.pid);
        const thread = getOrCreateRendererThread(process, event.tid);
        const completeEvent = makeCompleteEvent(event);
        if (!completeEvent) {
            return;
        }
        thread.entries.push(completeEvent);
        allRendererEvents.push(completeEvent);
        return;
    }
    if (Types.TraceEvents.isTraceEventInstant(event) || Types.TraceEvents.isTraceEventComplete(event)) {
        const process = getOrCreateRendererProcess(processes, event.pid);
        const thread = getOrCreateRendererThread(process, event.tid);
        thread.entries.push(event);
        allRendererEvents.push(event);
    }
}
export async function finalize() {
    if (handlerState !== 2 /* HandlerState.INITIALIZED */) {
        throw new Error('Renderer Handler is not initialized');
    }
    const { mainFrameId, rendererProcessesByFrame, threadsInProcess } = metaHandlerData();
    assignMeta(processes, mainFrameId, rendererProcessesByFrame, threadsInProcess);
    sanitizeProcesses(processes);
    buildHierarchy(processes, { filter: KNOWN_EVENTS });
    sanitizeThreads(processes);
    handlerState = 3 /* HandlerState.FINALIZED */;
}
export function data() {
    if (handlerState !== 3 /* HandlerState.FINALIZED */) {
        throw new Error('Renderer Handler is not finalized');
    }
    return {
        processes: new Map(processes),
        compositorTileWorkers: new Map(gatherCompositorThreads()),
        entryToNode: new Map(entryToNode),
        allRendererEvents: [...allRendererEvents],
    };
}
function gatherCompositorThreads() {
    const threadsByProcess = new Map();
    for (const worker of compositorTileWorkers) {
        const byProcess = threadsByProcess.get(worker.pid) || [];
        byProcess.push(worker.tid);
        threadsByProcess.set(worker.pid, byProcess);
    }
    return threadsByProcess;
}
/**
 * Steps through all the renderer processes we've located so far in the meta
 * handler, obtaining their URL, checking whether they are the main frame, and
 * collecting each one of their threads' name. This meta handler's data is
 * assigned to the renderer handler's data.
 */
export function assignMeta(processes, mainFrameId, rendererProcessesByFrame, threadsInProcess) {
    assignOrigin(processes, rendererProcessesByFrame);
    assignIsMainFrame(processes, mainFrameId, rendererProcessesByFrame);
    assignThreadName(processes, rendererProcessesByFrame, threadsInProcess);
}
/**
 * Assigns origins to all threads in all processes.
 * @see assignMeta
 */
export function assignOrigin(processes, rendererProcessesByFrame) {
    for (const renderProcessesByPid of rendererProcessesByFrame.values()) {
        for (const [pid, processWindows] of renderProcessesByPid) {
            for (const processInfo of processWindows.flat()) {
                const process = getOrCreateRendererProcess(processes, pid);
                // Sometimes a single process is responsible with rendering multiple
                // frames at the same time. For example, see https://crbug.com/1334563.
                // When this happens, we'd still like to assign a single url per process
                // so: 1) use the first frame rendered by this process as the url source
                // and 2) if the last url is "about:blank", use the next frame's url,
                // data from about:blank is irrelevant.
                if (process.url === null || process.url === 'about:blank') {
                    // If we are here, it's because we care about this process and the URL. But before we store
                    // it, we check if it is a valid URL by trying to create a URL object. If it isn't, we won't
                    // set it, and this process will be filtered out later.
                    try {
                        new URL(processInfo.frame.url);
                        process.url = processInfo.frame.url;
                    }
                    catch (e) {
                        process.url = null;
                    }
                }
            }
        }
    }
}
/**
 * Assigns whether or not a thread is the main frame to all threads in all processes.
 * @see assignMeta
 */
export function assignIsMainFrame(processes, mainFrameId, rendererProcessesByFrame) {
    for (const [frameId, renderProcessesByPid] of rendererProcessesByFrame) {
        for (const [pid] of renderProcessesByPid) {
            const process = getOrCreateRendererProcess(processes, pid);
            // We have this go in one direction; once a renderer has been flagged as
            // being on the main frame, we don't unset it to false if were to show up
            // in a subframe. Equally, if we already saw this renderer in a subframe,
            // but it becomes the main frame, the flag would get updated.
            if (frameId === mainFrameId) {
                process.isOnMainFrame = true;
            }
        }
    }
}
/**
 * Assigns the thread name to all threads in all processes.
 * @see assignMeta
 */
export function assignThreadName(processes, rendererProcessesByFrame, threadsInProcess) {
    for (const [, renderProcessesByPid] of rendererProcessesByFrame) {
        for (const [pid] of renderProcessesByPid) {
            const process = getOrCreateRendererProcess(processes, pid);
            for (const [tid, threadInfo] of threadsInProcess.get(pid) ?? []) {
                const thread = getOrCreateRendererThread(process, tid);
                thread.name = threadInfo?.args.name ?? `${tid}`;
            }
        }
    }
}
/**
 * Removes unneeded trace data opportunistically stored while handling events.
 * This currently does the following:
 *  - Deletes processes with an unkonwn origin.
 */
export function sanitizeProcesses(processes) {
    for (const [pid, process] of processes) {
        // If the process had no url, or if it had a malformed url that could not be
        // parsed for some reason, or if it's an "about:" origin, delete it.
        // This is done because we don't really care about processes for which we
        // can't provide actionable insights to the user (e.g. about:blank pages).
        if (process.url === null) {
            processes.delete(pid);
            continue;
        }
        const asUrl = new URL(process.url);
        if (asUrl.protocol === 'about:') {
            processes.delete(pid);
        }
    }
}
/**
 * Removes unneeded trace data opportunistically stored while handling events.
 * This currently does the following:
 *  - Deletes threads with no roots.
 */
export function sanitizeThreads(processes) {
    for (const [, process] of processes) {
        for (const [tid, thread] of process.threads) {
            // If the thread has no roots, delete it. Otherwise, there's going to
            // be space taken, even though nothing is rendered in the track manager.
            if (!thread.tree?.roots.size) {
                process.threads.delete(tid);
            }
        }
    }
}
/**
 * Creates a hierarchical structure from the trace events. Each thread in each
 * process will contribute to their own individual hierarchy.
 *
 * The trace data comes in as a contiguous array of events, against which we
 * make a couple of assumptions:
 *
 *  1. Events are temporally-ordered in terms of start time (though they're
 *     not necessarily ordered as such in the data stream).
 *  2. If event B's start and end times are within event A's time boundaries
 *     we assume that A is the parent of B.
 *
 * Therefore we expect to reformulate something like:
 *
 * [ Task A ][ Task B ][ Task C ][ Task D ][ Task E ]
 *
 * Into something hierarchically-arranged like below:
 *
 * |------------- Task A -------------||-- Task E --|
 *  |-- Task B --||-- Task D --|
 *   |- Task C -|
 */
export function buildHierarchy(processes, options) {
    for (const [pid, process] of processes) {
        for (const [tid, thread] of process.threads) {
            if (!thread.entries.length) {
                thread.tree = makeEmptyRendererTree();
                continue;
            }
            // Step 1. Massage the data.
            Helpers.Trace.sortTraceEventsInPlace(thread.entries);
            // Step 2. Inject profile calls from samples
            const cpuProfile = samplesHandlerData().profilesInProcess.get(pid)?.get(tid)?.parsedProfile;
            const samplesIntegrator = cpuProfile && new Helpers.SamplesIntegrator.SamplesIntegrator(cpuProfile, pid, tid);
            const profileCalls = samplesIntegrator?.buildProfileCalls(thread.entries);
            if (profileCalls) {
                thread.entries = Helpers.Trace.mergeEventsInOrder(thread.entries, profileCalls);
            }
            // Step 3. Build the tree.
            thread.tree = treify(thread.entries, options);
        }
    }
}
/**
 * Builds a hierarchy of the entries (trace events and profile calls) in
 * a particular thread of a particular process, assuming that they're
 * sorted, by iterating through all of the events in order.
 *
 * The approach is analogous to how a parser would be implemented. A
 * stack maintains local context. A scanner peeks and pops from the data
 * stream. Various "tokens" (events) are treated as "whitespace"
 * (ignored).
 *
 * The tree starts out empty and is populated as the hierarchy is built.
 * The nodes are also assumed to be created empty, with no known parent
 * or children.
 *
 * Complexity: O(n), where n = number of events
 */
export function treify(entries, options) {
    const stack = [];
    // Reset the node id counter for every new renderer.
    nodeIdCount = -1;
    const tree = makeEmptyRendererTree();
    for (let i = 0; i < entries.length; i++) {
        const event = entries[i];
        // If the current event should not be part of the tree, then simply proceed
        // with the next event.
        if (!options.filter.has(event.name)) {
            continue;
        }
        const duration = event.dur || 0;
        const nodeId = makeRendererEntrytNodeId();
        const node = makeEmptyRendererEventNode(event, nodeId);
        // If the parent stack is empty, then the current event is a root. Create a
        // node for it, mark it as a root, then proceed with the next event.
        if (stack.length === 0) {
            tree.nodes.set(nodeId, node);
            tree.roots.add(nodeId);
            event.selfTime = Types.Timing.MicroSeconds(duration);
            stack.push(node);
            tree.maxDepth = Math.max(tree.maxDepth, stack.length);
            entryToNode.set(event, node);
            continue;
        }
        const parentNode = stack.at(-1);
        if (parentNode === undefined) {
            throw new Error('Impossible: no parent node found in the stack');
        }
        const parentEvent = parentNode.entry;
        const begin = event.ts;
        const parentBegin = parentEvent.ts;
        const parentDuration = parentEvent.dur || 0;
        const end = begin + duration;
        const parentEnd = parentBegin + parentDuration;
        // Check the relationship between the parent event at the top of the stack,
        // and the current event being processed. There are only 4 distinct
        // possiblities, only 2 of them actually valid, given the assumed sorting:
        // 1. Current event starts before the parent event, ends whenever. (invalid)
        // 2. Current event starts after the parent event, ends whenever. (valid)
        // 3. Current event starts during the parent event, ends after. (invalid)
        // 4. Current event starts and ends during the parent event. (valid)
        // 1. If the current event starts before the parent event, then the data is
        //    not sorted properly, messed up some way, or this logic is incomplete.
        const startsBeforeParent = begin < parentBegin;
        if (startsBeforeParent) {
            throw new Error('Impossible: current event starts before the parent event');
        }
        // 2. If the current event starts after the parent event, then it's a new
        //    parent. Pop, then handle current event again.
        const startsAfterParent = begin >= parentEnd;
        if (startsAfterParent) {
            stack.pop();
            i--;
            // The last created node has been discarded, so discard this id.
            nodeIdCount--;
            continue;
        }
        // 3. If the current event starts during the parent event, but ends
        //    after it, then the data is messed up some way, for example a
        //    profile call was sampled too late after its start, ignore the
        //    problematic event.
        const endsAfterParent = end > parentEnd;
        if (endsAfterParent) {
            continue;
        }
        // 4. The only remaining case is the common case, where the current event is
        //    contained within the parent event. Create a node for the current
        //    event, establish the parent/child relationship, then proceed with the
        //    next event.
        tree.nodes.set(nodeId, node);
        node.depth = stack.length;
        node.parentId = parentNode.id;
        parentNode.childrenIds.add(nodeId);
        event.selfTime = Types.Timing.MicroSeconds(duration);
        if (parentEvent.selfTime !== undefined) {
            parentEvent.selfTime = Types.Timing.MicroSeconds(parentEvent.selfTime - (event.dur || 0));
        }
        stack.push(node);
        tree.maxDepth = Math.max(tree.maxDepth, stack.length);
        entryToNode.set(event, node);
    }
    return tree;
}
export function makeCompleteEvent(event) {
    if (Types.TraceEvents.isTraceEventEnd(event)) {
        // Quietly ignore unbalanced close events, they're legit (we could
        // have missed start one).
        const beginEvent = completeEventStack.pop();
        if (!beginEvent) {
            return null;
        }
        if (beginEvent.name !== event.name || beginEvent.cat !== event.cat) {
            console.error('Begin/End events mismatch at ' + beginEvent.ts + ' (' + beginEvent.name + ') vs. ' + event.ts + ' (' +
                event.name + ')');
            return null;
        }
        // Update the begin event's duration using the timestamp of the end
        // event.
        beginEvent.dur = Types.Timing.MicroSeconds(event.ts - beginEvent.ts);
        return null;
    }
    // Create a synthetic event using the begin event, when we find the
    // matching end event later we will update its duration.
    const syntheticComplete = {
        ...event,
        ph: "X" /* Types.TraceEvents.Phase.COMPLETE */,
        dur: Types.Timing.MicroSeconds(0),
    };
    completeEventStack.push(syntheticComplete);
    return syntheticComplete;
}
export function deps() {
    return ['Meta', 'Samples'];
}
class RendererEventNodeIdTag {
    /* eslint-disable-next-line no-unused-private-class-members */
    #tag;
}
//# sourceMappingURL=RendererHandler.js.map