feat(zone): add support for long stack traces

This commit is contained in:
vsavkin 2014-12-11 11:36:05 -08:00
parent d5fcac4d7a
commit df21c3c77d
14 changed files with 283 additions and 49 deletions

View File

@ -31,6 +31,7 @@ var _HTLM_DEFAULT_SCRIPTS_JS = [
{src: '/rtts_assert/lib/rtts_assert.js', mimeType: 'text/javascript'}, {src: '/rtts_assert/lib/rtts_assert.js', mimeType: 'text/javascript'},
{src: '/deps/es6-module-loader-sans-promises.src.js', mimeType: 'text/javascript'}, {src: '/deps/es6-module-loader-sans-promises.src.js', mimeType: 'text/javascript'},
{src: '/deps/zone.js', mimeType: 'text/javascript'}, {src: '/deps/zone.js', mimeType: 'text/javascript'},
{src: '/deps/long-stack-trace-zone.js', mimeType: 'text/javascript'},
{src: '/deps/system.src.js', mimeType: 'text/javascript'}, {src: '/deps/system.src.js', mimeType: 'text/javascript'},
{src: '/deps/extension-register.js', mimeType: 'text/javascript'}, {src: '/deps/extension-register.js', mimeType: 'text/javascript'},
{src: '/deps/runtime_paths.js', mimeType: 'text/javascript'}, {src: '/deps/runtime_paths.js', mimeType: 'text/javascript'},
@ -68,6 +69,7 @@ var CONFIG = {
"node_modules/systemjs/dist/system.src.js", "node_modules/systemjs/dist/system.src.js",
"node_modules/systemjs/lib/extension-register.js", "node_modules/systemjs/lib/extension-register.js",
"node_modules/zone.js/zone.js", "node_modules/zone.js/zone.js",
"node_modules/zone.js/long-stack-trace-zone.js",
"tools/build/runtime_paths.js", "tools/build/runtime_paths.js",
"node_modules/angular/angular.js" "node_modules/angular/angular.js"
] ]

View File

@ -20,6 +20,7 @@ module.exports = function(config) {
'node_modules/systemjs/dist/system.src.js', 'node_modules/systemjs/dist/system.src.js',
'node_modules/systemjs/lib/extension-register.js', 'node_modules/systemjs/lib/extension-register.js',
'node_modules/zone.js/zone.js', 'node_modules/zone.js/zone.js',
'node_modules/zone.js/long-stack-trace-zone.js',
'tools/build/file2modulename.js', 'tools/build/file2modulename.js',
'test-main.js' 'test-main.js'

View File

@ -2,6 +2,7 @@ name: core
environment: environment:
sdk: '>=1.4.0' sdk: '>=1.4.0'
dependencies: dependencies:
stack_trace: '1.1.1'
change_detection: change_detection:
path: ../change_detection path: ../change_detection
di: di:

View File

@ -1,5 +1,5 @@
import {Injector, bind, OpaqueToken} from 'di/di'; import {Injector, bind, OpaqueToken} from 'di/di';
import {Type, FIELD, isBlank, isPresent, BaseException, assertionsEnabled} from 'facade/lang'; import {Type, FIELD, isBlank, isPresent, BaseException, assertionsEnabled, print} from 'facade/lang';
import {DOM, Element} from 'facade/dom'; import {DOM, Element} from 'facade/dom';
import {Compiler, CompilerCache} from './compiler/compiler'; import {Compiler, CompilerCache} from './compiler/compiler';
import {ProtoView} from './compiler/view'; import {ProtoView} from './compiler/view';
@ -11,7 +11,8 @@ import {RecordRange} from 'change_detection/record_range';
import {TemplateLoader} from './compiler/template_loader'; import {TemplateLoader} from './compiler/template_loader';
import {DirectiveMetadataReader} from './compiler/directive_metadata_reader'; import {DirectiveMetadataReader} from './compiler/directive_metadata_reader';
import {AnnotatedType} from './compiler/annotated_type'; import {AnnotatedType} from './compiler/annotated_type';
import {ListWrapper} from 'facade/collection'; import {List, ListWrapper} from 'facade/collection';
import {PromiseWrapper} from 'facade/async';
import {VmTurnZone} from 'core/zone/vm_turn_zone'; import {VmTurnZone} from 'core/zone/vm_turn_zone';
import {LifeCycle} from 'core/life_cycle/life_cycle'; import {LifeCycle} from 'core/life_cycle/life_cycle';
@ -77,24 +78,47 @@ function _injectorBindings(appComponentType) {
documentDependentBindings(appComponentType)); documentDependentBindings(appComponentType));
} }
function _createVmZone(givenReporter:Function){
var defaultErrorReporter = (exception, stackTrace) => {
var longStackTrace = ListWrapper.join(stackTrace, "\n\n-----async gap-----\n");
print(`${exception}\n\n${longStackTrace}`);
throw exception;
};
var reporter = isPresent(givenReporter) ? givenReporter : defaultErrorReporter;
var zone = new VmTurnZone({enableLongStackTrace: assertionsEnabled()});
zone.initCallbacks({onErrorHandler: reporter});
return zone;
}
// Multiple calls to this method are allowed. Each application would only share // Multiple calls to this method are allowed. Each application would only share
// _rootInjector, which is not user-configurable by design, thus safe to share. // _rootInjector, which is not user-configurable by design, thus safe to share.
export function bootstrap(appComponentType: Type, bindings=null) { export function bootstrap(appComponentType: Type, bindings=null, givenBootstrapErrorReporter=null) {
// TODO(rado): prepopulate template cache, so applications with only var bootstrapProcess = PromiseWrapper.completer();
// index.html and main.js are possible.
var zone = _createVmZone(givenBootstrapErrorReporter);
zone.run(() => {
// TODO(rado): prepopulate template cache, so applications with only
// index.html and main.js are possible.
var zone = new VmTurnZone();
return zone.run(() => {
if (isBlank(_rootInjector)) _rootInjector = new Injector(_rootBindings); if (isBlank(_rootInjector)) _rootInjector = new Injector(_rootBindings);
var appInjector = _rootInjector.createChild(_injectorBindings(appComponentType)); var appInjector = _rootInjector.createChild(_injectorBindings(appComponentType));
if (isPresent(bindings)) appInjector = appInjector.createChild(bindings); if (isPresent(bindings)) appInjector = appInjector.createChild(bindings);
return appInjector.asyncGet(LifeCycle). PromiseWrapper.then(appInjector.asyncGet(LifeCycle),
then((lc) => { (lc) => {
lc.registerWith(zone); lc.registerWith(zone);
lc.tick(); lc.tick(); //the first tick that will bootstrap the app
}).
then((_) => appInjector); bootstrapProcess.complete(appInjector);
},
(err) => {
bootstrapProcess.reject(err)
});
}); });
return bootstrapProcess.promise;
} }

View File

@ -1,6 +1,7 @@
import {FIELD} from 'facade/lang'; import {FIELD, print} from 'facade/lang';
import {ChangeDetector} from 'change_detection/change_detector'; import {ChangeDetector} from 'change_detection/change_detector';
import {VmTurnZone} from 'core/zone/vm_turn_zone'; import {VmTurnZone} from 'core/zone/vm_turn_zone';
import {ListWrapper} from 'facade/collection';
export class LifeCycle { export class LifeCycle {
_changeDetector:ChangeDetector; _changeDetector:ChangeDetector;
@ -10,7 +11,15 @@ export class LifeCycle {
} }
registerWith(zone:VmTurnZone) { registerWith(zone:VmTurnZone) {
// temporary error handler, we should inject one
var errorHandler = (exception, stackTrace) => {
var longStackTrace = ListWrapper.join(stackTrace, "\n\n-----async gap-----\n");
print(`${exception}\n\n${longStackTrace}`);
throw exception;
};
zone.initCallbacks({ zone.initCallbacks({
onErrorHandler: errorHandler,
onTurnDone: () => this.tick() onTurnDone: () => this.tick()
}); });
} }

View File

@ -1,45 +1,68 @@
library angular.zone; library angular.zone;
import 'dart:async' as async; import 'dart:async' as async;
import 'package:stack_trace/stack_trace.dart' show Chain;
class VmTurnZone { class VmTurnZone {
Function _onTurnStart; Function _onTurnStart;
Function _onTurnDone; Function _onTurnDone;
Function _onScheduleMicrotask; Function _onScheduleMicrotask;
Function _onErrorHandler;
async.Zone _outerZone; async.Zone _outerZone;
async.Zone _innerZone; async.Zone _innerZone;
int _nestedRunCounter; int _nestedRunCounter;
VmTurnZone() { VmTurnZone({bool enableLongStackTrace}) {
_nestedRunCounter = 0; _nestedRunCounter = 0;
_outerZone = async.Zone.current; _outerZone = async.Zone.current;
_innerZone = _outerZone.fork(specification: new async.ZoneSpecification( _innerZone = _createInnerZoneWithErrorHandling(enableLongStackTrace);
}
initCallbacks({Function onTurnStart, Function onTurnDone, Function onScheduleMicrotask, Function onErrorHandler}) {
this._onTurnStart = onTurnStart;
this._onTurnDone = onTurnDone;
this._onScheduleMicrotask = onScheduleMicrotask;
this._onErrorHandler = onErrorHandler;
}
dynamic run(fn()) => _innerZone.run(fn);
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);
}
}
async.Zone _createInnerZone(async.Zone zone) {
return zone.fork(specification: new async.ZoneSpecification(
run: _onRun, run: _onRun,
runUnary: _onRunUnary, runUnary: _onRunUnary,
scheduleMicrotask: _onMicrotask scheduleMicrotask: _onMicrotask
)); ));
} }
initCallbacks({Function onTurnStart, Function onTurnDone, Function onScheduleMicrotask}) {
this._onTurnStart = onTurnStart;
this._onTurnDone = onTurnDone;
this._onScheduleMicrotask = onScheduleMicrotask;
}
dynamic run(fn()) => _innerZone.run(fn);
dynamic runOutsideAngular(fn()) => _outerZone.run(fn);
dynamic _onRunBase(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn()) { dynamic _onRunBase(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn()) {
_nestedRunCounter++; _nestedRunCounter++;
try { try {
if (_nestedRunCounter == 1 && _onTurnStart != null) delegate.run(zone, _onTurnStart); if (_nestedRunCounter == 1 && _onTurnStart != null) delegate.run(zone, _onTurnStart);
return fn(); return fn();
} catch (e, s) {
if (_onErrorHandler != null && _nestedRunCounter == 1) {
_onErrorHandler(e, [s.toString()]);
} else {
rethrow;
}
} finally { } finally {
_nestedRunCounter--; _nestedRunCounter--;
if (_nestedRunCounter == 0 && _onTurnDone != null) _finishTurn(zone, delegate); if (_nestedRunCounter == 0 && _onTurnDone != null) _finishTurn(zone, delegate);
@ -47,10 +70,10 @@ class VmTurnZone {
} }
dynamic _onRun(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn()) => dynamic _onRun(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn()) =>
_onRunBase(self, delegate, zone, () => delegate.run(zone, fn)); _onRunBase(self, delegate, zone, () => delegate.run(zone, fn));
dynamic _onRunUnary(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn(args), args) => dynamic _onRunUnary(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn(args), args) =>
_onRunBase(self, delegate, zone, () => delegate.runUnary(zone, fn, args)); _onRunBase(self, delegate, zone, () => delegate.runUnary(zone, fn, args));
void _finishTurn(zone, delegate) { void _finishTurn(zone, delegate) {
delegate.run(zone, _onTurnDone); delegate.run(zone, _onTurnDone);
@ -58,9 +81,25 @@ class VmTurnZone {
_onMicrotask(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn) { _onMicrotask(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn) {
if (this._onScheduleMicrotask != null) { if (this._onScheduleMicrotask != null) {
this._onScheduleMicrotask(fn); _onScheduleMicrotask(fn);
} else { } else {
delegate.scheduleMicrotask(zone, fn); 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) {
if (_onErrorHandler != null) {
_onErrorHandler(exception, traces);
} else {
_outerZone.handleUncaughtError(exception, singleTrace);
}
}
} }

View File

@ -1,5 +1,5 @@
import {List, ListWrapper} from 'facade/collection'; import {List, ListWrapper, StringMapWrapper} from 'facade/collection';
import {normalizeBlank} from 'facade/lang'; import {normalizeBlank, isPresent} from 'facade/lang';
export class VmTurnZone { export class VmTurnZone {
_outerZone; _outerZone;
@ -7,24 +7,24 @@ export class VmTurnZone {
_onTurnStart:Function; _onTurnStart:Function;
_onTurnDone:Function; _onTurnDone:Function;
_onErrorHandler:Function;
_nestedRunCounter:number; _nestedRunCounter:number;
constructor() { constructor({enableLongStackTrace}) {
this._nestedRunCounter = 0; this._nestedRunCounter = 0;
this._onTurnStart = null; this._onTurnStart = null;
this._onTurnDone = null; this._onTurnDone = null;
this._onErrorHandler = null;
this._outerZone = window.zone; this._outerZone = window.zone;
this._innerZone = this._outerZone.fork({ this._innerZone = this._createInnerZone(this._outerZone, enableLongStackTrace);
beforeTask: () => this._beforeTask(),
afterTask: () => this._afterTask()
});
} }
initCallbacks({onTurnStart, onTurnDone, onScheduleMicrotask} = {}) { initCallbacks({onTurnStart, onTurnDone, onScheduleMicrotask, onErrorHandler} = {}) {
this._onTurnStart = normalizeBlank(onTurnStart); this._onTurnStart = normalizeBlank(onTurnStart);
this._onTurnDone = normalizeBlank(onTurnDone); this._onTurnDone = normalizeBlank(onTurnDone);
this._onErrorHandler = normalizeBlank(onErrorHandler);
} }
run(fn) { run(fn) {
@ -35,6 +35,29 @@ export class VmTurnZone {
return this._outerZone.run(fn); return this._outerZone.run(fn);
} }
_createInnerZone(zone, enableLongStackTrace) {
var vmTurnZone = this;
var errorHandling;
if (enableLongStackTrace) {
errorHandling = StringMapWrapper.merge(Zone.longStackTraceZone, {
onError: function (e) {
vmTurnZone._onError(this, e)
}
});
} else {
errorHandling = {
onError: function (e) {
vmTurnZone._onError(this, e)
}
};
}
return zone.fork(errorHandling).fork({
beforeTask: () => {this._beforeTask()},
afterTask: () => {this._afterTask()}
});
}
_beforeTask(){ _beforeTask(){
this._nestedRunCounter ++; this._nestedRunCounter ++;
@ -49,4 +72,18 @@ export class VmTurnZone {
this._onTurnDone(); this._onTurnDone();
} }
} }
_onError(zone, e) {
if (isPresent(this._onErrorHandler)) {
var trace = [normalizeBlank(e.stack)];
while (zone && zone.constructedAtException) {
trace.push(zone.constructedAtException.get());
zone = zone.parent;
}
this._onErrorHandler(e, trace);
} else {
throw e;
}
}
} }

View File

@ -54,7 +54,7 @@ export function main() {
describe('bootstrap factory method', () => { describe('bootstrap factory method', () => {
it('should throw if no element is found', (done) => { it('should throw if no element is found', (done) => {
var injectorPromise = bootstrap(HelloRootCmp); var injectorPromise = bootstrap(HelloRootCmp, [], (e,t) => {throw e;});
PromiseWrapper.then(injectorPromise, null, (reason) => { PromiseWrapper.then(injectorPromise, null, (reason) => {
expect(reason.message).toContain( expect(reason.message).toContain(
'The app selector "hello-app" did not match any elements'); 'The app selector "hello-app" did not match any elements');

1
modules/core/test/packages Symbolic link
View File

@ -0,0 +1 @@
../packages

View File

@ -10,7 +10,7 @@ export function main() {
beforeEach(() => { beforeEach(() => {
log = new Log(); log = new Log();
zone = new VmTurnZone(); zone = new VmTurnZone({enableLongStackTrace: true});
zone.initCallbacks({ zone.initCallbacks({
onTurnStart: log.fn('onTurnStart'), onTurnStart: log.fn('onTurnStart'),
onTurnDone: log.fn('onTurnDone') onTurnDone: log.fn('onTurnDone')
@ -73,12 +73,95 @@ export function main() {
}); });
describe("exceptions", () => { describe("exceptions", () => {
it('should rethrow exceptions from the body', () => { var trace, exception, saveStackTrace;
beforeEach(() => {
trace = null;
exception = null;
saveStackTrace = (e, t) => {
exception = e;
trace = t;
};
});
it('should call the on error callback when it is defined', () => {
zone.initCallbacks({onErrorHandler: saveStackTrace});
zone.run(() => {
throw new BaseException('aaa');
});
expect(exception).toBeDefined();
});
it('should rethrow exceptions from the body when no callback defined', () => {
expect(() => { expect(() => {
zone.run(() => { zone.run(() => {
throw new BaseException('hello'); throw new BaseException('bbb');
}); });
}).toThrowError('hello'); }).toThrowError('bbb');
});
it('should produce long stack traces', (done) => {
zone.initCallbacks({onErrorHandler: saveStackTrace});
var c = PromiseWrapper.completer();
zone.run(function () {
PromiseWrapper.setTimeout(function () {
PromiseWrapper.setTimeout(function () {
c.complete(null);
throw new BaseException('ccc');
}, 0);
}, 0);
});
c.promise.then((_) => {
// then number of traces for JS and Dart is different
expect(trace.length).toBeGreaterThan(1);
done();
});
});
it('should produce long stack traces (when using promises)', (done) => {
zone.initCallbacks({onErrorHandler: saveStackTrace});
var c = PromiseWrapper.completer();
zone.run(function () {
PromiseWrapper.resolve(null).then((_) => {
return PromiseWrapper.resolve(null).then((__) => {
c.complete(null);
throw new BaseException("ddd");
});
});
});
c.promise.then((_) => {
// then number of traces for JS and Dart is different
expect(trace.length).toBeGreaterThan(1);
done();
});
});
it('should disable long stack traces', (done) => {
var zone = new VmTurnZone({enableLongStackTrace: false});
zone.initCallbacks({onErrorHandler: saveStackTrace});
var c = PromiseWrapper.completer();
zone.run(function () {
PromiseWrapper.setTimeout(function () {
PromiseWrapper.setTimeout(function () {
c.complete(null);
throw new BaseException('ccc');
}, 0);
}, 0);
});
c.promise.then((_) => {
expect(trace.length).toEqual(1);
done();
});
}); });
}); });
}); });

View File

@ -12,11 +12,11 @@ class PromiseWrapper {
return new Future.error(obj); return new Future.error(obj);
} }
static Future<List> all(List<Future> promises){ static Future<List> all(List<Future> promises) {
return Future.wait(promises); return Future.wait(promises);
} }
static Future then(Future promise, Function success, Function onError){ static Future then(Future promise, Function success, Function onError) {
if (success == null) return promise.catchError(onError); if (success == null) return promise.catchError(onError);
return promise.then(success, onError: onError); return promise.then(success, onError: onError);
} }
@ -24,13 +24,24 @@ class PromiseWrapper {
static completer(){ static completer(){
return new _Completer(new Completer()); return new _Completer(new Completer());
} }
static setTimeout(fn, millis) {
new Timer(new Duration(milliseconds: millis), fn);
}
} }
class _Completer { class _Completer {
Completer c; Completer c;
_Completer(this.c); _Completer(this.c);
get promise => c.future; get promise => c.future;
get complete => c.complete;
get reject => c.completeError; complete(v) {
c.complete(v);
}
reject(v) {
c.completeError(v);
}
} }

View File

@ -33,4 +33,8 @@ export class PromiseWrapper {
reject: reject reject: reject
}; };
} }
static setTimeout(fn, millis) {
window.setTimeout(fn, millis);
}
} }

View File

@ -59,6 +59,24 @@ export class StringMapWrapper {
} }
} }
} }
static merge(m1, m2) {
var m = {};
for (var attr in m1) {
if (m1.hasOwnProperty(attr)){
m[attr] = m1[attr];
}
}
for (var attr in m2) {
if (m2.hasOwnProperty(attr)){
m[attr] = m2[attr];
}
}
return m;
}
} }
export class ListWrapper { export class ListWrapper {

View File

@ -214,4 +214,8 @@ export function assertionsEnabled() {
} catch (e) { } catch (e) {
return true; return true;
} }
}
export function print(obj) {
console.log(obj);
} }