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:
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -225,3 +225,5 @@ String elementText(n) {
|
||||
|
||||
return DOM.getText(n);
|
||||
}
|
||||
|
||||
String getCurrentZoneName() => Zone.current['_name'];
|
||||
|
@ -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;
|
||||
}
|
||||
|
Reference in New Issue
Block a user