| // 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; |
| } |
| } |