How to create a Memory Leak

in an Angular App

Garbage Collection

Mark

Sweep

marked=false

marked=false

marked=true

marked=true

marked=true

marked=true

Observables / Observer Pattern

new Observable(constructor_fn)
                      .subscribe(subscriber_fn)
const s = new Subject().subscribe(subscriber_fn)

s.next('some value')

register

Test Setup

  • Sub Component shown / hidden periodically
@Component({
  selector: 'app-root',
  template: `<app-sub *ngIf="hide"></app-sub>`
})
export class AppComponent {
  hide = false;

  constructor() {
    setInterval(() => this.hide = !this.hide, 50);
  }
}
@Injectable()
export class DummyService {
  behavior$ = new BehaviorSubject(2);
  replay$ = new ReplaySubject(1);

  private registeredComponents = [];

  register(component) {
    this.registeredComponents.push(component);
  }
}

live.voxvote.com   PIN: 90566

1.) Is there a memory leak?

@Component({
  selector:'app-sub',
  template: 'mega {{rand}}'
})
export class SubComponent {
  rand = Math.random();
  rand2 = 0;
  subject = new BehaviorSubject(42);

  arr = [];

  constructor(private dummyService: DummyService) {
    for (let i = 0; i < 100000; ++i) {
      this.arr.push(Math.random());
    }
  }
}

Reminder: SubComponent is shown / hidden periodically with *ngIf

2.) Is there a memory leak?

@Component({
  selector:'app-sub',
  template: 'mega {{rand}}'
})
export class SubComponent {
  rand = Math.random();
  rand2 = 0;
  subject = new BehaviorSubject(42);

  arr = [];

  constructor(private dummyService: DummyService) {
    for (let i = 0; i < 100000; ++i) {
      this.arr.push(Math.random());
    }

    this.subject.subscribe();
  }
}

Reminder: SubComponent is shown / hidden periodically with *ngIf

3.) Is there a memory leak?

@Component({
  selector:'app-sub',
  template: 'mega {{rand}}'
})
export class SubComponent {
  rand = Math.random();
  rand2 = 0;
  subject = new BehaviorSubject(42);

  arr = [];

  constructor(private dummyService: DummyService) {
    for (let i = 0; i < 100000; ++i) {
      this.arr.push(Math.random());
    }

    this.subject.subscribe(() => {const blub = 34;});
  }
}

Reminder: SubComponent is shown / hidden periodically with *ngIf

4.) Is there a memory leak?

@Component({
  selector:'app-sub',
  template: 'mega {{rand}}'
})
export class SubComponent {
  rand = Math.random();
  rand2 = 0;
  subject = new BehaviorSubject(42);

  arr = [];

  constructor(private dummyService: DummyService) {
    for (let i = 0; i < 100000; ++i) {
      this.arr.push(Math.random());
    }

    this.subject.subscribe(() => this.rand2 = 33);
  }
}

Reminder: SubComponent is shown / hidden periodically with *ngIf

5.) Is there a memory leak?

@Component({
  selector:'app-sub',
  template: 'mega {{rand}}'
})
export class SubComponent {
  rand = Math.random();
  rand2 = 0;
  subject = new BehaviorSubject(42);

  arr = [];

  constructor(private dummyService: DummyService) {
    for (let i = 0; i < 100000; ++i) {
      this.arr.push(Math.random());
    }

    this.dummyService.behavior$.subscribe();
  }
}

Reminder: SubComponent is shown / hidden periodically with *ngIf

6.) Is there a memory leak?

@Component({
  selector:'app-sub',
  template: 'mega {{rand}}'
})
export class SubComponent {
  rand = Math.random();
  rand2 = 0;
  subject = new BehaviorSubject(42);

  arr = [];

  constructor(private dummyService: DummyService) {
    for (let i = 0; i < 100000; ++i) {
      this.arr.push(Math.random());
    }

    this.dummyService.behavior$.subscribe(() => {const blub = 34;});
  }
}

Reminder: SubComponent is shown / hidden periodically with *ngIf

7.) Is there a memory leak?

@Component({
  selector:'app-sub',
  template: 'mega {{rand}}'
})
export class SubComponent {
  rand = Math.random();
  rand2 = 0;
  subject = new BehaviorSubject(42);

  arr = [];

  constructor(private dummyService: DummyService) {
    for (let i = 0; i < 100000; ++i) {
      this.arr.push(Math.random());
    }

    this.dummyService.behavior$.subscribe(() => this.rand2 = 33);
  }
}

Reminder: SubComponent is shown / hidden periodically with *ngIf

8.) Is there a memory leak?

@Component({
  selector:'app-sub',
  template: 'mega {{rand}}'
})
export class SubComponent {
  rand = Math.random();
  rand2 = 0;
  subject = new BehaviorSubject(42);

  arr = [];

  constructor(private dummyService: DummyService) {
    for (let i = 0; i < 100000; ++i) {
      this.arr.push(Math.random());
    }

    this.dummyService.register(this);
  }
}
@Injectable()
export class DummyService {
  behavior$ = new BehaviorSubject(2);
  replay$ = new ReplaySubject(1);

  private registeredComponents = [];

  register(component) {
    this.registeredComponents.push(component);
  }
}

Reminder: SubComponent is shown / hidden periodically with *ngIf

When is it a memory leak?

No Leak

ng

Component

window

Observable

ng

Component

window

Observable

Service

Subscribers

Leak

Subscribers

Answers

  1. no
  2. no
  3. no
  4. no
  5. yes, a small one
  6. yes, a small one
  7. yes, a big one
  8. yes a big one

When is it a memory leak?

  • observable initialized in different class
  • subscribe adds us to the list of subscribers
  • subscription isn't unsubscribed
  • although component is destroyed, the list of subscribers still exists (e.g. in service)
  • great impact: subscription references component
    .subscribe(() => this.doBla())

Conclusion

  • complex and non-obvious situations lead to memory leaks
  • tiny changes, possibly in non-obvious places like services, can have a great impact

How to avoid a subscription memory leak?

@Component({
  selector:'app-sub',
  template: 'mega {{rand}}'
})
export class SubComponent implements ngOnDestroy {
  private destroy$ = new Subject<void>();

  constructor(private dummyService: DummyService) {
    this.dummyService.behavior$.pipe(
      takeUntil(this.destroy$)
    ).subscribe((this => this.rand2 = 33));
    
    this.dummyService.behavior$.pipe(
      takeUntil(this.destroy$)
      shareReplay(1),
      takeUntil(this.destroy$)
    ).subscribe((this => this.rand2 = 33));
  }
  
  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Recipe

.subscribe() and shareReplay() must be preceded by takeUntil(this.destroy$)

How to avoid a subscription memory leak?

npm install @angular-extensions/lint-rules --save-dev

Use the "angular-rxjs-takeuntil-before-subscribe" rule
from @angular-extensions/lint-rules

{
  "extends": ["@angular-extensions/lint-rule"],
  "rules": { ... }
}
ng lint

tslint.sjon

Caveats

The rule is strict and in some situations takeUntil(this.destroy$) would not be nessary, e.g.

this.httpClient.get('/something')
  .subscribe(result => this.something = result);


this.dummyService.willFireOnce$
  .subscribe(result => this.something = result);


this.dummyService.replay$.pipe(
  first()
).subscribe(result => this.something = result);
  • observables that are guaranteed to complete
  • observables created in the component

However, these situations are hard to analyze statically and without the rule, refactorings can introduce memory-leaks if not re-evaluated

Further Reading

Chrome Developer Tools

Memory Tab / Performance Monitor

Analyze the Heap

  • Retained size: shallow size plus the shallow sizes of the objects that are accessible, directly or indirectly, only from this object.
  • In other words, the retained size represents the amount of memory that will be freed by the garbage collector when this object is collected.

Analyze the Heap

  • Retained size: shallow size plus the shallow sizes of the objects that are accessible, directly or indirectly, only from this object.
  • In other words, the retained size represents the amount of memory that will be freed by the garbage collector when this object is collected.

Analyze the Heap

Analyze the Heap

With Memory Leak

Without Memory Leak

Analyze the Heap

With (subtle) Memory Leak

Analyze the Tab Memory

Important

  • Close the Developer Tools
  • Tab must be visible

Thanks for your attention!

SCS: Memory Leak

By Esteban Gehring

SCS: Memory Leak

  • 629