From 036722b275f239b8619a25a7cf59d18d73f1b0e9 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 10 Apr 2015 19:24:16 -0700 Subject: [PATCH 01/22] [Containment] Disallow composition in immutable objects Disallow composition in objects which cannot be created, under the rationale that creatable objects must also be immutable. WTD-1098. --- platform/containment/bundle.json | 5 ++++ .../src/CompositionMutabilityPolicy.js | 30 +++++++++++++++++++ .../test/CompositionMutabilityPolicySpec.js | 26 ++++++++++++++++ platform/containment/test/suite.json | 1 + 4 files changed, 62 insertions(+) create mode 100644 platform/containment/src/CompositionMutabilityPolicy.js create mode 100644 platform/containment/test/CompositionMutabilityPolicySpec.js diff --git a/platform/containment/bundle.json b/platform/containment/bundle.json index a31eb60c4a..e61085f66f 100644 --- a/platform/containment/bundle.json +++ b/platform/containment/bundle.json @@ -7,6 +7,11 @@ "depends": [ "$injector" ], "message": "Objects of this type cannot contain objects of that type." }, + { + "category": "composition", + "implementation": "CompositionMutabilityPolicy.js", + "message": "Objects of this type cannot be modified." + }, { "category": "action", "implementation": "ComposeActionPolicy.js", diff --git a/platform/containment/src/CompositionMutabilityPolicy.js b/platform/containment/src/CompositionMutabilityPolicy.js new file mode 100644 index 0000000000..21dc02fafc --- /dev/null +++ b/platform/containment/src/CompositionMutabilityPolicy.js @@ -0,0 +1,30 @@ +/*global define*/ + +define( + [], + function () { + "use strict"; + + /** + * Disallow composition changes to objects which are not mutable. + * @constructor + */ + function CompositionMutabilityPolicy() { + return { + /** + * Is the type identified by the candidate allowed to + * contain the type described by the context? + * @param {Type} candidate the type of domain object + */ + allow: function (candidate) { + // Equate creatability with mutability; that is, users + // can only modify objects of types they can create, and + // vice versa. + return candidate.hasFeature('creation'); + } + }; + } + + return CompositionMutabilityPolicy; + } +); \ No newline at end of file diff --git a/platform/containment/test/CompositionMutabilityPolicySpec.js b/platform/containment/test/CompositionMutabilityPolicySpec.js new file mode 100644 index 0000000000..460f8b6870 --- /dev/null +++ b/platform/containment/test/CompositionMutabilityPolicySpec.js @@ -0,0 +1,26 @@ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +define( + ["../src/CompositionMutabilityPolicy"], + function (CompositionMutabilityPolicy) { + "use strict"; + + describe("The composition mutability policy", function () { + var mockType, + policy; + + beforeEach(function () { + mockType = jasmine.createSpyObj('type', ['hasFeature']); + policy = new CompositionMutabilityPolicy(); + }); + + it("only allows composition for types which will have a composition capability", function () { + expect(policy.allow(mockType)).toBeFalsy(); + mockType.hasFeature.andReturn(true); + expect(policy.allow(mockType)).toBeTruthy(); + expect(mockType.hasFeature).toHaveBeenCalledWith('creation'); + }); + }); + + } +); \ No newline at end of file diff --git a/platform/containment/test/suite.json b/platform/containment/test/suite.json index a82d203c53..987ef9a86c 100644 --- a/platform/containment/test/suite.json +++ b/platform/containment/test/suite.json @@ -1,6 +1,7 @@ [ "CapabilityTable", "ComposeActionPolicy", + "CompositionMutabilityPolicy", "CompositionPolicy", "ContainmentTable" ] \ No newline at end of file From 774c4dec1d80072ff3a882cdc0f2bfc4b8658641 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Thu, 16 Apr 2015 16:01:51 -0700 Subject: [PATCH 02/22] [Telemetry] Separate out delegator Separate out handling of delegation related to telemetry, WTD-806. --- platform/telemetry/src/TelemetryDelegator.js | 45 +++++++++++++++++++ platform/telemetry/src/TelemetryRequester.js | 34 ++++++++++++++ .../telemetry/src/TelemetrySubscription.js | 25 +++-------- 3 files changed, 84 insertions(+), 20 deletions(-) create mode 100644 platform/telemetry/src/TelemetryDelegator.js create mode 100644 platform/telemetry/src/TelemetryRequester.js diff --git a/platform/telemetry/src/TelemetryDelegator.js b/platform/telemetry/src/TelemetryDelegator.js new file mode 100644 index 0000000000..df66e84407 --- /dev/null +++ b/platform/telemetry/src/TelemetryDelegator.js @@ -0,0 +1,45 @@ +/*global define*/ + +define( + [], + function () { + "use strict"; + + /** + * Used to handle telemetry delegation associated with a + * given domain object. + */ + function TelemetryDelegator($q) { + return { + /** + * Promise telemetry-providing objects associated with + * this domain object (either the domain object itself, + * or the objects it delegates) + * @returns {Promise.} domain objects with + * a telemetry capability + */ + promiseTelemetryObjects: function (domainObject) { + // If object has been cleared, there are no relevant + // telemetry-providing domain objects. + if (!domainObject) { + return $q.when([]); + } + + // Otherwise, try delegation first, and attach the + // object itself if it has a telemetry capability. + return $q.when(domainObject.useCapability( + "delegation", + "telemetry" + )).then(function (result) { + var head = domainObject.hasCapability("telemetry") ? + [ domainObject ] : [], + tail = result || []; + return head.concat(tail); + }); + } + }; + } + + return TelemetryDelegator; + } +); \ No newline at end of file diff --git a/platform/telemetry/src/TelemetryRequester.js b/platform/telemetry/src/TelemetryRequester.js new file mode 100644 index 0000000000..f400083b28 --- /dev/null +++ b/platform/telemetry/src/TelemetryRequester.js @@ -0,0 +1,34 @@ +/*global define*/ + +define( + ['./TelemetryDelegator'], + function (TelemetryDelegator) { + "use strict"; + + + /** + * A TelemetryRequester provides an easy interface to request + * telemetry associated with a set of domain objects. + * + * @constructor + * @param $q Angular's $q + */ + function TelemetryRequester($q) { + var delegator = new TelemetryDelegator($q); + + // Look up domain objects which have telemetry capabilities. + // This will either be the object in view, or object that + // this object delegates its telemetry capability to. + function promiseRelevantObjects(domainObject) { + return delegator.promiseTelemetryObjects(domainObject); + } + + return { + + }; + } + + return TelemetryRequester; + + } +); \ No newline at end of file diff --git a/platform/telemetry/src/TelemetrySubscription.js b/platform/telemetry/src/TelemetrySubscription.js index cce24d48dd..adbe08ba51 100644 --- a/platform/telemetry/src/TelemetrySubscription.js +++ b/platform/telemetry/src/TelemetrySubscription.js @@ -1,8 +1,8 @@ /*global define*/ define( - ['./TelemetryQueue', './TelemetryTable'], - function (TelemetryQueue, TelemetryTable) { + ['./TelemetryQueue', './TelemetryTable', './TelemetryDelegator'], + function (TelemetryQueue, TelemetryTable, TelemetryDelegator) { "use strict"; @@ -31,7 +31,8 @@ define( * the callback once, with access to the latest data */ function TelemetrySubscription($q, $timeout, domainObject, callback, lossless) { - var unsubscribePromise, + var delegator = new TelemetryDelegator($q), + unsubscribePromise, latestValues = {}, telemetryObjects = [], pool = lossless ? new TelemetryQueue() : new TelemetryTable(), @@ -42,23 +43,7 @@ define( // This will either be the object in view, or object that // this object delegates its telemetry capability to. function promiseRelevantObjects(domainObject) { - // If object has been cleared, there are no relevant - // telemetry-providing domain objects. - if (!domainObject) { - return $q.when([]); - } - - // Otherwise, try delegation first, and attach the - // object itself if it has a telemetry capability. - return $q.when(domainObject.useCapability( - "delegation", - "telemetry" - )).then(function (result) { - var head = domainObject.hasCapability("telemetry") ? - [ domainObject ] : [], - tail = result || []; - return head.concat(tail); - }); + return delegator.promiseTelemetryObjects(domainObject); } function updateValuesFromPool() { From 60e888e16ece2ad59384db3de6840cd999c8af84 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Thu, 16 Apr 2015 16:27:25 -0700 Subject: [PATCH 03/22] [Telemetry] Begin adding telemetry handler Begin adding a general purpose handler for telemetry which extends on the behavior associated with the telemetrySubscriber by supporting access to historical data as well. WTD-806. --- platform/telemetry/src/TelemetryHandle.js | 81 +++++++++++++++++++ platform/telemetry/src/TelemetryHandler.js | 33 ++++++++ platform/telemetry/src/TelemetryRequester.js | 34 -------- .../telemetry/src/TelemetrySubscription.js | 16 +++- 4 files changed, 128 insertions(+), 36 deletions(-) create mode 100644 platform/telemetry/src/TelemetryHandle.js create mode 100644 platform/telemetry/src/TelemetryHandler.js delete mode 100644 platform/telemetry/src/TelemetryRequester.js diff --git a/platform/telemetry/src/TelemetryHandle.js b/platform/telemetry/src/TelemetryHandle.js new file mode 100644 index 0000000000..eef57caba6 --- /dev/null +++ b/platform/telemetry/src/TelemetryHandle.js @@ -0,0 +1,81 @@ +/*global define*/ + +define( + [], + function () { + "use strict"; + + function TelemetryHandle($q, subscription) { + var seriesMap = {}, + self = Object.create(subscription); + + function requestSeries(telemetryObject, request, callback) { + var id = telemetryObject.getId(), + telemetry = telemetryObject.getCapability('telemetry'); + + function receiveSeries(series) { + // Store it for subsequent lookup + seriesMap[id] = series; + // Notify callback of new series data, if there is one + if (callback) { + callback(telemetryObject, series); + } + // Pass it along for promise-chaining + return series; + } + + // Issue the request via the object's telemetry capability + return telemetry.requestData(request).then(receiveSeries); + } + + + /** + * Get the most recently obtained telemetry data series associated + * with this domain object. + * @param {DomainObject} the domain object which has telemetry + * data associated with it + * @return {TelemetrySeries} the most recent telemetry series + * (or undefined if there is not one) + */ + self.getSeries = function (domainObject) { + var id = domainObject.getId(); + return seriesMap[id]; + }; + + + /** + * Change the request duration. + * @param {object|number} request the duration of historical + * data to look at; or, the request to issue + * @param {Function} [callback] a callback that will be + * invoked as new data becomes available, with the + * domain object for which new data is available. + */ + self.request = function (request, callback) { + // Issue (and handle) the new request from this object + function issueRequest(telemetryObject) { + return requestSeries(telemetryObject, request, callback); + } + + // Map the request to all telemetry objects + function issueRequests(telemetryObjects) { + return $q.all(telemetryObjects.map(issueRequest)); + } + + // If the request is a simple number, treat it as a duration + request = (typeof request === 'number') ? + { duration: request } : request; + + // Look up telemetry-providing objects from the subscription, + // then issue new requests. + return subscription.promiseTelemetryObjects() + .then(issueRequests); + }; + + return self; + } + + return TelemetryHandle; + + } +); \ No newline at end of file diff --git a/platform/telemetry/src/TelemetryHandler.js b/platform/telemetry/src/TelemetryHandler.js new file mode 100644 index 0000000000..2b3aa954eb --- /dev/null +++ b/platform/telemetry/src/TelemetryHandler.js @@ -0,0 +1,33 @@ +/*global define*/ + +define( + ['./TelemetryHandle'], + function (TelemetryHandle) { + "use strict"; + + + /** + * A TelemetryRequester provides an easy interface to request + * telemetry associated with a set of domain objects. + * + * @constructor + * @param $q Angular's $q + */ + function TelemetryHandler($q, telemetrySubscriber) { + return { + handle: function (domainObject, callback, lossless) { + var subscription = telemetrySubscriber.subscribe( + domainObject, + callback, + lossless + ); + + return new TelemetryHandle($q, subscription); + } + }; + } + + return TelemetryHandler; + + } +); \ No newline at end of file diff --git a/platform/telemetry/src/TelemetryRequester.js b/platform/telemetry/src/TelemetryRequester.js deleted file mode 100644 index f400083b28..0000000000 --- a/platform/telemetry/src/TelemetryRequester.js +++ /dev/null @@ -1,34 +0,0 @@ -/*global define*/ - -define( - ['./TelemetryDelegator'], - function (TelemetryDelegator) { - "use strict"; - - - /** - * A TelemetryRequester provides an easy interface to request - * telemetry associated with a set of domain objects. - * - * @constructor - * @param $q Angular's $q - */ - function TelemetryRequester($q) { - var delegator = new TelemetryDelegator($q); - - // Look up domain objects which have telemetry capabilities. - // This will either be the object in view, or object that - // this object delegates its telemetry capability to. - function promiseRelevantObjects(domainObject) { - return delegator.promiseTelemetryObjects(domainObject); - } - - return { - - }; - } - - return TelemetryRequester; - - } -); \ No newline at end of file diff --git a/platform/telemetry/src/TelemetrySubscription.js b/platform/telemetry/src/TelemetrySubscription.js index adbe08ba51..2afae3495c 100644 --- a/platform/telemetry/src/TelemetrySubscription.js +++ b/platform/telemetry/src/TelemetrySubscription.js @@ -33,6 +33,7 @@ define( function TelemetrySubscription($q, $timeout, domainObject, callback, lossless) { var delegator = new TelemetryDelegator($q), unsubscribePromise, + telemetryObjectPromise, latestValues = {}, telemetryObjects = [], pool = lossless ? new TelemetryQueue() : new TelemetryTable(), @@ -137,8 +138,8 @@ define( // will be unsubscribe functions. (This must be a promise // because delegation is supported, and retrieving delegate // telemetry-capable objects may be an asynchronous operation.) - unsubscribePromise = - promiseRelevantObjects(domainObject) + telemetryObjectPromise = promiseRelevantObjects(domainObject); + unsubscribePromise = telemetryObjectPromise .then(cacheObjectReferences) .then(subscribeAll); @@ -224,6 +225,17 @@ define( */ getMetadata: function () { return metadatas; + }, + /** + * Get a promise for all telemetry-providing objects + * associated with this subscription. + * @returns {Promise.} a promise for + * telemetry-providing objects + */ + promiseTelemetryObjects: function () { + // Unsubscribe promise is available after objects + // are loaded. + return telemetryObjectPromise; } }; } From 03e1633a227aa7137d9d0e606916675bd2bfc890 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Thu, 16 Apr 2015 16:41:09 -0700 Subject: [PATCH 04/22] [Timeline] Track realtime index in updater Track realtime index in PlotUpdater to facilitate prepending of historical data portion on-demand, WTD-806. --- .../features/plot/src/elements/PlotUpdater.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/platform/features/plot/src/elements/PlotUpdater.js b/platform/features/plot/src/elements/PlotUpdater.js index ed81035b6b..250a140cfc 100644 --- a/platform/features/plot/src/elements/PlotUpdater.js +++ b/platform/features/plot/src/elements/PlotUpdater.js @@ -29,6 +29,7 @@ define( domainOffset, buffers = {}, lengths = {}, + realtimeIndex = {}, lengthArray = [], bufferArray = []; @@ -52,6 +53,8 @@ define( } else { // Just shift the existing buffer buffer.set(buffer.subarray(2)); + // Update the realtime index accordingly + realtimeIndex[id] = Math.max(realtimeIndex[id] - 1, 0); } } @@ -91,6 +94,11 @@ define( // Observe max/min range values max[1] = Math.max(max[1], rangeValue); min[1] = Math.min(min[1], rangeValue); + // Update the cutoff point for when we started receiving + // realtime data, to aid in clearing historical data later + if (realtimeIndex[id] === undefined) { + realtimeIndex[id] = index; + } } return buffer; @@ -111,6 +119,11 @@ define( }); } + // Update historical data for this domain object + function setHistorical(domainObject) { + + } + // Handle new telemetry data function update() { var objects = subscription.getTelemetryObjects(); @@ -205,7 +218,11 @@ define( /** * Update with latest data. */ - update: update + update: update, + /** + * Fill in historical data. + */ + setHistorical: setHistorical }; } From 9215eb1427d98a6d6c30f04490cacc290444d8b5 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Thu, 16 Apr 2015 16:48:03 -0700 Subject: [PATCH 05/22] [Plot] Begin updating plot Begin updating plot to merge realtime and historical telemetry, WTD-806. --- .../features/plot/src/elements/PlotUpdater.js | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/platform/features/plot/src/elements/PlotUpdater.js b/platform/features/plot/src/elements/PlotUpdater.js index 250a140cfc..0ca8684ad7 100644 --- a/platform/features/plot/src/elements/PlotUpdater.js +++ b/platform/features/plot/src/elements/PlotUpdater.js @@ -61,21 +61,7 @@ define( return buffer; } - // Add data to the plot. - function addData(obj) { - var id = obj.getId(), - index = lengths[id] || 0, - buffer = buffers[id], - domainValue = subscription.getDomainValue(obj, domain), - rangeValue = subscription.getRangeValue(obj, range); - - // If we don't already have a data buffer for that ID, - // make one. - if (!buffer) { - buffer = new Float32Array(INITIAL_SIZE); - buffers[id] = buffer; - } - + function setData(buffer, id, index, domainValue, rangeValue) { // Make sure there's data to add, and then add it if (domainValue !== undefined && rangeValue !== undefined && (index < 1 || domainValue !== buffer[index * 2 - 2])) { @@ -94,14 +80,39 @@ define( // Observe max/min range values max[1] = Math.max(max[1], rangeValue); min[1] = Math.min(min[1], rangeValue); - // Update the cutoff point for when we started receiving - // realtime data, to aid in clearing historical data later - if (realtimeIndex[id] === undefined) { - realtimeIndex[id] = index; - } + } + return buffer; + } + + // Add data to the plot. + function addData(obj) { + var id = obj.getId(), + index = lengths[id] || 0, + buffer = buffers[id], + domainValue = subscription.getDomainValue(obj, domain), + rangeValue = subscription.getRangeValue(obj, range); + + // If we don't already have a data buffer for that ID, + // make one. + if (!buffer) { + buffer = new Float32Array(INITIAL_SIZE); + buffers[id] = buffer; } - return buffer; + // Update the cutoff point for when we started receiving + // realtime data, to aid in clearing historical data later + if (realtimeIndex[id] === undefined) { + realtimeIndex[id] = index; + } + + // Put the data in the buffer + return setData( + buffer, + id, + index, + domainValue, + rangeValue + ); } // Update min/max domain values for these objects @@ -121,7 +132,15 @@ define( // Update historical data for this domain object function setHistorical(domainObject) { + var id = domainObject.getId(), + buffer = buffers[id], + endIndex = realtimeIndex[id] || 0; + // Make sure the buffer is big enough + + // Move the realtime data into the correct position + + // Insert the historical data before it } // Handle new telemetry data From 15b1c824e3491a2ac1f0c472fa4357bd8cfdcc27 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 17 Apr 2015 11:35:24 -0700 Subject: [PATCH 06/22] [Plot] Begin separating out plot line handling Begin separating out plot line buffer from the rest of plot; managing this buffer separately will aid in merging realtime and hsitorical data, WTD-806. --- .../features/plot/src/elements/PlotLine.js | 102 +++++++++++++++++ .../plot/src/elements/PlotLineBuffer.js | 107 ++++++++++++++++++ .../plot/src/elements/PlotSeriesWindow.js | 45 ++++++++ .../features/plot/src/elements/PlotUpdater.js | 17 ++- .../plot/test/elements/PlotLineBufferSpec.js | 72 ++++++++++++ platform/features/plot/test/suite.json | 1 + 6 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 platform/features/plot/src/elements/PlotLine.js create mode 100644 platform/features/plot/src/elements/PlotLineBuffer.js create mode 100644 platform/features/plot/src/elements/PlotSeriesWindow.js create mode 100644 platform/features/plot/test/elements/PlotLineBufferSpec.js diff --git a/platform/features/plot/src/elements/PlotLine.js b/platform/features/plot/src/elements/PlotLine.js new file mode 100644 index 0000000000..3c2257c317 --- /dev/null +++ b/platform/features/plot/src/elements/PlotLine.js @@ -0,0 +1,102 @@ +/*global define,Float32Array*/ + +define( + ['./PlotSeriesWindow'], + function (PlotSeriesWindow) { + "use strict"; + + + function PlotLine(initialSize, maxPoints) { + var buffer, + length = 0, + timeWindow; + + // Binary search the buffer to find the index where + // a point with this timestamp should be inserted. + // After is a flag indicating whether it is preferred + // to insert after or before its nearest timestamp + function searchBuffer(timestamp, after) { + // Binary search for an appropriate insertion index. + function binSearch(min, max) { + var mid = Math.floor((min + max) / 2), + ts; + + if (max < min) { + return -1; + } + + ts = buffer[mid * 2]; + + // Check for an exact match... + if (ts === timestamp) { + // This is a case where we'll need to + // split up what we want to insert. + return mid + after ? -1 : 1; + } else { + // Found our index? + if (max === min) { + return max; + } + // Otherwise, search recursively + if (ts < timestamp) { + + } else { + + } + } + + } + + // Booleanize + after = !!after; + + return binSearch(0, length - 1); + } + + function insertSeriesWindow(seriesWindow) { + var startIndex = findStartIndex(), + endIndex = findEndIndex(); + + if (startIndex === endIndex) { + + } else { + // Split it up, and add the two halves + seriesWindow.split().forEach(insertSeriesWindow); + } + } + + function createWindow(series, domain, range) { + // TODO: Enforce time window, too! + return new PlotSeriesWindow( + series, + domain, + range, + 0, + series.getPointCount() + ); + } + + return { + addData: function (domainValue, rangeValue) { + // Should append to buffer + }, + addSeries: function (series, domain, range) { + // Should try to add via insertion if a + // clear insertion point is available; + // if not, should split and add each half. + // Insertion operation also needs to factor out + // redundant timestamps, for overlapping data + insertSeriesWindow(createWindow(series, domain, range)); + }, + setTimeWindow: function (start, end) { + timeWindow = [ start, end ]; + }, + clearTimeWindow: function () { + timeWindow = undefined; + } + }; + } + + return PlotLine; + } +); \ No newline at end of file diff --git a/platform/features/plot/src/elements/PlotLineBuffer.js b/platform/features/plot/src/elements/PlotLineBuffer.js new file mode 100644 index 0000000000..8a1f6a715f --- /dev/null +++ b/platform/features/plot/src/elements/PlotLineBuffer.js @@ -0,0 +1,107 @@ +/*global define,Float32Array*/ + +define( + [], + function () { + "use strict"; + + function PlotLineBuffer(domainOffset, initialSize, maxSize) { + var buffer = new Float32Array(initialSize * 2), + length = 0; + + // Binary search for an insertion index + function binSearch(value, min, max) { + var mid = Math.floor((min + max) / 2), + found = buffer[mid * 2]; + + // Collisions are not wanted + if (found === value) { + return -1; + } + + // Otherwise, if we're down to a single index, + // we've found our insertion point + if (min >= max) { + // Compare the found timestamp with the search + // value to decide if we'll insert after or before. + return min + ((found < value) ? 1 : 0); + } + + // Finally, do the recursive step + if (found < value) { + return binSearch(value, mid + 1, max); + } else { + return binSearch(value, min, mid - 1); + } + } + + // Increase the size of the buffer + function doubleBufferSize() { + var sz = Math.min(maxSize, buffer.length * 2), + canDouble = sz > buffer.length, + doubled = canDouble && new Float32Array(sz); + + if (canDouble) { + doubled.set(buffer); // Copy contents of original + buffer = doubled; + } + + return canDouble; + } + + return { + getBuffer: function () { + return buffer; + }, + insert: function (series, index) { + var sz = series.getPointCount(), + free = (buffer.length / 2) - length, + i; + + // Don't allow append after the end; that doesn't make sense + if (index > length) { + index = length; + } + + // Resize if necessary + if (sz > free) { + if (!doubleBufferSize()) { + // TODO: Figure out which data to discard + i = 0; + } + } + + // Insert data into the set + for (i = 0; i < series.getPointCount(); i += 1) { + buffer[(i + index) * 2] = + series.getDomainValue(i) - domainOffset; + buffer[(i + index) * 2 + 1] = + series.getRangeValue(i); + } + + // Increase the length + length += sz; + }, + /** + * Find an index for inserting data with this + * timestamp. The second argument indicates whether + * we are searching for insert-before or insert-after + * positions. + * Timestamps are meant to be unique, so if a collision + * occurs, this will return -1. + * @param {number} timestamp timestamp to insert + * @returns {number} the index for insertion (or -1) + */ + findInsertionIndex: function (timestamp) { + return binSearch( + timestamp - domainOffset, + 0, + length - 1 + ); + } + }; + } + + return PlotLineBuffer; + } +); \ No newline at end of file diff --git a/platform/features/plot/src/elements/PlotSeriesWindow.js b/platform/features/plot/src/elements/PlotSeriesWindow.js new file mode 100644 index 0000000000..614c09b525 --- /dev/null +++ b/platform/features/plot/src/elements/PlotSeriesWindow.js @@ -0,0 +1,45 @@ +/*global define*/ +define( + [], + function () { + "use strict"; + + /** + * Provides a window on a telemetry data series, to support + * insertion into a plot line. + */ + function PlotSeriesWindow(series, domain, range, start, end) { + return { + getPointCount: function () { + return end - start; + }, + getDomainValue: function (index) { + return series.getDomainValue(index - start, domain); + }, + getRangeValue: function (index) { + return series.getRangeValue(index - start, range); + }, + split: function () { + var mid = Math.floor((end + start) / 2); + return end > start ? + [ + new PlotSeriesWindow( + series, + domain, + range, + start, + mid + ), + new PlotSeriesWindow( + series, + domain, + range, + mid + 1, + end + ) + ] : []; + } + }; + } + } +); \ No newline at end of file diff --git a/platform/features/plot/src/elements/PlotUpdater.js b/platform/features/plot/src/elements/PlotUpdater.js index 0ca8684ad7..3b9be1d7a3 100644 --- a/platform/features/plot/src/elements/PlotUpdater.js +++ b/platform/features/plot/src/elements/PlotUpdater.js @@ -133,8 +133,23 @@ define( // Update historical data for this domain object function setHistorical(domainObject) { var id = domainObject.getId(), + // Buffer to expand buffer = buffers[id], - endIndex = realtimeIndex[id] || 0; + // Index where historical data ends (and realtime begins) + endIndex = realtimeIndex[id] || 0, + // Look up the data series + series = subscription.getSeries(domainObject), + // Get its length + seriesLength = series ? series.getPointCount() : 0, + // Get the current buffer length... + length = lengths[id] || 0, + // As well as the length of the realtime segment + realtimeLength = length - endIndex, + // Determine the new total length of the existing + // realtime + new historical segment. + totalLength = + Math.min(seriesLength + realtimeLength, maxPoints), + seriesFit = Math.max(0, totalLength - realtimeLength); // Make sure the buffer is big enough diff --git a/platform/features/plot/test/elements/PlotLineBufferSpec.js b/platform/features/plot/test/elements/PlotLineBufferSpec.js new file mode 100644 index 0000000000..7838d3bbd4 --- /dev/null +++ b/platform/features/plot/test/elements/PlotLineBufferSpec.js @@ -0,0 +1,72 @@ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +/** + * MergeModelsSpec. Created by vwoeltje on 11/6/14. + */ +define( + ["../../src/elements/PlotLineBuffer"], + function (PlotLineBuffer) { + "use strict"; + + var TEST_INITIAL_SIZE = 10, + TEST_MAX_SIZE = 40, + TEST_DOMAIN_OFFSET = 42; + + describe("A plot line buffer", function () { + var mockSeries, + testDomainValues, + testRangeValues, + buffer; + + beforeEach(function () { + testDomainValues = [ 1, 3, 7, 9, 14, 15 ]; + testRangeValues = [ 8, 0, 3, 9, 8, 11 ]; + mockSeries = jasmine.createSpyObj( + "series", + ['getPointCount', 'getDomainValue', 'getRangeValue'] + ); + mockSeries.getPointCount.andCallFake(function () { + return testDomainValues.length; + }); + mockSeries.getDomainValue.andCallFake(function (i) { + return testDomainValues[i]; + }); + mockSeries.getRangeValue.andCallFake(function (i) { + return testRangeValues[i]; + }); + + buffer = new PlotLineBuffer( + TEST_DOMAIN_OFFSET, + TEST_INITIAL_SIZE, + TEST_MAX_SIZE + ); + + // Start with some data in there + buffer.insert(mockSeries, 0); + }); + + it("allows insertion of series data", function () { + // Convert to a regular array for checking. + // Verify that domain/ranges were interleaved and + // that domain offset was adjusted for. + expect( + Array.prototype.slice.call(buffer.getBuffer()).slice(0, 12) + ).toEqual([ -41, 8, -39, 0, -35, 3, -33, 9, -28, 8, -27, 11]); + }); + + it("finds insertion indexes", function () { + expect(buffer.findInsertionIndex(0)).toEqual(0); + expect(buffer.findInsertionIndex(2)).toEqual(1); + expect(buffer.findInsertionIndex(5)).toEqual(2); + expect(buffer.findInsertionIndex(10)).toEqual(4); + expect(buffer.findInsertionIndex(14.5)).toEqual(5); + expect(buffer.findInsertionIndex(20)).toEqual(6); + + // 9 is already in there, disallow insertion + expect(buffer.findInsertionIndex(9)).toEqual(-1); + }); + + + }); + } +); \ No newline at end of file diff --git a/platform/features/plot/test/suite.json b/platform/features/plot/test/suite.json index 92ee3b07c8..7d099f14da 100644 --- a/platform/features/plot/test/suite.json +++ b/platform/features/plot/test/suite.json @@ -6,6 +6,7 @@ "SubPlot", "SubPlotFactory", "elements/PlotAxis", + "elements/PlotLineBuffer", "elements/PlotPalette", "elements/PlotPanZoomStack", "elements/PlotPanZoomStackGroup", From b20643018a95dced760488a7a96de8379a2226df Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 17 Apr 2015 12:06:44 -0700 Subject: [PATCH 07/22] [Plot] Complete PlotLineBuffer Complete implementation of PlotLineBuffer, which is responsible for managing the displayable buffer for an individual plot line. WTD-806. --- .../plot/src/elements/PlotLineBuffer.js | 74 ++++++++++++++++++- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/platform/features/plot/src/elements/PlotLineBuffer.js b/platform/features/plot/src/elements/PlotLineBuffer.js index 8a1f6a715f..d875c9bc06 100644 --- a/platform/features/plot/src/elements/PlotLineBuffer.js +++ b/platform/features/plot/src/elements/PlotLineBuffer.js @@ -5,6 +5,13 @@ define( function () { "use strict"; + /** + * Contains the buffer used to draw a plot. + * @param {number} domainOffset number to subtract from domain values + * @param {number} initialSize initial buffer size + * @param {number} maxSize maximum buffer size + * @constructor + */ function PlotLineBuffer(domainOffset, initialSize, maxSize) { var buffer = new Float32Array(initialSize * 2), length = 0; @@ -49,10 +56,60 @@ define( return canDouble; } + // Decrease the size of the buffer + function halveBufferSize() { + var sz = Math.max(initialSize, buffer.length / 2), + canHalve = sz < buffer.length; + + if (canHalve) { + buffer = new Float32Array(buffer.subarray(0, sz)); + } + + return canHalve; + } + + return { + /** + * Get the WebGL-displayable buffer of points to plot. + * @returns {Float32Array} displayable buffer for this line + */ getBuffer: function () { return buffer; }, + /** + * Remove values from this buffer. + * Normally, values are removed from the start + * of the buffer; a truthy value in the second argument + * will cause values to be removed from the end. + * @param {number} count number of values to remove + * @param {boolean} [fromEnd] true if the most recent + * values should be removed + */ + trim: function (count, fromEnd) { + // If we're removing values from the start... + if (!fromEnd) { + // ...do so by shifting buffer contents over + buffer.set(buffer.subarray(2 * count)); + } + // Reduce used buffer size accordingly + length -= count; + // Finally, if less than half of the buffer is being + // used, free up some memory. + if (length < buffer.length / 4) { + halveBufferSize(); + } + }, + /** + * Insert data from the provided series at the specified + * index. If this would exceed the buffer's maximum capacity, + * this operation fails and the buffer is unchanged. + * @param {TelemetrySeries} series the series to insert + * @param {number} index the index at which to insert this + * series + * @returns {boolean} true if insertion succeeded; otherwise + * false + */ insert: function (series, index) { var sz = series.getPointCount(), free = (buffer.length / 2) - length, @@ -66,13 +123,21 @@ define( // Resize if necessary if (sz > free) { if (!doubleBufferSize()) { - // TODO: Figure out which data to discard - i = 0; + // Can't make room for this, insertion fails + return false; } } + // Shift data over if necessary + if (index < length) { + buffer.set( + buffer.subarray(index, length - index), + index + sz + ); + } + // Insert data into the set - for (i = 0; i < series.getPointCount(); i += 1) { + for (i = 0; i < sz; i += 1) { buffer[(i + index) * 2] = series.getDomainValue(i) - domainOffset; buffer[(i + index) * 2 + 1] = @@ -81,6 +146,9 @@ define( // Increase the length length += sz; + + // Indicate that insertion was successful + return true; }, /** * Find an index for inserting data with this From 6551e9212db7408d65fb90b9624beddbfe12c06a Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 17 Apr 2015 12:13:17 -0700 Subject: [PATCH 08/22] [Plot] Fix insertion in PlotLineBuffer WTD-806. --- platform/features/plot/src/elements/PlotLineBuffer.js | 4 ++-- .../features/plot/test/elements/PlotLineBufferSpec.js | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/platform/features/plot/src/elements/PlotLineBuffer.js b/platform/features/plot/src/elements/PlotLineBuffer.js index d875c9bc06..307abae666 100644 --- a/platform/features/plot/src/elements/PlotLineBuffer.js +++ b/platform/features/plot/src/elements/PlotLineBuffer.js @@ -131,8 +131,8 @@ define( // Shift data over if necessary if (index < length) { buffer.set( - buffer.subarray(index, length - index), - index + sz + buffer.subarray(index * 2, length * 2), + (index + sz) * 2 ); } diff --git a/platform/features/plot/test/elements/PlotLineBufferSpec.js b/platform/features/plot/test/elements/PlotLineBufferSpec.js index 7838d3bbd4..5d6b294da8 100644 --- a/platform/features/plot/test/elements/PlotLineBufferSpec.js +++ b/platform/features/plot/test/elements/PlotLineBufferSpec.js @@ -66,6 +66,14 @@ define( expect(buffer.findInsertionIndex(9)).toEqual(-1); }); + it("allows insertion in the middle", function () { + var head = [ -41, 8, -39, 0, -35, 3 ], + tail = [ -33, 9, -28, 8, -27, 11]; + buffer.insert(mockSeries, 3); + expect( + Array.prototype.slice.call(buffer.getBuffer()).slice(0, 24) + ).toEqual(head.concat(head).concat(tail).concat(tail)); + }); }); } From 4d34f19aa2842a7637fa3c58d986f4d9c1bd072f Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 17 Apr 2015 12:26:44 -0700 Subject: [PATCH 09/22] [Plot] Complete coverage of PlotLineBuffer WTD-806. --- .../plot/src/elements/PlotLineBuffer.js | 15 ++++-- .../plot/test/elements/PlotLineBufferSpec.js | 46 +++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/platform/features/plot/src/elements/PlotLineBuffer.js b/platform/features/plot/src/elements/PlotLineBuffer.js index 307abae666..1504fe021c 100644 --- a/platform/features/plot/src/elements/PlotLineBuffer.js +++ b/platform/features/plot/src/elements/PlotLineBuffer.js @@ -44,7 +44,7 @@ define( // Increase the size of the buffer function doubleBufferSize() { - var sz = Math.min(maxSize, buffer.length * 2), + var sz = Math.min(maxSize * 2, buffer.length * 2), canDouble = sz > buffer.length, doubled = canDouble && new Float32Array(sz); @@ -58,7 +58,7 @@ define( // Decrease the size of the buffer function halveBufferSize() { - var sz = Math.max(initialSize, buffer.length / 2), + var sz = Math.max(initialSize * 2, buffer.length / 2), canHalve = sz < buffer.length; if (canHalve) { @@ -77,6 +77,13 @@ define( getBuffer: function () { return buffer; }, + /** + * Get the number of points stored in this buffer. + * @returns {number} the number of points stored + */ + getLength: function () { + return length; + }, /** * Remove values from this buffer. * Normally, values are removed from the start @@ -116,9 +123,7 @@ define( i; // Don't allow append after the end; that doesn't make sense - if (index > length) { - index = length; - } + index = Math.min(index, length); // Resize if necessary if (sz > free) { diff --git a/platform/features/plot/test/elements/PlotLineBufferSpec.js b/platform/features/plot/test/elements/PlotLineBufferSpec.js index 5d6b294da8..3fc7b06c6e 100644 --- a/platform/features/plot/test/elements/PlotLineBufferSpec.js +++ b/platform/features/plot/test/elements/PlotLineBufferSpec.js @@ -52,6 +52,7 @@ define( expect( Array.prototype.slice.call(buffer.getBuffer()).slice(0, 12) ).toEqual([ -41, 8, -39, 0, -35, 3, -33, 9, -28, 8, -27, 11]); + expect(buffer.getLength()).toEqual(6); }); it("finds insertion indexes", function () { @@ -73,6 +74,51 @@ define( expect( Array.prototype.slice.call(buffer.getBuffer()).slice(0, 24) ).toEqual(head.concat(head).concat(tail).concat(tail)); + expect(buffer.getLength()).toEqual(12); + }); + + it("allows values to be trimmed from the start", function () { + buffer.trim(2); + expect(buffer.getLength()).toEqual(4); + expect( + Array.prototype.slice.call(buffer.getBuffer()).slice(0, 8) + ).toEqual([ -35, 3, -33, 9, -28, 8, -27, 11]); + }); + + it("ensures a maximum size", function () { + var i; + + // Should be able to insert 6 series of 6 points each + // (After that, we'll hit the test max of 40) + for (i = 1; i < 6; i += 1) { + expect(buffer.getLength()).toEqual(6 * i); + expect(buffer.insert(mockSeries, Number.POSITIVE_INFINITY)) + .toBeTruthy(); + } + + // Should be maxed out now + expect(buffer.getLength()).toEqual(36); + expect(buffer.insert(mockSeries, Number.POSITIVE_INFINITY)) + .toBeFalsy(); + expect(buffer.getLength()).toEqual(36); + + }); + + it("reduces buffer size when space is no longer needed", function () { + // Check that actual buffer is sized to the initial size + // (double TEST_INITIAL_SIZE, since two elements are needed per + // point; one for domain, one for range) + expect(buffer.getBuffer().length).toEqual(20); + // Should have 6 elements now... grow to 24 + buffer.insert(mockSeries, Number.POSITIVE_INFINITY); + buffer.insert(mockSeries, Number.POSITIVE_INFINITY); + buffer.insert(mockSeries, Number.POSITIVE_INFINITY); + // This should have doubled the actual buffer size + expect(buffer.getBuffer().length).toEqual(80); + // Remove some values + buffer.trim(20); + // Actual buffer size should have been reduced accordingly + expect(buffer.getBuffer().length).toBeLessThan(80); }); }); From 7370b1a87ff59fc97b13a9b8618518ecc7208a62 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 17 Apr 2015 14:00:00 -0700 Subject: [PATCH 10/22] [Plot] Use new API from PlotUpdater Use PlotLine and PlotLineBuffer from PlotUpdater, to enable merging of real-time and historical telemetry. WTD-806. --- .../features/plot/src/elements/PlotLine.js | 90 ++---- .../plot/src/elements/PlotLineBuffer.js | 84 ++++- .../features/plot/src/elements/PlotUpdater.js | 293 +++++++++--------- .../plot/src/modes/PlotOverlayMode.js | 6 +- .../features/plot/src/modes/PlotStackMode.js | 6 +- 5 files changed, 255 insertions(+), 224 deletions(-) diff --git a/platform/features/plot/src/elements/PlotLine.js b/platform/features/plot/src/elements/PlotLine.js index 3c2257c317..3a507868d9 100644 --- a/platform/features/plot/src/elements/PlotLine.js +++ b/platform/features/plot/src/elements/PlotLine.js @@ -6,67 +6,35 @@ define( "use strict"; - function PlotLine(initialSize, maxPoints) { - var buffer, - length = 0, - timeWindow; + function PlotLine(buffer) { - // Binary search the buffer to find the index where - // a point with this timestamp should be inserted. - // After is a flag indicating whether it is preferred - // to insert after or before its nearest timestamp - function searchBuffer(timestamp, after) { - // Binary search for an appropriate insertion index. - function binSearch(min, max) { - var mid = Math.floor((min + max) / 2), - ts; + // Insert a time-windowed data series into the buffer + function insertSeriesWindow(seriesWindow) { + var count = seriesWindow.getPointCount(); - if (max < min) { - return -1; - } + function doInsert() { + var firstTimestamp = buffer.getDomainValue(0), + lastTimestamp = buffer.getDomainValue(count - 1), + startIndex = buffer.findInsertionIndex(firstTimestamp), + endIndex = buffer.findInsertionIndex(lastTimestamp); - ts = buffer[mid * 2]; - - // Check for an exact match... - if (ts === timestamp) { - // This is a case where we'll need to - // split up what we want to insert. - return mid + after ? -1 : 1; + // Does the whole series fit in between two adjacent indexes? + if ((startIndex === endIndex) && startIndex > -1) { + // Insert it in between + buffer.insert(seriesWindow, startIndex); } else { - // Found our index? - if (max === min) { - return max; - } - // Otherwise, search recursively - if (ts < timestamp) { - - } else { - - } + // Split it up, and add the two halves + seriesWindow.split().forEach(insertSeriesWindow); } - } - // Booleanize - after = !!after; - - return binSearch(0, length - 1); - } - - function insertSeriesWindow(seriesWindow) { - var startIndex = findStartIndex(), - endIndex = findEndIndex(); - - if (startIndex === endIndex) { - - } else { - // Split it up, and add the two halves - seriesWindow.split().forEach(insertSeriesWindow); + // Only insert if there are points to insert + if (count > 0) { + doInsert(); } } function createWindow(series, domain, range) { - // TODO: Enforce time window, too! return new PlotSeriesWindow( series, domain, @@ -77,8 +45,20 @@ define( } return { - addData: function (domainValue, rangeValue) { - // Should append to buffer + getLineBuffer: function () { + return buffer; + }, + addPoint: function (domainValue, rangeValue) { + var index = buffer.findInsertionIndex(domainValue); + if (index > -1) { + // Insert the point + if (!buffer.insertPoint(domainValue, rangeValue, index)) { + // If insertion failed, trim from the beginning... + buffer.trim(1); + // ...and try again. + buffer.insertPoint(domainValue, rangeValue, index); + } + } }, addSeries: function (series, domain, range) { // Should try to add via insertion if a @@ -87,12 +67,6 @@ define( // Insertion operation also needs to factor out // redundant timestamps, for overlapping data insertSeriesWindow(createWindow(series, domain, range)); - }, - setTimeWindow: function (start, end) { - timeWindow = [ start, end ]; - }, - clearTimeWindow: function () { - timeWindow = undefined; } }; } diff --git a/platform/features/plot/src/elements/PlotLineBuffer.js b/platform/features/plot/src/elements/PlotLineBuffer.js index 1504fe021c..724347ed0c 100644 --- a/platform/features/plot/src/elements/PlotLineBuffer.js +++ b/platform/features/plot/src/elements/PlotLineBuffer.js @@ -14,6 +14,7 @@ define( */ function PlotLineBuffer(domainOffset, initialSize, maxSize) { var buffer = new Float32Array(initialSize * 2), + rangeExtrema = [ Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY ], length = 0; // Binary search for an insertion index @@ -68,6 +69,17 @@ define( return canHalve; } + // Set a value in the buffer + function setValue(index, domainValue, rangeValue) { + buffer[index * 2] = domainValue - domainOffset; + buffer[index * 2 + 1] = rangeValue; + // Track min/max of range values (min/max for + // domain values can be read directly from buffer) + rangeExtrema = [ + Math.min(rangeExtrema[0], rangeValue), + Math.max(rangeExtrema[1], rangeValue) + ]; + } return { /** @@ -84,6 +96,29 @@ define( getLength: function () { return length; }, + /** + * Get the min/max range values that are currently in this + * buffer. Unlike range extrema, these will change as the + * buffer gets trimmed. + * @returns {number[]} min, max domain values + */ + getDomainExtrema: function () { + // Since these are ordered in the buffer, assume + // these are the values at the first and last index + return [ + buffer[0] + domainOffset, + buffer[length * 2 - 2] + domainOffset + ]; + }, + /** + * Get the min/max range values that have been observed for this + * buffer. Note that these values may have been trimmed out at + * some point. + * @returns {number[]} min, max range values + */ + getRangeExtrema: function () { + return rangeExtrema; + }, /** * Remove values from this buffer. * Normally, values are removed from the start @@ -119,14 +154,13 @@ define( */ insert: function (series, index) { var sz = series.getPointCount(), - free = (buffer.length / 2) - length, i; // Don't allow append after the end; that doesn't make sense index = Math.min(index, length); // Resize if necessary - if (sz > free) { + while (sz > ((buffer.length / 2) - length)) { if (!doubleBufferSize()) { // Can't make room for this, insertion fails return false; @@ -143,10 +177,11 @@ define( // Insert data into the set for (i = 0; i < sz; i += 1) { - buffer[(i + index) * 2] = - series.getDomainValue(i) - domainOffset; - buffer[(i + index) * 2 + 1] = - series.getRangeValue(i); + setValue( + i + index, + series.getDomainValue(i), + series.getRangeValue(i) + ); } // Increase the length @@ -155,6 +190,29 @@ define( // Indicate that insertion was successful return true; }, + /** + * Append a single data point. + */ + insertPoint: function (domainValue, rangeValue, index) { + // Don't allow + index = Math.min(length, index); + + // Ensure there is space for this point + if (length >= (buffer.length / 2)) { + if (!doubleBufferSize()) { + return false; + } + } + + // Put the data in the buffer + setValue(length, domainValue, rangeValue); + + // Update length + length += 1; + + // Indicate that this was successful + return true; + }, /** * Find an index for inserting data with this * timestamp. The second argument indicates whether @@ -166,11 +224,15 @@ define( * @returns {number} the index for insertion (or -1) */ findInsertionIndex: function (timestamp) { - return binSearch( - timestamp - domainOffset, - 0, - length - 1 - ); + var value = timestamp - domainOffset; + + // Handle empty buffer case and check for an + // append opportunity (which is most common case for + // real-time data so is optimized-for) before falling + // back to a binary search for the insertion point. + return (length < 1) ? 0 : + (value > buffer[length * 2 - 2]) ? length : + binSearch(value, 0, length - 1); } }; } diff --git a/platform/features/plot/src/elements/PlotUpdater.js b/platform/features/plot/src/elements/PlotUpdater.js index 3b9be1d7a3..f39b77437b 100644 --- a/platform/features/plot/src/elements/PlotUpdater.js +++ b/platform/features/plot/src/elements/PlotUpdater.js @@ -5,7 +5,8 @@ * the conversion from data API to displayable buffers. */ define( - function () { + ['./PlotLine', './PlotLineBuffer'], + function (PlotLine, PlotLineBuffer) { 'use strict'; var MAX_POINTS = 86400, @@ -17,164 +18,171 @@ define( * 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 {TelemetryHandle} handle the handle to telemetry access * @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, maxPoints) { - var max = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], - min = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], - x, - y, - domainOffset, - buffers = {}, - lengths = {}, - realtimeIndex = {}, - lengthArray = [], - bufferArray = []; + function PlotUpdater(handle, domain, range, maxPoints) { + var ids = [], + lines = {}, + dimensions = [0, 0], + origin = [0, 0], + domainExtrema, + rangeExtrema, + bufferArray = [], + domainOffset; - // Double the size of a Float32Array - function doubleSize(buffer) { - var doubled = new Float32Array(buffer.length * 2); - doubled.set(buffer); // Copy contents of original - return doubled; + // Look up a domain object's id (for mapping, below) + function getId(domainObject) { + return domainObject.getId(); } - // Make sure there is enough space in a buffer to accomodate a - // new point at the specified index. This will updates buffers[id] - // if necessary. - function ensureBufferSize(buffer, id, index) { - // Check if we don't have enough room - if (index > (buffer.length / 2 - 1)) { - // If we don't, can we expand? - if (index < maxPoints) { - // Double the buffer size - buffer = buffers[id] = doubleSize(buffer); - } else { - // Just shift the existing buffer - buffer.set(buffer.subarray(2)); - // Update the realtime index accordingly - realtimeIndex[id] = Math.max(realtimeIndex[id] - 1, 0); - } + // Check if this set of ids matches the current set of ids + // (used to detect if line preparation can be skipped) + function idsMatch(nextIds) { + return nextIds.map(function (id, index) { + return ids[index] === id; + }).reduce(function (a, b) { + return a && b; + }, true); + } + + // Prepare plot lines for this group of telemetry objects + function prepareLines(telemetryObjects) { + var nextIds = telemetryObjects.map(getId), + next = {}; + + // Detect if we already have everything we need prepared + if (ids.length === nextIds.length && idsMatch(nextIds)) { + // Nothing to prepare, move on + return; } - return buffer; - } + // Update list of ids in use + ids = nextIds; - function setData(buffer, id, index, domainValue, rangeValue) { - // Make sure there's data to add, and then add it - 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, maxPoints - 1); - // Update the buffer - buffer[index * 2] = domainValue - domainOffset; - buffer[index * 2 + 1] = rangeValue; - // Update length - lengths[id] = Math.min(index + 1, maxPoints); - // Observe max/min range values - max[1] = Math.max(max[1], rangeValue); - min[1] = Math.min(min[1], rangeValue); - } - return buffer; - } - - // Add data to the plot. - function addData(obj) { - var id = obj.getId(), - index = lengths[id] || 0, - buffer = buffers[id], - domainValue = subscription.getDomainValue(obj, domain), - rangeValue = subscription.getRangeValue(obj, range); - - // If we don't already have a data buffer for that ID, - // make one. - if (!buffer) { - buffer = new Float32Array(INITIAL_SIZE); - buffers[id] = buffer; + // Built up a set of ids. Note that we can only + // create plot lines after our domain offset has + // been determined. + if (domainOffset !== undefined) { + bufferArray = ids.map(function (id) { + var buffer = new PlotLineBuffer( + domainOffset, + INITIAL_SIZE, + maxPoints + ); + next[id] = lines[id] || new PlotLine(buffer); + return buffer; + }); } - // Update the cutoff point for when we started receiving - // realtime data, to aid in clearing historical data later - if (realtimeIndex[id] === undefined) { - realtimeIndex[id] = index; + // If there are no more lines, clear the domain offset + if (Object.keys(lines).length < 1) { + domainOffset = undefined; } - // Put the data in the buffer - return setData( - buffer, - id, - index, - domainValue, - rangeValue - ); + // Update to the current set of lines + lines = next; } - // Update min/max domain values for these objects - 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], - low = buffer[0] + domainOffset, - high = buffer[length * 2 - 2] + domainOffset; - max[0] = Math.max(high, max[0]); - min[0] = Math.min(low, min[0]); - }); + // Initialize the domain offset, based on these observed values + function initializeDomainOffset(values) { + domainOffset = + ((domainOffset === undefined) && (values.length > 0)) ? + (values.reduce(function (a, b) { + return (a || 0) + (b || 0); + }, 0) / values.length) : + domainOffset; } - // Update historical data for this domain object - function setHistorical(domainObject) { - var id = domainObject.getId(), - // Buffer to expand - buffer = buffers[id], - // Index where historical data ends (and realtime begins) - endIndex = realtimeIndex[id] || 0, - // Look up the data series - series = subscription.getSeries(domainObject), - // Get its length - seriesLength = series ? series.getPointCount() : 0, - // Get the current buffer length... - length = lengths[id] || 0, - // As well as the length of the realtime segment - realtimeLength = length - endIndex, - // Determine the new total length of the existing - // realtime + new historical segment. - totalLength = - Math.min(seriesLength + realtimeLength, maxPoints), - seriesFit = Math.max(0, totalLength - realtimeLength); + // Used in the reduce step of updateExtrema + function reduceExtrema(a, b) { + return [ Math.min(a[0], b[0]), Math.max(a[1], b[1]) ]; + } - // Make sure the buffer is big enough + // Convert a domain/range extrema to plot dimensions + function dimensionsOf(extrema) { + return extrema[1] - extrema[0]; + } - // Move the realtime data into the correct position + // Convert a domain/range extrema to a plot origin + function originOf(extrema) { + return extrema[0]; + } - // Insert the historical data before it + // Update dimensions and origin based on extrema of plots + function updateExtrema() { + domainExtrema = bufferArray.map(function (lineBuffer) { + return lineBuffer.getDomainExtrema(); + }).reduce(reduceExtrema); + + rangeExtrema = bufferArray.map(function (lineBuffer) { + return lineBuffer.getRangeExtrema(); + }).reduce(reduceExtrema); + + dimensions = (rangeExtrema[0] === rangeExtrema[1]) ? + [dimensionsOf(domainExtrema), 2.0 ] : + [dimensionsOf(domainExtrema), dimensionsOf(rangeExtrema)]; + origin = [originOf(domainExtrema), originOf(rangeExtrema)]; } // Handle new telemetry data function update() { - var objects = subscription.getTelemetryObjects(); - bufferArray = objects.map(addData); - lengthArray = objects.map(function (obj) { - return lengths[obj.getId()]; + var objects = handle.getTelemetryObjects(); + + // Initialize domain offset if necessary + if (domainOffset === undefined) { + initializeDomainOffset(objects.map(function (obj) { + return handle.getDomainValue(obj, domain); + })); + } + + // Make sure lines are available + prepareLines(objects); + + // Add new data + objects.forEach(function (obj, index) { + lines[obj.getId()].addPoint( + handle.getDomainValue(obj, domain), + handle.getRangeValue(obj, range) + ); }); - updateDomainExtrema(objects); + + // Finally, update extrema + updateExtrema(); } - // Prepare buffers and related state for this object - function prepare(telemetryObject) { - var id = telemetryObject.getId(); - lengths[id] = 0; - buffers[id] = new Float32Array(INITIAL_SIZE); - lengthArray.push(lengths[id]); - bufferArray.push(buffers[id]); + // Add historical data for this domain object + function setHistorical(domainObject, series) { + var count = series.getPointCount(), + line; + + // Nothing to do if it's an empty series + if (count < 1) { + return; + } + + // Initialize domain offset if necessary + if (domainOffset === undefined && series) { + initializeDomainOffset([ + series.getDomainValue(0, domain), + series.getDomainValue(count - 1, domain) + ]); + } + + // Make sure lines are available + prepareLines(handle.getTelemetryObjects()); + + // Look up the line for this domain object + line = lines[domainObject.getId()]; + + // ...and put the data into it. + if (line) { + line.addSeries(series, domain, range); + } + + // Finally, update extrema + updateExtrema(); } // Use a default MAX_POINTS if none is provided @@ -183,7 +191,7 @@ define( // Initially prepare state for these objects. // Note that this may be an empty array at this time, // so we also need to check during update cycles. - subscription.getTelemetryObjects().forEach(prepare); + prepareLines(handle.getTelemetryObjects()); return { /** @@ -193,10 +201,7 @@ define( * @returns {number[]} the dimensions which bound this data set */ getDimensions: function () { - // Pad range if necessary - return (max[1] === min[1]) ? - [max[0] - min[0], 2.0 ] : - [max[0] - min[0], max[1] - min[1]]; + return dimensions; }, /** * Get the origin of this data set's boundary. @@ -207,7 +212,7 @@ define( */ getOrigin: function () { // Pad range if necessary - return (max[1] === min[1]) ? [ min[0], min[1] - 1.0 ] : min; + return origin; }, /** * Get the domain offset; this offset will have been subtracted @@ -236,19 +241,9 @@ define( * * @returns {Float32Array[]} the buffers for these traces */ - getBuffers: function () { + getLineBuffers: function () { return bufferArray; }, - /** - * Get the number of points in the buffer with the specified - * index. Buffers are padded to minimize memory allocations, - * so user code will need this information to know how much - * data to plot. - * @returns {number} the number of points in this buffer - */ - getLength: function (index) { - return lengthArray[index] || 0; - }, /** * Update with latest data. */ diff --git a/platform/features/plot/src/modes/PlotOverlayMode.js b/platform/features/plot/src/modes/PlotOverlayMode.js index 342bbcc1c3..093ae07b1b 100644 --- a/platform/features/plot/src/modes/PlotOverlayMode.js +++ b/platform/features/plot/src/modes/PlotOverlayMode.js @@ -34,11 +34,11 @@ define( subplot.setDomainOffset(prepared.getDomainOffset()); // Draw the buffers. Select color by index. - subplot.getDrawingObject().lines = prepared.getBuffers().map(function (buf, i) { + subplot.getDrawingObject().lines = prepared.getLineBuffers().map(function (buf, i) { return { - buffer: buf, + buffer: buf.getBuffer(), color: PlotPalette.getFloatColor(i), - points: prepared.getLength(i) + points: buf.getLength() }; }); diff --git a/platform/features/plot/src/modes/PlotStackMode.js b/platform/features/plot/src/modes/PlotStackMode.js index 5fe3895a61..a92179dcdb 100644 --- a/platform/features/plot/src/modes/PlotStackMode.js +++ b/platform/features/plot/src/modes/PlotStackMode.js @@ -23,7 +23,7 @@ define( }); function plotTelemetryTo(subplot, prepared, index) { - var buffer = prepared.getBuffers()[index]; + var buffer = prepared.getLineBuffers()[index]; // Track the domain offset, used to bias domain values // to minimize loss of precision when converted to 32-bit @@ -33,9 +33,9 @@ define( // Draw the buffers. Always use the 0th color, because there // is one line per plot. subplot.getDrawingObject().lines = [{ - buffer: buffer, + buffer: buffer.getBuffer(), color: PlotPalette.getFloatColor(0), - points: prepared.getLength(index) + points: buffer.getLength() }]; subplot.update(); From 9f390de21382a8ae13b680c003cb3b8d592a5ec0 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 17 Apr 2015 14:04:46 -0700 Subject: [PATCH 11/22] [Plot] Add clarifying comments Add clarifying comments to PlotLine, added to simplify merging of real-time with historical data, WTD-806. --- platform/features/plot/src/elements/PlotLine.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/platform/features/plot/src/elements/PlotLine.js b/platform/features/plot/src/elements/PlotLine.js index 3a507868d9..d5bba0e320 100644 --- a/platform/features/plot/src/elements/PlotLine.js +++ b/platform/features/plot/src/elements/PlotLine.js @@ -45,9 +45,11 @@ define( } return { - getLineBuffer: function () { - return buffer; - }, + /** + * Add a point to this plot line. + * @param {number} domainValue the domain value + * @param {number} rangeValue the range value + */ addPoint: function (domainValue, rangeValue) { var index = buffer.findInsertionIndex(domainValue); if (index > -1) { @@ -60,6 +62,14 @@ define( } } }, + /** + * Add a series of telemetry data to this plot line. + * @param {TelemetrySeries} series the data series + * @param {string} [domain] the key indicating which domain + * to use when looking up data from this series + * @param {string} [range] the key indicating which range + * to use when looking up data from this series + */ addSeries: function (series, domain, range) { // Should try to add via insertion if a // clear insertion point is available; From 5cc89e798316bd9fa5b0f7be0e56a211bd2849ff Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 17 Apr 2015 14:09:39 -0700 Subject: [PATCH 12/22] [Plot] Update PlotController Update plot controller to request historical telemetry, WTD-806. --- platform/features/plot/src/PlotController.js | 36 +++++++++++-------- .../features/plot/src/elements/PlotUpdater.js | 2 +- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/platform/features/plot/src/PlotController.js b/platform/features/plot/src/PlotController.js index 6449e0f3a1..a2f5695f42 100644 --- a/platform/features/plot/src/PlotController.js +++ b/platform/features/plot/src/PlotController.js @@ -30,13 +30,13 @@ define( * * @constructor */ - function PlotController($scope, telemetryFormatter, telemetrySubscriber) { + function PlotController($scope, telemetryFormatter, telemetryHandler) { var subPlotFactory = new SubPlotFactory(telemetryFormatter), modeOptions = new PlotModeOptions([], subPlotFactory), subplots = [], cachedObjects = [], updater, - subscription, + handle, domainOffset; // Populate the scope with axis information (specifically, options @@ -77,7 +77,7 @@ define( // new subscription.) This will clear the plot. function recreateUpdater() { updater = new PlotUpdater( - subscription, + handle, ($scope.axes[0].active || {}).key, ($scope.axes[1].active || {}).key ); @@ -85,8 +85,8 @@ define( // Handle new telemetry data in this plot function updateValues() { - if (subscription) { - setupModes(subscription.getTelemetryObjects()); + if (handle) { + setupModes(handle.getTelemetryObjects()); } if (updater) { updater.update(); @@ -95,29 +95,37 @@ define( update(); } + // Issue a new request for historical telemetry + function requestTelemetry() { + if (handle && updater) { + handle.request({}, updater.addHistorical); + } + } + // Create a new subscription; telemetrySubscriber gets // to do the meaningful work here. function subscribe(domainObject) { - if (subscription) { - subscription.unsubscribe(); + if (handle) { + handle.unsubscribe(); } - subscription = domainObject && telemetrySubscriber.subscribe( + handle = domainObject && telemetryHandler.handle( domainObject, updateValues, true // Lossless ); - if (subscription) { - setupModes(subscription.getTelemetryObjects()); - setupAxes(subscription.getMetadata()); + if (handle) { + setupModes(handle.getTelemetryObjects()); + setupAxes(handle.getMetadata()); recreateUpdater(); + requestTelemetry(); } } // Release the current subscription (called when scope is destroyed) function releaseSubscription() { - if (subscription) { - subscription.unsubscribe(); - subscription = undefined; + if (handle) { + handle.unsubscribe(); + handle = undefined; } } diff --git a/platform/features/plot/src/elements/PlotUpdater.js b/platform/features/plot/src/elements/PlotUpdater.js index f39b77437b..3e155131e4 100644 --- a/platform/features/plot/src/elements/PlotUpdater.js +++ b/platform/features/plot/src/elements/PlotUpdater.js @@ -251,7 +251,7 @@ define( /** * Fill in historical data. */ - setHistorical: setHistorical + addHistorical: setHistorical }; } From 8ba9c0553a745f90bf98c36e918fdde0478be482 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 17 Apr 2015 14:53:21 -0700 Subject: [PATCH 13/22] [Plot] Begin integrating with telemetry handler Miscellaneous tweaks and fixes to begin showing merged real-time and historical telemetry, WTD-806. --- platform/features/plot/bundle.json | 2 +- .../features/plot/src/elements/PlotLine.js | 16 ++++-- .../plot/src/elements/PlotLineBuffer.js | 6 +-- .../plot/src/elements/PlotSeriesWindow.js | 2 + .../features/plot/src/elements/PlotUpdater.js | 53 +++++++++++-------- platform/telemetry/bundle.json | 5 ++ 6 files changed, 54 insertions(+), 30 deletions(-) diff --git a/platform/features/plot/bundle.json b/platform/features/plot/bundle.json index 67c69d9e4b..ab8cf89b7c 100644 --- a/platform/features/plot/bundle.json +++ b/platform/features/plot/bundle.json @@ -22,7 +22,7 @@ { "key": "PlotController", "implementation": "PlotController.js", - "depends": [ "$scope", "telemetryFormatter", "telemetrySubscriber" ] + "depends": [ "$scope", "telemetryFormatter", "telemetryHandler" ] } ] } diff --git a/platform/features/plot/src/elements/PlotLine.js b/platform/features/plot/src/elements/PlotLine.js index d5bba0e320..00a65cbfac 100644 --- a/platform/features/plot/src/elements/PlotLine.js +++ b/platform/features/plot/src/elements/PlotLine.js @@ -13,8 +13,8 @@ define( var count = seriesWindow.getPointCount(); function doInsert() { - var firstTimestamp = buffer.getDomainValue(0), - lastTimestamp = buffer.getDomainValue(count - 1), + var firstTimestamp = seriesWindow.getDomainValue(0), + lastTimestamp = seriesWindow.getDomainValue(count - 1), startIndex = buffer.findInsertionIndex(firstTimestamp), endIndex = buffer.findInsertionIndex(lastTimestamp); @@ -51,8 +51,16 @@ define( * @param {number} rangeValue the range value */ addPoint: function (domainValue, rangeValue) { - var index = buffer.findInsertionIndex(domainValue); - if (index > -1) { + var index; + // Make sure we got real/useful values here... + if (domainValue !== undefined && rangeValue !== undefined) { + index = buffer.findInsertionIndex(domainValue); + + // Already in the buffer? Skip insertion + if (index < 0) { + return; + } + // Insert the point if (!buffer.insertPoint(domainValue, rangeValue, index)) { // If insertion failed, trim from the beginning... diff --git a/platform/features/plot/src/elements/PlotLineBuffer.js b/platform/features/plot/src/elements/PlotLineBuffer.js index 724347ed0c..7511ab4870 100644 --- a/platform/features/plot/src/elements/PlotLineBuffer.js +++ b/platform/features/plot/src/elements/PlotLineBuffer.js @@ -75,10 +75,8 @@ define( buffer[index * 2 + 1] = rangeValue; // Track min/max of range values (min/max for // domain values can be read directly from buffer) - rangeExtrema = [ - Math.min(rangeExtrema[0], rangeValue), - Math.max(rangeExtrema[1], rangeValue) - ]; + rangeExtrema[0] = Math.min(rangeExtrema[0], rangeValue); + rangeExtrema[1] = Math.max(rangeExtrema[1], rangeValue); } return { diff --git a/platform/features/plot/src/elements/PlotSeriesWindow.js b/platform/features/plot/src/elements/PlotSeriesWindow.js index 614c09b525..d8308eb3eb 100644 --- a/platform/features/plot/src/elements/PlotSeriesWindow.js +++ b/platform/features/plot/src/elements/PlotSeriesWindow.js @@ -41,5 +41,7 @@ define( } }; } + + return PlotSeriesWindow; } ); \ No newline at end of file diff --git a/platform/features/plot/src/elements/PlotUpdater.js b/platform/features/plot/src/elements/PlotUpdater.js index 3e155131e4..815b91b4e5 100644 --- a/platform/features/plot/src/elements/PlotUpdater.js +++ b/platform/features/plot/src/elements/PlotUpdater.js @@ -58,13 +58,14 @@ define( return; } - // Update list of ids in use - ids = nextIds; - // Built up a set of ids. Note that we can only // create plot lines after our domain offset has // been determined. if (domainOffset !== undefined) { + // Update list of ids in use + ids = nextIds; + + // Create buffers for these objects bufferArray = ids.map(function (id) { var buffer = new PlotLineBuffer( domainOffset, @@ -112,18 +113,31 @@ define( // Update dimensions and origin based on extrema of plots function updateExtrema() { - domainExtrema = bufferArray.map(function (lineBuffer) { - return lineBuffer.getDomainExtrema(); - }).reduce(reduceExtrema); + if (bufferArray.length > 0) { + domainExtrema = bufferArray.map(function (lineBuffer) { + return lineBuffer.getDomainExtrema(); + }).reduce(reduceExtrema); - rangeExtrema = bufferArray.map(function (lineBuffer) { - return lineBuffer.getRangeExtrema(); - }).reduce(reduceExtrema); + rangeExtrema = bufferArray.map(function (lineBuffer) { + return lineBuffer.getRangeExtrema(); + }).reduce(reduceExtrema); - dimensions = (rangeExtrema[0] === rangeExtrema[1]) ? - [dimensionsOf(domainExtrema), 2.0 ] : - [dimensionsOf(domainExtrema), dimensionsOf(rangeExtrema)]; - origin = [originOf(domainExtrema), originOf(rangeExtrema)]; + dimensions = (rangeExtrema[0] === rangeExtrema[1]) ? + [dimensionsOf(domainExtrema), 2.0 ] : + [dimensionsOf(domainExtrema), dimensionsOf(rangeExtrema)]; + origin = [originOf(domainExtrema), originOf(rangeExtrema)]; + } + } + + // Add latest data for this domain object + function addPointFor(domainObject) { + var line = lines[domainObject.getId()]; + if (line) { + line.addPoint( + handle.getDomainValue(domainObject, domain), + handle.getRangeValue(domainObject, range) + ); + } } // Handle new telemetry data @@ -134,6 +148,8 @@ define( if (domainOffset === undefined) { initializeDomainOffset(objects.map(function (obj) { return handle.getDomainValue(obj, domain); + }).filter(function (value) { + return typeof value === 'number'; })); } @@ -141,12 +157,7 @@ define( prepareLines(objects); // Add new data - objects.forEach(function (obj, index) { - lines[obj.getId()].addPoint( - handle.getDomainValue(obj, domain), - handle.getRangeValue(obj, range) - ); - }); + objects.forEach(addPointFor); // Finally, update extrema updateExtrema(); @@ -154,7 +165,7 @@ define( // Add historical data for this domain object function setHistorical(domainObject, series) { - var count = series.getPointCount(), + var count = series ? series.getPointCount() : 0, line; // Nothing to do if it's an empty series @@ -163,7 +174,7 @@ define( } // Initialize domain offset if necessary - if (domainOffset === undefined && series) { + if (domainOffset === undefined) { initializeDomainOffset([ series.getDomainValue(0, domain), series.getDomainValue(count - 1, domain) diff --git a/platform/telemetry/bundle.json b/platform/telemetry/bundle.json index f1500a67bf..a93c61bb02 100644 --- a/platform/telemetry/bundle.json +++ b/platform/telemetry/bundle.json @@ -43,6 +43,11 @@ "key": "telemetrySubscriber", "implementation": "TelemetrySubscriber.js", "depends": [ "$q", "$timeout" ] + }, + { + "key": "telemetryHandler", + "implementation": "TelemetryHandler.js", + "depends": [ "$q", "telemetrySubscriber" ] } ], "licenses": [ From 62958280b7748263e3a9d7725ce0eb62df92edeb Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 17 Apr 2015 15:05:56 -0700 Subject: [PATCH 14/22] [Plot] Avoid clearing domain offset Avoid clearing domain offset unnecessarily by fixing variable reference; WTD-806. --- platform/features/plot/src/elements/PlotUpdater.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/features/plot/src/elements/PlotUpdater.js b/platform/features/plot/src/elements/PlotUpdater.js index 815b91b4e5..d714e8f939 100644 --- a/platform/features/plot/src/elements/PlotUpdater.js +++ b/platform/features/plot/src/elements/PlotUpdater.js @@ -78,7 +78,7 @@ define( } // If there are no more lines, clear the domain offset - if (Object.keys(lines).length < 1) { + if (Object.keys(next).length < 1) { domainOffset = undefined; } From 955c4209f0345748657cf87bda142229de2af472 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 17 Apr 2015 15:11:22 -0700 Subject: [PATCH 15/22] [Plot] Add placeholder specs Add placeholder specs for new scripts introduced to support merging real-time and historical telemetry, WTD-806. --- platform/features/plot/test/elements/PlotLineSpec.js | 12 ++++++++++++ .../plot/test/elements/PlotSeriesWindowSpec.js | 12 ++++++++++++ platform/features/plot/test/suite.json | 2 ++ platform/telemetry/test/TelemetryDelegatorSpec.js | 12 ++++++++++++ platform/telemetry/test/TelemetryHandleSpec.js | 12 ++++++++++++ platform/telemetry/test/TelemetryHandlerSpec.js | 12 ++++++++++++ platform/telemetry/test/suite.json | 3 +++ 7 files changed, 65 insertions(+) create mode 100644 platform/features/plot/test/elements/PlotLineSpec.js create mode 100644 platform/features/plot/test/elements/PlotSeriesWindowSpec.js create mode 100644 platform/telemetry/test/TelemetryDelegatorSpec.js create mode 100644 platform/telemetry/test/TelemetryHandleSpec.js create mode 100644 platform/telemetry/test/TelemetryHandlerSpec.js diff --git a/platform/features/plot/test/elements/PlotLineSpec.js b/platform/features/plot/test/elements/PlotLineSpec.js new file mode 100644 index 0000000000..d2cf845bc2 --- /dev/null +++ b/platform/features/plot/test/elements/PlotLineSpec.js @@ -0,0 +1,12 @@ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +define( + ["../../src/elements/PlotLine"], + function (PlotLine) { + "use strict"; + + describe("A plot line", function () { + + }); + } +); \ No newline at end of file diff --git a/platform/features/plot/test/elements/PlotSeriesWindowSpec.js b/platform/features/plot/test/elements/PlotSeriesWindowSpec.js new file mode 100644 index 0000000000..448d031e5c --- /dev/null +++ b/platform/features/plot/test/elements/PlotSeriesWindowSpec.js @@ -0,0 +1,12 @@ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +define( + ["../../src/elements/PlotSeriesWindow"], + function (PlotSeriesWindow) { + "use strict"; + + describe("A plot's window on a telemetry series", function () { + + }); + } +); \ No newline at end of file diff --git a/platform/features/plot/test/suite.json b/platform/features/plot/test/suite.json index 7d099f14da..e3fa3fb796 100644 --- a/platform/features/plot/test/suite.json +++ b/platform/features/plot/test/suite.json @@ -6,12 +6,14 @@ "SubPlot", "SubPlotFactory", "elements/PlotAxis", + "elements/PlotLine", "elements/PlotLineBuffer", "elements/PlotPalette", "elements/PlotPanZoomStack", "elements/PlotPanZoomStackGroup", "elements/PlotPosition", "elements/PlotPreparer", + "elements/PlotSeriesWindow", "elements/PlotTickGenerator", "elements/PlotUpdater", "modes/PlotModeOptions", diff --git a/platform/telemetry/test/TelemetryDelegatorSpec.js b/platform/telemetry/test/TelemetryDelegatorSpec.js new file mode 100644 index 0000000000..9ff80d3c34 --- /dev/null +++ b/platform/telemetry/test/TelemetryDelegatorSpec.js @@ -0,0 +1,12 @@ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +define( + ["../src/TelemetryDelegator"], + function (TelemetryDelegator) { + "use strict"; + + describe("The telemetry delegator", function () { + + }); + } +); diff --git a/platform/telemetry/test/TelemetryHandleSpec.js b/platform/telemetry/test/TelemetryHandleSpec.js new file mode 100644 index 0000000000..b8fc361814 --- /dev/null +++ b/platform/telemetry/test/TelemetryHandleSpec.js @@ -0,0 +1,12 @@ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +define( + ["../src/TelemetryHandle"], + function (TelemetryHandle) { + "use strict"; + + describe("A telemetry handle", function () { + + }); + } +); diff --git a/platform/telemetry/test/TelemetryHandlerSpec.js b/platform/telemetry/test/TelemetryHandlerSpec.js new file mode 100644 index 0000000000..b488f156b8 --- /dev/null +++ b/platform/telemetry/test/TelemetryHandlerSpec.js @@ -0,0 +1,12 @@ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +define( + ["../src/TelemetryHandler"], + function (TelemetryHandler) { + "use strict"; + + describe("The telemetry handler", function () { + + }); + } +); diff --git a/platform/telemetry/test/suite.json b/platform/telemetry/test/suite.json index 0ed5887ac5..8881466572 100644 --- a/platform/telemetry/test/suite.json +++ b/platform/telemetry/test/suite.json @@ -2,7 +2,10 @@ "TelemetryAggregator", "TelemetryCapability", "TelemetryController", + "TelemetryDelegator", "TelemetryFormatter", + "TelemetryHandle", + "TelemetryHandler", "TelemetryQueue", "TelemetrySubscriber", "TelemetrySubscription", From 74b9d68dc8f417f84bc81ae5f90ea32cc5fc4de0 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 17 Apr 2015 16:42:15 -0700 Subject: [PATCH 16/22] [Plot] Update failing specs Update failing specs after changes for WTD-806. --- .../features/plot/src/elements/PlotUpdater.js | 2 +- .../features/plot/test/PlotControllerSpec.js | 41 +++++------ .../plot/test/elements/PlotUpdaterSpec.js | 72 ++----------------- .../plot/test/modes/PlotOverlayModeSpec.js | 22 ++++-- .../plot/test/modes/PlotStackModeSpec.js | 17 +++-- 5 files changed, 55 insertions(+), 99 deletions(-) diff --git a/platform/features/plot/src/elements/PlotUpdater.js b/platform/features/plot/src/elements/PlotUpdater.js index d714e8f939..80074870cc 100644 --- a/platform/features/plot/src/elements/PlotUpdater.js +++ b/platform/features/plot/src/elements/PlotUpdater.js @@ -202,7 +202,7 @@ define( // Initially prepare state for these objects. // Note that this may be an empty array at this time, // so we also need to check during update cycles. - prepareLines(handle.getTelemetryObjects()); + update(); return { /** diff --git a/platform/features/plot/test/PlotControllerSpec.js b/platform/features/plot/test/PlotControllerSpec.js index 865ad2c335..301873ae16 100644 --- a/platform/features/plot/test/PlotControllerSpec.js +++ b/platform/features/plot/test/PlotControllerSpec.js @@ -11,8 +11,8 @@ define( describe("The plot controller", function () { var mockScope, mockFormatter, - mockSubscriber, - mockSubscription, + mockHandler, + mockHandle, mockDomainObject, controller; @@ -30,28 +30,29 @@ define( "domainObject", [ "getId", "getModel", "getCapability" ] ); - mockSubscriber = jasmine.createSpyObj( + mockHandler = jasmine.createSpyObj( "telemetrySubscriber", - ["subscribe"] + ["handle"] ); - mockSubscription = jasmine.createSpyObj( + mockHandle = jasmine.createSpyObj( "subscription", [ "unsubscribe", "getTelemetryObjects", "getMetadata", "getDomainValue", - "getRangeValue" + "getRangeValue", + "request" ] ); - mockSubscriber.subscribe.andReturn(mockSubscription); - mockSubscription.getTelemetryObjects.andReturn([mockDomainObject]); - mockSubscription.getMetadata.andReturn([{}]); - mockSubscription.getDomainValue.andReturn(123); - mockSubscription.getRangeValue.andReturn(42); + mockHandler.handle.andReturn(mockHandle); + mockHandle.getTelemetryObjects.andReturn([mockDomainObject]); + mockHandle.getMetadata.andReturn([{}]); + mockHandle.getDomainValue.andReturn(123); + mockHandle.getRangeValue.andReturn(42); - controller = new PlotController(mockScope, mockFormatter, mockSubscriber); + controller = new PlotController(mockScope, mockFormatter, mockHandler); }); it("provides plot colors", function () { @@ -71,7 +72,7 @@ define( // Make an object available mockScope.$watch.mostRecentCall.args[1](mockDomainObject); // Should have subscribed - expect(mockSubscriber.subscribe).toHaveBeenCalledWith( + expect(mockHandler.handle).toHaveBeenCalledWith( mockDomainObject, jasmine.any(Function), true // Lossless @@ -92,7 +93,7 @@ define( expect(controller.getSubPlots().length > 0).toBeTruthy(); // Broadcast data - mockSubscriber.subscribe.mostRecentCall.args[1](); + mockHandler.handle.mostRecentCall.args[1](); controller.getSubPlots().forEach(function (subplot) { expect(subplot.getDrawingObject().lines) @@ -104,17 +105,17 @@ define( // Make an object available mockScope.$watch.mostRecentCall.args[1](mockDomainObject); // Verify precondition - shouldn't unsubscribe yet - expect(mockSubscription.unsubscribe).not.toHaveBeenCalled(); + expect(mockHandle.unsubscribe).not.toHaveBeenCalled(); // Remove the domain object mockScope.$watch.mostRecentCall.args[1](undefined); // Should have unsubscribed - expect(mockSubscription.unsubscribe).toHaveBeenCalled(); + expect(mockHandle.unsubscribe).toHaveBeenCalled(); }); it("changes modes depending on number of objects", function () { // Act like one object is available - mockSubscription.getTelemetryObjects.andReturn([ + mockHandle.getTelemetryObjects.andReturn([ mockDomainObject ]); @@ -124,7 +125,7 @@ define( expect(controller.getModeOptions().length).toEqual(1); // Act like one object is available - mockSubscription.getTelemetryObjects.andReturn([ + mockHandle.getTelemetryObjects.andReturn([ mockDomainObject, mockDomainObject, mockDomainObject @@ -180,11 +181,11 @@ define( // Make sure $destroy is what's listened for expect(mockScope.$on.mostRecentCall.args[0]).toEqual('$destroy'); // Also verify precondition - expect(mockSubscription.unsubscribe).not.toHaveBeenCalled(); + expect(mockHandle.unsubscribe).not.toHaveBeenCalled(); // Destroy the scope mockScope.$on.mostRecentCall.args[1](); // Should have unsubscribed - expect(mockSubscription.unsubscribe).toHaveBeenCalled(); + expect(mockHandle.unsubscribe).toHaveBeenCalled(); }); }); } diff --git a/platform/features/plot/test/elements/PlotUpdaterSpec.js b/platform/features/plot/test/elements/PlotUpdaterSpec.js index 41be699578..250182922a 100644 --- a/platform/features/plot/test/elements/PlotUpdaterSpec.js +++ b/platform/features/plot/test/elements/PlotUpdaterSpec.js @@ -55,57 +55,14 @@ define( }); it("provides one buffer per telemetry object", function () { - expect(updater.getBuffers().length).toEqual(3); + expect(updater.getLineBuffers().length).toEqual(3); }); it("changes buffer count if telemetry object counts change", function () { mockSubscription.getTelemetryObjects .andReturn([makeMockDomainObject('a')]); updater.update(); - expect(updater.getBuffers().length).toEqual(1); - }); - - it("maintains a buffer of received telemetry", function () { - // Count should be large enough to trigger a buffer resize - var count = 750, - i; - - // Increment values exposed by subscription - function increment() { - Object.keys(testDomainValues).forEach(function (k) { - testDomainValues[k] += 1; - testRangeValues[k] += 1; - }); - } - - // Simulate a lot of telemetry updates - for (i = 0; i < count; i += 1) { - updater.update(); - expect(updater.getLength(0)).toEqual(i + 1); - expect(updater.getLength(1)).toEqual(i + 1); - expect(updater.getLength(2)).toEqual(i + 1); - increment(); - } - - // Domain offset should be lowest domain value - expect(updater.getDomainOffset()).toEqual(3); - - // Test against initial values, offset by count, - // as was the case during each update - for (i = 0; i < count; i += 1) { - expect(updater.getBuffers()[0][i * 2]) - .toEqual(3 + i - 3); - expect(updater.getBuffers()[0][i * 2 + 1]) - .toEqual(123 + i); - expect(updater.getBuffers()[1][i * 2]) - .toEqual(7 + i - 3); - expect(updater.getBuffers()[1][i * 2 + 1]) - .toEqual(456 + i); - expect(updater.getBuffers()[2][i * 2]) - .toEqual(13 + i - 3); - expect(updater.getBuffers()[2][i * 2 + 1]) - .toEqual(789 + i); - } + expect(updater.getLineBuffers().length).toEqual(1); }); it("can handle delayed telemetry object availability", function () { @@ -124,7 +81,7 @@ define( ); // Should have 0 buffers for 0 objects - expect(updater.getBuffers().length).toEqual(0); + expect(updater.getLineBuffers().length).toEqual(0); // Restore the three objects the test subscription would // normally have. @@ -132,30 +89,9 @@ define( updater.update(); // Should have 3 buffers for 3 objects - expect(updater.getBuffers().length).toEqual(3); + expect(updater.getLineBuffers().length).toEqual(3); }); - - it("shifts buffer upon expansion", function () { - // Count should be large enough to hit buffer's max size - var count = 1400, - i; - - // Initial update; should have 3 in first position - // (a's initial domain value) - updater.update(); - expect(updater.getBuffers()[0][1]).toEqual(123); - - // Simulate a lot of telemetry updates - for (i = 0; i < count; i += 1) { - testDomainValues.a += 1; - testRangeValues.a += 1; - updater.update(); - } - - // Value at front of the buffer should have been pushed out - expect(updater.getBuffers()[0][1]).not.toEqual(123); - }); }); } ); \ No newline at end of file diff --git a/platform/features/plot/test/modes/PlotOverlayModeSpec.js b/platform/features/plot/test/modes/PlotOverlayModeSpec.js index 8c2922cf22..ee1be40bfc 100644 --- a/platform/features/plot/test/modes/PlotOverlayModeSpec.js +++ b/platform/features/plot/test/modes/PlotOverlayModeSpec.js @@ -57,18 +57,30 @@ define( // Prepared telemetry data mockPrepared = jasmine.createSpyObj( "prepared", - [ "getDomainOffset", "getOrigin", "getDimensions", "getBuffers", "getLength" ] + [ + "getDomainOffset", + "getOrigin", + "getDimensions", + "getLineBuffers" + ] ); mockSubPlotFactory.createSubPlot.andCallFake(createMockSubPlot); // Act as if we have three buffers full of data - testBuffers = [["a"], ["b"], ["c"]]; - mockPrepared.getBuffers.andReturn(testBuffers); + testBuffers = ['a', 'b', 'c'].map(function (id) { + var mockBuffer = jasmine.createSpyObj( + 'buffer-' + id, + ['getBuffer', 'getLength'] + ); + mockBuffer.getBuffer.andReturn([id]); + mockBuffer.getLength.andReturn(3); + return mockBuffer; + }); + mockPrepared.getLineBuffers.andReturn(testBuffers); mockPrepared.getDomainOffset.andReturn(1234); mockPrepared.getOrigin.andReturn([10, 10]); mockPrepared.getDimensions.andReturn([500, 500]); - mockPrepared.getLength.andReturn(3); // Clear out drawing objects testDrawingObjects = []; @@ -104,7 +116,7 @@ define( // Make sure the right buffer was drawn to the // right subplot. testDrawingObject.lines.forEach(function (line, j) { - expect(line.buffer).toEqual(testBuffers[j]); + expect(line.buffer).toEqual(testBuffers[j].getBuffer()); }); }); }); diff --git a/platform/features/plot/test/modes/PlotStackModeSpec.js b/platform/features/plot/test/modes/PlotStackModeSpec.js index 4c2c11a308..6101d2ccc8 100644 --- a/platform/features/plot/test/modes/PlotStackModeSpec.js +++ b/platform/features/plot/test/modes/PlotStackModeSpec.js @@ -57,18 +57,25 @@ define( // Prepared telemetry data mockPrepared = jasmine.createSpyObj( "prepared", - [ "getDomainOffset", "getOrigin", "getDimensions", "getBuffers", "getLength" ] + [ "getDomainOffset", "getOrigin", "getDimensions", "getLineBuffers" ] ); mockSubPlotFactory.createSubPlot.andCallFake(createMockSubPlot); // Act as if we have three buffers full of data - testBuffers = [["a"], ["b"], ["c"]]; - mockPrepared.getBuffers.andReturn(testBuffers); + testBuffers = ['a', 'b', 'c'].map(function (id) { + var mockBuffer = jasmine.createSpyObj( + 'buffer-' + id, + ['getBuffer', 'getLength'] + ); + mockBuffer.getBuffer.andReturn([id]); + mockBuffer.getLength.andReturn(3); + return mockBuffer; + }); + mockPrepared.getLineBuffers.andReturn(testBuffers); mockPrepared.getDomainOffset.andReturn(1234); mockPrepared.getOrigin.andReturn([10, 10]); mockPrepared.getDimensions.andReturn([500, 500]); - mockPrepared.getLength.andReturn(3); // Objects that will be drawn to in sub-plots testDrawingObjects = []; @@ -104,7 +111,7 @@ define( // Make sure the right buffer was drawn to the // right subplot. expect(testDrawingObject.lines[0].buffer) - .toEqual(testBuffers[i]); + .toEqual(testBuffers[i].getBuffer()); }); }); From ffcd91f88d2db82544b2d246d56c0cd5c2764423 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 17 Apr 2015 16:55:49 -0700 Subject: [PATCH 17/22] [Plot] Add test cases Add test cases to spec for PlotUpdater to reflect changes for merging streaming and historical data. WTD-806. --- .../plot/test/elements/PlotUpdaterSpec.js | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/platform/features/plot/test/elements/PlotUpdaterSpec.js b/platform/features/plot/test/elements/PlotUpdaterSpec.js index 250182922a..9b33f769c9 100644 --- a/platform/features/plot/test/elements/PlotUpdaterSpec.js +++ b/platform/features/plot/test/elements/PlotUpdaterSpec.js @@ -14,6 +14,7 @@ define( testRange, testDomainValues, testRangeValues, + mockSeries, updater; function makeMockDomainObject(id) { @@ -33,6 +34,10 @@ define( "subscription", [ "getDomainValue", "getRangeValue", "getTelemetryObjects" ] ); + mockSeries = jasmine.createSpyObj( + 'series', + ['getPointCount', 'getDomainValue', 'getRangeValue'] + ); testDomain = "testDomain"; testRange = "testRange"; testDomainValues = { a: 3, b: 7, c: 13 }; @@ -92,6 +97,76 @@ define( expect(updater.getLineBuffers().length).toEqual(3); }); + it("accepts historical telemetry updates", function () { + var mockObject = mockSubscription.getTelemetryObjects()[0]; + + mockSeries.getPointCount.andReturn(3); + mockSeries.getDomainValue.andCallFake(function (i) { + return 1000 + i * 1000; + }); + mockSeries.getRangeValue.andReturn(10); + + // PlotLine & PlotLineBuffer are tested for most of the + // details here, so just check for some expected side + // effect; in this case, should see more points in the buffer + expect(updater.getLineBuffers()[0].getLength()).toEqual(1); + updater.addHistorical(mockObject, mockSeries); + expect(updater.getLineBuffers()[0].getLength()).toEqual(4); + }); + + it("clears the domain offset if no objects are present", function () { + mockSubscription.getTelemetryObjects.andReturn([]); + updater.update(); + expect(updater.getDomainOffset()).toBeUndefined(); + }); + + it("handles empty historical telemetry updates", function () { + // General robustness check for when a series is empty + var mockObject = mockSubscription.getTelemetryObjects()[0]; + + mockSeries.getPointCount.andReturn(0); + mockSeries.getDomainValue.andCallFake(function (i) { + return 1000 + i * 1000; + }); + mockSeries.getRangeValue.andReturn(10); + + // PlotLine & PlotLineBuffer are tested for most of the + // details here, so just check for some expected side + // effect; in this case, should see more points in the buffer + expect(updater.getLineBuffers()[0].getLength()).toEqual(1); + updater.addHistorical(mockObject, mockSeries); + expect(updater.getLineBuffers()[0].getLength()).toEqual(1); + }); + + it("can initialize domain offset from historical telemetry", function () { + var tmp = mockSubscription.getTelemetryObjects(); + + mockSubscription.getTelemetryObjects.andReturn([]); + + // Reinstantiate with the empty subscription + updater = new PlotUpdater( + mockSubscription, + testDomain, + testRange + ); + + // Restore subscription, provide some historical data + mockSubscription.getTelemetryObjects.andReturn(tmp); + mockSeries.getPointCount.andReturn(3); + mockSeries.getDomainValue.andCallFake(function (i) { + return 1000 + i * 1000; + }); + mockSeries.getRangeValue.andReturn(10); + + // PlotLine & PlotLineBuffer are tested for most of the + // details here, so just check for some expected side + // effect; in this case, should see more points in the buffer + expect(updater.getDomainOffset()).toBeUndefined(); + updater.addHistorical(tmp[0], mockSeries); + expect(updater.getDomainOffset()).toBeDefined(); + }); + + }); } ); \ No newline at end of file From e21cbbe2c6ecbf786ed4aceffe3c056964e4adec Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 20 Apr 2015 11:00:54 -0700 Subject: [PATCH 18/22] [Plot] Add test cases WTD-806 --- .../plot/test/elements/PlotLineSpec.js | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/platform/features/plot/test/elements/PlotLineSpec.js b/platform/features/plot/test/elements/PlotLineSpec.js index d2cf845bc2..41ce6e2dfc 100644 --- a/platform/features/plot/test/elements/PlotLineSpec.js +++ b/platform/features/plot/test/elements/PlotLineSpec.js @@ -6,6 +6,97 @@ define( "use strict"; describe("A plot line", function () { + var mockBuffer, + mockSeries, + testDomainBuffer, + testRangeBuffer, + testSeries, + line; + + beforeEach(function () { + testDomainBuffer = []; + testRangeBuffer = []; + testSeries = []; + + mockBuffer = jasmine.createSpyObj( + 'buffer', + ['findInsertionIndex', 'insert', 'insertPoint'] + ); + mockSeries = jasmine.createSpyObj( + 'series', + ['getPointCount', 'getDomainValue', 'getRangeValue'] + ); + + mockSeries.getPointCount.andCallFake(function () { + return testSeries.length; + }); + mockSeries.getDomainValue.andCallFake(function (i) { + return testSeries[i][0]; + }); + mockSeries.getRangeValue.andCallFake(function (i) { + return testSeries[i][1]; + }); + + // Function like PlotLineBuffer, to aid in testability + mockBuffer.findInsertionIndex.andCallFake(function (v) { + var index = 0; + if (testDomainBuffer.indexOf(v) !== -1) { + return -1; + } + while ((index < testDomainBuffer.length) && + (testDomainBuffer[index] < v)) { + index += 1; + } + return index; + }); + mockBuffer.insert.andCallFake(function (series, index) { + var domains = [], ranges = [], i; + for (i = 0; i < series.getPointCount(); i += 1) { + domains.push(series.getDomainValue(i)); + ranges.push(series.getRangeValue(i)); + } + testDomainBuffer = testDomainBuffer.slice(0, index) + .concat(domains) + .concat(testDomainBuffer.slice(index)); + testRangeBuffer = testRangeBuffer.slice(0, index) + .concat(ranges) + .concat(testRangeBuffer.slice(index)); + return true; + }); + mockBuffer.insertPoint.andCallFake(function (dv, rv, index) { + testDomainBuffer.splice(index, 0, dv); + testRangeBuffer.splice(index, 0, rv); + return true; + }); + + line = new PlotLine(mockBuffer); + }); + + it("allows single point insertion", function () { + line.addPoint(100, 200); + line.addPoint(50, 42); + line.addPoint(150, 12321); + // Should have managed insertion index choices to get to... + expect(testDomainBuffer).toEqual([50, 100, 150]); + expect(testRangeBuffer).toEqual([42, 200, 12321]); + }); + + it("allows series insertion", function () { + testSeries = [ [ 50, 42 ], [ 100, 200 ], [ 150, 12321 ] ]; + line.addSeries(mockSeries); + // Should have managed insertion index choices to get to... + expect(testDomainBuffer).toEqual([50, 100, 150]); + expect(testRangeBuffer).toEqual([42, 200, 12321]); + }); + + it("splits series insertion when necessary", function () { + testSeries = [ [ 50, 42 ], [ 100, 200 ], [ 150, 12321 ] ]; + line.addPoint(75, 1); + line.addSeries(mockSeries); + // Should have managed insertion index choices to get to... + expect(testDomainBuffer).toEqual([50, 75, 100, 150]); + expect(testRangeBuffer).toEqual([42, 1, 200, 12321]); + }); }); } From 4d288950fd0b671c6ddb354f64563d877ef11d0b Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 20 Apr 2015 14:31:29 -0700 Subject: [PATCH 19/22] [Plot] Add test cases Add test cases for plot line management, WTD-806. --- .../plot/src/elements/PlotSeriesWindow.js | 8 +-- .../plot/test/elements/PlotLineBufferSpec.js | 25 ++++++++ .../plot/test/elements/PlotLineSpec.js | 17 ++++- .../test/elements/PlotSeriesWindowSpec.js | 62 +++++++++++++++++++ 4 files changed, 105 insertions(+), 7 deletions(-) diff --git a/platform/features/plot/src/elements/PlotSeriesWindow.js b/platform/features/plot/src/elements/PlotSeriesWindow.js index d8308eb3eb..572c2c419c 100644 --- a/platform/features/plot/src/elements/PlotSeriesWindow.js +++ b/platform/features/plot/src/elements/PlotSeriesWindow.js @@ -14,14 +14,14 @@ define( return end - start; }, getDomainValue: function (index) { - return series.getDomainValue(index - start, domain); + return series.getDomainValue(index + start, domain); }, getRangeValue: function (index) { - return series.getRangeValue(index - start, range); + return series.getRangeValue(index + start, range); }, split: function () { var mid = Math.floor((end + start) / 2); - return end > start ? + return ((end - start) > 1) ? [ new PlotSeriesWindow( series, @@ -34,7 +34,7 @@ define( series, domain, range, - mid + 1, + mid, end ) ] : []; diff --git a/platform/features/plot/test/elements/PlotLineBufferSpec.js b/platform/features/plot/test/elements/PlotLineBufferSpec.js index 3fc7b06c6e..a152ba5a4c 100644 --- a/platform/features/plot/test/elements/PlotLineBufferSpec.js +++ b/platform/features/plot/test/elements/PlotLineBufferSpec.js @@ -85,6 +85,31 @@ define( ).toEqual([ -35, 3, -33, 9, -28, 8, -27, 11]); }); + it("expands buffer when needed to accommodate more data", function () { + var i; + + // Initial underlying buffer should be twice initial size... + // (Since each pair will take up two elements) + expect(buffer.getBuffer().length).toEqual(20); + + // Should be able to insert 6 series of 6 points each + // (After that, we'll hit the test max of 40) + for (i = 1; i < 15; i += 1) { + expect(buffer.insertPoint(i * 10, Math.sin(i), i)) + .toBeTruthy(); + } + + // Buffer should have expanded in the process + expect(buffer.getBuffer().length).toEqual(40); + + // Push to maximum size just to make sure... + for (i = 1; i < 150; i += 1) { + buffer.insertPoint(i * 10, Math.sin(i), i); + } + + expect(buffer.getBuffer().length).toEqual(80); + }); + it("ensures a maximum size", function () { var i; diff --git a/platform/features/plot/test/elements/PlotLineSpec.js b/platform/features/plot/test/elements/PlotLineSpec.js index 41ce6e2dfc..027e628bae 100644 --- a/platform/features/plot/test/elements/PlotLineSpec.js +++ b/platform/features/plot/test/elements/PlotLineSpec.js @@ -20,7 +20,7 @@ define( mockBuffer = jasmine.createSpyObj( 'buffer', - ['findInsertionIndex', 'insert', 'insertPoint'] + ['findInsertionIndex', 'insert', 'insertPoint', 'trim'] ); mockSeries = jasmine.createSpyObj( 'series', @@ -31,10 +31,10 @@ define( return testSeries.length; }); mockSeries.getDomainValue.andCallFake(function (i) { - return testSeries[i][0]; + return (testSeries[i] || [])[0]; }); mockSeries.getRangeValue.andCallFake(function (i) { - return testSeries[i][1]; + return (testSeries[i] || [])[1]; }); // Function like PlotLineBuffer, to aid in testability @@ -98,6 +98,17 @@ define( expect(testRangeBuffer).toEqual([42, 1, 200, 12321]); }); + it("attempts to remove points when insertion fails", function () { + // Verify precondition - normally doesn't try to trim + line.addPoint(1, 2); + expect(mockBuffer.trim).not.toHaveBeenCalled(); + + // But if insertPoint fails, it should trim + mockBuffer.insertPoint.andReturn(false); + line.addPoint(2, 3); + expect(mockBuffer.trim).toHaveBeenCalled(); + }); + }); } ); \ No newline at end of file diff --git a/platform/features/plot/test/elements/PlotSeriesWindowSpec.js b/platform/features/plot/test/elements/PlotSeriesWindowSpec.js index 448d031e5c..ad88874e9a 100644 --- a/platform/features/plot/test/elements/PlotSeriesWindowSpec.js +++ b/platform/features/plot/test/elements/PlotSeriesWindowSpec.js @@ -6,6 +6,68 @@ define( "use strict"; describe("A plot's window on a telemetry series", function () { + var mockSeries, + testSeries, + window; + + beforeEach(function () { + testSeries = [ + [ 0, 42 ], + [ 10, 1 ], + [ 20, 4 ], + [ 30, 9 ], + [ 40, 3 ] + ]; + + mockSeries = jasmine.createSpyObj( + 'series', + ['getPointCount', 'getDomainValue', 'getRangeValue'] + ); + + mockSeries.getPointCount.andCallFake(function () { + return testSeries.length; + }); + mockSeries.getDomainValue.andCallFake(function (i) { + return testSeries[i][0]; + }); + mockSeries.getRangeValue.andCallFake(function (i) { + return testSeries[i][1]; + }); + + window = new PlotSeriesWindow( + mockSeries, + "testDomain", + "testRange", + 1, + testSeries.length + ); + }); + + it("provides a window upon a data series", function () { + expect(window.getPointCount()).toEqual(4); + expect(window.getDomainValue(0)).toEqual(10); + expect(window.getRangeValue(0)).toEqual(1); + }); + + it("looks up using specific domain/range keys", function () { + window.getDomainValue(0); + window.getRangeValue(0); + expect(mockSeries.getDomainValue) + .toHaveBeenCalledWith(1, 'testDomain'); + expect(mockSeries.getRangeValue) + .toHaveBeenCalledWith(1, 'testRange'); + }); + + it("can be split into smaller windows", function () { + var windows = window.split(); + expect(windows.length).toEqual(2); + expect(windows[0].getPointCount()).toEqual(2); + expect(windows[1].getPointCount()).toEqual(2); + expect(windows[0].getDomainValue(0)).toEqual(10); + expect(windows[1].getDomainValue(0)).toEqual(30); + expect(windows[0].getRangeValue(0)).toEqual(1); + expect(windows[1].getRangeValue(0)).toEqual(9); + }); }); } From ffc122fb5c540242d683dc4a91e34723afd3be86 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 20 Apr 2015 16:51:08 -0700 Subject: [PATCH 20/22] [Telemetry] Add test casese Add test cases for general support for handling real-time telemetry, WTD-806. --- platform/telemetry/src/TelemetryHandle.js | 10 +++ .../telemetry/test/TelemetryHandleSpec.js | 76 +++++++++++++++++++ .../telemetry/test/TelemetryHandlerSpec.js | 52 +++++++++++++ .../test/TelemetrySubscriptionSpec.js | 8 ++ 4 files changed, 146 insertions(+) diff --git a/platform/telemetry/src/TelemetryHandle.js b/platform/telemetry/src/TelemetryHandle.js index eef57caba6..fd5d7582f9 100644 --- a/platform/telemetry/src/TelemetryHandle.js +++ b/platform/telemetry/src/TelemetryHandle.js @@ -5,10 +5,20 @@ define( function () { "use strict"; + /** + * A telemetry handle acts as a helper in issuing requests for + * new telemetry as well as subscribing to real-time updates + * for the same telemetry series. This is exposed through the + * `telemetryHandler` service. + * @param $q Angular's $q, for promises + * @param {TelemetrySubscription} subscription a subscription + * to supplied telemetry + */ function TelemetryHandle($q, subscription) { var seriesMap = {}, self = Object.create(subscription); + // Request a telemetry series for this specific object function requestSeries(telemetryObject, request, callback) { var id = telemetryObject.getId(), telemetry = telemetryObject.getCapability('telemetry'); diff --git a/platform/telemetry/test/TelemetryHandleSpec.js b/platform/telemetry/test/TelemetryHandleSpec.js index b8fc361814..c7db9ac835 100644 --- a/platform/telemetry/test/TelemetryHandleSpec.js +++ b/platform/telemetry/test/TelemetryHandleSpec.js @@ -6,7 +6,83 @@ define( "use strict"; describe("A telemetry handle", function () { + var mockQ, + mockSubscription, + mockDomainObject, + mockTelemetry, + mockSeries, + mockCallback, + handle; + function asPromise(v) { + return (v || {}).then ? v : { + then: function (callback) { + return asPromise(callback(v)); + } + }; + } + + beforeEach(function () { + mockQ = jasmine.createSpyObj('$q', ['when', 'all']); + mockSubscription = jasmine.createSpyObj( + 'subscription', + ['unsubscribe', 'getTelemetryObjects', 'promiseTelemetryObjects'] + ); + mockDomainObject = jasmine.createSpyObj( + 'domainObject', + ['getId', 'getCapability'] + ); + mockTelemetry = jasmine.createSpyObj( + 'telemetry', + ['requestData'] + ); + mockSeries = jasmine.createSpyObj( + 'series', + ['getPointCount', 'getDomainValue', 'getRangeValue'] + ); + mockCallback = jasmine.createSpy('callback'); + + // Simulate $q.all, at least for asPromise-provided promises + mockQ.all.andCallFake(function (values) { + return values.map(function (v) { + var r; + asPromise(v).then(function (value) { r = value; }); + return r; + }); + }); + mockQ.when.andCallFake(asPromise); + mockSubscription.getTelemetryObjects + .andReturn([mockDomainObject]); + mockSubscription.promiseTelemetryObjects + .andReturn(asPromise([mockDomainObject])); + mockDomainObject.getId.andReturn('testId'); + mockDomainObject.getCapability.andReturn(mockTelemetry); + mockTelemetry.requestData.andReturn(asPromise(mockSeries)); + + handle = new TelemetryHandle(mockQ, mockSubscription); + }); + + it("exposes subscription API", function () { + // Should still expose methods from the provided subscription + expect(handle.unsubscribe) + .toBe(mockSubscription.unsubscribe); + expect(handle.getTelemetryObjects) + .toBe(mockSubscription.getTelemetryObjects); + }); + + it("provides an interface for historical requests", function () { + handle.request({}, mockCallback); + expect(mockCallback).toHaveBeenCalledWith( + mockDomainObject, + mockSeries + ); + }); + + it("provides the latest series for domain objects", function () { + handle.request({}); + expect(handle.getSeries(mockDomainObject)) + .toEqual(mockSeries); + }); }); } ); diff --git a/platform/telemetry/test/TelemetryHandlerSpec.js b/platform/telemetry/test/TelemetryHandlerSpec.js index b488f156b8..be0588a0e9 100644 --- a/platform/telemetry/test/TelemetryHandlerSpec.js +++ b/platform/telemetry/test/TelemetryHandlerSpec.js @@ -6,6 +6,58 @@ define( "use strict"; describe("The telemetry handler", function () { + // TelemetryHandler just provides a factory + // for TelemetryHandle, so most real testing + // should happen there. + var mockQ, + mockSubscriber, + mockDomainObject, + mockCallback, + mockSubscription, + handler; + + beforeEach(function () { + mockQ = jasmine.createSpyObj("$q", ["when"]); + mockSubscriber = jasmine.createSpyObj( + 'telemetrySubscriber', + ['subscribe'] + ); + mockDomainObject = jasmine.createSpyObj( + 'domainObject', + ['getId', 'getCapability'] + ); + mockCallback = jasmine.createSpy('callback'); + mockSubscription = jasmine.createSpyObj( + 'subscription', + [ + 'unsubscribe', + 'getTelemetryObjects', + 'getRangeValue', + 'getDomainValue' + ] + ); + + mockSubscriber.subscribe.andReturn(mockSubscription); + + handler = new TelemetryHandler(mockQ, mockSubscriber); + }); + + it("acts as a factory for subscription objects", function () { + var handle = handler.handle( + mockDomainObject, + mockCallback + ); + // Just verify that this looks like a TelemetrySubscription + [ + "unsubscribe", + "getTelemetryObjects", + "getRangeValue", + "getDomainValue", + "request" + ].forEach(function (method) { + expect(handle[method]).toEqual(jasmine.any(Function)); + }); + }); }); } diff --git a/platform/telemetry/test/TelemetrySubscriptionSpec.js b/platform/telemetry/test/TelemetrySubscriptionSpec.js index 6dd27320c9..057aa97748 100644 --- a/platform/telemetry/test/TelemetrySubscriptionSpec.js +++ b/platform/telemetry/test/TelemetrySubscriptionSpec.js @@ -184,6 +184,14 @@ define( it("fires callback when telemetry objects are available", function () { expect(mockCallback.calls.length).toEqual(1); }); + + it("exposes a promise for telemetry objects", function () { + var mockCallback2 = jasmine.createSpy('callback'); + subscription.promiseTelemetryObjects().then(mockCallback2); + + expect(mockCallback2) + .toHaveBeenCalledWith([ mockDomainObject ]); + }); }); } ); \ No newline at end of file From 6400a670fab8e7015689276df5123355b482729b Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 20 Apr 2015 17:00:30 -0700 Subject: [PATCH 21/22] [Plot] Update plot with historical data Trigger update of displayed plot data when new historical data becomes available, WTD-806. --- platform/features/plot/src/PlotController.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/platform/features/plot/src/PlotController.js b/platform/features/plot/src/PlotController.js index a2f5695f42..046a9f4fb9 100644 --- a/platform/features/plot/src/PlotController.js +++ b/platform/features/plot/src/PlotController.js @@ -95,10 +95,17 @@ define( update(); } + // Display new historical data as it becomes available + function addHistoricalData(domainObject, series) { + updater.addHistorical(domainObject, series); + modeOptions.getModeHandler().plotTelemetry(updater); + update(); + } + // Issue a new request for historical telemetry function requestTelemetry() { if (handle && updater) { - handle.request({}, updater.addHistorical); + handle.request({}, addHistoricalData); } } From 186ae05686485aaa7f3b40d1059441cc87e687f9 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 20 Apr 2015 17:04:34 -0700 Subject: [PATCH 22/22] [Plot] Add test case Add test case which exercises handling of historical telemetry, WTD-806. --- platform/features/plot/test/PlotControllerSpec.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/platform/features/plot/test/PlotControllerSpec.js b/platform/features/plot/test/PlotControllerSpec.js index 301873ae16..846bac452f 100644 --- a/platform/features/plot/test/PlotControllerSpec.js +++ b/platform/features/plot/test/PlotControllerSpec.js @@ -14,6 +14,7 @@ define( mockHandler, mockHandle, mockDomainObject, + mockSeries, controller; @@ -45,6 +46,10 @@ define( "request" ] ); + mockSeries = jasmine.createSpyObj( + 'series', + ['getPointCount', 'getDomainValue', 'getRangeValue'] + ); mockHandler.handle.andReturn(mockHandle); mockHandle.getTelemetryObjects.andReturn([mockDomainObject]); @@ -175,6 +180,15 @@ define( expect(controller.isRequestPending()).toBeFalsy(); }); + it("requests historical telemetry", function () { + mockScope.$watch.mostRecentCall.args[1](mockDomainObject); + expect(mockHandle.request).toHaveBeenCalled(); + mockHandle.request.mostRecentCall.args[1]( + mockDomainObject, + mockSeries + ); + }); + it("unsubscribes when destroyed", function () { // Make an object available mockScope.$watch.mostRecentCall.args[1](mockDomainObject);