Home | History | Annotate | Download | only in performance_monitor
      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 cr.define('performance_monitor', function() {
      6   'use strict';
      7 
      8   /**
      9    * Map of available time resolutions.
     10    * @type {Object.<string, PerformanceMonitor.TimeResolution>}
     11    * @private
     12    */
     13   var TimeResolutions_ = {
     14     // Prior 15 min, resolution of 15 seconds.
     15     minutes: {id: 0, i18nKey: 'timeLastFifteenMinutes', timeSpan: 900 * 1000,
     16               pointResolution: 1000 * 15},
     17 
     18     // Prior hour, resolution of 1 minute.
     19     // Labels at 5 point (5 min) intervals.
     20     hour: {id: 1, i18nKey: 'timeLastHour', timeSpan: 3600 * 1000,
     21            pointResolution: 1000 * 60},
     22 
     23     // Prior day, resolution of 24 min.
     24     // Labels at 5 point (2 hour) intervals.
     25     day: {id: 2, i18nKey: 'timeLastDay', timeSpan: 24 * 3600 * 1000,
     26           pointResolution: 1000 * 60 * 24},
     27 
     28     // Prior week, resolution of 2.8 hours (168 min).
     29     // Labels at ~8.5 point (daily) intervals.
     30     week: {id: 3, i18nKey: 'timeLastWeek', timeSpan: 7 * 24 * 3600 * 1000,
     31            pointResolution: 1000 * 60 * 168},
     32 
     33     // Prior month (30 days), resolution of 12 hours.
     34     // Labels at 14 point (weekly) intervals.
     35     month: {id: 4, i18nKey: 'timeLastMonth', timeSpan: 30 * 24 * 3600 * 1000,
     36             pointResolution: 1000 * 3600 * 12},
     37 
     38     // Prior quarter (90 days), resolution of 36 hours.
     39     // Labels at ~9.3 point (fortnightly) intervals.
     40     quarter: {id: 5, i18nKey: 'timeLastQuarter',
     41               timeSpan: 90 * 24 * 3600 * 1000,
     42               pointResolution: 1000 * 3600 * 36},
     43   };
     44 
     45   /**
     46    * Map of available date formats in Flot-style format strings.
     47    * @type {Object.<string, string>}
     48    * @private
     49    */
     50   var TimeFormats_ = {
     51     time: '%h:%M %p',
     52     monthDayTime: '%b %d<br/>%h:%M %p',
     53     monthDay: '%b %d',
     54     yearMonthDay: '%y %b %d',
     55   };
     56 
     57   /*
     58    * Table of colors to use for metrics and events. Basically boxing the
     59    * colorwheel, but leaving out yellows and fully saturated colors.
     60    * @type {Array.<string>}
     61    * @private
     62    */
     63   var ColorTable_ = [
     64     'rgb(255, 128, 128)', 'rgb(128, 255, 128)', 'rgb(128, 128, 255)',
     65     'rgb(128, 255, 255)', 'rgb(255, 128, 255)', // No bright yellow
     66     'rgb(255,  64,  64)', 'rgb( 64, 255,  64)', 'rgb( 64,  64, 255)',
     67     'rgb( 64, 255, 255)', 'rgb(255,  64, 255)', // No medium yellow either
     68     'rgb(128,  64,  64)', 'rgb( 64, 128,  64)', 'rgb( 64,  64, 128)',
     69     'rgb( 64, 128, 128)', 'rgb(128,  64, 128)', 'rgb(128, 128,  64)'
     70   ];
     71 
     72   /*
     73    * Offset, in ms, by which to subtract to convert GMT to local time.
     74    * @type {number}
     75    * @private
     76    */
     77   var timezoneOffset_ = new Date().getTimezoneOffset() * 60000;
     78 
     79   /*
     80    * Additional range multiplier to ensure that points don't hit the top of
     81    * the graph.
     82    * @type {number}
     83    * @private
     84    */
     85   var yAxisMargin_ = 1.05;
     86 
     87   /*
     88    * Number of time resolution periods to wait between automated update of
     89    * graphs.
     90    * @type {number}
     91    * @private
     92    */
     93   var intervalMultiple_ = 2;
     94 
     95   /*
     96    * Number of milliseconds to wait before deciding that the most recent
     97    * resize event is not going to be followed immediately by another, and
     98    * thus needs handling.
     99    * @type {number}
    100    * @private
    101    */
    102   var resizeDelay_ = 500;
    103 
    104   /*
    105    * The value of the 'No Aggregation' option enum (AGGREGATION_METHOD_NONE) on
    106    * the C++ side. We use this to warn the user that selecting this aggregation
    107    * option will be slow.
    108    */
    109   var aggregationMethodNone = 0;
    110 
    111   /*
    112    * The value of the default aggregation option, 'Median Aggregation'
    113    * (AGGREGATION_METHOD_MEDIAN), on the C++ side.
    114    */
    115   var aggregationMethodMedian = 1;
    116 
    117   /** @constructor */
    118   function PerformanceMonitor() {
    119     this.__proto__ = PerformanceMonitor.prototype;
    120 
    121     /** Information regarding a certain time resolution option, including an
    122      *  enumerative id, a readable name, the timespan in milliseconds prior to
    123      *  |now|, data point resolution in milliseconds, and time-label frequency
    124      *  in data points per label.
    125      *  @typedef {{
    126      *    id: number,
    127      *    name: string,
    128      *    timeSpan: number,
    129      *    pointResolution: number,
    130      *    labelEvery: number,
    131      *  }}
    132      */
    133     PerformanceMonitor.TimeResolution;
    134 
    135     /**
    136      * Detailed information on a metric in the UI. |metricId| is a unique
    137      * identifying number for the metric, provided by the webui, and assumed to
    138      * be densely populated. |description| is a localized string description
    139      * suitable for mouseover on the metric. |category| corresponds to a
    140      * category object to which the metric belongs (see |metricCategoryMap_|).
    141      * |color| is the color in which the metric is displayed on the graphs.
    142      * |maxValue| is a value by which to scale the y-axis, in order to avoid
    143      * constant resizing to fit the present data. |checkbox| is the HTML element
    144      * for the checkbox which toggles the metric's display. |enabled| indicates
    145      * whether or not the metric is being actively displayed. |data| is the
    146      * collection of data for the metric.
    147      *
    148      * For |data|, the inner-most array represents a point in a pair of numbers,
    149      * representing time and value (this will always be of length 2). The
    150      * array above is the collection of points within a series, which is an
    151      * interval for which PerformanceMonitor was active. The outer-most array
    152      * is the collection of these series.
    153      *
    154      * @typedef {{
    155      *   metricId: number,
    156      *   description: string,
    157      *   category: !Object,
    158      *   color: string,
    159      *   maxValue: number,
    160      *   checkbox: HTMLElement,
    161      *   enabled: boolean,
    162      *   data: ?Array.<Array<Array<number> > >
    163      * }}
    164      */
    165     PerformanceMonitor.MetricDetails;
    166 
    167     /**
    168      * Similar data for events as for metrics, though no y-axis info is needed
    169      * since events are simply labeled markers at X locations.
    170      *
    171      * The |data| field follows a special rule not describable in
    172      * JSDoc: Aside from the |time| key, each event type has varying other
    173      * properties, with unknown key names, which properties must still be
    174      * displayed. Such properties always have value of form
    175      * {label: 'some label', value: 'some value'}, with label and value
    176      * internationalized.
    177      *
    178      * @typedef {{
    179      *   eventId: number,
    180      *   name: string,
    181      *   popupTitle: string,
    182      *   description: string,
    183      *   color: string,
    184      *   checkbox: HTMLElement,
    185      *   enabled: boolean
    186      *   data: ?Array.<{time: number}>
    187      * }}
    188      */
    189     PerformanceMonitor.EventDetails;
    190 
    191     /**
    192      * The collection of divs that compose a chart on the UI, plus the metricIds
    193      * of any metric which should be shown on the chart (whether the metric is
    194      * enabled or not). The |mainDiv| is the full element, under which all other
    195      * divs are nested. The |grid| is the div into which the |plot| (which is
    196      * the core of the graph, including the axis, gridlines, dataseries, etc)
    197      * goes. The |yaxisLabel| is nested under the mainDiv, and shows the units
    198      * for the chart.
    199      *
    200      * @typedef {{
    201      *   mainDiv: HTMLDivElement,
    202      *   grid: HTMLDivElement,
    203      *   plot: HTMLDivElement,
    204      *   yaxisLabel: HTMLDivElement,
    205      *   metricIds: ?Array.<number>
    206      */
    207     PerformanceMonitor.Chart;
    208 
    209     /**
    210      * The time range which we are currently viewing, with the start and end of
    211      * the range, the TimeResolution, and an appropriate for display (this
    212      * format is the string structure which Flot expects for its setting).
    213      * @typedef {{
    214      * @type {{
    215      *   start: number,
    216      *   end: number,
    217      *   resolution: PerformanceMonitor.TimeResolution
    218      *   format: string
    219      * }}
    220      * @private
    221      */
    222     this.range_ = { 'start': 0, 'end': 0, 'resolution': undefined };
    223 
    224     /**
    225      * The map containing the available TimeResolutions and the radio button to
    226      * which each corresponds. The key is the id field from the TimeResolution
    227      * object.
    228      * @type {Object.<string, {
    229      *   option: PerformanceMonitor.TimeResolution,
    230      *   element: HTMLElement
    231      * }>}
    232      * @private
    233      */
    234     this.timeResolutionRadioMap_ = {};
    235 
    236     /**
    237      * The map containing the available Aggregation Methods and the radio button
    238      * to which each corresponds. The different methods are retrieved from the
    239      * WebUI, and the information about the method is stored in the 'option'
    240      * field. The key to the map is the id of the aggregation method.
    241      *
    242      * @type {Object.<string, {
    243      *   option: {
    244      *     id: number,
    245      *     name: string,
    246      *     description: string,
    247      *   },
    248      *   element: HTMLElement
    249      * }>}
    250      * @private
    251      */
    252     this.aggregationRadioMap_ = {};
    253 
    254     /**
    255      * Metrics fall into categories that have common units and thus may
    256      * share a common graph, or share y-axes within a multi-y-axis graph.
    257      * Each category has a unique identifying metricCategoryId; a localized
    258      * name, mouseover description, and unit; and an array of all the metrics
    259      * which are in this category. The key is |metricCategoryId|.
    260      *
    261      * @type {Object.<string, {
    262      *   metricCategoryId: number,
    263      *   name: string,
    264      *   description: string,
    265      *   unit: string,
    266      *   details: Array.<{!PerformanceMonitor.MetricDetails}>,
    267      * }>}
    268      * @private
    269      */
    270     this.metricCategoryMap_ = {};
    271 
    272     /**
    273      * Comprehensive map from metricId to MetricDetails.
    274      * @type {Object.<string, {PerformanceMonitor.MetricDetails}>}
    275      * @private
    276      */
    277     this.metricDetailsMap_ = {};
    278 
    279     /**
    280      * Events fall into categories just like metrics, above. This category
    281      * grouping is not as important as that for metrics, since events
    282      * needn't share maxima, y-axes, nor units, and since events appear on
    283      * all charts. But grouping of event categories in the event-selection
    284      * UI is still useful. The key is the id of the event category.
    285      *
    286      * @type {Object.<string, {
    287      *   eventCategoryId: number,
    288      *   name: string,
    289      *   description: string,
    290      *   details: !Array.<!PerformanceMonitor.EventDetails>,
    291      * }>}
    292      * @private
    293      */
    294     this.eventCategoryMap_ = {};
    295 
    296     /**
    297      * Comprehensive map from eventId to EventDetails.
    298      * @type {Object.<string, {PerformanceMonitor.EventDetails}>}
    299      * @private
    300      */
    301     this.eventDetailsMap_ = {};
    302 
    303     /**
    304      * Time periods in which the browser was active and collecting metrics
    305      * and events.
    306      * @type {!Array.<{start: number, end: number}>}
    307      * @private
    308      */
    309     this.intervals_ = [];
    310 
    311     /**
    312      * The record of all the warnings which are currently active (or empty if no
    313      * warnings are being displayed).
    314      * @type {!Array.<string>}
    315      * @private
    316      */
    317     this.activeWarnings_ = [];
    318 
    319     /**
    320      * Handle of timer interval function used to update charts
    321      * @type {Object}
    322      * @private
    323      */
    324     this.updateTimer_ = null;
    325 
    326     /**
    327      * Handle of timer interval function used to check for resizes. Nonnull
    328      * only when resize events are coming steadily.
    329      * @type {Object}
    330      * @private
    331      */
    332     this.resizeTimer_ = null;
    333 
    334     /**
    335      * The status of all calls for data, stored in order to keep track of the
    336      * internal state. This stores an attribute for each type of repeated data
    337      * call (for now, only metrics and events), which will be true if we are
    338      * awaiting data and false otherwise.
    339      * @type {Object.<string, boolean>}
    340      * @private
    341      */
    342     this.awaitingDataCalls_ = {};
    343 
    344     /**
    345      * The progress into the initialization process. This must be stored, since
    346      * certain tasks must be performed in a specific order which cannot be
    347      * statically determined. Mainly, we must not request any data until the
    348      * metrics, events, aggregation method, and time range have all been set.
    349      * This object contains an attribute for each stage of the initialization
    350      * process, which is set to true if the stage has been completed.
    351      * @type {Object.<string, boolean>}
    352      * @private
    353      */
    354     this.initProgress_ = { 'aggregation': false,
    355                            'events': false,
    356                            'metrics': false,
    357                            'timeRange': false };
    358 
    359     /**
    360      * All PerformanceMonitor.Chart objects available in the display, whether
    361      * hidden or visible.
    362      * @type {Array.<PerformanceMonitor.Chart>}
    363      * @private
    364      */
    365     this.charts_ = [];
    366 
    367     this.setupStaticControlPanelFeatures_();
    368     chrome.send('getFlagEnabled');
    369     chrome.send('getAggregationTypes');
    370     chrome.send('getEventTypes');
    371     chrome.send('getMetricTypes');
    372   }
    373 
    374   PerformanceMonitor.prototype = {
    375     /**
    376      * Display the appropriate warning at the top of the page.
    377      * @param {string} warningId the id of the HTML element with the warning
    378      *     to display; this does not include the '#'.
    379      */
    380     showWarning: function(warningId) {
    381       if (this.activeWarnings_.indexOf(warningId) != -1)
    382         return;
    383 
    384       if (this.activeWarnings_.length == 0)
    385         $('#warnings-box')[0].style.display = 'block';
    386       $('#' + warningId)[0].style.display = 'block';
    387       this.activeWarnings_.push(warningId);
    388     },
    389 
    390     /**
    391      * Hide the warning, and, if that was the only warning showing, the entire
    392      * warnings box.
    393      * @param {string} warningId the id of the HTML element with the warning
    394      *     to display; this does not include the '#'.
    395      */
    396     hideWarning: function(warningId) {
    397       var index = this.activeWarnings_.indexOf(warningId);
    398       if (index == -1)
    399         return;
    400       $('#' + warningId)[0].style.display = 'none';
    401       this.activeWarnings_.splice(index, 1);
    402 
    403       if (this.activeWarnings_.length == 0)
    404         $('#warnings-box')[0].style.display = 'none';
    405     },
    406 
    407     /**
    408      * Receive an indication of whether or not the kPerformanceMonitorGathering
    409      * flag has been enabled and, if not, warn the user of such.
    410      * @param {boolean} flagEnabled indicates whether or not the flag has been
    411      *     enabled.
    412      */
    413     getFlagEnabledCallback: function(flagEnabled) {
    414       if (!flagEnabled)
    415         this.showWarning('flag-not-enabled-warning');
    416     },
    417 
    418     /**
    419      * Return true if we are not awaiting any returning data calls, and false
    420      * otherwise.
    421      * @return {boolean} The value indicating whether or not we are actively
    422      *     fetching data.
    423      */
    424     fetchingData_: function() {
    425       return this.awaitingDataCalls_.metrics == true ||
    426              this.awaitingDataCalls_.events == true;
    427     },
    428 
    429     /**
    430      * Return true if the main steps of initialization prior to the first draw
    431      * are complete, and false otherwise.
    432      * @return {boolean} The value indicating whether or not the initialization
    433      *     process has finished.
    434      */
    435     isInitialized_: function() {
    436       return this.initProgress_.aggregation == true &&
    437              this.initProgress_.events == true &&
    438              this.initProgress_.metrics == true &&
    439              this.initProgress_.timeRange == true;
    440     },
    441 
    442     /**
    443      * Refresh all data areas.
    444      */
    445     refreshAll: function() {
    446       this.refreshMetrics();
    447       this.refreshEvents();
    448     },
    449 
    450     /**
    451      * Receive a list of all the aggregation methods. Populate
    452      * |this.aggregationRadioMap_| to reflect said list. Create the section of
    453      * radio buttons for the aggregation methods, and choose the first method
    454      * by default.
    455      * @param {Array<{
    456      *   id: number,
    457      *   name: string,
    458      *   description: string
    459      * }>} methods All aggregation methods needing radio buttons.
    460      */
    461     getAggregationTypesCallback: function(methods) {
    462       methods.forEach(function(method) {
    463         this.aggregationRadioMap_[method.id] = { 'option': method };
    464       }, this);
    465 
    466       this.setupRadioButtons_($('#choose-aggregation')[0],
    467                               this.aggregationRadioMap_,
    468                               this.setAggregationMethod,
    469                               aggregationMethodMedian,
    470                               'aggregation-methods');
    471       this.setAggregationMethod(aggregationMethodMedian);
    472       this.initProgress_.aggregation = true;
    473       if (this.isInitialized_())
    474         this.refreshAll();
    475     },
    476 
    477     /**
    478      * Receive a list of all metric categories, each with its corresponding
    479      * list of metric details. Populate |this.metricCategoryMap_| and
    480      * |this.metricDetailsMap_| to reflect said list. Reconfigure the
    481      * checkbox set for metric selection.
    482      * @param {Array.<{
    483      *   metricCategoryId: number,
    484      *   name: string,
    485      *   unit: string,
    486      *   description: string,
    487      *   details: Array.<{
    488      *     metricId: number,
    489      *     name: string,
    490      *     description: string
    491      *   }>
    492      * }>} categories All metric categories needing charts and checkboxes.
    493      */
    494     getMetricTypesCallback: function(categories) {
    495       categories.forEach(function(category) {
    496         this.addCategoryChart_(category);
    497         this.metricCategoryMap_[category.metricCategoryId] = category;
    498 
    499         category.details.forEach(function(metric) {
    500           metric.color = ColorTable_[metric.metricId % ColorTable_.length];
    501           metric.maxValue = 1;
    502           metric.divs = [];
    503           metric.data = null;
    504           metric.category = category;
    505           this.metricDetailsMap_[metric.metricId] = metric;
    506         }, this);
    507       }, this);
    508 
    509       this.setupCheckboxes_($('#choose-metrics')[0],
    510           this.metricCategoryMap_, 'metricId', this.addMetric, this.dropMetric);
    511 
    512       for (var metric in this.metricDetailsMap_) {
    513         this.metricDetailsMap_[metric].checkbox.checked = true;
    514         this.metricDetailsMap_[metric].enabled = true;
    515       }
    516 
    517       this.initProgress_.metrics = true;
    518       if (this.isInitialized_())
    519         this.refreshAll();
    520     },
    521 
    522     /**
    523      * Receive a list of all event categories, each with its correspoinding
    524      * list of event details. Populate |this.eventCategoryMap_| and
    525      * |this.eventDetailsMap| to reflect said list. Reconfigure the
    526      * checkbox set for event selection.
    527      * @param {Array.<{
    528      *   eventCategoryId: number,
    529      *   name: string,
    530      *   description: string,
    531      *   details: Array.<{
    532      *     eventId: number,
    533      *     name: string,
    534      *     description: string
    535      *   }>
    536      * }>} categories All event categories needing charts and checkboxes.
    537      */
    538     getEventTypesCallback: function(categories) {
    539       categories.forEach(function(category) {
    540         this.eventCategoryMap_[category.eventCategoryId] = category;
    541 
    542         category.details.forEach(function(event) {
    543           event.color = ColorTable_[event.eventId % ColorTable_.length];
    544           event.divs = [];
    545           event.data = null;
    546           this.eventDetailsMap_[event.eventId] = event;
    547         }, this);
    548       }, this);
    549 
    550       this.setupCheckboxes_($('#choose-events')[0], this.eventCategoryMap_,
    551           'eventId', this.addEventType, this.dropEventType);
    552 
    553       this.initProgress_.events = true;
    554       if (this.isInitialized_())
    555         this.refreshAll();
    556     },
    557 
    558     /**
    559      * Set up the aspects of the control panel which are not dependent upon the
    560      * information retrieved from PerformanceMonitor's database; this includes
    561      * the Time Resolutions and Aggregation Methods radio sections.
    562      * @private
    563      */
    564     setupStaticControlPanelFeatures_: function() {
    565       // Initialize the options in the |timeResolutionRadioMap_| and set the
    566       // localized names for the time resolutions.
    567       for (var key in TimeResolutions_) {
    568         var resolution = TimeResolutions_[key];
    569         this.timeResolutionRadioMap_[resolution.id] = { 'option': resolution };
    570         resolution.name = loadTimeData.getString(resolution.i18nKey);
    571       }
    572 
    573       // Setup the Time Resolution radio buttons, and select the default option
    574       // of minutes (finer resolution in order to ensure that the user sees
    575       // something at startup).
    576       this.setupRadioButtons_($('#choose-time-range')[0],
    577                               this.timeResolutionRadioMap_,
    578                               this.changeTimeResolution_,
    579                               TimeResolutions_.minutes.id,
    580                               'time-resolutions');
    581 
    582       // Set the default selection to 'Minutes' and set the time range.
    583       this.setTimeRange(TimeResolutions_.minutes,
    584                         Date.now(),
    585                         true);  // Auto-refresh the chart.
    586 
    587       var forwardButton = $('#forward-time')[0];
    588       forwardButton.addEventListener('click', this.forwardTime.bind(this));
    589       var backButton = $('#back-time')[0];
    590       backButton.addEventListener('click', this.backTime.bind(this));
    591 
    592       this.initProgress_.timeRange = true;
    593       if (this.isInitialized_())
    594         this.refreshAll();
    595     },
    596 
    597     /**
    598      * Change the current time resolution. The visible range will stay centered
    599      * around the current center unless the latest edge crosses now(), in which
    600      * case it will be pinned there and start auto-updating.
    601      * @param {number} mapId the index into the |timeResolutionRadioMap_| of the
    602      *     selected resolution.
    603      */
    604     changeTimeResolution_: function(mapId) {
    605       var newEnd;
    606       var now = Date.now();
    607       var newResolution = this.timeResolutionRadioMap_[mapId].option;
    608 
    609       // If we are updating the timer, then we know that we are already ending
    610       // at the perceived current time (which may be different than the actual
    611       // current time, since we don't update continuously).
    612       newEnd = this.updateTimer_ ? now :
    613           Math.min(now, this.range_.end + (newResolution.timeSpan -
    614               this.range_.resolution.timeSpan) / 2);
    615 
    616       this.setTimeRange(newResolution, newEnd, newEnd == now);
    617     },
    618 
    619     /**
    620      * Generalized function to create checkboxes for either events
    621      * or metrics, given a |div| into which to put the checkboxes, and a
    622      * |optionCategoryMap| describing the checkbox structure.
    623      *
    624      * For instance, |optionCategoryMap| might be metricCategoryMap_, with
    625      * contents thus:
    626      *
    627      * optionCategoryMap : {
    628      *   1: {
    629      *     name: 'CPU',
    630      *     details: [
    631      *       {
    632      *         metricId: 1,
    633      *         name: 'CPU Usage',
    634      *         description:
    635      *             'The combined CPU usage of all processes related to Chrome',
    636      *         color: 'rgb(255, 128, 128)'
    637      *       }
    638      *     ],
    639      *   2: {
    640      *     name : 'Memory',
    641      *     details: [
    642      *       {
    643      *         metricId: 2,
    644      *         name: 'Private Memory Usage',
    645      *         description:
    646      *             'The combined private memory usage of all processes related
    647      *             to Chrome',
    648      *         color: 'rgb(128, 255, 128)'
    649      *       },
    650      *       {
    651      *         metricId: 3,
    652      *         name: 'Shared Memory Usage',
    653      *         description:
    654      *             'The combined shared memory usage of all processes related
    655      *             to Chrome',
    656      *         color: 'rgb(128, 128, 255)'
    657      *       }
    658      *     ]
    659      *  }
    660      *
    661      * and we would call setupCheckboxes_ thus:
    662      *
    663      * this.setupCheckboxes_(<parent div>, this.metricCategoryMap_, 'metricId',
    664      *     this.addMetric, this.dropMetric);
    665      *
    666      * MetricCategoryMap_'s values each have a |name| and |details| property.
    667      * SetupCheckboxes_ creates one major header for each such value, with title
    668      * given by the |name| field. Under each major header are checkboxes,
    669      * one for each element in the |details| property. The checkbox titles
    670      * come from the |name| property of each |details| object,
    671      * and they each have an associated colored icon matching the |color|
    672      * property of the details object.
    673      *
    674      * So, for the example given, the generated HTML looks thus:
    675      *
    676      * <div>
    677      *   <h3 class="category-heading">CPU</h3>
    678      *   <div class="checkbox-group">
    679      *     <div>
    680      *       <label class="input-label" title=
    681      *           "The combined CPU usage of all processes related to Chrome">
    682      *         <input type="checkbox">
    683      *         <span>CPU</span>
    684      *       </label>
    685      *     </div>
    686      *   </div>
    687      * </div>
    688      * <div>
    689      *   <h3 class="category-heading">Memory</h3>
    690      *   <div class="checkbox-group">
    691      *     <div>
    692      *       <label class="input-label" title= "The combined private memory \
    693      *           usage of all processes related to Chrome">
    694      *         <input type="checkbox">
    695      *         <span>Private Memory</span>
    696      *       </label>
    697      *     </div>
    698      *     <div>
    699      *       <label class="input-label" title= "The combined shared memory \
    700      *           usage of all processes related to Chrome">
    701      *         <input type="checkbox">
    702      *         <span>Shared Memory</span>
    703      *       </label>
    704      *     </div>
    705      *   </div>
    706      * </div>
    707      *
    708      * The checkboxes for each details object call addMetric or
    709      * dropMetric as they are checked and unchecked, passing the relevant
    710      * |metricId| value. Parameter 'metricId' identifies key |metricId| as the
    711      * identifying property to pass to the methods. So, for instance, checking
    712      * the CPU Usage box results in a call to this.addMetric(1), since
    713      * metricCategoryMap_[1].details[0].metricId == 1.
    714      *
    715      * In general, |optionCategoryMap| must have values that each include
    716      * a property |name|, and a property |details|. The |details| value must
    717      * be an array of objects that in turn each have an identifying property
    718      * with key given by parameter |idKey|, plus a property |name| and a
    719      * property |color|.
    720      *
    721      * @param {!HTMLDivElement} div A <div> into which to put checkboxes.
    722      * @param {!Object} optionCategoryMap A map of metric/event categories.
    723      * @param {string} idKey The key of the id property.
    724      * @param {!function(this:Controller, Object)} check
    725      *     The function to select an entry (metric or event).
    726      * @param {!function(this:Controller, Object)} uncheck
    727      *     The function to deselect an entry (metric or event).
    728      * @private
    729      */
    730     setupCheckboxes_: function(div, optionCategoryMap, idKey, check, uncheck) {
    731       var categoryTemplate = $('#category-template')[0];
    732       var checkboxTemplate = $('#checkbox-template')[0];
    733 
    734       for (var c in optionCategoryMap) {
    735         var category = optionCategoryMap[c];
    736         var template = categoryTemplate.cloneNode(true);
    737         template.id = '';
    738 
    739         var heading = template.querySelector('.category-heading');
    740         heading.innerText = category.name;
    741         heading.title = category.description;
    742 
    743         var checkboxGroup = template.querySelector('.checkbox-group');
    744         category.details.forEach(function(details) {
    745           var checkbox = checkboxTemplate.cloneNode(true);
    746           checkbox.id = '';
    747           var input = checkbox.querySelector('input');
    748 
    749           details.checkbox = input;
    750           input.checked = false;
    751           input.option = details[idKey];
    752           input.addEventListener('change', function(e) {
    753             (e.target.checked ? check : uncheck).call(this, e.target.option);
    754           }.bind(this));
    755 
    756           checkbox.querySelector('span').innerText = details.name;
    757           checkbox.querySelector('.input-label').title = details.description;
    758 
    759           checkboxGroup.appendChild(checkbox);
    760         }, this);
    761 
    762         div.appendChild(template);
    763       }
    764     },
    765 
    766     /**
    767      * Generalized function to create radio buttons in a collection of
    768      * |collectionName|, given a |div| into which the radio buttons are placed
    769      * and a |optionMap| describing the radio buttons' options.
    770      *
    771      * optionMaps have two guaranteed fields - 'option' and 'element'. The
    772      * 'option' field corresponds to the item which the radio button will be
    773      * representing (e.g., a particular aggregation method).
    774      *   - Each 'option' is guaranteed to have a 'value', a 'name', and a
    775      *     'description'. 'Value' holds the id of the option, while 'name' and
    776      *     'description' are internationalized strings for the radio button's
    777      *     content.
    778      *   - 'Element' is the field devoted to the HTMLElement for the radio
    779      *     button corresponding to that entry; this will be set in this
    780      *     function.
    781      *
    782      * Assume that |optionMap| is |aggregationRadioMap_|, as follows:
    783      * optionMap: {
    784      *   0: {
    785      *     option: {
    786      *       id: 0
    787      *       name: 'Median'
    788      *       description: 'Aggregate using median calculations to reduce
    789      *           noisiness in reporting'
    790      *     },
    791      *     element: null
    792      *   },
    793      *   1: {
    794      *     option: {
    795      *       id: 1
    796      *       name: 'Mean'
    797      *       description: 'Aggregate using mean calculations for the most
    798      *           accurate average in reporting'
    799      *     },
    800      *     element: null
    801      *   }
    802      * }
    803      *
    804      * and we would call setupRadioButtons_ with:
    805      * this.setupRadioButtons_(<parent_div>, this.aggregationRadioMap_,
    806      *     this.setAggregationMethod, 0, 'aggregation-methods');
    807      *
    808      * The resultant HTML would be:
    809      * <div class="radio">
    810      *   <label class="input-label" title="Aggregate using median \
    811      *       calculations to reduce noisiness in reporting">
    812      *     <input type="radio" name="aggregation-methods" value=0>
    813      *     <span>Median</span>
    814      *   </label>
    815      * </div>
    816      * <div class="radio">
    817      *   <label class="input-label" title="Aggregate using mean \
    818      *       calculations for the most accurate average in reporting">
    819      *     <input type="radio" name="aggregation-methods" value=1>
    820      *     <span>Mean</span>
    821      *   </label>
    822      * </div>
    823      *
    824      * If a radio button is selected, |onSelect| is called with the radio
    825      * button's value. The |defaultKey| is used to choose which radio button
    826      * to select at startup; the |onSelect| method is not called on this
    827      * selection.
    828      *
    829      * @param {!HTMLDivElement} div A <div> into which we place the radios.
    830      * @param {!Object} optionMap A map containing the radio button information.
    831      * @param {!function(this:Controller, Object)} onSelect
    832      *     The function called when a radio is selected.
    833      * @param {string} defaultKey The key to the radio which should be selected
    834      *     initially.
    835      * @param {string} collectionName The name of the radio button collection.
    836      * @private
    837      */
    838     setupRadioButtons_: function(div,
    839                                  optionMap,
    840                                  onSelect,
    841                                  defaultKey,
    842                                  collectionName) {
    843       var radioTemplate = $('#radio-template')[0];
    844       for (var key in optionMap) {
    845         var entry = optionMap[key];
    846         var radio = radioTemplate.cloneNode(true);
    847         radio.id = '';
    848         var input = radio.querySelector('input');
    849 
    850         input.name = collectionName;
    851         input.enumerator = entry.option.id;
    852         input.option = entry;
    853         radio.querySelector('span').innerText = entry.option.name;
    854         if (entry.option.description != undefined)
    855           radio.querySelector('.input-label').title = entry.option.description;
    856         div.appendChild(radio);
    857         entry.element = input;
    858       }
    859 
    860       optionMap[defaultKey].element.click();
    861 
    862       div.addEventListener('click', function(e) {
    863         if (!e.target.webkitMatchesSelector('input[type="radio"]'))
    864           return;
    865 
    866         onSelect.call(this, e.target.enumerator);
    867       }.bind(this));
    868     },
    869 
    870     /**
    871      * Add a new chart for |category|, making it initially hidden,
    872      * with no metrics displayed in it.
    873      * @param {!Object} category The metric category for which to create
    874      *     the chart. Category is a value from metricCategoryMap_.
    875      * @private
    876      */
    877     addCategoryChart_: function(category) {
    878       var chartParent = $('#charts')[0];
    879       var mainDiv = $('#chart-template')[0].cloneNode(true);
    880       mainDiv.id = '';
    881 
    882       var yaxisLabel = mainDiv.querySelector('h4');
    883       yaxisLabel.innerText = category.unit;
    884 
    885       // Rotation is weird in html. The length of the text affects the x-axis
    886       // placement of the label. We shift it back appropriately.
    887       var width = -1 * (yaxisLabel.offsetWidth / 2) + 20;
    888       var widthString = width.toString() + 'px';
    889       yaxisLabel.style.webkitMarginStart = widthString;
    890 
    891       var grid = mainDiv.querySelector('.grid');
    892 
    893       mainDiv.hidden = true;
    894       chartParent.appendChild(mainDiv);
    895 
    896       grid.hovers = [];
    897 
    898       // Set the various fields for the PerformanceMonitor.Chart object, and
    899       // add the new object to |charts_|.
    900       var chart = {};
    901       chart.mainDiv = mainDiv;
    902       chart.yaxisLabel = yaxisLabel;
    903       chart.grid = grid;
    904       chart.metricIds = [];
    905 
    906       category.details.forEach(function(details) {
    907         chart.metricIds.push(details.metricId);
    908       });
    909 
    910       this.charts_.push(chart);
    911 
    912       // Receive hover events from Flot.
    913       // Attached to chart will be properties 'hovers', a list of {x, div}
    914       // pairs. As pos events arrive, check each hover to see if it should
    915       // be hidden or made visible.
    916       $(grid).bind('plothover', function(event, pos, item) {
    917         var tolerance = this.range_.resolution.pointResolution;
    918 
    919         grid.hovers.forEach(function(hover) {
    920           hover.div.hidden = hover.x < pos.x - tolerance ||
    921               hover.x > pos.x + tolerance;
    922         });
    923 
    924       }.bind(this));
    925 
    926       $(window).resize(function() {
    927         if (this.resizeTimer_ != null)
    928           clearTimeout(this.resizeTimer_);
    929         this.resizeTimer_ = setTimeout(this.checkResize_.bind(this),
    930             resizeDelay_);
    931       }.bind(this));
    932     },
    933 
    934     /**
    935      * |resizeDelay_| ms have elapsed since the last resize event, and the timer
    936      * for redrawing has triggered. Clear it, and redraw all the charts.
    937      * @private
    938      */
    939     checkResize_: function() {
    940       clearTimeout(this.resizeTimer_);
    941       this.resizeTimer_ = null;
    942 
    943       this.drawCharts();
    944     },
    945 
    946     /**
    947      * Set the time range for which to display metrics and events. For
    948      * now, the time range always ends at 'now', but future implementations
    949      * may allow time ranges not so anchored. Also set the format string for
    950      * Flot.
    951      *
    952      * @param {TimeResolution} resolution
    953      *     The time resolution at which to display the data.
    954      * @param {number} end Ending time, in ms since epoch, to which to
    955      *     set the new time range.
    956      * @param {boolean} autoRefresh Indicates whether we should restart the
    957      *     range-update timer.
    958      */
    959     setTimeRange: function(resolution, end, autoRefresh) {
    960       // If we have a timer and we are no longer updating, or if we need a timer
    961       // for a different resolution, disable the current timer.
    962       if (this.updateTimer_ &&
    963               (this.range_.resolution != resolution || !autoRefresh)) {
    964         clearInterval(this.updateTimer_);
    965         this.updateTimer_ = null;
    966       }
    967 
    968       if (autoRefresh && !this.updateTimer_) {
    969         this.updateTimer_ = setInterval(
    970             this.forwardTime.bind(this),
    971             intervalMultiple_ * resolution.pointResolution);
    972       }
    973 
    974       this.range_.resolution = resolution;
    975       this.range_.end = Math.floor(end / resolution.pointResolution) *
    976           resolution.pointResolution;
    977       this.range_.start = this.range_.end - resolution.timeSpan;
    978       this.setTimeFormat_();
    979 
    980       if (this.isInitialized_())
    981         this.refreshAll();
    982     },
    983 
    984     /**
    985      * Set the format string for Flot. For time formats, we display the time
    986      * if we are showing data only for the current day; we display the month,
    987      * day, and time if we are showing data for multiple days at a fine
    988      * resolution; we display the month and day if we are showing data for
    989      * multiple days within the same year at course resolution; and we display
    990      * the year, month, and day if we are showing data for multiple years.
    991      * @private
    992      */
    993     setTimeFormat_: function() {
    994       // If the range is set to a week or less, then we will need to show times.
    995       if (this.range_.resolution.id <= TimeResolutions_['week'].id) {
    996         var dayStart = new Date();
    997         dayStart.setHours(0);
    998         dayStart.setMinutes(0);
    999 
   1000         if (this.range_.start >= dayStart.getTime())
   1001           this.range_.format = TimeFormats_['time'];
   1002         else
   1003           this.range_.format = TimeFormats_['monthDayTime'];
   1004       } else {
   1005         var yearStart = new Date();
   1006         yearStart.setMonth(0);
   1007         yearStart.setDate(0);
   1008 
   1009         if (this.range_.start >= yearStart.getTime())
   1010           this.range_.format = TimeFormats_['monthDay'];
   1011         else
   1012           this.range_.format = TimeFormats_['yearMonthDay'];
   1013       }
   1014     },
   1015 
   1016     /**
   1017      * Back up the time range by 1/2 of its current span, and cause chart
   1018      * redraws.
   1019      */
   1020     backTime: function() {
   1021       this.setTimeRange(this.range_.resolution,
   1022                         this.range_.end - this.range_.resolution.timeSpan / 2,
   1023                         false);
   1024     },
   1025 
   1026     /**
   1027      * Advance the time range by 1/2 of its current span, or up to the point
   1028      * where it ends at the present time, whichever is less.
   1029      */
   1030     forwardTime: function() {
   1031       var now = Date.now();
   1032       var newEnd =
   1033           Math.min(now, this.range_.end + this.range_.resolution.timeSpan / 2);
   1034 
   1035       this.setTimeRange(this.range_.resolution, newEnd, newEnd == now);
   1036     },
   1037 
   1038     /**
   1039      * Set the aggregation method.
   1040      * @param {number} methodId The id of the aggregation method.
   1041      */
   1042     setAggregationMethod: function(methodId) {
   1043       if (methodId != aggregationMethodNone)
   1044         this.hideWarning('no-aggregation-warning');
   1045       else
   1046         this.showWarning('no-aggregation-warning');
   1047 
   1048       this.aggregationMethod = methodId;
   1049       if (this.isInitialized_())
   1050         this.refreshMetrics();
   1051     },
   1052 
   1053     /**
   1054      * Add a new metric to the display, fetching its data and triggering a
   1055      * chart redraw.
   1056      * @param {number} metricId The id of the metric to start displaying.
   1057      */
   1058     addMetric: function(metricId) {
   1059       var metric = this.metricDetailsMap_[metricId];
   1060       metric.enabled = true;
   1061       this.refreshMetrics();
   1062     },
   1063 
   1064     /**
   1065      * Remove a metric from its homechart, triggering a chart redraw.
   1066      * @param {number} metricId The metric to stop displaying.
   1067      */
   1068     dropMetric: function(metricId) {
   1069       var metric = this.metricDetailsMap_[metricId];
   1070       metric.enabled = false;
   1071       this.drawCharts();
   1072     },
   1073 
   1074     /**
   1075      * Refresh all metrics which are active on the graph in one call to the
   1076      * webui. Results will be returned in getMetricsCallback().
   1077      */
   1078     refreshMetrics: function() {
   1079       var metrics = [];
   1080 
   1081       for (var metric in this.metricDetailsMap_) {
   1082         if (this.metricDetailsMap_[metric].enabled)
   1083           metrics.push(this.metricDetailsMap_[metric].metricId);
   1084       }
   1085 
   1086       if (!metrics.length)
   1087         return;
   1088 
   1089       this.awaitingDataCalls_.metrics = true;
   1090       chrome.send('getMetrics',
   1091                   [metrics,
   1092                    this.range_.start, this.range_.end,
   1093                    this.range_.resolution.pointResolution,
   1094                    this.aggregationMethod]);
   1095     },
   1096 
   1097     /**
   1098      * The callback from refreshing the metrics. The resulting metrics will be
   1099      * returned in a list, containing for each active metric a list of data
   1100      * point series, representing the time periods for which PerformanceMonitor
   1101      * was active. These data will be in sorted order, and will be aggregated
   1102      * according to |aggregationMethod_|. These data are put into a Flot-style
   1103      * series, with each point stored in an array of length 2, comprised of the
   1104      * time and the value of the point.
   1105      * @param Array<{
   1106      *   metricId: number,
   1107      *   data: Array<{time: number, value: number}>,
   1108      *   maxValue: number
   1109      * }> results The data for the requested metrics.
   1110      */
   1111     getMetricsCallback: function(results) {
   1112       results.forEach(function(metric) {
   1113         var metricDetails = this.metricDetailsMap_[metric.metricId];
   1114 
   1115         metricDetails.data = [];
   1116 
   1117         // Each data series sent back represents a interval for which
   1118         // PerformanceMonitor was active. Iterate through the points of each
   1119         // series, converting them to Flot standard (an array of time, value
   1120         // pairs).
   1121         metric.metrics.forEach(function(series) {
   1122           var seriesData = [];
   1123           series.forEach(function(point) {
   1124             seriesData.push([point.time - timezoneOffset_, point.value]);
   1125           });
   1126           metricDetails.data.push(seriesData);
   1127         });
   1128 
   1129         metricDetails.maxValue = Math.max(metricDetails.maxValue,
   1130                                           metric.maxValue);
   1131       }, this);
   1132 
   1133       this.awaitingDataCalls_.metrics = false;
   1134       this.drawCharts();
   1135     },
   1136 
   1137     /**
   1138      * Add a new event to the display, fetching its data and triggering a
   1139      * redraw.
   1140      * @param {number} eventType The type of event to start displaying.
   1141      */
   1142     addEventType: function(eventId) {
   1143       this.eventDetailsMap_[eventId].enabled = true;
   1144       this.refreshEvents();
   1145     },
   1146 
   1147     /*
   1148      * Remove an event from the display, triggering a redraw.
   1149      * @param {number} eventId The type of event to stop displaying.
   1150      */
   1151     dropEventType: function(eventId) {
   1152       this.eventDetailsMap_[eventId].enabled = false;
   1153       this.drawCharts();
   1154     },
   1155 
   1156     /**
   1157      * Refresh all events which are active on the graph in one call to the
   1158      * webui. Results will be returned in getEventsCallback().
   1159      */
   1160     refreshEvents: function() {
   1161       var events = [];
   1162       for (var eventType in this.eventDetailsMap_) {
   1163         if (this.eventDetailsMap_[eventType].enabled)
   1164           events.push(this.eventDetailsMap_[eventType].eventId);
   1165       }
   1166       if (!events.length)
   1167         return;
   1168 
   1169       this.awaitingDataCalls_.events = true;
   1170       chrome.send('getEvents', [events, this.range_.start, this.range_.end]);
   1171     },
   1172 
   1173     /**
   1174      * The callback from refreshing events. Resulting events are stored in a
   1175      * list object, which contains for each event type requested a series
   1176      * of event points. Each event point contains a time and an arbitrary list
   1177      * of additional properties to be displayed as a tooltip message for the
   1178      * event.
   1179      * @param Array.<{
   1180      *   eventId: number,
   1181      *   Array.<{time: number}>
   1182      * }> results The collection of events for the requested types.
   1183      */
   1184     getEventsCallback: function(results) {
   1185       results.forEach(function(eventSet) {
   1186         var eventType = this.eventDetailsMap_[eventSet.eventId];
   1187 
   1188         eventSet.events.forEach(function(eventData) {
   1189           eventData.time -= timezoneOffset_;
   1190         });
   1191         eventType.data = eventSet.events;
   1192       }, this);
   1193 
   1194       this.awaitingDataCalls_.events = false;
   1195       this.drawCharts();
   1196     },
   1197 
   1198     /**
   1199      * Create and return an array of 'markings' (per Flot), representing
   1200      * vertical lines at the event time, in the event's color. Also add
   1201      * (not per Flot) a |popupTitle| property to each, to be used for
   1202      * labeling description popups.
   1203      * @return {!Array.<{
   1204      *   color: string,
   1205      *   popupContent: string,
   1206      *   xaxis: {from: number, to: number}
   1207      * }>} A marks data structure for Flot to use.
   1208      * @private
   1209      */
   1210     getEventMarks_: function() {
   1211       var enabledEvents = [];
   1212       var markings = [];
   1213       var explanation;
   1214       var date;
   1215 
   1216       for (var eventType in this.eventDetailsMap_) {
   1217         if (this.eventDetailsMap_[eventType].enabled)
   1218           enabledEvents.push(this.eventDetailsMap_[eventType]);
   1219       }
   1220 
   1221       enabledEvents.forEach(function(eventValue) {
   1222         eventValue.data.forEach(function(point) {
   1223           if (point.time >= this.range_.start - timezoneOffset_ &&
   1224               point.time <= this.range_.end - timezoneOffset_) {
   1225             date = new Date(point.time + timezoneOffset_);
   1226             explanation = '<b>' + eventValue.popupTitle + '<br/>' +
   1227                 date.toLocaleString() + '</b><br/>';
   1228 
   1229             for (var key in point) {
   1230               if (key != 'time') {
   1231                 var datum = point[key];
   1232 
   1233                 // We display all fields with a label-value pair.
   1234                 if ('label' in datum && 'value' in datum) {
   1235                   explanation = explanation + '<b>' + datum.label + ': </b>' +
   1236                       datum.value + ' <br/>';
   1237                 }
   1238               }
   1239             }
   1240             markings.push({
   1241               color: eventValue.color,
   1242               popupContent: explanation,
   1243               xaxis: { from: point.time, to: point.time }
   1244             });
   1245           } else {
   1246             console.log('Event out of time range ' + this.range_.start +
   1247                 ' -> ' + this.range_.end + ' at: ' + point.time);
   1248           }
   1249         }, this);
   1250       }, this);
   1251 
   1252       return markings;
   1253     },
   1254 
   1255     /**
   1256      * Return an object containing an array of series for Flot to chart, as well
   1257      * as a series of axes (currently this will only be one axis).
   1258      * @param {Array.<PerformanceMonitor.MetricDetails>} activeMetrics
   1259      *     The metrics for which we are generating series.
   1260      * @return {!{
   1261      *   series: !Array.<{
   1262      *     color: string,
   1263      *     data: !Array<{time: number, value: number},
   1264      *     yaxis: {min: number, max: number, labelWidth: number}
   1265      *   },
   1266      *   yaxes: !Array.<{min: number, max: number, labelWidth: number}>
   1267      * }}
   1268      * @private
   1269      */
   1270     getChartSeriesAndAxes_: function(activeMetrics) {
   1271       var seriesList = [];
   1272       var axisList = [];
   1273       var axisMap = {};
   1274       activeMetrics.forEach(function(metric) {
   1275         var categoryId = metric.category.metricCategoryId;
   1276         var yaxisNumber = axisMap[categoryId];
   1277 
   1278         // Add a new y-axis if we are encountering this category of metric
   1279         // for the first time. Otherwise, update the existing y-axis with
   1280         // a new max value if needed. (Presently, we expect only one category
   1281         // of metric per chart, but this design permits more in the future.)
   1282         if (yaxisNumber === undefined) {
   1283           axisList.push({min: 0,
   1284                          max: metric.maxValue * yAxisMargin_,
   1285                          labelWidth: 60});
   1286           axisMap[categoryId] = yaxisNumber = axisList.length;
   1287         } else {
   1288           axisList[yaxisNumber - 1].max =
   1289               Math.max(axisList[yaxisNumber - 1].max,
   1290                        metric.maxValue * yAxisMargin_);
   1291         }
   1292 
   1293         // Create a Flot-style series for each data series in the metric.
   1294         for (var i = 0; i < metric.data.length; ++i) {
   1295           seriesList.push({
   1296             color: metric.color,
   1297             data: metric.data[i],
   1298             label: i == 0 ? metric.name : null,
   1299             yaxis: yaxisNumber
   1300           });
   1301         }
   1302       }, this);
   1303 
   1304       return { series: seriesList, yaxes: axisList };
   1305     },
   1306 
   1307     /**
   1308      * Draw each chart which has at least one enabled metric, along with all
   1309      * the event markers, if and only if we do not have outstanding calls for
   1310      * data.
   1311      */
   1312     drawCharts: function() {
   1313       // If we are currently waiting for data, do nothing - the callbacks will
   1314       // re-call drawCharts when they are done. This way, we can avoid any
   1315       // conflicts.
   1316       if (this.fetchingData_())
   1317         return;
   1318 
   1319       // All charts will share the same xaxis and events.
   1320       var eventMarks = this.getEventMarks_();
   1321       var xaxis = {
   1322         mode: 'time',
   1323         timeformat: this.range_.format,
   1324         min: this.range_.start - timezoneOffset_,
   1325         max: this.range_.end - timezoneOffset_
   1326       };
   1327 
   1328       this.charts_.forEach(function(chart) {
   1329         var activeMetrics = [];
   1330         chart.metricIds.forEach(function(id) {
   1331           if (this.metricDetailsMap_[id].enabled)
   1332             activeMetrics.push(this.metricDetailsMap_[id]);
   1333         }, this);
   1334 
   1335         if (!activeMetrics.length) {
   1336           chart.hidden = true;
   1337           return;
   1338         }
   1339 
   1340         chart.mainDiv.hidden = false;
   1341 
   1342         var chartData = this.getChartSeriesAndAxes_(activeMetrics);
   1343 
   1344         // There is the possibility that we have no data for this particular
   1345         // time window and metric, but Flot will not draw the grid without at
   1346         // least one data point (regardless of whether that datapoint is
   1347         // displayed). Thus, we will add the point (-1, -1) (which is guaranteed
   1348         // not to show with our axis bounds), and force Flot to show the chart.
   1349         if (chartData.series.length == 0)
   1350           chartData.series = [[-1, -1]];
   1351 
   1352         chart.plot = $.plot(chart.grid, chartData.series, {
   1353           yaxes: chartData.yaxes,
   1354           xaxis: xaxis,
   1355           points: { show: true, radius: 1},
   1356           lines: { show: true},
   1357           grid: {
   1358             markings: eventMarks,
   1359             hoverable: true,
   1360             autoHighlight: true,
   1361             backgroundColor: { colors: ['#fff', '#f0f6fc'] },
   1362           },
   1363         });
   1364 
   1365         // For each event in |eventMarks|, create also a label div, with left
   1366         // edge colinear with the event vertical line. Top of label is
   1367         // presently a hack-in, putting labels in three tiers of 25px height
   1368         // each to avoid overlap. Will need something better.
   1369         var labelTemplate = $('#label-template')[0];
   1370         for (var i = 0; i < eventMarks.length; i++) {
   1371           var mark = eventMarks[i];
   1372           var point = chart.plot.pointOffset(
   1373               {x: mark.xaxis.to, y: chartData.yaxes[0].max, yaxis: 1});
   1374           var labelDiv = labelTemplate.cloneNode(true);
   1375           labelDiv.innerHTML = mark.popupContent;
   1376           labelDiv.style.left = point.left + 'px';
   1377           labelDiv.style.top = (point.top + 100 * (i % 3)) + 'px';
   1378 
   1379           chart.grid.appendChild(labelDiv);
   1380           labelDiv.hidden = true;
   1381           chart.grid.hovers.push({x: mark.xaxis.to, div: labelDiv});
   1382         }
   1383       }, this);
   1384     },
   1385   };
   1386   return {
   1387     PerformanceMonitor: PerformanceMonitor
   1388   };
   1389 });
   1390 
   1391 var PerformanceMonitor = new performance_monitor.PerformanceMonitor();
   1392