diff --git a/goldens/size-tracking/aio-payloads.json b/goldens/size-tracking/aio-payloads.json index eeaa8c301a71..887687022645 100755 --- a/goldens/size-tracking/aio-payloads.json +++ b/goldens/size-tracking/aio-payloads.json @@ -12,7 +12,7 @@ "aio-local": { "uncompressed": { "runtime": 4325, - "main": 459842, + "main": 460353, "polyfills": 33922, "styles": 73640, "light-theme": 78276, diff --git a/goldens/size-tracking/integration-payloads.json b/goldens/size-tracking/integration-payloads.json index 6c15eef37023..74020cd991ed 100644 --- a/goldens/size-tracking/integration-payloads.json +++ b/goldens/size-tracking/integration-payloads.json @@ -2,7 +2,7 @@ "cli-hello-world": { "uncompressed": { "runtime": 1083, - "main": 125325, + "main": 125830, "polyfills": 33824 } }, @@ -19,14 +19,14 @@ "cli-hello-world-ivy-compat": { "uncompressed": { "runtime": 1102, - "main": 132311, + "main": 132816, "polyfills": 33957 } }, "cli-hello-world-ivy-i18n": { "uncompressed": { "runtime": 926, - "main": 124982, + "main": 125487, "polyfills": 35252 } }, @@ -55,7 +55,7 @@ "standalone-bootstrap": { "uncompressed": { "runtime": 1090, - "main": 83013, + "main": 83515, "polyfills": 33945 } }, diff --git a/packages/core/src/zone/ng_zone.ts b/packages/core/src/zone/ng_zone.ts index cf9889f7e95d..ad04e4964110 100644 --- a/packages/core/src/zone/ng_zone.ts +++ b/packages/core/src/zone/ng_zone.ts @@ -138,6 +138,11 @@ export class NgZone { self._outer = self._inner = Zone.current; + if ((Zone as any)['AsyncStackTaggingZoneSpec']) { + const AsyncStackTaggingZoneSpec = (Zone as any)['AsyncStackTaggingZoneSpec']; + self._inner = self._inner.fork(new AsyncStackTaggingZoneSpec('Angular')); + } + if ((Zone as any)['TaskTrackingZoneSpec']) { self._inner = self._inner.fork(new ((Zone as any)['TaskTrackingZoneSpec'] as any)); } diff --git a/packages/zone.js/bundles.bzl b/packages/zone.js/bundles.bzl index 90fec738b9c0..fd4db037cd24 100644 --- a/packages/zone.js/bundles.bzl +++ b/packages/zone.js/bundles.bzl @@ -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", }, diff --git a/packages/zone.js/dist/BUILD.bazel b/packages/zone.js/dist/BUILD.bazel index f998e89f4500..200a24b90411 100644 --- a/packages/zone.js/dist/BUILD.bazel +++ b/packages/zone.js/dist/BUILD.bazel @@ -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", diff --git a/packages/zone.js/dist/tools.bzl b/packages/zone.js/dist/tools.bzl index 6ac4af5fef84..27f695f93a2e 100644 --- a/packages/zone.js/dist/tools.bzl +++ b/packages/zone.js/dist/tools.bzl @@ -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", diff --git a/packages/zone.js/lib/zone-spec/async-stack-tagging.ts b/packages/zone.js/lib/zone-spec/async-stack-tagging.ts new file mode 100644 index 000000000000..e8f5a79ab9c4 --- /dev/null +++ b/packages/zone.js/lib/zone-spec/async-stack-tagging.ts @@ -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; diff --git a/packages/zone.js/plugins/BUILD.bazel b/packages/zone.js/plugins/BUILD.bazel index 8c03c51d5843..9cb0ac946482 100644 --- a/packages/zone.js/plugins/BUILD.bazel +++ b/packages/zone.js/plugins/BUILD.bazel @@ -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", diff --git a/packages/zone.js/plugins/async-stack-tagging.min/package.json b/packages/zone.js/plugins/async-stack-tagging.min/package.json new file mode 100644 index 000000000000..1bacb2f4610c --- /dev/null +++ b/packages/zone.js/plugins/async-stack-tagging.min/package.json @@ -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" +} diff --git a/packages/zone.js/plugins/async-stack-tagging/package.json b/packages/zone.js/plugins/async-stack-tagging/package.json new file mode 100644 index 000000000000..1f5587932e0b --- /dev/null +++ b/packages/zone.js/plugins/async-stack-tagging/package.json @@ -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" +} diff --git a/packages/zone.js/test/BUILD.bazel b/packages/zone.js/test/BUILD.bazel index e7345d07b35a..14e1da52a703 100644 --- a/packages/zone.js/test/BUILD.bazel +++ b/packages/zone.js/test/BUILD.bazel @@ -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 = [ @@ -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 = [ diff --git a/packages/zone.js/test/browser-zone-setup.ts b/packages/zone.js/test/browser-zone-setup.ts index 264d797b7ac9..9f0888a56c25 100644 --- a/packages/zone.js/test/browser-zone-setup.ts +++ b/packages/zone.js/test/browser-zone-setup.ts @@ -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'; diff --git a/packages/zone.js/test/browser_entry_point.ts b/packages/zone.js/test/browser_entry_point.ts index 3954d5e373a0..0775838658f1 100644 --- a/packages/zone.js/test/browser_entry_point.ts +++ b/packages/zone.js/test/browser_entry_point.ts @@ -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: @@ -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'; diff --git a/packages/zone.js/test/karma_test.bzl b/packages/zone.js/test/karma_test.bzl index b511783f22fb..b4986ff91797 100644 --- a/packages/zone.js/test/karma_test.bzl +++ b/packages/zone.js/test/karma_test.bzl @@ -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", ] diff --git a/packages/zone.js/test/npm_package/npm_package.spec.ts b/packages/zone.js/test/npm_package/npm_package.spec.ts index 739a622e0093..8c4e39403dd3 100644 --- a/packages/zone.js/test/npm_package/npm_package.spec.ts +++ b/packages/zone.js/test/npm_package/npm_package.spec.ts @@ -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', () => { @@ -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', @@ -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', @@ -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', diff --git a/packages/zone.js/test/zone-spec/async-tagging-console.spec.ts b/packages/zone.js/test/zone-spec/async-tagging-console.spec.ts new file mode 100644 index 000000000000..8fa173a7d9ac --- /dev/null +++ b/packages/zone.js/test/zone-spec/async-tagging-console.spec.ts @@ -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); + }); + })); + }); +});