[Plot] Wire in updater

Wire in updater for use in plots (to cache values at
the plot level, allowing removal of a global cache to
reduce memory consumption for WTD-751.)
This commit is contained in:
Victor Woeltjen 2015-01-29 09:48:53 -08:00
parent f3900cdd2a
commit b615a3c888
3 changed files with 224 additions and 36 deletions

View File

@ -5,13 +5,13 @@
*/
define(
[
"./elements/PlotPreparer",
"./elements/PlotUpdater",
"./elements/PlotPalette",
"./elements/PlotAxis",
"./modes/PlotModeOptions",
"./SubPlotFactory"
],
function (PlotPreparer, PlotPalette, PlotAxis, PlotModeOptions, SubPlotFactory) {
function (PlotUpdater, PlotPalette, PlotAxis, PlotModeOptions, SubPlotFactory) {
"use strict";
var AXIS_DEFAULTS = [
@ -34,6 +34,7 @@ define(
var subPlotFactory = new SubPlotFactory(telemetryFormatter),
modeOptions = new PlotModeOptions([], subPlotFactory),
subplots = [],
updater,
subscription,
domainOffset;
@ -46,38 +47,6 @@ define(
];
}
// Respond to newly-available telemetry data; update the
// drawing area accordingly.
function plotTelemetry() {
var prepared, datas, telemetry;
// Get a reference to the TelemetryController
telemetry = $scope.telemetry;
// Nothing to plot without TelemetryController
if (!telemetry) {
return;
}
// Ensure axes have been initialized (we will want to
// get the active axis below)
if (!$scope.axes) {
setupAxes(telemetry.getMetadata());
}
// Get data sets
datas = telemetry.getResponse();
// Prepare data sets for rendering
prepared = new PlotPreparer(
datas,
($scope.axes[0].active || {}).key,
($scope.axes[1].active || {}).key
);
modeOptions.getModeHandler().plotTelemetry(prepared);
}
// Trigger an update of a specific subplot;
// used in a loop to update all subplots.
function updateSubplot(subplot) {
@ -100,19 +69,35 @@ define(
.forEach(updateSubplot);
}
function updateValues() {
function recreateUpdater() {
updater = new PlotUpdater(
subscription,
($scope.axes[0].active || {}).key,
($scope.axes[1].active || {}).key
);
}
function updateValues() {
modeOptions.getModeHandler().plotTelemetry(updater);
update();
}
// Create a new subscription; telemetrySubscriber gets
// to do the meaningful work here.
function subscribe(domainObject) {
if (subscription) {
subscription.unsubscribe();
}
subscription = domainObject && telemetrySubscriber.subscribe(
domainObject,
updateValues
);
setupAxes(subscription.getMetadata());
recreateUpdater();
}
$scope.$watch('domainObject', subscribe);
return {
/**
* Get the color (as a style-friendly string) to use
@ -166,7 +151,7 @@ define(
*/
setMode: function (mode) {
modeOptions.setMode(mode);
plotTelemetry();
updateValues();
},
/**
* Get all individual plots contained within this Plot view.

View File

@ -0,0 +1,178 @@
/*global define,Float32Array*/
/**
* Prepares data to be rendered in a GL Plot. Handles
* the conversion from data API to displayable buffers.
*/
define(
function () {
'use strict';
var MAX_POINTS = 86400,
INITIAL_SIZE = 675; // 1/128 of MAX_POINTS
function identity(x) { return x; }
/**
* The PlotPreparer is responsible for handling data sets and
* preparing them to be rendered. It creates a WebGL-plottable
* Float32Array for each trace, and tracks the boundaries of the
* data sets (since this is convenient to do during the same pass).
* @constructor
* @param {Telemetry[]} datas telemetry data objects
* @param {string} domain the key to use when looking up domain values
* @param {string} range the key to use when looking up range values
*/
function PlotUpdater(subscription, domain, range) {
var max = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
min = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
x,
y,
domainOffset,
buffers = {},
lengths = {},
bufferArray;
function ensureBufferSize(buffer, id, index) {
// Check if we don't have enough room
if (index > buffer.length / 2) {
// If we don't, can we expand?
if (index < MAX_POINTS) {
// Double the buffer size
buffer = buffers[id] =
new Float32Array(buffer, 0, buffer.length * 2);
} else {
// Just shift the existing buffer
buffer.copyWithin(0, 2);
}
}
return buffer;
}
function addData(obj) {
var id = obj.getId(),
index = lengths[id],
buffer = buffers[id],
domainValue = subscription.getDomainValue(obj),
rangeValue = subscription.getRangeValue(obj);
if (domainValue !== undefined && rangeValue !== undefined &&
(index < 1 || domainValue !== buffer[index * 2 - 2])) {
// Use the first observed domain value as a domainOffset
domainOffset = domainOffset !== undefined ?
domainOffset : domainValue;
// Ensure there is space for the new buffer
buffer = ensureBufferSize(buffer, id, index);
// Account for shifting that may have occurred
index = Math.min(index, MAX_POINTS - 2);
// Update the buffer
buffer[index * 2] = domainValue - domainOffset;
buffer[index * 2 + 1] = rangeValue;
// Update length
lengths[id] = Math.min(index + 1, MAX_POINTS);
// Observe max/min range values
max[1] = Math.max(max[1], rangeValue);
min[1] = Math.min(min[1], rangeValue);
}
return buffer;
}
function updateDomainExtrema(objects) {
max[0] = Number.NEGATIVE_INFINITY;
min[0] = Number.POSITIVE_INFINITY;
objects.forEach(function (obj) {
var id = obj.getId(),
buffer = buffers[id],
length = lengths[id],
value = buffer[length * 2 - 2] + domainOffset;
max[0] = Math.max(value, max[0]);
min[0] = Math.min(value, min[0]);
});
}
function padRange() {
// If range is empty, add some padding
if (max[1] === min[1]) {
max[1] = max[1] + 1.0;
min[1] = min[1] - 1.0;
}
}
function update() {
var objects = subscription.getTelemetryObjects();
bufferArray = objects.map(addData);
updateDomainExtrema(objects);
padRange();
}
function prepare(telemetryObject) {
var id = telemetryObject.getId();
lengths[id] = 0;
buffers[id] = new Float32Array(INITIAL_SIZE);
}
subscription.getTelemetryObjects().forEach(prepare);
return {
/**
* Get the dimensions which bound all data in the provided
* data sets. This is given as a two-element array where the
* first element is domain, and second is range.
* @returns {number[]} the dimensions which bound this data set
*/
getDimensions: function () {
return [max[0] - min[0], max[1] - min[1]];
},
/**
* Get the origin of this data set's boundary.
* This is given as a two-element array where the
* first element is domain, and second is range.
* The domain value here is not adjusted by the domain offset.
* @returns {number[]} the origin of this data set's boundary
*/
getOrigin: function () {
return min;
},
/**
* Get the domain offset; this offset will have been subtracted
* from all domain values in all buffers returned by this
* preparer, in order to minimize loss-of-precision due to
* conversion to the 32-bit float format needed by WebGL.
* @returns {number} the domain offset
*/
getDomainOffset: function () {
return domainOffset;
},
/**
* Get all renderable buffers for this data set. This will
* be returned as an array which can be correlated back to
* the provided telemetry data objects (from the constructor
* call) by index.
*
* Internally, these are flattened; each buffer contains a
* sequence of alternating domain and range values.
*
* All domain values in all buffers will have been adjusted
* from their original values by subtraction of the domain
* offset; this minimizes loss-of-precision resulting from
* the conversion to 32-bit floats, which may otherwise
* cause aliasing artifacts (particularly for timestamps)
*
* @returns {Float32Array[]} the buffers for these traces
*/
getBuffers: function () {
return buffers;
},
/**
* Update with latest data.
*/
update: update
};
}
return PlotUpdater;
}
);

View File

@ -30,6 +30,7 @@ define(
var unsubscribePromise,
latestValues = {},
telemetryObjects = [],
metadatas,
updatePending;
// Look up domain objects which have telemetry capabilities.
@ -96,6 +97,14 @@ define(
});
}
// Look up metadata associated with an object's telemetry
function lookupMetadata(domainObject) {
var telemetryCapability =
domainObject.getCapability("telemetry");
return telemetryCapability &&
telemetryCapability.getMetadata();
}
// Prepare subscriptions to all relevant telemetry-providing
// domain objects.
function subscribeAll(domainObjects) {
@ -108,6 +117,7 @@ define(
// to return a non-Promise to simplify usage elsewhere.
function cacheObjectReferences(objects) {
telemetryObjects = objects;
metadatas = objects.map(lookupMetadata);
return objects;
}
@ -189,6 +199,21 @@ define(
*/
getTelemetryObjects: function () {
return telemetryObjects;
},
/**
* Get all telemetry metadata associated with
* telemetry-providing domain objects managed by
* this controller.
*
* This will ordered in the
* same manner as `getTelemetryObjects()` or
* `getResponse()`; that is, the metadata at a
* given index will correspond to the telemetry-providing
* domain object at the same index.
* @returns {Array} an array of metadata objects
*/
getMetadata: function () {
return metadatas;
}
};
}