Next.js website for Rocky Mountain Vending company featuring: - Product catalog with Stripe integration - Service areas and parts pages - Admin dashboard with Clerk authentication - SEO optimized pages with JSON-LD structured data Co-authored-by: Cursor <cursoragent@cursor.com>
347 lines
No EOL
15 KiB
Text
347 lines
No EOL
15 KiB
Text
// 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 { data as auctionWorkletsData } from './AuctionWorkletsHandler.js';
|
|
import * as HandlerHelpers from './helpers.js';
|
|
import { data as metaHandlerData } from './MetaHandler.js';
|
|
import { data as networkRequestHandlerData } from './NetworkRequestsHandler.js';
|
|
import { data as samplesHandlerData } from './SamplesHandler.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();
|
|
let entityMappings = {
|
|
eventsByEntity: new Map(),
|
|
entityByEvent: new Map(),
|
|
createdEntityCache: new Map(),
|
|
entityByUrlCache: 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 completeEventStack = [];
|
|
let config = Types.Configuration.defaults();
|
|
const makeRendererProcess = () => ({
|
|
url: null,
|
|
isOnMainFrame: false,
|
|
threads: new Map(),
|
|
});
|
|
const makeRendererThread = () => ({
|
|
name: null,
|
|
entries: [],
|
|
profileCalls: [],
|
|
layoutEvents: [],
|
|
updateLayoutTreeEvents: [],
|
|
});
|
|
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 handleUserConfig(userConfig) {
|
|
config = userConfig;
|
|
}
|
|
export function reset() {
|
|
processes.clear();
|
|
entryToNode.clear();
|
|
entityMappings.eventsByEntity.clear();
|
|
entityMappings.entityByEvent.clear();
|
|
entityMappings.createdEntityCache.clear();
|
|
entityMappings.entityByUrlCache.clear();
|
|
completeEventStack.length = 0;
|
|
compositorTileWorkers.length = 0;
|
|
}
|
|
export function handleEvent(event) {
|
|
if (Types.Events.isThreadName(event) && event.args.name?.startsWith('CompositorTileWorker')) {
|
|
compositorTileWorkers.push({
|
|
pid: event.pid,
|
|
tid: event.tid,
|
|
});
|
|
}
|
|
if (Types.Events.isBegin(event) || Types.Events.isEnd(event)) {
|
|
const process = getOrCreateRendererProcess(processes, event.pid);
|
|
const thread = getOrCreateRendererThread(process, event.tid);
|
|
const completeEvent = makeCompleteEvent(event);
|
|
if (!completeEvent) {
|
|
return;
|
|
}
|
|
thread.entries.push(completeEvent);
|
|
return;
|
|
}
|
|
if (Types.Events.isInstant(event) || Types.Events.isComplete(event)) {
|
|
const process = getOrCreateRendererProcess(processes, event.pid);
|
|
const thread = getOrCreateRendererThread(process, event.tid);
|
|
thread.entries.push(event);
|
|
}
|
|
if (Types.Events.isLayout(event)) {
|
|
const process = getOrCreateRendererProcess(processes, event.pid);
|
|
const thread = getOrCreateRendererThread(process, event.tid);
|
|
thread.layoutEvents.push(event);
|
|
}
|
|
if (Types.Events.isUpdateLayoutTree(event)) {
|
|
const process = getOrCreateRendererProcess(processes, event.pid);
|
|
const thread = getOrCreateRendererThread(process, event.tid);
|
|
thread.updateLayoutTreeEvents.push(event);
|
|
}
|
|
}
|
|
export async function finalize() {
|
|
const { mainFrameId, rendererProcessesByFrame, threadsInProcess } = metaHandlerData();
|
|
entityMappings = networkRequestHandlerData().entityMappings;
|
|
assignMeta(processes, mainFrameId, rendererProcessesByFrame, threadsInProcess);
|
|
sanitizeProcesses(processes);
|
|
buildHierarchy(processes);
|
|
sanitizeThreads(processes);
|
|
}
|
|
export function data() {
|
|
return {
|
|
processes,
|
|
compositorTileWorkers: gatherCompositorThreads(),
|
|
entryToNode,
|
|
// We only shallow clone the data in the processor, so these nested
|
|
// values need to be manually cloned.
|
|
entityMappings: {
|
|
entityByEvent: new Map(entityMappings.entityByEvent),
|
|
eventsByEntity: new Map(entityMappings.eventsByEntity),
|
|
createdEntityCache: new Map(entityMappings.createdEntityCache),
|
|
entityByUrlCache: new Map(entityMappings.entityByUrlCache),
|
|
},
|
|
};
|
|
}
|
|
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, 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 {
|
|
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, threadsInProcess) {
|
|
for (const [pid, process] of processes) {
|
|
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 unknown origin.
|
|
*/
|
|
export function sanitizeProcesses(processes) {
|
|
const auctionWorklets = auctionWorkletsData().worklets;
|
|
const metaData = metaHandlerData();
|
|
if (metaData.traceIsGeneric) {
|
|
return;
|
|
}
|
|
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).
|
|
//
|
|
// There is one exception; AuctionWorklet processes get parsed in a
|
|
// separate handler, so at this point we check to see if the process has
|
|
// been found by the AuctionWorkletsHandler, and if so we update the URL.
|
|
// This ensures that we keep this process around and do not drop it due to
|
|
// the lack of a URL.
|
|
if (process.url === null) {
|
|
const maybeWorklet = auctionWorklets.get(pid);
|
|
if (maybeWorklet) {
|
|
process.url = maybeWorklet.host;
|
|
}
|
|
else {
|
|
processes.delete(pid);
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* 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) {
|
|
const samplesData = samplesHandlerData();
|
|
for (const [pid, process] of processes) {
|
|
for (const [tid, thread] of process.threads) {
|
|
if (!thread.entries.length) {
|
|
thread.tree = Helpers.TreeHelpers.makeEmptyTraceEntryTree();
|
|
continue;
|
|
}
|
|
// Step 1. Massage the data.
|
|
Helpers.Trace.sortTraceEventsInPlace(thread.entries);
|
|
// Step 2. Inject profile calls from samples
|
|
const samplesDataForThread = samplesData.profilesInProcess.get(pid)?.get(tid);
|
|
if (samplesDataForThread) {
|
|
const cpuProfile = samplesDataForThread.parsedProfile;
|
|
const samplesIntegrator = cpuProfile &&
|
|
new Helpers.SamplesIntegrator.SamplesIntegrator(cpuProfile, samplesDataForThread.profileId, pid, tid, config);
|
|
const profileCalls = samplesIntegrator?.buildProfileCalls(thread.entries);
|
|
if (samplesIntegrator && profileCalls) {
|
|
thread.entries = Helpers.Trace.mergeEventsInOrder(thread.entries, profileCalls);
|
|
thread.profileCalls = profileCalls;
|
|
// We'll also inject the instant JSSample events (in debug mode only)
|
|
const jsSamples = samplesIntegrator.jsSampleEvents;
|
|
if (jsSamples.length) {
|
|
thread.entries = Helpers.Trace.mergeEventsInOrder(thread.entries, jsSamples);
|
|
}
|
|
}
|
|
}
|
|
// Step 3. Build the tree.
|
|
const treeData = Helpers.TreeHelpers.treify(thread.entries, options);
|
|
thread.tree = treeData.tree;
|
|
// Update the entryToNode map with the entries from this thread
|
|
for (const [entry, node] of treeData.entryToNode) {
|
|
entryToNode.set(entry, node);
|
|
// Entity mapping is unrelated to the tree, but calling here as we need to call on every node anyway.
|
|
HandlerHelpers.addEventToEntityMapping(entry, entityMappings);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
export function makeCompleteEvent(event) {
|
|
if (Types.Events.isEnd(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.Micro(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: Types.Events.Phase.COMPLETE,
|
|
dur: Types.Timing.Micro(0),
|
|
};
|
|
completeEventStack.push(syntheticComplete);
|
|
return syntheticComplete;
|
|
}
|
|
export function deps() {
|
|
return ['Meta', 'Samples', 'AuctionWorklets', 'NetworkRequests'];
|
|
}
|
|
//# sourceMappingURL=RendererHandler.js.map |