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     comparisonFuncWithReversing_: function(a, b) {
    154       var result = this.comparisonFunction_(a, b);
    155       if (this.doSortBackwards_)
    156         result *= -1;
    157       return result;
    158     },
    159 
    160     sort_: function() {
    161       var sourceEntries = [];
    162       for (var id in this.sourceIdToRowMap_) {
    163         sourceEntries.push(this.sourceIdToRowMap_[id].getSourceEntry());
    164       }
    165       sourceEntries.sort(this.comparisonFuncWithReversing_.bind(this));
    166 
    167       // Reposition source rows from back to front.
    168       for (var i = sourceEntries.length - 2; i >= 0; --i) {
    169         var sourceRow = this.sourceIdToRowMap_[sourceEntries[i].getSourceId()];
    170         var nextSourceId = sourceEntries[i + 1].getSourceId();
    171         if (sourceRow.getNextNodeSourceId() != nextSourceId) {
    172           var nextSourceRow = this.sourceIdToRowMap_[nextSourceId];
    173           sourceRow.moveBefore(nextSourceRow);
    174         }
    175       }
    176     },
    177 
    178     setFilter_: function(filterText) {
    179       var lastComparisonFunction = this.comparisonFunction_;
    180       var lastDoSortBackwards = this.doSortBackwards_;
    181 
    182       var filterParser = new SourceFilterParser(filterText);
    183       this.currentFilter_ = filterParser.filter;
    184 
    185       this.pickSortFunction_(filterParser.sort);
    186 
    187       if (lastComparisonFunction != this.comparisonFunction_ ||
    188           lastDoSortBackwards != this.doSortBackwards_) {
    189         this.sort_();
    190       }
    191 
    192       // Iterate through all of the rows and see if they match the filter.
    193       for (var id in this.sourceIdToRowMap_) {
    194         var entry = this.sourceIdToRowMap_[id];
    195         entry.setIsMatchedByFilter(this.currentFilter_(entry.getSourceEntry()));
    196       }
    197     },
    198 
    199     /**
    200      * Given a "sort" object with "method" and "backwards" keys, looks up and
    201      * sets |comparisonFunction_| and |doSortBackwards_|.  If the ID does not
    202      * correspond to a sort function, defaults to sorting by ID.
    203      */
    204     pickSortFunction_: function(sort) {
    205       this.doSortBackwards_ = sort.backwards;
    206       this.comparisonFunction_ = COMPARISON_FUNCTION_TABLE[sort.method];
    207       if (!this.comparisonFunction_) {
    208         this.doSortBackwards_ = false;
    209         this.comparisonFunction_ = compareSourceId_;
    210       }
    211     },
    212 
    213     /**
    214      * Repositions |sourceRow|'s in the table using an insertion sort.
    215      * Significantly faster than sorting the entire table again, when only
    216      * one entry has changed.
    217      */
    218     insertionSort_: function(sourceRow) {
    219       // SourceRow that should be after |sourceRow|, if it needs
    220       // to be moved earlier in the list.
    221       var sourceRowAfter = sourceRow;
    222       while (true) {
    223         var prevSourceId = sourceRowAfter.getPreviousNodeSourceId();
    224         if (prevSourceId == null)
    225           break;
    226         var prevSourceRow = this.sourceIdToRowMap_[prevSourceId];
    227         if (this.comparisonFuncWithReversing_(
    228                 sourceRow.getSourceEntry(),
    229                 prevSourceRow.getSourceEntry()) >= 0) {
    230           break;
    231         }
    232         sourceRowAfter = prevSourceRow;
    233       }
    234       if (sourceRowAfter != sourceRow) {
    235         sourceRow.moveBefore(sourceRowAfter);
    236         return;
    237       }
    238 
    239       var sourceRowBefore = sourceRow;
    240       while (true) {
    241         var nextSourceId = sourceRowBefore.getNextNodeSourceId();
    242         if (nextSourceId == null)
    243           break;
    244         var nextSourceRow = this.sourceIdToRowMap_[nextSourceId];
    245         if (this.comparisonFuncWithReversing_(
    246                 sourceRow.getSourceEntry(),
    247                 nextSourceRow.getSourceEntry()) <= 0) {
    248           break;
    249         }
    250         sourceRowBefore = nextSourceRow;
    251       }
    252       if (sourceRowBefore != sourceRow)
    253         sourceRow.moveAfter(sourceRowBefore);
    254     },
    255 
    256     /**
    257      * Called whenever SourceEntries are updated with new log entries.  Updates
    258      * the corresponding table rows, sort order, and the details view as needed.
    259      */
    260     onSourceEntriesUpdated: function(sourceEntries) {
    261       var isUpdatedSourceSelected = false;
    262       var numNewSourceEntries = 0;
    263 
    264       for (var i = 0; i < sourceEntries.length; ++i) {
    265         var sourceEntry = sourceEntries[i];
    266 
    267         // Lookup the row.
    268         var sourceRow = this.sourceIdToRowMap_[sourceEntry.getSourceId()];
    269 
    270         if (!sourceRow) {
    271           sourceRow = new SourceRow(this, sourceEntry);
    272           this.sourceIdToRowMap_[sourceEntry.getSourceId()] = sourceRow;
    273           ++numNewSourceEntries;
    274         } else {
    275           sourceRow.onSourceUpdated();
    276         }
    277 
    278         if (sourceRow.isSelected())
    279           isUpdatedSourceSelected = true;
    280 
    281         // TODO(mmenke): Fix sorting when sorting by duration.
    282         //               Duration continuously increases for all entries that
    283         //               are still active.  This can result in incorrect
    284         //               sorting, until sort_ is called.
    285         this.insertionSort_(sourceRow);
    286       }
    287 
    288       if (isUpdatedSourceSelected)
    289         this.invalidateDetailsView_();
    290       if (numNewSourceEntries)
    291         this.incrementPrefilterCount(numNewSourceEntries);
    292     },
    293 
    294     /**
    295      * Returns the SourceRow with the specified ID, if there is one.
    296      * Otherwise, returns undefined.
    297      */
    298     getSourceRow: function(id) {
    299       return this.sourceIdToRowMap_[id];
    300     },
    301 
    302     /**
    303      * Called whenever all log events are deleted.
    304      */
    305     onAllSourceEntriesDeleted: function() {
    306       this.initializeSourceList_();
    307     },
    308 
    309     /**
    310      * Called when either a log file is loaded, after clearing the old entries,
    311      * but before getting any new ones.
    312      */
    313     onLoadLogStart: function() {
    314       // Needed to sort new sourceless entries correctly.
    315       this.maxReceivedSourceId_ = 0;
    316     },
    317 
    318     onLoadLogFinish: function(data) {
    319       return true;
    320     },
    321 
    322     incrementPrefilterCount: function(offset) {
    323       this.numPrefilter_ += offset;
    324       this.invalidateFilterCounter_();
    325     },
    326 
    327     incrementPostfilterCount: function(offset) {
    328       this.numPostfilter_ += offset;
    329       this.invalidateFilterCounter_();
    330     },
    331 
    332     onSelectionChanged: function() {
    333       this.invalidateDetailsView_();
    334     },
    335 
    336     clearSelection: function() {
    337       var prevSelection = this.currentSelectedRows_;
    338       this.currentSelectedRows_ = [];
    339 
    340       // Unselect everything that is currently selected.
    341       for (var i = 0; i < prevSelection.length; ++i) {
    342         prevSelection[i].setSelected(false);
    343       }
    344 
    345       this.onSelectionChanged();
    346     },
    347 
    348     selectAll_: function(event) {
    349       for (var id in this.sourceIdToRowMap_) {
    350         var sourceRow = this.sourceIdToRowMap_[id];
    351         if (sourceRow.isMatchedByFilter()) {
    352           sourceRow.setSelected(true);
    353         }
    354       }
    355       event.preventDefault();
    356     },
    357 
    358     unselectAll_: function() {
    359       var entries = this.currentSelectedRows_.slice(0);
    360       for (var i = 0; i < entries.length; ++i) {
    361         entries[i].setSelected(false);
    362       }
    363     },
    364 
    365     /**
    366      * If |params| includes a query, replaces the current filter and unselects.
    367      * all items.  If it includes a selection, tries to select the relevant
    368      * item.
    369      */
    370     setParameters: function(params) {
    371       if (params.q) {
    372         this.unselectAll_();
    373         this.setFilterText_(params.q);
    374       }
    375 
    376       if (params.s) {
    377         var sourceRow = this.sourceIdToRowMap_[params.s];
    378         if (sourceRow) {
    379           sourceRow.setSelected(true);
    380           this.scrollToSourceId(params.s);
    381         }
    382       }
    383     },
    384 
    385     /**
    386      * Scrolls to the source indicated by |sourceId|, if displayed.
    387      */
    388     scrollToSourceId: function(sourceId) {
    389       this.detailsView_.scrollToSourceId(sourceId);
    390     },
    391 
    392     /**
    393      * If already using the specified sort method, flips direction.  Otherwise,
    394      * removes pre-existing sort parameter before adding the new one.
    395      */
    396     toggleSortMethod_: function(sortMethod) {
    397       // Get old filter text and remove old sort directives, if any.
    398       var filterParser = new SourceFilterParser(this.getFilterText_());
    399       var filterText = filterParser.filterTextWithoutSort;
    400 
    401       filterText = 'sort:' + sortMethod + ' ' + filterText;
    402 
    403       // If already using specified sortMethod, sort backwards.
    404       if (!this.doSortBackwards_ &&
    405           COMPARISON_FUNCTION_TABLE[sortMethod] == this.comparisonFunction_) {
    406         filterText = '-' + filterText;
    407       }
    408 
    409       this.setFilterText_(filterText.trim());
    410     },
    411 
    412     sortById_: function(event) {
    413       this.toggleSortMethod_('id');
    414     },
    415 
    416     sortBySourceType_: function(event) {
    417       this.toggleSortMethod_('source');
    418     },
    419 
    420     sortByDescription_: function(event) {
    421       this.toggleSortMethod_('desc');
    422     },
    423 
    424     /**
    425      * Modifies the map of selected rows to include/exclude the one with
    426      * |sourceId|, if present.  Does not modify checkboxes or the LogView.
    427      * Should only be called by a SourceRow in response to its selection
    428      * state changing.
    429      */
    430     modifySelectionArray: function(sourceId, addToSelection) {
    431       var sourceRow = this.sourceIdToRowMap_[sourceId];
    432       if (!sourceRow)
    433         return;
    434       // Find the index for |sourceEntry| in the current selection list.
    435       var index = -1;
    436       for (var i = 0; i < this.currentSelectedRows_.length; ++i) {
    437         if (this.currentSelectedRows_[i] == sourceRow) {
    438           index = i;
    439           break;
    440         }
    441       }
    442 
    443       if (index != -1 && !addToSelection) {
    444         // Remove from the selection.
    445         this.currentSelectedRows_.splice(index, 1);
    446       }
    447 
    448       if (index == -1 && addToSelection) {
    449         this.currentSelectedRows_.push(sourceRow);
    450       }
    451     },
    452 
    453     getSelectedSourceEntries_: function() {
    454       var sourceEntries = [];
    455       for (var i = 0; i < this.currentSelectedRows_.length; ++i) {
    456         sourceEntries.push(this.currentSelectedRows_[i].getSourceEntry());
    457       }
    458       return sourceEntries;
    459     },
    460 
    461     invalidateDetailsView_: function() {
    462       this.detailsView_.setData(this.getSelectedSourceEntries_());
    463     },
    464 
    465     invalidateFilterCounter_: function() {
    466       if (!this.outstandingRepaintFilterCounter_) {
    467         this.outstandingRepaintFilterCounter_ = true;
    468         window.setTimeout(this.repaintFilterCounter_.bind(this),
    469                           REPAINT_FILTER_COUNTER_TIMEOUT_MS);
    470       }
    471     },
    472 
    473     repaintFilterCounter_: function() {
    474       this.outstandingRepaintFilterCounter_ = false;
    475       this.filterCount_.innerHTML = '';
    476       addTextNode(this.filterCount_,
    477                   this.numPostfilter_ + ' of ' + this.numPrefilter_);
    478     }
    479   };  // end of prototype.
    480 
    481   // ------------------------------------------------------------------------
    482   // Helper code for comparisons
    483   // ------------------------------------------------------------------------
    484 
    485   var COMPARISON_FUNCTION_TABLE = {
    486     // sort: and sort:- are allowed
    487     '': compareSourceId_,
    488     'active': compareActive_,
    489     'desc': compareDescription_,
    490     'description': compareDescription_,
    491     'duration': compareDuration_,
    492     'id': compareSourceId_,
    493     'source': compareSourceType_,
    494     'type': compareSourceType_
    495   };
    496 
    497   /**
    498    * Sorts active entries first.  If both entries are inactive, puts the one
    499    * that was active most recently first.  If both are active, uses source ID,
    500    * which puts longer lived events at the top, and behaves better than using
    501    * duration or time of first event.
    502    */
    503   function compareActive_(source1, source2) {
    504     if (!source1.isInactive() && source2.isInactive())
    505       return -1;
    506     if (source1.isInactive() && !source2.isInactive())
    507       return 1;
    508     if (source1.isInactive()) {
    509       var deltaEndTime = source1.getEndTime() - source2.getEndTime();
    510       if (deltaEndTime != 0) {
    511         // The one that ended most recently (Highest end time) should be sorted
    512         // first.
    513         return -deltaEndTime;
    514       }
    515       // If both ended at the same time, then odds are they were related events,
    516       // started one after another, so sort in the opposite order of their
    517       // source IDs to get a more intuitive ordering.
    518       return -compareSourceId_(source1, source2);
    519     }
    520     return compareSourceId_(source1, source2);
    521   }
    522 
    523   function compareDescription_(source1, source2) {
    524     var source1Text = source1.getDescription().toLowerCase();
    525     var source2Text = source2.getDescription().toLowerCase();
    526     var compareResult = source1Text.localeCompare(source2Text);
    527     if (compareResult != 0)
    528       return compareResult;
    529     return compareSourceId_(source1, source2);
    530   }
    531 
    532   function compareDuration_(source1, source2) {
    533     var durationDifference = source2.getDuration() - source1.getDuration();
    534     if (durationDifference)
    535       return durationDifference;
    536     return compareSourceId_(source1, source2);
    537   }
    538 
    539   /**
    540    * For the purposes of sorting by source IDs, entries without a source
    541    * appear right after the SourceEntry with the highest source ID received
    542    * before the sourceless entry. Any ambiguities are resolved by ordering
    543    * the entries without a source by the order in which they were received.
    544    */
    545   function compareSourceId_(source1, source2) {
    546     var sourceId1 = source1.getSourceId();
    547     if (sourceId1 < 0)
    548       sourceId1 = source1.getMaxPreviousEntrySourceId();
    549     var sourceId2 = source2.getSourceId();
    550     if (sourceId2 < 0)
    551       sourceId2 = source2.getMaxPreviousEntrySourceId();
    552 
    553     if (sourceId1 != sourceId2)
    554       return sourceId1 - sourceId2;
    555 
    556     // One or both have a negative ID. In either case, the source with the
    557     // highest ID should be sorted first.
    558     return source2.getSourceId() - source1.getSourceId();
    559   }
    560 
    561   function compareSourceType_(source1, source2) {
    562     var source1Text = source1.getSourceTypeString();
    563     var source2Text = source2.getSourceTypeString();
    564     var compareResult = source1Text.localeCompare(source2Text);
    565     if (compareResult != 0)
    566       return compareResult;
    567     return compareSourceId_(source1, source2);
    568   }
    569 
    570   return EventsView;
    571 })();
    572