Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(343)

Side by Side Diff: chrome/browser/resources/performance_monitor/chart.js

Issue 547063003: Remove the unmaintained performance monitor. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Address Devlin's comments Created 6 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
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();
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698