blob: 70a3c4bd5b691f8e5ca8de00abb387c3027a0921 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @implements {PerfUI.FlameChartDataProvider}
* @unrestricted
*/
Timeline.TimelineFlameChartNetworkDataProvider = class {
constructor() {
this._font = '11px ' + Host.fontFamily();
this.setModel(null);
this._style = {
padding: 4,
height: 17,
collapsible: true,
color: UI.themeSupport.patchColorText('#222', UI.ThemeSupport.ColorUsage.Foreground),
font: this._font,
backgroundColor: UI.themeSupport.patchColorText('white', UI.ThemeSupport.ColorUsage.Background),
nestingLevel: 0,
useFirstLineForOverview: false,
useDecoratorsForOverview: true,
shareHeaderLine: false
};
this._group = {startLevel: 0, name: Common.UIString('Network'), expanded: false, style: this._style};
this._minimumBoundary = 0;
this._maximumBoundary = 0;
this._timeSpan = 0;
}
/**
* @param {?Timeline.PerformanceModel} performanceModel
*/
setModel(performanceModel) {
this._model = performanceModel && performanceModel.timelineModel();
this._maxLevel = 0;
this._timelineData = null;
/** @type {!Array<!TimelineModel.TimelineModel.NetworkRequest>} */
this._requests = [];
}
/**
* @return {boolean}
*/
isEmpty() {
this.timelineData();
return !this._requests.length;
}
/**
* @override
* @return {number}
*/
maxStackDepth() {
return this._maxLevel;
}
/**
* @override
* @return {!PerfUI.FlameChart.TimelineData}
*/
timelineData() {
if (this._timelineData) {
return this._timelineData;
}
/** @type {!Array<!TimelineModel.TimelineModel.NetworkRequest>} */
this._requests = [];
this._timelineData = new PerfUI.FlameChart.TimelineData([], [], [], []);
if (this._model) {
this._appendTimelineData();
}
return this._timelineData;
}
/**
* @override
* @return {number}
*/
minimumBoundary() {
return this._minimumBoundary;
}
/**
* @override
* @return {number}
*/
totalTime() {
return this._timeSpan;
}
/**
* @param {number} startTime
* @param {number} endTime
*/
setWindowTimes(startTime, endTime) {
this._startTime = startTime;
this._endTime = endTime;
this._updateTimelineData();
}
/**
* @param {number} index
* @return {?Timeline.TimelineSelection}
*/
createSelection(index) {
if (index === -1) {
return null;
}
const request = this._requests[index];
this._lastSelection =
new Timeline.TimelineFlameChartView.Selection(Timeline.TimelineSelection.fromNetworkRequest(request), index);
return this._lastSelection.timelineSelection;
}
/**
* @param {?Timeline.TimelineSelection} selection
* @return {number}
*/
entryIndexForSelection(selection) {
if (!selection) {
return -1;
}
if (this._lastSelection && this._lastSelection.timelineSelection.object() === selection.object()) {
return this._lastSelection.entryIndex;
}
if (selection.type() !== Timeline.TimelineSelection.Type.NetworkRequest) {
return -1;
}
const request = /** @type{!TimelineModel.TimelineModel.NetworkRequest} */ (selection.object());
const index = this._requests.indexOf(request);
if (index !== -1) {
this._lastSelection =
new Timeline.TimelineFlameChartView.Selection(Timeline.TimelineSelection.fromNetworkRequest(request), index);
}
return index;
}
/**
* @override
* @param {number} index
* @return {string}
*/
entryColor(index) {
const request = /** @type {!TimelineModel.TimelineModel.NetworkRequest} */ (this._requests[index]);
const category = Timeline.TimelineUIUtils.networkRequestCategory(request);
return Timeline.TimelineUIUtils.networkCategoryColor(category);
}
/**
* @override
* @param {number} index
* @return {string}
*/
textColor(index) {
return Timeline.FlameChartStyle.textColor;
}
/**
* @override
* @param {number} index
* @return {?string}
*/
entryTitle(index) {
const request = /** @type {!TimelineModel.TimelineModel.NetworkRequest} */ (this._requests[index]);
const parsedURL = new Common.ParsedURL(request.url || '');
return parsedURL.isValid ? `${parsedURL.displayName} (${parsedURL.host})` : request.url || null;
}
/**
* @override
* @param {number} index
* @return {?string}
*/
entryFont(index) {
return this._font;
}
/**
* @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} timeToPixelRatio
* @return {boolean}
*/
decorateEntry(index, context, text, barX, barY, barWidth, barHeight, unclippedBarX, timeToPixelRatio) {
const request = /** @type {!TimelineModel.TimelineModel.NetworkRequest} */ (this._requests[index]);
if (!request.timing) {
return false;
}
const beginTime = request.beginTime();
/**
* @param {number} time
* @return {number}
*/
const timeToPixel = time => Math.floor(unclippedBarX + (time - beginTime) * timeToPixelRatio);
const minBarWidthPx = 2;
const startTime = request.getStartTime();
const endTime = request.endTime;
const {sendStartTime, headersEndTime} = request.getSendReceiveTiming();
const sendStart = Math.max(timeToPixel(sendStartTime), unclippedBarX);
const headersEnd = Math.max(timeToPixel(headersEndTime), sendStart);
const finish = Math.max(timeToPixel(request.finishTime || endTime), headersEnd + minBarWidthPx);
const start = timeToPixel(startTime);
const end = Math.max(timeToPixel(endTime), finish);
// Draw waiting time.
context.fillStyle = 'hsla(0, 100%, 100%, 0.8)';
context.fillRect(sendStart + 0.5, barY + 0.5, headersEnd - sendStart - 0.5, barHeight - 2);
// Clear portions of initial rect to prepare for the ticks.
context.fillStyle = UI.themeSupport.patchColorText('white', UI.ThemeSupport.ColorUsage.Background);
context.fillRect(barX, barY - 0.5, sendStart - barX, barHeight);
context.fillRect(finish, barY - 0.5, barX + barWidth - finish, barHeight);
// If the request is from cache, pushStart refers to the original request, and hence cannot be used.
if (!request.cached() && request.timing.pushStart) {
const pushStart = timeToPixel(request.timing.pushStart * 1000);
const pushEnd = request.timing.pushEnd ? timeToPixel(request.timing.pushEnd * 1000) : start;
const dentSize = Number.constrain(pushEnd - pushStart - 2, 0, 4);
const padding = 1;
context.save();
context.beginPath();
context.moveTo(pushStart + dentSize, barY + barHeight / 2);
context.lineTo(pushStart, barY + padding);
context.lineTo(pushEnd - dentSize, barY + padding);
context.lineTo(pushEnd, barY + barHeight / 2);
context.lineTo(pushEnd - dentSize, barY + barHeight - padding);
context.lineTo(pushStart, barY + barHeight - padding);
context.closePath();
if (request.timing.pushEnd) {
context.fillStyle = this.entryColor(index);
} else {
// Use a gradient to indicate that `pushEnd` is not known here to work
// around BUG(chromium:998411).
const gradient = context.createLinearGradient(pushStart, 0, pushEnd, 0);
gradient.addColorStop(0, this.entryColor(index));
gradient.addColorStop(1, 'white');
context.fillStyle = gradient;
}
context.globalAlpha = 0.3;
context.fill();
context.restore();
}
/**
* @param {number} begin
* @param {number} end
* @param {number} y
*/
function drawTick(begin, end, y) {
const /** @const */ tickHeightPx = 6;
context.moveTo(begin, y - tickHeightPx / 2);
context.lineTo(begin, y + tickHeightPx / 2);
context.moveTo(begin, y);
context.lineTo(end, y);
}
context.beginPath();
context.lineWidth = 1;
context.strokeStyle = '#ccc';
const lineY = Math.floor(barY + barHeight / 2) + 0.5;
const leftTick = start + 0.5;
const rightTick = end - 0.5;
drawTick(leftTick, sendStart, lineY);
drawTick(rightTick, finish, lineY);
context.stroke();
if (typeof request.priority === 'string') {
const color = this._colorForPriority(request.priority);
if (color) {
context.fillStyle = color;
context.fillRect(sendStart + 0.5, barY + 0.5, 3.5, 3.5);
}
}
const textStart = Math.max(sendStart, 0);
const textWidth = finish - textStart;
const /** @const */ minTextWidthPx = 20;
if (textWidth >= minTextWidthPx) {
text = this.entryTitle(index) || '';
if (request.fromServiceWorker) {
text = '⚙ ' + text;
}
if (text) {
const /** @const */ textPadding = 4;
const /** @const */ textBaseline = 5;
const textBaseHeight = barHeight - textBaseline;
const trimmedText = UI.trimTextEnd(context, text, textWidth - 2 * textPadding);
context.fillStyle = '#333';
context.fillText(trimmedText, textStart + textPadding, barY + textBaseHeight);
}
}
return true;
}
/**
* @override
* @param {number} index
* @return {boolean}
*/
forceDecoration(index) {
return true;
}
/**
* @override
* @param {number} index
* @return {?Element}
*/
prepareHighlightedEntryInfo(index) {
const /** @const */ maxURLChars = 80;
const request = /** @type {!TimelineModel.TimelineModel.NetworkRequest} */ (this._requests[index]);
if (!request.url) {
return null;
}
const element = createElement('div');
const root = UI.createShadowRootWithCoreStyles(element, 'timeline/timelineFlamechartPopover.css');
const contents = root.createChild('div', 'timeline-flamechart-popover');
const startTime = request.getStartTime();
const duration = request.endTime - startTime;
if (startTime && isFinite(duration)) {
contents.createChild('span', 'timeline-info-network-time').textContent = Number.millisToString(duration, true);
}
if (typeof request.priority === 'string') {
const div = contents.createChild('span');
div.textContent =
PerfUI.uiLabelForNetworkPriority(/** @type {!Protocol.Network.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(priority) {
if (!this._priorityToValue) {
const priorities = Protocol.Network.ResourcePriority;
this._priorityToValue = new Map([
[priorities.VeryLow, 1], [priorities.Low, 2], [priorities.Medium, 3], [priorities.High, 4],
[priorities.VeryHigh, 5]
]);
}
const value = this._priorityToValue.get(priority);
return value ? `hsla(214, 80%, 50%, ${value / 5})` : null;
}
_appendTimelineData() {
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() {
if (!this._timelineData) {
return;
}
const lastTimeByLevel = [];
let maxLevel = 0;
for (let i = 0; i < this._requests.length; ++i) {
const r = this._requests[i];
const beginTime = r.beginTime();
const visible = beginTime < this._endTime && r.endTime > this._startTime;
if (!visible) {
this._timelineData.entryLevels[i] = -1;
continue;
}
while (lastTimeByLevel.length && lastTimeByLevel.peekLast() <= beginTime) {
lastTimeByLevel.pop();
}
this._timelineData.entryLevels[i] = lastTimeByLevel.length;
lastTimeByLevel.push(r.endTime);
maxLevel = Math.max(maxLevel, lastTimeByLevel.length);
}
for (let i = 0; i < this._requests.length; ++i) {
if (this._timelineData.entryLevels[i] === -1) {
this._timelineData.entryLevels[i] = maxLevel;
}
}
this._timelineData = new PerfUI.FlameChart.TimelineData(
this._timelineData.entryLevels, this._timelineData.entryTotalTimes, this._timelineData.entryStartTimes,
[this._group]);
this._maxLevel = maxLevel;
}
/**
* @param {!TimelineModel.TimelineModel.NetworkRequest} request
*/
_appendEntry(request) {
this._requests.push(request);
this._timelineData.entryStartTimes.push(request.beginTime());
this._timelineData.entryTotalTimes.push(request.endTime - request.beginTime());
this._timelineData.entryLevels.push(this._requests.length - 1);
}
/**
* @return {number}
*/
preferredHeight() {
return this._style.height * (this._group.expanded ? Number.constrain(this._maxLevel + 1, 4, 8.5) : 1);
}
/**
* @return {boolean}
*/
isExpanded() {
return this._group.expanded;
}
/**
* @override
* @param {number} value
* @param {number=} precision
* @return {string}
*/
formatValue(value, precision) {
return Number.preciseMillisToString(value, precision);
}
/**
* @override
* @param {number} entryIndex
* @return {boolean}
*/
canJumpToEntry(entryIndex) {
return false;
}
};