From b615a3c888f504b4f1fd3fcffe1a686f0f3092ba Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Thu, 29 Jan 2015 09:48:53 -0800 Subject: [PATCH] [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.) --- platform/features/plot/src/PlotController.js | 57 +++--- .../features/plot/src/elements/PlotUpdater.js | 178 ++++++++++++++++++ .../telemetry/src/TelemetrySubscription.js | 25 +++ 3 files changed, 224 insertions(+), 36 deletions(-) create mode 100644 platform/features/plot/src/elements/PlotUpdater.js diff --git a/platform/features/plot/src/PlotController.js b/platform/features/plot/src/PlotController.js index ceb04a8708..05ba514a73 100644 --- a/platform/features/plot/src/PlotController.js +++ b/platform/features/plot/src/PlotController.js @@ -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. diff --git a/platform/features/plot/src/elements/PlotUpdater.js b/platform/features/plot/src/elements/PlotUpdater.js new file mode 100644 index 0000000000..1e60c4fe17 --- /dev/null +++ b/platform/features/plot/src/elements/PlotUpdater.js @@ -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; + + } +); \ No newline at end of file diff --git a/platform/telemetry/src/TelemetrySubscription.js b/platform/telemetry/src/TelemetrySubscription.js index 5e929d8ee4..e0abaa2668 100644 --- a/platform/telemetry/src/TelemetrySubscription.js +++ b/platform/telemetry/src/TelemetrySubscription.js @@ -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; } }; }