blob: 098f82b6f3136b9e2e17eb8666b63cb12a201bcd [file] [log] [blame]
/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
* 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 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,
* element: HTMLElement
* }}
* @private
*/
var TimeRange_ = {
// 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 20s, at most 180 points.
// Labels at 15 point (5 min) intervals.
hour: {value: 1, name: 'Last Hour', timeSpan: 3600 * 1000,
resolution: 1000 * 20, labelEvery: 15, format: 'MM-dd'},
// 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 * 5, labelEvery: 36, format: 'MM-dd'},
// 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 * 3600, labelEvery: 24, format: 'M/d'},
// 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 * 4, labelEvery: 42, format: 'M/d'},
// 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 * 12, labelEvery: 28, format: 'M/yy'},
};
/*
* 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 resizeDelay_ = 500;
/** @constructor */
function PerformanceMonitor() {
this.__proto__ = PerformanceMonitor.prototype;
/**
* 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}>
* }}
*/
PerformanceMonitor.MetricDetails;
/**
* Similar data for events as for metrics, though no y-axis 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 metricDetailsMap_
* above. The |data| field follows a special rule not describable in
* JSDoc: Aside from the |time| key, each event type has varying other
* properties, with unknown key names, which properties must still be
* displayed. Such properties always have value of form
* {label: 'some label', value: 'some value'}, with label and value
* internationalized.
* @typedef {{
* eventId: number,
* name: string,
* popupTitle: string,
* description: string,
* color: string,
* divs: !Array.<HTMLDivElement>,
* 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.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
* and events.
* @type {!Array.<{start: number, end: number}>}
* @private
*/
this.intervals_ = [];
/**
* 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;
/**
* 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 metric categories, each with its corresponding
* 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.metricCategoryMap_, 'metricId', this.addMetric, this.dropMetric);
},
/**
* 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.
*/
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);
},
/**
* Set up the radio button set to choose time range. Use div#radio-template
* as a template.
* @private
*/
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_) {
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;
}
timeDiv.addEventListener('click', function(e) {
if (!e.target.webkitMatchesSelector('input[type="radio"]'))
return;
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 to create checkboxes for either events
* or metrics, given a |div| into which to put the checkboxes, and a
* |optionCategoryMap| describing the checkbox structure.
*
* For instance, |optionCategoryMap| might be metricCategoryMap_, with
* contents thus:
*
* optionCategoryMap : {
* 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)'}
* ]
* }
*
* and we would call setupCheckboxes_ thus:
*
* this.setupCheckboxes_(<parent div>, this.metricCategoryMap_, 'metricId',
* this.addMetric, this.dropMetric);
*
* MetricCategoryMap_'s values each have a |name| and |details| property.
* SetupCheckboxes_ creates one major header for each such value, with title
* given by the |name| field. Under each major header are checkboxes,
* one for each element in the |details| property. The checkbox titles
* come from the |name| property of each |details| object,
* and they each have an associated colored icon matching the |color|
* property of the details object.
*
* So, for the example given, the generated HTML looks thus:
*
* <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>
*
* The checkboxes for each details object call addMetric or
* dropMetric as they are checked and unchecked, passing the relevant
* |metricId| value. Parameter 'metricId' identifies key |metricId| as the
* identifying property to pass to the methods. So, for instance, checking
* the CPU Usage box results in a call to this.addMetric(1), since
* metricCategoryMap_[1].details[0].metricId == 1.
*
* In general, |optionCategoryMap| must have values that each include
* a property |name|, and a property |details|. The |details| value must
* be an array of objects that in turn each have an identifying property
* with key given by parameter |idKey|, plus a property |name| and a
* property |color|.
*
* @param {!HTMLDivElement} div A <div> into which to put checkboxes.
* @param {!Object} optionCategoryMap A map of metric/event categories.
* @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, optionCategoryMap, idKey, check, uncheck) {
var checkboxTemplate = $('#detail-checkbox-template')[0];
var labelTemplate = $('#category-label-template')[0];
for (var c in optionCategoryMap) {
var category = optionCategoryMap[c];
var label = labelTemplate.cloneNode(true);
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);
}
},
/**
* 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
*/
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() {
if (this.resizeTimer_ != null)
clearTimeout(this.resizeTimer_);
this.resizeTimer_ = setTimeout(this.checkResize_.bind(this),
resizeDelay_);
}.bind(this));
},
/**
* |resizeDelay_| ms have elapsed since the last resize event, and the timer
* for redrawing has triggered. Clear it, and redraw all the charts.
* @private
*/
checkResize_: function() {
clearTimeout(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
* 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, end, startTimer) {
this.range = range;
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;
this.requestIntervals();
},
/**
* 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() {
chrome.send('getActiveIntervals', [this.start, this.end]);
},
/**
* 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
* The new intervals.
*/
getActiveIntervalsCallback: function(intervals) {
this.intervals_ = intervals;
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.eventDetailsMap_) {
var eventValue = this.eventDetailsMap_[eventType];
if (eventValue.divs.length > 0)
this.refreshEventType(eventValue.eventId);
}
},
/**
* 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(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);
},
/**
* Remove a metric from the chart(s).
* @param {string} metric The metric to stop displaying.
*/
dropMetric: function(metric) {
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);
},
/**
* Request new metric data, assuming the metric table and chart already
* exist.
* @param {string} metricDetails The metric details for which to get data.
*/
refreshMetric: function(metricDetails) {
metricDetails.data = null; // Mark metric as awaiting response.
chrome.send('getMetric', [metricDetails.metricId,
this.start, this.end, this.range.resolution]);
},
/**
* Receive new datapoints for a metric, convert the data to Flot-usable
* form, and redraw all affected charts.
* @param {!{
* 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 metricDetails = this.metricDetailsMap_[result.metricId];
// Might have been dropped while waiting for data.
if (metricDetails.divs.length == 0)
return;
var 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)
// 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.metrics.length &&
intervalIndex < this.intervals_.length) {
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.
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]);
}
}
metricDetails.maxValue = Math.max(metricDetails.maxValue,
result.maxValue);
metricDetails.divs.forEach(this.drawChart, this);
},
/**
* Add a new event to the chart(s).
* @param {string} eventType The type of event to start displaying.
*/
addEventType: function(eventType) {
// Events show on all charts.
this.eventDetailsMap_[eventType].divs = this.charts_;
this.refreshEventType(eventType);
},
/*
* Remove an event from the chart(s).
* @param {string} eventType The type of event to stop displaying.
*/
dropEventType: function(eventType) {
var eventValue = this.eventDetailsMap_[eventType];
var affectedCharts = eventValue.divs;
eventValue.divs = []; // Gotta do this now for correct drawChart results.
affectedCharts.forEach(this.drawChart, this);
},
/**
* Request new data for |eventType|, for times in the current range.
* @param {string} eventType The type of event to get new data for.
*/
refreshEventType: function(eventType) {
// Mark eventType as awaiting response.
this.eventDetailsMap_[eventType].data = null;
chrome.send('getEvents', [eventType, this.start, this.end]);
},
/**
* 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.
* @param {!{
* id: number,
* events: !Array.<{time: number}>
* }} result An object giving eventType id, and times at which that event
* type occurred 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.eventDetailsMap_[result.eventId];
if (eventValue.divs.length == 0)
return;
result.events.forEach(function(element) {
element.time -= timezoneOffset_;
});
eventValue.data = result.events;
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 The <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.metricDetailsMap_) {
var metricDetails = this.metricDetailsMap_[metric];
if (metricDetails.divs.indexOf(chart) != -1)
result.metrics.push(metricDetails);
}
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)
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 handler response to get their current
* data).
* @param {!{metrics: !Array,<Object>, events: !Array.<Object>}} chartData
* The event/metric data to check for readiness.
* @return {boolean} Whether data is ready.
* @private
*/
isDataReady_: function(chartData) {
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
* vertical lines at the event time, in the event's color. Also add
* (not per Flot) a |popupTitle| property to each, to be used for
* labelling description popups.
* @param {!Array.<{
* description: string,
* color: string,
* data: !Array.<{time: number}>
* }>} eventValues The events to make markings for.
* @return {!Array.<{
* color: string,
* description: string,
* xaxis: {from: number, to: number}
* }>} A marks data structure for Flot to use.
* @private
*/
getEventMarks_: function(eventValues) {
var markings = [];
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,
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;
},
/**
* 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 The <div> to redraw.
*/
drawChart: function(chart) {
var chartData = this.getChartData_(chart);
var axisMap = {}; // Maps category ids to y-axis numbers
if (chart.hidden || !this.isDataReady_(chartData))
return;
var seriesSeq = [];
var yAxes = [];
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: metricDetails.color,
data: metricDetails.data[i],
label: i == 0 ? metricDetails.name +
' (' + metricCategory.unit + ')' : null,
yaxis: yAxisNumber
});
}
});
var markings = this.getEventMarks_(chartData.events);
var plot = $.plot(chart, seriesSeq, {
yaxes: yAxes,
xaxis: {
mode: 'time',
min: this.start - timezoneOffset_,
max: this.end - timezoneOffset_
},
points: {show: true, radius: 1},
lines: {show: true},
grid: {markings: markings, hoverable: true, autoHighlight: true}
});
// 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 = $('#label-template')[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.popupContent;
labelDiv.style.left = point.left + '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});
}
}
};
return {
PerformanceMonitor: PerformanceMonitor
};
});
var PerformanceMonitor = new performance_monitor.PerformanceMonitor();