【追記 2019/06/17】パッチを含んだ RMagick v3.2.0 がリリースされました。
RMagick を使用すると馬鹿みたいにメモリ使用量があがると言われ続けてましたが、修正方法の見込みがたちようやく直しました。
何が起きていたか
RubyKaigi 2019 で登壇した際にも話しましたが、RMagick が引き起こしていた多数のメモリリークは修正済み だったので、Ruby の GC が動作すると抱え込んでいる不要なメモリ領域は解放されていました。
問題は Ruby の GC がなかなか動作してくれないことにありました。例えば RMagick で画像用のオブジェクトを生成しても、サイズはたかだか 40 bytes しかありません。
require 'rmagick' require 'objspace' img = Magick::Image.new(1000, 1000) puts ObjectSpace.memsize_of(img) #=> 40 bytes
Magick::Image.new()
の引数の値を変更してもサイズが変わることなく、巨大な画像を生成しても Ruby は 40 bytes 確保されたんだとしか認識してくれませんでした。実際はそんなことはなく、RMagick が ImageMagick に対して指定のサイズで画像用の領域をを確保しろと指示するため ImageMagick 内で大量のメモリが消費されていました。
require 'rmagick' 1000.times do |i| img = Magick::Image.new(1000, 1000) end
のようなコードを実行すると、以下のグラフのようにメモリ使用量が線形的に増加していました。
画像用のオブジェクトに関連するメモリ領域を ImageMagick が大量に抱え込んでいても、Ruby から見るとたかだか 40 bytes ずつしか使用量が増えてないため、まだまだ GC を実行しなくても平気だろうと認識され、このようなグラフが描かれます。
解決案
- Ruby には
rb_gc_adjust_memory_usage()
という API があり、ImageMagick が確保したメモリ使用量を通知すれば、Ruby の GC が適切に動きそう。(参照:Ruby 2.4の新機能:rb_gc_adjust_memory_usage() - バインディングとGCの関係を改善するAPI - ImageMagick がメモリを確保・解放するときに Ruby の
xmalloc
/xfree
を使ってくれれば、ImageMagick を含めた全体のメモリ使用量を Ruby が把握できる。
幸い、ImageMagick にはユーザー定義のメモリアロケータをセットできる API が用意されていたので、方法 2
を採用しました。
(https://github.com/rmagick/rmagick/blob/9a704f18f3688a7bb98cad2264681cfb1519103c/ext/RMagick/rmmain.c#L110)
(内部でどれだけメモリを確保したかを教えてくれる API が ImageMagick には無かったため 1
の方法は無理そうだった)
ImageMagick の修正
ImageMagick でユーザー定義のメモリアロケータを使わずに直接 malloc
を使っている箇所がありました。その部分でのメモリ確保量が結構大きいらしく xmalloc
/ xfree
を使うようにしても、まだ期待通りに GC が動いてくれませんでした...
パッチはすでに取り込まれているため、Ruby の GC を適切に動作させるのに必要な情報はとれるようになりました。
結果
ImageMagick と RMagick を修正しました。これにより、以前は不要なオブジェクトがたかだか 40 bytes しかメモリを使ってないからまだ GC を動かすのを止められていたのが、オブジェクトに関連するメモリ領域を ImageMagick が大量に抱え込んでいるのも把握できるようになったので Ruby の GC が適切に動作するようにしました。
修正パッチが含まれている ImageMagick が ver. 6.9.10-49 と ver. 7.0.8-49 としてリリースされています。そのバージョンを使用すると、以下のグラフの赤線のようなメモリ使用量に落ち着きます。
RMagick の修正はこちら。もうすこしで RMagick v3.2.0 としてリリースできそうです。
ImageMagick の ver. 6.9.10-49 と ver. 7.0.8-49 以降と組み合わせて使うと効果は絶大ですが、古い ImageMagick でも多少は効果がありそうな感じでした。