UNPKG

chrome-devtools-frontend

Version:
254 lines (221 loc) • 8.97 kB
// Copyright 2023 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 type * as Types from './types/types.js'; import * as Handlers from './handlers/handlers.js'; const enum Status { IDLE = 'IDLE', PARSING = 'PARSING', FINISHED_PARSING = 'FINISHED_PARSING', ERRORED_WHILE_PARSING = 'ERRORED_WHILE_PARSING', } export type TraceParseEventProgressData = { index: number, total: number, }; export class TraceParseProgressEvent extends Event { static readonly eventName = 'traceparseprogress'; constructor(public data: TraceParseEventProgressData, init: EventInit = {bubbles: true}) { super(TraceParseProgressEvent.eventName, init); } } declare global { interface HTMLElementEventMap { [TraceParseProgressEvent.eventName]: TraceParseProgressEvent; } } export class TraceProcessor<EnabledModelHandlers extends {[key: string]: Handlers.Types.TraceEventHandler}> extends EventTarget { // We force the Meta handler to be enabled, so the TraceHandlers type here is // the model handlers the user passes in and the Meta handler. // eslint-disable-next-line @typescript-eslint/naming-convention readonly #traceHandlers: Handlers.Types.HandlersWithMeta<EnabledModelHandlers>; #pauseDuration: number; #eventsPerChunk: number; #status = Status.IDLE; static createWithAllHandlers(): TraceProcessor<typeof Handlers.ModelHandlers> { return new TraceProcessor(Handlers.ModelHandlers); } constructor(traceHandlers: EnabledModelHandlers, {pauseDuration = 1, eventsPerChunk = 15_000} = {}) { super(); this.#verifyHandlers(traceHandlers); this.#traceHandlers = { Meta: Handlers.ModelHandlers.Meta, ...traceHandlers, }; this.#pauseDuration = pauseDuration; this.#eventsPerChunk = eventsPerChunk; } /** * When the user passes in a set of handlers, we want to ensure that we have all * the required handlers. Handlers can depend on other handlers, so if the user * passes in FooHandler which depends on BarHandler, they must also pass in * BarHandler too. This method verifies that all dependencies are met, and * throws if not. **/ #verifyHandlers(providedHandlers: EnabledModelHandlers): void { // Tiny optimisation: if the amount of provided handlers matches the amount // of handlers in the Handlers.ModelHandlers object, that means that the // user has passed in every handler we have. So therefore they cannot have // missed any, and there is no need to iterate through the handlers and // check the dependencies. if (Object.keys(providedHandlers).length === Object.keys(Handlers.ModelHandlers).length) { return; } const requiredHandlerKeys: Set<Handlers.Types.TraceEventHandlerName> = new Set(); for (const [handlerName, handler] of Object.entries(providedHandlers)) { requiredHandlerKeys.add(handlerName as Handlers.Types.TraceEventHandlerName); for (const depName of (handler.deps?.() || [])) { requiredHandlerKeys.add(depName); } } const providedHandlerKeys = new Set(Object.keys(providedHandlers)); // We always force the Meta handler to be enabled when creating the // Processor, so if it is missing from the set the user gave us that is OK, // as we will have enabled it anyway. requiredHandlerKeys.delete('Meta'); for (const requiredKey of requiredHandlerKeys) { if (!providedHandlerKeys.has(requiredKey)) { throw new Error(`Required handler ${requiredKey} not provided.`); } } } reset(): void { if (this.#status === Status.PARSING) { throw new Error('Trace processor can\'t reset while parsing.'); } const handlers = Object.values(this.#traceHandlers); for (const handler of handlers) { handler.reset(); } this.#status = Status.IDLE; } async parse(traceEvents: readonly Types.TraceEvents.TraceEventData[], freshRecording = false): Promise<void> { if (this.#status !== Status.IDLE) { throw new Error(`Trace processor can't start parsing when not idle. Current state: ${this.#status}`); } try { this.#status = Status.PARSING; await this.#parse(traceEvents, freshRecording); this.#status = Status.FINISHED_PARSING; } catch (e) { this.#status = Status.ERRORED_WHILE_PARSING; throw e; } } async #parse(traceEvents: readonly Types.TraceEvents.TraceEventData[], freshRecording: boolean): Promise<void> { // This iterator steps through all events, periodically yielding back to the // main thread to avoid blocking execution. It uses `dispatchEvent` to // provide status update events, and other various bits of config like the // pause duration and frequency. const traceEventIterator = new TraceEventIterator(traceEvents, this.#pauseDuration, this.#eventsPerChunk); // Convert to array so that we are able to iterate all handlers multiple times. const sortedHandlers = [...sortHandlers(this.#traceHandlers).values()]; // Reset. for (const handler of sortedHandlers) { handler.reset(); } // Initialize. for (const handler of sortedHandlers) { handler.initialize?.(freshRecording); } // Handle each event. for await (const item of traceEventIterator) { if (item.kind === IteratorItemType.STATUS_UPDATE) { this.dispatchEvent(new TraceParseProgressEvent(item.data)); continue; } for (const handler of sortedHandlers) { handler.handleEvent(item.data); } } // Finalize. for (const handler of sortedHandlers) { await handler.finalize?.(); } } get data(): Handlers.Types.EnabledHandlerDataWithMeta<EnabledModelHandlers>|null { if (this.#status !== Status.FINISHED_PARSING) { return null; } const data = {}; for (const [name, handler] of Object.entries(this.#traceHandlers)) { Object.assign(data, {[name]: handler.data()}); } return data as Handlers.Types.EnabledHandlerDataWithMeta<EnabledModelHandlers>; } } /** * Some Handlers need data provided by others. Dependencies of a handler handler are * declared in the `deps` field. * @returns A map from trace event handler name to trace event hander whose entries * iterate in such a way that each handler is visited after its dependencies. */ export function sortHandlers( traceHandlers: Partial<{[key in Handlers.Types.TraceEventHandlerName]: Handlers.Types.TraceEventHandler}>): Map<Handlers.Types.TraceEventHandlerName, Handlers.Types.TraceEventHandler> { const sortedMap = new Map<Handlers.Types.TraceEventHandlerName, Handlers.Types.TraceEventHandler>(); const visited = new Set<Handlers.Types.TraceEventHandlerName>(); const visitHandler = (handlerName: Handlers.Types.TraceEventHandlerName): void => { if (sortedMap.has(handlerName)) { return; } if (visited.has(handlerName)) { let stackPath = ''; for (const handler of visited) { if (stackPath || handler === handlerName) { stackPath += `${handler}->`; } } stackPath += handlerName; throw new Error(`Found dependency cycle in trace event handlers: ${stackPath}`); } visited.add(handlerName); const handler = traceHandlers[handlerName]; if (!handler) { return; } const deps = handler.deps?.(); if (deps) { deps.forEach(visitHandler); } sortedMap.set(handlerName, handler); }; for (const handlerName of Object.keys(traceHandlers)) { visitHandler(handlerName as Handlers.Types.TraceEventHandlerName); } return sortedMap; } const enum IteratorItemType { TRACE_EVENT = 1, STATUS_UPDATE = 2, } type IteratorItem = IteratorTraceEventItem|IteratorStatusUpdateItem; type IteratorTraceEventItem = { kind: IteratorItemType.TRACE_EVENT, data: Types.TraceEvents.TraceEventData, }; type IteratorStatusUpdateItem = { kind: IteratorItemType.STATUS_UPDATE, data: TraceParseEventProgressData, }; class TraceEventIterator { #eventCount: number; constructor( private traceEvents: readonly Types.TraceEvents.TraceEventData[], private pauseDuration: number, private eventsPerChunk: number) { this.#eventCount = 0; } async * [Symbol.asyncIterator](): AsyncGenerator<IteratorItem, void, void> { for (let i = 0, length = this.traceEvents.length; i < length; i++) { // Every so often we take a break just to render. if (++this.#eventCount % this.eventsPerChunk === 0) { // Take the opportunity to provide status update events. yield {kind: IteratorItemType.STATUS_UPDATE, data: {index: i, total: length}}; // Wait for rendering before resuming. await new Promise(resolve => setTimeout(resolve, this.pauseDuration)); } yield {kind: IteratorItemType.TRACE_EVENT, data: this.traceEvents[i]}; } } }