X Tutup
Skip to content

Commit e8a6c95

Browse files
committed
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.
1 parent 358a675 commit e8a6c95

File tree

13 files changed

+2557
-221
lines changed

13 files changed

+2557
-221
lines changed

karma-js.conf.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ module.exports = function(config) {
1212
// Loaded through the es6-module-loader, in `test-main.js`.
1313
{pattern: 'dist/js/dev/es5/**', included: false, watched: false},
1414

15+
// Promise monkey patch & zone should be included first
16+
'zone/es6-promise.js',
17+
'zone/zone.js',
18+
'zone/inner-zone.js',
19+
'zone/long-stack-trace-zone.js',
20+
1521
'node_modules/traceur/bin/traceur-runtime.js',
1622
'node_modules/es6-module-loader/dist/es6-module-loader-sans-promises.src.js',
1723
// Including systemjs because it defines `__eval`, which produces correct stack traces.
@@ -20,9 +26,6 @@ module.exports = function(config) {
2026
'node_modules/systemjs/lib/extension-cjs.js',
2127
'node_modules/rx/dist/rx.js',
2228
'node_modules/reflect-metadata/Reflect.js',
23-
'node_modules/zone.js/zone.js',
24-
'node_modules/zone.js/long-stack-trace-zone.js',
25-
2629
'tools/build/file2modulename.js',
2730
'test-main.js'
2831
],
Lines changed: 142 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
library angular.zone;
22

3-
import 'dart:async' as async;
3+
import 'dart:async';
44
import 'package:stack_trace/stack_trace.dart' show Chain;
55

66
/**
@@ -15,49 +15,97 @@ import 'package:stack_trace/stack_trace.dart' show Chain;
1515
* The wrapper maintains an "inner" and "outer" `Zone`. The application code will executes
1616
* in the "inner" zone unless `runOutsideAngular` is explicitely called.
1717
*
18-
* A typical application will create a singleton `VmTurnZone` whose outer `Zone` is the root `Zone`
19-
* and whose default `onTurnDone` runs the Angular digest.
18+
* A typical application will create a singleton `VmTurnZone`. The outer `Zone` is a fork of the root
19+
* `Zone`. The default `onTurnDone` runs the Angular change detection.
2020
*/
2121
class VmTurnZone {
2222
Function _onTurnStart;
2323
Function _onTurnDone;
24-
Function _onScheduleMicrotask;
2524
Function _onErrorHandler;
2625

27-
async.Zone _outerZone;
28-
async.Zone _innerZone;
29-
30-
int _nestedRunCounter;
26+
// Code executed in _outerZone does not trigger the onTurnDone.
27+
Zone _outerZone;
28+
// _innerZone is the child of _outerZone. Any code executed in this zone will trigger the
29+
// onTurnDone hook at the end of the current VM turn.
30+
Zone _innerZone;
31+
32+
// Number of microtasks pending from _outerZone (& descendants)
33+
int _pendingMicrotasks = 0;
34+
// Whether some code has been executed in the _innerZone (& descendants) in the current turn
35+
bool _hasExecutedCodeInInnerZone = false;
36+
// Whether the onTurnStart hook is executing
37+
bool _inTurnStart = false;
38+
// _outerRun() call depth. 0 at the end of a macrotask
39+
// zone.run(() => { // top-level call
40+
// zone.run(() => {}); // nested call -> in-turn
41+
// }); // we should only check for the end of a turn once the top-level run ends
42+
int _nestedRun = 0;
3143

3244
/**
3345
* Associates with this
3446
*
35-
* - an "outer" `Zone`, which is the one that created this.
36-
* - an "inner" `Zone`, which is a child of the outer `Zone`.
47+
* - an "outer" [Zone], which is a child of the one that created this.
48+
* - an "inner" [Zone], which is a child of the outer [Zone].
3749
*
3850
* @param {bool} enableLongStackTrace whether to enable long stack trace. They should only be
3951
* enabled in development mode as they significantly impact perf.
4052
*/
4153
VmTurnZone({bool enableLongStackTrace}) {
42-
_nestedRunCounter = 0;
43-
_outerZone = async.Zone.current;
44-
_innerZone = _createInnerZoneWithErrorHandling(enableLongStackTrace);
54+
// The _outerZone captures microtask scheduling so that we can run onTurnDone when the queue
55+
// is exhausted and code has been executed in the _innerZone.
56+
if (enableLongStackTrace) {
57+
_outerZone = Chain.capture(
58+
() {
59+
return Zone.current.fork(
60+
specification: new ZoneSpecification(
61+
scheduleMicrotask: _scheduleMicrotask,
62+
run: _outerRun,
63+
runUnary: _outerRunUnary,
64+
runBinary: _outerRunBinary
65+
),
66+
zoneValues: {'_name': 'outer'}
67+
);
68+
}, onError: _onErrorWithLongStackTrace);
69+
} else {
70+
_outerZone = Zone.current.fork(
71+
specification: new ZoneSpecification(
72+
scheduleMicrotask: _scheduleMicrotask,
73+
run: _outerRun,
74+
runUnary: _outerRunUnary,
75+
runBinary: _outerRunBinary,
76+
handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone, error, StackTrace trace) =>
77+
_onErrorWithoutLongStackTrace(error, trace)
78+
),
79+
zoneValues: {'_name': 'outer'}
80+
);
81+
}
82+
83+
// Instruments the inner [Zone] to detect when code is executed in this (or a descendant) zone.
84+
// Also runs the onTurnStart hook the first time this zone executes some code in each turn.
85+
_innerZone = _outerZone.fork(
86+
specification: new ZoneSpecification(
87+
run: _innerRun,
88+
runUnary: _innerRunUnary,
89+
runBinary: _innerRunBinary
90+
),
91+
zoneValues: {'_name': 'inner'});
4592
}
4693

4794
/**
4895
* Initializes the zone hooks.
4996
*
97+
* The given error handler should re-throw the passed exception. Otherwise, exceptions will not
98+
* propagate outside of the [VmTurnZone] and can alter the application execution flow.
99+
* Not re-throwing could be used to help testing the code or advanced use cases.
100+
*
50101
* @param {Function} onTurnStart called before code executes in the inner zone for each VM turn
51102
* @param {Function} onTurnDone called at the end of a VM turn if code has executed in the inner zone
52-
* @param {Function} onScheduleMicrotask
53103
* @param {Function} onErrorHandler called when an exception is thrown by a macro or micro task
54104
*/
55-
initCallbacks({Function onTurnStart, Function onTurnDone,
56-
Function onScheduleMicrotask, Function onErrorHandler}) {
57-
this._onTurnStart = onTurnStart;
58-
this._onTurnDone = onTurnDone;
59-
this._onScheduleMicrotask = onScheduleMicrotask;
60-
this._onErrorHandler = onErrorHandler;
105+
void initCallbacks({Function onTurnStart, Function onTurnDone, Function onErrorHandler}) {
106+
_onTurnStart = onTurnStart;
107+
_onTurnDone = onTurnDone;
108+
_onErrorHandler = onErrorHandler;
61109
}
62110

63111
/**
@@ -76,7 +124,12 @@ class VmTurnZone {
76124
* }
77125
* ```
78126
*/
79-
dynamic run(fn()) => _innerZone.run(fn);
127+
dynamic run(fn()) {
128+
// Using runGuarded() is required when executing sync code with Dart otherwise handleUncaughtError()
129+
// would not be called on exceptions.
130+
// see https://code.google.com/p/dart/issues/detail?id=19566 for details.
131+
return _innerZone.runGuarded(fn);
132+
}
80133

81134
/**
82135
* Runs `fn` in the outer zone and returns whatever it returns.
@@ -97,81 +150,94 @@ class VmTurnZone {
97150
* }
98151
* ```
99152
*/
100-
dynamic runOutsideAngular(fn()) => _outerZone.run(fn);
153+
dynamic runOutsideAngular(fn()) {
154+
return _outerZone.runGuarded(fn);
155+
}
101156

102-
async.Zone _createInnerZoneWithErrorHandling(bool enableLongStackTrace) {
103-
if (enableLongStackTrace) {
104-
return Chain.capture(() {
105-
return _createInnerZone(async.Zone.current);
106-
}, onError: this._onErrorWithLongStackTrace);
107-
} else {
108-
return async.runZoned(() {
109-
return _createInnerZone(async.Zone.current);
110-
}, onError: this._onErrorWithoutLongStackTrace);
111-
}
157+
// Executes code in the [_innerZone] & trigger the onTurnStart hook when code is executed for the
158+
// first time in a turn.
159+
dynamic _innerRun(Zone self, ZoneDelegate parent, Zone zone, fn()) {
160+
_maybeStartVmTurn(parent, zone);
161+
return parent.run(zone, fn);
112162
}
113163

114-
async.Zone _createInnerZone(async.Zone zone) {
115-
return zone.fork(
116-
specification: new async.ZoneSpecification(
117-
run: _onRun,
118-
runUnary: _onRunUnary,
119-
scheduleMicrotask: _onMicrotask));
164+
dynamic _innerRunUnary(Zone self, ZoneDelegate parent, Zone zone, fn(arg), arg) {
165+
_maybeStartVmTurn(parent, zone);
166+
return parent.runUnary(zone, fn, arg);
120167
}
121168

122-
dynamic _onRunBase(
123-
async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn()) {
124-
_nestedRunCounter++;
125-
try {
126-
if (_nestedRunCounter == 1 && _onTurnStart != null) delegate.run(
127-
zone, _onTurnStart);
128-
return fn();
129-
} catch (e, s) {
130-
if (_onErrorHandler != null && _nestedRunCounter == 1) {
131-
_onErrorHandler(e, [s.toString()]);
132-
} else {
133-
rethrow;
169+
dynamic _innerRunBinary(Zone self, ZoneDelegate parent, Zone zone, fn(arg1, arg2), arg1, arg2) {
170+
_maybeStartVmTurn(parent, zone);
171+
return parent.runBinary(zone, fn, arg1, arg2);
172+
}
173+
174+
void _maybeStartVmTurn(ZoneDelegate parent, Zone zone) {
175+
if (!_hasExecutedCodeInInnerZone) {
176+
_hasExecutedCodeInInnerZone = true;
177+
if (_onTurnStart != null) {
178+
_inTurnStart = true;
179+
parent.run(zone, _onTurnStart);
134180
}
181+
}
182+
}
183+
184+
dynamic _outerRun(Zone self, ZoneDelegate parent, Zone zone, fn()) {
185+
try {
186+
_nestedRun++;
187+
return parent.run(zone, fn);
135188
} finally {
136-
_nestedRunCounter--;
137-
if (_nestedRunCounter == 0 && _onTurnDone != null) _finishTurn(
138-
zone, delegate);
189+
_nestedRun--;
190+
// If there are no more pending microtasks, we are at the end of a VM turn (or in onTurnStart)
191+
// _nestedRun will be 0 at the end of a macrotasks (it could be > 0 when there are nested calls
192+
// to _outerRun()).
193+
if (_pendingMicrotasks == 0 && _nestedRun == 0) {
194+
if (_onTurnDone != null && !_inTurnStart && _hasExecutedCodeInInnerZone) {
195+
// Trigger onTurnDone at the end of a turn if _innerZone has executed some code
196+
try {
197+
parent.run(_innerZone, _onTurnDone);
198+
} finally {
199+
_hasExecutedCodeInInnerZone = false;
200+
}
201+
}
202+
}
203+
_inTurnStart = false;
139204
}
140205
}
141206

142-
dynamic _onRun(async.Zone self, async.ZoneDelegate delegate, async.Zone zone,
143-
fn()) => _onRunBase(self, delegate, zone, () => delegate.run(zone, fn));
207+
dynamic _outerRunUnary(Zone self, ZoneDelegate parent, Zone zone, fn(arg), arg) =>
208+
_outerRun(self, parent, zone, () => fn(arg));
144209

145-
dynamic _onRunUnary(async.Zone self, async.ZoneDelegate delegate,
146-
async.Zone zone, fn(args), args) =>
147-
_onRunBase(self, delegate, zone, () => delegate.runUnary(zone, fn, args));
210+
dynamic _outerRunBinary(Zone self, ZoneDelegate parent, Zone zone, fn(arg1, arg2), arg1, arg2) =>
211+
_outerRun(self, parent, zone, () => fn(arg1, arg2));
148212

149-
void _finishTurn(zone, delegate) {
150-
delegate.run(zone, _onTurnDone);
213+
void _scheduleMicrotask(Zone self, ZoneDelegate parent, Zone zone, fn) {
214+
_pendingMicrotasks++;
215+
var microtask = () {
216+
try {
217+
fn();
218+
} finally {
219+
_pendingMicrotasks--;
220+
}
221+
};
222+
parent.scheduleMicrotask(zone, microtask);
151223
}
152224

153-
_onMicrotask(
154-
async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn) {
155-
if (this._onScheduleMicrotask != null) {
156-
_onScheduleMicrotask(fn);
225+
// Called by Chain.capture() on errors when long stack traces are enabled
226+
void _onErrorWithLongStackTrace(error, Chain chain) {
227+
if (_onErrorHandler != null) {
228+
final traces = chain.terse.traces.map((t) => t.toString()).toList();
229+
_onErrorHandler(error, traces);
157230
} else {
158-
delegate.scheduleMicrotask(zone, fn);
231+
throw error;
159232
}
160233
}
161234

162-
_onErrorWithLongStackTrace(exception, Chain chain) {
163-
final traces = chain.terse.traces.map((t) => t.toString()).toList();
164-
_onError(exception, traces, chain.traces[0]);
165-
}
166-
_onErrorWithoutLongStackTrace(exception, StackTrace trace) {
167-
_onError(exception, [trace.toString()], trace);
168-
}
169-
170-
_onError(exception, List<String> traces, StackTrace singleTrace) {
235+
// Outer zone handleUnchaughtError when long stack traces are not used
236+
void _onErrorWithoutLongStackTrace(error, StackTrace trace) {
171237
if (_onErrorHandler != null) {
172-
_onErrorHandler(exception, traces);
238+
_onErrorHandler(error, [trace.toString()]);
173239
} else {
174-
_outerZone.handleUncaughtError(exception, singleTrace);
240+
throw error;
175241
}
176242
}
177243
}

0 commit comments

Comments
 (0)
X Tutup