diff --git a/benchmark/watch_group_perf.dart b/benchmark/watch_group_perf.dart index 4eb9c065f..58776480c 100644 --- a/benchmark/watch_group_perf.dart +++ b/benchmark/watch_group_perf.dart @@ -51,7 +51,7 @@ class _CollectionCheck extends BenchmarkBase { _fieldRead() { var watchGrp = new RootWatchGroup(_dynamicFieldGetterFactory, - new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), new _Obj()) + new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), new _Obj(), null) ..watch(_parse('a'), _reactionFn) ..watch(_parse('b'), _reactionFn) ..watch(_parse('c'), _reactionFn) @@ -80,7 +80,7 @@ _fieldRead() { _fieldReadGetter() { var watchGrp= new RootWatchGroup(_staticFieldGetterFactory, - new DirtyCheckingChangeDetector(_staticFieldGetterFactory), new _Obj()) + new DirtyCheckingChangeDetector(_staticFieldGetterFactory), new _Obj(), null) ..watch(_parse('a'), _reactionFn) ..watch(_parse('b'), _reactionFn) ..watch(_parse('c'), _reactionFn) @@ -114,7 +114,7 @@ _mapRead() { 'k': 0, 'l': 1, 'm': 2, 'n': 3, 'o': 4, 'p': 0, 'q': 1, 'r': 2, 's': 3, 't': 4}; var watchGrp = new RootWatchGroup(_dynamicFieldGetterFactory, - new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), map) + new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), map, null) ..watch(_parse('a'), _reactionFn) ..watch(_parse('b'), _reactionFn) ..watch(_parse('c'), _reactionFn) @@ -144,7 +144,7 @@ _methodInvoke0() { var context = new _Obj(); context.a = new _Obj(); var watchGrp = new RootWatchGroup(_dynamicFieldGetterFactory, - new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), context) + new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), context, null) ..watch(_method('a', 'methodA'), _reactionFn) ..watch(_method('a', 'methodB'), _reactionFn) ..watch(_method('a', 'methodC'), _reactionFn) @@ -174,7 +174,7 @@ _methodInvoke1() { var context = new _Obj(); context.a = new _Obj(); var watchGrp = new RootWatchGroup(_dynamicFieldGetterFactory, - new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), context) + new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), context, null) ..watch(_method('a', 'methodA1', [_parse('a')]), _reactionFn) ..watch(_method('a', 'methodB1', [_parse('a')]), _reactionFn) ..watch(_method('a', 'methodC1', [_parse('a')]), _reactionFn) @@ -203,7 +203,7 @@ _methodInvoke1() { _function2() { var context = new _Obj(); var watchGrp = new RootWatchGroup(_dynamicFieldGetterFactory, - new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), context) + new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), context, null) ..watch(_add(0, _parse('a'), _parse('a')), _reactionFn) ..watch(_add(1, _parse('a'), _parse('a')), _reactionFn) ..watch(_add(2, _parse('a'), _parse('a')), _reactionFn) diff --git a/example/web/todo.dart b/example/web/todo.dart index b4c1c7bbb..57e92aada 100644 --- a/example/web/todo.dart +++ b/example/web/todo.dart @@ -96,7 +96,9 @@ main() { print(window.location.search); var module = new Module() ..bind(Todo) - ..bind(PlaybackHttpBackendConfig); + ..bind(PlaybackHttpBackendConfig) + ..bind(ExecutionStats, toFactory: (i) => new ExecutionStats(15, i.get(ExecutionStatsEmitter))) + ..bind(ExecutionStatsEmitter); // If these is a query in the URL, use the server-backed // TodoController. Otherwise, use the stored-data controller. diff --git a/lib/application.dart b/lib/application.dart index 2f80a8e4d..705f2db17 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -162,8 +162,7 @@ abstract class Application { } Injector run() { - publishToJavaScript(); - return zone.run(() { + var injector = zone.run(() { var rootElements = [element]; Injector injector = createInjector(); ExceptionHandler exceptionHandler = injector.getByKey(EXCEPTION_HANDLER_KEY); @@ -178,6 +177,8 @@ abstract class Application { }); return injector; }); + publishToJavaScript(injector); + return injector; } /** diff --git a/lib/change_detection/change_detection.dart b/lib/change_detection/change_detection.dart index 5919f1cc2..10773d476 100644 --- a/lib/change_detection/change_detection.dart +++ b/lib/change_detection/change_detection.dart @@ -1,5 +1,7 @@ library change_detection; +import 'package:angular/change_detection/execution_stats.dart'; + typedef void EvalExceptionHandler(error, stack); /** @@ -54,6 +56,7 @@ abstract class ChangeDetector extends ChangeDetectorGroup { * same order as they were registered. */ Iterator> collectChanges({EvalExceptionHandler exceptionHandler, + ExecutionStats executionStats, Stopwatch executionStopwatch, AvgStopwatch stopwatch }); } diff --git a/lib/change_detection/dirty_checking_change_detector.dart b/lib/change_detection/dirty_checking_change_detector.dart index d59ebb7e3..1ed73afd6 100644 --- a/lib/change_detection/dirty_checking_change_detector.dart +++ b/lib/change_detection/dirty_checking_change_detector.dart @@ -2,6 +2,7 @@ library dirty_checking_change_detector; import 'dart:collection'; import 'package:angular/change_detection/change_detection.dart'; +import 'package:angular/change_detection/execution_stats.dart'; /** * [DirtyCheckingChangeDetector] determines which object properties have changed @@ -304,6 +305,8 @@ class DirtyCheckingChangeDetector extends DirtyCheckingChangeDetectorGroup } Iterator> collectChanges({EvalExceptionHandler exceptionHandler, + ExecutionStats executionStats, + Stopwatch executionStopwatch, AvgStopwatch stopwatch}) { if (stopwatch != null) stopwatch.start(); DirtyCheckingRecord changeTail = _fakeHead; @@ -312,7 +315,19 @@ class DirtyCheckingChangeDetector extends DirtyCheckingChangeDetectorGroup int count = 0; while (current != null) { try { - if (current.check()) changeTail = changeTail._nextChange = current; + if (executionStats != null && executionStats.config.enabled) { + executionStopwatch.reset(); + executionStopwatch.start(); + } + var check = current.check(); + if (executionStats != null && executionStats.config.enabled) { + executionStopwatch.stop(); + if (executionStopwatch.elapsedMicroseconds >= executionStats.config.threshold) { + executionStats.addDirtyCheckEntry(new ExecutionEntry( + executionStopwatch.elapsedMicroseconds, current)); + } + } + if (check) changeTail = changeTail._nextChange = current; count++; } catch (e, s) { if (exceptionHandler == null) { diff --git a/lib/change_detection/execution_stats.dart b/lib/change_detection/execution_stats.dart new file mode 100644 index 000000000..dee336686 --- /dev/null +++ b/lib/change_detection/execution_stats.dart @@ -0,0 +1,143 @@ +library angular.change_detection.execution_stats; + +import 'package:angular/core/annotation_src.dart'; + +@Injectable() +class ExecutionStats { + final ExecutionStatsConfig config; + final ExecutionStatsEmitter emitter; + List _dirtyCheckStats; + List _dirtyWatchStats; + List _evalStats; + int _evalsCount = 0; + int _dirtyWatchCount = 0; + int _dirtyCheckCount = 0; + + int get _capacity => config.maxEntries; + + ExecutionStats(this.emitter, this.config) { + reset(); + } + + void addDirtyCheckEntry(ExecutionEntry entry) { + if( ++_dirtyCheckCount >= _capacity) _shrinkDirtyCheck(); + _dirtyCheckStats[_dirtyCheckCount] = entry; + } + + void addDirtyWatchEntry(ExecutionEntry entry) { + if( ++_dirtyWatchCount >= _capacity) _shrinkDirtyWatch(); + _dirtyWatchStats[_dirtyWatchCount] = entry; + } + + void addEvalEntry(ExecutionEntry entry) { + if( ++_evalsCount >= _capacity) _shrinkEval(); + _evalStats[_evalsCount] = entry; + } + + void showEvalStats() { + emitter.showEvalStats(this); + } + + void showReactionFnStats() { + emitter.showReactionFnStats(this); + } + + void showDirtyCheckStats() { + emitter.showDirtyCheckStats(this); + } + + Iterable get dirtyCheckStats { + _shrinkDirtyWatch(); + return _dirtyCheckStats.getRange(0, _capacity).where((e) => e.time > 0); + } + + Iterable get evalStats { + _shrinkDirtyWatch(); + return _evalStats.getRange(0, _capacity).where((e) => e.time > 0); + } + + Iterable get reactionFnStats { + _shrinkDirtyWatch(); + return _dirtyWatchStats.getRange(0, _capacity).where((e) => e.time > 0); + } + + void enable() { + config.enabled = true; + } + + void disable() { + config.enabled = false; + } + + void reset() { + _dirtyCheckStats = new List.filled(3 * _capacity, new ExecutionEntry(0, null)); + _dirtyWatchStats = new List.filled(3 * _capacity, new ExecutionEntry(0, null)); + _evalStats = new List.filled(3 * _capacity, new ExecutionEntry(0, null)); + _evalsCount = 0; + _dirtyWatchCount = 0; + _dirtyCheckCount = 0; + } + + void _shrinkDirtyCheck() { + _dirtyCheckStats.sort((ExecutionEntry x, ExecutionEntry y) => y.time.compareTo(x.time)); + for(int i = _capacity; i < 3 * _capacity; i++) _dirtyCheckStats[i] = new ExecutionEntry(0, null); + _dirtyCheckCount = _capacity; + } + + void _shrinkDirtyWatch() { + _dirtyWatchStats.sort((ExecutionEntry x, ExecutionEntry y) => x.time.compareTo(y.time) * -1); + for(int i = _capacity; i < 3 * _capacity; i++) _dirtyWatchStats[i] = new ExecutionEntry(0, null); + _dirtyWatchCount = _capacity; + } + + void _shrinkEval() { + _evalStats.sort((ExecutionEntry x, ExecutionEntry y) => x.time.compareTo(y.time) * -1); + for(int i = _capacity; i < 3 * _capacity; i++) _evalStats[i] = new ExecutionEntry(0, null); + _evalsCount = _capacity; + } +} + +@Injectable() +class ExecutionStatsEmitter { + void showDirtyCheckStats(ExecutionStats fnStats) { + _printLine('Time (us)', 'Field'); + fnStats.dirtyCheckStats.forEach((ExecutionEntry entry) => + _printLine('${entry.time}', '${entry.value}')); + } + + void showEvalStats(ExecutionStats fnStats) { + _printLine('Time (us)', 'Name'); + fnStats.evalStats.forEach((ExecutionEntry entry) => + _printLine('${entry.time}', '${entry.value}')); + } + + void showReactionFnStats(ExecutionStats fnStats) { + _printLine('Time (us)', 'Expression'); + fnStats.reactionFnStats.forEach((ExecutionEntry entry) => + _printLine('${entry.time}', '${entry.value}')); + } + + _printLine(String first, String second) { + var timesColLength = 10; + var expressionsColPrefix = 5; + var timesCol = ' ' * (timesColLength - first.length); + var expressionsCol = ' ' * expressionsColPrefix; + print('${timesCol + first}${expressionsCol + second}'); + } + +} + +class ExecutionEntry { + final num time; + final dynamic value; //Record or Watch + + ExecutionEntry(this.time, this.value); +} + +class ExecutionStatsConfig { + bool enabled; + int threshold; + int maxEntries; + + ExecutionStatsConfig({this.enabled: false, this.threshold, this.maxEntries: 15}); +} \ No newline at end of file diff --git a/lib/change_detection/watch_group.dart b/lib/change_detection/watch_group.dart index f257260d5..704f9485f 100644 --- a/lib/change_detection/watch_group.dart +++ b/lib/change_detection/watch_group.dart @@ -1,7 +1,8 @@ library angular.watch_group; -import 'package:angular/change_detection/change_detection.dart'; import 'dart:collection'; +import 'package:angular/change_detection/change_detection.dart'; +import 'package:angular/change_detection/execution_stats.dart'; part 'linked_list.dart'; part 'ast.dart'; @@ -367,6 +368,8 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { class RootWatchGroup extends WatchGroup { final FieldGetterFactory _fieldGetterFactory; Watch _dirtyWatchHead, _dirtyWatchTail; + Stopwatch executionStopwatch; + final ExecutionStats executionStats; /** * Every time a [WatchGroup] is destroyed we increment the counter. During @@ -378,10 +381,10 @@ class RootWatchGroup extends WatchGroup { int _removeCount = 0; - RootWatchGroup(this._fieldGetterFactory, - ChangeDetector changeDetector, - Object context) - : super._root(changeDetector, context); + RootWatchGroup(this._fieldGetterFactory, ChangeDetector changeDetector, Object context, + this.executionStats) : super._root(changeDetector, context) { + if (executionStats != null) executionStopwatch = new Stopwatch(); + } RootWatchGroup get _rootGroup => this; @@ -405,6 +408,8 @@ class RootWatchGroup extends WatchGroup { Iterator> changedRecordIterator = (_changeDetector as ChangeDetector<_Handler>).collectChanges( exceptionHandler:exceptionHandler, + executionStats: executionStats, + executionStopwatch: executionStopwatch, stopwatch: fieldStopwatch); if (processStopwatch != null) processStopwatch.start(); while (changedRecordIterator.moveNext()) { @@ -423,7 +428,19 @@ class RootWatchGroup extends WatchGroup { while (evalRecord != null) { try { if (evalStopwatch != null) evalCount++; - if (evalRecord.check() && changeLog != null) { + if (executionStats != null && executionStats.config.enabled) { + executionStopwatch.reset(); + executionStopwatch.start(); + } + var evalCheck = evalRecord.check(); + if (executionStats != null && executionStats.config.enabled) { + executionStopwatch.stop(); + if (executionStopwatch.elapsedMicroseconds >= executionStats.config.threshold) { + executionStats.addEvalEntry(new ExecutionEntry( + executionStopwatch.elapsedMicroseconds, evalRecord)); + } + } + if (evalCheck && changeLog != null) { changeLog(evalRecord.handler.expression, evalRecord.currentValue, evalRecord.previousValue); @@ -448,7 +465,18 @@ class RootWatchGroup extends WatchGroup { count++; try { if (root._removeCount == 0 || dirtyWatch._watchGroup.isAttached) { + if (executionStats != null && executionStats.config.enabled) { + executionStopwatch.reset(); + executionStopwatch.start(); + } dirtyWatch.invoke(); + if (executionStats != null && executionStats.config.enabled) { + executionStopwatch.stop(); + if (executionStopwatch.elapsedMicroseconds >= executionStats.config.threshold) { + executionStats.addDirtyWatchEntry(new ExecutionEntry( + executionStopwatch.elapsedMicroseconds, dirtyWatch)); + } + } } } catch (e, s) { if (exceptionHandler == null) rethrow; else exceptionHandler(e, s); @@ -516,6 +544,10 @@ class Watch { _WatchList._remove(handler, this); handler.release(); } + + String toString() { + return '${_watchGroup.id}:${expression}'; + } } /** diff --git a/lib/core/module.dart b/lib/core/module.dart index ba7a5c243..205c7b9b8 100644 --- a/lib/core/module.dart +++ b/lib/core/module.dart @@ -12,6 +12,9 @@ library angular.core; export "package:angular/change_detection/watch_group.dart" show ReactionFn; +export 'package:angular/change_detection/execution_stats.dart' show + ExecutionStats, ExecutionStatsEmitter, ExecutionStatsConfig; + export "package:angular/core/parser/parser.dart" show Parser; diff --git a/lib/core/module_internal.dart b/lib/core/module_internal.dart index 04ed84507..97ddb6ccc 100644 --- a/lib/core/module_internal.dart +++ b/lib/core/module_internal.dart @@ -18,6 +18,8 @@ export 'package:angular/change_detection/watch_group.dart'; import 'package:angular/change_detection/ast_parser.dart'; import 'package:angular/change_detection/change_detection.dart'; import 'package:angular/change_detection/dirty_checking_change_detector.dart'; +import 'package:angular/change_detection/execution_stats.dart'; +export 'package:angular/change_detection/execution_stats.dart'; import 'package:angular/core/formatter.dart'; export 'package:angular/core/formatter.dart'; import 'package:angular/core/parser/utils.dart'; @@ -43,6 +45,7 @@ class CoreModule extends Module { bind(RootScope); bind(Scope, toFactory: (injector) => injector.getByKey(ROOT_SCOPE_KEY)); bind(ClosureMap, toFactory: (_) => throw "Must provide dynamic/static ClosureMap."); + bind(ExecutionStats, toValue: null); bind(ScopeStats); bind(ScopeStatsEmitter); bind(ScopeStatsConfig, toFactory: (i) => new ScopeStatsConfig()); diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 9a875d8c2..a29c33db9 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -315,8 +315,8 @@ class Scope { var child = new Scope(childContext, rootScope, this, _readWriteGroup.newGroup(childContext), _readOnlyGroup.newGroup(childContext), - '$id:${_childScopeNextId++}', - _stats); + '$id:${_childScopeNextId++}', + _stats); var prev = _childTail; child._prev = prev; @@ -618,17 +618,17 @@ class RootScope extends Scope { RootScope(Object context, Parser parser, ASTParser astParser, FieldGetterFactory fieldGetterFactory, FormatterMap formatters, this._exceptionHandler, this._ttl, this._zone, - ScopeStats _scopeStats) - : _scopeStats = _scopeStats, + ScopeStats scopeStats, ExecutionStats execStats) + : _scopeStats = scopeStats, _parser = parser, _astParser = astParser, super(context, null, null, new RootWatchGroup(fieldGetterFactory, - new DirtyCheckingChangeDetector(fieldGetterFactory), context), + new DirtyCheckingChangeDetector(fieldGetterFactory), context, execStats), new RootWatchGroup(fieldGetterFactory, - new DirtyCheckingChangeDetector(fieldGetterFactory), context), + new DirtyCheckingChangeDetector(fieldGetterFactory), context, execStats), '', - _scopeStats) + scopeStats) { _zone.onTurnDone = apply; _zone.onError = (e, s, ls) => _exceptionHandler(e, s); diff --git a/lib/introspection_js.dart b/lib/introspection_js.dart index b3b99f867..07b0a0c86 100644 --- a/lib/introspection_js.dart +++ b/lib/introspection_js.dart @@ -16,7 +16,7 @@ import 'package:angular/core_dom/module_internal.dart'; */ var elementExpando = new Expando('element'); -void publishToJavaScript() { +void publishToJavaScript(Injector rootInjector) { js.context ..['ngProbe'] = new js.JsFunction.withThis((_, nodeOrSelector) => _jsProbe(ngProbe(nodeOrSelector))) @@ -26,7 +26,8 @@ void publishToJavaScript() { _jsScope(ngScope(nodeOrSelector), ngProbe(nodeOrSelector).injector.getByKey(SCOPE_STATS_CONFIG_KEY))) ..['ngQuery'] = new js.JsFunction.withThis((_, dom.Node node, String selector, - [String containsText]) => new js.JsArray.from(ngQuery(node, selector, containsText))); + [String containsText]) => new js.JsArray.from(ngQuery(node, selector, containsText))) + ..['ngStats'] = new js.JsFunction.withThis((_) => _jsReactionFnStats(rootInjector.get(ExecutionStats))); } js.JsObject _jsProbe(ElementProbe probe) { @@ -59,4 +60,15 @@ js.JsObject _jsScope(Scope scope, ScopeStatsConfig config) { })..['_dart_'] = scope; } +js.JsObject _jsReactionFnStats(ExecutionStats fnStats) { + return new js.JsObject.jsify({ + "showDirtyCheckStats": fnStats.showDirtyCheckStats, + "showEvalStats": fnStats.showEvalStats, + "showReactionFnStats": fnStats.showReactionFnStats, + "enable": fnStats.enable, + "disable": fnStats.disable, + "reset": fnStats.reset, + })..['_dart_'] = fnStats; +} + _jsDirective(directive) => directive; diff --git a/test/angular_spec.dart b/test/angular_spec.dart index d4b90bd47..e283a69d1 100644 --- a/test/angular_spec.dart +++ b/test/angular_spec.dart @@ -94,6 +94,9 @@ main() { var ALLOWED_NAMES = [ "angular.app.AngularModule", "angular.app.Application", + "angular.change_detection.execution_stats.ExecutionStats", + "angular.change_detection.execution_stats.ExecutionStatsConfig", + "angular.change_detection.execution_stats.ExecutionStatsEmitter", "angular.core.annotation.ShadowRootAware", "angular.core.annotation_src.AttachAware", "angular.core.annotation_src.Component", diff --git a/test/change_detection/watch_group_spec.dart b/test/change_detection/watch_group_spec.dart index ba5dcdfbd..8af5b1da7 100644 --- a/test/change_detection/watch_group_spec.dart +++ b/test/change_detection/watch_group_spec.dart @@ -27,7 +27,7 @@ void main() { context = {}; var getterFactory = new DynamicFieldGetterFactory(); changeDetector = new DirtyCheckingChangeDetector(getterFactory); - watchGrp = new RootWatchGroup(getterFactory, changeDetector, context); + watchGrp = new RootWatchGroup(getterFactory, changeDetector, context, null); logger = _logger; parser = _parser; astParser = _astParser; @@ -68,7 +68,7 @@ void main() { context = {}; var getterFactory = new DynamicFieldGetterFactory(); changeDetector = new DirtyCheckingChangeDetector(getterFactory); - watchGrp = new RootWatchGroup(getterFactory, changeDetector, context); + watchGrp = new RootWatchGroup(getterFactory, changeDetector, context, null); logger = _logger; })); @@ -957,6 +957,61 @@ void main() { }); }); + describe('profile', () { + var getterFactory, execStats; + + beforeEach(() { + getterFactory = new DynamicFieldGetterFactory(); + changeDetector = new DirtyCheckingChangeDetector(getterFactory); + execStats = null; + context['a'] = 'hello'; + }); + + it('should allow profiling iff ExecutionStats object is provided', () { + ExecutionStats execStats = new ExecutionStats(new ExecutionStatsEmitter(), + new ExecutionStatsConfig(threshold: 0, enabled: true)); + RootWatchGroup watchGrp = new RootWatchGroup(getterFactory, changeDetector, context, execStats); + + List log = []; + watchGrp.watch(parse('a'), (v, p) => log.add(v)); + watchGrp.detectChanges(); + + expect(log.length).toBe(1); + expect(log.first).toEqual('hello'); + expect(execStats.reactionFnStats.length).toBe(1); + }); + + it('should allow one to enable, disable and reset stats', () { + ExecutionStats execStats = new ExecutionStats(new ExecutionStatsEmitter(), + new ExecutionStatsConfig(threshold: 0, enabled: true)); + RootWatchGroup watchGrp = new RootWatchGroup(getterFactory, changeDetector, context, execStats); + + watchGrp.watch(parse('a'), (v, p) {}); + watchGrp.detectChanges(); + expect(execStats.reactionFnStats.length).toBe(1); + + execStats.disable(); + context['a'] = 'hola'; + watchGrp.detectChanges(); + expect(execStats.reactionFnStats.length).toBe(1); + + execStats.enable(); + + context['a'] = 'ola'; + watchGrp.detectChanges(); + expect(execStats.reactionFnStats.length).toBe(2); + + execStats.reset(); + expect(execStats.reactionFnStats.length).toBe(0); + }); + + it('should have executionStats set to null iff ExecutionStats object is null', () { + RootWatchGroup watchGrp = new RootWatchGroup(getterFactory, changeDetector, context, null); + expect(watchGrp.executionStats).toBeNull(); + expect(watchGrp.executionStopwatch).toBeNull(); + }); + }); + }); }