From 658f22a1ae628c5a9ce5ed6ff6052560c1b2ddb0 Mon Sep 17 00:00:00 2001 From: "deploy-test@travis-ci.org" Date: Thu, 12 Jun 2014 23:58:29 +1000 Subject: [PATCH] feat(WatchGroup): Allow measuring cost of running reaction functions and reporting it. To be able to measure cost of reaction function, dirty check and/or dirty watch one needs to define ExecutionStats. ExecutionStats is configured using ExecutionStatsConfig object. This object can be used to define how many records to display when showing the stats, what's the minimum threshold (in microseconds) for a record to be considered as relevant as well as to enable and disable tracking the stats. One can define ExecutionStats, ExectionStatsEmitter and ExecutionStatsConfig like: m..bind(ExectionStats) ..bind(ExectionStatsEmitter) ..bind(ExectionStatsConfig, toFactory: (i) => new ExectionStats(...)) The results can be displayed using ngStats object. To obtain a reference to an ngStats object type (in the console): ng = ngStats(); To display maxNumber (defined using ExectionStatsConfig object) of the most expensive reaction functions type: ng.showReactionFnStats(); To display maxNumber of most expensive evals type: ng.showEvalStats(); To display maxNumber of most expensive dirty checks type: ng.showDirtyCheckStats(); One can use ng.enable() method to enable tracking and ng.disable() method to disable trackin. To clear the results use ng.reset() method. To achieve least performance impact on the application make sure that the ExectionStats is null. m..bind(ExectionStats, toValue: null); That way no extra objects will be created and the impact on the actual application will be minimal. --- benchmark/watch_group_perf.dart | 12 +- example/web/todo.dart | 4 +- lib/application.dart | 5 +- lib/change_detection/change_detection.dart | 3 + .../dirty_checking_change_detector.dart | 17 ++- lib/change_detection/execution_stats.dart | 143 ++++++++++++++++++ lib/change_detection/watch_group.dart | 44 +++++- lib/core/module.dart | 3 + lib/core/module_internal.dart | 3 + lib/core/scope.dart | 14 +- lib/introspection_js.dart | 16 +- test/angular_spec.dart | 3 + test/change_detection/watch_group_spec.dart | 59 +++++++- 13 files changed, 299 insertions(+), 27 deletions(-) create mode 100644 lib/change_detection/execution_stats.dart 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(); + }); + }); + }); }