Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / resources / performance_monitor / chart.js
blob4d77aa0e7e6a225c9a4f2f64bda0a5257a880240
1 /* Copyright (c) 2012 The Chromium Authors. All rights reserved.
2  * Use of this source code is governed by a BSD-style license that can be
3  * found in the LICENSE file. */
5 cr.define('performance_monitor', function() {
6   'use strict';
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},
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},
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},
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},
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},
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   };
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   };
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   ];
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;
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;
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;
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;
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;
111   /*
112    * The value of the default aggregation option, 'Median Aggregation'
113    * (AGGREGATION_METHOD_MEDIAN), on the C++ side.
114    */
115   var aggregationMethodMedian = 1;
117   /** @constructor */
118   function PerformanceMonitor() {
119     this.__proto__ = PerformanceMonitor.prototype;
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;
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;
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;
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;
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 };
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_ = {};
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_ = {};
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_ = {};
272     /**
273      * Comprehensive map from metricId to MetricDetails.
274      * @type {Object.<string, {PerformanceMonitor.MetricDetails}>}
275      * @private
276      */
277     this.metricDetailsMap_ = {};
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_ = {};
296     /**
297      * Comprehensive map from eventId to EventDetails.
298      * @type {Object.<string, {PerformanceMonitor.EventDetails}>}
299      * @private
300      */
301     this.eventDetailsMap_ = {};
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_ = [];
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_ = [];
319     /**
320      * Handle of timer interval function used to update charts
321      * @type {Object}
322      * @private
323      */
324     this.updateTimer_ = null;
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;
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_ = {};
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 };
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_ = [];
367     this.setupStaticControlPanelFeatures_();
368     chrome.send('getFlagEnabled');
369     chrome.send('getAggregationTypes');
370     chrome.send('getEventTypes');
371     chrome.send('getMetricTypes');
372   }
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;
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     },
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);
403       if (this.activeWarnings_.length == 0)
404         $('#warnings-box')[0].style.display = 'none';
405     },
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     },
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     },
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     },
442     /**
443      * Refresh all data areas.
444      */
445     refreshAll: function() {
446       this.refreshMetrics();
447       this.refreshEvents();
448     },
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);
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     },
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;
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);
509       this.setupCheckboxes_($('#choose-metrics')[0],
510           this.metricCategoryMap_, 'metricId', this.addMetric, this.dropMetric);
512       for (var metric in this.metricDetailsMap_) {
513         this.metricDetailsMap_[metric].checkbox.checked = true;
514         this.metricDetailsMap_[metric].enabled = true;
515       }
517       this.initProgress_.metrics = true;
518       if (this.isInitialized_())
519         this.refreshAll();
520     },
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;
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);
550       this.setupCheckboxes_($('#choose-events')[0], this.eventCategoryMap_,
551           'eventId', this.addEventType, this.dropEventType);
553       this.initProgress_.events = true;
554       if (this.isInitialized_())
555         this.refreshAll();
556     },
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       }
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');
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.
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));
592       this.initProgress_.timeRange = true;
593       if (this.isInitialized_())
594         this.refreshAll();
595     },
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;
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);
616       this.setTimeRange(newResolution, newEnd, newEnd == now);
617     },
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];
734       for (var c in optionCategoryMap) {
735         var category = optionCategoryMap[c];
736         var template = categoryTemplate.cloneNode(true);
737         template.id = '';
739         var heading = template.querySelector('.category-heading');
740         heading.innerText = category.name;
741         heading.title = category.description;
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');
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));
756           checkbox.querySelector('span').innerText = details.name;
757           checkbox.querySelector('.input-label').title = details.description;
759           checkboxGroup.appendChild(checkbox);
760         }, this);
762         div.appendChild(template);
763       }
764     },
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');
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       }
860       optionMap[defaultKey].element.click();
862       div.addEventListener('click', function(e) {
863         if (!e.target.webkitMatchesSelector('input[type="radio"]'))
864           return;
866         onSelect.call(this, e.target.enumerator);
867       }.bind(this));
868     },
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 = '';
882       var yaxisLabel = mainDiv.querySelector('h4');
883       yaxisLabel.innerText = category.unit;
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;
891       var grid = mainDiv.querySelector('.grid');
893       mainDiv.hidden = true;
894       chartParent.appendChild(mainDiv);
896       grid.hovers = [];
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 = [];
906       category.details.forEach(function(details) {
907         chart.metricIds.push(details.metricId);
908       });
910       this.charts_.push(chart);
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;
919         grid.hovers.forEach(function(hover) {
920           hover.div.hidden = hover.x < pos.x - tolerance ||
921               hover.x > pos.x + tolerance;
922         });
924       }.bind(this));
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     },
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;
943       this.drawCharts();
944     },
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       }
968       if (autoRefresh && !this.updateTimer_) {
969         this.updateTimer_ = setInterval(
970             this.forwardTime.bind(this),
971             intervalMultiple_ * resolution.pointResolution);
972       }
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_();
980       if (this.isInitialized_())
981         this.refreshAll();
982     },
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);
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);
1009         if (this.range_.start >= yearStart.getTime())
1010           this.range_.format = TimeFormats_['monthDay'];
1011         else
1012           this.range_.format = TimeFormats_['yearMonthDay'];
1013       }
1014     },
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     },
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);
1035       this.setTimeRange(this.range_.resolution, newEnd, newEnd == now);
1036     },
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');
1048       this.aggregationMethod = methodId;
1049       if (this.isInitialized_())
1050         this.refreshMetrics();
1051     },
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     },
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     },
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 = [];
1081       for (var metric in this.metricDetailsMap_) {
1082         if (this.metricDetailsMap_[metric].enabled)
1083           metrics.push(this.metricDetailsMap_[metric].metricId);
1084       }
1086       if (!metrics.length)
1087         return;
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     },
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];
1115         metricDetails.data = [];
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         });
1129         metricDetails.maxValue = Math.max(metricDetails.maxValue,
1130                                           metric.maxValue);
1131       }, this);
1133       this.awaitingDataCalls_.metrics = false;
1134       this.drawCharts();
1135     },
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     },
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     },
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;
1169       this.awaitingDataCalls_.events = true;
1170       chrome.send('getEvents', [events, this.range_.start, this.range_.end]);
1171     },
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];
1188         eventSet.events.forEach(function(eventData) {
1189           eventData.time -= timezoneOffset_;
1190         });
1191         eventType.data = eventSet.events;
1192       }, this);
1194       this.awaitingDataCalls_.events = false;
1195       this.drawCharts();
1196     },
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;
1216       for (var eventType in this.eventDetailsMap_) {
1217         if (this.eventDetailsMap_[eventType].enabled)
1218           enabledEvents.push(this.eventDetailsMap_[eventType]);
1219       }
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/>';
1229             for (var key in point) {
1230               if (key != 'time') {
1231                 var datum = point[key];
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);
1252       return markings;
1253     },
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];
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         }
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);
1304       return { series: seriesList, yaxes: axisList };
1305     },
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;
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       };
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);
1335         if (!activeMetrics.length) {
1336           chart.hidden = true;
1337           return;
1338         }
1340         chart.mainDiv.hidden = false;
1342         var chartData = this.getChartSeriesAndAxes_(activeMetrics);
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]];
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         });
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';
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   };
1391 var PerformanceMonitor = new performance_monitor.PerformanceMonitor();