Chromium Code Reviews| Index: chrome/browser/resources/performance_monitor/chart.js |
| diff --git a/chrome/browser/resources/performance_monitor/chart.js b/chrome/browser/resources/performance_monitor/chart.js |
| index acc2102fc6b78513515b58cf310497a1e38c95d5..07db240e2f890700813f91b7aafdbab63d705bb5 100644 |
| --- a/chrome/browser/resources/performance_monitor/chart.js |
| +++ b/chrome/browser/resources/performance_monitor/chart.js |
| @@ -2,99 +2,206 @@ |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. */ |
| - |
| cr.define('performance_monitor', function() { |
| 'use strict'; |
| /** |
| - * Enum for time ranges, giving a descriptive name, time span prior to |now|, |
| - * data point resolution, and time-label frequency and format for each. |
| + * Enum for time ranges, giving for each a descriptive name, time span in ms |
| + * prior to |now|, data point resolution in ms, time-label frequency in data |
| + * points per label, and format using Date.js standards, for each. |
| + * |
| + * The additional |element| field is added in setupTimeRangeTab_, giving |
| + * the HTML input tag for the radio button selecting the given time range. |
| * @enum {{ |
| * value: number, |
| * name: string, |
| * timeSpan: number, |
| * resolution: number, |
| * labelEvery: number, |
| - * format: string |
| + * format: string, |
| + * element: HTMLElement |
| * }} |
| * @private |
| */ |
| var TimeRange_ = { |
| - // Prior 12 min, resolution of 1s, at most 720 points. |
| - // Labels at 60 point (1 min) intervals. |
| - minutes: {value: 0, name: 'Last 12 min', timeSpan: 720 * 1000, |
| - resolution: 1000, labelEvery: 60, format: 'MM-dd'}, |
| + // Prior 15 min, resolution of 5s, at most 180 points. |
| + // Labels at 12 point (1 min) intervals. |
| + minutes: {value: 0, name: 'Last 15 min', timeSpan: 900 * 1000, |
| + resolution: 1000 * 5, labelEvery: 12, format: 'MM-dd'}, |
| - // Prior hour, resolution of 5s, at most 720 points. |
| - // Labels at 60 point (5 min) intervals. |
| + // Prior hour, resolution of 20s, at most 180 points. |
| + // Labels at 15 point (5 min) intervals. |
| hour: {value: 1, name: 'Last Hour', timeSpan: 3600 * 1000, |
| - resolution: 1000 * 5, labelEvery: 60, format: 'MM-dd'}, |
| + resolution: 1000 * 20, labelEvery: 15, format: 'MM-dd'}, |
| - // Prior day, resolution of 2 min, at most 720 points. |
| - // Labels at 90 point (3 hour) intervals. |
| + // Prior day, resolution of 5 min, at most 288 points. |
| + // Labels at 36 point (3 hour) intervals. |
| day: {value: 2, name: 'Last Day', timeSpan: 24 * 3600 * 1000, |
| - resolution: 1000 * 60 * 2, labelEvery: 90, format: 'MM-dd'}, |
| + resolution: 1000 * 60 * 5, labelEvery: 36, format: 'MM-dd'}, |
| - // Prior week, resolution of 15 min -- at most 672 data points. |
| - // Labels at 96 point (daily) intervals. |
| + // Prior week, resolution of 1 hr -- at most 168 data points. |
| + // Labels at 24 point (daily) intervals. |
| week: {value: 3, name: 'Last Week', timeSpan: 7 * 24 * 3600 * 1000, |
| - resolution: 1000 * 60 * 15, labelEvery: 96, format: 'M/d'}, |
| + resolution: 1000 * 3600, labelEvery: 24, format: 'M/d'}, |
| - // Prior month (30 days), resolution of 1 hr -- at most 720 data points. |
| - // Labels at 168 point (weekly) intervals. |
| + // Prior month (30 days), resolution of 4 hr -- at most 180 data points. |
| + // Labels at 42 point (weekly) intervals. |
| month: {value: 4, name: 'Last Month', timeSpan: 30 * 24 * 3600 * 1000, |
| - resolution: 1000 * 3600, labelEvery: 168, format: 'M/d'}, |
| + resolution: 1000 * 3600 * 4, labelEvery: 42, format: 'M/d'}, |
| - // Prior quarter (90 days), resolution of 3 hr -- at most 720 data points. |
| - // Labels at 112 point (fortnightly) intervals. |
| + // Prior quarter (90 days), resolution of 12 hr -- at most 180 data points. |
| + // Labels at 28 point (fortnightly) intervals. |
| quarter: {value: 5, name: 'Last Quarter', timeSpan: 90 * 24 * 3600 * 1000, |
| - resolution: 1000 * 3600 * 3, labelEvery: 112, format: 'M/yy'}, |
| + resolution: 1000 * 3600 * 12, labelEvery: 28, format: 'M/yy'}, |
| }; |
| /* |
| - * Offset, in ms, by which to subtract to convert GMT to local time |
| + * Table of colors to use for metrics and events. Basically boxing the |
| + * colorwheel, but leaving out yellows and fully saturated colors. |
| + * @type {Array.<string>} |
| + * @private |
| + */ |
| + var ColorTable_ = [ |
| + 'rgb(255, 128, 128)', 'rgb(128, 255, 128)', 'rgb(128, 128, 255)', |
| + 'rgb(128, 255, 255)', 'rgb(255, 128, 255)', // No bright yellow |
| + 'rgb(255, 64, 64)', 'rgb( 64, 255, 64)', 'rgb( 64, 64, 255)', |
| + 'rgb( 64, 255, 255)', 'rgb(255, 64, 255)', // No medium yellow either |
| + 'rgb(128, 64, 64)', 'rgb( 64, 128, 64)', 'rgb( 64, 64, 128)', |
| + 'rgb( 64, 128, 128)', 'rgb(128, 64, 128)', 'rgb(128, 128, 64)' |
| + ]; |
| + |
| + /* |
| + * Offset, in ms, by which to subtract to convert GMT to local time. |
| + * @type {number} |
| + * @private |
| + */ |
| + var timezoneOffset_ = new Date().getTimezoneOffset() * 60000; |
| + |
| + /* |
| + * Additional range multiplier to ensure that points don't hit the top of |
| + * the graph. |
| + * @type {number} |
| + * @private |
| + */ |
| + var yAxisMargin_ = 1.05; |
| + |
| + /* |
| + * Number of time resolution periods to wait between automated update of |
| + * graphs. |
| * @type {number} |
| + * @private |
| + */ |
| + var intervalMultiple_ = 2; |
| + |
| + /* |
| + * Number of milliseconds to wait before deciding that the most recent |
| + * resize event is not going to be followed immediately by another, and |
| + * thus needs handling. |
| + * @type {number} |
| + * @private |
| */ |
| - var timezoneOffset = new Date().getTimezoneOffset() * 60000; |
| + var resizeDelay_ = 500; |
|
Jeffrey Yasskin
2012/09/05 22:21:54
Fair enough.
|
| /** @constructor */ |
| function PerformanceMonitor() { |
| this.__proto__ = PerformanceMonitor.prototype; |
| + |
| /** |
| - * All metrics have entries, but those not displayed have an empty div list. |
| - * If a div list is not empty, the associated data will be non-null, or |
| - * null but about to be filled by webui handler response. Thus, any metric |
| - * with non-empty div list but null data is awaiting a data response |
| - * from the webui handler. The webui handler uses numbers to uniquely |
| - * identify metric and event types, so we use the same numbers (in |
| - * string form) for the map key, repeating the id number in the |id| |
| - * field, as a number. |
| - * @type {Object.<string, { |
| - * id: number, |
| - * description: string, |
| - * units: string, |
| - * yAxis: !{max: number, color: string}, |
| - * divs: !Array.<HTMLDivElement>, |
| - * data: ?Array.<{time: number, value: number}> |
| - * }>} |
| - * @private |
| + * Detailed information on a metric in the UI. MetricId is a unique |
| + * identifying number for the metric, provided by the webui, and assumed |
| + * to be densely populated. All metrics also have a description and |
| + * an associated category giving their unit information and home chart. |
| + * They also have a color in which they are displayed, and a maximum value |
| + * by which to scale their y-axis. |
| + * |
| + * Although in the present UI each metric appears only in the home chart of |
| + * its metric category, we keep the divs property to allow future |
| + * modifications in which the same metric might appear in several charts |
| + * for side-by-side comparisons. Metrics not being displayed have an |
| + * empty div list. If a div list is not empty, the associated data will |
| + * be non-null, or null but about to be filled by webui handler response. |
| + * Thus, any metric with non-empty div list but null data is awaiting a |
| + * data response from the webui handler. |
| + * @typedef {{ |
| + * metricId: number, |
| + * description: string, |
| + * category: !Object, |
| + * color: string, |
| + * maxValue: number, |
| + * divs: !Array.<HTMLDivElement>, |
| + * data: ?Array.<{time: number, value: number}> |
| + * }} |
| */ |
| - this.metricMap_ = {}; |
| + PerformanceMonitor.MetricDetails; |
| - /* |
| - * Similar data for events, though no yAxis info is needed since events |
| - * are simply labelled markers at X locations. Rules regarding null data |
| - * with non-empty div list apply here as for metricMap_ above. |
| - * @type {Object.<number, { |
| - * id: number, |
| + /** |
| + * Similar data for events as for metrics, though no yAxis info is needed |
|
Devlin
2012/09/07 18:44:54
nit: Be consistent in spelling y-axis/yAxis in com
clintstaley
2012/09/07 20:39:52
Done.
|
| + * since events are simply labelled markers at X locations. Rules regarding |
| + * null data with non-empty div list apply here as for metricDetailsMap_ |
| + * above. The |data| field follows a special rule not describable in |
| + * JSDoc: Each array element may include arbitrary other properties with |
| + * value {label: 'some label', value: 'some value'} for display in the |
| + * event mouseover. These will vary per event type. |
|
Jeffrey Yasskin
2012/09/05 22:21:54
I see. So effectively each array element maps a ti
clintstaley
2012/09/07 20:39:52
I assume you mean for me to revise the comment acc
|
| + * @typedef {{ |
| + * eventId: number, |
| + * name: string, |
| + * popupTitle: string, |
| * description: string, |
| * color: string, |
| * divs: !Array.<HTMLDivElement>, |
| - * data: ?Array.<{time: number, longDescription: string}> |
| + * data: ?Array.<{time: number}> |
| + * }} |
| + */ |
| + PerformanceMonitor.EventDetails; |
| + |
| + /** |
| + * Metrics fall into categories that have common units and thus may |
| + * share a common graph, or share y-axes within a multi-y-axis graph. |
| + * Each category has one home chart in which metrics of that category |
| + * are displayed. Currently this is also the only chart in which such |
| + * metrics are displayed, but the design permits a metric to show in |
| + * several charts if this is useful later on. |
| + * @type {Object.<string, { |
| + * metricCategoryId: number, |
| + * name: string, |
| + * description: string, |
| + * unit: string, |
| + * details: Array.<{!PerformanceMonitor.MetricDetails}>, |
| + * homeChart: !HTMLDivElement |
| + * }>} |
| + * @private |
| + */ |
| + this.metricCategoryMap_ = {}; |
| + |
| + /** |
| + * Comprehensive map from metricId to MetricDetails. |
| + * @type {Object.<number, {PerformanceMonitor.MetricDetails}>} |
| + * @private |
| + */ |
| + this.metricDetailsMap_ = {}; |
| + |
| + /** |
| + * Events fall into categories just like metrics, above. This category |
| + * grouping is not as important as that for metrics, since events |
| + * needn't share maxima, y-axes, nor units, and since events appear on |
| + * all charts. But grouping of event categories in the event-selection |
| + * UI is still useful. |
| + * @type {Object.<string, { |
| + * eventCategoryId: number, |
| + * name: string, |
| + * description: string, |
| + * details: !Array.<!PerformanceMonitor.EventDetails>, |
| * }>} |
| * @private |
| */ |
| - this.eventMap_ = {}; |
| + this.eventCategoryMap_ = {}; |
| + |
| + /** |
| + * Comprehensive map from eventId to EventDetails. |
| + * @type {Object.<number, {PerformanceMonitor.EventDetails}>} |
| + * @private |
| + */ |
| + this.eventDetailsMap_ = {}; |
| /** |
| * Time periods in which the browser was active and collecting metrics |
| @@ -104,60 +211,110 @@ cr.define('performance_monitor', function() { |
| */ |
| this.intervals_ = []; |
| - this.setupTimeRangeChooser_(); |
| - chrome.send('getAllEventTypes'); |
| - chrome.send('getAllMetricTypes'); |
| - this.setupMainChart_(); |
| + /** |
| + * Handle of timer interval function used to update charts |
| + * @type {Object} |
| + * @private |
| + */ |
| + this.updateTimer_ = null; |
| + |
| + /** |
| + * Handle of timer interval function used to check for resizes. Nonnull |
| + * only when resize events are coming steadily. |
| + * @type {Object} |
| + * @private |
| + */ |
| + this.resizeTimer_ = null; |
| + |
| + /** |
| + * Time of last UNHANDLED resize event, or null if no resizes or if |
| + * last one has been handled. |
| + * @type {number} |
| + * @private |
| + */ |
| + this.lastResizeTime_ = null; |
| + |
| + /** |
| + * All chart divs in the display, whether hidden or not. Presently |
| + * this has one entry for each metric category in |this.metricCategoryMap|. |
| + * @type {Array.<HTMLDivElement>} |
| + * @private |
| + */ |
| + this.charts_ = []; |
| + |
| + this.setupTimeRangeTab_(); |
| + chrome.send('getEventTypes'); |
| + chrome.send('getMetricTypes'); |
| TimeRange_.day.element.click(); |
| } |
| PerformanceMonitor.prototype = { |
| /** |
| - * Receive a list of all metrics, and populate |this.metricMap_| to |
| - * reflect said list. Reconfigure the checkbox set for metric selection. |
| - * @param {Array.<{metricType: string, shortDescription: string}>} |
| - * allMetrics All metrics from which to select. |
| - */ |
| - getAllMetricTypesCallback: function(allMetrics) { |
| - for (var i = 0; i < allMetrics.length; i++) { |
| - var metric = allMetrics[i]; |
| - |
| - this.metricMap_[metric.metricType] = { |
| - id: metric.metricType, |
| - description: metric.shortDescription, |
| - units: metric.units, |
| - yAxis: {min: 0, max: metric.maxValue, |
| - color: jQuery.color.parse('blue')}, |
| - divs: [], |
| - data: null |
| - }; |
| - } |
| + * Receive a list of all metric categories, each with its correspoinding |
|
Devlin
2012/09/07 18:44:54
nit: corresponding
clintstaley
2012/09/07 20:39:52
Done.
|
| + * list of metric details. Populate |this.metricCategoryMap_| and |
| + * |this.metrictDetailsMap_| to reflect said list. Reconfigure the |
| + * checkbox set for metric selection. |
| + * @param {Array.<{ |
| + * metricCategoryId: number, |
| + * name: string, |
| + * unit: string, |
| + * description: string, |
| + * details: Array.<{ |
| + * metricId: number, |
| + * name: string, |
| + * description: string |
| + * }> |
| + * }>} categories All metric categories needing charts and checkboxes. |
| + */ |
| + getMetricTypesCallback: function(categories) { |
| + categories.forEach(function(category) { |
| + this.addCategoryChart_(category); |
| + this.metricCategoryMap_[category.metricCategoryId] = category; |
| + |
| + category.details.forEach(function(metric) { |
| + metric.color = ColorTable_[metric.metricId % ColorTable_.length]; |
| + metric.maxValue = 1; |
| + metric.divs = []; |
| + metric.data = null; |
| + metric.category = category; |
| + this.metricDetailsMap_[metric.metricId] = metric; |
| + }, this); |
| + }, this); |
| this.setupCheckboxes_($('#choose-metrics')[0], |
| - this.metricMap_, this.addMetric, this.dropMetric); |
| + this.metricCategoryMap_, 'metricId', this.addMetric, this.dropMetric); |
| }, |
| /** |
| - * Receive a list of all event types, and populate |this.eventMap_| to |
| - * reflect said list. Reconfigure the checkbox set for event selection. |
| - * @param {Array.<{eventType: string, shortDescription: string}>} |
| - * allEvents All events from which to select. |
| + * Receive a list of all event categories, each with its correspoinding |
| + * list of event details. Populate |this.eventCategoryMap_| and |
| + * |this.eventDetailsMap| to reflect said list. Reconfigure the |
| + * checkbox set for event selection. |
| + * @param {Array.<{ |
| + * eventCategoryId: number, |
| + * name: string, |
| + * description: string, |
| + * details: Array.<{ |
| + * eventId: number, |
| + * name: string, |
| + * description: string |
| + * }> |
| + * }>} categories All event categories needing charts checkboxes. |
| */ |
| - getAllEventTypesCallback: function(allEvents) { |
| - for (var i = 0; i < allEvents.length; i++) { |
| - var eventInfo = allEvents[i]; |
| - |
| - this.eventMap_[eventInfo.eventType] = { |
| - id: eventInfo.eventType, |
| - color: jQuery.color.parse('red'), |
| - data: null, |
| - description: eventInfo.shortDescription, |
| - divs: [] |
| - }; |
| - } |
| - |
| - this.setupCheckboxes_($('#choose-events')[0], |
| - this.eventMap_, this.addEventType, this.dropEventType); |
| + getEventTypesCallback: function(categories) { |
| + categories.forEach(function(category) { |
| + this.eventCategoryMap_[category.eventCategoryId] = category; |
| + |
| + category.details.forEach(function(event) { |
| + event.color = ColorTable_[event.eventId % ColorTable_.length]; |
| + event.divs = []; |
| + event.data = null; |
| + this.eventDetailsMap_[event.eventId] = event; |
| + }, this); |
| + }, this); |
| + |
| + this.setupCheckboxes_($('#choose-events')[0], this.eventCategoryMap_, |
| + 'eventId', this.addEventType, this.dropEventType); |
| }, |
| /** |
| @@ -165,8 +322,10 @@ cr.define('performance_monitor', function() { |
| * as a template. |
| * @private |
| */ |
| - setupTimeRangeChooser_: function() { |
| + setupTimeRangeTab_: function() { |
| var timeDiv = $('#choose-time-range')[0]; |
| + var backButton = $('#back-time')[0]; |
| + var forwardButton = $('#forward-time')[0]; |
| var radioTemplate = $('#radio-template')[0]; |
| for (var time in TimeRange_) { |
| @@ -185,66 +344,202 @@ cr.define('performance_monitor', function() { |
| if (!e.target.webkitMatchesSelector('input[type="radio"]')) |
| return; |
| - this.setTimeRange(e.target.timeRange); |
| + this.setTimeRange(e.target.timeRange, Date.now(), true); |
| }.bind(this)); |
| + |
| + forwardButton.addEventListener('click', this.forwardTime.bind(this)); |
| + backButton.addEventListener('click', this.backTime.bind(this)); |
| }, |
| /** |
| * Generalized function for setting up checkbox blocks for either events |
| * or metrics. Take a div |div| into which to place the checkboxes, |
| - * and a map |optionMap| with values that each include a property |
| - * |description|. Set up one checkbox for each entry in |optionMap| |
| - * labelled with that description. Arrange callbacks to function |check| |
| - * or |uncheck|, passing them the key of the checked or unchecked option, |
| - * when the relevant checkbox state changes. |
| + * and a map |optionCategoryMap| with values that each include a property |
| + * |name|, and a property |details|, which gives an array of objects |
| + * that in turn each have an id property with key given by parameter |
| + * |idKey|, a property |name| and a property |color|. |
| + * |
| + * For instance, |optionCategoryMap| might be metricCategoryMap_, and |
| + * |idKey| would be 'metricID'. MetricCategoryMap_'s values each |
| + * have a name and details property. The details property in |
| + * this case would be an array of MetricDetails, each having properties |
| + * metricId, name, and color. The |check| and |uncheck| parameters |
| + * would be addMetric and dropMetric. So, assuming the following data: |
| + * |
| + * optionCategoryMap : { |
|
Jeffrey Yasskin
2012/09/05 22:21:54
Ok, great. Now you can simplify the previous two p
clintstaley
2012/09/07 20:39:52
I rephrased the comment to put the concrete exampl
|
| + * 1: { |
| + * name: 'CPU', |
| + * details: [ |
| + * {metricId: 1, name: 'CPU Usage', color: 'rgb(255, 128, 128)'} |
| + * ], |
| + * 2: { |
| + * name : 'Memory', |
| + * details: [ |
| + * {metricId: 2, name: 'Private Memory Usage', |
| + * color: 'rgb(128, 255, 128)'}, |
| + * {metricId: 3, name: 'Shared Memory Usage', |
| + * color: 'rgb(128, 128, 255)'} |
| + * ] |
| + * } |
| + * |
| + * we'd get HTML thus, with each checkbox's check and uncheck events |
| + * handled by addMetric and dropMetric, with parameter equal to the |
| + * corresponding metricId of the metric in question. |
| + * |
| + * <div id="category-label-template" class="category-label">CPU</div> |
| + * <div id="detail-checkbox-template" class="detail-checkbox"> |
| + * <div class="horizontal-box"> |
| + * <input type="checkbox"> |
| + * <div class="detail-label">CPU Usage</div> |
| + * <div class="spacer"></div> |
| + * <div class="color-icon" |
| + * style="background-color: rgb(255, 128, 128);"></div> |
| + * </div> |
| + * </div> |
| + * </div> |
| + * |
| + * <div id="category-label-template" class="category-label">Memory</div> |
| + * <div id="detail-checkbox-template" class="detail-checkbox"> |
| + * <div class="horizontal-box"> |
| + * <input type="checkbox"> |
| + * <div class="detail-label">Private Memory Usage</div> |
| + * <div class="spacer"></div> |
| + * <div class="color-icon" |
| + * style="background-color: rgb(128, 255, 128);"></div> |
| + * </div> |
| + * </div> |
| + * </div><div id="detail-checkbox-template" class="detail-checkbox"> |
| + * <div class="horizontal-box"> |
| + * <input type="checkbox"> |
| + * <div class="detail-label">Shared Memory Usage</div> |
| + * <div class="spacer"></div> |
| + * <div class="color-icon" |
| + * style="background-color: rgb(128, 128, 255);"></div> |
| + * </div> |
| + * </div> |
| + * </div> |
| + * |
| * @param {!HTMLDivElement} div A <div> into which to put checkboxes. |
| - * @param {!Object} optionMap A map of metric/event entries. |
| + * @param {!Object} optionCategory Map A map of metric/event categories. |
|
Devlin
2012/09/07 18:44:54
nit: This should be optionCategoryMap (no space),
clintstaley
2012/09/07 20:39:52
Good eyes. Done
|
| + * @param {string} idKey The key of the id property. |
| * @param {!function(this:Controller, Object)} check |
| * The function to select an entry (metric or event). |
| * @param {!function(this:Controller, Object)} uncheck |
| * The function to deselect an entry (metric or event). |
| * @private |
| */ |
| - setupCheckboxes_: function(div, optionMap, check, uncheck) { |
| - var checkboxTemplate = $('#checkbox-template')[0]; |
| + setupCheckboxes_: function(div, optionCategoryMap, idKey, check, uncheck) { |
| + var checkboxTemplate = $('#detail-checkbox-template')[0]; |
| + var labelTemplate = $('#category-label-template')[0]; |
| - for (var option in optionMap) { |
| - var checkbox = checkboxTemplate.cloneNode(true); |
| - checkbox.querySelector('span').innerText = 'Show ' + |
| - optionMap[option].description; |
| - div.appendChild(checkbox); |
| + for (var c in optionCategoryMap) { |
| + var category = optionCategoryMap[c]; |
| + var label = labelTemplate.cloneNode(true); |
| - var input = checkbox.querySelector('input'); |
| - input.option = optionMap[option].id; |
| - input.addEventListener('change', function(e) { |
| - (e.target.checked ? check : uncheck).call(this, e.target.option); |
| - }.bind(this)); |
| + label.innerText = category.name; |
| + div.appendChild(label); |
| + |
| + category.details.forEach(function(details) { |
| + var checkbox = checkboxTemplate.cloneNode(true); |
| + var input = checkbox.querySelector('input'); |
| + var label = checkbox.querySelector('.detail-label'); |
| + var icon = checkbox.querySelector('.color-icon'); |
| + |
| + input.option = details[idKey]; |
| + input.icon = icon; |
| + input.addEventListener('change', function(e) { |
| + (e.target.checked ? check : uncheck).call(this, e.target.option); |
| + e.target.icon.style.visibility = |
| + e.target.checked ? 'visible' : 'hidden'; |
| + }.bind(this)); |
| + |
| + label.innerText = details.name; |
| + |
| + icon.style.backgroundColor = details.color; |
| + |
| + div.appendChild(checkbox); |
| + }, this); |
| } |
| }, |
| /** |
| - * Set up just one chart in which all metrics will be displayed |
| - * initially. But, the design readily accommodates addition of |
| - * new charts, and movement of metrics into those other charts. |
| + * Add a new chart for |category|, making it initially hidden, |
| + * with no metrics displayed in it. |
| + * @param {!Object} category The metric category for which to create |
| + * the chart. Category is a value from metricCategoryMap_. |
| * @private |
| */ |
| - setupMainChart_: function() { |
| - this.chartParent = $('#charts')[0]; |
| - this.charts = [document.createElement('div')]; |
| - this.charts[0].className = 'chart'; |
| - this.chartParent.appendChild(this.charts[0]); |
| + addCategoryChart_: function(category) { |
| + var chartParent = $('#charts')[0]; |
| + var chart = document.createElement('div'); |
| + |
| + chart.className = 'chart'; |
| + chart.hidden = true; |
| + chartParent.appendChild(chart); |
| + this.charts_.push(chart); |
| + category.homeChart = chart; |
| + chart.refs = 0; |
| + chart.hovers = []; |
| + |
| + // Receive hover events from Flot. |
| + // Attached to chart will be properties 'hovers', a list of {x, div} |
| + // pairs. As pos events arrive, check each hover to see if it should |
| + // be hidden or made visible. |
| + $(chart).bind('plothover', function(event, pos, item) { |
| + var tolerance = this.range.resolution; |
| + |
| + chart.hovers.forEach(function(hover) { |
| + hover.div.hidden = hover.x < pos.x - tolerance || |
| + hover.x > pos.x + tolerance; |
| + }); |
| + |
| + }.bind(this)); |
| + |
| + $(window).resize(function() { |
| + this.lastResizeTime_ = Date.now(); |
| + if (this.resizeTimer_ == null) |
| + this.resizeTimer_ = setInterval(this.checkResize_.bind(this), 100); |
|
Jeffrey Yasskin
2012/09/05 22:21:54
Ah, *I* had missed that you reset lastResizeTime o
clintstaley
2012/09/07 20:39:52
I considered this, but didn't like recreating and
Jeffrey Yasskin
2012/09/07 21:27:17
I don't care about this enough to insist either wa
clintstaley
2012/09/07 21:46:14
I changed it anyway. Code is cleaner, at least.
|
| + }.bind(this)); |
| + }, |
| + |
| + /** |
| + * Check to see if resize events have recently occured, and if a long |
|
Devlin
2012/09/07 18:44:54
nit: occurred
clintstaley
2012/09/07 20:39:52
Done.
|
| + * enough time has passed since the last one to justify actually redrawing |
| + * all the charts. If so, redraw them all, and clear the resize state. |
| + * @private |
| + */ |
| + checkResize_: function() { |
| + if (this.lastResizeTime_ != null && Date.now() - this.lastResizeTime_ > |
| + resizeDelay_) { |
| + this.lastResizeTime_ = null; |
| + |
| + // Timer's done its job. Turn it off till more resize events arrive. |
| + clearInterval(this.resizeTimer_); |
| + this.resizeTimer_ = null; |
| + |
| + this.charts_.forEach(function(chart) {this.drawChart(chart);}, this); |
| + } |
| }, |
| /** |
| * Set the time range for which to display metrics and events. For |
| - * now, the time range always ends at "now", but future implementations |
| + * now, the time range always ends at 'now', but future implementations |
| * may allow time ranges not so anchored. |
| * @param {!{start: number, end: number, resolution: number}} range |
| * The time range for which to get display data. |
| + * @param {number} end Ending time, in ms since epoch, to which to |
| + * set the new time range. |
| + * @param {boolean} startTimer Indicates whether we should restart the |
| + * range-update timer. |
| */ |
| - setTimeRange: function(range) { |
| + setTimeRange: function(range, end, startTimer) { |
| this.range = range; |
| - this.end = Math.floor(Date.now() / range.resolution) * |
| + if (this.updateTimer_ != null) |
| + clearInterval(this.updateTimer_); |
| + if (startTimer) |
| + this.updateTimer_ = setInterval(this.forwardTime.bind(this), |
| + intervalMultiple_ * range.resolution); |
| + this.end = Math.floor(end / range.resolution) * |
| range.resolution; |
| this.start = this.end - range.timeSpan; |
| @@ -252,6 +547,25 @@ cr.define('performance_monitor', function() { |
| }, |
| /** |
| + * Back up the time range by 1/2 of its current span, and cause chart |
| + * redraws. |
| + */ |
| + backTime: function() { |
| + this.setTimeRange(this.range, this.end - this.range.timeSpan / 2, false); |
| + }, |
| + |
| + /** |
| + * Advance the time range by 1/2 of its current span, or up to the point |
| + * where it ends at the present time, whichever is less. |
| + */ |
| + forwardTime: function() { |
| + var now = Date.now(); |
| + var newEnd = Math.min(now, this.end + this.range.timeSpan / 2); |
| + |
| + this.setTimeRange(this.range, newEnd, newEnd == now); |
| + }, |
| + |
| + /** |
| * Request activity intervals in the current time range. |
| */ |
| requestIntervals: function() { |
| @@ -268,26 +582,34 @@ cr.define('performance_monitor', function() { |
| getActiveIntervalsCallback: function(intervals) { |
| this.intervals_ = intervals; |
| - for (var metric in this.metricMap_) { |
| - var metricValue = this.metricMap_[metric]; |
| - if (metricValue.divs.length > 0) // if we're displaying this metric. |
| - this.refreshMetric(metric); |
| + for (var metric in this.metricDetailsMap_) { |
| + var metricDetails = this.metricDetailsMap_[metric]; |
| + if (metricDetails.divs.length > 0) // if we're displaying this metric. |
| + this.refreshMetric(metricDetails); |
| } |
| - for (var eventType in this.eventMap_) { |
| - var eventValue = this.eventMap_[eventType]; |
| + for (var eventType in this.eventDetailsMap_) { |
| + var eventValue = this.eventDetailsMap_[eventType]; |
| if (eventValue.divs.length > 0) |
| - this.refreshEventType(eventValue.id); |
| + this.refreshEventType(eventValue.eventId); |
| } |
| }, |
| /** |
| - * Add a new metric to the main (currently only) chart. |
| - * @param {string} metric The metric to start displaying. |
| + * Add a new metric to the display, showing it on its category's home |
| + * chart. Un-hide the home chart if needed. |
| + * @param {number} metricId The id of the metric to start displaying. |
| */ |
| - addMetric: function(metric) { |
| - this.metricMap_[metric].divs.push(this.charts[0]); |
| - this.refreshMetric(metric); |
| + addMetric: function(metricId) { |
| + var details = this.metricDetailsMap_[metricId]; |
| + var homeChart = details.category.homeChart; |
| + |
| + details.divs.push(homeChart); |
| + if (homeChart.refs == 0) |
| + homeChart.hidden = false; |
| + homeChart.refs++; |
| + |
| + this.refreshMetric(details); |
| }, |
| /** |
| @@ -295,23 +617,27 @@ cr.define('performance_monitor', function() { |
| * @param {string} metric The metric to stop displaying. |
| */ |
| dropMetric: function(metric) { |
| - var metricValue = this.metricMap_[metric]; |
| - var affectedCharts = metricValue.divs; |
| - metricValue.divs = []; |
| + var details = this.metricDetailsMap_[metric]; |
| + var affectedCharts = details.divs; |
| + |
| + details.divs = []; // Gotta do this now to get correct drawChart result. |
| + affectedCharts.forEach(function(chart) { |
| + chart.refs--; |
| + if (chart.refs == 0) |
| + chart.hidden = true; |
| + this.drawChart(chart); |
| + }, this); |
| - affectedCharts.forEach(this.drawChart, this); |
| }, |
| /** |
| * Request new metric data, assuming the metric table and chart already |
| * exist. |
| - * @param {string} metric The metric for which to get data. |
| + * @param {string} metricDetails The metric details for which to get data. |
| */ |
| - refreshMetric: function(metric) { |
| - var metricValue = this.metricMap_[metric]; |
| - |
| - metricValue.data = null; // Mark metric as awaiting response. |
| - chrome.send('getMetric', [metricValue.id, |
| + refreshMetric: function(metricDetails) { |
| + metricDetails.data = null; // Mark metric as awaiting response. |
| + chrome.send('getMetric', [metricDetails.metricId, |
| this.start, this.end, this.range.resolution]); |
| }, |
| @@ -319,43 +645,48 @@ cr.define('performance_monitor', function() { |
| * Receive new datapoints for a metric, convert the data to Flot-usable |
| * form, and redraw all affected charts. |
| * @param {!{ |
| - * type: number, |
| - * points: !Array.<{time: number, value: number}> |
| - * }} result An object giving metric ID, and time/value point pairs for |
| - * that id. |
| + * id: number, |
| + * max: number, |
| + * metrics: !Array.<{time: number, value: number}> |
| + * }} result An object giving metric ID, max expected value of the data, |
| + * and time/value point pairs for that id. |
| */ |
| getMetricCallback: function(result) { |
| - var metricValue = this.metricMap_[result.metricType]; |
| + |
| + var metricDetails = this.metricDetailsMap_[result.metricId]; |
| // Might have been dropped while waiting for data. |
| - if (metricValue.divs.length == 0) |
| + if (metricDetails.divs.length == 0) |
| return; |
| var series = []; |
| - metricValue.data = [series]; // Data ends with current open series. |
| + metricDetails.data = [series]; // Data ends with current open series. |
| // Traverse the points, and the intervals, in parallel. Both are in |
| - // ascending time order. Create a sequence of data "series" (per Flot) |
| + // ascending time order. Create a sequence of data 'series' (per Flot) |
| // arrays, with each series comprising all points within a given interval. |
| var interval = this.intervals_[0]; |
| var intervalIndex = 0; |
| var pointIndex = 0; |
| - while (pointIndex < result.points.length && |
| + while (pointIndex < result.metrics.length && |
| intervalIndex < this.intervals_.length) { |
| - var point = result.points[pointIndex++]; |
| + var point = result.metrics[pointIndex++]; |
| while (intervalIndex < this.intervals_.length && |
| point.time > interval.end) { |
| interval = this.intervals_[++intervalIndex]; // Jump to new interval. |
| if (series.length > 0) { |
| series = []; // Start a new series. |
| - metricValue.data.push(series); // Put it on the end of the data. |
| + metricDetails.data.push(series); // Put it on the end of the data. |
| } |
| } |
| if (intervalIndex < this.intervals_.length && |
| point.time > interval.start) { |
| - series.push([point.time - timezoneOffset, point.value]); |
| + series.push([point.time - timezoneOffset_, point.value]); |
| } |
| } |
| - metricValue.divs.forEach(this.drawChart, this); |
| + metricDetails.maxValue = Math.max(metricDetails.maxValue, |
| + result.maxValue); |
| + |
| + metricDetails.divs.forEach(this.drawChart, this); |
| }, |
| /** |
| @@ -364,7 +695,7 @@ cr.define('performance_monitor', function() { |
| */ |
| addEventType: function(eventType) { |
| // Events show on all charts. |
| - this.eventMap_[eventType].divs = this.charts; |
| + this.eventDetailsMap_[eventType].divs = this.charts_; |
| this.refreshEventType(eventType); |
| }, |
| @@ -373,10 +704,10 @@ cr.define('performance_monitor', function() { |
| * @param {string} eventType The type of event to stop displaying. |
| */ |
| dropEventType: function(eventType) { |
| - var eventValue = this.eventMap_[eventType]; |
| + var eventValue = this.eventDetailsMap_[eventType]; |
| var affectedCharts = eventValue.divs; |
| - eventValue.divs = []; |
| + eventValue.divs = []; // Gotta do this now for correct drawChart results. |
| affectedCharts.forEach(this.drawChart, this); |
| }, |
| @@ -386,7 +717,7 @@ cr.define('performance_monitor', function() { |
| */ |
| refreshEventType: function(eventType) { |
| // Mark eventType as awaiting response. |
| - this.eventMap_[eventType].data = null; |
| + this.eventDetailsMap_[eventType].data = null; |
| chrome.send('getEvents', [eventType, this.start, this.end]); |
| }, |
| @@ -395,24 +726,26 @@ cr.define('performance_monitor', function() { |
| * Receive new events for |eventType|. If |eventType| has been deselected |
| * while awaiting webui handler response, do nothing. Otherwise, save the |
| * data directly, since events are handled differently than metrics |
| - * when drawing (no "series"), and redraw all the affected charts. |
| + * when drawing (no 'series'), and redraw all the affected charts. |
| * @param {!{ |
| - * type: number, |
| - * points: !Array.<{time: number, longDescription: string}> |
| - * }} result An object giving eventType ID, and time/description pairs for |
| - * each event of that type in the range requested. |
| + * id: number, |
| + * events: !Array.<{time: number}> |
| + * }} result An object giving eventType id, and times at which that event |
| + * type occured in the range requested. Each object in the array may |
| + * also have an arbitrary list of properties to be displayed as |
| + * a tooltip message for the event. |
| */ |
| getEventsCallback: function(result) { |
| - var eventValue = this.eventMap_[result.eventType]; |
| + var eventValue = this.eventDetailsMap_[result.eventId]; |
| if (eventValue.divs.length == 0) |
| return; |
| - result.points.forEach(function(element) { |
| - element.time -= timezoneOffset; |
| + result.events.forEach(function(element) { |
| + element.time -= timezoneOffset_; |
| }); |
| - eventValue.data = result.points; |
| + eventValue.data = result.events; |
| eventValue.divs.forEach(this.drawChart, this); |
| }, |
| @@ -426,15 +759,15 @@ cr.define('performance_monitor', function() { |
| getChartData_: function(chart) { |
| var result = {metrics: [], events: []}; |
| - for (var metric in this.metricMap_) { |
| - var metricValue = this.metricMap_[metric]; |
| + for (var metric in this.metricDetailsMap_) { |
| + var metricDetails = this.metricDetailsMap_[metric]; |
| - if (metricValue.divs.indexOf(chart) != -1) |
| - result.metrics.push(metricValue); |
| + if (metricDetails.divs.indexOf(chart) != -1) |
| + result.metrics.push(metricDetails); |
| } |
| - for (var eventType in this.eventMap_) { |
| - var eventValue = this.eventMap_[eventType]; |
| + for (var eventType in this.eventDetailsMap_) { |
| + var eventValue = this.eventDetailsMap_[eventType]; |
| // Events post to all divs, if they post to any. |
| if (eventValue.divs.length > 0) |
| @@ -455,24 +788,15 @@ cr.define('performance_monitor', function() { |
| * @private |
| */ |
| isDataReady_: function(chartData) { |
| - for (var i = 0; i < chartData.metrics.length; i++) { |
| - if (!chartData.metrics[i].data) |
| - return false; |
| - } |
| - |
| - for (var i = 0; i < chartData.events.length; i++) { |
| - if (!chartData.events[i].data) |
| - return false; |
| - } |
| - |
| - return true; |
| + return chartData.metrics.every(function(metric) {return metric.data;}) && |
| + chartData.events.every(function(event) {return event.data;}); |
| }, |
| /** |
| - * Create and return an array of "markings" (per Flot), representing |
| + * Create and return an array of 'markings' (per Flot), representing |
| * vertical lines at the event time, in the event's color. Also add |
| - * (not per Flot) a |description| property to each, to be used for hand |
| - * creating description boxes. |
| + * (not per Flot) a |popupTitle| property to each, to be used for |
| + * labelling description popups. |
| * @param {!Array.<{ |
| * description: string, |
| * color: string, |
| @@ -487,24 +811,42 @@ cr.define('performance_monitor', function() { |
| */ |
| getEventMarks_: function(eventValues) { |
| var markings = []; |
| - |
| - for (var i = 0; i < eventValues.length; i++) { |
| - var eventValue = eventValues[i]; |
| - for (var d = 0; d < eventValue.data.length; d++) { |
| - var point = eventValue.data[d]; |
| - if (point.time >= this.start - timezoneOffset && |
| - point.time <= this.end - timezoneOffset) { |
| + var explanation; |
| + var date, hours, minutes; |
| + |
| + eventValues.forEach(function(eventValue) { |
| + eventValue.data.forEach(function(point) { |
| + if (point.time >= this.start - timezoneOffset_ && |
| + point.time <= this.end - timezoneOffset_) { |
| + |
| + // Date wants Zulu time. |
| + date = new Date(point.time + timezoneOffset_); |
| + hours = date.getHours(); |
| + minutes = date.getMinutes(); |
| + explanation = eventValue.popupTitle + '\nAt: ' + |
| + (date.getMonth() + 1) + '/' + date.getDate() + ' ' + |
| + (hours < 10 ? '0' : '') + hours + ':' + |
| + (minutes < 10 ? '0' : '') + minutes + '\n'; |
| + |
| + for (var key in point) { |
| + if (key != 'time') { |
| + var datum = point[key]; |
| + if ('label' in datum && 'value' in datum) |
| + explanation = explanation + datum.label + ': ' + |
| + datum.value + '\n'; |
| + } |
| + } |
| markings.push({ |
| color: eventValue.color, |
| - description: eventValue.description, |
| + popupContent: explanation, |
| xaxis: {from: point.time, to: point.time} |
| }); |
| } else { |
| console.log('Event out of time range ' + this.start + ' -> ' + |
| this.end + ' at: ' + point.time); |
| } |
| - } |
| - } |
| + }, this); |
| + }, this); |
| return markings; |
| }, |
| @@ -517,40 +859,52 @@ cr.define('performance_monitor', function() { |
| */ |
| drawChart: function(chart) { |
| var chartData = this.getChartData_(chart); |
| + var axisMap = {}; // Maps category ids to yAxis numbers |
|
Devlin
2012/09/07 18:44:54
nit: |yAxisNumber|s? Make it clear this is a varia
clintstaley
2012/09/07 20:39:52
I'm going to differ on this. yAxisNumber is just
|
| - if (!this.isDataReady_(chartData)) |
| + if (chart.hidden || !this.isDataReady_(chartData)) |
| return; |
| var seriesSeq = []; |
| var yAxes = []; |
| - chartData.metrics.forEach(function(value) { |
| - yAxes.push(value.yAxis); |
| - for (var i = 0; i < value.data.length; i++) { |
| + chartData.metrics.forEach(function(metricDetails) { |
| + var metricCategory = metricDetails.category; |
| + var yAxisNumber = axisMap[metricCategory.metricCategoryId]; |
| + |
| + // Add a new y-axis if we are encountering this category of metric |
| + // for the first time. Otherwise, update the existing y-axis with |
| + // a new max value if needed. (Presently, we expect only one category |
| + // of metric per chart, but this design permits more in the future.) |
| + if (yAxisNumber === undefined) { |
| + yAxes.push({min: 0, max: metricDetails.maxValue * yAxisMargin_, |
| + labelWidth: 60}); |
| + axisMap[metricCategory.metricCategoryId] = yAxisNumber = yAxes.length; |
| + } else { |
| + yAxes[yAxisNumber - 1].max = Math.max(yAxes[yAxisNumber - 1].max, |
| + metricDetails.maxValue * yAxisMargin_); |
| + } |
| + |
| + for (var i = 0; i < metricDetails.data.length; i++) { |
| seriesSeq.push({ |
| - color: value.yAxis.color, |
| - data: value.data[i], |
| - label: i == 0 ? value.description + ' (' + value.units + ')' : null, |
| - yaxis: yAxes.length, // Use just-added Y axis. |
| + color: metricDetails.color, |
| + data: metricDetails.data[i], |
| + label: i == 0 ? metricDetails.name + |
| + ' (' + metricCategory.unit + ')' : null, |
| + yaxis: yAxisNumber |
| }); |
| } |
| }); |
| - // Ensure at least one y axis, as a reference for event placement. |
| - if (yAxes.length == 0) |
| - yAxes.push({max: 1.0}); |
| - |
| var markings = this.getEventMarks_(chartData.events); |
| - var chart = this.charts[0]; |
| var plot = $.plot(chart, seriesSeq, { |
| yaxes: yAxes, |
| xaxis: { |
| mode: 'time', |
| - min: this.start - timezoneOffset, |
| - max: this.end - timezoneOffset |
| + min: this.start - timezoneOffset_, |
| + max: this.end - timezoneOffset_ |
| }, |
| points: {show: true, radius: 1}, |
| lines: {show: true}, |
| - grid: {markings: markings} |
| + grid: {markings: markings, hoverable: true, autoHighlight: true} |
| }); |
| // For each event in |markings|, create also a label div, with left |
| @@ -563,11 +917,13 @@ cr.define('performance_monitor', function() { |
| var point = |
| plot.pointOffset({x: mark.xaxis.to, y: yAxes[0].max, yaxis: 1}); |
| var labelDiv = labelTemplate.cloneNode(true); |
| - labelDiv.innerText = mark.description; |
| + labelDiv.innerText = mark.popupContent; |
| labelDiv.style.left = point.left + 'px'; |
| - labelDiv.style.top = (point.top + 25 * (i % 3)) + 'px'; |
| + labelDiv.style.top = (point.top + 100 * (i % 3)) + 'px'; |
| chart.appendChild(labelDiv); |
| + labelDiv.hidden = true; |
| + chart.hovers.push({x: mark.xaxis.to, div: labelDiv}); |
| } |
| } |
| }; |