きりかノート 3冊め

おあそびプログラミング

RubyCocoaアプリでTableViewのスクロール時に落ちることがある問題の調査

もうずいぶん前から調べてはいたんだけど、一定の結論に達したので記録。

調査結果

  • 現象としては、GCされたRubyVALUEを参照していることが原因。
    • 落ちる場所は一定で、ovmix_ffi_closure()のrb_obj_kind_of()。
  • RubyCocoaObjective-CのidとRubyVALUEの対応を保持しているキャッシュが、RubyオブジェクトがGCされても残っているケースがある。
    • GCされた場合、そのオブジェクトのファイナライザでキャッシュから除外しているが遅延したときに問題が発生する可能性がある。
    • キャッシュを無効にすると問題は発生しなくなる。

落ちる場所はOverrideMixin.mのこのあたり。

      147     if (!NIL_P(arg)
      148         && rb_obj_is_kind_of(arg, objid_s_class()) == Qtrue  // <= crash!!
      149         && !OBJCID_DATA_PTR(arg)->retained) {
      150             OVMIX_LOG("retaining %p", OBJCID_ID(arg));
      151       [OBJCID_ID(arg) retain];
      152       OBJCID_DATA_PTR(arg)->retained = YES;
      153       OBJCID_DATA_PTR(arg)->can_be_released = YES;
      154     }

RubyCocoaではObjective-CのオブジェクトをRubyに持ってきたときに、OSX::ObjCClassNameなクラスのRubyオブジェクトとして扱うんだけど、それと合わせてObjective-Cのidと生成したRubyオブジェクトのVALUEをペアにしてst_tableにキャッシュとして保存するようになっている(ocda_conv.m)。これは主にパフォーマンスのためだと思うんだけど、これによって同じObjective-Cのオブジェクト(id)には同じRubyのオブジェクト(VALUE)を持たせることができている。

RubyCocoaObjective-CRubyのオブジェクト変換の流れ。

  1. idに対応するVALUEがキャッシュに保存されているかを確認。あればそのVALUEを使う。
  2. なければ新しいVALUEを生成する。
  3. id -> VALUE の対応をst_tableにキャッシュとして保存する。

で、このst_tableに入れている値なんだけど、VALUEGCされたときにはファイナライザでst_tableから削除されるようになっている。オブジェクト本体のGCとファイナライザの実行にタイムラグがあると、キャッシュからGC済みのVALUEをとってきてしまい、もちろんプロセスは落ちる。

CRuby(1.8.7)のgc.cを読んだ感じだと、ファイナライザの実行は遅延されることがあるのでこれはありうる状況だと思う。遅延される条件に該当するかどうかがちょっと自身なし。

NSTableViewのdelegateなんかはスクロール時などにセルごとにdelegateが呼び出されるので、NSTableViewやNSTableColumnは(表示されている)セルの数だけ1 RunLoopでRuby界に出現するのでこの問題にハマリやすいということなんだと思う。GC.stress下でテーブルをスクロールさせると再現しやすい。

ということでこの推測に基づいて、今後対応していくことになる。

今後の予定

で、対策としてどうするの?ってことなんだけど

  • キャッシュを無効にできるようにする。
  • 通常はGCを無効にして、GCDで排他かけた上でてきとうなタイミングでgc_start()を実行する。

あたりですかねえ。

前者は、

  • 実装は簡単。
  • パフォーマンスが落ちる可能性あり。どのくらいかはやってみないとわからない。試してる範囲ではわからない程度に感じる。
  • 同じObjective-Cで別のRubyオブジェクトてときにobj#==otherやobj.eql?(other)が成立するようにしたげたほうがいいかな。

後者は、

  • パフォーマンスは現状とあまり変わらないと思う。GC頻度をどう制御するかにもよる。
  • 「てきとう」てのが難しいよね。。

という感じです。時間かけてもしゃーないのでとりあえず前者ができた時点で次のバージョン1.1.0をリリースします。