Home | History | Annotate | Download | only in net_internals
      1 // Copyright (c) 2012 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 /**
      6  * EventsView displays a filtered list of all events sharing a source, and
      7  * a details pane for the selected sources.
      8  *
      9  *  +----------------------++----------------+
     10  *  |      filter box      ||                |
     11  *  +----------------------+|                |
     12  *  |                      ||                |
     13  *  |                      ||                |
     14  *  |                      ||                |
     15  *  |                      ||                |
     16  *  |     source list      ||    details     |
     17  *  |                      ||    view        |
     18  *  |                      ||                |
     19  *  |                      ||                |
     20  *  |                      ||                |
     21  *  |                      ||                |
     22  *  |                      ||                |
     23  *  |                      ||                |
     24  *  +----------------------++----------------+
     25  */
     26 var EventsView = (function() {
     27   'use strict';
     28 
     29   // How soon after updating the filter list the counter should be updated.
     30   var REPAINT_FILTER_COUNTER_TIMEOUT_MS = 0;
     31 
     32   // We inherit from View.
     33   var superClass = View;
     34 
     35   /*
     36    * @constructor
     37    */
     38   function EventsView() {
     39     assertFirstConstructorCall(EventsView);
     40 
     41     // Call superclass's constructor.
     42     superClass.call(this);
     43 
     44     // Initialize the sub-views.
     45     var leftPane = new VerticalSplitView(new DivView(EventsView.TOPBAR_ID),
     46                                          new DivView(EventsView.LIST_BOX_ID));
     47 
     48     this.detailsView_ = new DetailsView(EventsView.DETAILS_LOG_BOX_ID);
     49 
     50     this.splitterView_ = new ResizableVerticalSplitView(
     51         leftPane, this.detailsView_, new DivView(EventsView.SIZER_ID));
     52 
     53     SourceTracker.getInstance().addSourceEntryObserver(this);
     54 
     55     this.tableBody_ = $(EventsView.TBODY_ID);
     56 
     57     this.filterInput_ = $(EventsView.FILTER_INPUT_ID);
     58     this.filterCount_ = $(EventsView.FILTER_COUNT_ID);
     59 
     60     this.filterInput_.addEventListener('search',
     61         this.onFilterTextChanged_.bind(this), true);
     62 
     63     $(EventsView.SELECT_ALL_ID).addEventListener(
     64         'click', this.selectAll_.bind(this), true);
     65 
     66     $(EventsView.SORT_BY_ID_ID).addEventListener(
     67         'click', this.sortById_.bind(this), true);
     68 
     69     $(EventsView.SORT_BY_SOURCE_TYPE_ID).addEventListener(
     70         'click', this.sortBySourceType_.bind(this), true);
     71 
     72     $(EventsView.SORT_BY_DESCRIPTION_ID).addEventListener(
     73         'click', this.sortByDescription_.bind(this), true);
     74 
     75     new MouseOverHelp(EventsView.FILTER_HELP_ID,
     76                       EventsView.FILTER_HELP_HOVER_ID);
     77 
     78     // Sets sort order and filter.
     79     this.setFilter_('');
     80 
     81     this.initializeSourceList_();
     82   }
     83 
     84   EventsView.TAB_ID = 'tab-handle-events';
     85   EventsView.TAB_NAME = 'Events';
     86   EventsView.TAB_HASH = '#events';
     87 
     88   // IDs for special HTML elements in events_view.html
     89   EventsView.TBODY_ID = 'events-view-source-list-tbody';
     90   EventsView.FILTER_INPUT_ID = 'events-view-filter-input';
     91   EventsView.FILTER_COUNT_ID = 'events-view-filter-count';
     92   EventsView.FILTER_HELP_ID = 'events-view-filter-help';
     93   EventsView.FILTER_HELP_HOVER_ID = 'events-view-filter-help-hover';
     94   EventsView.SELECT_ALL_ID = 'events-view-select-all';
     95   EventsView.SORT_BY_ID_ID = 'events-view-sort-by-id';
     96   EventsView.SORT_BY_SOURCE_TYPE_ID = 'events-view-sort-by-source';
     97   EventsView.SORT_BY_DESCRIPTION_ID = 'events-view-sort-by-description';
     98   EventsView.DETAILS_LOG_BOX_ID = 'events-view-details-log-box';
     99   EventsView.TOPBAR_ID = 'events-view-filter-box';
    100   EventsView.LIST_BOX_ID = 'events-view-source-list';
    101   EventsView.SIZER_ID = 'events-view-splitter-box';
    102 
    103   cr.addSingletonGetter(EventsView);
    104 
    105   EventsView.prototype = {
    106     // Inherit the superclass's methods.
    107     __proto__: superClass.prototype,
    108 
    109     /**
    110      * Initializes the list of source entries.  If source entries are already,
    111      * being displayed, removes them all in the process.
    112      */
    113     initializeSourceList_: function() {
    114       this.currentSelectedRows_ = [];
    115       this.sourceIdToRowMap_ = {};
    116       this.tableBody_.innerHTML = '';
    117       this.numPrefilter_ = 0;
    118       this.numPostfilter_ = 0;
    119       this.invalidateFilterCounter_();
    120       this.invalidateDetailsView_();
    121     },
    122 
    123     setGeometry: function(left, top, width, height) {
    124       superClass.prototype.setGeometry.call(this, left, top, width, height);
    125       this.splitterView_.setGeometry(left, top, width, height);
    126     },
    127 
    128     show: function(isVisible) {
    129       superClass.prototype.show.call(this, isVisible);
    130       this.splitterView_.show(isVisible);
    131     },
    132 
    133     getFilterText_: function() {
    134       return this.filterInput_.value;
    135     },
    136 
    137     setFilterText_: function(filterText) {
    138       this.filterInput_.value = filterText;
    139       this.onFilterTextChanged_();
    140     },
    141 
    142     onFilterTextChanged_: function() {
    143       this.setFilter_(this.getFilterText_());
    144     },
    145 
    146     /**
    147      * Updates text in the details view when privacy stripping is toggled.
    148      */
    149     onPrivacyStrippingChanged: function() {
    150       this.invalidateDetailsView_();
    151     },
    152 
    153     /**
    154      * Updates text in the details view when time display mode is toggled.
    155      */
    156     onUseRelativeTimesChanged: function() {
    157       this.invalidateDetailsView_();
    158     },
    159 
    160     comparisonFuncWithReversing_: function(a, b) {
    161       var result = this.comparisonFunction_(a, b);
    162       if (this.doSortBackwards_)
    163         result *= -1;
    164       return result;
    165     },
    166 
    167     sort_: function() {
    168       var sourceEntries = [];
    169       for (var id in this.sourceIdToRowMap_) {
    170         sourceEntries.push(this.sourceIdToRowMap_[id].getSourceEntry());
    171       }
    172       sourceEntries.sort(this.comparisonFuncWithReversing_.bind(this));
    173 
    174       // Reposition source rows from back to front.
    175       for (var i = sourceEntries.length - 2; i >= 0; --i) {
    176         var sourceRow = this.sourceIdToRowMap_[sourceEntries[i].getSourceId()];
    177         var nextSourceId = sourceEntries[i + 1].getSourceId();
    178         if (sourceRow.getNextNodeSourceId() != nextSourceId) {
    179           var nextSourceRow = this.sourceIdToRowMap_[nextSourceId];
    180           sourceRow.moveBefore(nextSourceRow);
    181         }
    182       }
    183     },
    184 
    185     setFilter_: function(filterText) {
    186       var lastComparisonFunction = this.comparisonFunction_;
    187       var lastDoSortBackwards = this.doSortBackwards_;
    188 
    189       var filterParser = new SourceFilterParser(filterText);
    190       this.currentFilter_ = filterParser.filter;
    191 
    192       this.pickSortFunction_(filterParser.sort);
    193 
    194       if (lastComparisonFunction != this.comparisonFunction_ ||
    195           lastDoSortBackwards != this.doSortBackwards_) {
    196         this.sort_();
    197       }
    198 
    199       // Iterate through all of the rows and see if they match the filter.
    200       for (var id in this.sourceIdToRowMap_) {
    201         var entry = this.sourceIdToRowMap_[id];
    202         entry.setIsMatchedByFilter(this.currentFilter_(entry.getSourceEntry()));
    203       }
    204     },
    205 
    206     /**
    207      * Given a "sort" object with "method" and "backwards" keys, looks up and
    208      * sets |comparisonFunction_| and |doSortBackwards_|.  If the ID does not
    209      * correspond to a sort function, defaults to sorting by ID.
    210      */
    211     pickSortFunction_: function(sort) {
    212       this.doSortBackwards_ = sort.backwards;
    213       this.comparisonFunction_ = COMPARISON_FUNCTION_TABLE[sort.method];
    214       if (!this.comparisonFunction_) {
    215         this.doSortBackwards_ = false;
    216         this.comparisonFunction_ = compareSourceId_;
    217       }
    218     },
    219 
    220     /**
    221      * Repositions |sourceRow|'s in the table using an insertion sort.
    222      * Significantly faster than sorting the entire table again, when only
    223      * one entry has changed.
    224      */
    225     insertionSort_: function(sourceRow) {
    226       // SourceRow that should be after |sourceRow|, if it needs
    227       // to be moved earlier in the list.
    228       var sourceRowAfter = sourceRow;
    229       while (true) {
    230         var prevSourceId = sourceRowAfter.getPreviousNodeSourceId();
    231         if (prevSourceId == null)
    232           break;
    233         var prevSourceRow = this.sourceIdToRowMap_[prevSourceId];
    234         if (this.comparisonFuncWithReversing_(
    235                 sourceRow.getSourceEntry(),
    236                 prevSourceRow.getSourceEntry()) >= 0) {
    237           break;
    238         }
    239         sourceRowAfter = prevSourceRow;
    240       }
    241       if (sourceRowAfter != sourceRow) {
    242         sourceRow.moveBefore(sourceRowAfter);
    243         return;
    244       }
    245 
    246       var sourceRowBefore = sourceRow;
    247       while (true) {
    248         var nextSourceId = sourceRowBefore.getNextNodeSourceId();
    249         if (nextSourceId == null)
    250           break;
    251         var nextSourceRow = this.sourceIdToRowMap_[nextSourceId];
    252         if (this.comparisonFuncWithReversing_(
    253                 sourceRow.getSourceEntry(),
    254                 nextSourceRow.getSourceEntry()) <= 0) {
    255           break;
    256         }
    257         sourceRowBefore = nextSourceRow;
    258       }
    259       if (sourceRowBefore != sourceRow)
    260         sourceRow.moveAfter(sourceRowBefore);
    261     },
    262 
    263     /**
    264      * Called whenever SourceEntries are updated with new log entries.  Updates
    265      * the corresponding table rows, sort order, and the details view as needed.
    266      */
    267     onSourceEntriesUpdated: function(sourceEntries) {
    268       var isUpdatedSourceSelected = false;
    269       var numNewSourceEntries = 0;
    270 
    271       for (var i = 0; i < sourceEntries.length; ++i) {
    272         var sourceEntry = sourceEntries[i];
    273 
    274         // Lookup the row.
    275         var sourceRow = this.sourceIdToRowMap_[sourceEntry.getSourceId()];
    276 
    277         if (!sourceRow) {
    278           sourceRow = new SourceRow(this, sourceEntry);
    279           this.sourceIdToRowMap_[sourceEntry.getSourceId()] = sourceRow;
    280           ++numNewSourceEntries;
    281         } else {
    282           sourceRow.onSourceUpdated();
    283         }
    284 
    285         if (sourceRow.isSelected())
    286           isUpdatedSourceSelected = true;
    287 
    288         // TODO(mmenke): Fix sorting when sorting by duration.
    289         //               Duration continuously increases for all entries that
    290         //               are still active.  This can result in incorrect
    291         //               sorting, until sort_ is called.
    292         this.insertionSort_(sourceRow);
    293       }
    294 
    295       if (isUpdatedSourceSelected)
    296         this.invalidateDetailsView_();
    297       if (numNewSourceEntries)
    298         this.incrementPrefilterCount(numNewSourceEntries);
    299     },
    300 
    301     /**
    302      * Returns the SourceRow with the specified ID, if there is one.
    303      * Otherwise, returns undefined.
    304      */
    305     getSourceRow: function(id) {
    306       return this.sourceIdToRowMap_[id];
    307     },
    308 
    309     /**
    310      * Called whenever all log events are deleted.
    311      */
    312     onAllSourceEntriesDeleted: function() {
    313       this.initializeSourceList_();
    314     },
    315 
    316     /**
    317      * Called when either a log file is loaded, after clearing the old entries,
    318      * but before getting any new ones.
    319      */
    320     onLoadLogStart: function() {
    321       // Needed to sort new sourceless entries correctly.
    322       this.maxReceivedSourceId_ = 0;
    323     },
    324 
    325     onLoadLogFinish: function(data) {
    326       return true;
    327     },
    328 
    329     incrementPrefilterCount: function(offset) {
    330       this.numPrefilter_ += offset;
    331       this.invalidateFilterCounter_();
    332     },
    333 
    334     incrementPostfilterCount: function(offset) {
    335       this.numPostfilter_ += offset;
    336       this.invalidateFilterCounter_();
    337     },
    338 
    339     onSelectionChanged: function() {
    340       this.invalidateDetailsView_();
    341     },
    342 
    343     clearSelection: function() {
    344       var prevSelection = this.currentSelectedRows_;
    345       this.currentSelectedRows_ = [];
    346 
    347       // Unselect everything that is currently selected.
    348       for (var i = 0; i < prevSelection.length; ++i) {
    349         prevSelection[i].setSelected(false);
    350       }
    351 
    352       this.onSelectionChanged();
    353     },
    354 
    355     selectAll_: function(event) {
    356       for (var id in this.sourceIdToRowMap_) {
    357         var sourceRow = this.sourceIdToRowMap_[id];
    358         if (sourceRow.isMatchedByFilter()) {
    359           sourceRow.setSelected(true);
    360         }
    361       }
    362       event.preventDefault();
    363     },
    364 
    365     unselectAll_: function() {
    366       var entries = this.currentSelectedRows_.slice(0);
    367       for (var i = 0; i < entries.length; ++i) {
    368         entries[i].setSelected(false);
    369       }
    370     },
    371 
    372     /**
    373      * If |params| includes a query, replaces the current filter and unselects.
    374      * all items.  If it includes a selection, tries to select the relevant
    375      * item.
    376      */
    377     setParameters: function(params) {
    378       if (params.q) {
    379         this.unselectAll_();
    380         this.setFilterText_(params.q);
    381       }
    382 
    383       if (params.s) {
    384         var sourceRow = this.sourceIdToRowMap_[params.s];
    385         if (sourceRow) {
    386           sourceRow.setSelected(true);
    387           this.scrollToSourceId(params.s);
    388         }
    389       }
    390     },
    391 
    392     /**
    393      * Scrolls to the source indicated by |sourceId|, if displayed.
    394      */
    395     scrollToSourceId: function(sourceId) {
    396       this.detailsView_.scrollToSourceId(sourceId);
    397     },
    398 
    399     /**
    400      * If already using the specified sort method, flips direction.  Otherwise,
    401      * removes pre-existing sort parameter before adding the new one.
    402      */
    403     toggleSortMethod_: function(sortMethod) {
    404       // Get old filter text and remove old sort directives, if any.
    405       var filterParser = new SourceFilterParser(this.getFilterText_());
    406       var filterText = filterParser.filterTextWithoutSort;
    407 
    408       filterText = 'sort:' + sortMethod + ' ' + filterText;
    409 
    410       // If already using specified sortMethod, sort backwards.
    411       if (!this.doSortBackwards_ &&
    412           COMPARISON_FUNCTION_TABLE[sortMethod] == this.comparisonFunction_) {
    413         filterText = '-' + filterText;
    414       }
    415 
    416       this.setFilterText_(filterText.trim());
    417     },
    418 
    419     sortById_: function(event) {
    420       this.toggleSortMethod_('id');
    421     },
    422 
    423     sortBySourceType_: function(event) {
    424       this.toggleSortMethod_('source');
    425     },
    426 
    427     sortByDescription_: function(event) {
    428       this.toggleSortMethod_('desc');
    429     },
    430 
    431     /**
    432      * Modifies the map of selected rows to include/exclude the one with
    433      * |sourceId|, if present.  Does not modify checkboxes or the LogView.
    434      * Should only be called by a SourceRow in response to its selection
    435      * state changing.
    436      */
    437     modifySelectionArray: function(sourceId, addToSelection) {
    438       var sourceRow = this.sourceIdToRowMap_[sourceId];
    439       if (!sourceRow)
    440         return;
    441       // Find the index for |sourceEntry| in the current selection list.
    442       var index = -1;
    443       for (var i = 0; i < this.currentSelectedRows_.length; ++i) {
    444         if (this.currentSelectedRows_[i] == sourceRow) {
    445           index = i;
    446           break;
    447         }
    448       }
    449 
    450       if (index != -1 && !addToSelection) {
    451         // Remove from the selection.
    452         this.currentSelectedRows_.splice(index, 1);
    453       }
    454 
    455       if (index == -1 && addToSelection) {
    456         this.currentSelectedRows_.push(sourceRow);
    457       }
    458     },
    459 
    460     getSelectedSourceEntries_: function() {
    461       var sourceEntries = [];
    462       for (var i = 0; i < this.currentSelectedRows_.length; ++i) {
    463         sourceEntries.push(this.currentSelectedRows_[i].getSourceEntry());
    464       }
    465       return sourceEntries;
    466     },
    467 
    468     invalidateDetailsView_: function() {
    469       this.detailsView_.setData(this.getSelectedSourceEntries_());
    470     },
    471 
    472     invalidateFilterCounter_: function() {
    473       if (!this.outstandingRepaintFilterCounter_) {
    474         this.outstandingRepaintFilterCounter_ = true;
    475         window.setTimeout(this.repaintFilterCounter_.bind(this),
    476                           REPAINT_FILTER_COUNTER_TIMEOUT_MS);
    477       }
    478     },
    479 
    480     repaintFilterCounter_: function() {
    481       this.outstandingRepaintFilterCounter_ = false;
    482       this.filterCount_.innerHTML = '';
    483       addTextNode(this.filterCount_,
    484                   this.numPostfilter_ + ' of ' + this.numPrefilter_);
    485     }
    486   };  // end of prototype.
    487 
    488   // ------------------------------------------------------------------------
    489   // Helper code for comparisons
    490   // ------------------------------------------------------------------------
    491 
    492   var COMPARISON_FUNCTION_TABLE = {
    493     // sort: and sort:- are allowed
    494     '': compareSourceId_,
    495     'active': compareActive_,
    496     'desc': compareDescription_,
    497     'description': compareDescription_,
    498     'duration': compareDuration_,
    499     'id': compareSourceId_,
    500     'source': compareSourceType_,
    501     'type': compareSourceType_
    502   };
    503 
    504   /**
    505    * Sorts active entries first.  If both entries are inactive, puts the one
    506    * that was active most recently first.  If both are active, uses source ID,
    507    * which puts longer lived events at the top, and behaves better than using
    508    * duration or time of first event.
    509    */
    510   function compareActive_(source1, source2) {
    511     if (!source1.isInactive() && source2.isInactive())
    512       return -1;
    513     if (source1.isInactive() && !source2.isInactive())
    514       return 1;
    515     if (source1.isInactive()) {
    516       var deltaEndTime = source1.getEndTicks() - source2.getEndTicks();
    517       if (deltaEndTime != 0) {
    518         // The one that ended most recently (Highest end time) should be sorted
    519         // first.
    520         return -deltaEndTime;
    521       }
    522       // If both ended at the same time, then odds are they were related events,
    523       // started one after another, so sort in the opposite order of their
    524       // source IDs to get a more intuitive ordering.
    525       return -compareSourceId_(source1, source2);
    526     }
    527     return compareSourceId_(source1, source2);
    528   }
    529 
    530   function compareDescription_(source1, source2) {
    531     var source1Text = source1.getDescription().toLowerCase();
    532     var source2Text = source2.getDescription().toLowerCase();
    533     var compareResult = source1Text.localeCompare(source2Text);
    534     if (compareResult != 0)
    535       return compareResult;
    536     return compareSourceId_(source1, source2);
    537   }
    538 
    539   function compareDuration_(source1, source2) {
    540     var durationDifference = source2.getDuration() - source1.getDuration();
    541     if (durationDifference)
    542       return durationDifference;
    543     return compareSourceId_(source1, source2);
    544   }
    545 
    546   /**
    547    * For the purposes of sorting by source IDs, entries without a source
    548    * appear right after the SourceEntry with the highest source ID received
    549    * before the sourceless entry. Any ambiguities are resolved by ordering
    550    * the entries without a source by the order in which they were received.
    551    */
    552   function compareSourceId_(source1, source2) {
    553     var sourceId1 = source1.getSourceId();
    554     if (sourceId1 < 0)
    555       sourceId1 = source1.getMaxPreviousEntrySourceId();
    556     var sourceId2 = source2.getSourceId();
    557     if (sourceId2 < 0)
    558       sourceId2 = source2.getMaxPreviousEntrySourceId();
    559 
    560     if (sourceId1 != sourceId2)
    561       return sourceId1 - sourceId2;
    562 
    563     // One or both have a negative ID. In either case, the source with the
    564     // highest ID should be sorted first.
    565     return source2.getSourceId() - source1.getSourceId();
    566   }
    567 
    568   function compareSourceType_(source1, source2) {
    569     var source1Text = source1.getSourceTypeString();
    570     var source2Text = source2.getSourceTypeString();
    571     var compareResult = source1Text.localeCompare(source2Text);
    572     if (compareResult != 0)
    573       return compareResult;
    574     return compareSourceId_(source1, source2);
    575   }
    576 
    577   return EventsView;
    578 })();
    579