blob: bc189fcc9c29c44d7da9430fd9c187758a957a42 [file] [log] [blame]
/*
* Copyright (C) 2014 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @constructor
* @implements {WebInspector.FlameChartDataProvider}
* @param {!WebInspector.TimelineModel} model
* @param {!Array<!WebInspector.TimelineModel.Filter>} filters
*/
WebInspector.TimelineFlameChartDataProviderBase = function(model, filters)
{
WebInspector.FlameChartDataProvider.call(this);
this.reset();
this._model = model;
/** @type {?WebInspector.FlameChart.TimelineData} */
this._timelineData;
this._font = "11px " + WebInspector.fontFamily();
this._filters = filters;
}
WebInspector.TimelineFlameChartDataProviderBase.prototype = {
/**
* @override
* @return {number}
*/
barHeight: function()
{
return 17;
},
/**
* @override
* @return {number}
*/
textBaseline: function()
{
return 5;
},
/**
* @override
* @return {number}
*/
textPadding: function()
{
return 4;
},
/**
* @override
* @param {number} entryIndex
* @return {string}
*/
entryFont: function(entryIndex)
{
return this._font;
},
/**
* @override
* @param {number} entryIndex
* @return {?string}
*/
entryTitle: function(entryIndex)
{
return null;
},
reset: function()
{
this._timelineData = null;
},
/**
* @override
* @return {number}
*/
minimumBoundary: function()
{
return this._minimumBoundary;
},
/**
* @override
* @return {number}
*/
totalTime: function()
{
return this._timeSpan;
},
/**
* @override
* @param {number} value
* @param {number=} precision
* @return {string}
*/
formatValue: function(value, precision)
{
return Number.preciseMillisToString(value, precision);
},
/**
* @override
* @return {number}
*/
maxStackDepth: function()
{
return this._currentLevel;
},
/**
* @override
* @param {number} entryIndex
* @return {?Element}
*/
prepareHighlightedEntryInfo: function(entryIndex)
{
return null;
},
/**
* @override
* @param {number} entryIndex
* @return {boolean}
*/
canJumpToEntry: function(entryIndex)
{
return false;
},
/**
* @override
* @param {number} entryIndex
* @return {string}
*/
entryColor: function(entryIndex)
{
return "red";
},
/**
* @override
* @param {number} index
* @return {boolean}
*/
forceDecoration: function(index)
{
return false;
},
/**
* @override
* @param {number} entryIndex
* @param {!CanvasRenderingContext2D} context
* @param {?string} text
* @param {number} barX
* @param {number} barY
* @param {number} barWidth
* @param {number} barHeight
* @param {number} unclippedBarX
* @param {number} timeToPixels
* @return {boolean}
*/
decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight, unclippedBarX, timeToPixels)
{
return false;
},
/**
* @override
* @param {number} startTime
* @param {number} endTime
* @return {?Array.<number>}
*/
dividerOffsets: function(startTime, endTime)
{
return null;
},
/**
* @override
* @return {number}
*/
paddingLeft: function()
{
return 0;
},
/**
* @override
* @param {number} entryIndex
* @return {string}
*/
textColor: function(entryIndex)
{
return "#333";
},
/**
* @override
* @param {number} entryIndex
* @return {?{startTime: number, endTime: number}}
*/
highlightTimeRange: function(entryIndex)
{
var startTime = this._timelineData.entryStartTimes[entryIndex];
return {
startTime: startTime,
endTime: startTime + this._timelineData.entryTotalTimes[entryIndex]
};
},
/**
* @param {number} entryIndex
* @return {?WebInspector.TimelineSelection}
*/
createSelection: function(entryIndex)
{
return null;
},
/**
* @override
* @return {!WebInspector.FlameChart.TimelineData}
*/
timelineData: function()
{
throw new Error("Not implemented");
},
/**
* @param {!WebInspector.TracingModel.Event} event
* @return {boolean}
*/
_isVisible: function(event)
{
return this._filters.every(function(filter) { return filter.accept(event); });
}
}
/**
* @enum {symbol}
*/
WebInspector.TimelineFlameChartEntryType = {
Frame: Symbol("Frame"),
Event: Symbol("Event"),
InteractionRecord: Symbol("InteractionRecord"),
};
/**
* @constructor
* @extends {WebInspector.TimelineFlameChartDataProviderBase}
* @param {!WebInspector.TimelineModel} model
* @param {!WebInspector.TimelineFrameModel} frameModel
* @param {!WebInspector.TimelineIRModel} irModel
* @param {!Array<!WebInspector.TimelineModel.Filter>} filters
*/
WebInspector.TimelineFlameChartDataProvider = function(model, frameModel, irModel, filters)
{
WebInspector.TimelineFlameChartDataProviderBase.call(this, model, filters);
this._frameModel = frameModel;
this._irModel = irModel;
this._consoleColorGenerator = new WebInspector.FlameChart.ColorGenerator(
{ min: 30, max: 55 },
{ min: 70, max: 100, count: 6 },
50, 0.7);
this._headerLevel1 = {
padding: 4,
height: 17,
collapsible: true,
color: WebInspector.themeSupport.patchColor("#222", WebInspector.ThemeSupport.ColorUsage.Foreground),
font: this._font,
backgroundColor: WebInspector.themeSupport.patchColor("white", WebInspector.ThemeSupport.ColorUsage.Background),
nestingLevel: 0
};
this._headerLevel2 = {
padding: 2,
height: 17,
collapsible: false,
font: this._font,
color: WebInspector.themeSupport.patchColor("#222", WebInspector.ThemeSupport.ColorUsage.Foreground),
backgroundColor: WebInspector.themeSupport.patchColor("white", WebInspector.ThemeSupport.ColorUsage.Background),
nestingLevel: 1,
shareHeaderLine: true
};
this._interactionsHeaderLevel1 = {
padding: 4,
height: 17,
collapsible: true,
color: WebInspector.themeSupport.patchColor("#222", WebInspector.ThemeSupport.ColorUsage.Foreground),
font: this._font,
backgroundColor: WebInspector.themeSupport.patchColor("white", WebInspector.ThemeSupport.ColorUsage.Background),
nestingLevel: 0,
useFirstLineForOverview: true,
shareHeaderLine: true
};
this._interactionsHeaderLevel2 = {
padding: 2,
height: 17,
collapsible: true,
color: WebInspector.themeSupport.patchColor("#222", WebInspector.ThemeSupport.ColorUsage.Foreground),
font: this._font,
backgroundColor: WebInspector.themeSupport.patchColor("white", WebInspector.ThemeSupport.ColorUsage.Background),
nestingLevel: 1,
shareHeaderLine: true
};
}
WebInspector.TimelineFlameChartDataProvider.InstantEventVisibleDurationMs = 0.001;
WebInspector.TimelineFlameChartDataProvider.prototype = {
/**
* @override
* @param {number} entryIndex
* @return {?string}
*/
entryTitle: function(entryIndex)
{
var entryType = this._entryType(entryIndex);
if (entryType === WebInspector.TimelineFlameChartEntryType.Event) {
var event = /** @type {!WebInspector.TracingModel.Event} */ (this._entryData[entryIndex]);
if (event.phase === WebInspector.TracingModel.Phase.AsyncStepInto || event.phase === WebInspector.TracingModel.Phase.AsyncStepPast)
return event.name + ":" + event.args["step"];
if (event._blackboxRoot)
return WebInspector.UIString("Blackboxed");
var name = WebInspector.TimelineUIUtils.eventStyle(event).title;
// TODO(yurys): support event dividers
var detailsText = WebInspector.TimelineUIUtils.buildDetailsTextForTraceEvent(event, this._model.targetByEvent(event));
if (event.name === WebInspector.TimelineModel.RecordType.JSFrame && detailsText)
return detailsText;
return detailsText ? WebInspector.UIString("%s (%s)", name, detailsText) : name;
}
var title = this._entryIndexToTitle[entryIndex];
if (!title) {
title = WebInspector.UIString("Unexpected entryIndex %d", entryIndex);
console.error(title);
}
return title;
},
/**
* @override
* @param {number} index
* @return {string}
*/
textColor: function(index)
{
var event = this._entryData[index];
if (event && event._blackboxRoot)
return "#888";
else
return WebInspector.TimelineFlameChartDataProviderBase.prototype.textColor.call(this, index);
},
/**
* @override
*/
reset: function()
{
WebInspector.TimelineFlameChartDataProviderBase.prototype.reset.call(this);
/** @type {!Array<!WebInspector.TracingModel.Event|!WebInspector.TimelineFrame|!WebInspector.TimelineIRModel.Phases>} */
this._entryData = [];
/** @type {!Array<!WebInspector.TimelineFlameChartEntryType>} */
this._entryTypeByLevel = [];
/** @type {!Array<string>} */
this._entryIndexToTitle = [];
/** @type {!Array<!WebInspector.TimelineFlameChartMarker>} */
this._markers = [];
/** @type {!Map<!WebInspector.TimelineCategory, string>} */
this._asyncColorByCategory = new Map();
/** @type {!Map<!WebInspector.TimelineIRModel.Phases, string>} */
this._asyncColorByInteractionPhase = new Map();
},
/**
* @override
* @return {!WebInspector.FlameChart.TimelineData}
*/
timelineData: function()
{
if (this._timelineData)
return this._timelineData;
this._timelineData = new WebInspector.FlameChart.TimelineData([], [], [], []);
this._flowEventIndexById = {};
this._minimumBoundary = this._model.minimumRecordTime();
this._timeSpan = this._model.isEmpty() ? 1000 : this._model.maximumRecordTime() - this._minimumBoundary;
this._currentLevel = 0;
this._appendFrameBars(this._frameModel.frames());
this._appendHeader(WebInspector.UIString("Interactions"), this._interactionsHeaderLevel1);
this._appendInteractionRecords();
var asyncEventGroups = WebInspector.TimelineModel.AsyncEventGroup;
var inputLatencies = this._model.mainThreadAsyncEvents().get(asyncEventGroups.input);
if (inputLatencies && inputLatencies.length) {
var title = WebInspector.TimelineUIUtils.titleForAsyncEventGroup(asyncEventGroups.input);
this._appendAsyncEventsGroup(title, inputLatencies, this._interactionsHeaderLevel2);
}
var animations = this._model.mainThreadAsyncEvents().get(asyncEventGroups.animation);
if (animations && animations.length) {
var title = WebInspector.TimelineUIUtils.titleForAsyncEventGroup(asyncEventGroups.animation);
this._appendAsyncEventsGroup(title, animations, this._interactionsHeaderLevel2);
}
var threads = this._model.virtualThreads();
this._appendThreadTimelineData(WebInspector.UIString("Main"), this._model.mainThreadEvents(), this._model.mainThreadAsyncEvents(), true);
var compositorThreads = threads.filter(thread => thread.name.startsWith("CompositorTileWorker"));
var otherThreads = threads.filter(thread => !thread.name.startsWith("CompositorTileWorker"));
if (compositorThreads.length) {
this._appendHeader(WebInspector.UIString("Raster"), this._headerLevel1);
for (var i = 0; i < compositorThreads.length; ++i)
this._appendSyncEvents(compositorThreads[i].events, WebInspector.UIString("Rasterizer Thread %d", i), this._headerLevel2);
}
this._appendGPUEvents();
otherThreads.forEach(thread => this._appendThreadTimelineData(thread.name, thread.events, thread.asyncEventsByGroup));
/**
* @param {!WebInspector.TimelineFlameChartMarker} a
* @param {!WebInspector.TimelineFlameChartMarker} b
*/
function compareStartTime(a, b)
{
return a.startTime() - b.startTime();
}
this._markers.sort(compareStartTime);
this._timelineData.markers = this._markers;
this._flowEventIndexById = {};
return this._timelineData;
},
/**
* @param {string} threadTitle
* @param {!Array<!WebInspector.TracingModel.Event>} syncEvents
* @param {!Map<!WebInspector.TimelineModel.AsyncEventGroup, !Array<!WebInspector.TracingModel.AsyncEvent>>} asyncEvents
* @param {boolean=} forceExpanded
*/
_appendThreadTimelineData: function(threadTitle, syncEvents, asyncEvents, forceExpanded)
{
this._appendAsyncEvents(asyncEvents);
this._appendSyncEvents(syncEvents, threadTitle, this._headerLevel1, forceExpanded);
},
/**
* @param {!Array<!WebInspector.TracingModel.Event>} events
* @param {string} title
* @param {!WebInspector.FlameChart.GroupStyle} style
* @param {boolean=} forceExpanded
*/
_appendSyncEvents: function(events, title, style, forceExpanded)
{
var openEvents = [];
var flowEventsEnabled = Runtime.experiments.isEnabled("timelineFlowEvents");
var blackboxingEnabled = Runtime.experiments.isEnabled("blackboxJSFramesOnTimeline");
var maxStackDepth = 0;
for (var i = 0; i < events.length; ++i) {
var e = events[i];
if (WebInspector.TimelineModel.isMarkerEvent(e))
this._markers.push(new WebInspector.TimelineFlameChartMarker(e.startTime, e.startTime - this._model.minimumRecordTime(), WebInspector.TimelineUIUtils.markerStyleForEvent(e)));
if (!WebInspector.TracingModel.isFlowPhase(e.phase)) {
if (!e.endTime && e.phase !== WebInspector.TracingModel.Phase.Instant)
continue;
if (WebInspector.TracingModel.isAsyncPhase(e.phase))
continue;
if (!this._isVisible(e))
continue;
}
while (openEvents.length && openEvents.peekLast().endTime <= e.startTime)
openEvents.pop();
e._blackboxRoot = false;
if (blackboxingEnabled && this._isBlackboxedEvent(e)) {
var parent = openEvents.peekLast();
if (parent && parent._blackboxRoot)
continue;
e._blackboxRoot = true;
}
if (title) {
this._appendHeader(title, style, forceExpanded);
title = "";
}
var level = this._currentLevel + openEvents.length;
this._appendEvent(e, level);
if (flowEventsEnabled)
this._appendFlowEvent(e, level);
maxStackDepth = Math.max(maxStackDepth, openEvents.length + 1);
if (e.endTime)
openEvents.push(e);
}
this._entryTypeByLevel.length = this._currentLevel + maxStackDepth;
this._entryTypeByLevel.fill(WebInspector.TimelineFlameChartEntryType.Event, this._currentLevel);
this._currentLevel += maxStackDepth;
},
/**
* @param {!WebInspector.TracingModel.Event} event
* @return {boolean}
*/
_isBlackboxedEvent: function(event)
{
if (event.name !== WebInspector.TimelineModel.RecordType.JSFrame)
return false;
var url = event.args["data"]["url"];
return url && this._isBlackboxedURL(url);
},
/**
* @param {string} url
* @return {boolean}
*/
_isBlackboxedURL: function(url)
{
return WebInspector.blackboxManager.isBlackboxedURL(url);
},
/**
* @param {!Map<!WebInspector.TimelineModel.AsyncEventGroup, !Array<!WebInspector.TracingModel.AsyncEvent>>} asyncEvents
*/
_appendAsyncEvents: function(asyncEvents)
{
var groups = WebInspector.TimelineModel.AsyncEventGroup;
var groupArray = Object.keys(groups).map(key => groups[key]);
groupArray.remove(groups.animation);
groupArray.remove(groups.input);
for (var groupIndex = 0; groupIndex < groupArray.length; ++groupIndex) {
var group = groupArray[groupIndex];
var events = asyncEvents.get(group);
if (!events)
continue;
var title = WebInspector.TimelineUIUtils.titleForAsyncEventGroup(group);
this._appendAsyncEventsGroup(title, events, this._headerLevel1);
}
},
/**
* @param {string} header
* @param {!Array<!WebInspector.TracingModel.AsyncEvent>} events
* @param {!WebInspector.FlameChart.GroupStyle} style
*/
_appendAsyncEventsGroup: function(header, events, style)
{
var lastUsedTimeByLevel = [];
var groupHeaderAppended = false;
for (var i = 0; i < events.length; ++i) {
var asyncEvent = events[i];
if (!this._isVisible(asyncEvent))
continue;
if (!groupHeaderAppended) {
this._appendHeader(header, style);
groupHeaderAppended = true;
}
var startTime = asyncEvent.startTime;
var level;
for (level = 0; level < lastUsedTimeByLevel.length && lastUsedTimeByLevel[level] > startTime; ++level) {}
this._appendAsyncEvent(asyncEvent, this._currentLevel + level);
lastUsedTimeByLevel[level] = asyncEvent.endTime;
}
this._entryTypeByLevel.length = this._currentLevel + lastUsedTimeByLevel.length;
this._entryTypeByLevel.fill(WebInspector.TimelineFlameChartEntryType.Event, this._currentLevel);
this._currentLevel += lastUsedTimeByLevel.length;
},
_appendGPUEvents: function()
{
if (this._appendSyncEvents(this._model.gpuEvents(), WebInspector.UIString("GPU"), this._headerLevel1, false))
++this._currentLevel;
},
_appendInteractionRecords: function()
{
this._irModel.interactionRecords().forEach(this._appendSegment, this);
this._entryTypeByLevel[this._currentLevel++] = WebInspector.TimelineFlameChartEntryType.InteractionRecord;
},
/**
* @param {!Array.<!WebInspector.TimelineFrame>} frames
*/
_appendFrameBars: function(frames)
{
var style = WebInspector.TimelineUIUtils.markerStyleForFrame();
this._entryTypeByLevel[this._currentLevel] = WebInspector.TimelineFlameChartEntryType.Frame;
for (var i = 0; i < frames.length; ++i) {
this._markers.push(new WebInspector.TimelineFlameChartMarker(frames[i].startTime, frames[i].startTime - this._model.minimumRecordTime(), style));
this._appendFrame(frames[i]);
}
++this._currentLevel;
},
/**
* @param {number} entryIndex
* @return {!WebInspector.TimelineFlameChartEntryType}
*/
_entryType: function(entryIndex)
{
return this._entryTypeByLevel[this._timelineData.entryLevels[entryIndex]];
},
/**
* @override
* @param {number} entryIndex
* @return {?Element}
*/
prepareHighlightedEntryInfo: function(entryIndex)
{
var time = "";
var title;
var warning;
var type = this._entryType(entryIndex);
if (type === WebInspector.TimelineFlameChartEntryType.Event) {
var event = /** @type {!WebInspector.TracingModel.Event} */ (this._entryData[entryIndex]);
var totalTime = event.duration;
var selfTime = event.selfTime;
var /** @const */ eps = 1e-6;
if (typeof totalTime === "number") {
time = Math.abs(totalTime - selfTime) > eps && selfTime > eps ?
WebInspector.UIString("%s (self %s)", Number.millisToString(totalTime, true), Number.millisToString(selfTime, true)) :
Number.millisToString(totalTime, true);
}
title = this.entryTitle(entryIndex);
warning = WebInspector.TimelineUIUtils.eventWarning(event);
} else if (type === WebInspector.TimelineFlameChartEntryType.Frame) {
var frame = /** @type {!WebInspector.TimelineFrame} */ (this._entryData[entryIndex]);
time = WebInspector.UIString("%s ~ %.0f\u2009fps", Number.preciseMillisToString(frame.duration, 1), (1000 / frame.duration));
title = frame.idle ? WebInspector.UIString("Idle Frame") : WebInspector.UIString("Frame");
if (frame.hasWarnings()) {
warning = createElement("span");
warning.textContent = WebInspector.UIString("Long frame");
}
} else {
return null;
}
var element = createElement("div");
var root = WebInspector.createShadowRootWithCoreStyles(element, "timeline/timelineFlamechartPopover.css");
var contents = root.createChild("div", "timeline-flamechart-popover");
contents.createChild("span", "timeline-info-time").textContent = time;
contents.createChild("span", "timeline-info-title").textContent = title;
if (warning) {
warning.classList.add("timeline-info-warning");
contents.appendChild(warning);
}
return element;
},
/**
* @override
* @param {number} entryIndex
* @return {string}
*/
entryColor: function(entryIndex)
{
// This is not annotated due to closure compiler failure to properly infer cache container's template type.
function patchColorAndCache(cache, key, lookupColor)
{
var color = cache.get(key);
if (color)
return color;
var parsedColor = WebInspector.Color.parse(lookupColor(key));
color = parsedColor.setAlpha(0.7).asString(WebInspector.Color.Format.RGBA) || "";
cache.set(key, color);
return color;
}
var type = this._entryType(entryIndex);
if (type === WebInspector.TimelineFlameChartEntryType.Event) {
var event = /** @type {!WebInspector.TracingModel.Event} */ (this._entryData[entryIndex]);
if (!WebInspector.TracingModel.isAsyncPhase(event.phase))
return WebInspector.TimelineUIUtils.eventColor(event);
if (event.hasCategory(WebInspector.TimelineModel.Category.Console) || event.hasCategory(WebInspector.TimelineModel.Category.UserTiming))
return this._consoleColorGenerator.colorForID(event.name);
if (event.hasCategory(WebInspector.TimelineModel.Category.LatencyInfo)) {
var phase = WebInspector.TimelineIRModel.phaseForEvent(event) || WebInspector.TimelineIRModel.Phases.Uncategorized;
return patchColorAndCache(this._asyncColorByInteractionPhase, phase, WebInspector.TimelineUIUtils.interactionPhaseColor);
}
var category = WebInspector.TimelineUIUtils.eventStyle(event).category;
return patchColorAndCache(this._asyncColorByCategory, category, () => category.color);
}
if (type === WebInspector.TimelineFlameChartEntryType.Frame)
return "white";
if (type === WebInspector.TimelineFlameChartEntryType.InteractionRecord)
return "transparent";
return "";
},
/**
* @override
* @param {number} entryIndex
* @param {!CanvasRenderingContext2D} context
* @param {?string} text
* @param {number} barX
* @param {number} barY
* @param {number} barWidth
* @param {number} barHeight
* @param {number} unclippedBarX
* @param {number} timeToPixels
* @return {boolean}
*/
decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight, unclippedBarX, timeToPixels)
{
var data = this._entryData[entryIndex];
var type = this._entryType(entryIndex);
if (type === WebInspector.TimelineFlameChartEntryType.Frame) {
var /** @const */ vPadding = 1;
var /** @const */ hPadding = 1;
var frame = /** {!WebInspector.TimelineFrame} */ (data);
barX += hPadding;
barWidth -= 2 * hPadding;
barY += vPadding;
barHeight -= 2 * vPadding + 1;
context.fillStyle = frame.idle ? "white" : (frame.hasWarnings() ? "#fad1d1" : "#d7f0d1");
context.fillRect(barX, barY, barWidth, barHeight);
var frameDurationText = Number.preciseMillisToString(frame.duration, 1);
var textWidth = context.measureText(frameDurationText).width;
if (barWidth >= textWidth) {
context.fillStyle = this.textColor(entryIndex);
context.fillText(frameDurationText, barX + (barWidth - textWidth) / 2, barY + barHeight - 3);
}
return true;
}
if (type === WebInspector.TimelineFlameChartEntryType.InteractionRecord) {
var color = WebInspector.TimelineUIUtils.interactionPhaseColor(/** @type {!WebInspector.TimelineIRModel.Phases} */ (this._entryData[entryIndex]));
context.fillStyle = color;
context.fillRect(barX, barY, barWidth - 1, 2);
context.fillRect(barX, barY - 3, 2, 3);
context.fillRect(barX + barWidth - 3, barY - 3, 2, 3);
return false;
}
if (type === WebInspector.TimelineFlameChartEntryType.Event) {
var event = /** @type {!WebInspector.TracingModel.Event} */ (this._entryData[entryIndex]);
if (event.hasCategory(WebInspector.TimelineModel.Category.LatencyInfo) && event.timeWaitingForMainThread) {
context.fillStyle = "hsla(0, 70%, 60%, 1)";
var width = Math.floor(unclippedBarX - barX + event.timeWaitingForMainThread * timeToPixels);
context.fillRect(barX, barY + barHeight - 3, width, 2);
}
if (event.warning)
paintWarningDecoration(barX, barWidth - 1.5);
}
/**
* @param {number} x
* @param {number} width
*/
function paintWarningDecoration(x, width)
{
var /** @const */ triangleSize = 8;
context.save();
context.beginPath();
context.rect(x, barY, width, barHeight);
context.clip();
context.beginPath();
context.fillStyle = "red";
context.moveTo(x + width - triangleSize, barY);
context.lineTo(x + width, barY);
context.lineTo(x + width, barY + triangleSize);
context.fill();
context.restore();
}
return false;
},
/**
* @override
* @param {number} entryIndex
* @return {boolean}
*/
forceDecoration: function(entryIndex)
{
var type = this._entryType(entryIndex);
return type === WebInspector.TimelineFlameChartEntryType.Frame ||
type === WebInspector.TimelineFlameChartEntryType.Event && !!(/** @type {!WebInspector.TracingModel.Event} */ (this._entryData[entryIndex]).warning);
},
/**
* @param {string} title
* @param {!WebInspector.FlameChart.GroupStyle} style
* @param {boolean=} expanded
*/
_appendHeader: function(title, style, expanded)
{
this._timelineData.groups.push({startLevel: this._currentLevel, name: title, expanded: expanded, style: style});
},
/**
* @param {!WebInspector.TracingModel.Event} event
* @param {number} level
*/
_appendEvent: function(event, level)
{
var index = this._entryData.length;
this._entryData.push(event);
this._timelineData.entryLevels[index] = level;
var duration;
if (WebInspector.TimelineModel.isMarkerEvent(event))
duration = undefined;
else
duration = event.duration || WebInspector.TimelineFlameChartDataProvider.InstantEventVisibleDurationMs;
this._timelineData.entryTotalTimes[index] = duration;
this._timelineData.entryStartTimes[index] = event.startTime;
},
/**
* @param {!WebInspector.TracingModel.Event} event
* @param {number} level
*/
_appendFlowEvent: function(event, level)
{
var timelineData = this._timelineData;
/**
* @param {!WebInspector.TracingModel.Event} event
* @return {number}
*/
function pushStartFlow(event)
{
var flowIndex = timelineData.flowStartTimes.length;
timelineData.flowStartTimes.push(event.startTime);
timelineData.flowStartLevels.push(level);
return flowIndex;
}
/**
* @param {!WebInspector.TracingModel.Event} event
* @param {number} flowIndex
*/
function pushEndFlow(event, flowIndex)
{
timelineData.flowEndTimes[flowIndex] = event.startTime;
timelineData.flowEndLevels[flowIndex] = level;
}
switch (event.phase) {
case WebInspector.TracingModel.Phase.FlowBegin:
this._flowEventIndexById[event.id] = pushStartFlow(event);
break;
case WebInspector.TracingModel.Phase.FlowStep:
pushEndFlow(event, this._flowEventIndexById[event.id]);
this._flowEventIndexById[event.id] = pushStartFlow(event);
break;
case WebInspector.TracingModel.Phase.FlowEnd:
pushEndFlow(event, this._flowEventIndexById[event.id]);
delete this._flowEventIndexById[event.id];
break;
}
},
/**
* @param {!WebInspector.TracingModel.AsyncEvent} asyncEvent
* @param {number} level
*/
_appendAsyncEvent: function(asyncEvent, level)
{
if (WebInspector.TracingModel.isNestableAsyncPhase(asyncEvent.phase)) {
// FIXME: also add steps once we support event nesting in the FlameChart.
this._appendEvent(asyncEvent, level);
return;
}
var steps = asyncEvent.steps;
// If we have past steps, put the end event for each range rather than start one.
var eventOffset = steps.length > 1 && steps[1].phase === WebInspector.TracingModel.Phase.AsyncStepPast ? 1 : 0;
for (var i = 0; i < steps.length - 1; ++i) {
var index = this._entryData.length;
this._entryData.push(steps[i + eventOffset]);
var startTime = steps[i].startTime;
this._timelineData.entryLevels[index] = level;
this._timelineData.entryTotalTimes[index] = steps[i + 1].startTime - startTime;
this._timelineData.entryStartTimes[index] = startTime;
}
},
/**
* @param {!WebInspector.TimelineFrame} frame
*/
_appendFrame: function(frame)
{
var index = this._entryData.length;
this._entryData.push(frame);
this._entryIndexToTitle[index] = Number.millisToString(frame.duration, true);
this._timelineData.entryLevels[index] = this._currentLevel;
this._timelineData.entryTotalTimes[index] = frame.duration;
this._timelineData.entryStartTimes[index] = frame.startTime;
},
/**
* @param {!WebInspector.Segment} segment
*/
_appendSegment: function(segment)
{
var index = this._entryData.length;
this._entryData.push(/** @type {!WebInspector.TimelineIRModel.Phases} */(segment.data));
this._entryIndexToTitle[index] = /** @type {string} */ (segment.data);
this._timelineData.entryLevels[index] = this._currentLevel;
this._timelineData.entryTotalTimes[index] = segment.end - segment.begin;
this._timelineData.entryStartTimes[index] = segment.begin;
},
/**
* @override
* @param {number} entryIndex
* @return {?WebInspector.TimelineSelection}
*/
createSelection: function(entryIndex)
{
var type = this._entryType(entryIndex);
var timelineSelection = null;
if (type === WebInspector.TimelineFlameChartEntryType.Event)
timelineSelection = WebInspector.TimelineSelection.fromTraceEvent(/** @type {!WebInspector.TracingModel.Event} */ (this._entryData[entryIndex]))
else if (type === WebInspector.TimelineFlameChartEntryType.Frame)
timelineSelection = WebInspector.TimelineSelection.fromFrame(/** @type {!WebInspector.TimelineFrame} */ (this._entryData[entryIndex]));
if (timelineSelection)
this._lastSelection = new WebInspector.TimelineFlameChartView.Selection(timelineSelection, entryIndex);
return timelineSelection;
},
/**
* @param {?WebInspector.TimelineSelection} selection
* @return {number}
*/
entryIndexForSelection: function(selection)
{
if (!selection || selection.type() === WebInspector.TimelineSelection.Type.Range)
return -1;
if (this._lastSelection && this._lastSelection.timelineSelection.object() === selection.object())
return this._lastSelection.entryIndex;
var index = this._entryData.indexOf(/** @type {!WebInspector.TracingModel.Event|!WebInspector.TimelineFrame|!WebInspector.TimelineIRModel.Phases} */(selection.object()));
if (index !== -1)
this._lastSelection = new WebInspector.TimelineFlameChartView.Selection(selection, index);
return index;
},
__proto__: WebInspector.TimelineFlameChartDataProviderBase.prototype
}
/**
* @constructor
* @extends {WebInspector.TimelineFlameChartDataProviderBase}
* @param {!WebInspector.TimelineModel} model
*/
WebInspector.TimelineFlameChartNetworkDataProvider = function(model)
{
WebInspector.TimelineFlameChartDataProviderBase.call(this, model, []);
var loadingCategory = WebInspector.TimelineUIUtils.categories()["loading"];
this._waitingColor = loadingCategory.childColor;
this._processingColor = loadingCategory.color;
}
WebInspector.TimelineFlameChartNetworkDataProvider.prototype = {
/**
* @override
* @return {!WebInspector.FlameChart.TimelineData}
*/
timelineData: function()
{
if (this._timelineData)
return this._timelineData;
/** @type {!Array<!WebInspector.TimelineModel.NetworkRequest>} */
this._requests = [];
this._timelineData = new WebInspector.FlameChart.TimelineData([], [], [], []);
this._appendTimelineData(this._model.mainThreadEvents());
return this._timelineData;
},
/**
* @override
*/
reset: function()
{
WebInspector.TimelineFlameChartDataProviderBase.prototype.reset.call(this);
/** @type {!Array<!WebInspector.TimelineModel.NetworkRequest>} */
this._requests = [];
},
/**
* @param {number} startTime
* @param {number} endTime
*/
setWindowTimes: function(startTime, endTime)
{
this._startTime = startTime;
this._endTime = endTime;
this._updateTimelineData();
},
/**
* @override
* @param {number} index
* @return {?WebInspector.TimelineSelection}
*/
createSelection: function(index)
{
if (index === -1)
return null;
var request = this._requests[index];
this._lastSelection = new WebInspector.TimelineFlameChartView.Selection(WebInspector.TimelineSelection.fromNetworkRequest(request), index);
return this._lastSelection.timelineSelection;
},
/**
* @param {?WebInspector.TimelineSelection} selection
* @return {number}
*/
entryIndexForSelection: function(selection)
{
if (!selection)
return -1;
if (this._lastSelection && this._lastSelection.timelineSelection.object() === selection.object())
return this._lastSelection.entryIndex;
if (selection.type() !== WebInspector.TimelineSelection.Type.NetworkRequest)
return -1;
var request = /** @type{!WebInspector.TimelineModel.NetworkRequest} */ (selection.object());
var index = this._requests.indexOf(request);
if (index !== -1)
this._lastSelection = new WebInspector.TimelineFlameChartView.Selection(WebInspector.TimelineSelection.fromNetworkRequest(request), index);
return index;
},
/**
* @override
* @param {number} index
* @return {string}
*/
entryColor: function(index)
{
var request = /** @type {!WebInspector.TimelineModel.NetworkRequest} */ (this._requests[index]);
var category = WebInspector.TimelineUIUtils.networkRequestCategory(request);
return WebInspector.TimelineUIUtils.networkCategoryColor(category);
},
/**
* @override
* @param {number} index
* @return {?string}
*/
entryTitle: function(index)
{
var request = /** @type {!WebInspector.TimelineModel.NetworkRequest} */ (this._requests[index]);
return request.url || null;
},
/**
* @override
* @param {number} index
* @param {!CanvasRenderingContext2D} context
* @param {?string} text
* @param {number} barX
* @param {number} barY
* @param {number} barWidth
* @param {number} barHeight
* @param {number} unclippedBarX
* @param {number} timeToPixels
* @return {boolean}
*/
decorateEntry: function(index, context, text, barX, barY, barWidth, barHeight, unclippedBarX, timeToPixels)
{
var minTransferWidthPx = 2;
var request = /** @type {!WebInspector.TimelineModel.NetworkRequest} */ (this._requests[index]);
var startTime = request.startTime;
var endTime = request.endTime;
var lastX = unclippedBarX;
context.fillStyle = "hsla(0, 0%, 100%, 0.6)";
for (var i = 0; i < request.children.length; ++i) {
var event = request.children[i];
var t0 = event.startTime;
var t1 = event.endTime || event.startTime;
var x0 = Math.floor(unclippedBarX + (t0 - startTime) * timeToPixels - 1);
var x1 = Math.floor(unclippedBarX + (t1 - startTime) * timeToPixels + 1);
if (x0 > lastX)
context.fillRect(lastX, barY, x0 - lastX, barHeight);
lastX = x1;
}
var endX = unclippedBarX + (endTime - startTime) * timeToPixels;
if (endX > lastX)
context.fillRect(lastX, barY, Math.min(endX - lastX, 1e5), barHeight);
if (typeof request.priority === "string") {
var color = this._colorForPriority(request.priority);
if (color) {
context.fillStyle = "hsl(0, 0%, 100%)";
context.fillRect(barX, barY, 4, 4);
context.fillStyle = color;
context.fillRect(barX, barY, 3, 3);
}
}
return false;
},
/**
* @override
* @param {number} index
* @return {boolean}
*/
forceDecoration: function(index)
{
return true;
},
/**
* @override
* @param {number} index
* @return {?Element}
*/
prepareHighlightedEntryInfo: function(index)
{
var /** @const */ maxURLChars = 80;
var request = /** @type {!WebInspector.TimelineModel.NetworkRequest} */ (this._requests[index]);
if (!request.url)
return null;
var element = createElement("div");
var root = WebInspector.createShadowRootWithCoreStyles(element, "timeline/timelineFlamechartPopover.css");
var contents = root.createChild("div", "timeline-flamechart-popover");
var duration = request.endTime - request.startTime;
if (request.startTime && isFinite(duration))
contents.createChild("span", "timeline-info-network-time").textContent = Number.millisToString(duration);
if (typeof request.priority === "string") {
var div = contents.createChild("span");
div.textContent = WebInspector.uiLabelForPriority(/** @type {!NetworkAgent.ResourcePriority} */ (request.priority));
div.style.color = this._colorForPriority(request.priority) || "black";
}
contents.createChild("span").textContent = request.url.trimMiddle(maxURLChars);
return element;
},
/**
* @param {string} priority
* @return {?string}
*/
_colorForPriority: function(priority)
{
switch (/** @type {!NetworkAgent.ResourcePriority} */ (priority)) {
case NetworkAgent.ResourcePriority.VeryLow: return "#080";
case NetworkAgent.ResourcePriority.Low: return "#6c0";
case NetworkAgent.ResourcePriority.Medium: return "#fa0";
case NetworkAgent.ResourcePriority.High: return "#f60";
case NetworkAgent.ResourcePriority.VeryHigh: return "#f00";
}
return null;
},
/**
* @param {!Array.<!WebInspector.TracingModel.Event>} events
*/
_appendTimelineData: function(events)
{
this._minimumBoundary = this._model.minimumRecordTime();
this._maximumBoundary = this._model.maximumRecordTime();
this._timeSpan = this._model.isEmpty() ? 1000 : this._maximumBoundary - this._minimumBoundary;
this._model.networkRequests().forEach(this._appendEntry.bind(this));
this._updateTimelineData();
},
_updateTimelineData: function()
{
if (!this._timelineData)
return;
var index = -1;
var lastTime = Infinity;
for (var i = 0; i < this._requests.length; ++i) {
var r = this._requests[i];
var visible = r.startTime < this._endTime && r.endTime > this._startTime;
if (!visible) {
this._timelineData.entryLevels[i] = -1;
continue;
}
if (lastTime > r.startTime)
++index;
lastTime = r.endTime;
this._timelineData.entryLevels[i] = index;
}
++index;
for (var i = 0; i < this._requests.length; ++i) {
if (this._timelineData.entryLevels[i] === -1)
this._timelineData.entryLevels[i] = index;
}
this._timelineData = new WebInspector.FlameChart.TimelineData(
this._timelineData.entryLevels,
this._timelineData.entryTotalTimes,
this._timelineData.entryStartTimes,
null);
this._currentLevel = index;
},
/**
* @param {!WebInspector.TimelineModel.NetworkRequest} request
*/
_appendEntry: function(request)
{
this._requests.push(request);
this._timelineData.entryStartTimes.push(request.startTime);
this._timelineData.entryTotalTimes.push(request.endTime - request.startTime);
this._timelineData.entryLevels.push(this._requests.length - 1);
},
__proto__: WebInspector.TimelineFlameChartDataProviderBase.prototype
}
/**
* @constructor
* @implements {WebInspector.FlameChartMarker}
* @param {number} startTime
* @param {number} startOffset
* @param {!WebInspector.TimelineMarkerStyle} style
*/
WebInspector.TimelineFlameChartMarker = function(startTime, startOffset, style)
{
this._startTime = startTime;
this._startOffset = startOffset;
this._style = style;
}
WebInspector.TimelineFlameChartMarker.prototype = {
/**
* @override
* @return {number}
*/
startTime: function()
{
return this._startTime;
},
/**
* @override
* @return {string}
*/
color: function()
{
return this._style.color;
},
/**
* @override
* @return {string}
*/
title: function()
{
var startTime = Number.millisToString(this._startOffset);
return WebInspector.UIString("%s at %s", this._style.title, startTime);
},
/**
* @override
* @param {!CanvasRenderingContext2D} context
* @param {number} x
* @param {number} height
* @param {number} pixelsPerMillisecond
*/
draw: function(context, x, height, pixelsPerMillisecond)
{
var lowPriorityVisibilityThresholdInPixelsPerMs = 4;
if (this._style.lowPriority && pixelsPerMillisecond < lowPriorityVisibilityThresholdInPixelsPerMs)
return;
context.save();
if (!this._style.lowPriority) {
context.strokeStyle = this._style.color;
context.lineWidth = 2;
context.beginPath();
context.moveTo(x, 0);
context.lineTo(x, height);
context.stroke();
}
if (this._style.tall) {
context.strokeStyle = this._style.color;
context.lineWidth = this._style.lineWidth;
context.translate(this._style.lineWidth < 1 || (this._style.lineWidth & 1) ? 0.5 : 0, 0.5);
context.beginPath();
context.moveTo(x, height);
context.setLineDash(this._style.dashStyle);
context.lineTo(x, context.canvas.height);
context.stroke();
}
context.restore();
}
}
/**
* @constructor
* @extends {WebInspector.VBox}
* @implements {WebInspector.TimelineModeView}
* @implements {WebInspector.FlameChartDelegate}
* @param {!WebInspector.TimelineModeViewDelegate} delegate
* @param {!WebInspector.TimelineModel} timelineModel
* @param {!WebInspector.TimelineFrameModel} frameModel
* @param {!WebInspector.TimelineIRModel} irModel
* @param {!Array<!WebInspector.TimelineModel.Filter>} filters
*/
WebInspector.TimelineFlameChartView = function(delegate, timelineModel, frameModel, irModel, filters)
{
WebInspector.VBox.call(this);
this.element.classList.add("timeline-flamechart");
this._delegate = delegate;
this._model = timelineModel;
this._splitWidget = new WebInspector.SplitWidget(false, false, "timelineFlamechartMainView", 150);
this._dataProvider = new WebInspector.TimelineFlameChartDataProvider(this._model, frameModel, irModel, filters);
var mainViewGroupExpansionSetting = WebInspector.settings.createSetting("timelineFlamechartMainViewGroupExpansion", {});
this._mainView = new WebInspector.FlameChart(this._dataProvider, this, mainViewGroupExpansionSetting);
this._networkDataProvider = new WebInspector.TimelineFlameChartNetworkDataProvider(this._model);
this._networkView = new WebInspector.FlameChart(this._networkDataProvider, this);
this._splitWidget.setMainWidget(this._mainView);
this._splitWidget.setSidebarWidget(this._networkView);
this._splitWidget.show(this.element);
this._onMainEntrySelected = this._onEntrySelected.bind(this, this._dataProvider);
this._onNetworkEntrySelected = this._onEntrySelected.bind(this, this._networkDataProvider);
this._mainView.addEventListener(WebInspector.FlameChart.Events.EntrySelected, this._onMainEntrySelected, this);
this._networkView.addEventListener(WebInspector.FlameChart.Events.EntrySelected, this._onNetworkEntrySelected, this);
WebInspector.blackboxManager.addChangeListener(this.refreshRecords, this);
}
WebInspector.TimelineFlameChartView.prototype = {
/**
* @override
*/
dispose: function()
{
this._mainView.removeEventListener(WebInspector.FlameChart.Events.EntrySelected, this._onMainEntrySelected, this);
this._networkView.removeEventListener(WebInspector.FlameChart.Events.EntrySelected, this._onNetworkEntrySelected, this);
WebInspector.blackboxManager.removeChangeListener(this.refreshRecords, this);
},
/**
* @override
* @return {?Element}
*/
resizerElement: function()
{
return null;
},
/**
* @override
* @param {number} windowStartTime
* @param {number} windowEndTime
*/
requestWindowTimes: function(windowStartTime, windowEndTime)
{
this._delegate.requestWindowTimes(windowStartTime, windowEndTime);
},
/**
* @override
* @param {number} startTime
* @param {number} endTime
*/
updateRangeSelection: function(startTime, endTime)
{
this._delegate.select(WebInspector.TimelineSelection.fromRange(startTime, endTime));
},
/**
* @override
*/
refreshRecords: function()
{
this._dataProvider.reset();
this._mainView.scheduleUpdate();
this._networkDataProvider.reset();
this._networkView.scheduleUpdate();
},
/**
* @override
* @param {?WebInspector.TracingModel.Event} event
*/
highlightEvent: function(event)
{
var entryIndex = event ? this._dataProvider.entryIndexForSelection(WebInspector.TimelineSelection.fromTraceEvent(event)) : -1;
if (entryIndex >= 0)
this._mainView.highlightEntry(entryIndex);
else
this._mainView.hideHighlight();
},
/**
* @override
*/
wasShown: function()
{
this._mainView.scheduleUpdate();
this._networkView.scheduleUpdate();
},
/**
* @override
* @return {!WebInspector.Widget}
*/
view: function()
{
return this;
},
/**
* @override
*/
reset: function()
{
this._dataProvider.reset();
this._mainView.reset();
this._mainView.setWindowTimes(0, Infinity);
this._networkDataProvider.reset();
this._networkView.reset();
this._networkView.setWindowTimes(0, Infinity);
},
/**
* @override
* @param {number} startTime
* @param {number} endTime
*/
setWindowTimes: function(startTime, endTime)
{
this._mainView.setWindowTimes(startTime, endTime);
this._networkView.setWindowTimes(startTime, endTime);
this._networkDataProvider.setWindowTimes(startTime, endTime);
},
/**
* @override
* @param {?WebInspector.TracingModel.Event} event
* @param {string=} regex
* @param {boolean=} select
*/
highlightSearchResult: function(event, regex, select)
{
if (!event) {
this._delegate.select(null);
return;
}
var entryIndex = this._dataProvider._entryData.indexOf(event);
var timelineSelection = this._dataProvider.createSelection(entryIndex);
if (timelineSelection)
this._delegate.select(timelineSelection);
},
/**
* @override
* @param {?WebInspector.TimelineSelection} selection
*/
setSelection: function(selection)
{
var index = this._dataProvider.entryIndexForSelection(selection);
this._mainView.setSelectedEntry(index);
index = this._networkDataProvider.entryIndexForSelection(selection);
this._networkView.setSelectedEntry(index);
},
/**
* @param {!WebInspector.FlameChartDataProvider} dataProvider
* @param {!WebInspector.Event} event
*/
_onEntrySelected: function(dataProvider, event)
{
var entryIndex = /** @type{number} */ (event.data);
this._delegate.select(dataProvider.createSelection(entryIndex));
},
/**
* @param {boolean} enable
* @param {boolean=} animate
*/
enableNetworkPane: function(enable, animate)
{
if (enable)
this._splitWidget.showBoth(animate);
else
this._splitWidget.hideSidebar(animate);
},
__proto__: WebInspector.VBox.prototype
}
/**
* @constructor
* @param {!WebInspector.TimelineSelection} selection
* @param {number} entryIndex
*/
WebInspector.TimelineFlameChartView.Selection = function(selection, entryIndex)
{
this.timelineSelection = selection;
this.entryIndex = entryIndex;
}