• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
28function 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
87inherits(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 */
93EventsView.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.
104EventsView.REPAINT_FILTER_COUNTER_TIMEOUT_MS = 0;
105
106EventsView.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
111EventsView.prototype.show = function(isVisible) {
112  EventsView.superClass_.show.call(this, isVisible);
113  this.splitterView_.show(isVisible);
114};
115
116EventsView.prototype.getFilterText_ = function() {
117  return this.filterInput_.value;
118};
119
120EventsView.prototype.setFilterText_ = function(filterText) {
121  this.filterInput_.value = filterText;
122  this.onFilterTextChanged_();
123};
124
125EventsView.prototype.onFilterTextChanged_ = function() {
126  this.setFilter_(this.getFilterText_());
127};
128
129/**
130 * Updates text in the details view when security stripping is toggled.
131 */
132EventsView.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 */
142EventsView.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
162EventsView.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
171EventsView.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 */
184EventsView.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
200EventsView.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
209EventsView.prototype.comparisonFuncWithReversing_ = function(a, b) {
210  var result = this.comparisonFunction_(a, b);
211  if (this.doSortBackwards_)
212    result *= -1;
213  return result;
214};
215
216EventsView.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
228EventsView.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 */
255EventsView.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 */
276EventsView.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 */
297EventsView.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 */
323EventsView.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 */
346EventsView.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 */
366EventsView.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
375EventsView.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 */
400EventsView.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
432EventsView.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 */
462EventsView.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 */
470EventsView.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 */
485EventsView.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 */
494EventsView.prototype.onSetIsViewingLogFile = function(isViewingLogFile) {
495  // Needed to sort new sourceless entries correctly.
496  this.maxReceivedSourceId_ = 0;
497};
498
499EventsView.prototype.incrementPrefilterCount = function(offset) {
500  this.numPrefilter_ += offset;
501  this.invalidateFilterCounter_();
502};
503
504EventsView.prototype.incrementPostfilterCount = function(offset) {
505  this.numPostfilter_ += offset;
506  this.invalidateFilterCounter_();
507};
508
509EventsView.prototype.onSelectionChanged = function() {
510  this.invalidateDetailsView_();
511};
512
513EventsView.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
525EventsView.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
534EventsView.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
544EventsView.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 */
555EventsView.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 */
566EventsView.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
580EventsView.prototype.sortById_ = function(event) {
581  this.toggleSortMethod_('id');
582};
583
584EventsView.prototype.sortBySourceType_ = function(event) {
585  this.toggleSortMethod_('source');
586};
587
588EventsView.prototype.sortByDescription_ = function(event) {
589  this.toggleSortMethod_('desc');
590};
591
592EventsView.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
613EventsView.prototype.invalidateDetailsView_ = function() {
614  this.detailsView_.setData(this.currentSelectedSources_);
615};
616
617EventsView.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
625EventsView.prototype.repaintFilterCounter_ = function() {
626  this.outstandingRepaintFilterCounter_ = false;
627  this.filterCount_.innerHTML = '';
628  addTextNode(this.filterCount_,
629              this.numPostfilter_ + ' of ' + this.numPrefilter_);
630};
631