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.  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