株式会社クリアコード > ククログ

ククログ


OSS Gateワークショップin札幌2016-09-24を開催 #oss_gate

OSS Gateは「OSS開発に参加する人を増やす」取り組みで、それを実現するための1つの手段として「OSS Gateワークショップ」を開催しています。これまでは東京でだけ開催していましたが、OSS Gateは「『東京の』OSS開発に参加する人を増やす」取り組みではないので、東京以外でも開催したいという考えがありました。

それが実現したのがOSS Gateワークショップin札幌2016-09-24です。札幌開催は札幌在住の@tricknotesを中心に準備しました。東京以外でもだれか中心になる人がいると開催できることがわかったので、東京以外の自分たちの地域でもOSS Gateワークショップを開催したい!という人はOSS Gateのチャットルームに来てください。相談しましょう!

今回はインフィニットループさんに会場を提供してもらって開催しました。1回目の開催で8名のビギナー(OSS開発未経験者)が参加しました。これは東京での1回目の開催時(4名)より多いです。札幌でも「OSS開発に参加する人を増やす」取り組みは必要とされていそうです。

参加した人がブログを書いています。

参加者のアンケート結果もあるので、内容に興味がある人は参考にしてください。

なお、同日に東京でもOSS Gateワークショップを開催していました。東京の内容に興味のある人は以下を参照してください。

この記事では内容ではなく運営方面視点のことをまとめます。(東京・札幌以外の)自分たちの地域でもこのワークショップを開催したいという人の参考になるはずです。

今回は1回目なので東京から以下の3人が応援に行きました。

  • @ktou:ワークショップの進行役として。札幌の人たちは@tricknotesも含めて東京でやっているワークショップを経験したことがないので、東京でのやり方を経験してもらうために応援に行った。(今後、東京でのやり方と同じにやってもよいし、札幌独自のやり方になっていってもよい。)
  • @mtsmfm:メンター(OSS開発経験者)として。東京でのワークショップでメンター経験あり。
  • @kakutani:メンターとして。東京でのワークショップを経験したことはないが、一般的なワークショップに慣れているので初参加でも頼もしい。

応援に行った目的は次の通りです。札幌以外でも新しい地域で開催するときは応援に行く予定です。

  • 札幌の人たちに、たたき台として東京でのやり方を経験してもらう
  • 札幌の人たち(主にメンターの人たち)が経験することに集中できるように、負荷を下げる
  • 札幌の人たち(主にメンターの人たち)が経験することに集中できるように、イレギュラーな対応を巻き取る

「負荷を下げる」と「イレギュラーな対応を巻き取る」を少し補足します。

東京のワークショップではメンター1人でビギナー2人をサポートするという体制をとっています。札幌ではメンター1人でビギナー1人をサポートする体制としました。まずは、ビギナーをサポートするとはどういうことかを経験することに集中できるようにするためです。これを実現するために東京から応援に行ったメンターでビギナー3人をサポートしました。これが「負荷を下げる」ということです。

OSS Gateワークショップではビギナーとして想定している範囲はかなり広めですが、それでもたまに想定の範囲外のビギナーも参加します。その場合は個別の対応が必要になります。今回の場合は、普段開発はしていないしGitの使い方も不安というビギナーが参加していましたが、このケースは想定の範囲外です。(想定の範囲外ですが、ワークショップに来る気持ちがある人ならいい感じにサポートしたいので、そんな人でも遠慮せずに飛び込んできてください。)札幌の人たちがこのケースのサポートをすると想定の範囲内のサポートを経験する機会がなくなるので、東京から応援に行ったメンターで対応しました。他にもメンターくらいの経験の人がビギナーとして参加していましたが、これも想定の範囲外のケースでした。やはり、こちらも東京から行ったメンターで対応しました。これが「イレギュラーな対応を巻き取る」ということです。

東京以外の自分たちの地域でもOSS Gateワークショップを開催したい!という人はOSS Gateのチャットルームに来てください。相談しましょう!前述の通り最初のワークショップをサポートしに行きますし、OSS Gate ワークショップ in 札幌 キックオフ - 2016-07-16というようにワークショップ開催前のキックオフを開催して仲間集めをするところのサポートもできます。どういう風に進めていけばよいか一緒に考えましょう。

2016-10-07

Groongaでのmrubyの組み込み方:ビルド周り

全文検索エンジンGroongamrubyを組み込んでいます。理由は、速度はそれほど必要ではなく込み入った処理をCではなくRubyで書けると開発が捗るからです。

この記事ではどのようにmrubyを組み込んでいるかについてビルド周りだけを説明します。(ビルド周り以外には、バインディングをどうやって書くか、.rbファイルをどこに置くか、実装をCにするかRubyにするかの判断基準などの話があります。)

方針

mrubyはRakeを使ったビルドシステムを使っています。GroongaはGNU AutotoolsまたはCMakeを使ったビルドシステムを使っています。(どちらでもビルドできます。)

mrubyはRakeを使ってビルド、GroongaはGNU Autotoolsを使ってビルド、とするとコンパイルオプションの統一・クロスコンパイルあたりで面倒になります。また、Rakeを使ってビルドするためにはビルド時にRubyが必要になるのも面倒です。Groongaの開発者がGroongaをビルドするためにRubyが必要になるのはよいですが、GroongaのユーザーがGroongaをビルドするためにRubyが必要になるのはビルドの敷居が上がるのでできれば避けたいです。

以上の理由からGroongaではmrubyをビルドするためにRakeを使っていません。

ではどうしているかというと、次のようにしています。

  1. Groonga開発者はRakeを使って(= Rubyが必要)mrubyのビルドに必要なファイルを生成(一部自動生成のファイルがあるため)
  2. Groongaはmrubyのビルドに必要なファイルをGNU Autotools(またはCMake)を使ったビルドシステムに統合(= mrubyのビルドにRakeを使っていない = Rubyは必要ない)
  3. Groonga開発者はリリース版のソースアーカイブに1.で生成したファイルをすべて含める(= Groongaユーザーは1.を実行する必要がない = Rubyは必要ない)

それぞれどのようにしているか説明します。

Groongaの開発者はRakeを使ってmrubyのビルドに必要なファイルを生成

Groongaはvendor/mruby-sourceをGitのsubmoduleにしています。つまり、mrubyのリポジトリーのソースをまるっと参照しています。

この状態ではmrubyのビルドに必要なファイルは足りません。具体的にはRubyで実装されたコードをmrubyに組み込むファイル(mrblib.cmrbc -Bで生成するファイル。)や利用するmrbgemsを組み込むファイル(mrbgems_init.c)が足りません。

これらを生成するためにRakeを使ってビルドします。出力先はvendor/mruby-build/にしています。ビルドした後vendor/mruby-build/に出力されたファイルの中から必要なファイルをvendor/mruby/にコピーします。vendor/mruby-build/を直接ビルドに使って「いません」。ビルドに使っているのはvendor/mruby/にコピーしたファイルです。このあたりの実装がvendor/mruby/mruby_build.rbです。

Groongaはmrubyのビルドに必要なファイルをGNU Autotools(またはCMake)を使ったビルドシステムに統合

必要なファイルが揃ったらGNU Autotools(またはCMake)のビルドシステムに統合することは難しくありません。

GNU Autotoolsの場合はvendor/mruby/Makefile.amで実現しています。

CMakeの場合はvendor/mruby/CMakeLists.txtで実現しています。

工夫しているところはソースファイルのリストを共有しているところくらいです。mruby本体やmrbgemsが更新されるとソースファイルのリストは変わることがあるのでvendor/mruby/update.rbで自動生成しています。Makefile.amCMakeLists.txtでは自動生成されたリストを読み込んでビルドシステムに統合しています。

Groonga開発者はリリース版のソースアーカイブに生成したファイルをすべて含める

mrubyをRakeでビルドして生成されたファイルをソースアーカイブに含めることでGroongaユーザーはビルドするときにRubyがなくてもビルドできます。

ソースアーカイブには生成されたファイルといつ生成されたかを示すタイムスタンプファイルを入れています。タイムスタンプファイルが新しければ再生成(mrubyをRakeでビルドし直すこと)せずにすでにあるファイルを使うようにしています。こうすることでソースアーカイブからビルドするGroongaユーザーはRubyがなくてもビルドできるようになっています。

実現方法のポイントはvendor/mruby/Makefile.amBUILT_SOURCESlibmruby_la_SOURCESに追加しているところとmruby_build.timestampがあったら再生成しないようにしているところです。

libmruby_la_SOURCES += $(BUILT_SOURCES)
mrblib.c: mruby_build.timestamp
mrbgems_init.c: mruby_build.timestamp
# ...

mruby_build.timestamp: build_config.rb version
#	...

まとめ

Groongaがどうやってmrubyを組み込んでいるかについてビルド周りだけを説明しました。

Groongaのmruby組み込み周りを触る人や自分のアプリケーションにmrubyを組み込みたい人は参考にしてください。

タグ: Groonga
2016-10-13

v0.14 Outputプラグインの仕様解説

はじめに

クリアコードはFluentdというソフトウェアの開発に参加しています。Fluentdはログ収集や収集したデータの分配・集約などを行うソフトウェアです。

v0.14での新機能を使ったプラグインを作成する際にはこれまでの Fluent 以下のクラスではなく、Fluent::Plugin 以下のクラスを継承し、実装する必要が出てきました。 また、v0.14のOutputプラグインはv0.12とは異なり、Fluent::Plugin::Output クラスに様々な機能が入っています。これらの機能をプラグイン開発者向けに解説することを目指します。

この記事はv0.14.8以降が対象です。 まずは、Outputプラグインが必ず実装するべきメソッドについてのおさらいです。

non-buffered

def emit(tag, es, chain)
  # ...
  chain.next
end

def process(tag, es)
  # ...
end

と読み替えます。 output#process(tag, es) だけを実装するとnon-bufferedプラグインになります。

例えば、out_relabel の使用例があります。

buffered synchronous

output#write(chunk) を実装するとbuffered outputプラグインになります。

def write(chunk)
  # ...
end

例えば、out_stdout の使用例があります。

buffered asynchronous

output#try_write(chunk) を実装するとbuffered asynchronous outputプラグインになります。

def try_write(chunk)
  # ...
end

out_stdout の使用例があります。ただし、これはテスト用の実装のため、実用のものとは異なることに注意してください。

また、#commit_write(chunk_id) を呼び、chunkのwriteを確定させることが必要です。 rollback_writecommit_write が行われないまま指定秒数が経過した chunk に対して自動的に呼ばれるので、プラグイン開発者が明示的に呼ぶ必要は通常はありません(秒数は delayed_commit_timeout で設定から制御可能)。

ここまでがv0.14のOutputプラグインの基本的な事柄です。

では、さらにv0.14のプラグイン開発者にとって必要なことを順々に見ていきましょう。

custom format

#format(tag, time, record) を実装すると、bufferのchunkでmsgpack以外のformatが使用できるようになります。

#format を使用すると、

def formatted_to_msgpack_binary
  true
end

としてtrueを返すようにしなければ chunk#msgpack_each メソッドは使用できません。

chunk#msgpack_each

v0.12のObjectBufferedOutput互換になるのは #format を実装していない場合です。 #format の有無や、 #formatted_to_msgpack_binary の返り値によって挙動が異なってくるのに注意してください。

standard format

chunk#msgpack_each でyieldされてくる値は #format を実装している時とそうでない時で異なります。

def write(chunk)
  chunk.msgpack_each do |time, record|
    # ...
  end
end

ただし、#msgpack_each は互換性のために残されているものです。 通常は chunk.each を使ってください。msgpack_each も(主に互換性の関係から) alias が定義されていますが、本来 chunk の内部フォーマット(msgpack)を意識させたメソッドを使うのは好ましくありません。

tagが必要な場合は、

config_section :buffer do
  config_set_default :@type, DEFAULT_BUFFER_TYPE
  config_set_default :chunk_keys, ['tag']
end

のようなbufferのdefault confを足し、chunk.metadata.tag で取得してください。

また、tag が必要な場合 config_set_default :chunk_keys, ['tag'] を指定しておくのはよいですが、これは設定で上書きされる可能性があるため #configure でチェックを行うべきです。

def configure(conf)
  super

  raise Fluent::ConfigError, "chunk keys must include 'tag' for this plugin" unless @chunk_key_tag
  # ...
end
custom format

#format(tag, time, record) を実装した場合は、to_msgpackでmsgpackへパックした順にmsgpack_eachをすると得られます。 また、#formatted_to_msgpack_binary をオーバーライドしてtrueを返すようにしてください。

def format(tag, time, record)
  [tag, time, record].to_msgpack
end

def formatted_to_msgpack_binary
  true
end

def write(chunk)
  chunk.msgpack_each do |tag, time, record|
    # ...
  end
end

injectヘルパーを使う場合は #format(tag, time, record) を通すことでより見通しが良くなります。そのため、 #format を実装し、その中で inject_values_to_record(tag, time, record) を呼ぶようにしてください。

発展形

v0.14のOutputプラグインはオーバーライドするメソッドや実装するメソッドにより、confの設定により実行時に3種の異なる種別のOutputプラグインへ切り替えることができます。

non-bufferedとbufferedの切り替え

これは以下の優先順位で行われます:

  1. 実装メソッドによる分岐 (例: #process しか実装されていない → non-buffered)
  2. 両方実装されている場合で、かつ設定において <buffer> セクションが指定されている場合 → buffered
  3. 両方実装されており設定にセクションが指定されていない場合 → #prefer_buffered_processing を呼んで判定
buffered synchronous/asynchronousの切り替え

output#writeoutput#try_write を実装して #prefer_delayed_commit の返り値のtrue/falseでbuffered synchronousとbuffered asynchronousを切り替えられます。

  • true -> buffered asynchronous
  • false -> buffered synchronous

output#writeoutput#try_write のどちらか一方だけ実装している場合は、#prefer_delayed_commit は呼ばれません。

bufferedプラグインの注意点

#write, #try_write を実装していないOutputプラグインへのconfigには <buffer> ディレクティブが使用できません。

複合形
#prefer_delayed_commit #prefer_buffered_processing 結果
  false             |           false              |     non-buffered
  false             |           true               | buffered synchronous
  true              |           true               | buffered asynchronous
  true              |           false              |      選択不可

secondaryの扱い

secondaryに指定されたプラグインはbufferingのサポートが必要です。out_fileなどのbufferingをサポートしたoutputプラグインを指定できます。

bufferディレクティブのCHUNK_KEYSアトリビュート

<buffer CHUNK_KEYS>のようにbufferディレクティブにはCHUNK_KEYSのアトリビュートの指定が可能です。 tag, timekey, variablesの指定ができるようになっています。これはこのアトリビュートによってチャンクをひとまとめにするためにあります。

  • tag →タグごとにチャンクがまとめられる
  • timekey →time formatごとにチャンクがまとめられる
  • variables →レコードの中のキーごとのチャンクがまとめられる

buffered outputプラグインのflushで用いられるthread

start時に <buffer> ディレクティブにある flush_thread_count で指定されている数のスレッドを作ります。#submit_flush_once は単にそれらのスレッドを明示的にアクティブにしているだけです。

v0.12のbuffered outputプラグインの自前スレッドの書き換え

プラグインが自前で作成していたスレッドは以下のようにできるはずです。

  • 定期的にある処理を行う必要があった場合 → timer plugin helper を使う
  • Fluent::Output プラグインを継承していたが(ある設定が有効なときのみ)バックグラウンドでflushするような処理を自前で書いていた → #process および #write 両方を実装して設定により挙動を切り替える
  • socketをlistenしていた → socket/server plugin helper を使う(これから実装される)

それ以外の場合は thread plugin helper を使います。自前で Thread.new するべきではありません。thread plugin helperを使う場合、plugin test driverがそのスレッドの状態管理などの面倒を見てくれるため、たまに失敗するテスト、などの危険性が大幅に低下します。

プレースホルダ

chunk.metadata が実際にどの値を有しているかは <buffer CHUNK_KEYS>CHUNK_KEYS に何をユーザが指定したか(あるいは config_set_default で何が指定されていたか)により異なります。 が、プラグイン作者が独自にチェックするべきではなく #configure 内で #placeholder_validate!("name_of_parameter", @name_of_parameter) を使うべきです。使われているプレースホルダと chunk key の間に不整合があれば configuration error が上がります。 (もっと細かい制御もやろうと思えばできますが、コーナーケースです。こちらの議論を参照してください。)

つまりプラグイン作者は #configure 内で #placeholder_validate! し、そこが通っているならあとは #writeextract_placeholder(@name_of_parameter, chunk.metadata) するだけでよいです。

${tag}

chunkに含まれるタグに展開されます。 また、tag1.tag2.tag3.... のようなタグとなっている場合、 ${tag[0]}, ${tag[1]}, ${tag[2]},...のようにタグの添え字を指定することで個別に取り出すことができます。

strftime形式(%Y%m%dなど)

strftimeのフォーマットに準じて展開されます。 variable_%Y-%m-%dT%H:%M:%S.%N のように用います。 これは variable_2015-12-25T12:34:56.123450000 のように展開されます。

まとめ

v0.14のOutputプラグインの仕様をFluentdの開発者の協力を仰ぎ*1書き出してみました。v0.12のoutputプラグインと変わっている箇所も多く、単純にv0.14への移行は難しい箇所もあります。 v0.14のAPIを使うように移行するとプラグインヘルパーやプレースホルダーの機能により、より柔軟なconfの設定を書くことが可能になります。例えば、プレースホルダーの機能を使ったものとしては、fluent-plugin-mysql のテーブル名へのプレースホルダーを指定可能にする機能*2 を実装したものがあります。このようにタグや日付ごとのデータ集計をサポートする機能を簡単に実装できるようになるというメリットがあるため、v0.14のAPIを使うように移行を試みてみるのはいかがでしょうか?

*1 この記事を書くに当たって @tagomoris さんのレビューの協力を仰ぎました。ありがとうございます。

*2 https://github.com/tagomoris/fluent-plugin-mysql#configuration-examplebulk-insert-with-tag-placeholder-for-table-name や https://github.com/tagomoris/fluent-plugin-mysql#configuration-examplebulk-insert-with-time-format-placeholder-for-table-name を参照。

タグ: Fluentd
2016-10-20

バグを踏んだら開発元(upstream)に報告するのをまわりに勧めてみよう

はじめに

クリアコードでは、普段の開発で実践していることを開発スタイルとしていくつかまとめています。 そのなかの一つが、「問題を見つけたらupstreamで直す」です。

今回は、自分達で実践するだけではなくて、「問題を見つけたらupstreamで直す」のをまわりに勧めてうまくいった事例(Kermitの不具合を報告)を紹介します。

Kermitとは

Kermitとはシリアル通信でファイル転送をするためのプロトコルであり、組み込み用途にEmbedded Kermitが用意されています。 今回の事例では、シリアル通信でファイルを転送するのにEmbedded KermitのJavaの実装を採用していました。

どんなバグを踏んだのか

Kermitを利用してファイル転送をしていると、ファイルの内容が破損するという問題に遭遇しました。 Embedded KermitのJavaの実装は10年以上修正されていないので、それなりに実装が枯れていると思われるのにも関わらず、です。

問題はKermitのJavaの実装におけるパケットのシーケンス番号の取扱いにありました。

Kermitのプロトコルは仕様がPDFで公開されています。

このPDFの4章に記載されているパケットフォーマットの仕様を見るとわかるのですが、4.1.のパケットのフィールドにおけるシーケンス番号の説明には次のようにあります。*1

The packet sequence number, modulo 64, ranging from 0 to 63.  Sequence numbers "wrap around" to 0 after each group of 64 packet

パケットのシーケンス番号のとりうる値は0から63であり、63の次は0に戻るmodulo 64で処理しなければならないと規定されています。

この問題に対して、次のようなパッチを対策として当てることになりました。

diff --git a/src/com/lucent/kermit/Kermit.java b/src/com/lucent/kermit/Kermit.java
index cef2545..ece21d2 100644
--- a/src/com/lucent/kermit/Kermit.java
+++ b/src/com/lucent/kermit/Kermit.java
@@ -1294,7 +1294,7 @@ public class Kermit {
   /** Returns the time we should wait for a timeout a*/
   public int getTimeout() { return sendTime; }
 
-  protected int getNextSeq( int num ) { return (num >=64)?0:num+1; }
+  protected int getNextSeq( int num ) { return (num >=63)?0:num+1; }
 
   public String toString() {
   return "kermit:"

修正前の処理だとgetNextSeq()の引数であるnumが63の場合に、次のシーケンス番号としてインクリメントした値(64)を返してしまいます。 ここで次のシーケンス番号は仕様書の規定どおりに0を返さなければいけないので、仕様に合致していません。 従ってシーケンス番号の判定条件が明らかに誤っています。

修正後の判定条件ではnumが63以上の場合には次のシーケンス番号として仕様通りに0を返すようになっています。

上記のJavaの実装では1パケット(シーケンス)で最大1024バイトまで送れるようになっていました。*2 そのため、この問題が発覚するのは、サイズが64KBを越えるファイルを転送した場合に限られていました。

10年ものの不具合を踏みぬいたということは、もしかすると64KBを越えるファイルサイズのシリアル通信という用途にJavaの実装は使われていなかったのかもしれません。

問題は無事解決、その後は?

これでシリアル通信で、転送したファイルが壊れてしまう問題が解決しました。 解決はしたのですが、開発元へのフィードバックはまだなされていないようでした。

そこで、開発元にこのパッチをフィードバックしてみませんか、と開発に参加していたプロジェクト内で働きかけてみました。 独自にパッチをメンテナンスし続けるにはそれなりのコストがかかります。むしろ開発元に反映してその成果物を利用するほうが、結果としてメンテナンスコストが下がるというメリットが得られるためです。

そのプロジェクトではフリーソフトウェアに対する理解がもともとあったので、働きかけが実を結び作成されたパッチは無事開発元に反映されました。

まとめ

今回は、バグを踏んだら開発元に報告するのをまわりに勧めてうまくいった事例を紹介しました。

遭遇した問題に対するパッチも作成して報告するというのは理想ではありますが、パッチを作成することそれ自体は必須ではありません。 バグ報告した内容が再現可能であれば、それだけでも開発元にとって助けになります。

フリーソフトウェアのバグを踏み抜いて、まだそのフィードバックをしていない事例を見つけたら、ぜひまわりにもバグ報告することを勧めてみてください。 フリーソフトウェアをよりよくすることで、その普及に繋げることができます。

*1 p15を参照のこと

*2 7.1 Long Packetsという仕様を実装しているコードにおける最大値。

2016-10-27

Ruby 2.4の新機能:rb_gc_adjust_memory_usage() - バインディングとGCの関係を改善するAPI

(おそらく)2016年のクリスマスにRuby 2.4.0がリリースされます。Ruby 2.4.0で導入されるrb_gc_adjust_memory_usage()というAPIについて紹介します。

バインディングとGCの関係

このAPIはバインディングとGCの関係を改善するためのAPIです。(バインディングについてはRubyKaigi 2016:How to create bindings 2016を参照してください。)

まず、バインディングとGCの関係にどのような問題があったかを説明します。RubyのGCはRubyのオブジェクトがどのくらいメモリーを使っているかを知っているのでメモリーを結構使っているなーと思ったらGCして不要なメモリーを解放したり再利用したりします。これにより、プロセスのメモリー使用量の肥大化を防いでいます。しかし、RubyのGCはバインディング対象のライブラリーがどのくらいメモリーを使っているかを知りません。そのため、バインディング対象のライブラリー内で結構メモリーを使ってもRubyのGCはなかなか走りません。その結果、プロセスのメモリー使用量が肥大化しがちです。マルチメディア系のライブラリー(画像や動画を扱うライブラリー)やインプロセスのデータベースライブラリー(たとえばRroonga)はこうなりがちです。

rb_gc_adjust_memory_usage()はこの問題を解決する手段を提供します。バインディングはrb_gc_adjust_memory_usage()を使うことでバインディング対象のライブラリーがどのくらいメモリーを使っているかをRubyのGCに伝えることができます。これでRubyのGCはメモリー使用量をより正しく知ることができます。その結果、適切なタイミングでGCを実行しやすくなります。

使い方

rb_gc_adjust_memory_usage()の使い方は簡単です。RubyのGCが知らないところでメモリーを確保したらそのサイズを正の値で呼び出し、そのメモリーを解放したらそのサイズを負の値にして呼び出します。なお、RubyのGCに伝えるサイズは確保したサイズと解放したサイズが同じになれば正確でなくても構いません。GCを実行するタイミングの参考に使われるだけなので概算で大丈夫です。

具体的な使用例を見てみましょう。rcairoという画像を扱うライブラリーのバインディングの使用例を見ます。rcairoはrb_gc_adjust_memory_usage()に対応しているバインディングです。

rcairoでは2次元画像を扱うクラスCairo::ImageSurfaceのインスタンスを生成したときに画像のバッファーサイズを確保したメモリーサイズとしてrb_gc_adjust_memory_usage()を呼び出しています。バッファーサイズは1行のバイト数(cairo_image_surface_get_stride(surface))×行数(cairo_image_surface_get_height(surface))で計算できます。ソースはext/cairo/rb_cairo_surface.cです。

cairo_surface_t *surface;
surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
                                      NUM2INT (width),
                                      NUM2INT (height));
{
  ssize_t added_memory_usage;
  added_memory_usage =
    cairo_image_surface_get_stride (surface) *
    cairo_image_surface_get_height (surface);
  rb_gc_adjust_memory_usage (added_memory_usage);
}

インスタンスを解放したときは画像のバッファーサイズを負の値にしてrb_gc_adjust_memory_usage()を呼び出します。

{
  ssize_t freed_memory_usage;
  freed_memory_usage =
    -(cairo_image_surface_get_stride (surface) *
      cairo_image_surface_get_height (surface));
  rb_gc_adjust_memory_usage (freed_memory_usage);
}
cairo_surface_destroy (surface)

これだけです。実際は画像のバッファーサイズ+αのメモリーを確保していますが、そこは画像のバッファーサイズに比べれば誤差ですし正確にカウントすることも面倒なので、RubyのGCには伝えていません。

効果

実際にどのくらい効果があるかrcairoのケースを見てみましょう。rcairoは画像を扱うライブラリーのバインディングなのでバインディング対象のライブラリーが大きめのメモリーを確保する傾向にあります。そのため、rb_gc_adjust_memory_usage()が有効なケースです。

次のような600x600のサイズの画像を1000個作るだけのスクリプトを考えます。

require "cairo"

width = 600
height = 600
1000.times do
  Cairo::ImageSurface.new(:argb32, width, height)
end

rb_gc_adjust_memory_usage()を使ってバインディング対象のライブラリーが使っているメモリー使用量をRubyのGCに伝えた場合となにもしない場合のメモリー使用量は次のようになりました。横軸が繰り返し回数で縦軸がメモリー使用量です。青い線がrb_gc_adjust_memory_usage()を使った場合で黄色い線が使っていない場合です。青い線の方は最大メモリー使用量が抑えられていますが、黄色い線の方は最大メモリー使用量が増え続けています。(繰り返し回数が少ないうちは黄色い線の方がメモリー使用量が少ないことは興味深い結果です。)

メモリー使用量

なお、このグラフは次のようなスクリプトでデータを取得しgnuplotで描画しました。gnuplotのスクリプトはgc-triggerで使っているものを少しいじって使いました。

require "cairo"

width = 600
height = 600
1000.times do |i|
  Cairo::ImageSurface.new(:argb32, width, height)
  case File.readlines("/proc/self/status").grep(/\AVmRSS:/)[0]
  when /\AVmRSS:\s+(\d+)\s+kB/
    vm_rss_mib = $1.to_i / 1024.0
  end
  puts [i, GC.count, vm_rss_mib].join("\t")
end

まとめ

Ruby 2.4の新機能であるrb_gc_adjust_memory_usage()を紹介しました。バインディングを作っている人は活用してください。

この機能は東京Ruby会議11で話したことがきっかけでRuby本体に提案した機能です。前からどうにかならないかなぁと問題意識は持っていましたが、よい案が浮かばないこともあり特になにもアクションを起こしていませんでした。話す機会があったことでちゃんと考える機会になり、結果としてRubyがよくなることにつながりました。みなさんも問題意識があることをまとめる機会を作ってみてはどうでしょうか。まとめることで解決案を思いついて改善につながるかもしれません。

タグ: Ruby
2016-10-28

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|