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>
591 lines
22 KiB
Text
591 lines
22 KiB
Text
/**
|
|
* @license
|
|
* Copyright 2020 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import {MainThreadTasks} from './main-thread-tasks.js';
|
|
|
|
const SAMPLER_TRACE_EVENT_NAME = 'FunctionCall-SynthesizedByProfilerModel';
|
|
|
|
/**
|
|
* @fileoverview
|
|
*
|
|
* This model converts the `Profile` and `ProfileChunk` mega trace events from the `disabled-by-default-v8.cpu_profiler`
|
|
* category into B/E-style trace events that main-thread-tasks.js already knows how to parse into a task tree.
|
|
*
|
|
* The V8 CPU profiler measures where time is being spent by sampling the stack (See https://www.jetbrains.com/help/profiler/Profiling_Guidelines__Choosing_the_Right_Profiling_Mode.html
|
|
* for a generic description of the differences between tracing and sampling).
|
|
*
|
|
* A `Profile` event is a record of the stack that was being executed at different sample points in time.
|
|
* It has a structure like this:
|
|
*
|
|
* nodes: [function A, function B, function C]
|
|
* samples: [node with id 2, node with id 1, ...]
|
|
* timeDeltas: [4125μs since last sample, 121μs since last sample, ...]
|
|
*
|
|
* Note that this is subtly different from the protocol-based Crdp.Profiler.Profile type.
|
|
*
|
|
* Helpful prior art:
|
|
* @see https://cs.chromium.org/chromium/src/third_party/devtools-frontend/src/front_end/sdk/CPUProfileDataModel.js?sq=package:chromium&g=0&l=42
|
|
* @see https://github.com/v8/v8/blob/99ca333b0efba3236954b823101315aefeac51ab/tools/profile.js
|
|
* @see https://github.com/jlfwong/speedscope/blob/9ed1eb192cb7e9dac43a5f25bd101af169dc654a/src/import/chrome.ts#L200
|
|
*/
|
|
|
|
/**
|
|
* @typedef CpuProfile
|
|
* @property {string} id
|
|
* @property {number} pid
|
|
* @property {number} tid
|
|
* @property {number} startTime
|
|
* @property {Required<LH.TraceCpuProfile>['nodes']} nodes
|
|
* @property {Array<number>} samples
|
|
* @property {Array<number>} timeDeltas
|
|
*/
|
|
|
|
/** @typedef {Required<Required<LH.TraceEvent['args']>['data']>['_syntheticProfilerRange']} ProfilerRange */
|
|
/** @typedef {LH.TraceEvent & {args: {data: {_syntheticProfilerRange: ProfilerRange}}}} SynthethicEvent */
|
|
/** @typedef {Omit<LH.Artifacts.TaskNode, 'event'> & {event: SynthethicEvent, endEvent: SynthethicEvent}} SynthethicTaskNode */
|
|
|
|
class CpuProfileModel {
|
|
/**
|
|
* @param {CpuProfile} profile
|
|
*/
|
|
constructor(profile) {
|
|
this._profile = profile;
|
|
this._nodesById = this._createNodeMap();
|
|
this._activeNodeArraysById = this._createActiveNodeArrays();
|
|
}
|
|
|
|
/**
|
|
* Initialization function to enable O(1) access to nodes by node ID.
|
|
* @return {Map<number, CpuProfile['nodes'][0]>}
|
|
*/
|
|
_createNodeMap() {
|
|
/** @type {Map<number, CpuProfile['nodes'][0]>} */
|
|
const map = new Map();
|
|
for (const node of this._profile.nodes) {
|
|
map.set(node.id, node);
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
/**
|
|
* Initialization function to enable O(1) access to the set of active nodes in the stack by node ID.
|
|
* @return {Map<number, Array<number>>}
|
|
*/
|
|
_createActiveNodeArrays() {
|
|
/** @type {Map<number, Array<number>>} */
|
|
const map = new Map();
|
|
/** @param {number} id @return {Array<number>} */
|
|
const getActiveNodes = id => {
|
|
if (map.has(id)) return map.get(id) || [];
|
|
|
|
const node = this._nodesById.get(id);
|
|
if (!node) throw new Error(`No such node ${id}`);
|
|
if (typeof node.parent === 'number') {
|
|
const array = getActiveNodes(node.parent).concat([id]);
|
|
map.set(id, array);
|
|
return array;
|
|
} else {
|
|
return [id];
|
|
}
|
|
};
|
|
|
|
for (const node of this._profile.nodes) {
|
|
map.set(node.id, getActiveNodes(node.id));
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
/**
|
|
* Returns all the node IDs in a stack when a specific nodeId is at the top of the stack
|
|
* (i.e. a stack's node ID and the node ID of all of its parents).
|
|
*
|
|
* @param {number} nodeId
|
|
* @return {Array<number>}
|
|
*/
|
|
_getActiveNodeIds(nodeId) {
|
|
const activeNodeIds = this._activeNodeArraysById.get(nodeId);
|
|
if (!activeNodeIds) throw new Error(`No such node ID ${nodeId}`);
|
|
return activeNodeIds;
|
|
}
|
|
|
|
/**
|
|
* Generates the necessary B/E-style trace events for a single transition from stack A to stack B
|
|
* at the given latest timestamp (includes possible range in event.args.data).
|
|
*
|
|
* Example:
|
|
*
|
|
* latestPossibleTimestamp 1234
|
|
* previousNodeIds 1,2,3
|
|
* currentNodeIds 1,2,4
|
|
*
|
|
* yields [end 3 at ts 1234, begin 4 at ts 1234]
|
|
*
|
|
* @param {number} earliestPossibleTimestamp
|
|
* @param {number} latestPossibleTimestamp
|
|
* @param {Array<number>} previousNodeIds
|
|
* @param {Array<number>} currentNodeIds
|
|
* @return {Array<SynthethicEvent>}
|
|
*/
|
|
_synthesizeTraceEventsForTransition(
|
|
earliestPossibleTimestamp,
|
|
latestPossibleTimestamp,
|
|
previousNodeIds,
|
|
currentNodeIds
|
|
) {
|
|
const startNodes = currentNodeIds
|
|
.filter(id => !previousNodeIds.includes(id))
|
|
.map(id => this._nodesById.get(id))
|
|
.filter(node => !!node);
|
|
const endNodes = previousNodeIds
|
|
.filter(id => !currentNodeIds.includes(id))
|
|
.map(id => this._nodesById.get(id))
|
|
.filter(node => !!node);
|
|
|
|
/** @param {CpuProfile['nodes'][0]} node @return {SynthethicEvent} */
|
|
const createSyntheticEvent = node => ({
|
|
ts: Number.isFinite(latestPossibleTimestamp)
|
|
? latestPossibleTimestamp
|
|
: earliestPossibleTimestamp,
|
|
pid: this._profile.pid,
|
|
tid: this._profile.tid,
|
|
dur: 0,
|
|
ph: 'I',
|
|
// This trace event name is Lighthouse-specific and wouldn't be found in a real trace.
|
|
// Attribution logic in main-thread-tasks.js special cases this event.
|
|
name: SAMPLER_TRACE_EVENT_NAME,
|
|
cat: 'lighthouse',
|
|
args: {
|
|
data: {
|
|
callFrame: node.callFrame,
|
|
_syntheticProfilerRange: {earliestPossibleTimestamp, latestPossibleTimestamp},
|
|
},
|
|
},
|
|
});
|
|
|
|
/** @type {Array<SynthethicEvent>} */
|
|
const startEvents = startNodes.map(createSyntheticEvent).map(evt => ({...evt, ph: 'B'}));
|
|
/** @type {Array<SynthethicEvent>} */
|
|
const endEvents = endNodes.map(createSyntheticEvent).map(evt => ({...evt, ph: 'E'}));
|
|
// Ensure we put end events in first to finish prior tasks before starting new ones.
|
|
return [...endEvents.reverse(), ...startEvents];
|
|
}
|
|
|
|
/**
|
|
* @param {LH.TraceEvent | undefined} event
|
|
* @return {event is SynthethicEvent}
|
|
*/
|
|
static isSyntheticEvent(event) {
|
|
if (!event) return false;
|
|
return Boolean(
|
|
event.name === SAMPLER_TRACE_EVENT_NAME &&
|
|
event.args.data?._syntheticProfilerRange
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {LH.Artifacts.TaskNode} task
|
|
* @return {task is SynthethicTaskNode}
|
|
*/
|
|
static isSyntheticTask(task) {
|
|
return CpuProfileModel.isSyntheticEvent(task.event) &&
|
|
CpuProfileModel.isSyntheticEvent(task.endEvent);
|
|
}
|
|
|
|
/**
|
|
* Finds all the tasks that started or ended (depending on `type`) within the provided time range.
|
|
* Uses a memory index to remember the place in the array the last invocation left off to avoid
|
|
* re-traversing the entire array, but note that this index might still be slightly off from the
|
|
* true start position.
|
|
*
|
|
* @param {Array<{startTime: number, endTime: number}>} knownTasks
|
|
* @param {{type: 'startTime'|'endTime', initialIndex: number, earliestPossibleTimestamp: number, latestPossibleTimestamp: number}} options
|
|
*/
|
|
static _getTasksInRange(knownTasks, options) {
|
|
const {type, initialIndex, earliestPossibleTimestamp, latestPossibleTimestamp} = options;
|
|
|
|
// We may have overshot a little from last time, so back up to find the real starting index.
|
|
let startIndex = initialIndex;
|
|
while (startIndex > 0) {
|
|
const task = knownTasks[startIndex];
|
|
if (task && task[type] < earliestPossibleTimestamp) break;
|
|
startIndex--;
|
|
}
|
|
|
|
/** @type {Array<{startTime: number, endTime: number}>} */
|
|
const matchingTasks = [];
|
|
for (let i = startIndex; i < knownTasks.length; i++) {
|
|
const task = knownTasks[i];
|
|
// Task is before our range of interest, keep looping.
|
|
if (task[type] < earliestPossibleTimestamp) continue;
|
|
|
|
// Task is after our range of interest, we're done.
|
|
if (task[type] > latestPossibleTimestamp) {
|
|
return {tasks: matchingTasks, lastIndex: i};
|
|
}
|
|
|
|
// Task is in our range of interest, add it to our list.
|
|
matchingTasks.push(task);
|
|
}
|
|
|
|
// We went through all tasks before reaching the end of our range.
|
|
return {tasks: matchingTasks, lastIndex: knownTasks.length};
|
|
}
|
|
|
|
/**
|
|
* Given a particular time range and a set of known true tasks, find the correct timestamp to use
|
|
* for a transition between tasks.
|
|
*
|
|
* Because the sampling profiler only provides a *range* of start/stop function boundaries, this
|
|
* method uses knowledge of a known set of tasks to find the most accurate timestamp for a particular
|
|
* range. For example, if we know that a function ended between 800ms and 810ms, we can use the
|
|
* knowledge that a toplevel task ended at 807ms to use 807ms as the correct endtime for this function.
|
|
*
|
|
* @param {{syntheticTask: SynthethicTaskNode, eventType: 'start'|'end', allEventsAtTs: {naive: Array<SynthethicEvent>, refined: Array<SynthethicEvent>}, knownTaskStartTimeIndex: number, knownTaskEndTimeIndex: number, knownTasksByStartTime: Array<{startTime: number, endTime: number}>, knownTasksByEndTime: Array<{startTime: number, endTime: number}>}} data
|
|
* @return {{timestamp: number, lastStartTimeIndex: number, lastEndTimeIndex: number}}
|
|
*/
|
|
static _findEffectiveTimestamp(data) {
|
|
const {
|
|
eventType,
|
|
syntheticTask,
|
|
allEventsAtTs,
|
|
knownTasksByStartTime,
|
|
knownTaskStartTimeIndex,
|
|
knownTasksByEndTime,
|
|
knownTaskEndTimeIndex,
|
|
} = data;
|
|
|
|
const targetEvent = eventType === 'start' ? syntheticTask.event : syntheticTask.endEvent;
|
|
const pairEvent = eventType === 'start' ? syntheticTask.endEvent : syntheticTask.event;
|
|
|
|
const timeRange = targetEvent.args.data._syntheticProfilerRange;
|
|
const pairTimeRange = pairEvent.args.data._syntheticProfilerRange;
|
|
|
|
const {tasks: knownTasksStarting, lastIndex: lastStartTimeIndex} = this._getTasksInRange(
|
|
knownTasksByStartTime,
|
|
{
|
|
type: 'startTime',
|
|
initialIndex: knownTaskStartTimeIndex,
|
|
earliestPossibleTimestamp: timeRange.earliestPossibleTimestamp,
|
|
latestPossibleTimestamp: timeRange.latestPossibleTimestamp,
|
|
}
|
|
);
|
|
|
|
const {tasks: knownTasksEnding, lastIndex: lastEndTimeIndex} = this._getTasksInRange(
|
|
knownTasksByEndTime,
|
|
{
|
|
type: 'endTime',
|
|
initialIndex: knownTaskEndTimeIndex,
|
|
earliestPossibleTimestamp: timeRange.earliestPossibleTimestamp,
|
|
latestPossibleTimestamp: timeRange.latestPossibleTimestamp,
|
|
}
|
|
);
|
|
|
|
// First, find all the tasks that span *across* (not fully contained within) our ambiguous range.
|
|
const knownTasksStartingNotContained = knownTasksStarting
|
|
.filter(t => !knownTasksEnding.includes(t));
|
|
const knownTasksEndingNotContained = knownTasksEnding
|
|
.filter(t => !knownTasksStarting.includes(t));
|
|
|
|
// Each one of these spanning tasks can be in one of three situations:
|
|
// - Task is a parent of the sample.
|
|
// - Task is a child of the sample.
|
|
// - Task has no overlap with the sample.
|
|
|
|
// Parent tasks must satisfy...
|
|
// parentTask.startTime <= syntheticTask.startTime
|
|
// AND
|
|
// syntheticTask.endTime <= parentTask.endTime
|
|
const parentTasks =
|
|
eventType === 'start'
|
|
? knownTasksStartingNotContained.filter(
|
|
t => t.endTime >= pairTimeRange.earliestPossibleTimestamp
|
|
)
|
|
: knownTasksEndingNotContained.filter(
|
|
t => t.startTime <= pairTimeRange.latestPossibleTimestamp
|
|
);
|
|
|
|
// Child tasks must satisfy...
|
|
// syntheticTask.startTime <= childTask.startTime
|
|
// AND
|
|
// childTask.endTime <= syntheticTask.endTime
|
|
const childTasks =
|
|
eventType === 'start'
|
|
? knownTasksStartingNotContained.filter(
|
|
t => t.endTime < pairTimeRange.earliestPossibleTimestamp
|
|
)
|
|
: knownTasksEndingNotContained.filter(
|
|
t => t.startTime > pairTimeRange.latestPossibleTimestamp
|
|
);
|
|
|
|
// Unrelated tasks must satisfy...
|
|
// unrelatedTask.endTime <= syntheticTask.startTime
|
|
// OR
|
|
// syntheticTask.endTime <= unrelatedTask.startTime
|
|
const unrelatedTasks =
|
|
eventType === 'start' ? knownTasksEndingNotContained : knownTasksStartingNotContained;
|
|
|
|
// Now we narrow our allowable range using the three types of tasks and the other events
|
|
// that we've already refined.
|
|
const minimumTs = Math.max(
|
|
// Sampled event couldn't be earlier than this to begin with.
|
|
timeRange.earliestPossibleTimestamp,
|
|
// Sampled start event can't be before its parent started.
|
|
// Sampled end event can't be before its child ended.
|
|
...(eventType === 'start'
|
|
? parentTasks.map(t => t.startTime)
|
|
: childTasks.map(t => t.endTime)),
|
|
// Sampled start event can't be before unrelated tasks ended.
|
|
...(eventType === 'start' ? unrelatedTasks.map(t => t.endTime) : []),
|
|
// Sampled start event can't be before the other `E` events at its same timestamp.
|
|
...(eventType === 'start'
|
|
? allEventsAtTs.refined.filter(e => e.ph === 'E').map(e => e.ts)
|
|
: [])
|
|
);
|
|
|
|
const maximumTs = Math.min(
|
|
// Sampled event couldn't be later than this to begin with.
|
|
timeRange.latestPossibleTimestamp,
|
|
// Sampled start event can't be after its child started.
|
|
// Sampled end event can't be after its parent ended.
|
|
...(eventType === 'start'
|
|
? childTasks.map(t => t.startTime)
|
|
: parentTasks.map(t => t.endTime)),
|
|
// Sampled end event can't be after unrelated tasks started.
|
|
...(eventType === 'start' ? [] : unrelatedTasks.map(t => t.startTime)),
|
|
// Sampled end event can't be after the other `B` events at its same timestamp.
|
|
// This is _currently_ only possible in contrived scenarios due to the sorted order of processing,
|
|
// but it's a non-obvious observation and case to account for.
|
|
...(eventType === 'start'
|
|
? []
|
|
: allEventsAtTs.refined.filter(e => e.ph === 'B').map(e => e.ts))
|
|
);
|
|
|
|
// We want to maximize the size of the sampling tasks within our constraints, so we'll pick
|
|
// the _earliest_ possible time for start events and the _latest_ possible time for end events.
|
|
const effectiveTimestamp =
|
|
(eventType === 'start' && Number.isFinite(minimumTs)) || !Number.isFinite(maximumTs)
|
|
? minimumTs
|
|
: maximumTs;
|
|
|
|
return {timestamp: effectiveTimestamp, lastStartTimeIndex, lastEndTimeIndex};
|
|
}
|
|
|
|
/**
|
|
* Creates the B/E-style trace events using only data from the profile itself. Each B/E event will
|
|
* include the actual _range_ the timestamp could have been in its metadata that is used for
|
|
* refinement later.
|
|
*
|
|
* @return {Array<SynthethicEvent>}
|
|
*/
|
|
_synthesizeNaiveTraceEvents() {
|
|
const profile = this._profile;
|
|
const length = profile.samples.length;
|
|
if (profile.timeDeltas.length !== length) throw new Error(`Invalid CPU profile length`);
|
|
|
|
/** @type {Array<SynthethicEvent>} */
|
|
const events = [];
|
|
|
|
let currentProfilerTimestamp = profile.startTime;
|
|
let earliestPossibleTimestamp = -Infinity;
|
|
|
|
/** @type {Array<number>} */
|
|
let lastActiveNodeIds = [];
|
|
for (let i = 0; i < profile.samples.length; i++) {
|
|
const nodeId = profile.samples[i];
|
|
const timeDelta = Math.max(profile.timeDeltas[i], 1);
|
|
const node = this._nodesById.get(nodeId);
|
|
if (!node) throw new Error(`Missing node ${nodeId}`);
|
|
|
|
currentProfilerTimestamp += timeDelta;
|
|
|
|
const activeNodeIds = this._getActiveNodeIds(nodeId);
|
|
events.push(
|
|
...this._synthesizeTraceEventsForTransition(
|
|
earliestPossibleTimestamp,
|
|
currentProfilerTimestamp,
|
|
lastActiveNodeIds,
|
|
activeNodeIds
|
|
)
|
|
);
|
|
|
|
earliestPossibleTimestamp = currentProfilerTimestamp;
|
|
lastActiveNodeIds = activeNodeIds;
|
|
}
|
|
|
|
events.push(
|
|
...this._synthesizeTraceEventsForTransition(
|
|
currentProfilerTimestamp,
|
|
Infinity,
|
|
lastActiveNodeIds,
|
|
[]
|
|
)
|
|
);
|
|
|
|
return events;
|
|
}
|
|
|
|
/**
|
|
* Creates a copy of B/E-style trace events with refined timestamps using knowledge from the
|
|
* tasks that have definitive timestamps.
|
|
*
|
|
* With the sampling profiler we know that a function started/ended _sometime between_ two points,
|
|
* but not exactly when. Using the information from other tasks gives us more information to be
|
|
* more precise with timings and allows us to create a valid task tree later on.
|
|
*
|
|
* @param {Array<{startTime: number, endTime: number}>} knownTasks
|
|
* @param {Array<SynthethicTaskNode>} syntheticTasks
|
|
* @param {Array<SynthethicEvent>} syntheticEvents
|
|
* @return {Array<SynthethicEvent>}
|
|
*/
|
|
_refineTraceEventsWithTasks(knownTasks, syntheticTasks, syntheticEvents) {
|
|
/** @type {Array<SynthethicEvent>} */
|
|
const refinedEvents = [];
|
|
|
|
/** @type {Map<number, {naive: Array<SynthethicEvent>, refined: Array<SynthethicEvent>}>} */
|
|
const syntheticEventsByTs = new Map();
|
|
for (const event of syntheticEvents) {
|
|
const group = syntheticEventsByTs.get(event.ts) || {naive: [], refined: []};
|
|
group.naive.push(event);
|
|
syntheticEventsByTs.set(event.ts, group);
|
|
}
|
|
|
|
/** @type {Map<SynthethicEvent, SynthethicTaskNode>} */
|
|
const syntheticTasksByEvent = new Map();
|
|
for (const task of syntheticTasks) {
|
|
syntheticTasksByEvent.set(task.event, task);
|
|
syntheticTasksByEvent.set(task.endEvent, task);
|
|
}
|
|
|
|
const knownTasksByStartTime = knownTasks.slice().sort((a, b) => a.startTime - b.startTime);
|
|
const knownTasksByEndTime = knownTasks.slice().sort((a, b) => a.endTime - b.endTime);
|
|
|
|
let knownTaskStartTimeIndex = 0;
|
|
let knownTaskEndTimeIndex = 0;
|
|
|
|
for (const event of syntheticEvents) {
|
|
const syntheticTask = syntheticTasksByEvent.get(event);
|
|
if (!syntheticTask) throw new Error('Impossible - all events have a task');
|
|
const allEventsAtTs = syntheticEventsByTs.get(event.ts);
|
|
if (!allEventsAtTs) throw new Error('Impossible - we just mapped every event');
|
|
|
|
const effectiveTimestampData = CpuProfileModel._findEffectiveTimestamp({
|
|
eventType: event.ph === 'B' ? 'start' : 'end',
|
|
syntheticTask,
|
|
allEventsAtTs,
|
|
knownTaskStartTimeIndex,
|
|
knownTaskEndTimeIndex,
|
|
knownTasksByStartTime,
|
|
knownTasksByEndTime,
|
|
});
|
|
|
|
knownTaskStartTimeIndex = effectiveTimestampData.lastStartTimeIndex;
|
|
knownTaskEndTimeIndex = effectiveTimestampData.lastEndTimeIndex;
|
|
|
|
const refinedEvent = {...event, ts: effectiveTimestampData.timestamp};
|
|
refinedEvents.push(refinedEvent);
|
|
allEventsAtTs.refined.push(refinedEvent);
|
|
}
|
|
|
|
return refinedEvents;
|
|
}
|
|
|
|
/**
|
|
* Creates B/E-style trace events from a CpuProfile object created by `collectProfileEvents()`.
|
|
* An optional set of tasks can be passed in to refine the start/end times.
|
|
*
|
|
* @param {Array<LH.Artifacts.TaskNode>} [knownTaskNodes]
|
|
* @return {Array<LH.TraceEvent>}
|
|
*/
|
|
synthesizeTraceEvents(knownTaskNodes = []) {
|
|
const naiveEvents = this._synthesizeNaiveTraceEvents();
|
|
if (!naiveEvents.length) return [];
|
|
|
|
let finalEvents = naiveEvents;
|
|
if (knownTaskNodes.length) {
|
|
// If we have task information, put the times back into raw trace event ts scale.
|
|
/** @type {(baseTs: number) => (node: LH.Artifacts.TaskNode) => LH.Artifacts.TaskNode} */
|
|
const rebaseTaskTime = baseTs => node => ({
|
|
...node,
|
|
startTime: baseTs + node.startTime * 1000,
|
|
endTime: baseTs + node.endTime * 1000,
|
|
duration: node.duration * 1000,
|
|
});
|
|
|
|
// The first task node might not be time 0, so recompute the baseTs.
|
|
const baseTs = knownTaskNodes[0].event.ts - knownTaskNodes[0].startTime * 1000;
|
|
const knownTasks = knownTaskNodes.map(rebaseTaskTime(baseTs));
|
|
|
|
// We'll also create tasks for our naive events so we have the B/E pairs readily available.
|
|
const naiveProfilerTasks = MainThreadTasks.getMainThreadTasks(naiveEvents, [], Infinity)
|
|
.map(rebaseTaskTime(naiveEvents[0].ts))
|
|
.filter(CpuProfileModel.isSyntheticTask);
|
|
if (!naiveProfilerTasks.length) throw new Error('Failed to create naive profiler tasks');
|
|
|
|
finalEvents = this._refineTraceEventsWithTasks(knownTasks, naiveProfilerTasks, naiveEvents);
|
|
}
|
|
|
|
return finalEvents;
|
|
}
|
|
|
|
/**
|
|
* Creates B/E-style trace events from a CpuProfile object created by `collectProfileEvents()`
|
|
*
|
|
* @param {CpuProfile} profile
|
|
* @param {Array<LH.Artifacts.TaskNode>} tasks
|
|
* @return {Array<LH.TraceEvent>}
|
|
*/
|
|
static synthesizeTraceEvents(profile, tasks) {
|
|
const model = new CpuProfileModel(profile);
|
|
return model.synthesizeTraceEvents(tasks);
|
|
}
|
|
|
|
/**
|
|
* Merges the data of all the `ProfileChunk` trace events into a single CpuProfile object for consumption
|
|
* by `synthesizeTraceEvents()`.
|
|
*
|
|
* @param {Array<LH.TraceEvent>} traceEvents
|
|
* @return {Array<CpuProfile>}
|
|
*/
|
|
static collectProfileEvents(traceEvents) {
|
|
/** @type {Map<string, CpuProfile>} */
|
|
const profiles = new Map();
|
|
for (const event of traceEvents) {
|
|
if (event.name !== 'Profile' && event.name !== 'ProfileChunk') continue;
|
|
if (typeof event.id !== 'string') continue;
|
|
|
|
// `Profile` or `ProfileChunk` can partially define these across multiple events.
|
|
// We'll fallback to empty values and worry about validation in the `synthesizeTraceEvents` phase.
|
|
const cpuProfileArg = event.args.data?.cpuProfile || {};
|
|
const timeDeltas = event.args.data?.timeDeltas || cpuProfileArg.timeDeltas;
|
|
let profile = profiles.get(event.id);
|
|
|
|
if (event.name === 'Profile') {
|
|
profile = {
|
|
id: event.id,
|
|
pid: event.pid,
|
|
tid: event.tid,
|
|
startTime: event.args.data?.startTime || event.ts,
|
|
nodes: cpuProfileArg.nodes || [],
|
|
samples: cpuProfileArg.samples || [],
|
|
timeDeltas: timeDeltas || [],
|
|
};
|
|
} else {
|
|
if (!profile) continue;
|
|
profile.nodes.push(...(cpuProfileArg.nodes || []));
|
|
profile.samples.push(...(cpuProfileArg.samples || []));
|
|
profile.timeDeltas.push(...(timeDeltas || []));
|
|
}
|
|
|
|
profiles.set(profile.id, profile);
|
|
}
|
|
|
|
return Array.from(profiles.values());
|
|
}
|
|
}
|
|
|
|
export {CpuProfileModel};
|