mikutterの最新の情報は、mikutter blogに引っ越しました。

2011年3月2日水曜日

streamingプラグインの最適化

streamingプラグインはuserstreamを実現するためだけのプラグイン。(core/addon/streaming.rb)

UserStreamは大量のデータが落ちてくることがしばしばあり、フォローが多いと秒間に数個からヘタをすると数十個のイベントが落ちてくることもあるかも知れない。
(ここでつぶやきではなくイベントと言ってるのは、フォロー関係の変更やDM、favなども落ちてくるので)
mikutterでは、正確にはわからないが、まあフォローが1000を超えていたら絶対に動かないだろう。これは、流速が処理速度を超えるから。
そういう極端に速い環境を切り捨てるのは仕方ないにせよ、全員が定期的に呟いている訳でもないので毎週火曜夜に流速が極端に速くなるということもあるかもしれない。なので、できるだけ対策をしてみようと思った。

これの33行目くらいから変な魔術を使ってる。

# イベントを定義するためメソッド
    def self.define_event(event_name)
      type_strict event_name => tcor(Symbol, String)
      queue = Queue.new
      service = nil
      Thread.new{
        sleep(1) while not service
        loop{
          datum = queue.pop
          yield(service, datum)
          sleep(1) } }
      define_method("event_#{event_name}"){ |json|
        type_strict json => tcor(Array, Hash)
        service ||= @service
        queue << json } end

    # favられイベントの定義
    define_event(:favorite) do |service, json|
      by = service.__send__(:parse_json, json['source'], :user_show)
      to = service.__send__(:parse_json, json['target_object'], :status_show)
      if(by.respond_to?(:first) and to.respond_to?(:first) and to.first.respond_to?(:add_favorited_by))
        to.first.add_favorited_by(by.first, Time.parse(json['created_at'])) end end

event_favoriteメソッドがfavられた時に呼ばれる。こいつが実際のイベントをよんでいるんだけど、いちいちdefine_eventを使って定義している。
このdefine_eventで定義されるevent_favoriteは、Queueに引数をそのまま積んでおしまい。実際には、定義されたと同時に作られた別のスレッドがイベントの処理をする。
これによって、イベント発行にだらだらと時間を使ったりせず、効率的にイベントを発行できるというわけ。
・・・ではなくて、見ていただければわかるとおり、1つキューを消化するごとにsleep(1)を実行しています。
なので、たとえば同時に100favされたとすると、普通の実装だと死ぬほどイベントを発行して、効果音がけたたましく鳴り響いてTLの更新がしばらくおっつかなくなるんですが、この実装によって、最初の1favは即時、残りは99秒かけて1秒間に1つづつイベントが実行される。これによってイベントの実行が遅れる場合が出てくるが、乱発されない限り目立った遅延は起こらない。1つづつしかイベントが発生しないので、処理もすぐに終わるからたいした負荷もかからない。もちろん、フォローなどについても同様。

唯一の例外はつぶやき受信関連。これだけはちょっと違って(詳しくはソースみてくださいそんなにありがたいものじゃないが)、つぶやき関連のイベントは、1回のイベントにいくつでもつぶやきを持たせることができる。なので、1回のループで、キューに溜っているイベントを全て取り出して、一気にイベントを発生させる。
これによって、1秒間に大量のつぶやきを処理する可能性があるけれど、実はイベントの発行って結構非効率なので、10イベンtの発行するよりも1イベントで10ツイートを処理するほうがはるかに経済的。ていうかそんなのが頻繁に起こるならどのみちmikutterつかえない

重要なのは、同じイベントは1秒に1回しか発生しないようになった、ということ。どんな速度でイベントが発生しても、ここでイベントの流量が制限されるようになるわけ。これによって、例えば1分間に100favとかされたときにフリーズすることがなくなりました。

今後の展望(というか実験中のこと)としては、GUIイベントを処理している時など、今やったらユーザの操作に影響が出るぞっていう時にはイベントの発行をさらに1秒遅らせるというの。ウィンドウが完全に応答なしになってるのに次々イベントほりこんでもしゃーないでしょ、という話。TL速い時に起動が数秒遅くなってるんですが、これは起動直後にウィンドウに夥しい量のつぶやきが放り込まれて固まってる間にたくさんイベントが発生しているからに他ならないんですよ。

一方、本当に重くしてる原因は他にあると思うので、そっちを潰すのが先かなぁとも。やはり動きが良いのは良いことです。