blob: cb78e363b6729669a81a3b8cb70d54bcaa469971 [file] [log] [blame]
/*
* Copyright (C) 2013 Google Inc. All rights reserved.
* Copyright (C) 2012 Intel 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.
*/
/**
* @unrestricted
*/
Timeline.TimelineUIUtils = class {
/**
* @return {!Object.<string, !Timeline.TimelineRecordStyle>}
*/
static _initEventStyles() {
if (Timeline.TimelineUIUtils._eventStylesMap) {
return Timeline.TimelineUIUtils._eventStylesMap;
}
const type = TimelineModel.TimelineModel.RecordType;
const categories = Timeline.TimelineUIUtils.categories();
const rendering = categories['rendering'];
const scripting = categories['scripting'];
const loading = categories['loading'];
const painting = categories['painting'];
const other = categories['other'];
const idle = categories['idle'];
const eventStyles = {};
eventStyles[type.Task] = new Timeline.TimelineRecordStyle(ls`Task`, other);
eventStyles[type.Program] = new Timeline.TimelineRecordStyle(ls`Other`, other);
eventStyles[type.Animation] = new Timeline.TimelineRecordStyle(ls`Animation`, rendering);
eventStyles[type.EventDispatch] = new Timeline.TimelineRecordStyle(ls`Event`, scripting);
eventStyles[type.RequestMainThreadFrame] =
new Timeline.TimelineRecordStyle(ls`Request Main Thread Frame`, rendering, true);
eventStyles[type.BeginFrame] = new Timeline.TimelineRecordStyle(ls`Frame Start`, rendering, true);
eventStyles[type.BeginMainThreadFrame] =
new Timeline.TimelineRecordStyle(ls`Frame Start (main thread)`, rendering, true);
eventStyles[type.DrawFrame] = new Timeline.TimelineRecordStyle(ls`Draw Frame`, rendering, true);
eventStyles[type.HitTest] = new Timeline.TimelineRecordStyle(ls`Hit Test`, rendering);
eventStyles[type.ScheduleStyleRecalculation] =
new Timeline.TimelineRecordStyle(ls`Schedule Style Recalculation`, rendering);
eventStyles[type.RecalculateStyles] = new Timeline.TimelineRecordStyle(ls`Recalculate Style`, rendering);
eventStyles[type.UpdateLayoutTree] = new Timeline.TimelineRecordStyle(ls`Recalculate Style`, rendering);
eventStyles[type.InvalidateLayout] = new Timeline.TimelineRecordStyle(ls`Invalidate Layout`, rendering, true);
eventStyles[type.Layout] = new Timeline.TimelineRecordStyle(ls`Layout`, rendering);
eventStyles[type.PaintSetup] = new Timeline.TimelineRecordStyle(ls`Paint Setup`, painting);
eventStyles[type.PaintImage] = new Timeline.TimelineRecordStyle(ls`Paint Image`, painting, true);
eventStyles[type.UpdateLayer] = new Timeline.TimelineRecordStyle(ls`Update Layer`, painting, true);
eventStyles[type.UpdateLayerTree] = new Timeline.TimelineRecordStyle(ls`Update Layer Tree`, rendering);
eventStyles[type.Paint] = new Timeline.TimelineRecordStyle(ls`Paint`, painting);
eventStyles[type.RasterTask] = new Timeline.TimelineRecordStyle(ls`Rasterize Paint`, painting);
eventStyles[type.ScrollLayer] = new Timeline.TimelineRecordStyle(ls`Scroll`, rendering);
eventStyles[type.CompositeLayers] = new Timeline.TimelineRecordStyle(ls`Composite Layers`, painting);
eventStyles[type.ParseHTML] = new Timeline.TimelineRecordStyle(ls`Parse HTML`, loading);
eventStyles[type.ParseAuthorStyleSheet] = new Timeline.TimelineRecordStyle(ls`Parse Stylesheet`, loading);
eventStyles[type.TimerInstall] = new Timeline.TimelineRecordStyle(ls`Install Timer`, scripting);
eventStyles[type.TimerRemove] = new Timeline.TimelineRecordStyle(ls`Remove Timer`, scripting);
eventStyles[type.TimerFire] = new Timeline.TimelineRecordStyle(ls`Timer Fired`, scripting);
eventStyles[type.XHRReadyStateChange] = new Timeline.TimelineRecordStyle(ls`XHR Ready State Change`, scripting);
eventStyles[type.XHRLoad] = new Timeline.TimelineRecordStyle(ls`XHR Load`, scripting);
eventStyles[type.CompileScript] = new Timeline.TimelineRecordStyle(ls`Compile Script`, scripting);
eventStyles[type.EvaluateScript] = new Timeline.TimelineRecordStyle(ls`Evaluate Script`, scripting);
eventStyles[type.CompileModule] = new Timeline.TimelineRecordStyle(ls`Compile Module`, scripting);
eventStyles[type.EvaluateModule] = new Timeline.TimelineRecordStyle(ls`Evaluate Module`, scripting);
eventStyles[type.StreamingCompileScript] = new Timeline.TimelineRecordStyle(ls`Streaming Compile Task`, other);
eventStyles[type.StreamingCompileScriptWaiting] = new Timeline.TimelineRecordStyle(ls`Waiting for Network`, idle);
eventStyles[type.StreamingCompileScriptParsing] =
new Timeline.TimelineRecordStyle(ls`Parse and Compile`, scripting);
eventStyles[type.WasmStreamFromResponseCallback] =
new Timeline.TimelineRecordStyle(ls`Streaming Wasm Response`, scripting);
eventStyles[type.WasmCompiledModule] = new Timeline.TimelineRecordStyle(ls`Compiled Wasm Module`, scripting);
eventStyles[type.WasmCachedModule] = new Timeline.TimelineRecordStyle(ls`Cached Wasm Module`, scripting);
eventStyles[type.WasmModuleCacheHit] = new Timeline.TimelineRecordStyle(ls`Wasm Module Cache Hit`, scripting);
eventStyles[type.WasmModuleCacheInvalid] =
new Timeline.TimelineRecordStyle(ls`Wasm Module Cache Invalid`, scripting);
eventStyles[type.FrameStartedLoading] = new Timeline.TimelineRecordStyle(ls`Frame Started Loading`, loading, true);
eventStyles[type.MarkLoad] = new Timeline.TimelineRecordStyle(ls`Onload Event`, scripting, true);
eventStyles[type.MarkDOMContent] = new Timeline.TimelineRecordStyle(ls`DOMContentLoaded Event`, scripting, true);
eventStyles[type.MarkFirstPaint] = new Timeline.TimelineRecordStyle(ls`First Paint`, painting, true);
eventStyles[type.MarkFCP] = new Timeline.TimelineRecordStyle(ls`First Contentful Paint`, rendering, true);
eventStyles[type.MarkFMP] = new Timeline.TimelineRecordStyle(ls`First Meaningful Paint`, rendering, true);
eventStyles[type.MarkLCPCandidate] =
new Timeline.TimelineRecordStyle(ls`Largest Contentful Paint`, rendering, true);
eventStyles[type.TimeStamp] = new Timeline.TimelineRecordStyle(ls`Timestamp`, scripting);
eventStyles[type.ConsoleTime] = new Timeline.TimelineRecordStyle(ls`Console Time`, scripting);
eventStyles[type.UserTiming] = new Timeline.TimelineRecordStyle(ls`User Timing`, scripting);
eventStyles[type.ResourceWillSendRequest] = new Timeline.TimelineRecordStyle(ls`Will Send Request`, loading);
eventStyles[type.ResourceSendRequest] = new Timeline.TimelineRecordStyle(ls`Send Request`, loading);
eventStyles[type.ResourceReceiveResponse] = new Timeline.TimelineRecordStyle(ls`Receive Response`, loading);
eventStyles[type.ResourceFinish] = new Timeline.TimelineRecordStyle(ls`Finish Loading`, loading);
eventStyles[type.ResourceReceivedData] = new Timeline.TimelineRecordStyle(ls`Receive Data`, loading);
eventStyles[type.RunMicrotasks] = new Timeline.TimelineRecordStyle(ls`Run Microtasks`, scripting);
eventStyles[type.FunctionCall] = new Timeline.TimelineRecordStyle(ls`Function Call`, scripting);
eventStyles[type.GCEvent] = new Timeline.TimelineRecordStyle(ls`GC Event`, scripting);
eventStyles[type.MajorGC] = new Timeline.TimelineRecordStyle(ls`Major GC`, scripting);
eventStyles[type.MinorGC] = new Timeline.TimelineRecordStyle(ls`Minor GC`, scripting);
eventStyles[type.JSFrame] = new Timeline.TimelineRecordStyle(ls`JS Frame`, scripting);
eventStyles[type.RequestAnimationFrame] = new Timeline.TimelineRecordStyle(ls`Request Animation Frame`, scripting);
eventStyles[type.CancelAnimationFrame] = new Timeline.TimelineRecordStyle(ls`Cancel Animation Frame`, scripting);
eventStyles[type.FireAnimationFrame] = new Timeline.TimelineRecordStyle(ls`Animation Frame Fired`, scripting);
eventStyles[type.RequestIdleCallback] = new Timeline.TimelineRecordStyle(ls`Request Idle Callback`, scripting);
eventStyles[type.CancelIdleCallback] = new Timeline.TimelineRecordStyle(ls`Cancel Idle Callback`, scripting);
eventStyles[type.FireIdleCallback] = new Timeline.TimelineRecordStyle(ls`Fire Idle Callback`, scripting);
eventStyles[type.WebSocketCreate] = new Timeline.TimelineRecordStyle(ls`Create WebSocket`, scripting);
eventStyles[type.WebSocketSendHandshakeRequest] =
new Timeline.TimelineRecordStyle(ls`Send WebSocket Handshake`, scripting);
eventStyles[type.WebSocketReceiveHandshakeResponse] =
new Timeline.TimelineRecordStyle(ls`Receive WebSocket Handshake`, scripting);
eventStyles[type.WebSocketDestroy] = new Timeline.TimelineRecordStyle(ls`Destroy WebSocket`, scripting);
eventStyles[type.EmbedderCallback] = new Timeline.TimelineRecordStyle(ls`Embedder Callback`, scripting);
eventStyles[type.DecodeImage] = new Timeline.TimelineRecordStyle(ls`Image Decode`, painting);
eventStyles[type.ResizeImage] = new Timeline.TimelineRecordStyle(ls`Image Resize`, painting);
eventStyles[type.GPUTask] = new Timeline.TimelineRecordStyle(ls`GPU`, categories['gpu']);
eventStyles[type.LatencyInfo] = new Timeline.TimelineRecordStyle(ls`Input Latency`, scripting);
eventStyles[type.GCCollectGarbage] = new Timeline.TimelineRecordStyle(ls`DOM GC`, scripting);
eventStyles[type.CryptoDoEncrypt] = new Timeline.TimelineRecordStyle(ls`Encrypt`, scripting);
eventStyles[type.CryptoDoEncryptReply] = new Timeline.TimelineRecordStyle(ls`Encrypt Reply`, scripting);
eventStyles[type.CryptoDoDecrypt] = new Timeline.TimelineRecordStyle(ls`Decrypt`, scripting);
eventStyles[type.CryptoDoDecryptReply] = new Timeline.TimelineRecordStyle(ls`Decrypt Reply`, scripting);
eventStyles[type.CryptoDoDigest] = new Timeline.TimelineRecordStyle(ls`Digest`, scripting);
eventStyles[type.CryptoDoDigestReply] = new Timeline.TimelineRecordStyle(ls`Digest Reply`, scripting);
eventStyles[type.CryptoDoSign] = new Timeline.TimelineRecordStyle(ls`Sign`, scripting);
eventStyles[type.CryptoDoSignReply] = new Timeline.TimelineRecordStyle(ls`Sign Reply`, scripting);
eventStyles[type.CryptoDoVerify] = new Timeline.TimelineRecordStyle(ls`Verify`, scripting);
eventStyles[type.CryptoDoVerifyReply] = new Timeline.TimelineRecordStyle(ls`Verify Reply`, scripting);
eventStyles[type.AsyncTask] = new Timeline.TimelineRecordStyle(ls`Async Task`, categories['async']);
Timeline.TimelineUIUtils._eventStylesMap = eventStyles;
return eventStyles;
}
static setEventStylesMap(eventStyles) {
Timeline.TimelineUIUtils._eventStylesMap = eventStyles;
}
/**
* @param {!TimelineModel.TimelineIRModel.InputEvents} inputEventType
* @return {?string}
*/
static inputEventDisplayName(inputEventType) {
if (!Timeline.TimelineUIUtils._inputEventToDisplayName) {
const inputEvent = TimelineModel.TimelineIRModel.InputEvents;
/** @type {!Map<!TimelineModel.TimelineIRModel.InputEvents, string>} */
Timeline.TimelineUIUtils._inputEventToDisplayName = new Map([
[inputEvent.Char, ls`Key Character`],
[inputEvent.KeyDown, ls`Key Down`],
[inputEvent.KeyDownRaw, ls`Key Down`],
[inputEvent.KeyUp, ls`Key Up`],
[inputEvent.Click, ls`Click`],
[inputEvent.ContextMenu, ls`Context Menu`],
[inputEvent.MouseDown, ls`Mouse Down`],
[inputEvent.MouseMove, ls`Mouse Move`],
[inputEvent.MouseUp, ls`Mouse Up`],
[inputEvent.MouseWheel, ls`Mouse Wheel`],
[inputEvent.ScrollBegin, ls`Scroll Begin`],
[inputEvent.ScrollEnd, ls`Scroll End`],
[inputEvent.ScrollUpdate, ls`Scroll Update`],
[inputEvent.FlingStart, ls`Fling Start`],
[inputEvent.FlingCancel, ls`Fling Halt`],
[inputEvent.Tap, ls`Tap`],
[inputEvent.TapCancel, ls`Tap Halt`],
[inputEvent.ShowPress, ls`Tap Begin`],
[inputEvent.TapDown, ls`Tap Down`],
[inputEvent.TouchCancel, ls`Touch Cancel`],
[inputEvent.TouchEnd, ls`Touch End`],
[inputEvent.TouchMove, ls`Touch Move`],
[inputEvent.TouchStart, ls`Touch Start`],
[inputEvent.PinchBegin, ls`Pinch Begin`],
[inputEvent.PinchEnd, ls`Pinch End`],
[inputEvent.PinchUpdate, ls`Pinch Update`]
]);
}
return Timeline.TimelineUIUtils._inputEventToDisplayName.get(inputEventType) || null;
}
/**
* @param {!Protocol.Runtime.CallFrame} frame
* @return {string}
*/
static frameDisplayName(frame) {
if (!TimelineModel.TimelineJSProfileProcessor.isNativeRuntimeFrame(frame)) {
return UI.beautifyFunctionName(frame.functionName);
}
const nativeGroup = TimelineModel.TimelineJSProfileProcessor.nativeGroup(frame.functionName);
const groups = TimelineModel.TimelineJSProfileProcessor.NativeGroups;
switch (nativeGroup) {
case groups.Compile:
return ls`Compile`;
case groups.Parse:
return ls`Parse`;
}
return frame.functionName;
}
/**
* @param {!SDK.TracingModel.Event} traceEvent
* @param {!RegExp} regExp
* @return {boolean}
*/
static testContentMatching(traceEvent, regExp) {
const title = Timeline.TimelineUIUtils.eventStyle(traceEvent).title;
const tokens = [title];
const url = TimelineModel.TimelineData.forEvent(traceEvent).url;
if (url) {
tokens.push(url);
}
appendObjectProperties(traceEvent.args, 2);
return regExp.test(tokens.join('|'));
/**
* @param {!Object} object
* @param {number} depth
*/
function appendObjectProperties(object, depth) {
if (!depth) {
return;
}
for (const key in object) {
const value = object[key];
const type = typeof value;
if (type === 'string') {
tokens.push(value);
} else if (type === 'number') {
tokens.push(String(value));
} else if (type === 'object') {
appendObjectProperties(value, depth - 1);
}
}
}
}
/**
* @param {!SDK.TracingModel.Event} event
* @return {?string}
*/
static eventURL(event) {
const data = event.args['data'] || event.args['beginData'];
const url = data && data.url;
if (url) {
return url;
}
const stackTrace = data && data['stackTrace'];
const frame =
stackTrace && stackTrace.length && stackTrace[0] || TimelineModel.TimelineData.forEvent(event).topFrame();
return frame && frame.url || null;
}
/**
* @param {!SDK.TracingModel.Event} event
* @return {!Timeline.TimelineRecordStyle}
*/
static eventStyle(event) {
const eventStyles = Timeline.TimelineUIUtils._initEventStyles();
if (event.hasCategory(TimelineModel.TimelineModel.Category.Console) ||
event.hasCategory(TimelineModel.TimelineModel.Category.UserTiming)) {
return new Timeline.TimelineRecordStyle(event.name, Timeline.TimelineUIUtils.categories()['scripting']);
}
if (event.hasCategory(TimelineModel.TimelineModel.Category.LatencyInfo)) {
/** @const */
const prefix = 'InputLatency::';
const inputEventType = event.name.startsWith(prefix) ? event.name.substr(prefix.length) : event.name;
const displayName = Timeline.TimelineUIUtils.inputEventDisplayName(
/** @type {!TimelineModel.TimelineIRModel.InputEvents} */ (inputEventType));
return new Timeline.TimelineRecordStyle(
displayName || inputEventType, Timeline.TimelineUIUtils.categories()['scripting']);
}
let result = eventStyles[event.name];
if (!result) {
result = new Timeline.TimelineRecordStyle(event.name, Timeline.TimelineUIUtils.categories()['other'], true);
eventStyles[event.name] = result;
}
return result;
}
/**
* @param {!SDK.TracingModel.Event} event
* @return {string}
*/
static eventColor(event) {
if (event.name === TimelineModel.TimelineModel.RecordType.JSFrame) {
const frame = event.args['data'];
if (Timeline.TimelineUIUtils.isUserFrame(frame)) {
return Timeline.TimelineUIUtils.colorForId(frame.url);
}
}
const color = Timeline.TimelineUIUtils.eventStyle(event).category.color;
// This event is considered idle time but still rendered as a scripting event here
// to connect the StreamingCompileScriptParsing events it belongs to.
if (event.name === TimelineModel.TimelineModel.RecordType.StreamingCompileScriptWaiting) {
return /** @type string */ (
Common.Color.parse(Timeline.TimelineUIUtils.categories().scripting.color).setAlpha(0.3).asString(null));
}
return color;
}
/**
* @param {!TimelineModel.TimelineModel} model
* @param {!Map<string, string>} urlToColorCache
* @param {!SDK.TracingModel.Event} event
* @return {string}
*/
static eventColorByProduct(model, urlToColorCache, event) {
const url = Timeline.TimelineUIUtils.eventURL(event) || '';
let color = urlToColorCache.get(url);
if (color) {
return color;
}
const defaultColor = '#f2ecdc';
const parsedURL = Common.ParsedURL.fromString(url);
if (!parsedURL) {
return defaultColor;
}
const name = parsedURL.host;
const rootFrames = model.rootFrames();
if (rootFrames.some(pageFrame => new Common.ParsedURL(pageFrame.url).host === name)) {
color = defaultColor;
}
if (!color) {
color = defaultColor;
}
urlToColorCache.set(url, color);
return color;
}
/**
* @param {!SDK.TracingModel.Event} event
* @return {string}
*/
static eventTitle(event) {
const recordType = TimelineModel.TimelineModel.RecordType;
const eventData = event.args['data'];
if (event.name === recordType.JSFrame) {
return Timeline.TimelineUIUtils.frameDisplayName(eventData);
}
const title = Timeline.TimelineUIUtils.eventStyle(event).title;
if (event.hasCategory(TimelineModel.TimelineModel.Category.Console)) {
return title;
}
if (event.name === recordType.TimeStamp) {
return ls`${title}: ${eventData['message']}`;
}
if (event.name === recordType.Animation && eventData && eventData['name']) {
return ls`${title}: ${eventData['name']}`;
}
if (event.name === recordType.EventDispatch && eventData && eventData['type']) {
return ls`${title}: ${eventData['type']}`;
}
return title;
}
/**
* !Map<!TimelineModel.TimelineIRModel.Phases, !{color: string, label: string}>
*/
static _interactionPhaseStyles() {
let map = Timeline.TimelineUIUtils._interactionPhaseStylesMap;
if (!map) {
map = new Map([
[TimelineModel.TimelineIRModel.Phases.Idle, {color: 'white', label: 'Idle'}],
[TimelineModel.TimelineIRModel.Phases.Response, {color: 'hsl(43, 83%, 64%)', label: ls`Response`}],
[TimelineModel.TimelineIRModel.Phases.Scroll, {color: 'hsl(256, 67%, 70%)', label: ls`Scroll`}],
[TimelineModel.TimelineIRModel.Phases.Fling, {color: 'hsl(256, 67%, 70%)', label: ls`Fling`}],
[TimelineModel.TimelineIRModel.Phases.Drag, {color: 'hsl(256, 67%, 70%)', label: ls`Drag`}],
[TimelineModel.TimelineIRModel.Phases.Animation, {color: 'hsl(256, 67%, 70%)', label: ls`Animation`}],
[TimelineModel.TimelineIRModel.Phases.Uncategorized, {color: 'hsl(0, 0%, 87%)', label: ls`Uncategorized`}]
]);
Timeline.TimelineUIUtils._interactionPhaseStylesMap = map;
}
return map;
}
/**
* @param {!TimelineModel.TimelineIRModel.Phases} phase
* @return {string}
*/
static interactionPhaseColor(phase) {
return Timeline.TimelineUIUtils._interactionPhaseStyles().get(phase).color;
}
/**
* @param {!TimelineModel.TimelineIRModel.Phases} phase
* @return {string}
*/
static interactionPhaseLabel(phase) {
return Timeline.TimelineUIUtils._interactionPhaseStyles().get(phase).label;
}
/**
* @param {!Protocol.Runtime.CallFrame} frame
* @return {boolean}
*/
static isUserFrame(frame) {
return frame.scriptId !== '0' && !(frame.url && frame.url.startsWith('native '));
}
/**
* @param {!TimelineModel.TimelineModel.NetworkRequest} request
* @return {!Timeline.TimelineUIUtils.NetworkCategory}
*/
static networkRequestCategory(request) {
const categories = Timeline.TimelineUIUtils.NetworkCategory;
switch (request.mimeType) {
case 'text/html':
return categories.HTML;
case 'application/javascript':
case 'application/x-javascript':
case 'text/javascript':
return categories.Script;
case 'text/css':
return categories.Style;
case 'audio/ogg':
case 'image/gif':
case 'image/jpeg':
case 'image/png':
case 'image/svg+xml':
case 'image/webp':
case 'image/x-icon':
case 'font/opentype':
case 'font/woff2':
case 'application/font-woff':
return categories.Media;
default:
return categories.Other;
}
}
/**
* @param {!Timeline.TimelineUIUtils.NetworkCategory} category
* @return {string}
*/
static networkCategoryColor(category) {
const categories = Timeline.TimelineUIUtils.NetworkCategory;
switch (category) {
case categories.HTML:
return 'hsl(214, 67%, 66%)';
case categories.Script:
return 'hsl(43, 83%, 64%)';
case categories.Style:
return 'hsl(256, 67%, 70%)';
case categories.Media:
return 'hsl(109, 33%, 55%)';
default:
return 'hsl(0, 0%, 70%)';
}
}
/**
* @param {!SDK.TracingModel.Event} event
* @param {?SDK.Target} target
* @return {?string}
*/
static buildDetailsTextForTraceEvent(event, target) {
const recordType = TimelineModel.TimelineModel.RecordType;
let detailsText;
const eventData = event.args['data'];
switch (event.name) {
case recordType.GCEvent:
case recordType.MajorGC:
case recordType.MinorGC: {
const delta = event.args['usedHeapSizeBefore'] - event.args['usedHeapSizeAfter'];
detailsText = Common.UIString('%s collected', Number.bytesToString(delta));
break;
}
case recordType.FunctionCall:
if (eventData) {
detailsText =
linkifyLocationAsText(eventData['scriptId'], eventData['lineNumber'], eventData['columnNumber']);
}
break;
case recordType.JSFrame:
detailsText = Timeline.TimelineUIUtils.frameDisplayName(eventData);
break;
case recordType.EventDispatch:
detailsText = eventData ? eventData['type'] : null;
break;
case recordType.Paint: {
const width = Timeline.TimelineUIUtils.quadWidth(eventData.clip);
const height = Timeline.TimelineUIUtils.quadHeight(eventData.clip);
if (width && height) {
detailsText = Common.UIString('%d\xa0\u00d7\xa0%d', width, height);
}
break;
}
case recordType.ParseHTML: {
const startLine = event.args['beginData']['startLine'];
const endLine = event.args['endData'] && event.args['endData']['endLine'];
const url = Bindings.displayNameForURL(event.args['beginData']['url']);
if (endLine >= 0) {
detailsText = Common.UIString('%s [%s\u2026%s]', url, startLine + 1, endLine + 1);
} else {
detailsText = Common.UIString('%s [%s\u2026]', url, startLine + 1);
}
break;
}
case recordType.CompileModule:
detailsText = Bindings.displayNameForURL(event.args['fileName']);
break;
case recordType.CompileScript:
case recordType.EvaluateScript: {
const url = eventData && eventData['url'];
if (url) {
detailsText = Bindings.displayNameForURL(url) + ':' + (eventData['lineNumber'] + 1);
}
break;
}
case recordType.WasmCompiledModule:
case recordType.WasmModuleCacheHit: {
const url = event.args['url'];
if (url) {
detailsText = Bindings.displayNameForURL(url);
}
break;
}
case recordType.StreamingCompileScript:
case recordType.XHRReadyStateChange:
case recordType.XHRLoad: {
const url = eventData['url'];
if (url) {
detailsText = Bindings.displayNameForURL(url);
}
break;
}
case recordType.TimeStamp:
detailsText = eventData['message'];
break;
case recordType.WebSocketCreate:
case recordType.WebSocketSendHandshakeRequest:
case recordType.WebSocketReceiveHandshakeResponse:
case recordType.WebSocketDestroy:
case recordType.ResourceWillSendRequest:
case recordType.ResourceSendRequest:
case recordType.ResourceReceivedData:
case recordType.ResourceReceiveResponse:
case recordType.ResourceFinish:
case recordType.PaintImage:
case recordType.DecodeImage:
case recordType.ResizeImage:
case recordType.DecodeLazyPixelRef: {
const url = TimelineModel.TimelineData.forEvent(event).url;
if (url) {
detailsText = Bindings.displayNameForURL(url);
}
break;
}
case recordType.EmbedderCallback:
detailsText = eventData['callbackName'];
break;
case recordType.Animation:
detailsText = eventData && eventData['name'];
break;
case recordType.AsyncTask:
detailsText = eventData ? eventData['name'] : null;
break;
default:
if (event.hasCategory(TimelineModel.TimelineModel.Category.Console)) {
detailsText = null;
} else {
detailsText = linkifyTopCallFrameAsText();
}
break;
}
return detailsText;
/**
* @param {string} scriptId
* @param {number} lineNumber
* @param {number} columnNumber
* @return {?string}
*/
function linkifyLocationAsText(scriptId, lineNumber, columnNumber) {
const debuggerModel = target ? target.model(SDK.DebuggerModel) : null;
if (!target || target.isDisposed() || !scriptId || !debuggerModel) {
return null;
}
const rawLocation = debuggerModel.createRawLocationByScriptId(scriptId, lineNumber, columnNumber);
if (!rawLocation) {
return null;
}
const uiLocation = Bindings.debuggerWorkspaceBinding.rawLocationToUILocation(rawLocation);
return uiLocation ? uiLocation.linkText() : null;
}
/**
* @return {?string}
*/
function linkifyTopCallFrameAsText() {
const frame = TimelineModel.TimelineData.forEvent(event).topFrame();
if (!frame) {
return null;
}
let text = linkifyLocationAsText(frame.scriptId, frame.lineNumber, frame.columnNumber);
if (!text) {
text = frame.url;
if (typeof frame.lineNumber === 'number') {
text += ':' + (frame.lineNumber + 1);
}
}
return text;
}
}
/**
* @param {!SDK.TracingModel.Event} event
* @param {?SDK.Target} target
* @param {!Components.Linkifier} linkifier
* @return {?Node}
*/
static buildDetailsNodeForTraceEvent(event, target, linkifier) {
const recordType = TimelineModel.TimelineModel.RecordType;
let details = null;
let detailsText;
const eventData = event.args['data'];
switch (event.name) {
case recordType.GCEvent:
case recordType.MajorGC:
case recordType.MinorGC:
case recordType.EventDispatch:
case recordType.Paint:
case recordType.Animation:
case recordType.EmbedderCallback:
case recordType.ParseHTML:
case recordType.WasmStreamFromResponseCallback:
case recordType.WasmCompiledModule:
case recordType.WasmModuleCacheHit:
case recordType.WasmCachedModule:
case recordType.WasmModuleCacheInvalid:
case recordType.WebSocketCreate:
case recordType.WebSocketSendHandshakeRequest:
case recordType.WebSocketReceiveHandshakeResponse:
case recordType.WebSocketDestroy:
detailsText = Timeline.TimelineUIUtils.buildDetailsTextForTraceEvent(event, target);
break;
case recordType.PaintImage:
case recordType.DecodeImage:
case recordType.ResizeImage:
case recordType.DecodeLazyPixelRef:
case recordType.XHRReadyStateChange:
case recordType.XHRLoad:
case recordType.ResourceWillSendRequest:
case recordType.ResourceSendRequest:
case recordType.ResourceReceivedData:
case recordType.ResourceReceiveResponse:
case recordType.ResourceFinish: {
const url = TimelineModel.TimelineData.forEvent(event).url;
if (url) {
details = Components.Linkifier.linkifyURL(url);
}
break;
}
case recordType.FunctionCall:
case recordType.JSFrame:
details = createElement('span');
details.createTextChild(Timeline.TimelineUIUtils.frameDisplayName(eventData));
const location = linkifyLocation(
eventData['scriptId'], eventData['url'], eventData['lineNumber'], eventData['columnNumber']);
if (location) {
details.createTextChild(' @ ');
details.appendChild(location);
}
break;
case recordType.CompileModule:
details = linkifyLocation('', event.args['fileName'], 0, 0);
break;
case recordType.CompileScript:
case recordType.EvaluateScript: {
const url = eventData['url'];
if (url) {
details = linkifyLocation('', url, eventData['lineNumber'], 0);
}
break;
}
case recordType.StreamingCompileScript: {
const url = eventData['url'];
if (url) {
details = linkifyLocation('', url, 0, 0);
}
break;
}
default:
if (event.hasCategory(TimelineModel.TimelineModel.Category.Console)) {
detailsText = null;
} else {
details = linkifyTopCallFrame();
}
break;
}
if (!details && detailsText) {
details = createTextNode(detailsText);
}
return details;
/**
* @param {string} scriptId
* @param {string} url
* @param {number} lineNumber
* @param {number=} columnNumber
* @return {?Element}
*/
function linkifyLocation(scriptId, url, lineNumber, columnNumber) {
return linkifier.linkifyScriptLocation(target, scriptId, url, lineNumber, columnNumber, 'timeline-details');
}
/**
* @return {?Element}
*/
function linkifyTopCallFrame() {
const frame = TimelineModel.TimelineData.forEvent(event).topFrame();
return frame ? linkifier.maybeLinkifyConsoleCallFrame(target, frame, 'timeline-details') : null;
}
}
/**
* @param {!SDK.TracingModel.Event} event
* @return {!Element}
*/
static buildDetailsNodeForPerformanceEvent(event) {
/** @type {string} */
let link =
'https://developers.google.com/web/fundamentals/performance/user-centric-performance-metrics#user-centric_performance_metrics';
let name = 'page performance metrics';
const recordType = TimelineModel.TimelineModel.RecordType;
switch (event.name) {
case recordType.MarkLCPCandidate:
link = 'https://web.dev/largest-contentful-paint';
name = 'largest contentful paint';
break;
case recordType.MarkFCP:
link = 'https://web.dev/first-contentful-paint';
name = 'first contentful paint';
break;
case recordType.MarkFMP:
link = 'https://web.dev/first-meaningful-paint/';
name = 'first meaningful paint';
break;
default:
break;
}
return UI.html`<div>${UI.XLink.create(link, ls`Learn more`)} about ${name}.</div>`;
}
/**
* @param {!SDK.TracingModel.Event} event
* @param {!TimelineModel.TimelineModel} model
* @param {!Components.Linkifier} linkifier
* @param {boolean} detailed
* @return {!Promise<!DocumentFragment>}
*/
static async buildTraceEventDetails(event, model, linkifier, detailed) {
const maybeTarget = model.targetByEvent(event);
/** @type {?Map<number, ?SDK.DOMNode>} */
let relatedNodesMap = null;
if (maybeTarget) {
const target = /** @type {!SDK.Target} */ (maybeTarget);
if (typeof event[Timeline.TimelineUIUtils._previewElementSymbol] === 'undefined') {
let previewElement = null;
const url = TimelineModel.TimelineData.forEvent(event).url;
if (url) {
previewElement = await Components.ImagePreview.build(
target, url, false, {imageAltText: Components.ImagePreview.defaultAltTextForImageURL(url)});
} else if (TimelineModel.TimelineData.forEvent(event).picture) {
previewElement = await Timeline.TimelineUIUtils.buildPicturePreviewContent(event, target);
}
event[Timeline.TimelineUIUtils._previewElementSymbol] = previewElement;
}
/** @type {!Set<number>} */
const nodeIdsToResolve = new Set();
const timelineData = TimelineModel.TimelineData.forEvent(event);
if (timelineData.backendNodeId) {
nodeIdsToResolve.add(timelineData.backendNodeId);
}
const invalidationTrackingEvents = TimelineModel.InvalidationTracker.invalidationEventsFor(event);
if (invalidationTrackingEvents) {
Timeline.TimelineUIUtils._collectInvalidationNodeIds(nodeIdsToResolve, invalidationTrackingEvents);
}
if (nodeIdsToResolve.size) {
const domModel = target.model(SDK.DOMModel);
if (domModel) {
relatedNodesMap = await domModel.pushNodesByBackendIdsToFrontend(nodeIdsToResolve);
}
}
}
const recordTypes = TimelineModel.TimelineModel.RecordType;
// This message may vary per event.name;
let relatedNodeLabel;
const contentHelper = new Timeline.TimelineDetailsContentHelper(model.targetByEvent(event), linkifier);
const color = model.isMarkerEvent(event) ? Timeline.TimelineUIUtils.markerStyleForEvent(event).color :
Timeline.TimelineUIUtils.eventStyle(event).category.color;
contentHelper.addSection(Timeline.TimelineUIUtils.eventTitle(event), color);
const eventData = event.args['data'];
const timelineData = TimelineModel.TimelineData.forEvent(event);
const initiator = timelineData.initiator();
let url = null;
if (timelineData.warning) {
contentHelper.appendWarningRow(event);
}
if (event.name === recordTypes.JSFrame && eventData['deoptReason']) {
contentHelper.appendWarningRow(event, TimelineModel.TimelineModel.WarningType.V8Deopt);
}
if (detailed && !Number.isNaN(event.duration + 0)) {
contentHelper.appendTextRow(ls`Total Time`, Number.millisToString(event.duration, true));
contentHelper.appendTextRow(ls`Self Time`, Number.millisToString(event.selfTime, true));
}
if (model.isGenericTrace()) {
for (const key in event.args) {
try {
contentHelper.appendTextRow(key, JSON.stringify(event.args[key]));
} catch (e) {
contentHelper.appendTextRow(key, `<${typeof event.args[key]}>`);
}
}
return contentHelper.fragment;
}
switch (event.name) {
case recordTypes.GCEvent:
case recordTypes.MajorGC:
case recordTypes.MinorGC:
const delta = event.args['usedHeapSizeBefore'] - event.args['usedHeapSizeAfter'];
contentHelper.appendTextRow(ls`Collected`, Number.bytesToString(delta));
break;
case recordTypes.JSFrame:
case recordTypes.FunctionCall:
const detailsNode =
Timeline.TimelineUIUtils.buildDetailsNodeForTraceEvent(event, model.targetByEvent(event), linkifier);
if (detailsNode) {
contentHelper.appendElementRow(ls`Function`, detailsNode);
}
break;
case recordTypes.TimerFire:
case recordTypes.TimerInstall:
case recordTypes.TimerRemove:
contentHelper.appendTextRow(ls`Timer ID`, eventData['timerId']);
if (event.name === recordTypes.TimerInstall) {
contentHelper.appendTextRow(ls`Timeout`, Number.millisToString(eventData['timeout']));
contentHelper.appendTextRow(ls`Repeats`, !eventData['singleShot']);
}
break;
case recordTypes.FireAnimationFrame:
contentHelper.appendTextRow(ls`Callback ID`, eventData['id']);
break;
case recordTypes.ResourceWillSendRequest:
case recordTypes.ResourceSendRequest:
case recordTypes.ResourceReceiveResponse:
case recordTypes.ResourceReceivedData:
case recordTypes.ResourceFinish:
url = timelineData.url;
if (url) {
contentHelper.appendElementRow(ls`Resource`, Components.Linkifier.linkifyURL(url));
}
if (eventData['requestMethod']) {
contentHelper.appendTextRow(ls`Request Method`, eventData['requestMethod']);
}
if (typeof eventData['statusCode'] === 'number') {
contentHelper.appendTextRow(ls`Status Code`, eventData['statusCode']);
}
if (eventData['mimeType']) {
contentHelper.appendTextRow(ls`MIME Type`, eventData['mimeType']);
}
if ('priority' in eventData) {
const priority = PerfUI.uiLabelForNetworkPriority(eventData['priority']);
contentHelper.appendTextRow(ls`Priority`, priority);
}
if (eventData['encodedDataLength']) {
contentHelper.appendTextRow(ls`Encoded Data`, ls`${eventData['encodedDataLength']} Bytes`);
}
if (eventData['decodedBodyLength']) {
contentHelper.appendTextRow(ls`Decoded Body`, ls`${eventData['decodedBodyLength']} Bytes`);
}
break;
case recordTypes.CompileModule:
contentHelper.appendLocationRow(ls`Module`, event.args['fileName'], 0);
break;
case recordTypes.CompileScript:
url = eventData && eventData['url'];
if (url) {
contentHelper.appendLocationRow(ls`Script`, url, eventData['lineNumber'], eventData['columnNumber']);
}
contentHelper.appendTextRow(ls`Streamed`, eventData['streamed']);
const producedCacheSize = eventData && eventData['producedCacheSize'];
if (producedCacheSize) {
contentHelper.appendTextRow(ls`Produced Cache Size`, producedCacheSize);
}
const cacheConsumeOptions = eventData && eventData['cacheConsumeOptions'];
if (cacheConsumeOptions) {
contentHelper.appendTextRow(ls`Cache Consume Options`, cacheConsumeOptions);
contentHelper.appendTextRow(ls`Consumed Cache Size`, eventData['consumedCacheSize']);
contentHelper.appendTextRow(ls`Cache Rejected`, eventData['cacheRejected']);
}
break;
case recordTypes.EvaluateScript:
url = eventData && eventData['url'];
if (url) {
contentHelper.appendLocationRow(ls`Script`, url, eventData['lineNumber'], eventData['columnNumber']);
}
break;
case recordTypes.WasmStreamFromResponseCallback:
case recordTypes.WasmCompiledModule:
case recordTypes.WasmCachedModule:
case recordTypes.WasmModuleCacheHit:
case recordTypes.WasmModuleCacheInvalid:
if (eventData) {
url = event.args['url'];
if (url) {
contentHelper.appendTextRow(ls`Url`, url);
}
const producedCachedSize = event.args['producedCachedSize'];
if (producedCachedSize) {
contentHelper.appendTextRow(ls`Produced Cache Size`, producedCachedSize);
}
const consumedCachedSize = event.args['consumedCachedSize'];
if (consumedCachedSize) {
contentHelper.appendTextRow(ls`Consumed Cache Size`, consumedCachedSize);
}
}
break;
case recordTypes.Paint:
const clip = eventData['clip'];
contentHelper.appendTextRow(ls`Location`, ls`(${clip[0]}, ${clip[1]})`);
const clipWidth = Timeline.TimelineUIUtils.quadWidth(clip);
const clipHeight = Timeline.TimelineUIUtils.quadHeight(clip);
contentHelper.appendTextRow(ls`Dimensions`, ls`${clipWidth} × ${clipHeight}`);
// Fall-through intended.
case recordTypes.PaintSetup:
case recordTypes.Rasterize:
case recordTypes.ScrollLayer:
relatedNodeLabel = ls`Layer Root`;
break;
case recordTypes.PaintImage:
case recordTypes.DecodeLazyPixelRef:
case recordTypes.DecodeImage:
case recordTypes.ResizeImage:
case recordTypes.DrawLazyPixelRef:
relatedNodeLabel = ls`Owner Element`;
url = timelineData.url;
if (url) {
contentHelper.appendElementRow(ls`Image URL`, Components.Linkifier.linkifyURL(url));
}
break;
case recordTypes.ParseAuthorStyleSheet:
url = eventData['styleSheetUrl'];
if (url) {
contentHelper.appendElementRow(ls`Stylesheet URL`, Components.Linkifier.linkifyURL(url));
}
break;
case recordTypes.UpdateLayoutTree: // We don't want to see default details.
case recordTypes.RecalculateStyles:
contentHelper.appendTextRow(ls`Elements Affected`, event.args['elementCount']);
break;
case recordTypes.Layout:
const beginData = event.args['beginData'];
contentHelper.appendTextRow(
ls`Nodes That Need Layout`, ls`${beginData['dirtyObjects']} of ${beginData['totalObjects']}`);
relatedNodeLabel = ls`Layout root`;
break;
case recordTypes.ConsoleTime:
contentHelper.appendTextRow(ls`Message`, event.name);
break;
case recordTypes.WebSocketCreate:
case recordTypes.WebSocketSendHandshakeRequest:
case recordTypes.WebSocketReceiveHandshakeResponse:
case recordTypes.WebSocketDestroy:
const initiatorData = initiator ? initiator.args['data'] : eventData;
if (typeof initiatorData['webSocketURL'] !== 'undefined') {
contentHelper.appendTextRow(ls`URL`, initiatorData['webSocketURL']);
}
if (typeof initiatorData['webSocketProtocol'] !== 'undefined') {
contentHelper.appendTextRow(ls`WebSocket Protocol`, initiatorData['webSocketProtocol']);
}
if (typeof eventData['message'] !== 'undefined') {
contentHelper.appendTextRow(ls`Message`, eventData['message']);
}
break;
case recordTypes.EmbedderCallback:
contentHelper.appendTextRow(ls`Callback Function`, eventData['callbackName']);
break;
case recordTypes.Animation:
if (event.phase === SDK.TracingModel.Phase.NestableAsyncInstant) {
contentHelper.appendTextRow(ls`State`, eventData['state']);
}
break;
case recordTypes.ParseHTML: {
const beginData = event.args['beginData'];
const startLine = beginData['startLine'] - 1;
const endLine = event.args['endData'] ? event.args['endData']['endLine'] - 1 : undefined;
url = beginData['url'];
if (url) {
contentHelper.appendLocationRange(ls`Range`, url, startLine, endLine);
}
break;
}
case recordTypes.FireIdleCallback:
contentHelper.appendTextRow(ls`Allotted Time`, Number.millisToString(eventData['allottedMilliseconds']));
contentHelper.appendTextRow(ls`Invoked by Timeout`, eventData['timedOut']);
// Fall-through intended.
case recordTypes.RequestIdleCallback:
case recordTypes.CancelIdleCallback:
contentHelper.appendTextRow(ls`Callback ID`, eventData['id']);
break;
case recordTypes.EventDispatch:
contentHelper.appendTextRow(ls`Type`, eventData['type']);
break;
case recordTypes.MarkLCPCandidate:
contentHelper.appendTextRow(ls`Type`, String(eventData['type']));
contentHelper.appendTextRow(ls`Size`, String(eventData['size']));
// Fall-through intended.
case recordTypes.MarkFirstPaint:
case recordTypes.MarkFCP:
case recordTypes.MarkFMP:
case recordTypes.MarkLoad:
case recordTypes.MarkDOMContent:
contentHelper.appendTextRow(
ls`Timestamp`, Number.preciseMillisToString(event.startTime - model.minimumRecordTime(), 1));
contentHelper.appendElementRow(
ls`Details`, Timeline.TimelineUIUtils.buildDetailsNodeForPerformanceEvent(event));
break;
default: {
const detailsNode =
Timeline.TimelineUIUtils.buildDetailsNodeForTraceEvent(event, model.targetByEvent(event), linkifier);
if (detailsNode) {
contentHelper.appendElementRow(ls`Details`, detailsNode);
}
break;
}
}
if (timelineData.timeWaitingForMainThread) {
contentHelper.appendTextRow(
ls`Time Waiting for Main Thread`, Number.millisToString(timelineData.timeWaitingForMainThread, true));
}
const relatedNode = relatedNodesMap && relatedNodesMap.get(timelineData.backendNodeId);
if (relatedNode) {
const nodeSpan = await Common.Linkifier.linkify(relatedNode);
contentHelper.appendElementRow(relatedNodeLabel || ls`Related Node`, nodeSpan);
}
if (event[Timeline.TimelineUIUtils._previewElementSymbol]) {
contentHelper.addSection(ls`Preview`);
contentHelper.appendElementRow('', event[Timeline.TimelineUIUtils._previewElementSymbol]);
}
if (initiator || timelineData.stackTraceForSelfOrInitiator() ||
TimelineModel.InvalidationTracker.invalidationEventsFor(event)) {
Timeline.TimelineUIUtils._generateCauses(event, model.targetByEvent(event), relatedNodesMap, contentHelper);
}
const stats = {};
const showPieChart = detailed && Timeline.TimelineUIUtils._aggregatedStatsForTraceEvent(stats, model, event);
if (showPieChart) {
contentHelper.addSection(ls`Aggregated Time`);
const pieChart = Timeline.TimelineUIUtils.generatePieChart(
stats, Timeline.TimelineUIUtils.eventStyle(event).category, event.selfTime);
contentHelper.appendElementRow('', pieChart);
}
return contentHelper.fragment;
}
/**
* @param {!Array<!SDK.TracingModel.Event>} events
* @param {number} startTime
* @param {number} endTime
* @return {!Object<string, number>}
*/
static statsForTimeRange(events, startTime, endTime) {
if (!events.length) {
return {'idle': endTime - startTime};
}
buildRangeStatsCacheIfNeeded(events);
const aggregatedStats = subtractStats(aggregatedStatsAtTime(endTime), aggregatedStatsAtTime(startTime));
const aggregatedTotal = Object.values(aggregatedStats).reduce((a, b) => a + b, 0);
aggregatedStats['idle'] = Math.max(0, endTime - startTime - aggregatedTotal);
return aggregatedStats;
/**
* @param {number} time
* @return {!Object}
*/
function aggregatedStatsAtTime(time) {
const stats = {};
const cache = events[Timeline.TimelineUIUtils._categoryBreakdownCacheSymbol];
for (const category in cache) {
const categoryCache = cache[category];
const index = categoryCache.time.upperBound(time);
let value;
if (index === 0) {
value = 0;
} else if (index === categoryCache.time.length) {
value = categoryCache.value.peekLast();
} else {
const t0 = categoryCache.time[index - 1];
const t1 = categoryCache.time[index];
const v0 = categoryCache.value[index - 1];
const v1 = categoryCache.value[index];
value = v0 + (v1 - v0) * (time - t0) / (t1 - t0);
}
stats[category] = value;
}
return stats;
}
/**
* @param {!Object<string, number>} a
* @param {!Object<string, number>} b
* @return {!Object<string, number>}
*/
function subtractStats(a, b) {
const result = Object.assign({}, a);
for (const key in b) {
result[key] -= b[key];
}
return result;
}
/**
* @param {!Array<!SDK.TracingModel.Event>} events
*/
function buildRangeStatsCacheIfNeeded(events) {
if (events[Timeline.TimelineUIUtils._categoryBreakdownCacheSymbol]) {
return;
}
// aggeregatedStats is a map by categories. For each category there's an array
// containing sorted time points which records accumulated value of the category.
const aggregatedStats = {};
const categoryStack = [];
let lastTime = 0;
TimelineModel.TimelineModel.forEachEvent(
events, onStartEvent, onEndEvent, undefined, undefined, undefined, filterForStats());
/**
* @return {function(!SDK.TracingModel.Event):boolean}
*/
function filterForStats() {
const visibleEventsFilter = Timeline.TimelineUIUtils.visibleEventsFilter();
return event => visibleEventsFilter.accept(event) || SDK.TracingModel.isTopLevelEvent(event);
}
/**
* @param {string} category
* @param {number} time
*/
function updateCategory(category, time) {
let statsArrays = aggregatedStats[category];
if (!statsArrays) {
statsArrays = {time: [], value: []};
aggregatedStats[category] = statsArrays;
}
if (statsArrays.time.length && statsArrays.time.peekLast() === time) {
return;
}
const lastValue = statsArrays.value.length ? statsArrays.value.peekLast() : 0;
statsArrays.value.push(lastValue + time - lastTime);
statsArrays.time.push(time);
}
/**
* @param {?string} from
* @param {?string} to
* @param {number} time
*/
function categoryChange(from, to, time) {
if (from) {
updateCategory(from, time);
}
lastTime = time;
if (to) {
updateCategory(to, time);
}
}
/**
* @param {!SDK.TracingModel.Event} e
*/
function onStartEvent(e) {
const category = Timeline.TimelineUIUtils.eventStyle(e).category.name;
const parentCategory = categoryStack.length ? categoryStack.peekLast() : null;
if (category !== parentCategory) {
categoryChange(parentCategory, category, e.startTime);
}
categoryStack.push(category);
}
/**
* @param {!SDK.TracingModel.Event} e
*/
function onEndEvent(e) {
const category = categoryStack.pop();
const parentCategory = categoryStack.length ? categoryStack.peekLast() : null;
if (category !== parentCategory) {
categoryChange(category, parentCategory, e.endTime);
}
}
const obj = /** @type {!Object} */ (events);
obj[Timeline.TimelineUIUtils._categoryBreakdownCacheSymbol] = aggregatedStats;
}
}
/**
* @param {!TimelineModel.TimelineModel.NetworkRequest} request
* @param {!TimelineModel.TimelineModel} model
* @param {!Components.Linkifier} linkifier
* @return {!Promise<!DocumentFragment>}
*/
static async buildNetworkRequestDetails(request, model, linkifier) {
const target = model.targetByEvent(request.children[0]);
const contentHelper = new Timeline.TimelineDetailsContentHelper(target, linkifier);
const category = Timeline.TimelineUIUtils.networkRequestCategory(request);
const color = Timeline.TimelineUIUtils.networkCategoryColor(category);
contentHelper.addSection(ls`Network request`, color);
if (request.url) {
contentHelper.appendElementRow(ls`URL`, Components.Linkifier.linkifyURL(request.url));
}
// The time from queueing the request until resource processing is finished.
const fullDuration = request.endTime - (request.getStartTime() || -Infinity);
if (isFinite(fullDuration)) {
let textRow = Number.millisToString(fullDuration, true);
// The time from queueing the request until the download is finished. This
// corresponds to the total time reported for the request in the network tab.
const networkDuration = request.finishTime - request.getStartTime();
// The time it takes to make the resource available to the renderer process.
const processingDuration = request.endTime - request.finishTime;
if (isFinite(networkDuration) && isFinite(processingDuration)) {
const networkDurationStr = Number.millisToString(networkDuration, true);
const processingDurationStr = Number.millisToString(processingDuration, true);
const cacheOrNetworkLabel = request.cached() ? ls`load from cache` : ls`network transfer`;
textRow += ls` (${networkDurationStr} ${cacheOrNetworkLabel} + ${processingDurationStr} resource loading)`;
}
contentHelper.appendTextRow(ls`Duration`, textRow);
}
if (request.requestMethod) {
contentHelper.appendTextRow(ls`Request Method`, request.requestMethod);
}
if (typeof request.priority === 'string') {
const priority =
PerfUI.uiLabelForNetworkPriority(/** @type {!Protocol.Network.ResourcePriority} */ (request.priority));
contentHelper.appendTextRow(ls`Priority`, priority);
}
if (request.mimeType) {
contentHelper.appendTextRow(ls`Mime Type`, request.mimeType);
}
let lengthText = '';
if (request.memoryCached()) {
lengthText += ls` (from memory cache)`;
} else if (request.cached()) {
lengthText += ls` (from cache)`;
} else if (request.timing && request.timing.pushStart) {
lengthText += ls` (from push)`;
}
if (request.fromServiceWorker) {
lengthText += ls` (from service worker)`;
}
if (request.encodedDataLength || !lengthText) {
lengthText = `${Number.bytesToString(request.encodedDataLength)}${lengthText}`;
}
contentHelper.appendTextRow(ls`Encoded Data`, lengthText);
if (request.decodedBodyLength) {
contentHelper.appendTextRow(ls`Decoded Body`, Number.bytesToString(request.decodedBodyLength));
}
const title = ls`Initiator`;
const sendRequest = request.children[0];
const topFrame = TimelineModel.TimelineData.forEvent(sendRequest).topFrame();
if (topFrame) {
const link = linkifier.maybeLinkifyConsoleCallFrame(target, topFrame);
if (link) {
contentHelper.appendElementRow(title, link);
}
} else {
const initiator = TimelineModel.TimelineData.forEvent(sendRequest).initiator();
if (initiator) {
const initiatorURL = TimelineModel.TimelineData.forEvent(initiator).url;
if (initiatorURL) {
const link = linkifier.maybeLinkifyScriptLocation(target, null, initiatorURL, 0);
if (link) {
contentHelper.appendElementRow(title, link);
}
}
}
}
if (!request.previewElement && request.url && target) {
request.previewElement = await Components.ImagePreview.build(
target, request.url, false, {imageAltText: Components.ImagePreview.defaultAltTextForImageURL(request.url)});
}
if (request.previewElement) {
contentHelper.appendElementRow(ls`Preview`, request.previewElement);
}
return contentHelper.fragment;
}
/**
* @param {!Array<!Protocol.Runtime.CallFrame>} callFrames
* @return {!Protocol.Runtime.StackTrace}
*/
static _stackTraceFromCallFrames(callFrames) {
return /** @type {!Protocol.Runtime.StackTrace} */ ({callFrames: callFrames});
}
/**
* @param {!SDK.TracingModel.Event} event
* @param {?SDK.Target} target
* @param {?Map<number, ?SDK.DOMNode>} relatedNodesMap
* @param {!Timeline.TimelineDetailsContentHelper} contentHelper
*/
static _generateCauses(event, target, relatedNodesMap, contentHelper) {
const recordTypes = TimelineModel.TimelineModel.RecordType;
let callSiteStackLabel;
let stackLabel;
switch (event.name) {
case recordTypes.TimerFire:
callSiteStackLabel = ls`Timer Installed`;
break;
case recordTypes.FireAnimationFrame:
callSiteStackLabel = ls`Animation Frame Requested`;
break;
case recordTypes.FireIdleCallback:
callSiteStackLabel = ls`Idle Callback Requested`;
break;
case recordTypes.UpdateLayoutTree:
case recordTypes.RecalculateStyles:
stackLabel = ls`Recalculation Forced`;
break;
case recordTypes.Layout:
callSiteStackLabel = ls`First Layout Invalidation`;
stackLabel = ls`Layout Forced`;
break;
}
const timelineData = TimelineModel.TimelineData.forEvent(event);
// Direct cause.
if (timelineData.stackTrace && timelineData.stackTrace.length) {
contentHelper.addSection(ls`Call Stacks`);
contentHelper.appendStackTrace(
stackLabel || ls`Stack Trace`, Timeline.TimelineUIUtils._stackTraceFromCallFrames(timelineData.stackTrace));
}
const initiator = TimelineModel.TimelineData.forEvent(event).initiator();
// Indirect causes.
if (TimelineModel.InvalidationTracker.invalidationEventsFor(event) && target) {
// Full invalidation tracking (experimental).
contentHelper.addSection(ls`Invalidations`);
Timeline.TimelineUIUtils._generateInvalidations(event, target, relatedNodesMap, contentHelper);
} else if (initiator) { // Partial invalidation tracking.
const delay = event.startTime - initiator.startTime;
contentHelper.appendTextRow(ls`Pending for`, Number.preciseMillisToString(delay, 1));
const link = createElementWithClass('span', 'devtools-link');
link.textContent = ls`Reveal`;
link.addEventListener('click', () => {
Timeline.TimelinePanel.instance().select(
Timeline.TimelineSelection.fromTraceEvent(/** @type {!SDK.TracingModel.Event} */ (initiator)));
});
contentHelper.appendElementRow(ls`Initiator`, link);
const initiatorStackTrace = TimelineModel.TimelineData.forEvent(initiator).stackTrace;
if (initiatorStackTrace) {
contentHelper.appendStackTrace(
callSiteStackLabel || ls`First Invalidated`,
Timeline.TimelineUIUtils._stackTraceFromCallFrames(initiatorStackTrace));
}
}
}
/**
* @param {!SDK.TracingModel.Event} event
* @param {!SDK.Target} target
* @param {?Map<number, ?SDK.DOMNode>} relatedNodesMap
* @param {!Timeline.TimelineDetailsContentHelper} contentHelper
*/
static _generateInvalidations(event, target, relatedNodesMap, contentHelper) {
const invalidationTrackingEvents = TimelineModel.InvalidationTracker.invalidationEventsFor(event);
const invalidations = {};
invalidationTrackingEvents.forEach(function(invalidation) {
if (!invalidations[invalidation.type]) {
invalidations[invalidation.type] = [invalidation];
} else {
invalidations[invalidation.type].push(invalidation);
}
});
Object.keys(invalidations).forEach(function(type) {
Timeline.TimelineUIUtils._generateInvalidationsForType(
type, target, invalidations[type], relatedNodesMap, contentHelper);
});
}
/**
* @param {string} type
* @param {!SDK.Target} target
* @param {!Array.<!TimelineModel.InvalidationTrackingEvent>} invalidations
* @param {?Map<number, ?SDK.DOMNode>} relatedNodesMap
* @param {!Timeline.TimelineDetailsContentHelper} contentHelper
*/
static _generateInvalidationsForType(type, target, invalidations, relatedNodesMap, contentHelper) {
let title;
switch (type) {
case TimelineModel.TimelineModel.RecordType.StyleRecalcInvalidationTracking:
title = ls`Style Invalidations`;
break;
case TimelineModel.TimelineModel.RecordType.LayoutInvalidationTracking:
title = ls`Layout Invalidations`;
break;
default:
title = ls`Other Invalidations`;
break;
}
const invalidationsTreeOutline = new UI.TreeOutlineInShadow();
invalidationsTreeOutline.registerRequiredCSS('timeline/invalidationsTree.css');
invalidationsTreeOutline.element.classList.add('invalidations-tree');
const invalidationGroups = groupInvalidationsByCause(invalidations);
invalidationGroups.forEach(function(group) {
const groupElement =
new Timeline.TimelineUIUtils.InvalidationsGroupElement(target, relatedNodesMap, contentHelper, group);
invalidationsTreeOutline.appendChild(groupElement);
});
contentHelper.appendElementRow(title, invalidationsTreeOutline.element, false, true);
/**
* @param {!Array<!TimelineModel.InvalidationTrackingEvent>} invalidations
* @return {!Array<!Array<!TimelineModel.InvalidationTrackingEvent>>}
*/
function groupInvalidationsByCause(invalidations) {
/** @type {!Map<string, !Array<!TimelineModel.InvalidationTrackingEvent>>} */
const causeToInvalidationMap = new Map();
for (let index = 0; index < invalidations.length; index++) {
const invalidation = invalidations[index];
let causeKey = '';
if (invalidation.cause.reason) {
causeKey += invalidation.cause.reason + '.';
}
if (invalidation.cause.stackTrace) {
invalidation.cause.stackTrace.forEach(function(stackFrame) {
causeKey += stackFrame['functionName'] + '.';
causeKey += stackFrame['scriptId'] + '.';
causeKey += stackFrame['url'] + '.';
causeKey += stackFrame['lineNumber'] + '.';
causeKey += stackFrame['columnNumber'] + '.';
});
}
if (causeToInvalidationMap.has(causeKey)) {
causeToInvalidationMap.get(causeKey).push(invalidation);
} else {
causeToInvalidationMap.set(causeKey, [invalidation]);
}
}
return causeToInvalidationMap.valuesArray();
}
}
/**
* @param {!Set<number>} nodeIds
* @param {!Array<!TimelineModel.InvalidationTrackingEvent>} invalidations
*/
static _collectInvalidationNodeIds(nodeIds, invalidations) {
nodeIds.addAll(invalidations.map(invalidation => invalidation.nodeId).filter(id => id));
}
/**
* @param {!Object} total
* @param {!TimelineModel.TimelineModel} model
* @param {!SDK.TracingModel.Event} event
* @return {boolean}
*/
static _aggregatedStatsForTraceEvent(total, model, event) {
const events = model.inspectedTargetEvents();
/**
* @param {number} startTime
* @param {!SDK.TracingModel.Event} e
* @return {number}
*/
function eventComparator(startTime, e) {
return startTime - e.startTime;
}
const index = events.binaryIndexOf(event.startTime, eventComparator);
// Not a main thread event?
if (index < 0) {
return false;
}
let hasChildren = false;
const endTime = event.endTime;
if (endTime) {
for (let i = index; i < events.length; i++) {
const nextEvent = events[i];
if (nextEvent.startTime >= endTime) {
break;
}
if (!nextEvent.selfTime) {
continue;
}
if (nextEvent.thread !== event.thread) {
continue;
}
if (i > index) {
hasChildren = true;
}
const categoryName = Timeline.TimelineUIUtils.eventStyle(nextEvent).category.name;
total[categoryName] = (total[categoryName] || 0) + nextEvent.selfTime;
}
}
if (SDK.TracingModel.isAsyncPhase(event.phase)) {
if (event.endTime) {
let aggregatedTotal = 0;
for (const categoryName in total) {
aggregatedTotal += total[categoryName];
}
total['idle'] = Math.max(0, event.endTime - event.startTime - aggregatedTotal);
}
return false;
}
return hasChildren;
}
/**
* @param {!SDK.TracingModel.Event} event
* @param {!SDK.Target} target
* @return {!Promise<?Element>}
*/
static async buildPicturePreviewContent(event, target) {
const snapshotWithRect = await new TimelineModel.LayerPaintEvent(event, target).snapshotPromise();
if (!snapshotWithRect) {
return null;
}
const imageURLPromise = snapshotWithRect.snapshot.replay();
snapshotWithRect.snapshot.release();
const imageURL = await imageURLPromise;
if (!imageURL) {
return null;
}
const container = createElement('div');
UI.appendStyle(container, 'components/imagePreview.css');
container.classList.add('image-preview-container', 'vbox', 'link');
const img = container.createChild('img');
img.src = imageURL;
img.alt = Components.ImagePreview.defaultAltTextForImageURL(imageURL);
const paintProfilerButton = container.createChild('a');
paintProfilerButton.textContent = ls`Paint Profiler`;
container.addEventListener(
'click', () => Timeline.TimelinePanel.instance().select(Timeline.TimelineSelection.fromTraceEvent(event)),
false);
return container;
}
/**
* @param {!SDK.TracingModel.Event} event
* @param {number} zeroTime
* @return {!Element}
*/
static createEventDivider(event, zeroTime) {
const eventDivider = createElementWithClass('div', 'resources-event-divider');
const startTime = Number.millisToString(event.startTime - zeroTime);
eventDivider.title = Common.UIString('%s at %s', Timeline.TimelineUIUtils.eventTitle(event), startTime);
const style = Timeline.TimelineUIUtils.markerStyleForEvent(event);
if (style.tall) {
eventDivider.style.backgroundColor = style.color;
}
return eventDivider;
}
/**
* @return {!Array.<string>}
*/
static _visibleTypes() {
const eventStyles = Timeline.TimelineUIUtils._initEventStyles();
const result = [];
for (const name in eventStyles) {
if (!eventStyles[name].hidden) {
result.push(name);
}
}
return result;
}
/**
* @return {!TimelineModel.TimelineModelFilter}
*/
static visibleEventsFilter() {
return new TimelineModel.TimelineVisibleEventsFilter(Timeline.TimelineUIUtils._visibleTypes());
}
/**
* @return {!Object.<string, !Timeline.TimelineCategory>}
*/
static categories() {
if (Timeline.TimelineUIUtils._categories) {
return Timeline.TimelineUIUtils._categories;
}
Timeline.TimelineUIUtils._categories = {
loading: new Timeline.TimelineCategory('loading', ls`Loading`, true, 'hsl(214, 67%, 74%)', 'hsl(214, 67%, 66%)'),
scripting:
new Timeline.TimelineCategory('scripting', ls`Scripting`, true, 'hsl(43, 83%, 72%)', 'hsl(43, 83%, 64%) '),
rendering:
new Timeline.TimelineCategory('rendering', ls`Rendering`, true, 'hsl(256, 67%, 76%)', 'hsl(256, 67%, 70%)'),
painting:
new Timeline.TimelineCategory('painting', ls`Painting`, true, 'hsl(109, 33%, 64%)', 'hsl(109, 33%, 55%)'),
gpu: new Timeline.TimelineCategory('gpu', ls`GPU`, false, 'hsl(109, 33%, 64%)', 'hsl(109, 33%, 55%)'),
async: new Timeline.TimelineCategory('async', ls`Async`, false, 'hsl(0, 100%, 50%)', 'hsl(0, 100%, 40%)'),
other: new Timeline.TimelineCategory('other', ls`System`, false, 'hsl(0, 0%, 87%)', 'hsl(0, 0%, 79%)'),
idle: new Timeline.TimelineCategory('idle', ls`Idle`, false, 'hsl(0, 0%, 98%)', 'hsl(0, 0%, 98%)')
};
return Timeline.TimelineUIUtils._categories;
}
/**
* @param {!Object.<string, !Timeline.TimelineCategory>} categories
*/
static setCategories(categories) {
Timeline.TimelineUIUtils._categories = categories;
}
/**
* @return {!Array}
*/
static getTimelineMainEventCategories() {
if (Timeline.TimelineUIUtils._eventCategories) {
return Timeline.TimelineUIUtils._eventCategories;
}
Timeline.TimelineUIUtils._eventCategories = ['idle', 'loading', 'painting', 'rendering', 'scripting', 'other'];
return Timeline.TimelineUIUtils._eventCategories;
}
/**
* @param {!Array} categories
*/
static setTimelineMainEventCategories(categories) {
Timeline.TimelineUIUtils._eventCategories = categories;
}
/**
* @param {!Object} aggregatedStats
* @param {!Timeline.TimelineCategory=} selfCategory
* @param {number=} selfTime
* @return {!Element}
*/
static generatePieChart(aggregatedStats, selfCategory, selfTime) {
let total = 0;
for (const categoryName in aggregatedStats) {
total += aggregatedStats[categoryName];
}
const element = createElementWithClass('div', 'timeline-details-view-pie-chart-wrapper hbox');
const pieChart = new PerfUI.PieChart({
chartName: ls`Time spent in rendering`,
size: 110,
formatter: value => Number.preciseMillisToString(value),
showLegend: true,
});
pieChart.element.classList.add('timeline-details-view-pie-chart');
pieChart.setTotal(total);
const pieChartContainer = element.createChild('div', 'vbox');
pieChartContainer.appendChild(pieChart.element);
/**
* @param {string} name
* @param {string} title
* @param {number} value
* @param {string} color
*/
function appendLegendRow(name, title, value, color) {
if (!value) {
return;
}
pieChart.addSlice(value, color, title);
}
// In case of self time, first add self, then children of the same category.
if (selfCategory) {
if (selfTime) {
appendLegendRow(
selfCategory.name, Common.UIString('%s (self)', selfCategory.title), selfTime, selfCategory.color);
}
// Children of the same category.
const categoryTime = aggregatedStats[selfCategory.name];
const value = categoryTime - selfTime;
if (value > 0) {
appendLegendRow(
selfCategory.name, Common.UIString('%s (children)', selfCategory.title), value, selfCategory.childColor);
}
}
// Add other categories.
for (const categoryName in Timeline.TimelineUIUtils.categories()) {
const category = Timeline.TimelineUIUtils.categories()[categoryName];
if (category === selfCategory) {
continue;
}
appendLegendRow(category.name, category.title, aggregatedStats[category.name], category.childColor);
}
return element;
}
/**
* @param {!TimelineModel.TimelineFrame} frame
* @param {?SDK.FilmStripModel.Frame} filmStripFrame
* @return {!Element}
*/
static generateDetailsContentForFrame(frame, filmStripFrame) {
const contentHelper = new Timeline.TimelineDetailsContentHelper(null, null);
contentHelper.addSection(ls`Frame`);
const duration = Timeline.TimelineUIUtils.frameDuration(frame);
contentHelper.appendElementRow(ls`Duration`, duration, frame.hasWarnings());
const durationInMillis = frame.endTime - frame.startTime;
contentHelper.appendTextRow(ls`FPS`, Math.floor(1000 / durationInMillis));
contentHelper.appendTextRow(ls`CPU time`, Number.millisToString(frame.cpuTime, true));
if (filmStripFrame) {
const filmStripPreview = createElementWithClass('div', 'timeline-filmstrip-preview');
filmStripFrame.imageDataPromise()
.then(data => UI.loadImageFromData(data))
.then(image => image && filmStripPreview.appendChild(image));
contentHelper.appendElementRow('', filmStripPreview);
filmStripPreview.addEventListener('click', frameClicked.bind(null, filmStripFrame), false);
}
if (frame.layerTree) {
contentHelper.appendElementRow(ls`Layer tree`, Components.Linkifier.linkifyRevealable(frame.layerTree, ls`Show`));
}
/**
* @param {!SDK.FilmStripModel.Frame} filmStripFrame
*/
function frameClicked(filmStripFrame) {
new PerfUI.FilmStripView.Dialog(filmStripFrame, 0);
}
return contentHelper.fragment;
}
/**
* @param {!TimelineModel.TimelineFrame} frame
* @return {!Element}
*/
static frameDuration(frame) {
const durationText = Common.UIString(
'%s (at %s)', Number.millisToString(frame.endTime - frame.startTime, true),
Number.millisToString(frame.startTimeOffset, true));
if (!frame.hasWarnings()) {
return UI.formatLocalized('%s', [durationText]);
}
const link = UI.XLink.create('https://developers.google.com/web/fundamentals/performance/rendering/', ls`jank`);
return UI.formatLocalized('%s. Long frame times are an indication of %s', [durationText, link]);
}
/**
* @param {!CanvasRenderingContext2D} context
* @param {number} width
* @param {number} height
* @param {string} color0
* @param {string} color1
* @param {string} color2
* @return {!CanvasGradient}
*/
static createFillStyle(context, width, height, color0, color1, color2) {
const gradient = context.createLinearGradient(0, 0, width, height);
gradient.addColorStop(0, color0);
gradient.addColorStop(0.25, color1);
gradient.addColorStop(0.75, color1);
gradient.addColorStop(1, color2);
return gradient;
}
/**
* @param {!Array.<number>} quad
* @return {number}
*/
static quadWidth(quad) {
return Math.round(Math.sqrt(Math.pow(quad[0] - quad[2], 2) + Math.pow(quad[1] - quad[3], 2)));
}
/**
* @param {!Array.<number>} quad
* @return {number}
*/
static quadHeight(quad) {
return Math.round(Math.sqrt(Math.pow(quad[0] - quad[6], 2) + Math.pow(quad[1] - quad[7], 2)));
}
/**
* @return {!Array.<!Timeline.TimelineUIUtils.EventDispatchTypeDescriptor>}
*/
static eventDispatchDesciptors() {
if (Timeline.TimelineUIUtils._eventDispatchDesciptors) {
return Timeline.TimelineUIUtils._eventDispatchDesciptors;
}
const lightOrange = 'hsl(40,100%,80%)';
const orange = 'hsl(40,100%,50%)';
const green = 'hsl(90,100%,40%)';
const purple = 'hsl(256,100%,75%)';
Timeline.TimelineUIUtils._eventDispatchDesciptors = [
new Timeline.TimelineUIUtils.EventDispatchTypeDescriptor(
1, lightOrange, ['mousemove', 'mouseenter', 'mouseleave', 'mouseout', 'mouseover']),
new Timeline.TimelineUIUtils.EventDispatchTypeDescriptor(
1, lightOrange, ['pointerover', 'pointerout', 'pointerenter', 'pointerleave', 'pointermove']),
new Timeline.TimelineUIUtils.EventDispatchTypeDescriptor(2, green, ['wheel']),
new Timeline.TimelineUIUtils.EventDispatchTypeDescriptor(3, orange, ['click', 'mousedown', 'mouseup']),
new Timeline.TimelineUIUtils.EventDispatchTypeDescriptor(
3, orange, ['touchstart', 'touchend', 'touchmove', 'touchcancel']),
new Timeline.TimelineUIUtils.EventDispatchTypeDescriptor(
3, orange, ['pointerdown', 'pointerup', 'pointercancel', 'gotpointercapture', 'lostpointercapture']),
new Timeline.TimelineUIUtils.EventDispatchTypeDescriptor(3, purple, ['keydown', 'keyup', 'keypress'])
];
return Timeline.TimelineUIUtils._eventDispatchDesciptors;
}
/**
* @param {!SDK.TracingModel.Event} event
* @return {?string}
*/
static markerShortTitle(event) {
const recordTypes = TimelineModel.TimelineModel.RecordType;
switch (event.name) {
case recordTypes.MarkDOMContent:
return ls`DCL`;
case recordTypes.MarkLoad:
return ls`L`;
case recordTypes.MarkFirstPaint:
return ls`FP`;
case recordTypes.MarkFCP:
return ls`FCP`;
case recordTypes.MarkFMP:
return ls`FMP`;
case recordTypes.MarkLCPCandidate:
return ls`LCP`;
}
return null;
}
/**
* @param {!SDK.TracingModel.Event} event
* @return {!Timeline.TimelineMarkerStyle}
*/
static markerStyleForEvent(event) {
const tallMarkerDashStyle = [6, 4];
const title = Timeline.TimelineUIUtils.eventTitle(event);
if (event.hasCategory(TimelineModel.TimelineModel.Category.Console) ||
event.hasCategory(TimelineModel.TimelineModel.Category.UserTiming)) {
return {
title: title,
dashStyle: tallMarkerDashStyle,
lineWidth: 0.5,
color: event.hasCategory(TimelineModel.TimelineModel.Category.UserTiming) ? 'purple' : 'orange',
tall: false,
lowPriority: false,
};
}
const recordTypes = TimelineModel.TimelineModel.RecordType;
let tall = false;
let color = 'grey';
switch (event.name) {
case recordTypes.FrameStartedLoading:
color = 'green';
tall = true;
break;
case recordTypes.MarkDOMContent:
color = '#0867CB';
tall = true;
break;
case recordTypes.MarkLoad:
color = '#B31412';
tall = true;
break;
case recordTypes.MarkFirstPaint:
color = '#228847';
tall = true;
break;
case recordTypes.MarkFCP:
color = '#1A6937';
tall = true;
break;
case recordTypes.MarkFMP:
color = '#134A26';
tall = true;
break;
case recordTypes.MarkLCPCandidate:
color = '#1A3422';
tall = true;
break;
case recordTypes.TimeStamp:
color = 'orange';
break;
}
return {
title: title,
dashStyle: tallMarkerDashStyle,
lineWidth: 0.5,
color: color,
tall: tall,
lowPriority: false,
};
}
/**
* @return {!Timeline.TimelineMarkerStyle}
*/
static markerStyleForFrame() {
return {
title: ls`Frame`,
color: 'rgba(100, 100, 100, 0.4)',
lineWidth: 3,
dashStyle: [3],
tall: true,
lowPriority: true
};
}
/**
* @param {string} id
* @return {string}
*/
static colorForId(id) {
if (!Timeline.TimelineUIUtils.colorForId._colorGenerator) {
Timeline.TimelineUIUtils.colorForId._colorGenerator =
new Common.Color.Generator({min: 30, max: 330}, {min: 50, max: 80, count: 3}, 85);
Timeline.TimelineUIUtils.colorForId._colorGenerator.setColorForID('', '#f2ecdc');
}
return Timeline.TimelineUIUtils.colorForId._colorGenerator.colorForID(id);
}
/**
* @param {!SDK.TracingModel.Event} event
* @param {string=} warningType
* @return {?Element}
*/
static eventWarning(event, warningType) {
const timelineData = TimelineModel.TimelineData.forEvent(event);
const warning = warningType || timelineData.warning;
if (!warning) {
return null;
}
const warnings = TimelineModel.TimelineModel.WarningType;
const span = createElement('span');
const eventData = event.args['data'];
switch (warning) {
case warnings.ForcedStyle:
case warnings.ForcedLayout:
const forcedReflowLink = UI.createDocumentationLink(
'../../fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing#avoid-forced-synchronous-layouts',
ls`Forced reflow`);
span.appendChild(UI.formatLocalized('%s is a likely performance bottleneck.', [forcedReflowLink]));
break;
case warnings.IdleDeadlineExceeded:
const exceededMs = Number.millisToString(event.duration - eventData['allottedMilliseconds'], true);
span.textContent = ls`Idle callback execution extended beyond deadline by ${exceededMs}`;
break;
case warnings.LongHandler:
span.textContent = Common.UIString('Handler took %s', Number.millisToString(event.duration, true));
break;
case warnings.LongRecurringHandler:
span.textContent = Common.UIString('Recurring handler took %s', Number.millisToString(event.duration, true));
break;
case warnings.LongTask:
const longTaskLink =
UI.createDocumentationLink('../../fundamentals/performance/rail#goals-and-guidelines', ls`Long task`);
span.appendChild(
UI.formatLocalized('%s took %s.', [longTaskLink, Number.millisToString(event.duration, true)]));
break;
case warnings.V8Deopt:
span.appendChild(UI.XLink.create(
'https://github.com/GoogleChrome/devtools-docs/issues/53', Common.UIString('Not optimized')));
span.createTextChild(Common.UIString(': %s', eventData['deoptReason']));
break;
default:
console.assert(false, 'Unhandled TimelineModel.WarningType');
}
return span;
}
/**
* @param {!TimelineModel.TimelineModel.PageFrame} frame
* @param {number=} trimAt
*/
static displayNameForFrame(frame, trimAt) {
const url = frame.url;
if (!trimAt) {
trimAt = 30;
}
return url.startsWith('about:') ? `"${frame.name.trimMiddle(trimAt)}"` : frame.url.trimEnd(trimAt);
}
};
Timeline.TimelineRecordStyle = class {
/**
* @param {string} title
* @param {!Timeline.TimelineCategory} category
* @param {boolean=} hidden
*/
constructor(title, category, hidden = false) {
this.title = title;
this.category = category;
this.hidden = hidden;
}
};
/**
* @enum {symbol}
*/
Timeline.TimelineUIUtils.NetworkCategory = {
HTML: Symbol('HTML'),
Script: Symbol('Script'),
Style: Symbol('Style'),
Media: Symbol('Media'),
Other: Symbol('Other')
};
Timeline.TimelineUIUtils._aggregatedStatsKey = Symbol('aggregatedStats');
/**
* @unrestricted
*/
Timeline.TimelineUIUtils.InvalidationsGroupElement = class extends UI.TreeElement {
/**
* @param {!SDK.Target} target
* @param {?Map<number, ?SDK.DOMNode>} relatedNodesMap
* @param {!Timeline.TimelineDetailsContentHelper} contentHelper
* @param {!Array.<!TimelineModel.InvalidationTrackingEvent>} invalidations
*/
constructor(target, relatedNodesMap, contentHelper, invalidations) {
super('', true);
this.listItemElement.classList.add('header');
this.selectable = false;
this.toggleOnClick = true;
this._relatedNodesMap = relatedNodesMap;
this._contentHelper = contentHelper;
this._invalidations = invalidations;
this.title = this._createTitle(target);
}
/**
* @param {!SDK.Target} target
* @return {!Element}
*/
_createTitle(target) {
const first = this._invalidations[0];
const reason = first.cause.reason || ls`Unknown cause`;
const topFrame = first.cause.stackTrace && first.cause.stackTrace[0];
const truncatedNodesElement = this._getTruncatedNodesElement(this._invalidations);
if (truncatedNodesElement === null) {
return UI.formatLocalized(reason, []);
}
const title = UI.formatLocalized('%s for %s', [reason, truncatedNodesElement]);
if (topFrame && this._contentHelper.linkifier()) {
const stack = createElementWithClass('span', 'monospace');
const completeTitle = UI.formatLocalized('%s. %s', [title, stack]);
stack.createChild('span').textContent = Timeline.TimelineUIUtils.frameDisplayName(topFrame);
const link = this._contentHelper.linkifier().maybeLinkifyConsoleCallFrame(target, topFrame);
if (link) {
stack.createChild('span').textContent = ' @ ';
stack.createChild('span').appendChild(link);
}
return completeTitle;
}
return title;
}
/**
* @override
* @returns {!Promise}
*/
async onpopulate() {
const content = createElementWithClass('div', 'content');
const first = this._invalidations[0];
if (first.cause.stackTrace) {
const stack = content.createChild('div');
stack.createTextChild(ls`Stack trace:`);
this._contentHelper.createChildStackTraceElement(
stack, Timeline.TimelineUIUtils._stackTraceFromCallFrames(first.cause.stackTrace));
}
content.createTextChild(this._invalidations.length !== 1 ? ls`Nodes:` : ls`Node:`);
const nodeList = content.createChild('div', 'node-list');
let firstNode = true;
for (let i = 0; i < this._invalidations.length; i++) {
const invalidation = this._invalidations[i];
const invalidationNode = this._createInvalidationNode(invalidation, true);
if (invalidationNode) {
if (!firstNode) {
nodeList.createTextChild(ls`, `);
}
firstNode = false;
nodeList.appendChild(invalidationNode);
const extraData = invalidation.extraData ? ', ' + invalidation.extraData : '';
if (invalidation.changedId) {
nodeList.createTextChild(Common.UIString('(changed id to "%s"%s)', invalidation.changedId, extraData));
} else if (invalidation.changedClass) {
nodeList.createTextChild(Common.UIString('(changed class to "%s"%s)', invalidation.changedClass, extraData));
} else if (invalidation.changedAttribute) {
nodeList.createTextChild(
Common.UIString('(changed attribute to "%s"%s)', invalidation.changedAttribute, extraData));
} else if (invalidation.changedPseudo) {
nodeList.createTextChild(
Common.UIString('(changed pesudo to "%s"%s)', invalidation.changedPseudo, extraData));
} else if (invalidation.selectorPart) {
nodeList.createTextChild(Common.UIString('(changed "%s"%s)', invalidation.selectorPart, extraData));
}
}
}
const contentTreeElement = new UI.TreeElement(content, false);
contentTreeElement.selectable = false;
this.appendChild(contentTreeElement);
}
/**
* @param {!Array.<!TimelineModel.InvalidationTrackingEvent>} invalidations
* @returns {?Element}
*/
_getTruncatedNodesElement(invalidations) {
const invalidationNodes = [];
const invalidationNodeIdMap = {};
for (let i = 0; i < invalidations.length; i++) {
const invalidation = invalidations[i];
const invalidationNode = this._createInvalidationNode(invalidation, false);
invalidationNode.addEventListener('click', e => e.consume(), false);
if (invalidationNode && !invalidationNodeIdMap[invalidation.nodeId]) {
invalidationNodes.push(invalidationNode);
invalidationNodeIdMap[invalidation.nodeId] = true;
}
}
if (invalidationNodes.length === 1) {
return invalidationNodes[0];
} else if (invalidationNodes.length === 2) {
return UI.formatLocalized('%s and %s', invalidationNodes);
} else if (invalidationNodes.length === 3) {
return UI.formatLocalized('%s, %s, and 1 other', invalidationNodes.slice(0, 2));
} else if (invalidationNodes.length >= 4) {
return UI.formatLocalized(
'%s, %s, and %s others', [...invalidationNodes.slice(0, 2), (invalidationNodes.length - 2).toString()]);
}
return null;
}
/**
* @param {!TimelineModel.InvalidationTrackingEvent} invalidation
* @param {boolean} showUnknownNodes
*/
_createInvalidationNode(invalidation, showUnknownNodes) {
const node = (invalidation.nodeId && this._relatedNodesMap) ? this._relatedNodesMap.get(invalidation.nodeId) : null;
if (node) {
const nodeSpan = createElement('span');
Common.Linkifier.linkify(node).then(link => nodeSpan.appendChild(link));
return nodeSpan;
}
if (invalidation.nodeName) {
const nodeSpan = createElement('span');
nodeSpan.textContent = Common.UIString('[ %s ]', invalidation.nodeName);
return nodeSpan;
}
if (showUnknownNodes) {
const nodeSpan = createElement('span');
return nodeSpan.createTextChild(Common.UIString('[ unknown node ]'));
}
}
};
Timeline.TimelineUIUtils._previewElementSymbol = Symbol('previewElement');
/**
* @unrestricted
*/
Timeline.TimelineUIUtils.EventDispatchTypeDescriptor = class {
/**
* @param {number} priority
* @param {string} color
* @param {!Array.<string>} eventTypes
*/
constructor(priority, color, eventTypes) {
this.priority = priority;
this.color = color;
this.eventTypes = eventTypes;
}
};
/**
* @unrestricted
*/
Timeline.TimelineCategory = class extends Common.Object {
/**
* @param {string} name
* @param {string} title
* @param {boolean} visible
* @param {string} childColor
* @param {string} color
*/
constructor(name, title, visible, childColor, color) {
super();
this.name = name;
this.title = title;
this.visible = visible;
this.childColor = childColor;
this.color = color;
this.hidden = false;
}
/**
* @return {boolean}
*/
get hidden() {
return this._hidden;
}
/**
* @param {boolean} hidden
*/
set hidden(hidden) {
this._hidden = hidden;
this.dispatchEventToListeners(Timeline.TimelineCategory.Events.VisibilityChanged, this);
}
};
/** @enum {symbol} */
Timeline.TimelineCategory.Events = {
VisibilityChanged: Symbol('VisibilityChanged')
};
/**
* @typedef {!{
* title: string,
* color: string,
* lineWidth: number,
* dashStyle: !Array.<number>,
* tall: boolean,
* lowPriority: boolean
* }}
*/
Timeline.TimelineMarkerStyle;
/**
* @unrestricted
*/
Timeline.TimelinePopupContentHelper = class {
/**
* @param {string} title
*/
constructor(title) {
this._contentTable = createElement('table');
const titleCell = this._createCell(Common.UIString('%s - Details', title), 'timeline-details-title');
titleCell.colSpan = 2;
const titleRow = createElement('tr');
titleRow.appendChild(titleCell);
this._contentTable.appendChild(titleRow);
}
/**
* @return {!Element}
*/
contentTable() {
return this._contentTable;
}
/**
* @param {string|number} content
* @param {string=} styleName
*/
_createCell(content, styleName) {
const text = createElement('label');
text.createTextChild(String(content));
const cell = createElement('td');
cell.className = 'timeline-details';
if (styleName) {
cell.className += ' ' + styleName;
}
cell.textContent = content;
return cell;
}
/**
* @param {string} title
* @param {string|number} content
*/
appendTextRow(title, content) {
const row = createElement('tr');
row.appendChild(this._createCell(title, 'timeline-details-row-title'));
row.appendChild(this._createCell(content, 'timeline-details-row-data'));
this._contentTable.appendChild(row);
}
/**
* @param {string} title
* @param {!Node|string} content
*/
appendElementRow(title, content) {
const row = createElement('tr');
const titleCell = this._createCell(title, 'timeline-details-row-title');
row.appendChild(titleCell);
const cell = createElement('td');
cell.className = 'details';
if (content instanceof Node) {
cell.appendChild(content);
} else {
cell.createTextChild(content || '');
}
row.appendChild(cell);
this._contentTable.appendChild(row);
}
};
/**
* @unrestricted
*/
Timeline.TimelineDetailsContentHelper = class {
/**
* @param {?SDK.Target} target
* @param {?Components.Linkifier} linkifier
*/
constructor(target, linkifier) {
this.fragment = createDocumentFragment();
this._linkifier = linkifier;
this._target = target;
this.element = createElementWithClass('div', 'timeline-details-view-block');
this._tableElement = this.element.createChild('div', 'vbox timeline-details-chip-body');
this.fragment.appendChild(this.element);
}
/**
* @param {string} title
* @param {string=} swatchColor
*/
addSection(title, swatchColor) {
if (!this._tableElement.hasChildNodes()) {
this.element.removeChildren();
} else {
this.element = createElementWithClass('div', 'timeline-details-view-block');
this.fragment.appendChild(this.element);
}
if (title) {
const titleElement = this.element.createChild('div', 'timeline-details-chip-title');
if (swatchColor) {
titleElement.createChild('div').style.backgroundColor = swatchColor;
}
titleElement.createTextChild(title);
}
this._tableElement = this.element.createChild('div', 'vbox timeline-details-chip-body');
this.fragment.appendChild(this.element);
}
/**
* @return {?Components.Linkifier}
*/
linkifier() {
return this._linkifier;
}
/**
* @param {string} title
* @param {string|number|boolean} value
*/
appendTextRow(title, value) {
const rowElement = this._tableElement.createChild('div', 'timeline-details-view-row');
rowElement.createChild('div', 'timeline-details-view-row-title').textContent = title;
rowElement.createChild('div', 'timeline-details-view-row-value').textContent = value;
}
/**
* @param {string} title
* @param {!Node|string} content
* @param {boolean=} isWarning
* @param {boolean=} isStacked
*/
appendElementRow(title, content, isWarning, isStacked) {
const rowElement = this._tableElement.createChild('div', 'timeline-details-view-row');
if (isWarning) {
rowElement.classList.add('timeline-details-warning');
}
if (isStacked) {
rowElement.classList.add('timeline-details-stack-values');
}
const titleElement = rowElement.createChild('div', 'timeline-details-view-row-title');
titleElement.textContent = title;
const valueElement = rowElement.createChild('div', 'timeline-details-view-row-value');
if (content instanceof Node) {
valueElement.appendChild(content);
} else {
valueElement.createTextChild(content || '');
}
}
/**
* @param {string} title
* @param {string} url
* @param {number} startLine
* @param {number=} startColumn
*/
appendLocationRow(title, url, startLine, startColumn) {
if (!this._linkifier || !this._target) {
return;
}
const link = this._linkifier.maybeLinkifyScriptLocation(this._target, null, url, startLine, startColumn);
if (!link) {
return;
}
this.appendElementRow(title, link);
}
/**
* @param {string} title
* @param {string} url
* @param {number} startLine
* @param {number=} endLine
*/
appendLocationRange(title, url, startLine, endLine) {
if (!this._linkifier || !this._target) {
return;
}
const locationContent = createElement('span');
const link = this._linkifier.maybeLinkifyScriptLocation(this._target, null, url, startLine);
if (!link) {
return;
}
locationContent.appendChild(link);
locationContent.createTextChild(String.sprintf(' [%s\u2026%s]', startLine + 1, endLine + 1 || ''));
this.appendElementRow(title, locationContent);
}
/**
* @param {string} title
* @param {!Protocol.Runtime.StackTrace} stackTrace
*/
appendStackTrace(title, stackTrace) {
if (!this._linkifier || !this._target) {
return;
}
const rowElement = this._tableElement.createChild('div', 'timeline-details-view-row');
rowElement.createChild('div', 'timeline-details-view-row-title').textContent = title;
this.createChildStackTraceElement(rowElement, stackTrace);
}
/**
* @param {!Element} parentElement
* @param {!Protocol.Runtime.StackTrace} stackTrace
*/
createChildStackTraceElement(parentElement, stackTrace) {
if (!this._linkifier || !this._target) {
return;
}
parentElement.classList.add('timeline-details-stack-values');
const stackTraceElement =
parentElement.createChild('div', 'timeline-details-view-row-value timeline-details-view-row-stack-trace');
const callFrameContents =
Components.JSPresentationUtils.buildStackTracePreviewContents(this._target, this._linkifier, stackTrace);
stackTraceElement.appendChild(callFrameContents.element);
}
/**
* @param {!SDK.TracingModel.Event} event
* @param {string=} warningType
*/
appendWarningRow(event, warningType) {
const warning = Timeline.TimelineUIUtils.eventWarning(event, warningType);
if (warning) {
this.appendElementRow(ls`Warning`, warning, true);
}
}
};
Timeline.TimelineUIUtils._categoryBreakdownCacheSymbol = Symbol('categoryBreakdownCache');