(Mac OS X での) プロセスの消費メモリの測り方

[追記] (Mac OS X / LINUX での) 外部コマンドの消費メモリのモニタリング - ny23の日記 にて task_info を使う方法を書いた.
Mac OS X でプロセスの(最大)消費メモリを測るにはどうすれば良いのだろう.ある時点での消費メモリを知るには,ちょっと調べた限り以下のような方法があるのだが,

  • ps -p $PID -o rss (32bit Leopard だと,4GB までしか測れず; 64bit Snow Leopard だと 4GB 以上でも測れるのだろうか)
  • top -l 1 (オーバーヘッドが大きそう)
  • valgrind --tool=massif command && ms_print massif.out.$PID (実行速度が極端に遅くなる,粒度が細か過ぎる; vector 等のコンテナのリサイズで瞬間的に 1.5 倍のメモリ消費になるのまで記録してしまう)
  • getrusage (Linux のみ?)(間違い.ru_maxrss を読むべきなのだがセットされない)
// #include <sys/resource.h>
static void _printMemUsage (const char* label) {
  struct rusage rs;
  long long int sz = getpagesize ();
  getrusage (RUSAGE_SELF, &rs);
  std::fprintf (stderr, "memory usage (%s): %d bytes\n", label, rs.ru_minflt * sz); // これはpage faultの総計なので誤り(1Gで10回new/deleteすると10Gになってしまう).
}
  • /proc/$PID/status (vmRSS; Linux のみ)

どれも帯に短し襷に長しで,オーバヘッドの無視出来る(時間計測と同時にやりたい)測り方が分からない.Activity Monitor とにらめっこするのもアホなので、以下のようなゴミスクリプトで時間計測とは別に実行している.

#!/bin/env ruby

command  = ARGV.shift
pid = spawn(command)
mem = Array.new
while 1 do
  sleep 1
  # ps = open("| ps -p #{pid} -o rss") # < 4GB
  ps = open ("| top -cn -RFX -l 1 -o rsize -P 'PID RSIZE' -p '$aaaaa $jjjjjjjjjjjj'")
  str = ps.read
  ps.close
  if str =~ /^\s*#{pid}\s+(\d+)/o
    size = $1.to_i
    mem.push size
  else
    break
  end
end

b, = mem.sort.reverse
kb = b / 1024.0
mb = kb / 1024.0
# top = top.to_s
# 1 while top.sub! (/^([-+]?\d+)(\d\d\d)/) { "#{$1},#{$2}" } # splitting
printf("MAX RSS: %d b = %.1f Kb = %.1f Mb\n", b, kb, mb);

で,

> ruby1.9 memory_monitor.rb command 

他に良い手段は無いものか.
[追記] GNU time (/usr/bin/time) だと,最大使用メモリサイズ (Maximum Resident Set Size) が出せるようだ.Mac OS X には,オプションが違う BSD time が入っていたので man time してみると rusage を出してるだけ,とある.rusage は Mac OS X プログラム内から読んでも ru_maxrss (maximum resident set size) も ru_minflt (page reclaims) も 0 なのだが BSD time (-lp) だと正の値が帰ってくる・・・何故だ・・・ただ,やはり 4GB (signed long なので 2GB) を超えると適切に表示できなさげ.こういう時は,ソースに当たれということで,GNU timeの方のソースを見ると,プロセスをフォークして rusage を見ているだけ.子プロセスなら値が入るのか.オーバーフローするのは rusage の ru_maxrss の型が long のせいだろうか(long が 8 bytes なら良かろうと -m64 をつけてコンパイルしてみたけどダメだった).
[追記] 対応する xnu のソースコードを眺めてみると,64bit 版の rusage は入っているようで,自プロセスのメモリを計測する場合には task_info を直接呼べば 2GB 以上の最大消費メモリも計測できるようだ.しかし,実際には time コマンドがしているように,fork して子プロセスでコマンド実行して wait4 で得られる rusage で 64 bit の値が保存されていないと,任意のコマンドのメモリを計測するのには使えない (task_for_pid で調べたいプロセスの pid を入力して返り値を task_info に渡せば ok なはずだけど,root じゃないと task_for_pid の実行に失敗するため使えず).getrusage や wait4 などではプロセスが終了時に書き出す maxrss を読みに行っているのだけど,代入するどこかの値で 32bit の rusage を経由しているのだろうと思われる.ここかな.もしそうだとしても,コンパイルされたカーネルを使っている限り手も足も出ないわけだけど.
結局まとめると,10.5 (Leopard) だと,rusage 構造体の変数 (ru) を用意して,getrusage (RUSAGE_CHILDREN, ..) か,wait* を使って,32bit (signed なので 2GB)の範囲で外部コマンドの消費メモリを (ru.ru_maxrss 経由で) 計測できる.10.6 (Snow Leopard) だとどちらも無効.自プロセスの消費メモリは 10.5/10.6 とも task_info を直接呼べば >2GB でも計測可能.
なお,上で ps だとダメだけど top だと64bitで使用メモリを見れると書いたが,実際,top のソースコードではTASK_BASIC_INFO_64_COUNTをセットして64bitでメモリサイズを取得している.root 権限で実行しないと動かないのには変わりないが (/usr/bin の top は root が owner で chmod u+s してある).