blob: 82f5c6c363617cd91c0439360e69276cdb22f742 [file] [log] [blame]
/*
* Copyright (C) 2010 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.
*/
/**
* @unrestricted
*/
export default class RequestTimingView extends UI.VBox {
/**
* @param {!SDK.NetworkRequest} request
* @param {!Network.NetworkTimeCalculator} calculator
*/
constructor(request, calculator) {
super();
this.element.classList.add('resource-timing-view');
this._request = request;
this._calculator = calculator;
}
/**
* @param {!RequestTimeRangeNames} name
* @return {string}
*/
static _timeRangeTitle(name) {
switch (name) {
case RequestTimeRangeNames.Push:
return Common.UIString('Receiving Push');
case RequestTimeRangeNames.Queueing:
return Common.UIString('Queueing');
case RequestTimeRangeNames.Blocking:
return Common.UIString('Stalled');
case RequestTimeRangeNames.Connecting:
return Common.UIString('Initial connection');
case RequestTimeRangeNames.DNS:
return Common.UIString('DNS Lookup');
case RequestTimeRangeNames.Proxy:
return Common.UIString('Proxy negotiation');
case RequestTimeRangeNames.ReceivingPush:
return Common.UIString('Reading Push');
case RequestTimeRangeNames.Receiving:
return Common.UIString('Content Download');
case RequestTimeRangeNames.Sending:
return Common.UIString('Request sent');
case RequestTimeRangeNames.ServiceWorker:
return Common.UIString('Request to ServiceWorker');
case RequestTimeRangeNames.ServiceWorkerPreparation:
return Common.UIString('ServiceWorker Preparation');
case RequestTimeRangeNames.SSL:
return Common.UIString('SSL');
case RequestTimeRangeNames.Total:
return Common.UIString('Total');
case RequestTimeRangeNames.Waiting:
return Common.UIString('Waiting (TTFB)');
default:
return Common.UIString(name);
}
}
/**
* @param {!SDK.NetworkRequest} request
* @param {number} navigationStart
* @return {!Array.<!Network.RequestTimeRange>}
*/
static calculateRequestTimeRanges(request, navigationStart) {
const result = [];
/**
* @param {!RequestTimeRangeNames} name
* @param {number} start
* @param {number} end
*/
function addRange(name, start, end) {
if (start < Number.MAX_VALUE && start <= end) {
result.push({name: name, start: start, end: end});
}
}
/**
* @param {!Array.<number>} numbers
* @return {number|undefined}
*/
function firstPositive(numbers) {
for (let i = 0; i < numbers.length; ++i) {
if (numbers[i] > 0) {
return numbers[i];
}
}
return undefined;
}
/**
* @param {!RequestTimeRangeNames} name
* @param {number} start
* @param {number} end
*/
function addOffsetRange(name, start, end) {
if (start >= 0 && end >= 0) {
addRange(name, startTime + (start / 1000), startTime + (end / 1000));
}
}
const timing = request.timing;
if (!timing) {
const start = request.issueTime() !== -1 ? request.issueTime() : request.startTime !== -1 ? request.startTime : 0;
const middle = (request.responseReceivedTime === -1) ? Number.MAX_VALUE : request.responseReceivedTime;
const end = (request.endTime === -1) ? Number.MAX_VALUE : request.endTime;
addRange(RequestTimeRangeNames.Total, start, end);
addRange(RequestTimeRangeNames.Blocking, start, middle);
addRange(RequestTimeRangeNames.Receiving, middle, end);
return result;
}
const issueTime = request.issueTime();
const startTime = timing.requestTime;
const endTime = firstPositive([request.endTime, request.responseReceivedTime]) || startTime;
addRange(RequestTimeRangeNames.Total, issueTime < startTime ? issueTime : startTime, endTime);
if (timing.pushStart) {
const pushEnd = timing.pushEnd || endTime;
// Only show the part of push that happened after the navigation/reload.
// Pushes that happened on the same connection before we started main request will not be shown.
if (pushEnd > navigationStart) {
addRange(RequestTimeRangeNames.Push, Math.max(timing.pushStart, navigationStart), pushEnd);
}
}
if (issueTime < startTime) {
addRange(RequestTimeRangeNames.Queueing, issueTime, startTime);
}
const responseReceived = (request.responseReceivedTime - startTime) * 1000;
if (request.fetchedViaServiceWorker) {
addOffsetRange(RequestTimeRangeNames.Blocking, 0, timing.workerStart);
addOffsetRange(RequestTimeRangeNames.ServiceWorkerPreparation, timing.workerStart, timing.workerReady);
addOffsetRange(RequestTimeRangeNames.ServiceWorker, timing.workerReady, timing.sendEnd);
addOffsetRange(RequestTimeRangeNames.Waiting, timing.sendEnd, responseReceived);
} else if (!timing.pushStart) {
const blockingEnd =
firstPositive([timing.dnsStart, timing.connectStart, timing.sendStart, responseReceived]) || 0;
addOffsetRange(RequestTimeRangeNames.Blocking, 0, blockingEnd);
addOffsetRange(RequestTimeRangeNames.Proxy, timing.proxyStart, timing.proxyEnd);
addOffsetRange(RequestTimeRangeNames.DNS, timing.dnsStart, timing.dnsEnd);
addOffsetRange(RequestTimeRangeNames.Connecting, timing.connectStart, timing.connectEnd);
addOffsetRange(RequestTimeRangeNames.SSL, timing.sslStart, timing.sslEnd);
addOffsetRange(RequestTimeRangeNames.Sending, timing.sendStart, timing.sendEnd);
addOffsetRange(
RequestTimeRangeNames.Waiting,
Math.max(timing.sendEnd, timing.connectEnd, timing.dnsEnd, timing.proxyEnd, blockingEnd), responseReceived);
}
if (request.endTime !== -1) {
addRange(
timing.pushStart ? RequestTimeRangeNames.ReceivingPush : RequestTimeRangeNames.Receiving,
request.responseReceivedTime, endTime);
}
return result;
}
/**
* @param {!SDK.NetworkRequest} request
* @param {!Network.NetworkTimeCalculator} calculator
* @return {!Element}
*/
static createTimingTable(request, calculator) {
const tableElement = createElementWithClass('table', 'network-timing-table');
UI.appendStyle(tableElement, 'network/networkTimingTable.css');
const colgroup = tableElement.createChild('colgroup');
colgroup.createChild('col', 'labels');
colgroup.createChild('col', 'bars');
colgroup.createChild('col', 'duration');
const timeRanges = RequestTimingView.calculateRequestTimeRanges(request, calculator.minimumBoundary());
const startTime = timeRanges.map(r => r.start).reduce((a, b) => Math.min(a, b));
const endTime = timeRanges.map(r => r.end).reduce((a, b) => Math.max(a, b));
const scale = 100 / (endTime - startTime);
let connectionHeader;
let dataHeader;
let queueingHeader;
let totalDuration = 0;
const startTimeHeader = tableElement.createChild('thead', 'network-timing-start');
const tableHeaderRow = startTimeHeader.createChild('tr');
const activityHeaderCell = tableHeaderRow.createChild('th');
activityHeaderCell.createChild('span', 'network-timing-hidden-header').textContent = ls`Label`;
activityHeaderCell.scope = 'col';
const waterfallHeaderCell = tableHeaderRow.createChild('th');
waterfallHeaderCell.createChild('span', 'network-timing-hidden-header').textContent = ls`Waterfall`;
waterfallHeaderCell.scope = 'col';
const durationHeaderCell = tableHeaderRow.createChild('th');
durationHeaderCell.createChild('span', 'network-timing-hidden-header').textContent = ls`Duration`;
durationHeaderCell.scope = 'col';
const queuedCell = startTimeHeader.createChild('tr').createChild('td');
const startedCell = startTimeHeader.createChild('tr').createChild('td');
queuedCell.colSpan = startedCell.colSpan = 3;
queuedCell.createTextChild(Common.UIString('Queued at %s', calculator.formatValue(request.issueTime(), 2)));
startedCell.createTextChild(Common.UIString('Started at %s', calculator.formatValue(request.startTime, 2)));
let right;
for (let i = 0; i < timeRanges.length; ++i) {
const range = timeRanges[i];
const rangeName = range.name;
if (rangeName === RequestTimeRangeNames.Total) {
totalDuration = range.end - range.start;
continue;
}
if (rangeName === RequestTimeRangeNames.Push) {
createHeader(Common.UIString('Server Push'));
} else if (rangeName === RequestTimeRangeNames.Queueing) {
if (!queueingHeader) {
queueingHeader = createHeader(ls`Resource Scheduling`);
}
} else if (ConnectionSetupRangeNames.has(rangeName)) {
if (!connectionHeader) {
connectionHeader = createHeader(Common.UIString('Connection Start'));
}
} else {
if (!dataHeader) {
dataHeader = createHeader(Common.UIString('Request/Response'));
}
}
const left = (scale * (range.start - startTime));
right = (scale * (endTime - range.end));
const duration = range.end - range.start;
const tr = tableElement.createChild('tr');
tr.createChild('td').createTextChild(RequestTimingView._timeRangeTitle(rangeName));
const row = tr.createChild('td').createChild('div', 'network-timing-row');
const bar = row.createChild('span', 'network-timing-bar ' + rangeName);
bar.style.left = left + '%';
bar.style.right = right + '%';
bar.textContent = '\u200B'; // Important for 0-time items to have 0 width.
UI.ARIAUtils.setAccessibleName(row, ls`Started at ${calculator.formatValue(range.start, 2)}`);
const label = tr.createChild('td').createChild('div', 'network-timing-bar-title');
label.textContent = Number.secondsToString(duration, true);
}
if (!request.finished) {
const cell = tableElement.createChild('tr').createChild('td', 'caution');
cell.colSpan = 3;
cell.createTextChild(Common.UIString('CAUTION: request is not finished yet!'));
}
const footer = tableElement.createChild('tr', 'network-timing-footer');
const note = footer.createChild('td');
note.colSpan = 1;
note.appendChild(
UI.createDocumentationLink('network-performance/reference#timing-explanation', Common.UIString('Explanation')));
footer.createChild('td');
footer.createChild('td').createTextChild(Number.secondsToString(totalDuration, true));
const serverTimings = request.serverTimings;
if (!serverTimings) {
return tableElement;
}
const lastTimingRightEdge = right === undefined ? 100 : right;
const breakElement = tableElement.createChild('tr', 'network-timing-table-header').createChild('td');
breakElement.colSpan = 3;
breakElement.createChild('hr', 'break');
const serverHeader = tableElement.createChild('tr', 'network-timing-table-header');
serverHeader.createChild('td').createTextChild(Common.UIString('Server Timing'));
serverHeader.createChild('td');
serverHeader.createChild('td').createTextChild(Common.UIString('TIME'));
serverTimings.filter(item => item.metric.toLowerCase() !== 'total')
.forEach(item => addTiming(item, lastTimingRightEdge));
serverTimings.filter(item => item.metric.toLowerCase() === 'total')
.forEach(item => addTiming(item, lastTimingRightEdge));
return tableElement;
/**
* @param {!SDK.ServerTiming} serverTiming
* @param {number} right
*/
function addTiming(serverTiming, right) {
const colorGenerator = new Common.Color.Generator({min: 0, max: 360, count: 36}, {min: 50, max: 80}, 80);
const isTotal = serverTiming.metric.toLowerCase() === 'total';
const tr = tableElement.createChild('tr', isTotal ? 'network-timing-footer' : '');
const metric = tr.createChild('td', 'network-timing-metric');
const description = serverTiming.description || serverTiming.metric;
metric.createTextChild(description);
metric.title = description;
const row = tr.createChild('td').createChild('div', 'network-timing-row');
if (serverTiming.value === null) {
return;
}
const left = scale * (endTime - startTime - (serverTiming.value / 1000));
if (left >= 0) { // don't chart values too big or too small
const bar = row.createChild('span', 'network-timing-bar server-timing');
bar.style.left = left + '%';
bar.style.right = right + '%';
bar.textContent = '\u200B'; // Important for 0-time items to have 0 width.
if (!isTotal) {
bar.style.backgroundColor = colorGenerator.colorForID(serverTiming.metric);
}
}
const label = tr.createChild('td').createChild('div', 'network-timing-bar-title');
label.textContent = Number.millisToString(serverTiming.value, true);
}
/**
* @param {string} title
* @return {!Element}
*/
function createHeader(title) {
const dataHeader = tableElement.createChild('tr', 'network-timing-table-header');
const headerCell = dataHeader.createChild('td');
headerCell.createTextChild(title);
UI.ARIAUtils.markAsHeading(headerCell, 2);
dataHeader.createChild('td').createTextChild('');
dataHeader.createChild('td').createTextChild(ls`DURATION`);
return dataHeader;
}
}
/**
* @override
*/
wasShown() {
this._request.addEventListener(SDK.NetworkRequest.Events.TimingChanged, this._refresh, this);
this._request.addEventListener(SDK.NetworkRequest.Events.FinishedLoading, this._refresh, this);
this._calculator.addEventListener(Network.NetworkTimeCalculator.Events.BoundariesChanged, this._refresh, this);
this._refresh();
}
/**
* @override
*/
willHide() {
this._request.removeEventListener(SDK.NetworkRequest.Events.TimingChanged, this._refresh, this);
this._request.removeEventListener(SDK.NetworkRequest.Events.FinishedLoading, this._refresh, this);
this._calculator.removeEventListener(Network.NetworkTimeCalculator.Events.BoundariesChanged, this._refresh, this);
}
_refresh() {
if (this._tableElement) {
this._tableElement.remove();
}
this._tableElement = RequestTimingView.createTimingTable(this._request, this._calculator);
this._tableElement.classList.add('resource-timing-table');
this.element.appendChild(this._tableElement);
}
}
/** @enum {string} */
export const RequestTimeRangeNames = {
Push: 'push',
Queueing: 'queueing',
Blocking: 'blocking',
Connecting: 'connecting',
DNS: 'dns',
Proxy: 'proxy',
Receiving: 'receiving',
ReceivingPush: 'receiving-push',
Sending: 'sending',
ServiceWorker: 'serviceworker',
ServiceWorkerPreparation: 'serviceworker-preparation',
SSL: 'ssl',
Total: 'total',
Waiting: 'waiting'
};
export const ConnectionSetupRangeNames = new Set([
RequestTimeRangeNames.Queueing, RequestTimeRangeNames.Blocking, RequestTimeRangeNames.Connecting,
RequestTimeRangeNames.DNS, RequestTimeRangeNames.Proxy, RequestTimeRangeNames.SSL
]);
/* Legacy exported object */
self.Network = self.Network || {};
/* Legacy exported object */
Network = Network || {};
/** @typedef {{name: !RequestTimeRangeNames, start: number, end: number}} */
Network.RequestTimeRange;
/**
* @constructor
*/
Network.RequestTimingView = RequestTimingView;
Network.RequestTimingView.ConnectionSetupRangeNames = ConnectionSetupRangeNames;
/** @enum {string} */
Network.RequestTimeRangeNames = RequestTimeRangeNames;