Angular Memory Leak Detector
Memory leaks in web applications, especially complex ones like Angular SPAs, can lead to degraded performance and eventual crashes. Identifying and resolving these leaks is crucial for maintaining a stable and responsive user experience. This challenge asks you to build a system to help detect common types of memory leaks within an Angular application.
Problem Description
Your task is to create an Angular service that monitors for potential memory leaks. Specifically, you need to detect components and services that are subscribed to observables but are not properly unsubscribing when they are destroyed.
Key Requirements:
- Subscription Tracking: The service should be able to track active subscriptions initiated by Angular components or services.
- Unsubscription Hook: The service needs to hook into the Angular component/directive lifecycle to know when an instance is destroyed.
- Leak Detection: If a component/service instance is destroyed while still holding active subscriptions, the service should log a warning indicating a potential memory leak.
- Service-Based: The solution should be implemented as an Angular service that can be easily integrated into different parts of the application.
- Configurable: Allow for optional configuration, such as a delay before reporting a leak to account for asynchronous operations that might complete shortly after destruction.
Expected Behavior:
When a component that has subscribed to an observable is destroyed without unsubscribing, and its subscriptions are still active after a configurable grace period, the service should log a message to the console (e.g., using console.warn) detailing which component/service instance and which subscriptions are potentially leaking memory.
Edge Cases to Consider:
- Components/services with no subscriptions.
- Subscriptions that complete automatically.
- Components/services that are themselves dependencies of other leaking components.
- The delay mechanism for reporting leaks.
Examples
Example 1: Simple Component Leak
Assume the following component:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { interval, Subscription } from 'rxjs';
@Component({
selector: 'app-leaky',
template: '<div>Leaky Component</div>',
})
export class LeakyComponent implements OnInit, OnDestroy {
private intervalSubscription: Subscription;
ngOnInit(): void {
this.intervalSubscription = interval(1000).subscribe(val => {
console.log('Leaky Component Tick:', val);
});
}
ngOnDestroy(): void {
// Missing unsubscribe for intervalSubscription
console.log('Leaky Component Destroyed');
}
}
Input: The LeakyComponent is created, the ngOnInit runs, subscribing to an interval observable. Then, the component is destroyed (e.g., by navigation or conditional rendering) without this.intervalSubscription.unsubscribe().
Output (Logged to Console by the Memory Leak Detector Service):
[Memory Leak Detector] Potential memory leak detected in LeakyComponent instance: <instance_id>.
Subscriptions that did not unsubscribe:
- Subscription ID: <unique_id_1> (Observable type: interval)
Explanation: The LeakyComponent was destroyed, but its intervalSubscription was still active. The memory leak detector service identified this situation after the configured grace period and logged a warning.
Example 2: Service-Oriented Leak
Assume a service that holds subscriptions:
import { Injectable } from '@angular/core';
import { timer, Subscription } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class DataService {
private timerSubscription: Subscription;
constructor() {
this.timerSubscription = timer(5000).subscribe(() => {
console.log('DataService timer fired.');
});
}
// Imagine this service is injected into a component and the subscription is never cleared
// ... (no explicit ngOnDestroy in the service itself, but its lifecycle is managed by Angular's DI)
}
And a component using it:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { DataService } from './data.service';
@Component({
selector: 'app-user-profile',
template: '<div>User Profile</div>',
})
export class UserProfileComponent implements OnInit, OnDestroy {
constructor(private dataService: DataService) {}
ngOnInit(): void {
console.log('UserProfileComponent initialized');
// The DataService's subscription is managed here indirectly.
// If DataService doesn't unsubscribe, and it's a singleton, it's a root-level leak.
}
ngOnDestroy(): void {
console.log('UserProfileComponent Destroyed');
// If DataService's subscription was somehow tied to UserProfileComponent's lifecycle
// and not unsubscribed here, it would be a leak.
// For simplicity, let's assume DataService's subscription is the problem.
}
}
Input: The DataService is instantiated (e.g., because UserProfileComponent injects it). DataService creates a timerSubscription. UserProfileComponent is created and destroyed. The DataService's timerSubscription is never unsubscribed from.
Output (Logged to Console by the Memory Leak Detector Service):
[Memory Leak Detector] Potential memory leak detected in DataService instance: <instance_id>.
Subscriptions that did not unsubscribe:
- Subscription ID: <unique_id_2> (Observable type: timer)
Explanation: Although UserProfileComponent itself might not have a direct leak, the DataService (which might be a singleton) has an active subscription that will never be cleared because the service instance persists. The detector identifies this persistent subscription.
Constraints
- The solution must be implemented using Angular's dependency injection and lifecycle hooks.
- The service should not significantly impact application performance in a production environment (i.e., overhead should be minimal).
- Consider a grace period for reporting leaks, e.g., 500ms to 2000ms, to avoid false positives from short-lived async operations.
- The detector should be able to differentiate between different subscription instances.
Notes
- Think about how to wrap observable subscriptions to automatically register them with your service.
- Angular's
OnDestroylifecycle hook is critical for this task. - Consider using Angular's
ComponentRefor other internal Angular mechanisms to identify component instances. - You might need to create a custom RxJS operator or a helper function to easily subscribe through your detector service.
- The goal is to identify potential leaks. Some subscriptions might be intended to live for the application's lifetime (e.g., global event listeners), and your system might need a way to exclude them, though for this challenge, focus on detecting unclosed subscriptions from components/services that should be cleaned up.