17

I found that using the AngularFireAuthModule from '@angular/fire/auth'; causes a memory leak that crashes the browser after 20 hours.

Version:

I use the latest version updated today using ncu -u for all packages.

Angular Fire: "@angular/fire": "^5.2.3",

Firebase version: "firebase": "^7.5.0",

How to reproduce:

I made a minimum reproducible code on StackBliztz editor

Here is the link to test the bug directly StackBlizt test

Symptom:

You can check yourself that the code does nothing. It just prints hello world. However, the JavaScript memory used by the Angular App increases by 11 kb/s (Chrome Task Manager CRTL+ESC). After 10 hours leaving the browser open, the memory used reaches approx 800 mb (the memory footprint is around twice 1.6 Gb!)

As a result, the browser runs out of memory and the chrome tab crashes.

After further investigation using the memory profiling of chrome under the performance tab, I clearly noticed that the number of listeners increases by 2 every second and so the JS heap increases accordingly.

enter image description here

Code that causes the memory leak:

I found that using the AngularFireAuthModule module causes the memory leak whether it is injected in a component constructor or in a service.

import { Component } from '@angular/core';
import {AngularFireAuth} from '@angular/fire/auth';
import {AngularFirestore} from '@angular/fire/firestore';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'memoryleak';
  constructor(public auth: AngularFireAuth){

  }
}

Question:

It could be a bug in the implementation of FirebaseAuth and I already open a Github issue, but I am looking for a workaround for this issue. I am desperate for a solution. I don't mind even if the sessions across tabs not synchronized. I don't need that feature. I read somewhere that

if you don't require this functionality, the Firebase V6 modularization efforts will allow you to switch to localStorage which has storage events for detecting changes cross tabs, and possibly will provide you the ability to define your own storage interface.

If that's the only solution, how to implement that?

I just need any solution that stops this unnecessary increase of listener because it slows down the computer and crashes my app. My app needs to run for more than 20 hours so it now unusable due to this issue. I am desperate for a solution.

TSR
  • 17,242
  • 27
  • 93
  • 197

1 Answers1

7

TLDR: Increasing listener number is expected behavior and will be reset upon Garbage Collection. The bug that causes memory leaks in Firebase Auth has already been fixed in Firebase v7.5.0, see #1121, check your package-lock.json to confirm that you are using the right version. If unsure, reinstall the firebase package.

Previous versions of Firebase was polling IndexedDB via Promise chaining, which causes memory leaks, see JavaScript's Promise Leaks Memory

var repeat = function() {
  self.poll_ =
      goog.Timer.promise(fireauth.storage.IndexedDB.POLLING_DELAY_)
      .then(goog.bind(self.sync_, self))
      .then(function(keys) {
        // If keys modified, call listeners.
        if (keys.length > 0) {
          goog.array.forEach(
              self.storageListeners_,
              function(listener) {
                listener(keys);
              });
        }
      })
      .then(repeat)
      .thenCatch(function(error) {
        // Do not repeat if cancelled externally.
        if (error.message != fireauth.storage.IndexedDB.STOP_ERROR_) {
          repeat();
        }
      });
  return self.poll_;
};
repeat();

Fixed in subsequent versions using non-recursive function calls:

var repeat = function() {
  self.pollTimerId_ = setTimeout(
      function() {
        self.poll_ = self.sync_()
            .then(function(keys) {
              // If keys modified, call listeners.
              if (keys.length > 0) {
                goog.array.forEach(
                    self.storageListeners_,
                    function(listener) {
                      listener(keys);
                    });
              }
            })
            .then(function() {
              repeat();
            })
            .thenCatch(function(error) {
              if (error.message != fireauth.storage.IndexedDB.STOP_ERROR_) {
                repeat();
              }
            });
      },
      fireauth.storage.IndexedDB.POLLING_DELAY_);
};
repeat();

Regarding linearly increasing listener number:

Linearly increasing listener count is expected as this is what Firebase is doing to poll IndexedDB. However, listeners will be removed whenever the GC wants to.

Read Issue 576302: Incorrectly showing memory (listeners xhr & load) leak

V8 performs Minor GC periodically, which causes those small drops of the heap size. You can actually see them on the flame chart. The minor GCs however may not collect all the garbage, which obviously happens for listeners.

The toolbar button invokes the Major GC which is able to collect listeners.

DevTools tries to not interfere with the running application, so it does not force GC on its own.


To confirm that detached listeners are garbage collected, I added this snippet to pressure the JS heap, thereby forcing GC to trigger:

var x = ''
setInterval(function () {
  for (var i = 0; i < 10000; i++) {
    x += 'x'
  }
}, 1000)

Listeners are garbage collected

As you can see, detached listeners are removed periodically when GC is triggered.



Similar stackoverflow questions and GitHub issues regarding listener number and memory leaks:

  1. Listeners in Chrome dev tools' performance profiling results
  2. JavaScript listeners keep increasing
  3. Simple app causing a memory leak?
  4. $http 'GET' memory leak (NOT!)--number of listeners (AngularJS v.1.4.7/8)
Community
  • 1
  • 1
Joshua Chan
  • 1,797
  • 8
  • 16
  • 1
    I confirm using 7.5.0 and tested multiple times on different environments. Even this.auth.auth.setPersistence('none') does not prevent the memory leak. Please test it by yourself using the code here https://stackblitz.com/edit/angular-zuabzz – TSR Dec 10 '19 at 07:27
  • what are your testing steps? Do I need to leave it overnight to see my browser crash? In my case, the listener number always resets after the GC kick in and memory is always back to 160mb. – Joshua Chan Dec 10 '19 at 09:51
  • @TSR call `this.auth.auth.setPersistence('none')` in `ngOnInit` instead of the constructor to disable persistence. – Joshua Chan Dec 10 '19 at 10:30
  • @JoshuaChan does it matter when to call a method of a service? It's being injected in a constructor and available right in it's body. Why should it go in `ngOnInit`? – Sergey Dec 10 '19 at 16:43
  • @Sergey mostly for best practice. But for this specific case, I ran CPU profiling for both ways of calling `setPersistence` and find that if it is done in the constructor, function calls are still made to IndexedDB, whereas if it is done in `ngOnInit`, no calls were made to IndexedDB, not exactly sure why though – Joshua Chan Dec 10 '19 at 18:29
  • The issue is not fixed, even if the firebase team denies it and locks down commenting on the issue: https://github.com/firebase/firebase-js-sdk/issues/1420, pretending and denying it doesn't exist. Pathetic, really. I mean, you can fool some of the people all of the time, and all of the people some of the time, but you can not fool all of the people all of the time. – Nick Ribal Aug 08 '20 at 07:22