Add ICU message format support
[chromium-blink-merge.git] / chrome / browser / resources / profiler / profiler.js
blobfd7a65bdf61d3d2255cc5dcf9e65facda25cfaac
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.
5 var g_browserBridge;
6 var g_mainView;
8 // TODO(eroman): The handling of "max" across snapshots is not correct.
9 // For starters the browser needs to be aware to generate new maximums.
10 // Secondly, we need to take into account the "max" of intermediary snapshots,
11 // not just the terminal ones.
13 /**
14  * Main entry point called once the page has loaded.
15  */
16 function onLoad() {
17   g_browserBridge = new BrowserBridge();
18   g_mainView = new MainView();
21 document.addEventListener('DOMContentLoaded', onLoad);
23 /**
24  * This class provides a "bridge" for communicating between the javascript and
25  * the browser. Used as a singleton.
26  */
27 var BrowserBridge = (function() {
28   'use strict';
30   /**
31    * @constructor
32    */
33   function BrowserBridge() {
34   }
36   BrowserBridge.prototype = {
37     //--------------------------------------------------------------------------
38     // Messages sent to the browser
39     //--------------------------------------------------------------------------
41     sendGetData: function() {
42       chrome.send('getData');
43     },
45     //--------------------------------------------------------------------------
46     // Messages received from the browser.
47     //--------------------------------------------------------------------------
49     receivedData: function(data) {
50       // TODO(eroman): The browser should give an indication of which snapshot
51       // this data belongs to. For now we always assume it is for the latest.
52       g_mainView.addDataToSnapshot(data);
53     },
54   };
56   return BrowserBridge;
57 })();
59 /**
60  * This class handles the presentation of our profiler view. Used as a
61  * singleton.
62  */
63 var MainView = (function() {
64   'use strict';
66   // --------------------------------------------------------------------------
67   // Important IDs in the HTML document
68   // --------------------------------------------------------------------------
70   // The search box to filter results.
71   var FILTER_SEARCH_ID = 'filter-search';
73   // The container node to put all the "Group by" dropdowns into.
74   var GROUP_BY_CONTAINER_ID = 'group-by-container';
76   // The container node to put all the "Sort by" dropdowns into.
77   var SORT_BY_CONTAINER_ID = 'sort-by-container';
79   // The DIV to put all the tables into.
80   var RESULTS_DIV_ID = 'results-div';
82   // The container node to put all the column (visibility) checkboxes into.
83   var COLUMN_TOGGLES_CONTAINER_ID = 'column-toggles-container';
85   // The container node to put all the column (merge) checkboxes into.
86   var COLUMN_MERGE_TOGGLES_CONTAINER_ID = 'column-merge-toggles-container';
88   // The anchor which toggles visibility of column checkboxes.
89   var EDIT_COLUMNS_LINK_ID = 'edit-columns-link';
91   // The container node to show/hide when toggling the column checkboxes.
92   var EDIT_COLUMNS_ROW = 'edit-columns-row';
94   // The checkbox which controls whether things like "Worker Threads" and
95   // "PAC threads" will be merged together.
96   var MERGE_SIMILAR_THREADS_CHECKBOX_ID = 'merge-similar-threads-checkbox';
98   var TOGGLE_SNAPSHOTS_LINK_ID = 'snapshots-link';
99   var SNAPSHOTS_ROW = 'snapshots-row';
100   var SNAPSHOT_SELECTION_SUMMARY_ID = 'snapshot-selection-summary';
101   var TAKE_SNAPSHOT_BUTTON_ID = 'take-snapshot-button';
103   var SAVE_SNAPSHOTS_BUTTON_ID = 'save-snapshots-button';
104   var SNAPSHOT_FILE_LOADER_ID = 'snapshot-file-loader';
105   var LOAD_ERROR_ID = 'file-load-error';
107   var DOWNLOAD_ANCHOR_ID = 'download-anchor';
109   // --------------------------------------------------------------------------
110   // Row keys
111   // --------------------------------------------------------------------------
113   // Each row of our data is an array of values rather than a dictionary. This
114   // avoids some overhead from repeating the key string multiple times, and
115   // speeds up the property accesses a bit. The following keys are well-known
116   // indexes into the array for various properties.
117   //
118   // Note that the declaration order will also define the default display order.
120   var BEGIN_KEY = 1;  // Start at 1 rather than 0 to simplify sorting code.
121   var END_KEY = BEGIN_KEY;
123   var KEY_COUNT = END_KEY++;
124   var KEY_RUN_TIME = END_KEY++;
125   var KEY_AVG_RUN_TIME = END_KEY++;
126   var KEY_MAX_RUN_TIME = END_KEY++;
127   var KEY_QUEUE_TIME = END_KEY++;
128   var KEY_AVG_QUEUE_TIME = END_KEY++;
129   var KEY_MAX_QUEUE_TIME = END_KEY++;
130   var KEY_BIRTH_THREAD = END_KEY++;
131   var KEY_DEATH_THREAD = END_KEY++;
132   var KEY_PROCESS_TYPE = END_KEY++;
133   var KEY_PROCESS_ID = END_KEY++;
134   var KEY_FUNCTION_NAME = END_KEY++;
135   var KEY_SOURCE_LOCATION = END_KEY++;
136   var KEY_FILE_NAME = END_KEY++;
137   var KEY_LINE_NUMBER = END_KEY++;
139   var NUM_KEYS = END_KEY - BEGIN_KEY;
141   // --------------------------------------------------------------------------
142   // Aggregators
143   // --------------------------------------------------------------------------
145   // To generalize computing/displaying the aggregate "counts" for each column,
146   // we specify an optional "Aggregator" class to use with each property.
148   // The following are actually "Aggregator factories". They create an
149   // aggregator instance by calling 'create()'. The instance is then fed
150   // each row one at a time via the 'consume()' method. After all rows have
151   // been consumed, the 'getValueAsText()' method will return the aggregated
152   // value.
154   /**
155    * This aggregator counts the number of unique values that were fed to it.
156    */
157   var UniquifyAggregator = (function() {
158     function Aggregator(key) {
159       this.key_ = key;
160       this.valuesSet_ = {};
161     }
163     Aggregator.prototype = {
164       consume: function(e) {
165         this.valuesSet_[e[this.key_]] = true;
166       },
168       getValueAsText: function() {
169         return getDictionaryKeys(this.valuesSet_).length + ' unique';
170       },
171     };
173     return {
174       create: function(key) { return new Aggregator(key); }
175     };
176   })();
178   /**
179    * This aggregator sums a numeric field.
180    */
181   var SumAggregator = (function() {
182     function Aggregator(key) {
183       this.key_ = key;
184       this.sum_ = 0;
185     }
187     Aggregator.prototype = {
188       consume: function(e) {
189         this.sum_ += e[this.key_];
190       },
192       getValue: function() {
193         return this.sum_;
194       },
196       getValueAsText: function() {
197         return formatNumberAsText(this.getValue());
198       },
199     };
201     return {
202       create: function(key) { return new Aggregator(key); }
203     };
204   })();
206   /**
207    * This aggregator computes an average by summing two
208    * numeric fields, and then dividing the totals.
209    */
210   var AvgAggregator = (function() {
211     function Aggregator(numeratorKey, divisorKey) {
212       this.numeratorKey_ = numeratorKey;
213       this.divisorKey_ = divisorKey;
215       this.numeratorSum_ = 0;
216       this.divisorSum_ = 0;
217     }
219     Aggregator.prototype = {
220       consume: function(e) {
221         this.numeratorSum_ += e[this.numeratorKey_];
222         this.divisorSum_ += e[this.divisorKey_];
223       },
225       getValue: function() {
226         return this.numeratorSum_ / this.divisorSum_;
227       },
229       getValueAsText: function() {
230         return formatNumberAsText(this.getValue());
231       },
232     };
234     return {
235       create: function(numeratorKey, divisorKey) {
236         return {
237           create: function(key) {
238             return new Aggregator(numeratorKey, divisorKey);
239           },
240         };
241       }
242     };
243   })();
245   /**
246    * This aggregator finds the maximum for a numeric field.
247    */
248   var MaxAggregator = (function() {
249     function Aggregator(key) {
250       this.key_ = key;
251       this.max_ = -Infinity;
252     }
254     Aggregator.prototype = {
255       consume: function(e) {
256         this.max_ = Math.max(this.max_, e[this.key_]);
257       },
259       getValue: function() {
260         return this.max_;
261       },
263       getValueAsText: function() {
264         return formatNumberAsText(this.getValue());
265       },
266     };
268     return {
269       create: function(key) { return new Aggregator(key); }
270     };
271   })();
273   // --------------------------------------------------------------------------
274   // Key properties
275   // --------------------------------------------------------------------------
277   // Custom comparator for thread names (sorts main thread and IO thread
278   // higher than would happen lexicographically.)
279   var threadNameComparator =
280       createLexicographicComparatorWithExceptions([
281           'CrBrowserMain',
282           'Chrome_IOThread',
283           'Chrome_FileThread',
284           'Chrome_HistoryThread',
285           'Chrome_DBThread',
286           'Still_Alive',
287       ]);
289   function diffFuncForCount(a, b) {
290     return b - a;
291   }
293   function diffFuncForMax(a, b) {
294     return b;
295   }
297   /**
298    * Enumerates information about various keys. Such as whether their data is
299    * expected to be numeric or is a string, a descriptive name (title) for the
300    * property, and what function should be used to aggregate the property when
301    * displayed in a column.
302    *
303    * --------------------------------------
304    * The following properties are required:
305    * --------------------------------------
306    *
307    *   [name]: This is displayed as the column's label.
308    *   [aggregator]: Aggregator factory that is used to compute an aggregate
309    *                 value for this column.
310    *
311    * --------------------------------------
312    * The following properties are optional:
313    * --------------------------------------
314    *
315    *   [inputJsonKey]: The corresponding key for this property in the original
316    *                   JSON dictionary received from the browser. If this is
317    *                   present, values for this key will be automatically
318    *                   populated during import.
319    *   [comparator]: A comparator function for sorting this column.
320    *   [textPrinter]: A function that transforms values into the user-displayed
321    *                  text shown in the UI. If unspecified, will default to the
322    *                  "toString()" function.
323    *   [cellAlignment]: The horizonal alignment to use for columns of this
324    *                    property (for instance 'right'). If unspecified will
325    *                    default to left alignment.
326    *   [sortDescending]: When first clicking on this column, we will default to
327    *                     sorting by |comparator| in ascending order. If this
328    *                     property is true, we will reverse that to descending.
329    *   [diff]: Function to call to compute a "difference" value between
330    *           parameters (a, b). This is used when calculating the difference
331    *           between two snapshots. Diffing numeric quantities generally
332    *           involves subtracting, but some fields like max may need to do
333    *           something different.
334    */
335   var KEY_PROPERTIES = [];
337   KEY_PROPERTIES[KEY_PROCESS_ID] = {
338     name: 'PID',
339     cellAlignment: 'right',
340     aggregator: UniquifyAggregator,
341   };
343   KEY_PROPERTIES[KEY_PROCESS_TYPE] = {
344     name: 'Process type',
345     aggregator: UniquifyAggregator,
346   };
348   KEY_PROPERTIES[KEY_BIRTH_THREAD] = {
349     name: 'Birth thread',
350     inputJsonKey: 'birth_thread',
351     aggregator: UniquifyAggregator,
352     comparator: threadNameComparator,
353   };
355   KEY_PROPERTIES[KEY_DEATH_THREAD] = {
356     name: 'Exec thread',
357     inputJsonKey: 'death_thread',
358     aggregator: UniquifyAggregator,
359     comparator: threadNameComparator,
360   };
362   KEY_PROPERTIES[KEY_FUNCTION_NAME] = {
363     name: 'Function name',
364     inputJsonKey: 'birth_location.function_name',
365     aggregator: UniquifyAggregator,
366   };
368   KEY_PROPERTIES[KEY_FILE_NAME] = {
369     name: 'File name',
370     inputJsonKey: 'birth_location.file_name',
371     aggregator: UniquifyAggregator,
372   };
374   KEY_PROPERTIES[KEY_LINE_NUMBER] = {
375     name: 'Line number',
376     cellAlignment: 'right',
377     inputJsonKey: 'birth_location.line_number',
378     aggregator: UniquifyAggregator,
379   };
381   KEY_PROPERTIES[KEY_COUNT] = {
382     name: 'Count',
383     cellAlignment: 'right',
384     sortDescending: true,
385     textPrinter: formatNumberAsText,
386     inputJsonKey: 'death_data.count',
387     aggregator: SumAggregator,
388     diff: diffFuncForCount,
389   };
391   KEY_PROPERTIES[KEY_QUEUE_TIME] = {
392     name: 'Total queue time',
393     cellAlignment: 'right',
394     sortDescending: true,
395     textPrinter: formatNumberAsText,
396     inputJsonKey: 'death_data.queue_ms',
397     aggregator: SumAggregator,
398     diff: diffFuncForCount,
399   };
401   KEY_PROPERTIES[KEY_MAX_QUEUE_TIME] = {
402     name: 'Max queue time',
403     cellAlignment: 'right',
404     sortDescending: true,
405     textPrinter: formatNumberAsText,
406     inputJsonKey: 'death_data.queue_ms_max',
407     aggregator: MaxAggregator,
408     diff: diffFuncForMax,
409   };
411   KEY_PROPERTIES[KEY_RUN_TIME] = {
412     name: 'Total run time',
413     cellAlignment: 'right',
414     sortDescending: true,
415     textPrinter: formatNumberAsText,
416     inputJsonKey: 'death_data.run_ms',
417     aggregator: SumAggregator,
418     diff: diffFuncForCount,
419   };
421   KEY_PROPERTIES[KEY_AVG_RUN_TIME] = {
422     name: 'Avg run time',
423     cellAlignment: 'right',
424     sortDescending: true,
425     textPrinter: formatNumberAsText,
426     aggregator: AvgAggregator.create(KEY_RUN_TIME, KEY_COUNT),
427   };
429   KEY_PROPERTIES[KEY_MAX_RUN_TIME] = {
430     name: 'Max run time',
431     cellAlignment: 'right',
432     sortDescending: true,
433     textPrinter: formatNumberAsText,
434     inputJsonKey: 'death_data.run_ms_max',
435     aggregator: MaxAggregator,
436     diff: diffFuncForMax,
437   };
439   KEY_PROPERTIES[KEY_AVG_QUEUE_TIME] = {
440     name: 'Avg queue time',
441     cellAlignment: 'right',
442     sortDescending: true,
443     textPrinter: formatNumberAsText,
444     aggregator: AvgAggregator.create(KEY_QUEUE_TIME, KEY_COUNT),
445   };
447   KEY_PROPERTIES[KEY_SOURCE_LOCATION] = {
448     name: 'Source location',
449     type: 'string',
450     aggregator: UniquifyAggregator,
451   };
453   /**
454    * Returns the string name for |key|.
455    */
456   function getNameForKey(key) {
457     var props = KEY_PROPERTIES[key];
458     if (props == undefined)
459       throw 'Did not define properties for key: ' + key;
460     return props.name;
461   }
463   /**
464    * Ordered list of all keys. This is the order we generally want
465    * to display the properties in. Default to declaration order.
466    */
467   var ALL_KEYS = [];
468   for (var k = BEGIN_KEY; k < END_KEY; ++k)
469     ALL_KEYS.push(k);
471   // --------------------------------------------------------------------------
472   // Default settings
473   // --------------------------------------------------------------------------
475   /**
476    * List of keys for those properties which we want to initially omit
477    * from the table. (They can be re-enabled by clicking [Edit columns]).
478    */
479   var INITIALLY_HIDDEN_KEYS = [
480     KEY_FILE_NAME,
481     KEY_LINE_NUMBER,
482     KEY_QUEUE_TIME,
483   ];
485   /**
486    * The ordered list of grouping choices to expose in the "Group by"
487    * dropdowns. We don't include the numeric properties, since they
488    * leads to awkward bucketing.
489    */
490   var GROUPING_DROPDOWN_CHOICES = [
491     KEY_PROCESS_TYPE,
492     KEY_PROCESS_ID,
493     KEY_BIRTH_THREAD,
494     KEY_DEATH_THREAD,
495     KEY_FUNCTION_NAME,
496     KEY_SOURCE_LOCATION,
497     KEY_FILE_NAME,
498     KEY_LINE_NUMBER,
499   ];
501   /**
502    * The ordered list of sorting choices to expose in the "Sort by"
503    * dropdowns.
504    */
505   var SORT_DROPDOWN_CHOICES = ALL_KEYS;
507   /**
508    * The ordered list of all columns that can be displayed in the tables (not
509    * including whatever has been hidden via [Edit Columns]).
510    */
511   var ALL_TABLE_COLUMNS = ALL_KEYS;
513   /**
514    * The initial keys to sort by when loading the page (can be changed later).
515    */
516   var INITIAL_SORT_KEYS = [-KEY_COUNT];
518   /**
519    * The default sort keys to use when nothing has been specified.
520    */
521   var DEFAULT_SORT_KEYS = [-KEY_COUNT];
523   /**
524    * The initial keys to group by when loading the page (can be changed later).
525    */
526   var INITIAL_GROUP_KEYS = [];
528   /**
529    * The columns to give the option to merge on.
530    */
531   var MERGEABLE_KEYS = [
532     KEY_PROCESS_ID,
533     KEY_PROCESS_TYPE,
534     KEY_BIRTH_THREAD,
535     KEY_DEATH_THREAD,
536   ];
538   /**
539    * The columns to merge by default.
540    */
541   var INITIALLY_MERGED_KEYS = [];
543   /**
544    * The full set of columns which define the "identity" for a row. A row is
545    * considered equivalent to another row if it matches on all of these
546    * fields. This list is used when merging the data, to determine which rows
547    * should be merged together. The remaining columns not listed in
548    * IDENTITY_KEYS will be aggregated.
549    */
550   var IDENTITY_KEYS = [
551     KEY_BIRTH_THREAD,
552     KEY_DEATH_THREAD,
553     KEY_PROCESS_TYPE,
554     KEY_PROCESS_ID,
555     KEY_FUNCTION_NAME,
556     KEY_SOURCE_LOCATION,
557     KEY_FILE_NAME,
558     KEY_LINE_NUMBER,
559   ];
561   /**
562    * The time (in milliseconds) to wait after receiving new data before
563    * re-drawing it to the screen. The reason we wait a bit is to avoid
564    * repainting repeatedly during the loading phase (which can slow things
565    * down). Note that this only slows down the addition of new data. It does
566    * not impact the  latency of user-initiated operations like sorting or
567    * merging.
568    */
569   var PROCESS_DATA_DELAY_MS = 500;
571   /**
572    * The initial number of rows to display (the rest are hidden) when no
573    * grouping is selected. We use a higher limit than when grouping is used
574    * since there is a lot of vertical real estate.
575    */
576   var INITIAL_UNGROUPED_ROW_LIMIT = 30;
578   /**
579    * The initial number of rows to display (rest are hidden) for each group.
580    */
581   var INITIAL_GROUP_ROW_LIMIT = 10;
583   /**
584    * The number of extra rows to show/hide when clicking the "Show more" or
585    * "Show less" buttons.
586    */
587   var LIMIT_INCREMENT = 10;
589   // --------------------------------------------------------------------------
590   // General utility functions
591   // --------------------------------------------------------------------------
593   /**
594    * Returns a list of all the keys in |dict|.
595    */
596   function getDictionaryKeys(dict) {
597     var keys = [];
598     for (var key in dict) {
599       keys.push(key);
600     }
601     return keys;
602   }
604   /**
605    * Formats the number |x| as a decimal integer. Strips off any decimal parts,
606    * and comma separates the number every 3 characters.
607    */
608   function formatNumberAsText(x) {
609     var orig = x.toFixed(0);
611     var parts = [];
612     for (var end = orig.length; end > 0; ) {
613       var chunk = Math.min(end, 3);
614       parts.push(orig.substr(end - chunk, chunk));
615       end -= chunk;
616     }
617     return parts.reverse().join(',');
618   }
620   /**
621    * Simple comparator function which works for both strings and numbers.
622    */
623   function simpleCompare(a, b) {
624     if (a == b)
625       return 0;
626     if (a < b)
627       return -1;
628     return 1;
629   }
631   /**
632    * Returns a comparator function that compares values lexicographically,
633    * but special-cases the values in |orderedList| to have a higher
634    * rank.
635    */
636   function createLexicographicComparatorWithExceptions(orderedList) {
637     var valueToRankMap = {};
638     for (var i = 0; i < orderedList.length; ++i)
639       valueToRankMap[orderedList[i]] = i;
641     function getCustomRank(x) {
642       var rank = valueToRankMap[x];
643       if (rank == undefined)
644         rank = Infinity;  // Unmatched.
645       return rank;
646     }
648     return function(a, b) {
649       var aRank = getCustomRank(a);
650       var bRank = getCustomRank(b);
652       // Not matched by any of our exceptions.
653       if (aRank == bRank)
654         return simpleCompare(a, b);
656       if (aRank < bRank)
657         return -1;
658       return 1;
659     };
660   }
662   /**
663    * Returns dict[key]. Note that if |key| contains periods (.), they will be
664    * interpreted as meaning a sub-property.
665    */
666   function getPropertyByPath(dict, key) {
667     var cur = dict;
668     var parts = key.split('.');
669     for (var i = 0; i < parts.length; ++i) {
670       if (cur == undefined)
671         return undefined;
672       cur = cur[parts[i]];
673     }
674     return cur;
675   }
677   /**
678    * Creates and appends a DOM node of type |tagName| to |parent|. Optionally,
679    * sets the new node's text to |opt_text|. Returns the newly created node.
680    */
681   function addNode(parent, tagName, opt_text) {
682     var n = parent.ownerDocument.createElement(tagName);
683     parent.appendChild(n);
684     if (opt_text != undefined) {
685       addText(n, opt_text);
686     }
687     return n;
688   }
690   /**
691    * Adds |text| to |parent|.
692    */
693   function addText(parent, text) {
694     var textNode = parent.ownerDocument.createTextNode(text);
695     parent.appendChild(textNode);
696     return textNode;
697   }
699   /**
700    * Deletes all the strings in |array| which appear in |valuesToDelete|.
701    */
702   function deleteValuesFromArray(array, valuesToDelete) {
703     var valueSet = arrayToSet(valuesToDelete);
704     for (var i = 0; i < array.length; ) {
705       if (valueSet[array[i]]) {
706         array.splice(i, 1);
707       } else {
708         i++;
709       }
710     }
711   }
713   /**
714    * Deletes all the repeated ocurrences of strings in |array|.
715    */
716   function deleteDuplicateStringsFromArray(array) {
717     // Build up set of each entry in array.
718     var seenSoFar = {};
720     for (var i = 0; i < array.length; ) {
721       var value = array[i];
722       if (seenSoFar[value]) {
723         array.splice(i, 1);
724       } else {
725         seenSoFar[value] = true;
726         i++;
727       }
728     }
729   }
731   /**
732    * Builds a map out of the array |list|.
733    */
734   function arrayToSet(list) {
735     var set = {};
736     for (var i = 0; i < list.length; ++i)
737       set[list[i]] = true;
738     return set;
739   }
741   function trimWhitespace(text) {
742     var m = /^\s*(.*)\s*$/.exec(text);
743     return m[1];
744   }
746   /**
747    * Selects the option in |select| which has a value of |value|.
748    */
749   function setSelectedOptionByValue(select, value) {
750     for (var i = 0; i < select.options.length; ++i) {
751       if (select.options[i].value == value) {
752         select.options[i].selected = true;
753         return true;
754       }
755     }
756     return false;
757   }
759   /**
760    * Adds a checkbox to |parent|. The checkbox will have a label on its right
761    * with text |label|. Returns the checkbox input node.
762    */
763   function addLabeledCheckbox(parent, label) {
764     var labelNode = addNode(parent, 'label');
765     var checkbox = addNode(labelNode, 'input');
766     checkbox.type = 'checkbox';
767     addText(labelNode, label);
768     return checkbox;
769   }
771   /**
772    * Return the last component in a path which is separated by either forward
773    * slashes or backslashes.
774    */
775   function getFilenameFromPath(path) {
776     var lastSlash = Math.max(path.lastIndexOf('/'),
777                              path.lastIndexOf('\\'));
778     if (lastSlash == -1)
779       return path;
781     return path.substr(lastSlash + 1);
782   }
784   /**
785    * Returns the current time in milliseconds since unix epoch.
786    */
787   function getTimeMillis() {
788     return (new Date()).getTime();
789   }
791   /**
792    * Toggle a node between hidden/invisible.
793    */
794   function toggleNodeDisplay(n) {
795     if (n.style.display == '') {
796       n.style.display = 'none';
797     } else {
798       n.style.display = '';
799     }
800   }
802   /**
803    * Set the visibility state of a node.
804    */
805   function setNodeDisplay(n, visible) {
806     if (visible) {
807       n.style.display = '';
808     } else {
809       n.style.display = 'none';
810     }
811   }
813   // --------------------------------------------------------------------------
814   // Functions that augment, bucket, and compute aggregates for the input data.
815   // --------------------------------------------------------------------------
817   /**
818    * Adds new derived properties to row. Mutates the provided dictionary |e|.
819    */
820   function augmentDataRow(e) {
821     computeDataRowAverages(e);
822     e[KEY_SOURCE_LOCATION] = e[KEY_FILE_NAME] + ' [' + e[KEY_LINE_NUMBER] + ']';
823   }
825   function computeDataRowAverages(e) {
826     e[KEY_AVG_QUEUE_TIME] = e[KEY_QUEUE_TIME] / e[KEY_COUNT];
827     e[KEY_AVG_RUN_TIME] = e[KEY_RUN_TIME] / e[KEY_COUNT];
828   }
830   /**
831    * Creates and initializes an aggregator object for each key in |columns|.
832    * Returns an array whose keys are values from |columns|, and whose
833    * values are Aggregator instances.
834    */
835   function initializeAggregates(columns) {
836     var aggregates = [];
838     for (var i = 0; i < columns.length; ++i) {
839       var key = columns[i];
840       var aggregatorFactory = KEY_PROPERTIES[key].aggregator;
841       aggregates[key] = aggregatorFactory.create(key);
842     }
844     return aggregates;
845   }
847   function consumeAggregates(aggregates, row) {
848     for (var key in aggregates)
849       aggregates[key].consume(row);
850   }
852   function bucketIdenticalRows(rows, identityKeys, propertyGetterFunc) {
853     var identicalRows = {};
854     for (var i = 0; i < rows.length; ++i) {
855       var r = rows[i];
857       var rowIdentity = [];
858       for (var j = 0; j < identityKeys.length; ++j)
859         rowIdentity.push(propertyGetterFunc(r, identityKeys[j]));
860       rowIdentity = rowIdentity.join('\n');
862       var l = identicalRows[rowIdentity];
863       if (!l) {
864         l = [];
865         identicalRows[rowIdentity] = l;
866       }
867       l.push(r);
868     }
869     return identicalRows;
870   }
872   /**
873    * Merges the rows in |origRows|, by collapsing the columns listed in
874    * |mergeKeys|. Returns an array with the merged rows (in no particular
875    * order).
876    *
877    * If |mergeSimilarThreads| is true, then threads with a similar name will be
878    * considered equivalent. For instance, "WorkerThread-1" and "WorkerThread-2"
879    * will be remapped to "WorkerThread-*".
880    *
881    * If |outputAsDictionary| is false then the merged rows will be returned as a
882    * flat list. Otherwise the result will be a dictionary, where each row
883    * has a unique key.
884    */
885   function mergeRows(origRows, mergeKeys, mergeSimilarThreads,
886                      outputAsDictionary) {
887     // Define a translation function for each property. Normally we copy over
888     // properties as-is, but if we have been asked to "merge similar threads" we
889     // we will remap the thread names that end in a numeric suffix.
890     var propertyGetterFunc;
892     if (mergeSimilarThreads) {
893       propertyGetterFunc = function(row, key) {
894         var value = row[key];
895         // If the property is a thread name, try to remap it.
896         if (key == KEY_BIRTH_THREAD || key == KEY_DEATH_THREAD) {
897           var m = /^(.*[^\d])(\d+)$/.exec(value);
898           if (m)
899             value = m[1] + '*';
900         }
901         return value;
902       };
903     } else {
904       propertyGetterFunc = function(row, key) { return row[key]; };
905     }
907     // Determine which sets of properties a row needs to match on to be
908     // considered identical to another row.
909     var identityKeys = IDENTITY_KEYS.slice(0);
910     deleteValuesFromArray(identityKeys, mergeKeys);
912     // Set |aggregateKeys| to everything else, since we will be aggregating
913     // their value as part of the merge.
914     var aggregateKeys = ALL_KEYS.slice(0);
915     deleteValuesFromArray(aggregateKeys, IDENTITY_KEYS);
916     deleteValuesFromArray(aggregateKeys, mergeKeys);
918     // Group all the identical rows together, bucketed into |identicalRows|.
919     var identicalRows =
920         bucketIdenticalRows(origRows, identityKeys, propertyGetterFunc);
922     var mergedRows = outputAsDictionary ? {} : [];
924     // Merge the rows and save the results to |mergedRows|.
925     for (var k in identicalRows) {
926       // We need to smash the list |l| down to a single row...
927       var l = identicalRows[k];
929       var newRow = [];
931       if (outputAsDictionary) {
932         mergedRows[k] = newRow;
933       } else {
934         mergedRows.push(newRow);
935       }
937       // Copy over all the identity columns to the new row (since they
938       // were the same for each row matched).
939       for (var i = 0; i < identityKeys.length; ++i)
940         newRow[identityKeys[i]] = propertyGetterFunc(l[0], identityKeys[i]);
942       // Compute aggregates for the other columns.
943       var aggregates = initializeAggregates(aggregateKeys);
945       // Feed the rows to the aggregators.
946       for (var i = 0; i < l.length; ++i)
947         consumeAggregates(aggregates, l[i]);
949       // Suck out the data generated by the aggregators.
950       for (var aggregateKey in aggregates)
951         newRow[aggregateKey] = aggregates[aggregateKey].getValue();
952     }
954     return mergedRows;
955   }
957   /**
958    * Takes two dictionaries data1 and data2, and returns a new flat list which
959    * represents the difference between them. The exact meaning of "difference"
960    * is column specific, but for most numeric fields (like the count, or total
961    * time), it is found by subtracting.
962    *
963    * Rows in data1 and data2 are expected to use the same scheme for the keys.
964    * In other words, data1[k] is considered the analagous row to data2[k].
965    */
966   function subtractSnapshots(data1, data2, columnsToExclude) {
967     // These columns are computed from the other columns. We won't bother
968     // diffing/aggregating these, but rather will derive them again from the
969     // final row.
970     var COMPUTED_AGGREGATE_KEYS = [KEY_AVG_QUEUE_TIME, KEY_AVG_RUN_TIME];
972     // These are the keys which determine row equality. Since we are not doing
973     // any merging yet at this point, it is simply the list of all identity
974     // columns.
975     var identityKeys = IDENTITY_KEYS.slice(0);
976     deleteValuesFromArray(identityKeys, columnsToExclude);
978     // The columns to compute via aggregation is everything else.
979     var aggregateKeys = ALL_KEYS.slice(0);
980     deleteValuesFromArray(aggregateKeys, IDENTITY_KEYS);
981     deleteValuesFromArray(aggregateKeys, COMPUTED_AGGREGATE_KEYS);
982     deleteValuesFromArray(aggregateKeys, columnsToExclude);
984     var diffedRows = [];
986     for (var rowId in data2) {
987       var row1 = data1[rowId];
988       var row2 = data2[rowId];
990       var newRow = [];
992       // Copy over all the identity columns to the new row (since they
993       // were the same for each row matched).
994       for (var i = 0; i < identityKeys.length; ++i)
995         newRow[identityKeys[i]] = row2[identityKeys[i]];
997       // Diff the two rows.
998       if (row1) {
999         for (var i = 0; i < aggregateKeys.length; ++i) {
1000           var aggregateKey = aggregateKeys[i];
1001           var a = row1[aggregateKey];
1002           var b = row2[aggregateKey];
1004           var diffFunc = KEY_PROPERTIES[aggregateKey].diff;
1005           newRow[aggregateKey] = diffFunc(a, b);
1006         }
1007       } else {
1008         // If the the row doesn't appear in snapshot1, then there is nothing to
1009         // diff, so just copy row2 as is.
1010         for (var i = 0; i < aggregateKeys.length; ++i) {
1011           var aggregateKey = aggregateKeys[i];
1012           newRow[aggregateKey] = row2[aggregateKey];
1013         }
1014       }
1016       if (newRow[KEY_COUNT] == 0) {
1017         // If a row's count has gone to zero, it means there were no new
1018         // occurrences of it in the second snapshot, so remove it.
1019         continue;
1020       }
1022       // Since we excluded the averages during the diffing phase, re-compute
1023       // them using the diffed totals.
1024       computeDataRowAverages(newRow);
1025       diffedRows.push(newRow);
1026     }
1028     return diffedRows;
1029   }
1031   // --------------------------------------------------------------------------
1032   // HTML drawing code
1033   // --------------------------------------------------------------------------
1035   function getTextValueForProperty(key, value) {
1036     if (value == undefined) {
1037       // A value may be undefined as a result of having merging rows. We
1038       // won't actually draw it, but this might be called by the filter.
1039       return '';
1040     }
1042     var textPrinter = KEY_PROPERTIES[key].textPrinter;
1043     if (textPrinter)
1044       return textPrinter(value);
1045     return value.toString();
1046   }
1048   /**
1049    * Renders the property value |value| into cell |td|. The name of this
1050    * property is |key|.
1051    */
1052   function drawValueToCell(td, key, value) {
1053     // Get a text representation of the value.
1054     var text = getTextValueForProperty(key, value);
1056     // Apply the desired cell alignment.
1057     var cellAlignment = KEY_PROPERTIES[key].cellAlignment;
1058     if (cellAlignment)
1059       td.align = cellAlignment;
1061     if (key == KEY_SOURCE_LOCATION) {
1062       // Linkify the source column so it jumps to the source code. This doesn't
1063       // take into account the particular code this build was compiled from, or
1064       // local edits to source. It should however work correctly for top of tree
1065       // builds.
1066       var m = /^(.*) \[(\d+)\]$/.exec(text);
1067       if (m) {
1068         var filepath = m[1];
1069         var filename = getFilenameFromPath(filepath);
1070         var linenumber = m[2];
1072         var link = addNode(td, 'a', filename + ' [' + linenumber + ']');
1074         link.href = 'https://code.google.com/p/chromium/codesearch#search/&q=' +
1075                     encodeURIComponent(filename) + ':' + linenumber +
1076                     '&sq=package:chromium&type=cs';
1077         link.target = '_blank';
1078         return;
1079       }
1080     }
1082     // String values can get pretty long. If the string contains no spaces, then
1083     // CSS fails to wrap it, and it overflows the cell causing the table to get
1084     // really big. We solve this using a hack: insert a <wbr> element after
1085     // every single character. This will allow the rendering engine to wrap the
1086     // value, and hence avoid it overflowing!
1087     var kMinLengthBeforeWrap = 20;
1089     addText(td, text.substr(0, kMinLengthBeforeWrap));
1090     for (var i = kMinLengthBeforeWrap; i < text.length; ++i) {
1091       addNode(td, 'wbr');
1092       addText(td, text.substr(i, 1));
1093     }
1094   }
1096   // --------------------------------------------------------------------------
1097   // Helper code for handling the sort and grouping dropdowns.
1098   // --------------------------------------------------------------------------
1100   function addOptionsForGroupingSelect(select) {
1101     // Add "no group" choice.
1102     addNode(select, 'option', '---').value = '';
1104     for (var i = 0; i < GROUPING_DROPDOWN_CHOICES.length; ++i) {
1105       var key = GROUPING_DROPDOWN_CHOICES[i];
1106       var option = addNode(select, 'option', getNameForKey(key));
1107       option.value = key;
1108     }
1109   }
1111   function addOptionsForSortingSelect(select) {
1112     // Add "no sort" choice.
1113     addNode(select, 'option', '---').value = '';
1115     // Add a divider.
1116     addNode(select, 'optgroup').label = '';
1118     for (var i = 0; i < SORT_DROPDOWN_CHOICES.length; ++i) {
1119       var key = SORT_DROPDOWN_CHOICES[i];
1120       addNode(select, 'option', getNameForKey(key)).value = key;
1121     }
1123     // Add a divider.
1124     addNode(select, 'optgroup').label = '';
1126     // Add the same options, but for descending.
1127     for (var i = 0; i < SORT_DROPDOWN_CHOICES.length; ++i) {
1128       var key = SORT_DROPDOWN_CHOICES[i];
1129       var n = addNode(select, 'option', getNameForKey(key) + ' (DESC)');
1130       n.value = reverseSortKey(key);
1131     }
1132   }
1134   /**
1135    * Helper function used to update the sorting and grouping lists after a
1136    * dropdown changes.
1137    */
1138   function updateKeyListFromDropdown(list, i, select) {
1139     // Update the list.
1140     if (i < list.length) {
1141       list[i] = select.value;
1142     } else {
1143       list.push(select.value);
1144     }
1146     // Normalize the list, so setting 'none' as primary zeros out everything
1147     // else.
1148     for (var i = 0; i < list.length; ++i) {
1149       if (list[i] == '') {
1150         list.splice(i, list.length - i);
1151         break;
1152       }
1153     }
1154   }
1156   /**
1157    * Comparator for property |key|, having values |value1| and |value2|.
1158    * If the key has defined a custom comparator use it. Otherwise use a
1159    * default "less than" comparison.
1160    */
1161   function compareValuesForKey(key, value1, value2) {
1162     var comparator = KEY_PROPERTIES[key].comparator;
1163     if (comparator)
1164       return comparator(value1, value2);
1165     return simpleCompare(value1, value2);
1166   }
1168   function reverseSortKey(key) {
1169     return -key;
1170   }
1172   function sortKeyIsReversed(key) {
1173     return key < 0;
1174   }
1176   function sortKeysMatch(key1, key2) {
1177     return Math.abs(key1) == Math.abs(key2);
1178   }
1180   function getKeysForCheckedBoxes(checkboxes) {
1181     var keys = [];
1182     for (var k in checkboxes) {
1183       if (checkboxes[k].checked)
1184         keys.push(k);
1185     }
1186     return keys;
1187   }
1189   // --------------------------------------------------------------------------
1191   /**
1192    * @constructor
1193    */
1194   function MainView() {
1195     // Make sure we have a definition for each key.
1196     for (var k = BEGIN_KEY; k < END_KEY; ++k) {
1197       if (!KEY_PROPERTIES[k])
1198         throw 'KEY_PROPERTIES[] not defined for key: ' + k;
1199     }
1201     this.init_();
1202   }
1204   MainView.prototype = {
1205     addDataToSnapshot: function(data) {
1206       // TODO(eroman): We need to know which snapshot this data belongs to!
1207       // For now we assume it is the most recent snapshot.
1208       var snapshotIndex = this.snapshots_.length - 1;
1210       var snapshot = this.snapshots_[snapshotIndex];
1212       var pid = data.process_id;
1213       var ptype = data.process_type;
1215       // Save the browser's representation of the data
1216       snapshot.origData.push(data);
1218       // Augment each data row with the process information.
1219       var rows = data.list;
1220       for (var i = 0; i < rows.length; ++i) {
1221         // Transform the data from a dictionary to an array. This internal
1222         // representation is more compact and faster to access.
1223         var origRow = rows[i];
1224         var newRow = [];
1226         newRow[KEY_PROCESS_ID] = pid;
1227         newRow[KEY_PROCESS_TYPE] = ptype;
1229         // Copy over the known properties which have a 1:1 mapping with JSON.
1230         for (var k = BEGIN_KEY; k < END_KEY; ++k) {
1231           var inputJsonKey = KEY_PROPERTIES[k].inputJsonKey;
1232           if (inputJsonKey != undefined) {
1233             newRow[k] = getPropertyByPath(origRow, inputJsonKey);
1234           }
1235         }
1237         if (newRow[KEY_COUNT] == 0) {
1238           // When resetting the data, it is possible for the backend to give us
1239           // counts of "0". There is no point adding these rows (in fact they
1240           // will cause us to do divide by zeros when calculating averages and
1241           // stuff), so we skip past them.
1242           continue;
1243         }
1245         // Add our computed properties.
1246         augmentDataRow(newRow);
1248         snapshot.flatData.push(newRow);
1249       }
1251       if (!arrayToSet(this.getSelectedSnapshotIndexes_())[snapshotIndex]) {
1252         // Optimization: If this snapshot is not a data dependency for the
1253         // current display, then don't bother updating anything.
1254         return;
1255       }
1257       // We may end up calling addDataToSnapshot_() repeatedly (once for each
1258       // process). To avoid this from slowing us down we do bulk updates on a
1259       // timer.
1260       this.updateMergedDataSoon_();
1261     },
1263     updateMergedDataSoon_: function() {
1264       if (this.updateMergedDataPending_) {
1265         // If a delayed task has already been posted to re-merge the data,
1266         // then we don't need to do anything extra.
1267         return;
1268       }
1270       // Otherwise schedule updateMergedData_() to be called later. We want it
1271       // to be called no more than once every PROCESS_DATA_DELAY_MS
1272       // milliseconds.
1274       if (this.lastUpdateMergedDataTime_ == undefined)
1275         this.lastUpdateMergedDataTime_ = 0;
1277       var timeSinceLastMerge = getTimeMillis() - this.lastUpdateMergedDataTime_;
1278       var timeToWait = Math.max(0, PROCESS_DATA_DELAY_MS - timeSinceLastMerge);
1280       var functionToRun = function() {
1281         // Do the actual update.
1282         this.updateMergedData_();
1283         // Keep track of when we last ran.
1284         this.lastUpdateMergedDataTime_ = getTimeMillis();
1285         this.updateMergedDataPending_ = false;
1286       }.bind(this);
1288       this.updateMergedDataPending_ = true;
1289       window.setTimeout(functionToRun, timeToWait);
1290     },
1292     /**
1293      * Returns a list of the currently selected snapshots. This list is
1294      * guaranteed to be of length 1 or 2.
1295      */
1296     getSelectedSnapshotIndexes_: function() {
1297       var indexes = this.getSelectedSnapshotBoxes_();
1298       for (var i = 0; i < indexes.length; ++i)
1299         indexes[i] = indexes[i].__index;
1300       return indexes;
1301     },
1303     /**
1304      * Same as getSelectedSnapshotIndexes_(), only it returns the actual
1305      * checkbox input DOM nodes rather than the snapshot ID.
1306      */
1307     getSelectedSnapshotBoxes_: function() {
1308       // Figure out which snaphots to use for our data.
1309       var boxes = [];
1310       for (var i = 0; i < this.snapshots_.length; ++i) {
1311         var box = this.getSnapshotCheckbox_(i);
1312         if (box.checked)
1313           boxes.push(box);
1314       }
1315       return boxes;
1316     },
1318     /**
1319      * Re-draw the description that explains which snapshots are currently
1320      * selected (if two snapshots were selected we explain that the *difference*
1321      * between them is being displayed).
1322      */
1323     updateSnapshotSelectionSummaryDiv_: function() {
1324       var summaryDiv = $(SNAPSHOT_SELECTION_SUMMARY_ID);
1326       var selectedSnapshots = this.getSelectedSnapshotIndexes_();
1327       if (selectedSnapshots.length == 0) {
1328         // This can occur during an attempt to load a file or following file
1329         // load failure.  We just ignore it and move on.
1330       } else if (selectedSnapshots.length == 1) {
1331         // If only one snapshot is chosen then we will display that snapshot's
1332         // data in its entirety.
1333         this.flatData_ = this.snapshots_[selectedSnapshots[0]].flatData;
1335         // Don't bother displaying any text when just 1 snapshot is selected,
1336         // since it is obvious what this should do.
1337         summaryDiv.innerText = '';
1338       } else if (selectedSnapshots.length == 2) {
1339         // Otherwise if two snapshots were chosen, show the difference between
1340         // them.
1341         var snapshot1 = this.snapshots_[selectedSnapshots[0]];
1342         var snapshot2 = this.snapshots_[selectedSnapshots[1]];
1344         var timeDeltaInSeconds =
1345             ((snapshot2.time - snapshot1.time) / 1000).toFixed(0);
1347         // Explain that what is being shown is the difference between two
1348         // snapshots.
1349         summaryDiv.innerText =
1350             'Showing the difference between snapshots #' +
1351             selectedSnapshots[0] + ' and #' +
1352             selectedSnapshots[1] + ' (' + timeDeltaInSeconds +
1353             ' seconds worth of data)';
1354       } else {
1355         // This shouldn't be possible...
1356         throw 'Unexpected number of selected snapshots';
1357       }
1358     },
1360     updateMergedData_: function() {
1361       // Retrieve the merge options.
1362       var mergeColumns = this.getMergeColumns_();
1363       var shouldMergeSimilarThreads = this.shouldMergeSimilarThreads_();
1365       var selectedSnapshots = this.getSelectedSnapshotIndexes_();
1367       // We do merges a bit differently depending if we are displaying the diffs
1368       // between two snapshots, or just displaying a single snapshot.
1369       if (selectedSnapshots.length == 1) {
1370         var snapshot = this.snapshots_[selectedSnapshots[0]];
1371         this.mergedData_ = mergeRows(snapshot.flatData,
1372                                      mergeColumns,
1373                                      shouldMergeSimilarThreads,
1374                                      false);
1376       } else if (selectedSnapshots.length == 2) {
1377         var snapshot1 = this.snapshots_[selectedSnapshots[0]];
1378         var snapshot2 = this.snapshots_[selectedSnapshots[1]];
1380         // Merge the data for snapshot1.
1381         var mergedRows1 = mergeRows(snapshot1.flatData,
1382                                     mergeColumns,
1383                                     shouldMergeSimilarThreads,
1384                                     true);
1386         // Merge the data for snapshot2.
1387         var mergedRows2 = mergeRows(snapshot2.flatData,
1388                                     mergeColumns,
1389                                     shouldMergeSimilarThreads,
1390                                     true);
1392         // Do a diff between the two snapshots.
1393         this.mergedData_ = subtractSnapshots(mergedRows1,
1394                                              mergedRows2,
1395                                              mergeColumns);
1396       } else {
1397         throw 'Unexpected number of selected snapshots';
1398       }
1400       // Recompute filteredData_ (since it is derived from mergedData_)
1401       this.updateFilteredData_();
1402     },
1404     updateFilteredData_: function() {
1405       // Recompute filteredData_.
1406       this.filteredData_ = [];
1407       var filterFunc = this.getFilterFunction_();
1408       for (var i = 0; i < this.mergedData_.length; ++i) {
1409         var r = this.mergedData_[i];
1410         if (!filterFunc(r)) {
1411           // Not matched by our filter, discard.
1412           continue;
1413         }
1414         this.filteredData_.push(r);
1415       }
1417       // Recompute groupedData_ (since it is derived from filteredData_)
1418       this.updateGroupedData_();
1419     },
1421     updateGroupedData_: function() {
1422       // Recompute groupedData_.
1423       var groupKeyToData = {};
1424       var entryToGroupKeyFunc = this.getGroupingFunction_();
1425       for (var i = 0; i < this.filteredData_.length; ++i) {
1426         var r = this.filteredData_[i];
1428         var groupKey = entryToGroupKeyFunc(r);
1430         var groupData = groupKeyToData[groupKey];
1431         if (!groupData) {
1432           groupData = {
1433             key: JSON.parse(groupKey),
1434             aggregates: initializeAggregates(ALL_KEYS),
1435             rows: [],
1436           };
1437           groupKeyToData[groupKey] = groupData;
1438         }
1440         // Add the row to our list.
1441         groupData.rows.push(r);
1443         // Update aggregates for each column.
1444         consumeAggregates(groupData.aggregates, r);
1445       }
1446       this.groupedData_ = groupKeyToData;
1448       // Figure out a display order for the groups themselves.
1449       this.sortedGroupKeys_ = getDictionaryKeys(groupKeyToData);
1450       this.sortedGroupKeys_.sort(this.getGroupSortingFunction_());
1452       // Sort the group data.
1453       this.sortGroupedData_();
1454     },
1456     sortGroupedData_: function() {
1457       var sortingFunc = this.getSortingFunction_();
1458       for (var k in this.groupedData_)
1459         this.groupedData_[k].rows.sort(sortingFunc);
1461       // Every cached data dependency is now up to date, all that is left is
1462       // to actually draw the result.
1463       this.redrawData_();
1464     },
1466     getVisibleColumnKeys_: function() {
1467       // Figure out what columns to include, based on the selected checkboxes.
1468       var columns = this.getSelectionColumns_();
1469       columns = columns.slice(0);
1471       // Eliminate columns which we are merging on.
1472       deleteValuesFromArray(columns, this.getMergeColumns_());
1474       // Eliminate columns which we are grouped on.
1475       if (this.sortedGroupKeys_.length > 0) {
1476         // The grouping will be the the same for each so just pick the first.
1477         var randomGroupKey = this.groupedData_[this.sortedGroupKeys_[0]].key;
1479         // The grouped properties are going to be the same for each row in our,
1480         // table, so avoid drawing them in our table!
1481         var keysToExclude = [];
1483         for (var i = 0; i < randomGroupKey.length; ++i)
1484           keysToExclude.push(randomGroupKey[i].key);
1485         deleteValuesFromArray(columns, keysToExclude);
1486       }
1488       // If we are currently showing a "diff", hide the max columns, since we
1489       // are not populating it correctly. See the TODO at the top of this file.
1490       if (this.getSelectedSnapshotIndexes_().length > 1)
1491         deleteValuesFromArray(columns, [KEY_MAX_RUN_TIME, KEY_MAX_QUEUE_TIME]);
1493       return columns;
1494     },
1496     redrawData_: function() {
1497       // Clear the results div, sine we may be overwriting older data.
1498       var parent = $(RESULTS_DIV_ID);
1499       parent.innerHTML = '';
1501       var columns = this.getVisibleColumnKeys_();
1503       // Draw each group.
1504       for (var i = 0; i < this.sortedGroupKeys_.length; ++i) {
1505         var k = this.sortedGroupKeys_[i];
1506         this.drawGroup_(parent, k, columns);
1507       }
1508     },
1510     /**
1511      * Renders the information for a particular group.
1512      */
1513     drawGroup_: function(parent, groupKey, columns) {
1514       var groupData = this.groupedData_[groupKey];
1516       var div = addNode(parent, 'div');
1517       div.className = 'group-container';
1519       this.drawGroupTitle_(div, groupData.key);
1521       var table = addNode(div, 'table');
1523       this.drawDataTable_(table, groupData, columns, groupKey);
1524     },
1526     /**
1527      * Draws a title into |parent| that describes |groupKey|.
1528      */
1529     drawGroupTitle_: function(parent, groupKey) {
1530       if (groupKey.length == 0) {
1531         // Empty group key means there was no grouping.
1532         return;
1533       }
1535       var parent = addNode(parent, 'div');
1536       parent.className = 'group-title-container';
1538       // Each component of the group key represents the "key=value" constraint
1539       // for this group. Show these as an AND separated list.
1540       for (var i = 0; i < groupKey.length; ++i) {
1541         if (i > 0)
1542           addNode(parent, 'i', ' and ');
1543         var e = groupKey[i];
1544         addNode(parent, 'b', getNameForKey(e.key) + ' = ');
1545         addNode(parent, 'span', e.value);
1546       }
1547     },
1549     /**
1550      * Renders a table which summarizes all |column| fields for |data|.
1551      */
1552     drawDataTable_: function(table, data, columns, groupKey) {
1553       table.className = 'results-table';
1554       var thead = addNode(table, 'thead');
1555       var tbody = addNode(table, 'tbody');
1557       var displaySettings = this.getGroupDisplaySettings_(groupKey);
1558       var limit = displaySettings.limit;
1560       this.drawAggregateRow_(thead, data.aggregates, columns);
1561       this.drawTableHeader_(thead, columns);
1562       this.drawTableBody_(tbody, data.rows, columns, limit);
1563       this.drawTruncationRow_(tbody, data.rows.length, limit, columns.length,
1564                               groupKey);
1565     },
1567     drawTableHeader_: function(thead, columns) {
1568       var tr = addNode(thead, 'tr');
1569       for (var i = 0; i < columns.length; ++i) {
1570         var key = columns[i];
1571         var th = addNode(tr, 'th', getNameForKey(key));
1572         th.onclick = this.onClickColumn_.bind(this, key);
1574         // Draw an indicator if we are currently sorted on this column.
1575         // TODO(eroman): Should use an icon instead of asterisk!
1576         for (var j = 0; j < this.currentSortKeys_.length; ++j) {
1577           if (sortKeysMatch(this.currentSortKeys_[j], key)) {
1578             var sortIndicator = addNode(th, 'span', '*');
1579             sortIndicator.style.color = 'red';
1580             if (sortKeyIsReversed(this.currentSortKeys_[j])) {
1581               // Use double-asterisk for descending columns.
1582               addText(sortIndicator, '*');
1583             }
1584             break;
1585           }
1586         }
1587       }
1588     },
1590     drawTableBody_: function(tbody, rows, columns, limit) {
1591       for (var i = 0; i < rows.length && i < limit; ++i) {
1592         var e = rows[i];
1594         var tr = addNode(tbody, 'tr');
1596         for (var c = 0; c < columns.length; ++c) {
1597           var key = columns[c];
1598           var value = e[key];
1600           var td = addNode(tr, 'td');
1601           drawValueToCell(td, key, value);
1602         }
1603       }
1604     },
1606     /**
1607      * Renders a row that describes all the aggregate values for |columns|.
1608      */
1609     drawAggregateRow_: function(tbody, aggregates, columns) {
1610       var tr = addNode(tbody, 'tr');
1611       tr.className = 'aggregator-row';
1613       for (var i = 0; i < columns.length; ++i) {
1614         var key = columns[i];
1615         var td = addNode(tr, 'td');
1617         // Most of our outputs are numeric, so we want to align them to the
1618         // right. However for the  unique counts we will center.
1619         if (KEY_PROPERTIES[key].aggregator == UniquifyAggregator) {
1620           td.align = 'center';
1621         } else {
1622           td.align = 'right';
1623         }
1625         var aggregator = aggregates[key];
1626         if (aggregator)
1627           td.innerText = aggregator.getValueAsText();
1628       }
1629     },
1631     /**
1632      * Renders a row which describes how many rows the table has, how many are
1633      * currently hidden, and a set of buttons to show more.
1634      */
1635     drawTruncationRow_: function(tbody, numRows, limit, numColumns, groupKey) {
1636       var numHiddenRows = Math.max(numRows - limit, 0);
1637       var numVisibleRows = numRows - numHiddenRows;
1639       var tr = addNode(tbody, 'tr');
1640       tr.className = 'truncation-row';
1641       var td = addNode(tr, 'td');
1642       td.colSpan = numColumns;
1644       addText(td, numRows + ' rows');
1645       if (numHiddenRows > 0) {
1646         var s = addNode(td, 'span', ' (' + numHiddenRows + ' hidden) ');
1647         s.style.color = 'red';
1648       }
1650       if (numVisibleRows > LIMIT_INCREMENT) {
1651         addNode(td, 'button', 'Show less').onclick =
1652             this.changeGroupDisplayLimit_.bind(
1653                 this, groupKey, -LIMIT_INCREMENT);
1654       }
1655       if (numVisibleRows > 0) {
1656         addNode(td, 'button', 'Show none').onclick =
1657             this.changeGroupDisplayLimit_.bind(this, groupKey, -Infinity);
1658       }
1660       if (numHiddenRows > 0) {
1661         addNode(td, 'button', 'Show more').onclick =
1662             this.changeGroupDisplayLimit_.bind(this, groupKey, LIMIT_INCREMENT);
1663         addNode(td, 'button', 'Show all').onclick =
1664             this.changeGroupDisplayLimit_.bind(this, groupKey, Infinity);
1665       }
1666     },
1668     /**
1669      * Adjusts the row limit for group |groupKey| by |delta|.
1670      */
1671     changeGroupDisplayLimit_: function(groupKey, delta) {
1672       // Get the current settings for this group.
1673       var settings = this.getGroupDisplaySettings_(groupKey, true);
1675       // Compute the adjusted limit.
1676       var newLimit = settings.limit;
1677       var totalNumRows = this.groupedData_[groupKey].rows.length;
1678       newLimit = Math.min(totalNumRows, newLimit);
1679       newLimit += delta;
1680       newLimit = Math.max(0, newLimit);
1682       // Update the settings with the new limit.
1683       settings.limit = newLimit;
1685       // TODO(eroman): It isn't necessary to redraw *all* the data. Really we
1686       // just need to insert the missing rows (everything else stays the same)!
1687       this.redrawData_();
1688     },
1690     /**
1691      * Returns the rendering settings for group |groupKey|. This includes things
1692      * like how many rows to display in the table.
1693      */
1694     getGroupDisplaySettings_: function(groupKey, opt_create) {
1695       var settings = this.groupDisplaySettings_[groupKey];
1696       if (!settings) {
1697         // If we don't have any settings for this group yet, create some
1698         // default ones.
1699         if (groupKey == '[]') {
1700           // (groupKey of '[]' is what we use for ungrouped data).
1701           settings = {limit: INITIAL_UNGROUPED_ROW_LIMIT};
1702         } else {
1703           settings = {limit: INITIAL_GROUP_ROW_LIMIT};
1704         }
1705         if (opt_create)
1706           this.groupDisplaySettings_[groupKey] = settings;
1707       }
1708       return settings;
1709     },
1711     init_: function() {
1712       this.snapshots_ = [];
1714       // Start fetching the data from the browser; this will be our snapshot #0.
1715       this.takeSnapshot_();
1717       // Data goes through the following pipeline:
1718       // (1) Raw data received from browser, and transformed into our own
1719       //     internal row format (where properties are indexed by KEY_*
1720       //     constants.)
1721       // (2) We "augment" each row by adding some extra computed columns
1722       //     (like averages).
1723       // (3) The rows are merged using current merge settings.
1724       // (4) The rows that don't match current search expression are
1725       //     tossed out.
1726       // (5) The rows are organized into "groups" based on current settings,
1727       //     and aggregate values are computed for each resulting group.
1728       // (6) The rows within each group are sorted using current settings.
1729       // (7) The grouped rows are drawn to the screen.
1730       this.mergedData_ = [];
1731       this.filteredData_ = [];
1732       this.groupedData_ = {};
1733       this.sortedGroupKeys_ = [];
1735       this.groupDisplaySettings_ = {};
1737       this.fillSelectionCheckboxes_($(COLUMN_TOGGLES_CONTAINER_ID));
1738       this.fillMergeCheckboxes_($(COLUMN_MERGE_TOGGLES_CONTAINER_ID));
1740       $(FILTER_SEARCH_ID).onsearch = this.onChangedFilter_.bind(this);
1742       this.currentSortKeys_ = INITIAL_SORT_KEYS.slice(0);
1743       this.currentGroupingKeys_ = INITIAL_GROUP_KEYS.slice(0);
1745       this.fillGroupingDropdowns_();
1746       this.fillSortingDropdowns_();
1748       $(EDIT_COLUMNS_LINK_ID).onclick =
1749           toggleNodeDisplay.bind(null, $(EDIT_COLUMNS_ROW));
1751       $(TOGGLE_SNAPSHOTS_LINK_ID).onclick =
1752           toggleNodeDisplay.bind(null, $(SNAPSHOTS_ROW));
1754       $(MERGE_SIMILAR_THREADS_CHECKBOX_ID).onchange =
1755           this.onMergeSimilarThreadsCheckboxChanged_.bind(this);
1757       $(TAKE_SNAPSHOT_BUTTON_ID).onclick = this.takeSnapshot_.bind(this);
1759       $(SAVE_SNAPSHOTS_BUTTON_ID).onclick = this.saveSnapshots_.bind(this);
1760       $(SNAPSHOT_FILE_LOADER_ID).onchange = this.loadFileChanged_.bind(this);
1761     },
1763     takeSnapshot_: function() {
1764       // Start a new empty snapshot. Make note of the current time, so we know
1765       // when the snaphot was taken.
1766       this.snapshots_.push({flatData: [], origData: [], time: getTimeMillis()});
1768       // Update the UI to reflect the new snapshot.
1769       this.addSnapshotToList_(this.snapshots_.length - 1);
1771       // Ask the browser for the profiling data. We will receive the data
1772       // later through a callback to addDataToSnapshot_().
1773       g_browserBridge.sendGetData();
1774     },
1776     saveSnapshots_: function() {
1777       var snapshots = [];
1778       for (var i = 0; i < this.snapshots_.length; ++i) {
1779         snapshots.push({ data: this.snapshots_[i].origData,
1780                          timestamp: Math.floor(
1781                                  this.snapshots_[i].time / 1000) });
1782       }
1784       var dump = {
1785         'userAgent': navigator.userAgent,
1786         'version': 1,
1787         'snapshots': snapshots
1788       };
1790       var dumpText = JSON.stringify(dump, null, ' ');
1791       var textBlob = new Blob([dumpText],
1792                               { type: 'octet/stream', endings: 'native' });
1793       var blobUrl = window.URL.createObjectURL(textBlob);
1794       $(DOWNLOAD_ANCHOR_ID).href = blobUrl;
1795       $(DOWNLOAD_ANCHOR_ID).click();
1796     },
1798     loadFileChanged_: function() {
1799       this.loadSnapshots_($(SNAPSHOT_FILE_LOADER_ID).files[0]);
1800     },
1802     loadSnapshots_: function(file) {
1803       if (file) {
1804         var fileReader = new FileReader();
1806         fileReader.onload = this.onLoadSnapshotsFile_.bind(this, file);
1807         fileReader.onerror = this.onLoadSnapshotsFileError_.bind(this, file);
1809         fileReader.readAsText(file);
1810       }
1811     },
1813     onLoadSnapshotsFile_: function(file, event) {
1814       try {
1815         var parsed = null;
1816         parsed = JSON.parse(event.target.result);
1818         if (parsed.version != 1) {
1819           throw new Error('Unrecognized version: ' + parsed.version);
1820         }
1822         if (parsed.snapshots.length < 1) {
1823           throw new Error('File contains no data');
1824         }
1826         this.displayLoadedFile_(file, parsed);
1827         this.hideFileLoadError_();
1828       } catch (error) {
1829         this.displayFileLoadError_('File load failure: ' + error.message);
1830       }
1831     },
1833     clearExistingSnapshots_: function() {
1834       var tbody = $('snapshots-tbody');
1835       this.snapshots_ = [];
1836       tbody.innerHTML = '';
1837       this.updateMergedDataSoon_();
1838     },
1840     displayLoadedFile_: function(file, content) {
1841       this.clearExistingSnapshots_();
1842       $(TAKE_SNAPSHOT_BUTTON_ID).disabled = true;
1843       $(SAVE_SNAPSHOTS_BUTTON_ID).disabled = true;
1845       if (content.snapshots.length > 1) {
1846         setNodeDisplay($(SNAPSHOTS_ROW), true);
1847       }
1849       for (var i = 0; i < content.snapshots.length; ++i) {
1850         var snapshot = content.snapshots[i];
1851         this.snapshots_.push({flatData: [], origData: [],
1852                               time: snapshot.timestamp * 1000});
1853         this.addSnapshotToList_(this.snapshots_.length - 1);
1854         var snapshotData = snapshot.data;
1855         for (var j = 0; j < snapshotData.length; ++j) {
1856           this.addDataToSnapshot(snapshotData[j]);
1857         }
1858       }
1859       this.redrawData_();
1860     },
1862     onLoadSnapshotsFileError_: function(file, filedata) {
1863       this.displayFileLoadError_('Error loading ' + file.name);
1864     },
1866     displayFileLoadError_: function(message) {
1867       $(LOAD_ERROR_ID).textContent = message;
1868       $(LOAD_ERROR_ID).hidden = false;
1869     },
1871     hideFileLoadError_: function() {
1872       $(LOAD_ERROR_ID).textContent = '';
1873       $(LOAD_ERROR_ID).hidden = true;
1874     },
1876     getSnapshotCheckbox_: function(i) {
1877       return $(this.getSnapshotCheckboxId_(i));
1878     },
1880     getSnapshotCheckboxId_: function(i) {
1881       return 'snapshotCheckbox-' + i;
1882     },
1884     addSnapshotToList_: function(i) {
1885       var tbody = $('snapshots-tbody');
1887       var tr = addNode(tbody, 'tr');
1889       var id = this.getSnapshotCheckboxId_(i);
1891       var checkboxCell = addNode(tr, 'td');
1892       var checkbox = addNode(checkboxCell, 'input');
1893       checkbox.type = 'checkbox';
1894       checkbox.id = id;
1895       checkbox.__index = i;
1896       checkbox.onclick = this.onSnapshotCheckboxChanged_.bind(this);
1898       addNode(tr, 'td', '#' + i);
1900       var labelCell = addNode(tr, 'td');
1901       var l = addNode(labelCell, 'label');
1903       var dateString = new Date(this.snapshots_[i].time).toLocaleString();
1904       addText(l, dateString);
1905       l.htmlFor = id;
1907       // If we are on snapshot 0, make it the default.
1908       if (i == 0) {
1909         checkbox.checked = true;
1910         checkbox.__time = getTimeMillis();
1911         this.updateSnapshotCheckboxStyling_();
1912       }
1913     },
1915     updateSnapshotCheckboxStyling_: function() {
1916       for (var i = 0; i < this.snapshots_.length; ++i) {
1917         var checkbox = this.getSnapshotCheckbox_(i);
1918         checkbox.parentNode.parentNode.className =
1919             checkbox.checked ? 'selected_snapshot' : '';
1920       }
1921     },
1923     onSnapshotCheckboxChanged_: function(event) {
1924       // Keep track of when we clicked this box (for when we need to uncheck
1925       // older boxes).
1926       event.target.__time = getTimeMillis();
1928       // Find all the checked boxes. Either 1 or 2 can be checked. If a third
1929       // was just checked, then uncheck one of the earlier ones so we only have
1930       // 2.
1931       var checked = this.getSelectedSnapshotBoxes_();
1932       checked.sort(function(a, b) { return b.__time - a.__time; });
1933       if (checked.length > 2) {
1934         for (var i = 2; i < checked.length; ++i)
1935           checked[i].checked = false;
1936         checked.length = 2;
1937       }
1939       // We should always have at least 1 selection. Prevent the user from
1940       // unselecting the final box.
1941       if (checked.length == 0)
1942         event.target.checked = true;
1944       this.updateSnapshotCheckboxStyling_();
1945       this.updateSnapshotSelectionSummaryDiv_();
1947       // Recompute mergedData_ (since it is derived from selected snapshots).
1948       this.updateMergedData_();
1949     },
1951     fillSelectionCheckboxes_: function(parent) {
1952       this.selectionCheckboxes_ = {};
1954       var onChangeFunc = this.onSelectCheckboxChanged_.bind(this);
1956       for (var i = 0; i < ALL_TABLE_COLUMNS.length; ++i) {
1957         var key = ALL_TABLE_COLUMNS[i];
1958         var checkbox = addLabeledCheckbox(parent, getNameForKey(key));
1959         checkbox.checked = true;
1960         checkbox.onchange = onChangeFunc;
1961         addText(parent, ' ');
1962         this.selectionCheckboxes_[key] = checkbox;
1963       }
1965       for (var i = 0; i < INITIALLY_HIDDEN_KEYS.length; ++i) {
1966         this.selectionCheckboxes_[INITIALLY_HIDDEN_KEYS[i]].checked = false;
1967       }
1968     },
1970     getSelectionColumns_: function() {
1971       return getKeysForCheckedBoxes(this.selectionCheckboxes_);
1972     },
1974     getMergeColumns_: function() {
1975       return getKeysForCheckedBoxes(this.mergeCheckboxes_);
1976     },
1978     shouldMergeSimilarThreads_: function() {
1979       return $(MERGE_SIMILAR_THREADS_CHECKBOX_ID).checked;
1980     },
1982     fillMergeCheckboxes_: function(parent) {
1983       this.mergeCheckboxes_ = {};
1985       var onChangeFunc = this.onMergeCheckboxChanged_.bind(this);
1987       for (var i = 0; i < MERGEABLE_KEYS.length; ++i) {
1988         var key = MERGEABLE_KEYS[i];
1989         var checkbox = addLabeledCheckbox(parent, getNameForKey(key));
1990         checkbox.onchange = onChangeFunc;
1991         addText(parent, ' ');
1992         this.mergeCheckboxes_[key] = checkbox;
1993       }
1995       for (var i = 0; i < INITIALLY_MERGED_KEYS.length; ++i) {
1996         this.mergeCheckboxes_[INITIALLY_MERGED_KEYS[i]].checked = true;
1997       }
1998     },
2000     fillGroupingDropdowns_: function() {
2001       var parent = $(GROUP_BY_CONTAINER_ID);
2002       parent.innerHTML = '';
2004       for (var i = 0; i <= this.currentGroupingKeys_.length; ++i) {
2005         // Add a dropdown.
2006         var select = addNode(parent, 'select');
2007         select.onchange = this.onChangedGrouping_.bind(this, select, i);
2009         addOptionsForGroupingSelect(select);
2011         if (i < this.currentGroupingKeys_.length) {
2012           var key = this.currentGroupingKeys_[i];
2013           setSelectedOptionByValue(select, key);
2014         }
2015       }
2016     },
2018     fillSortingDropdowns_: function() {
2019       var parent = $(SORT_BY_CONTAINER_ID);
2020       parent.innerHTML = '';
2022       for (var i = 0; i <= this.currentSortKeys_.length; ++i) {
2023         // Add a dropdown.
2024         var select = addNode(parent, 'select');
2025         select.onchange = this.onChangedSorting_.bind(this, select, i);
2027         addOptionsForSortingSelect(select);
2029         if (i < this.currentSortKeys_.length) {
2030           var key = this.currentSortKeys_[i];
2031           setSelectedOptionByValue(select, key);
2032         }
2033       }
2034     },
2036     onChangedGrouping_: function(select, i) {
2037       updateKeyListFromDropdown(this.currentGroupingKeys_, i, select);
2038       this.fillGroupingDropdowns_();
2039       this.updateGroupedData_();
2040     },
2042     onChangedSorting_: function(select, i) {
2043       updateKeyListFromDropdown(this.currentSortKeys_, i, select);
2044       this.fillSortingDropdowns_();
2045       this.sortGroupedData_();
2046     },
2048     onSelectCheckboxChanged_: function() {
2049       this.redrawData_();
2050     },
2052     onMergeCheckboxChanged_: function() {
2053       this.updateMergedData_();
2054     },
2056     onMergeSimilarThreadsCheckboxChanged_: function() {
2057       this.updateMergedData_();
2058     },
2060     onChangedFilter_: function() {
2061       this.updateFilteredData_();
2062     },
2064     /**
2065      * When left-clicking a column, change the primary sort order to that
2066      * column. If we were already sorted on that column then reverse the order.
2067      *
2068      * When alt-clicking, add a secondary sort column. Similarly, if
2069      * alt-clicking a column which was already being sorted on, reverse its
2070      * order.
2071      */
2072     onClickColumn_: function(key, event) {
2073       // If this property wants to start off in descending order rather then
2074       // ascending, flip it.
2075       if (KEY_PROPERTIES[key].sortDescending)
2076         key = reverseSortKey(key);
2078       // Scan through our sort order and see if we are already sorted on this
2079       // key. If so, reverse that sort ordering.
2080       var foundIndex = -1;
2081       for (var i = 0; i < this.currentSortKeys_.length; ++i) {
2082         var curKey = this.currentSortKeys_[i];
2083         if (sortKeysMatch(curKey, key)) {
2084           this.currentSortKeys_[i] = reverseSortKey(curKey);
2085           foundIndex = i;
2086           break;
2087         }
2088       }
2090       if (event.altKey) {
2091         if (foundIndex == -1) {
2092           // If we weren't already sorted on the column that was alt-clicked,
2093           // then add it to our sort.
2094           this.currentSortKeys_.push(key);
2095         }
2096       } else {
2097         if (foundIndex != 0 ||
2098             !sortKeysMatch(this.currentSortKeys_[foundIndex], key)) {
2099           // If the column we left-clicked wasn't already our primary column,
2100           // make it so.
2101           this.currentSortKeys_ = [key];
2102         } else {
2103           // If the column we left-clicked was already our primary column (and
2104           // we just reversed it), remove any secondary sorts.
2105           this.currentSortKeys_.length = 1;
2106         }
2107       }
2109       this.fillSortingDropdowns_();
2110       this.sortGroupedData_();
2111     },
2113     getSortingFunction_: function() {
2114       var sortKeys = this.currentSortKeys_.slice(0);
2116       // Eliminate the empty string keys (which means they were unspecified).
2117       deleteValuesFromArray(sortKeys, ['']);
2119       // If no sort is specified, use our default sort.
2120       if (sortKeys.length == 0)
2121         sortKeys = [DEFAULT_SORT_KEYS];
2123       return function(a, b) {
2124         for (var i = 0; i < sortKeys.length; ++i) {
2125           var key = Math.abs(sortKeys[i]);
2126           var factor = sortKeys[i] < 0 ? -1 : 1;
2128           var propA = a[key];
2129           var propB = b[key];
2131           var comparison = compareValuesForKey(key, propA, propB);
2132           comparison *= factor;  // Possibly reverse the ordering.
2134           if (comparison != 0)
2135             return comparison;
2136         }
2138         // Tie breaker.
2139         return simpleCompare(JSON.stringify(a), JSON.stringify(b));
2140       };
2141     },
2143     getGroupSortingFunction_: function() {
2144       return function(a, b) {
2145         var groupKey1 = JSON.parse(a);
2146         var groupKey2 = JSON.parse(b);
2148         for (var i = 0; i < groupKey1.length; ++i) {
2149           var comparison = compareValuesForKey(
2150               groupKey1[i].key,
2151               groupKey1[i].value,
2152               groupKey2[i].value);
2154           if (comparison != 0)
2155             return comparison;
2156         }
2158         // Tie breaker.
2159         return simpleCompare(a, b);
2160       };
2161     },
2163     getFilterFunction_: function() {
2164       var searchStr = $(FILTER_SEARCH_ID).value;
2166       // Normalize the search expression.
2167       searchStr = trimWhitespace(searchStr);
2168       searchStr = searchStr.toLowerCase();
2170       return function(x) {
2171         // Match everything when there was no filter.
2172         if (searchStr == '')
2173           return true;
2175         // Treat the search text as a LOWERCASE substring search.
2176         for (var k = BEGIN_KEY; k < END_KEY; ++k) {
2177           var propertyText = getTextValueForProperty(k, x[k]);
2178           if (propertyText.toLowerCase().indexOf(searchStr) != -1)
2179             return true;
2180         }
2182         return false;
2183       };
2184     },
2186     getGroupingFunction_: function() {
2187       var groupings = this.currentGroupingKeys_.slice(0);
2189       // Eliminate the empty string groupings (which means they were
2190       // unspecified).
2191       deleteValuesFromArray(groupings, ['']);
2193       // Eliminate duplicate primary/secondary group by directives, since they
2194       // are redundant.
2195       deleteDuplicateStringsFromArray(groupings);
2197       return function(e) {
2198         var groupKey = [];
2200         for (var i = 0; i < groupings.length; ++i) {
2201           var entry = {key: groupings[i],
2202                        value: e[groupings[i]]};
2203           groupKey.push(entry);
2204         }
2206         return JSON.stringify(groupKey);
2207       };
2208     },
2209   };
2211   return MainView;
2212 })();