@quick-game/cli
Version:
Command line interface for rapid qg development
282 lines • 13.4 kB
JavaScript
// 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 Types from '../types/types.js';
// We track the renderer processes we see in each frame on the way through the trace.
const rendererProcessesByFrameId = new Map();
// We will often want to key data by Frame IDs, and commonly we'll care most
// about the main frame's ID, so we store and expose that.
let mainFrameId = '';
let mainFrameURL = '';
const framesByProcessId = new Map();
// We will often want to key data by the browser process, GPU process and top
// level renderer IDs, so keep a track on those.
let browserProcessId = Types.TraceEvents.ProcessID(-1);
let browserThreadId = Types.TraceEvents.ThreadID(-1);
let gpuProcessId = Types.TraceEvents.ProcessID(-1);
let gpuThreadId = Types.TraceEvents.ThreadID(-1);
let viewportRect = null;
const topLevelRendererIds = new Set();
const traceBounds = {
min: Types.Timing.MicroSeconds(Number.POSITIVE_INFINITY),
max: Types.Timing.MicroSeconds(Number.NEGATIVE_INFINITY),
range: Types.Timing.MicroSeconds(Number.POSITIVE_INFINITY),
};
/**
* These represent the user navigating. Values such as First Contentful Paint,
* etc, are relative to the navigation.
*
* We store navigation events both by the frame and navigation ID. This means
* when we need to look them up, we can use whichever ID we have.
*
* Note that these Maps will have the same values in them; these are just keyed
* differently to make look-ups easier.
*
* We also additionally maintain an array of only navigations that occured on
* the main frame. In many places in the UI we only care about highlighting
* main frame navigations, so calculating this list here is better than
* filtering either of the below maps over and over again at the UI layer.
*/
const navigationsByFrameId = new Map();
const navigationsByNavigationId = new Map();
const mainFrameNavigations = [];
// Represents all the threads in the trace, organized by process. This is mostly for internal
// bookkeeping so that during the finalize pass we can obtain the main and browser thread IDs.
const threadsInProcess = new Map();
let traceStartedTime = Types.Timing.MicroSeconds(-1);
const eventPhasesOfInterestForTraceBounds = new Set([
"B" /* Types.TraceEvents.Phase.BEGIN */,
"E" /* Types.TraceEvents.Phase.END */,
"X" /* Types.TraceEvents.Phase.COMPLETE */,
"I" /* Types.TraceEvents.Phase.INSTANT */,
]);
let handlerState = 1 /* HandlerState.UNINITIALIZED */;
export function reset() {
navigationsByFrameId.clear();
navigationsByNavigationId.clear();
mainFrameNavigations.length = 0;
browserProcessId = Types.TraceEvents.ProcessID(-1);
browserThreadId = Types.TraceEvents.ThreadID(-1);
gpuProcessId = Types.TraceEvents.ProcessID(-1);
gpuThreadId = Types.TraceEvents.ThreadID(-1);
viewportRect = null;
topLevelRendererIds.clear();
threadsInProcess.clear();
rendererProcessesByFrameId.clear();
framesByProcessId.clear();
traceBounds.min = Types.Timing.MicroSeconds(Number.POSITIVE_INFINITY);
traceBounds.max = Types.Timing.MicroSeconds(Number.NEGATIVE_INFINITY);
traceBounds.range = Types.Timing.MicroSeconds(Number.POSITIVE_INFINITY);
traceStartedTime = Types.Timing.MicroSeconds(-1);
handlerState = 1 /* HandlerState.UNINITIALIZED */;
}
export function initialize() {
if (handlerState !== 1 /* HandlerState.UNINITIALIZED */) {
throw new Error('Meta Handler was not reset');
}
handlerState = 2 /* HandlerState.INITIALIZED */;
}
function updateRendererProcessByFrame(event, frame) {
const framesInProcessById = Platform.MapUtilities.getWithDefault(framesByProcessId, frame.processId, () => new Map());
framesInProcessById.set(frame.frame, frame);
const rendererProcessInFrame = Platform.MapUtilities.getWithDefault(rendererProcessesByFrameId, frame.frame, () => new Map());
const rendererProcessInfo = Platform.MapUtilities.getWithDefault(rendererProcessInFrame, frame.processId, () => {
return [];
});
const lastProcessData = rendererProcessInfo.at(-1);
// Only store a new entry if the URL changed, otherwise it's just
// redundant information.
if (lastProcessData && lastProcessData.frame.url === frame.url) {
return;
}
// For now we store the time of the event as the min. In the finalize we step
// through each of these windows and update their max and range values.
rendererProcessInfo.push({
frame,
window: {
min: event.ts,
max: Types.Timing.MicroSeconds(0),
range: Types.Timing.MicroSeconds(0),
},
});
}
export function handleEvent(event) {
if (handlerState !== 2 /* HandlerState.INITIALIZED */) {
throw new Error('Meta Handler is not initialized');
}
// If there is a timestamp (which meta events do not have), and the event does
// not end with ::UMA then it, and the event is in the set of valid phases,
// then it should be included for the purposes of calculating the trace bounds.
// The UMA events in particular seem to be reported on page unloading, which
// often extends the bounds of the trace unhelpfully.
if (event.ts !== 0 && !event.name.endsWith('::UMA') && eventPhasesOfInterestForTraceBounds.has(event.ph)) {
traceBounds.min = Types.Timing.MicroSeconds(Math.min(event.ts, traceBounds.min));
const eventDuration = event.dur || Types.Timing.MicroSeconds(0);
traceBounds.max = Types.Timing.MicroSeconds(Math.max(event.ts + eventDuration, traceBounds.max));
}
if (Types.TraceEvents.isProcessName(event) &&
(event.args.name === 'Browser' || event.args.name === 'HeadlessBrowser')) {
browserProcessId = event.pid;
return;
}
if (Types.TraceEvents.isProcessName(event) && (event.args.name === 'Gpu' || event.args.name === 'GPU Process')) {
gpuProcessId = event.pid;
return;
}
if (Types.TraceEvents.isThreadName(event) && event.args.name === 'CrGpuMain') {
gpuThreadId = event.tid;
return;
}
if (Types.TraceEvents.isThreadName(event) && event.args.name === 'CrBrowserMain') {
browserThreadId = event.tid;
}
if (Types.TraceEvents.isTraceEventMainFrameViewport(event) && viewportRect === null) {
const rectAsArray = event.args.data.viewport_rect;
const viewportX = rectAsArray[0];
const viewportY = rectAsArray[1];
const viewportWidth = rectAsArray[2];
const viewportHeight = rectAsArray[5];
viewportRect = new DOMRect(viewportX, viewportY, viewportWidth, viewportHeight);
}
// The TracingStartedInBrowser event includes the data on which frames are
// in scope at the start of the trace. We use this to identify the frame with
// no parent, i.e. the top level frame.
if (Types.TraceEvents.isTraceEventTracingStartedInBrowser(event)) {
traceStartedTime = event.ts;
if (!event.args.data) {
throw new Error('No frames found in trace data');
}
for (const frame of (event.args.data.frames ?? [])) {
updateRendererProcessByFrame(event, frame);
if (frame.parent) {
continue;
}
mainFrameId = frame.frame;
mainFrameURL = frame.url;
topLevelRendererIds.add(frame.processId);
}
return;
}
// FrameCommittedInBrowser events tell us information about each frame
// and we use these to track how long each individual renderer is active
// for. We track all renderers here (top level and those in frames), but
// for convenience we also populate a set of top level renderer IDs.
if (Types.TraceEvents.isTraceEventFrameCommittedInBrowser(event)) {
const frame = event.args.data;
if (!frame) {
return;
}
updateRendererProcessByFrame(event, frame);
if (frame.parent) {
return;
}
topLevelRendererIds.add(frame.processId);
return;
}
if (Types.TraceEvents.isTraceEventCommitLoad(event)) {
const frameData = event.args.data;
if (!frameData) {
return;
}
const { frame, name, url } = frameData;
updateRendererProcessByFrame(event, { processId: event.pid, frame, name, url });
return;
}
// Track all threads based on the process & thread IDs.
if (Types.TraceEvents.isThreadName(event)) {
const threads = Platform.MapUtilities.getWithDefault(threadsInProcess, event.pid, () => new Map());
threads.set(event.tid, event);
return;
}
// Track all navigation events. Note that there can be navigation start events
// but where the documentLoaderURL is empty. As far as the trace rendering is
// concerned, these events are noise so we filter them out here.
if (Types.TraceEvents.isTraceEventNavigationStartWithURL(event) && event.args.data) {
const navigationId = event.args.data.navigationId;
if (navigationsByNavigationId.has(navigationId)) {
throw new Error('Found multiple navigation start events with the same navigation ID.');
}
navigationsByNavigationId.set(navigationId, event);
const frameId = event.args.frame;
const existingFrameNavigations = navigationsByFrameId.get(frameId) || [];
existingFrameNavigations.push(event);
navigationsByFrameId.set(frameId, existingFrameNavigations);
if (frameId === mainFrameId) {
mainFrameNavigations.push(event);
}
return;
}
}
export async function finalize() {
if (handlerState !== 2 /* HandlerState.INITIALIZED */) {
throw new Error('Handler is not initialized');
}
traceBounds.min = traceStartedTime;
traceBounds.range = Types.Timing.MicroSeconds(traceBounds.max - traceBounds.min);
// If we go from foo.com to example.com we will get a new renderer, and
// therefore the "top level renderer" will have a different PID as it has
// changed. Here we step through each renderer process and updated its window
// bounds, such that we end up with the time ranges in the trace for when
// each particular renderer started and stopped being the main renderer
// process.
for (const [, processWindows] of rendererProcessesByFrameId) {
const processWindowValues = [...processWindows.values()].flat();
for (let i = 0; i < processWindowValues.length; i++) {
const currentWindow = processWindowValues[i];
const nextWindow = processWindowValues[i + 1];
// For the last window we set its max to be positive infinity.
// TODO: Move the trace bounds handler into meta so we can clamp first and last windows.
if (!nextWindow) {
currentWindow.window.max = Types.Timing.MicroSeconds(traceBounds.max);
currentWindow.window.range = Types.Timing.MicroSeconds(traceBounds.max - currentWindow.window.min);
}
else {
currentWindow.window.max = Types.Timing.MicroSeconds(nextWindow.window.min - 1);
currentWindow.window.range = Types.Timing.MicroSeconds(currentWindow.window.max - currentWindow.window.min);
}
}
}
// Frame ids which we didn't register using either the TracingStartedInBrowser or
// the FrameCommittedInBrowser events are considered noise, so we filter them out, as well
// as the navigations that belong to such frames.
for (const [frameId, navigations] of navigationsByFrameId) {
// The frames in the rendererProcessesByFrameId map come only from the
// TracingStartedInBrowser and FrameCommittedInBrowser events, so we can use it as point
// of comparison to determine if a frameId should be discarded.
if (rendererProcessesByFrameId.has(frameId)) {
continue;
}
navigationsByFrameId.delete(frameId);
for (const navigation of navigations) {
if (!navigation.args.data) {
continue;
}
navigationsByNavigationId.delete(navigation.args.data.navigationId);
}
}
handlerState = 3 /* HandlerState.FINALIZED */;
}
export function data() {
if (handlerState !== 3 /* HandlerState.FINALIZED */) {
throw new Error('Meta Handler is not finalized');
}
return {
traceBounds: { ...traceBounds },
browserProcessId,
browserThreadId,
gpuProcessId,
gpuThreadId: gpuThreadId === Types.TraceEvents.ThreadID(-1) ? undefined : gpuThreadId,
viewportRect: viewportRect || undefined,
mainFrameId,
mainFrameURL,
navigationsByFrameId: new Map(navigationsByFrameId),
navigationsByNavigationId: new Map(navigationsByNavigationId),
threadsInProcess: new Map(threadsInProcess),
rendererProcessesByFrame: new Map(rendererProcessesByFrameId),
topLevelRendererIds: new Set(topLevelRendererIds),
frameByProcessId: new Map(framesByProcessId),
mainFrameNavigations: [...mainFrameNavigations],
};
}
//# sourceMappingURL=MetaHandler.js.map