L'application Angular Firebase se bloque après 20 heures avec +1 gigaoctet d'allocation de mémoire

13

J'ai trouvé que l'utilisation de AngularFireAuthModulefrom '@angular/fire/auth';provoque une fuite de mémoire qui bloque le navigateur après 20 heures.

Version:

J'utilise la dernière version mise à jour aujourd'hui en utilisant ncu -u pour tous les packages.

Feu angulaire: "@angular/fire": "^5.2.3",

Version Firebase: "firebase": "^7.5.0",

Comment reproduire:

J'ai fait un code reproductible minimum sur l' éditeur StackBliztz

Voici le lien pour tester directement le bug StackBlizt test

Symptôme:

Vous pouvez vous vérifier que le code ne fait rien. Il imprime juste bonjour le monde. Cependant, la mémoire JavaScript utilisée par l'application angulaire augmente de 11 kb / s (Chrome Task Manager CRTL + ESC). Après 10 heures en laissant le navigateur ouvert, la mémoire utilisée atteint environ 800 Mo (l'empreinte mémoire est d'environ deux fois 1,6 Go !)

Par conséquent, le navigateur manque de mémoire et l'onglet Chrome se bloque.

Après une enquête plus approfondie en utilisant le profilage de la mémoire de chrome sous l'onglet performances, j'ai clairement remarqué que le nombre d'auditeurs augmente de 2 chaque seconde et donc le tas JS augmente en conséquence.

entrez la description de l'image ici

Code qui provoque la fuite de mémoire:

J'ai trouvé que l'utilisation du AngularFireAuthModule module provoque la fuite de mémoire, qu'il soit injecté dans un componentconstructeur ou dans un 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 :

Cela pourrait être un bogue dans l'implémentation de FirebaseAuth et j'ouvre déjà un problème Github, mais je cherche une solution de contournement pour ce problème. Je cherche désespérément une solution. Cela ne me dérange pas même si les sessions entre les onglets ne sont pas synchronisées. Je n'ai pas besoin de cette fonctionnalité. J'ai lu quelque part

si vous n'avez pas besoin de cette fonctionnalité, les efforts de modularisation de Firebase V6 vous permettront de passer à localStorage qui a des événements de stockage pour détecter les changements de tableaux croisés, et vous donnera peut-être la possibilité de définir votre propre interface de stockage.

Si c'est la seule solution, comment l'implémenter?

J'ai juste besoin d'une solution qui arrête cette augmentation inutile de l'auditeur car elle ralentit l'ordinateur et plante mon application. Mon application doit fonctionner pendant plus de 20 heures, elle est donc désormais inutilisable en raison de ce problème. Je cherche désespérément une solution.

TSR
la source
Je n'ai pas réussi à reproduire votre problème sur votre exemple
Sergey Mell
@SergeyMell Avez-vous utilisé le code que j'ai publié sur StackBlitz?
TSR
Oui. En fait, j'en parle.
Sergey Mell
Essayez de télécharger le code et de l'exécuter localement. Je l'ai également téléchargé dans le lecteur au cas où drive.google.com/file/d/1fvo8eJrbYpZWfSXM5h_bw5jh5tuoWAB2/…
TSR

Réponses:

7

TLDR: l'augmentation du nombre d'écouteurs est un comportement attendu et sera réinitialisée lors du garbage collection. Le bogue qui provoque des fuites de mémoire dans Firebase Auth a déjà été corrigé dans Firebase v7.5.0, voir # 1121 , vérifiez votre package-lock.jsonpour confirmer que vous utilisez la bonne version. En cas de doute, réinstallez le firebasepackage.

Les versions précédentes de Firebase interrogeaient IndexedDB via le chaînage Promise, ce qui provoque des fuites de mémoire, voir Promise Leaks Memory de JavaScript

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();

Corrigé dans les versions ultérieures utilisant des appels de fonction non récursifs:

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();


Concernant l'augmentation du nombre d'auditeurs de façon linéaire:

Le nombre d'écouteurs augmentant de manière linéaire est attendu car c'est ce que fait Firebase pour interroger IndexedDB. Cependant, les écouteurs seront supprimés chaque fois que le GC le souhaite.

Lire problème 576302: affichage incorrect de fuite de mémoire (écouteurs xhr et charge)

V8 exécute périodiquement Minor GC, ce qui provoque ces petites baisses de la taille du tas. Vous pouvez réellement les voir sur le graphique des flammes. Les GC mineurs ne peuvent cependant pas collecter toutes les ordures, ce qui se produit évidemment pour les auditeurs.

Le bouton de la barre d'outils appelle le GC principal qui peut collecter des écouteurs.

DevTools essaie de ne pas interférer avec l'application en cours d'exécution, il ne force donc pas le GC par lui-même.


Pour confirmer que les écouteurs détachés sont récupérés, j'ai ajouté cet extrait pour faire pression sur le tas JS, forçant ainsi GC à se déclencher:

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

Les auditeurs sont des ordures

Comme vous pouvez le voir, les écouteurs détachés sont supprimés périodiquement lorsque le GC est déclenché.



Questions de stackoverflow similaires et problèmes GitHub concernant le nombre d'écouteurs et les fuites de mémoire:

  1. Résultats du profilage des performances des écouteurs dans Chrome Dev Tools
  2. Les écouteurs JavaScript continuent d'augmenter
  3. Application simple provoquant une fuite de mémoire?
  4. $ http 'GET' fuite de mémoire (PAS!) - nombre d'auditeurs (AngularJS v.1.4.7 / 8)
Joshua Chan
la source
Je confirme avoir utilisé 7.5.0 et testé plusieurs fois sur différents environnements. Même this.auth.auth.setPersistence ('none') n'empêche pas la fuite de mémoire. Veuillez le tester vous-même en utilisant le code ici stackblitz.com/edit/angular-zuabzz
TSR
quelles sont vos étapes de test? Dois-je le laisser pendant la nuit pour voir le crash de mon navigateur? Dans mon cas, le numéro d'auditeur se réinitialise toujours après le coup de pied GC et la mémoire est toujours de retour à 160 Mo.
Joshua Chan
@TSR appelle this.auth.auth.setPersistence('none')au ngOnInitlieu du constructeur pour désactiver la persistance.
Joshua Chan
@JoshuaChan importe-t-il quand appeler une méthode de service? Il est injecté dans un constructeur et disponible directement dans son corps. Pourquoi devrait-il entrer ngOnInit?
Sergey
@Sergey principalement pour les meilleures pratiques. Mais pour ce cas spécifique, j'ai exécuté le profilage du processeur pour les deux façons d'appeler setPersistenceet je constate que si cela est fait dans le constructeur, les appels de fonction sont toujours effectués vers IndexedDB, alors que si c'est fait dans ngOnInit, aucun appel n'a été fait vers IndexedDB, pas exactement mais pourquoi
Joshua Chan