| OLD | NEW |
| (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(); | |
| OLD | NEW |