Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Witchcraft for Memory

Avatar for pocke pocke
June 28, 2025

Witchcraft for Memory

関西Ruby会議08
2025-06-28

リンクに飛べる便利バージョンはこちら https://drive.google.com/file/d/1Z-nia28E8ppoE6FARNRKaL0GFBt8dC4w/view

Avatar for pocke

pocke

June 28, 2025
Tweet

More Decks by pocke

Other Decks in Technology

Transcript

  1. pp self • Masataka Pocke Kuwabara • 株式会社マネーフォワード所属 • Ruby

    / RBS コミッター • Rails アプリケーションエンジニア • 岡山在住 / 大阪, 京都勤務 • 最近の趣味: ピアノ, ランニング, AIにC#を書か せること
  2. RBS / Steep とは • RBS • Rubyの静的型のための言語、ライブラリ • Steep

    • RBSを使った静的型検査ツール • CLIコマンド、LSPサーバーなどとして動作
  3. RBS を使われるようにするために • rbs collection • Community-driven RBS repository -

    RubyKaigi 2024 • https://github.com/ruby/gem_rbs_collection • rbs subtract • Let's write RBS! - RubyKaigi 2023
  4. メモリ使用量が増加する背景 SteepのLSP Serverでメモリ使用量が多い • SteepのLSP Serverはマルチプロセス構成になっている • 管理用プロセス1台、ワーカープロセス * コア数

    • コア数分だけ使用メモリも増えていく • LSP Serverはエディタに対して常駐する • 複数プロジェクトを開くと、その数だけLSP Serverも起動する
  5. 今日のtopic • Majo • A memory profiler focusing on long-lived

    objects • Refork feature for Steep • A memory-efficient process management
  6. memory_profiler gem とは • Rubyのメモリ使用をプロファイルするためのgem • 集計対象 • プロファイル中に生成されたすべてのオブジェクト(allocated objects)

    • プロファイル中に生成され、プロファイル終了時に残っているオブジェクト (retained objects) • 出力内容 • ファイル、行、クラスごとなどで集計 • メモリ使用量、オブジェクト数などを集計
  7. memory_profiler gemの出力例 Total allocated: 29742818 bytes (281359 objects) Total retained:

    9905 bytes (188 objects) allocated memory by gem ----------------------------------- 27874713 rbs-3.8.0 1763800 pathname 86296 set (snip) allocated memory by file ----------------------------------- 15759032 /path/to/gems/rbs-3.8.0/lib/rbs/parser_aux.rb 3139712 /path/to/gems/rbs-3.8.0/lib/rbs/types.rb 2120553 /path/to/gems/rbs-3.8.0/lib/rbs/environment_loader.rb 1984256 /path/to/gems/rbs-3.8.0/lib/rbs/environment.rb (snip) allocated memory by location ----------------------------------- 15755712 /path/to/gems/rbs-3.8.0/lib/rbs/parser_aux.rb:20 2099025 /path/to/gems/rbs-3.8.0/lib/rbs/environment_loader.rb:164 855200 /path/to/gems/rbs-3.8.0/lib/rbs/types.rb:374 780960 /path/to/gems/rbs-3.8.0/lib/rbs/types.rb:1001 (snip)
  8. memory_profiler gemの問題点 memory_profilerが集計するもの • プロファイル中に生成されたすべてのオブジェクト • Pros: オブジェクト生成が速度のボトルネックになっているようなコードを探すのに 便利 •

    Cons: ピークのメモリを知るにはノイズが多すぎる • プロファイル中に生成され、プロファイル終了時に残っているオブジェクト • Pros: メモリリークなどを探すのに便利 • Cons: メモリ使用量がピークのときにプロファイルを止める必要があるが、コード中 のどこがピークかは自明ではない (今回のケースに合わなかっただけで、memory_profiler gemはめちゃくちゃ便利で愛 用しています)
  9. 出力例 memory_profiler gem と同様の出力をする Total 15055372 bytes (159976 objects) Memory

    by file ----------------------------------- 10431760 /path/to/gems/rbs-3.5.1/lib/rbs/parser_aux.rb 1966348 /path/to/gems/rbs-3.5.1/lib/rbs/environment_loader.rb 980344 /path/to/gems/rbs-3.5.1/lib/rbs/types.rb (snip) Memory by location ----------------------------------- 10431760 /path/to/gems/rbs-3.5.1/lib/rbs/parser_aux.rb:20 1942812 /path/to/gems/rbs-3.5.1/lib/rbs/environment_loader.rb:159 249920 /path/to/gems/rbs-3.5.1/lib/rbs/types.rb:994 (snip)
  10. Majo の実装 • オブジェクトの生成、解放にフック • 生成時にオブジェクトの生成情報を記録 • 生成されたファイル、行、メソッド、オブジェクトのクラスなど • rb_gc_count()

    も記録 • 解放時にそのオブジェクトが指定回数以上GCを生き残っていれば、配列に記録 • 生成時、解放時の rb_gc_count() を比較 • ObjectSpace.memsize_of でメモリサイズを取得、保存
  11. 実装の詳細 - オブジェクトの生成、解放にフック Cレベルの TracePoint APIを使って、オブジェクトの生成、解放にフックする • RUBY_INTERNAL_EVENT_NEWOBJ • RUBY_INTERNAL_EVENT_FREEOBJ

    arg->newobj_trace = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_NEWOBJ, newobj_i, (void *)res); arg->freeobj_trace = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_FREEOBJ, freeobj_i, arg);
  12. Majo を使った効果 空のHash, Arrayが大量に重複していることがわかったので、重複を排除 • https://github.com/ruby/rbs/pull/1950 Arrayの重複排除 • 約8%のメモリ使用量削減 •

    https://github.com/ruby/rbs/pull/2308 Hashの重複排除 • 約13%のメモリ使用量削減 Hashに関しては面白い問題にも遭遇しました 不要な処理が実行速度を速くする謎を追う - Money Forward Developers Blog
  13. fork • *nix系OSで、プロセスを立ち上げる方法 • 自身のプロセスの複製を作成する worker_pids = [] Etc.nprocessors.times do

    # コア数だけループを回す worker_pids << fork do run_worker! # fork のブロックの中は、子プロセスで実行される end end # Handle workers
  14. Copy on Writeの例 x = [1, 2, 3] fork do

    # 子プロセスをfork p x # この時点では x は変更されていないので、親も子も同じメモリを参照 x << 4 # x が変更されたので、子プロセス用に新しいメモリが割り当てられる end CoWをうまく利用すれば、全体でのメモリ使用量を減らせるはず
  15. Refork workerプロセスから更にworkerプロセスをforkする Steep Master Steep Worker (1) Steep Worker (2)

    Steep Worker (3) fork fork fork 親workerプロセスと、その子workerプロセス間で同じメモリを共有できる HTTPサーバーであるPumaで実装されているテクニック
  16. IO.pipe IO.pipe はIOをforkによって親から子へ共有する io_read, io_write = IO.pipe # pipe となるIO

    を生成 fork do # 子プロセスをfork # 子プロセスでもio_read 変数からIO を参照できる io_write.close io_read.each_line do |line| # IO からの入力を読み込む puts "Received: #{line}" end end io_read.close io_write.puts 'Hello, World!' # IO に書き込む つまり管理プロセスから孫workerプロセスにIOを渡すには、子workerをforkすると きに孫worker用の IO.pipe をあらかじめ作成しておく必要がある
  17. FDの送受信 require 'socket' sock_parent, sock_child = UNIXSocket.pair # UNIX ドメインソケットを生成

    fork do # 子プロセスをfork sock_parent.close r = sock_child.recv_io # パイプを受信 while line = r.readline # IO からの入力を読み込む puts "Child: #{line}" end end r, w = IO.pipe # 管理プロセスでパイプを生成 sock_parent.send_io(r) # パイプを子プロセスへ送信 r.close w.puts "Hello, world!" # IO に書き込む
  18. SteepのSocket.pairの使い方 Steep Master Steep Worker (1) Steep Worker (2) Steep

    Worker (3) IO.pipe IO.pipe I IO.pipe Socket.pair IO Steep Worker (2') Steep Worker (3') Killed Killed Fork with IO Fork with IO
  19. Alternatives 通信のためにファイルシステムを利用する • mkfifo やUNIXドメインソケットは、ファイルシステム上にファイルを作成する • 例: /tmp/steep-worker-#{i}.sock を通じて通信 •

    Pros: • あらかじめ名前付けルールを決めておけば、FDをやり取りしなくても通信できる • Cons: • ファイルシステム上のファイルの権限管理に気を配る必要がある • プロセス終了時のファイル削除などの管理が必要
  20. Other key points of Refork • 孫プロセスの終了を待つ方法 • Process.wait は直接の子プロセスしか待てない

    • workerプロセスが孫workerプロセスの管理をするように • Process.warmup • Process.warmup を呼ぶと、GCの実行、メモリのコンパクション、カーネルへのメ モリの返却などが行われる • forkする直前に呼ぶと不要なメモリの共有を抑えられる
  21. Back to Matsuyama 今回の実装を通じて、RubyKaigiをもっと楽しめた • Performance Bugs and Low-level Ruby

    Observability APIs by Ivo • TracePointのC API • Postponed Job API • Bringing Linux pidfd to Ruby by Maciej • 孫プロセスのPID管理
  22. 本屋 いくつか私が推薦した本があります(全部入荷しているそうです!) • 誰のためのデザイン? • 自分の中のデザインについての考え方の基礎に なっている本です。 • @soutaro さんにおすすめされた記憶

    • Linuxプログラミングインターフェース • Steepのrefork機能の実装で、めちゃくちゃ参考 にしました。 • シグナル、プロセス管理について • 20, 21, 22章 シグナル:基礎, シグナルハンドラ, 応用 • 26章 子プロセスの監視 • プロセス間通信について • 43章 プロセス間通信:概要, 44章 パイプとFIFO, 57章 ソケット:UNIXドメイン • 論理学 • 10年近く前に読んだのですが、とても面白く読 み進めた記憶があります。 • 私と同い年の本(16日違い) • CODE コードから見たコンピュータのから くり 第2版 • 論理回路のレベルからコンピュータの構成要素 をボトムアップに知れてとても良い本でした。 • 私が読んだのは第1版ですが、第2版も良い本と 聞いています。