From d4fd6824b608425141d554736046fe3e2149b80d Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 5 Jan 2015 10:12:19 -0800 Subject: [PATCH 01/25] [Telemetry] Expose moment Expose the moment library for time/date operations. For use in parsing WARP server messages (WTD-644.) --- platform/telemetry/bundle.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/platform/telemetry/bundle.json b/platform/telemetry/bundle.json index b99d497fbc..bad85512b0 100644 --- a/platform/telemetry/bundle.json +++ b/platform/telemetry/bundle.json @@ -1,6 +1,16 @@ { "name": "Data bundle", "description": "Interfaces and infrastructure for real-time and historical data.", + "configuration": { + "paths": { + "moment": "moment.min.js" + }, + "shim": { + "moment": { + "exports": "moment" + } + } + }, "extensions": { "components": [ { From 3c37242fda3cc2505e9cc6b70792fd6ac0138a43 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Tue, 6 Jan 2015 08:49:49 -0800 Subject: [PATCH 02/25] [Telemetry] Don't format non-numeric values Don't try to format non-numeric values; rather, just echo them back directly. This is to allow simple printing of strings when received on a telemetry subscription. Performed in the context of WTD-644 (since the WARP Server returns many such strings.) --- platform/telemetry/src/TelemetryFormatter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/telemetry/src/TelemetryFormatter.js b/platform/telemetry/src/TelemetryFormatter.js index 85ec7ff29a..c8754f6626 100644 --- a/platform/telemetry/src/TelemetryFormatter.js +++ b/platform/telemetry/src/TelemetryFormatter.js @@ -22,7 +22,7 @@ define( } function formatRangeValue(v, key) { - return isNaN(v) ? "" : v.toFixed(3); + return isNaN(v) ? v : v.toFixed(3); } return { From 22d2be894248522874b962b0a079bc117f1dec7e Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Tue, 6 Jan 2015 10:01:01 -0800 Subject: [PATCH 03/25] [Views] Allow types to restrict views Allow type definitions to restrict views; this specifically supports the restriction of autoflow tabular views to packets, and the omission of scrolling/plot views as applicable to packets. WTD-644. --- platform/core/src/views/ViewProvider.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/platform/core/src/views/ViewProvider.js b/platform/core/src/views/ViewProvider.js index f6caec903a..d1535328e8 100644 --- a/platform/core/src/views/ViewProvider.js +++ b/platform/core/src/views/ViewProvider.js @@ -81,15 +81,32 @@ define( return capabilities.map(hasCapability).reduce(and, true); } + // Check if a view and domain object type can be paired; + // both can restrict the others they accept. + function viewMatchesType(view, type) { + var views = type && type.getDefinition().views, + matches = true; + + // View is restricted to a certain type + if (view.type) { + matches = matches && type && type.instanceOf(view.type); + } + + // Type wishes to restrict its specific views + if (Array.isArray(views)) { + matches = matches && (views.indexOf(view.key) > -1); + } + + return matches; + } + function getViews(domainObject) { var type = domainObject.useCapability("type"); // First, filter views by type (matched to domain object type.) // Second, filter by matching capabilities. return views.filter(function (view) { - return (!view.type) || type.instanceOf(view.type); - }).filter(function (view) { - return capabilitiesMatch( + return viewMatchesType(view, type) && capabilitiesMatch( domainObject, view.needs || [], view.delegation || false From 75a0cbe43d7ffa2ab1da5687e8926e3a1c18e7d4 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Tue, 6 Jan 2015 12:44:54 -0800 Subject: [PATCH 04/25] [Persistence] Add persistence cache Add a decorator to handle the caching of objects stored to and/or read from persistence (bring over from pre-Angular branch.) Supports the WARP Telemetry Adapter; the absence of such a cache has been observed to result in significant latency in instantiating autoflow tabular views for packets. WTD-644. Also, include an empty test file to ensure that the added decorator is included in code coverage estimation (and to provide a location for tests to be written later.) --- platform/persistence/bundle.json | 6 ++ .../src/CachingPersistenceDecorator.js | 100 ++++++++++++++++++ .../test/CachingPersistenceDecoratorSpec.js | 12 +++ platform/persistence/test/suite.json | 1 + 4 files changed, 119 insertions(+) create mode 100644 platform/persistence/src/CachingPersistenceDecorator.js create mode 100644 platform/persistence/test/CachingPersistenceDecoratorSpec.js diff --git a/platform/persistence/bundle.json b/platform/persistence/bundle.json index 011c46b666..0fb445a2ee 100644 --- a/platform/persistence/bundle.json +++ b/platform/persistence/bundle.json @@ -8,6 +8,12 @@ "type": "provider", "implementation": "CouchPersistenceProvider.js", "depends": [ "$http", "$q", "PERSISTENCE_SPACE", "COUCHDB_PATH" ] + }, + { + "provides": "persistenceService", + "type": "decorator", + "implementation": "CachingPersistenceDecorator.js", + "depends": [ "PERSISTENCE_SPACE" ] } ], "constants": [ diff --git a/platform/persistence/src/CachingPersistenceDecorator.js b/platform/persistence/src/CachingPersistenceDecorator.js new file mode 100644 index 0000000000..ca646fa11d --- /dev/null +++ b/platform/persistence/src/CachingPersistenceDecorator.js @@ -0,0 +1,100 @@ +/*global define*/ + +define( + [], + function () { + 'use strict'; + + /** + * A caching persistence decorator maintains local copies of persistent objects + * that have been loaded, and keeps them in sync after writes. This allows + * retrievals to occur more quickly after the first load. + * + * @constructor + * @param {string[]} CACHE_SPACES persistence space names which + * should be cached + * @param {PersistenceService} persistenceService the service which + * implements object persistence, whose inputs/outputs + * should be cached. + */ + function CachingPersistenceDecorator(CACHE_SPACES, persistenceService) { + var spaces = CACHE_SPACES || [], // List of spaces to cache + cache = {}; // Where objects will be stored + + // Utility function; avoid sharing one instance everywhere. + function clone(value) { + // Only clone truthy values (no need to clone undefined, false...) + return value && JSON.parse(JSON.stringify(value)); + } + + // Place value in the cache for space, if there is one. + function addToCache(space, key, value) { + if (cache[space]) { + cache[space][key] = { value: clone(value) }; + } + } + + // Create a function for putting value into a cache; + // useful for then-chaining. + function putCache(space, key) { + return function (value) { + addToCache(space, key, value); + return value; + }; + } + + // Wrap as a thenable; used instead of $q.when because that + // will resolve on a future tick, which can cause latency + // issues (which this decorator is intended to address.) + function fastPromise(value) { + return { + then: function (callback) { + return fastPromise(callback(value)); + } + }; + } + + // Arrayify list of spaces to cache, if necessary. + spaces = Array.isArray(spaces) ? spaces : [ spaces ]; + + // Initialize caches + spaces.forEach(function (space) { + cache[space] = {}; + }); + + // Provide PersistenceService interface; mostly delegate to the + // decorated service, intervene and cache where appropriate. + return { + listSpaces: function () { + return persistenceService.listSpaces(); + }, + listObjects: function (space) { + return persistenceService.listObjects(space); + }, + createObject: function (space, key, value) { + addToCache(space, key, value); + return persistenceService.createObject(space, key, value); + }, + readObject: function (space, key) { + return (cache[space] && cache[space][key]) ? + fastPromise(clone(cache[space][key].value)) : + persistenceService.readObject(space, key) + .then(putCache(space, key)); + }, + updateObject: function (space, key, value) { + addToCache(space, key, value); + return persistenceService.updateObject(space, key, value); + }, + deleteObject: function (space, key, value) { + if (cache[space]) { + delete cache[space][key]; + } + return persistenceService.deleteObject(space, key, value); + } + }; + + } + + return CachingPersistenceDecorator; + } +); \ No newline at end of file diff --git a/platform/persistence/test/CachingPersistenceDecoratorSpec.js b/platform/persistence/test/CachingPersistenceDecoratorSpec.js new file mode 100644 index 0000000000..2904544b76 --- /dev/null +++ b/platform/persistence/test/CachingPersistenceDecoratorSpec.js @@ -0,0 +1,12 @@ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +define( + ["../src/CachingPersistenceDecorator"], + function (CachingPersistenceDecorator) { + "use strict"; + + + describe("The caching persistence decorator", function () { + }); + } +); \ No newline at end of file diff --git a/platform/persistence/test/suite.json b/platform/persistence/test/suite.json index 9a5621116d..3beda3a01d 100644 --- a/platform/persistence/test/suite.json +++ b/platform/persistence/test/suite.json @@ -1,4 +1,5 @@ [ + "CachingPersistenceDecorator", "CouchDocument", "CouchIndicator", "CouchPersistenceProvider" From 65a0a921330b195424eca3794aa65ebc10bd532b Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Wed, 7 Jan 2015 10:12:58 -0800 Subject: [PATCH 05/25] [Persistence] Add tests, in-line doc to persistence cache Add tests and in-line documentation for the caching persistence decorator, transitioned to support performance of taxonomy provided by the WARP Server Adapter, WTD-644. --- .../src/CachingPersistenceDecorator.js | 55 +++++++++++++++ .../test/CachingPersistenceDecoratorSpec.js | 70 +++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/platform/persistence/src/CachingPersistenceDecorator.js b/platform/persistence/src/CachingPersistenceDecorator.js index ca646fa11d..9ca964637e 100644 --- a/platform/persistence/src/CachingPersistenceDecorator.js +++ b/platform/persistence/src/CachingPersistenceDecorator.js @@ -65,26 +65,81 @@ define( // Provide PersistenceService interface; mostly delegate to the // decorated service, intervene and cache where appropriate. return { + /** + * List all persistence spaces that are supported by the + * decorated service. + * @memberof CachingPersistenceDecorator# + * @returns {Promise.} spaces supported + */ listSpaces: function () { return persistenceService.listSpaces(); }, + /** + * List all objects in a specific space. + * @memberof CachingPersistenceDecorator# + * @param {string} space the space in which to list objects + * @returns {Promise.} keys for objects in this space + */ listObjects: function (space) { return persistenceService.listObjects(space); }, + /** + * Create an object in a specific space. This will + * be cached to expedite subsequent retrieval. + * @memberof CachingPersistenceDecorator# + * @param {string} space the space in which to create the object + * @param {string} key the key associate with the object for + * subsequent lookup + * @param {object} value a JSONifiable object to store + * @returns {Promise.} an indicator of the success or + * failure of this request + */ createObject: function (space, key, value) { addToCache(space, key, value); return persistenceService.createObject(space, key, value); }, + /** + * Read an object from a specific space. This will read from a + * cache if the object is available. + * @memberof CachingPersistenceDecorator# + * @param {string} space the space in which to create the object + * @param {string} key the key which identifies the object + * @returns {Promise.} a promise for the object; may + * resolve to undefined (if the object does not exist + * in this space) + */ readObject: function (space, key) { return (cache[space] && cache[space][key]) ? fastPromise(clone(cache[space][key].value)) : persistenceService.readObject(space, key) .then(putCache(space, key)); }, + /** + * Update an object in a specific space. This will + * be cached to expedite subsequent retrieval. + * @memberof CachingPersistenceDecorator# + * @param {string} space the space in which to create the object + * @param {string} key the key associate with the object for + * subsequent lookup + * @param {object} value a JSONifiable object to store + * @returns {Promise.} an indicator of the success or + * failure of this request + */ updateObject: function (space, key, value) { addToCache(space, key, value); return persistenceService.updateObject(space, key, value); }, + /** + * Delete an object in a specific space. This will + * additionally be cleared from the cache. + * @memberof CachingPersistenceDecorator# + * @param {string} space the space in which to create the object + * @param {string} key the key associate with the object for + * subsequent lookup + * @param {object} value a JSONifiable object to delete + * @returns {Promise.} an indicator of the success or + * failure of this request + */ deleteObject: function (space, key, value) { if (cache[space]) { delete cache[space][key]; diff --git a/platform/persistence/test/CachingPersistenceDecoratorSpec.js b/platform/persistence/test/CachingPersistenceDecoratorSpec.js index 2904544b76..9f2758a99d 100644 --- a/platform/persistence/test/CachingPersistenceDecoratorSpec.js +++ b/platform/persistence/test/CachingPersistenceDecoratorSpec.js @@ -5,8 +5,78 @@ define( function (CachingPersistenceDecorator) { "use strict"; + var PERSISTENCE_METHODS = [ + "listSpaces", + "listObjects", + "createObject", + "readObject", + "updateObject", + "deleteObject" + ]; describe("The caching persistence decorator", function () { + var testSpace, + mockPersistence, + mockCallback, + decorator; + + function mockPromise(value) { + return { + then: function (callback) { + return mockPromise(callback(value)); + } + }; + } + + beforeEach(function () { + + + testSpace = "TEST"; + mockPersistence = jasmine.createSpyObj( + "persistenceService", + PERSISTENCE_METHODS + ); + mockCallback = jasmine.createSpy("callback"); + + PERSISTENCE_METHODS.forEach(function (m) { + mockPersistence[m].andReturn(mockPromise({ + method: m + })); + }); + + decorator = new CachingPersistenceDecorator( + testSpace, + mockPersistence + ); + }); + + it("delegates all methods", function () { + PERSISTENCE_METHODS.forEach(function (m) { + // Reset the callback + mockCallback = jasmine.createSpy("callback"); + // Invoke the method; avoid using a key that will be cached + decorator[m](testSpace, "testKey" + m, "testValue") + .then(mockCallback); + // Should have gotten that method's plain response + expect(mockCallback).toHaveBeenCalledWith({ method: m }); + }); + }); + + it("does not repeat reads of cached objects", function () { + // Perform two reads + decorator.readObject(testSpace, "someKey", "someValue") + .then(mockCallback); + decorator.readObject(testSpace, "someKey", "someValue") + .then(mockCallback); + + // Should have only delegated once + expect(mockPersistence.readObject.calls.length).toEqual(1); + + // But both promises should have resolved + expect(mockCallback.calls.length).toEqual(2); + + }); + }); } ); \ No newline at end of file From f9a4ac548c67285d6cf0236c829f6400d06f6264 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Wed, 7 Jan 2015 11:53:32 -0800 Subject: [PATCH 06/25] [Views] Add tests for type-view restrictions Add tests for type-view restrictions added to support stricter coupling of Packet objects to Autoflow Tabular View, WTD-644. --- platform/core/src/views/ViewProvider.js | 2 +- platform/core/test/views/ViewProviderSpec.js | 51 ++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/platform/core/src/views/ViewProvider.js b/platform/core/src/views/ViewProvider.js index d1535328e8..50b47f4068 100644 --- a/platform/core/src/views/ViewProvider.js +++ b/platform/core/src/views/ViewProvider.js @@ -84,7 +84,7 @@ define( // Check if a view and domain object type can be paired; // both can restrict the others they accept. function viewMatchesType(view, type) { - var views = type && type.getDefinition().views, + var views = type && (type.getDefinition() || {}).views, matches = true; // View is restricted to a certain type diff --git a/platform/core/test/views/ViewProviderSpec.js b/platform/core/test/views/ViewProviderSpec.js index 47d44d0058..bb0fdcf552 100644 --- a/platform/core/test/views/ViewProviderSpec.js +++ b/platform/core/test/views/ViewProviderSpec.js @@ -87,6 +87,57 @@ define( expect(mockLog.warn).toHaveBeenCalledWith(jasmine.any(String)); }); + it("restricts typed views to matching types", function () { + var testType = "testType", + testView = { key: "x", type: testType }, + provider = new ViewProvider([testView], mockLog); + + // Include a "type" capability + capabilities.type = jasmine.createSpyObj( + "type", + ["instanceOf", "invoke", "getDefinition"] + ); + capabilities.type.invoke.andReturn(capabilities.type); + + // Should be included when types match + capabilities.type.instanceOf.andReturn(true); + expect(provider.getViews(mockDomainObject)) + .toEqual([testView]); + expect(capabilities.type.instanceOf) + .toHaveBeenCalledWith(testType); + + // ...but not when they don't + capabilities.type.instanceOf.andReturn(false); + expect(provider.getViews(mockDomainObject)) + .toEqual([]); + + }); + + it("enforces view restrictions from types", function () { + var testType = "testType", + testView = { key: "x" }, + provider = new ViewProvider([testView], mockLog); + + // Include a "type" capability + capabilities.type = jasmine.createSpyObj( + "type", + ["instanceOf", "invoke", "getDefinition"] + ); + capabilities.type.invoke.andReturn(capabilities.type); + + // Should be included when view keys match + capabilities.type.getDefinition + .andReturn({ views: [testView.key]}); + expect(provider.getViews(mockDomainObject)) + .toEqual([testView]); + + // ...but not when they don't + capabilities.type.getDefinition + .andReturn({ views: ["somethingElse"]}); + expect(provider.getViews(mockDomainObject)) + .toEqual([]); + }); + }); } ); \ No newline at end of file From 782edfc3b7424d17036fe1d5e06beba9e9ad83f3 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 9 Jan 2015 16:16:58 -0800 Subject: [PATCH 07/25] [WARP Telemetry] Work around platform issues Work around issues in platform which manifest as bugs or latency issues when attempting to visualize telemetry from the WARP Server. Specifically: * Handle the possibility that there is no matching representation for an object from EditRepresenter. * Don't attempt to look up colon-separated domain object identifiers from persistence. This is a workaround to avoid latency issues from excessive persistence calls; filed WTD-659 to improve this solution. Changes made to support resolution of WTD-644. --- platform/commonUI/edit/src/EditRepresenter.js | 2 +- platform/core/src/models/PersistedModelProvider.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/platform/commonUI/edit/src/EditRepresenter.js b/platform/commonUI/edit/src/EditRepresenter.js index 6cf40aadd2..bf3dfc721b 100644 --- a/platform/commonUI/edit/src/EditRepresenter.js +++ b/platform/commonUI/edit/src/EditRepresenter.js @@ -73,7 +73,7 @@ define( // Handle a specific representation of a specific domain object function represent(representation, representedObject) { // Track the key, to know which view configuration to save to. - key = representation.key; + key = (representation || {}).key; // Track the represented object domainObject = representedObject; diff --git a/platform/core/src/models/PersistedModelProvider.js b/platform/core/src/models/PersistedModelProvider.js index 8b49f6b538..88ec5afee7 100644 --- a/platform/core/src/models/PersistedModelProvider.js +++ b/platform/core/src/models/PersistedModelProvider.js @@ -21,7 +21,9 @@ define( */ function PersistedModelProvider(persistenceService, $q, SPACE) { function promiseModels(ids) { - return $q.all(ids.map(function (id) { + return $q.all(ids.filter(function (id) { + return id.indexOf(":") === -1; + }).map(function (id) { return persistenceService.readObject(SPACE, id); })).then(function (models) { var result = {}; From e3851c4e9c3f2ec3a42c322beac536fb8d3a6740 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 9 Jan 2015 17:01:03 -0800 Subject: [PATCH 08/25] [Telemetry] Subscribe from telemetry controller Initiate subscriptions from telemetry controller. Allows plotting of streaming data from the WARP Server, WTD-644. --- platform/telemetry/src/TelemetryController.js | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/platform/telemetry/src/TelemetryController.js b/platform/telemetry/src/TelemetryController.js index 23ed15165e..94aa0358ef 100644 --- a/platform/telemetry/src/TelemetryController.js +++ b/platform/telemetry/src/TelemetryController.js @@ -47,6 +47,9 @@ define( // is being issued. broadcasting: false, + // Active subscriptions + subscriptions: [], + // Used for getTelemetryObjects; a reference is // stored so that this can be called in a watch telemetryObjects: [], @@ -183,6 +186,25 @@ define( } } + // Subscribe to streaming telemetry updates + function subscribe(domainObject) { + var telemetryCapability = + domainObject.getCapability("telemetry"); + return telemetryCapability.subscribe(function () { + requestTelemetryForId( + domainObject.getId(), + false + ); + }); + } + + // Stop listening to active subscriptions + function unsubscribe() { + self.subscriptions.forEach(function (s) { + return s && s(); + }); + } + // Build response containers (as above) for all // domain objects, and update some controller-internal // state to support subsequent calls. @@ -205,6 +227,9 @@ define( return self.response[id].metadata; }); + // Subscribe to all telemetry objects + self.subscriptions = domainObjects.map(subscribe); + // Issue a request for the new objects, if we // know what our request looks like if (self.request) { @@ -217,6 +242,7 @@ define( // scope. This will be the domain object itself, or // its telemetry delegates, or both. function getTelemetryObjects(domainObject) { + unsubscribe(); promiseRelevantDomainObjects(domainObject) .then(buildResponseContainers); } @@ -241,6 +267,7 @@ define( // Stop polling for changes function deactivate() { + unsubscribe(); self.active = false; } From d916111060ffcd049383e6c97bdf2c65c5287368 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 9 Jan 2015 17:40:34 -0800 Subject: [PATCH 09/25] [Tree] Restore wait spinner Restore wait spinner in tree (missed during Angular transition); this feedback is important when waiting for the contents of the Packets node (WTD-644) to load. --- platform/commonUI/general/res/templates/tree.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/platform/commonUI/general/res/templates/tree.html b/platform/commonUI/general/res/templates/tree.html index 46a3259634..92eb25ef7a 100644 --- a/platform/commonUI/general/res/templates/tree.html +++ b/platform/commonUI/general/res/templates/tree.html @@ -1,4 +1,10 @@