home *** CD-ROM | disk | FTP | other *** search
/ Freelog 115 / FreelogNo115-MaiJuin2013.iso / Internet / AvantBrowser / asetup.exe / _data / webkit / chrome.dll / 0 / BINDATA / 592 < prev    next >
Encoding:
Text File  |  2013-04-03  |  68.7 KB  |  2,222 lines

  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_IFRAME_ID = 'download-iframe';
  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 blobBuilder = new Blob([dumpText, 'native'], {type: 'octet/stream'});
  1803.       var blobUrl = window.webkitURL.createObjectURL(textBlob);
  1804.       $(DOWNLOAD_IFRAME_ID).src = blobUrl;
  1805.     },
  1806.  
  1807.     loadFileChanged_: function() {
  1808.       this.loadSnapshots_($(SNAPSHOT_FILE_LOADER_ID).files[0]);
  1809.     },
  1810.  
  1811.     loadSnapshots_: function(file) {
  1812.       if (file) {
  1813.         var fileReader = new FileReader();
  1814.  
  1815.         fileReader.onload = this.onLoadSnapshotsFile_.bind(this, file);
  1816.         fileReader.onerror = this.onLoadSnapshotsFileError_.bind(this, file);
  1817.  
  1818.         fileReader.readAsText(file);
  1819.       }
  1820.     },
  1821.  
  1822.     onLoadSnapshotsFile_: function(file, event) {
  1823.       try {
  1824.         var parsed = null;
  1825.         parsed = JSON.parse(event.target.result);
  1826.  
  1827.         if (parsed.version != 1) {
  1828.           throw new Error('Unrecognized version: ' + parsed.version);
  1829.         }
  1830.  
  1831.         if (parsed.snapshots.length < 1) {
  1832.           throw new Error('File contains no data');
  1833.         }
  1834.  
  1835.         this.displayLoadedFile_(file, parsed);
  1836.         this.hideFileLoadError_();
  1837.       } catch (error) {
  1838.         this.displayFileLoadError_('File load failure: ' + error.message);
  1839.       }
  1840.     },
  1841.  
  1842.     clearExistingSnapshots_: function() {
  1843.       var tbody = $('snapshots-tbody');
  1844.       this.snapshots_ = [];
  1845.       tbody.innerHTML = '';
  1846.       this.updateMergedDataSoon_();
  1847.     },
  1848.  
  1849.     displayLoadedFile_: function(file, content) {
  1850.       this.clearExistingSnapshots_();
  1851.       $(TAKE_SNAPSHOT_BUTTON_ID).disabled = true;
  1852.       $(SAVE_SNAPSHOTS_BUTTON_ID).disabled = true;
  1853.  
  1854.       if (content.snapshots.length > 1) {
  1855.         setNodeDisplay($(SNAPSHOTS_ROW), true);
  1856.       }
  1857.  
  1858.       for (var i = 0; i < content.snapshots.length; ++i) {
  1859.         var snapshot = content.snapshots[i];
  1860.         this.snapshots_.push({flatData: [], origData: [],
  1861.                               time: snapshot.timestamp * 1000});
  1862.         this.addSnapshotToList_(this.snapshots_.length - 1);
  1863.         var snapshotData = snapshot.data;
  1864.         for (var j = 0; j < snapshotData.length; ++j) {
  1865.           this.addDataToSnapshot(snapshotData[j]);
  1866.         }
  1867.       }
  1868.       this.redrawData_();
  1869.     },
  1870.  
  1871.     onLoadSnapshotsFileError_: function(file, filedata) {
  1872.       this.displayFileLoadError_('Error loading ' + file.name);
  1873.     },
  1874.  
  1875.     displayFileLoadError_: function(message) {
  1876.       $(LOAD_ERROR_ID).textContent = message;
  1877.       $(LOAD_ERROR_ID).hidden = false;
  1878.     },
  1879.  
  1880.     hideFileLoadError_: function() {
  1881.       $(LOAD_ERROR_ID).textContent = '';
  1882.       $(LOAD_ERROR_ID).hidden = true;
  1883.     },
  1884.  
  1885.     getSnapshotCheckbox_: function(i) {
  1886.       return $(this.getSnapshotCheckboxId_(i));
  1887.     },
  1888.  
  1889.     getSnapshotCheckboxId_: function(i) {
  1890.       return 'snapshotCheckbox-' + i;
  1891.     },
  1892.  
  1893.     addSnapshotToList_: function(i) {
  1894.       var tbody = $('snapshots-tbody');
  1895.  
  1896.       var tr = addNode(tbody, 'tr');
  1897.  
  1898.       var id = this.getSnapshotCheckboxId_(i);
  1899.  
  1900.       var checkboxCell = addNode(tr, 'td');
  1901.       var checkbox = addNode(checkboxCell, 'input');
  1902.       checkbox.type = 'checkbox';
  1903.       checkbox.id = id;
  1904.       checkbox.__index = i;
  1905.       checkbox.onclick = this.onSnapshotCheckboxChanged_.bind(this);
  1906.  
  1907.       addNode(tr, 'td', '#' + i);
  1908.  
  1909.       var labelCell = addNode(tr, 'td');
  1910.       var l = addNode(labelCell, 'label');
  1911.  
  1912.       var dateString = new Date(this.snapshots_[i].time).toLocaleString();
  1913.       addText(l, dateString);
  1914.       l.htmlFor = id;
  1915.  
  1916.       // If we are on snapshot 0, make it the default.
  1917.       if (i == 0) {
  1918.         checkbox.checked = true;
  1919.         checkbox.__time = getTimeMillis();
  1920.         this.updateSnapshotCheckboxStyling_();
  1921.       }
  1922.     },
  1923.  
  1924.     updateSnapshotCheckboxStyling_: function() {
  1925.       for (var i = 0; i < this.snapshots_.length; ++i) {
  1926.         var checkbox = this.getSnapshotCheckbox_(i);
  1927.         checkbox.parentNode.parentNode.className =
  1928.             checkbox.checked ? 'selected_snapshot' : '';
  1929.       }
  1930.     },
  1931.  
  1932.     onSnapshotCheckboxChanged_: function(event) {
  1933.       // Keep track of when we clicked this box (for when we need to uncheck
  1934.       // older boxes).
  1935.       event.target.__time = getTimeMillis();
  1936.  
  1937.       // Find all the checked boxes. Either 1 or 2 can be checked. If a third
  1938.       // was just checked, then uncheck one of the earlier ones so we only have
  1939.       // 2.
  1940.       var checked = this.getSelectedSnapshotBoxes_();
  1941.       checked.sort(function(a, b) { return b.__time - a.__time; });
  1942.       if (checked.length > 2) {
  1943.         for (var i = 2; i < checked.length; ++i)
  1944.           checked[i].checked = false;
  1945.         checked.length = 2;
  1946.       }
  1947.  
  1948.       // We should always have at least 1 selection. Prevent the user from
  1949.       // unselecting the final box.
  1950.       if (checked.length == 0)
  1951.         event.target.checked = true;
  1952.  
  1953.       this.updateSnapshotCheckboxStyling_();
  1954.       this.updateSnapshotSelectionSummaryDiv_();
  1955.  
  1956.       // Recompute mergedData_ (since it is derived from selected snapshots).
  1957.       this.updateMergedData_();
  1958.     },
  1959.  
  1960.     fillSelectionCheckboxes_: function(parent) {
  1961.       this.selectionCheckboxes_ = {};
  1962.  
  1963.       var onChangeFunc = this.onSelectCheckboxChanged_.bind(this);
  1964.  
  1965.       for (var i = 0; i < ALL_TABLE_COLUMNS.length; ++i) {
  1966.         var key = ALL_TABLE_COLUMNS[i];
  1967.         var checkbox = addLabeledCheckbox(parent, getNameForKey(key));
  1968.         checkbox.checked = true;
  1969.         checkbox.onchange = onChangeFunc;
  1970.         addText(parent, ' ');
  1971.         this.selectionCheckboxes_[key] = checkbox;
  1972.       }
  1973.  
  1974.       for (var i = 0; i < INITIALLY_HIDDEN_KEYS.length; ++i) {
  1975.         this.selectionCheckboxes_[INITIALLY_HIDDEN_KEYS[i]].checked = false;
  1976.       }
  1977.     },
  1978.  
  1979.     getSelectionColumns_: function() {
  1980.       return getKeysForCheckedBoxes(this.selectionCheckboxes_);
  1981.     },
  1982.  
  1983.     getMergeColumns_: function() {
  1984.       return getKeysForCheckedBoxes(this.mergeCheckboxes_);
  1985.     },
  1986.  
  1987.     shouldMergeSimilarThreads_: function() {
  1988.       return $(MERGE_SIMILAR_THREADS_CHECKBOX_ID).checked;
  1989.     },
  1990.  
  1991.     fillMergeCheckboxes_: function(parent) {
  1992.       this.mergeCheckboxes_ = {};
  1993.  
  1994.       var onChangeFunc = this.onMergeCheckboxChanged_.bind(this);
  1995.  
  1996.       for (var i = 0; i < MERGEABLE_KEYS.length; ++i) {
  1997.         var key = MERGEABLE_KEYS[i];
  1998.         var checkbox = addLabeledCheckbox(parent, getNameForKey(key));
  1999.         checkbox.onchange = onChangeFunc;
  2000.         addText(parent, ' ');
  2001.         this.mergeCheckboxes_[key] = checkbox;
  2002.       }
  2003.  
  2004.       for (var i = 0; i < INITIALLY_MERGED_KEYS.length; ++i) {
  2005.         this.mergeCheckboxes_[INITIALLY_MERGED_KEYS[i]].checked = true;
  2006.       }
  2007.     },
  2008.  
  2009.     fillGroupingDropdowns_: function() {
  2010.       var parent = $(GROUP_BY_CONTAINER_ID);
  2011.       parent.innerHTML = '';
  2012.  
  2013.       for (var i = 0; i <= this.currentGroupingKeys_.length; ++i) {
  2014.         // Add a dropdown.
  2015.         var select = addNode(parent, 'select');
  2016.         select.onchange = this.onChangedGrouping_.bind(this, select, i);
  2017.  
  2018.         addOptionsForGroupingSelect(select);
  2019.  
  2020.         if (i < this.currentGroupingKeys_.length) {
  2021.           var key = this.currentGroupingKeys_[i];
  2022.           setSelectedOptionByValue(select, key);
  2023.         }
  2024.       }
  2025.     },
  2026.  
  2027.     fillSortingDropdowns_: function() {
  2028.       var parent = $(SORT_BY_CONTAINER_ID);
  2029.       parent.innerHTML = '';
  2030.  
  2031.       for (var i = 0; i <= this.currentSortKeys_.length; ++i) {
  2032.         // Add a dropdown.
  2033.         var select = addNode(parent, 'select');
  2034.         select.onchange = this.onChangedSorting_.bind(this, select, i);
  2035.  
  2036.         addOptionsForSortingSelect(select);
  2037.  
  2038.         if (i < this.currentSortKeys_.length) {
  2039.           var key = this.currentSortKeys_[i];
  2040.           setSelectedOptionByValue(select, key);
  2041.         }
  2042.       }
  2043.     },
  2044.  
  2045.     onChangedGrouping_: function(select, i) {
  2046.       updateKeyListFromDropdown(this.currentGroupingKeys_, i, select);
  2047.       this.fillGroupingDropdowns_();
  2048.       this.updateGroupedData_();
  2049.     },
  2050.  
  2051.     onChangedSorting_: function(select, i) {
  2052.       updateKeyListFromDropdown(this.currentSortKeys_, i, select);
  2053.       this.fillSortingDropdowns_();
  2054.       this.sortGroupedData_();
  2055.     },
  2056.  
  2057.     onSelectCheckboxChanged_: function() {
  2058.       this.redrawData_();
  2059.     },
  2060.  
  2061.     onMergeCheckboxChanged_: function() {
  2062.       this.updateMergedData_();
  2063.     },
  2064.  
  2065.     onMergeSimilarThreadsCheckboxChanged_: function() {
  2066.       this.updateMergedData_();
  2067.     },
  2068.  
  2069.     onChangedFilter_: function() {
  2070.       this.updateFilteredData_();
  2071.     },
  2072.  
  2073.     /**
  2074.      * When left-clicking a column, change the primary sort order to that
  2075.      * column. If we were already sorted on that column then reverse the order.
  2076.      *
  2077.      * When alt-clicking, add a secondary sort column. Similarly, if
  2078.      * alt-clicking a column which was already being sorted on, reverse its
  2079.      * order.
  2080.      */
  2081.     onClickColumn_: function(key, event) {
  2082.       // If this property wants to start off in descending order rather then
  2083.       // ascending, flip it.
  2084.       if (KEY_PROPERTIES[key].sortDescending)
  2085.         key = reverseSortKey(key);
  2086.  
  2087.       // Scan through our sort order and see if we are already sorted on this
  2088.       // key. If so, reverse that sort ordering.
  2089.       var found_i = -1;
  2090.       for (var i = 0; i < this.currentSortKeys_.length; ++i) {
  2091.         var curKey = this.currentSortKeys_[i];
  2092.         if (sortKeysMatch(curKey, key)) {
  2093.           this.currentSortKeys_[i] = reverseSortKey(curKey);
  2094.           found_i = i;
  2095.           break;
  2096.         }
  2097.       }
  2098.  
  2099.       if (event.altKey) {
  2100.         if (found_i == -1) {
  2101.           // If we weren't already sorted on the column that was alt-clicked,
  2102.           // then add it to our sort.
  2103.           this.currentSortKeys_.push(key);
  2104.         }
  2105.       } else {
  2106.         if (found_i != 0 ||
  2107.             !sortKeysMatch(this.currentSortKeys_[found_i], key)) {
  2108.           // If the column we left-clicked wasn't already our primary column,
  2109.           // make it so.
  2110.           this.currentSortKeys_ = [key];
  2111.         } else {
  2112.           // If the column we left-clicked was already our primary column (and
  2113.           // we just reversed it), remove any secondary sorts.
  2114.           this.currentSortKeys_.length = 1;
  2115.         }
  2116.       }
  2117.  
  2118.       this.fillSortingDropdowns_();
  2119.       this.sortGroupedData_();
  2120.     },
  2121.  
  2122.     getSortingFunction_: function() {
  2123.       var sortKeys = this.currentSortKeys_.slice(0);
  2124.  
  2125.       // Eliminate the empty string keys (which means they were unspecified).
  2126.       deleteValuesFromArray(sortKeys, ['']);
  2127.  
  2128.       // If no sort is specified, use our default sort.
  2129.       if (sortKeys.length == 0)
  2130.         sortKeys = [DEFAULT_SORT_KEYS];
  2131.  
  2132.       return function(a, b) {
  2133.         for (var i = 0; i < sortKeys.length; ++i) {
  2134.           var key = Math.abs(sortKeys[i]);
  2135.           var factor = sortKeys[i] < 0 ? -1 : 1;
  2136.  
  2137.           var propA = a[key];
  2138.           var propB = b[key];
  2139.  
  2140.           var comparison = compareValuesForKey(key, propA, propB);
  2141.           comparison *= factor;  // Possibly reverse the ordering.
  2142.  
  2143.           if (comparison != 0)
  2144.             return comparison;
  2145.         }
  2146.  
  2147.         // Tie breaker.
  2148.         return simpleCompare(JSON.stringify(a), JSON.stringify(b));
  2149.       };
  2150.     },
  2151.  
  2152.     getGroupSortingFunction_: function() {
  2153.       return function(a, b) {
  2154.         var groupKey1 = JSON.parse(a);
  2155.         var groupKey2 = JSON.parse(b);
  2156.  
  2157.         for (var i = 0; i < groupKey1.length; ++i) {
  2158.           var comparison = compareValuesForKey(
  2159.               groupKey1[i].key,
  2160.               groupKey1[i].value,
  2161.               groupKey2[i].value);
  2162.  
  2163.           if (comparison != 0)
  2164.             return comparison;
  2165.         }
  2166.  
  2167.         // Tie breaker.
  2168.         return simpleCompare(a, b);
  2169.       };
  2170.     },
  2171.  
  2172.     getFilterFunction_: function() {
  2173.       var searchStr = $(FILTER_SEARCH_ID).value;
  2174.  
  2175.       // Normalize the search expression.
  2176.       searchStr = trimWhitespace(searchStr);
  2177.       searchStr = searchStr.toLowerCase();
  2178.  
  2179.       return function(x) {
  2180.         // Match everything when there was no filter.
  2181.         if (searchStr == '')
  2182.           return true;
  2183.  
  2184.         // Treat the search text as a LOWERCASE substring search.
  2185.         for (var k = BEGIN_KEY; k < END_KEY; ++k) {
  2186.           var propertyText = getTextValueForProperty(k, x[k]);
  2187.           if (propertyText.toLowerCase().indexOf(searchStr) != -1)
  2188.             return true;
  2189.         }
  2190.  
  2191.         return false;
  2192.       };
  2193.     },
  2194.  
  2195.     getGroupingFunction_: function() {
  2196.       var groupings = this.currentGroupingKeys_.slice(0);
  2197.  
  2198.       // Eliminate the empty string groupings (which means they were
  2199.       // unspecified).
  2200.       deleteValuesFromArray(groupings, ['']);
  2201.  
  2202.       // Eliminate duplicate primary/secondary group by directives, since they
  2203.       // are redundant.
  2204.       deleteDuplicateStringsFromArray(groupings);
  2205.  
  2206.       return function(e) {
  2207.         var groupKey = [];
  2208.  
  2209.         for (var i = 0; i < groupings.length; ++i) {
  2210.           var entry = {key: groupings[i],
  2211.                        value: e[groupings[i]]};
  2212.           groupKey.push(entry);
  2213.         }
  2214.  
  2215.         return JSON.stringify(groupKey);
  2216.       };
  2217.     },
  2218.   };
  2219.  
  2220.   return MainView;
  2221. })();
  2222.