Skip to content

Commit

Permalink
feat(zone.js): add AsyncStackTaggingZoneSpec implementation (#46693)
Browse files Browse the repository at this point in the history
Chrome has an experimental API to improve the debug experience of the
async tasks.
The details can be found here https://bugs.chromium.org/p/chromium/issues/detail?id=332624#c29

This commit add the `async stack tagging` support in the `zone.js`.
User can `import 'zone.js/plugins/async-stack-tagging';` to enable this
feature.

PR Close #46693
  • Loading branch information
JiaLiPassion authored and thePunderWoman committed Jul 11, 2022
1 parent b6a950c commit 848a009
Show file tree
Hide file tree
Showing 13 changed files with 237 additions and 5 deletions.
3 changes: 3 additions & 0 deletions packages/zone.js/bundles.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ BUNDLES_ENTRY_POINTS = {
"async-test": {
"entrypoint": _DIR + "testing/async-testing",
},
"async-stack-tagging": {
"entrypoint": _DIR + "zone-spec/async-stack-tagging",
},
"fake-async-test": {
"entrypoint": _DIR + "testing/fake-async",
},
Expand Down
2 changes: 2 additions & 0 deletions packages/zone.js/dist/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ js_library(
filegroup(
name = "dist_bundle_group",
srcs = [
":async-stack-tagging.js",
":async-stack-tagging.min.js",
":async-test.js",
":async-test.min.js",
":fake-async-test.js",
Expand Down
1 change: 1 addition & 0 deletions packages/zone.js/dist/tools.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ ES5_BUNDLES = [
"zone-node",
"zone-testing-node-bundle",
"async-test",
"async-stack-tagging",
"fake-async-test",
"long-stack-trace-zone",
"proxy",
Expand Down
67 changes: 67 additions & 0 deletions packages/zone.js/lib/zone-spec/async-stack-tagging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

interface Console {
scheduleAsyncTask(name: string, recurring?: boolean): number;
startAsyncTask(task: number): void;
finishAsyncTask(task: number): void;
cancelAsyncTask(task: number): void;
}

interface Task {
asyncId?: number;
}

class AsyncStackTaggingZoneSpec implements ZoneSpec {
scheduleAsyncTask: Console['scheduleAsyncTask'];
startAsyncTask: Console['startAsyncTask'];
finishAsyncTask: Console['finishAsyncTask'];
cancelAsyncTask: Console['finishAsyncTask'];

constructor(namePrefix: string, consoleAsyncStackTaggingImpl: Console = console) {
this.name = 'asyncStackTagging for ' + namePrefix;
this.scheduleAsyncTask = consoleAsyncStackTaggingImpl?.scheduleAsyncTask ?? (() => {});
this.startAsyncTask = consoleAsyncStackTaggingImpl?.startAsyncTask ?? (() => {});
this.finishAsyncTask = consoleAsyncStackTaggingImpl?.finishAsyncTask ?? (() => {});
this.cancelAsyncTask = consoleAsyncStackTaggingImpl?.cancelAsyncTask ?? (() => {});
}

// ZoneSpec implementation below.

name: string;

onScheduleTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task {
task.asyncId = this.scheduleAsyncTask(
task.source || task.type, task.data?.isPeriodic || task.type === 'eventTask');
return delegate.scheduleTask(target, task);
}

onInvokeTask(
delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task, applyThis: any,
applyArgs?: any[]) {
task.asyncId && this.startAsyncTask(task.asyncId);
try {
return delegate.invokeTask(targetZone, task, applyThis, applyArgs);
} finally {
task.asyncId && this.finishAsyncTask(task.asyncId);
if (task.type !== 'eventTask' && !task.data?.isPeriodic) {
task.asyncId = undefined;
}
}
}

onCancelTask(delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) {
task.asyncId && this.cancelAsyncTask(task.asyncId);
task.asyncId = undefined;
return delegate.cancelTask(targetZone, task);
}
}

// Export the class so that new instances can be created with proper
// constructor params.
(Zone as any)['AsyncStackTaggingZoneSpec'] = AsyncStackTaggingZoneSpec;
2 changes: 2 additions & 0 deletions packages/zone.js/plugins/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package(default_visibility = ["//visibility:public"])
filegroup(
name = "plugin_bundle_group",
srcs = [
"//packages/zone.js/plugins:async-stack-tagging.min/package.json",
"//packages/zone.js/plugins:async-stack-tagging/package.json",
"//packages/zone.js/plugins:async-test.min/package.json",
"//packages/zone.js/plugins:async-test/package.json",
"//packages/zone.js/plugins:fake-async-test.min/package.json",
Expand Down
7 changes: 7 additions & 0 deletions packages/zone.js/plugins/async-stack-tagging.min/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "zone.js/async-stack-tagging.min",
"main": "../../bundles/async-stack-tagging.umd.min.js",
"fesm2015": "../../fesm2015/async-stack-tagging.min.js",
"es2015": "../../fesm2015/async-stack-tagging.min.js",
"module": "../../fesm2015/async-stack-tagging.min.js"
}
7 changes: 7 additions & 0 deletions packages/zone.js/plugins/async-stack-tagging/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "zone.js/async-stack-tagging",
"main": "../../bundles/async-stack-tagging.umd.js",
"fesm2015": "../../fesm2015/async-stack-tagging.js",
"es2015": "../../fesm2015/async-stack-tagging.js",
"module": "../../fesm2015/async-stack-tagging.js"
}
2 changes: 2 additions & 0 deletions packages/zone.js/test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ ts_library(
exclude = [
"common/Error.spec.ts",
"common/promise-disable-wrap-uncaught-promise-rejection.spec.ts",
"zone-spec/async-tagging-console.spec.ts",
],
),
deps = [
Expand Down Expand Up @@ -264,6 +265,7 @@ test_srcs = glob(
"jasmine-patch.spec.ts",
"common_tests.ts",
"browser_entry_point.ts",
"zone-spec/async-tagging-console.spec.ts",
]

test_deps = [
Expand Down
1 change: 1 addition & 0 deletions packages/zone.js/test/browser-zone-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import '../lib/browser/webapis-media-query';
import '../lib/testing/zone-testing';
import '../lib/zone-spec/task-tracking';
import '../lib/zone-spec/wtf';
import '../lib/zone-spec/async-stack-tagging';
import '../lib/extra/cordova';
import '../lib/testing/promise-testing';
import '../lib/testing/async-testing';
Expand Down
2 changes: 1 addition & 1 deletion packages/zone.js/test/browser_entry_point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
import '../lib/common/error-rewrite';

// import 'core-js/features/set';
// import 'core-js/features/map';
// List all tests here:
Expand All @@ -30,3 +29,4 @@ import './jasmine-patch.spec';
import './browser/messageport.spec';
import './extra/cordova.spec';
import './browser/queue-microtask.spec';
import './zone-spec/async-tagging-console.spec';
1 change: 1 addition & 0 deletions packages/zone.js/test/karma_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def karma_test(name, env_srcs, env_deps, env_entry_point, test_srcs, test_deps,
"//packages/zone.js/bundles:zone-patch-resize-observer.umd.js",
"//packages/zone.js/bundles:zone-patch-message-port.umd.js",
"//packages/zone.js/bundles:zone-patch-user-media.umd.js",
"//packages/zone.js/bundles:async-stack-tagging.umd.js",
":" + name + "_rollup.umd",
]

Expand Down
13 changes: 9 additions & 4 deletions packages/zone.js/test/npm_package/npm_package.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ function checkInSubFolder(subFolder: string, testFn: Function) {
}

describe('Zone.js npm_package', () => {
beforeEach(
() => {shx.cd(
path.dirname(require.resolve('angular/packages/zone.js/npm_package/package.json')))});
beforeEach(() => {
shx.cd(path.dirname(require.resolve('angular/packages/zone.js/npm_package/package.json')));
});
describe('misc root files', () => {
describe('README.md', () => {
it('should have a README.md file with basic info', () => {
Expand Down Expand Up @@ -112,10 +112,11 @@ describe('Zone.js npm_package', () => {
});
});


describe('plugins folder check', () => {
it('should contain all plugin folders in ./plugins', () => {
const expected = [
'async-stack-tagging',
'async-stack-tagging.min',
'async-test',
'async-test.min',
'fake-async-test',
Expand Down Expand Up @@ -196,6 +197,8 @@ describe('Zone.js npm_package', () => {
describe('bundles file list', () => {
it('should contain all files', () => {
const expected = [
'async-stack-tagging.js',
'async-stack-tagging.min.js',
'async-test.js',
'async-test.min.js',
'fake-async-test.js',
Expand Down Expand Up @@ -290,6 +293,8 @@ describe('Zone.js npm_package', () => {
it('should contain all original folders in /dist', () => {
const list = shx.ls('./dist').stdout.split('\n').sort().slice(1);
const expected = [
'async-stack-tagging.js',
'async-stack-tagging.min.js',
'async-test.js',
'async-test.min.js',
'fake-async-test.js',
Expand Down
134 changes: 134 additions & 0 deletions packages/zone.js/test/zone-spec/async-tagging-console.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {ifEnvSupports, ifEnvSupportsWithDone} from '../test-util';

describe('AsyncTaggingConsoleTest', () => {
const AsyncStackTaggingZoneSpec = (Zone as any)['AsyncStackTaggingZoneSpec'];

describe('should call console async stack tagging API', () => {
let idx = 1;
const scheduleAsyncTaskSpy = jasmine.createSpy('scheduleAsyncTask').and.callFake(() => {
return idx++;
});
const startAsyncTaskSpy = jasmine.createSpy('startAsyncTask');
const finishAsyncTaskSpy = jasmine.createSpy('finishAsyncTask');
const cancelAsyncTaskSpy = jasmine.createSpy('cancelAsyncTask');
let asyncStackTaggingZone: Zone;

beforeEach(() => {
scheduleAsyncTaskSpy.calls.reset();
startAsyncTaskSpy.calls.reset();
finishAsyncTaskSpy.calls.reset();
cancelAsyncTaskSpy.calls.reset();
asyncStackTaggingZone = Zone.current.fork(new AsyncStackTaggingZoneSpec('test', {
scheduleAsyncTask: scheduleAsyncTaskSpy,
startAsyncTask: startAsyncTaskSpy,
finishAsyncTask: finishAsyncTaskSpy,
cancelAsyncTask: cancelAsyncTaskSpy,
}));
});
it('setTimeout', (done: DoneFn) => {
asyncStackTaggingZone.run(() => {
setTimeout(() => {});
});
setTimeout(() => {
expect(scheduleAsyncTaskSpy).toHaveBeenCalledWith('setTimeout', false);
expect(startAsyncTaskSpy.calls.count()).toBe(1);
expect(finishAsyncTaskSpy.calls.count()).toBe(1);
done();
});
});
it('clearTimeout', (done: DoneFn) => {
asyncStackTaggingZone.run(() => {
const id = setTimeout(() => {});
clearTimeout(id);
});
setTimeout(() => {
expect(scheduleAsyncTaskSpy).toHaveBeenCalledWith('setTimeout', false);
expect(startAsyncTaskSpy).not.toHaveBeenCalled();
expect(finishAsyncTaskSpy).not.toHaveBeenCalled();
expect(cancelAsyncTaskSpy.calls.count()).toBe(1);
done();
});
});
it('setInterval', (done: DoneFn) => {
asyncStackTaggingZone.run(() => {
let count = 0;
const id = setInterval(() => {
count++;
if (count === 2) {
clearInterval(id);
}
}, 10);
});
setTimeout(() => {
expect(scheduleAsyncTaskSpy).toHaveBeenCalledWith('setInterval', true);
expect(startAsyncTaskSpy.calls.count()).toBe(2);
expect(finishAsyncTaskSpy.calls.count()).toBe(1);
expect(cancelAsyncTaskSpy.calls.count()).toBe(1);
done();
}, 50);
});
it('Promise', (done: DoneFn) => {
asyncStackTaggingZone.run(() => {
Promise.resolve(1).then(() => {});
});
setTimeout(() => {
expect(scheduleAsyncTaskSpy).toHaveBeenCalledWith('Promise.then', false);
expect(startAsyncTaskSpy.calls.count()).toBe(1);
expect(finishAsyncTaskSpy.calls.count()).toBe(1);
done();
});
});

it('XMLHttpRequest', ifEnvSupportsWithDone('XMLHttpRequest', (done: DoneFn) => {
asyncStackTaggingZone.run(() => {
const req = new XMLHttpRequest();
req.onload = () => {
Zone.root.run(() => {
setTimeout(() => {
expect(scheduleAsyncTaskSpy.calls.all()[0].args).toEqual([
'XMLHttpRequest.addEventListener:load',
true,
]);
expect(scheduleAsyncTaskSpy.calls.all()[1].args).toEqual([
'XMLHttpRequest.send',
false,
]);
expect(startAsyncTaskSpy.calls.count()).toBe(2);
expect(finishAsyncTaskSpy.calls.count()).toBe(2);
done();
});
});
};
req.open('get', '/', true);
req.send();
});
}));

it('button click', ifEnvSupports('document', () => {
asyncStackTaggingZone.run(() => {
const button = document.createElement('button');
const clickEvent = document.createEvent('Event');
clickEvent.initEvent('click', true, true);
document.body.appendChild(button);
const handler = () => {};
button.addEventListener('click', handler);
button.dispatchEvent(clickEvent);
button.dispatchEvent(clickEvent);
button.removeEventListener('click', handler);
expect(scheduleAsyncTaskSpy)
.toHaveBeenCalledWith('HTMLButtonElement.addEventListener:click', true);
expect(startAsyncTaskSpy.calls.count()).toBe(2);
expect(finishAsyncTaskSpy.calls.count()).toBe(2);
expect(cancelAsyncTaskSpy.calls.count()).toBe(1);
});
}));
});
});

0 comments on commit 848a009

Please sign in to comment.