Skip to content

Commit 99ad18a

Browse files
atscottAndrewKushnir
authored andcommitted
feat(core): Add stability debugging utility
This commit adds a utility method to debug why the application has not stabilized after a set period of time (9 seconds, or `hydrationTimeout-1`). fixes angular#52912
1 parent 244b54c commit 99ad18a

File tree

17 files changed

+248
-2
lines changed

17 files changed

+248
-2
lines changed

adev/src/content/guide/hydration.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,24 @@ Keep in mind that adding the `ngSkipHydration` attribute to your root applicatio
174174

175175
Application stability is an important part of the hydration process. Hydration and any post-hydration processes only occur once the application has reported stability. There are a number of ways that stability can be delayed. Examples include setting timeouts and intervals, unresolved promises, and pending microtasks. In those cases, you may encounter the [Application remains unstable](errors/NG0506) error, which indicates that your app has not yet reached the stable state after 10 seconds. If you're finding that your application is not hydrating right away, take a look at what is impacting application stability and refactor to avoid causing these delays.
176176

177+
### Debugging Application Stability
178+
179+
The `provideStabilityDebugging` utility helps identify why your application fails to stabilize. This utility is provided by default in dev mode when using `provideClientHydration`. You can also add it manually to the application providers for use in production bundles or when using SSR without hydration, for example. The feature logs information to the console if the application takes longer than expected to stabilize.
180+
181+
```typescript
182+
import {provideStabilityDebugging} from '@angular/core';
183+
import {bootstrapApplication} from '@angular/platform-browser';
184+
import 'zone.js/plugins/task-tracking'; // Use if you have Zone.js with `provideZoneChangeDetection`
185+
186+
bootstrapApplication(AppComponent, {
187+
providers: [provideStabilityDebugging()],
188+
});
189+
```
190+
191+
When enabled, the utility logs pending tasks (`PendingTasks`) to the console. If your application uses Zone.js, you can also import `zone.js/plugins/task-tracking` to see which macrotasks are keeping the Angular Zone from stabilizing. This plugin provides the stack trace of the macrotask creation, effectively helping you identify the source of the delay.
192+
193+
IMPORTANT: Angular does not remove the zone.js task tracking plugin or this utility from production bundles. Use them only for temporary debugging of stability issues during development, including for optimized production builds.
194+
177195
## I18N
178196

179197
HELPFUL: By default, Angular will skip hydration for components that use i18n blocks, effectively re-rendering those components from scratch.

goldens/public-api/core/index.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,6 +1493,9 @@ export type Provider = TypeProvider | ValueProvider | ClassProvider | Constructo
14931493
// @public
14941494
export type ProviderToken<T> = Type<T> | AbstractType<T> | InjectionToken<T>;
14951495

1496+
// @public
1497+
export function provideStabilityDebugging(): EnvironmentProviders;
1498+
14961499
// @public
14971500
export function provideZoneChangeDetection(options?: NgZoneOptions): EnvironmentProviders;
14981501

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {InjectionToken} from '../di/injection_token';
10+
11+
export const DEBUG_TASK_TRACKER = new InjectionToken<DebugTaskTracker>(
12+
typeof ngDevMode !== 'undefined' && ngDevMode ? 'DEBUG_TASK_TRACKER' : '',
13+
);
14+
export interface DebugTaskTracker {
15+
add(taskId: number): void;
16+
remove(taskId: number): void;
17+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {EnvironmentProviders, inject, makeEnvironmentProviders} from '../di';
10+
import {NgZone} from '../zone';
11+
import {provideAppInitializer} from './application_init';
12+
import {ApplicationRef} from './application_ref';
13+
import {APPLICATION_IS_STABLE_TIMEOUT} from '../hydration/api';
14+
import {DEBUG_TASK_TRACKER, DebugTaskTracker} from './stability_debug';
15+
16+
const STABILITY_WARNING_THRESHOLD = APPLICATION_IS_STABLE_TIMEOUT - 1_000;
17+
18+
class DebugTaskTrackerImpl implements DebugTaskTracker {
19+
readonly openTasks = new Map<number, Error>();
20+
21+
add(taskId: number): void {
22+
this.openTasks.set(taskId, new Error('Task stack tracking error'));
23+
}
24+
25+
remove(taskId: number): void {
26+
this.openTasks.delete(taskId);
27+
}
28+
}
29+
30+
/**
31+
* Provides an application initializer that will log information about what tasks are keeping
32+
* the application from stabilizing if the application does not stabilize within 9 seconds.
33+
*
34+
* The logged information includes the stack of the tasks preventing stability. This stack can be traced
35+
* back to the source in the application code.
36+
*
37+
* If you are using Zone.js, it is recommended that you also temporarily import "zone.js/plugins/task-tracking".
38+
* This Zone.js plugin provides additional information about which macrotasks are scheduled in the Angular Zone
39+
* and keeping the Zone from stabilizing.
40+
*
41+
* @usageNotes
42+
*
43+
* ```ts
44+
* import 'zone.js/plugins/task-tracking';
45+
*
46+
* bootstrapApplication(AppComponent, {providers: [provideStabilityDebugging()]});
47+
* ```
48+
*
49+
* IMPORTANT: Neither the zone.js task tracking plugin nor this utility are removed from production bundles.
50+
* They are intended for temporary use while debugging stability issues during development, including for
51+
* optimized production builds.
52+
*
53+
* @publicApi 21.1
54+
*/
55+
export function provideStabilityDebugging(): EnvironmentProviders {
56+
const taskTracker = new DebugTaskTrackerImpl();
57+
const {openTasks} = taskTracker;
58+
return makeEnvironmentProviders([
59+
{
60+
provide: DEBUG_TASK_TRACKER,
61+
useValue: taskTracker,
62+
},
63+
provideAppInitializer(() => {
64+
if (typeof ngDevMode === 'undefined' || !ngDevMode) {
65+
console.warn(
66+
'Stability debugging untility was provided in production mode. ' +
67+
'This will cause debug code to be included in production bundles. ' +
68+
'If this is intentional because you are debugging stability issues in a production environment, you can ignore this warning.',
69+
);
70+
}
71+
const ngZone = inject(NgZone);
72+
const applicationRef = inject(ApplicationRef);
73+
74+
// From TaskTrackingZone:
75+
// https://github.com/angular/angular/blob/ae0c59028a2f393ea5716bf222db2c38e7a3989f/packages/zone.js/lib/zone-spec/task-tracking.ts#L46
76+
let _taskTrackingZone: {macroTasks: Array<{creationLocation: Error}>} | null = null;
77+
if (typeof Zone !== 'undefined') {
78+
ngZone.run(() => {
79+
_taskTrackingZone = Zone.current.get('TaskTrackingZone');
80+
});
81+
}
82+
ngZone.runOutsideAngular(() => {
83+
const timeoutId = setTimeout(() => {
84+
console.debug(
85+
`---- Application did not stabilize within ${STABILITY_WARNING_THRESHOLD / 1000} seconds ----`,
86+
);
87+
if (typeof Zone !== 'undefined' && !_taskTrackingZone) {
88+
console.info(
89+
'Zone.js is present but no TaskTrackingZone found. To enable better debugging of tasks in the Angular Zone, ' +
90+
'import "zone.js/plugins/task-tracking" in your application.',
91+
);
92+
}
93+
if (_taskTrackingZone?.macroTasks?.length) {
94+
console.group('Macrotasks keeping Angular Zone unstable:');
95+
for (const t of _taskTrackingZone?.macroTasks ?? []) {
96+
console.debug(t.creationLocation.stack);
97+
}
98+
console.groupEnd();
99+
}
100+
console.group('PendingTasks keeping application unstable:');
101+
for (const error of openTasks.values()) {
102+
console.debug(error.stack);
103+
}
104+
console.groupEnd();
105+
}, STABILITY_WARNING_THRESHOLD);
106+
107+
applicationRef.whenStable().then(() => {
108+
clearTimeout(timeoutId);
109+
});
110+
});
111+
}),
112+
]);
113+
}

packages/core/src/core.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export {
5454
ANIMATION_MODULE_TYPE,
5555
CSP_NONCE,
5656
} from './application/application_tokens';
57+
export {provideStabilityDebugging} from './application/stability_debug_impl';
5758
export {
5859
APP_INITIALIZER,
5960
ApplicationInitStatus,

packages/core/src/hydration/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ let isIncrementalHydrationRuntimeSupportEnabled = false;
8383
* Defines a period of time that Angular waits for the `ApplicationRef.isStable` to emit `true`.
8484
* If there was no event with the `true` value during this time, Angular reports a warning.
8585
*/
86-
const APPLICATION_IS_STABLE_TIMEOUT = 10_000;
86+
export const APPLICATION_IS_STABLE_TIMEOUT = 10_000;
8787

8888
/**
8989
* Brings the necessary hydration code in tree-shakable manner.

packages/core/src/pending_tasks_internal.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {BehaviorSubject, Observable} from 'rxjs';
1010

1111
import {ɵɵdefineInjectable} from './di/interface/defs';
1212
import {OnDestroy} from './change_detection/lifecycle_hooks';
13+
import {DEBUG_TASK_TRACKER} from './application/stability_debug';
14+
import {inject} from './di';
1315

1416
/**
1517
* Internal implementation of the pending tasks service.
@@ -19,8 +21,8 @@ export class PendingTasksInternal implements OnDestroy {
1921
private taskId = 0;
2022
private pendingTasks = new Set<number>();
2123
private destroyed = false;
22-
2324
private pendingTask = new BehaviorSubject<boolean>(false);
25+
private debugTaskTracker = inject(DEBUG_TASK_TRACKER, {optional: true});
2426

2527
get hasPendingTasks(): boolean {
2628
// Accessing the value of a closed `BehaviorSubject` throws an error.
@@ -50,6 +52,7 @@ export class PendingTasksInternal implements OnDestroy {
5052
}
5153
const taskId = this.taskId++;
5254
this.pendingTasks.add(taskId);
55+
this.debugTaskTracker?.add(taskId);
5356
return taskId;
5457
}
5558

@@ -59,6 +62,7 @@ export class PendingTasksInternal implements OnDestroy {
5962

6063
remove(taskId: number): void {
6164
this.pendingTasks.delete(taskId);
65+
this.debugTaskTracker?.remove(taskId);
6266
if (this.pendingTasks.size === 0 && this.hasPendingTasks) {
6367
this.pendingTask.next(false);
6468
}

packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"ANIMATION_PREFIX",
88
"ANIMATION_QUEUE",
99
"ANY_STATE",
10+
"APPLICATION_IS_STABLE_TIMEOUT",
1011
"APP_BOOTSTRAP_LISTENER",
1112
"APP_ID",
1213
"APP_ID_ATTRIBUTE_NAME",
@@ -66,6 +67,7 @@
6667
"ComponentRef2",
6768
"ConsumerObserver",
6869
"DASH_CASE_REGEXP",
70+
"DEBUG_TASK_TRACKER",
6971
"DECLARATION_COMPONENT_VIEW",
7072
"DECLARATION_LCONTAINER",
7173
"DECLARATION_VIEW",
@@ -226,6 +228,7 @@
226228
"SHARED_ANIMATION_PROVIDERS",
227229
"SIGNAL",
228230
"SIMPLE_CHANGES_STORE",
231+
"STABILITY_WARNING_THRESHOLD",
229232
"STAR_CLASSNAME",
230233
"STAR_SELECTOR",
231234
"SUBSTITUTION_EXPR_END",

packages/core/test/bundling/create_component/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"AFTER_RENDER_SEQUENCES_TO_ADD",
55
"ANIMATIONS",
66
"ANIMATION_QUEUE",
7+
"APPLICATION_IS_STABLE_TIMEOUT",
78
"APP_BOOTSTRAP_LISTENER",
89
"APP_ID",
910
"APP_ID_ATTRIBUTE_NAME",
@@ -42,6 +43,7 @@
4243
"ComponentRef",
4344
"ComponentRef2",
4445
"ConsumerObserver",
46+
"DEBUG_TASK_TRACKER",
4547
"DECLARATION_COMPONENT_VIEW",
4648
"DECLARATION_LCONTAINER",
4749
"DECLARATION_VIEW",
@@ -174,6 +176,7 @@
174176
"SIGNAL",
175177
"SIGNAL_NODE",
176178
"SIMPLE_CHANGES_STORE",
179+
"STABILITY_WARNING_THRESHOLD",
177180
"SVG_NAMESPACE",
178181
"SafeSubscriber",
179182
"Sanitizer",

packages/core/test/bundling/defer/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"AFTER_RENDER_SEQUENCES_TO_ADD",
6262
"ANIMATIONS",
6363
"ANIMATION_QUEUE",
64+
"APPLICATION_IS_STABLE_TIMEOUT",
6465
"APP_BOOTSTRAP_LISTENER",
6566
"APP_ID",
6667
"APP_INITIALIZER",
@@ -93,6 +94,7 @@
9394
"ComponentRef",
9495
"ComponentRef2",
9596
"ConsumerObserver",
97+
"DEBUG_TASK_TRACKER",
9698
"DECLARATION_COMPONENT_VIEW",
9799
"DECLARATION_LCONTAINER",
98100
"DECLARATION_VIEW",
@@ -218,6 +220,7 @@
218220
"SIMPLE_CHANGES_STORE",
219221
"SSR_BLOCK_STATE",
220222
"SSR_UNIQUE_ID",
223+
"STABILITY_WARNING_THRESHOLD",
221224
"SVG_NAMESPACE",
222225
"SafeSubscriber",
223226
"Sanitizer",

0 commit comments

Comments
 (0)