How to create, detect & fix a Memory Leak

in an Angular App

Garbage Collection

Mark

Sweep

marked=false

marked=false

marked=true

marked=true

marked=true

marked=true

Part I: Create The Memory Leak

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: 65902

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 bla = 55});
  }
}

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 bla = 55});
  }
}

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()
  • shareReplay()
  • publishReplay()
  • publish()
  • publishBehavior()

 

must be preceded by

takeUntil(this.destroy$)

How to avoid a subscription memory leak?

npm install @mobi/rwc-lint-rules-jslib --save-dev

Use the "rxjs-prefer-angular-takeuntil-before-subscribe" rule
from
@mobi/rwc-lint-rules-jslib

{
  "extends": ["@mobi/rwc-lint-rules-jslib"],
  "rules": {
    "rxjs-prefer-angular-takeuntil-before-subscribe": { "severity": "error" },
    ...
  }
}
ng lint

tslint.json

Danger Zone

export class BaseComponent implements OnDestroy {
  private destroy$ = new Subject();
  
  constructor(private dummyService: DummyService) {
    this.dummyService.behavior$.pipe(takeUntil(this.destroy$))
      .subscribe(() => this.doSomething());
  }
  
  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  } 
}

export class FancyComponent extends BaseComponent implements OnDestroy {
  private _destroy$ = new Subject();
  
  constructor(private dummyService: DummyService) {
    this.dummyService.behavior$.pipe(takeUntil(this._destroy$))
      .subscribe(() => this.doSomethingElse());
  }
  
  ngOnDestroy() {
    this._destroy$.next();
    this._destroy$.complete();
  }
}

Danger Zone

export class BaseComponent implements OnDestroy {
  private destroy$ = new Subject();
  
  constructor(private dummyService: DummyService) {
    this.dummyService.behavior$.pipe(takeUntil(this.destroy$))
      .subscribe(() => this.doSomething());
  }
  
  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  } 
}

export class FancyComponent extends BaseComponent implements OnDestroy {
  private _destroy$ = new Subject();
  
  constructor(private dummyService: DummyService) {
    this.dummyService.behavior$.pipe(takeUntil(this._destroy$))
      .subscribe(() => this.doSomethingElse());
  }
  
  ngOnDestroy() {
    super.ngOnDestroy();
    this._destroy$.next();
    this._destroy$.complete();
  }
}

Further Reading

Part II: Detect The Memory Leak

Script

var count = 0;
var intervalTime = 10*1000; // 10s
var inter = setInterval(function(){
    console.log("Leak Test: Click " + (count + 1));

    var menuItems = $('.app-navigation-body .menu .menu-item');
	
    menuItems[count % menuItems.length].click();
    count++;
            
}, intervalTime);

// Stop it:
// clearInterval(inter)

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!

Mobi: Create, Detect and Fix Memory Leak

By Esteban Gehring

Mobi: Create, Detect and Fix Memory Leak

  • 804