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

Add onTurnStart to VmTurnZone #890

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 40 additions & 9 deletions lib/core/zone.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
part of angular.core_internal;

/**
* Handles an [NgZone] onTurnDone event.
* Handles an [VmTurnZone] onTurnDone event.
*/
typedef void ZoneOnTurn();
typedef void ZoneOnTurnDone();

/**
* Handles an [NgZone] onError event.
* Handles an [VmTurnZone] onTurnDone event.
*/
typedef void ZoneOnTurnStart();

/**
* Handles an [VmTurnZone] onError event.
*/
typedef void ZoneOnError(dynamic error, dynamic stacktrace,
LongStackTrace longStacktrace);
Expand Down Expand Up @@ -42,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.
*/
Expand All @@ -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