Home | History | Annotate | Download | only in profiler
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 var g_browserBridge;
      6 var 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  */
     16 function onLoad() {
     17   g_browserBridge = new BrowserBridge();
     18   g_mainView = new MainView();
     19 }
     20 
     21 document.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  */
     27 var 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  */
     67 var 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