From 46597c8e9959b5cde75da7ad6483b7891896699a Mon Sep 17 00:00:00 2001 From: Marko Vuksanovic Date: Fri, 18 Apr 2014 16:34:32 +1000 Subject: [PATCH 1/2] feat(Zone): add onTurnStart to NgZone. OnTurnStart is executed at the beginning of every turn. Any microtasks scheduled in onTurnStart are executed before the ones scheduled in run. Closes #83 --- lib/core/zone.dart | 43 +++++++++++-- test/core/zone_spec.dart | 132 ++++++++++++++++++++++++++++++--------- 2 files changed, 141 insertions(+), 34 deletions(-) diff --git a/lib/core/zone.dart b/lib/core/zone.dart index d00669528..72d02c708 100644 --- a/lib/core/zone.dart +++ b/lib/core/zone.dart @@ -3,7 +3,12 @@ part of angular.core_internal; /** * Handles an [NgZone] onTurnDone event. */ -typedef void ZoneOnTurn(); +typedef void ZoneOnTurnDone(); + +/** + * Handles an [NgZone] onTurnDone event. + */ +typedef void ZoneOnTurnStart(); /** * Handles an [NgZone] onError event. @@ -69,14 +74,20 @@ class VmTurnZone { )); onError = _defaultOnError; onTurnDone = _defaultOnTurnDone; + onTurnStart = _defaultOnTurnStart; } List _asyncQueue = []; bool _errorThrownFromOnRun = false; + var _currentlyInTurn = false; _onRunBase(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn()) { _runningInTurn++; try { + if (!_currentlyInTurn) { + _currentlyInTurn = true; + delegate.run(zone, onTurnStart); + } return fn(); } catch (e, s) { onError(e, s, _longStacktrace); @@ -115,11 +126,18 @@ class VmTurnZone { // Two loops here: the inner one runs all queued microtasks, // the outer runs onTurnDone (e.g. scope.digest) and then // any microtasks which may have been queued from onTurnDone. + // If any microtasks were scheduled during onTurnDone, onTurnStart + // will be executed before those microtasks. do { + if (!_currentlyInTurn) { + _currentlyInTurn = true; + delegate.run(zone, onTurnStart); + } while (!_asyncQueue.isEmpty) { delegate.run(zone, _asyncQueue.removeAt(0)); } delegate.run(zone, onTurnDone); + _currentlyInTurn = false; } while (!_asyncQueue.isEmpty); } catch (e, s) { onError(e, s, _longStacktrace); @@ -141,18 +159,31 @@ class VmTurnZone { void _defaultOnError(dynamic e, dynamic s, LongStackTrace ls) => _outerZone.handleUncaughtError(e, s); + /** + * Called at the beginning of each VM turn in which inner zone code runs. + * "At the beginning" means before any of the microtasks from the private + * microtask queue of the inner zone is executed. Notes + * - [onTurnStart] runs repeatedly until no more microstasks are scheduled + * within [onTurnStart], [run] or [onTurnDone]. You usually don't want it to + * schedule any. For example, if its first line of code is `new Future.value()`, + * the turn will _never_ end. + */ + ZoneOnTurnStart onTurnStart; + void _defaultOnTurnStart() => null; + + /** * Called at the end of each VM turn in which inner zone code runs. * "At the end" means after the private microtask queue of the inner zone is * exhausted but before the next VM turn. Notes * - This won't wait for microtasks scheduled in zones other than the inner * zone, e.g. those scheduled with [runOutsideAngular]. - * - [onTurnDone] runs repeatedly until it fails to schedule any more - * microtasks, so you usually don't want it to schedule any. For example, - * if its first line of code is `new Future.value()`, the turn will _never_ - * end. + * - [onTurnDone] runs repeatedly until no more tasks are scheduled within + * [onTurnStart], [run] or [onTurnDone]. You usually don't want it to + * schedule any. For example, if its first line of code is `new Future.value()`, + * the turn will _never_ end. */ - ZoneOnTurn onTurnDone; + ZoneOnTurnDone onTurnDone; void _defaultOnTurnDone() => null; LongStackTrace _longStacktrace = null; diff --git a/test/core/zone_spec.dart b/test/core/zone_spec.dart index 715835291..9e33ae62d 100644 --- a/test/core/zone_spec.dart +++ b/test/core/zone_spec.dart @@ -18,6 +18,9 @@ void main() { zone.onTurnDone = () { log('onTurnDone'); }; + zone.onTurnStart = () { + log('onTurnStart'); + }; zone.onError = (e, s, ls) => eh(e, s); }); @@ -139,7 +142,7 @@ void main() { zone.run(() { log('run'); }); - expect(log.result()).toEqual('run; onTurnDone'); + expect(log.result()).toEqual('onTurnStart; run; onTurnDone'); }); @@ -148,43 +151,52 @@ void main() { }); - it('should call onTurnDone for a scheduleMicrotask in onTurnDone', async((Logger log) { + it('should call onTurnStart before executing a microtask scheduled in onTurnDone as well as ' + 'onTurnDone after executing the task', async((Logger log) { var ran = false; zone.onTurnDone = () { + log('onTurnDone(begin)'); if (!ran) { - scheduleMicrotask(() { ran = true; log('onTurnAsync'); }); + scheduleMicrotask(() { ran = true; log('executedMicrotask'); }); } - log('onTurnDone'); + log('onTurnDone(end)'); }; zone.run(() { log('run'); }); microLeap(); - expect(log.result()).toEqual('run; onTurnDone; onTurnAsync; onTurnDone'); + expect(log.result()).toEqual('onTurnStart; run; onTurnDone(begin); onTurnDone(end); onTurnStart; executedMicrotask; onTurnDone(begin); onTurnDone(end)'); })); - it('should call onTurnDone for a scheduleMicrotask in onTurnDone triggered by a scheduleMicrotask in run', async((Logger log) { + it('should call onTurnStart and onTurnDone for a scheduleMicrotask in onTurnDone triggered by a scheduleMicrotask in run', async((Logger log) { var ran = false; zone.onTurnDone = () { + log('onTurnDone(begin)'); if (!ran) { - scheduleMicrotask(() { ran = true; log('onTurnAsync'); }); + log('onTurnDone(scheduleMicrotask)'); + scheduleMicrotask(() { + ran = true; + log('onTurnDone(executeMicrotask)'); + }); } - log('onTurnDone'); + log('onTurnDone(end)'); }; zone.run(() { - scheduleMicrotask(() { log('scheduleMicrotask'); }); - log('run'); + log('scheduleMicrotask'); + scheduleMicrotask(() { + log('run(executeMicrotask)'); + }); }); microLeap(); - expect(log.result()).toEqual('run; scheduleMicrotask; onTurnDone; onTurnAsync; onTurnDone'); + expect(log.result()).toEqual('onTurnStart; scheduleMicrotask; run(executeMicrotask); onTurnDone(begin); onTurnDone(scheduleMicrotask); onTurnDone(end); onTurnStart; onTurnDone(executeMicrotask); onTurnDone(begin); onTurnDone(end)'); })); - it('should call onTurnDone once after a turn', async((Logger log) { + it('should call onTurnStart once before a turn and onTurnDone once after the turn', async((Logger log) { zone.run(() { log('run start'); scheduleMicrotask(() { @@ -194,18 +206,61 @@ void main() { }); microLeap(); - expect(log.result()).toEqual('run start; run end; async; onTurnDone'); + expect(log.result()).toEqual('onTurnStart; run start; run end; async; onTurnDone'); })); it('should work for Future.value as well', async((Logger log) { var futureRan = false; zone.onTurnDone = () { + log('onTurnDone(begin)'); if (!futureRan) { - new Future.value(null).then((_) { log('onTurn future'); }); + log('onTurnDone(scheduleFuture)'); + new Future.value(null).then((_) { log('onTurnDone(executeFuture)'); }); futureRan = true; } - log('onTurnDone'); + log('onTurnDone(end)'); + }; + + zone.run(() { + log('run start'); + new Future.value(null) + .then((_) { + log('future then'); + new Future.value(null) + .then((_) { log('future foo'); }); + return new Future.value(null); + }) + .then((_) { + log('future bar'); + }); + log('run end'); + }); + microLeap(); + + expect(log.result()).toEqual('onTurnStart; run start; run end; future then; future foo; future bar; onTurnDone(begin); onTurnDone(scheduleFuture); onTurnDone(end); onTurnStart; onTurnDone(executeFuture); onTurnDone(begin); onTurnDone(end)'); + })); + + it('should execute futures scheduled in onTurnStart before Futures scheduled in run', async((Logger log) { + var doneFutureRan = false; + var startFutureRan = false; + zone.onTurnStart = () { + log('onTurnStart(begin)'); + if (!startFutureRan) { + log('onTurnStart(scheduleFuture)'); + new Future.value(null).then((_) { log('onTurnStart(executeFuture)'); }); + startFutureRan = true; + } + log('onTurnStart(end)'); + }; + zone.onTurnDone = () { + log('onTurnDone(begin)'); + if (!doneFutureRan) { + log('onTurnDone(scheduleFuture)'); + new Future.value(null).then((_) { log('onTurnDone(executeFuture)'); }); + doneFutureRan = true; + } + log('onTurnDone(end)'); }; zone.run(() { @@ -214,21 +269,21 @@ void main() { .then((_) { log('future then'); new Future.value(null) - .then((_) { log('future ?'); }); + .then((_) { log('future foo'); }); return new Future.value(null); }) .then((_) { - log('future ?'); + log('future bar'); }); log('run end'); }); microLeap(); - expect(log.result()).toEqual('run start; run end; future then; future ?; future ?; onTurnDone; onTurn future; onTurnDone'); + expect(log.result()).toEqual('onTurnStart(begin); onTurnStart(scheduleFuture); onTurnStart(end); run start; run end; onTurnStart(executeFuture); future then; future foo; future bar; onTurnDone(begin); onTurnDone(scheduleFuture); onTurnDone(end); onTurnStart(begin); onTurnStart(end); onTurnDone(executeFuture); onTurnDone(begin); onTurnDone(end)'); })); - it('should call onTurnDone after each turn', async((Logger log) { + it('should call onTurnStart and onTurnDone before and after each turn, respectively', async((Logger log) { Completer a, b; zone.run(() { a = new Completer(); @@ -247,11 +302,11 @@ void main() { }); microLeap(); - expect(log.result()).toEqual('run start; onTurnDone; a then; onTurnDone; b then; onTurnDone'); + expect(log.result()).toEqual('onTurnStart; run start; onTurnDone; onTurnStart; a then; onTurnDone; onTurnStart; b then; onTurnDone'); })); - it('should call onTurnDone after each turn in a chain', async((Logger log) { + it('should call onTurnStart and onTurnDone before and after (respectively) all turns in a chain', async((Logger log) { zone.run(() { log('run start'); scheduleMicrotask(() { @@ -264,10 +319,10 @@ void main() { }); microLeap(); - expect(log.result()).toEqual('run start; run end; async1; async2; onTurnDone'); + expect(log.result()).toEqual('onTurnStart; run start; run end; async1; async2; onTurnDone'); })); - it('should call onTurnDone for futures created outside of run body', async((Logger log) { + it('should call onTurnStart and onTurnDone for futures created outside of run body', async((Logger log) { var future = new Future.value(4).then((x) => new Future.value(x)); zone.run(() { future.then((_) => log('future then')); @@ -275,7 +330,7 @@ void main() { }); microLeap(); - expect(log.result()).toEqual('zone run; onTurnDone; future then; onTurnDone'); + expect(log.result()).toEqual('onTurnStart; zone run; onTurnDone; onTurnStart; future then; onTurnDone'); })); @@ -286,7 +341,20 @@ void main() { throw 'zoneError'; })).toThrow('zoneError'); expect(() => zone.assertInTurn()).toThrow(); - expect(log.result()).toEqual('zone run; onError; onTurnDone'); + expect(log.result()).toEqual('onTurnStart; zone run; onError; onTurnDone'); + })); + + it('should call onTurnDone even if there was an exception in onTurnStart', async((Logger log) { + zone.onError = (e, s, l) => log('onError'); + zone.onTurnStart = (){ + log('onTurnStart'); + throw 'zoneError'; + }; + expect(() => zone.run(() { + log('zone run'); + })).toThrow('zoneError'); + expect(() => zone.assertInTurn()).toThrow(); + expect(log.result()).toEqual('onTurnStart; onError; onTurnDone'); })); @@ -303,11 +371,15 @@ void main() { microLeap(); expect(() => zone.assertInTurn()).toThrow(); - expect(log.result()).toEqual('zone run; scheduleMicrotask; onError; onTurnDone'); + expect(log.result()).toEqual('onTurnStart; zone run; scheduleMicrotask; onError; onTurnDone'); })); it('should support assertInZone', async(() { var calls = ''; + zone.onTurnStart = () { + zone.assertInZone(); + calls += 'start;'; + }; zone.onTurnDone = () { zone.assertInZone(); calls += 'done;'; @@ -322,7 +394,7 @@ void main() { }); microLeap(); - expect(calls).toEqual('sync;async;done;'); + expect(calls).toEqual('start;sync;async;done;'); })); it('should throw outside of the zone', () { @@ -335,6 +407,10 @@ void main() { it('should support assertInTurn', async(() { var calls = ''; + zone.onTurnStart = () { + zone.assertInTurn(); + calls += 'start;'; + }; zone.onTurnDone = () { calls += 'done;'; zone.assertInTurn(); @@ -349,7 +425,7 @@ void main() { }); microLeap(); - expect(calls).toEqual('sync;async;done;'); + expect(calls).toEqual('start;sync;async;done;'); })); From fd84c9e4e73b48c576055daa8ed9e9138d270d4b Mon Sep 17 00:00:00 2001 From: Marko Vuksanovic Date: Fri, 18 Apr 2014 17:47:51 +1000 Subject: [PATCH 2/2] docs(VmTurnZone): change documentation for VmTurnZone to use term VmTurnZone instead of old NgZone. --- lib/core/zone.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/core/zone.dart b/lib/core/zone.dart index 72d02c708..737e6de6c 100644 --- a/lib/core/zone.dart +++ b/lib/core/zone.dart @@ -1,17 +1,17 @@ part of angular.core_internal; /** - * Handles an [NgZone] onTurnDone event. + * Handles an [VmTurnZone] onTurnDone event. */ typedef void ZoneOnTurnDone(); /** - * Handles an [NgZone] onTurnDone event. + * Handles an [VmTurnZone] onTurnDone event. */ typedef void ZoneOnTurnStart(); /** - * Handles an [NgZone] onError event. + * Handles an [VmTurnZone] onError event. */ typedef void ZoneOnError(dynamic error, dynamic stacktrace, LongStackTrace longStacktrace); @@ -47,7 +47,7 @@ class LongStackTrace { * all the microtasks scheduled on the inner [Zone]. * * In a typical app, [ngDynamicApp] or [ngStaticApp] will create a singleton - * [NgZone] whose outer [Zone] is the root [Zone] and whose default [onTurnDone] + * [VmTurnZone] whose outer [Zone] is the root [Zone] and whose default [onTurnDone] * runs the Angular digest. A component may want to inject this singleton if it * needs to run code _outside_ the Angular digest. */