| /* |
| * Copyright (C) 2013 Google Inc. All rights reserved. |
| * Copyright (C) 2012 Intel Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.TimelineUIUtils = class { |
| /** |
| * @return {!Object.<string, !Timeline.TimelineRecordStyle>} |
| */ |
| static _initEventStyles() { |
| if (Timeline.TimelineUIUtils._eventStylesMap) |
| return Timeline.TimelineUIUtils._eventStylesMap; |
| |
| const recordTypes = TimelineModel.TimelineModel.RecordType; |
| const categories = Timeline.TimelineUIUtils.categories(); |
| |
| const eventStyles = {}; |
| eventStyles[recordTypes.Task] = new Timeline.TimelineRecordStyle(Common.UIString('Task'), categories['other']); |
| eventStyles[recordTypes.Program] = new Timeline.TimelineRecordStyle(Common.UIString('Other'), categories['other']); |
| eventStyles[recordTypes.Animation] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Animation'), categories['rendering']); |
| eventStyles[recordTypes.EventDispatch] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Event'), categories['scripting']); |
| eventStyles[recordTypes.RequestMainThreadFrame] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Request Main Thread Frame'), categories['rendering'], true); |
| eventStyles[recordTypes.BeginFrame] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Frame Start'), categories['rendering'], true); |
| eventStyles[recordTypes.BeginMainThreadFrame] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Frame Start (main thread)'), categories['rendering'], true); |
| eventStyles[recordTypes.DrawFrame] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Draw Frame'), categories['rendering'], true); |
| eventStyles[recordTypes.HitTest] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Hit Test'), categories['rendering']); |
| eventStyles[recordTypes.ScheduleStyleRecalculation] = new Timeline.TimelineRecordStyle( |
| Common.UIString('Schedule Style Recalculation'), categories['rendering'], true); |
| eventStyles[recordTypes.RecalculateStyles] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Recalculate Style'), categories['rendering']); |
| eventStyles[recordTypes.UpdateLayoutTree] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Recalculate Style'), categories['rendering']); |
| eventStyles[recordTypes.InvalidateLayout] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Invalidate Layout'), categories['rendering'], true); |
| eventStyles[recordTypes.Layout] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Layout'), categories['rendering']); |
| eventStyles[recordTypes.PaintSetup] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Paint Setup'), categories['painting']); |
| eventStyles[recordTypes.PaintImage] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Paint Image'), categories['painting'], true); |
| eventStyles[recordTypes.UpdateLayer] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Update Layer'), categories['painting'], true); |
| eventStyles[recordTypes.UpdateLayerTree] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Update Layer Tree'), categories['rendering']); |
| eventStyles[recordTypes.Paint] = new Timeline.TimelineRecordStyle(Common.UIString('Paint'), categories['painting']); |
| eventStyles[recordTypes.RasterTask] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Rasterize Paint'), categories['painting']); |
| eventStyles[recordTypes.ScrollLayer] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Scroll'), categories['rendering']); |
| eventStyles[recordTypes.CompositeLayers] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Composite Layers'), categories['painting']); |
| eventStyles[recordTypes.ParseHTML] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Parse HTML'), categories['loading']); |
| eventStyles[recordTypes.ParseAuthorStyleSheet] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Parse Stylesheet'), categories['loading']); |
| eventStyles[recordTypes.TimerInstall] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Install Timer'), categories['scripting']); |
| eventStyles[recordTypes.TimerRemove] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Remove Timer'), categories['scripting']); |
| eventStyles[recordTypes.TimerFire] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Timer Fired'), categories['scripting']); |
| eventStyles[recordTypes.XHRReadyStateChange] = |
| new Timeline.TimelineRecordStyle(Common.UIString('XHR Ready State Change'), categories['scripting']); |
| eventStyles[recordTypes.XHRLoad] = |
| new Timeline.TimelineRecordStyle(Common.UIString('XHR Load'), categories['scripting']); |
| eventStyles[recordTypes.CompileScript] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Compile Script'), categories['scripting']); |
| eventStyles[recordTypes.EvaluateScript] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Evaluate Script'), categories['scripting']); |
| eventStyles[recordTypes.CompileModule] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Compile Module'), categories['scripting']); |
| eventStyles[recordTypes.EvaluateModule] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Evaluate Module'), categories['scripting']); |
| eventStyles[recordTypes.ParseScriptOnBackground] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Parse Script'), categories['scripting']); |
| eventStyles[recordTypes.FrameStartedLoading] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Frame Started Loading'), categories['loading'], true); |
| eventStyles[recordTypes.MarkLoad] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Load event'), categories['scripting'], true); |
| eventStyles[recordTypes.MarkDOMContent] = |
| new Timeline.TimelineRecordStyle(Common.UIString('DOMContentLoaded event'), categories['scripting'], true); |
| eventStyles[recordTypes.MarkFirstPaint] = |
| new Timeline.TimelineRecordStyle(Common.UIString('First paint'), categories['painting'], true); |
| eventStyles[recordTypes.MarkFCP] = |
| new Timeline.TimelineRecordStyle(Common.UIString('First contentful paint'), categories['rendering'], true); |
| eventStyles[recordTypes.MarkFMP] = |
| new Timeline.TimelineRecordStyle(Common.UIString('First meaningful paint'), categories['rendering'], true); |
| eventStyles[recordTypes.MarkFMPCandidate] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Meaningful paint candidate'), categories['rendering'], true); |
| eventStyles[recordTypes.TimeStamp] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Timestamp'), categories['scripting']); |
| eventStyles[recordTypes.ConsoleTime] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Console Time'), categories['scripting']); |
| eventStyles[recordTypes.UserTiming] = |
| new Timeline.TimelineRecordStyle(Common.UIString('User Timing'), categories['scripting']); |
| eventStyles[recordTypes.ResourceSendRequest] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Send Request'), categories['loading']); |
| eventStyles[recordTypes.ResourceReceiveResponse] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Receive Response'), categories['loading']); |
| eventStyles[recordTypes.ResourceFinish] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Finish Loading'), categories['loading']); |
| eventStyles[recordTypes.ResourceReceivedData] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Receive Data'), categories['loading']); |
| eventStyles[recordTypes.RunMicrotasks] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Run Microtasks'), categories['scripting']); |
| eventStyles[recordTypes.FunctionCall] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Function Call'), categories['scripting']); |
| eventStyles[recordTypes.GCEvent] = |
| new Timeline.TimelineRecordStyle(Common.UIString('GC Event'), categories['scripting']); |
| eventStyles[recordTypes.MajorGC] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Major GC'), categories['scripting']); |
| eventStyles[recordTypes.MinorGC] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Minor GC'), categories['scripting']); |
| eventStyles[recordTypes.JSFrame] = |
| new Timeline.TimelineRecordStyle(Common.UIString('JS Frame'), categories['scripting']); |
| eventStyles[recordTypes.RequestAnimationFrame] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Request Animation Frame'), categories['scripting']); |
| eventStyles[recordTypes.CancelAnimationFrame] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Cancel Animation Frame'), categories['scripting']); |
| eventStyles[recordTypes.FireAnimationFrame] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Animation Frame Fired'), categories['scripting']); |
| eventStyles[recordTypes.RequestIdleCallback] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Request Idle Callback'), categories['scripting']); |
| eventStyles[recordTypes.CancelIdleCallback] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Cancel Idle Callback'), categories['scripting']); |
| eventStyles[recordTypes.FireIdleCallback] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Fire Idle Callback'), categories['scripting']); |
| eventStyles[recordTypes.WebSocketCreate] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Create WebSocket'), categories['scripting']); |
| eventStyles[recordTypes.WebSocketSendHandshakeRequest] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Send WebSocket Handshake'), categories['scripting']); |
| eventStyles[recordTypes.WebSocketReceiveHandshakeResponse] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Receive WebSocket Handshake'), categories['scripting']); |
| eventStyles[recordTypes.WebSocketDestroy] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Destroy WebSocket'), categories['scripting']); |
| eventStyles[recordTypes.EmbedderCallback] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Embedder Callback'), categories['scripting']); |
| eventStyles[recordTypes.DecodeImage] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Image Decode'), categories['painting']); |
| eventStyles[recordTypes.ResizeImage] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Image Resize'), categories['painting']); |
| eventStyles[recordTypes.GPUTask] = new Timeline.TimelineRecordStyle(Common.UIString('GPU'), categories['gpu']); |
| eventStyles[recordTypes.LatencyInfo] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Input Latency'), categories['scripting']); |
| |
| eventStyles[recordTypes.GCIdleLazySweep] = |
| new Timeline.TimelineRecordStyle(Common.UIString('DOM GC'), categories['scripting']); |
| eventStyles[recordTypes.GCCompleteSweep] = |
| new Timeline.TimelineRecordStyle(Common.UIString('DOM GC'), categories['scripting']); |
| eventStyles[recordTypes.GCCollectGarbage] = |
| new Timeline.TimelineRecordStyle(Common.UIString('DOM GC'), categories['scripting']); |
| |
| eventStyles[recordTypes.CryptoDoEncrypt] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Encrypt'), categories['scripting']); |
| eventStyles[recordTypes.CryptoDoEncryptReply] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Encrypt Reply'), categories['scripting']); |
| eventStyles[recordTypes.CryptoDoDecrypt] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Decrypt'), categories['scripting']); |
| eventStyles[recordTypes.CryptoDoDecryptReply] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Decrypt Reply'), categories['scripting']); |
| eventStyles[recordTypes.CryptoDoDigest] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Digest'), categories['scripting']); |
| eventStyles[recordTypes.CryptoDoDigestReply] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Digest Reply'), categories['scripting']); |
| eventStyles[recordTypes.CryptoDoSign] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Sign'), categories['scripting']); |
| eventStyles[recordTypes.CryptoDoSignReply] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Sign Reply'), categories['scripting']); |
| eventStyles[recordTypes.CryptoDoVerify] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Verify'), categories['scripting']); |
| eventStyles[recordTypes.CryptoDoVerifyReply] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Verify Reply'), categories['scripting']); |
| |
| eventStyles[recordTypes.AsyncTask] = |
| new Timeline.TimelineRecordStyle(Common.UIString('Async Task'), categories['async']); |
| |
| Timeline.TimelineUIUtils._eventStylesMap = eventStyles; |
| return eventStyles; |
| } |
| |
| /** |
| * @param {!TimelineModel.TimelineIRModel.InputEvents} inputEventType |
| * @return {?string} |
| */ |
| static inputEventDisplayName(inputEventType) { |
| if (!Timeline.TimelineUIUtils._inputEventToDisplayName) { |
| const inputEvent = TimelineModel.TimelineIRModel.InputEvents; |
| |
| /** @type {!Map<!TimelineModel.TimelineIRModel.InputEvents, string>} */ |
| Timeline.TimelineUIUtils._inputEventToDisplayName = new Map([ |
| [inputEvent.Char, Common.UIString('Key Character')], |
| [inputEvent.KeyDown, Common.UIString('Key Down')], |
| [inputEvent.KeyDownRaw, Common.UIString('Key Down')], |
| [inputEvent.KeyUp, Common.UIString('Key Up')], |
| [inputEvent.Click, Common.UIString('Click')], |
| [inputEvent.ContextMenu, Common.UIString('Context Menu')], |
| [inputEvent.MouseDown, Common.UIString('Mouse Down')], |
| [inputEvent.MouseMove, Common.UIString('Mouse Move')], |
| [inputEvent.MouseUp, Common.UIString('Mouse Up')], |
| [inputEvent.MouseWheel, Common.UIString('Mouse Wheel')], |
| [inputEvent.ScrollBegin, Common.UIString('Scroll Begin')], |
| [inputEvent.ScrollEnd, Common.UIString('Scroll End')], |
| [inputEvent.ScrollUpdate, Common.UIString('Scroll Update')], |
| [inputEvent.FlingStart, Common.UIString('Fling Start')], |
| [inputEvent.FlingCancel, Common.UIString('Fling Halt')], |
| [inputEvent.Tap, Common.UIString('Tap')], |
| [inputEvent.TapCancel, Common.UIString('Tap Halt')], |
| [inputEvent.ShowPress, Common.UIString('Tap Begin')], |
| [inputEvent.TapDown, Common.UIString('Tap Down')], |
| [inputEvent.TouchCancel, Common.UIString('Touch Cancel')], |
| [inputEvent.TouchEnd, Common.UIString('Touch End')], |
| [inputEvent.TouchMove, Common.UIString('Touch Move')], |
| [inputEvent.TouchStart, Common.UIString('Touch Start')], |
| [inputEvent.PinchBegin, Common.UIString('Pinch Begin')], |
| [inputEvent.PinchEnd, Common.UIString('Pinch End')], |
| [inputEvent.PinchUpdate, Common.UIString('Pinch Update')] |
| ]); |
| } |
| return Timeline.TimelineUIUtils._inputEventToDisplayName.get(inputEventType) || null; |
| } |
| |
| /** |
| * @param {!Protocol.Runtime.CallFrame} frame |
| * @return {string} |
| */ |
| static frameDisplayName(frame) { |
| if (!TimelineModel.TimelineJSProfileProcessor.isNativeRuntimeFrame(frame)) |
| return UI.beautifyFunctionName(frame.functionName); |
| const nativeGroup = TimelineModel.TimelineJSProfileProcessor.nativeGroup(frame.functionName); |
| const groups = TimelineModel.TimelineJSProfileProcessor.NativeGroups; |
| switch (nativeGroup) { |
| case groups.Compile: |
| return Common.UIString('Compile'); |
| case groups.Parse: |
| return Common.UIString('Parse'); |
| } |
| return frame.functionName; |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} traceEvent |
| * @param {!RegExp} regExp |
| * @return {boolean} |
| */ |
| static testContentMatching(traceEvent, regExp) { |
| const title = Timeline.TimelineUIUtils.eventStyle(traceEvent).title; |
| const tokens = [title]; |
| const url = TimelineModel.TimelineData.forEvent(traceEvent).url; |
| if (url) |
| tokens.push(url); |
| appendObjectProperties(traceEvent.args, 2); |
| return regExp.test(tokens.join('|')); |
| |
| /** |
| * @param {!Object} object |
| * @param {number} depth |
| */ |
| function appendObjectProperties(object, depth) { |
| if (!depth) |
| return; |
| for (const key in object) { |
| const value = object[key]; |
| const type = typeof value; |
| if (type === 'string') |
| tokens.push(value); |
| else if (type === 'number') |
| tokens.push(String(value)); |
| else if (type === 'object') |
| appendObjectProperties(value, depth - 1); |
| } |
| } |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @return {?string} |
| */ |
| static eventURL(event) { |
| const data = event.args['data'] || event.args['beginData']; |
| const url = data && data.url; |
| if (url) |
| return url; |
| const stackTrace = data && data['stackTrace']; |
| const frame = |
| stackTrace && stackTrace.length && stackTrace[0] || TimelineModel.TimelineData.forEvent(event).topFrame(); |
| return frame && frame.url || null; |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @return {!{title: string, category: !Timeline.TimelineCategory}} |
| */ |
| static eventStyle(event) { |
| const eventStyles = Timeline.TimelineUIUtils._initEventStyles(); |
| if (event.hasCategory(TimelineModel.TimelineModel.Category.Console) || |
| event.hasCategory(TimelineModel.TimelineModel.Category.UserTiming)) |
| return {title: event.name, category: Timeline.TimelineUIUtils.categories()['scripting']}; |
| |
| if (event.hasCategory(TimelineModel.TimelineModel.Category.LatencyInfo)) { |
| /** @const */ |
| const prefix = 'InputLatency::'; |
| const inputEventType = event.name.startsWith(prefix) ? event.name.substr(prefix.length) : event.name; |
| const displayName = Timeline.TimelineUIUtils.inputEventDisplayName( |
| /** @type {!TimelineModel.TimelineIRModel.InputEvents} */ (inputEventType)); |
| return {title: displayName || inputEventType, category: Timeline.TimelineUIUtils.categories()['scripting']}; |
| } |
| let result = eventStyles[event.name]; |
| if (!result) { |
| result = new Timeline.TimelineRecordStyle(event.name, Timeline.TimelineUIUtils.categories()['other'], true); |
| eventStyles[event.name] = result; |
| } |
| return result; |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @return {string} |
| */ |
| static eventColor(event) { |
| if (event.name === TimelineModel.TimelineModel.RecordType.JSFrame) { |
| const frame = event.args['data']; |
| if (Timeline.TimelineUIUtils.isUserFrame(frame)) |
| return Timeline.TimelineUIUtils.colorForId(frame.url); |
| } |
| return Timeline.TimelineUIUtils.eventStyle(event).category.color; |
| } |
| |
| /** |
| * @param {!ProductRegistry.Registry} productRegistry |
| * @param {!TimelineModel.TimelineModel} model |
| * @param {!Map<string, string>} urlToColorCache |
| * @param {!SDK.TracingModel.Event} event |
| * @return {string} |
| */ |
| static eventColorByProduct(productRegistry, model, urlToColorCache, event) { |
| const url = Timeline.TimelineUIUtils.eventURL(event) || ''; |
| let color = urlToColorCache.get(url); |
| if (color) |
| return color; |
| const defaultColor = '#f2ecdc'; |
| const parsedURL = url.asParsedURL(); |
| if (!parsedURL) |
| return defaultColor; |
| let name = productRegistry && productRegistry.nameForUrl(parsedURL); |
| if (!name) { |
| name = parsedURL.host; |
| const rootFrames = model.rootFrames(); |
| if (rootFrames.some(pageFrame => new Common.ParsedURL(pageFrame.url).host === name)) |
| color = defaultColor; |
| } |
| if (!color) |
| color = name ? ProductRegistry.BadgePool.colorForEntryName(name) : defaultColor; |
| urlToColorCache.set(url, color); |
| return color; |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @return {string} |
| */ |
| static eventTitle(event) { |
| const recordType = TimelineModel.TimelineModel.RecordType; |
| const eventData = event.args['data']; |
| if (event.name === recordType.JSFrame) |
| return Timeline.TimelineUIUtils.frameDisplayName(eventData); |
| const title = Timeline.TimelineUIUtils.eventStyle(event).title; |
| if (event.hasCategory(TimelineModel.TimelineModel.Category.Console)) |
| return title; |
| if (event.name === recordType.TimeStamp) |
| return Common.UIString('%s: %s', title, eventData['message']); |
| if (event.name === recordType.Animation && eventData && eventData['name']) |
| return Common.UIString('%s: %s', title, eventData['name']); |
| return title; |
| } |
| |
| /** |
| * !Map<!TimelineModel.TimelineIRModel.Phases, !{color: string, label: string}> |
| */ |
| static _interactionPhaseStyles() { |
| let map = Timeline.TimelineUIUtils._interactionPhaseStylesMap; |
| if (!map) { |
| map = new Map([ |
| [TimelineModel.TimelineIRModel.Phases.Idle, {color: 'white', label: 'Idle'}], |
| [ |
| TimelineModel.TimelineIRModel.Phases.Response, |
| {color: 'hsl(43, 83%, 64%)', label: Common.UIString('Response')} |
| ], |
| [TimelineModel.TimelineIRModel.Phases.Scroll, {color: 'hsl(256, 67%, 70%)', label: Common.UIString('Scroll')}], |
| [TimelineModel.TimelineIRModel.Phases.Fling, {color: 'hsl(256, 67%, 70%)', label: Common.UIString('Fling')}], |
| [TimelineModel.TimelineIRModel.Phases.Drag, {color: 'hsl(256, 67%, 70%)', label: Common.UIString('Drag')}], |
| [ |
| TimelineModel.TimelineIRModel.Phases.Animation, |
| {color: 'hsl(256, 67%, 70%)', label: Common.UIString('Animation')} |
| ], |
| [ |
| TimelineModel.TimelineIRModel.Phases.Uncategorized, |
| {color: 'hsl(0, 0%, 87%)', label: Common.UIString('Uncategorized')} |
| ] |
| ]); |
| Timeline.TimelineUIUtils._interactionPhaseStylesMap = map; |
| } |
| return map; |
| } |
| |
| /** |
| * @param {!TimelineModel.TimelineIRModel.Phases} phase |
| * @return {string} |
| */ |
| static interactionPhaseColor(phase) { |
| return Timeline.TimelineUIUtils._interactionPhaseStyles().get(phase).color; |
| } |
| |
| /** |
| * @param {!TimelineModel.TimelineIRModel.Phases} phase |
| * @return {string} |
| */ |
| static interactionPhaseLabel(phase) { |
| return Timeline.TimelineUIUtils._interactionPhaseStyles().get(phase).label; |
| } |
| |
| /** |
| * @param {!Protocol.Runtime.CallFrame} frame |
| * @return {boolean} |
| */ |
| static isUserFrame(frame) { |
| return frame.scriptId !== '0' && !(frame.url && frame.url.startsWith('native ')); |
| } |
| |
| /** |
| * @param {!TimelineModel.TimelineModel.NetworkRequest} request |
| * @return {!Timeline.TimelineUIUtils.NetworkCategory} |
| */ |
| static networkRequestCategory(request) { |
| const categories = Timeline.TimelineUIUtils.NetworkCategory; |
| switch (request.mimeType) { |
| case 'text/html': |
| return categories.HTML; |
| case 'application/javascript': |
| case 'application/x-javascript': |
| case 'text/javascript': |
| return categories.Script; |
| case 'text/css': |
| return categories.Style; |
| case 'audio/ogg': |
| case 'image/gif': |
| case 'image/jpeg': |
| case 'image/png': |
| case 'image/svg+xml': |
| case 'image/webp': |
| case 'image/x-icon': |
| case 'font/opentype': |
| case 'font/woff2': |
| case 'application/font-woff': |
| return categories.Media; |
| default: |
| return categories.Other; |
| } |
| } |
| |
| /** |
| * @param {!Timeline.TimelineUIUtils.NetworkCategory} category |
| * @return {string} |
| */ |
| static networkCategoryColor(category) { |
| const categories = Timeline.TimelineUIUtils.NetworkCategory; |
| switch (category) { |
| case categories.HTML: |
| return 'hsl(214, 67%, 66%)'; |
| case categories.Script: |
| return 'hsl(43, 83%, 64%)'; |
| case categories.Style: |
| return 'hsl(256, 67%, 70%)'; |
| case categories.Media: |
| return 'hsl(109, 33%, 55%)'; |
| default: |
| return 'hsl(0, 0%, 70%)'; |
| } |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @param {?SDK.Target} target |
| * @return {?string} |
| */ |
| static buildDetailsTextForTraceEvent(event, target) { |
| const recordType = TimelineModel.TimelineModel.RecordType; |
| let detailsText; |
| const eventData = event.args['data']; |
| switch (event.name) { |
| case recordType.GCEvent: |
| case recordType.MajorGC: |
| case recordType.MinorGC: { |
| const delta = event.args['usedHeapSizeBefore'] - event.args['usedHeapSizeAfter']; |
| detailsText = Common.UIString('%s collected', Number.bytesToString(delta)); |
| break; |
| } |
| case recordType.FunctionCall: |
| if (eventData) { |
| detailsText = |
| linkifyLocationAsText(eventData['scriptId'], eventData['lineNumber'], eventData['columnNumber']); |
| } |
| break; |
| case recordType.JSFrame: |
| detailsText = Timeline.TimelineUIUtils.frameDisplayName(eventData); |
| break; |
| case recordType.EventDispatch: |
| detailsText = eventData ? eventData['type'] : null; |
| break; |
| case recordType.Paint: { |
| const width = Timeline.TimelineUIUtils.quadWidth(eventData.clip); |
| const height = Timeline.TimelineUIUtils.quadHeight(eventData.clip); |
| if (width && height) |
| detailsText = Common.UIString('%d\xa0\u00d7\xa0%d', width, height); |
| break; |
| } |
| case recordType.ParseHTML: { |
| const endLine = event.args['endData'] && event.args['endData']['endLine']; |
| const url = Bindings.displayNameForURL(event.args['beginData']['url']); |
| detailsText = Common.UIString( |
| '%s [%s\u2026%s]', url, event.args['beginData']['startLine'] + 1, endLine >= 0 ? endLine + 1 : ''); |
| break; |
| } |
| case recordType.CompileModule: |
| detailsText = Bindings.displayNameForURL(event.args['fileName']); |
| break; |
| case recordType.CompileScript: |
| case recordType.EvaluateScript: { |
| const url = eventData && eventData['url']; |
| if (url) |
| detailsText = Bindings.displayNameForURL(url) + ':' + (eventData['lineNumber'] + 1); |
| break; |
| } |
| case recordType.ParseScriptOnBackground: |
| case recordType.XHRReadyStateChange: |
| case recordType.XHRLoad: { |
| const url = eventData['url']; |
| if (url) |
| detailsText = Bindings.displayNameForURL(url); |
| break; |
| } |
| case recordType.TimeStamp: |
| detailsText = eventData['message']; |
| break; |
| |
| case recordType.WebSocketCreate: |
| case recordType.WebSocketSendHandshakeRequest: |
| case recordType.WebSocketReceiveHandshakeResponse: |
| case recordType.WebSocketDestroy: |
| case recordType.ResourceSendRequest: |
| case recordType.ResourceReceivedData: |
| case recordType.ResourceReceiveResponse: |
| case recordType.ResourceFinish: |
| case recordType.PaintImage: |
| case recordType.DecodeImage: |
| case recordType.ResizeImage: |
| case recordType.DecodeLazyPixelRef: { |
| const url = TimelineModel.TimelineData.forEvent(event).url; |
| if (url) |
| detailsText = Bindings.displayNameForURL(url); |
| break; |
| } |
| |
| case recordType.EmbedderCallback: |
| detailsText = eventData['callbackName']; |
| break; |
| |
| case recordType.Animation: |
| detailsText = eventData && eventData['name']; |
| break; |
| |
| case recordType.GCIdleLazySweep: |
| detailsText = Common.UIString('idle sweep'); |
| break; |
| |
| case recordType.GCCompleteSweep: |
| detailsText = Common.UIString('complete sweep'); |
| break; |
| |
| case recordType.GCCollectGarbage: |
| detailsText = Common.UIString('collect'); |
| break; |
| |
| case recordType.AsyncTask: |
| detailsText = eventData ? eventData['name'] : null; |
| break; |
| |
| default: |
| if (event.hasCategory(TimelineModel.TimelineModel.Category.Console)) |
| detailsText = null; |
| else |
| detailsText = linkifyTopCallFrameAsText(); |
| break; |
| } |
| |
| return detailsText; |
| |
| /** |
| * @param {string} scriptId |
| * @param {number} lineNumber |
| * @param {number} columnNumber |
| * @return {?string} |
| */ |
| function linkifyLocationAsText(scriptId, lineNumber, columnNumber) { |
| const debuggerModel = target ? target.model(SDK.DebuggerModel) : null; |
| if (!target || target.isDisposed() || !scriptId || !debuggerModel) |
| return null; |
| const rawLocation = debuggerModel.createRawLocationByScriptId(scriptId, lineNumber, columnNumber); |
| if (!rawLocation) |
| return null; |
| const uiLocation = Bindings.debuggerWorkspaceBinding.rawLocationToUILocation(rawLocation); |
| return uiLocation ? uiLocation.linkText() : null; |
| } |
| |
| /** |
| * @return {?string} |
| */ |
| function linkifyTopCallFrameAsText() { |
| const frame = TimelineModel.TimelineData.forEvent(event).topFrame(); |
| if (!frame) |
| return null; |
| let text = linkifyLocationAsText(frame.scriptId, frame.lineNumber, frame.columnNumber); |
| if (!text) { |
| text = frame.url; |
| if (typeof frame.lineNumber === 'number') |
| text += ':' + (frame.lineNumber + 1); |
| } |
| return text; |
| } |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @param {?SDK.Target} target |
| * @param {!Components.Linkifier} linkifier |
| * @return {?Node} |
| */ |
| static buildDetailsNodeForTraceEvent(event, target, linkifier) { |
| const recordType = TimelineModel.TimelineModel.RecordType; |
| let details = null; |
| let detailsText; |
| const eventData = event.args['data']; |
| switch (event.name) { |
| case recordType.GCEvent: |
| case recordType.MajorGC: |
| case recordType.MinorGC: |
| case recordType.EventDispatch: |
| case recordType.Paint: |
| case recordType.Animation: |
| case recordType.EmbedderCallback: |
| case recordType.ParseHTML: |
| case recordType.WebSocketCreate: |
| case recordType.WebSocketSendHandshakeRequest: |
| case recordType.WebSocketReceiveHandshakeResponse: |
| case recordType.WebSocketDestroy: |
| case recordType.GCIdleLazySweep: |
| case recordType.GCCompleteSweep: |
| case recordType.GCCollectGarbage: |
| detailsText = Timeline.TimelineUIUtils.buildDetailsTextForTraceEvent(event, target); |
| break; |
| case recordType.PaintImage: |
| case recordType.DecodeImage: |
| case recordType.ResizeImage: |
| case recordType.DecodeLazyPixelRef: |
| case recordType.XHRReadyStateChange: |
| case recordType.XHRLoad: |
| case recordType.ResourceSendRequest: |
| case recordType.ResourceReceivedData: |
| case recordType.ResourceReceiveResponse: |
| case recordType.ResourceFinish: { |
| const url = TimelineModel.TimelineData.forEvent(event).url; |
| if (url) |
| details = Components.Linkifier.linkifyURL(url); |
| break; |
| } |
| case recordType.FunctionCall: |
| case recordType.JSFrame: |
| details = createElement('span'); |
| details.createTextChild(Timeline.TimelineUIUtils.frameDisplayName(eventData)); |
| const location = linkifyLocation( |
| eventData['scriptId'], eventData['url'], eventData['lineNumber'], eventData['columnNumber']); |
| if (location) { |
| details.createTextChild(' @ '); |
| details.appendChild(location); |
| } |
| break; |
| case recordType.CompileModule: |
| details = linkifyLocation('', event.args['fileName'], 0, 0); |
| break; |
| case recordType.CompileScript: |
| case recordType.EvaluateScript: { |
| const url = eventData['url']; |
| if (url) |
| details = linkifyLocation('', url, eventData['lineNumber'], 0); |
| break; |
| } |
| case recordType.ParseScriptOnBackground: { |
| const url = eventData['url']; |
| if (url) |
| details = linkifyLocation('', url, 0, 0); |
| break; |
| } |
| default: |
| if (event.hasCategory(TimelineModel.TimelineModel.Category.Console)) |
| detailsText = null; |
| else |
| details = linkifyTopCallFrame(); |
| break; |
| } |
| |
| if (!details && detailsText) |
| details = createTextNode(detailsText); |
| return details; |
| |
| /** |
| * @param {string} scriptId |
| * @param {string} url |
| * @param {number} lineNumber |
| * @param {number=} columnNumber |
| * @return {?Element} |
| */ |
| function linkifyLocation(scriptId, url, lineNumber, columnNumber) { |
| return linkifier.linkifyScriptLocation(target, scriptId, url, lineNumber, columnNumber, 'timeline-details'); |
| } |
| |
| /** |
| * @return {?Element} |
| */ |
| function linkifyTopCallFrame() { |
| const frame = TimelineModel.TimelineData.forEvent(event).topFrame(); |
| return frame ? linkifier.maybeLinkifyConsoleCallFrame(target, frame, 'timeline-details') : null; |
| } |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @param {!TimelineModel.TimelineModel} model |
| * @param {!Components.Linkifier} linkifier |
| * @param {!ProductRegistry.BadgePool} badgePool |
| * @param {boolean} detailed |
| * @return {!Promise<!DocumentFragment>} |
| */ |
| static async buildTraceEventDetails(event, model, linkifier, badgePool, detailed) { |
| const maybeTarget = model.targetByEvent(event); |
| /** @type {?Map<number, ?SDK.DOMNode>} */ |
| let relatedNodesMap = null; |
| if (maybeTarget) { |
| const target = /** @type {!SDK.Target} */ (maybeTarget); |
| if (typeof event[Timeline.TimelineUIUtils._previewElementSymbol] === 'undefined') { |
| let previewElement = null; |
| const url = TimelineModel.TimelineData.forEvent(event).url; |
| if (url) |
| previewElement = await BrowserComponents.ImagePreview.build(target, url, false); |
| else if (TimelineModel.TimelineData.forEvent(event).picture) |
| previewElement = await Timeline.TimelineUIUtils.buildPicturePreviewContent(event, target); |
| event[Timeline.TimelineUIUtils._previewElementSymbol] = previewElement; |
| } |
| |
| /** @type {!Set<number>} */ |
| const nodeIdsToResolve = new Set(); |
| const timelineData = TimelineModel.TimelineData.forEvent(event); |
| if (timelineData.backendNodeId) |
| nodeIdsToResolve.add(timelineData.backendNodeId); |
| const invalidationTrackingEvents = TimelineModel.InvalidationTracker.invalidationEventsFor(event); |
| if (invalidationTrackingEvents) |
| Timeline.TimelineUIUtils._collectInvalidationNodeIds(nodeIdsToResolve, invalidationTrackingEvents); |
| if (nodeIdsToResolve.size) { |
| const domModel = target.model(SDK.DOMModel); |
| if (domModel) |
| relatedNodesMap = await domModel.pushNodesByBackendIdsToFrontend(nodeIdsToResolve); |
| } |
| } |
| |
| const recordTypes = TimelineModel.TimelineModel.RecordType; |
| // This message may vary per event.name; |
| let relatedNodeLabel; |
| |
| const contentHelper = new Timeline.TimelineDetailsContentHelper(model.targetByEvent(event), linkifier); |
| contentHelper.addSection( |
| Timeline.TimelineUIUtils.eventTitle(event), Timeline.TimelineUIUtils.eventStyle(event).category.color); |
| |
| const eventData = event.args['data']; |
| const timelineData = TimelineModel.TimelineData.forEvent(event); |
| const initiator = timelineData.initiator(); |
| let url = null; |
| |
| if (timelineData.warning) |
| contentHelper.appendWarningRow(event); |
| if (event.name === recordTypes.JSFrame && eventData['deoptReason']) |
| contentHelper.appendWarningRow(event, TimelineModel.TimelineModel.WarningType.V8Deopt); |
| |
| if (detailed) { |
| contentHelper.appendTextRow(Common.UIString('Total Time'), Number.millisToString(event.duration || 0, true)); |
| contentHelper.appendTextRow(Common.UIString('Self Time'), Number.millisToString(event.selfTime, true)); |
| } |
| |
| if (model.isGenericTrace()) { |
| for (const key in event.args) { |
| try { |
| contentHelper.appendTextRow(key, JSON.stringify(event.args[key])); |
| } catch (e) { |
| contentHelper.appendTextRow(key, `<${typeof event.args[key]}>`); |
| } |
| } |
| return contentHelper.fragment; |
| } |
| |
| switch (event.name) { |
| case recordTypes.GCEvent: |
| case recordTypes.MajorGC: |
| case recordTypes.MinorGC: |
| const delta = event.args['usedHeapSizeBefore'] - event.args['usedHeapSizeAfter']; |
| contentHelper.appendTextRow(Common.UIString('Collected'), Number.bytesToString(delta)); |
| break; |
| case recordTypes.JSFrame: |
| case recordTypes.FunctionCall: |
| const detailsNode = |
| Timeline.TimelineUIUtils.buildDetailsNodeForTraceEvent(event, model.targetByEvent(event), linkifier); |
| if (detailsNode) |
| contentHelper.appendElementRow(Common.UIString('Function'), detailsNode); |
| break; |
| case recordTypes.TimerFire: |
| case recordTypes.TimerInstall: |
| case recordTypes.TimerRemove: |
| contentHelper.appendTextRow(Common.UIString('Timer ID'), eventData['timerId']); |
| if (event.name === recordTypes.TimerInstall) { |
| contentHelper.appendTextRow(Common.UIString('Timeout'), Number.millisToString(eventData['timeout'])); |
| contentHelper.appendTextRow(Common.UIString('Repeats'), !eventData['singleShot']); |
| } |
| break; |
| case recordTypes.FireAnimationFrame: |
| contentHelper.appendTextRow(Common.UIString('Callback ID'), eventData['id']); |
| break; |
| case recordTypes.ResourceSendRequest: |
| case recordTypes.ResourceReceiveResponse: |
| case recordTypes.ResourceReceivedData: |
| case recordTypes.ResourceFinish: |
| url = timelineData.url; |
| if (url) |
| contentHelper.appendElementRow(Common.UIString('Resource'), Components.Linkifier.linkifyURL(url)); |
| if (eventData['requestMethod']) |
| contentHelper.appendTextRow(Common.UIString('Request Method'), eventData['requestMethod']); |
| if (typeof eventData['statusCode'] === 'number') |
| contentHelper.appendTextRow(Common.UIString('Status Code'), eventData['statusCode']); |
| if (eventData['mimeType']) |
| contentHelper.appendTextRow(Common.UIString('MIME Type'), eventData['mimeType']); |
| if ('priority' in eventData) { |
| const priority = PerfUI.uiLabelForNetworkPriority(eventData['priority']); |
| contentHelper.appendTextRow(Common.UIString('Priority'), priority); |
| } |
| if (eventData['encodedDataLength']) { |
| contentHelper.appendTextRow( |
| Common.UIString('Encoded Data'), Common.UIString('%d Bytes', eventData['encodedDataLength'])); |
| } |
| if (eventData['decodedBodyLength']) { |
| contentHelper.appendTextRow( |
| Common.UIString('Decoded Body'), Common.UIString('%d Bytes', eventData['decodedBodyLength'])); |
| } |
| break; |
| case recordTypes.CompileModule: |
| contentHelper.appendLocationRow(Common.UIString('Module'), event.args['fileName'], 0); |
| break; |
| case recordTypes.CompileScript: |
| url = eventData && eventData['url']; |
| if (url) { |
| contentHelper.appendLocationRow( |
| Common.UIString('Script'), url, eventData['lineNumber'], eventData['columnNumber']); |
| } |
| contentHelper.appendTextRow(Common.UIString('Streamed'), Common.UIString('%s', eventData['streamed'])); |
| const cacheProduceOptions = eventData && eventData['cacheProduceOptions']; |
| if (cacheProduceOptions) { |
| contentHelper.appendTextRow( |
| Common.UIString('Cache Produce Options'), Common.UIString('%s', cacheProduceOptions)); |
| contentHelper.appendTextRow( |
| Common.UIString('Produced Cache Size'), Common.UIString('%d', eventData['producedCacheSize'])); |
| } |
| const cacheConsumeOptions = eventData && eventData['cacheConsumeOptions']; |
| if (cacheConsumeOptions) { |
| contentHelper.appendTextRow( |
| Common.UIString('Cache Consume Options'), Common.UIString('%s', cacheConsumeOptions)); |
| contentHelper.appendTextRow( |
| Common.UIString('Consumed Cache Size'), Common.UIString('%d', eventData['consumedCacheSize'])); |
| contentHelper.appendTextRow( |
| Common.UIString('Cache Rejected'), Common.UIString('%s', eventData['cacheRejected'])); |
| } |
| break; |
| case recordTypes.EvaluateScript: |
| url = eventData && eventData['url']; |
| if (url) { |
| contentHelper.appendLocationRow( |
| Common.UIString('Script'), url, eventData['lineNumber'], eventData['columnNumber']); |
| } |
| break; |
| case recordTypes.Paint: |
| const clip = eventData['clip']; |
| contentHelper.appendTextRow(Common.UIString('Location'), Common.UIString('(%d, %d)', clip[0], clip[1])); |
| const clipWidth = Timeline.TimelineUIUtils.quadWidth(clip); |
| const clipHeight = Timeline.TimelineUIUtils.quadHeight(clip); |
| contentHelper.appendTextRow(Common.UIString('Dimensions'), Common.UIString('%d × %d', clipWidth, clipHeight)); |
| // Fall-through intended. |
| |
| case recordTypes.PaintSetup: |
| case recordTypes.Rasterize: |
| case recordTypes.ScrollLayer: |
| relatedNodeLabel = Common.UIString('Layer Root'); |
| break; |
| case recordTypes.PaintImage: |
| case recordTypes.DecodeLazyPixelRef: |
| case recordTypes.DecodeImage: |
| case recordTypes.ResizeImage: |
| case recordTypes.DrawLazyPixelRef: |
| relatedNodeLabel = Common.UIString('Owner Element'); |
| url = timelineData.url; |
| if (url) |
| contentHelper.appendElementRow(Common.UIString('Image URL'), Components.Linkifier.linkifyURL(url)); |
| break; |
| case recordTypes.ParseAuthorStyleSheet: |
| url = eventData['styleSheetUrl']; |
| if (url) |
| contentHelper.appendElementRow(Common.UIString('Stylesheet URL'), Components.Linkifier.linkifyURL(url)); |
| break; |
| case recordTypes.UpdateLayoutTree: // We don't want to see default details. |
| case recordTypes.RecalculateStyles: |
| contentHelper.appendTextRow(Common.UIString('Elements Affected'), event.args['elementCount']); |
| break; |
| case recordTypes.Layout: |
| const beginData = event.args['beginData']; |
| contentHelper.appendTextRow( |
| Common.UIString('Nodes That Need Layout'), |
| Common.UIString('%s of %s', beginData['dirtyObjects'], beginData['totalObjects'])); |
| relatedNodeLabel = Common.UIString('Layout root'); |
| break; |
| case recordTypes.ConsoleTime: |
| contentHelper.appendTextRow(Common.UIString('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(Common.UIString('URL'), initiatorData['webSocketURL']); |
| if (typeof initiatorData['webSocketProtocol'] !== 'undefined') |
| contentHelper.appendTextRow(Common.UIString('WebSocket Protocol'), initiatorData['webSocketProtocol']); |
| if (typeof eventData['message'] !== 'undefined') |
| contentHelper.appendTextRow(Common.UIString('Message'), eventData['message']); |
| break; |
| case recordTypes.EmbedderCallback: |
| contentHelper.appendTextRow(Common.UIString('Callback Function'), eventData['callbackName']); |
| break; |
| case recordTypes.Animation: |
| if (event.phase === SDK.TracingModel.Phase.NestableAsyncInstant) |
| contentHelper.appendTextRow(Common.UIString('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(Common.UIString('Range'), url, startLine, endLine); |
| break; |
| } |
| |
| case recordTypes.FireIdleCallback: |
| contentHelper.appendTextRow( |
| Common.UIString('Allotted Time'), Number.millisToString(eventData['allottedMilliseconds'])); |
| contentHelper.appendTextRow(Common.UIString('Invoked by Timeout'), eventData['timedOut']); |
| // Fall-through intended. |
| |
| case recordTypes.RequestIdleCallback: |
| case recordTypes.CancelIdleCallback: |
| contentHelper.appendTextRow(Common.UIString('Callback ID'), eventData['id']); |
| break; |
| case recordTypes.EventDispatch: |
| contentHelper.appendTextRow(Common.UIString('Type'), eventData['type']); |
| break; |
| |
| default: { |
| const detailsNode = |
| Timeline.TimelineUIUtils.buildDetailsNodeForTraceEvent(event, model.targetByEvent(event), linkifier); |
| if (detailsNode) |
| contentHelper.appendElementRow(Common.UIString('Details'), detailsNode); |
| break; |
| } |
| } |
| |
| Timeline.TimelineUIUtils._maybeAppendProductToDetails( |
| contentHelper, badgePool, url || eventData && eventData['url']); |
| |
| if (timelineData.timeWaitingForMainThread) { |
| contentHelper.appendTextRow( |
| Common.UIString('Time Waiting for Main Thread'), |
| Number.millisToString(timelineData.timeWaitingForMainThread, true)); |
| } |
| |
| const relatedNode = relatedNodesMap && relatedNodesMap.get(timelineData.backendNodeId); |
| if (relatedNode) { |
| const nodeSpan = await Common.Linkifier.linkify(relatedNode); |
| contentHelper.appendElementRow(relatedNodeLabel || Common.UIString('Related Node'), nodeSpan); |
| } |
| |
| if (event[Timeline.TimelineUIUtils._previewElementSymbol]) { |
| contentHelper.addSection(Common.UIString('Preview')); |
| contentHelper.appendElementRow('', event[Timeline.TimelineUIUtils._previewElementSymbol]); |
| } |
| |
| if (initiator || timelineData.stackTraceForSelfOrInitiator() || |
| TimelineModel.InvalidationTracker.invalidationEventsFor(event)) |
| Timeline.TimelineUIUtils._generateCauses(event, model.targetByEvent(event), relatedNodesMap, contentHelper); |
| |
| const stats = {}; |
| const showPieChart = detailed && Timeline.TimelineUIUtils._aggregatedStatsForTraceEvent(stats, model, event); |
| if (showPieChart) { |
| contentHelper.addSection(Common.UIString('Aggregated Time')); |
| const pieChart = Timeline.TimelineUIUtils.generatePieChart( |
| stats, Timeline.TimelineUIUtils.eventStyle(event).category, event.selfTime); |
| contentHelper.appendElementRow('', pieChart); |
| } |
| |
| return contentHelper.fragment; |
| } |
| |
| /** |
| * @param {!Timeline.TimelineDetailsContentHelper} contentHelper |
| * @param {!ProductRegistry.BadgePool} badgePool |
| * @param {?string} url |
| */ |
| static _maybeAppendProductToDetails(contentHelper, badgePool, url) { |
| const parsedURL = url ? url.asParsedURL() : null; |
| if (parsedURL) |
| contentHelper.appendElementRow('', badgePool.badgeForURL(parsedURL)); |
| } |
| |
| /** |
| * @param {!Array<!SDK.TracingModel.Event>} events |
| * @param {number} startTime |
| * @param {number} endTime |
| * @return {!Object<string, number>} |
| */ |
| static statsForTimeRange(events, startTime, endTime) { |
| if (!events.length) |
| return {'idle': endTime - startTime}; |
| const symbol = Timeline.TimelineUIUtils._categoryBreakdownCacheSymbol; |
| Timeline.TimelineUIUtils._buildRangeStatsCacheIfNeeded(events); |
| |
| const before = findCachedStatsAfterTime(startTime); |
| const statsBefore = |
| subtractStats(before.stats, Timeline.TimelineUIUtils._slowStatsForTimeRange(events, startTime, before.time)); |
| |
| const after = findCachedStatsAfterTime(endTime); |
| const statsAfter = |
| subtractStats(after.stats, Timeline.TimelineUIUtils._slowStatsForTimeRange(events, endTime, after.time)); |
| |
| const aggregatedStats = subtractStats(statsAfter, statsBefore); |
| const aggregatedTotal = Object.values(aggregatedStats).reduce((a, b) => a + b, 0); |
| aggregatedStats['idle'] = Math.max(0, endTime - startTime - aggregatedTotal); |
| return aggregatedStats; |
| |
| /** |
| * @param {number} atTime |
| * @return {!{time: number, stats: !Object<string, number>}} |
| */ |
| function findCachedStatsAfterTime(atTime) { |
| let index = events.lowerBound(atTime, (time, event) => time - (event.endTime || event.startTime)); |
| while (index < events.length && !events[index][symbol]) |
| index++; |
| if (index === events.length) { |
| const lastEvent = events.peekLast(); |
| return {time: lastEvent.endTime || lastEvent.startTime, stats: events[symbol]}; |
| } |
| const event = events[index]; |
| return {time: event.endTime || event.startTime, stats: event[symbol]}; |
| } |
| |
| /** |
| * @param {!Object<string, number>} a |
| * @param {!Object<string, number>} b |
| * @return {!Object<string, number>} |
| */ |
| function subtractStats(a, b) { |
| const result = Object.assign({}, a); |
| for (const key in b) |
| result[key] -= b[key]; |
| return result; |
| } |
| } |
| |
| /** |
| * @param {!Array<!SDK.TracingModel.Event>} events |
| * @param {number} startTime |
| * @param {number} endTime |
| */ |
| static _slowStatsForTimeRange(events, startTime, endTime) { |
| /** @type {!Object<string, number>} */ |
| const stats = {}; |
| const ownTimes = []; |
| |
| TimelineModel.TimelineModel.forEachEvent( |
| events, onStartEvent, onEndEvent, undefined, startTime, endTime, Timeline.TimelineUIUtils._filterForStats()); |
| |
| /** |
| * @param {!SDK.TracingModel.Event} e |
| */ |
| function onStartEvent(e) { |
| const duration = Math.min(e.endTime, endTime) - Math.max(e.startTime, startTime); |
| if (ownTimes.length) |
| ownTimes[ownTimes.length - 1] -= duration; |
| ownTimes.push(duration); |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} e |
| */ |
| function onEndEvent(e) { |
| const category = Timeline.TimelineUIUtils.eventStyle(e).category.name; |
| stats[category] = (stats[category] || 0) + ownTimes.pop(); |
| } |
| return stats; |
| } |
| |
| /** |
| * @return {function(!SDK.TracingModel.Event):boolean} |
| */ |
| static _filterForStats() { |
| const visibleEventsFilter = Timeline.TimelineUIUtils.visibleEventsFilter(); |
| return event => visibleEventsFilter.accept(event) || SDK.TracingModel.isTopLevelEvent(event); |
| } |
| |
| /** |
| * @param {!Array<!SDK.TracingModel.Event>} events |
| */ |
| static _buildRangeStatsCacheIfNeeded(events) { |
| if (events[Timeline.TimelineUIUtils._categoryBreakdownCacheSymbol]) |
| return; |
| |
| const aggregatedStats = {}; |
| const ownTimes = []; |
| TimelineModel.TimelineModel.forEachEvent( |
| events, onStartEvent, onEndEvent, undefined, undefined, undefined, Timeline.TimelineUIUtils._filterForStats()); |
| |
| /** |
| * @param {!SDK.TracingModel.Event} e |
| */ |
| function onStartEvent(e) { |
| if (ownTimes.length) |
| ownTimes[ownTimes.length - 1] -= e.duration; |
| ownTimes.push(e.duration); |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} e |
| */ |
| function onEndEvent(e) { |
| const category = Timeline.TimelineUIUtils.eventStyle(e).category.name; |
| aggregatedStats[category] = (aggregatedStats[category] || 0) + ownTimes.pop(); |
| if (!ownTimes.length) |
| e[Timeline.TimelineUIUtils._categoryBreakdownCacheSymbol] = Object.assign({}, aggregatedStats); |
| } |
| |
| const obj = /** @type {!Object} */ (events); |
| obj[Timeline.TimelineUIUtils._categoryBreakdownCacheSymbol] = Object.assign({}, aggregatedStats); |
| } |
| |
| /** |
| * @param {!TimelineModel.TimelineModel.NetworkRequest} request |
| * @param {!TimelineModel.TimelineModel} model |
| * @param {!Components.Linkifier} linkifier |
| * @param {!ProductRegistry.BadgePool} badgePool |
| * @return {!Promise<!DocumentFragment>} |
| */ |
| static async buildNetworkRequestDetails(request, model, linkifier, badgePool) { |
| const target = model.targetByEvent(request.children[0]); |
| const contentHelper = new Timeline.TimelineDetailsContentHelper(target, linkifier); |
| const category = Timeline.TimelineUIUtils.networkRequestCategory(request); |
| const color = Timeline.TimelineUIUtils.networkCategoryColor(category); |
| contentHelper.addSection(Common.UIString('Network request'), color); |
| |
| const duration = request.endTime - (request.startTime || -Infinity); |
| if (request.url) |
| contentHelper.appendElementRow(Common.UIString('URL'), Components.Linkifier.linkifyURL(request.url)); |
| Timeline.TimelineUIUtils._maybeAppendProductToDetails(contentHelper, badgePool, request.url); |
| if (isFinite(duration)) |
| contentHelper.appendTextRow(Common.UIString('Duration'), Number.millisToString(duration, true)); |
| if (request.requestMethod) |
| contentHelper.appendTextRow(Common.UIString('Request Method'), request.requestMethod); |
| if (typeof request.priority === 'string') { |
| const priority = |
| PerfUI.uiLabelForNetworkPriority(/** @type {!Protocol.Network.ResourcePriority} */ (request.priority)); |
| contentHelper.appendTextRow(Common.UIString('Priority'), priority); |
| } |
| if (request.mimeType) |
| contentHelper.appendTextRow(Common.UIString('Mime Type'), request.mimeType); |
| let lengthText = ''; |
| if (request.fromCache) |
| lengthText += Common.UIString(' (from cache)'); |
| if (request.fromServiceWorker) |
| lengthText += Common.UIString(' (from service worker)'); |
| if (request.encodedDataLength || !lengthText) |
| lengthText = `${Number.bytesToString(request.encodedDataLength)}${lengthText}`; |
| contentHelper.appendTextRow(Common.UIString('Encoded Data'), lengthText); |
| if (request.decodedBodyLength) |
| contentHelper.appendTextRow(Common.UIString('Decoded Body'), Number.bytesToString(request.decodedBodyLength)); |
| const title = Common.UIString('Initiator'); |
| const sendRequest = request.children[0]; |
| const topFrame = TimelineModel.TimelineData.forEvent(sendRequest).topFrame(); |
| if (topFrame) { |
| const link = linkifier.maybeLinkifyConsoleCallFrame(target, topFrame); |
| if (link) |
| contentHelper.appendElementRow(title, link); |
| } else { |
| const initiator = TimelineModel.TimelineData.forEvent(sendRequest).initiator(); |
| if (initiator) { |
| const initiatorURL = TimelineModel.TimelineData.forEvent(initiator).url; |
| if (initiatorURL) { |
| const link = linkifier.maybeLinkifyScriptLocation(target, null, initiatorURL, 0); |
| if (link) |
| contentHelper.appendElementRow(title, link); |
| } |
| } |
| } |
| |
| if (!request.previewElement && request.url && target) |
| request.previewElement = await BrowserComponents.ImagePreview.build(target, request.url, false); |
| if (request.previewElement) |
| contentHelper.appendElementRow(Common.UIString('Preview'), request.previewElement); |
| return contentHelper.fragment; |
| } |
| |
| /** |
| * @param {!Array<!Protocol.Runtime.CallFrame>} callFrames |
| * @return {!Protocol.Runtime.StackTrace} |
| */ |
| static _stackTraceFromCallFrames(callFrames) { |
| return /** @type {!Protocol.Runtime.StackTrace} */ ({callFrames: callFrames}); |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @param {?SDK.Target} target |
| * @param {?Map<number, ?SDK.DOMNode>} relatedNodesMap |
| * @param {!Timeline.TimelineDetailsContentHelper} contentHelper |
| */ |
| static _generateCauses(event, target, relatedNodesMap, contentHelper) { |
| const recordTypes = TimelineModel.TimelineModel.RecordType; |
| |
| let callSiteStackLabel; |
| let stackLabel; |
| |
| switch (event.name) { |
| case recordTypes.TimerFire: |
| callSiteStackLabel = Common.UIString('Timer Installed'); |
| break; |
| case recordTypes.FireAnimationFrame: |
| callSiteStackLabel = Common.UIString('Animation Frame Requested'); |
| break; |
| case recordTypes.FireIdleCallback: |
| callSiteStackLabel = Common.UIString('Idle Callback Requested'); |
| break; |
| case recordTypes.UpdateLayoutTree: |
| case recordTypes.RecalculateStyles: |
| stackLabel = Common.UIString('Recalculation Forced'); |
| break; |
| case recordTypes.Layout: |
| callSiteStackLabel = Common.UIString('First Layout Invalidation'); |
| stackLabel = Common.UIString('Layout Forced'); |
| break; |
| } |
| |
| const timelineData = TimelineModel.TimelineData.forEvent(event); |
| // Direct cause. |
| if (timelineData.stackTrace && timelineData.stackTrace.length) { |
| contentHelper.addSection(Common.UIString('Call Stacks')); |
| contentHelper.appendStackTrace( |
| stackLabel || Common.UIString('Stack Trace'), |
| Timeline.TimelineUIUtils._stackTraceFromCallFrames(timelineData.stackTrace)); |
| } |
| |
| const initiator = TimelineModel.TimelineData.forEvent(event).initiator(); |
| // Indirect causes. |
| if (TimelineModel.InvalidationTracker.invalidationEventsFor(event) && target) { |
| // Full invalidation tracking (experimental). |
| contentHelper.addSection(Common.UIString('Invalidations')); |
| Timeline.TimelineUIUtils._generateInvalidations(event, target, relatedNodesMap, contentHelper); |
| } else if (initiator) { // Partial invalidation tracking. |
| const delay = event.startTime - initiator.startTime; |
| contentHelper.appendTextRow(Common.UIString('Pending for'), Number.preciseMillisToString(delay, 1)); |
| |
| const link = createElementWithClass('span', 'devtools-link'); |
| link.textContent = Common.UIString('reveal'); |
| link.addEventListener('click', () => { |
| Timeline.TimelinePanel.instance().select( |
| Timeline.TimelineSelection.fromTraceEvent(/** @type {!SDK.TracingModel.Event} */ (initiator))); |
| }); |
| contentHelper.appendElementRow(Common.UIString('Initiator'), link); |
| |
| const initiatorStackTrace = TimelineModel.TimelineData.forEvent(initiator).stackTrace; |
| if (initiatorStackTrace) { |
| contentHelper.appendStackTrace( |
| callSiteStackLabel || Common.UIString('First Invalidated'), |
| Timeline.TimelineUIUtils._stackTraceFromCallFrames(initiatorStackTrace)); |
| } |
| } |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @param {!SDK.Target} target |
| * @param {?Map<number, ?SDK.DOMNode>} relatedNodesMap |
| * @param {!Timeline.TimelineDetailsContentHelper} contentHelper |
| */ |
| static _generateInvalidations(event, target, relatedNodesMap, contentHelper) { |
| const invalidationTrackingEvents = TimelineModel.InvalidationTracker.invalidationEventsFor(event); |
| const invalidations = {}; |
| invalidationTrackingEvents.forEach(function(invalidation) { |
| if (!invalidations[invalidation.type]) |
| invalidations[invalidation.type] = [invalidation]; |
| else |
| invalidations[invalidation.type].push(invalidation); |
| }); |
| |
| Object.keys(invalidations).forEach(function(type) { |
| Timeline.TimelineUIUtils._generateInvalidationsForType( |
| type, target, invalidations[type], relatedNodesMap, contentHelper); |
| }); |
| } |
| |
| /** |
| * @param {string} type |
| * @param {!SDK.Target} target |
| * @param {!Array.<!TimelineModel.InvalidationTrackingEvent>} invalidations |
| * @param {?Map<number, ?SDK.DOMNode>} relatedNodesMap |
| * @param {!Timeline.TimelineDetailsContentHelper} contentHelper |
| */ |
| static _generateInvalidationsForType(type, target, invalidations, relatedNodesMap, contentHelper) { |
| let title; |
| switch (type) { |
| case TimelineModel.TimelineModel.RecordType.StyleRecalcInvalidationTracking: |
| title = Common.UIString('Style Invalidations'); |
| break; |
| case TimelineModel.TimelineModel.RecordType.LayoutInvalidationTracking: |
| title = Common.UIString('Layout Invalidations'); |
| break; |
| default: |
| title = Common.UIString('Other Invalidations'); |
| break; |
| } |
| |
| const invalidationsTreeOutline = new UI.TreeOutlineInShadow(); |
| invalidationsTreeOutline.registerRequiredCSS('timeline/invalidationsTree.css'); |
| invalidationsTreeOutline.element.classList.add('invalidations-tree'); |
| |
| const invalidationGroups = groupInvalidationsByCause(invalidations); |
| invalidationGroups.forEach(function(group) { |
| const groupElement = |
| new Timeline.TimelineUIUtils.InvalidationsGroupElement(target, relatedNodesMap, contentHelper, group); |
| invalidationsTreeOutline.appendChild(groupElement); |
| }); |
| contentHelper.appendElementRow(title, invalidationsTreeOutline.element, false, true); |
| |
| /** |
| * @param {!Array<!TimelineModel.InvalidationTrackingEvent>} invalidations |
| * @return {!Array<!Array<!TimelineModel.InvalidationTrackingEvent>>} |
| */ |
| function groupInvalidationsByCause(invalidations) { |
| /** @type {!Map<string, !Array<!TimelineModel.InvalidationTrackingEvent>>} */ |
| const causeToInvalidationMap = new Map(); |
| for (let index = 0; index < invalidations.length; index++) { |
| const invalidation = invalidations[index]; |
| let causeKey = ''; |
| if (invalidation.cause.reason) |
| causeKey += invalidation.cause.reason + '.'; |
| if (invalidation.cause.stackTrace) { |
| invalidation.cause.stackTrace.forEach(function(stackFrame) { |
| causeKey += stackFrame['functionName'] + '.'; |
| causeKey += stackFrame['scriptId'] + '.'; |
| causeKey += stackFrame['url'] + '.'; |
| causeKey += stackFrame['lineNumber'] + '.'; |
| causeKey += stackFrame['columnNumber'] + '.'; |
| }); |
| } |
| |
| if (causeToInvalidationMap.has(causeKey)) |
| causeToInvalidationMap.get(causeKey).push(invalidation); |
| else |
| causeToInvalidationMap.set(causeKey, [invalidation]); |
| } |
| return causeToInvalidationMap.valuesArray(); |
| } |
| } |
| |
| /** |
| * @param {!Set<number>} nodeIds |
| * @param {!Array<!TimelineModel.InvalidationTrackingEvent>} invalidations |
| */ |
| static _collectInvalidationNodeIds(nodeIds, invalidations) { |
| nodeIds.addAll(invalidations.map(invalidation => invalidation.nodeId).filter(id => id)); |
| } |
| |
| /** |
| * @param {!Object} total |
| * @param {!TimelineModel.TimelineModel} model |
| * @param {!SDK.TracingModel.Event} event |
| * @return {boolean} |
| */ |
| static _aggregatedStatsForTraceEvent(total, model, event) { |
| const events = model.inspectedTargetEvents(); |
| /** |
| * @param {number} startTime |
| * @param {!SDK.TracingModel.Event} e |
| * @return {number} |
| */ |
| function eventComparator(startTime, e) { |
| return startTime - e.startTime; |
| } |
| const index = events.binaryIndexOf(event.startTime, eventComparator); |
| // Not a main thread event? |
| if (index < 0) |
| return false; |
| let hasChildren = false; |
| const endTime = event.endTime; |
| if (endTime) { |
| for (let i = index; i < events.length; i++) { |
| const nextEvent = events[i]; |
| if (nextEvent.startTime >= endTime) |
| break; |
| if (!nextEvent.selfTime) |
| continue; |
| if (nextEvent.thread !== event.thread) |
| continue; |
| if (i > index) |
| hasChildren = true; |
| const categoryName = Timeline.TimelineUIUtils.eventStyle(nextEvent).category.name; |
| total[categoryName] = (total[categoryName] || 0) + nextEvent.selfTime; |
| } |
| } |
| if (SDK.TracingModel.isAsyncPhase(event.phase)) { |
| if (event.endTime) { |
| let aggregatedTotal = 0; |
| for (const categoryName in total) |
| aggregatedTotal += total[categoryName]; |
| total['idle'] = Math.max(0, event.endTime - event.startTime - aggregatedTotal); |
| } |
| return false; |
| } |
| return hasChildren; |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @param {!SDK.Target} target |
| * @return {!Promise<?Element>} |
| */ |
| static async buildPicturePreviewContent(event, target) { |
| const snapshotWithRect = await new TimelineModel.LayerPaintEvent(event, target).snapshotPromise(); |
| if (!snapshotWithRect) |
| return null; |
| const imageURLPromise = snapshotWithRect.snapshot.replay(); |
| snapshotWithRect.snapshot.release(); |
| const imageURL = await imageURLPromise; |
| if (!imageURL) |
| return null; |
| const container = createElement('div'); |
| UI.appendStyle(container, 'browser_components/imagePreview.css'); |
| container.classList.add('image-preview-container', 'vbox', 'link'); |
| const img = container.createChild('img'); |
| img.src = imageURL; |
| const paintProfilerButton = container.createChild('a'); |
| paintProfilerButton.textContent = Common.UIString('Paint Profiler'); |
| container.addEventListener( |
| 'click', () => Timeline.TimelinePanel.instance().select(Timeline.TimelineSelection.fromTraceEvent(event)), |
| false); |
| return container; |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @param {number} zeroTime |
| * @return {!Element} |
| */ |
| static createEventDivider(event, zeroTime) { |
| const eventDivider = createElementWithClass('div', 'resources-event-divider'); |
| const startTime = Number.millisToString(event.startTime - zeroTime); |
| eventDivider.title = Common.UIString('%s at %s', Timeline.TimelineUIUtils.eventTitle(event), startTime); |
| const style = Timeline.TimelineUIUtils.markerStyleForEvent(event); |
| if (style.tall) |
| eventDivider.style.backgroundColor = style.color; |
| return eventDivider; |
| } |
| |
| /** |
| * @return {!Array.<string>} |
| */ |
| static _visibleTypes() { |
| const eventStyles = Timeline.TimelineUIUtils._initEventStyles(); |
| const result = []; |
| for (const name in eventStyles) { |
| if (!eventStyles[name].hidden) |
| result.push(name); |
| } |
| return result; |
| } |
| |
| /** |
| * @return {!TimelineModel.TimelineModelFilter} |
| */ |
| static visibleEventsFilter() { |
| return new TimelineModel.TimelineVisibleEventsFilter(Timeline.TimelineUIUtils._visibleTypes()); |
| } |
| |
| /** |
| * @return {!TimelineModel.TimelineModelFilter} |
| */ |
| static paintEventsFilter() { |
| const recordTypes = TimelineModel.TimelineModel.RecordType; |
| return new TimelineModel.TimelineInvisibleEventsFilter( |
| !Runtime.experiments.isEnabled('timelinePaintTimingMarkers') ? |
| [recordTypes.MarkFCP, recordTypes.MarkFMP, recordTypes.MarkFMPCandidate] : |
| []); |
| } |
| |
| /** |
| * @return {!Object.<string, !Timeline.TimelineCategory>} |
| */ |
| static categories() { |
| if (Timeline.TimelineUIUtils._categories) |
| return Timeline.TimelineUIUtils._categories; |
| Timeline.TimelineUIUtils._categories = { |
| loading: new Timeline.TimelineCategory( |
| 'loading', Common.UIString('Loading'), true, 'hsl(214, 67%, 74%)', 'hsl(214, 67%, 66%)'), |
| scripting: new Timeline.TimelineCategory( |
| 'scripting', Common.UIString('Scripting'), true, 'hsl(43, 83%, 72%)', 'hsl(43, 83%, 64%) '), |
| rendering: new Timeline.TimelineCategory( |
| 'rendering', Common.UIString('Rendering'), true, 'hsl(256, 67%, 76%)', 'hsl(256, 67%, 70%)'), |
| painting: new Timeline.TimelineCategory( |
| 'painting', Common.UIString('Painting'), true, 'hsl(109, 33%, 64%)', 'hsl(109, 33%, 55%)'), |
| gpu: new Timeline.TimelineCategory( |
| 'gpu', Common.UIString('GPU'), false, 'hsl(109, 33%, 64%)', 'hsl(109, 33%, 55%)'), |
| async: new Timeline.TimelineCategory( |
| 'async', Common.UIString('Async'), false, 'hsl(0, 100%, 50%)', 'hsl(0, 100%, 40%)'), |
| other: |
| new Timeline.TimelineCategory('other', Common.UIString('Other'), false, 'hsl(0, 0%, 87%)', 'hsl(0, 0%, 79%)'), |
| idle: new Timeline.TimelineCategory('idle', Common.UIString('Idle'), false, 'hsl(0, 0%, 98%)', 'hsl(0, 0%, 98%)') |
| }; |
| return Timeline.TimelineUIUtils._categories; |
| } |
| |
| /** |
| * @param {!Object} aggregatedStats |
| * @param {!Timeline.TimelineCategory=} selfCategory |
| * @param {number=} selfTime |
| * @return {!Element} |
| */ |
| static generatePieChart(aggregatedStats, selfCategory, selfTime) { |
| let total = 0; |
| for (const categoryName in aggregatedStats) |
| total += aggregatedStats[categoryName]; |
| |
| const element = createElementWithClass('div', 'timeline-details-view-pie-chart-wrapper hbox'); |
| const pieChart = new PerfUI.PieChart(110, value => Number.preciseMillisToString(value), true); |
| pieChart.element.classList.add('timeline-details-view-pie-chart'); |
| pieChart.setTotal(total); |
| const pieChartContainer = element.createChild('div', 'vbox'); |
| pieChartContainer.appendChild(pieChart.element); |
| const footerElement = element.createChild('div', 'timeline-aggregated-info-legend'); |
| |
| /** |
| * @param {string} name |
| * @param {string} title |
| * @param {number} value |
| * @param {string} color |
| */ |
| function appendLegendRow(name, title, value, color) { |
| if (!value) |
| return; |
| pieChart.addSlice(value, color); |
| const rowElement = footerElement.createChild('div'); |
| rowElement.createChild('span', 'timeline-aggregated-legend-value').textContent = |
| Number.preciseMillisToString(value, 1); |
| rowElement.createChild('span', 'timeline-aggregated-legend-swatch').style.backgroundColor = color; |
| rowElement.createChild('span', 'timeline-aggregated-legend-title').textContent = title; |
| } |
| |
| // In case of self time, first add self, then children of the same category. |
| if (selfCategory) { |
| if (selfTime) { |
| appendLegendRow( |
| selfCategory.name, Common.UIString('%s (self)', selfCategory.title), selfTime, selfCategory.color); |
| } |
| // Children of the same category. |
| const categoryTime = aggregatedStats[selfCategory.name]; |
| const value = categoryTime - selfTime; |
| if (value > 0) { |
| appendLegendRow( |
| selfCategory.name, Common.UIString('%s (children)', selfCategory.title), value, selfCategory.childColor); |
| } |
| } |
| |
| // Add other categories. |
| for (const categoryName in Timeline.TimelineUIUtils.categories()) { |
| const category = Timeline.TimelineUIUtils.categories()[categoryName]; |
| if (category === selfCategory) |
| continue; |
| appendLegendRow(category.name, category.title, aggregatedStats[category.name], category.childColor); |
| } |
| return element; |
| } |
| |
| /** |
| * @param {!TimelineModel.TimelineFrame} frame |
| * @param {?SDK.FilmStripModel.Frame} filmStripFrame |
| * @return {!Element} |
| */ |
| static generateDetailsContentForFrame(frame, filmStripFrame) { |
| const contentHelper = new Timeline.TimelineDetailsContentHelper(null, null); |
| contentHelper.addSection(Common.UIString('Frame')); |
| |
| const duration = Timeline.TimelineUIUtils.frameDuration(frame); |
| contentHelper.appendElementRow(Common.UIString('Duration'), duration, frame.hasWarnings()); |
| const durationInMillis = frame.endTime - frame.startTime; |
| contentHelper.appendTextRow(Common.UIString('FPS'), Math.floor(1000 / durationInMillis)); |
| contentHelper.appendTextRow(Common.UIString('CPU time'), Number.millisToString(frame.cpuTime, true)); |
| if (filmStripFrame) { |
| const filmStripPreview = createElementWithClass('div', 'timeline-filmstrip-preview'); |
| filmStripFrame.imageDataPromise() |
| .then(data => UI.loadImageFromData(data)) |
| .then(image => image && filmStripPreview.appendChild(image)); |
| contentHelper.appendElementRow('', filmStripPreview); |
| filmStripPreview.addEventListener('click', frameClicked.bind(null, filmStripFrame), false); |
| } |
| |
| if (frame.layerTree) { |
| contentHelper.appendElementRow( |
| Common.UIString('Layer tree'), |
| Components.Linkifier.linkifyRevealable(frame.layerTree, Common.UIString('show'))); |
| } |
| |
| /** |
| * @param {!SDK.FilmStripModel.Frame} filmStripFrame |
| */ |
| function frameClicked(filmStripFrame) { |
| new PerfUI.FilmStripView.Dialog(filmStripFrame, 0); |
| } |
| |
| return contentHelper.fragment; |
| } |
| |
| /** |
| * @param {!TimelineModel.TimelineFrame} frame |
| * @return {!Element} |
| */ |
| static frameDuration(frame) { |
| const durationText = Common.UIString( |
| '%s (at %s)', Number.millisToString(frame.endTime - frame.startTime, true), |
| Number.millisToString(frame.startTimeOffset, true)); |
| const element = createElement('span'); |
| element.createTextChild(durationText); |
| if (!frame.hasWarnings()) |
| return element; |
| element.createTextChild(Common.UIString('. Long frame times are an indication of ')); |
| element.appendChild(UI.XLink.create( |
| 'https://developers.google.com/web/fundamentals/performance/rendering/', Common.UIString('jank'))); |
| element.createTextChild('.'); |
| return element; |
| } |
| |
| /** |
| * @param {!CanvasRenderingContext2D} context |
| * @param {number} width |
| * @param {number} height |
| * @param {string} color0 |
| * @param {string} color1 |
| * @param {string} color2 |
| * @return {!CanvasGradient} |
| */ |
| static createFillStyle(context, width, height, color0, color1, color2) { |
| const gradient = context.createLinearGradient(0, 0, width, height); |
| gradient.addColorStop(0, color0); |
| gradient.addColorStop(0.25, color1); |
| gradient.addColorStop(0.75, color1); |
| gradient.addColorStop(1, color2); |
| return gradient; |
| } |
| |
| /** |
| * @param {!Array.<number>} quad |
| * @return {number} |
| */ |
| static quadWidth(quad) { |
| return Math.round(Math.sqrt(Math.pow(quad[0] - quad[2], 2) + Math.pow(quad[1] - quad[3], 2))); |
| } |
| |
| /** |
| * @param {!Array.<number>} quad |
| * @return {number} |
| */ |
| static quadHeight(quad) { |
| return Math.round(Math.sqrt(Math.pow(quad[0] - quad[6], 2) + Math.pow(quad[1] - quad[7], 2))); |
| } |
| |
| /** |
| * @return {!Array.<!Timeline.TimelineUIUtils.EventDispatchTypeDescriptor>} |
| */ |
| static eventDispatchDesciptors() { |
| if (Timeline.TimelineUIUtils._eventDispatchDesciptors) |
| return Timeline.TimelineUIUtils._eventDispatchDesciptors; |
| const lightOrange = 'hsl(40,100%,80%)'; |
| const orange = 'hsl(40,100%,50%)'; |
| const green = 'hsl(90,100%,40%)'; |
| const purple = 'hsl(256,100%,75%)'; |
| Timeline.TimelineUIUtils._eventDispatchDesciptors = [ |
| new Timeline.TimelineUIUtils.EventDispatchTypeDescriptor( |
| 1, lightOrange, ['mousemove', 'mouseenter', 'mouseleave', 'mouseout', 'mouseover']), |
| new Timeline.TimelineUIUtils.EventDispatchTypeDescriptor( |
| 1, lightOrange, ['pointerover', 'pointerout', 'pointerenter', 'pointerleave', 'pointermove']), |
| new Timeline.TimelineUIUtils.EventDispatchTypeDescriptor(2, green, ['wheel']), |
| new Timeline.TimelineUIUtils.EventDispatchTypeDescriptor(3, orange, ['click', 'mousedown', 'mouseup']), |
| new Timeline.TimelineUIUtils.EventDispatchTypeDescriptor( |
| 3, orange, ['touchstart', 'touchend', 'touchmove', 'touchcancel']), |
| new Timeline.TimelineUIUtils.EventDispatchTypeDescriptor( |
| 3, orange, ['pointerdown', 'pointerup', 'pointercancel', 'gotpointercapture', 'lostpointercapture']), |
| new Timeline.TimelineUIUtils.EventDispatchTypeDescriptor(3, purple, ['keydown', 'keyup', 'keypress']) |
| ]; |
| return Timeline.TimelineUIUtils._eventDispatchDesciptors; |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @return {!Timeline.TimelineMarkerStyle} |
| */ |
| static markerStyleForEvent(event) { |
| const tallMarkerDashStyle = [10, 5]; |
| const title = Timeline.TimelineUIUtils.eventTitle(event); |
| |
| if (event.hasCategory(TimelineModel.TimelineModel.Category.Console) || |
| event.hasCategory(TimelineModel.TimelineModel.Category.UserTiming)) { |
| return { |
| title: title, |
| dashStyle: tallMarkerDashStyle, |
| lineWidth: 0.5, |
| color: event.hasCategory(TimelineModel.TimelineModel.Category.UserTiming) ? 'purple' : 'orange', |
| tall: false, |
| lowPriority: false, |
| }; |
| } |
| const recordTypes = TimelineModel.TimelineModel.RecordType; |
| let tall = false; |
| let color = 'grey'; |
| switch (event.name) { |
| case recordTypes.FrameStartedLoading: |
| color = 'green'; |
| tall = true; |
| break; |
| case recordTypes.MarkDOMContent: |
| color = 'blue'; |
| tall = true; |
| break; |
| case recordTypes.MarkLoad: |
| color = 'red'; |
| tall = true; |
| break; |
| case recordTypes.MarkFirstPaint: |
| color = 'hsl(180, 45%, 79%)'; |
| tall = true; |
| break; |
| case recordTypes.MarkFCP: |
| color = 'hsl(160, 43%, 58%)'; |
| tall = true; |
| break; |
| case recordTypes.MarkFMPCandidate: |
| color = 'hsl(146, 57%, 40%)'; |
| tall = true; |
| break; |
| case recordTypes.MarkFMP: |
| color = 'hsl(144, 100%, 21%)'; |
| tall = true; |
| break; |
| case recordTypes.TimeStamp: |
| color = 'orange'; |
| break; |
| } |
| return { |
| title: title, |
| dashStyle: tallMarkerDashStyle, |
| lineWidth: 0.5, |
| color: color, |
| tall: tall, |
| lowPriority: false, |
| }; |
| } |
| |
| /** |
| * @return {!Timeline.TimelineMarkerStyle} |
| */ |
| static markerStyleForFrame() { |
| return { |
| title: Common.UIString('Frame'), |
| color: 'rgba(100, 100, 100, 0.4)', |
| lineWidth: 3, |
| dashStyle: [3], |
| tall: true, |
| lowPriority: true |
| }; |
| } |
| |
| /** |
| * @param {string} id |
| * @return {string} |
| */ |
| static colorForId(id) { |
| if (!Timeline.TimelineUIUtils.colorForId._colorGenerator) { |
| Timeline.TimelineUIUtils.colorForId._colorGenerator = |
| new Common.Color.Generator({min: 30, max: 330}, {min: 50, max: 80, count: 3}, 85); |
| Timeline.TimelineUIUtils.colorForId._colorGenerator.setColorForID('', '#f2ecdc'); |
| } |
| return Timeline.TimelineUIUtils.colorForId._colorGenerator.colorForID(id); |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @param {string=} warningType |
| * @return {?Element} |
| */ |
| static eventWarning(event, warningType) { |
| const timelineData = TimelineModel.TimelineData.forEvent(event); |
| const warning = warningType || timelineData.warning; |
| if (!warning) |
| return null; |
| const warnings = TimelineModel.TimelineModel.WarningType; |
| const span = createElement('span'); |
| const eventData = event.args['data']; |
| |
| switch (warning) { |
| case warnings.ForcedStyle: |
| case warnings.ForcedLayout: |
| span.appendChild(UI.createDocumentationLink( |
| '../../fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing#avoid-forced-synchronous-layouts', |
| Common.UIString('Forced reflow'))); |
| span.createTextChild(Common.UIString(' is a likely performance bottleneck.')); |
| break; |
| case warnings.IdleDeadlineExceeded: |
| span.textContent = Common.UIString( |
| 'Idle callback execution extended beyond deadline by ' + |
| Number.millisToString(event.duration - eventData['allottedMilliseconds'], true)); |
| break; |
| case warnings.LongHandler: |
| span.textContent = Common.UIString('Handler took %s', Number.millisToString(event.duration, true)); |
| break; |
| case warnings.LongRecurringHandler: |
| span.textContent = Common.UIString('Recurring handler took %s', Number.millisToString(event.duration, true)); |
| break; |
| case warnings.V8Deopt: |
| span.appendChild(UI.XLink.create( |
| 'https://github.com/GoogleChrome/devtools-docs/issues/53', Common.UIString('Not optimized'))); |
| span.createTextChild(Common.UIString(': %s', eventData['deoptReason'])); |
| break; |
| default: |
| console.assert(false, 'Unhandled TimelineModel.WarningType'); |
| } |
| return span; |
| } |
| |
| /** |
| * @param {!TimelineModel.TimelineModel.PageFrame} frame |
| * @param {number=} trimAt |
| */ |
| static displayNameForFrame(frame, trimAt) { |
| const url = frame.url; |
| if (!trimAt) |
| trimAt = 30; |
| return url.startsWith('about:') ? `"${frame.name.trimMiddle(trimAt)}"` : frame.url.trimEnd(trimAt); |
| } |
| }; |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.TimelineRecordStyle = class { |
| /** |
| * @param {string} title |
| * @param {!Timeline.TimelineCategory} category |
| * @param {boolean=} hidden |
| */ |
| constructor(title, category, hidden) { |
| this.title = title; |
| this.category = category; |
| this.hidden = !!hidden; |
| } |
| }; |
| |
| |
| /** |
| * @enum {symbol} |
| */ |
| Timeline.TimelineUIUtils.NetworkCategory = { |
| HTML: Symbol('HTML'), |
| Script: Symbol('Script'), |
| Style: Symbol('Style'), |
| Media: Symbol('Media'), |
| Other: Symbol('Other') |
| }; |
| |
| |
| Timeline.TimelineUIUtils._aggregatedStatsKey = Symbol('aggregatedStats'); |
| |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.TimelineUIUtils.InvalidationsGroupElement = class extends UI.TreeElement { |
| /** |
| * @param {!SDK.Target} target |
| * @param {?Map<number, ?SDK.DOMNode>} relatedNodesMap |
| * @param {!Timeline.TimelineDetailsContentHelper} contentHelper |
| * @param {!Array.<!TimelineModel.InvalidationTrackingEvent>} invalidations |
| */ |
| constructor(target, relatedNodesMap, contentHelper, invalidations) { |
| super('', true); |
| |
| this.listItemElement.classList.add('header'); |
| this.selectable = false; |
| this.toggleOnClick = true; |
| |
| this._relatedNodesMap = relatedNodesMap; |
| this._contentHelper = contentHelper; |
| this._invalidations = invalidations; |
| this.title = this._createTitle(target); |
| } |
| |
| /** |
| * @param {!SDK.Target} target |
| * @return {!Element} |
| */ |
| _createTitle(target) { |
| const first = this._invalidations[0]; |
| const reason = first.cause.reason; |
| const topFrame = first.cause.stackTrace && first.cause.stackTrace[0]; |
| |
| const title = createElement('span'); |
| if (reason) |
| title.createTextChild(Common.UIString('%s for ', reason)); |
| else |
| title.createTextChild(Common.UIString('Unknown cause for ')); |
| |
| this._appendTruncatedNodeList(title, this._invalidations); |
| |
| if (topFrame && this._contentHelper.linkifier()) { |
| title.createTextChild(Common.UIString('. ')); |
| const stack = title.createChild('span', 'monospace'); |
| stack.createChild('span').textContent = Timeline.TimelineUIUtils.frameDisplayName(topFrame); |
| const link = this._contentHelper.linkifier().maybeLinkifyConsoleCallFrame(target, topFrame); |
| if (link) { |
| stack.createChild('span').textContent = ' @ '; |
| stack.createChild('span').appendChild(link); |
| } |
| } |
| |
| return title; |
| } |
| |
| /** |
| * @override |
| */ |
| onpopulate() { |
| const content = createElementWithClass('div', 'content'); |
| |
| const first = this._invalidations[0]; |
| if (first.cause.stackTrace) { |
| const stack = content.createChild('div'); |
| stack.createTextChild(Common.UIString('Stack trace:')); |
| this._contentHelper.createChildStackTraceElement( |
| stack, Timeline.TimelineUIUtils._stackTraceFromCallFrames(first.cause.stackTrace)); |
| } |
| |
| content.createTextChild(this._invalidations.length > 1 ? Common.UIString('Nodes:') : Common.UIString('Node:')); |
| const nodeList = content.createChild('div', 'node-list'); |
| let firstNode = true; |
| for (let i = 0; i < this._invalidations.length; i++) { |
| const invalidation = this._invalidations[i]; |
| const invalidationNode = this._createInvalidationNode(invalidation, true); |
| if (invalidationNode) { |
| if (!firstNode) |
| nodeList.createTextChild(Common.UIString(', ')); |
| firstNode = false; |
| |
| nodeList.appendChild(invalidationNode); |
| |
| const extraData = invalidation.extraData ? ', ' + invalidation.extraData : ''; |
| if (invalidation.changedId) { |
| nodeList.createTextChild(Common.UIString('(changed id to "%s"%s)', invalidation.changedId, extraData)); |
| } else if (invalidation.changedClass) { |
| nodeList.createTextChild(Common.UIString('(changed class to "%s"%s)', invalidation.changedClass, extraData)); |
| } else if (invalidation.changedAttribute) { |
| nodeList.createTextChild( |
| Common.UIString('(changed attribute to "%s"%s)', invalidation.changedAttribute, extraData)); |
| } else if (invalidation.changedPseudo) { |
| nodeList.createTextChild( |
| Common.UIString('(changed pesudo to "%s"%s)', invalidation.changedPseudo, extraData)); |
| } else if (invalidation.selectorPart) { |
| nodeList.createTextChild(Common.UIString('(changed "%s"%s)', invalidation.selectorPart, extraData)); |
| } |
| } |
| } |
| |
| const contentTreeElement = new UI.TreeElement(content, false); |
| contentTreeElement.selectable = false; |
| this.appendChild(contentTreeElement); |
| } |
| |
| /** |
| * @param {!Element} parentElement |
| * @param {!Array.<!TimelineModel.InvalidationTrackingEvent>} invalidations |
| */ |
| _appendTruncatedNodeList(parentElement, invalidations) { |
| const invalidationNodes = []; |
| const invalidationNodeIdMap = {}; |
| for (let i = 0; i < invalidations.length; i++) { |
| const invalidation = invalidations[i]; |
| const invalidationNode = this._createInvalidationNode(invalidation, false); |
| invalidationNode.addEventListener('click', e => e.consume(), false); |
| if (invalidationNode && !invalidationNodeIdMap[invalidation.nodeId]) { |
| invalidationNodes.push(invalidationNode); |
| invalidationNodeIdMap[invalidation.nodeId] = true; |
| } |
| } |
| |
| if (invalidationNodes.length === 1) { |
| parentElement.appendChild(invalidationNodes[0]); |
| } else if (invalidationNodes.length === 2) { |
| parentElement.appendChild(invalidationNodes[0]); |
| parentElement.createTextChild(Common.UIString(' and ')); |
| parentElement.appendChild(invalidationNodes[1]); |
| } else if (invalidationNodes.length >= 3) { |
| parentElement.appendChild(invalidationNodes[0]); |
| parentElement.createTextChild(Common.UIString(', ')); |
| parentElement.appendChild(invalidationNodes[1]); |
| parentElement.createTextChild(Common.UIString(', and %s others', invalidationNodes.length - 2)); |
| } |
| } |
| |
| /** |
| * @param {!TimelineModel.InvalidationTrackingEvent} invalidation |
| * @param {boolean} showUnknownNodes |
| */ |
| _createInvalidationNode(invalidation, showUnknownNodes) { |
| const node = (invalidation.nodeId && this._relatedNodesMap) ? this._relatedNodesMap.get(invalidation.nodeId) : null; |
| if (node) { |
| const nodeSpan = createElement('span'); |
| Common.Linkifier.linkify(node).then(link => nodeSpan.appendChild(link)); |
| return nodeSpan; |
| } |
| if (invalidation.nodeName) { |
| const nodeSpan = createElement('span'); |
| nodeSpan.textContent = Common.UIString('[ %s ]', invalidation.nodeName); |
| return nodeSpan; |
| } |
| if (showUnknownNodes) { |
| const nodeSpan = createElement('span'); |
| return nodeSpan.createTextChild(Common.UIString('[ unknown node ]')); |
| } |
| } |
| }; |
| |
| Timeline.TimelineUIUtils._previewElementSymbol = Symbol('previewElement'); |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.TimelineUIUtils.EventDispatchTypeDescriptor = class { |
| /** |
| * @param {number} priority |
| * @param {string} color |
| * @param {!Array.<string>} eventTypes |
| */ |
| constructor(priority, color, eventTypes) { |
| this.priority = priority; |
| this.color = color; |
| this.eventTypes = eventTypes; |
| } |
| }; |
| |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.TimelineCategory = class extends Common.Object { |
| /** |
| * @param {string} name |
| * @param {string} title |
| * @param {boolean} visible |
| * @param {string} childColor |
| * @param {string} color |
| */ |
| constructor(name, title, visible, childColor, color) { |
| super(); |
| this.name = name; |
| this.title = title; |
| this.visible = visible; |
| this.childColor = childColor; |
| this.color = color; |
| this.hidden = false; |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| get hidden() { |
| return this._hidden; |
| } |
| |
| /** |
| * @param {boolean} hidden |
| */ |
| set hidden(hidden) { |
| this._hidden = hidden; |
| this.dispatchEventToListeners(Timeline.TimelineCategory.Events.VisibilityChanged, this); |
| } |
| }; |
| |
| /** @enum {symbol} */ |
| Timeline.TimelineCategory.Events = { |
| VisibilityChanged: Symbol('VisibilityChanged') |
| }; |
| |
| /** |
| * @typedef {!{ |
| * title: string, |
| * color: string, |
| * lineWidth: number, |
| * dashStyle: !Array.<number>, |
| * tall: boolean, |
| * lowPriority: boolean |
| * }} |
| */ |
| Timeline.TimelineMarkerStyle; |
| |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.TimelinePopupContentHelper = class { |
| /** |
| * @param {string} title |
| */ |
| constructor(title) { |
| this._contentTable = createElement('table'); |
| const titleCell = this._createCell(Common.UIString('%s - Details', title), 'timeline-details-title'); |
| titleCell.colSpan = 2; |
| const titleRow = createElement('tr'); |
| titleRow.appendChild(titleCell); |
| this._contentTable.appendChild(titleRow); |
| } |
| |
| /** |
| * @return {!Element} |
| */ |
| contentTable() { |
| return this._contentTable; |
| } |
| |
| /** |
| * @param {string|number} content |
| * @param {string=} styleName |
| */ |
| _createCell(content, styleName) { |
| const text = createElement('label'); |
| text.createTextChild(String(content)); |
| const cell = createElement('td'); |
| cell.className = 'timeline-details'; |
| if (styleName) |
| cell.className += ' ' + styleName; |
| cell.textContent = content; |
| return cell; |
| } |
| |
| /** |
| * @param {string} title |
| * @param {string|number} content |
| */ |
| appendTextRow(title, content) { |
| const row = createElement('tr'); |
| row.appendChild(this._createCell(title, 'timeline-details-row-title')); |
| row.appendChild(this._createCell(content, 'timeline-details-row-data')); |
| this._contentTable.appendChild(row); |
| } |
| |
| /** |
| * @param {string} title |
| * @param {!Node|string} content |
| */ |
| appendElementRow(title, content) { |
| const row = createElement('tr'); |
| const titleCell = this._createCell(title, 'timeline-details-row-title'); |
| row.appendChild(titleCell); |
| const cell = createElement('td'); |
| cell.className = 'details'; |
| if (content instanceof Node) |
| cell.appendChild(content); |
| else |
| cell.createTextChild(content || ''); |
| row.appendChild(cell); |
| this._contentTable.appendChild(row); |
| } |
| }; |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.TimelineDetailsContentHelper = class { |
| /** |
| * @param {?SDK.Target} target |
| * @param {?Components.Linkifier} linkifier |
| */ |
| constructor(target, linkifier) { |
| this.fragment = createDocumentFragment(); |
| |
| this._linkifier = linkifier; |
| this._target = target; |
| |
| this.element = createElementWithClass('div', 'timeline-details-view-block'); |
| this._tableElement = this.element.createChild('div', 'vbox timeline-details-chip-body'); |
| this.fragment.appendChild(this.element); |
| } |
| |
| /** |
| * @param {string} title |
| * @param {string=} swatchColor |
| */ |
| addSection(title, swatchColor) { |
| if (!this._tableElement.hasChildNodes()) { |
| this.element.removeChildren(); |
| } else { |
| this.element = createElementWithClass('div', 'timeline-details-view-block'); |
| this.fragment.appendChild(this.element); |
| } |
| |
| if (title) { |
| const titleElement = this.element.createChild('div', 'timeline-details-chip-title'); |
| if (swatchColor) |
| titleElement.createChild('div').style.backgroundColor = swatchColor; |
| titleElement.createTextChild(title); |
| } |
| |
| this._tableElement = this.element.createChild('div', 'vbox timeline-details-chip-body'); |
| this.fragment.appendChild(this.element); |
| } |
| |
| /** |
| * @return {?Components.Linkifier} |
| */ |
| linkifier() { |
| return this._linkifier; |
| } |
| |
| /** |
| * @param {string} title |
| * @param {string|number|boolean} value |
| */ |
| appendTextRow(title, value) { |
| const rowElement = this._tableElement.createChild('div', 'timeline-details-view-row'); |
| rowElement.createChild('div', 'timeline-details-view-row-title').textContent = title; |
| rowElement.createChild('div', 'timeline-details-view-row-value').textContent = value; |
| } |
| |
| /** |
| * @param {string} title |
| * @param {!Node|string} content |
| * @param {boolean=} isWarning |
| * @param {boolean=} isStacked |
| */ |
| appendElementRow(title, content, isWarning, isStacked) { |
| const rowElement = this._tableElement.createChild('div', 'timeline-details-view-row'); |
| if (isWarning) |
| rowElement.classList.add('timeline-details-warning'); |
| if (isStacked) |
| rowElement.classList.add('timeline-details-stack-values'); |
| const titleElement = rowElement.createChild('div', 'timeline-details-view-row-title'); |
| titleElement.textContent = title; |
| const valueElement = rowElement.createChild('div', 'timeline-details-view-row-value'); |
| if (content instanceof Node) |
| valueElement.appendChild(content); |
| else |
| valueElement.createTextChild(content || ''); |
| } |
| |
| /** |
| * @param {string} title |
| * @param {string} url |
| * @param {number} startLine |
| * @param {number=} startColumn |
| */ |
| appendLocationRow(title, url, startLine, startColumn) { |
| if (!this._linkifier || !this._target) |
| return; |
| const link = this._linkifier.maybeLinkifyScriptLocation(this._target, null, url, startLine, startColumn); |
| if (!link) |
| return; |
| this.appendElementRow(title, link); |
| } |
| |
| /** |
| * @param {string} title |
| * @param {string} url |
| * @param {number} startLine |
| * @param {number=} endLine |
| */ |
| appendLocationRange(title, url, startLine, endLine) { |
| if (!this._linkifier || !this._target) |
| return; |
| const locationContent = createElement('span'); |
| const link = this._linkifier.maybeLinkifyScriptLocation(this._target, null, url, startLine); |
| if (!link) |
| return; |
| locationContent.appendChild(link); |
| locationContent.createTextChild(String.sprintf(' [%s\u2026%s]', startLine + 1, endLine + 1 || '')); |
| this.appendElementRow(title, locationContent); |
| } |
| |
| /** |
| * @param {string} title |
| * @param {!Protocol.Runtime.StackTrace} stackTrace |
| */ |
| appendStackTrace(title, stackTrace) { |
| if (!this._linkifier || !this._target) |
| return; |
| |
| const rowElement = this._tableElement.createChild('div', 'timeline-details-view-row'); |
| rowElement.createChild('div', 'timeline-details-view-row-title').textContent = title; |
| this.createChildStackTraceElement(rowElement, stackTrace); |
| } |
| |
| /** |
| * @param {!Element} parentElement |
| * @param {!Protocol.Runtime.StackTrace} stackTrace |
| */ |
| createChildStackTraceElement(parentElement, stackTrace) { |
| if (!this._linkifier || !this._target) |
| return; |
| parentElement.classList.add('timeline-details-stack-values'); |
| const stackTraceElement = |
| parentElement.createChild('div', 'timeline-details-view-row-value timeline-details-view-row-stack-trace'); |
| const callFrameElem = |
| Components.JSPresentationUtils.buildStackTracePreviewContents(this._target, this._linkifier, stackTrace); |
| stackTraceElement.appendChild(callFrameElem); |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @param {string=} warningType |
| */ |
| appendWarningRow(event, warningType) { |
| const warning = Timeline.TimelineUIUtils.eventWarning(event, warningType); |
| if (warning) |
| this.appendElementRow(Common.UIString('Warning'), warning, true); |
| } |
| }; |
| |
| Timeline.TimelineUIUtils._categoryBreakdownCacheSymbol = Symbol('categoryBreakdownCache'); |