• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
26var 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