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. 4 5 var WaterfallRow = (function() { 6 'use strict'; 7 8 /** 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). 12 * 13 * @constructor 14 */ 15 16 // TODO(viona): 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; 22 23 this.description_ = sourceEntry.getDescription(); 24 25 this.createRow_(); 26 } 27 28 // Offset of popup from mouse location. 29 var POPUP_OFFSET_FROM_CURSOR = 25; 30 31 WaterfallRow.prototype = { 32 onSourceUpdated: function() { 33 this.updateRow(); 34 }, 35 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_); 45 } 46 47 this.rowCell_.innerHTML = ''; 48 49 var matchingEventPairs = 50 WaterfallRow.findUrlRequestEvents(this.sourceEntry_); 51 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); 57 58 var barCell = addNode(this.rowCell_, 'div'); 59 barCell.classList.add('waterfall-view-bar'); 60 61 if (this.sourceEntry_.isError()) { 62 barCell.classList.add('error'); 63 } 64 65 var currentEnd = sourceStartTicks; 66 67 for (var i = 0; i < matchingEventPairs.length; ++i) { 68 var event = matchingEventPairs[i]; 69 var startTicks = 70 timeutil.convertTimeTicksToRelativeTime(event.startEntry.time); 71 var endTicks = 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; 77 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'); 83 } 84 85 // Creates event bars. 86 var eventNode = this.createNode_( 87 barCell, event.eventDuration, EventTypeNames[event.eventType], 88 event); 89 currentEnd = event.startTime + event.eventDuration; 90 } 91 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'); 98 } 99 }, 100 101 getStartTicks: function() { 102 return timeutil.convertTimeTicksToRelativeTime( 103 this.sourceEntry_.getStartTicks()); 104 }, 105 106 getEndTicks: function() { 107 return timeutil.convertTimeTicksToRelativeTime( 108 this.sourceEntry_.getEndTicks()); 109 }, 110 111 clearPopup_: function(parentNode) { 112 parentNode.innerHTML = ''; 113 }, 114 115 createPopup_: function(parentNode, event, eventType, duration, mouse) { 116 var newPopup = addNode(parentNode, 'div'); 117 newPopup.classList.add('waterfall-view-popup'); 118 119 var popupList = addNode(newPopup, 'ul'); 120 popupList.classList.add('waterfall-view-popup-list'); 121 122 popupList.style.maxWidth = 123 $(WaterfallView.MAIN_BOX_ID).offsetWidth * 0.5 + 'px'; 124 125 this.createPopupItem_( 126 popupList, 'Has Error', this.sourceEntry_.isError()); 127 128 this.createPopupItem_(popupList, 'Event Type', eventType); 129 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'); 137 } 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_); 148 149 urlListItem.classList.add('waterfall-view-popup-list-url-item'); 150 151 // Fixes cases where the popup appears 'off-screen'. 152 var popupLeft = mouse.pageX - newPopup.offsetWidth; 153 if (popupLeft < 0) { 154 popupLeft = mouse.pageX; 155 } 156 newPopup.style.left = popupLeft + 157 $(WaterfallView.MAIN_BOX_ID).scrollLeft - 158 $(WaterfallView.BAR_TABLE_ID).offsetLeft + 'px'; 159 160 var popupTop = mouse.pageY - newPopup.offsetHeight - 161 POPUP_OFFSET_FROM_CURSOR; 162 if (popupTop < 0) { 163 popupTop = mouse.pageY; 164 } 165 newPopup.style.top = popupTop + 166 $(WaterfallView.MAIN_BOX_ID).scrollTop - 167 $(WaterfallView.BAR_TABLE_ID).offsetTop + 'px'; 168 }, 169 170 createPopupItem_: function(parentPopup, key, popupInformation) { 171 var popupItem = addNode(parentPopup, 'li'); 172 addTextNode(popupItem, key + ': ' + popupInformation); 173 return popupItem; 174 }, 175 176 createRow_: function() { 177 // Create a row. 178 var tr = addNode($(WaterfallView.BAR_TBODY_ID), 'tr'); 179 tr.classList.add('waterfall-view-table-row'); 180 181 // Creates the color bar. 182 183 var rowCell = addNode(tr, 'td'); 184 rowCell.classList.add('waterfall-view-row'); 185 this.rowCell_ = rowCell; 186 187 this.sourceTypeString_ = this.sourceEntry_.getSourceTypeString(); 188 189 var infoTr = addNode($(WaterfallView.INFO_TBODY_ID), 'tr'); 190 infoTr.classList.add('waterfall-view-information-row'); 191 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; 197 198 var sourceCell = addNode(infoTr, 'td'); 199 sourceCell.classList.add('waterfall-view-url-cell'); 200 addTextNode(sourceCell, this.description_); 201 this.sourceCell_ = sourceCell; 202 203 this.updateRow(); 204 }, 205 206 // Generates nodes. 207 createNode_: function(parentNode, duration, eventTypeString, event) { 208 var linkNode = addNode(parentNode, 'a'); 209 linkNode.href = '#events&s=' + this.sourceEntry_.getSourceId(); 210 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( 217 'mouseover', 218 this.createPopup_.bind(this, newNode, event, eventTypeString, 219 duration), 220 true); 221 newNode.addEventListener( 222 'mouseout', this.clearPopup_.bind(this, newNode), true); 223 return newNode; 224 }, 225 }; 226 227 /** 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. 231 */ 232 WaterfallRow.findUrlRequestEvents = function(sourceEntry) { 233 var eventPairs = []; 234 if (!sourceEntry) { 235 return eventPairs; 236 } 237 238 // One level down from URL Requests. 239 240 var httpStreamJobSources = findDependenciesOfType_( 241 sourceEntry, EventType.HTTP_STREAM_REQUEST_BOUND_TO_JOB); 242 243 var httpTransactionReadHeadersPairs = findEntryPairsFromSourceEntries_( 244 [sourceEntry], EventType.HTTP_TRANSACTION_READ_HEADERS); 245 eventPairs = eventPairs.concat(httpTransactionReadHeadersPairs); 246 247 var proxyServicePairs = findEntryPairsFromSourceEntries_( 248 httpStreamJobSources, EventType.PROXY_SERVICE); 249 eventPairs = eventPairs.concat(proxyServicePairs); 250 251 if (httpStreamJobSources.length > 0) { 252 for (var i = 0; i < httpStreamJobSources.length; ++i) { 253 // Two levels down from URL Requests. 254 255 var hostResolverImplSources = findDependenciesOfType_( 256 httpStreamJobSources[i], EventType.HOST_RESOLVER_IMPL); 257 258 var socketSources = findDependenciesOfType_( 259 httpStreamJobSources[i], EventType.SOCKET_POOL_BOUND_TO_SOCKET); 260 261 // Three levels down from URL Requests. 262 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); 269 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 273 // problem. 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, 278 // comparisons. 279 minTime = Number(minTime); 280 281 var tcpConnectPairs = findEntryPairsFromSourceEntries_( 282 socketSources, EventType.TCP_CONNECT); 283 284 var sslConnectPairs = findEntryPairsFromSourceEntries_( 285 socketSources, EventType.SSL_CONNECT); 286 287 var connectionPairs = tcpConnectPairs.concat(sslConnectPairs); 288 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) { 295 eventInRange = true; 296 } else if (eventPair.endEntry.time > minTime) { 297 eventInRange = true; 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 + ''; 302 } 303 if (eventInRange) { 304 eventPairs.push(eventPair); 305 } 306 } 307 } 308 } 309 eventPairs.sort(function(a, b) { 310 return a.startEntry.time - b.startEntry.time; 311 }); 312 return eventPairs; 313 } 314 315 function eventTypeToCssClass_(eventType) { 316 return eventType.toLowerCase().replace(/_/g, '-'); 317 } 318 319 /** 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. 322 */ 323 function findEntryPairsFromSourceEntries_(sourceEntryList, eventType) { 324 var eventPairs = []; 325 for (var i = 0; i < sourceEntryList.length; ++i) { 326 var sourceEntry = sourceEntryList[i]; 327 if (sourceEntry) { 328 var entries = sourceEntry.getLogEntries(); 329 var matchingEventPairs = findEntryPairsByType_(entries, eventType); 330 eventPairs = eventPairs.concat(matchingEventPairs); 331 } 332 } 333 return eventPairs; 334 } 335 336 /** 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. 339 */ 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) { 346 continue; 347 } 348 if (currentEntry.phase == EventPhase.PHASE_BEGIN) { 349 startEntry = currentEntry; 350 } 351 if (startEntry && currentEntry.phase == EventPhase.PHASE_END) { 352 var event = { 353 startEntry: startEntry, 354 endEntry: currentEntry, 355 }; 356 matchingEventPairs.push(event); 357 startEntry = null; 358 } 359 } 360 return matchingEventPairs; 361 } 362 363 /** 364 * Returns an ordered list of SourceEntries that are dependencies for 365 * events of the given type. 366 */ 367 function findDependenciesOfType_(sourceEntry, eventType) { 368 var sourceEntryList = []; 369 if (sourceEntry) { 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); 375 } 376 } 377 } 378 return sourceEntryList; 379 } 380 381 /** 382 * Returns an ordered list of events from the given sourceEntry with the 383 * given type. 384 */ 385 function findEventsInSourceEntry_(sourceEntry, eventType) { 386 var entries = sourceEntry.getLogEntries(); 387 var events = []; 388 for (var i = 0; i < entries.length; ++i) { 389 var currentEntry = entries[i]; 390 if (currentEntry.type == eventType) { 391 events.push(currentEntry); 392 } 393 } 394 return events; 395 } 396 397 /** 398 * Follows the event to obtain the sourceEntry that is the source 399 * dependency. 400 */ 401 function findSourceEntryFromEvent_(event) { 402 if (!('params' in event) || !('source_dependency' in event.params)) { 403 return undefined; 404 } else { 405 var id = event.params.source_dependency.id; 406 return SourceTracker.getInstance().getSourceEntry(id); 407 } 408 } 409 410 return WaterfallRow; 411 })(); 412