Home | History | Annotate | Download | only in net_internals
      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