Merge pull request #5 from nasa/open1202

[Plot] Improve plotting performance
This commit is contained in:
Victor Woeltjen
2015-06-19 16:05:18 -07:00
10 changed files with 189 additions and 20 deletions

View File

@@ -184,6 +184,11 @@
{
"key": "now",
"implementation": "services/Now.js"
},
{
"key": "throttle",
"implementation": "services/Throttle.js",
"depends": [ "$timeout" ]
}
],
"roots": [

View File

@@ -0,0 +1,63 @@
/*global define*/
define(
[],
function () {
"use strict";
/**
* Throttler for function executions, registered as the `throttle`
* service.
*
* Usage:
*
* throttle(fn, delay, [apply])
*
* Returns a function that, when invoked, will invoke `fn` after
* `delay` milliseconds, only if no other invocations are pending.
* The optional argument `apply` determines whether.
*
* The returned function will itself return a `Promise` which will
* resolve to the returned value of `fn` whenever that is invoked.
*
* @returns {Function}
*/
function Throttle($timeout) {
/**
* Throttle this function.
* @param {Function} fn the function to throttle
* @param {number} [delay] the delay, in milliseconds, before
* executing this function; defaults to 0.
* @param {boolean} apply true if a `$apply` call should be
* invoked after this function executes; defaults to
* `false`.
*/
return function (fn, delay, apply) {
var activeTimeout;
// Clear active timeout, so that next invocation starts
// a new one.
function clearActiveTimeout() {
activeTimeout = undefined;
}
// Defaults
delay = delay || 0;
apply = apply || false;
return function () {
// Start a timeout if needed
if (!activeTimeout) {
activeTimeout = $timeout(fn, delay, apply);
activeTimeout.then(clearActiveTimeout);
}
// Return whichever timeout is active (to get
// a promise for the results of fn)
return activeTimeout;
};
};
}
return Throttle;
}
);

View File

@@ -0,0 +1,49 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/services/Throttle"],
function (Throttle) {
"use strict";
describe("The 'throttle' service", function () {
var throttle,
mockTimeout,
mockFn,
mockPromise;
beforeEach(function () {
mockTimeout = jasmine.createSpy("$timeout");
mockPromise = jasmine.createSpyObj("promise", ["then"]);
mockFn = jasmine.createSpy("fn");
mockTimeout.andReturn(mockPromise);
throttle = new Throttle(mockTimeout);
});
it("provides functions which run on a timeout", function () {
var throttled = throttle(mockFn);
// Verify precondition: Not called at throttle-time
expect(mockTimeout).not.toHaveBeenCalled();
expect(throttled()).toEqual(mockPromise);
expect(mockTimeout).toHaveBeenCalledWith(mockFn, 0, false);
});
it("schedules only one timeout at a time", function () {
var throttled = throttle(mockFn);
throttled();
throttled();
throttled();
expect(mockTimeout.calls.length).toEqual(1);
});
it("schedules additional invocations after resolution", function () {
var throttled = throttle(mockFn);
throttled();
mockPromise.then.mostRecentCall.args[0](); // Resolve timeout
throttled();
mockPromise.then.mostRecentCall.args[0]();
throttled();
expect(mockTimeout.calls.length).toEqual(3);
});
});
}
);

View File

@@ -24,6 +24,7 @@
"objects/DomainObjectProvider",
"services/Now",
"services/Throttle",
"types/MergeModels",
"types/TypeCapability",

View File

@@ -23,7 +23,7 @@
{
"key": "PlotController",
"implementation": "PlotController.js",
"depends": [ "$scope", "telemetryFormatter", "telemetryHandler" ]
"depends": [ "$scope", "telemetryFormatter", "telemetryHandler", "throttle" ]
}
]
}

View File

@@ -51,13 +51,14 @@ define(
*
* @constructor
*/
function PlotController($scope, telemetryFormatter, telemetryHandler) {
function PlotController($scope, telemetryFormatter, telemetryHandler, throttle) {
var subPlotFactory = new SubPlotFactory(telemetryFormatter),
modeOptions = new PlotModeOptions([], subPlotFactory),
subplots = [],
cachedObjects = [],
updater,
handle,
scheduleUpdate,
domainOffset;
// Populate the scope with axis information (specifically, options
@@ -89,9 +90,7 @@ define(
// Update all sub-plots
function update() {
modeOptions.getModeHandler()
.getSubPlots()
.forEach(updateSubplot);
scheduleUpdate();
}
// Reinstantiate the plot updater (e.g. because we have a
@@ -162,6 +161,12 @@ define(
// Unsubscribe when the plot is destroyed
$scope.$on("$destroy", releaseSubscription);
// Create a throttled update function
scheduleUpdate = throttle(function () {
modeOptions.getModeHandler().getSubPlots()
.forEach(updateSubplot);
});
return {
/**

View File

@@ -62,8 +62,6 @@ define(
points: buf.getLength()
};
});
subplot.update();
}
return {

View File

@@ -58,8 +58,6 @@ define(
color: PlotPalette.getFloatColor(0),
points: buffer.getLength()
}];
subplot.update();
}
function plotTelemetry(prepared) {

View File

@@ -33,6 +33,7 @@ define(
var mockScope,
mockFormatter,
mockHandler,
mockThrottle,
mockHandle,
mockDomainObject,
mockSeries,
@@ -56,6 +57,7 @@ define(
"telemetrySubscriber",
["handle"]
);
mockThrottle = jasmine.createSpy("throttle");
mockHandle = jasmine.createSpyObj(
"subscription",
[
@@ -73,12 +75,18 @@ define(
);
mockHandler.handle.andReturn(mockHandle);
mockThrottle.andCallFake(function (fn) { return fn; });
mockHandle.getTelemetryObjects.andReturn([mockDomainObject]);
mockHandle.getMetadata.andReturn([{}]);
mockHandle.getDomainValue.andReturn(123);
mockHandle.getRangeValue.andReturn(42);
controller = new PlotController(mockScope, mockFormatter, mockHandler);
controller = new PlotController(
mockScope,
mockFormatter,
mockHandler,
mockThrottle
);
});
it("provides plot colors", function () {
@@ -224,4 +232,4 @@ define(
});
});
}
);
);

View File

@@ -35,28 +35,68 @@ define(
* @constructor
*/
function TelemetryQueue() {
var queue = [];
// General approach here:
// * Maintain a queue as an array of objects containing key-value
// pairs. Putting values into the queue will assign to the
// earliest-available queue position for the associated key
// (appending to the array if necessary.)
// * Maintain a set of counts for each key, such that determining
// the next available queue position is easy; O(1) insertion.
// * When retrieving objects, pop off the queue and decrement
// counts. This provides O(n+k) or O(k) retrieval for a queue
// of length n with k unique keys; this depends on whether
// the browser's implementation of Array.prototype.shift is
// O(n) or O(1).
// Graphically (indexes at top, keys along side, values as *'s),
// if we have a queue that looks like:
// 0 1 2 3 4
// a * * * * *
// b * *
// c * * *
//
// And we put a new value for b, we expect:
// 0 1 2 3 4
// a * * * * *
// b * * *
// c * * *
var queue = [],
counts = {};
// Look up an object in the queue that does not have a value
// assigned to this key (or, add a new one)
function getFreeObject(key) {
var index = 0, object;
var index = counts[key] || 0, object;
// Look for an existing queue position where we can store
// a value to this key without overwriting an existing value.
for (index = 0; index < queue.length; index += 1) {
if (queue[index][key] === undefined) {
return queue[index];
}
// Track the largest free position for this key
counts[key] = index + 1;
// If it's before the end of the queue, add it there
if (index < queue.length) {
return queue[index];
}
// If we made it through the loop, values have been assigned
// Otherwise, values have been assigned
// to that key in all queued containers, so we need to queue
// up a new container for key-value pairs.
object = {};
queue.push(object);
return object;
}
// Decrement counts for a specific key
function decrementCount(key) {
if (counts[key] < 2) {
delete counts[key];
} else {
counts[key] -= 1;
}
}
// Decrement all counts
function decrementCounts() {
Object.keys(counts).forEach(decrementCount);
}
return {
/**
@@ -74,6 +114,8 @@ define(
* @return {object} key-value pairs
*/
poll: function () {
// Decrement counts for the object that will be popped
decrementCounts();
return queue.shift();
},
/**