1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5var g_browserBridge;
6var g_mainView;
7
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.
12
13/**
14 * Main entry point called once the page has loaded.
15 */
16function onLoad() {
17  g_browserBridge = new BrowserBridge();
18  g_mainView = new MainView();
19}
20
21document.addEventListener('DOMContentLoaded', onLoad);
22
23/**
24 * This class provides a "bridge" for communicating between the javascript and
25 * the browser. Used as a singleton.
26 */
27var BrowserBridge = (function() {
28  'use strict';
29
30  /**
31   * @constructor
32   */
33  function BrowserBridge() {
34  }
35
36  BrowserBridge.prototype = {
37    //--------------------------------------------------------------------------
38    // Messages sent to the browser
39    //--------------------------------------------------------------------------
40
41    sendGetData: function() {
42      chrome.send('getData');
43    },
44
45    sendResetData: function() {
46      chrome.send('resetData');
47    },
48
49    //--------------------------------------------------------------------------
50    // Messages received from the browser.
51    //--------------------------------------------------------------------------
52
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  };
59
60  return BrowserBridge;
61})();
62
63/**
64 * This class handles the presentation of our profiler view. Used as a
65 * singleton.
66 */
67var MainView = (function() {
68  'use strict';
69
70  // --------------------------------------------------------------------------
71  // Important IDs in the HTML document
72  // --------------------------------------------------------------------------
73
74  // The search box to filter results.
75  var FILTER_SEARCH_ID = 'filter-search';
76
77  // The container node to put all the "Group by" dropdowns into.
78  var GROUP_BY_CONTAINER_ID = 'group-by-container';
79
80  // The container node to put all the "Sort by" dropdowns into.
81  var SORT_BY_CONTAINER_ID = 'sort-by-container';
82
83  // The DIV to put all the tables into.
84  var RESULTS_DIV_ID = 'results-div';
85
86  // The container node to put all the column (visibility) checkboxes into.
87  var COLUMN_TOGGLES_CONTAINER_ID = 'column-toggles-container';
88
89  // The container node to put all the column (merge) checkboxes into.
90  var COLUMN_MERGE_TOGGLES_CONTAINER_ID = 'column-merge-toggles-container';
91
92  // The anchor which toggles visibility of column checkboxes.
93  var EDIT_COLUMNS_LINK_ID = 'edit-columns-link';
94
95  // The container node to show/hide when toggling the column checkboxes.
96  var EDIT_COLUMNS_ROW = 'edit-columns-row';
97
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';
101
102  var RESET_DATA_LINK_ID = 'reset-data-link';
103
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';
108
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';
112
113  var DOWNLOAD_ANCHOR_ID = 'download-anchor';
114
115  // --------------------------------------------------------------------------
116  // Row keys
117  // --------------------------------------------------------------------------
118
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.
125
126  var BEGIN_KEY = 1;  // Start at 1 rather than 0 to simplify sorting code.
127  var END_KEY = BEGIN_KEY;
128
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++;
144
145  var NUM_KEYS = END_KEY - BEGIN_KEY;
146
147  // --------------------------------------------------------------------------
148  // Aggregators
149  // --------------------------------------------------------------------------
150
151  // To generalize computing/displaying the aggregate "counts" for each column,
152  // we specify an optional "Aggregator" class to use with each property.
153
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.
159
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    }
168
169    Aggregator.prototype = {
170      consume: function(e) {
171        this.valuesSet_[e[this.key_]] = true;
172      },
173
174      getValueAsText: function() {
175        return getDictionaryKeys(this.valuesSet_).length + ' unique';
176      },
177    };
178
179    return {
180      create: function(key) { return new Aggregator(key); }
181    };
182  })();
183
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    }
192
193    Aggregator.prototype = {
194      consume: function(e) {
195        this.sum_ += e[this.key_];
196      },
197
198      getValue: function() {
199        return this.sum_;
200      },
201
202      getValueAsText: function() {
203        return formatNumberAsText(this.getValue());
204      },
205    };
206
207    return {
208      create: function(key) { return new Aggregator(key); }
209    };
210  })();
211
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;
220
221      this.numeratorSum_ = 0;
222      this.divisorSum_ = 0;
223    }
224
225    Aggregator.prototype = {
226      consume: function(e) {
227        this.numeratorSum_ += e[this.numeratorKey_];
228        this.divisorSum_ += e[this.divisorKey_];
229      },
230
231      getValue: function() {
232        return this.numeratorSum_ / this.divisorSum_;
233      },
234
235      getValueAsText: function() {
236        return formatNumberAsText(this.getValue());
237      },
238    };
239
240    return {
241      create: function(numeratorKey, divisorKey) {
242        return {
243          create: function(key) {
244            return new Aggregator(numeratorKey, divisorKey);
245          },
246        };
247      }
248    };
249  })();
250
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    }
259
260    Aggregator.prototype = {
261      consume: function(e) {
262        this.max_ = Math.max(this.max_, e[this.key_]);
263      },
264
265      getValue: function() {
266        return this.max_;
267      },
268
269      getValueAsText: function() {
270        return formatNumberAsText(this.getValue());
271      },
272    };
273
274    return {
275      create: function(key) { return new Aggregator(key); }
276    };
277  })();
278
279  // --------------------------------------------------------------------------
280  // Key properties
281  // --------------------------------------------------------------------------
282
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      ]);
294
295  function diffFuncForCount(a, b) {
296    return b - a;
297  }
298
299  function diffFuncForMax(a, b) {
300    return b;
301  }
302
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 = [];
342
343  KEY_PROPERTIES[KEY_PROCESS_ID] = {
344    name: 'PID',
345    cellAlignment: 'right',
346    aggregator: UniquifyAggregator,
347  };
348
349  KEY_PROPERTIES[KEY_PROCESS_TYPE] = {
350    name: 'Process type',
351    aggregator: UniquifyAggregator,
352  };
353
354  KEY_PROPERTIES[KEY_BIRTH_THREAD] = {
355    name: 'Birth thread',
356    inputJsonKey: 'birth_thread',
357    aggregator: UniquifyAggregator,
358    comparator: threadNameComparator,
359  };
360
361  KEY_PROPERTIES[KEY_DEATH_THREAD] = {
362    name: 'Exec thread',
363    inputJsonKey: 'death_thread',
364    aggregator: UniquifyAggregator,
365    comparator: threadNameComparator,
366  };
367
368  KEY_PROPERTIES[KEY_FUNCTION_NAME] = {
369    name: 'Function name',
370    inputJsonKey: 'birth_location.function_name',
371    aggregator: UniquifyAggregator,
372  };
373
374  KEY_PROPERTIES[KEY_FILE_NAME] = {
375    name: 'File name',
376    inputJsonKey: 'birth_location.file_name',
377    aggregator: UniquifyAggregator,
378  };
379
380  KEY_PROPERTIES[KEY_LINE_NUMBER] = {
381    name: 'Line number',
382    cellAlignment: 'right',
383    inputJsonKey: 'birth_location.line_number',
384    aggregator: UniquifyAggregator,
385  };
386
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  };
396
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  };
406
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  };
416
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  };
426
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  };
434
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  };
444
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  };
452
453  KEY_PROPERTIES[KEY_SOURCE_LOCATION] = {
454    name: 'Source location',
455    type: 'string',
456    aggregator: UniquifyAggregator,
457  };
458
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  }
468
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);
476
477  // --------------------------------------------------------------------------
478  // Default settings
479  // --------------------------------------------------------------------------
480
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  ];
490
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  ];
506
507  /**
508   * The ordered list of sorting choices to expose in the "Sort by"
509   * dropdowns.
510   */
511  var SORT_DROPDOWN_CHOICES = ALL_KEYS;
512
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;
518
519  /**
520   * The initial keys to sort by when loading the page (can be changed later).
521   */
522  var INITIAL_SORT_KEYS = [-KEY_COUNT];
523
524  /**
525   * The default sort keys to use when nothing has been specified.
526   */
527  var DEFAULT_SORT_KEYS = [-KEY_COUNT];
528
529  /**
530   * The initial keys to group by when loading the page (can be changed later).
531   */
532  var INITIAL_GROUP_KEYS = [];
533
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  ];
543
544  /**
545   * The columns to merge by default.
546   */
547  var INITIALLY_MERGED_KEYS = [];
548
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  ];
566
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;
576
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;
583
584  /**
585   * The initial number of rows to display (rest are hidden) for each group.
586   */
587  var INITIAL_GROUP_ROW_LIMIT = 10;
588
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;
594
595  // --------------------------------------------------------------------------
596  // General utility functions
597  // --------------------------------------------------------------------------
598
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  }
609
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);
616
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  }
625
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  }
636
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;
646
647    function getCustomRank(x) {
648      var rank = valueToRankMap[x];
649      if (rank == undefined)
650        rank = Infinity;  // Unmatched.
651      return rank;
652    }
653
654    return function(a, b) {
655      var aRank = getCustomRank(a);
656      var bRank = getCustomRank(b);
657
658      // Not matched by any of our exceptions.
659      if (aRank == bRank)
660        return simpleCompare(a, b);
661
662      if (aRank < bRank)
663        return -1;
664      return 1;
665    };
666  }
667
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  }
682
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  }
695
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  }
704
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  }
718
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 = {};
725
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  }
736
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  }
746
747  function trimWhitespace(text) {
748    var m = /^\s*(.*)\s*$/.exec(text);
749    return m[1];
750  }
751
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  }
764
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  }
776
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;
786
787    return path.substr(lastSlash + 1);
788  }
789
790  /**
791   * Returns the current time in milliseconds since unix epoch.
792   */
793  function getTimeMillis() {
794    return (new Date()).getTime();
795  }
796
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  }
807
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  }
818
819  // --------------------------------------------------------------------------
820  // Functions that augment, bucket, and compute aggregates for the input data.
821  // --------------------------------------------------------------------------
822
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  }
830
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  }
835
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 = [];
843
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    }
849
850    return aggregates;
851  }
852
853  function consumeAggregates(aggregates, row) {
854    for (var key in aggregates)
855      aggregates[key].consume(row);
856  }
857
858  function bucketIdenticalRows(rows, identityKeys, propertyGetterFunc) {
859    var identicalRows = {};
860    for (var i = 0; i < rows.length; ++i) {
861      var r = rows[i];
862
863      var rowIdentity = [];
864      for (var j = 0; j < identityKeys.length; ++j)
865        rowIdentity.push(propertyGetterFunc(r, identityKeys[j]));
866      rowIdentity = rowIdentity.join('\n');
867
868      var l = identicalRows[rowIdentity];
869      if (!l) {
870        l = [];
871        identicalRows[rowIdentity] = l;
872      }
873      l.push(r);
874    }
875    return identicalRows;
876  }
877
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;
897
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    }
912
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);
917
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);
923
924    // Group all the identical rows together, bucketed into |identicalRows|.
925    var identicalRows =
926        bucketIdenticalRows(origRows, identityKeys, propertyGetterFunc);
927
928    var mergedRows = outputAsDictionary ? {} : [];
929
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];
934
935      var newRow = [];
936
937      if (outputAsDictionary) {
938        mergedRows[k] = newRow;
939      } else {
940        mergedRows.push(newRow);
941      }
942
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]);
947
948      // Compute aggregates for the other columns.
949      var aggregates = initializeAggregates(aggregateKeys);
950
951      // Feed the rows to the aggregators.
952      for (var i = 0; i < l.length; ++i)
953        consumeAggregates(aggregates, l[i]);
954
955      // Suck out the data generated by the aggregators.
956      for (var aggregateKey in aggregates)
957        newRow[aggregateKey] = aggregates[aggregateKey].getValue();
958    }
959
960    return mergedRows;
961  }
962
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];
977
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);
983
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);
989
990    var diffedRows = [];
991
992    for (var rowId in data2) {
993      var row1 = data1[rowId];
994      var row2 = data2[rowId];
995
996      var newRow = [];
997
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]];
1002
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];
1009
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      }
1021
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      }
1027
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    }
1033
1034    return diffedRows;
1035  }
1036
1037  // --------------------------------------------------------------------------
1038  // HTML drawing code
1039  // --------------------------------------------------------------------------
1040
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    }
1047
1048    var textPrinter = KEY_PROPERTIES[key].textPrinter;
1049    if (textPrinter)
1050      return textPrinter(value);
1051    return value.toString();
1052  }
1053
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);
1061
1062    // Apply the desired cell alignment.
1063    var cellAlignment = KEY_PROPERTIES[key].cellAlignment;
1064    if (cellAlignment)
1065      td.align = cellAlignment;
1066
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];
1077
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    }
1089
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;
1096
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  }
1103
1104  // --------------------------------------------------------------------------
1105  // Helper code for handling the sort and grouping dropdowns.
1106  // --------------------------------------------------------------------------
1107
1108  function addOptionsForGroupingSelect(select) {
1109    // Add "no group" choice.
1110    addNode(select, 'option', '---').value = '';
1111
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  }
1118
1119  function addOptionsForSortingSelect(select) {
1120    // Add "no sort" choice.
1121    addNode(select, 'option', '---').value = '';
1122
1123    // Add a divider.
1124    addNode(select, 'optgroup').label = '';
1125
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    }
1130
1131    // Add a divider.
1132    addNode(select, 'optgroup').label = '';
1133
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  }
1141
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    }
1153
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  }
1163
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  }
1175
1176  function reverseSortKey(key) {
1177    return -key;
1178  }
1179
1180  function sortKeyIsReversed(key) {
1181    return key < 0;
1182  }
1183
1184  function sortKeysMatch(key1, key2) {
1185    return Math.abs(key1) == Math.abs(key2);
1186  }
1187
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  }
1196
1197  // --------------------------------------------------------------------------
1198
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    }
1208
1209    this.init_();
1210  }
1211
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;
1217
1218      var snapshot = this.snapshots_[snapshotIndex];
1219
1220      var pid = data.process_id;
1221      var ptype = data.process_type;
1222
1223      // Save the browser's representation of the data
1224      snapshot.origData.push(data);
1225
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 = [];
1233
1234        newRow[KEY_PROCESS_ID] = pid;
1235        newRow[KEY_PROCESS_TYPE] = ptype;
1236
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        }
1244
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        }
1252
1253        // Add our computed properties.
1254        augmentDataRow(newRow);
1255
1256        snapshot.flatData.push(newRow);
1257      }
1258
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      }
1264
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    },
1270
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      }
1277
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.
1281
1282      if (this.lastUpdateMergedDataTime_ == undefined)
1283        this.lastUpdateMergedDataTime_ = 0;
1284
1285      var timeSinceLastMerge = getTimeMillis() - this.lastUpdateMergedDataTime_;
1286      var timeToWait = Math.max(0, PROCESS_DATA_DELAY_MS - timeSinceLastMerge);
1287
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);
1295
1296      this.updateMergedDataPending_ = true;
1297      window.setTimeout(functionToRun, timeToWait);
1298    },
1299
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    },
1310
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    },
1325
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);
1333
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;
1342
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]];
1351
1352        var timeDeltaInSeconds =
1353            ((snapshot2.time - snapshot1.time) / 1000).toFixed(0);
1354
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    },
1367
1368    updateMergedData_: function() {
1369      // Retrieve the merge options.
1370      var mergeColumns = this.getMergeColumns_();
1371      var shouldMergeSimilarThreads = this.shouldMergeSimilarThreads_();
1372
1373      var selectedSnapshots = this.getSelectedSnapshotIndexes_();
1374
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);
1383
1384      } else if (selectedSnapshots.length == 2) {
1385        var snapshot1 = this.snapshots_[selectedSnapshots[0]];
1386        var snapshot2 = this.snapshots_[selectedSnapshots[1]];
1387
1388        // Merge the data for snapshot1.
1389        var mergedRows1 = mergeRows(snapshot1.flatData,
1390                                    mergeColumns,
1391                                    shouldMergeSimilarThreads,
1392                                    true);
1393
1394        // Merge the data for snapshot2.
1395        var mergedRows2 = mergeRows(snapshot2.flatData,
1396                                    mergeColumns,
1397                                    shouldMergeSimilarThreads,
1398                                    true);
1399
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      }
1407
1408      // Recompute filteredData_ (since it is derived from mergedData_)
1409      this.updateFilteredData_();
1410    },
1411
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      }
1424
1425      // Recompute groupedData_ (since it is derived from filteredData_)
1426      this.updateGroupedData_();
1427    },
1428
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];
1435
1436        var groupKey = entryToGroupKeyFunc(r);
1437
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        }
1447
1448        // Add the row to our list.
1449        groupData.rows.push(r);
1450
1451        // Update aggregates for each column.
1452        consumeAggregates(groupData.aggregates, r);
1453      }
1454      this.groupedData_ = groupKeyToData;
1455
1456      // Figure out a display order for the groups themselves.
1457      this.sortedGroupKeys_ = getDictionaryKeys(groupKeyToData);
1458      this.sortedGroupKeys_.sort(this.getGroupSortingFunction_());
1459
1460      // Sort the group data.
1461      this.sortGroupedData_();
1462    },
1463
1464    sortGroupedData_: function() {
1465      var sortingFunc = this.getSortingFunction_();
1466      for (var k in this.groupedData_)
1467        this.groupedData_[k].rows.sort(sortingFunc);
1468
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    },
1473
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);
1478
1479      // Eliminate columns which we are merging on.
1480      deleteValuesFromArray(columns, this.getMergeColumns_());
1481
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;
1486
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 = [];
1490
1491        for (var i = 0; i < randomGroupKey.length; ++i)
1492          keysToExclude.push(randomGroupKey[i].key);
1493        deleteValuesFromArray(columns, keysToExclude);
1494      }
1495
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]);
1500
1501      return columns;
1502    },
1503
1504    redrawData_: function() {
1505      // Clear the results div, sine we may be overwriting older data.
1506      var parent = $(RESULTS_DIV_ID);
1507      parent.innerHTML = '';
1508
1509      var columns = this.getVisibleColumnKeys_();
1510
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    },
1517
1518    /**
1519     * Renders the information for a particular group.
1520     */
1521    drawGroup_: function(parent, groupKey, columns) {
1522      var groupData = this.groupedData_[groupKey];
1523
1524      var div = addNode(parent, 'div');
1525      div.className = 'group-container';
1526
1527      this.drawGroupTitle_(div, groupData.key);
1528
1529      var table = addNode(div, 'table');
1530
1531      this.drawDataTable_(table, groupData, columns, groupKey);
1532    },
1533
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      }
1542
1543      var parent = addNode(parent, 'div');
1544      parent.className = 'group-title-container';
1545
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    },
1556
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');
1564
1565      var displaySettings = this.getGroupDisplaySettings_(groupKey);
1566      var limit = displaySettings.limit;
1567
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    },
1574
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);
1581
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    },
1597
1598    drawTableBody_: function(tbody, rows, columns, limit) {
1599      for (var i = 0; i < rows.length && i < limit; ++i) {
1600        var e = rows[i];
1601
1602        var tr = addNode(tbody, 'tr');
1603
1604        for (var c = 0; c < columns.length; ++c) {
1605          var key = columns[c];
1606          var value = e[key];
1607
1608          var td = addNode(tr, 'td');
1609          drawValueToCell(td, key, value);
1610        }
1611      }
1612    },
1613
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';
1620
1621      for (var i = 0; i < columns.length; ++i) {
1622        var key = columns[i];
1623        var td = addNode(tr, 'td');
1624
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        }
1632
1633        var aggregator = aggregates[key];
1634        if (aggregator)
1635          td.innerText = aggregator.getValueAsText();
1636      }
1637    },
1638
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;
1646
1647      var tr = addNode(tbody, 'tr');
1648      tr.className = 'truncation-row';
1649      var td = addNode(tr, 'td');
1650      td.colSpan = numColumns;
1651
1652      addText(td, numRows + ' rows');
1653      if (numHiddenRows > 0) {
1654        var s = addNode(td, 'span', ' (' + numHiddenRows + ' hidden) ');
1655        s.style.color = 'red';
1656      }
1657
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      }
1667
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    },
1675
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);
1682
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);
1689
1690      // Update the settings with the new limit.
1691      settings.limit = newLimit;
1692
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    },
1697
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    },
1718
1719    init_: function() {
1720      this.snapshots_ = [];
1721
1722      // Start fetching the data from the browser; this will be our snapshot #0.
1723      this.takeSnapshot_();
1724
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_ = [];
1742
1743      this.groupDisplaySettings_ = {};
1744
1745      this.fillSelectionCheckboxes_($(COLUMN_TOGGLES_CONTAINER_ID));
1746      this.fillMergeCheckboxes_($(COLUMN_MERGE_TOGGLES_CONTAINER_ID));
1747
1748      $(FILTER_SEARCH_ID).onsearch = this.onChangedFilter_.bind(this);
1749
1750      this.currentSortKeys_ = INITIAL_SORT_KEYS.slice(0);
1751      this.currentGroupingKeys_ = INITIAL_GROUP_KEYS.slice(0);
1752
1753      this.fillGroupingDropdowns_();
1754      this.fillSortingDropdowns_();
1755
1756      $(EDIT_COLUMNS_LINK_ID).onclick =
1757          toggleNodeDisplay.bind(null, $(EDIT_COLUMNS_ROW));
1758
1759      $(TOGGLE_SNAPSHOTS_LINK_ID).onclick =
1760          toggleNodeDisplay.bind(null, $(SNAPSHOTS_ROW));
1761
1762      $(MERGE_SIMILAR_THREADS_CHECKBOX_ID).onchange =
1763          this.onMergeSimilarThreadsCheckboxChanged_.bind(this);
1764
1765      $(RESET_DATA_LINK_ID).onclick =
1766          g_browserBridge.sendResetData.bind(g_browserBridge);
1767
1768      $(TAKE_SNAPSHOT_BUTTON_ID).onclick = this.takeSnapshot_.bind(this);
1769
1770      $(SAVE_SNAPSHOTS_BUTTON_ID).onclick = this.saveSnapshots_.bind(this);
1771      $(SNAPSHOT_FILE_LOADER_ID).onchange = this.loadFileChanged_.bind(this);
1772    },
1773
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()});
1778
1779      // Update the UI to reflect the new snapshot.
1780      this.addSnapshotToList_(this.snapshots_.length - 1);
1781
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    },
1786
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      }
1794
1795      var dump = {
1796        'userAgent': navigator.userAgent,
1797        'version': 1,
1798        'snapshots': snapshots
1799      };
1800
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    },
1808
1809    loadFileChanged_: function() {
1810      this.loadSnapshots_($(SNAPSHOT_FILE_LOADER_ID).files[0]);
1811    },
1812
1813    loadSnapshots_: function(file) {
1814      if (file) {
1815        var fileReader = new FileReader();
1816
1817        fileReader.onload = this.onLoadSnapshotsFile_.bind(this, file);
1818        fileReader.onerror = this.onLoadSnapshotsFileError_.bind(this, file);
1819
1820        fileReader.readAsText(file);
1821      }
1822    },
1823
1824    onLoadSnapshotsFile_: function(file, event) {
1825      try {
1826        var parsed = null;
1827        parsed = JSON.parse(event.target.result);
1828
1829        if (parsed.version != 1) {
1830          throw new Error('Unrecognized version: ' + parsed.version);
1831        }
1832
1833        if (parsed.snapshots.length < 1) {
1834          throw new Error('File contains no data');
1835        }
1836
1837        this.displayLoadedFile_(file, parsed);
1838        this.hideFileLoadError_();
1839      } catch (error) {
1840        this.displayFileLoadError_('File load failure: ' + error.message);
1841      }
1842    },
1843
1844    clearExistingSnapshots_: function() {
1845      var tbody = $('snapshots-tbody');
1846      this.snapshots_ = [];
1847      tbody.innerHTML = '';
1848      this.updateMergedDataSoon_();
1849    },
1850
1851    displayLoadedFile_: function(file, content) {
1852      this.clearExistingSnapshots_();
1853      $(TAKE_SNAPSHOT_BUTTON_ID).disabled = true;
1854      $(SAVE_SNAPSHOTS_BUTTON_ID).disabled = true;
1855
1856      if (content.snapshots.length > 1) {
1857        setNodeDisplay($(SNAPSHOTS_ROW), true);
1858      }
1859
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    },
1872
1873    onLoadSnapshotsFileError_: function(file, filedata) {
1874      this.displayFileLoadError_('Error loading ' + file.name);
1875    },
1876
1877    displayFileLoadError_: function(message) {
1878      $(LOAD_ERROR_ID).textContent = message;
1879      $(LOAD_ERROR_ID).hidden = false;
1880    },
1881
1882    hideFileLoadError_: function() {
1883      $(LOAD_ERROR_ID).textContent = '';
1884      $(LOAD_ERROR_ID).hidden = true;
1885    },
1886
1887    getSnapshotCheckbox_: function(i) {
1888      return $(this.getSnapshotCheckboxId_(i));
1889    },
1890
1891    getSnapshotCheckboxId_: function(i) {
1892      return 'snapshotCheckbox-' + i;
1893    },
1894
1895    addSnapshotToList_: function(i) {
1896      var tbody = $('snapshots-tbody');
1897
1898      var tr = addNode(tbody, 'tr');
1899
1900      var id = this.getSnapshotCheckboxId_(i);
1901
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);
1908
1909      addNode(tr, 'td', '#' + i);
1910
1911      var labelCell = addNode(tr, 'td');
1912      var l = addNode(labelCell, 'label');
1913
1914      var dateString = new Date(this.snapshots_[i].time).toLocaleString();
1915      addText(l, dateString);
1916      l.htmlFor = id;
1917
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    },
1925
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    },
1933
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();
1938
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      }
1949
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;
1954
1955      this.updateSnapshotCheckboxStyling_();
1956      this.updateSnapshotSelectionSummaryDiv_();
1957
1958      // Recompute mergedData_ (since it is derived from selected snapshots).
1959      this.updateMergedData_();
1960    },
1961
1962    fillSelectionCheckboxes_: function(parent) {
1963      this.selectionCheckboxes_ = {};
1964
1965      var onChangeFunc = this.onSelectCheckboxChanged_.bind(this);
1966
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      }
1975
1976      for (var i = 0; i < INITIALLY_HIDDEN_KEYS.length; ++i) {
1977        this.selectionCheckboxes_[INITIALLY_HIDDEN_KEYS[i]].checked = false;
1978      }
1979    },
1980
1981    getSelectionColumns_: function() {
1982      return getKeysForCheckedBoxes(this.selectionCheckboxes_);
1983    },
1984
1985    getMergeColumns_: function() {
1986      return getKeysForCheckedBoxes(this.mergeCheckboxes_);
1987    },
1988
1989    shouldMergeSimilarThreads_: function() {
1990      return $(MERGE_SIMILAR_THREADS_CHECKBOX_ID).checked;
1991    },
1992
1993    fillMergeCheckboxes_: function(parent) {
1994      this.mergeCheckboxes_ = {};
1995
1996      var onChangeFunc = this.onMergeCheckboxChanged_.bind(this);
1997
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      }
2005
2006      for (var i = 0; i < INITIALLY_MERGED_KEYS.length; ++i) {
2007        this.mergeCheckboxes_[INITIALLY_MERGED_KEYS[i]].checked = true;
2008      }
2009    },
2010
2011    fillGroupingDropdowns_: function() {
2012      var parent = $(GROUP_BY_CONTAINER_ID);
2013      parent.innerHTML = '';
2014
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);
2019
2020        addOptionsForGroupingSelect(select);
2021
2022        if (i < this.currentGroupingKeys_.length) {
2023          var key = this.currentGroupingKeys_[i];
2024          setSelectedOptionByValue(select, key);
2025        }
2026      }
2027    },
2028
2029    fillSortingDropdowns_: function() {
2030      var parent = $(SORT_BY_CONTAINER_ID);
2031      parent.innerHTML = '';
2032
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);
2037
2038        addOptionsForSortingSelect(select);
2039
2040        if (i < this.currentSortKeys_.length) {
2041          var key = this.currentSortKeys_[i];
2042          setSelectedOptionByValue(select, key);
2043        }
2044      }
2045    },
2046
2047    onChangedGrouping_: function(select, i) {
2048      updateKeyListFromDropdown(this.currentGroupingKeys_, i, select);
2049      this.fillGroupingDropdowns_();
2050      this.updateGroupedData_();
2051    },
2052
2053    onChangedSorting_: function(select, i) {
2054      updateKeyListFromDropdown(this.currentSortKeys_, i, select);
2055      this.fillSortingDropdowns_();
2056      this.sortGroupedData_();
2057    },
2058
2059    onSelectCheckboxChanged_: function() {
2060      this.redrawData_();
2061    },
2062
2063    onMergeCheckboxChanged_: function() {
2064      this.updateMergedData_();
2065    },
2066
2067    onMergeSimilarThreadsCheckboxChanged_: function() {
2068      this.updateMergedData_();
2069    },
2070
2071    onChangedFilter_: function() {
2072      this.updateFilteredData_();
2073    },
2074
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);
2088
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      }
2100
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      }
2119
2120      this.fillSortingDropdowns_();
2121      this.sortGroupedData_();
2122    },
2123
2124    getSortingFunction_: function() {
2125      var sortKeys = this.currentSortKeys_.slice(0);
2126
2127      // Eliminate the empty string keys (which means they were unspecified).
2128      deleteValuesFromArray(sortKeys, ['']);
2129
2130      // If no sort is specified, use our default sort.
2131      if (sortKeys.length == 0)
2132        sortKeys = [DEFAULT_SORT_KEYS];
2133
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;
2138
2139          var propA = a[key];
2140          var propB = b[key];
2141
2142          var comparison = compareValuesForKey(key, propA, propB);
2143          comparison *= factor;  // Possibly reverse the ordering.
2144
2145          if (comparison != 0)
2146            return comparison;
2147        }
2148
2149        // Tie breaker.
2150        return simpleCompare(JSON.stringify(a), JSON.stringify(b));
2151      };
2152    },
2153
2154    getGroupSortingFunction_: function() {
2155      return function(a, b) {
2156        var groupKey1 = JSON.parse(a);
2157        var groupKey2 = JSON.parse(b);
2158
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);
2164
2165          if (comparison != 0)
2166            return comparison;
2167        }
2168
2169        // Tie breaker.
2170        return simpleCompare(a, b);
2171      };
2172    },
2173
2174    getFilterFunction_: function() {
2175      var searchStr = $(FILTER_SEARCH_ID).value;
2176
2177      // Normalize the search expression.
2178      searchStr = trimWhitespace(searchStr);
2179      searchStr = searchStr.toLowerCase();
2180
2181      return function(x) {
2182        // Match everything when there was no filter.
2183        if (searchStr == '')
2184          return true;
2185
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        }
2192
2193        return false;
2194      };
2195    },
2196
2197    getGroupingFunction_: function() {
2198      var groupings = this.currentGroupingKeys_.slice(0);
2199
2200      // Eliminate the empty string groupings (which means they were
2201      // unspecified).
2202      deleteValuesFromArray(groupings, ['']);
2203
2204      // Eliminate duplicate primary/secondary group by directives, since they
2205      // are redundant.
2206      deleteDuplicateStringsFromArray(groupings);
2207
2208      return function(e) {
2209        var groupKey = [];
2210
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        }
2216
2217        return JSON.stringify(groupKey);
2218      };
2219    },
2220  };
2221
2222  return MainView;
2223})();
2224