diff --git a/benchmark/pubspec.lock b/benchmark/pubspec.lock index cdf0619ed..d29fe4deb 100644 --- a/benchmark/pubspec.lock +++ b/benchmark/pubspec.lock @@ -70,7 +70,7 @@ packages: route_hierarchical: description: route_hierarchical source: hosted - version: "0.4.20" + version: "0.4.21" source_maps: description: source_maps source: hosted diff --git a/benchmark/pubspec.yaml b/benchmark/pubspec.yaml index ca22114fd..141c3f449 100644 --- a/benchmark/pubspec.yaml +++ b/benchmark/pubspec.yaml @@ -1,4 +1,4 @@ -name: angular-perf +name: angularperf version: 0.10.0 description: Angular benchmarks dependencies: @@ -8,3 +8,8 @@ dev_dependencies: benchmark_harness: '>=1.0.0' unittest: '>=0.10.1 <0.12.0' mock: '>=0.10.0 <0.12.0' +transformers: +- angular +- $dart2js: + minify: false + checked: false diff --git a/benchmark/web/bp.js b/benchmark/web/bp.js new file mode 100644 index 000000000..c81c20366 --- /dev/null +++ b/benchmark/web/bp.js @@ -0,0 +1,136 @@ +window.benchmarkSteps = []; + +window.addEventListener('DOMContentLoaded', function() { + var container = document.querySelector('#benchmarkContainer'); + + // Add links to everything + var linkDiv = document.createElement('div'); + linkDiv.style['margin-bottom'] = "1.5em"; + var linkHtml = [ + '', + 'Benchmark Versions: ' + ].join('\n'); + + [ + // Add new benchmark suites here + ['tree.html', 'TreeComponent'] + ].forEach((function (link) { + linkHtml += [ + '', + link[1], + '' + ].join(''); + })); + + linkDiv.innerHTML = linkHtml; + container.appendChild(linkDiv); + + + // Benchmark runner + var btn = document.createElement('button'); + btn.innerText = "Loop"; + var running = false; + btn.addEventListener('click', loopBenchmark); + + container.appendChild(btn); + + function loopBenchmark() { + if (running) { + btn.innerText = "Loop"; + running = false; + } else { + window.requestAnimationFrame(function() { + btn.innerText = "Pause"; + running = true; + var loopB = function() { + if (running) { + window.requestAnimationFrame(function() { + if (running) runBenchmarkSteps(loopB); + }); + } + }; + loopB(); + }); + } + } + + + var onceBtn = document.createElement('button'); + onceBtn.innerText = "Once"; + onceBtn.addEventListener('click', function() { + window.requestAnimationFrame(function() { + onceBtn.innerText = "..."; + window.requestAnimationFrame(function() { + runBenchmarkSteps(function() { + onceBtn.innerText = "Once"; + }); + }); + }); + }); + container.appendChild(onceBtn); + + var infoDiv = document.createElement('div'); + infoDiv.style['font-family'] = 'monospace'; + container.appendChild(infoDiv); + + + var numMilliseconds; + var performance = window.performance; + if (performance != null && typeof performance.now == "function") { + numMilliseconds = function numMillisecondsWPN() { + return performance.now(); + } + } else if (performance != null && typeof performance.webkitNow == "function") { + numMilliseconds = function numMillisecondsWebkit() { + return performance.webkitNow(); + } + } else { + console.log('using Date.now'); + numMilliseconds = function numMillisecondsDateNow() { + return Date.now(); + }; + } + + function runBenchmarkSteps(done) { + // Run all the steps; + var times = {}; + window.benchmarkSteps.forEach(function(bs) { + var startTime = numMilliseconds(); + bs.fn(); + times[bs.name] = numMilliseconds() - startTime; + }); + calcStats(times); + + done(); + } + + var timesPerAction = {}; + + var NUM_SAMPLES = 10; + function calcStats(times) { + var iH = ''; + window.benchmarkSteps.forEach(function(bs) { + var tpa = timesPerAction[bs.name]; + if (!tpa) { + tpa = timesPerAction[bs.name] = { + times: [], // circular buffer + fmtTimes: [], + nextEntry: 0 + } + } + tpa.fmtTimes[tpa.nextEntry] = ('' + times[bs.name]).substr(0,6); + tpa.times[tpa.nextEntry++] = times[bs.name]; + tpa.nextEntry %= NUM_SAMPLES; + var avg = 0; + tpa.times.forEach(function(x) { avg += x; }); + avg /= Math.min(NUM_SAMPLES, tpa.times.length); + avg = ('' + avg).substr(0,6); + iH += '
' + (' ' + bs.name).slice(-10).replace(/ /g, ' ') + ': avg-' + NUM_SAMPLES + ':' + avg + 'ms [' + tpa.fmtTimes.join(', ') + ']ms
'; + }); + infoDiv.innerHTML = iH; + } +}); diff --git a/benchmark/web/tree.dart b/benchmark/web/tree.dart new file mode 100644 index 000000000..cc3975311 --- /dev/null +++ b/benchmark/web/tree.dart @@ -0,0 +1,290 @@ +import 'package:di/di.dart'; +import 'package:angular/angular.dart'; +import 'package:angular/core_dom/module_internal.dart'; +import 'package:angular/application_factory.dart'; + +import 'dart:html'; +import 'dart:math'; +import 'dart:js' as js; + +@Component( + selector: 'tree', + template: ' {{ctrl.data.value}}' + '' + '' + '', + publishAs: 'ctrl') +class TreeComponent { + @NgOneWay('data') + var data; +} + + +// This is a baseline implementation of TreeComponent. +// It assumes the data never changes and simply throws elements on the DOM +@Component( + selector: 'ng-free-tree', + template: '' + ) +class NgFreeTree implements ShadowRootAware { + var _data; + + @NgOneWay('data') + set data(v) { + _data = v; + if (sroot != null) + updateElement(sroot, _data); + } + + ShadowRoot sroot; + + void onShadowRoot(root) { + sroot = root; + if (_data != null) updateElement(sroot, _data); + } + + Element newFreeTree(tree) { + var elt = new Element.tag('ng-fre-tree'); + var root = elt.createShadowRoot(); + + var s = new SpanElement(); + root.append(s); + var value = tree['value']; + if (value != null) { + s.text = " $value"; + } + if (tree.containsKey('right')) { + s.append(new SpanElement() + ..append(newFreeTree(tree['right']))); + } + if (tree.containsKey('left')) { + s.append(new SpanElement() + ..append(newFreeTree(tree['left']))); + } + return elt; + } + + updateElement(root, tree) { + // Not quite acurate + root.innerHtml = ''; + root.append(newFreeTree(tree)); + } +} + +/** + * A baseline version of TreeComponent which uses Angular's Scope to + * manage data. This version is setting up data binding so arbitrary + * elements in the tree can change. + * + * Note that removing subtrees is not implemented as that feature + * is never exercised in the benchmark. + */ +@Component( + selector: 'ng-free-tree-scoped', + template: '' + ) +class NgFreeTreeScoped implements ShadowRootAware { + var _data; + + @NgOneWay('data') + set data(v) { + _data = v; + if (sroot != null) + updateElement(sroot, _data); + } + + ShadowRoot sroot; + Scope scope; + NgFreeTreeScoped(Scope this.scope); + + void onShadowRoot(root) { + sroot = root; + if (_data != null) updateElement(sroot, _data); + } + + Element newFreeTree(parentScope, treeExpr) { + var elt = new Element.tag('ng-fre-tree'); + var root = elt.createShadowRoot(); + var scope = parentScope.createChild({}); + + parentScope.watch(treeExpr, (v, _) { + scope.context['tree'] = v; + }); + + var s = new SpanElement(); + root.append(s); + scope.watch('tree.value', (v, _) { + if (v != null) { + s.text = " $v"; + } + }); + + scope.watch('tree.right != null', (v, _) { + if (v != true) return; + s.append(new SpanElement() + ..append(newFreeTree(scope, 'tree.right'))); + }); + + scope.watch('tree.left != null', (v, _) { + if (v != true) return; + s.append(new SpanElement() + ..append(newFreeTree(scope, 'tree.left'))); + }); + + return elt; + } + + Scope treeScope; + updateElement(root, tree) { + // Not quite acurate + if (treeScope != null) { + treeScope.destroy(); + } + treeScope = scope.createChild({}); + treeScope.context['tree'] = tree; + root.innerHtml = ''; + root.append(newFreeTree(treeScope, 'tree')); + } +} + + +/** + * A scope-backed baseline that data-binds through a Dart object. + * This is the pattern that we are using in Components. + * + * The benchmark does not show this approach as any slower than + * binding to the model directly. + */ +class FreeTreeClass { + // One-way bound + var tree; + Scope parentScope; + + FreeTreeClass(this.parentScope, treeExpr) { + parentScope.watch(treeExpr, (v, _) { + tree = v; + }); + } + + Element element() { + var elt = new Element.tag('ng-fre-tree'); + var root = elt.createShadowRoot(); + var scope = parentScope.createChild(this); + + var s = new SpanElement(); + root.append(s); + scope.watch('tree.value', (v, _) { + if (v != null) { + s.text = " $v"; + } + }); + + scope.watch('tree.right != null', (v, _) { + if (v != true) return; + s.append(new SpanElement() + ..append(new FreeTreeClass(scope, 'tree.right').element())); + }); + + scope.watch('tree.left != null', (v, _) { + if (v != true) return; + s.append(new SpanElement() + ..append(new FreeTreeClass(scope, 'tree.left').element())); + }); + + return elt; + } +} + +@Component( + selector: 'ng-free-tree-class', + template: '' + ) +class NgFreeTreeClass implements ShadowRootAware { + var _data; + + @NgOneWay('data') + set data(v) { + _data = v; + if (sroot != null) + updateElement(sroot, _data); + } + + ShadowRoot sroot; + Scope scope; + NgFreeTreeClass(Scope this.scope); + + void onShadowRoot(root) { + sroot = root; + if (_data != null) updateElement(sroot, _data); + } + + + var treeScope; + updateElement(root, tree) { + // Not quite acurate + if (treeScope != null) { + treeScope.destroy(); + } + treeScope = scope.createChild({}); + treeScope.context['tree'] = tree; + root.innerHtml = ''; + root.append(new FreeTreeClass(treeScope, 'tree').element()); + } +} + + +// Main function runs the benchmark. +main() { + var cleanup, createDom; + + var module = new Module() + ..type(TreeComponent) + ..type(NgFreeTree) + ..type(NgFreeTreeScoped) + ..type(NgFreeTreeClass) + ..factory(ScopeDigestTTL, (i) => new ScopeDigestTTL.value(15)); + + var injector = applicationFactory().addModule(module).run(); + assert(injector != null); + VmTurnZone zone = injector.get(VmTurnZone); + Scope scope = injector.get(Scope); + + scope.context['initData'] = { + "value": "top", + "right": { + "value": "right" + }, + "left": { + "value": "left" + } + }; + + buildTree(maxDepth, values, curDepth) { + if (maxDepth == curDepth) return {}; + return { + "value": values[curDepth], + "right": buildTree(maxDepth, values, curDepth+1), + "left": buildTree(maxDepth, values, curDepth+1) + + }; + } + cleanup = (_) => zone.run(() { + scope.context['initData'] = {}; + }); + + var count = 0; + createDom = (_) => zone.run(() { + var maxDepth = 9; + 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', '-']; + scope.context['initData'] = buildTree(maxDepth, values, 0); + }); + + js.context['benchmarkSteps'].add(new js.JsObject.jsify({ + "name": "cleanup", "fn": new js.JsFunction.withThis(cleanup) + })); + js.context['benchmarkSteps'].add(new js.JsObject.jsify({ + "name": "createDom", "fn": new js.JsFunction.withThis(createDom) + })); +} diff --git a/benchmark/web/tree.html b/benchmark/web/tree.html new file mode 100644 index 000000000..f8839c187 --- /dev/null +++ b/benchmark/web/tree.html @@ -0,0 +1,32 @@ + + + TreeComponent Benchmark + + + + + + + +
+

+ Render a 2^9 prefix tree with AngularDart +

+ +
Default:
+
Baseline:
+
Baseline + scope:
+
Baseline + class:
+ + + + + + + + + +
{{tree}}
+ + +