diff --git a/example/profiling/bundle.json b/example/profiling/bundle.json new file mode 100644 index 0000000000..b6090717d2 --- /dev/null +++ b/example/profiling/bundle.json @@ -0,0 +1,10 @@ +{ + "extensions": { + "indicators": [ + { + "implementation": "WatchIndicator.js", + "depends": ["$interval", "$rootScope"] + } + ] + } +} \ No newline at end of file diff --git a/example/profiling/src/WatchIndicator.js b/example/profiling/src/WatchIndicator.js new file mode 100644 index 0000000000..03a484927c --- /dev/null +++ b/example/profiling/src/WatchIndicator.js @@ -0,0 +1,77 @@ +/*global define*/ + +define( + [], + function () { + "use strict"; + + /** + * Updates a count of currently-active Angular watches. + * @constructor + * @param $interval Angular's $interval + */ + function WatchIndicator($interval, $rootScope) { + var watches = 0; + + function count(scope) { + if (scope) { + watches += (scope.$$watchers || []).length; + count(scope.$$childHead); + count(scope.$$nextSibling); + } + } + + function update() { + watches = 0; + count($rootScope); + } + + // Update state every second + $interval(update, 1000); + + // Provide initial state, too + update(); + + return { + /** + * Get the glyph (single character used as an icon) + * to display in this indicator. This will return ".", + * which should appear as a dataflow icon. + * @returns {string} the character of the database icon + */ + getGlyph: function () { + return "E"; + }, + /** + * Get the name of the CSS class to apply to the glyph. + * This is used to color the glyph to match its + * state (one of ok, caution or err) + * @returns {string} the CSS class to apply to this glyph + */ + getGlyphClass: function () { + return (watches > 2000) ? "caution" : + (watches < 1000) ? "ok" : + undefined; + }, + /** + * Get the text that should appear in the indicator. + * @returns {string} brief summary of connection status + */ + getText: function () { + return watches + " watches"; + }, + /** + * Get a longer-form description of the current connection + * space, suitable for display in a tooltip + * @returns {string} longer summary of connection status + */ + getDescription: function () { + return ""; + } + }; + } + + return WatchIndicator; + + } +); \ No newline at end of file diff --git a/platform/commonUI/general/src/directives/MCTResize.js b/platform/commonUI/general/src/directives/MCTResize.js index 10e4256c50..9e6d5a29f1 100644 --- a/platform/commonUI/general/src/directives/MCTResize.js +++ b/platform/commonUI/general/src/directives/MCTResize.js @@ -35,7 +35,8 @@ define( // Link; start listening for changes to an element's size function link(scope, element, attrs) { - var lastBounds; + var lastBounds, + active = true; // Determine how long to wait before the next update function currentInterval() { @@ -62,9 +63,19 @@ define( width: element[0].offsetWidth, height: element[0].offsetHeight }); - $timeout(onInterval, currentInterval()); + if (active) { + $timeout(onInterval, currentInterval()); + } } + // Stop running in the background + function deactivate() { + active = false; + } + + // Unregister once out-of-scope + scope.$on("$destroy", deactivate); + // Handle the initial callback onInterval(); } diff --git a/platform/commonUI/general/test/directives/MCTResizeSpec.js b/platform/commonUI/general/test/directives/MCTResizeSpec.js index 4f07701d50..b320d5e881 100644 --- a/platform/commonUI/general/test/directives/MCTResizeSpec.js +++ b/platform/commonUI/general/test/directives/MCTResizeSpec.js @@ -14,7 +14,7 @@ define( beforeEach(function () { mockTimeout = jasmine.createSpy("$timeout"); - mockScope = jasmine.createSpyObj("$scope", ["$eval"]); + mockScope = jasmine.createSpyObj("$scope", ["$eval", "$on"]); testElement = { offsetWidth: 100, offsetHeight: 200 }; testAttrs = { mctResize: "some-expr" }; @@ -63,6 +63,32 @@ define( ); }); + it("stops size checking for size changes after destroy", function () { + mctResize.link(mockScope, [testElement], testAttrs); + + // First, make sure there's a $destroy observer + expect(mockScope.$on) + .toHaveBeenCalledWith("$destroy", jasmine.any(Function)); + + // Should have scheduled the first timeout + expect(mockTimeout.calls.length).toEqual(1); + + // Fire the timeout + mockTimeout.mostRecentCall.args[0](); + + // Should have scheduled another timeout + expect(mockTimeout.calls.length).toEqual(2); + + // Broadcast a destroy event + mockScope.$on.mostRecentCall.args[1](); + + // Fire the timeout + mockTimeout.mostRecentCall.args[0](); + + // Should NOT have scheduled another timeout + expect(mockTimeout.calls.length).toEqual(2); + }); + }); } ); \ No newline at end of file diff --git a/platform/features/plot/src/MCTChart.js b/platform/features/plot/src/MCTChart.js index 5c01bc6efc..43ebaa891f 100644 --- a/platform/features/plot/src/MCTChart.js +++ b/platform/features/plot/src/MCTChart.js @@ -46,6 +46,7 @@ define( function linkChart(scope, element) { var canvas = element.find("canvas")[0], + activeInterval, chart; // Try to initialize GLChart, which allows drawing using WebGL. @@ -110,12 +111,22 @@ define( } } + // Stop watching for changes to size (scope destroyed) + function releaseInterval() { + if (activeInterval) { + $interval.cancel(activeInterval); + } + } + // Check for resize, on a timer - $interval(drawIfResized, 1000); + activeInterval = $interval(drawIfResized, 1000); // Watch "draw" for external changes to the set of // things to be drawn. scope.$watchCollection("draw", doDraw); + + // Stop checking for resize when scope is destroyed + scope.$on("$destroy", releaseInterval); } return { diff --git a/platform/features/plot/test/MCTChartSpec.js b/platform/features/plot/test/MCTChartSpec.js index 84d6a27368..a8ddc24096 100644 --- a/platform/features/plot/test/MCTChartSpec.js +++ b/platform/features/plot/test/MCTChartSpec.js @@ -15,6 +15,7 @@ define( mockElement, mockCanvas, mockGL, + mockPromise, mctChart; beforeEach(function () { @@ -23,9 +24,11 @@ define( mockLog = jasmine.createSpyObj("$log", ["warn", "info", "debug"]); mockScope = - jasmine.createSpyObj("$scope", ["$watchCollection"]); + jasmine.createSpyObj("$scope", ["$watchCollection", "$on"]); mockElement = jasmine.createSpyObj("element", ["find"]); + mockInterval.cancel = jasmine.createSpy("cancelInterval"); + mockPromise = jasmine.createSpyObj("promise", ["then"]); // mct-chart uses GLChart, so it needs WebGL API @@ -70,6 +73,7 @@ define( mockElement.find.andReturn([mockCanvas]); mockCanvas.getContext.andReturn(mockGL); + mockInterval.andReturn(mockPromise); mctChart = new MCTChart(mockInterval, mockLog); }); @@ -150,6 +154,26 @@ define( expect(mockLog.warn).not.toHaveBeenCalled(); }); + // Avoid resource leaks + it("stops polling for size changes on destroy", function () { + mctChart.link(mockScope, mockElement); + + // Should be listening for a destroy event + expect(mockScope.$on).toHaveBeenCalledWith( + "$destroy", + jasmine.any(Function) + ); + + // Precondition - interval still active + expect(mockInterval.cancel).not.toHaveBeenCalled(); + + // Broadcast a $destroy + mockScope.$on.mostRecentCall.args[1](); + + // Should have stopped the interval + expect(mockInterval.cancel).toHaveBeenCalledWith(mockPromise); + }); + }); } ); \ No newline at end of file