diff --git a/package.json b/package.json
index e5ee518a..6c8cc185 100644
--- a/package.json
+++ b/package.json
@@ -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": [
{
diff --git a/perf/apps/event-delegation/app.js b/perf/apps/event-delegation/app.js
new file mode 100755
index 00000000..aeb48684
--- /dev/null
+++ b/perf/apps/event-delegation/app.js
@@ -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 = '
';
+ for (var i=0; i'+options.text+' ';
+ }
+ templateHtml += '
';
+
+ var compiledTemplate = $compile(templateHtml);
+ var rows = [];
+ for (var i=0; i
+
+
+
+ Event delegation
+
+
+
+
+
+
+
+
+
+
+Benchmark: impact of event delegation
+
+
+How to run:
+
+ For most stable results, run this in Chrome with the following command line option:
+ --js-flags="--expose-gc"
+
+
+
+How to read the results:
+
+ The benchmark measures how long it takes to instantiate a given number of directives
+ 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.
+
+ 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.
+
+ 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.
+
+
+
+Results as of 7/31/2014:
+
+ 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.
+ 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
+ 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.
+
+
+
+Benchmark Options:
+
+
+ Number of ngRepeats:
+
+
+
+
+ Number of manual repeats inside the ngRepeat:
+
+
+
+
+
+
+
+
+
diff --git a/perf/apps/event-delegation/ng_benchmark.html b/perf/apps/event-delegation/ng_benchmark.html
new file mode 100644
index 00000000..25469be2
--- /dev/null
+++ b/perf/apps/event-delegation/ng_benchmark.html
@@ -0,0 +1,27 @@
+Benchmarks:
+
+
+
+ Toggle all
+ Run
+ Debug once
+
+
+Benchmark work area:
+
\ No newline at end of file
diff --git a/perf/apps/event-delegation/ng_benchmark.js b/perf/apps/event-delegation/ng_benchmark.js
new file mode 100644
index 00000000..72e6c6b4
--- /dev/null
+++ b/perf/apps/event-delegation/ng_benchmark.js
@@ -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;
+ }
+})();