Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(zone.js): add AsyncStackTaggingZoneSpec implementation
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.
  • Loading branch information
JiaLiPassion committed Jul 11, 2022
commit 1c2245ef989cfa7601611697750d66d4b73b4ea4
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);
});
}));
});
});