O aplicativo Angular Firebase falha após 20 horas com +1 gigabyte de alocação de memória

13

Descobri que o uso AngularFireAuthModulede '@angular/fire/auth';causa um vazamento de memória que trava o navegador após 20 horas.

Versão:

Eu uso a versão mais recente atualizada hoje usando ncu -u para todos os pacotes.

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

Versão Firebase: "firebase": "^7.5.0",

Como se reproduzir:

Eu criei um código mínimo reproduzível no editor StackBliztz

Aqui está o link para testar o bug diretamente Teste StackBlizt

Sintoma:

Você pode verificar se o código não faz nada. Apenas imprime olá mundo. No entanto, a memória JavaScript usada pelo aplicativo Angular aumenta em 11 kb / s (Chrome Task Manager CRTL + ESC). Após 10 horas deixando o navegador aberto, a memória usada atinge aproximadamente 800 mb (o espaço de memória é cerca de duas vezes 1,6 Gb !)

Como resultado, o navegador fica sem memória e a guia chrome falha.

Após uma investigação mais aprofundada usando o perfil de memória do chrome na guia desempenho, notei claramente que o número de ouvintes aumenta em 2 a cada segundo e, portanto, o heap JS aumenta de acordo.

insira a descrição da imagem aqui

Código que causa o vazamento de memória:

Eu descobri que o uso do AngularFireAuthModule módulo causa vazamento de memória, seja ele injetado em um componentconstrutor ou em um 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){

  }
}

Pergunta :

Pode ser um bug na implementação do FirebaseAuth e já abro um problema no Github, mas estou procurando uma solução alternativa para esse problema. Estou desesperado por uma solução. Eu não me importo, mesmo que as sessões entre guias não estejam sincronizadas. Eu não preciso desse recurso. Eu li em algum lugar que

se você não precisar dessa funcionalidade, os esforços de modularização do Firebase V6 permitirão que você alterne para o localStorage, que possui eventos de armazenamento para detectar alterações nas guias cruzadas, e possivelmente fornecerá a capacidade de definir sua própria interface de armazenamento.

Se essa é a única solução, como implementar isso?

Eu só preciso de qualquer solução que interrompa esse aumento desnecessário de ouvinte, porque diminui a velocidade do computador e trava meu aplicativo. Meu aplicativo precisa ser executado por mais de 20 horas e, portanto, agora não pode ser usado devido a esse problema. Estou desesperado por uma solução.

TSR
fonte
Não consegui reproduzir o seu problema no seu exemplo
Sergey Mell
@SergeyMell Você usou o código que eu publiquei no StackBlitz?
TSR
Sim. Na verdade, estou falando sobre isso.
Sergey Mell
Tente baixar o código e executá-lo localmente. Também o enviei no drive, no caso de drive.google.com/file/d/1fvo8eJrbYpZWfSXM5h_bw5jh5tuoWAB2/…
TSR

Respostas:

7

TLDR: O aumento do número de ouvintes é um comportamento esperado e será redefinido na coleta de lixo. O bug que causa vazamento de memória no Firebase Auth já foi corrigido no Firebase v7.5.0, consulte o nº 1121 , verifique package-lock.jsonse você está usando a versão correta. Se não tiver certeza, reinstale o firebasepacote.

As versões anteriores do Firebase estavam pesquisando o IndexedDB por meio do encadeamento Promise, o que causa vazamentos de memória, consulte Promise Leaks Memory do 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();

Corrigido nas versões subseqüentes usando chamadas de função não 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();


Em relação ao aumento linear do número de ouvintes:

Espera-se um aumento linear da contagem de ouvintes, pois é isso que o Firebase está fazendo para pesquisar o IndexedDB. No entanto, os ouvintes serão removidos sempre que o GC desejar.

Leia a edição 576302: Exibição incorreta de vazamento de memória (ouvintes xhr & load)

A V8 executa GC secundário periodicamente, o que causa pequenas quedas do tamanho da pilha. Você pode realmente vê-los no gráfico de chama. Os GCs menores, no entanto, podem não coletar todo o lixo, o que obviamente acontece para os ouvintes.

O botão da barra de ferramentas chama o GC principal, que é capaz de coletar ouvintes.

O DevTools tenta não interferir com o aplicativo em execução, portanto, não força o GC sozinho.


Para confirmar que os ouvintes desanexados são coletados de lixo, adicionei este snippet para pressionar o heap JS, forçando o GC a disparar:

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

Ouvintes são coletados de lixo

Como você pode ver, os ouvintes desanexados são removidos periodicamente quando o GC é acionado.



Perguntas similares ao stackoverflow e problemas do GitHub com relação ao número do ouvinte e vazamentos de memória:

  1. Ouvintes nos resultados de perfil de desempenho das ferramentas de desenvolvimento do Chrome
  2. Ouvintes JavaScript continuam aumentando
  3. Aplicativo simples causando um vazamento de memória?
  4. Vazamento de memória $ http 'GET' (NOT!) - número de ouvintes (AngularJS v.1.4.7 / 8)
Joshua Chan
fonte
Confirmo usando o 7.5.0 e testei várias vezes em diferentes ambientes. Mesmo this.auth.auth.setPersistence ('none') não impede o vazamento de memória. Por favor, teste-o usando o código aqui stackblitz.com/edit/angular-zuabzz
TSR
quais são suas etapas de teste? Preciso deixá-lo durante a noite para ver meu navegador travar? No meu caso, o número do ouvinte sempre é redefinido depois que o GC é ativado e a memória sempre volta a 160mb.
Joshua Chan
@TSR chama this.auth.auth.setPersistence('none')em ngOnInitvez do construtor para desativar a persistência.
Joshua Chan
@ JoshuaChan importa quando chamar um método de serviço? Ele está sendo injetado em um construtor e disponível diretamente no corpo. Por que deveria entrar ngOnInit?
Sergey
@ Emerge principalmente para as melhores práticas. Mas, nesse caso específico, executei o perfil da CPU para as duas formas de chamada setPersistencee descobri que, se for feito no construtor, ainda serão feitas chamadas de função ao IndexedDB, enquanto que, se for feito ngOnInit, nenhuma chamada será feita ao IndexedDB, não exatamente certo por que embora
Joshua Chan