Home | History | Annotate | Download | only in net_internals
      1 // Copyright (c) 2011 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  *  |      action bar      ||                |
     24  *  +----------------------++----------------+
     25  *
     26  * @constructor
     27  */
     28 function EventsView(tableBodyId, filterInputId, filterCountId,
     29                     deleteSelectedId, deleteAllId, selectAllId, sortByIdId,
     30                     sortBySourceTypeId, sortByDescriptionId,
     31                     tabHandlesContainerId, logTabId, timelineTabId,
     32                     detailsLogBoxId, detailsTimelineBoxId,
     33                     topbarId, middleboxId, bottombarId, sizerId) {
     34   View.call(this);
     35 
     36   // Used for sorting entries with automatically assigned IDs.
     37   this.maxReceivedSourceId_ = 0;
     38 
     39   // Initialize the sub-views.
     40   var leftPane = new TopMidBottomView(new DivView(topbarId),
     41                                       new DivView(middleboxId),
     42                                       new DivView(bottombarId));
     43 
     44   this.detailsView_ = new DetailsView(tabHandlesContainerId,
     45                                       logTabId,
     46                                       timelineTabId,
     47                                       detailsLogBoxId,
     48                                       detailsTimelineBoxId);
     49 
     50   this.splitterView_ = new ResizableVerticalSplitView(
     51       leftPane, this.detailsView_, new DivView(sizerId));
     52 
     53   g_browser.addLogObserver(this);
     54 
     55   this.tableBody_ = document.getElementById(tableBodyId);
     56 
     57   this.filterInput_ = document.getElementById(filterInputId);
     58   this.filterCount_ = document.getElementById(filterCountId);
     59 
     60   this.filterInput_.addEventListener('search',
     61       this.onFilterTextChanged_.bind(this), true);
     62 
     63   document.getElementById(deleteSelectedId).onclick =
     64       this.deleteSelected_.bind(this);
     65 
     66   document.getElementById(deleteAllId).onclick =
     67       g_browser.deleteAllEvents.bind(g_browser);
     68 
     69   document.getElementById(selectAllId).addEventListener(
     70       'click', this.selectAll_.bind(this), true);
     71 
     72   document.getElementById(sortByIdId).addEventListener(
     73       'click', this.sortById_.bind(this), true);
     74 
     75   document.getElementById(sortBySourceTypeId).addEventListener(
     76       'click', this.sortBySourceType_.bind(this), true);
     77 
     78   document.getElementById(sortByDescriptionId).addEventListener(
     79       'click', this.sortByDescription_.bind(this), true);
     80 
     81   // Sets sort order and filter.
     82   this.setFilter_('');
     83 
     84   this.initializeSourceList_();
     85 }
     86 
     87 inherits(EventsView, View);
     88 
     89 /**
     90  * Initializes the list of source entries.  If source entries are already,
     91  * being displayed, removes them all in the process.
     92  */
     93 EventsView.prototype.initializeSourceList_ = function() {
     94   this.currentSelectedSources_ = [];
     95   this.sourceIdToEntryMap_ = {};
     96   this.tableBody_.innerHTML = '';
     97   this.numPrefilter_ = 0;
     98   this.numPostfilter_ = 0;
     99   this.invalidateFilterCounter_();
    100   this.invalidateDetailsView_();
    101 };
    102 
    103 // How soon after updating the filter list the counter should be updated.
    104 EventsView.REPAINT_FILTER_COUNTER_TIMEOUT_MS = 0;
    105 
    106 EventsView.prototype.setGeometry = function(left, top, width, height) {
    107   EventsView.superClass_.setGeometry.call(this, left, top, width, height);
    108   this.splitterView_.setGeometry(left, top, width, height);
    109 };
    110 
    111 EventsView.prototype.show = function(isVisible) {
    112   EventsView.superClass_.show.call(this, isVisible);
    113   this.splitterView_.show(isVisible);
    114 };
    115 
    116 EventsView.prototype.getFilterText_ = function() {
    117   return this.filterInput_.value;
    118 };
    119 
    120 EventsView.prototype.setFilterText_ = function(filterText) {
    121   this.filterInput_.value = filterText;
    122   this.onFilterTextChanged_();
    123 };
    124 
    125 EventsView.prototype.onFilterTextChanged_ = function() {
    126   this.setFilter_(this.getFilterText_());
    127 };
    128 
    129 /**
    130  * Updates text in the details view when security stripping is toggled.
    131  */
    132 EventsView.prototype.onSecurityStrippingChanged = function() {
    133   this.invalidateDetailsView_();
    134 }
    135 
    136 /**
    137  * Sorts active entries first.   If both entries are inactive, puts the one
    138  * that was active most recently first.  If both are active, uses source ID,
    139  * which puts longer lived events at the top, and behaves better than using
    140  * duration or time of first event.
    141  */
    142 EventsView.compareActive_ = function(source1, source2) {
    143   if (source1.isActive() && !source2.isActive())
    144     return -1;
    145   if (!source1.isActive() && source2.isActive())
    146     return  1;
    147   if (!source1.isActive()) {
    148     var deltaEndTime = source1.getEndTime() - source2.getEndTime();
    149     if (deltaEndTime != 0) {
    150       // The one that ended most recently (Highest end time) should be sorted
    151       // first.
    152       return -deltaEndTime;
    153     }
    154     // If both ended at the same time, then odds are they were related events,
    155     // started one after another, so sort in the opposite order of their
    156     // source IDs to get a more intuitive ordering.
    157     return -EventsView.compareSourceId_(source1, source2);
    158   }
    159   return EventsView.compareSourceId_(source1, source2);
    160 };
    161 
    162 EventsView.compareDescription_ = function(source1, source2) {
    163   var source1Text = source1.getDescription().toLowerCase();
    164   var source2Text = source2.getDescription().toLowerCase();
    165   var compareResult = source1Text.localeCompare(source2Text);
    166   if (compareResult != 0)
    167     return compareResult;
    168   return EventsView.compareSourceId_(source1, source2);
    169 };
    170 
    171 EventsView.compareDuration_ = function(source1, source2) {
    172   var durationDifference = source2.getDuration() - source1.getDuration();
    173   if (durationDifference)
    174     return durationDifference;
    175   return EventsView.compareSourceId_(source1, source2);
    176 };
    177 
    178 /**
    179  * For the purposes of sorting by source IDs, entries without a source
    180  * appear right after the SourceEntry with the highest source ID received
    181  * before the sourceless entry. Any ambiguities are resolved by ordering
    182  * the entries without a source by the order in which they were received.
    183  */
    184 EventsView.compareSourceId_ = function(source1, source2) {
    185   var sourceId1 = source1.getSourceId();
    186   if (sourceId1 < 0)
    187     sourceId1 = source1.getMaxPreviousEntrySourceId();
    188   var sourceId2 = source2.getSourceId();
    189   if (sourceId2 < 0)
    190     sourceId2 = source2.getMaxPreviousEntrySourceId();
    191 
    192   if (sourceId1 != sourceId2)
    193     return sourceId1 - sourceId2;
    194 
    195   // One or both have a negative ID. In either case, the source with the
    196   // highest ID should be sorted first.
    197   return source2.getSourceId() - source1.getSourceId();
    198 };
    199 
    200 EventsView.compareSourceType_ = function(source1, source2) {
    201   var source1Text = source1.getSourceTypeString();
    202   var source2Text = source2.getSourceTypeString();
    203   var compareResult = source1Text.localeCompare(source2Text);
    204   if (compareResult != 0)
    205     return compareResult;
    206   return EventsView.compareSourceId_(source1, source2);
    207 };
    208 
    209 EventsView.prototype.comparisonFuncWithReversing_ = function(a, b) {
    210   var result = this.comparisonFunction_(a, b);
    211   if (this.doSortBackwards_)
    212     result *= -1;
    213   return result;
    214 };
    215 
    216 EventsView.comparisonFunctionTable_ = {
    217   // sort: and sort:- are allowed
    218   '':            EventsView.compareSourceId_,
    219   'active':      EventsView.compareActive_,
    220   'desc':        EventsView.compareDescription_,
    221   'description': EventsView.compareDescription_,
    222   'duration':    EventsView.compareDuration_,
    223   'id':          EventsView.compareSourceId_,
    224   'source':      EventsView.compareSourceType_,
    225   'type':        EventsView.compareSourceType_
    226 };
    227 
    228 EventsView.prototype.Sort_ = function() {
    229   var sourceEntries = [];
    230   for (var id in this.sourceIdToEntryMap_) {
    231     // Can only sort items with an actual row in the table.
    232     if (this.sourceIdToEntryMap_[id].hasRow())
    233       sourceEntries.push(this.sourceIdToEntryMap_[id]);
    234   }
    235   sourceEntries.sort(this.comparisonFuncWithReversing_.bind(this));
    236 
    237   for (var i = sourceEntries.length - 2; i >= 0; --i) {
    238     if (sourceEntries[i].getNextNodeSourceId() !=
    239         sourceEntries[i + 1].getSourceId())
    240       sourceEntries[i].moveBefore(sourceEntries[i + 1]);
    241   }
    242 };
    243 
    244 /**
    245  * Looks for the first occurence of |directive|:parameter in |sourceText|.
    246  * Parameter can be an empty string.
    247  *
    248  * On success, returns an object with two fields:
    249  *   |remainingText| - |sourceText| with |directive|:parameter removed,
    250                        and excess whitespace deleted.
    251  *   |parameter| - the parameter itself.
    252  *
    253  * On failure, returns null.
    254  */
    255 EventsView.prototype.parseDirective_ = function(sourceText, directive) {
    256   // Adding a leading space allows a single regexp to be used, regardless of
    257   // whether or not the directive is at the start of the string.
    258   sourceText = ' ' + sourceText;
    259   regExp = new RegExp('\\s+' + directive + ':(\\S*)\\s*', 'i');
    260   matchInfo = regExp.exec(sourceText);
    261   if (matchInfo == null)
    262     return null;
    263 
    264   return {'remainingText': sourceText.replace(regExp, ' ').trim(),
    265           'parameter': matchInfo[1]};
    266 };
    267 
    268 /**
    269  * Just like parseDirective_, except can optionally be a '-' before or
    270  * the parameter, to negate it.  Before is more natural, after
    271  * allows more convenient toggling.
    272  *
    273  * Returned value has the additional field |isNegated|, and a leading
    274  * '-' will be removed from |parameter|, if present.
    275  */
    276 EventsView.prototype.parseNegatableDirective_ = function(sourceText,
    277                                                          directive) {
    278   var matchInfo = this.parseDirective_(sourceText, directive);
    279   if (matchInfo == null)
    280     return null;
    281 
    282   // Remove any leading or trailing '-' from the directive.
    283   var negationInfo = /^(-?)(\S*?)$/.exec(matchInfo.parameter);
    284   matchInfo.parameter = negationInfo[2];
    285   matchInfo.isNegated = (negationInfo[1] == '-');
    286   return matchInfo;
    287 };
    288 
    289 /**
    290  * Parse any "sort:" directives, and update |comparisonFunction_| and
    291  * |doSortBackwards_|as needed.  Note only the last valid sort directive
    292  * is used.
    293  *
    294  * Returns |filterText| with all sort directives removed, including
    295  * invalid ones.
    296  */
    297 EventsView.prototype.parseSortDirectives_ = function(filterText) {
    298   this.comparisonFunction_ = EventsView.compareSourceId_;
    299   this.doSortBackwards_ = false;
    300 
    301   while (true) {
    302     var sortInfo = this.parseNegatableDirective_(filterText, 'sort');
    303     if (sortInfo == null)
    304       break;
    305     var comparisonName = sortInfo.parameter.toLowerCase();
    306     if (EventsView.comparisonFunctionTable_[comparisonName] != null) {
    307       this.comparisonFunction_ =
    308           EventsView.comparisonFunctionTable_[comparisonName];
    309       this.doSortBackwards_ = sortInfo.isNegated;
    310     }
    311     filterText = sortInfo.remainingText;
    312   }
    313 
    314   return filterText;
    315 };
    316 
    317 /**
    318  * Parse any "is:" directives, and update |filter| accordingly.
    319  *
    320  * Returns |filterText| with all "is:" directives removed, including
    321  * invalid ones.
    322  */
    323 EventsView.prototype.parseRestrictDirectives_ = function(filterText, filter) {
    324   while (true) {
    325     var filterInfo = this.parseNegatableDirective_(filterText, 'is');
    326     if (filterInfo == null)
    327       break;
    328     if (filterInfo.parameter == 'active') {
    329       if (!filterInfo.isNegated)
    330         filter.isActive = true;
    331       else
    332         filter.isInactive = true;
    333     }
    334     filterText = filterInfo.remainingText;
    335   }
    336   return filterText;
    337 };
    338 
    339 /**
    340  * Parses all directives that take arbitrary strings as input,
    341  * and updates |filter| accordingly.  Directives of these types
    342  * are stored as lists.
    343  *
    344  * Returns |filterText| with all recognized directives removed.
    345  */
    346 EventsView.prototype.parseStringDirectives_ = function(filterText, filter) {
    347   var directives = ['type', 'id'];
    348   for (var i = 0; i < directives.length; ++i) {
    349     while (true) {
    350       var directive = directives[i];
    351       var filterInfo = this.parseDirective_(filterText, directive);
    352       if (filterInfo == null)
    353         break;
    354       if (!filter[directive])
    355         filter[directive] = [];
    356       filter[directive].push(filterInfo.parameter);
    357       filterText = filterInfo.remainingText;
    358     }
    359   }
    360   return filterText;
    361 };
    362 
    363 /*
    364  * Converts |filterText| into an object representing the filter.
    365  */
    366 EventsView.prototype.createFilter_ = function(filterText) {
    367   var filter = {};
    368   filterText = filterText.toLowerCase();
    369   filterText = this.parseRestrictDirectives_(filterText, filter);
    370   filterText = this.parseStringDirectives_(filterText, filter);
    371   filter.text = filterText.trim();
    372   return filter;
    373 };
    374 
    375 EventsView.prototype.setFilter_ = function(filterText) {
    376   var lastComparisonFunction = this.comparisonFunction_;
    377   var lastDoSortBackwards = this.doSortBackwards_;
    378 
    379   filterText = this.parseSortDirectives_(filterText);
    380 
    381   if (lastComparisonFunction != this.comparisonFunction_ ||
    382       lastDoSortBackwards != this.doSortBackwards_) {
    383     this.Sort_();
    384   }
    385 
    386   this.currentFilter_ = this.createFilter_(filterText);
    387 
    388   // Iterate through all of the rows and see if they match the filter.
    389   for (var id in this.sourceIdToEntryMap_) {
    390     var entry = this.sourceIdToEntryMap_[id];
    391     entry.setIsMatchedByFilter(entry.matchesFilter(this.currentFilter_));
    392   }
    393 };
    394 
    395 /**
    396  * Repositions |sourceEntry|'s row in the table using an insertion sort.
    397  * Significantly faster than sorting the entire table again, when only
    398  * one entry has changed.
    399  */
    400 EventsView.prototype.InsertionSort_ = function(sourceEntry) {
    401   // SourceEntry that should be after |sourceEntry|, if it needs
    402   // to be moved earlier in the list.
    403   var sourceEntryAfter = sourceEntry;
    404   while (true) {
    405     var prevSourceId = sourceEntryAfter.getPreviousNodeSourceId();
    406     if (prevSourceId == null)
    407       break;
    408     var prevSourceEntry = this.sourceIdToEntryMap_[prevSourceId];
    409     if (this.comparisonFuncWithReversing_(sourceEntry, prevSourceEntry) >= 0)
    410       break;
    411     sourceEntryAfter = prevSourceEntry;
    412   }
    413   if (sourceEntryAfter != sourceEntry) {
    414     sourceEntry.moveBefore(sourceEntryAfter);
    415     return;
    416   }
    417 
    418   var sourceEntryBefore = sourceEntry;
    419   while (true) {
    420     var nextSourceId = sourceEntryBefore.getNextNodeSourceId();
    421     if (nextSourceId == null)
    422       break;
    423     var nextSourceEntry = this.sourceIdToEntryMap_[nextSourceId];
    424     if (this.comparisonFuncWithReversing_(sourceEntry, nextSourceEntry) <= 0)
    425       break;
    426     sourceEntryBefore = nextSourceEntry;
    427   }
    428   if (sourceEntryBefore != sourceEntry)
    429     sourceEntry.moveAfter(sourceEntryBefore);
    430 };
    431 
    432 EventsView.prototype.onLogEntryAdded = function(logEntry) {
    433   var id = logEntry.source.id;
    434 
    435   // Lookup the source.
    436   var sourceEntry = this.sourceIdToEntryMap_[id];
    437 
    438   if (!sourceEntry) {
    439     sourceEntry = new SourceEntry(this, this.maxReceivedSourceId_);
    440     this.sourceIdToEntryMap_[id] = sourceEntry;
    441     this.incrementPrefilterCount(1);
    442     if (id > this.maxReceivedSourceId_)
    443       this.maxReceivedSourceId_ = id;
    444   }
    445 
    446   sourceEntry.update(logEntry);
    447 
    448   if (sourceEntry.isSelected())
    449     this.invalidateDetailsView_();
    450 
    451   // TODO(mmenke): Fix sorting when sorting by duration.
    452   //               Duration continuously increases for all entries that are
    453   //               still active.  This can result in incorrect sorting, until
    454   //               Sort_ is called.
    455   this.InsertionSort_(sourceEntry);
    456 };
    457 
    458 /**
    459  * Returns the SourceEntry with the specified ID, if there is one.
    460  * Otherwise, returns undefined.
    461  */
    462 EventsView.prototype.getSourceEntry = function(id) {
    463   return this.sourceIdToEntryMap_[id];
    464 };
    465 
    466 /**
    467  * Called whenever some log events are deleted.  |sourceIds| lists
    468  * the source IDs of all deleted log entries.
    469  */
    470 EventsView.prototype.onLogEntriesDeleted = function(sourceIds) {
    471   for (var i = 0; i < sourceIds.length; ++i) {
    472     var id = sourceIds[i];
    473     var entry = this.sourceIdToEntryMap_[id];
    474     if (entry) {
    475       entry.remove();
    476       delete this.sourceIdToEntryMap_[id];
    477       this.incrementPrefilterCount(-1);
    478     }
    479   }
    480 };
    481 
    482 /**
    483  * Called whenever all log events are deleted.
    484  */
    485 EventsView.prototype.onAllLogEntriesDeleted = function() {
    486   this.initializeSourceList_();
    487 };
    488 
    489 /**
    490  * Called when either a log file is loaded or when going back to actively
    491  * logging events.  In either case, called after clearing the old entries,
    492  * but before getting any new ones.
    493  */
    494 EventsView.prototype.onSetIsViewingLogFile = function(isViewingLogFile) {
    495   // Needed to sort new sourceless entries correctly.
    496   this.maxReceivedSourceId_ = 0;
    497 };
    498 
    499 EventsView.prototype.incrementPrefilterCount = function(offset) {
    500   this.numPrefilter_ += offset;
    501   this.invalidateFilterCounter_();
    502 };
    503 
    504 EventsView.prototype.incrementPostfilterCount = function(offset) {
    505   this.numPostfilter_ += offset;
    506   this.invalidateFilterCounter_();
    507 };
    508 
    509 EventsView.prototype.onSelectionChanged = function() {
    510   this.invalidateDetailsView_();
    511 };
    512 
    513 EventsView.prototype.clearSelection = function() {
    514   var prevSelection = this.currentSelectedSources_;
    515   this.currentSelectedSources_ = [];
    516 
    517   // Unselect everything that is currently selected.
    518   for (var i = 0; i < prevSelection.length; ++i) {
    519     prevSelection[i].setSelected(false);
    520   }
    521 
    522   this.onSelectionChanged();
    523 };
    524 
    525 EventsView.prototype.deleteSelected_ = function() {
    526   var sourceIds = [];
    527   for (var i = 0; i < this.currentSelectedSources_.length; ++i) {
    528     var entry = this.currentSelectedSources_[i];
    529     sourceIds.push(entry.getSourceId());
    530   }
    531   g_browser.deleteEventsBySourceId(sourceIds);
    532 };
    533 
    534 EventsView.prototype.selectAll_ = function(event) {
    535   for (var id in this.sourceIdToEntryMap_) {
    536     var entry = this.sourceIdToEntryMap_[id];
    537     if (entry.isMatchedByFilter()) {
    538       entry.setSelected(true);
    539     }
    540   }
    541   event.preventDefault();
    542 };
    543 
    544 EventsView.prototype.unselectAll_ = function() {
    545   var entries = this.currentSelectedSources_.slice(0);
    546   for (var i = 0; i < entries.length; ++i) {
    547     entries[i].setSelected(false);
    548   }
    549 };
    550 
    551 /**
    552  * If |params| includes a query, replaces the current filter and unselects.
    553  * all items.
    554  */
    555 EventsView.prototype.setParameters = function(params) {
    556   if (params.q) {
    557     this.unselectAll_();
    558     this.setFilterText_(params.q);
    559   }
    560 };
    561 
    562 /**
    563  * If already using the specified sort method, flips direction.  Otherwise,
    564  * removes pre-existing sort parameter before adding the new one.
    565  */
    566 EventsView.prototype.toggleSortMethod_ = function(sortMethod) {
    567   // Remove old sort directives, if any.
    568   var filterText = this.parseSortDirectives_(this.getFilterText_());
    569 
    570   // If already using specified sortMethod, sort backwards.
    571   if (!this.doSortBackwards_ &&
    572       EventsView.comparisonFunctionTable_[sortMethod] ==
    573           this.comparisonFunction_)
    574     sortMethod = '-' + sortMethod;
    575 
    576   filterText = 'sort:' + sortMethod + ' ' + filterText;
    577   this.setFilterText_(filterText.trim());
    578 };
    579 
    580 EventsView.prototype.sortById_ = function(event) {
    581   this.toggleSortMethod_('id');
    582 };
    583 
    584 EventsView.prototype.sortBySourceType_ = function(event) {
    585   this.toggleSortMethod_('source');
    586 };
    587 
    588 EventsView.prototype.sortByDescription_ = function(event) {
    589   this.toggleSortMethod_('desc');
    590 };
    591 
    592 EventsView.prototype.modifySelectionArray = function(
    593     sourceEntry, addToSelection) {
    594   // Find the index for |sourceEntry| in the current selection list.
    595   var index = -1;
    596   for (var i = 0; i < this.currentSelectedSources_.length; ++i) {
    597     if (this.currentSelectedSources_[i] == sourceEntry) {
    598       index = i;
    599       break;
    600     }
    601   }
    602 
    603   if (index != -1 && !addToSelection) {
    604     // Remove from the selection.
    605     this.currentSelectedSources_.splice(index, 1);
    606   }
    607 
    608   if (index == -1 && addToSelection) {
    609     this.currentSelectedSources_.push(sourceEntry);
    610   }
    611 };
    612 
    613 EventsView.prototype.invalidateDetailsView_ = function() {
    614   this.detailsView_.setData(this.currentSelectedSources_);
    615 };
    616 
    617 EventsView.prototype.invalidateFilterCounter_ = function() {
    618   if (!this.outstandingRepaintFilterCounter_) {
    619     this.outstandingRepaintFilterCounter_ = true;
    620     window.setTimeout(this.repaintFilterCounter_.bind(this),
    621                       EventsView.REPAINT_FILTER_COUNTER_TIMEOUT_MS);
    622   }
    623 };
    624 
    625 EventsView.prototype.repaintFilterCounter_ = function() {
    626   this.outstandingRepaintFilterCounter_ = false;
    627   this.filterCount_.innerHTML = '';
    628   addTextNode(this.filterCount_,
    629               this.numPostfilter_ + ' of ' + this.numPrefilter_);
    630 };
    631