Angular performance testing via ports

Leave hooks in the AngularJS application to allow measuring how long individual actions take.

I always advise to refactor code if it is not easily testable. For example, exposing a grouping function from the CommonJS module to be able to unit test it

hard to test
1
2
3
4
5
6
7
8
9
10
function action(inputs) {
function group() {
return doSomethingWith(inputs);
}

return doSomethingElse(group());
}
module.exports = {
action: action
};

There are ways to unit test private functions like group using NodeJS hooks, for example using describe-it, but it is much better to refactor the above code for simple unit testing

simple to test
1
2
3
4
5
6
7
8
9
10
function group(data) {
return doSomethingWith(data);
}
function action(inputs) {
return doSomethingElse(group(inputs));
}
module.exports = {
action: action,
group: group
};

We have added group to the exported object, and are passing the inputs to the function, instead of using the closure. This makes it simple to write a unit test

unit test spec
1
2
3
4
5
6
7
8
var group = require('./action').group;
describe('group', function () {
it('works', function () {
var data = ...;
var result = group(data);
expect(result).to.equal(...);
});
});

AngularJS is a very testable framework. Values, services, controllers - all can be quickly injected and unit tested from specs. We even wrote ng-describe to remove the usual boilerplate code from the testing code

test a controller / scope
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
angular.module('S', [])
.controller('sample', function ($timeout, $scope) {
$scope.foo = 'foo';
$scope.update = function () {
$timeout(function () {
$scope.foo = 'bar';
}, 1000);
};
});
ngDescribe({
name: 'timeout in controller',
modules: 'S',
// inject $timeout so we can flush the timeout queue
inject: ['$timeout'],
controllers: 'sample',
tests: function (deps) {
// deps.sample = $scope object injected into sample controller
it('has initial values', function () {
la(deps.sample.foo === 'foo');
});
it('updates after timeout', function () {
deps.sample.update();
deps.$timeout.flush();
la(deps.sample.foo === 'bar');
});
}
});

What about the performance testing? How do we know if our live application performs fast enough for the end users? Unit testing the small benchmarks is very time consuming in my opinion. A much better approach is too treat this problem as a it develops. Typically, a client or a tester contacts the team with a complain:

When I click on this button, the application freezes for a long time.

This is when I start profiling the application using live data. The Angular framework makes some aspects of the performance profiling very simple. Since most of the data and logic is attached to the scope objects, it can be accessed from the browser's console. In the example below, we can grab the reference to a method via an element's scope, then monitor and debug it.

debug method

Even better, all the boilerplate code to attach and time specific scope methods can be stored outside the application in Chrome's code snippets. In the code snippet below, available in bahmutov/code-snippets we can wrap a specific method with a code that starts the Chrome profiler. When the method finishes (either by returning a value, or when the returned promise completes), the profiler stops, giving us a very accurate timing information.

wrap method

Performance diagnostic port

Returning a promise from an asynchronous scope method, even if the promise is not used in the other application parts, is very useful for diagnosing performance problems. You can think of this similar to modern cars having a standard port for reading the engine's log and various sensor data.

sensor

Most Angular asynchronous services already return a promise by default. For example, we can time how long a method takes if it uses $timeout service by returning it. Start with the application code

application code without a port
1
2
3
4
5
$scope.sleepNseconds = function (n) {
$timeout(function () {
// done!
}, n * 1000);
}

and refactor it to return a promise, exposing a "port" for quick profiling from the browser's console

application code WITH a port
1
2
3
4
5
6
7
8
9
10
11
12
$scope.sleepNseconds = function (n) {
return $timeout(function () {
// done!
}, n * 1000);
}
// from the browser console
console.profile('sleep');
var action = angular.element($0).scope().sleepNSeconds;
$q.when(action())
.finally(function () {
console.profileEnd('sleep');
});

Sometimes making a promise-returning port is more complicated. For example, the directives might exchange messages instead of calling methods and returning a promise. In this case, you have to refactor more code in order to allow an external code to profile the action's performance. For example, imagine that the parent controller broadcasts a message to all children. Every child does something for random number of milliseconds. You can see the application in action below. Click the "action" button to broadcast the message.

original code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var randomMillis = function() {
return Math.floor(Math.random() * 5000);
}
angular.module('App', [])
.controller('parent', function ($scope) {
$scope.items = ['one', 'two', 'three'];
$scope.action = function action() {
$scope.$broadcast('action');
console.log('parent sent action broadcast');
};
})
.controller('child', function ($scope, $timeout) {
$scope.$on('action', function childAction() {
var ms = randomMillis();
console.log('starting action in child for', ms, 'ms');
$timeout(function () {
console.log('action completed after', ms, 'ms');
}, ms);
});
});

The DOM will have markup for the parent and children controllers

1
2
3
4
5
6
7
<div ng-controller="parent">
this is parent controller.
<button id="action" ng-click="action()">action</button>
<div ng-repeat="item in items" ng-controller="child">
<h3>child controller {{$index}}</h3>
</div>
</div>

This code cannot be measured or profiled accurately. The profiler could be started and stopped manually, but this is inaccurate and not very repeatable. Let us refactor the code to return a promise from the action. First, we need to keep track how many children controllers are executing. Second, we need to resolve the promise when every child controller finishes. To do this, the child controllers will emit additional messages: one when starting an action, and another when the action is complete. This allows the parent controller to know when every part is complete.

code with a port
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var randomMillis = function() {
return Math.floor(Math.random() * 5000);
}
angular.module('App', [])
.controller('parent', function ($scope, $q) {
$scope.items = ['one', 'two', 'three'];
var defer;
$scope.action = function action() {
$scope.actionStarted = 0;
$scope.$broadcast('action');
console.log('parent sent action broadcast');
defer = $q.defer();
return defer.promise;
};
$scope.$on('actionStarted', function () {
$scope.actionStarted += 1;
});
$scope.$on('actionCompleted', function () {
$scope.actionStarted -= 1;
console.log('remaining actions', $scope.actionStarted);
if ($scope.actionStarted === 0) {
defer.resolve();
}
});
})
.controller('child', function ($scope, $timeout) {
$scope.$on('action', function childAction() {
var ms = randomMillis();
console.log('starting action in child for', ms, 'ms');
$scope.$emit('actionStarted');
$timeout(function () {
console.log('action completed after', ms, 'ms');
$scope.$emit('actionCompleted');
}, ms);
});
});

Normally, I would profile this code from the browser's console using ng-profile-scope-method.js code snippet. Because the code is embedded in an iframe in the example, I had to place the wrapping code into the Angular application itself. Here is the additional code in the parent's controller

1
2
3
4
5
6
7
8
9
10
11
12
13
$scope.profileAction = function profileAction() {
var _action = $scope.action;
$scope.action = function () {
console.profile('action');
console.time('action');
$q.when(_action())
.finally(function () {
console.timeEnd('action');
console.profileEnd('action');
$scope.action = _action;
});
};
};

Open the browser console, then click the "profile action" and then the "action" buttons.

You should see the timing information and the generated profile. The last message in the console comes from console.time() - console.timeEnd() calls and should accurately reflect how long the entire action took.

We had to add code to our action method, but the new code makes it simple to profile. I am sure that this boilerplate code is easy to factor out into a service to make a truly reusable performance diagnostic tool.