- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / net_internals / events_view.js
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 })();