[go: nahoru, domu]

blob: ac721c3652c3312f20276042f64e468e8b43bab2 [file] [log] [blame]
/*
* Copyright (C) 2012 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/* eslint-disable rulesdir/no_underscored_properties */
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as Common from '../common/common.js';
import * as i18n from '../i18n/i18n.js';
import * as Platform from '../platform/platform.js';
import * as Root from '../root/root.js';
import * as SDK from '../sdk/sdk.js';
import {TimelineJSProfileProcessor} from './TimelineJSProfile.js';
export const UIStrings = {
/**
*@description Text for the name of a thread of the page
*@example {1} PH1
*/
threadS: 'Thread {PH1}',
/**
*@description Title of a worker in the timeline flame chart of the Performance panel
*@example {https://google.com} PH1
*/
workerS: '`Worker` — {PH1}',
/**
*@description Title of a worker in the timeline flame chart of the Performance panel
*/
dedicatedWorker: 'Dedicated `Worker`',
/**
*@description Title of a worker in the timeline flame chart of the Performance panel
*@example {FormatterWorker} PH1
*@example {https://google.com} PH2
*/
workerSS: '`Worker`: {PH1} — {PH2}',
};
const str_ = i18n.i18n.registerUIStrings('timeline_model/TimelineModel.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class TimelineModelImpl {
_isGenericTrace!: boolean;
_tracks!: Track[];
_namedTracks!: Map<TrackType, Track>;
_inspectedTargetEvents!: SDK.TracingModel.Event[];
_timeMarkerEvents!: SDK.TracingModel.Event[];
_sessionId!: string|null;
_mainFrameNodeId!: number|null;
_pageFrames!: Map<string, PageFrame>;
_cpuProfiles!: SDK.CPUProfileDataModel.CPUProfileDataModel[];
_workerIdByThread!: WeakMap<SDK.TracingModel.Thread, string>;
_requestsFromBrowser!: Map<string, SDK.TracingModel.Event>;
_mainFrame!: PageFrame;
_minimumRecordTime: number;
_maximumRecordTime: number;
_totalBlockingTime: number;
_estimatedTotalBlockingTime: number;
_asyncEventTracker!: TimelineAsyncEventTracker;
_invalidationTracker!: InvalidationTracker;
_layoutInvalidate!: {
[x: string]: SDK.TracingModel.Event|null,
};
_lastScheduleStyleRecalculation!: {
[x: string]: SDK.TracingModel.Event,
};
_paintImageEventByPixelRefId!: {
[x: string]: SDK.TracingModel.Event,
};
_lastPaintForLayer!: {
[x: string]: SDK.TracingModel.Event,
};
_lastRecalculateStylesEvent!: SDK.TracingModel.Event|null;
_currentScriptEvent!: SDK.TracingModel.Event|null;
_eventStack!: SDK.TracingModel.Event[];
_knownInputEvents!: Set<string>;
_browserFrameTracking!: boolean;
_persistentIds!: boolean;
_legacyCurrentPage!: any;
_currentTaskLayoutAndRecalcEvents: SDK.TracingModel.Event[];
_tracingModel: SDK.TracingModel.TracingModel|null;
_mainFrameLayerTreeId?: any;
constructor() {
this._minimumRecordTime = 0;
this._maximumRecordTime = 0;
this._totalBlockingTime = 0;
this._estimatedTotalBlockingTime = 0;
this._reset();
this._resetProcessingState();
this._currentTaskLayoutAndRecalcEvents = [];
this._tracingModel = null;
}
static forEachEvent(
events: SDK.TracingModel.Event[], onStartEvent: (arg0: SDK.TracingModel.Event) => void,
onEndEvent: (arg0: SDK.TracingModel.Event) => void,
onInstantEvent?: ((arg0: SDK.TracingModel.Event, arg1: SDK.TracingModel.Event|null) => any), startTime?: number,
endTime?: number, filter?: ((arg0: SDK.TracingModel.Event) => boolean)): void {
startTime = startTime || 0;
endTime = endTime || Infinity;
const stack: SDK.TracingModel.Event[] = [];
const startEvent = TimelineModelImpl._topLevelEventEndingAfter(events, startTime);
for (let i = startEvent; i < events.length; ++i) {
const e = events[i];
if ((e.endTime || e.startTime) < startTime) {
continue;
}
if (e.startTime >= endTime) {
break;
}
if (SDK.TracingModel.TracingModel.isAsyncPhase(e.phase) || SDK.TracingModel.TracingModel.isFlowPhase(e.phase)) {
continue;
}
let last: SDK.TracingModel.Event = stack[stack.length - 1];
while (last && last.endTime !== undefined && last.endTime <= e.startTime) {
stack.pop();
onEndEvent(last);
last = stack[stack.length - 1];
}
if (filter && !filter(e)) {
continue;
}
if (e.duration) {
onStartEvent(e);
stack.push(e);
} else {
onInstantEvent && onInstantEvent(e, stack[stack.length - 1] || null);
}
}
while (stack.length) {
const last = stack.pop();
if (last) {
onEndEvent(last);
}
}
}
static _topLevelEventEndingAfter(events: SDK.TracingModel.Event[], time: number): number {
let index = Platform.ArrayUtilities.upperBound(events, time, (time, event) => time - event.startTime) - 1;
while (index > 0 && !SDK.TracingModel.TracingModel.isTopLevelEvent(events[index])) {
index--;
}
return Math.max(index, 0);
}
isMarkerEvent(event: SDK.TracingModel.Event): boolean {
switch (event.name) {
case RecordType.TimeStamp:
return true;
case RecordType.MarkFirstPaint:
case RecordType.MarkFCP:
return Boolean(this._mainFrame) && event.args.frame === this._mainFrame.frameId && Boolean(event.args.data);
case RecordType.MarkDOMContent:
case RecordType.MarkLoad:
case RecordType.MarkLCPCandidate:
case RecordType.MarkLCPInvalidate:
return Boolean(event.args['data']['isMainFrame']);
default:
return false;
}
}
isInteractiveTimeEvent(event: SDK.TracingModel.Event): boolean {
return event.name === RecordType.InteractiveTime;
}
isLayoutShiftEvent(event: SDK.TracingModel.Event): boolean {
return event.name === RecordType.LayoutShift;
}
isUserTimingEvent(event: SDK.TracingModel.Event): boolean {
return event.categoriesString === TimelineModelImpl.Category.UserTiming;
}
isParseHTMLEvent(event: SDK.TracingModel.Event): boolean {
return event.name === RecordType.ParseHTML;
}
isLCPCandidateEvent(event: SDK.TracingModel.Event): boolean {
return event.name === RecordType.MarkLCPCandidate && Boolean(event.args['data']['isMainFrame']);
}
isLCPInvalidateEvent(event: SDK.TracingModel.Event): boolean {
return event.name === RecordType.MarkLCPInvalidate && Boolean(event.args['data']['isMainFrame']);
}
isFCPEvent(event: SDK.TracingModel.Event): boolean {
return event.name === RecordType.MarkFCP && Boolean(this._mainFrame) &&
event.args['frame'] === this._mainFrame.frameId;
}
isLongRunningTask(event: SDK.TracingModel.Event): boolean {
return event.name === RecordType.Task &&
TimelineData.forEvent(event).warning === TimelineModelImpl.WarningType.LongTask;
}
isNavigationStartEvent(event: SDK.TracingModel.Event): boolean {
return event.name === RecordType.NavigationStart;
}
isMainFrameNavigationStartEvent(event: SDK.TracingModel.Event): boolean {
return this.isNavigationStartEvent(event) && event.args['data']['isLoadingMainFrame'] &&
event.args['data']['documentLoaderURL'];
}
static globalEventId(event: SDK.TracingModel.Event, field: string): string {
const data = event.args['data'] || event.args['beginData'];
const id = data && data[field];
if (!id) {
return '';
}
return `${event.thread.process().id()}.${id}`;
}
static eventFrameId(event: SDK.TracingModel.Event): string {
const data = event.args['data'] || event.args['beginData'];
return data && data['frame'] || '';
}
cpuProfiles(): SDK.CPUProfileDataModel.CPUProfileDataModel[] {
return this._cpuProfiles;
}
totalBlockingTime(): {
time: number,
estimated: boolean,
} {
if (this._totalBlockingTime === -1) {
return {time: this._estimatedTotalBlockingTime, estimated: true};
}
return {time: this._totalBlockingTime, estimated: false};
}
targetByEvent(event: SDK.TracingModel.Event): SDK.SDKModel.Target|null {
// FIXME: Consider returning null for loaded traces.
const workerId = this._workerIdByThread.get(event.thread);
const mainTarget = SDK.SDKModel.TargetManager.instance().mainTarget();
return workerId ? SDK.SDKModel.TargetManager.instance().targetById(workerId) : mainTarget;
}
navStartTimes(): Map<string, SDK.TracingModel.Event> {
if (!this._tracingModel) {
return new Map();
}
return this._tracingModel.navStartTimes();
}
setEvents(tracingModel: SDK.TracingModel.TracingModel): void {
this._reset();
this._resetProcessingState();
this._tracingModel = tracingModel;
this._minimumRecordTime = tracingModel.minimumRecordTime();
this._maximumRecordTime = tracingModel.maximumRecordTime();
// Remove LayoutShift events from the main thread list of events because they are
// represented in the experience track. This is done prior to the main thread being processed for its own events.
const layoutShiftEvents = [];
for (const process of tracingModel.sortedProcesses()) {
if (process.name() !== 'Renderer') {
continue;
}
for (const thread of process.sortedThreads()) {
const shifts = thread.removeEventsByName(RecordType.LayoutShift);
layoutShiftEvents.push(...shifts);
}
}
this._processSyncBrowserEvents(tracingModel);
if (this._browserFrameTracking) {
this._processThreadsForBrowserFrames(tracingModel);
} else {
// The next line is for loading legacy traces recorded before M67.
// TODO(alph): Drop the support at some point.
const metadataEvents = this._processMetadataEvents(tracingModel);
this._isGenericTrace = !metadataEvents;
if (metadataEvents) {
this._processMetadataAndThreads(tracingModel, metadataEvents);
} else {
this._processGenericTrace(tracingModel);
}
}
this._inspectedTargetEvents.sort(SDK.TracingModel.Event.compareStartTime);
this._processAsyncBrowserEvents(tracingModel);
this._buildGPUEvents(tracingModel);
this._buildLoadingEvents(tracingModel, layoutShiftEvents);
this._resetProcessingState();
}
_processGenericTrace(tracingModel: SDK.TracingModel.TracingModel): void {
let browserMainThread = SDK.TracingModel.TracingModel.browserMainThread(tracingModel);
if (!browserMainThread && tracingModel.sortedProcesses().length) {
browserMainThread = tracingModel.sortedProcesses()[0].sortedThreads()[0];
}
for (const process of tracingModel.sortedProcesses()) {
for (const thread of process.sortedThreads()) {
this._processThreadEvents(
tracingModel, [{from: 0, to: Infinity}], thread, thread === browserMainThread, false, true, null);
}
}
}
_processMetadataAndThreads(tracingModel: SDK.TracingModel.TracingModel, metadataEvents: MetadataEvents): void {
let startTime = 0;
for (let i = 0, length = metadataEvents.page.length; i < length; i++) {
const metaEvent = metadataEvents.page[i];
const process = metaEvent.thread.process();
const endTime = i + 1 < length ? metadataEvents.page[i + 1].startTime : Infinity;
if (startTime === endTime) {
continue;
}
this._legacyCurrentPage = metaEvent.args['data'] && metaEvent.args['data']['page'];
for (const thread of process.sortedThreads()) {
let workerUrl: null = null;
if (thread.name() === TimelineModelImpl.WorkerThreadName ||
thread.name() === TimelineModelImpl.WorkerThreadNameLegacy) {
const workerMetaEvent = metadataEvents.workers.find(e => {
if (e.args['data']['workerThreadId'] !== thread.id()) {
return false;
}
// This is to support old traces.
if (e.args['data']['sessionId'] === this._sessionId) {
return true;
}
return Boolean(this._pageFrames.get(TimelineModelImpl.eventFrameId(e)));
});
if (!workerMetaEvent) {
continue;
}
const workerId = workerMetaEvent.args['data']['workerId'];
if (workerId) {
this._workerIdByThread.set(thread, workerId);
}
workerUrl = workerMetaEvent.args['data']['url'] || '';
}
this._processThreadEvents(
tracingModel, [{from: startTime, to: endTime}], thread, thread === metaEvent.thread, Boolean(workerUrl),
true, workerUrl);
}
startTime = endTime;
}
}
_processThreadsForBrowserFrames(tracingModel: SDK.TracingModel.TracingModel): void {
const processData = new Map<number, {
from: number,
to: number,
main: boolean,
url: string,
}[]>();
for (const frame of this._pageFrames.values()) {
for (let i = 0; i < frame.processes.length; i++) {
const pid = frame.processes[i].processId;
let data = processData.get(pid);
if (!data) {
data = [];
processData.set(pid, data);
}
const to = i === frame.processes.length - 1 ? (frame.deletedTime || Infinity) : frame.processes[i + 1].time;
data.push({from: frame.processes[i].time, to: to, main: !frame.parent, url: frame.processes[i].url});
}
}
const allMetadataEvents = tracingModel.devToolsMetadataEvents();
for (const process of tracingModel.sortedProcesses()) {
const data = processData.get(process.id());
if (!data) {
continue;
}
data.sort((a, b) => a.from - b.from || a.to - b.to);
const ranges = [];
let lastUrl: string|null = null;
let lastMainUrl: string|null = null;
let hasMain = false;
for (const item of data) {
const last = ranges[ranges.length - 1];
if (!last || item.from > last.to) {
ranges.push({from: item.from, to: item.to});
} else {
last.to = item.to;
}
if (item.main) {
hasMain = true;
}
if (item.url) {
if (item.main) {
lastMainUrl = item.url;
}
lastUrl = item.url;
}
}
for (const thread of process.sortedThreads()) {
if (thread.name() === TimelineModelImpl.RendererMainThreadName) {
this._processThreadEvents(
tracingModel, ranges, thread, true /* isMainThread */, false /* isWorker */, hasMain,
hasMain ? lastMainUrl : lastUrl);
} else if (
thread.name() === TimelineModelImpl.WorkerThreadName ||
thread.name() === TimelineModelImpl.WorkerThreadNameLegacy) {
const workerMetaEvent = allMetadataEvents.find(e => {
if (e.name !== TimelineModelImpl.DevToolsMetadataEvent.TracingSessionIdForWorker) {
return false;
}
if (e.thread.process() !== process) {
return false;
}
if (e.args['data']['workerThreadId'] !== thread.id()) {
return false;
}
return Boolean(this._pageFrames.get(TimelineModelImpl.eventFrameId(e)));
});
if (!workerMetaEvent) {
continue;
}
this._workerIdByThread.set(thread, workerMetaEvent.args['data']['workerId'] || '');
this._processThreadEvents(
tracingModel, ranges, thread, false /* isMainThread */, true /* isWorker */, false /* forMainFrame */,
workerMetaEvent.args['data']['url'] || '');
} else {
this._processThreadEvents(
tracingModel, ranges, thread, false /* isMainThread */, false /* isWorker */, false /* forMainFrame */,
null);
}
}
}
}
_processMetadataEvents(tracingModel: SDK.TracingModel.TracingModel): MetadataEvents|null {
const metadataEvents = tracingModel.devToolsMetadataEvents();
const pageDevToolsMetadataEvents = [];
const workersDevToolsMetadataEvents = [];
for (const event of metadataEvents) {
if (event.name === TimelineModelImpl.DevToolsMetadataEvent.TracingStartedInPage) {
pageDevToolsMetadataEvents.push(event);
if (event.args['data'] && event.args['data']['persistentIds']) {
this._persistentIds = true;
}
const frames = ((event.args['data'] && event.args['data']['frames']) || [] as PageFrame[]);
frames.forEach((payload: PageFrame) => this._addPageFrame(event, payload));
this._mainFrame = this.rootFrames()[0];
} else if (event.name === TimelineModelImpl.DevToolsMetadataEvent.TracingSessionIdForWorker) {
workersDevToolsMetadataEvents.push(event);
} else if (event.name === TimelineModelImpl.DevToolsMetadataEvent.TracingStartedInBrowser) {
console.assert(!this._mainFrameNodeId, 'Multiple sessions in trace');
this._mainFrameNodeId = event.args['frameTreeNodeId'];
}
}
if (!pageDevToolsMetadataEvents.length) {
return null;
}
const sessionId =
pageDevToolsMetadataEvents[0].args['sessionId'] || pageDevToolsMetadataEvents[0].args['data']['sessionId'];
this._sessionId = sessionId;
const mismatchingIds = new Set<any>();
function checkSessionId(event: SDK.TracingModel.Event): boolean {
let args = event.args;
// FIXME: put sessionId into args["data"] for TracingStartedInPage event.
if (args['data']) {
args = args['data'];
}
const id = args['sessionId'];
if (id === sessionId) {
return true;
}
mismatchingIds.add(id);
return false;
}
const result = {
page: pageDevToolsMetadataEvents.filter(checkSessionId).sort(SDK.TracingModel.Event.compareStartTime),
workers: workersDevToolsMetadataEvents.sort(SDK.TracingModel.Event.compareStartTime),
};
if (mismatchingIds.size) {
Common.Console.Console.instance().error(
'Timeline recording was started in more than one page simultaneously. Session id mismatch: ' +
this._sessionId + ' and ' + [...mismatchingIds] + '.');
}
return result;
}
_processSyncBrowserEvents(tracingModel: SDK.TracingModel.TracingModel): void {
const browserMain = SDK.TracingModel.TracingModel.browserMainThread(tracingModel);
if (browserMain) {
browserMain.events().forEach(this._processBrowserEvent, this);
}
}
_processAsyncBrowserEvents(tracingModel: SDK.TracingModel.TracingModel): void {
const browserMain = SDK.TracingModel.TracingModel.browserMainThread(tracingModel);
if (browserMain) {
this._processAsyncEvents(browserMain, [{from: 0, to: Infinity}]);
}
}
_buildGPUEvents(tracingModel: SDK.TracingModel.TracingModel): void {
const thread = tracingModel.threadByName('GPU Process', 'CrGpuMain');
if (!thread) {
return;
}
const gpuEventName = RecordType.GPUTask;
const track = this._ensureNamedTrack(TrackType.GPU);
track.thread = thread;
track.events = thread.events().filter(event => event.name === gpuEventName);
}
_buildLoadingEvents(tracingModel: SDK.TracingModel.TracingModel, events: SDK.TracingModel.Event[]): void {
const thread = tracingModel.threadByName('Renderer', 'CrRendererMain');
if (!thread) {
return;
}
const experienceCategory = 'experience';
const track = this._ensureNamedTrack(TrackType.Experience);
track.thread = thread;
track.events = events;
// Even though the event comes from 'loading', in order to color it differently we
// rename its category.
for (const trackEvent of track.events) {
trackEvent.categoriesString = experienceCategory;
if (trackEvent.name === RecordType.LayoutShift) {
const eventData = trackEvent.args['data'] || trackEvent.args['beginData'] || {};
const timelineData = TimelineData.forEvent(trackEvent);
timelineData.backendNodeId = eventData['impacted_nodes'] && eventData['impacted_nodes'].length > 0 ?
eventData['impacted_nodes'][0]['node_id'] :
0;
}
}
}
_resetProcessingState(): void {
this._asyncEventTracker = new TimelineAsyncEventTracker();
this._invalidationTracker = new InvalidationTracker();
this._layoutInvalidate = {};
this._lastScheduleStyleRecalculation = {};
this._paintImageEventByPixelRefId = {};
this._lastPaintForLayer = {};
this._lastRecalculateStylesEvent = null;
this._currentScriptEvent = null;
this._eventStack = [];
this._knownInputEvents = new Set();
this._browserFrameTracking = false;
this._persistentIds = false;
this._legacyCurrentPage = null;
}
_extractCpuProfile(tracingModel: SDK.TracingModel.TracingModel, thread: SDK.TracingModel.Thread):
SDK.CPUProfileDataModel.CPUProfileDataModel|null {
const events = thread.events();
let cpuProfile;
let target: (SDK.SDKModel.Target|null)|null = null;
// Check for legacy CpuProfile event format first.
let cpuProfileEvent: (SDK.TracingModel.Event|undefined)|SDK.TracingModel.Event = events[events.length - 1];
if (cpuProfileEvent && cpuProfileEvent.name === RecordType.CpuProfile) {
const eventData = cpuProfileEvent.args['data'];
cpuProfile = (eventData && eventData['cpuProfile'] as Protocol.Profiler.Profile | null);
target = this.targetByEvent(cpuProfileEvent);
}
if (!cpuProfile) {
cpuProfileEvent = events.find(e => e.name === RecordType.Profile);
if (!cpuProfileEvent) {
return null;
}
target = this.targetByEvent(cpuProfileEvent);
const profileGroup = tracingModel.profileGroup(cpuProfileEvent);
if (!profileGroup) {
Common.Console.Console.instance().error('Invalid CPU profile format.');
return null;
}
cpuProfile = ({
startTime: cpuProfileEvent.startTime * 1000,
endTime: 0,
nodes: [],
samples: [],
timeDeltas: [],
lines: [],
} as any);
for (const profileEvent of profileGroup.children) {
const eventData = profileEvent.args['data'];
if ('startTime' in eventData) {
// Do not use |eventData['startTime']| as it is in CLOCK_MONOTONIC domain,
// but use |profileEvent.startTime| (|ts| in the trace event) which has
// been translated to Perfetto's clock domain.
//
// Also convert from ms to us.
cpuProfile.startTime = profileEvent.startTime * 1000;
}
if ('endTime' in eventData) {
// Do not use |eventData['endTime']| as it is in CLOCK_MONOTONIC domain,
// but use |profileEvent.startTime| (|ts| in the trace event) which has
// been translated to Perfetto's clock domain.
//
// Despite its name, |profileEvent.startTime| was recorded right after
// |eventData['endTime']| within v8 and is a reasonable substitute.
//
// Also convert from ms to us.
cpuProfile.endTime = profileEvent.startTime * 1000;
}
const nodesAndSamples = eventData['cpuProfile'] || {};
const samples = nodesAndSamples['samples'] || [];
const lines = eventData['lines'] || Array(samples.length).fill(0);
cpuProfile.nodes.push(...(nodesAndSamples['nodes'] || []));
cpuProfile.lines.push(...lines);
if (cpuProfile.samples) {
cpuProfile.samples.push(...samples);
}
if (cpuProfile.timeDeltas) {
cpuProfile.timeDeltas.push(...(eventData['timeDeltas'] || []));
}
if (cpuProfile.samples && cpuProfile.timeDeltas && cpuProfile.samples.length !== cpuProfile.timeDeltas.length) {
Common.Console.Console.instance().error('Failed to parse CPU profile.');
return null;
}
}
if (!cpuProfile.endTime && cpuProfile.timeDeltas) {
const timeDeltas: number[] = cpuProfile.timeDeltas;
cpuProfile.endTime = timeDeltas.reduce((x, y) => x + y, cpuProfile.startTime);
}
}
try {
const profile = (cpuProfile as Protocol.Profiler.Profile);
const jsProfileModel = new SDK.CPUProfileDataModel.CPUProfileDataModel(profile, target);
this._cpuProfiles.push(jsProfileModel);
return jsProfileModel;
} catch (e) {
Common.Console.Console.instance().error('Failed to parse CPU profile.');
}
return null;
}
_injectJSFrameEvents(tracingModel: SDK.TracingModel.TracingModel, thread: SDK.TracingModel.Thread):
SDK.TracingModel.Event[] {
const jsProfileModel = this._extractCpuProfile(tracingModel, thread);
let events = thread.events();
const jsSamples =
jsProfileModel ? TimelineJSProfileProcessor.generateTracingEventsFromCpuProfile(jsProfileModel, thread) : null;
if (jsSamples && jsSamples.length) {
events = Platform.ArrayUtilities.mergeOrdered(events, jsSamples, SDK.TracingModel.Event.orderedCompareStartTime);
}
if (jsSamples || events.some(e => e.name === RecordType.JSSample)) {
const jsFrameEvents = TimelineJSProfileProcessor.generateJSFrameEvents(events, {
showAllEvents: Root.Runtime.experiments.isEnabled('timelineShowAllEvents'),
showRuntimeCallStats: Root.Runtime.experiments.isEnabled('timelineV8RuntimeCallStats'),
showNativeFunctions: Common.Settings.Settings.instance().moduleSetting('showNativeFunctionsInJSProfile').get(),
});
if (jsFrameEvents && jsFrameEvents.length) {
events =
Platform.ArrayUtilities.mergeOrdered(jsFrameEvents, events, SDK.TracingModel.Event.orderedCompareStartTime);
}
}
return events;
}
_processThreadEvents(
tracingModel: SDK.TracingModel.TracingModel, ranges: {
from: number,
to: number,
}[],
thread: SDK.TracingModel.Thread, isMainThread: boolean, isWorker: boolean, forMainFrame: boolean,
url: string|null): void {
const track = new Track();
track.name = thread.name() || i18nString(UIStrings.threadS, {PH1: thread.id()});
track.type = TrackType.Other;
track.thread = thread;
if (isMainThread) {
track.type = TrackType.MainThread;
track.url = url || '';
track.forMainFrame = forMainFrame;
} else if (isWorker) {
track.type = TrackType.Worker;
track.url = url || '';
track.name = track.url ? i18nString(UIStrings.workerS, {PH1: track.url}) : i18nString(UIStrings.dedicatedWorker);
} else if (thread.name().startsWith('CompositorTileWorker')) {
track.type = TrackType.Raster;
}
this._tracks.push(track);
const events = this._injectJSFrameEvents(tracingModel, thread);
this._eventStack = [];
const eventStack = this._eventStack;
// Get the worker name from the target.
if (isWorker) {
const cpuProfileEvent = events.find(event => event.name === RecordType.Profile);
if (cpuProfileEvent) {
const target = this.targetByEvent(cpuProfileEvent);
if (target) {
track.name = i18nString(UIStrings.workerSS, {PH1: target.name(), PH2: track.url});
}
}
}
for (const range of ranges) {
let i = Platform.ArrayUtilities.lowerBound(events, range.from, (time, event) => time - event.startTime);
for (; i < events.length; i++) {
const event = events[i];
if (event.startTime >= range.to) {
break;
}
// There may be several TTI events, only take the first one.
if (this.isInteractiveTimeEvent(event) && this._totalBlockingTime === -1) {
this._totalBlockingTime = event.args['args']['total_blocking_time_ms'];
}
const isLongRunningTask = event.name === RecordType.Task && event.duration && event.duration > 50;
if (isMainThread && isLongRunningTask && event.duration) {
// We only track main thread events that are over 50ms, and the amount of time in the
// event (over 50ms) is what constitutes the blocking time. An event of 70ms, therefore,
// contributes 20ms to TBT.
this._estimatedTotalBlockingTime += event.duration - 50;
}
let last: SDK.TracingModel.Event = eventStack[eventStack.length - 1];
while (last && last.endTime !== undefined && last.endTime <= event.startTime) {
eventStack.pop();
last = eventStack[eventStack.length - 1];
}
if (!this._processEvent(event)) {
continue;
}
if (!SDK.TracingModel.TracingModel.isAsyncPhase(event.phase) && event.duration) {
if (eventStack.length) {
const parent = eventStack[eventStack.length - 1];
if (parent) {
parent.selfTime -= event.duration;
if (parent.selfTime < 0) {
this._fixNegativeDuration(parent, event);
}
}
}
event.selfTime = event.duration;
if (!eventStack.length) {
track.tasks.push(event);
}
eventStack.push(event);
}
if (this.isMarkerEvent(event)) {
this._timeMarkerEvents.push(event);
}
track.events.push(event);
this._inspectedTargetEvents.push(event);
}
}
this._processAsyncEvents(thread, ranges);
}
_fixNegativeDuration(event: SDK.TracingModel.Event, child: SDK.TracingModel.Event): void {
const epsilon = 1e-3;
if (event.selfTime < -epsilon) {
console.error(
`Children are longer than parent at ${event.startTime} ` +
`(${(child.startTime - this.minimumRecordTime()).toFixed(3)} by ${(-event.selfTime).toFixed(3)}`);
}
event.selfTime = 0;
}
_processAsyncEvents(thread: SDK.TracingModel.Thread, ranges: {
from: number,
to: number,
}[]): void {
const asyncEvents = thread.asyncEvents();
const groups = new Map<TrackType, SDK.TracingModel.AsyncEvent[]>();
function group(type: TrackType): SDK.TracingModel.AsyncEvent[] {
if (!groups.has(type)) {
groups.set(type, []);
}
return groups.get(type) as SDK.TracingModel.AsyncEvent[];
}
for (const range of ranges) {
let i = Platform.ArrayUtilities.lowerBound(asyncEvents, range.from, function(time, asyncEvent) {
return time - asyncEvent.startTime;
});
for (; i < asyncEvents.length; ++i) {
const asyncEvent = asyncEvents[i];
if (asyncEvent.startTime >= range.to) {
break;
}
if (asyncEvent.hasCategory(TimelineModelImpl.Category.Console)) {
group(TrackType.Console).push(asyncEvent);
continue;
}
if (asyncEvent.hasCategory(TimelineModelImpl.Category.UserTiming)) {
group(TrackType.Timings).push(asyncEvent);
continue;
}
if (asyncEvent.name === RecordType.Animation) {
group(TrackType.Animation).push(asyncEvent);
continue;
}
if (asyncEvent.hasCategory(TimelineModelImpl.Category.LatencyInfo) ||
asyncEvent.name === RecordType.ImplSideFling) {
const lastStep = asyncEvent.steps[asyncEvent.steps.length - 1];
if (!lastStep) {
throw new Error('AsyncEvent.steps access is out of bounds.');
}
// FIXME: fix event termination on the back-end instead.
if (lastStep.phase !== SDK.TracingModel.Phase.AsyncEnd) {
continue;
}
const data = lastStep.args['data'];
asyncEvent.causedFrame = Boolean(data && data['INPUT_EVENT_LATENCY_RENDERER_SWAP_COMPONENT']);
if (asyncEvent.hasCategory(TimelineModelImpl.Category.LatencyInfo)) {
if (lastStep.id && !this._knownInputEvents.has(lastStep.id)) {
continue;
}
if (asyncEvent.name === RecordType.InputLatencyMouseMove && !asyncEvent.causedFrame) {
continue;
}
// Coalesced events are not really been processed, no need to track them.
if (data['is_coalesced']) {
continue;
}
const rendererMain = data['INPUT_EVENT_LATENCY_RENDERER_MAIN_COMPONENT'];
if (rendererMain) {
const time = rendererMain['time'] / 1000;
TimelineData.forEvent(asyncEvent.steps[0]).timeWaitingForMainThread =
time - asyncEvent.steps[0].startTime;
}
}
group(TrackType.Input).push(asyncEvent);
continue;
}
}
}
for (const [type, events] of groups) {
const track = this._ensureNamedTrack(type);
track.thread = thread;
track.asyncEvents =
Platform.ArrayUtilities.mergeOrdered(track.asyncEvents, events, SDK.TracingModel.Event.compareStartTime);
}
}
_processEvent(event: SDK.TracingModel.Event): boolean {
const eventStack = this._eventStack;
if (!eventStack.length) {
if (this._currentTaskLayoutAndRecalcEvents && this._currentTaskLayoutAndRecalcEvents.length) {
const totalTime = this._currentTaskLayoutAndRecalcEvents.reduce((time, event) => {
return event.duration === undefined ? time : time + event.duration;
}, 0);
if (totalTime > TimelineModelImpl.Thresholds.ForcedLayout) {
for (const e of this._currentTaskLayoutAndRecalcEvents) {
const timelineData = TimelineData.forEvent(e);
timelineData.warning = e.name === RecordType.Layout ? TimelineModelImpl.WarningType.ForcedLayout :
TimelineModelImpl.WarningType.ForcedStyle;
}
}
}
this._currentTaskLayoutAndRecalcEvents = [];
}
if (this._currentScriptEvent) {
if (this._currentScriptEvent.endTime !== undefined && event.startTime > this._currentScriptEvent.endTime) {
this._currentScriptEvent = null;
}
}
const eventData = event.args['data'] || event.args['beginData'] || {};
const timelineData = TimelineData.forEvent(event);
if (eventData['stackTrace']) {
timelineData.stackTrace = eventData['stackTrace'];
}
if (timelineData.stackTrace && event.name !== RecordType.JSSample) {
// TraceEvents come with 1-based line & column numbers. The frontend code
// requires 0-based ones. Adjust the values.
for (let i = 0; i < timelineData.stackTrace.length; ++i) {
--timelineData.stackTrace[i].lineNumber;
--timelineData.stackTrace[i].columnNumber;
}
}
let pageFrameId = TimelineModelImpl.eventFrameId(event);
const last = eventStack[eventStack.length - 1];
if (!pageFrameId && last) {
pageFrameId = TimelineData.forEvent(last).frameId;
}
timelineData.frameId = pageFrameId || (this._mainFrame && this._mainFrame.frameId) || '';
this._asyncEventTracker.processEvent(event);
if (this.isMarkerEvent(event)) {
this._ensureNamedTrack(TrackType.Timings);
}
switch (event.name) {
case RecordType.ResourceSendRequest:
case RecordType.WebSocketCreate: {
timelineData.setInitiator(eventStack[eventStack.length - 1] || null);
timelineData.url = eventData['url'];
break;
}
case RecordType.ScheduleStyleRecalculation: {
this._lastScheduleStyleRecalculation[eventData['frame']] = event;
break;
}
case RecordType.UpdateLayoutTree:
case RecordType.RecalculateStyles: {
this._invalidationTracker.didRecalcStyle(event);
if (event.args['beginData']) {
timelineData.setInitiator(this._lastScheduleStyleRecalculation[event.args['beginData']['frame']]);
}
this._lastRecalculateStylesEvent = event;
if (this._currentScriptEvent) {
this._currentTaskLayoutAndRecalcEvents.push(event);
}
break;
}
case RecordType.ScheduleStyleInvalidationTracking:
case RecordType.StyleRecalcInvalidationTracking:
case RecordType.StyleInvalidatorInvalidationTracking:
case RecordType.LayoutInvalidationTracking: {
this._invalidationTracker.addInvalidation(new InvalidationTrackingEvent(event));
break;
}
case RecordType.InvalidateLayout: {
// Consider style recalculation as a reason for layout invalidation,
// but only if we had no earlier layout invalidation records.
let layoutInitator: (SDK.TracingModel.Event|null)|SDK.TracingModel.Event = event;
const frameId = eventData['frame'];
if (!this._layoutInvalidate[frameId] && this._lastRecalculateStylesEvent &&
this._lastRecalculateStylesEvent.endTime !== undefined &&
this._lastRecalculateStylesEvent.endTime > event.startTime) {
layoutInitator = TimelineData.forEvent(this._lastRecalculateStylesEvent).initiator();
}
this._layoutInvalidate[frameId] = layoutInitator;
break;
}
case RecordType.Layout: {
this._invalidationTracker.didLayout(event);
const frameId = event.args['beginData']['frame'];
timelineData.setInitiator(this._layoutInvalidate[frameId]);
// In case we have no closing Layout event, endData is not available.
if (event.args['endData']) {
timelineData.backendNodeId = event.args['endData']['rootNode'];
}
this._layoutInvalidate[frameId] = null;
if (this._currentScriptEvent) {
this._currentTaskLayoutAndRecalcEvents.push(event);
}
break;
}
case RecordType.Task: {
if (event.duration !== undefined && event.duration > TimelineModelImpl.Thresholds.LongTask) {
timelineData.warning = TimelineModelImpl.WarningType.LongTask;
}
break;
}
case RecordType.EventDispatch: {
if (event.duration !== undefined && event.duration > TimelineModelImpl.Thresholds.RecurringHandler) {
timelineData.warning = TimelineModelImpl.WarningType.LongHandler;
}
break;
}
case RecordType.TimerFire:
case RecordType.FireAnimationFrame: {
if (event.duration !== undefined && event.duration > TimelineModelImpl.Thresholds.RecurringHandler) {
timelineData.warning = TimelineModelImpl.WarningType.LongRecurringHandler;
}
break;
}
// @ts-ignore fallthrough intended.
case RecordType.FunctionCall: {
// Compatibility with old format.
if (typeof eventData['scriptName'] === 'string') {
eventData['url'] = eventData['scriptName'];
}
if (typeof eventData['scriptLine'] === 'number') {
eventData['lineNumber'] = eventData['scriptLine'];
}
}
case RecordType.EvaluateScript:
// @ts-ignore fallthrough intended.
case RecordType.CompileScript: {
if (typeof eventData['lineNumber'] === 'number') {
--eventData['lineNumber'];
}
if (typeof eventData['columnNumber'] === 'number') {
--eventData['columnNumber'];
}
}
case RecordType.RunMicrotasks: {
// Microtasks technically are not necessarily scripts, but for purpose of
// forced sync style recalc or layout detection they are.
if (!this._currentScriptEvent) {
this._currentScriptEvent = event;
}
break;
}
case RecordType.SetLayerTreeId: {
// This is to support old traces.
if (this._sessionId && eventData['sessionId'] && this._sessionId === eventData['sessionId']) {
this._mainFrameLayerTreeId = eventData['layerTreeId'];
break;
}
// We currently only show layer tree for the main frame.
const frameId = TimelineModelImpl.eventFrameId(event);
const pageFrame = this._pageFrames.get(frameId);
if (!pageFrame || pageFrame.parent) {
return false;
}
this._mainFrameLayerTreeId = eventData['layerTreeId'];
break;
}
case RecordType.Paint: {
this._invalidationTracker.didPaint(event);
timelineData.backendNodeId = eventData['nodeId'];
// Only keep layer paint events, skip paints for subframes that get painted to the same layer as parent.
if (!eventData['layerId']) {
break;
}
const layerId = eventData['layerId'];
this._lastPaintForLayer[layerId] = event;
break;
}
case RecordType.DisplayItemListSnapshot:
case RecordType.PictureSnapshot: {
const layerUpdateEvent = this._findAncestorEvent(RecordType.UpdateLayer);
if (!layerUpdateEvent || layerUpdateEvent.args['layerTreeId'] !== this._mainFrameLayerTreeId) {
break;
}
const paintEvent = this._lastPaintForLayer[layerUpdateEvent.args['layerId']];
if (paintEvent) {
TimelineData.forEvent(paintEvent).picture = (event as SDK.TracingModel.ObjectSnapshot);
}
break;
}
case RecordType.ScrollLayer: {
timelineData.backendNodeId = eventData['nodeId'];
break;
}
case RecordType.PaintImage: {
timelineData.backendNodeId = eventData['nodeId'];
timelineData.url = eventData['url'];
break;
}
case RecordType.DecodeImage:
case RecordType.ResizeImage: {
let paintImageEvent = this._findAncestorEvent(RecordType.PaintImage);
if (!paintImageEvent) {
const decodeLazyPixelRefEvent = this._findAncestorEvent(RecordType.DecodeLazyPixelRef);
paintImageEvent = decodeLazyPixelRefEvent &&
this._paintImageEventByPixelRefId[decodeLazyPixelRefEvent.args['LazyPixelRef']];
}
if (!paintImageEvent) {
break;
}
const paintImageData = TimelineData.forEvent(paintImageEvent);
timelineData.backendNodeId = paintImageData.backendNodeId;
timelineData.url = paintImageData.url;
break;
}
case RecordType.DrawLazyPixelRef: {
const paintImageEvent = this._findAncestorEvent(RecordType.PaintImage);
if (!paintImageEvent) {
break;
}
this._paintImageEventByPixelRefId[event.args['LazyPixelRef']] = paintImageEvent;
const paintImageData = TimelineData.forEvent(paintImageEvent);
timelineData.backendNodeId = paintImageData.backendNodeId;
timelineData.url = paintImageData.url;
break;
}
case RecordType.FrameStartedLoading: {
if (timelineData.frameId !== event.args['frame']) {
return false;
}
break;
}
case RecordType.MarkLCPCandidate: {
timelineData.backendNodeId = eventData['nodeId'];
break;
}
case RecordType.MarkDOMContent:
case RecordType.MarkLoad: {
const frameId = TimelineModelImpl.eventFrameId(event);
if (!this._pageFrames.has(frameId)) {
return false;
}
break;
}
case RecordType.CommitLoad: {
if (this._browserFrameTracking) {
break;
}
const frameId = TimelineModelImpl.eventFrameId(event);
const isMainFrame = Boolean(eventData['isMainFrame']);
const pageFrame = this._pageFrames.get(frameId);
if (pageFrame) {
pageFrame.update(event.startTime, eventData);
} else {
// We should only have one main frame which has persistent id,
// unless it's an old trace without 'persistentIds' flag.
if (!this._persistentIds) {
if (eventData['page'] && eventData['page'] !== this._legacyCurrentPage) {
return false;
}
} else if (isMainFrame) {
return false;
} else if (!this._addPageFrame(event, eventData)) {
return false;
}
}
if (isMainFrame) {
const frame = this._pageFrames.get(frameId);
if (frame) {
this._mainFrame = frame;
}
}
break;
}
case RecordType.FireIdleCallback: {
if (event.duration !== undefined &&
event.duration > eventData['allottedMilliseconds'] + TimelineModelImpl.Thresholds.IdleCallbackAddon) {
timelineData.warning = TimelineModelImpl.WarningType.IdleDeadlineExceeded;
}
break;
}
}
return true;
}
_processBrowserEvent(event: SDK.TracingModel.Event): void {
if (event.name === RecordType.LatencyInfoFlow) {
const frameId = event.args['frameTreeNodeId'];
if (typeof frameId === 'number' && frameId === this._mainFrameNodeId && event.bind_id) {
this._knownInputEvents.add(event.bind_id);
}
return;
}
if (event.name === RecordType.ResourceWillSendRequest) {
const requestId = event.args['data']['requestId'];
if (typeof requestId === 'string') {
this._requestsFromBrowser.set(requestId, event);
}
return;
}
if (event.hasCategory(SDK.TracingModel.DevToolsMetadataEventCategory) && event.args['data']) {
const data = event.args['data'];
if (event.name === TimelineModelImpl.DevToolsMetadataEvent.TracingStartedInBrowser) {
if (!data['persistentIds']) {
return;
}
this._browserFrameTracking = true;
this._mainFrameNodeId = data['frameTreeNodeId'];
const frames: any[] = data['frames'] || [];
frames.forEach(payload => {
const parent = payload['parent'] && this._pageFrames.get(payload['parent']);
if (payload['parent'] && !parent) {
return;
}
let frame = this._pageFrames.get(payload['frame']);
if (!frame) {
frame = new PageFrame(payload);
this._pageFrames.set(frame.frameId, frame);
if (parent) {
parent.addChild(frame);
} else {
this._mainFrame = frame;
}
}
// TODO(dgozman): this should use event.startTime, but due to races between tracing start
// in different processes we cannot do this yet.
frame.update(this._minimumRecordTime, payload);
});
return;
}
if (event.name === TimelineModelImpl.DevToolsMetadataEvent.FrameCommittedInBrowser &&
this._browserFrameTracking) {
let frame = this._pageFrames.get(data['frame']);
if (!frame) {
const parent = data['parent'] && this._pageFrames.get(data['parent']);
if (!parent) {
return;
}
frame = new PageFrame(data);
this._pageFrames.set(frame.frameId, frame);
parent.addChild(frame);
}
frame.update(event.startTime, data);
return;
}
if (event.name === TimelineModelImpl.DevToolsMetadataEvent.ProcessReadyInBrowser && this._browserFrameTracking) {
const frame = this._pageFrames.get(data['frame']);
if (frame) {
frame.processReady(data['processPseudoId'], data['processId']);
}
return;
}
if (event.name === TimelineModelImpl.DevToolsMetadataEvent.FrameDeletedInBrowser && this._browserFrameTracking) {
const frame = this._pageFrames.get(data['frame']);
if (frame) {
frame.deletedTime = event.startTime;
}
return;
}
}
}
_ensureNamedTrack(type: TrackType): Track {
let track = this._namedTracks.get(type);
if (track) {
return track;
}
track = new Track();
track.type = type;
this._tracks.push(track);
this._namedTracks.set(type, track);
return track;
}
_findAncestorEvent(name: string): SDK.TracingModel.Event|null {
for (let i = this._eventStack.length - 1; i >= 0; --i) {
const event = this._eventStack[i];
if (event.name === name) {
return event;
}
}
return null;
}
_addPageFrame(event: SDK.TracingModel.Event, payload: any): boolean {
const parent = payload['parent'] && this._pageFrames.get(payload['parent']);
if (payload['parent'] && !parent) {
return false;
}
const pageFrame = new PageFrame(payload);
this._pageFrames.set(pageFrame.frameId, pageFrame);
pageFrame.update(event.startTime, payload);
if (parent) {
parent.addChild(pageFrame);
}
return true;
}
_reset(): void {
this._isGenericTrace = false;
this._tracks = [];
this._namedTracks = new Map();
this._inspectedTargetEvents = [];
this._timeMarkerEvents = [];
this._sessionId = null;
this._mainFrameNodeId = null;
this._cpuProfiles = [];
this._workerIdByThread = new WeakMap();
this._pageFrames = new Map();
this._requestsFromBrowser = new Map();
this._minimumRecordTime = 0;
this._maximumRecordTime = 0;
this._totalBlockingTime = -1;
this._estimatedTotalBlockingTime = 0;
}
isGenericTrace(): boolean {
return this._isGenericTrace;
}
tracingModel(): SDK.TracingModel.TracingModel|null {
return this._tracingModel;
}
minimumRecordTime(): number {
return this._minimumRecordTime;
}
maximumRecordTime(): number {
return this._maximumRecordTime;
}
inspectedTargetEvents(): SDK.TracingModel.Event[] {
return this._inspectedTargetEvents;
}
tracks(): Track[] {
return this._tracks;
}
isEmpty(): boolean {
return this.minimumRecordTime() === 0 && this.maximumRecordTime() === 0;
}
timeMarkerEvents(): SDK.TracingModel.Event[] {
return this._timeMarkerEvents;
}
rootFrames(): PageFrame[] {
return Array.from(this._pageFrames.values()).filter(frame => !frame.parent);
}
pageURL(): string {
return this._mainFrame && this._mainFrame.url || '';
}
pageFrameById(frameId: string): PageFrame|null {
return frameId ? this._pageFrames.get(frameId) || null : null;
}
networkRequests(): NetworkRequest[] {
if (this.isGenericTrace()) {
return [];
}
const requests = new Map<string, NetworkRequest>();
const requestsList: NetworkRequest[] = [];
const zeroStartRequestsList: NetworkRequest[] = [];
const resourceTypes = new Set<string>([
RecordType.ResourceWillSendRequest,
RecordType.ResourceSendRequest,
RecordType.ResourceReceiveResponse,
RecordType.ResourceReceivedData,
RecordType.ResourceFinish,
RecordType.ResourceMarkAsCached,
]);
const events = this.inspectedTargetEvents();
for (let i = 0; i < events.length; ++i) {
const e = events[i];
if (!resourceTypes.has(e.name)) {
continue;
}
const id = TimelineModelImpl.globalEventId(e, 'requestId');
if (e.name === RecordType.ResourceSendRequest && this._requestsFromBrowser.has(e.args.data.requestId)) {
const requestId = e.args.data.requestId;
const event = this._requestsFromBrowser.get(requestId);
if (event) {
addRequest(event, id);
}
}
addRequest(e, id);
}
function addRequest(e: SDK.TracingModel.Event, id: string): void {
let request = requests.get(id);
if (request) {
request.addEvent(e);
} else {
request = new NetworkRequest(e);
requests.set(id, request);
if (request.startTime) {
requestsList.push(request);
} else {
zeroStartRequestsList.push(request);
}
}
}
return zeroStartRequestsList.concat(requestsList);
}
}
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum RecordType {
Task = 'RunTask',
Program = 'Program',
EventDispatch = 'EventDispatch',
GPUTask = 'GPUTask',
Animation = 'Animation',
RequestMainThreadFrame = 'RequestMainThreadFrame',
BeginFrame = 'BeginFrame',
NeedsBeginFrameChanged = 'NeedsBeginFrameChanged',
BeginMainThreadFrame = 'BeginMainThreadFrame',
ActivateLayerTree = 'ActivateLayerTree',
DrawFrame = 'DrawFrame',
DroppedFrame = 'DroppedFrame',
HitTest = 'HitTest',
ScheduleStyleRecalculation = 'ScheduleStyleRecalculation',
RecalculateStyles = 'RecalculateStyles',
UpdateLayoutTree = 'UpdateLayoutTree',
InvalidateLayout = 'InvalidateLayout',
Layout = 'Layout',
LayoutShift = 'LayoutShift',
UpdateLayer = 'UpdateLayer',
UpdateLayerTree = 'UpdateLayerTree',
PaintSetup = 'PaintSetup',
Paint = 'Paint',
PaintImage = 'PaintImage',
Rasterize = 'Rasterize',
RasterTask = 'RasterTask',
ScrollLayer = 'ScrollLayer',
CompositeLayers = 'CompositeLayers',
InteractiveTime = 'InteractiveTime',
ScheduleStyleInvalidationTracking = 'ScheduleStyleInvalidationTracking',
StyleRecalcInvalidationTracking = 'StyleRecalcInvalidationTracking',
StyleInvalidatorInvalidationTracking = 'StyleInvalidatorInvalidationTracking',
LayoutInvalidationTracking = 'LayoutInvalidationTracking',
ParseHTML = 'ParseHTML',
ParseAuthorStyleSheet = 'ParseAuthorStyleSheet',
TimerInstall = 'TimerInstall',
TimerRemove = 'TimerRemove',
TimerFire = 'TimerFire',
XHRReadyStateChange = 'XHRReadyStateChange',
XHRLoad = 'XHRLoad',
CompileScript = 'v8.compile',
EvaluateScript = 'EvaluateScript',
CompileModule = 'v8.compileModule',
EvaluateModule = 'v8.evaluateModule',
WasmStreamFromResponseCallback = 'v8.wasm.streamFromResponseCallback',
WasmCompiledModule = 'v8.wasm.compiledModule',
WasmCachedModule = 'v8.wasm.cachedModule',
WasmModuleCacheHit = 'v8.wasm.moduleCacheHit',
WasmModuleCacheInvalid = 'v8.wasm.moduleCacheInvalid',
FrameStartedLoading = 'FrameStartedLoading',
CommitLoad = 'CommitLoad',
MarkLoad = 'MarkLoad',
MarkDOMContent = 'MarkDOMContent',
MarkFirstPaint = 'firstPaint',
MarkFCP = 'firstContentfulPaint',
MarkLCPCandidate = 'largestContentfulPaint::Candidate',
MarkLCPInvalidate = 'largestContentfulPaint::Invalidate',
NavigationStart = 'navigationStart',
TimeStamp = 'TimeStamp',
ConsoleTime = 'ConsoleTime',
UserTiming = 'UserTiming',
ResourceWillSendRequest = 'ResourceWillSendRequest',
ResourceSendRequest = 'ResourceSendRequest',
ResourceReceiveResponse = 'ResourceReceiveResponse',
ResourceReceivedData = 'ResourceReceivedData',
ResourceFinish = 'ResourceFinish',
ResourceMarkAsCached = 'ResourceMarkAsCached',
RunMicrotasks = 'RunMicrotasks',
FunctionCall = 'FunctionCall',
GCEvent = 'GCEvent',
MajorGC = 'MajorGC',
MinorGC = 'MinorGC',
JSFrame = 'JSFrame',
JSSample = 'JSSample',
// V8Sample events are coming from tracing and contain raw stacks with function addresses.
// After being processed with help of JitCodeAdded and JitCodeMoved events they
// get translated into function infos and stored as stacks in JSSample events.
V8Sample = 'V8Sample',
JitCodeAdded = 'JitCodeAdded',
JitCodeMoved = 'JitCodeMoved',
StreamingCompileScript = 'v8.parseOnBackground',
StreamingCompileScriptWaiting = 'v8.parseOnBackgroundWaiting',
StreamingCompileScriptParsing = 'v8.parseOnBackgroundParsing',
V8Execute = 'V8.Execute',
UpdateCounters = 'UpdateCounters',
RequestAnimationFrame = 'RequestAnimationFrame',
CancelAnimationFrame = 'CancelAnimationFrame',
FireAnimationFrame = 'FireAnimationFrame',
RequestIdleCallback = 'RequestIdleCallback',
CancelIdleCallback = 'CancelIdleCallback',
FireIdleCallback = 'FireIdleCallback',
WebSocketCreate = 'WebSocketCreate',
WebSocketSendHandshakeRequest = 'WebSocketSendHandshakeRequest',
WebSocketReceiveHandshakeResponse = 'WebSocketReceiveHandshakeResponse',
WebSocketDestroy = 'WebSocketDestroy',
EmbedderCallback = 'EmbedderCallback',
SetLayerTreeId = 'SetLayerTreeId',
TracingStartedInPage = 'TracingStartedInPage',
TracingSessionIdForWorker = 'TracingSessionIdForWorker',
DecodeImage = 'Decode Image',
ResizeImage = 'Resize Image',
DrawLazyPixelRef = 'Draw LazyPixelRef',
DecodeLazyPixelRef = 'Decode LazyPixelRef',
LazyPixelRef = 'LazyPixelRef',
LayerTreeHostImplSnapshot = 'cc::LayerTreeHostImpl',
PictureSnapshot = 'cc::Picture',
DisplayItemListSnapshot = 'cc::DisplayItemList',
LatencyInfo = 'LatencyInfo',
LatencyInfoFlow = 'LatencyInfo.Flow',
InputLatencyMouseMove = 'InputLatency::MouseMove',
InputLatencyMouseWheel = 'InputLatency::MouseWheel',
ImplSideFling = 'InputHandlerProxy::HandleGestureFling::started',
GCCollectGarbage = 'BlinkGC.AtomicPhase',
CryptoDoEncrypt = 'DoEncrypt',
CryptoDoEncryptReply = 'DoEncryptReply',
CryptoDoDecrypt = 'DoDecrypt',
CryptoDoDecryptReply = 'DoDecryptReply',
CryptoDoDigest = 'DoDigest',
CryptoDoDigestReply = 'DoDigestReply',
CryptoDoSign = 'DoSign',
CryptoDoSignReply = 'DoSignReply',
CryptoDoVerify = 'DoVerify',
CryptoDoVerifyReply = 'DoVerifyReply',
// CpuProfile is a virtual event created on frontend to support
// serialization of CPU Profiles within tracing timeline data.
CpuProfile = 'CpuProfile',
Profile = 'Profile',
AsyncTask = 'AsyncTask',
}
export namespace TimelineModelImpl {
export const Category = {
Console: 'blink.console',
UserTiming: 'blink.user_timing',
LatencyInfo: 'latencyInfo',
Loading: 'loading',
};
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line rulesdir/const_enum
export enum WarningType {
LongTask = 'LongTask',
ForcedStyle = 'ForcedStyle',
ForcedLayout = 'ForcedLayout',
IdleDeadlineExceeded = 'IdleDeadlineExceeded',
LongHandler = 'LongHandler',
LongRecurringHandler = 'LongRecurringHandler',
V8Deopt = 'V8Deopt',
}
export const WorkerThreadName = 'DedicatedWorker thread';
export const WorkerThreadNameLegacy = 'DedicatedWorker Thread';
export const RendererMainThreadName = 'CrRendererMain';
export const BrowserMainThreadName = 'CrBrowserMain';
export const DevToolsMetadataEvent = {
TracingStartedInBrowser: 'TracingStartedInBrowser',
TracingStartedInPage: 'TracingStartedInPage',
TracingSessionIdForWorker: 'TracingSessionIdForWorker',
FrameCommittedInBrowser: 'FrameCommittedInBrowser',
ProcessReadyInBrowser: 'ProcessReadyInBrowser',
FrameDeletedInBrowser: 'FrameDeletedInBrowser',
};
export const Thresholds = {
LongTask: 50,
Handler: 150,
RecurringHandler: 50,
ForcedLayout: 30,
IdleCallbackAddon: 5,
};
}
export class Track {
name: string;
type: TrackType;
forMainFrame: boolean;
url: string;
events: SDK.TracingModel.Event[];
asyncEvents: SDK.TracingModel.AsyncEvent[];
tasks: SDK.TracingModel.Event[];
_syncEvents: SDK.TracingModel.Event[]|null;
thread: SDK.TracingModel.Thread|null;
constructor() {
this.name = '';
this.type = TrackType.Other;
// TODO(dgozman): replace forMainFrame with a list of frames, urls and time ranges.
this.forMainFrame = false;
this.url = '';
// TODO(dgozman): do not distinguish between sync and async events.
this.events = [];
this.asyncEvents = [];
this.tasks = [];
this._syncEvents = null;
this.thread = null;
}
syncEvents(): SDK.TracingModel.Event[] {
if (this.events.length) {
return this.events;
}
if (this._syncEvents) {
return this._syncEvents;
}
const stack: SDK.TracingModel.Event[] = [];
function peekLastEndTime(): number {
const last = stack[stack.length - 1];
if (last !== undefined) {
const endTime = last.endTime;
if (endTime !== undefined) {
return endTime;
}
}
throw new Error('End time does not exist on event.');
}
this._syncEvents = [];
for (const event of this.asyncEvents) {
const startTime = event.startTime;
let endTime: number|(number | undefined) = event.endTime;
if (endTime === undefined) {
endTime = startTime;
}
while (stack.length && startTime >= peekLastEndTime()) {
stack.pop();
}
if (stack.length && endTime > peekLastEndTime()) {
this._syncEvents = [];
break;
}
const syncEvent = new SDK.TracingModel.Event(
event.categoriesString, event.name, SDK.TracingModel.Phase.Complete, startTime, event.thread);
syncEvent.setEndTime(endTime);
syncEvent.addArgs(event.args);
this._syncEvents.push(syncEvent);
stack.push(syncEvent);
}
return this._syncEvents;
}
}
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum TrackType {
MainThread = 'MainThread',
Worker = 'Worker',
Input = 'Input',
Animation = 'Animation',
Timings = 'Timings',
Console = 'Console',
Raster = 'Raster',
GPU = 'GPU',
Experience = 'Experience',
Other = 'Other',
}
export class PageFrame {
frameId: any;
url: any;
name: any;
children: PageFrame[];
parent: PageFrame|null;
processes: {
time: number,
processId: number,
processPseudoId: string|null,
url: string,
}[];
deletedTime: number|null;
ownerNode: SDK.DOMModel.DeferredDOMNode|null;
constructor(payload: any) {
this.frameId = payload['frame'];
this.url = payload['url'] || '';
this.name = payload['name'];
this.children = [];
this.parent = null;
this.processes = [];
this.deletedTime = null;
// TODO(dgozman): figure this out.
// this.ownerNode = target && payload['nodeId'] ? new SDK.DOMModel.DeferredDOMNode(target, payload['nodeId']) : null;
this.ownerNode = null;
}
update(time: number, payload: any): void {
this.url = payload['url'] || '';
this.name = payload['name'];
if (payload['processId']) {
this.processes.push(
{time: time, processId: payload['processId'], processPseudoId: '', url: payload['url'] || ''});
} else {
this.processes.push(
{time: time, processId: -1, processPseudoId: payload['processPseudoId'], url: payload['url'] || ''});
}
}
processReady(processPseudoId: string, processId: number): void {
for (const process of this.processes) {
if (process.processPseudoId === processPseudoId) {
process.processPseudoId = '';
process.processId = processId;
}
}
}
addChild(child: PageFrame): void {
this.children.push(child);
child.parent = this;
}
}
export class NetworkRequest {
startTime: number;
endTime: number;
encodedDataLength: number;
decodedBodyLength: number;
children: SDK.TracingModel.Event[];
timing!: {
pushStart: number,
requestTime: number,
sendStart: number,
receiveHeadersEnd: number,
};
mimeType!: string;
url!: string;
requestMethod!: string;
_transferSize: number;
_maybeDiskCached: boolean;
_memoryCached: boolean;
priority?: any;
finishTime?: number;
responseTime?: number;
fromServiceWorker?: boolean;
hasCachedResource?: boolean;
constructor(event: SDK.TracingModel.Event) {
const isInitial =
event.name === RecordType.ResourceSendRequest || event.name === RecordType.ResourceWillSendRequest;
this.startTime = isInitial ? event.startTime : 0;
this.endTime = Infinity;
this.encodedDataLength = 0;
this.decodedBodyLength = 0;
this.children = [];
this._transferSize = 0;
this._maybeDiskCached = false;
this._memoryCached = false;
this.addEvent(event);
}
addEvent(event: SDK.TracingModel.Event): void {
this.children.push(event);
// This Math.min is likely because of BUG(chromium:865066).
this.startTime = Math.min(this.startTime, event.startTime);
const eventData = event.args['data'];
if (eventData['mimeType']) {
this.mimeType = eventData['mimeType'];
}
if ('priority' in eventData) {
this.priority = eventData['priority'];
}
if (event.name === RecordType.ResourceFinish) {
this.endTime = event.startTime;
}
if (eventData['finishTime']) {
this.finishTime = eventData['finishTime'] * 1000;
}
if (!this.responseTime &&
(event.name === RecordType.ResourceReceiveResponse || event.name === RecordType.ResourceReceivedData)) {
this.responseTime = event.startTime;
}
const encodedDataLength = eventData['encodedDataLength'] || 0;
if (event.name === RecordType.ResourceMarkAsCached) {
// This is a reliable signal for memory caching.
this._memoryCached = true;
}
if (event.name === RecordType.ResourceReceiveResponse) {
if (eventData['fromCache']) {
// See BUG(chromium:998397): back-end over-approximates caching.
this._maybeDiskCached = true;
}
if (eventData['fromServiceWorker']) {
this.fromServiceWorker = true;
}
if (eventData['hasCachedResource']) {
this.hasCachedResource = true;
}
this.encodedDataLength = encodedDataLength;
}
if (event.name === RecordType.ResourceReceivedData) {
this.encodedDataLength += encodedDataLength;
}
if (event.name === RecordType.ResourceFinish && encodedDataLength) {
this.encodedDataLength = encodedDataLength;
// If a ResourceFinish event with an encoded data length is received,
// then the resource was not cached; it was fetched before it was
// requested, e.g. because it was pushed in this navigation.
this._transferSize = encodedDataLength;
}
const decodedBodyLength = eventData['decodedBodyLength'];
if (event.name === RecordType.ResourceFinish && decodedBodyLength) {
this.decodedBodyLength = decodedBodyLength;
}
if (!this.url) {
this.url = eventData['url'];
}
if (!this.requestMethod) {
this.requestMethod = eventData['requestMethod'];
}
if (!this.timing) {
this.timing = eventData['timing'];
}
if (eventData['fromServiceWorker']) {
this.fromServiceWorker = true;
}
}
/**
* Return whether this request was cached. This works around BUG(chromium:998397),
* which reports pushed resources, and resources serverd by a service worker as
* disk cached. Pushed resources that were not disk cached, however, have a non-zero
* `_transferSize`.
*/
cached(): boolean {
return Boolean(this._memoryCached) ||
(Boolean(this._maybeDiskCached) && !this._transferSize && !this.fromServiceWorker);
}
/**
* Return whether this request was served from a memory cache.
*/
memoryCached(): boolean {
return this._memoryCached;
}
/**
* Get the timing information for this request. If the request was cached,
* the timing refers to the original (uncached) load, and should not be used.
*/
getSendReceiveTiming(): {
sendStartTime: number,
headersEndTime: number,
} {
if (this.cached() || !this.timing) {
// If the request is served from cache, the timing refers to the original
// resource load, and should not be used.
return {sendStartTime: this.startTime, headersEndTime: this.startTime};
}
const requestTime = this.timing.requestTime * 1000;
const sendStartTime = requestTime + this.timing.sendStart;
const headersEndTime = requestTime + this.timing.receiveHeadersEnd;
return {sendStartTime, headersEndTime};
}
/**
* Get the start time of this request, i.e. the time when the browser or
* renderer queued this request. There are two cases where request time is
* earlier than `startTime`: (1) if the request is served from cache, because
* it refers to the original load of the resource. (2) if the request was
* initiated by the browser instead of the renderer. Only in case (2) the
* the request time must be used instead of the start time to work around
* BUG(chromium:865066).
*/
getStartTime(): number {
return Math.min(this.startTime, !this.cached() && this.timing && this.timing.requestTime * 1000 || Infinity);
}
/**
* Returns the time where the earliest event belonging to this request starts.
* This differs from `getStartTime()` if a previous HTTP/2 request pushed the
* resource proactively: Then `beginTime()` refers to the time the push was received.
*/
beginTime(): number {
// `pushStart` is referring to the original push if the request was cached (i.e. in
// general not the most recent push), and should hence only be used for requests that were not cached.
return Math.min(this.getStartTime(), !this.cached() && this.timing && this.timing.pushStart * 1000 || Infinity);
}
}
export class InvalidationTrackingEvent {
type: string;
startTime: number;
_tracingEvent: SDK.TracingModel.Event;
frame: number;
nodeId: number|null;
nodeName: string|null;
invalidationSet: number|null;
invalidatedSelectorId: string|null;
changedId: string|null;
changedClass: string|null;
changedAttribute: string|null;
changedPseudo: string|null;
selectorPart: string|null;
extraData: string|null;
invalidationList: {
[x: string]: number,
}[]|null;
cause: InvalidationCause;
linkedRecalcStyleEvent: boolean;
linkedLayoutEvent: boolean;
constructor(event: SDK.TracingModel.Event) {
this.type = event.name;
this.startTime = event.startTime;
this._tracingEvent = event;
const eventData = event.args['data'];
this.frame = eventData['frame'];
this.nodeId = eventData['nodeId'];
this.nodeName = eventData['nodeName'];
this.invalidationSet = eventData['invalidationSet'];
this.invalidatedSelectorId = eventData['invalidatedSelectorId'];
this.changedId = eventData['changedId'];
this.changedClass = eventData['changedClass'];
this.changedAttribute = eventData['changedAttribute'];
this.changedPseudo = eventData['changedPseudo'];
this.selectorPart = eventData['selectorPart'];
this.extraData = eventData['extraData'];
this.invalidationList = eventData['invalidationList'];
this.cause = {reason: eventData['reason'], stackTrace: eventData['stackTrace']};
this.linkedRecalcStyleEvent = false;
this.linkedLayoutEvent = false;
// FIXME: Move this to TimelineUIUtils.js.
if (!this.cause.reason && this.cause.stackTrace && this.type === RecordType.LayoutInvalidationTracking) {
this.cause.reason = 'Layout forced';
}
}
}
export class InvalidationTracker {
_lastRecalcStyle: SDK.TracingModel.Event|null;
_lastPaintWithLayer: SDK.TracingModel.Event|null;
_didPaint: boolean;
_invalidations: {
[x: string]: InvalidationTrackingEvent[],
};
_invalidationsByNodeId: {
[x: number]: InvalidationTrackingEvent[],
};
constructor() {
this._lastRecalcStyle = null;
this._lastPaintWithLayer = null;
this._didPaint = false;
this._initializePerFrameState();
this._invalidations = {};
this._invalidationsByNodeId = {};
}
static invalidationEventsFor(event: SDK.TracingModel.Event): InvalidationTrackingEvent[]|null {
return eventToInvalidation.get(event) || null;
}
addInvalidation(invalidation: InvalidationTrackingEvent): void {
this._startNewFrameIfNeeded();
if (!invalidation.nodeId) {
console.error('Invalidation lacks node information.');
console.error(invalidation);
return;
}
// Suppress StyleInvalidator StyleRecalcInvalidationTracking invalidations because they
// will be handled by StyleInvalidatorInvalidationTracking.
// FIXME: Investigate if we can remove StyleInvalidator invalidations entirely.
if (invalidation.type === RecordType.StyleRecalcInvalidationTracking &&
invalidation.cause.reason === 'StyleInvalidator') {
return;
}
// Style invalidation events can occur before and during recalc style. didRecalcStyle
// handles style invalidations that occur before the recalc style event but we need to
// handle style recalc invalidations during recalc style here.
const styleRecalcInvalidation =
(invalidation.type === RecordType.ScheduleStyleInvalidationTracking ||
invalidation.type === RecordType.StyleInvalidatorInvalidationTracking ||
invalidation.type === RecordType.StyleRecalcInvalidationTracking);
if (styleRecalcInvalidation) {
const duringRecalcStyle = invalidation.startTime && this._lastRecalcStyle &&
this._lastRecalcStyle.endTime !== undefined && invalidation.startTime >= this._lastRecalcStyle.startTime &&
invalidation.startTime <= this._lastRecalcStyle.endTime;
if (duringRecalcStyle) {
this._associateWithLastRecalcStyleEvent(invalidation);
}
}
// Record the invalidation so later events can look it up.
if (this._invalidations[invalidation.type]) {
this._invalidations[invalidation.type].push(invalidation);
} else {
this._invalidations[invalidation.type] = [invalidation];
}
if (invalidation.nodeId) {
if (this._invalidationsByNodeId[invalidation.nodeId]) {
this._invalidationsByNodeId[invalidation.nodeId].push(invalidation);
} else {
this._invalidationsByNodeId[invalidation.nodeId] = [invalidation];
}
}
}
didRecalcStyle(recalcStyleEvent: SDK.TracingModel.Event): void {
this._lastRecalcStyle = recalcStyleEvent;
const types = [
RecordType.ScheduleStyleInvalidationTracking,
RecordType.StyleInvalidatorInvalidationTracking,
RecordType.StyleRecalcInvalidationTracking,
];
for (const invalidation of this._invalidationsOfTypes(types)) {
this._associateWithLastRecalcStyleEvent(invalidation);
}
}
_associateWithLastRecalcStyleEvent(invalidation: InvalidationTrackingEvent): void {
if (invalidation.linkedRecalcStyleEvent) {
return;
}
if (!this._lastRecalcStyle) {
throw new Error('Last recalculate style event not set.');
}
const recalcStyleFrameId = this._lastRecalcStyle.args['beginData']['frame'];
if (invalidation.type === RecordType.StyleInvalidatorInvalidationTracking) {
// Instead of calling _addInvalidationToEvent directly, we create synthetic
// StyleRecalcInvalidationTracking events which will be added in _addInvalidationToEvent.
this._addSyntheticStyleRecalcInvalidations(this._lastRecalcStyle, recalcStyleFrameId, invalidation);
} else if (invalidation.type === RecordType.ScheduleStyleInvalidationTracking) {
// ScheduleStyleInvalidationTracking events are only used for adding information to
// StyleInvalidatorInvalidationTracking events. See: _addSyntheticStyleRecalcInvalidations.
} else {
this._addInvalidationToEvent(this._lastRecalcStyle, recalcStyleFrameId, invalidation);
}
invalidation.linkedRecalcStyleEvent = true;
}
_addSyntheticStyleRecalcInvalidations(
event: SDK.TracingModel.Event, frameId: number, styleInvalidatorInvalidation: InvalidationTrackingEvent): void {
if (!styleInvalidatorInvalidation.invalidationList) {
this._addSyntheticStyleRecalcInvalidation(
styleInvalidatorInvalidation._tracingEvent, styleInvalidatorInvalidation);
return;
}
if (!styleInvalidatorInvalidation.nodeId) {
console.error('Invalidation lacks node information.');
console.error(styleInvalidatorInvalidation);
return;
}
for (let i = 0; i < styleInvalidatorInvalidation.invalidationList.length; i++) {
const setId = styleInvalidatorInvalidation.invalidationList[i]['id'];
let lastScheduleStyleRecalculation;
const nodeInvalidations = this._invalidationsByNodeId[styleInvalidatorInvalidation.nodeId] || [];
for (let j = 0; j < nodeInvalidations.length; j++) {
const invalidation = nodeInvalidations[j];
if (invalidation.frame !== frameId || invalidation.invalidationSet !== setId ||
invalidation.type !== RecordType.ScheduleStyleInvalidationTracking) {
continue;
}
lastScheduleStyleRecalculation = invalidation;
}
if (!lastScheduleStyleRecalculation) {
console.error('Failed to lookup the event that scheduled a style invalidator invalidation.');
continue;
}
this._addSyntheticStyleRecalcInvalidation(
lastScheduleStyleRecalculation._tracingEvent, styleInvalidatorInvalidation);
}
}
_addSyntheticStyleRecalcInvalidation(
baseEvent: SDK.TracingModel.Event, styleInvalidatorInvalidation: InvalidationTrackingEvent): void {
const invalidation = new InvalidationTrackingEvent(baseEvent);
invalidation.type = RecordType.StyleRecalcInvalidationTracking;
if (styleInvalidatorInvalidation.cause.reason) {
invalidation.cause.reason = styleInvalidatorInvalidation.cause.reason;
}
if (styleInvalidatorInvalidation.selectorPart) {
invalidation.selectorPart = styleInvalidatorInvalidation.selectorPart;
}
this.addInvalidation(invalidation);
if (!invalidation.linkedRecalcStyleEvent) {
this._associateWithLastRecalcStyleEvent(invalidation);
}
}
didLayout(layoutEvent: SDK.TracingModel.Event): void {
const layoutFrameId = layoutEvent.args['beginData']['frame'];
for (const invalidation of this._invalidationsOfTypes([RecordType.LayoutInvalidationTracking])) {
if (invalidation.linkedLayoutEvent) {
continue;
}
this._addInvalidationToEvent(layoutEvent, layoutFrameId, invalidation);
invalidation.linkedLayoutEvent = true;
}
}
didPaint(_paintEvent: SDK.TracingModel.Event): void {
this._didPaint = true;
}
_addInvalidationToEvent(event: SDK.TracingModel.Event, eventFrameId: number, invalidation: InvalidationTrackingEvent):
void {
if (eventFrameId !== invalidation.frame) {
return;
}
const invalidations = eventToInvalidation.get(event);
if (!invalidations) {
eventToInvalidation.set(event, [invalidation]);
} else {
invalidations.push(invalidation);
}
}
_invalidationsOfTypes(types?: string[]): Generator<InvalidationTrackingEvent, any, any> {
const invalidations = this._invalidations;
if (!types) {
types = Object.keys(invalidations);
}
function* generator(): Generator<InvalidationTrackingEvent, void, unknown> {
if (!types) {
return;
}
for (let i = 0; i < types.length; ++i) {
const invalidationList = invalidations[types[i]] || [];
for (let j = 0; j < invalidationList.length; ++j) {
yield invalidationList[j];
}
}
}
return generator();
}
_startNewFrameIfNeeded(): void {
if (!this._didPaint) {
return;
}
this._initializePerFrameState();
}
_initializePerFrameState(): void {
this._invalidations = {};
this._invalidationsByNodeId = {};
this._lastRecalcStyle = null;
this._lastPaintWithLayer = null;
this._didPaint = false;
}
}
export class TimelineAsyncEventTracker {
_initiatorByType: Map<RecordType, Map<RecordType, SDK.TracingModel.Event>>;
constructor() {
TimelineAsyncEventTracker._initialize();
this._initiatorByType = new Map();
if (TimelineAsyncEventTracker._asyncEvents) {
for (const initiator of TimelineAsyncEventTracker._asyncEvents.keys()) {
this._initiatorByType.set(initiator, new Map());
}
}
}
static _initialize(): void {
if (TimelineAsyncEventTracker._asyncEvents) {
return;
}
const events = new Map<RecordType, {
causes: RecordType[],
joinBy: string,
}>();
events.set(RecordType.TimerInstall, {causes: [RecordType.TimerFire], joinBy: 'timerId'});
events.set(RecordType.ResourceSendRequest, {
causes: [
RecordType.ResourceMarkAsCached,
RecordType.ResourceReceiveResponse,
RecordType.ResourceReceivedData,
RecordType.ResourceFinish,
],
joinBy: 'requestId',
});
events.set(RecordType.RequestAnimationFrame, {causes: [RecordType.FireAnimationFrame], joinBy: 'id'});
events.set(RecordType.RequestIdleCallback, {causes: [RecordType.FireIdleCallback], joinBy: 'id'});
events.set(RecordType.WebSocketCreate, {
causes: [
RecordType.WebSocketSendHandshakeRequest,
RecordType.WebSocketReceiveHandshakeResponse,
RecordType.WebSocketDestroy,
],
joinBy: 'identifier',
});
TimelineAsyncEventTracker._asyncEvents = events;
TimelineAsyncEventTracker._typeToInitiator = new Map();
for (const entry of events) {
const types = entry[1].causes;
for (const currentType of types) {
TimelineAsyncEventTracker._typeToInitiator.set(currentType, entry[0]);
}
}
}
processEvent(event: SDK.TracingModel.Event): void {
if (!TimelineAsyncEventTracker._typeToInitiator || !TimelineAsyncEventTracker._asyncEvents) {
return;
}
let initiatorType: RecordType|undefined =
TimelineAsyncEventTracker._typeToInitiator.get((event.name as RecordType));
const isInitiator = !initiatorType;
if (!initiatorType) {
initiatorType = (event.name as RecordType);
}
const initiatorInfo = TimelineAsyncEventTracker._asyncEvents.get(initiatorType);
if (!initiatorInfo) {
return;
}
const id = (TimelineModelImpl.globalEventId(event, initiatorInfo.joinBy) as RecordType);
if (!id) {
return;
}
const initiatorMap: Map<RecordType, SDK.TracingModel.Event>|undefined = this._initiatorByType.get(initiatorType);
if (initiatorMap) {
if (isInitiator) {
initiatorMap.set(id, event);
return;
}
const initiator = initiatorMap.get(id);
const timelineData = TimelineData.forEvent(event);
timelineData.setInitiator(initiator ? initiator : null);
if (!timelineData.frameId && initiator) {
timelineData.frameId = TimelineModelImpl.eventFrameId(initiator);
}
}
}
static _asyncEvents: Map<RecordType, {causes: RecordType[], joinBy: string}>|null = null;
static _typeToInitiator: Map<RecordType, RecordType>|null = null;
}
export class TimelineData {
warning: string|null;
previewElement: Element|null;
url: string|null;
backendNodeId: number;
stackTrace: Protocol.Runtime.CallFrame[]|null;
picture: SDK.TracingModel.ObjectSnapshot|null;
_initiator: SDK.TracingModel.Event|null;
frameId: string;
timeWaitingForMainThread?: number;
constructor() {
this.warning = null;
this.previewElement = null;
this.url = null;
this.backendNodeId = 0;
this.stackTrace = null;
this.picture = null;
this._initiator = null;
this.frameId = '';
}
setInitiator(initiator: SDK.TracingModel.Event|null): void {
this._initiator = initiator;
if (!initiator || this.url) {
return;
}
const initiatorURL = TimelineData.forEvent(initiator).url;
if (initiatorURL) {
this.url = initiatorURL;
}
}
initiator(): SDK.TracingModel.Event|null {
return this._initiator;
}
topFrame(): Protocol.Runtime.CallFrame|null {
const stackTrace = this.stackTraceForSelfOrInitiator();
return stackTrace && stackTrace[0] || null;
}
stackTraceForSelfOrInitiator(): Protocol.Runtime.CallFrame[]|null {
return this.stackTrace || (this._initiator && TimelineData.forEvent(this._initiator).stackTrace);
}
static forEvent(event: SDK.TracingModel.Event): TimelineData {
let data = eventToData.get(event);
if (!data) {
data = new TimelineData();
eventToData.set(event, data);
}
return data;
}
}
const eventToData = new WeakMap();
const eventToInvalidation = new WeakMap();
export interface InvalidationCause {
reason: string;
stackTrace: Protocol.Runtime.CallFrame[]|null;
}
export interface MetadataEvents {
page: SDK.TracingModel.Event[];
workers: SDK.TracingModel.Event[];
}