Firefoxのメモリ消費量が右肩上がりで増加する場合の対策 - 2018-10-24 - ククログ

ククログ

株式会社クリアコード > ククログ > Firefoxのメモリ消費量が右肩上がりで増加する場合の対策

Firefoxのメモリ消費量が右肩上がりで増加する場合の対策

FirefoxをWindows Serverベースのシンクライアント環境で使っている組織において、Firefoxを更新したりWindows Serverをリプレースしたりといったタイミング以降で、急にパフォーマンスが低下しだしたというお問い合わせを複数頂きました。調査の結果、それらの現象は同一の原因である可能性が高い事、同様の対策が有効に作用する事が分かりましたので、その際に得られた調査結果を知見としてご紹介します。

なお、本エントリで紹介する設定はすべてFirefox ESR52とFirefox ESR60向けの物です。 将来のバージョンのFirefoxでは有効でない可能性がありますので、ご注意下さい。

パフォーマンス低下の原因

お問い合わせを頂いた複数の事例において、サーバー全体のリソース状況を監視して頂いた所、業務の開始と共にメモリの消費量が右肩上がりに増大しており、退勤時刻になってユーザーがログオフし始めるまでメモリ不足の状況が続くという状況である事が分かりました。また、この時メモリを最も消費しているのはFirefoxのプロセスであるという事も判明しました。

(メモリ消費量の傾向のイメージ)

いくつかの事例では、FirefoxやWindows Serverのリプレース以前にはこのような現象は発生していなかったという事が分かっていました。そのため、これは単純にメモリ消費量が増大したというよりも、 使い終わったメモリ領域がいつまでも開放されないという問題(いわゆるメモリリーク) であると考えられました。

一般に、プログラムの動作速度と消費リソースの量は比例します。現在のFirefoxは体感速度の向上に努めているため、キャッシュを多めに保持したり、複数のプロセスを並行動作させたりといった形でリソースの消費が多くなりがちです。その過程で必ずしも必要でないような大量のメモリ領域を確保する事があり、それが何らかの理由で解放されないままとなってしまうと、物理メモリの枯渇に繋がり、ディスクドライブ上の仮想メモリとの間でスワップが頻発して却ってパフォーマンスが悪化してしまう事になります。

よって、この問題を解消するためには何らかの方法でメモリ消費量を削減する必要があります。

端的にメモリ消費量を抑える

Firefoxが消費するメモリの上限を強制的に設定する事はできないのでしょうか? 実は、できます。 Firefox全体での消費メモリ量の上限は設定できませんが、FirefoxのJavaScript実行エンジンが使用できるメモリの量は上限を設定できるようになっており、例えば以下のようにするとJavaScript実行エンジンの使用可能なメモリの上限が1GBに設定されます。

// 単位はbytes。
// 1GB=1024MB、1MB=1024KB、1KB=1024bytesのため、
// 1024 * 1024 * 1024 で1GBを表す。
lockPref("javascript.options.mem.max", 1024 * 1024 * 1024);

このlockPref()という書き方は、MCDと呼ばれる設定方法に特有の物です。Windows Serverであれば、そのマシン上のFirefoxのインストール先に上記の内容を記載したautoconfig.cfgを設置することで、そのWindows Severにログオンするすべてのユーザーに設定が反映されます。

ただし、この方法で上限を設定した場合に上限を超える量のメモリが必要な状況が発生すると、Firefoxがクラッシュしたり、フリーズしたり、Webページを表示できなくなったりといったトラブルが発生する可能性があります。 現代のWebページは1ページあたり数百MBのメモリを消費する事も多く、安心して動作できるだけの数値を設定すると、多人数でリソースを共有するWindows Serverにおいては結局は各ユーザーで物理メモリを容易に食い潰してしまうという結果になるでしょう。 ですので、この設定は今回のような事例では実際には有用ではありません。

Firefoxのプロセス数を減らす

(2023年3月2日追記:2023年時点のバージョンのFirefoxはマルチプロセス動作有効状態での使用が基本となっており、ここで紹介している設定を行ってもシングルプロセス動作にはなりません。また、強制的にシングルプロセス動作させることもできますが、その場合一部の機能が正常に動作しなくなります。)

安全に行える範囲の設定では、マルチプロセス機能の無効化がメモリ消費量の削減にある程度有効です。

現在のFirefoxは、体感速度や安定性、セキュリティを向上する事を目的に、処理を複数のプロセスで並行して行う設計となっています。 しかし基本的にFirefoxはそれなりの量のメモリを消費する設計のため、プロセス数が増えれば増えるほどその分余計なメモリを消費する事になります。 マルチプロセス機能を無効化すると、前述の利点を失う事と引き替えにメモリ消費量を削減する事ができます。

マルチプロセス機能についてはFirefoxの設定画面からもある程度の設定は変更できますが、抜本的な無効化には隠し設定の変更が必要です。 前述のMCDで行う場合は以下のようになります。

// マルチプロセス機能の無効化
lockPref("browser.tabs.remote.autostart", false);
lockPref("browser.tabs.remote.desktopbehavior", false);
lockPref("dom.ipc.multiOptOut", 1);
lockPref("browser.tabs.remote.force-enable", false);

ただ、メモリ消費量の増大がメモリリークに起因している状況では、これも根本的な対策とはなりません。 プロセス数が少ない分、メモリ消費量の増大ペースは緩やかになる事が予想されますが、それでもいつかはメモリの枯渇が発生します。

開けるタブの最大数を制限する

プロセス数の削減と似た対策として、Firefox上で開けるタブの最大数を制限するという方法もあります。 これはFirefox本体の設定のみでは不可能で、Lean Tab Limiterというアドオンを導入する事で行えます。 全体向けに導入する場合、ポリシー設定でpolicies.Extensions.Installを設定するか、特定のフォルダにファイルを設置することによるサイドローディング若干情報が古い日本語での解説)でインストールする必要があります。

ただし、2018年10月24日現在公開されているLean Tab Limiter 0.1は管理者による設定の変更に対応していないため、タブの最大数の制限は各ユーザーが設定しなくてはなりません。 管理者側でタブの最大数を設定したい場合は、Storage Manifestを使った管理者による設定に対応した改造版であるLean Tab Limiter Advancedを使う必要があります。以下は、タブの最大数を5に設定しつつ、制限を超えた数のタブを開こうとした場合に通知メッセージを端的に表示する場合の設定例です。

{
  "name": "lean-tab-limiter-advanced@clear-code.com",
  "description": "Managed storage manifest for lean-tab-limiter-advanced",
  "type": "storage",
  "data": {
    "tab-limit": 5,
    "notify-blocked": true,
    "notify-blocked-title": "%s+1個以上のタブ及びウィンドウは開けません",
    "notify-blocked-message": " "
  }
}

原因を踏まえてメモリの過大な消費を抑制する

Firefoxにおいてメモリリークに類する現象が発生する場合、ガーベジコレクションが適切に実行されていない事が原因となっている場合があります。

ガーベジコレクションはJavaScriptなどの言語において不要になったメモリ領域の回収・解放を行う仕組みですが、ガーベジコレクションを行うためには基本的に他のすべてを停止する必要があるため、単純に頻繁に実行すると体感のパフォーマンスが低下します。 そのため初期状態では、一定時間以上操作が行われていなかったり、改修が可能そうなメモリの量がある程度以上の量溜まっていると検出されたり、といった条件が満たされた時に初めて実行されるという設定になっています。 よって、何らかの理由からガーベジコレクションの実行条件が満たされにくい状態にあると、未使用メモリが回収されないまま新たにメモリが確保されるという事が繰り返されるために、見た目のメモリ消費量が右肩上がりに増大してしまうという状況が発生します。

また、Firefoxの見た目のメモリ消費量が急上昇する背景には、Firefoxの「こまめにメモリ領域を確保するのではなく、ある程度のまとまった量のメモリ領域を先んじて確保しておき、必要に応じてFirefox内部でそれを小分けにして使う」という戦略があります。 これは、OSに対してのメモリ確保要求は一般的に時間がかかる事が多いため、このような戦略をとった方が体感速度の向上に繋がるという理由によるものですが、この事とガーベジコレクションが実行されないという現象が重なると、見た目のメモリ消費量が異常な速度で上昇していくという状況が発生します。

よって、以下のような設定を行えば、体感速度の低下と引き替えにメモリ消費量の異常な増大を防ぐ事ができると考えられます。

  • ガーベジコレクションの実行頻度を上げる。

  • 一度に確保するメモリ領域の大きさを小さくする。

実際に、デスクトップPCに比べて搭載メモリ量が小さいAndroid端末用の初期設定は、Firefox 52時点ではそのような趣旨に沿った内容になっています1

以上の事を踏まえての、効果が大きく且つ体感速度の低下度合いが小さいと考えられる設定項目とその設定値の例は以下の通りです。

// GCを実行する基準にするメモリ消費量(単位:MB)。0〜1000の間で設定。
// 数値を小さくする事で、GCの実行頻度を上げ、
// パフォーマンス低下と引き替えにメモリ使用量を削減できる可能性がある。
// 初期値は30。
lockPref("javascript.options.mem.gc_allocation_threshold_mb", 3);

// 高頻度でGCとメモリ領域の確保が実行される場面での、最大の
// メモリ確保増加量(パーセンテージ)。
// 値を小さくする事で、パフォーマンス低下と引き替えにメモリ使用量を
// 削減できる可能性がある。
// 初期値は300。
lockPref("javascript.options.mem.gc_high_frequency_heap_growth_max", 150);

// 高頻度でGCとメモリ領域の確保が実行される場面での、最小の
// メモリ確保増加量(パーセンテージ)。
// 値を小さくする事で、パフォーマンス低下と引き替えにメモリ使用量を
// 削減できる可能性がある。
// 初期値は150。
lockPref("javascript.options.mem.gc_high_frequency_heap_growth_min", 150);

// 高頻度でGCとメモリ領域の確保が実行される場面での、最大の
// メモリ確保増加量(単位:MB)。
// 値を小さくする事で、パフォーマンス低下と引き替えにメモリ使用量を
// 削減できる可能性がある。
// 初期値は500。
lockPref("javascript.options.mem.gc_high_frequency_high_limit_mb", 100);

// 高頻度でGCとメモリ領域の確保が実行される場面での、最小の
// メモリ確保増加量(単位:MB)。
// 値を小さくする事で、パフォーマンス低下と引き替えにメモリ使用量を
// 削減できる可能性がある。
// 初期値は100。
lockPref("javascript.options.mem.gc_high_frequency_low_limit_mb", 100);

// GCを行うしきい値となるゾーンごとのGC対象のメモリ使用量(単位:MB)。
// 数値を小さくする事で、より頻繁にGCが実行されるようになり、
// パフォーマンス低下と引き替えにメモリ使用量を削減できると考えられる。
// 初期値は128。
lockPref("javascript.options.mem.high_water_mark", 16);

また、まだお客様環境への反映実績はないものの、Firefox ESR60の時点でデスクトップ版とAndroid版の間で初期設定が変更されている項目として以下の設定も有効である可能性があります。

// GC対象になりやすい短命なオブジェクトを保持するためのメモリ領域の最大サイズ(単位:KB)。
// 数値を小さくする事で、単純に必要なメモリ領域の量を削減できると考えられる。
// また、空きメモリ領域がこの値より小さい時は常にアイドル時にGCが実行される。
// デスクトップ版Firefoxでの初期値は16384、Android版の初期値は4096。
lockPref("javascript.options.mem.nursery.max_kb", 4096);

実際に、お問い合わせがあったお客様の環境ではこれらの設定によってメモリ消費量の右肩上がりの増大が収まり、旧環境とほぼ同水準のメモリ消費量に収まるようになったという結果が得られています。 しかしその一方で、消費リソースを削減するという事から事前に予想された通り、体感速度の低下も発生しており、ユーザー体験の悪化が許容範囲内となるバランスを模索する必要があったようです。 本エントリを参考に設定を反映する場合でも、上記の設定を叩き台として、ピーク時にもFirefox以外のアプリケーションを実行できる程度の空きメモリ領域を確保できる程度の範囲で、なるべく体感速度が損なわれないよう数値を調整する事を強くお勧めします。

その他、メモリ消費量と体感速度に関係している可能性がある設定

なお、Firefox ESR52を対象とした調査の過程で、メモリ消費量と体感速度に影響を与える可能性がある設定項目として、前項に挙げた項目の他にも以下の物がある事が分かりました。 それぞれどの程度の効果があるかは不明ですが、参考として記載しておきます。

  • javascript.options.asmjs(初期値:true):asm.jsのコンパイラを有効にする。無効化する事で、パフォーマンス低下と引き替えに若干メモリ消費が減る可能性がある。

  • javascript.options.baselinejit(初期値:true):Firefox上で実行されるJavaScriptの全てについて、実行時コンパイルを行い処理を高速化する。無効化する事で、パフォーマンス低下と引き替えに若干メモリ消費が減る可能性がある。

  • javascript.options.compact_on_user_inactive(初期値:true):ユーザーが離席している時にGCを行う。無効化するとGCの実行頻度が下がる。

  • javascript.options.compact_on_user_inactive_delay(初期値:300000):ユーザーが離席していると判断する時間(ミリ秒)。初期値は30秒だが、これを小さくするとより頻繁にGCが実行され、メモリ消費が減る可能性がある。

  • javascript.options.discardSystemSource(初期値:false):コンパイルした関数のソースをメモリ上から破棄するかどうか。有効化する事でメモリ消費が減る可能性があるが、Function.prototype.toSource()が機能しなくなるため、この機能を使用しているスクリプトを実行する場合は期待通りに動作しなくなる恐れがある。

  • javascript.options.gc_on_memory_pressure(初期値:true):空きメモリが逼迫して充分な使用メモリ領域を確保できなかった時にGCを実行するかどうか。無効化するとGCの実行頻度が下がる。

  • javascript.options.ion(初期値:true):Firefox上でのJavaScriptの実行時コンパイルについて、新型のエンジンであるIonMonkeyを使用する。無効化する事で、パフォーマンス低下と引き替えに若干メモリ消費が減る可能性がある。

  • javascript.options.ion.offthread_compilation(初期値:true):IonMonkeyにおいて別スレッドでコンパイルを実行する。無効化する事で、パフォーマンス低下と引き替えに若干メモリ消費が減る可能性がある。

  • javascript.options.mem.gc_compacting(初期値:true):GCの実行時に使用メモリの縮小も行うかどうか。インクリメンタルGCの場合には影響しない。無効化するとメモリ使用量が増大する可能性があり、変更しない事が望ましいと考えられる。

  • javascript.options.mem.gc_dynamic_heap_growth(初期値:true):使用メモリ領域を確保する時の一度に確保するメモリの量を動的に変えるかどうか。無効化すると「300%」固定になる。使用メモリを最小化する事を優先する場合、300%固定にされるよりは、動的な計算でgc_low_frequency_heap_growthgc_high_frequency_heap_growth_maxで上限を110%などに設定した方が有効と思われる。

  • javascript.options.mem.gc_dynamic_mark_slice(初期値:true):高頻度でメモリ領域の確保が行われている時に、インクリメンタルGCにおいて処理単位を大きくする。無効化するとインクリメンタルGCの効率が落ちメモリ使用量が減りにくくなると予想される。

  • javascript.options.mem.gc_high_frequency_time_limit_ms(初期値:1000):GCとメモリ領域の確保が頻繁に行われていると判断する基準の時間(ミリ秒)。値を大きくすることで、パフォーマンス低下と引き替えにメモリ使用量を削減できる可能性がある。

  • javascript.options.mem.gc_incremental(初期値:true):インクリメンタルGC(段階的なGC)を行うかどうか。無効化するとGCの実行頻度が下がりメモリ使用量の増大に繋がる可能性がある。

  • javascript.options.mem.gc_incremental_slice_ms(初期値:10):インクリメンタルGCを行う時間間隔(ミリ秒)。既に充分小さい間隔なので、間隔を小さくして頻度をこれ以上上げてもメモリ使用量の削減という効果はあまり変わらないと予想される。逆に、間隔を大きくして頻度を下げるとメモリ使用量が増大すると考えられる。以上の事から、変更しない事が望ましいと考えられる。

  • javascript.options.mem.gc_low_frequency_heap_growth(初期値:150):低頻度(1秒に1回レベル)でGCとメモリ領域の確保が実行される場面での、メモリ確保増加量(パーセンテージ)。値を小さくする事で、パフォーマンス低下と引き替えにメモリ使用量を削減できる可能性がある。

  • javascript.options.mem.gc_max_empty_chunk_count(初期値:30):GCに使用するメモリ領域について、キャッシュ的に確保しておく領域の最大数。小さい値を設定すると、パフォーマンス低下と引き替えにメモリ使用量を削減できる可能性がある。

  • javascript.options.mem.gc_min_empty_chunk_count(初期値:1):GCに使用するメモリ領域について、キャッシュ的に確保しておく領域の最小数。小さい値(0)を設定すると、パフォーマンス低下と引き替えにメモリ使用量を削減できる可能性がある。

  • javascript.options.mem.gc_per_zone(初期値:true):GCをゾーンごとに行うかどうか。インクリメンタルGCが有効な場合はこの設定に関わらずインクリメンタルGCが行われれるので、設定変更の必要はないと考えられる。

  • javascript.options.mem.gc_refresh_frame_slices_enabled(初期値:true):インクリメンタルGCの進行中に、メモリの回収を細かい単位で行うかどうか。無効化するとメモリ使用量が増大する可能性がある。

  • javascript.options.parallel_parsing(初期値:true):JavaScriptの解釈を別スレッドで行う。無効化すると、パフォーマンス低下と引き替えにメモリ使用量を削減できる可能性がある。

  • javascript.options.showInConsole(初期値:true):JavaScriptの内部エラーをコンソールに表示する。無効化するとコンソールのためのバッファの消費が減るため、デバッグ情報を収集しにくくなる事と引き替えにメモリ使用量を削減できると考えられる。

  • javascript.options.wasm_baselinejit(初期値:false):WebAssemblyの実行をさらに高速化するかどうか。有効化するとパフォーマンス改善に繋がるが、メモリ使用量は増大すると予想される。

まとめ

以上、Firefoxのメモリ使用量が右肩上がりに増大する状況でのメモリ使用量削減に効果があると思われる対策をご紹介しました。

当社のOSSサポートサービスでは、このエントリに記載したような、企業でFirefoxを運用中に発生したトラブルについての原因および解決策の有無の調査を有償にて承っております。 FirefoxやThunderbirdの運用でお困りの情報システム担当社の方は、お問い合わせフォームよりお問い合わせ下さい。

  1. 一方、現在はこれらのAndroid版用の設定はなくなっていて、一部を除くとデスクトップ版と共通の設定値になっています