La aplicación Angular Firebase se bloquea después de 20 horas con +1 gigabyte de asignación de memoria

13

Descubrí que el uso de AngularFireAuthModulefrom '@angular/fire/auth';causa una pérdida de memoria que bloquea el navegador después de 20 horas.

Versión:

Utilizo la última versión actualizada hoy usando ncu -u para todos los paquetes.

Fuego angular: "@angular/fire": "^5.2.3",

Versión base de fuego: "firebase": "^7.5.0",

Cómo reproducir:

Hice un código mínimo reproducible en el editor StackBliztz

Aquí está el enlace para probar el error directamente Prueba StackBlizt

Síntoma:

Puede comprobar usted mismo que el código no hace nada. Simplemente imprime hola mundo. Sin embargo, la memoria de JavaScript utilizada por la aplicación Angular aumenta en 11 kb / s (Chrome Task Manager CRTL + ESC). Después de 10 horas dejando el navegador abierto, la memoria utilizada alcanza aproximadamente 800 mb (¡la huella de la memoria es aproximadamente el doble de 1,6 Gb !)

Como resultado, el navegador se queda sin memoria y la pestaña de Chrome se bloquea.

Después de una mayor investigación utilizando el perfil de memoria de Chrome en la pestaña de rendimiento, noté claramente que el número de oyentes aumenta en 2 por segundo y, por lo tanto, el montón JS aumenta en consecuencia.

ingrese la descripción de la imagen aquí

Código que causa la pérdida de memoria:

Descubrí que el uso del AngularFireAuthModule módulo causa la pérdida de memoria si se inyecta en un componentconstructor o en 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){

  }
}

Pregunta :

Podría ser un error en la implementación de FirebaseAuth y ya abro un problema de Github, pero estoy buscando una solución para este problema. Estoy desesperado por una solución. No me importa incluso si las sesiones en las pestañas no están sincronizadas. No necesito esa característica. Leí en alguna parte que

Si no necesita esta funcionalidad, los esfuerzos de modularización de Firebase V6 le permitirán cambiar a localStorage, que tiene eventos de almacenamiento para detectar cambios en las pestañas, y posiblemente le brinde la capacidad de definir su propia interfaz de almacenamiento.

Si esa es la única solución, ¿cómo implementar eso?

Solo necesito cualquier solución que detenga este aumento innecesario de oyentes porque ralentiza la computadora y bloquea mi aplicación. Mi aplicación debe funcionar durante más de 20 horas, por lo que ahora no se puede usar debido a este problema. Estoy desesperado por una solución.

TSR
fuente
No pude reproducir su problema en su ejemplo
Sergey Mell, el
@SergeyMell ¿Usó el código que publiqué en StackBlitz?
TSR
Si. En realidad, estoy hablando de eso.
Sergey Mell
Intente descargar el código y ejecutarlo localmente. También lo cargué en la unidad por si drive.google.com/file/d/1fvo8eJrbYpZWfSXM5h_bw5jh5tuoWAB2/…
TSR

Respuestas:

7

TLDR: aumentar el número de oyentes es un comportamiento esperado y se restablecerá en la recolección de basura. El error que causa pérdidas de memoria en Firebase Auth ya se ha solucionado en Firebase v7.5.0, consulte # 1121 , verifique package-lock.jsonque confirme que está utilizando la versión correcta. Si no está seguro, reinstale el firebasepaquete.

Las versiones anteriores de Firebase sondeaban IndexedDB a través del encadenamiento de Promise, lo que causa pérdidas de memoria, consulte la memoria de Promise Leaks 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();

Solucionado en versiones posteriores que utilizan llamadas a funciones no recursivas:

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


En cuanto al número de oyentes que aumenta linealmente:

Se espera un recuento de oyentes en aumento lineal, ya que esto es lo que Firebase está haciendo para sondear IndexedDB. Sin embargo, los oyentes serán eliminados cuando el GC lo desee.

Lea el problema 576302: muestra incorrectamente la pérdida de memoria (oyentes xhr y carga)

V8 realiza GC menor periódicamente, lo que provoca esas pequeñas caídas del tamaño de almacenamiento dinámico. De hecho, puedes verlos en la tabla de llamas. Sin embargo, los GC menores pueden no recolectar toda la basura, lo que obviamente sucede para los oyentes.

El botón de la barra de herramientas invoca el GC principal que puede recopilar oyentes.

DevTools intenta no interferir con la aplicación en ejecución, por lo que no fuerza al GC por sí solo.


Para confirmar que los oyentes separados son basura recolectada, agregué este fragmento para presionar el montón JS, forzando así a GC a disparar:

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

Los oyentes son basura recolectada

Como puede ver, los oyentes desconectados se eliminan periódicamente cuando se activa GC.



Preguntas similares de stackoverflow y problemas de GitHub con respecto al número de escucha y pérdidas de memoria:

  1. Oyentes en los resultados del perfil de rendimiento de las herramientas de desarrollo de Chrome
  2. Los oyentes de JavaScript siguen aumentando
  3. ¿Una aplicación simple que causa una pérdida de memoria?
  4. Pérdida de memoria $ http 'GET' (¡NO!) - número de oyentes (AngularJS v.1.4.7 / 8)
Joshua Chan
fuente
Confirmo que uso 7.5.0 y probé varias veces en diferentes entornos. Incluso this.auth.auth.setPersistence ('none') no evita la pérdida de memoria. Pruébelo
TSR
¿Cuáles son tus pasos de prueba? ¿Tengo que dejarlo durante la noche para ver el bloqueo de mi navegador? En mi caso, el número del oyente siempre se restablece después de la activación del GC y la memoria siempre vuelve a 160mb.
Joshua Chan
Llamada @TSR this.auth.auth.setPersistence('none')en ngOnInitlugar del constructor para deshabilitar la persistencia.
Joshua Chan
@JoshuaChan ¿Importa cuándo llamar a un método de un servicio? Se está inyectando en un constructor y está disponible directamente en su cuerpo. ¿Por qué debería entrar ngOnInit?
Sergey
@Sergey principalmente para las mejores prácticas. Pero para este caso específico, ejecuté el perfil de CPU para ambas formas de llamar setPersistencey descubrí que si se realiza en el constructor, las llamadas de función aún se realizan a IndexedDB, mientras que si se realiza en ngOnInit, no se realizaron llamadas a IndexedDB, no exactamente seguro por qué sin embargo
Joshua Chan