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 4497f588e10a4d017540819932c605ee7312907e..1f0f4ef3804c6d9604596d59f8089d53ab747b3a 100644 |
--- a/chrome/browser/resources/performance_monitor/chart.js |
+++ b/chrome/browser/resources/performance_monitor/chart.js |
@@ -4,359 +4,591 @@ |
'use strict'; |
-google.load('visualization', '1', {packages: ['corechart']}); |
- |
-function $(criterion) { |
- return document.querySelector(criterion); |
-} |
- |
-var controller = new function() { |
- // Tabular setup for various time ranges, giving a descriptive name, time span |
- // prior to |now|, data point resolution, and time-label frequency and format |
- // for each. |
- this.TimeRange = { |
- // Prior day, resolution of 2 min, at most 720 points, |
- // Labels at 90 point (3 hour) intervals |
+/* convert to cr.define('PerformanceMonitor', function() {... |
+ * when integrating into webui */ |
+ |
+var Installer = function() { |
+ /** |
+ * 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 {{ |
+ * value: !number, |
+ * name: !string, |
+ * timeSpan: !number, |
+ * resolution: !number, |
+ * labelEvery: !number, |
+ * format: !string |
+ * }} |
+ * @private |
+ */ |
+ var TimeRange_ = { |
+ // Prior day, resolution of 2 min, at most 720 points. |
+ // Labels at 90 point (3 hour) intervals. |
day: {value: 0, name: 'Last Day', timeSpan: 24 * 3600 * 1000, |
resolution: 1000 * 60 * 2, labelEvery: 90, format: 'MM-dd'}, |
- // Prior week, resolution of 15 min -- at most 672 data points |
- // Labels at 96 point (daily) intervals |
+ // Prior week, resolution of 15 min -- at most 672 data points. |
+ // Labels at 96 point (daily) intervals. |
week: {value: 1, name: 'Last Week', timeSpan: 7 * 24 * 3600 * 1000, |
resolution: 1000 * 60 * 15, labelEvery: 96, 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 1 hr -- at most 720 data points. |
+ // Labels at 168 point (weekly) intervals. |
month: {value: 2, name: 'Last Month', timeSpan: 30 * 24 * 3600 * 1000, |
resolution: 1000 * 3600, labelEvery: 168, 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 3 hr -- at most 720 data points. |
+ // Labels at 112 point (fortnightly) intervals. |
quarter: {value: 3, name: 'Last Quarter', timeSpan: 90 * 24 * 3600 * 1000, |
resolution: 1000 * 3600 * 3, labelEvery: 112, format: 'M/yy'}, |
}; |
- // Parent container of all line graphs |
- this.chartDiv = $('#charts'); |
- |
- // Parent container of checkboxes to choose metrics to display |
- this.chooseMetricsDiv = $('#chooseMetrics'); |
- |
- // Parent container of checkboxes to choose event types to display |
- this.chooseEventsDiv = $('#chooseEvents'); |
- |
- // Parent container of radio buttons to select time range |
- this.timeDiv = $('#chooseTimeRange'); |
- |
- this.metricMap = {}; // MetricName => {div, lineChart, dataTable} objects |
- this.eventMap = {}; // EventName => event point lists, as returned by webui |
- this.intervals = []; // Array of objects {start, end} |
- |
- this.setTimeRange = function(range) { |
- this.range = range; |
- this.end = Math.floor(new Date().getTime() / range.resolution) * |
- range.resolution; |
- this.start = this.end - range.timeSpan; |
- this.requestIntervals(); |
- } |
- |
- // Return mock interval set for testing |
- this.getMockIntervals = function() { |
- var interval = this.end - this.start; |
- return [ |
- {'start': this.start + interval * .1, |
- 'end': this.start + interval * .2}, |
- {'start': this.start + interval * .7, 'end': this.start + interval * .9} |
- ]; |
- } |
- // Request array of objects with start and end fields showing browser |
- // activity intervals in the specified time range |
- this.requestIntervals = function() { |
- this.onReceiveIntervals(this.getMockIntervals()); |
- // Replace with: chrome.send('getIntervals', this.start, this.end, |
- // this.onReceiveIntervals); |
+ /** @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 response. Thus, any metric with |
+ * non-empty div list but null data is awaiting a data response from the |
+ * webui. |
+ * @type {Object.<string, { |
+ * divs: !Array.<HTMLDivElement>, |
+ * yAxis: !{max: !number, color: !string}, |
+ * data: ?Array.<{time: !number, value: !number}>, |
+ * description: !string, |
+ * units: !string |
+ * }>} |
+ * @private |
+ */ |
+ this.metricMap_ = { |
+ jankiness: { |
+ divs: [], |
+ yAxis: {max: 100, color: 'rgb(255, 128, 128)'}, |
+ data: null, |
+ description: 'Jankiness', |
+ units: 'milliJanks' |
+ }, |
+ oddness: { |
+ divs: [], |
+ yAxis: {max: 20, color: 'rgb(0, 192, 0)'}, |
+ data: null, |
+ description: 'Oddness', |
+ units: 'kOdds' |
+ } |
+ }; |
+ |
+ /* |
+ * 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.<string, { |
+ * divs: !Array.<HTMLDivElement>, |
+ * description: !string, |
+ * color: !string, |
+ * data: ?Array.<{time: !number, longDescription: !string}> |
+ * }>} |
+ * @private |
+ */ |
+ this.eventMap_ = { |
+ wampusAttacks: { |
+ divs: [], |
+ description: 'Wampus Attack', |
+ color: 'rgb(0, 0, 255)', |
+ data: null |
+ }, |
+ solarEclipses: { |
+ divs: [], |
+ description: 'Solar Eclipse', |
+ color: 'rgb(255, 0, 255)', |
+ data: null |
+ } |
+ }; |
+ |
+ /** |
+ * Time periods in which the browser was active and collecting metrics. |
+ * and events. |
+ * @type {!Array.<{start: !number, end: !number}>} |
+ * @private |
+ */ |
+ this.intervals_ = []; |
+ |
+ this.setupCheckboxes_( |
+ '#chooseMetrics', this.metricMap_, this.addMetric, this.dropMetric); |
+ this.setupCheckboxes_( |
+ '#chooseEvents', this.eventMap_, this.addEventType, this.dropEventType); |
+ this.setupTimeRangeChooser_(); |
+ this.setupMainChart_(); |
+ TimeRange_.day.element.click().call(this); |
} |
- // Webui callback delivering response from requestIntervals call. Assumes |
- // this is a new time range choice, which results in complete refresh of |
- // all metrics and event types that are currently selected. |
- this.onReceiveIntervals = function(intervals) { |
- this.intervals = intervals; |
- for (var metric in this.metricMap) |
- this.refreshMetric(metric); |
- for (var eventType in this.eventMap) |
- this.refreshEvent(eventType); |
- } |
+ PerformanceMonitor.prototype = { |
+ /** |
+ * Set up the radio button set to choose time range. Use div#radioTemplate |
+ * as a template. |
+ * @private |
+ */ |
+ setupTimeRangeChooser_: function() { |
+ var timeDiv = $('#chooseTimeRange')[0]; |
+ var radioTemplate = $('#radioTemplate')[0]; |
+ |
+ for (var time in TimeRange_) { |
+ var timeRange = TimeRange_[time]; |
+ var radio = radioTemplate.cloneNode(true); |
+ var input = radio.querySelector('input'); |
+ |
+ input.value = timeRange.value; |
+ input.timeRange = timeRange; |
+ radio.querySelector('span').innerText = timeRange.name; |
+ timeDiv.appendChild(radio); |
+ timeRange.element = input; |
+ } |
- // Add a new metric, and its associated linegraph. The linegraph for |
- // each metric has a discrete domain of times. This is not continuous |
- // because of breaks for each interval of activity. (No point in showing |
- // a lot of "dead air" when the browser wasn't running.) Column 1 of |
- // its DataTable is the metric data, and higher numbered columns are added |
- // in pairs for each event type currently chosen. Each pair gives the |
- // event occurence points, and mouseover detailed description for one event |
- // type. |
- this.addMetric = function(metric) { |
- if (!(metric in this.metricMap)) { |
- var div = document.createElement('div'); |
- |
- div.className = 'chart'; |
- this.chartDiv.appendChild(div); |
- |
- var table = new google.visualization.DataTable(); |
- var chart = new google.visualization.LineChart(div); |
- this.metricMap[metric] = {'div': div, 'chart': chart, 'table': table}; |
- |
- table.addColumn('string', 'Time'); // Default domain column |
- table.addColumn('number', 'Value'); // Only numerical range column |
- for (var event in this.events) |
- this.addEventColumns(table, event); |
+ timeDiv.addEventListener('click', function(e) { |
+ if (!e.target.webkitMatchesSelector('input[type="radio"]')) |
+ return; |
+ |
+ this.setTimeRange(e.target.timeRange); |
+ }.bind(this)); |
+ }, |
+ |
+ /** |
+ * Generalized function for setting up checkbox blocks for either events |
+ * or metrics. Take a div ID |divId| 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. |
+ * @param {!string} divId Id of division into which to put checkboxes |
+ * @param {!Object} optionMap map of metric/event entries |
+ * @param {!function(this:Controller, Object)} check |
+ * function to select an entry (metric or event) |
+ * @param {!function(this:Controller, Object)} uncheck |
+ * function to deselect an entry (metric or event) |
+ * @private |
+ */ |
+ setupCheckboxes_: function(divId, optionMap, check, uncheck) { |
+ var checkboxTemplate = $('#checkboxTemplate')[0]; |
+ var chooseMetricsDiv = $(divId)[0]; |
+ |
+ for (var option in optionMap) { |
+ var checkbox = checkboxTemplate.cloneNode(true); |
+ checkbox.querySelector('span').innerText = 'Show ' + |
+ optionMap[option].description; |
+ chooseMetricsDiv.appendChild(checkbox); |
+ |
+ var input = checkbox.querySelector('input'); |
+ input.option = option; |
+ input.addEventListener('change', function(e) { |
+ if (e.target.checked) |
+ check.call(this, e.target.option); |
+ else |
+ uncheck.call(this, e.target.option); |
+ }.bind(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. |
+ * @private |
+ */ |
+ setupMainChart_: function() { |
+ this.chartParent = $('#charts')[0]; |
+ this.charts = [document.createElement('div')]; |
+ this.charts[0].className = 'chart'; |
+ this.chartParent.appendChild(this.charts[0]); |
+ }, |
+ |
+ /** |
+ * Set the time range for which to display metrics and events. For |
+ * 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 |
+ */ |
+ setTimeRange: function(range) { |
+ this.range = range; |
+ this.end = Math.floor(Date.now() / range.resolution) * |
+ range.resolution; |
+ |
+ // Take the GMT value of this.end ("now") and subtract from it the |
+ // number of minutes by which we lag GMT in the present timezone, |
+ // X mS/minute. This will show time in the present timezone. |
+ this.end -= new Date().getTimezoneOffset() * 60000; |
+ this.start = this.end - range.timeSpan; |
+ this.requestIntervals(); |
+ }, |
+ |
+ /** |
+ * Return mock interval set for testing. |
+ * @return {!Array.<{start: !number, end: !number}>} intervals |
+ */ |
+ getMockIntervals: function() { |
+ var interval = this.end - this.start; |
+ |
+ return [ |
+ {start: this.start + interval * .1, |
+ end: this.start + interval * .2}, |
+ {start: this.start + interval * .7, |
+ end: this.start + interval} |
+ ]; |
+ }, |
+ |
+ /** |
+ * Request activity intervals in the current time range. |
+ */ |
+ requestIntervals: function() { |
+ this.onReceiveIntervals(this.getMockIntervals()); |
+ // Replace with: chrome.send('getIntervals', this.start, this.end, |
+ // this.onReceiveIntervals); |
+ }, |
+ |
+ /** |
+ * Webui callback delivering response from requestIntervals call. Assumes |
+ * this is a new time range choice, which results in complete refresh of |
+ * all metrics and event types that are currently selected. |
+ * @param {!Array.<{start: !number, end: !number}>} intervals new intervals |
+ */ |
+ onReceiveIntervals: 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 eventType in this.eventMap_) { |
+ var eventValue = this.eventMap_[eventType]; |
+ if (eventValue.divs.length > 0) |
+ this.refreshEventType(eventType); |
+ } |
+ }, |
+ |
+ /** |
+ * Add a new metric to the main (currently only) chart. |
+ * @param {!string} metric Metric to start displaying |
+ */ |
+ addMetric: function(metric) { |
+ this.metricMap_[metric].divs.push(this.charts[0]); |
this.refreshMetric(metric); |
- } |
- } |
- |
- // Remove a metric from the UI |
- this.dropMetric = function(metric) { |
- if (metric in this.metricMap) { |
- this.chartDiv.removeChild(this.metricMap[metric].div); |
- delete this.metricMap[metric]; |
- } |
- } |
- |
- // Return mock metric data points for testing |
- this.getMockDataPoints = function() { |
- var dataPoints = []; |
- |
- for (var x = 0; x < this.intervals.length; x++) { |
- // Rise from low 0 to high 100 every 20 min |
- for (var time = this.intervals[x].start; time <= this.intervals[x].end; |
- time += this.range.resolution) |
- dataPoints.push({'time': time, 'value': time % 1000000 / 10000}); |
- } |
- return dataPoints; |
- } |
- |
- // Request new metric data, assuming the metric table and chart already |
- // exist. |
- this.refreshMetric = function(metric) { |
- this.onReceiveMetric(metric, this.getMockDataPoints()); |
- // Replace with: |
- // chrome.send("getMetric", this.range.start, this.range.end, |
- // this.range.resolution, this.onReceiveMetric); |
- } |
- |
- // Receive new datapoints for |metric|, and completely refresh the DataTable |
- // for that metric, redrawing the chart. (We cannot preserve the event |
- // columns because entirely new rows may be implied by the new metric |
- // datapoints.) |
- this.onReceiveMetric = function(metric, points) { |
- // Might have been dropped while waiting for data |
- if (!(metric in this.metricMap)) |
- return; |
- |
- var data = this.metricMap[metric].table; |
- |
- data.removeRows(0, data.getNumberOfRows()); |
- |
- // Traverse the points, which are in time order, and the intervals, |
- // placing each value in the interval (if any) in which it belongs. |
- var interval = this.intervals[0]; |
- var intervalIndex = 0; |
- var valueIndex = 0; |
- var value; |
- while (valueIndex < points.length && |
- intervalIndex < this.intervals.length) { |
- value = points[valueIndex++]; |
- while (value.time > interval.end && |
- intervalIndex < this.intervals.length) { |
- interval = this.intervals[++intervalIndex]; // Jump to new interval |
- data.addRow(null, null); // Force gap in line chart |
+ }, |
+ |
+ /** |
+ * Remove a metric from the chart(s). |
+ * @param {!string} metric Metric to stop displaying |
+ */ |
+ dropMetric: function(metric) { |
+ var metricValue = this.metricMap_[metric]; |
+ var affectedCharts = metricValue.divs; |
+ metricValue.divs = []; |
+ |
+ affectedCharts.forEach(this.drawChart, this); |
+ }, |
+ |
+ /** |
+ * Return mock metric data points for testing. Give values ranging from |
+ * offset to max-offset. (This lets us avoid direct overlap of |
+ * different mock data sets in an ugly way that will die in the next |
+ * version anyway.) |
+ * @param {!number} max Max data value to return, less offset |
+ * @param {!number} offset Adjustment factor |
+ * @return {!Array.<{time: !number, value: !number}>} |
+ */ |
+ getMockDataPoints: function(max, offset) { |
+ var dataPoints = []; |
+ |
+ for (var i = 0; i < this.intervals_.length; i++) { |
+ // Rise from low offset to high max-offset in 100 point steps. |
+ for (var time = this.intervals_[i].start; |
+ time <= this.intervals_[i].end; time += this.range.resolution) { |
+ var numPoints = time / this.range.resolution; |
+ dataPoints.push({time: time, value: offset + (numPoints % 100) * |
+ (max - 2 * offset) / 100}); |
+ } |
+ } |
+ return dataPoints; |
+ }, |
+ |
+ /** |
+ * Request new metric data, assuming the metric table and chart already |
+ * exist. |
+ * @param {!string} metric Metric for which to get data |
+ */ |
+ refreshMetric: function(metric) { |
+ var metricValue = this.metricMap_[metric]; |
+ |
+ metricValue.data = null; // Mark metric as awaiting response. |
+ this.onReceiveMetric(metric, |
+ this.getMockDataPoints(metricValue.yAxis.max, 5)); |
+ // Replace with: |
+ // chrome.send("getMetric", this.range.start, this.range.end, |
+ // this.range.resolution, this.onReceiveMetric); |
+ }, |
+ |
+ /** |
+ * Receive new datapoints for |metric|, convert the data to Flot-usable |
+ * form, and redraw all affected charts. |
+ * @param {!string} metric Metric to which |points| applies |
+ * @param {!Array.<{time: !number, value: !number}>} points |
+ * new data points |
+ */ |
+ onReceiveMetric: function(metric, points) { |
+ var metricValue = this.metricMap_[metric]; |
+ |
+ // Might have been dropped while waiting for data. |
+ if (metricValue.divs.length == 0) |
+ return; |
+ |
+ var series = []; |
+ metricValue.data = [series]; |
+ |
+ // Traverse the points, and the intervals, in parallel. Both are in |
+ // 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 < points.length && |
+ intervalIndex < this.intervals_.length) { |
+ var point = points[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. |
+ } |
+ } |
+ if (intervalIndex < this.intervals_.length && |
+ point.time > interval.start) |
+ series.push([point.time, point.value]); |
} |
- if (intervalIndex < this.intervals.length && value.time > interval.start) |
- if (data.getNumberOfRows() % this.range.labelEvery == 0) |
- data.addRow([new Date(value.time).toString(this.range.format), |
- value.value]); |
- else |
- data.addRow(['', value.value]); |
- } |
- this.drawChart(metric); |
- } |
- |
- // Add a new event to all line graphs. |
- this.addEventType = function(eventType) { |
- if (!(eventType in this.eventMap)) { |
- this.eventMap[eventType] = []; |
- this.refreshEventType(eventType); |
- } |
- } |
- |
- // Return mock event point for testing |
- this.getMockEventValues = function(eventType) { |
- var mockValues = []; |
- for (var i = 0; i < this.intervals.length; i++) { |
- var interval = this.intervals[i]; |
- |
- mockValues.push({ |
- time: interval.start, |
- shortDescription: eventType, |
- longDescription: eventType + ' at ' + |
- new Date(interval.start) + ' blah, blah blah'}); |
- mockValues.push({ |
- time: (interval.start + interval.end) / 2, |
- shortDescription: eventType, |
- longDescription: eventType + ' at ' + |
- new Date((interval.start + interval.end) / 2) + ' blah, blah blah'}); |
- mockValues.push({ |
- time: interval.end, |
- shortDescription: eventType, |
- longDescription: eventType + ' at ' + new Date(interval.end) + |
- ' blah, blah blah'}); |
- } |
- return mockValues; |
- } |
- // Request new data for |eventType|, for times in the current range. |
- this.refreshEventType = function(eventType) { |
- this.onReceiveEventType(eventType, this.getMockEventValues(eventType)); |
- // Replace with: |
- // chrome.send("getEvents", eventType, this.range.start, this.range.end); |
- } |
+ metricValue.divs.forEach(this.drawChart, this); |
+ }, |
- // Add an event column pair to DataTable |table| for |eventType| |
- this.addEventColumns = function(table, eventType) { |
- var annotationCol = table.addColumn({'id': eventType, type: 'string', |
- role: 'annotation'}); |
- var rolloverCol = table.addColumn({'id': eventType + 'Tip', type: 'string', |
- role: 'annotationText'}); |
- var values = this.eventMap[eventType]; |
- var interval = this.intervals[0], intervalIndex = 0; |
- |
- for (var i = 0; i < values.length; i++) { |
- var event = values[i]; |
- var rowIndex = 0; |
- while (event.time > interval.end && |
- intervalIndex < this.intervals.length - 1) |
- { |
- // Skip interval times, inclusive of interval.end, and of following null |
- rowIndex += (interval.end - interval.start) / this.range.resolution + 2; |
- interval = this.intervals[++intervalIndex]; |
+ /** |
+ * Add a new event to the chart(s). |
+ * @param {!string} eventType type of event to start displaying |
+ */ |
+ addEventType: function(eventType) { |
+ // Events show on all charts. |
+ this.eventMap_[eventType].divs = this.charts; |
+ this.refreshEventType(eventType); |
+ }, |
+ |
+ /* |
+ * Remove an event from the chart(s). |
+ * @param {!string} eventType type of event to stop displaying |
+ */ |
+ dropEventType: function(eventType) { |
+ var eventValue = this.eventMap_[eventType]; |
+ var affectedCharts = eventValue.divs; |
+ eventValue.divs = []; |
+ |
+ affectedCharts.forEach(this.drawChart, this); |
+ }, |
+ |
+ /** |
+ * Return mock event points for testing. |
+ * @param {!string} eventType type of event to generate mock data for |
+ * @return {!Array.<{time: !number, longDescription: !string}>} |
+ */ |
+ getMockEventValues: function(eventType) { |
+ var mockValues = []; |
+ for (var i = 0; i < this.intervals_.length; i++) { |
+ var interval = this.intervals_[i]; |
+ |
+ mockValues.push({ |
+ time: interval.start, |
+ longDescription: eventType + ' at ' + |
+ new Date(interval.start) + ' blah, blah blah'}); |
+ mockValues.push({ |
+ time: (interval.start + interval.end) / 2, |
+ longDescription: eventType + ' at ' + |
+ new Date((interval.start + interval.end) / 2) + ' blah, blah'}); |
+ mockValues.push({ |
+ time: interval.end, |
+ longDescription: eventType + ' at ' + new Date(interval.end) + |
+ ' blah, blah blah'}); |
} |
- if (event.time >= interval.start && event.time <= interval.end) { |
- table.setCell(rowIndex + (event.time - interval.start) / |
- this.range.resolution, annotationCol, event.shortDescription); |
- table.setCell(rowIndex + (event.time - interval.start) / |
- this.range.resolution, rolloverCol, event.longDescription); |
+ return mockValues; |
+ }, |
+ |
+ /** |
+ * Request new data for |eventType|, for times in the current range. |
+ * @param {!string} eventType type of event to get new data for |
+ */ |
+ refreshEventType: function(eventType) { |
+ // Mark eventType as awaiting response. |
+ this.eventMap_[eventType].data = null; |
+ this.onReceiveEventType(eventType, this.getMockEventValues(eventType)); |
+ // Replace with: |
+ // chrome.send("getEvents", eventType, this.range.start, this.range.end); |
+ }, |
+ |
+ /** |
+ * Receive new data for |eventType|. If the event has been deselected while |
+ * awaiting webui 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. |
+ * @param {!string} eventType type of event the new data applies to |
+ * @param {!Array.<{time: !number, longDescription: !string}>} values |
+ * new event values |
+ */ |
+ onReceiveEventType: function(eventType, values) { |
+ var eventValue = this.eventMap_[eventType]; |
+ |
+ if (eventValue.divs.length == 0) |
+ return; |
+ |
+ eventValue.data = values; |
+ eventValue.divs.forEach(this.drawChart, this); |
+ }, |
+ |
+ /** |
+ * Return an object containing an array of metrics and another of events |
+ * that include |chart| as one of the divs into which they display. |
+ * @param {HTMLDivElement} chart div for which to get relevant items |
+ * @return {!{metrics: !Array,<Object>, events: !Array.<Object>}} |
+ * @private |
+ */ |
+ getChartData_: function(chart) { |
+ var result = {metrics: [], events: []}; |
+ |
+ for (var metric in this.metricMap_) { |
+ var metricValue = this.metricMap_[metric]; |
+ |
+ if (metricValue.divs.indexOf(chart) != -1) |
+ result.metrics.push(metricValue); |
} |
- } |
- } |
- |
- this.dropEventColumns = function(table, eventType) { |
- var colIndex, numCols = table.getNumberOfColumns(); |
- |
- for (colIndex = 0; colIndex < numCols; colIndex++) |
- if (table.getColumnId(colIndex) == eventType) |
- break; |
- |
- if (colIndex < numCols) { |
- table.removeColumn(colIndex + 1); |
- table.removeColumn(colIndex); |
- } |
- } |
- |
- // Receive new data for |eventType|. Save this in eventMap for future |
- // redraws. Then, for each metric linegraph remove any current column pair |
- // for |eventType| and replace it with a new pair, which will reflect the |
- // new data. Redraw the linegraph. |
- this.onReceiveEventType = function(eventType, values) { |
- this.eventMap[eventType] = values; |
- |
- for (var metric in this.metricMap) { |
- var table = this.metricMap[metric].table; |
- |
- this.dropEventColumns(table, eventType); |
- this.addEventColumns(table, eventType); |
- this.drawChart(metric); |
- } |
- } |
- |
- this.dropEventType = function(eventType) { |
- delete this.eventMap[eventType]; |
- for (var metric in this.metricMap) { |
- var table = this.metricMap[metric].table; |
+ for (var eventType in this.eventMap_) { |
+ var eventValue = this.eventMap_[eventType]; |
- this.dropEventColumns(table, eventType); |
- this.drawChart(metric); |
- } |
- } |
+ // Events post to all divs, if they post to any. |
+ if (eventValue.divs.length > 0) |
+ result.events.push(eventValue); |
+ } |
+ return result; |
+ }, |
+ |
+ /** |
+ * Check all entries in an object of the type returned from getChartData, |
+ * above, to see if all events and metrics have completed data (none is |
+ * awaiting an asynchronous webui response to get their current data). |
+ * @param {!{metrics: !Array,<Object>, events: !Array.<Object>}} chartData |
+ * event/metric data to check for readiness |
+ * @return {boolean} is data ready? |
+ * @private |
+ */ |
+ isDataReady_: function(chartData) { |
+ for (var i = 0; i < chartData.metrics.length; i++) { |
+ if (!chartData.metrics[i].data) |
+ return false; |
+ } |
- // Redraw the linegraph for |metric|, assuming its DataTable is fully up to |
- // date. |
- this.drawChart = function(metric) { |
- var entry = this.metricMap[metric]; |
+ for (var i = 0; i < chartData.events.length; i++) { |
+ if (!chartData.events[i].data) |
+ return false; |
+ } |
- entry.chart.draw(entry.table, {title: metric + ' for ' + this.range.name, |
- hAxis: {showTextEvery: this.range.labelEvery}}); |
- } |
+ return true; |
+ }, |
+ |
+ /** |
+ * 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. |
+ * @param {!Array.<{ |
+ * description: !string, |
+ * color: !string, |
+ * data: !Array.<{time: !number}> |
+ * }>} eventValues events to make markings for |
+ * @return {!Array.<{ |
+ * color: !string, |
+ * description: !string, |
+ * xaxis: {from: !number, to: !number} |
+ * }>} mark data structure for Flot to use |
+ * @private |
+ */ |
+ 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]; |
+ markings.push({ |
+ color: eventValue.color, |
+ description: eventValue.description, |
+ xaxis: {from: point.time, to: point.time} |
+ }); |
+ } |
+ } |
- this.setupTimeRangeChooser = function() { |
- var controller = this; |
- var radioTemplate = $('#radioTemplate'); |
- |
- for (var time in this.TimeRange) { |
- var range = this.TimeRange[time]; |
- var radio = radioTemplate.cloneNode(true); |
- var input = radio.querySelector('input'); |
- |
- input.value = range.value; |
- radio.querySelector('span').innerText = range.name; |
- this.timeDiv.appendChild(radio); |
- range.element = input; |
- radio.range = range; |
- radio.addEventListener('click', function() { |
- controller.setTimeRange(this.range); |
- }); |
+ return markings; |
+ }, |
+ |
+ /** |
+ * Redraw the chart in div |chart|, *if* all its dependent data is present. |
+ * Otherwise simply return, and await another call when all data is |
+ * available. |
+ * @param {HTMLDivElement} chart div to redraw |
+ */ |
+ drawChart: function(chart) { |
+ var chartData = this.getChartData_(chart); |
+ |
+ if (!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++) { |
+ 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. |
+ }); |
+ } |
+ }); |
+ |
+ var markings = this.getEventMarks_(chartData.events); |
+ var chart = this.charts[0]; |
+ var plot = $.plot(chart, seriesSeq, { |
+ yaxes: yAxes, |
+ xaxis: {mode: 'time'}, |
+ grid: {markings: markings} |
+ }); |
+ |
+ // For each event in |markings|, create also a label div, with left |
+ // edge colinear with the event vertical-line. Top of label is |
+ // presently a hack-in, putting labels in three tiers of 25px height |
+ // each to avoid overlap. Will need something better. |
+ var labelTemplate = $('#labelTemplate')[0]; |
+ for (var i = 0; i < markings.length; i++) { |
+ var mark = markings[i]; |
+ var point = |
+ plot.pointOffset({x: mark.xaxis.to, y: yAxes[0].max, yaxis: 1}); |
+ var labelDiv = labelTemplate.cloneNode(true); |
+ labelDiv.innerText = mark.description; |
+ labelDiv.style.left = point.left + 'px'; |
+ labelDiv.style.top = (point.top + 25 * (i % 3)) + 'px'; |
+ chart.appendChild(labelDiv); |
+ } |
} |
- } |
- |
- this.setupMetricChooser = function(metricTypes) { |
- var checkboxTemplate = $('#checkboxTemplate'); |
- |
- metricTypes.forEach(function(metric) { |
- var checkbox = checkboxTemplate.cloneNode(true); |
- var input = checkbox.querySelector('input'); |
- input.addEventListener('change', function() { |
- if (input.checked) |
- this.addMetric(metric); |
- else |
- this.dropMetric(metric); |
- }.bind(this)); |
- checkbox.getElementsByTagName('span')[0].innerText = 'Show ' + metric; |
- this.chooseMetricsDiv.appendChild(checkbox); |
- }, this); |
- } |
- |
- this.setupEventChooser = function(eventTypes) { |
- var checkboxTemplate = $('#checkboxTemplate'); |
- |
- eventTypes.forEach(function(event) { |
- var checkbox = checkboxTemplate.cloneNode(true); |
- var input = checkbox.querySelector('input'); |
- input.addEventListener('change', function() { |
- if (input.checked) |
- this.addEventType(event); |
- else |
- this.dropEventType(event); |
- }.bind(this)); |
- checkbox.getElementsByTagName('span')[0].innerText = 'Show ' + event; |
- this.chooseEventsDiv.appendChild(checkbox); |
- }, this); |
- } |
+ }; |
+ return { |
+ PerformanceMonitor: PerformanceMonitor |
+ }; |
+}(); |
- this.setupMetricChooser(['Oddness', 'Jankiness']); |
- this.setupEventChooser(['Wampus Attack', 'Solar Eclipse']); |
- this.setupTimeRangeChooser(); |
- this.TimeRange.day.element.click(); |
-} |
+var performanceMonitor = new Installer.PerformanceMonitor(); |