Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 52b77b6

Browse files
committed
chore(perf): add event delegation benchmark
1 parent e982581 commit 52b77b6

File tree

5 files changed

+323
-1
lines changed

5 files changed

+323
-1
lines changed

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
"jshint-stylish": "~0.1.5",
5454
"node-html-encoder": "0.0.2",
5555
"sorted-object": "^1.0.0",
56-
"qq": "^0.3.5"
56+
"qq": "^0.3.5",
57+
"benchmark": "1.x.x"
5758
},
5859
"licenses": [
5960
{

perf/apps/event-delegation/app.js

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
var app = angular.module('perf', ['ngBench'])
2+
.directive('noopDir', function() {
3+
return {
4+
compile: function($element, $attrs) {
5+
return function($scope, $element) {
6+
return 1;
7+
}
8+
}
9+
};
10+
})
11+
app.directive('nativeClick', ['$parse', function($parse) {
12+
return {
13+
compile: function($element, $attrs) {
14+
var expr = $parse($attrs.tstEvent);
15+
return function($scope, $element) {
16+
$element[0].addEventListener('click', function() {
17+
console.log('clicked');
18+
}, false);
19+
}
20+
}
21+
};
22+
}])
23+
.directive('dlgtClick', function() {
24+
return {
25+
compile: function($element, $attrs) {
26+
var evt = $attrs.dlgtClick;
27+
// We don't setup the global event listeners as the costs are small and one time only...
28+
}
29+
};
30+
})
31+
.controller('MainCtrl', ['$compile', '$rootScope', '$templateCache',
32+
function($compile, $rootScope, $templateCache) {
33+
// TODO: Make ngRepeatCount configurable via the UI!
34+
var self = this;
35+
this.ngRepeatCount = 20;
36+
this.manualRepeatCount = 5;
37+
this.benchmarks = [{
38+
title: 'ng-click',
39+
factory: function() {
40+
return createBenchmark({
41+
directive: 'ng-click="a()"'
42+
});
43+
},
44+
active: true
45+
},{
46+
title: 'ng-click without jqLite',
47+
factory: function() {
48+
return createBenchmark({
49+
directive: 'native-click="a()"'
50+
});
51+
},
52+
active: true
53+
},{
54+
title: 'baseline: ng-show',
55+
factory: function() {
56+
return createBenchmark({
57+
directive: 'ng-show="true"'
58+
});
59+
},
60+
active: true
61+
},{
62+
title: 'baseline: text interpolation',
63+
factory: function() {
64+
return createBenchmark({
65+
text: '{{row}}'
66+
});
67+
},
68+
active: true
69+
},{
70+
title: 'delegate event directive (only compile)',
71+
factory: function() {
72+
return createBenchmark({
73+
directive: 'dlgt-click="a()"'
74+
});
75+
},
76+
active: true
77+
},{
78+
title: 'baseline: noop directive (compile and link)',
79+
factory: function() {
80+
return createBenchmark({
81+
directive: 'noop-dir'
82+
});
83+
},
84+
active: true
85+
},{
86+
title: 'baseline: no directive',
87+
factory: function() {
88+
return createBenchmark({});
89+
},
90+
active: true
91+
}];
92+
93+
function createBenchmark(options) {
94+
options.directive = options.directive || '';
95+
options.text = options.text || '';
96+
97+
var templateHtml = '<div><span ng-repeat="row in rows">';
98+
for (var i=0; i<self.manualRepeatCount; i++) {
99+
templateHtml += '<span '+options.directive+'>'+options.text+'</span>';
100+
}
101+
templateHtml += '</span></div>';
102+
103+
var compiledTemplate = $compile(templateHtml);
104+
var rows = [];
105+
for (var i=0; i<self.ngRepeatCount; i++) {
106+
rows.push('row'+i);
107+
}
108+
return function(container) {
109+
var scope = $rootScope.$new();
110+
try {
111+
scope.rows = rows;
112+
compiledTemplate(scope, function(clone) {
113+
container.appendChild(clone[0]);
114+
});
115+
scope.$digest();
116+
} finally {
117+
scope.$destroy();
118+
}
119+
}
120+
}
121+
}])

perf/apps/event-delegation/index.html

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<!DOCTYPE html>
2+
<html ng-app="perf" ng-controller="MainCtrl as ctrl">
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>Event delegation</title>
6+
7+
<script src="../../../build/angular.js"></script>
8+
<script src="../../../node_modules/benchmark/benchmark.js"></script>
9+
<script src="ng_benchmark.js"></script>
10+
<script src="app.js"></script>
11+
12+
</head>
13+
<body>
14+
15+
<h1>
16+
Benchmark: impact of event delegation
17+
</h1>
18+
19+
How to run:
20+
<ul>
21+
<li>For most stable results, run this in Chrome with the following command line option:
22+
<pre>--js-flags="--expose-gc"</pre>
23+
</li>
24+
</ul>
25+
26+
How to read the results:
27+
<ul>
28+
<li>The benchmark measures how long it takes to instantiate a given number of directives</li>
29+
<li>ngClick is compared against ngShow and text interpolation as baseline. The results show
30+
how expensive ngClick is compared to other very simple directives that touch the DOM.
31+
</li>
32+
<li>To measure the impact of jqLite.on vs element.addEventListener there is also a benchmark
33+
that as a modified version of ngClick that uses element.addEventListener.
34+
</li>
35+
<li>The delegate event directive is compared against a noop directive with a compile and link function and the case with no directives.
36+
The result shows how expensive it is to add a link function to a directive, as the delegate event directive has none.
37+
</li>
38+
</ul>
39+
40+
Results as of 7/31/2014:
41+
<ul>
42+
<li>ngClick is very close to ngShow and text interpolation, especially when looking at a version of ngClick that does not use jqLite.on but element.addEventListener instead.</li>
43+
<li>A delegate event directive that has no link function has the same speed as a directive with link function. I.e. ngClick is slower compared to the delegate event directive only because ngClick touches
44+
the DOM for every element</li>
45+
<li>A delegate event directive could be about 2x faster than ngClick. However, the overall performance
46+
benefit depends on how many (and which) other directives are used on the same element
47+
and what other things are part of the measures use case.
48+
E.g. rows of a table with ngRepeat that use ngClick will probably also contain text interpolation.
49+
</li>
50+
</ul>
51+
52+
Benchmark Options:
53+
<p>
54+
<label>
55+
Number of ngRepeats:
56+
<input type="number" ng-model="ctrl.ngRepeatCount">
57+
</label>
58+
<br>
59+
<label>
60+
Number of manual repeats inside the ngRepeat:
61+
<input type="number" ng-model="ctrl.manualRepeatCount">
62+
</label>
63+
</p>
64+
65+
<div ng-bench="ctrl.benchmarks"></div>
66+
67+
</body>
68+
69+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Benchmarks:
2+
<table>
3+
<thead>
4+
<td>Name</td>
5+
<td>State</td>
6+
<td>Result</td>
7+
</thead>
8+
<tbody>
9+
<tr ng-repeat="bench in benchmarks">
10+
<td>
11+
<label><input type="checkbox" ng-model="bench.active">{{bench.title}}
12+
</label>
13+
</td>
14+
<td>{{bench.state}}</td>
15+
<td>{{bench.lastResult}}</td>
16+
</tr>
17+
<tbody>
18+
</table>
19+
20+
<div>
21+
<button ng-click="ngBenchCtrl.toggleAll()">Toggle all</button>
22+
<button ng-click="ngBenchCtrl.run()">Run</button>
23+
<button ng-click="ngBenchCtrl.runOnce()">Debug once</button>
24+
</div>
25+
26+
Benchmark work area:
27+
<div class="work" style="height:20px; overflow: auto"></div>
+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
(function() {
2+
3+
var ngBenchmarkTemplateUrl = getCurrentScript().replace('.js', '.html');
4+
5+
angular.module('ngBench', []).directive('ngBench', function() {
6+
return {
7+
scope: {
8+
'benchmarks': '=ngBench'
9+
},
10+
templateUrl: ngBenchmarkTemplateUrl,
11+
controllerAs: 'ngBenchCtrl',
12+
controller: ['$scope', '$element', NgBenchController]
13+
};
14+
});
15+
16+
function NgBenchController($scope, $element) {
17+
var container = $element[0].querySelector('.work');
18+
19+
this.toggleAll = function() {
20+
var newState = !$scope.benchmarks[0].active;
21+
$scope.benchmarks.forEach(function(benchmark) {
22+
benchmark.active = newState;
23+
});
24+
};
25+
26+
this.run = function() {
27+
var suite = new Benchmark.Suite();
28+
$scope.benchmarks.forEach(function(benchmark) {
29+
var options = {
30+
'model': benchmark,
31+
'onStart': function() {
32+
benchmark.state = 'running';
33+
$scope.$digest();
34+
},
35+
'setup': function() {
36+
window.gc && window.gc();
37+
},
38+
'onComplete': function(event) {
39+
benchmark.state = '';
40+
if (this.error) {
41+
benchmark.lastResult = this.error.stack;
42+
} else {
43+
benchmark.lastResult = benchResultToString(this);
44+
}
45+
$scope.$digest();
46+
},
47+
delegate: createBenchmarkFn(benchmark.factory)
48+
};
49+
benchmark.state = '';
50+
if (benchmark.active) {
51+
benchmark.state = 'waiting';
52+
suite.add(benchmark.title, 'this.delegate()', options);
53+
}
54+
});
55+
suite.run({'async': true});
56+
};
57+
58+
this.runOnce = function() {
59+
window.setTimeout(function() {
60+
$scope.benchmarks.forEach(function(benchmark) {
61+
benchmark.state = '';
62+
if (benchmark.active) {
63+
try {
64+
createBenchmarkFn(benchmark.factory)();
65+
benchmark.lastResult = '';
66+
} catch (e) {
67+
benchmark.lastResult = e.message;
68+
}
69+
}
70+
});
71+
$scope.$digest();
72+
});
73+
};
74+
75+
function createBenchmarkFn(factory) {
76+
var instance = factory();
77+
return function() {
78+
container.innerHTML = '';
79+
instance(container);
80+
}
81+
}
82+
}
83+
84+
// See benchmark.js, toStringBench,
85+
// but without showing the name
86+
function benchResultToString(bench) {
87+
var me = bench,
88+
hz = me.hz,
89+
stats = me.stats,
90+
size = stats.sample.length;
91+
92+
return Benchmark.formatNumber(hz.toFixed(hz < 100 ? 2 : 0)) + ' ops/sec +/-' +
93+
stats.rme.toFixed(2) + '% (' + size + ' run' + (size == 1 ? '' : 's') + ' sampled)';
94+
}
95+
96+
function getCurrentScript() {
97+
var script = document.currentScript;
98+
if (!script) {
99+
var scripts = document.getElementsByTagName('script');
100+
script = scripts[scripts.length - 1];
101+
}
102+
return script.src;
103+
}
104+
})();

0 commit comments

Comments
 (0)