Skip to content
This repository has been archived by the owner on Feb 22, 2018. It is now read-only.

Commit

Permalink
feat(Zone): add onTurnStart to NgZone.
Browse files Browse the repository at this point in the history
OnTurnStart is executed at the beginning of every turn. Any microtasks
scheduled in onTurnStart are executed before the ones scheduled in run.

Closes #83
  • Loading branch information
mvuksano authored and jbdeboer committed Apr 30, 2014
1 parent ec1f74a commit 366d529
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 34 deletions.
43 changes: 37 additions & 6 deletions lib/core/zone.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down
132 changes: 104 additions & 28 deletions test/core/zone_spec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ void main() {
zone.onTurnDone = () {
log('onTurnDone');
};
zone.onTurnStart = () {
log('onTurnStart');
};
zone.onError = (e, s, ls) => eh(e, s);
});

Expand Down Expand Up @@ -139,7 +142,7 @@ void main() {
zone.run(() {
log('run');
});
expect(log.result()).toEqual('run; onTurnDone');
expect(log.result()).toEqual('onTurnStart; run; onTurnDone');
});


Expand All @@ -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(() {
Expand All @@ -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(() {
Expand All @@ -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();
Expand All @@ -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(() {
Expand All @@ -264,18 +319,18 @@ 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'));
log('zone run');
});
microLeap();

expect(log.result()).toEqual('zone run; onTurnDone; future then; onTurnDone');
expect(log.result()).toEqual('onTurnStart; zone run; onTurnDone; onTurnStart; future then; onTurnDone');
}));


Expand All @@ -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');
}));


Expand All @@ -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;';
Expand All @@ -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', () {
Expand All @@ -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();
Expand All @@ -349,7 +425,7 @@ void main() {
});

microLeap();
expect(calls).toEqual('sync;async;done;');
expect(calls).toEqual('start;sync;async;done;');
}));


Expand Down

0 comments on commit 366d529

Please sign in to comment.