chore(perf): add event delegation benchmark

This commit is contained in:
Tobias Bosch
2014-07-31 06:26:10 -07:00
parent e98258184a
commit 52b77b6b89
5 changed files with 323 additions and 1 deletions

View File

@@ -53,7 +53,8 @@
"jshint-stylish": "~0.1.5",
"node-html-encoder": "0.0.2",
"sorted-object": "^1.0.0",
"qq": "^0.3.5"
"qq": "^0.3.5",
"benchmark": "1.x.x"
},
"licenses": [
{

121
perf/apps/event-delegation/app.js Executable file
View File

@@ -0,0 +1,121 @@
var app = angular.module('perf', ['ngBench'])
.directive('noopDir', function() {
return {
compile: function($element, $attrs) {
return function($scope, $element) {
return 1;
}
}
};
})
app.directive('nativeClick', ['$parse', function($parse) {
return {
compile: function($element, $attrs) {
var expr = $parse($attrs.tstEvent);
return function($scope, $element) {
$element[0].addEventListener('click', function() {
console.log('clicked');
}, false);
}
}
};
}])
.directive('dlgtClick', function() {
return {
compile: function($element, $attrs) {
var evt = $attrs.dlgtClick;
// We don't setup the global event listeners as the costs are small and one time only...
}
};
})
.controller('MainCtrl', ['$compile', '$rootScope', '$templateCache',
function($compile, $rootScope, $templateCache) {
// TODO: Make ngRepeatCount configurable via the UI!
var self = this;
this.ngRepeatCount = 20;
this.manualRepeatCount = 5;
this.benchmarks = [{
title: 'ng-click',
factory: function() {
return createBenchmark({
directive: 'ng-click="a()"'
});
},
active: true
},{
title: 'ng-click without jqLite',
factory: function() {
return createBenchmark({
directive: 'native-click="a()"'
});
},
active: true
},{
title: 'baseline: ng-show',
factory: function() {
return createBenchmark({
directive: 'ng-show="true"'
});
},
active: true
},{
title: 'baseline: text interpolation',
factory: function() {
return createBenchmark({
text: '{{row}}'
});
},
active: true
},{
title: 'delegate event directive (only compile)',
factory: function() {
return createBenchmark({
directive: 'dlgt-click="a()"'
});
},
active: true
},{
title: 'baseline: noop directive (compile and link)',
factory: function() {
return createBenchmark({
directive: 'noop-dir'
});
},
active: true
},{
title: 'baseline: no directive',
factory: function() {
return createBenchmark({});
},
active: true
}];
function createBenchmark(options) {
options.directive = options.directive || '';
options.text = options.text || '';
var templateHtml = '<div><span ng-repeat="row in rows">';
for (var i=0; i<self.manualRepeatCount; i++) {
templateHtml += '<span '+options.directive+'>'+options.text+'</span>';
}
templateHtml += '</span></div>';
var compiledTemplate = $compile(templateHtml);
var rows = [];
for (var i=0; i<self.ngRepeatCount; i++) {
rows.push('row'+i);
}
return function(container) {
var scope = $rootScope.$new();
try {
scope.rows = rows;
compiledTemplate(scope, function(clone) {
container.appendChild(clone[0]);
});
scope.$digest();
} finally {
scope.$destroy();
}
}
}
}])

View File

@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html ng-app="perf" ng-controller="MainCtrl as ctrl">
<head>
<meta charset="utf-8" />
<title>Event delegation</title>
<script src="../../../build/angular.js"></script>
<script src="../../../node_modules/benchmark/benchmark.js"></script>
<script src="ng_benchmark.js"></script>
<script src="app.js"></script>
</head>
<body>
<h1>
Benchmark: impact of event delegation
</h1>
How to run:
<ul>
<li>For most stable results, run this in Chrome with the following command line option:
<pre>--js-flags="--expose-gc"</pre>
</li>
</ul>
How to read the results:
<ul>
<li>The benchmark measures how long it takes to instantiate a given number of directives</li>
<li>ngClick is compared against ngShow and text interpolation as baseline. The results show
how expensive ngClick is compared to other very simple directives that touch the DOM.
</li>
<li>To measure the impact of jqLite.on vs element.addEventListener there is also a benchmark
that as a modified version of ngClick that uses element.addEventListener.
</li>
<li>The delegate event directive is compared against a noop directive with a compile and link function and the case with no directives.
The result shows how expensive it is to add a link function to a directive, as the delegate event directive has none.
</li>
</ul>
Results as of 7/31/2014:
<ul>
<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>
<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
the DOM for every element</li>
<li>A delegate event directive could be about 2x faster than ngClick. However, the overall performance
benefit depends on how many (and which) other directives are used on the same element
and what other things are part of the measures use case.
E.g. rows of a table with ngRepeat that use ngClick will probably also contain text interpolation.
</li>
</ul>
Benchmark Options:
<p>
<label>
Number of ngRepeats:
<input type="number" ng-model="ctrl.ngRepeatCount">
</label>
<br>
<label>
Number of manual repeats inside the ngRepeat:
<input type="number" ng-model="ctrl.manualRepeatCount">
</label>
</p>
<div ng-bench="ctrl.benchmarks"></div>
</body>
</html>

View File

@@ -0,0 +1,27 @@
Benchmarks:
<table>
<thead>
<td>Name</td>
<td>State</td>
<td>Result</td>
</thead>
<tbody>
<tr ng-repeat="bench in benchmarks">
<td>
<label><input type="checkbox" ng-model="bench.active">{{bench.title}}
</label>
</td>
<td>{{bench.state}}</td>
<td>{{bench.lastResult}}</td>
</tr>
<tbody>
</table>
<div>
<button ng-click="ngBenchCtrl.toggleAll()">Toggle all</button>
<button ng-click="ngBenchCtrl.run()">Run</button>
<button ng-click="ngBenchCtrl.runOnce()">Debug once</button>
</div>
Benchmark work area:
<div class="work" style="height:20px; overflow: auto"></div>

View File

@@ -0,0 +1,104 @@
(function() {
var ngBenchmarkTemplateUrl = getCurrentScript().replace('.js', '.html');
angular.module('ngBench', []).directive('ngBench', function() {
return {
scope: {
'benchmarks': '=ngBench'
},
templateUrl: ngBenchmarkTemplateUrl,
controllerAs: 'ngBenchCtrl',
controller: ['$scope', '$element', NgBenchController]
};
});
function NgBenchController($scope, $element) {
var container = $element[0].querySelector('.work');
this.toggleAll = function() {
var newState = !$scope.benchmarks[0].active;
$scope.benchmarks.forEach(function(benchmark) {
benchmark.active = newState;
});
};
this.run = function() {
var suite = new Benchmark.Suite();
$scope.benchmarks.forEach(function(benchmark) {
var options = {
'model': benchmark,
'onStart': function() {
benchmark.state = 'running';
$scope.$digest();
},
'setup': function() {
window.gc && window.gc();
},
'onComplete': function(event) {
benchmark.state = '';
if (this.error) {
benchmark.lastResult = this.error.stack;
} else {
benchmark.lastResult = benchResultToString(this);
}
$scope.$digest();
},
delegate: createBenchmarkFn(benchmark.factory)
};
benchmark.state = '';
if (benchmark.active) {
benchmark.state = 'waiting';
suite.add(benchmark.title, 'this.delegate()', options);
}
});
suite.run({'async': true});
};
this.runOnce = function() {
window.setTimeout(function() {
$scope.benchmarks.forEach(function(benchmark) {
benchmark.state = '';
if (benchmark.active) {
try {
createBenchmarkFn(benchmark.factory)();
benchmark.lastResult = '';
} catch (e) {
benchmark.lastResult = e.message;
}
}
});
$scope.$digest();
});
};
function createBenchmarkFn(factory) {
var instance = factory();
return function() {
container.innerHTML = '';
instance(container);
}
}
}
// See benchmark.js, toStringBench,
// but without showing the name
function benchResultToString(bench) {
var me = bench,
hz = me.hz,
stats = me.stats,
size = stats.sample.length;
return Benchmark.formatNumber(hz.toFixed(hz < 100 ? 2 : 0)) + ' ops/sec +/-' +
stats.rme.toFixed(2) + '% (' + size + ' run' + (size == 1 ? '' : 's') + ' sampled)';
}
function getCurrentScript() {
var script = document.currentScript;
if (!script) {
var scripts = document.getElementsByTagName('script');
script = scripts[scripts.length - 1];
}
return script.src;
}
})();