blob: 8a2a0fd572c282b8236944045822a09dd20674bb [file] [log] [blame]
// Copyright 2013 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.
var WaterfallRow = (function() {
'use strict';
/**
* A WaterfallRow represents the row corresponding to a single SourceEntry
* displayed by the EventsWaterfallView.
*
* @constructor
*/
// TODO(viona):
// -Support nested events.
// -Handle updating length when an event is stalled.
function WaterfallRow(parentView, sourceEntry) {
this.parentView_ = parentView;
this.sourceEntry_ = sourceEntry;
this.description_ = sourceEntry.getDescription();
this.createRow_();
}
// Offset of popup from mouse location.
var POPUP_OFFSET_FROM_CURSOR = 25;
WaterfallRow.prototype = {
onSourceUpdated: function() {
this.updateRow();
},
updateRow: function() {
var scale = this.parentView_.getScaleFactor();
// In some cases, the REQUEST_ALIVE event has been received, while the
// URL Request to start the job has not been received. In that case, the
// description obtained is incorrect. The following fixes that.
if (this.description_ == '') {
this.sourceCell_.innerHTML = '';
this.description_ = this.sourceEntry_.getDescription();
addTextNode(this.sourceCell_, this.description_);
}
this.rowCell_.innerHTML = '';
var matchingEventPairs =
WaterfallRow.findUrlRequestEvents(this.sourceEntry_);
// Creates the spacing in the beginning to show start time.
var startTime = this.parentView_.getStartTime();
var sourceEntryStartTime = this.getStartTime();
var delay = sourceEntryStartTime - startTime;
var frontNode = addNode(this.rowCell_, 'div');
frontNode.classList.add('waterfall-view-padding');
setNodeWidth(frontNode, delay * scale);
var barCell = addNode(this.rowCell_, 'div');
barCell.classList.add('waterfall-view-bar');
if (this.sourceEntry_.isError()) {
barCell.classList.add('error');
}
var currentEnd = sourceEntryStartTime;
for (var i = 0; i < matchingEventPairs.length; ++i) {
var event = matchingEventPairs[i];
var startTicks = event.startEntry.time;
var endTicks = event.endEntry.time;
event.eventType = event.startEntry.type;
event.startTime = timeutil.convertTimeTicksToTime(startTicks);
event.endTime = timeutil.convertTimeTicksToTime(endTicks);
event.eventDuration = event.endTime - event.startTime;
// Handles the spaces between events.
if (currentEnd < event.startTime) {
var eventDuration = event.startTime - currentEnd;
var padNode = this.createNode_(
barCell, eventDuration, this.sourceTypeString_, 'source');
}
// Creates event bars.
var eventNode = this.createNode_(
barCell, event.eventDuration, EventTypeNames[event.eventType],
event);
currentEnd = event.startTime + event.eventDuration;
}
// Creates a bar for the part after the last event.
if (this.getEndTime() > currentEnd) {
var endDuration = (this.getEndTime() - currentEnd);
var endNode = this.createNode_(
barCell, endDuration, this.sourceTypeString_, 'source');
}
},
getStartTime: function() {
return this.sourceEntry_.getStartTime();
},
getEndTime: function() {
return this.sourceEntry_.getEndTime();
},
clearPopup_: function(parentNode) {
parentNode.innerHTML = '';
},
createPopup_: function(parentNode, event, eventType, duration, mouse) {
var tableStart = this.parentView_.getStartTime();
var newPopup = addNode(parentNode, 'div');
newPopup.classList.add('waterfall-view-popup');
var popupList = addNode(newPopup, 'ul');
popupList.classList.add('waterfall-view-popup-list');
popupList.style.maxWidth =
$(WaterfallView.MAIN_BOX_ID).offsetWidth * 0.5 + 'px';
this.createPopupItem_(
popupList, 'Has Error', this.sourceEntry_.isError());
this.createPopupItem_(
popupList, 'Event Type', eventType);
if (event != 'source') {
this.createPopupItem_(
popupList, 'Event Duration', duration.toFixed(0) + 'ms');
this.createPopupItem_(
popupList, 'Event Start Time', event.startTime - tableStart + 'ms');
this.createPopupItem_(
popupList, 'Event End Time', event.endTime - tableStart + 'ms');
}
this.createPopupItem_(
popupList, 'Source Duration',
this.getEndTime() - this.getStartTime() + 'ms');
this.createPopupItem_(
popupList, 'Source Start Time',
this.getStartTime() - tableStart + 'ms');
this.createPopupItem_(
popupList, 'Source End Time', this.getEndTime() - tableStart + 'ms');
this.createPopupItem_(
popupList, 'Source ID', this.sourceEntry_.getSourceId());
var urlListItem = this.createPopupItem_(
popupList, 'Source Description', this.description_);
urlListItem.classList.add('waterfall-view-popup-list-url-item');
// Fixes cases where the popup appears 'off-screen'.
var popupLeft = mouse.pageX - newPopup.offsetWidth;
if (popupLeft < 0) {
popupLeft = mouse.pageX;
}
newPopup.style.left = popupLeft +
$(WaterfallView.MAIN_BOX_ID).scrollLeft -
$(WaterfallView.BAR_TABLE_ID).offsetLeft + 'px';
var popupTop = mouse.pageY - newPopup.offsetHeight -
POPUP_OFFSET_FROM_CURSOR;
if (popupTop < 0) {
popupTop = mouse.pageY;
}
newPopup.style.top = popupTop +
$(WaterfallView.MAIN_BOX_ID).scrollTop -
$(WaterfallView.BAR_TABLE_ID).offsetTop + 'px';
},
createPopupItem_: function(parentPopup, key, popupInformation) {
var popupItem = addNode(parentPopup, 'li');
addTextNode(popupItem, key + ': ' + popupInformation);
return popupItem;
},
createRow_: function() {
// Create a row.
var tr = addNode($(WaterfallView.BAR_TBODY_ID), 'tr');
tr.classList.add('waterfall-view-table-row');
// Creates the color bar.
var rowCell = addNode(tr, 'td');
rowCell.classList.add('waterfall-view-row');
this.rowCell_ = rowCell;
this.sourceTypeString_ = this.sourceEntry_.getSourceTypeString();
var infoTr = addNode($(WaterfallView.INFO_TBODY_ID), 'tr');
infoTr.classList.add('waterfall-view-information-row');
var idCell = addNode(infoTr, 'td');
idCell.classList.add('waterfall-view-id-cell');
var idValue = this.sourceEntry_.getSourceId();
var idLink = addNodeWithText(idCell, 'a', idValue);
idLink.href = '#events&s=' + idValue;
var sourceCell = addNode(infoTr, 'td');
sourceCell.classList.add('waterfall-view-url-cell');
addTextNode(sourceCell, this.description_);
this.sourceCell_ = sourceCell;
this.updateRow();
},
// Generates nodes.
createNode_: function(parentNode, duration, eventTypeString, event) {
var linkNode = addNode(parentNode, 'a');
linkNode.href = '#events&s=' + this.sourceEntry_.getSourceId();
var scale = this.parentView_.getScaleFactor();
var newNode = addNode(linkNode, 'div');
setNodeWidth(newNode, duration * scale);
newNode.classList.add(eventTypeToCssClass_(eventTypeString));
newNode.classList.add('waterfall-view-bar-component');
newNode.addEventListener(
'mouseover',
this.createPopup_.bind(this, newNode, event, eventTypeString,
duration),
true);
newNode.addEventListener(
'mouseout', this.clearPopup_.bind(this, newNode), true);
return newNode;
},
};
/**
* Identifies source dependencies and extracts events of interest for use in
* inlining in URL Request bars.
* Created as static function for testing purposes.
*/
WaterfallRow.findUrlRequestEvents = function(sourceEntry) {
var eventPairs = [];
if (!sourceEntry) {
return eventPairs;
}
// One level down from URL Requests.
var httpStreamJobSources = findDependenciesOfType_(
sourceEntry, EventType.HTTP_STREAM_REQUEST_BOUND_TO_JOB);
var httpTransactionReadHeadersPairs = findEntryPairsFromSourceEntries_(
[sourceEntry], EventType.HTTP_TRANSACTION_READ_HEADERS);
eventPairs = eventPairs.concat(httpTransactionReadHeadersPairs);
var proxyServicePairs = findEntryPairsFromSourceEntries_(
httpStreamJobSources, EventType.PROXY_SERVICE);
eventPairs = eventPairs.concat(proxyServicePairs);
if (httpStreamJobSources.length > 0) {
for (var i = 0; i < httpStreamJobSources.length; ++i) {
// Two levels down from URL Requests.
var hostResolverImplSources = findDependenciesOfType_(
httpStreamJobSources[i], EventType.HOST_RESOLVER_IMPL);
var socketSources = findDependenciesOfType_(
httpStreamJobSources[i], EventType.SOCKET_POOL_BOUND_TO_SOCKET);
// Three levels down from URL Requests.
// TODO(mmenke): Some of these may be nested in the PROXY_SERVICE
// event, resulting in incorrect display, since nested
// events aren't handled.
var hostResolverImplRequestPairs = findEntryPairsFromSourceEntries_(
hostResolverImplSources, EventType.HOST_RESOLVER_IMPL_REQUEST);
eventPairs = eventPairs.concat(hostResolverImplRequestPairs);
// Truncate times of connection events such that they don't occur before
// the HTTP_STREAM_JOB event or the PROXY_SERVICE event.
// TODO(mmenke): The last HOST_RESOLVER_IMPL_REQUEST may still be a
// problem.
var minTime = httpStreamJobSources[i].getLogEntries()[0].time;
if (proxyServicePairs.length > 0)
minTime = proxyServicePairs[0].endEntry.time;
// Convert to number so comparisons will be numeric, not string,
// comparisons.
minTime = Number(minTime);
var tcpConnectPairs = findEntryPairsFromSourceEntries_(
socketSources, EventType.TCP_CONNECT);
var sslConnectPairs = findEntryPairsFromSourceEntries_(
socketSources, EventType.SSL_CONNECT);
var connectionPairs = tcpConnectPairs.concat(sslConnectPairs);
// Truncates times of connection events such that they are shown after a
// proxy service event.
for (var k = 0; k < connectionPairs.length; ++k) {
var eventPair = connectionPairs[k];
var eventInRange = false;
if (eventPair.startEntry.time >= minTime) {
eventInRange = true;
} else if (eventPair.endEntry.time > minTime) {
eventInRange = true;
// Should not modify original object.
eventPair.startEntry = shallowCloneObject(eventPair.startEntry);
// Need to have a string, for consistency.
eventPair.startEntry.time = minTime + '';
}
if (eventInRange) {
eventPairs.push(eventPair);
}
}
}
}
eventPairs.sort(function(a, b) {
return a.startEntry.time - b.startEntry.time;
});
return eventPairs;
}
function eventTypeToCssClass_(eventType) {
return eventType.toLowerCase().replace(/_/g, '-');
}
/**
* Finds all events of input type from the input list of Source Entries.
* Returns an ordered list of start and end log entries.
*/
function findEntryPairsFromSourceEntries_(sourceEntryList, eventType) {
var eventPairs = [];
for (var i = 0; i < sourceEntryList.length; ++i) {
var sourceEntry = sourceEntryList[i];
if (sourceEntry) {
var entries = sourceEntry.getLogEntries();
var matchingEventPairs = findEntryPairsByType_(entries, eventType);
eventPairs = eventPairs.concat(matchingEventPairs);
}
}
return eventPairs;
}
/**
* Finds all events of input type from the input list of log entries.
* Returns an ordered list of start and end log entries.
*/
function findEntryPairsByType_(entries, eventType) {
var matchingEventPairs = [];
var startEntry = null;
for (var i = 0; i < entries.length; ++i) {
var currentEntry = entries[i];
if (eventType != currentEntry.type) {
continue;
}
if (currentEntry.phase == EventPhase.PHASE_BEGIN) {
startEntry = currentEntry;
}
if (startEntry && currentEntry.phase == EventPhase.PHASE_END) {
var event = {
startEntry: startEntry,
endEntry: currentEntry,
};
matchingEventPairs.push(event);
startEntry = null;
}
}
return matchingEventPairs;
}
/**
* Returns an ordered list of SourceEntries that are dependencies for
* events of the given type.
*/
function findDependenciesOfType_(sourceEntry, eventType) {
var sourceEntryList = [];
if (sourceEntry) {
var eventList = findEventsInSourceEntry_(sourceEntry, eventType);
for (var i = 0; i < eventList.length; ++i) {
var foundSourceEntry = findSourceEntryFromEvent_(eventList[i]);
if (foundSourceEntry) {
sourceEntryList.push(foundSourceEntry);
}
}
}
return sourceEntryList;
}
/**
* Returns an ordered list of events from the given sourceEntry with the
* given type.
*/
function findEventsInSourceEntry_(sourceEntry, eventType) {
var entries = sourceEntry.getLogEntries();
var events = [];
for (var i = 0; i < entries.length; ++i) {
var currentEntry = entries[i];
if (currentEntry.type == eventType) {
events.push(currentEntry);
}
}
return events;
}
/**
* Follows the event to obtain the sourceEntry that is the source
* dependency.
*/
function findSourceEntryFromEvent_(event) {
if (!('params' in event) || !('source_dependency' in event.params)) {
return undefined;
} else {
var id = event.params.source_dependency.id;
return SourceTracker.getInstance().getSourceEntry(id);
}
}
return WaterfallRow;
})();