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