Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular Firebase app crashes after 20 hours with +1 gigabyte of memory allocation

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.

like image 773
TSR Avatar asked Nov 29 '19 08:11

TSR


1 Answers

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)
like image 184
Joshua Chan Avatar answered Sep 27 '22 19:09

Joshua Chan