chrome-devtools-frontend
Version:
Chrome DevTools UI
254 lines (221 loc) • 8.97 kB
text/typescript
// 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]};
}
}
}