Angular Firebaseアプリが20時間後に+1ギガバイトのメモリ割り当てでクラッシュする


13

AngularFireAuthModulefrom を使用する'@angular/fire/auth';と、メモリリークが発生し、20時間後にブラウザがクラッシュすることがわかりました。

バージョン:

私はすべてのパッケージにncu -uを使用して今日更新された最新バージョンを使用します。

角火: "@angular/fire": "^5.2.3",

Firebaseバージョン: "firebase": "^7.5.0"

再現方法:

StackBliztzエディターで最小限の再現可能なコードを作成しました

StackBliztテストを直接バグをテストするためのリンクは次のとおりです

症状:

コードが何も実行しないことを確認できます。Hello Worldを出力するだけです。ただし、Angularアプリで使用されるJavaScriptメモリは11 kb / s増加します(ChromeタスクマネージャーCRTL + ESC)。ブラウザーを開いたまま10時間後、使用されるメモリは約800 MBに達します(メモリフットプリントは1.6 Gbの約2倍です)。

その結果、ブラウザのメモリが不足し、Chromeタブがクラッシュします。

パフォーマンスタブでクロムのメモリプロファイリングを使用してさらに調査したところ、リスナーの数が毎秒2ずつ増加し、それに応じてJSヒープが増加することにはっきりと気付きました。

ここに画像の説明を入力してください

メモリリークを引き起こすコード:

AngularFireAuthModule モジュールを使用すると、componentコンストラクターに注入されても、に注入されても、メモリリークが発生することがわかりました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){

  }
}

質問

FirebaseAuthの実装のバグである可能性があり、すでにGithubの問題を開いていますが、この問題の回避策を探しています。私は解決策を切望しています。タブ間のセッションが同期されていなくてもかまいません。その機能は必要ありません。どこかで読んだ

この機能が不要な場合、Firebase V6のモジュール化の取り組みにより、変更クロスタブを検出するためのストレージイベントを持つlocalStorageに切り替えることができ、独自のストレージインターフェースを定義できるようになる可能性があります。

それが唯一の解決策である場合、それを実装する方法は?

コンピューターが遅くなり、アプリがクラッシュするため、この不要なリスナーの増加を停止するソリューションが必要です。私のアプリは20時間以上実行する必要があるため、この問題のために使用できなくなりました。私は解決策を切望しています。



あなたの例であなたの問題を再現できませんでした
セルゲイメル

@SergeyMell StackBlitzに投稿したコードを使用しましたか?
TSR

はい。実際、私はそれについて話している。
Sergey Mell

コードをダウンロードしてローカルで実行してみてください。また、drive.google.com / file / d / 1fvo8eJrbYpZWfSXM5h_bw5jh5tuoWAB2 /…を念の
TSR

回答:


7

TLDR:リスナー番号の増加は予想される動作であり、ガベージコレクション時にリセットされます。Firebase認証でのメモリリークが既にFirebaseのV7.5.0で修正されている原因のバグは、参照#1121あなたのを確認し、package-lock.json正しいバージョンを使用していることを確認します。不明な場合は、firebaseパッケージを再インストールしてください。

Firebaseの以前のバージョンは、Promiseチェーンを介してIndexedDBをポーリングしていたため、メモリリークが発生しました。JavaScript のPromiseリークメモリを参照してください

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

非再帰関数呼び出しを使用する後続のバージョンで修正されました:

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


リスナー数の直線的な増加について:

これは、FirebaseがIndexedDBをポーリングするために実行するものであるため、リスナー数の直線的な増加が予想されます。ただし、リスナーは、GCが必要とするたびに削除されます。

課題576302を読む:メモリ(リスナーxhr&ロード)リークを誤って表示

V8はマイナーGCを定期的に実行するため、ヒープサイズの小さな低下が発生します。フレームチャートで実際に見ることができます。ただし、マイナーGCはすべてのガベージを収集するわけではありません。これは明らかにリスナーに起こります。

ツールバーボタンは、リスナーを収集できるMajor GCを呼び出します。

DevToolsは実行中のアプリケーションに干渉しないように努めるので、GC自体を強制することはありません。


デタッチされたリスナーがガベージコレクションされていることを確認するために、このスニペットを追加してJSヒープに圧力をかけ、GCを強制的にトリガーしました。

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

リスナーはガベージコレクションされます

ご覧のとおり、切り離されたリスナーは、GCがトリガーされると定期的に削除されます。



リスナー番号とメモリリークに関する同様のスタックオーバーフローの質問とGitHubの問題:

  1. Chrome開発ツールのリスナーのパフォーマンスプロファイリング結果
  2. JavaScriptリスナーは増加し続けています
  3. 単純なアプリがメモリリークを引き起こしていますか?
  4. $ http 'GET'メモリリーク(NOT!)-リスナーの数(AngularJS v.1.4.7 / 8)

7.5.0の使用を確認し、異なる環境で複数回テストしました。this.auth.auth.setPersistence( 'none')でもメモリリークを防ぐことはできません。こちらのコードを使用して、自分でテストしてください。stackblitz.com/edit/angular
TSR

テスト手順は何ですか?ブラウザがクラッシュするのを確認するために、一晩放置する必要がありますか?私の場合、GCが起動してリスナー番号が常にリセットされ、メモリは常に160MBに戻ります。
Joshua Chan

@TSRコールthis.auth.auth.setPersistence('none')ngOnInit無効に固執する代わりに、コンストラクタの。
Joshua Chan

@JoshuaChanサービスのメソッドを呼び出すタイミングは重要ですか?コンストラクターに注入されており、本体で直接使用できます。なぜそれを入れるべきngOnInitですか?
セルゲイ、

@Sergeyは主にベストプラクティスです。しかし、この特定のケースでは、両方の呼び出し方法でCPUプロファイリングをsetPersistence実行しましたが、コンストラクタで行われた場合、関数呼び出しは引き続きIndexedDBに対して行われngOnInitます。確かに理由
Joshua Chan
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.