1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 var WaterfallRow = (function() {
9 * A WaterfallRow represents the row corresponding to a single SourceEntry
10 * displayed by the EventsWaterfallView. All times are relative to the
11 * "base time" (Time of first event from any source).
17 // -Support nested events.
18 // -Handle updating length when an event is stalled.
19 function WaterfallRow(parentView, sourceEntry) {
20 this.parentView_ = parentView;
21 this.sourceEntry_ = sourceEntry;
23 this.description_ = sourceEntry.getDescription();
28 // Offset of popup from mouse location.
29 var POPUP_OFFSET_FROM_CURSOR = 25;
31 WaterfallRow.prototype = {
32 onSourceUpdated: function() {
36 updateRow: function() {
37 var scale = this.parentView_.getScaleFactor();
38 // In some cases, the REQUEST_ALIVE event has been received, while the
39 // URL Request to start the job has not been received. In that case, the
40 // description obtained is incorrect. The following fixes that.
41 if (this.description_ == '') {
42 this.sourceCell_.innerHTML = '';
43 this.description_ = this.sourceEntry_.getDescription();
44 addTextNode(this.sourceCell_, this.description_);
47 this.rowCell_.innerHTML = '';
49 var matchingEventPairs =
50 WaterfallRow.findUrlRequestEvents(this.sourceEntry_);
52 // Creates the spacing in the beginning to show start time.
53 var sourceStartTicks = this.getStartTicks();
54 var frontNode = addNode(this.rowCell_, 'div');
55 frontNode.classList.add('waterfall-view-padding');
56 setNodeWidth(frontNode, sourceStartTicks * scale);
58 var barCell = addNode(this.rowCell_, 'div');
59 barCell.classList.add('waterfall-view-bar');
61 if (this.sourceEntry_.isError()) {
62 barCell.classList.add('error');
65 var currentEnd = sourceStartTicks;
67 for (var i = 0; i < matchingEventPairs.length; ++i) {
68 var event = matchingEventPairs[i];
70 timeutil.convertTimeTicksToRelativeTime(event.startEntry.time);
72 timeutil.convertTimeTicksToRelativeTime(event.endEntry.time);
73 event.eventType = event.startEntry.type;
74 event.startTime = startTicks;
75 event.endTime = endTicks;
76 event.eventDuration = event.endTime - event.startTime;
78 // Handles the spaces between events.
79 if (currentEnd < event.startTime) {
80 var eventDuration = event.startTime - currentEnd;
81 var padNode = this.createNode_(
82 barCell, eventDuration, this.sourceTypeString_, 'source');
85 // Creates event bars.
86 var eventNode = this.createNode_(
87 barCell, event.eventDuration, EventTypeNames[event.eventType],
89 currentEnd = event.startTime + event.eventDuration;
92 // Creates a bar for the part after the last event.
93 var endTime = this.getEndTicks();
94 if (endTime > currentEnd) {
95 var endDuration = endTime - currentEnd;
96 var endNode = this.createNode_(
97 barCell, endDuration, this.sourceTypeString_, 'source');
101 getStartTicks: function() {
102 return timeutil.convertTimeTicksToRelativeTime(
103 this.sourceEntry_.getStartTicks());
106 getEndTicks: function() {
107 return timeutil.convertTimeTicksToRelativeTime(
108 this.sourceEntry_.getEndTicks());
111 clearPopup_: function(parentNode) {
112 parentNode.innerHTML = '';
115 createPopup_: function(parentNode, event, eventType, duration, mouse) {
116 var newPopup = addNode(parentNode, 'div');
117 newPopup.classList.add('waterfall-view-popup');
119 var popupList = addNode(newPopup, 'ul');
120 popupList.classList.add('waterfall-view-popup-list');
122 popupList.style.maxWidth =
123 $(WaterfallView.MAIN_BOX_ID).offsetWidth * 0.5 + 'px';
125 this.createPopupItem_(
126 popupList, 'Has Error', this.sourceEntry_.isError());
128 this.createPopupItem_(popupList, 'Event Type', eventType);
130 if (event != 'source') {
131 this.createPopupItem_(
132 popupList, 'Event Duration', duration.toFixed(0) + 'ms');
133 this.createPopupItem_(
134 popupList, 'Event Start Time', event.startTime + 'ms');
135 this.createPopupItem_(
136 popupList, 'Event End Time', event.endTime + 'ms');
138 this.createPopupItem_(popupList, 'Source Duration',
139 this.getEndTicks() - this.getStartTicks() + 'ms');
140 this.createPopupItem_(popupList, 'Source Start Time',
141 this.getStartTicks() + 'ms');
142 this.createPopupItem_(popupList, 'Source End Time',
143 this.getEndTicks() + 'ms');
144 this.createPopupItem_(
145 popupList, 'Source ID', this.sourceEntry_.getSourceId());
146 var urlListItem = this.createPopupItem_(
147 popupList, 'Source Description', this.description_);
149 urlListItem.classList.add('waterfall-view-popup-list-url-item');
151 // Fixes cases where the popup appears 'off-screen'.
152 var popupLeft = mouse.pageX - newPopup.offsetWidth;
154 popupLeft = mouse.pageX;
156 newPopup.style.left = popupLeft +
157 $(WaterfallView.MAIN_BOX_ID).scrollLeft -
158 $(WaterfallView.BAR_TABLE_ID).offsetLeft + 'px';
160 var popupTop = mouse.pageY - newPopup.offsetHeight -
161 POPUP_OFFSET_FROM_CURSOR;
163 popupTop = mouse.pageY;
165 newPopup.style.top = popupTop +
166 $(WaterfallView.MAIN_BOX_ID).scrollTop -
167 $(WaterfallView.BAR_TABLE_ID).offsetTop + 'px';
170 createPopupItem_: function(parentPopup, key, popupInformation) {
171 var popupItem = addNode(parentPopup, 'li');
172 addTextNode(popupItem, key + ': ' + popupInformation);
176 createRow_: function() {
178 var tr = addNode($(WaterfallView.BAR_TBODY_ID), 'tr');
179 tr.classList.add('waterfall-view-table-row');
181 // Creates the color bar.
183 var rowCell = addNode(tr, 'td');
184 rowCell.classList.add('waterfall-view-row');
185 this.rowCell_ = rowCell;
187 this.sourceTypeString_ = this.sourceEntry_.getSourceTypeString();
189 var infoTr = addNode($(WaterfallView.INFO_TBODY_ID), 'tr');
190 infoTr.classList.add('waterfall-view-information-row');
192 var idCell = addNode(infoTr, 'td');
193 idCell.classList.add('waterfall-view-id-cell');
194 var idValue = this.sourceEntry_.getSourceId();
195 var idLink = addNodeWithText(idCell, 'a', idValue);
196 idLink.href = '#events&s=' + idValue;
198 var sourceCell = addNode(infoTr, 'td');
199 sourceCell.classList.add('waterfall-view-url-cell');
200 addTextNode(sourceCell, this.description_);
201 this.sourceCell_ = sourceCell;
207 createNode_: function(parentNode, duration, eventTypeString, event) {
208 var linkNode = addNode(parentNode, 'a');
209 linkNode.href = '#events&s=' + this.sourceEntry_.getSourceId();
211 var scale = this.parentView_.getScaleFactor();
212 var newNode = addNode(linkNode, 'div');
213 setNodeWidth(newNode, duration * scale);
214 newNode.classList.add(eventTypeToCssClass_(eventTypeString));
215 newNode.classList.add('waterfall-view-bar-component');
216 newNode.addEventListener(
218 this.createPopup_.bind(this, newNode, event, eventTypeString,
221 newNode.addEventListener(
222 'mouseout', this.clearPopup_.bind(this, newNode), true);
228 * Identifies source dependencies and extracts events of interest for use in
229 * inlining in URL Request bars.
230 * Created as static function for testing purposes.
232 WaterfallRow.findUrlRequestEvents = function(sourceEntry) {
238 // One level down from URL Requests.
240 var httpStreamJobSources = findDependenciesOfType_(
241 sourceEntry, EventType.HTTP_STREAM_REQUEST_BOUND_TO_JOB);
243 var httpTransactionReadHeadersPairs = findEntryPairsFromSourceEntries_(
244 [sourceEntry], EventType.HTTP_TRANSACTION_READ_HEADERS);
245 eventPairs = eventPairs.concat(httpTransactionReadHeadersPairs);
247 var proxyServicePairs = findEntryPairsFromSourceEntries_(
248 httpStreamJobSources, EventType.PROXY_SERVICE);
249 eventPairs = eventPairs.concat(proxyServicePairs);
251 if (httpStreamJobSources.length > 0) {
252 for (var i = 0; i < httpStreamJobSources.length; ++i) {
253 // Two levels down from URL Requests.
255 var hostResolverImplSources = findDependenciesOfType_(
256 httpStreamJobSources[i], EventType.HOST_RESOLVER_IMPL);
258 var socketSources = findDependenciesOfType_(
259 httpStreamJobSources[i], EventType.SOCKET_POOL_BOUND_TO_SOCKET);
261 // Three levels down from URL Requests.
263 // TODO(mmenke): Some of these may be nested in the PROXY_SERVICE
264 // event, resulting in incorrect display, since nested
265 // events aren't handled.
266 var hostResolverImplRequestPairs = findEntryPairsFromSourceEntries_(
267 hostResolverImplSources, EventType.HOST_RESOLVER_IMPL_REQUEST);
268 eventPairs = eventPairs.concat(hostResolverImplRequestPairs);
270 // Truncate times of connection events such that they don't occur before
271 // the HTTP_STREAM_JOB event or the PROXY_SERVICE event.
272 // TODO(mmenke): The last HOST_RESOLVER_IMPL_REQUEST may still be a
274 var minTime = httpStreamJobSources[i].getLogEntries()[0].time;
275 if (proxyServicePairs.length > 0)
276 minTime = proxyServicePairs[0].endEntry.time;
277 // Convert to number so comparisons will be numeric, not string,
279 minTime = Number(minTime);
281 var tcpConnectPairs = findEntryPairsFromSourceEntries_(
282 socketSources, EventType.TCP_CONNECT);
284 var sslConnectPairs = findEntryPairsFromSourceEntries_(
285 socketSources, EventType.SSL_CONNECT);
287 var connectionPairs = tcpConnectPairs.concat(sslConnectPairs);
289 // Truncates times of connection events such that they are shown after a
290 // proxy service event.
291 for (var k = 0; k < connectionPairs.length; ++k) {
292 var eventPair = connectionPairs[k];
293 var eventInRange = false;
294 if (eventPair.startEntry.time >= minTime) {
296 } else if (eventPair.endEntry.time > minTime) {
298 // Should not modify original object.
299 eventPair.startEntry = shallowCloneObject(eventPair.startEntry);
300 // Need to have a string, for consistency.
301 eventPair.startEntry.time = minTime + '';
304 eventPairs.push(eventPair);
309 eventPairs.sort(function(a, b) {
310 return a.startEntry.time - b.startEntry.time;
315 function eventTypeToCssClass_(eventType) {
316 return eventType.toLowerCase().replace(/_/g, '-');
320 * Finds all events of input type from the input list of Source Entries.
321 * Returns an ordered list of start and end log entries.
323 function findEntryPairsFromSourceEntries_(sourceEntryList, eventType) {
325 for (var i = 0; i < sourceEntryList.length; ++i) {
326 var sourceEntry = sourceEntryList[i];
328 var entries = sourceEntry.getLogEntries();
329 var matchingEventPairs = findEntryPairsByType_(entries, eventType);
330 eventPairs = eventPairs.concat(matchingEventPairs);
337 * Finds all events of input type from the input list of log entries.
338 * Returns an ordered list of start and end log entries.
340 function findEntryPairsByType_(entries, eventType) {
341 var matchingEventPairs = [];
342 var startEntry = null;
343 for (var i = 0; i < entries.length; ++i) {
344 var currentEntry = entries[i];
345 if (eventType != currentEntry.type) {
348 if (currentEntry.phase == EventPhase.PHASE_BEGIN) {
349 startEntry = currentEntry;
351 if (startEntry && currentEntry.phase == EventPhase.PHASE_END) {
353 startEntry: startEntry,
354 endEntry: currentEntry,
356 matchingEventPairs.push(event);
360 return matchingEventPairs;
364 * Returns an ordered list of SourceEntries that are dependencies for
365 * events of the given type.
367 function findDependenciesOfType_(sourceEntry, eventType) {
368 var sourceEntryList = [];
370 var eventList = findEventsInSourceEntry_(sourceEntry, eventType);
371 for (var i = 0; i < eventList.length; ++i) {
372 var foundSourceEntry = findSourceEntryFromEvent_(eventList[i]);
373 if (foundSourceEntry) {
374 sourceEntryList.push(foundSourceEntry);
378 return sourceEntryList;
382 * Returns an ordered list of events from the given sourceEntry with the
385 function findEventsInSourceEntry_(sourceEntry, eventType) {
386 var entries = sourceEntry.getLogEntries();
388 for (var i = 0; i < entries.length; ++i) {
389 var currentEntry = entries[i];
390 if (currentEntry.type == eventType) {
391 events.push(currentEntry);
398 * Follows the event to obtain the sourceEntry that is the source
401 function findSourceEntryFromEvent_(event) {
402 if (!('params' in event) || !('source_dependency' in event.params)) {
405 var id = event.params.source_dependency.id;
406 return SourceTracker.getInstance().getSourceEntry(id);