メモリプロファイラ の続き

メモリプロファイルでGCが発生を認識する方法を考えてみました。ファイナライザを使う方法です。

  @@gccnt = 0
  def MemProf.gchandle
    lambda {|id|
      @@gccnt = @@gccnt + 1
      GCChecker.new
    }
  end

  class GCChecker
    def initialize
      ObjectSpace.define_finalizer(self, MemProf.gchandle)
    end
  end
  GCChecker.new
  GCChecker.new # ダミー、なぜ必要かわからないし、わかりたくない

こんな感じの考え方です。

  • ダミーのオブジェクトを用意してファイナライザを設定しておきます
  • オブジェクトは次GCが起きたら回収されるよう一切の参照を切っておきます
  • ファイナライザーのハンドラでgcの回数を増やします
  • さらに次のダミーオブジェクトを生成してファイナライザーを設定します。

GCChecker.newが2つありますが、なぜか1つだと回収されません。処理系内に秘密の参照があるように感じますが、あまり深入りしたくないのでそっとしておいています。そんな感じですので、バージョンによっては動かないかもしれないという代物です。

GCの回数を数えるバージョンのMemProfですが、不完全です。GCの回数は高々100回以内でオブジェクトの数は何百万になるのでグラフにするとGCの回数が潰れてしまいます。もちろん、グラフや軸を分ければいいのですが、サボってます。

#!/bin/env ruby
#
# メモリプロファイラ
#
require 'gnuplot'

module MemProf
  PROFILE_RESOLUTION = 0.01

  WATCH_TYPE = [
    :TOTAL, :T_ARRAY, :T_STRING, :T_NODE, :T_BIGNUM,
    :GC
  ]
  TEXT_OUTPUT = false

  GNUPLOT_OUTPUT = true
  GNPULOT_FORMAT = "png"
  GNUPLOT_OUTFILE = "foo.png"
  
  @@gccnt = 0
  def MemProf.gchandle
    lambda {|id|
      @@gccnt = @@gccnt + 1
      GCChecker.new
    }
  end

  class GCChecker
    def initialize
      ObjectSpace.define_finalizer(self, MemProf.gchandle)
    end
  end
  GCChecker.new
  GCChecker.new # ダミー、なぜ必要かわからないし、わかりたくない

  mem_snapshot = []
  Thread.new(mem_snapshot) do |ms|
    prevtime = 0
    while true do
      currenttime = Process.times.utime
      if currenttime - prevtime > PROFILE_RESOLUTION then
        cntobj = ObjectSpace.count_objects
        cntobj[:GC] = @@gccnt
        ms.push [currenttime, cntobj]
      end
      prevtime = currenttime
      Thread.pass
    end
  end
  
  END {
    if TEXT_OUTPUT == true then
      WATCH_TYPE.each do |ev|
        print "# #{ev} \n"
        mem_snapshot.each do |t, v|
          print "#{t} #{v[ev]}\n"
        end
        print "\n\n"
      end
    end

    if GNUPLOT_OUTPUT == true then
      Gnuplot.open do |gp|
        Gnuplot::Plot.new(gp) do |plot|
          plot.terminal GNPULOT_FORMAT
          plot.output GNUPLOT_OUTFILE
          plot.autoscale
          plot.title "Memory usage"
          x = mem_snapshot.map {|n| n[0]}
          WATCH_TYPE.each do |ev|
            y = mem_snapshot.map {|n| n[1][ev]}
            plot.data << Gnuplot::DataSet.new([x, y]) do |ds|
              ds.with = "line"
              ds.title = ev
            end
          end
        end
      end
    end
  }
end

$0 = ARGV[0]
fn = ARGV[0]
ARGV.shift

load fn, true