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. 11 * 12 * @constructor 13 */ 14 15 // TODO(viona): 16 // -Support nested events. 17 // -Handle updating length when an event is stalled. 18 function WaterfallRow(parentView, sourceEntry) { 19 this.parentView_ = parentView; 20 this.sourceEntry_ = sourceEntry; 21 22 this.description_ = sourceEntry.getDescription(); 23 24 this.createRow_(); 25 } 26 27 // Offset of popup from mouse location. 28 var POPUP_OFFSET_FROM_CURSOR = 25; 29 30 WaterfallRow.prototype = { 31 onSourceUpdated: function() { 32 this.updateRow(); 33 }, 34 35 updateRow: function() { 36 var scale = this.parentView_.getScaleFactor(); 37 // In some cases, the REQUEST_ALIVE event has been received, while the 38 // URL Request to start the job has not been received. In that case, the 39 // description obtained is incorrect. The following fixes that. 40 if (this.description_ == '') { 41 this.sourceCell_.innerHTML = ''; 42 this.description_ = this.sourceEntry_.getDescription(); 43 addTextNode(this.sourceCell_, this.description_); 44 } 45 46 this.rowCell_.innerHTML = ''; 47 48 var matchingEventPairs = 49 WaterfallRow.findUrlRequestEvents(this.sourceEntry_); 50 51 // Creates the spacing in the beginning to show start time. 52 var startTime = this.parentView_.getStartTime(); 53 var sourceEntryStartTime = this.getStartTime(); 54 var delay = sourceEntryStartTime - startTime; 55 var frontNode = addNode(this.rowCell_, 'div'); 56 frontNode.classList.add('waterfall-view-padding'); 57 setNodeWidth(frontNode, delay * scale); 58 59 var barCell = addNode(this.rowCell_, 'div'); 60 barCell.classList.add('waterfall-view-bar'); 61 62 if (this.sourceEntry_.isError()) { 63 barCell.classList.add('error'); 64 } 65 66 var currentEnd = sourceEntryStartTime; 67 68 for (var i = 0; i < matchingEventPairs.length; ++i) { 69 var event = matchingEventPairs[i]; 70 var startTicks = event.startEntry.time; 71 var endTicks = event.endEntry.time; 72 event.eventType = event.startEntry.type; 73 event.startTime = timeutil.convertTimeTicksToTime(startTicks); 74 event.endTime = timeutil.convertTimeTicksToTime(endTicks); 75 event.eventDuration = event.endTime - event.startTime; 76 77 // Handles the spaces between events. 78 if (currentEnd < event.startTime) { 79 var eventDuration = event.startTime - currentEnd; 80 var padNode = this.createNode_( 81 barCell, eventDuration, this.sourceTypeString_, 'source'); 82 } 83 84 // Creates event bars. 85 var eventNode = this.createNode_( 86 barCell, event.eventDuration, EventTypeNames[event.eventType], 87 event); 88 currentEnd = event.startTime + event.eventDuration; 89 } 90 91 // Creates a bar for the part after the last event. 92 if (this.getEndTime() > currentEnd) { 93 var endDuration = (this.getEndTime() - currentEnd); 94 var endNode = this.createNode_( 95 barCell, endDuration, this.sourceTypeString_, 'source'); 96 } 97 }, 98 99 getStartTime: function() { 100 return this.sourceEntry_.getStartTime(); 101 }, 102 103 getEndTime: function() { 104 return this.sourceEntry_.getEndTime(); 105 }, 106 107 clearPopup_: function(parentNode) { 108 parentNode.innerHTML = ''; 109 }, 110 111 createPopup_: function(parentNode, event, eventType, duration, mouse) { 112 var tableStart = this.parentView_.getStartTime(); 113 114 var newPopup = addNode(parentNode, 'div'); 115 newPopup.classList.add('waterfall-view-popup'); 116 117 var popupList = addNode(newPopup, 'ul'); 118 popupList.classList.add('waterfall-view-popup-list'); 119 120 popupList.style.maxWidth = 121 $(WaterfallView.MAIN_BOX_ID).offsetWidth * 0.5 + 'px'; 122 123 this.createPopupItem_( 124 popupList, 'Has Error', this.sourceEntry_.isError()); 125 126 this.createPopupItem_( 127 popupList, 'Event Type', eventType); 128 129 if (event != 'source') { 130 this.createPopupItem_( 131 popupList, 'Event Duration', duration.toFixed(0) + 'ms'); 132 this.createPopupItem_( 133 popupList, 'Event Start Time', event.startTime - tableStart + 'ms'); 134 this.createPopupItem_( 135 popupList, 'Event End Time', event.endTime - tableStart + 'ms'); 136 } 137 this.createPopupItem_( 138 popupList, 'Source Duration', 139 this.getEndTime() - this.getStartTime() + 'ms'); 140 this.createPopupItem_( 141 popupList, 'Source Start Time', 142 this.getStartTime() - tableStart + 'ms'); 143 this.createPopupItem_( 144 popupList, 'Source End Time', this.getEndTime() - tableStart + 'ms'); 145 this.createPopupItem_( 146 popupList, 'Source ID', this.sourceEntry_.getSourceId()); 147 var urlListItem = this.createPopupItem_( 148 popupList, 'Source Description', this.description_); 149 150 urlListItem.classList.add('waterfall-view-popup-list-url-item'); 151 152 // Fixes cases where the popup appears 'off-screen'. 153 var popupLeft = mouse.pageX - newPopup.offsetWidth; 154 if (popupLeft < 0) { 155 popupLeft = mouse.pageX; 156 } 157 newPopup.style.left = popupLeft + 158 $(WaterfallView.MAIN_BOX_ID).scrollLeft - 159 $(WaterfallView.BAR_TABLE_ID).offsetLeft + 'px'; 160 161 var popupTop = mouse.pageY - newPopup.offsetHeight - 162 POPUP_OFFSET_FROM_CURSOR; 163 if (popupTop < 0) { 164 popupTop = mouse.pageY; 165 } 166 newPopup.style.top = popupTop + 167 $(WaterfallView.MAIN_BOX_ID).scrollTop - 168 $(WaterfallView.BAR_TABLE_ID).offsetTop + 'px'; 169 }, 170 171 createPopupItem_: function(parentPopup, key, popupInformation) { 172 var popupItem = addNode(parentPopup, 'li'); 173 addTextNode(popupItem, key + ': ' + popupInformation); 174 return popupItem; 175 }, 176 177 createRow_: function() { 178 // Create a row. 179 var tr = addNode($(WaterfallView.BAR_TBODY_ID), 'tr'); 180 tr.classList.add('waterfall-view-table-row'); 181 182 // Creates the color bar. 183 184 var rowCell = addNode(tr, 'td'); 185 rowCell.classList.add('waterfall-view-row'); 186 this.rowCell_ = rowCell; 187 188 this.sourceTypeString_ = this.sourceEntry_.getSourceTypeString(); 189 190 var infoTr = addNode($(WaterfallView.INFO_TBODY_ID), 'tr'); 191 infoTr.classList.add('waterfall-view-information-row'); 192 193 var idCell = addNode(infoTr, 'td'); 194 idCell.classList.add('waterfall-view-id-cell'); 195 var idValue = this.sourceEntry_.getSourceId(); 196 var idLink = addNodeWithText(idCell, 'a', idValue); 197 idLink.href = '#events&s=' + idValue; 198 199 var sourceCell = addNode(infoTr, 'td'); 200 sourceCell.classList.add('waterfall-view-url-cell'); 201 addTextNode(sourceCell, this.description_); 202 this.sourceCell_ = sourceCell; 203 204 this.updateRow(); 205 }, 206 207 // Generates nodes. 208 createNode_: function(parentNode, duration, eventTypeString, event) { 209 var linkNode = addNode(parentNode, 'a'); 210 linkNode.href = '#events&s=' + this.sourceEntry_.getSourceId(); 211 212 var scale = this.parentView_.getScaleFactor(); 213 var newNode = addNode(linkNode, 'div'); 214 setNodeWidth(newNode, duration * scale); 215 newNode.classList.add(eventTypeToCssClass_(eventTypeString)); 216 newNode.classList.add('waterfall-view-bar-component'); 217 newNode.addEventListener( 218 'mouseover', 219 this.createPopup_.bind(this, newNode, event, eventTypeString, 220 duration), 221 true); 222 newNode.addEventListener( 223 'mouseout', this.clearPopup_.bind(this, newNode), true); 224 return newNode; 225 }, 226 }; 227 228 /** 229 * Identifies source dependencies and extracts events of interest for use in 230 * inlining in URL Request bars. 231 * Created as static function for testing purposes. 232 */ 233 WaterfallRow.findUrlRequestEvents = function(sourceEntry) { 234 var eventPairs = []; 235 if (!sourceEntry) { 236 return eventPairs; 237 } 238 239 // One level down from URL Requests. 240 241 var httpStreamJobSources = findDependenciesOfType_( 242 sourceEntry, EventType.HTTP_STREAM_REQUEST_BOUND_TO_JOB); 243 244 var httpTransactionReadHeadersPairs = findEntryPairsFromSourceEntries_( 245 [sourceEntry], EventType.HTTP_TRANSACTION_READ_HEADERS); 246 eventPairs = eventPairs.concat(httpTransactionReadHeadersPairs); 247 248 var proxyServicePairs = findEntryPairsFromSourceEntries_( 249 httpStreamJobSources, EventType.PROXY_SERVICE); 250 eventPairs = eventPairs.concat(proxyServicePairs); 251 252 if (httpStreamJobSources.length > 0) { 253 for (var i = 0; i < httpStreamJobSources.length; ++i) { 254 // Two levels down from URL Requests. 255 256 var hostResolverImplSources = findDependenciesOfType_( 257 httpStreamJobSources[i], EventType.HOST_RESOLVER_IMPL); 258 259 var socketSources = findDependenciesOfType_( 260 httpStreamJobSources[i], EventType.SOCKET_POOL_BOUND_TO_SOCKET); 261 262 // Three levels down from URL Requests. 263 264 // TODO(mmenke): Some of these may be nested in the PROXY_SERVICE 265 // event, resulting in incorrect display, since nested 266 // events aren't handled. 267 var hostResolverImplRequestPairs = findEntryPairsFromSourceEntries_( 268 hostResolverImplSources, EventType.HOST_RESOLVER_IMPL_REQUEST); 269 eventPairs = eventPairs.concat(hostResolverImplRequestPairs); 270 271 // Truncate times of connection events such that they don't occur before 272 // the HTTP_STREAM_JOB event or the PROXY_SERVICE event. 273 // TODO(mmenke): The last HOST_RESOLVER_IMPL_REQUEST may still be a 274 // problem. 275 var minTime = httpStreamJobSources[i].getLogEntries()[0].time; 276 if (proxyServicePairs.length > 0) 277 minTime = proxyServicePairs[0].endEntry.time; 278 // Convert to number so comparisons will be numeric, not string, 279 // comparisons. 280 minTime = Number(minTime); 281 282 var tcpConnectPairs = findEntryPairsFromSourceEntries_( 283 socketSources, EventType.TCP_CONNECT); 284 285 var sslConnectPairs = findEntryPairsFromSourceEntries_( 286 socketSources, EventType.SSL_CONNECT); 287 288 var connectionPairs = tcpConnectPairs.concat(sslConnectPairs); 289 290 // Truncates times of connection events such that they are shown after a 291 // proxy service event. 292 for (var k = 0; k < connectionPairs.length; ++k) { 293 var eventPair = connectionPairs[k]; 294 var eventInRange = false; 295 if (eventPair.startEntry.time >= minTime) { 296 eventInRange = true; 297 } else if (eventPair.endEntry.time > minTime) { 298 eventInRange = true; 299 // Should not modify original object. 300 eventPair.startEntry = shallowCloneObject(eventPair.startEntry); 301 // Need to have a string, for consistency. 302 eventPair.startEntry.time = minTime + ''; 303 } 304 if (eventInRange) { 305 eventPairs.push(eventPair); 306 } 307 } 308 } 309 } 310 eventPairs.sort(function(a, b) { 311 return a.startEntry.time - b.startEntry.time; 312 }); 313 return eventPairs; 314 } 315 316 function eventTypeToCssClass_(eventType) { 317 return eventType.toLowerCase().replace(/_/g, '-'); 318 } 319 320 /** 321 * Finds all events of input type from the input list of Source Entries. 322 * Returns an ordered list of start and end log entries. 323 */ 324 function findEntryPairsFromSourceEntries_(sourceEntryList, eventType) { 325 var eventPairs = []; 326 for (var i = 0; i < sourceEntryList.length; ++i) { 327 var sourceEntry = sourceEntryList[i]; 328 if (sourceEntry) { 329 var entries = sourceEntry.getLogEntries(); 330 var matchingEventPairs = findEntryPairsByType_(entries, eventType); 331 eventPairs = eventPairs.concat(matchingEventPairs); 332 } 333 } 334 return eventPairs; 335 } 336 337 /** 338 * Finds all events of input type from the input list of log entries. 339 * Returns an ordered list of start and end log entries. 340 */ 341 function findEntryPairsByType_(entries, eventType) { 342 var matchingEventPairs = []; 343 var startEntry = null; 344 for (var i = 0; i < entries.length; ++i) { 345 var currentEntry = entries[i]; 346 if (eventType != currentEntry.type) { 347 continue; 348 } 349 if (currentEntry.phase == EventPhase.PHASE_BEGIN) { 350 startEntry = currentEntry; 351 } 352 if (startEntry && currentEntry.phase == EventPhase.PHASE_END) { 353 var event = { 354 startEntry: startEntry, 355 endEntry: currentEntry, 356 }; 357 matchingEventPairs.push(event); 358 startEntry = null; 359 } 360 } 361 return matchingEventPairs; 362 } 363 364 /** 365 * Returns an ordered list of SourceEntries that are dependencies for 366 * events of the given type. 367 */ 368 function findDependenciesOfType_(sourceEntry, eventType) { 369 var sourceEntryList = []; 370 if (sourceEntry) { 371 var eventList = findEventsInSourceEntry_(sourceEntry, eventType); 372 for (var i = 0; i < eventList.length; ++i) { 373 var foundSourceEntry = findSourceEntryFromEvent_(eventList[i]); 374 if (foundSourceEntry) { 375 sourceEntryList.push(foundSourceEntry); 376 } 377 } 378 } 379 return sourceEntryList; 380 } 381 382 /** 383 * Returns an ordered list of events from the given sourceEntry with the 384 * given type. 385 */ 386 function findEventsInSourceEntry_(sourceEntry, eventType) { 387 var entries = sourceEntry.getLogEntries(); 388 var events = []; 389 for (var i = 0; i < entries.length; ++i) { 390 var currentEntry = entries[i]; 391 if (currentEntry.type == eventType) { 392 events.push(currentEntry); 393 } 394 } 395 return events; 396 } 397 398 /** 399 * Follows the event to obtain the sourceEntry that is the source 400 * dependency. 401 */ 402 function findSourceEntryFromEvent_(event) { 403 if (!('params' in event) || !('source_dependency' in event.params)) { 404 return undefined; 405 } else { 406 var id = event.params.source_dependency.id; 407 return SourceTracker.getInstance().getSourceEntry(id); 408 } 409 } 410 411 return WaterfallRow; 412 })(); 413