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 // TODO(viona): Write a README document/instructions.
      6 
      7 /** This view displays the event waterfall. */
      8 var WaterfallView = (function() {
      9   'use strict';
     10 
     11   // We inherit from DivView.
     12   var superClass = DivView;
     13 
     14   /**
     15    * @constructor
     16    */
     17   function WaterfallView() {
     18     assertFirstConstructorCall(WaterfallView);
     19 
     20     // Call superclass's constructor.
     21     superClass.call(this, WaterfallView.MAIN_BOX_ID);
     22 
     23     SourceTracker.getInstance().addSourceEntryObserver(this);
     24 
     25     // For adjusting the range of view.
     26     $(WaterfallView.SCALE_ID).addEventListener(
     27         'click', this.setStartEndTimes_.bind(this), true);
     28 
     29     $(WaterfallView.MAIN_BOX_ID).addEventListener(
     30         'mousewheel', this.scrollToZoom_.bind(this), true);
     31 
     32     this.initializeSourceList_();
     33   }
     34 
     35   WaterfallView.TAB_ID = 'tab-handle-waterfall';
     36   WaterfallView.TAB_NAME = 'Waterfall';
     37   WaterfallView.TAB_HASH = '#waterfall';
     38 
     39   // IDs for special HTML elements in events_waterfall_view.html.
     40   WaterfallView.MAIN_BOX_ID = 'waterfall-view-tab-content';
     41   WaterfallView.TBODY_ID = 'waterfall-view-source-list-tbody';
     42   WaterfallView.SCALE_ID = 'waterfall-view-adjust-to-window';
     43   WaterfallView.ID_HEADER_ID = 'waterfall-view-source-id';
     44   WaterfallView.SOURCE_HEADER_ID = 'waterfall-view-source-header';
     45   WaterfallView.TIME_SCALE_HEADER_ID = 'waterfall-view-time-scale-labels';
     46   WaterfallView.TIME_RANGE_ID = 'waterfall-view-time-range-submit';
     47   WaterfallView.START_TIME_ID = 'waterfall-view-start-input';
     48   WaterfallView.END_TIME_ID = 'waterfall-view-end-input';
     49 
     50   // The number of units mouse wheel deltas increase for each tick of the
     51   // wheel.
     52   var MOUSE_WHEEL_UNITS_PER_CLICK = 120;
     53 
     54   // Amount we zoom for one vertical tick of the mouse wheel, as a ratio.
     55   var MOUSE_WHEEL_ZOOM_RATE = 1.25;
     56   // Amount we scroll for one horizontal tick of the mouse wheel, in pixels.
     57   var MOUSE_WHEEL_SCROLL_RATE = MOUSE_WHEEL_UNITS_PER_CLICK;
     58 
     59   cr.addSingletonGetter(WaterfallView);
     60 
     61   WaterfallView.prototype = {
     62     // Inherit the superclass's methods.
     63     __proto__: superClass.prototype,
     64 
     65     /**
     66      * Creates new WaterfallRows for URL Requests when the sourceEntries are
     67      * updated if they do not already exist.
     68      * Updates pre-existing WaterfallRows that correspond to updated sources.
     69      */
     70     onSourceEntriesUpdated: function(sourceEntries) {
     71       if (this.startTime_ == null && sourceEntries.length > 0) {
     72         var logEntries = sourceEntries[0].getLogEntries();
     73         this.startTime_ = timeutil.convertTimeTicksToTime(logEntries[0].time);
     74         // Initial scale factor.
     75         this.scaleFactor_ = 0.1;
     76         this.eventsList_ = this.createEventsList_();
     77       }
     78       for (var i = 0; i < sourceEntries.length; ++i) {
     79         var sourceEntry = sourceEntries[i];
     80         var id = sourceEntry.getSourceId();
     81         for (var j = 0; j < this.eventsList_.length; ++j) {
     82           var eventObject = this.eventsList_[j];
     83           if (sourceEntry.getSourceType() == eventObject.mainEvent) {
     84             var row = this.sourceIdToRowMap_[id];
     85             if (!row) {
     86               var newRow = new WaterfallRow(
     87                   this, sourceEntry, eventObject.importantEventTypes);
     88               if (newRow == undefined) {
     89                 console.log(sourceEntry);
     90               }
     91               this.sourceIdToRowMap_[id] = newRow;
     92             } else {
     93               row.onSourceUpdated();
     94             }
     95           }
     96         }
     97       }
     98       this.updateTimeScale_(this.scaleFactor_);
     99     },
    100 
    101     onAllSourceEntriesDeleted: function() {
    102       this.initializeSourceList_();
    103     },
    104 
    105     onLoadLogFinish: function(data) {
    106       return true;
    107     },
    108 
    109     getScaleFactor: function() {
    110       return this.scaleFactor_;
    111     },
    112 
    113     getStartTime: function() {
    114       return this.startTime_;
    115     },
    116 
    117     /**
    118      * Initializes the list of source entries.  If source entries are already
    119      * being displayed, removes them all in the process.
    120      */
    121     initializeSourceList_: function() {
    122       this.sourceIdToRowMap_ = {};
    123       $(WaterfallView.TBODY_ID).innerHTML = '';
    124       this.startTime_ = null;
    125       this.scaleFactor_ = null;
    126     },
    127 
    128     /**
    129      * Changes scroll position of the window such that horizontally, everything
    130      * within the specified range fits into the user's viewport.
    131      * TODO(viona): Find a way to get rid of the magic number.
    132      */
    133     adjustToWindow_: function(windowStart, windowEnd) {
    134       var maxWidth = $(WaterfallView.MAIN_BOX_ID).offsetWidth - 50;
    135       $(WaterfallView.TBODY_ID).width = maxWidth + 'px';
    136       var totalDuration = 0;
    137       if (windowEnd != -1) {
    138         totalDuration = windowEnd - windowStart;
    139       } else {
    140         for (var id in this.sourceIdToRowMap_) {
    141           var row = this.sourceIdToRowMap_[id];
    142           var rowDuration = row.getEndTime() - this.startTime_;
    143           if (totalDuration < rowDuration && !row.hide) {
    144             totalDuration = rowDuration;
    145           }
    146         }
    147       }
    148       if (totalDuration <= 0) {
    149         return;
    150       }
    151       this.scaleAll_(maxWidth / totalDuration);
    152       var waterfallLeft = $(WaterfallView.TIME_SCALE_HEADER_ID).offsetLeft;
    153       $(WaterfallView.MAIN_BOX_ID).scrollLeft =
    154           windowStart * this.scaleFactor_ + waterfallLeft;
    155     },
    156 
    157     // Updates the time tick indicators.
    158     updateTimeScale_: function(scaleFactor) {
    159       var timePerTick = 1;
    160       var minTickDistance = 20;
    161 
    162       $(WaterfallView.TIME_SCALE_HEADER_ID).innerHTML = '';
    163 
    164       // Holder provides environment to prevent wrapping.
    165       var timeTickRow = addNode($(WaterfallView.TIME_SCALE_HEADER_ID), 'div');
    166       timeTickRow.classList.add('waterfall-view-time-scale-row');
    167 
    168       var availableWidth = $(WaterfallView.TBODY_ID).clientWidth;
    169       var tickDistance = scaleFactor * timePerTick;
    170 
    171       while (tickDistance < minTickDistance) {
    172         timePerTick = timePerTick * 10;
    173         tickDistance = scaleFactor * timePerTick;
    174       }
    175 
    176       var tickCount = availableWidth / tickDistance;
    177       for (var i = 0; i < tickCount; ++i) {
    178         var timeCell = addNode(timeTickRow, 'div');
    179         setNodeWidth(timeCell, tickDistance);
    180         timeCell.classList.add('waterfall-view-time-scale');
    181         timeCell.title = i * timePerTick + ' to ' +
    182            (i + 1) * timePerTick + ' ms';
    183         // Red marker for every 5th bar.
    184         if (i % 5 == 0) {
    185           timeCell.classList.add('waterfall-view-time-scale-special');
    186         }
    187       }
    188     },
    189 
    190     /**
    191      * Scales all existing rows by scaleFactor.
    192      */
    193     scaleAll_: function(scaleFactor) {
    194       this.scaleFactor_ = scaleFactor;
    195       for (var id in this.sourceIdToRowMap_) {
    196         var row = this.sourceIdToRowMap_[id];
    197         row.updateRow();
    198       }
    199       this.updateTimeScale_(scaleFactor);
    200     },
    201 
    202     scrollToZoom_: function(event) {
    203       // To use scrolling to control zoom, hold down the alt key and scroll.
    204       if ('wheelDelta' in event && event.altKey) {
    205         var zoomFactor = Math.pow(MOUSE_WHEEL_ZOOM_RATE,
    206              event.wheelDeltaY / MOUSE_WHEEL_UNITS_PER_CLICK);
    207 
    208         var waterfallLeft = $(WaterfallView.TIME_SCALE_HEADER_ID).offsetLeft;
    209         var oldCursorPosition = event.pageX +
    210             $(WaterfallView.MAIN_BOX_ID).scrollLeft;
    211         var oldCursorPositionInTable = oldCursorPosition - waterfallLeft;
    212 
    213         this.scaleAll_(this.scaleFactor_ * zoomFactor);
    214 
    215         // Shifts the view when scrolling. newScroll could be less than 0 or
    216         // more than the maximum scroll position, but both cases are handled
    217         // by the inbuilt scrollLeft implementation.
    218         var newScroll =
    219             oldCursorPositionInTable * zoomFactor - event.pageX + waterfallLeft;
    220         $(WaterfallView.MAIN_BOX_ID).scrollLeft = newScroll;
    221       }
    222     },
    223 
    224     // Parses user input, then calls adjustToWindow to shift that into view.
    225     setStartEndTimes_: function() {
    226       var windowStart = parseInt($(WaterfallView.START_TIME_ID).value);
    227       var windowEnd = parseInt($(WaterfallView.END_TIME_ID).value);
    228       if ($(WaterfallView.END_TIME_ID).value == '') {
    229         windowEnd = -1;
    230       }
    231       if ($(WaterfallView.START_TIME_ID).value == '') {
    232         windowStart = 0;
    233       }
    234       this.adjustToWindow_(windowStart, windowEnd);
    235     },
    236 
    237     // Provides a structure that can be used to define source entry types and
    238     // the log entries that are of interest.
    239     createEventsList_: function() {
    240       var eventsList = [];
    241       // Creating list of events.
    242       // TODO(viona): This is hard-coded. Consider user-input.
    243       //              Also, work on getting socket information inlined in the
    244       //              relevant URL_REQUEST events.
    245       var urlRequestEvents = [
    246         EventType.HTTP_STREAM_REQUEST,
    247         EventType.HTTP_STREAM_REQUEST_BOUND_TO_JOB,
    248         EventType.HTTP_TRANSACTION_READ_HEADERS
    249       ];
    250 
    251       eventsList.push(this.createEventTypeObjects_(
    252           EventSourceType.URL_REQUEST, urlRequestEvents));
    253 
    254       var httpStreamJobEvents = [EventType.PROXY_SERVICE];
    255       eventsList.push(this.createEventTypeObjects_(
    256           EventSourceType.HTTP_STREAM_JOB, httpStreamJobEvents));
    257 
    258       return eventsList;
    259     },
    260 
    261     // Creates objects that can be used to define source entry types and
    262     // the log entries that are of interest.
    263     createEventTypeObjects_: function(mainEventType, eventTypesList) {
    264       var eventTypeObject = {
    265         mainEvent: mainEventType,
    266         importantEventTypes: eventTypesList
    267       };
    268       return eventTypeObject;
    269     },
    270   };
    271 
    272   return WaterfallView;
    273 })();
    274