blob: 9d74f9bcb93b5a9f7bb69aeec0809b430235f135 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/*
* 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.
*/
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as TimelineModel from '../../models/timeline_model/timeline_model.js';
import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import type * as Protocol from '../../generated/protocol.js';
import invalidationsTreeStyles from './invalidationsTree.css.js';
// eslint-disable-next-line rulesdir/es_modules_import
import imagePreviewStyles from '../../ui/legacy/components/utils/imagePreview.css.js';
import {CLSRect} from './CLSLinkifier.js';
import {TimelinePanel, TimelineSelection} from './TimelinePanel.js';
const UIStrings = {
/**
*@description Text in Timeline UIUtils of the Performance panel
*@example {node1} PH1
*@example {node2} PH2
*/
sAndS: '{PH1} and {PH2}',
/**
*@description Text in Timeline UIUtils of the Performance panel
*@example {node1} PH1
*@example {node2} PH2
*/
sAndSOther: '{PH1}, {PH2}, and 1 other',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
task: 'Task',
/**
*@description Text for other types of items
*/
other: 'Other',
/**
*@description Text that refers to the animation of the web page
*/
animation: 'Animation',
/**
*@description Text that refers to some events
*/
event: 'Event',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
requestMainThreadFrame: 'Request Main Thread Frame',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
frameStart: 'Frame Start',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
frameStartMainThread: 'Frame Start (main thread)',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
drawFrame: 'Draw Frame',
/**
*@description The process the browser uses to determine a target element for a
*pointer event. Typically, this is determined by considering the pointer's
*location and also the visual layout of elements on the screen.
*/
hitTest: 'Hit Test',
/**
*@description Noun for an event in the Performance panel. The browser has decided
*that the styles for some elements need to be recalculated and scheduled that
*recalculation process at some time in the future.
*/
scheduleStyleRecalculation: 'Schedule Style Recalculation',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
recalculateStyle: 'Recalculate Style',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
invalidateLayout: 'Invalidate Layout',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
layout: 'Layout',
/**
*@description Noun for an event in the Performance panel. Paint setup is a
*step before the 'Paint' event. A paint event is when the browser draws pixels
*to the screen. This step is the setup beforehand.
*/
paintSetup: 'Paint Setup',
/**
*@description Noun for a paint event in the Performance panel, where an image
*was being painted. A paint event is when the browser draws pixels to the
*screen, in this case specifically for an image in a website.
*/
paintImage: 'Paint Image',
/**
*@description Noun for an event in the Performance panel. Pre-paint is a
*step before the 'Paint' event. A paint event is when the browser records the
*instructions for drawing the page. This step is the setup beforehand.
*/
prePaint: 'Pre-Paint',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
updateLayer: 'Update Layer',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
updateLayerTree: 'Update Layer Tree',
/**
*@description Noun for a paint event in the Performance panel. A paint event is when the browser draws pixels to the screen.
*/
paint: 'Paint',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
rasterizePaint: 'Rasterize Paint',
/**
*@description The action to scroll
*/
scroll: 'Scroll',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
compositeLayers: 'Composite Layers',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
computeIntersections: 'Compute Intersections',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
parseHtml: 'Parse HTML',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
parseStylesheet: 'Parse Stylesheet',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
installTimer: 'Install Timer',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
removeTimer: 'Remove Timer',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
timerFired: 'Timer Fired',
/**
*@description Text for an event. Shown in the timeline in the Performance panel.
* XHR refers to XmlHttpRequest, a Web API. This particular Web API has a property
* named 'readyState' (https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState). When
* the 'readyState' property changes the text is shown.
*/
xhrReadyStateChange: '`XHR` Ready State Change',
/**
* @description Text for an event. Shown in the timeline in the Perforamnce panel.
* XHR refers to XmlHttpRequest, a Web API. (see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)
* The text is shown when a XmlHttpRequest load event happens on the inspected page.
*/
xhrLoad: '`XHR` Load',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
compileScript: 'Compile Script',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
cacheScript: 'Cache Script Code',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
compileCode: 'Compile Code',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
optimizeCode: 'Optimize Code',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
evaluateScript: 'Evaluate Script',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
compileModule: 'Compile Module',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
cacheModule: 'Cache Module Code',
/**
* @description Text for an event. Shown in the timeline in the Perforamnce panel.
* "Module" refers to JavaScript modules: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
* JavaScript modules are a way to organize JavaScript code.
* "Evaluate" is the phase when the JavaScript code of a module is executed.
*/
evaluateModule: 'Evaluate Module',
/**
*@description Noun indicating that a compile task (type: streaming) happened.
*/
streamingCompileTask: 'Streaming Compile Task',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
waitingForNetwork: 'Waiting for Network',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
parseAndCompile: 'Parse and Compile',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
streamingWasmResponse: 'Streaming Wasm Response',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
compiledWasmModule: 'Compiled Wasm Module',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
cachedWasmModule: 'Cached Wasm Module',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
wasmModuleCacheHit: 'Wasm Module Cache Hit',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
wasmModuleCacheInvalid: 'Wasm Module Cache Invalid',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
frameStartedLoading: 'Frame Started Loading',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
onloadEvent: 'Onload Event',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
domcontentloadedEvent: 'DOMContentLoaded Event',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
firstPaint: 'First Paint',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
firstContentfulPaint: 'First Contentful Paint',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
largestContentfulPaint: 'Largest Contentful Paint',
/**
*@description Text for timestamps of items
*/
timestamp: 'Timestamp',
/**
*@description Noun for a 'time' event that happens in the Console (a tool in
* DevTools). The user can trigger console time events from their code, and
* they will show up in the Performance panel. Time events are used to measure
* the duration of something, e.g. the user will emit two time events at the
* start and end of some interesting task.
*/
consoleTime: 'Console Time',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
userTiming: 'User Timing',
/**
* @description Name for an event shown in the Performance panel. When a network
* request is about to be sent by the browser, the time is recorded and DevTools
* is notified that a network request will be sent momentarily.
*/
willSendRequest: 'Will Send Request',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
sendRequest: 'Send Request',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
receiveResponse: 'Receive Response',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
finishLoading: 'Finish Loading',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
receiveData: 'Receive Data',
/**
*@description Event category in the Performance panel for time spent to execute microtasks in JavaScript
*/
runMicrotasks: 'Run Microtasks',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
functionCall: 'Function Call',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
gcEvent: 'GC Event',
/**
*@description Event category in the Performance panel for time spent to perform a full Garbage Collection pass
*/
majorGc: 'Major GC',
/**
*@description Event category in the Performance panel for time spent to perform a quick Garbage Collection pass
*/
minorGc: 'Minor GC',
/**
*@description Event category in the Performance panel for time spent to execute JavaScript
*/
jsFrame: 'JS Frame',
/**
*@description Text for the request animation frame event
*/
requestAnimationFrame: 'Request Animation Frame',
/**
*@description Text to cancel the animation frame
*/
cancelAnimationFrame: 'Cancel Animation Frame',
/**
*@description Text for the event that an animation frame is fired
*/
animationFrameFired: 'Animation Frame Fired',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
requestIdleCallback: 'Request Idle Callback',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
cancelIdleCallback: 'Cancel Idle Callback',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
fireIdleCallback: 'Fire Idle Callback',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
createWebsocket: 'Create WebSocket',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
sendWebsocketHandshake: 'Send WebSocket Handshake',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
receiveWebsocketHandshake: 'Receive WebSocket Handshake',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
destroyWebsocket: 'Destroy WebSocket',
/**
*@description Event category in the Performance panel for time spent in the embedder of the WebView
*/
embedderCallback: 'Embedder Callback',
/**
*@description Event category in the Performance panel for time spent decoding an image
*/
imageDecode: 'Image Decode',
/**
*@description Event category in the Performance panel for time spent to resize an image
*/
imageResize: 'Image Resize',
/**
*@description Event category in the Performance panel for time spent in the GPU
*/
gpu: 'GPU',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
inputLatency: 'Input Latency',
/**
*@description Event category in the Performance panel for time spent to perform Garbage Collection for the Document Object Model
*/
domGc: 'DOM GC',
/**
*@description Event category in the Performance panel for time spent to perform encryption
*/
encrypt: 'Encrypt',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
encryptReply: 'Encrypt Reply',
/**
*@description Event category in the Performance panel for time spent to perform decryption
*/
decrypt: 'Decrypt',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
decryptReply: 'Decrypt Reply',
/**
* @description Noun phrase meaning 'the browser was preparing the digest'.
* Digest: https://developer.mozilla.org/en-US/docs/Glossary/Digest
*/
digest: 'Digest',
/**
*@description Noun phrase meaning 'the browser was preparing the digest
*reply'. Digest: https://developer.mozilla.org/en-US/docs/Glossary/Digest
*/
digestReply: 'Digest Reply',
/**
*@description The 'sign' stage of a web crypto event. Shown when displaying what the website was doing at a particular point in time.
*/
sign: 'Sign',
/**
* @description Noun phrase for an event of the Web Crypto API. The event is recorded when the signing process is concluded.
* Signature: https://developer.mozilla.org/en-US/docs/Glossary/Signature/Security
*/
signReply: 'Sign Reply',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
verify: 'Verify',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
verifyReply: 'Verify Reply',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
asyncTask: 'Async Task',
/**
*@description Text in Timeline for Layout Shift records
*/
layoutShift: 'Layout Shift',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
keyCharacter: 'Key — Character',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
keyDown: 'Key Down',
/**
*@description Noun for the end keyboard key event in the Performance panel. 'Up' refers to the keyboard key bouncing back up after being pushed down.
*/
keyUp: 'Key Up',
/**
*@description Noun for a mouse click event in the Performance panel.
*/
click: 'Click',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
contextMenu: 'Context Menu',
/**
*@description Noun for the start of a mouse event in the Performance panel. Down refers to the button on the mouse being pressed down.
*/
mouseDown: 'Mouse Down',
/**
*@description Noun for a mouse move event in the Performance panel.
*/
mouseMove: 'Mouse Move',
/**
*@description Noun for the end of a mouse event in the Performance panel. Up refers to the button on the mouse being released.
*/
mouseUp: 'Mouse Up',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
mouseWheel: 'Mouse Wheel',
/**
*@description Noun for the beginning of a mouse scroll wheel event in the Performance panel.
*/
scrollBegin: 'Scroll Begin',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
scrollEnd: 'Scroll End',
/**
*@description Noun for an update of a mouse scroll wheel event in the Performance panel.
*/
scrollUpdate: 'Scroll Update',
/**
*@description Noun for the beginning of a fling gesture event in the Performance panel.
*/
flingStart: 'Fling Start',
/**
*@description Noun for the end of a fling gesture event in the Performance panel.
*/
flingHalt: 'Fling Halt',
/**
*@description Noun for a tap event (tap on a touch screen device) in the Performance panel.
*/
tap: 'Tap',
/**
*@description Noun for the end of a tap event (tap on a touch screen device) in the Performance panel.
*/
tapHalt: 'Tap Halt',
/**
*@description Noun for the start of a tap event (tap on a touch screen device) in the Performance panel.
*/
tapBegin: 'Tap Begin',
/**
*@description Noun for the beginning of a tap gesture event in the Performance
*panel. 'Down' refers to the start (downward tap direction), as opposed to up
*(finger leaving the touch surface).
*/
tapDown: 'Tap Down',
/**
* @description Noun for the cancelation of an input touch event in the Performance panel.
* For example this can happen when the user touches the surface with too many fingers.
* This is opposed to a "Touch End" event, where the user lifts the finger from the surface.
*/
touchCancel: 'Touch Cancel',
/**
*@description Noun for the end of an input touch event in the Performance panel.
*/
touchEnd: 'Touch End',
/**
*@description Noun for an input touch event in the Performance panel.
*/
touchMove: 'Touch Move',
/**
*@description Noun for the start of an input touch event in the Performance panel.
*/
touchStart: 'Touch Start',
/**
*@description Noun for the beginning of a pinch gesture event in the Performance panel.
*/
pinchBegin: 'Pinch Begin',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
pinchEnd: 'Pinch End',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
pinchUpdate: 'Pinch Update',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
compile: 'Compile',
/**
*@description Text to parse something
*/
parse: 'Parse',
/**
*@description Text with two placeholders separated by a colon
*@example {Node removed} PH1
*@example {div#id1} PH2
*/
sS: '{PH1}: {PH2}',
/**
*@description Label of a field in a timeline. A Network response refers to the act of acknowledging a
network request. Should not be confused with answer.
*/
response: 'Response',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
fling: 'Fling',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
drag: 'Drag',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
uncategorized: 'Uncategorized',
/**
*@description Details text in Timeline UIUtils of the Performance panel
*@example {30 MB} PH1
*/
sCollected: '{PH1} collected',
/**
*@description Details text in Timeline UIUtils of the Performance panel
*@example {https://example.com} PH1
*@example {2} PH2
*@example {4} PH3
*/
sSs: '{PH1} [{PH2}…{PH3}]',
/**
*@description Details text in Timeline UIUtils of the Performance panel
*@example {https://example.com} PH1
*@example {2} PH2
*/
sSSquareBrackets: '{PH1} [{PH2}…]',
/**
*@description Text that is usually a hyperlink to more documentation
*/
learnMore: 'Learn more',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
compilationCacheStatus: 'Compilation cache status',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
compilationCacheSize: 'Compilation cache size',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
scriptLoadedFromCache: 'script loaded from cache',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
failedToLoadScriptFromCache: 'failed to load script from cache',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
scriptNotEligible: 'script not eligible',
/**
*@description Text for the total time of something
*/
totalTime: 'Total Time',
/**
*@description Time of a single activity, as opposed to the total time
*/
selfTime: 'Self Time',
/**
*@description Label in the summary view in the Performance panel for a number which indicates how much managed memory has been reclaimed by performing Garbage Collection
*/
collected: 'Collected',
/**
*@description Text for a programming function
*/
function: 'Function',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
timerId: 'Timer ID',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
timeout: 'Timeout',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
repeats: 'Repeats',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
callbackId: 'Callback ID',
/**
*@description Text that refers to the resources of the web page
*/
resource: 'Resource',
/**
*@description Text that refers to the network request method
*/
requestMethod: 'Request Method',
/**
*@description Status code of an event
*/
statusCode: 'Status Code',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
mimeTypeCaps: 'MIME Type',
/**
*@description Text to show the priority of an item
*/
priority: 'Priority',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
encodedData: 'Encoded Data',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
sBytes: '{n, plural, =1 {# Byte} other {# Bytes}}',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
decodedBody: 'Decoded Body',
/**
*@description Text for a module, the programming concept
*/
module: 'Module',
/**
*@description Label for a group of JavaScript files
*/
script: 'Script',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
streamed: 'Streamed',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
eagerCompile: 'Compiling all functions eagerly',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
url: 'Url',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
producedCacheSize: 'Produced Cache Size',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
consumedCacheSize: 'Consumed Cache Size',
/**
*@description Title for a group of cities
*/
location: 'Location',
/**
*@description Text in Timeline UIUtils of the Performance panel
*@example {2} PH1
*@example {2} PH2
*/
sSCurlyBrackets: '({PH1}, {PH2})',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
dimensions: 'Dimensions',
/**
*@description Text in Timeline UIUtils of the Performance panel
*@example {2} PH1
*@example {2} PH2
*/
sSDimensions: '{PH1} × {PH2}',
/**
*@description Related node label in Timeline UIUtils of the Performance panel
*/
layerRoot: 'Layer Root',
/**
*@description Related node label in Timeline UIUtils of the Performance panel
*/
ownerElement: 'Owner Element',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
imageUrl: 'Image URL',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
stylesheetUrl: 'Stylesheet URL',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
elementsAffected: 'Elements Affected',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
nodesThatNeedLayout: 'Nodes That Need Layout',
/**
*@description Text in Timeline UIUtils of the Performance panel
*@example {2} PH1
*@example {10} PH2
*/
sOfS: '{PH1} of {PH2}',
/**
*@description Related node label in Timeline UIUtils of the Performance panel
*/
layoutRoot: 'Layout root',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
message: 'Message',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
websocketProtocol: 'WebSocket Protocol',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
callbackFunction: 'Callback Function',
/**
*@description The current state of an item
*/
state: 'State',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
range: 'Range',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
allottedTime: 'Allotted Time',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
invokedByTimeout: 'Invoked by Timeout',
/**
*@description Text that refers to some types
*/
type: 'Type',
/**
*@description Text for the size of something
*/
size: 'Size',
/**
*@description Text for the details of something
*/
details: 'Details',
/**
*@description Title in Timeline for Cumulative Layout Shifts
*/
cumulativeLayoutShifts: 'Cumulative Layout Shifts',
/**
*@description Text for the link to the evolved CLS website
*/
evolvedClsLink: 'evolved',
/**
*@description Warning in Timeline that CLS can cause a poor user experience. It contains a link to inform developers about the recent changes to how CLS is measured. The new CLS metric is said to have evolved from the previous version.
*@example {Link to web.dev/metrics} PH1
*@example {Link to web.dev/evolving-cls which will always have the text 'evolved'} PH2
*/
sCLSInformation: '{PH1} can result in poor user experiences. It has recently {PH2}.',
/**
*@description Text to indicate an item is a warning
*/
warning: 'Warning',
/**
*@description Title for the Timeline CLS Score
*/
score: 'Score',
/**
*@description Text in Timeline for the cumulative CLS score
*/
cumulativeScore: 'Cumulative Score',
/**
*@description Text in Timeline for the current CLS score
*/
currentClusterScore: 'Current Cluster Score',
/**
*@description Text in Timeline for the current CLS cluster
*/
currentClusterId: 'Current Cluster ID',
/**
*@description Text in Timeline for whether input happened recently
*/
hadRecentInput: 'Had recent input',
/**
*@description Text in Timeline indicating that input has happened recently
*/
yes: 'Yes',
/**
*@description Text in Timeline indicating that input has not happened recently
*/
no: 'No',
/**
*@description Label for Cumulative Layout records, indicating where they moved from
*/
movedFrom: 'Moved from',
/**
*@description Label for Cumulative Layout records, indicating where they moved to
*/
movedTo: 'Moved to',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
timeWaitingForMainThread: 'Time Waiting for Main Thread',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
relatedNode: 'Related Node',
/**
*@description Text for previewing items
*/
preview: 'Preview',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
aggregatedTime: 'Aggregated Time',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
networkRequest: 'Network request',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
loadFromCache: 'load from cache',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
networkTransfer: 'network transfer',
/**
*@description Text in Timeline UIUtils of the Performance panel
*@example {1ms} PH1
*@example {network transfer} PH2
*@example {1ms} PH3
*/
SSSResourceLoading: ' ({PH1} {PH2} + {PH3} resource loading)',
/**
*@description Text for the duration of something
*/
duration: 'Duration',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
mimeType: 'Mime Type',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
FromMemoryCache: ' (from memory cache)',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
FromCache: ' (from cache)',
/**
*@description Label for a network request indicating that it was a HTTP2 server push instead of a regular network request, in the Performance panel
*/
FromPush: ' (from push)',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
FromServiceWorker: ' (from `service worker`)',
/**
*@description Text for the initiator of something
*/
initiator: 'Initiator',
/**
*@description Call site stack label in Timeline UIUtils of the Performance panel
*/
timerInstalled: 'Timer Installed',
/**
*@description Call site stack label in Timeline UIUtils of the Performance panel
*/
animationFrameRequested: 'Animation Frame Requested',
/**
*@description Call site stack label in Timeline UIUtils of the Performance panel
*/
idleCallbackRequested: 'Idle Callback Requested',
/**
*@description Stack label in Timeline UIUtils of the Performance panel
*/
recalculationForced: 'Recalculation Forced',
/**
*@description Call site stack label in Timeline UIUtils of the Performance panel
*/
firstLayoutInvalidation: 'First Layout Invalidation',
/**
*@description Stack label in Timeline UIUtils of the Performance panel
*/
layoutForced: 'Layout Forced',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
callStacks: 'Call Stacks',
/**
*@description Text for the execution stack trace
*/
stackTrace: 'Stack Trace',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
invalidations: 'Invalidations',
/**
* @description Text in Timeline UIUtils of the Performance panel. Phrase is followed by a number of milliseconds.
* Some events or tasks might have been only started, but have not ended yet. Such events or tasks are considered
* "pending".
*/
pendingFor: 'Pending for',
/**
*@description Text for revealing an item in its destination
*/
reveal: 'Reveal',
/**
*@description Noun label for a stack trace which indicates the first time some condition was invalidated.
*/
firstInvalidated: 'First Invalidated',
/**
*@description Title in Timeline UIUtils of the Performance panel
*/
styleInvalidations: 'Style Invalidations',
/**
*@description Title in Timeline UIUtils of the Performance panel
*/
layoutInvalidations: 'Layout Invalidations',
/**
*@description Title in Timeline UIUtils of the Performance panel
*/
otherInvalidations: 'Other Invalidations',
/**
*@description Title of the paint profiler, old name of the performance pane
*/
paintProfiler: 'Paint Profiler',
/**
*@description Text in Timeline Flame Chart View of the Performance panel
*@example {Frame} PH1
*@example {10ms} PH2
*/
sAtS: '{PH1} at {PH2}',
/**
*@description Category in the Summary view of the Performance panel to indicate time spent to load resources
*/
loading: 'Loading',
/**
*@description Text in Timeline for the Experience title
*/
experience: 'Experience',
/**
*@description Category in the Summary view of the Performance panel to indicate time spent in script execution
*/
scripting: 'Scripting',
/**
*@description Category in the Summary view of the Performance panel to indicate time spent in rendering the web page
*/
rendering: 'Rendering',
/**
*@description Category in the Summary view of the Performance panel to indicate time spent to visually represent the web page
*/
painting: 'Painting',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
async: 'Async',
/**
*@description Category in the Summary view of the Performance panel to indicate time spent in the rest of the system
*/
system: 'System',
/**
*@description Category in the Summary view of the Performance panel to indicate idle time
*/
idle: 'Idle',
/**
*@description Text in Timeline UIUtils of the Performance panel
*@example {blink.console} PH1
*/
sSelf: '{PH1} (self)',
/**
*@description Text in Timeline UIUtils of the Performance panel
*@example {blink.console} PH1
*/
sChildren: '{PH1} (children)',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
timeSpentInRendering: 'Time spent in rendering',
/**
*@description Text for a rendering frame
*/
frame: 'Frame',
/**
*@description Text in Timeline Event Overview of the Performance panel
*/
fps: 'FPS',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
cpuTime: 'CPU time',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
layerTree: 'Layer tree',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
show: 'Show',
/**
*@description Text in Timeline UIUtils of the Performance panel
*@example {10ms} PH1
*@example {10ms} PH2
*/
sAtSParentheses: '{PH1} (at {PH2})',
/**
*@description Text that only contain a placeholder
*@example {100ms (at 200ms)} PH1
*/
emptyPlaceholder: '{PH1}', // eslint-disable-line rulesdir/l10n_no_locked_or_placeholder_only_phrase
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
jank: 'jank',
/**
*@description Text in Timeline UIUtils of the Performance panel
*@example {Took 3ms} PH1
*@example {jank} PH2
*/
sLongFrameTimesAreAnIndicationOf: '{PH1}. Long frame times are an indication of {PH2}',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
forcedReflow: 'Forced reflow',
/**
*@description Text in Timeline UIUtils of the Performance panel
*@example {Forced reflow} PH1
*/
sIsALikelyPerformanceBottleneck: '{PH1} is a likely performance bottleneck.',
/**
*@description Span text content in Timeline UIUtils of the Performance panel
*@example {10ms} PH1
*/
idleCallbackExecutionExtended: 'Idle callback execution extended beyond deadline by {PH1}',
/**
*@description Span text content in Timeline UIUtils of the Performance panel
*@example {10ms} PH1
*/
handlerTookS: 'Handler took {PH1}',
/**
*@description Warning to the user in the Performance panel that an input handler, which was run multiple times, took too long. Placeholder text is time in ms.
*@example {20ms} PH1
*/
recurringHandlerTookS: 'Recurring handler took {PH1}',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
longTask: 'Long task',
/**
*@description Text in Timeline UIUtils of the Performance panel
*@example {task} PH1
*@example {10ms} PH2
*/
sTookS: '{PH1} took {PH2}.',
/**
*@description Text that indicates something is not optimized
*/
notOptimized: 'Not optimized',
/**
*@description Text that starts with a colon and includes a placeholder
*@example {3.0} PH1
*/
emptyPlaceholderColon: ': {PH1}',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
unknownCause: 'Unknown cause',
/**
*@description Text in Timeline UIUtils of the Performance panel
*@example {Unkown reason} PH1
*@example {node1} PH2
*/
sForS: '{PH1} for {PH2}',
/**
*@description Text in Timeline UIUtils of the Performance panel
*@example {StyleInvalidator for element} PH1
*@example {Stack trace: function line} PH2
*/
sSDot: '{PH1}. {PH2}',
/**
*@description Text in Object Properties Section
*/
unknown: 'unknown',
/**
*@description Text of a DOM element in Timeline UIUtils of the Performance panel
*/
stackTraceColon: 'Stack trace:',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
nodes: 'Nodes:',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
node: 'Node:',
/**
*@description Text of a DOM element in Timeline UIUtils of the Performance panel
*@example {id2} PH1
*@example {a, b} PH2
*/
changedIdToSs: '(changed id to "{PH1}"{PH2})',
/**
*@description Text of a DOM element in Timeline UIUtils of the Performance panel
*@example {class-name2} PH1
*@example {a, b} PH2
*/
changedClassToSs: '(changed class to "{PH1}"{PH2})',
/**
*@description Text of a DOM element in Timeline UIUtils of the Performance panel
*@example {attribute-name} PH1
*@example {a, b} PH2
*/
changedAttributeToSs: '(changed attribute to "{PH1}"{PH2})',
/**
*@description Text of a DOM element in Timeline UIUtils of the Performance panel
*@example {after} PH1
*@example {a, b} PH2
*/
changedPesudoToSs: '(changed pseudo to "{PH1}"{PH2})',
/**
*@description Text of a DOM element in Timeline UIUtils of the Performance panel
*@example {part} PH1
*@example {a, b} PH2
*/
changedSs: '(changed "{PH1}"{PH2})',
/**
*@description Text in Timeline UIUtils of the Performance panel
*@example {node1} PH1
*@example {node2} PH2
*@example {2} PH3
*/
sSAndSOthers: '{PH1}, {PH2}, and {PH3} others',
/**
*@description Text of a DOM element in Timeline UIUtils of the Performance panel
*/
UnknownNode: '[ unknown node ]',
};
const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelineUIUtils.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
let eventStylesMap: EventStylesMap;
let inputEventToDisplayName: Map<TimelineModel.TimelineIRModel.InputEvents, string>;
let interactionPhaseStylesMap: Map<TimelineModel.TimelineIRModel.Phases, {
color: string,
label: string,
}>;
let categories: {
[x: string]: TimelineCategory,
};
let eventCategories: string[];
let eventDispatchDesciptors: EventDispatchTypeDescriptor[];
let colorGenerator: Common.Color.Generator;
const requestPreviewElements = new WeakMap<TimelineModel.TimelineModel.NetworkRequest, HTMLImageElement>();
interface EventStylesMap {
[x: string]: TimelineRecordStyle;
}
export class TimelineUIUtils {
private static initEventStyles(): EventStylesMap {
if (eventStylesMap) {
return eventStylesMap;
}
const type = TimelineModel.TimelineModel.RecordType;
const categories = TimelineUIUtils.categories();
const rendering = categories['rendering'];
const scripting = categories['scripting'];
const loading = categories['loading'];
const experience = categories['experience'];
const painting = categories['painting'];
const other = categories['other'];
const idle = categories['idle'];
const eventStyles: EventStylesMap = {};
eventStyles[type.Task] = new TimelineRecordStyle(i18nString(UIStrings.task), other);
eventStyles[type.Program] = new TimelineRecordStyle(i18nString(UIStrings.other), other);
eventStyles[type.Animation] = new TimelineRecordStyle(i18nString(UIStrings.animation), rendering);
eventStyles[type.EventDispatch] = new TimelineRecordStyle(i18nString(UIStrings.event), scripting);
eventStyles[type.RequestMainThreadFrame] =
new TimelineRecordStyle(i18nString(UIStrings.requestMainThreadFrame), rendering, true);
eventStyles[type.BeginFrame] = new TimelineRecordStyle(i18nString(UIStrings.frameStart), rendering, true);
eventStyles[type.BeginMainThreadFrame] =
new TimelineRecordStyle(i18nString(UIStrings.frameStartMainThread), rendering, true);
eventStyles[type.DrawFrame] = new TimelineRecordStyle(i18nString(UIStrings.drawFrame), rendering, true);
eventStyles[type.HitTest] = new TimelineRecordStyle(i18nString(UIStrings.hitTest), rendering);
eventStyles[type.ScheduleStyleRecalculation] =
new TimelineRecordStyle(i18nString(UIStrings.scheduleStyleRecalculation), rendering);
eventStyles[type.RecalculateStyles] = new TimelineRecordStyle(i18nString(UIStrings.recalculateStyle), rendering);
eventStyles[type.UpdateLayoutTree] = new TimelineRecordStyle(i18nString(UIStrings.recalculateStyle), rendering);
eventStyles[type.InvalidateLayout] =
new TimelineRecordStyle(i18nString(UIStrings.invalidateLayout), rendering, true);
eventStyles[type.Layout] = new TimelineRecordStyle(i18nString(UIStrings.layout), rendering);
eventStyles[type.PaintSetup] = new TimelineRecordStyle(i18nString(UIStrings.paintSetup), painting);
eventStyles[type.PaintImage] = new TimelineRecordStyle(i18nString(UIStrings.paintImage), painting, true);
eventStyles[type.UpdateLayer] = new TimelineRecordStyle(i18nString(UIStrings.updateLayer), painting, true);
eventStyles[type.UpdateLayerTree] = new TimelineRecordStyle(i18nString(UIStrings.updateLayerTree), rendering);
eventStyles[type.Paint] = new TimelineRecordStyle(i18nString(UIStrings.paint), painting);
eventStyles[type.PrePaint] = new TimelineRecordStyle(i18nString(UIStrings.prePaint), rendering);
eventStyles[type.RasterTask] = new TimelineRecordStyle(i18nString(UIStrings.rasterizePaint), painting);
eventStyles[type.ScrollLayer] = new TimelineRecordStyle(i18nString(UIStrings.scroll), rendering);
eventStyles[type.CompositeLayers] = new TimelineRecordStyle(i18nString(UIStrings.compositeLayers), painting);
eventStyles[type.ComputeIntersections] =
new TimelineRecordStyle(i18nString(UIStrings.computeIntersections), rendering);
eventStyles[type.ParseHTML] = new TimelineRecordStyle(i18nString(UIStrings.parseHtml), loading);
eventStyles[type.ParseAuthorStyleSheet] = new TimelineRecordStyle(i18nString(UIStrings.parseStylesheet), loading);
eventStyles[type.TimerInstall] = new TimelineRecordStyle(i18nString(UIStrings.installTimer), scripting);
eventStyles[type.TimerRemove] = new TimelineRecordStyle(i18nString(UIStrings.removeTimer), scripting);
eventStyles[type.TimerFire] = new TimelineRecordStyle(i18nString(UIStrings.timerFired), scripting);
eventStyles[type.XHRReadyStateChange] =
new TimelineRecordStyle(i18nString(UIStrings.xhrReadyStateChange), scripting);
eventStyles[type.XHRLoad] = new TimelineRecordStyle(i18nString(UIStrings.xhrLoad), scripting);
eventStyles[type.CompileScript] = new TimelineRecordStyle(i18nString(UIStrings.compileScript), scripting);
eventStyles[type.CacheScript] = new TimelineRecordStyle(i18nString(UIStrings.cacheScript), scripting);
eventStyles[type.CompileCode] = new TimelineRecordStyle(i18nString(UIStrings.compileCode), scripting);
eventStyles[type.OptimizeCode] = new TimelineRecordStyle(i18nString(UIStrings.optimizeCode), scripting);
eventStyles[type.EvaluateScript] = new TimelineRecordStyle(i18nString(UIStrings.evaluateScript), scripting);
eventStyles[type.CompileModule] = new TimelineRecordStyle(i18nString(UIStrings.compileModule), scripting);
eventStyles[type.CacheModule] = new TimelineRecordStyle(i18nString(UIStrings.cacheModule), scripting);
eventStyles[type.EvaluateModule] = new TimelineRecordStyle(i18nString(UIStrings.evaluateModule), scripting);
eventStyles[type.StreamingCompileScript] =
new TimelineRecordStyle(i18nString(UIStrings.streamingCompileTask), other);
eventStyles[type.StreamingCompileScriptWaiting] =
new TimelineRecordStyle(i18nString(UIStrings.waitingForNetwork), idle);
eventStyles[type.StreamingCompileScriptParsing] =
new TimelineRecordStyle(i18nString(UIStrings.parseAndCompile), scripting);
eventStyles[type.WasmStreamFromResponseCallback] =
new TimelineRecordStyle(i18nString(UIStrings.streamingWasmResponse), scripting);
eventStyles[type.WasmCompiledModule] = new TimelineRecordStyle(i18nString(UIStrings.compiledWasmModule), scripting);
eventStyles[type.WasmCachedModule] = new TimelineRecordStyle(i18nString(UIStrings.cachedWasmModule), scripting);
eventStyles[type.WasmModuleCacheHit] = new TimelineRecordStyle(i18nString(UIStrings.wasmModuleCacheHit), scripting);
eventStyles[type.WasmModuleCacheInvalid] =
new TimelineRecordStyle(i18nString(UIStrings.wasmModuleCacheInvalid), scripting);
eventStyles[type.FrameStartedLoading] =
new TimelineRecordStyle(i18nString(UIStrings.frameStartedLoading), loading, true);
eventStyles[type.MarkLoad] = new TimelineRecordStyle(i18nString(UIStrings.onloadEvent), scripting, true);
eventStyles[type.MarkDOMContent] =
new TimelineRecordStyle(i18nString(UIStrings.domcontentloadedEvent), scripting, true);
eventStyles[type.MarkFirstPaint] = new TimelineRecordStyle(i18nString(UIStrings.firstPaint), painting, true);
eventStyles[type.MarkFCP] = new TimelineRecordStyle(i18nString(UIStrings.firstContentfulPaint), rendering, true);
eventStyles[type.MarkLCPCandidate] =
new TimelineRecordStyle(i18nString(UIStrings.largestContentfulPaint), rendering, true);
eventStyles[type.TimeStamp] = new TimelineRecordStyle(i18nString(UIStrings.timestamp), scripting);
eventStyles[type.ConsoleTime] = new TimelineRecordStyle(i18nString(UIStrings.consoleTime), scripting);
eventStyles[type.UserTiming] = new TimelineRecordStyle(i18nString(UIStrings.userTiming), scripting);
eventStyles[type.ResourceWillSendRequest] = new TimelineRecordStyle(i18nString(UIStrings.willSendRequest), loading);
eventStyles[type.ResourceSendRequest] = new TimelineRecordStyle(i18nString(UIStrings.sendRequest), loading);
eventStyles[type.ResourceReceiveResponse] = new TimelineRecordStyle(i18nString(UIStrings.receiveResponse), loading);
eventStyles[type.ResourceFinish] = new TimelineRecordStyle(i18nString(UIStrings.finishLoading), loading);
eventStyles[type.ResourceReceivedData] = new TimelineRecordStyle(i18nString(UIStrings.receiveData), loading);
eventStyles[type.RunMicrotasks] = new TimelineRecordStyle(i18nString(UIStrings.runMicrotasks), scripting);
eventStyles[type.FunctionCall] = new TimelineRecordStyle(i18nString(UIStrings.functionCall), scripting);
eventStyles[type.GCEvent] = new TimelineRecordStyle(i18nString(UIStrings.gcEvent), scripting);
eventStyles[type.MajorGC] = new TimelineRecordStyle(i18nString(UIStrings.majorGc), scripting);
eventStyles[type.MinorGC] = new TimelineRecordStyle(i18nString(UIStrings.minorGc), scripting);
eventStyles[type.JSFrame] = new TimelineRecordStyle(i18nString(UIStrings.jsFrame), scripting);
eventStyles[type.RequestAnimationFrame] =
new TimelineRecordStyle(i18nString(UIStrings.requestAnimationFrame), scripting);
eventStyles[type.CancelAnimationFrame] =
new TimelineRecordStyle(i18nString(UIStrings.cancelAnimationFrame), scripting);
eventStyles[type.FireAnimationFrame] =
new TimelineRecordStyle(i18nString(UIStrings.animationFrameFired), scripting);
eventStyles[type.RequestIdleCallback] =
new TimelineRecordStyle(i18nString(UIStrings.requestIdleCallback), scripting);
eventStyles[type.CancelIdleCallback] = new TimelineRecordStyle(i18nString(UIStrings.cancelIdleCallback), scripting);
eventStyles[type.FireIdleCallback] = new TimelineRecordStyle(i18nString(UIStrings.fireIdleCallback), scripting);
eventStyles[type.WebSocketCreate] = new TimelineRecordStyle(i18nString(UIStrings.createWebsocket), scripting);
eventStyles[type.WebSocketSendHandshakeRequest] =
new TimelineRecordStyle(i18nString(UIStrings.sendWebsocketHandshake), scripting);
eventStyles[type.WebSocketReceiveHandshakeResponse] =
new TimelineRecordStyle(i18nString(UIStrings.receiveWebsocketHandshake), scripting);
eventStyles[type.WebSocketDestroy] = new TimelineRecordStyle(i18nString(UIStrings.destroyWebsocket), scripting);
eventStyles[type.EmbedderCallback] = new TimelineRecordStyle(i18nString(UIStrings.embedderCallback), scripting);
eventStyles[type.DecodeImage] = new TimelineRecordStyle(i18nString(UIStrings.imageDecode), painting);
eventStyles[type.ResizeImage] = new TimelineRecordStyle(i18nString(UIStrings.imageResize), painting);
eventStyles[type.GPUTask] = new TimelineRecordStyle(i18nString(UIStrings.gpu), categories['gpu']);
eventStyles[type.LatencyInfo] = new TimelineRecordStyle(i18nString(UIStrings.inputLatency), scripting);
eventStyles[type.GCCollectGarbage] = new TimelineRecordStyle(i18nString(UIStrings.domGc), scripting);
eventStyles[type.CryptoDoEncrypt] = new TimelineRecordStyle(i18nString(UIStrings.encrypt), scripting);
eventStyles[type.CryptoDoEncryptReply] = new TimelineRecordStyle(i18nString(UIStrings.encryptReply), scripting);
eventStyles[type.CryptoDoDecrypt] = new TimelineRecordStyle(i18nString(UIStrings.decrypt), scripting);
eventStyles[type.CryptoDoDecryptReply] = new TimelineRecordStyle(i18nString(UIStrings.decryptReply), scripting);
eventStyles[type.CryptoDoDigest] = new TimelineRecordStyle(i18nString(UIStrings.digest), scripting);
eventStyles[type.CryptoDoDigestReply] = new TimelineRecordStyle(i18nString(UIStrings.digestReply), scripting);
eventStyles[type.CryptoDoSign] = new TimelineRecordStyle(i18nString(UIStrings.sign), scripting);
eventStyles[type.CryptoDoSignReply] = new TimelineRecordStyle(i18nString(UIStrings.signReply), scripting);
eventStyles[type.CryptoDoVerify] = new TimelineRecordStyle(i18nString(UIStrings.verify), scripting);
eventStyles[type.CryptoDoVerifyReply] = new TimelineRecordStyle(i18nString(UIStrings.verifyReply), scripting);
eventStyles[type.AsyncTask] = new TimelineRecordStyle(i18nString(UIStrings.asyncTask), categories['async']);
eventStyles[type.LayoutShift] = new TimelineRecordStyle(i18nString(UIStrings.layoutShift), experience);
eventStylesMap = eventStyles;
return eventStyles;
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static setEventStylesMap(eventStyles: any): void {
eventStylesMap = eventStyles;
}
static inputEventDisplayName(inputEventType: TimelineModel.TimelineIRModel.InputEvents): string|null {
if (!inputEventToDisplayName) {
const inputEvent = TimelineModel.TimelineIRModel.InputEvents;
inputEventToDisplayName = new Map([
[inputEvent.Char, i18nString(UIStrings.keyCharacter)],
[inputEvent.KeyDown, i18nString(UIStrings.keyDown)],
[inputEvent.KeyDownRaw, i18nString(UIStrings.keyDown)],
[inputEvent.KeyUp, i18nString(UIStrings.keyUp)],
[inputEvent.Click, i18nString(UIStrings.click)],
[inputEvent.ContextMenu, i18nString(UIStrings.contextMenu)],
[inputEvent.MouseDown, i18nString(UIStrings.mouseDown)],
[inputEvent.MouseMove, i18nString(UIStrings.mouseMove)],
[inputEvent.MouseUp, i18nString(UIStrings.mouseUp)],
[inputEvent.MouseWheel, i18nString(UIStrings.mouseWheel)],
[inputEvent.ScrollBegin, i18nString(UIStrings.scrollBegin)],
[inputEvent.ScrollEnd, i18nString(UIStrings.scrollEnd)],
[inputEvent.ScrollUpdate, i18nString(UIStrings.scrollUpdate)],
[inputEvent.FlingStart, i18nString(UIStrings.flingStart)],
[inputEvent.FlingCancel, i18nString(UIStrings.flingHalt)],
[inputEvent.Tap, i18nString(UIStrings.tap)],
[inputEvent.TapCancel, i18nString(UIStrings.tapHalt)],
[inputEvent.ShowPress, i18nString(UIStrings.tapBegin)],
[inputEvent.TapDown, i18nString(UIStrings.tapDown)],
[inputEvent.TouchCancel, i18nString(UIStrings.touchCancel)],
[inputEvent.TouchEnd, i18nString(UIStrings.touchEnd)],
[inputEvent.TouchMove, i18nString(UIStrings.touchMove)],
[inputEvent.TouchStart, i18nString(UIStrings.touchStart)],
[inputEvent.PinchBegin, i18nString(UIStrings.pinchBegin)],
[inputEvent.PinchEnd, i18nString(UIStrings.pinchEnd)],
[inputEvent.PinchUpdate, i18nString(UIStrings.pinchUpdate)],
]);
}
return inputEventToDisplayName.get(inputEventType) || null;
}
static frameDisplayName(frame: Protocol.Runtime.CallFrame): string {
if (!TimelineModel.TimelineJSProfile.TimelineJSProfileProcessor.isNativeRuntimeFrame(frame)) {
return UI.UIUtils.beautifyFunctionName(frame.functionName);
}
const nativeGroup = TimelineModel.TimelineJSProfile.TimelineJSProfileProcessor.nativeGroup(frame.functionName);
const groups = TimelineModel.TimelineJSProfile.TimelineJSProfileProcessor.NativeGroups;
switch (nativeGroup) {
case groups.Compile:
return i18nString(UIStrings.compile);
case groups.Parse:
return i18nString(UIStrings.parse);
}
return frame.functionName;
}
static testContentMatching(traceEvent: SDK.TracingModel.Event, regExp: RegExp): boolean {
const title = TimelineUIUtils.eventStyle(traceEvent).title;
const tokens = [title];
const url = TimelineModel.TimelineModel.TimelineData.forEvent(traceEvent).url;
if (url) {
tokens.push(url);
}
appendObjectProperties(traceEvent.args, 2);
return regExp.test(tokens.join('|'));
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function appendObjectProperties(object: any, depth: number): void {
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);
}
}
}
}
static eventURL(event: SDK.TracingModel.Event): string|null {
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.TimelineModel.TimelineData.forEvent(event).topFrame();
return frame && frame.url || null;
}
static eventStyle(event: SDK.TracingModel.Event): TimelineRecordStyle {
const eventStyles = TimelineUIUtils.initEventStyles();
if (event.hasCategory(TimelineModel.TimelineModel.TimelineModelImpl.Category.Console) ||
event.hasCategory(TimelineModel.TimelineModel.TimelineModelImpl.Category.UserTiming)) {
return new TimelineRecordStyle(event.name, TimelineUIUtils.categories()['scripting']);
}
if (event.hasCategory(TimelineModel.TimelineModel.TimelineModelImpl.Category.LatencyInfo)) {
/** @const */
const prefix = 'InputLatency::';
const inputEventType = event.name.startsWith(prefix) ? event.name.substr(prefix.length) : event.name;
const displayName =
TimelineUIUtils.inputEventDisplayName((inputEventType as TimelineModel.TimelineIRModel.InputEvents));
return new TimelineRecordStyle(displayName || inputEventType, TimelineUIUtils.categories()['scripting']);
}
let result: TimelineRecordStyle = eventStyles[event.name];
if (!result) {
result = new TimelineRecordStyle(event.name, TimelineUIUtils.categories()['other'], true);
eventStyles[event.name] = result;
}
return result;
}
static eventColor(event: SDK.TracingModel.Event): string {
if (event.name === TimelineModel.TimelineModel.RecordType.JSFrame) {
const frame = event.args['data'];
if (TimelineUIUtils.isUserFrame(frame)) {
return TimelineUIUtils.colorForId(frame.url);
}
}
const color = 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) {
const color = Common.Color.Color.parse(TimelineUIUtils.categories().scripting.color);
if (!color) {
throw new Error('Unable to parse color from TimelineUIUtils.categories().scripting.color');
}
return color.setAlpha(0.3).asString(null) as string;
}
return color;
}
static eventColorByProduct(
model: TimelineModel.TimelineModel.TimelineModelImpl, urlToColorCache: Map<string, string>,
event: SDK.TracingModel.Event): string {
const url = TimelineUIUtils.eventURL(event) || '';
let color = urlToColorCache.get(url);
if (color) {
return color;
}
const defaultColor = '#f2ecdc';
const parsedURL = Common.ParsedURL.ParsedURL.fromString(url);
if (!parsedURL) {
return defaultColor;
}
const name = parsedURL.host;
const rootFrames = model.rootFrames();
if (rootFrames.some(pageFrame => new Common.ParsedURL.ParsedURL(pageFrame.url).host === name)) {
color = defaultColor;
}
if (!color) {
color = defaultColor;
}
urlToColorCache.set(url, color);
return color;
}
static eventTitle(event: SDK.TracingModel.Event): string {
const recordType = TimelineModel.TimelineModel.RecordType;
const eventData = event.args['data'];
if (event.name === recordType.JSFrame) {
return TimelineUIUtils.frameDisplayName(eventData);
}
const title = TimelineUIUtils.eventStyle(event).title;
if (event.hasCategory(TimelineModel.TimelineModel.TimelineModelImpl.Category.Console)) {
return title;
}
if (event.name === recordType.TimeStamp) {
return i18nString(UIStrings.sS, {PH1: title, PH2: eventData['message']});
}
if (event.name === recordType.Animation && eventData && eventData['name']) {
return i18nString(UIStrings.sS, {PH1: title, PH2: eventData['name']});
}
if (event.name === recordType.EventDispatch && eventData && eventData['type']) {
return i18nString(UIStrings.sS, {PH1: title, PH2: eventData['type']});
}
return title;
}
private static interactionPhaseStyles(): Map<TimelineModel.TimelineIRModel.Phases, {
color: string,
label: string,
}> {
let map: Map<TimelineModel.TimelineIRModel.Phases, {
color: string,
label: string,
}>|Map<TimelineModel.TimelineIRModel.Phases, {
color: string,
label: string,
}> = interactionPhaseStylesMap;
if (!map) {
map = new Map([
[TimelineModel.TimelineIRModel.Phases.Idle, {color: 'white', label: 'Idle'}],
[
TimelineModel.TimelineIRModel.Phases.Response,
{color: 'hsl(43, 83%, 64%)', label: i18nString(UIStrings.response)},
],
[
TimelineModel.TimelineIRModel.Phases.Scroll,
{color: 'hsl(256, 67%, 70%)', label: i18nString(UIStrings.scroll)},
],
[TimelineModel.TimelineIRModel.Phases.Fling, {color: 'hsl(256, 67%, 70%)', label: i18nString(UIStrings.fling)}],
[TimelineModel.TimelineIRModel.Phases.Drag, {color: 'hsl(256, 67%, 70%)', label: i18nString(UIStrings.drag)}],
[
TimelineModel.TimelineIRModel.Phases.Animation,
{color: 'hsl(256, 67%, 70%)', label: i18nString(UIStrings.animation)},
],
[
TimelineModel.TimelineIRModel.Phases.Uncategorized,
{color: 'hsl(0, 0%, 87%)', label: i18nString(UIStrings.uncategorized)},
],
]);
interactionPhaseStylesMap = map;
}
return map;
}
static interactionPhaseColor(phase: TimelineModel.TimelineIRModel.Phases): string {
const interactionPhase = TimelineUIUtils.interactionPhaseStyles().get(phase);
if (!interactionPhase) {
throw new Error(`Unknown phase ${phase}`);
}
return interactionPhase.color;
}
static interactionPhaseLabel(phase: TimelineModel.TimelineIRModel.Phases): string {
const interactionPhase = TimelineUIUtils.interactionPhaseStyles().get(phase);
if (!interactionPhase) {
throw new Error(`Unknown phase ${phase}`);
}
return interactionPhase.label;
}
static isUserFrame(frame: Protocol.Runtime.CallFrame): boolean {
return frame.scriptId !== '0' && !(frame.url && frame.url.startsWith('native '));
}
static networkRequestCategory(request: TimelineModel.TimelineModel.NetworkRequest): NetworkCategory {
const categories = 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;
}
}
static networkCategoryColor(category: NetworkCategory): string {
const categories = 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%)';
}
}
static async buildDetailsTextForTraceEvent(event: SDK.TracingModel.Event): Promise<string|null> {
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 = i18nString(UIStrings.sCollected, {PH1: Platform.NumberUtilities.bytesToString(delta)});
break;
}
case recordType.FunctionCall:
if (eventData && eventData['url'] && eventData['lineNumber'] !== undefined &&
eventData['columnNumber'] !== undefined) {
detailsText = eventData.url + ':' + (eventData.lineNumber + 1) + ':' + (eventData.columnNumber + 1);
}
break;
case recordType.JSFrame:
detailsText = TimelineUIUtils.frameDisplayName(eventData);
break;
case recordType.EventDispatch:
detailsText = eventData ? eventData['type'] : null;
break;
case recordType.Paint: {
const width = TimelineUIUtils.quadWidth(eventData.clip);
const height = TimelineUIUtils.quadHeight(eventData.clip);
if (width && height) {
detailsText = i18nString(UIStrings.sSDimensions, {PH1: width, PH2: height});
}
break;
}
case recordType.ParseHTML: {
const startLine = event.args['beginData']['startLine'];
const endLine = event.args['endData'] && event.args['endData']['endLine'];
const url = Bindings.ResourceUtils.displayNameForURL(event.args['beginData']['url']);
if (endLine >= 0) {
detailsText = i18nString(UIStrings.sSs, {PH1: url, PH2: startLine + 1, PH3: endLine + 1});
} else {
detailsText = i18nString(UIStrings.sSSquareBrackets, {PH1: url, PH2: startLine + 1});
}
break;
}
case recordType.CompileModule:
case recordType.CacheModule:
detailsText = Bindings.ResourceUtils.displayNameForURL(event.args['fileName']);
break;
case recordType.CompileScript:
case recordType.CacheScript:
case recordType.EvaluateScript: {
const url = eventData && eventData['url'];
if (url) {
detailsText = Bindings.ResourceUtils.displayNameForURL(url) + ':' + (eventData['lineNumber'] + 1);
}
break;
}
case recordType.WasmCompiledModule:
case recordType.WasmModuleCacheHit: {
const url = event.args['url'];
if (url) {
detailsText = Bindings.ResourceUtils.displayNameForURL(url);
}
break;
}
case recordType.StreamingCompileScript:
case recordType.XHRReadyStateChange:
case recordType.XHRLoad: {
const url = eventData['url'];
if (url) {
detailsText = Bindings.ResourceUtils.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.TimelineModel.TimelineData.forEvent(event).url;
if (url) {
detailsText = Bindings.ResourceUtils.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.TimelineModelImpl.Category.Console)) {
detailsText = null;
} else {
detailsText = await linkifyTopCallFrameAsText();
}
break;
}
return detailsText;
async function linkifyTopCallFrameAsText(): Promise<string|null> {
const frame = TimelineModel.TimelineModel.TimelineData.forEvent(event).topFrame();
if (!frame) {
return null;
}
return frame.url + ':' + (frame.lineNumber + 1) + ':' + (frame.columnNumber + 1);
}
}
static async buildDetailsNodeForTraceEvent(
event: SDK.TracingModel.Event, target: SDK.Target.Target|null,
linkifier: Components.Linkifier.Linkifier): Promise<Node|null> {
const recordType = TimelineModel.TimelineModel.RecordType;
let details: HTMLElement|HTMLSpanElement|(Element | null)|Text|null = 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 = await TimelineUIUtils.buildDetailsTextForTraceEvent(event);
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.TimelineModel.TimelineData.forEvent(event).url;
if (url) {
const options = {
tabStop: true,
showColumnNumber: false,
inlineFrameIndex: 0,
};
details = Components.Linkifier.Linkifier.linkifyURL(url, options);
}
break;
}
case recordType.FunctionCall:
case recordType.JSFrame: {
details = document.createElement('span');
UI.UIUtils.createTextChild(details, TimelineUIUtils.frameDisplayName(eventData));
const location = linkifyLocation(
eventData['scriptId'], eventData['url'], eventData['lineNumber'], eventData['columnNumber']);
if (location) {
UI.UIUtils.createTextChild(details, ' @ ');
details.appendChild(location);
}
break;
}
case recordType.CompileModule:
case recordType.CacheModule: {
details = linkifyLocation(null, event.args['fileName'], 0, 0);
break;
}
case recordType.CompileScript:
case recordType.CacheScript:
case recordType.EvaluateScript: {
const url = eventData['url'];
if (url) {
details = linkifyLocation(null, url, eventData['lineNumber'], 0);
}
break;
}
case recordType.StreamingCompileScript: {
const url = eventData['url'];
if (url) {
details = linkifyLocation(null, url, 0, 0);
}
break;
}
default: {
if (event.hasCategory(TimelineModel.TimelineModel.TimelineModelImpl.Category.Console)) {
detailsText = null;
} else {
details = linkifyTopCallFrame();
}
break;
}
}
if (!details && detailsText) {
details = document.createTextNode(detailsText);
}
return details;
function linkifyLocation(
scriptId: Protocol.Runtime.ScriptId|null, url: string, lineNumber: number, columnNumber?: number): Element|
null {
const options =
{columnNumber, showColumnNumber: true, inlineFrameIndex: 0, className: 'timeline-details', tabStop: true};
return linkifier.linkifyScriptLocation(target, scriptId, url, lineNumber, options);
}
function linkifyTopCallFrame(): Element|null {
const frame = TimelineModel.TimelineModel.TimelineData.forEvent(event).topFrame();
return frame ? linkifier.maybeLinkifyConsoleCallFrame(
target, frame,
{className: 'timeline-details', tabStop: true, inlineFrameIndex: 0, showColumnNumber: true}) :
null;
}
}
static buildDetailsNodeForPerformanceEvent(event: SDK.TracingModel.Event): Element {
let link: string = 'https://web.dev/user-centric-performance-metrics/';
let name = 'page performance metrics';
const recordType = TimelineModel.TimelineModel.RecordType;
switch (event.name) {
case recordType.MarkLCPCandidate:
link = 'https://web.dev/lcp/';
name = 'largest contentful paint';
break;
case recordType.MarkFCP:
link = 'https://web.dev/first-contentful-paint/';
name = 'first contentful paint';
break;
default:
break;
}
return UI.Fragment.html`<div>${UI.XLink.XLink.create(link, i18nString(UIStrings.learnMore))} about ${name}.</div>`;
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static buildConsumeCacheDetails(eventData: any, contentHelper: TimelineDetailsContentHelper): void {
if ('consumedCacheSize' in eventData) {
contentHelper.appendTextRow(
i18nString(UIStrings.compilationCacheStatus), i18nString(UIStrings.scriptLoadedFromCache));
contentHelper.appendTextRow(
i18nString(UIStrings.compilationCacheSize),
Platform.NumberUtilities.bytesToString(eventData['consumedCacheSize']));
} else if (eventData && 'cacheRejected' in eventData && eventData['cacheRejected']) {
// Version mismatch or similar.
contentHelper.appendTextRow(
i18nString(UIStrings.compilationCacheStatus), i18nString(UIStrings.failedToLoadScriptFromCache));
} else {
contentHelper.appendTextRow(
i18nString(UIStrings.compilationCacheStatus), i18nString(UIStrings.scriptNotEligible));
}
}
static async buildTraceEventDetails(
event: SDK.TracingModel.Event, model: TimelineModel.TimelineModel.TimelineModelImpl,
linkifier: Components.Linkifier.Linkifier, detailed: boolean): Promise<DocumentFragment> {
const maybeTarget = model.targetByEvent(event);
let relatedNodesMap: (Map<number, SDK.DOMModel.DOMNode|null>|null)|null = null;
if (maybeTarget) {
const target = (maybeTarget as SDK.Target.Target);
// @ts-ignore TODO(crbug.com/1011811): Remove symbol usage.
if (typeof event[previewElementSymbol] === 'undefined') {
let previewElement: (Element|null)|null = null;
const url = TimelineModel.TimelineModel.TimelineData.forEvent(event).url;
if (url) {
previewElement = await Components.ImagePreview.ImagePreview.build(target, url, false, {
imageAltText: Components.ImagePreview.ImagePreview.defaultAltTextForImageURL(url),
precomputedFeatures: undefined,
});
} else if (TimelineModel.TimelineModel.TimelineData.forEvent(event).picture) {
previewElement = await TimelineUIUtils.buildPicturePreviewContent(event, target);
}
// @ts-ignore TODO(crbug.com/1011811): Remove symbol usage.
event[previewElementSymbol] = previewElement;
}
const nodeIdsToResolve = new Set<Protocol.DOM.BackendNodeId>();
const timelineData = TimelineModel.TimelineModel.TimelineData.forEvent(event);
if (timelineData.backendNodeIds) {
for (let i = 0; i < timelineData.backendNodeIds.length; ++i) {
nodeIdsToResolve.add(timelineData.backendNodeIds[i]);
}
}
const invalidationTrackingEvents = TimelineModel.TimelineModel.InvalidationTracker.invalidationEventsFor(event);
if (invalidationTrackingEvents) {
TimelineUIUtils.collectInvalidationNodeIds(nodeIdsToResolve, invalidationTrackingEvents);
}
if (nodeIdsToResolve.size) {
const domModel = target.model(SDK.DOMModel.DOMModel);
if (domModel) {
relatedNodesMap = await domModel.pushNodesByBackendIdsToFrontend(nodeIdsToResolve);
}
}
}
const recordTypes = TimelineModel.TimelineModel.RecordType;
if (event.name === recordTypes.LayoutShift) {
// Ensure that there are no pie charts or extended info for layout shifts.
detailed = false;
}
// This message may vary per event.name;
let relatedNodeLabel;
const contentHelper = new TimelineDetailsContentHelper(model.targetByEvent(event), linkifier);
const color = model.isMarkerEvent(event) ? TimelineUIUtils.markerStyleForEvent(event).color :
TimelineUIUtils.eventStyle(event).category.color;
contentHelper.addSection(TimelineUIUtils.eventTitle(event), color);
const eventData = event.args['data'];
const timelineData = TimelineModel.TimelineModel.TimelineData.forEvent(event);
const initiator = timelineData.initiator();
let url: (string|null)|null = null;
if (timelineData.warning) {
contentHelper.appendWarningRow(event);
}
if (event.name === recordTypes.JSFrame && eventData['deoptReason']) {
contentHelper.appendWarningRow(event, TimelineModel.TimelineModel.TimelineModelImpl.WarningType.V8Deopt);
}
if (detailed && !Number.isNaN(event.duration || 0)) {
contentHelper.appendTextRow(
i18nString(UIStrings.totalTime), i18n.TimeUtilities.millisToString(event.duration || 0, true));
contentHelper.appendTextRow(
i18nString(UIStrings.selfTime), i18n.TimeUtilities.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(i18nString(UIStrings.collected), Platform.NumberUtilities.bytesToString(delta));
break;
}
case recordTypes.JSFrame:
case recordTypes.FunctionCall: {
const detailsNode =
await TimelineUIUtils.buildDetailsNodeForTraceEvent(event, model.targetByEvent(event), linkifier);
if (detailsNode) {
contentHelper.appendElementRow(i18nString(UIStrings.function), detailsNode);
}
break;
}
case recordTypes.TimerFire:
case recordTypes.TimerInstall:
case recordTypes.TimerRemove: {
contentHelper.appendTextRow(i18nString(UIStrings.timerId), eventData['timerId']);
if (event.name === recordTypes.TimerInstall) {
contentHelper.appendTextRow(
i18nString(UIStrings.timeout), i18n.TimeUtilities.millisToString(eventData['timeout']));
contentHelper.appendTextRow(i18nString(UIStrings.repeats), !eventData['singleShot']);
}
break;
}
case recordTypes.FireAnimationFrame: {
contentHelper.appendTextRow(i18nString(UIStrings.callbackId), eventData['id']);
break;
}
case recordTypes.ResourceWillSendRequest:
case recordTypes.ResourceSendRequest:
case recordTypes.ResourceReceiveResponse:
case recordTypes.ResourceReceivedData:
case recordTypes.ResourceFinish: {
url = timelineData.url;
if (url) {
const options = {
tabStop: true,
showColumnNumber: false,
inlineFrameIndex: 0,
};
contentHelper.appendElementRow(
i18nString(UIStrings.resource), Components.Linkifier.Linkifier.linkifyURL(url, options));
}
if (eventData['requestMethod']) {
contentHelper.appendTextRow(i18nString(UIStrings.requestMethod), eventData['requestMethod']);
}
if (typeof eventData['statusCode'] === 'number') {
contentHelper.appendTextRow(i18nString(UIStrings.statusCode), eventData['statusCode']);
}
if (eventData['mimeType']) {
contentHelper.appendTextRow(i18nString(UIStrings.mimeTypeCaps), eventData['mimeType']);
}
if ('priority' in eventData) {
const priority = PerfUI.NetworkPriorities.uiLabelForNetworkPriority(eventData['priority']);
contentHelper.appendTextRow(i18nString(UIStrings.priority), priority);
}
if (eventData['encodedDataLength']) {
contentHelper.appendTextRow(
i18nString(UIStrings.encodedData), i18nString(UIStrings.sBytes, {n: eventData['encodedDataLength']}));
}
if (eventData['decodedBodyLength']) {
contentHelper.appendTextRow(
i18nString(UIStrings.decodedBody), i18nString(UIStrings.sBytes, {n: eventData['decodedBodyLength']}));
}
break;
}
case recordTypes.CompileModule: {
contentHelper.appendLocationRow(i18nString(UIStrings.module), event.args['fileName'], 0);
break;
}
case recordTypes.CompileScript: {
url = eventData && eventData['url'];
if (url) {
contentHelper.appendLocationRow(
i18nString(UIStrings.script), url, eventData['lineNumber'], eventData['columnNumber']);
}
const isEager = eventData['eager'] ?? false;
if (isEager) {
contentHelper.appendTextRow(i18nString(UIStrings.eagerCompile), true);
}
const isStreamed = eventData['streamed'];
contentHelper.appendTextRow(
i18nString(UIStrings.streamed), isStreamed + (isStreamed ? '' : `: ${eventData['notStreamedReason']}`));
TimelineUIUtils.buildConsumeCacheDetails(eventData, contentHelper);
break;
}
case recordTypes.CacheModule: {
url = eventData && eventData['url'];
contentHelper.appendTextRow(
i18nString(UIStrings.compilationCacheSize),
Platform.NumberUtilities.bytesToString(eventData['producedCacheSize']));
break;
}
case recordTypes.CacheScript: {
url = eventData && eventData['url'];
if (url) {
contentHelper.appendLocationRow(
i18nString(UIStrings.script), url, eventData['lineNumber'], eventData['columnNumber']);
}
contentHelper.appendTextRow(
i18nString(UIStrings.compilationCacheSize),
Platform.NumberUtilities.bytesToString(eventData['producedCacheSize']));
break;
}
case recordTypes.EvaluateScript: {
url = eventData && eventData['url'];
if (url) {
contentHelper.appendLocationRow(
i18nString(UIStrings.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(i18nString(UIStrings.url), url);
}
const producedCachedSize = event.args['producedCachedSize'];
if (producedCachedSize) {
contentHelper.appendTextRow(i18nString(UIStrings.producedCacheSize), producedCachedSize);
}
const consumedCachedSize = event.args['consumedCachedSize'];
if (consumedCachedSize) {
contentHelper.appendTextRow(i18nString(UIStrings.consumedCacheSize), consumedCachedSize);
}
}
break;
}
// @ts-ignore Fall-through intended.
case recordTypes.Paint: {
const clip = eventData['clip'];
contentHelper.appendTextRow(
i18nString(UIStrings.location), i18nString(UIStrings.sSCurlyBrackets, {PH1: clip[0], PH2: clip[1]}));
const clipWidth = TimelineUIUtils.quadWidth(clip);
const clipHeight = TimelineUIUtils.quadHeight(clip);
contentHelper.appendTextRow(
i18nString(UIStrings.dimensions), i18nString(UIStrings.sSDimensions, {PH1: clipWidth, PH2: clipHeight}));
}
case recordTypes.PaintSetup:
case recordTypes.Rasterize:
case recordTypes.ScrollLayer: {
relatedNodeLabel = i18nString(UIStrings.layerRoot);
break;
}
case recordTypes.PaintImage:
case recordTypes.DecodeLazyPixelRef:
case recordTypes.DecodeImage:
case recordTypes.ResizeImage:
case recordTypes.DrawLazyPixelRef: {
relatedNodeLabel = i18nString(UIStrings.ownerElement);
url = timelineData.url;
if (url) {
const options = {
tabStop: true,
showColumnNumber: false,
inlineFrameIndex: 0,
};
contentHelper.appendElementRow(
i18nString(UIStrings.imageUrl), Components.Linkifier.Linkifier.linkifyURL(url, options));
}
break;
}
case recordTypes.ParseAuthorStyleSheet: {
url = eventData['styleSheetUrl'];
if (url) {
const options = {
tabStop: true,
showColumnNumber: false,
inlineFrameIndex: 0,
};
contentHelper.appendElementRow(
i18nString(UIStrings.stylesheetUrl), Components.Linkifier.Linkifier.linkifyURL(url, options));
}
break;
}
case recordTypes.UpdateLayoutTree: // We don't want to see default details.
case recordTypes.RecalculateStyles: {
contentHelper.appendTextRow(i18nString(UIStrings.elementsAffected), event.args['elementCount']);
break;
}
case recordTypes.Layout: {
const beginData = event.args['beginData'];
contentHelper.appendTextRow(
i18nString(UIStrings.nodesThatNeedLayout),
i18nString(UIStrings.sOfS, {PH1: beginData['dirtyObjects'], PH2: beginData['totalObjects']}));
relatedNodeLabel = i18nString(UIStrings.layoutRoot);
break;
}
case recordTypes.ConsoleTime: {
contentHelper.appendTextRow(i18nString(UIStrings.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(i18n.i18n.lockedString('URL'), initiatorData['webSocketURL']);
}
if (typeof initiatorData['webSocketProtocol'] !== 'undefined') {
contentHelper.appendTextRow(i18nString(UIStrings.websocketProtocol), initiatorData['webSocketProtocol']);
}
if (typeof eventData['message'] !== 'undefined') {
contentHelper.appendTextRow(i18nString(UIStrings.message), eventData['message']);
}
break;
}
case recordTypes.EmbedderCallback: {
contentHelper.appendTextRow(i18nString(UIStrings.callbackFunction), eventData['callbackName']);
break;
}
case recordTypes.Animation: {
if (event.phase === SDK.TracingModel.Phase.NestableAsyncInstant) {
contentHelper.appendTextRow(i18nString(UIStrings.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(i18nString(UIStrings.range), url, startLine, endLine);
}
break;
}
// @ts-ignore Fall-through intended.
case recordTypes.FireIdleCallback: {
contentHelper.appendTextRow(
i18nString(UIStrings.allottedTime), i18n.TimeUtilities.millisToString(eventData['allottedMilliseconds']));
contentHelper.appendTextRow(i18nString(UIStrings.invokedByTimeout), eventData['timedOut']);
}
case recordTypes.RequestIdleCallback:
case recordTypes.CancelIdleCallback: {
contentHelper.appendTextRow(i18nString(UIStrings.callbackId), eventData['id']);
break;
}
case recordTypes.EventDispatch: {
contentHelper.appendTextRow(i18nString(UIStrings.type), eventData['type']);
break;
}
// @ts-ignore Fall-through intended.
case recordTypes.MarkLCPCandidate: {
contentHelper.appendTextRow(i18nString(UIStrings.type), String(eventData['type']));
contentHelper.appendTextRow(i18nString(UIStrings.size), String(eventData['size']));
}
case recordTypes.MarkFirstPaint:
case recordTypes.MarkFCP:
case recordTypes.MarkLoad:
case recordTypes.MarkDOMContent: {
let eventTime: number = event.startTime - model.minimumRecordTime();
// Find the appropriate navStart based on the navigation ID.
const {navigationId} = event.args.data;
if (navigationId) {
const navStartTime = model.navStartTimes().get(navigationId);
if (navStartTime) {
eventTime = event.startTime - navStartTime.startTime;
}
}
contentHelper.appendTextRow(
i18nString(UIStrings.timestamp), i18n.TimeUtilities.preciseMillisToString(eventTime, 1));
contentHelper.appendElementRow(
i18nString(UIStrings.details), TimelineUIUtils.buildDetailsNodeForPerformanceEvent(event));
break;
}
case recordTypes.LayoutShift: {
const warning = document.createElement('span');
const clsLink = UI.XLink.XLink.create('https://web.dev/cls/', i18nString(UIStrings.cumulativeLayoutShifts));
const evolvedClsLink =
UI.XLink.XLink.create('https://web.dev/evolving-cls/', i18nString(UIStrings.evolvedClsLink));
warning.appendChild(
i18n.i18n.getFormatLocalizedString(str_, UIStrings.sCLSInformation, {PH1: clsLink, PH2: evolvedClsLink}));
contentHelper.appendElementRow(i18nString(UIStrings.warning), warning, true);
contentHelper.appendTextRow(i18nString(UIStrings.score), eventData['score'].toPrecision(4));
contentHelper.appendTextRow(
i18nString(UIStrings.cumulativeScore), eventData['cumulative_score'].toPrecision(4));
if ('_current_cluster_id' in eventData) {
contentHelper.appendTextRow(i18nString(UIStrings.currentClusterId), eventData['_current_cluster_id']);
}
if ('_current_cluster_score' in eventData) {
contentHelper.appendTextRow(
i18nString(UIStrings.currentClusterScore), eventData['_current_cluster_score'].toPrecision(4));
}
contentHelper.appendTextRow(
i18nString(UIStrings.hadRecentInput),
eventData['had_recent_input'] ? i18nString(UIStrings.yes) : i18nString(UIStrings.no));
for (const impactedNode of eventData['impacted_nodes']) {
const oldRect = new CLSRect(impactedNode['old_rect']);
const newRect = new CLSRect(impactedNode['new_rect']);
const linkedOldRect = await Common.Linkifier.Linkifier.linkify(oldRect);
const linkedNewRect = await Common.Linkifier.Linkifier.linkify(newRect);
contentHelper.appendElementRow(i18nString(UIStrings.movedFrom), linkedOldRect);
contentHelper.appendElementRow(i18nString(UIStrings.movedTo), linkedNewRect);
}
break;
}
default: {
const detailsNode =
await TimelineUIUtils.buildDetailsNodeForTraceEvent(event, model.targetByEvent(event), linkifier);
if (detailsNode) {
contentHelper.appendElementRow(i18nString(UIStrings.details), detailsNode);
}
break;
}
}
if (timelineData.timeWaitingForMainThread) {
contentHelper.appendTextRow(
i18nString(UIStrings.timeWaitingForMainThread),
i18n.TimeUtilities.millisToString(timelineData.timeWaitingForMainThread, true));
}
for (let i = 0; i < timelineData.backendNodeIds.length; ++i) {
const relatedNode = relatedNodesMap && relatedNodesMap.get(timelineData.backendNodeIds[i]);
if (relatedNode) {
const nodeSpan = await Common.Linkifier.Linkifier.linkify(relatedNode);
contentHelper.appendElementRow(relatedNodeLabel || i18nString(UIStrings.relatedNode), nodeSpan);
}
}
// @ts-ignore TODO(crbug.com/1011811): Remove symbol usage.
if (event[previewElementSymbol]) {
contentHelper.addSection(i18nString(UIStrings.preview));
// @ts-ignore TODO(crbug.com/1011811): Remove symbol usage.
contentHelper.appendElementRow('', event[previewElementSymbol]);
}
if (initiator || timelineData.stackTraceForSelfOrInitiator() ||
TimelineModel.TimelineModel.InvalidationTracker.invalidationEventsFor(event)) {
TimelineUIUtils.generateCauses(event, model.targetByEvent(event), relatedNodesMap, contentHelper);
}
const stats: {
[x: string]: number,
} = {};
const showPieChart = detailed && TimelineUIUtils.aggregatedStatsForTraceEvent(stats, model, event);
if (showPieChart) {
contentHelper.addSection(i18nString(UIStrings.aggregatedTime));
const pieChart =
TimelineUIUtils.generatePieChart(stats, TimelineUIUtils.eventStyle(event).category, event.selfTime);
contentHelper.appendElementRow('', pieChart);
}
return contentHelper.fragment;
}
static statsForTimeRange(events: SDK.TracingModel.Event[], startTime: number, endTime: number): {
[x: string]: number,
} {
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;
function aggregatedStatsAtTime(time: number): {
[x: string]: number,
} {
const stats: {
[x: string]: number,
} = {};
// @ts-ignore TODO(crbug.com/1011811): Remove symbol usage.
const cache = events[categoryBreakdownCacheSymbol];
for (const category in cache) {
const categoryCache = cache[category];
const index =
Platform.ArrayUtilities.upperBound(categoryCache.time, time, Platform.ArrayUtilities.DEFAULT_COMPARATOR);
let value;
if (index === 0) {
value = 0;
} else if (index === categoryCache.time.length) {
value = categoryCache.value[categoryCache.value.length - 1];
} 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;
}
function subtractStats(
a: {
[x: string]: number,
},
b: {
[x: string]: number,
}): {
[x: string]: number,
} {
const result = Object.assign({}, a);
for (const key in b) {
result[key] -= b[key];
}
return result;
}
function buildRangeStatsCacheIfNeeded(events: SDK.TracingModel.Event[]): void {
// @ts-ignore TODO(crbug.com/1011811): Remove symbol usage.
if (events[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: {
[x: string]: {
time: number[],
value: number[],
},
} = {};
const categoryStack: string[] = [];
let lastTime = 0;
TimelineModel.TimelineModel.TimelineModelImpl.forEachEvent(
events, onStartEvent, onEndEvent, undefined, undefined, undefined, filterForStats());
function filterForStats(): (arg0: SDK.TracingModel.Event) => boolean {
const visibleEventsFilter = TimelineUIUtils.visibleEventsFilter();
return (event: SDK.TracingModel.Event): boolean =>
visibleEventsFilter.accept(event) || SDK.TracingModel.TracingModel.isTopLevelEvent(event);
}
function updateCategory(category: string, time: number): void {
let statsArrays: {
time: number[],
value: number[],
} = aggregatedStats[category];
if (!statsArrays) {
statsArrays = {time: [], value: []};
aggregatedStats[category] = statsArrays;
}
if (statsArrays.time.length && statsArrays.time[statsArrays.time.length - 1] === time || lastTime > time) {
return;
}
const lastValue = statsArrays.value.length > 0 ? statsArrays.value[statsArrays.value.length - 1] : 0;
statsArrays.value.push(lastValue + time - lastTime);
statsArrays.time.push(time);
}
function categoryChange(from: string|null, to: string|null, time: number): void {
if (from) {
updateCategory(from, time);
}
lastTime = time;
if (to) {
updateCategory(to, time);
}
}
function onStartEvent(e: SDK.TracingModel.Event): void {
const category = TimelineUIUtils.eventStyle(e).category.name;
const parentCategory = categoryStack.length ? categoryStack[categoryStack.length - 1] : null;
if (category !== parentCategory) {
categoryChange(parentCategory || null, category, e.startTime);
}
categoryStack.push(category);
}
function onEndEvent(e: SDK.TracingModel.Event): void {
const category = categoryStack.pop();
const parentCategory = categoryStack.length ? categoryStack[categoryStack.length - 1] : null;
if (category !== parentCategory) {
categoryChange(category || null, parentCategory || null, e.endTime || 0);
}
}
const obj = (events as Object);
// @ts-ignore TODO(crbug.com/1011811): Remove symbol usage.
obj[categoryBreakdownCacheSymbol] = aggregatedStats;
}
}
static async buildNetworkRequestDetails(
request: TimelineModel.TimelineModel.NetworkRequest, model: TimelineModel.TimelineModel.TimelineModelImpl,
linkifier: Components.Linkifier.Linkifier): Promise<DocumentFragment> {
const target = model.targetByEvent(request.children[0]);
const contentHelper = new TimelineDetailsContentHelper(target, linkifier);
const category = TimelineUIUtils.networkRequestCategory(request);
const color = TimelineUIUtils.networkCategoryColor(category);
contentHelper.addSection(i18nString(UIStrings.networkRequest), color);
if (request.url) {
const options = {
tabStop: true,
showColumnNumber: false,
inlineFrameIndex: 0,
};
contentHelper.appendElementRow(
i18n.i18n.lockedString('URL'), Components.Linkifier.Linkifier.linkifyURL(request.url, options));
}
// The time from queueing the request until resource processing is finished.
const fullDuration = request.endTime - (request.getStartTime() || -Infinity);
if (isFinite(fullDuration)) {
let textRow = i18n.TimeUtilities.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()) - request.getStartTime();
// The time it takes to make the resource available to the renderer process.
const processingDuration = request.endTime - (request.finishTime || 0);
if (isFinite(networkDuration) && isFinite(processingDuration)) {
const networkDurationStr = i18n.TimeUtilities.millisToString(networkDuration, true);
const processingDurationStr = i18n.TimeUtilities.millisToString(processingDuration, true);
const cacheOrNetworkLabel =
request.cached() ? i18nString(UIStrings.loadFromCache) : i18nString(UIStrings.networkTransfer);
textRow += i18nString(
UIStrings.SSSResourceLoading,
{PH1: networkDurationStr, PH2: cacheOrNetworkLabel, PH3: processingDurationStr});
}
contentHelper.appendTextRow(i18nString(UIStrings.duration), textRow);
}
if (request.requestMethod) {
contentHelper.appendTextRow(i18nString(UIStrings.requestMethod), request.requestMethod);
}
if (typeof request.priority === 'string') {
const priority =
PerfUI.NetworkPriorities.uiLabelForNetworkPriority((request.priority as Protocol.Network.ResourcePriority));
contentHelper.appendTextRow(i18nString(UIStrings.priority), priority);
}
if (request.mimeType) {
contentHelper.appendTextRow(i18nString(UIStrings.mimeType), request.mimeType);
}
let lengthText = '';
if (request.memoryCached()) {
lengthText += i18nString(UIStrings.FromMemoryCache);
} else if (request.cached()) {
lengthText += i18nString(UIStrings.FromCache);
} else if (request.timing && request.timing.pushStart) {
lengthText += i18nString(UIStrings.FromPush);
}
if (request.fromServiceWorker) {
lengthText += i18nString(UIStrings.FromServiceWorker);
}
if (request.encodedDataLength || !lengthText) {
lengthText = `${Platform.NumberUtilities.bytesToString(request.encodedDataLength)}${lengthText}`;
}
contentHelper.appendTextRow(i18nString(UIStrings.encodedData), lengthText);
if (request.decodedBodyLength) {
contentHelper.appendTextRow(
i18nString(UIStrings.decodedBody), Platform.NumberUtilities.bytesToString(request.decodedBodyLength));
}
const title = i18nString(UIStrings.initiator);
const sendRequest = request.children[0];
const topFrame = TimelineModel.TimelineModel.TimelineData.forEvent(sendRequest).topFrame();
if (topFrame) {
const link = linkifier.maybeLinkifyConsoleCallFrame(
target, topFrame, {tabStop: true, inlineFrameIndex: 0, showColumnNumber: true});
if (link) {
contentHelper.appendElementRow(title, link);
}
} else {
const initiator = TimelineModel.TimelineModel.TimelineData.forEvent(sendRequest).initiator();
if (initiator) {
const initiatorURL = TimelineModel.TimelineModel.TimelineData.forEvent(initiator).url;
if (initiatorURL) {
const link =
linkifier.maybeLinkifyScriptLocation(target, null, initiatorURL, 0, {tabStop: true, inlineFrameIndex: 0});
if (link) {
contentHelper.appendElementRow(title, link);
}
}
}
}
if (!requestPreviewElements.get(request) && request.url && target) {
const previewElement = (await Components.ImagePreview.ImagePreview.build(target, request.url, false, {
imageAltText: Components.ImagePreview.ImagePreview.defaultAltTextForImageURL(request.url),
precomputedFeatures: undefined,
}) as HTMLImageElement);
requestPreviewElements.set(request, previewElement);
}
const requestPreviewElement = requestPreviewElements.get(request);
if (requestPreviewElement) {
contentHelper.appendElementRow(i18nString(UIStrings.preview), requestPreviewElement);
}
return contentHelper.fragment;
}
static stackTraceFromCallFrames(callFrames: Protocol.Runtime.CallFrame[]): Protocol.Runtime.StackTrace {
return {callFrames: callFrames} as Protocol.Runtime.StackTrace;
}
private static generateCauses(
event: SDK.TracingModel.Event, target: SDK.Target.Target|null,
relatedNodesMap: Map<number, SDK.DOMModel.DOMNode|null>|null, contentHelper: TimelineDetailsContentHelper): void {
const recordTypes = TimelineModel.TimelineModel.RecordType;
let callSiteStackLabel;
let stackLabel;
switch (event.name) {
case recordTypes.TimerFire:
callSiteStackLabel = i18nString(UIStrings.timerInstalled);
break;
case recordTypes.FireAnimationFrame:
callSiteStackLabel = i18nString(UIStrings.animationFrameRequested);
break;
case recordTypes.FireIdleCallback:
callSiteStackLabel = i18nString(UIStrings.idleCallbackRequested);
break;
case recordTypes.UpdateLayoutTree:
case recordTypes.RecalculateStyles:
stackLabel = i18nString(UIStrings.recalculationForced);
break;
case recordTypes.Layout:
callSiteStackLabel = i18nString(UIStrings.firstLayoutInvalidation);
stackLabel = i18nString(UIStrings.layoutForced);
break;
}
const timelineData = TimelineModel.TimelineModel.TimelineData.forEvent(event);
// Direct cause.
if (timelineData.stackTrace && timelineData.stackTrace.length) {
contentHelper.addSection(i18nString(UIStrings.callStacks));
contentHelper.appendStackTrace(
stackLabel || i18nString(UIStrings.stackTrace),
TimelineUIUtils.stackTraceFromCallFrames(timelineData.stackTrace));
}
const initiator = TimelineModel.TimelineModel.TimelineData.forEvent(event).initiator();
// Indirect causes.
if (TimelineModel.TimelineModel.InvalidationTracker.invalidationEventsFor(event) && target) {
// Full invalidation tracking (experimental).
contentHelper.addSection(i18nString(UIStrings.invalidations));
TimelineUIUtils.generateInvalidations(event, target, relatedNodesMap, contentHelper);
} else if (initiator) { // Partial invalidation tracking.
const delay = event.startTime - initiator.startTime;
contentHelper.appendTextRow(i18nString(UIStrings.pendingFor), i18n.TimeUtilities.preciseMillisToString(delay, 1));
const link = document.createElement('span');
link.classList.add('devtools-link');
UI.ARIAUtils.markAsLink(link);
link.tabIndex = 0;
link.textContent = i18nString(UIStrings.reveal);
link.addEventListener('click', () => {
TimelinePanel.instance().select(TimelineSelection.fromTraceEvent((initiator as SDK.TracingModel.Event)));
});
link.addEventListener('keydown', event => {
if (event.key === 'Enter') {
TimelinePanel.instance().select(TimelineSelection.fromTraceEvent((initiator as SDK.TracingModel.Event)));
event.consume(true);
}
});
contentHelper.appendElementRow(i18nString(UIStrings.initiator), link);
const initiatorStackTrace = TimelineModel.TimelineModel.TimelineData.forEvent(initiator).stackTrace;
if (initiatorStackTrace) {
contentHelper.appendStackTrace(
callSiteStackLabel || i18nString(UIStrings.firstInvalidated),
TimelineUIUtils.stackTraceFromCallFrames(initiatorStackTrace));
}
}
}
private static generateInvalidations(
event: SDK.TracingModel.Event, target: SDK.Target.Target,
relatedNodesMap: Map<number, SDK.DOMModel.DOMNode|null>|null, contentHelper: TimelineDetailsContentHelper): void {
const invalidationTrackingEvents = TimelineModel.TimelineModel.InvalidationTracker.invalidationEventsFor(event);
if (!invalidationTrackingEvents) {
return;
}
const invalidations: {
[x: string]: TimelineModel.TimelineModel.InvalidationTrackingEvent[],
} = {};
for (const invalidation of invalidationTrackingEvents) {
if (!invalidations[invalidation.type]) {
invalidations[invalidation.type] = [];
}
invalidations[invalidation.type].push(invalidation);
}
Object.keys(invalidations).forEach(function(type) {
TimelineUIUtils.generateInvalidationsForType(type, target, invalidations[type], relatedNodesMap, contentHelper);
});
}
private static generateInvalidationsForType(
type: string, target: SDK.Target.Target, invalidations: TimelineModel.TimelineModel.InvalidationTrackingEvent[],
relatedNodesMap: Map<number, SDK.DOMModel.DOMNode|null>|null, contentHelper: TimelineDetailsContentHelper): void {
let title;
switch (type) {
case TimelineModel.TimelineModel.RecordType.StyleRecalcInvalidationTracking:
title = i18nString(UIStrings.styleInvalidations);
break;
case TimelineModel.TimelineModel.RecordType.LayoutInvalidationTracking:
title = i18nString(UIStrings.layoutInvalidations);
break;
default:
title = i18nString(UIStrings.otherInvalidations);
break;
}
const invalidationsTreeOutline = new UI.TreeOutline.TreeOutlineInShadow();
invalidationsTreeOutline.registerCSSFiles([invalidationsTreeStyles]);
invalidationsTreeOutline.element.classList.add('invalidations-tree');
const invalidationGroups = groupInvalidationsByCause(invalidations);
invalidationGroups.forEach(function(group) {
const groupElement = new InvalidationsGroupElement(target, relatedNodesMap, contentHelper, group);
invalidationsTreeOutline.appendChild(groupElement);
});
contentHelper.appendElementRow(title, invalidationsTreeOutline.element, false, true);
function groupInvalidationsByCause(invalidations: TimelineModel.TimelineModel.InvalidationTrackingEvent[]):
TimelineModel.TimelineModel.InvalidationTrackingEvent[][] {
const causeToInvalidationMap = new Map<string, TimelineModel.TimelineModel.InvalidationTrackingEvent[]>();
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'] + '.';
});
}
const causeToInvalidation = causeToInvalidationMap.get(causeKey);
if (causeToInvalidation) {
causeToInvalidation.push(invalidation);
} else {
causeToInvalidationMap.set(causeKey, [invalidation]);
}
}
return [...causeToInvalidationMap.values()];
}
}
private static collectInvalidationNodeIds(
nodeIds: Set<number>, invalidations: TimelineModel.TimelineModel.InvalidationTrackingEvent[]): void {
Platform.SetUtilities.addAll(nodeIds, invalidations.map(invalidation => invalidation.nodeId).filter(id => id));
}
private static aggregatedStatsForTraceEvent(
total: {
[x: string]: number,
},
model: TimelineModel.TimelineModel.TimelineModelImpl, event: SDK.TracingModel.Event): boolean {
const events = model.inspectedTargetEvents();
function eventComparator(startTime: number, e: SDK.TracingModel.Event): number {
return startTime - e.startTime;
}
const index = Platform.ArrayUtilities.binaryIndexOf(events, 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 = TimelineUIUtils.eventStyle(nextEvent).category.name;
total[categoryName] = (total[categoryName] || 0) + nextEvent.selfTime;
}
}
if (SDK.TracingModel.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;
}
static async buildPicturePreviewContent(event: SDK.TracingModel.Event, target: SDK.Target.Target):
Promise<Element|null> {
const snapshotWithRect =
await new TimelineModel.TimelineFrameModel.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 stylesContainer = document.createElement('div');
const shadowRoot = stylesContainer.attachShadow({mode: 'open'});
shadowRoot.adoptedStyleSheets = [imagePreviewStyles];
const container = shadowRoot.createChild('div') as HTMLDivElement;
container.classList.add('image-preview-container', 'vbox', 'link');
const img = (container.createChild('img') as HTMLImageElement);
img.src = imageURL;
img.alt = Components.ImagePreview.ImagePreview.defaultAltTextForImageURL(imageURL);
const paintProfilerButton = container.createChild('a');
paintProfilerButton.textContent = i18nString(UIStrings.paintProfiler);
UI.ARIAUtils.markAsLink(container);
container.tabIndex = 0;
container.addEventListener(
'click', () => TimelinePanel.instance().select(TimelineSelection.fromTraceEvent(event)), false);
container.addEventListener('keydown', keyEvent => {
if (keyEvent.key === 'Enter') {
TimelinePanel.instance().select(TimelineSelection.fromTraceEvent(event));
keyEvent.consume(true);
}
});
return stylesContainer;
}
static createEventDivider(event: SDK.TracingModel.Event, zeroTime: number): Element {
const eventDivider = document.createElement('div');
eventDivider.classList.add('resources-event-divider');
const startTime = i18n.TimeUtilities.millisToString(event.startTime - zeroTime);
UI.Tooltip.Tooltip.install(
eventDivider, i18nString(UIStrings.sAtS, {PH1: TimelineUIUtils.eventTitle(event), PH2: startTime}));
const style = TimelineUIUtils.markerStyleForEvent(event);
if (style.tall) {
eventDivider.style.backgroundColor = style.color;
}
return eventDivider;
}
private static visibleTypes(): string[] {
const eventStyles = TimelineUIUtils.initEventStyles();
const result = [];
for (const name in eventStyles) {
if (!eventStyles[name].hidden) {
result.push(name);
}
}
return result;
}
static visibleEventsFilter(): TimelineModel.TimelineModelFilter.TimelineModelFilter {
return new TimelineModel.TimelineModelFilter.TimelineVisibleEventsFilter(TimelineUIUtils.visibleTypes());
}
static categories(): {
[x: string]: TimelineCategory,
} {
if (categories) {
return categories;
}
categories = {
loading: new TimelineCategory(
'loading', i18nString(UIStrings.loading), true, 'hsl(214, 67%, 74%)', 'hsl(214, 67%, 66%)'),
experience: new TimelineCategory(
'experience', i18nString(UIStrings.experience), false, 'hsl(5, 80%, 74%)', 'hsl(5, 80%, 66%)'),
scripting: new TimelineCategory(
'scripting', i18nString(UIStrings.scripting), true, 'hsl(43, 83%, 72%)', 'hsl(43, 83%, 64%) '),
rendering: new TimelineCategory(
'rendering', i18nString(UIStrings.rendering), true, 'hsl(256, 67%, 76%)', 'hsl(256, 67%, 70%)'),
painting: new TimelineCategory(
'painting', i18nString(UIStrings.painting), true, 'hsl(109, 33%, 64%)', 'hsl(109, 33%, 55%)'),
gpu: new TimelineCategory('gpu', i18nString(UIStrings.gpu), false, 'hsl(109, 33%, 64%)', 'hsl(109, 33%, 55%)'),
async:
new TimelineCategory('async', i18nString(UIStrings.async), false, 'hsl(0, 100%, 50%)', 'hsl(0, 100%, 40%)'),
other: new TimelineCategory('other', i18nString(UIStrings.system), false, 'hsl(0, 0%, 87%)', 'hsl(0, 0%, 79%)'),
idle: new TimelineCategory('idle', i18nString(UIStrings.idle), false, 'hsl(0, 0%, 98%)', 'hsl(0, 0%, 98%)'),
};
return categories;
}
static setCategories(cats: {
[x: string]: TimelineCategory,
}): void {
categories = cats;
}
static getTimelineMainEventCategories(): string[] {
if (eventCategories) {
return eventCategories;
}
eventCategories = ['idle', 'loading', 'painting', 'rendering', 'scripting', 'other'];
return eventCategories;
}
static setTimelineMainEventCategories(categories: string[]): void {
eventCategories = categories;
}
static generatePieChart(
aggregatedStats: {
[x: string]: number,
},
selfCategory?: TimelineCategory, selfTime?: number): Element {
let total = 0;
for (const categoryName in aggregatedStats) {
total += aggregatedStats[categoryName];
}
const element = document.createElement('div');
element.classList.add('timeline-details-view-pie-chart-wrapper');
element.classList.add('hbox');
const pieChart = new PerfUI.PieChart.PieChart();
const slices: {
value: number,
color: string,
title: string,
}[] = [];
function appendLegendRow(name: string, title: string, value: number, color: string): void {
if (!value) {
return;
}
slices.push({value, color, title});
}
// In case of self time, first add self, then children of the same category.
if (selfCategory) {
if (selfTime) {
appendLegendRow(
selfCategory.name, i18nString(UIStrings.sSelf, {PH1: selfCategory.title}), selfTime, selfCategory.color);
}
// Children of the same category.
const categoryTime = aggregatedStats[selfCategory.name];
const value = categoryTime - (selfTime || 0);
if (value > 0) {
appendLegendRow(
selfCategory.name, i18nString(UIStrings.sChildren, {PH1: selfCategory.title}), value,
selfCategory.childColor);
}
}
// Add other categories.
for (const categoryName in TimelineUIUtils.categories()) {
const category = TimelineUIUtils.categories()[categoryName];
if (category === selfCategory) {
continue;
}
appendLegendRow(category.name, category.title, aggregatedStats[category.name], category.childColor);
}
pieChart.data = {
chartName: i18nString(UIStrings.timeSpentInRendering),
size: 110,
formatter: (value: number): string => i18n.TimeUtilities.preciseMillisToString(value),
showLegend: true,
total,
slices,
};
const pieChartContainer = element.createChild('div', 'vbox');
pieChartContainer.appendChild(pieChart);
return element;
}
static generateDetailsContentForFrame(
frame: TimelineModel.TimelineFrameModel.TimelineFrame,
filmStripFrame: SDK.FilmStripModel.Frame|null): DocumentFragment {
const contentHelper = new TimelineDetailsContentHelper(null, null);
contentHelper.addSection(i18nString(UIStrings.frame));
const duration = TimelineUIUtils.frameDuration(frame);
contentHelper.appendElementRow(i18nString(UIStrings.duration), duration, frame.hasWarnings());
const durationInMillis = frame.endTime - frame.startTime;
contentHelper.appendTextRow(i18nString(UIStrings.fps), Math.floor(1000 / durationInMillis));
contentHelper.appendTextRow(i18nString(UIStrings.cpuTime), i18n.TimeUtilities.millisToString(frame.cpuTime, true));
if (filmStripFrame) {
const filmStripPreview = document.createElement('div');
filmStripPreview.classList.add('timeline-filmstrip-preview');
void filmStripFrame.imageDataPromise()
.then(data => UI.UIUtils.loadImageFromData(data))
.then(image => image && filmStripPreview.appendChild(image));
contentHelper.appendElementRow('', filmStripPreview);
filmStripPreview.addEventListener('click', frameClicked.bind(null, filmStripFrame), false);
}
if (frame.layerTree) {
contentHelper.appendElementRow(
i18nString(UIStrings.layerTree),
Components.Linkifier.Linkifier.linkifyRevealable(frame.layerTree, i18nString(UIStrings.show)));
}
function frameClicked(filmStripFrame: SDK.FilmStripModel.Frame): void {
new PerfUI.FilmStripView.Dialog(filmStripFrame, 0);
}
return contentHelper.fragment;
}
static frameDuration(frame: TimelineModel.TimelineFrameModel.TimelineFrame): Element {
const durationText = i18nString(UIStrings.sAtSParentheses, {
PH1: i18n.TimeUtilities.millisToString(frame.endTime - frame.startTime, true),
PH2: i18n.TimeUtilities.millisToString(frame.startTimeOffset, true),
});
if (!frame.hasWarnings()) {
return i18n.i18n.getFormatLocalizedString(str_, UIStrings.emptyPlaceholder, {PH1: durationText});
}
const link = UI.XLink.XLink.create(
'https://developers.google.com/web/fundamentals/performance/rendering/', i18nString(UIStrings.jank));
return i18n.i18n.getFormatLocalizedString(
str_, UIStrings.sLongFrameTimesAreAnIndicationOf, {PH1: durationText, PH2: link});
}
static createFillStyle(
context: CanvasRenderingContext2D, width: number, height: number, color0: string, color1: string,
color2: string): CanvasGradient {
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;
}
static quadWidth(quad: number[]): number {
return Math.round(Math.sqrt(Math.pow(quad[0] - quad[2], 2) + Math.pow(quad[1] - quad[3], 2)));
}
static quadHeight(quad: number[]): number {
return Math.round(Math.sqrt(Math.pow(quad[0] - quad[6], 2) + Math.pow(quad[1] - quad[7], 2)));
}
static eventDispatchDesciptors(): EventDispatchTypeDescriptor[] {
if (eventDispatchDesciptors) {
return 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%)';
eventDispatchDesciptors = [
new EventDispatchTypeDescriptor(
1, lightOrange, ['mousemove', 'mouseenter', 'mouseleave', 'mouseout', 'mouseover']),
new EventDispatchTypeDescriptor(
1, lightOrange, ['pointerover', 'pointerout', 'pointerenter', 'pointerleave', 'pointermove']),
new EventDispatchTypeDescriptor(2, green, ['wheel']),
new EventDispatchTypeDescriptor(3, orange, ['click', 'mousedown', 'mouseup']),
new EventDispatchTypeDescriptor(3, orange, ['touchstart', 'touchend', 'touchmove', 'touchcancel']),
new EventDispatchTypeDescriptor(
3, orange, ['pointerdown', 'pointerup', 'pointercancel', 'gotpointercapture', 'lostpointercapture']),
new EventDispatchTypeDescriptor(3, purple, ['keydown', 'keyup', 'keypress']),
];
return eventDispatchDesciptors;
}
static markerShortTitle(event: SDK.TracingModel.Event): string|null {
const recordTypes = TimelineModel.TimelineModel.RecordType;
switch (event.name) {
case recordTypes.MarkDOMContent:
return i18n.i18n.lockedString('DCL');
case recordTypes.MarkLoad:
return i18n.i18n.lockedString('L');
case recordTypes.MarkFirstPaint:
return i18n.i18n.lockedString('FP');
case recordTypes.MarkFCP:
return i18n.i18n.lockedString('FCP');
case recordTypes.MarkLCPCandidate:
return i18n.i18n.lockedString('LCP');
}
return null;
}
static markerStyleForEvent(event: SDK.TracingModel.Event): TimelineMarkerStyle {
const tallMarkerDashStyle = [6, 4];
const title = TimelineUIUtils.eventTitle(event);
const recordTypes = TimelineModel.TimelineModel.RecordType;
if (event.name !== recordTypes.NavigationStart &&
(event.hasCategory(TimelineModel.TimelineModel.TimelineModelImpl.Category.Console) ||
event.hasCategory(TimelineModel.TimelineModel.TimelineModelImpl.Category.UserTiming))) {
return {
title: title,
dashStyle: tallMarkerDashStyle,
lineWidth: 0.5,
color: event.hasCategory(TimelineModel.TimelineModel.TimelineModelImpl.Category.UserTiming) ? 'purple' :
'orange',
tall: false,
lowPriority: false,
};
}
let tall = false;
let color = 'grey';
switch (event.name) {
case recordTypes.NavigationStart:
color = '#FF9800';
tall = true;
break;
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.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,
};
}
static markerStyleForFrame(): TimelineMarkerStyle {
return {
title: i18nString(UIStrings.frame),
color: 'rgba(100, 100, 100, 0.4)',
lineWidth: 3,
dashStyle: [3],
tall: true,
lowPriority: true,
};
}
static colorForId(id: string): string {
if (!colorGenerator) {
colorGenerator =
new Common.Color.Generator({min: 30, max: 330, count: undefined}, {min: 50, max: 80, count: 3}, 85);
colorGenerator.setColorForID('', '#f2ecdc');
}
return colorGenerator.colorForID(id);
}
static eventWarning(event: SDK.TracingModel.Event, warningType?: string): Element|null {
const timelineData = TimelineModel.TimelineModel.TimelineData.forEvent(event);
const warning = warningType || timelineData.warning;
if (!warning) {
return null;
}
const warnings = TimelineModel.TimelineModel.TimelineModelImpl.WarningType;
const span = document.createElement('span');
const eventData = event.args['data'];
switch (warning) {
case warnings.ForcedStyle:
case warnings.ForcedLayout: {
const forcedReflowLink = UI.XLink.XLink.create(
'https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing#avoid-forced-synchronous-layouts',
i18nString(UIStrings.forcedReflow));
span.appendChild(i18n.i18n.getFormatLocalizedString(
str_, UIStrings.sIsALikelyPerformanceBottleneck, {PH1: forcedReflowLink}));
break;
}
case warnings.IdleDeadlineExceeded: {
const exceededMs =
i18n.TimeUtilities.millisToString((event.duration || 0) - eventData['allottedMilliseconds'], true);
span.textContent = i18nString(UIStrings.idleCallbackExecutionExtended, {PH1: exceededMs});
break;
}
case warnings.LongHandler: {
span.textContent =
i18nString(UIStrings.handlerTookS, {PH1: i18n.TimeUtilities.millisToString((event.duration || 0), true)});
break;
}
case warnings.LongRecurringHandler: {
span.textContent = i18nString(
UIStrings.recurringHandlerTookS, {PH1: i18n.TimeUtilities.millisToString((event.duration || 0), true)});
break;
}
case warnings.LongTask: {
const longTaskLink =
UI.XLink.XLink.create('https://web.dev/rail/#goals-and-guidelines', i18nString(UIStrings.longTask));
span.appendChild(i18n.i18n.getFormatLocalizedString(
str_, UIStrings.sTookS,
{PH1: longTaskLink, PH2: i18n.TimeUtilities.millisToString((event.duration || 0), true)}));
break;
}
case warnings.V8Deopt: {
span.appendChild(UI.XLink.XLink.create(
'https://github.com/GoogleChrome/devtools-docs/issues/53', i18nString(UIStrings.notOptimized)));
UI.UIUtils.createTextChild(span, i18nString(UIStrings.emptyPlaceholderColon, {PH1: eventData['deoptReason']}));
break;
}
default: {
console.assert(false, 'Unhandled TimelineModel.WarningType');
}
}
return span;
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static displayNameForFrame(frame: TimelineModel.TimelineModel.PageFrame, trimAt?: number): any {
const url = frame.url;
if (!trimAt) {
trimAt = 30;
}
return url.startsWith('about:') ? `"${Platform.StringUtilities.trimMiddle(frame.name, trimAt)}"` :
frame.url.trimEnd(trimAt);
}
}
export class TimelineRecordStyle {
title: string;
category: TimelineCategory;
hidden: boolean;
constructor(title: string, category: TimelineCategory, hidden: boolean|undefined = false) {
this.title = title;
this.category = category;
this.hidden = hidden;
}
}
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum NetworkCategory {
HTML = 'HTML',
Script = 'Script',
Style = 'Style',
Media = 'Media',
Other = 'Other',
}
export const aggregatedStatsKey = Symbol('aggregatedStats');
export class InvalidationsGroupElement extends UI.TreeOutline.TreeElement {
toggleOnClick: boolean;
private readonly relatedNodesMap: Map<number, SDK.DOMModel.DOMNode|null>|null;
private readonly contentHelper: TimelineDetailsContentHelper;
private readonly invalidations: TimelineModel.TimelineModel.InvalidationTrackingEvent[];
constructor(
target: SDK.Target.Target, relatedNodesMap: Map<number, SDK.DOMModel.DOMNode|null>|null,
contentHelper: TimelineDetailsContentHelper,
invalidations: TimelineModel.TimelineModel.InvalidationTrackingEvent[]) {
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);
}
private createTitle(target: SDK.Target.Target): Element {
const first = this.invalidations[0];
const reason = first.cause.reason || i18nString(UIStrings.unknownCause);
const topFrame = first.cause.stackTrace && first.cause.stackTrace[0];
const truncatedNodesElement = this.getTruncatedNodesElement(this.invalidations);
if (truncatedNodesElement === null) {
return i18n.i18n.getFormatLocalizedString(str_, UIStrings.emptyPlaceholder, {PH1: reason});
}
const title = i18n.i18n.getFormatLocalizedString(str_, UIStrings.sForS, {PH1: reason, PH2: truncatedNodesElement});
if (topFrame && this.contentHelper.linkifier()) {
const stack = document.createElement('span');
stack.classList.add('monospace');
const completeTitle = i18n.i18n.getFormatLocalizedString(str_, UIStrings.sSDot, {PH1: title, PH2: stack});
stack.createChild('span').textContent = TimelineUIUtils.frameDisplayName(topFrame);
const linkifier = this.contentHelper.linkifier();
if (linkifier) {
const link =
linkifier.maybeLinkifyConsoleCallFrame(target, topFrame, {showColumnNumber: true, inlineFrameIndex: 0});
if (link) {
// Linkifier is using a workaround with the 'zero width space' (\u200b).
// TODO(szuend): Remove once the Linkifier is no longer using the workaround.
if (!link.textContent || link.textContent === '\u200b') {
link.textContent = i18nString(UIStrings.unknown);
}
stack.createChild('span').textContent = ' @ ';
stack.createChild('span').appendChild(link);
}
}
return completeTitle;
}
return title;
}
async onpopulate(): Promise<void> {
const content = document.createElement('div');
content.classList.add('content');
const first = this.invalidations[0];
if (first.cause.stackTrace) {
const stack = content.createChild('div');
UI.UIUtils.createTextChild(stack, i18nString(UIStrings.stackTraceColon));
this.contentHelper.createChildStackTraceElement(
stack, TimelineUIUtils.stackTraceFromCallFrames(first.cause.stackTrace));
}
UI.UIUtils.createTextChild(
content, this.invalidations.length !== 1 ? i18nString(UIStrings.nodes) : i18nString(UIStrings.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) {
UI.UIUtils.createTextChild(nodeList, ', ');
}
firstNode = false;
nodeList.appendChild(invalidationNode);
const extraData = invalidation.extraData ? ', ' + invalidation.extraData : '';
if (invalidation.changedId) {
UI.UIUtils.createTextChild(
nodeList, i18nString(UIStrings.changedIdToSs, {PH1: invalidation.changedId, PH2: extraData}));
} else if (invalidation.changedClass) {
UI.UIUtils.createTextChild(
nodeList, i18nString(UIStrings.changedClassToSs, {PH1: invalidation.changedClass, PH2: extraData}));
} else if (invalidation.changedAttribute) {
UI.UIUtils.createTextChild(
nodeList,
i18nString(UIStrings.changedAttributeToSs, {PH1: invalidation.changedAttribute, PH2: extraData}));
} else if (invalidation.changedPseudo) {
UI.UIUtils.createTextChild(
nodeList, i18nString(UIStrings.changedPesudoToSs, {PH1: invalidation.changedPseudo, PH2: extraData}));
} else if (invalidation.selectorPart) {
UI.UIUtils.createTextChild(
nodeList, i18nString(UIStrings.changedSs, {PH1: invalidation.selectorPart, extraData}));
}
}
}
const contentTreeElement = new UI.TreeOutline.TreeElement(content, false);
contentTreeElement.selectable = false;
this.appendChild(contentTreeElement);
}
private getTruncatedNodesElement(invalidations: TimelineModel.TimelineModel.InvalidationTrackingEvent[]): Element
|null {
const invalidationNodes = [];
const invalidationNodeIdMap: {
[x: number]: boolean,
} = {};
for (let i = 0; i < invalidations.length; i++) {
const invalidation = invalidations[i];
const invalidationNode = this.createInvalidationNode(invalidation, false);
invalidationNode.addEventListener(
'click',
(evt: Event) => evt.consume(), false);
if (invalidationNode && invalidation.nodeId && !invalidationNodeIdMap[invalidation.nodeId]) {
invalidationNodes.push(invalidationNode);
invalidationNodeIdMap[invalidation.nodeId] = true;
}
}
if (invalidationNodes.length === 1) {
const node = invalidationNodes[0];
if (node instanceof HTMLSpanElement) {
return node;
}
return null;
}
if (invalidationNodes.length === 2) {
return i18n.i18n.getFormatLocalizedString(
str_, UIStrings.sAndS, {PH1: invalidationNodes[0], PH2: invalidationNodes[1]});
}
if (invalidationNodes.length === 3) {
return i18n.i18n.getFormatLocalizedString(
str_, UIStrings.sAndSOther, {PH1: invalidationNodes[0], PH2: invalidationNodes[1]});
}
if (invalidationNodes.length >= 4) {
return i18n.i18n.getFormatLocalizedString(
str_, UIStrings.sSAndSOthers,
{PH1: invalidationNodes[0], PH2: invalidationNodes[1], PH3: String(invalidationNodes.length - 2)});
}
return null;
}
private createInvalidationNode(
invalidation: TimelineModel.TimelineModel.InvalidationTrackingEvent, showUnknownNodes: boolean): HTMLSpanElement
|Text {
const node = (invalidation.nodeId && this.relatedNodesMap) ? this.relatedNodesMap.get(invalidation.nodeId) : null;
if (node) {
const nodeSpan = document.createElement('span');
void Common.Linkifier.Linkifier.linkify(node).then(link => nodeSpan.appendChild(link));
return nodeSpan;
}
if (invalidation.nodeName) {
const nodeSpan = document.createElement('span');
nodeSpan.textContent = invalidation.nodeName;
return nodeSpan;
}
if (showUnknownNodes) {
const nodeSpan = document.createElement('span');
return UI.UIUtils.createTextChild(nodeSpan, i18nString(UIStrings.UnknownNode));
}
throw new Error('Unable to create invalidation node');
}
}
export const previewElementSymbol = Symbol('previewElement');
export class EventDispatchTypeDescriptor {
priority: number;
color: string;
eventTypes: string[];
constructor(priority: number, color: string, eventTypes: string[]) {
this.priority = priority;
this.color = color;
this.eventTypes = eventTypes;
}
}
export class TimelineCategory {
name: string;
title: string;
visible: boolean;
childColor: string;
color: string;
private hiddenInternal?: boolean;
constructor(name: string, title: string, visible: boolean, childColor: string, color: string) {
this.name = name;
this.title = title;
this.visible = visible;
this.childColor = childColor;
this.color = color;
this.hidden = false;
}
get hidden(): boolean {
return Boolean(this.hiddenInternal);
}
set hidden(hidden: boolean) {
this.hiddenInternal = hidden;
}
}
export class TimelineDetailsContentHelper {
fragment: DocumentFragment;
private linkifierInternal: Components.Linkifier.Linkifier|null;
private target: SDK.Target.Target|null;
element: HTMLDivElement;
private tableElement: HTMLElement;
constructor(target: SDK.Target.Target|null, linkifier: Components.Linkifier.Linkifier|null) {
this.fragment = document.createDocumentFragment();
this.linkifierInternal = linkifier;
this.target = target;
this.element = document.createElement('div');
this.element.classList.add('timeline-details-view-block');
this.tableElement = this.element.createChild('div', 'vbox timeline-details-chip-body');
this.fragment.appendChild(this.element);
}
addSection(title: string, swatchColor?: string): void {
if (!this.tableElement.hasChildNodes()) {
this.element.removeChildren();
} else {
this.element = document.createElement('div');
this.element.classList.add('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;
}
UI.UIUtils.createTextChild(titleElement, title);
}
this.tableElement = this.element.createChild('div', 'vbox timeline-details-chip-body');
this.fragment.appendChild(this.element);
}
linkifier(): Components.Linkifier.Linkifier|null {
return this.linkifierInternal;
}
appendTextRow(title: string, value: string|number|boolean): void {
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.toString();
}
appendElementRow(title: string, content: string|Node, isWarning?: boolean, isStacked?: boolean): void {
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 {
UI.UIUtils.createTextChild(valueElement, content || '');
}
}
appendLocationRow(title: string, url: string, startLine: number, startColumn?: number): void {
if (!this.linkifierInternal || !this.target) {
return;
}
const options = {
tabStop: true,
columnNumber: startColumn,
showColumnNumber: true,
inlineFrameIndex: 0,
};
const link = this.linkifierInternal.maybeLinkifyScriptLocation(this.target, null, url, startLine, options);
if (!link) {
return;
}
this.appendElementRow(title, link);
}
appendLocationRange(title: string, url: string, startLine: number, endLine?: number): void {
if (!this.linkifierInternal || !this.target) {
return;
}
const locationContent = document.createElement('span');
const link = this.linkifierInternal.maybeLinkifyScriptLocation(
this.target, null, url, startLine, {tabStop: true, inlineFrameIndex: 0});
if (!link) {
return;
}
locationContent.appendChild(link);
UI.UIUtils.createTextChild(
locationContent, Platform.StringUtilities.sprintf(' [%s…%s]', startLine + 1, (endLine || 0) + 1 || ''));
this.appendElementRow(title, locationContent);
}
appendStackTrace(title: string, stackTrace: Protocol.Runtime.StackTrace): void {
if (!this.linkifierInternal || !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);
}
createChildStackTraceElement(parentElement: Element, stackTrace: Protocol.Runtime.StackTrace): void {
if (!this.linkifierInternal || !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.linkifierInternal, {stackTrace, tabStops: true});
stackTraceElement.appendChild(callFrameContents.element);
}
appendWarningRow(event: SDK.TracingModel.Event, warningType?: string): void {
const warning = TimelineUIUtils.eventWarning(event, warningType);
if (warning) {
this.appendElementRow(i18nString(UIStrings.warning), warning, true);
}
}
}
export const categoryBreakdownCacheSymbol = Symbol('categoryBreakdownCache');
export interface TimelineMarkerStyle {
title: string;
color: string;
lineWidth: number;
dashStyle: number[];
tall: boolean;
lowPriority: boolean;
}
export function assignLayoutShiftsToClusters(layoutShifts: readonly SDK.TracingModel.Event[]): void {
const gapTimeInMs = 1000;
const limitTimeInMs = 5000;
let firstTimestamp = Number.NEGATIVE_INFINITY;
let previousTimestamp = Number.NEGATIVE_INFINITY;
let currentClusterId = 0;
let currentClusterScore = 0;
let currentCluster = new Set<SDK.TracingModel.Event>();
for (const event of layoutShifts) {
if (event.args['data']['had_recent_input'] || event.args['data']['weighted_score_delta'] === undefined) {
continue;
}
if (event.startTime - firstTimestamp > limitTimeInMs || event.startTime - previousTimestamp > gapTimeInMs) {
// This means the event does not fit into the current session/cluster, so we need to start a new cluster.
firstTimestamp = event.startTime;
// Update all the layout shifts we found in this cluster to associate them with the cluster.
for (const layoutShift of currentCluster) {
layoutShift.args['data']['_current_cluster_score'] = currentClusterScore;
layoutShift.args['data']['_current_cluster_id'] = currentClusterId;
}
// Increment the cluster ID and reset the data.
currentClusterId += 1;
currentClusterScore = 0;
currentCluster = new Set();
}
// Store the timestamp of the previous layout shift.
previousTimestamp = event.startTime;
// Update the score of the current cluster and store this event in that cluster
currentClusterScore += event.args['data']['weighted_score_delta'];
currentCluster.add(event);
}
// The last cluster we find may not get closed out - so if not, update all the shifts that we associate with it.
for (const layoutShift of currentCluster) {
layoutShift.args['data']['_current_cluster_score'] = currentClusterScore;
layoutShift.args['data']['_current_cluster_id'] = currentClusterId;
}
}