feat(VmTurnZone): Rework the implementation to minimize change detection runs

Before this PR there were only 2 zones: root zone = outer zone > inner
zone.
This PR creates the outer zone as a fork of the root zone: root > outer
> inner.

By doing this it is possible to detected microtasks scheduling in the
outer zone and run the change detection less often (no more than one
time per VM turn).

The PR also introduce a Promise monkey patch for the JS implementation.
It makes Promises aware of microtasks and again allow running the change
detection only once per turn.
This commit is contained in:
Victor Berchet
2015-04-10 12:42:33 +02:00
parent 358a6750ed
commit e8a6c95e2a
13 changed files with 2587 additions and 251 deletions

View File

@ -1,6 +1,6 @@
library angular.zone;
import 'dart:async' as async;
import 'dart:async';
import 'package:stack_trace/stack_trace.dart' show Chain;
/**
@ -15,49 +15,97 @@ import 'package:stack_trace/stack_trace.dart' show Chain;
* The wrapper maintains an "inner" and "outer" `Zone`. The application code will executes
* in the "inner" zone unless `runOutsideAngular` is explicitely called.
*
* A typical application will create a singleton `VmTurnZone` whose outer `Zone` is the root `Zone`
* and whose default `onTurnDone` runs the Angular digest.
* A typical application will create a singleton `VmTurnZone`. The outer `Zone` is a fork of the root
* `Zone`. The default `onTurnDone` runs the Angular change detection.
*/
class VmTurnZone {
Function _onTurnStart;
Function _onTurnDone;
Function _onScheduleMicrotask;
Function _onErrorHandler;
async.Zone _outerZone;
async.Zone _innerZone;
// Code executed in _outerZone does not trigger the onTurnDone.
Zone _outerZone;
// _innerZone is the child of _outerZone. Any code executed in this zone will trigger the
// onTurnDone hook at the end of the current VM turn.
Zone _innerZone;
int _nestedRunCounter;
// Number of microtasks pending from _outerZone (& descendants)
int _pendingMicrotasks = 0;
// Whether some code has been executed in the _innerZone (& descendants) in the current turn
bool _hasExecutedCodeInInnerZone = false;
// Whether the onTurnStart hook is executing
bool _inTurnStart = false;
// _outerRun() call depth. 0 at the end of a macrotask
// zone.run(() => { // top-level call
// zone.run(() => {}); // nested call -> in-turn
// }); // we should only check for the end of a turn once the top-level run ends
int _nestedRun = 0;
/**
* Associates with this
*
* - an "outer" `Zone`, which is the one that created this.
* - an "inner" `Zone`, which is a child of the outer `Zone`.
* - an "outer" [Zone], which is a child of the one that created this.
* - an "inner" [Zone], which is a child of the outer [Zone].
*
* @param {bool} enableLongStackTrace whether to enable long stack trace. They should only be
* enabled in development mode as they significantly impact perf.
*/
VmTurnZone({bool enableLongStackTrace}) {
_nestedRunCounter = 0;
_outerZone = async.Zone.current;
_innerZone = _createInnerZoneWithErrorHandling(enableLongStackTrace);
// The _outerZone captures microtask scheduling so that we can run onTurnDone when the queue
// is exhausted and code has been executed in the _innerZone.
if (enableLongStackTrace) {
_outerZone = Chain.capture(
() {
return Zone.current.fork(
specification: new ZoneSpecification(
scheduleMicrotask: _scheduleMicrotask,
run: _outerRun,
runUnary: _outerRunUnary,
runBinary: _outerRunBinary
),
zoneValues: {'_name': 'outer'}
);
}, onError: _onErrorWithLongStackTrace);
} else {
_outerZone = Zone.current.fork(
specification: new ZoneSpecification(
scheduleMicrotask: _scheduleMicrotask,
run: _outerRun,
runUnary: _outerRunUnary,
runBinary: _outerRunBinary,
handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone, error, StackTrace trace) =>
_onErrorWithoutLongStackTrace(error, trace)
),
zoneValues: {'_name': 'outer'}
);
}
// Instruments the inner [Zone] to detect when code is executed in this (or a descendant) zone.
// Also runs the onTurnStart hook the first time this zone executes some code in each turn.
_innerZone = _outerZone.fork(
specification: new ZoneSpecification(
run: _innerRun,
runUnary: _innerRunUnary,
runBinary: _innerRunBinary
),
zoneValues: {'_name': 'inner'});
}
/**
* Initializes the zone hooks.
*
* The given error handler should re-throw the passed exception. Otherwise, exceptions will not
* propagate outside of the [VmTurnZone] and can alter the application execution flow.
* Not re-throwing could be used to help testing the code or advanced use cases.
*
* @param {Function} onTurnStart called before code executes in the inner zone for each VM turn
* @param {Function} onTurnDone called at the end of a VM turn if code has executed in the inner zone
* @param {Function} onScheduleMicrotask
* @param {Function} onErrorHandler called when an exception is thrown by a macro or micro task
*/
initCallbacks({Function onTurnStart, Function onTurnDone,
Function onScheduleMicrotask, Function onErrorHandler}) {
this._onTurnStart = onTurnStart;
this._onTurnDone = onTurnDone;
this._onScheduleMicrotask = onScheduleMicrotask;
this._onErrorHandler = onErrorHandler;
void initCallbacks({Function onTurnStart, Function onTurnDone, Function onErrorHandler}) {
_onTurnStart = onTurnStart;
_onTurnDone = onTurnDone;
_onErrorHandler = onErrorHandler;
}
/**
@ -76,7 +124,12 @@ class VmTurnZone {
* }
* ```
*/
dynamic run(fn()) => _innerZone.run(fn);
dynamic run(fn()) {
// Using runGuarded() is required when executing sync code with Dart otherwise handleUncaughtError()
// would not be called on exceptions.
// see https://code.google.com/p/dart/issues/detail?id=19566 for details.
return _innerZone.runGuarded(fn);
}
/**
* Runs `fn` in the outer zone and returns whatever it returns.
@ -97,81 +150,94 @@ class VmTurnZone {
* }
* ```
*/
dynamic runOutsideAngular(fn()) => _outerZone.run(fn);
async.Zone _createInnerZoneWithErrorHandling(bool enableLongStackTrace) {
if (enableLongStackTrace) {
return Chain.capture(() {
return _createInnerZone(async.Zone.current);
}, onError: this._onErrorWithLongStackTrace);
} else {
return async.runZoned(() {
return _createInnerZone(async.Zone.current);
}, onError: this._onErrorWithoutLongStackTrace);
}
dynamic runOutsideAngular(fn()) {
return _outerZone.runGuarded(fn);
}
async.Zone _createInnerZone(async.Zone zone) {
return zone.fork(
specification: new async.ZoneSpecification(
run: _onRun,
runUnary: _onRunUnary,
scheduleMicrotask: _onMicrotask));
// Executes code in the [_innerZone] & trigger the onTurnStart hook when code is executed for the
// first time in a turn.
dynamic _innerRun(Zone self, ZoneDelegate parent, Zone zone, fn()) {
_maybeStartVmTurn(parent, zone);
return parent.run(zone, fn);
}
dynamic _onRunBase(
async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn()) {
_nestedRunCounter++;
try {
if (_nestedRunCounter == 1 && _onTurnStart != null) delegate.run(
zone, _onTurnStart);
return fn();
} catch (e, s) {
if (_onErrorHandler != null && _nestedRunCounter == 1) {
_onErrorHandler(e, [s.toString()]);
} else {
rethrow;
dynamic _innerRunUnary(Zone self, ZoneDelegate parent, Zone zone, fn(arg), arg) {
_maybeStartVmTurn(parent, zone);
return parent.runUnary(zone, fn, arg);
}
dynamic _innerRunBinary(Zone self, ZoneDelegate parent, Zone zone, fn(arg1, arg2), arg1, arg2) {
_maybeStartVmTurn(parent, zone);
return parent.runBinary(zone, fn, arg1, arg2);
}
void _maybeStartVmTurn(ZoneDelegate parent, Zone zone) {
if (!_hasExecutedCodeInInnerZone) {
_hasExecutedCodeInInnerZone = true;
if (_onTurnStart != null) {
_inTurnStart = true;
parent.run(zone, _onTurnStart);
}
}
}
dynamic _outerRun(Zone self, ZoneDelegate parent, Zone zone, fn()) {
try {
_nestedRun++;
return parent.run(zone, fn);
} finally {
_nestedRunCounter--;
if (_nestedRunCounter == 0 && _onTurnDone != null) _finishTurn(
zone, delegate);
_nestedRun--;
// If there are no more pending microtasks, we are at the end of a VM turn (or in onTurnStart)
// _nestedRun will be 0 at the end of a macrotasks (it could be > 0 when there are nested calls
// to _outerRun()).
if (_pendingMicrotasks == 0 && _nestedRun == 0) {
if (_onTurnDone != null && !_inTurnStart && _hasExecutedCodeInInnerZone) {
// Trigger onTurnDone at the end of a turn if _innerZone has executed some code
try {
parent.run(_innerZone, _onTurnDone);
} finally {
_hasExecutedCodeInInnerZone = false;
}
}
}
_inTurnStart = false;
}
}
dynamic _onRun(async.Zone self, async.ZoneDelegate delegate, async.Zone zone,
fn()) => _onRunBase(self, delegate, zone, () => delegate.run(zone, fn));
dynamic _outerRunUnary(Zone self, ZoneDelegate parent, Zone zone, fn(arg), arg) =>
_outerRun(self, parent, zone, () => fn(arg));
dynamic _onRunUnary(async.Zone self, async.ZoneDelegate delegate,
async.Zone zone, fn(args), args) =>
_onRunBase(self, delegate, zone, () => delegate.runUnary(zone, fn, args));
dynamic _outerRunBinary(Zone self, ZoneDelegate parent, Zone zone, fn(arg1, arg2), arg1, arg2) =>
_outerRun(self, parent, zone, () => fn(arg1, arg2));
void _finishTurn(zone, delegate) {
delegate.run(zone, _onTurnDone);
void _scheduleMicrotask(Zone self, ZoneDelegate parent, Zone zone, fn) {
_pendingMicrotasks++;
var microtask = () {
try {
fn();
} finally {
_pendingMicrotasks--;
}
};
parent.scheduleMicrotask(zone, microtask);
}
_onMicrotask(
async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn) {
if (this._onScheduleMicrotask != null) {
_onScheduleMicrotask(fn);
} else {
delegate.scheduleMicrotask(zone, fn);
}
}
_onErrorWithLongStackTrace(exception, Chain chain) {
final traces = chain.terse.traces.map((t) => t.toString()).toList();
_onError(exception, traces, chain.traces[0]);
}
_onErrorWithoutLongStackTrace(exception, StackTrace trace) {
_onError(exception, [trace.toString()], trace);
}
_onError(exception, List<String> traces, StackTrace singleTrace) {
// Called by Chain.capture() on errors when long stack traces are enabled
void _onErrorWithLongStackTrace(error, Chain chain) {
if (_onErrorHandler != null) {
_onErrorHandler(exception, traces);
final traces = chain.terse.traces.map((t) => t.toString()).toList();
_onErrorHandler(error, traces);
} else {
_outerZone.handleUncaughtError(exception, singleTrace);
throw error;
}
}
// Outer zone handleUnchaughtError when long stack traces are not used
void _onErrorWithoutLongStackTrace(error, StackTrace trace) {
if (_onErrorHandler != null) {
_onErrorHandler(error, [trace.toString()]);
} else {
throw error;
}
}
}

View File

@ -7,8 +7,8 @@ import {normalizeBlank, isPresent, global} from 'angular2/src/facade/lang';
* The wrapper maintains an "inner" and "outer" `Zone`. The application code will executes
* in the "inner" zone unless `runOutsideAngular` is explicitely called.
*
* A typical application will create a singleton `VmTurnZone` whose outer `Zone` is the root `Zone`
* and whose default `onTurnDone` runs the Angular digest.
* A typical application will create a singleton `VmTurnZone`. The outer `Zone` is a fork of the root
* `Zone`. The default `onTurnDone` runs the Angular change detection.
*
* @exportedAs angular2/core
*/
@ -20,25 +20,30 @@ export class VmTurnZone {
_onTurnDone:Function;
_onErrorHandler:Function;
_nestedRunCounter:number;
/**
* Associates with this
*
* - an "outer" zone, which is the one that created this.
* - an "outer" zone, which is a child of the one that created this.
* - an "inner" zone, which is a child of the outer zone.
*
* @param {bool} enableLongStackTrace whether to enable long stack trace. They should only be
* enabled in development mode as they significantly impact perf.
*/
constructor({enableLongStackTrace}) {
this._nestedRunCounter = 0;
this._onTurnStart = null;
this._onTurnDone = null;
this._onErrorHandler = null;
Zone._hasExecutedInnerCode = false;
this._outerZone = global.zone;
this._outerZone = global.zone.fork({
_name: 'outer',
beforeTurn: () => { this._beforeTurn(); },
afterTurn: () => { this._afterTurn(); }
});
this._innerZone = this._createInnerZone(this._outerZone, enableLongStackTrace);
// TODO('remove');
Zone.debug = true;
}
/**
@ -46,10 +51,9 @@ export class VmTurnZone {
*
* @param {Function} onTurnStart called before code executes in the inner zone for each VM turn
* @param {Function} onTurnDone called at the end of a VM turn if code has executed in the inner zone
* @param {Function} onScheduleMicrotask
* @param {Function} onErrorHandler called when an exception is thrown by a macro or micro task
*/
initCallbacks({onTurnStart, onTurnDone, onScheduleMicrotask, onErrorHandler} = {}) {
initCallbacks({onTurnStart, onTurnDone, onErrorHandler} = {}) {
this._onTurnStart = normalizeBlank(onTurnStart);
this._onTurnDone = normalizeBlank(onTurnDone);
this._onErrorHandler = normalizeBlank(onErrorHandler);
@ -62,10 +66,10 @@ export class VmTurnZone {
* Angular's auto digest mechanism.
*
* ```
* var zone: VmTurnZone = <ref to the application zone>;
* var zone: VmTurnZone = [ref to the application zone];
*
* zone.run(() => {
* // auto-digest will run after this function is called from JS
* // the change detection will run after this function and the microtasks it enqueues have executed.
* });
* ```
*/
@ -80,7 +84,7 @@ export class VmTurnZone {
* auto-digest mechanism.
*
* ```
* var zone: VmTurnZone = <ref to the application zone>;
* var zone: VmTurnZone = [ref to the application zone];
*
* zone.runOusideAngular(() => {
* element.onClick(() => {
@ -111,23 +115,39 @@ export class VmTurnZone {
};
}
return zone.fork(errorHandling).fork({
beforeTask: () => {this._beforeTask()},
afterTask: () => {this._afterTask()}
});
return zone
.fork(errorHandling)
.fork({
'$run': function (parentRun) {
return function () {
if (!Zone._hasExecutedInnerCode) {
// Execute the beforeTurn hook when code is first executed in the inner zone in the turn
Zone._hasExecutedInnerCode = true;
var oldZone = global.zone;
global.zone = this;
this.beforeTurn();
global.zone = oldZone;
}
return parentRun.apply(this, arguments);
}
},
_name: 'inner'
});
}
_beforeTask(){
this._nestedRunCounter ++;
if(this._nestedRunCounter === 1 && this._onTurnStart) {
this._onTurnStart();
}
_beforeTurn() {
this._onTurnStart && this._onTurnStart();
}
_afterTask(){
this._nestedRunCounter --;
if(this._nestedRunCounter === 0 && this._onTurnDone) {
this._onTurnDone();
_afterTurn() {
if (this._onTurnDone) {
if (Zone._hasExecutedInnerCode) {
// Execute the onTurnDone hook in the inner zone so that microtasks are enqueued there
// The hook gets executed when code has runned in the inner zone during the current turn
this._innerZone.run(this._onTurnDone, this);
Zone._hasExecutedInnerCode = false;
}
}
}

View File

@ -225,3 +225,5 @@ String elementText(n) {
return DOM.getText(n);
}
String getCurrentZoneName() => Zone.current['_name'];

View File

@ -135,11 +135,15 @@ function _it(jsmFn, name, fn) {
if (!(fn instanceof FunctionWithParamTokens)) {
fn = inject([], fn);
}
inIt = true;
fn.execute(injector);
inIt = false;
if (!async) done();
// Use setTimeout() to run tests in a macrotasks.
// Without this, tests would be executed in the context of a microtasks (a promise .then).
setTimeout(() => {
inIt = true;
fn.execute(injector);
inIt = false;
if (!async) done();
}, 0);
});
}
@ -351,3 +355,7 @@ function elementText(n) {
return DOM.getText(n);
}
function getCurrentZoneName(): string {
return zone._name;
}

View File

@ -9,171 +9,622 @@ import {
it,
xdescribe,
xit,
Log
} from 'angular2/test_lib';
import {Log, once} from 'angular2/test_lib';
import {PromiseWrapper} from 'angular2/src/facade/async';
import {ListWrapper} from 'angular2/src/facade/collection';
import {BaseException} from 'angular2/src/facade/lang';
import {VmTurnZone} from 'angular2/src/core/zone/vm_turn_zone';
// Schedules a macrotask (using a timer)
// The code is executed in the outer zone to properly detect VM turns - in Dart VM turns could not be properly detected
// in the root zone because scheduleMicrotask() is not overriden.
function macroTask(fn: Function): void {
_zone.runOutsideAngular(() => PromiseWrapper.setTimeout(fn, 0));
}
// Schedules a microtasks (using a resolved promise .then())
function microTask(fn: Function): void {
PromiseWrapper.resolve(null).then((_) => { fn(); });
}
var _log;
var _errors;
var _traces;
var _zone;
function logError(error, stackTrace) {
ListWrapper.push(_errors, error);
ListWrapper.push(_traces, stackTrace);
}
export function main() {
describe("VmTurnZone", () => {
var log, zone;
function createZone(enableLongStackTrace) {
var zone = new VmTurnZone({enableLongStackTrace: enableLongStackTrace});
zone.initCallbacks({
onTurnStart: _log.fn('onTurnStart'),
onTurnDone: _log.fn('onTurnDone')
});
return zone;
}
beforeEach(() => {
log = new Log();
zone = new VmTurnZone({enableLongStackTrace: true});
zone.initCallbacks({
onTurnStart: log.fn('onTurnStart'),
onTurnDone: log.fn('onTurnDone')
});
_log = new Log();
_errors = [];
_traces = [];
});
describe("run", () => {
it('should call onTurnStart and onTurnDone', () => {
zone.run(log.fn('run'));
expect(log.result()).toEqual('onTurnStart; run; onTurnDone');
});
it('should return the body return value from run', () => {
expect(zone.run(() => 6)).toEqual(6);
});
it('should not run onTurnStart and onTurnDone for nested Zone.run', () => {
zone.run(() => {
zone.run(log.fn('run'));
});
expect(log.result()).toEqual('onTurnStart; run; onTurnDone');
});
it('should call onTurnStart and onTurnDone before and after each top-level run', () => {
zone.run(log.fn('run1'));
zone.run(log.fn('run2'));
expect(log.result()).toEqual('onTurnStart; run1; onTurnDone; onTurnStart; run2; onTurnDone');
});
it('should call onTurnStart and onTurnDone before and after each turn', inject([AsyncTestCompleter], (async) => {
var a = PromiseWrapper.completer();
var b = PromiseWrapper.completer();
zone.run(() => {
log.add('run start');
a.promise.then((_) => log.add('a then'));
b.promise.then((_) => log.add('b then'));
});
a.resolve("a");
b.resolve("b");
PromiseWrapper.all([a.promise, b.promise]).then((_) => {
expect(log.result()).toEqual('onTurnStart; run start; onTurnDone; onTurnStart; a then; onTurnDone; onTurnStart; b then; onTurnDone');
async.done();
});
}));
});
describe("runOutsideAngular", () => {
it("should run a function outside of the angular zone", () => {
zone.runOutsideAngular(log.fn('run'));
expect(log.result()).toEqual('run');
});
});
describe("exceptions", () => {
var trace, exception, saveStackTrace;
describe('long stack trace', () => {
beforeEach(() => {
trace = null;
exception = null;
saveStackTrace = (e, t) => {
exception = e;
trace = t;
};
_zone = createZone(true);
});
it('should call the on error callback when it is defined', () => {
zone.initCallbacks({onErrorHandler: saveStackTrace});
commonTests();
zone.run(() => {
throw new BaseException('aaa');
});
it('should produce long stack traces', inject([AsyncTestCompleter],
(async) => {
macroTask(() => {
_zone.initCallbacks({onErrorHandler: logError});
var c = PromiseWrapper.completer();
expect(exception).toBeDefined();
});
it('should rethrow exceptions from the body when no callback defined', () => {
expect(() => {
zone.run(() => {
throw new BaseException('bbb');
});
}).toThrowError('bbb');
});
it('should produce long stack traces', inject([AsyncTestCompleter], (async) => {
zone.initCallbacks({onErrorHandler: saveStackTrace});
var c = PromiseWrapper.completer();
zone.run(function () {
PromiseWrapper.setTimeout(function () {
PromiseWrapper.setTimeout(function () {
c.resolve(null);
throw new BaseException('ccc');
_zone.run(() => {
PromiseWrapper.setTimeout(() => {
PromiseWrapper.setTimeout(() => {
c.resolve(null);
throw new BaseException('ccc');
}, 0);
}, 0);
}, 0);
});
});
c.promise.then((_) => {
// then number of traces for JS and Dart is different
expect(trace.length).toBeGreaterThan(1);
async.done();
c.promise.then((_) => {
expect(_traces.length).toBe(1);
expect(_traces[0].length).toBeGreaterThan(1);
async.done();
});
});
}));
it('should produce long stack traces (when using promises)', inject([AsyncTestCompleter], (async) => {
zone.initCallbacks({onErrorHandler: saveStackTrace});
it('should produce long stack traces (when using microtasks)', inject(
[AsyncTestCompleter], (async) => {
macroTask(() => {
_zone.initCallbacks({onErrorHandler: logError});
var c = PromiseWrapper.completer();
var c = PromiseWrapper.completer();
zone.run(function () {
PromiseWrapper.resolve(null).then((_) => {
return PromiseWrapper.resolve(null).then((__) => {
c.resolve(null);
throw new BaseException("ddd");
_zone.run(() => {
microTask(() => {
microTask(() => {
c.resolve(null);
throw new BaseException("ddd");
});
});
});
});
c.promise.then((_) => {
// then number of traces for JS and Dart is different
expect(trace.length).toBeGreaterThan(1);
async.done();
c.promise.then((_) => {
expect(_traces.length).toBe(1);
expect(_traces[0].length).toBeGreaterThan(1);
async.done();
});
});
}));
});
describe('short stack trace', () => {
beforeEach(() => {
_zone = createZone(false);
});
commonTests();
it('should disable long stack traces', inject([AsyncTestCompleter], (async) => {
var zone = new VmTurnZone({enableLongStackTrace: false});
zone.initCallbacks({onErrorHandler: saveStackTrace});
macroTask(() => {
_zone.initCallbacks({onErrorHandler: logError});
var c = PromiseWrapper.completer();
var c = PromiseWrapper.completer();
zone.run(function () {
PromiseWrapper.setTimeout(function () {
PromiseWrapper.setTimeout(function () {
c.resolve(null);
throw new BaseException('ccc');
_zone.run(() => {
PromiseWrapper.setTimeout(() => {
PromiseWrapper.setTimeout(() => {
c.resolve(null);
throw new BaseException('ccc');
}, 0);
}, 0);
}, 0);
});
});
c.promise.then((_) => {
expect(trace.length).toEqual(1);
async.done();
c.promise.then((_) => {
expect(_traces.length).toBe(1);
expect(_traces[0].length).toEqual(1);
async.done();
});
});
}));
});
});
}
function commonTests() {
describe("run", () => {
it('should return the body return value from run', inject([AsyncTestCompleter], (async) => {
macroTask(() => {
expect(_zone.run(() => {
return 6;
})).toEqual(6);
});
macroTask(() => {
async.done();
});
}));
it('should call onTurnStart and onTurnDone', inject([AsyncTestCompleter], (async) => {
macroTask(() => {
_zone.run(_log.fn('run'));
});
macroTask(() => {
expect(_log.result()).toEqual('onTurnStart; run; onTurnDone');
async.done();
});
}));
it('should call onTurnStart once before a turn and onTurnDone once after the turn',
inject([AsyncTestCompleter], (async) => {
macroTask(() => {
_zone.run(() => {
_log.add('run start');
microTask(() => { _log.add('async'); });
_log.add('run end');
});
});
macroTask(() => {
// The microtask (async) is executed after the macrotask (run)
expect(_log.result()).toEqual('onTurnStart; run start; run end; async; onTurnDone');
async.done();
});
}));
it('should not run onTurnStart and onTurnDone for nested Zone.run',
inject([AsyncTestCompleter], (async) => {
macroTask(() => {
_zone.run(() => {
_log.add('start run');
_zone.run(() => {
_log.add('nested run');
microTask(() => _log.add('nested run microtask'));
});
_log.add('end run');
});
});
macroTask(() => {
expect(_log.result()).toEqual('onTurnStart; start run; nested run; end run; nested run microtask; onTurnDone');
async.done();
});
}));
it('should call onTurnStart and onTurnDone before and after each top-level run',
inject([AsyncTestCompleter], (async) => {
macroTask(() => {
_zone.run(_log.fn('run1'));
_zone.run(_log.fn('run2'));
});
macroTask(() => {
_zone.run(_log.fn('run3'));
});
macroTask(() => {
expect(_log.result()).toEqual('onTurnStart; run1; run2; onTurnDone; onTurnStart; run3; onTurnDone');
async.done();
});
}));
it('should call onTurnStart and onTurnDone before and after each turn',
inject([AsyncTestCompleter], (async) => {
var a;
var b;
macroTask(() => {
a = PromiseWrapper.completer();
b = PromiseWrapper.completer();
_zone.run(() => {
_log.add('run start');
a.promise.then((_) => {
return _log.add('a then');
});
b.promise.then((_) => {
return _log.add('b then');
});
});
});
macroTask(() => {
a.resolve('a');
b.resolve('b');
});
macroTask(() => {
expect(_log.result()).toEqual('onTurnStart; run start; onTurnDone; onTurnStart; a then; b then; onTurnDone');
async.done();
});
}));
it('should run a function outside of the angular zone', inject([AsyncTestCompleter], (async) => {
macroTask(() => {
_zone.runOutsideAngular(_log.fn('run'));
});
macroTask(() => {
expect(_log.result()).toEqual('run');
async.done()
});
}));
it('should call onTurnStart and onTurnDone when an inner microtask is scheduled from outside angular',
inject([AsyncTestCompleter], (async) => {
var completer;
macroTask(() => {
_zone.runOutsideAngular(() => {
completer = PromiseWrapper.completer();
});
});
macroTask(() => {
_zone.run(() => {
completer.promise.then((_) => {
_log.add('executedMicrotask');
});
});
});
macroTask(() => {
_zone.runOutsideAngular(() => {
_log.add('scheduling a microtask');
completer.resolve(null);
});
});
macroTask(() => {
expect(_log.result()).toEqual(
// First VM turn => setup Promise then
'onTurnStart; onTurnDone; ' +
// Second VM turn (outside of anguler)
'scheduling a microtask; ' +
// Third VM Turn => execute the microtask (inside angular)
'onTurnStart; executedMicrotask; onTurnDone'
);
async.done();
});
}));
it('should not call onTurnStart and onTurnDone when an outer microtask is scheduled from inside angular',
inject([AsyncTestCompleter], (async) => {
var completer;
macroTask(() => {
_zone.runOutsideAngular(() => {
completer = PromiseWrapper.completer();
completer.promise.then((_) => {
_log.add('executedMicrotask');
});
});
});
macroTask(() => {
_zone.run(() => {
_log.add('scheduling a microtask');
completer.resolve(null);
});
});
macroTask(() => {
expect(_log.result()).toEqual(
'onTurnStart; scheduling a microtask; executedMicrotask; onTurnDone'
);
async.done();
});
}));
it('should call onTurnStart before executing a microtask scheduled in onTurnDone as well as ' +
'onTurnDone after executing the task', inject([AsyncTestCompleter], (async) => {
var ran = false;
_zone.initCallbacks({
onTurnStart: _log.fn('onTurnStart'),
onTurnDone: () => {
_log.add('onTurnDone(begin)');
if (!ran) {
microTask(() => {
ran = true;
_log.add('executedMicrotask');});
}
_log.add('onTurnDone(end)');
}});
macroTask(() => {
_zone.run(() => {
_log.add('run');
});
});
macroTask(() => {
expect(_log.result()).toEqual(
// First VM turn => 'run' macrotask
'onTurnStart; run; onTurnDone(begin); onTurnDone(end); ' +
// Second VM Turn => microtask enqueued from onTurnDone
'onTurnStart; executedMicrotask; onTurnDone(begin); onTurnDone(end)'
);
async.done();
});
}));
it('should call onTurnStart and onTurnDone for a scheduleMicrotask in onTurnDone triggered by ' +
'a scheduleMicrotask in run', inject([AsyncTestCompleter], (async) => {
var ran = false;
_zone.initCallbacks({
onTurnStart: _log.fn('onTurnStart'),
onTurnDone: () => {
_log.add('onTurnDone(begin)');
if (!ran) {
_log.add('onTurnDone(scheduleMicrotask)');
microTask(() => {
ran = true;
_log.add('onTurnDone(executeMicrotask)');
});
}
_log.add('onTurnDone(end)');
}});
macroTask(() => {
_zone.run(() => {
_log.add('scheduleMicrotask');
microTask(() => { _log.add('run(executeMicrotask)'); });
});
});
macroTask(() => {
expect(_log.result()).toEqual(
// First VM Turn => a macrotask + the microtask it enqueues
'onTurnStart; scheduleMicrotask; run(executeMicrotask); onTurnDone(begin); onTurnDone(scheduleMicrotask); onTurnDone(end); ' +
// Second VM Turn => the microtask enqueued from onTurnDone
'onTurnStart; onTurnDone(executeMicrotask); onTurnDone(begin); onTurnDone(end)'
);
async.done();
});
}));
it('should execute promises scheduled in onTurnStart before promises scheduled in run',
inject([AsyncTestCompleter], (async) => {
var donePromiseRan = false;
var startPromiseRan = false;
_zone.initCallbacks({
onTurnStart: () => {
_log.add('onTurnStart(begin)');
if (!startPromiseRan) {
_log.add('onTurnStart(schedulePromise)');
microTask(() => { _log.add('onTurnStart(executePromise)'); });
startPromiseRan = true;
}
_log.add('onTurnStart(end)');
},
onTurnDone: () => {
_log.add('onTurnDone(begin)');
if (!donePromiseRan) {
_log.add('onTurnDone(schedulePromise)');
microTask(() => { _log.add('onTurnDone(executePromise)'); });
donePromiseRan = true;
}
_log.add('onTurnDone(end)');
}});
macroTask(() => {
_zone.run(() => {
_log.add('run start');
PromiseWrapper.resolve(null)
.then((_) => {
_log.add('promise then');
PromiseWrapper.resolve(null).then((_) => { _log.add('promise foo'); });
return PromiseWrapper.resolve(null);
})
.then((_) => {
_log.add('promise bar');
});
_log.add('run end');
});
});
macroTask(() => {
expect(_log.result()).toEqual(
// First VM turn: enqueue a microtask in onTurnStart
'onTurnStart(begin); onTurnStart(schedulePromise); onTurnStart(end); ' +
// First VM turn: execute the macrotask which enqueues microtasks
'run start; run end; ' +
// First VM turn: execute enqueued microtasks
'onTurnStart(executePromise); promise then; promise foo; promise bar; ' +
// First VM turn: onTurnEnd, enqueue a microtask
'onTurnDone(begin); onTurnDone(schedulePromise); onTurnDone(end); ' +
// Second VM turn: execute the microtask from onTurnEnd
'onTurnStart(begin); onTurnStart(end); onTurnDone(executePromise); onTurnDone(begin); onTurnDone(end)'
);
async.done();
});
}));
it('should call onTurnStart and onTurnDone before and after each turn, respectively',
inject([AsyncTestCompleter], (async) => {
var completerA, completerB;
macroTask(() => {
_zone.run(() => {
completerA = PromiseWrapper.completer();
completerB = PromiseWrapper.completer();
completerA.promise.then((_) => _log.add('a then'));
completerB.promise.then((_) => _log.add('b then'));
_log.add('run start');
});
});
macroTask(() => {
_zone.run(() => {
completerA.resolve(null);
});
});
macroTask(() => {
_zone.run(() => {
completerB.resolve(null);
});
});
macroTask(() => {
expect(_log.result()).toEqual(
// First VM turn
'onTurnStart; run start; onTurnDone; ' +
// Second VM turn
'onTurnStart; a then; onTurnDone; ' +
// Third VM turn
'onTurnStart; b then; onTurnDone');
async.done();
});
}));
it('should call onTurnStart and onTurnDone before and after (respectively) all turns in a chain',
inject([AsyncTestCompleter], (async) => {
macroTask(() => {
_zone.run(() => {
_log.add('run start');
microTask(() => {
_log.add('async1');
microTask(() => {
_log.add('async2');
});
});
_log.add('run end');
});
});
macroTask(() => {
expect(_log.result()).toEqual('onTurnStart; run start; run end; async1; async2; onTurnDone');
async.done();
});
}));
it('should call onTurnStart and onTurnDone for promises created outside of run body',
inject([AsyncTestCompleter], (async) => {
var promise;
_zone.initCallbacks({
onTurnStart: _log.fn('onTurnStart'),
onTurnDone: _log.fn('onTurnDone')
});
macroTask(() => {
_zone.runOutsideAngular(() => {
promise = PromiseWrapper.resolve(4).then((x) => PromiseWrapper.resolve(x));
});
_zone.run(() => {
promise.then((_) => {
_log.add('promise then');
});
_log.add('zone run');
});
});
macroTask(() => {
expect(_log.result()).toEqual('onTurnStart; zone run; promise then; onTurnDone');
async.done();
});
}));
});
describe('exceptions', () => {
it('should call the on error callback when it is defined', inject([AsyncTestCompleter], (async) => {
macroTask(() => {
_zone.initCallbacks({onErrorHandler: logError});
var exception = new BaseException('sync');
_zone.run(() => {
throw exception;
});
expect(_errors.length).toBe(1);
expect(_errors[0]).toBe(exception);
async.done();
});
}));
it('should call onError for errors from microtasks', inject([AsyncTestCompleter], (async) => {
_zone.initCallbacks({onErrorHandler: logError});
var exception = new BaseException('async');
macroTask(() => {
_zone.run(() => {
microTask(() => { throw exception; });
});
});
macroTask(() => {
expect(_errors.length).toBe(1);
expect(_errors[0]).toEqual(exception);
async.done();
});
}));
it('should call onError when onTurnDone throws and the zone is sync',
inject([AsyncTestCompleter], (async) => {
var exception = new BaseException('fromOnTurnDone');
_zone.initCallbacks({
onErrorHandler: logError,
onTurnDone: () => { throw exception; }
});
macroTask(() => {
_zone.run(() => { });
});
macroTask(() => {
expect(_errors.length).toBe(1);
expect(_errors[0]).toEqual(exception);
async.done();
});
}));
it('should call onError when onTurnDone throws and the zone is async',
inject([AsyncTestCompleter], (async) => {
var asyncRan = false;
var exception = new BaseException('fromOnTurnDone');
_zone.initCallbacks({
onErrorHandler: logError,
onTurnDone: () => { throw exception; }});
macroTask(() => {
_zone.run(() => {
microTask(() => {
asyncRan = true;
});
});
});
macroTask(() => {
expect(asyncRan).toBe(true);
expect(_errors.length).toBe(1);
expect(_errors[0]).toEqual(exception);
async.done();
});
}));
});
}