Repository: angular/angular
File: packages/core/src/application/stability_debug.ts (TypeScript)

Code before:
 *
 * bootstrapApplication(AppComponent, {providers: [provideStabilityDebugging()]});
 * ```
 *
 * IMPORTANT: Neither the zone.js task tracking plugin nor this utility are removed from production bundles.
 * They are intended for temporary use while debugging stability issues during development, including for
 * optimized production builds.
 *
 * @publicApi 21.1
 */
export function provideStabilityDebugging(): EnvironmentProviders {
  const openTasks = new Map<number, Error>();
  return makeEnvironmentProviders([
    {
      provide: ENVIRONMENT_INITIALIZER,
      multi: true,
      useFactory: () => {
        const pendingTasks = inject(PendingTasksInternal);
        pendingTasks.debugTaskTracker = {
          add(taskId: number): void {
            openTasks.set(taskId, new Error('Task stack tracking error'));
          },
          remove(taskId: number): void {
            openTasks.delete(taskId);
          },
        };
        // We actually do our initialization in the factory itself rather than the return function
        // to ensure our initialization runs as early as possible, before any initializer callbacks are executed.
        // This ensures we capture pending tasks from initializer callbacks.
        return () => {};
      },
    },
    provideAppInitializer(() => {
      if (typeof ngDevMode === 'undefined' || !ngDevMode) {
        console.warn('Stability debugging untility was provided in production mode.');
      }
      const ngZone = inject(NgZone);
      const applicationRef = inject(ApplicationRef);

      // From TaskTrackingZone:
      // https://github.com/angular/angular/blob/ae0c59028a2f393ea5716bf222db2c38e7a3989f/packages/zone.js/lib/zone-spec/task-tracking.ts#L46
      let _taskTrackingZone: {macroTasks: Array<{creationLocation: Error}>} | null = null;
      if (typeof Zone !== 'undefined') {
        ngZone.run(() => {
          _taskTrackingZone = Zone.current.get('TaskTrackingZone');
        });
      }
      ngZone.runOutsideAngular(() => {
        const timeoutId = setTimeout(() => {
          console.debug('---- Application did not stabilize within 9 seconds ----');
          if (typeof Zone !== 'undefined' && !_taskTrackingZone) {

Review comment (bug):
Consider: Would it be feasible to make this another token which `PendingTasksInternal` optionally injects? Or is there no way for that timing to work?

```typescript
class DebugTaskTracker {
  private readonly openTasks = new Map<number, Error>();

  add(taskId: number): void { /* ... */ }
  remove(taskId: number): void { /* ... */ }
}

class PendingTasksInternal {
  private readonly debugTaskTracker = inject(DebugTaskTracker, undefined, {optional: true});

  // ...
}
```

Alternatively, I'm wondering if the timing implications here are maybe a sign that this shouldn't be done in a provider factory. Given that this information should be available when the first task executes (which is pretty early), what if we implemented this as a library with its state in the top-level scope?

```typescript
const openTasks = new Map<number, Error>();

export function addTask(taskId: number): void { /* ... */ }
export function removeTask(taskId: number): void { /* ... */ }
export function getTasks(): Error[] { /* ... */ }
```

We could gate these implementations / usages on `ngDevMode` so they tree shake and the capability can be directly depended upon by `PendingTasksInternal`.

Code after:
/**
 * @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.dev/license
 */

import {InjectionToken} from '../di/injection_token';

export const DEBUG_TASK_TRACKER = new InjectionToken<DebugTaskTracker>(
  typeof ngDevMode !== 'undefined' && ngDevMode ? 'DEBUG_TASK_TRACKER' : '',
);
export interface DebugTaskTracker {
  add(taskId: number): void;
  remove(taskId: number): void;
}