From 854b5b7da85ae25d22979b47e63a54eade45f8a7 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Tue, 21 Jul 2015 15:28:10 -0700 Subject: [PATCH] feat(benchmark): add static_tree benchmark Static binary component tree of depth 10, i.e. 1024 components. Current numbers for `pureScriptTime` are: JavaScript: Baseline: 27.10+-9% Ng2: 26.84+-8% Ng1: 55.30+-14% Dart: Baseline: 30.13+-4% Ng2: 45.94+-3% Ng1: 128.88+-10% I.e. in JS we are same speed as baseline right now! Some background: We had a recent change in the compiler that merges components into their parents already during compilation (#2529). This made Ng2 2x faster in this benchmark (before the Ng2 JS time was 49.59+-14%ms). Closes #3196 --- modules/benchmarks/e2e_test/static_tree.dart | 5 + modules/benchmarks/e2e_test/static_tree.ts | 54 +++ modules/benchmarks/src/index.html | 3 + .../src/static_tree/tree_benchmark.html | 45 +++ .../src/static_tree/tree_benchmark.ts | 316 ++++++++++++++++++ .../e2e_test/static_tree.dart | 3 + .../e2e_test/static_tree.ts | 15 + modules/benchmarks_external/src/index.html | 3 + .../src/static_tree/tree_benchmark.dart | 164 +++++++++ .../src/static_tree/tree_benchmark.html | 17 + .../src/static_tree/tree_benchmark.ts | 70 ++++ tools/broccoli/trees/browser_tree.ts | 2 + 12 files changed, 697 insertions(+) create mode 100644 modules/benchmarks/e2e_test/static_tree.dart create mode 100644 modules/benchmarks/e2e_test/static_tree.ts create mode 100644 modules/benchmarks/src/static_tree/tree_benchmark.html create mode 100644 modules/benchmarks/src/static_tree/tree_benchmark.ts create mode 100644 modules/benchmarks_external/e2e_test/static_tree.dart create mode 100644 modules/benchmarks_external/e2e_test/static_tree.ts create mode 100644 modules/benchmarks_external/src/static_tree/tree_benchmark.dart create mode 100644 modules/benchmarks_external/src/static_tree/tree_benchmark.html create mode 100644 modules/benchmarks_external/src/static_tree/tree_benchmark.ts diff --git a/modules/benchmarks/e2e_test/static_tree.dart b/modules/benchmarks/e2e_test/static_tree.dart new file mode 100644 index 0000000000..03b9f81738 --- /dev/null +++ b/modules/benchmarks/e2e_test/static_tree.dart @@ -0,0 +1,5 @@ +library benchmarks.e2e_test.static_tree_perf; + +main() { + +} diff --git a/modules/benchmarks/e2e_test/static_tree.ts b/modules/benchmarks/e2e_test/static_tree.ts new file mode 100644 index 0000000000..20adaa2208 --- /dev/null +++ b/modules/benchmarks/e2e_test/static_tree.ts @@ -0,0 +1,54 @@ +import {runClickBenchmark, verifyNoBrowserErrors} from 'angular2/src/test_lib/perf_util'; + +describe('ng2 static tree benchmark', function() { + + var URL = 'benchmarks/src/static_tree/tree_benchmark.html'; + + afterEach(verifyNoBrowserErrors); + + it('should log the ng stats with viewcache', function(done) { + runClickBenchmark({ + url: URL, + buttons: ['#ng2DestroyDom', '#ng2CreateDom'], + id: 'ng2.static.tree.create.viewcache', + params: [{name: 'viewcache', value: 'true'}] + }).then(done, done.fail); + }); + + it('should log the ng stats without viewcache', function(done) { + runClickBenchmark({ + url: URL, + buttons: ['#ng2DestroyDom', '#ng2CreateDom'], + id: 'ng2.static.tree.create.plain', + params: [{name: 'viewcache', value: 'false'}] + }).then(done, done.fail); + }); + + it('should log the ng stats (update)', function(done) { + runClickBenchmark({ + url: URL, + buttons: ['#ng2CreateDom'], + id: 'ng2.static.tree.update', + params: [{name: 'viewcache', value: 'true'}] + }).then(done, done.fail); + }); + + it('should log the baseline stats', function(done) { + runClickBenchmark({ + url: URL, + buttons: ['#baselineDestroyDom', '#baselineCreateDom'], + id: 'baseline.tree.create', + params: [{name: 'depth', value: 9, scale: 'log2'}] + }).then(done, done.fail); + }); + + it('should log the baseline stats (update)', function(done) { + runClickBenchmark({ + url: URL, + buttons: ['#baselineCreateDom'], + id: 'baseline.tree.update', + params: [{name: 'depth', value: 9, scale: 'log2'}] + }).then(done, done.fail); + }); + +}); diff --git a/modules/benchmarks/src/index.html b/modules/benchmarks/src/index.html index 74df5f249b..d0fe3d1877 100644 --- a/modules/benchmarks/src/index.html +++ b/modules/benchmarks/src/index.html @@ -20,6 +20,9 @@
  • Tree benchmark
  • +
  • + Static tree benchmark +
  • Naive infinite scroll benchmark
  • diff --git a/modules/benchmarks/src/static_tree/tree_benchmark.html b/modules/benchmarks/src/static_tree/tree_benchmark.html new file mode 100644 index 0000000000..2c8e2cbaf8 --- /dev/null +++ b/modules/benchmarks/src/static_tree/tree_benchmark.html @@ -0,0 +1,45 @@ + + + + +

    Params

    +
    +
    + Use Viewcache: + + +
    + +
    + +

    Angular2 static tree benchmark (depth 10)

    +

    + + + + +

    + +

    Baseline static tree benchmark (depth 10)

    +

    + + + + +

    + +
    + +
    + +
    + +
    + +$SCRIPTS$ + + diff --git a/modules/benchmarks/src/static_tree/tree_benchmark.ts b/modules/benchmarks/src/static_tree/tree_benchmark.ts new file mode 100644 index 0000000000..5584453222 --- /dev/null +++ b/modules/benchmarks/src/static_tree/tree_benchmark.ts @@ -0,0 +1,316 @@ +import {bootstrap, Compiler, Component, Directive, View, ViewContainerRef} from 'angular2/angular2'; + +import {LifeCycle} from 'angular2/src/core/life_cycle/life_cycle'; +import {reflector} from 'angular2/src/reflection/reflection'; +import {ReflectionCapabilities} from 'angular2/src/reflection/reflection_capabilities'; +import {DOM} from 'angular2/src/dom/dom_adapter'; +import {List} from 'angular2/src/facade/collection'; +import {window, document, gc} from 'angular2/src/facade/browser'; +import { + getIntParameter, + getStringParameter, + bindAction, + windowProfile, + windowProfileEnd +} from 'angular2/src/test_lib/benchmark_util'; +import {NgIf} from 'angular2/directives'; +import {BrowserDomAdapter} from 'angular2/src/dom/browser_adapter'; +import {APP_VIEW_POOL_CAPACITY} from 'angular2/src/core/compiler/view_pool'; +import {bind, Binding} from 'angular2/di'; + +function createBindings(): List { + var viewCacheCapacity = getStringParameter('viewcache') == 'true' ? 10000 : 0; + return [bind(APP_VIEW_POOL_CAPACITY).toValue(viewCacheCapacity)]; +} + +function setupReflector() { + reflector.reflectionCapabilities = new ReflectionCapabilities(); +} + +const MAX_DEPTH = 10; + +export function main() { + BrowserDomAdapter.makeCurrent(); + + setupReflector(); + + var app; + var lifeCycle; + var baselineRootTreeComponent; + var count = 0; + + function profile(create, destroy, name) { + return function() { + windowProfile(name + ' w GC'); + var duration = 0; + var count = 0; + while (count++ < 150) { + gc(); + var start = window.performance.now(); + create(); + duration += window.performance.now() - start; + destroy(); + } + windowProfileEnd(name + ' w GC'); + window.console.log(`Iterations: ${count}; time: ${duration / count} ms / iteration`); + + windowProfile(name + ' w/o GC'); + duration = 0; + count = 0; + while (count++ < 150) { + var start = window.performance.now(); + create(); + duration += window.performance.now() - start; + destroy(); + } + windowProfileEnd(name + ' w/o GC'); + window.console.log(`Iterations: ${count}; time: ${duration / count} ms / iteration`); + }; + } + + function noop() {} + + function createData(): TreeNode { + var values = count++ % 2 == 0 ? ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*'] : + ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', '-']; + return buildTree(MAX_DEPTH, values, 0); + } + + function ng2DestroyDom() { + app.initData = null; + lifeCycle.tick(); + } + + function ng2CreateDom() { + app.initData = createData(); + lifeCycle.tick(); + } + + function initNg2() { + bootstrap(AppComponentWithStaticTree, createBindings()) + .then((ref) => { + var injector = ref.injector; + lifeCycle = injector.get(LifeCycle); + + app = ref.hostComponent; + bindAction('#ng2DestroyDom', ng2DestroyDom); + bindAction('#ng2CreateDom', ng2CreateDom); + bindAction('#ng2UpdateDomProfile', profile(ng2CreateDom, noop, 'ng2-update')); + bindAction('#ng2CreateDomProfile', profile(ng2CreateDom, ng2DestroyDom, 'ng2-create')); + }); + } + + function baselineDestroyDom() { baselineRootTreeComponent.update(null); } + + function baselineCreateDom() { baselineRootTreeComponent.update(createData()); } + + function initBaseline() { + var tree = DOM.createElement('tree'); + DOM.appendChild(DOM.querySelector(document, 'baseline'), tree); + baselineRootTreeComponent = new BaselineAppComponent(tree, MAX_DEPTH); + + bindAction('#baselineDestroyDom', baselineDestroyDom); + bindAction('#baselineCreateDom', baselineCreateDom); + + bindAction('#baselineUpdateDomProfile', profile(baselineCreateDom, noop, 'baseline-update')); + bindAction('#baselineCreateDomProfile', + profile(baselineCreateDom, baselineDestroyDom, 'baseline-create')); + } + + initNg2(); + initBaseline(); +} + +class TreeNode { + value: string; + left: TreeNode; + right: TreeNode; + constructor(value, left, right) { + this.value = value; + this.left = left; + this.right = right; + } +} + +function buildTree(maxDepth, values, curDepth) { + if (maxDepth === curDepth) return new TreeNode('', null, null); + return new TreeNode(values[curDepth], buildTree(maxDepth, values, curDepth + 1), + buildTree(maxDepth, values, curDepth + 1)); +} + +// http://jsperf.com/nextsibling-vs-childnodes + +class BaselineAppComponent { + tree: BaseLineTreeComponent = null; + constructor(public element, public depth: number) {} + update(value: TreeNode) { + if (value === null) { + this.tree = null; + DOM.clearNodes(this.element); + } else { + if (this.tree === null) { + this.tree = new BaseLineTreeComponent(this.element, this.depth); + } + this.tree.update(value); + } + } +} + +var BASELINE_TREE_TEMPLATE = null; +class BaseLineTreeComponent { + static getTemplate() { + if (BASELINE_TREE_TEMPLATE === null) { + BASELINE_TREE_TEMPLATE = DOM.createTemplate('_'); + } + return BASELINE_TREE_TEMPLATE; + } + + value: BaseLineInterpolation; + left: BaseLineTreeComponent; + right: BaseLineTreeComponent; + terminal: boolean; + + constructor(public element, remainingDepth: number) { + var clone = DOM.firstChild(DOM.importIntoDoc(BaseLineTreeComponent.getTemplate().content)); + DOM.appendChild(this.element, clone); + var child = clone.firstChild; + this.value = new BaseLineInterpolation(child); + this.terminal = remainingDepth === 0; + if (!this.terminal) { + child = DOM.nextSibling(child); + this.left = new BaseLineTreeComponent(child, remainingDepth - 1); + child = DOM.nextSibling(child); + this.right = new BaseLineTreeComponent(child, remainingDepth - 1); + } + } + update(value: TreeNode) { + this.value.update(value.value); + if (!this.terminal) { + this.left.update(value.left); + this.right.update(value.right); + } + } +} + +class BaseLineInterpolation { + value: string; + textNode; + constructor(textNode) { + this.value = null; + this.textNode = textNode; + } + update(value: string) { + if (this.value !== value) { + this.value = value; + DOM.setText(this.textNode, value + ' '); + } + } +} + +class StaticTreeComponentBase { + _value: TreeNode; + constructor() { this.data = null; } + set data(value: TreeNode) { + // TODO: We need an initial value as otherwise the getter for data.value will fail + // --> this should be already caught in change detection! + value = value !== null ? value : new TreeNode('', null, null); + this._value = value; + } + get data() { return this._value; } +} + +@Component({selector: 'tree', properties: ['data']}) +@View({directives: [], template: '{{data.value}} '}) +class StaticTreeComponent0 extends StaticTreeComponentBase { +} + +@Component({selector: 'tree', properties: ['data']}) +@View({ + directives: [StaticTreeComponent0], + template: + ` {{data.value}} ` +}) +class StaticTreeComponent1 extends StaticTreeComponentBase { +} + +@Component({selector: 'tree', properties: ['data']}) +@View({ + directives: [StaticTreeComponent1], + template: + ` {{data.value}} ` +}) +class StaticTreeComponent2 extends StaticTreeComponentBase { + data: TreeNode; +} + +@Component({selector: 'tree', properties: ['data']}) +@View({ + directives: [StaticTreeComponent2], + template: + ` {{data.value}} ` +}) +class StaticTreeComponent3 extends StaticTreeComponentBase { +} + +@Component({selector: 'tree', properties: ['data']}) +@View({ + directives: [StaticTreeComponent3], + template: + ` {{data.value}} ` +}) +class StaticTreeComponent4 extends StaticTreeComponentBase { +} + +@Component({selector: 'tree', properties: ['data']}) +@View({ + directives: [StaticTreeComponent4], + template: + ` {{data.value}} ` +}) +class StaticTreeComponent5 extends StaticTreeComponentBase { +} + +@Component({selector: 'tree', properties: ['data']}) +@View({ + directives: [StaticTreeComponent5], + template: + ` {{data.value}} ` +}) +class StaticTreeComponent6 extends StaticTreeComponentBase { +} + +@Component({selector: 'tree', properties: ['data']}) +@View({ + directives: [StaticTreeComponent6], + template: + ` {{data.value}} ` +}) +class StaticTreeComponent7 extends StaticTreeComponentBase { +} + +@Component({selector: 'tree', properties: ['data']}) +@View({ + directives: [StaticTreeComponent7], + template: + ` {{data.value}} ` +}) +class StaticTreeComponent8 extends StaticTreeComponentBase { +} + +@Component({selector: 'tree', properties: ['data']}) +@View({ + directives: [StaticTreeComponent8], + template: + ` {{data.value}} ` +}) +class StaticTreeComponent9 extends StaticTreeComponentBase { +} + +@Component({selector: 'app'}) +@View({ + directives: [StaticTreeComponent9, NgIf], + template: `` +}) +class AppComponentWithStaticTree { + initData: TreeNode; +} diff --git a/modules/benchmarks_external/e2e_test/static_tree.dart b/modules/benchmarks_external/e2e_test/static_tree.dart new file mode 100644 index 0000000000..8868f131ba --- /dev/null +++ b/modules/benchmarks_external/e2e_test/static_tree.dart @@ -0,0 +1,3 @@ +library benchmarks_external.e2e_test.static_tree_perf; + +main() {} diff --git a/modules/benchmarks_external/e2e_test/static_tree.ts b/modules/benchmarks_external/e2e_test/static_tree.ts new file mode 100644 index 0000000000..08c4adf316 --- /dev/null +++ b/modules/benchmarks_external/e2e_test/static_tree.ts @@ -0,0 +1,15 @@ +import {runClickBenchmark, verifyNoBrowserErrors} from 'angular2/src/test_lib/perf_util'; + +describe('ng1.x tree benchmark', function() { + + var URL = 'benchmarks_external/src/static_tree/tree_benchmark.html'; + + afterEach(verifyNoBrowserErrors); + + it('should log the stats', function(done) { + runClickBenchmark( + {url: URL, buttons: ['#destroyDom', '#createDom'], id: 'ng1.static.tree', params: []}) + .then(done, done.fail); + }); + +}); diff --git a/modules/benchmarks_external/src/index.html b/modules/benchmarks_external/src/index.html index 5899411802..d9c74f9c2e 100644 --- a/modules/benchmarks_external/src/index.html +++ b/modules/benchmarks_external/src/index.html @@ -8,6 +8,9 @@
  • Tree benchmark
  • +
  • + Static tree benchmark +
  • Polymer Tree benchmark
  • diff --git a/modules/benchmarks_external/src/static_tree/tree_benchmark.dart b/modules/benchmarks_external/src/static_tree/tree_benchmark.dart new file mode 100644 index 0000000000..aa13adad95 --- /dev/null +++ b/modules/benchmarks_external/src/static_tree/tree_benchmark.dart @@ -0,0 +1,164 @@ +// static tree benchmark in AngularDart 1.x +library static_tree_benchmark_ng10; + +import 'package:angular/angular.dart'; +import 'package:angular/application_factory.dart'; +import 'package:angular2/src/test_lib/benchmark_util.dart'; + +setup() { + var m = new Module() + ..bind(CompilerConfig, + toValue: new CompilerConfig.withOptions(elementProbeEnabled: false)) + ..bind(ScopeDigestTTL, + toFactory: () => new ScopeDigestTTL.value(15), inject: []) + ..bind(TreeComponent0) + ..bind(TreeComponent1) + ..bind(TreeComponent2) + ..bind(TreeComponent3) + ..bind(TreeComponent4) + ..bind(TreeComponent5) + ..bind(TreeComponent6) + ..bind(TreeComponent7) + ..bind(TreeComponent8) + ..bind(TreeComponent9); + + final injector = applicationFactory().addModule(m).run(); + + return injector; +} + +const MAX_DEPTH = 10; + +main() { + final injector = setup(); + final zone = injector.get(VmTurnZone); + final rootScope = injector.get(Scope); + rootScope.context['initData'] = null; + var count = 0; + + TreeNode createData() { + var values = count++ % 2 == 0 + ? ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*'] + : ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', '-']; + return buildTree(MAX_DEPTH, values, 0); + } + + destroyDom() { + zone.run(() { + rootScope.context['initData'] = null; + }); + } + + createDom() { + zone.run(() { + rootScope.context['initData'] = createData(); + }); + } + + bindAction('#destroyDom', destroyDom); + bindAction('#createDom', createDom); +} + +@Component( + selector: 'tree0', + map: const {'data': '=>data'}, + template: ' {{data.value}} ' +) +class TreeComponent0 { + var data; +} + +@Component( + selector: 'tree1', + map: const {'data': '=>data'}, + template: ' {{data.value}} ' +) +class TreeComponent1 { + var data; +} + +@Component( + selector: 'tree2', + map: const {'data': '=>data'}, + template: ' {{data.value}} ' +) +class TreeComponent2 { + var data; +} + +@Component( + selector: 'tree3', + map: const {'data': '=>data'}, + template: ' {{data.value}} ' +) +class TreeComponent3 { + var data; +} + +@Component( + selector: 'tree4', + map: const {'data': '=>data'}, + template: ' {{data.value}} ' +) +class TreeComponent4 { + var data; +} + +@Component( + selector: 'tree5', + map: const {'data': '=>data'}, + template: ' {{data.value}} ' +) +class TreeComponent5 { + var data; +} + +@Component( + selector: 'tree6', + map: const {'data': '=>data'}, + template: ' {{data.value}} ' +) +class TreeComponent6 { + var data; +} + +@Component( + selector: 'tree7', + map: const {'data': '=>data'}, + template: ' {{data.value}} ' +) +class TreeComponent7 { + var data; +} + +@Component( + selector: 'tree8', + map: const {'data': '=>data'}, + template: ' {{data.value}} ' +) +class TreeComponent8 { + var data; +} + +@Component( + selector: 'tree9', + map: const {'data': '=>data'}, + template: ' {{data.value}} ' +) +class TreeComponent9 { + var data; +} + +buildTree(maxDepth, values, curDepth) { + if (maxDepth == curDepth) return new TreeNode(''); + return new TreeNode(values[curDepth], + buildTree(maxDepth, values, curDepth + 1), + buildTree(maxDepth, values, curDepth + 1)); +} + +class TreeNode { + var value; + TreeNode left; + TreeNode right; + TreeNode([this.value, this.left, this.right]); +} diff --git a/modules/benchmarks_external/src/static_tree/tree_benchmark.html b/modules/benchmarks_external/src/static_tree/tree_benchmark.html new file mode 100644 index 0000000000..8cb216a072 --- /dev/null +++ b/modules/benchmarks_external/src/static_tree/tree_benchmark.html @@ -0,0 +1,17 @@ + + + + +

    AngularJS/Dart 1.x static tree benchmark (depth 10)

    +

    + + +

    + +
    + +
    + +$SCRIPTS$ + + diff --git a/modules/benchmarks_external/src/static_tree/tree_benchmark.ts b/modules/benchmarks_external/src/static_tree/tree_benchmark.ts new file mode 100644 index 0000000000..1507208a10 --- /dev/null +++ b/modules/benchmarks_external/src/static_tree/tree_benchmark.ts @@ -0,0 +1,70 @@ +// static tree benchmark in AngularJS 1.x +import {getIntParameter, bindAction} from 'angular2/src/test_lib/benchmark_util'; +import angular = require("angular"); + +const MAX_DEPTH = 10; + +export function main() { + angular.bootstrap(document.querySelector('.app'), ['app']); +} + +function addTreeDirective(module, level: number) { + var template; + if (level <= 0) { + template = ` {{data.value}}` + } else { + template = + ` {{data.value}} `; + } + module.directive(`tree${level}`, function() { return {scope: {data: '='}, template: template}; }); +} + +var module = angular.module('app', []); +for (var depth = 0; depth < MAX_DEPTH; depth++) { + addTreeDirective(module, depth); +} +module.config([ + '$compileProvider', + function($compileProvider) { $compileProvider.debugInfoEnabled(false); } + ]) + .run([ + '$rootScope', + function($rootScope) { + var count = 0; + $rootScope.initData = null; + + bindAction('#destroyDom', destroyDom); + bindAction('#createDom', createDom); + + function createData(): TreeNode { + var values = count++ % 2 == 0 ? ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*'] : + ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', '-']; + return buildTree(MAX_DEPTH, values, 0); + } + + function destroyDom() { + $rootScope.$apply(function() { $rootScope.initData = null; }); + } + + function createDom() { + $rootScope.$apply(function() { $rootScope.initData = createData(); }); + } + } + ]); + +class TreeNode { + value: string; + left: TreeNode; + right: TreeNode; + constructor(value, left, right) { + this.value = value; + this.left = left; + this.right = right; + } +} + +function buildTree(maxDepth, values, curDepth) { + if (maxDepth === curDepth) return new TreeNode('', null, null); + return new TreeNode(values[curDepth], buildTree(maxDepth, values, curDepth + 1), + buildTree(maxDepth, values, curDepth + 1)); +} diff --git a/tools/broccoli/trees/browser_tree.ts b/tools/broccoli/trees/browser_tree.ts index 298d05f707..d90a37b6aa 100644 --- a/tools/broccoli/trees/browser_tree.ts +++ b/tools/broccoli/trees/browser_tree.ts @@ -28,6 +28,7 @@ const kServedPaths = [ 'benchmarks/src/largetable', 'benchmarks/src/naive_infinite_scroll', 'benchmarks/src/tree', + 'benchmarks/src/static_tree', // Relative (to /modules) paths to external benchmark directories 'benchmarks_external/src', @@ -36,6 +37,7 @@ const kServedPaths = [ 'benchmarks_external/src/naive_infinite_scroll', 'benchmarks_external/src/tree', 'benchmarks_external/src/tree/react', + 'benchmarks_external/src/static_tree', // Relative (to /modules) paths to example directories 'examples/src/benchpress',