こんにちは!ピクスタ開発部です。
RubyKaigi 2019の3日間、とても早かったですね!最終日の3日目も、開発部のメンバーが選りすぐりのセッションを紹介します。
Cleaning up a huge ruby application
大規模アプリケーションで、実行されていないコードをどのように特定して削除したかを紹介するセッションでした。cookpad_allのRubyコードは50万行以上ありますが、それらをサービスを停止せずに1年間で5万行削除できたそうです。
不要なコードを削除するメリットとして以下の点を挙げていました。
- コードが理解しやすくなる
- ライブラリの依存性が減る
- アプリケーションの実行速度が上がる
- テストが減る
ただし実際にはコードを消すことは難しいです。本当に削除していいか判断するのに時間がかかりますが、優先度の低い作業であり、常にコードは増えていきます。クックパッドでは、継続的にコードを消すための機構としてKitchenCleanerを開発しました。これは「未使用コードを検出→イシューを立てる→担当者を自動的にアサイン」するシステムです。具体的には、KitchenCleanerは以下のコードを検出します。
- 1年以上PVがないコントローラ
- 3ヶ月以上実行されていないバッチ
- 使われていないChankoのユニット
検出した後は、イシューが立てられ、gitのログからauthorsを特定してランダムに担当者に割り当てます。次のアクションとして、担当者はコードを削除または期間を決めて残すという判断をします。
他にも実行されていないコードを検出する取り組みをしており、Ruby 2.3から試験的に導入されたInstructionSequence(iseq)のlazy loadingを利用しています。これはiseqを初めて実行する時まで中身の読み込みを遅延するというものです。iseqのlazy loadにフックすることで、実行記録を得られるようにしました。実行記録はFluentdでRedshiftに保存し、解析してHTMLのレポートを作成しています。
ただし、iseqの実行記録ではif等の分岐があるときに削除していいコードかどうか判別できず、細かいコードの削除には向いていません。そのため、Ruby 2.6の新機能であるOneshot Coverageを利用しています。これは各行が 1 回でも実行されたかどうかを計測するカバレッジで、細かいコードの検出が可能です。取得したログは転送量を抑えるためにDynamoDBを経由してRedshiftに送ります。なお、DBは1日ごとに全削除しているとのことです。
上記の取り組みついては以下の記事が詳しいです。
本セッションは立ち見が出るほど大人気で、多くの方が不要なコードを削除することへの強い関心を持っていると感じました。今までサービスを支えてきたコードをリスペクトしつつ、使用しなくなったコードは継続的に削除する努力をしていきたいです。
The challenges behind Ruby type checking
Ruby 3の型検査のひとつであるSteepを開発している松本宗太郎さんによる、Rubyにおける型検査に関するセッションでした。
セッションでは、まず最初に現在のSteepの状況についての簡単な共有がされました。
昨年のRubyKaigiでの発表からの主な差分としては、バグ修正や機能改善を含む10個のリリースが行われたことと、Language Server Protocolを用いたエディタとの連携機能の実装に着手し始めたことの2点です。
この後、タイトルにもあるRubyにおける型検査の難しさに関する話が展開されました。
難しさの原因は大きくふたつあって、ひとつは、Rubyの特徴としても語られることの多いメタプログラミングによるものです。
例えば、Ruby on RailsのActiveRecord
におけるhas_many
などのDSLは、プログラムの実行時にメソッドを動的に定義しますが、これをどうやって正しく検査するか?を考えてみるとその難しさがわかると思います。
もうひとつは、Rubyが動的型付け言語であることに由来するものです。
わかりやすい例としては、メソッドのある引数や配列の各要素として全く関係のない異なる型のオブジェクトを指定できてしまう、というものでしょう。
これに対処するには、Union TypesやTuple Typesのようなより柔軟な型システムを導入する必要があります。
Rubyにおける型検査の難しさがわかったところで、セッションはRuby 3で導入予定の型注釈の記法の紹介へ移ります。
記法に関しては既におおよそ決まっていて、松本さんのGitHubに該当のリポジトリがあります。
そして、この記法を用いて標準ライブラリを型付けしたものが、リポジトリ内のstdlib/builtin
以下のディレクトリにある.rbi
ファイルです。
例えば、Array
クラスの型定義を一部抜粋すると次のようになります:
class Array[A] def map!: { (A) -> A } -> self | -> Enumerator[A, self] end
[A]
が型パラメータ、{ (A) -> A }
がブロック引数の型、-> ...
がメソッドの戻り値の型、self
が自分自身(この場合、Array
クラスのインスタンス)の型、|
がUnion Typesのための記号にそれぞれ対応します。
これなら普通に読めそうですし、Type Profilerによって自動生成された型定義ファイルを手で修正するのも簡単そうですね。*1
筆者(id:Yasaichi)は、初日のRuby 3 Progress Reportの中でちらっと出てきた型定義ファイルの書き方が気になっていたところだったので、本セッションでその詳細を知ることができて良かったです。
Performance Optimization Techniques of MessagePack-Ruby
www.slideshare.net
MessagePack-Ruby においてどのような最適化を行ったかを紹介するセッションでした。
MessagePack とは、バイナリ形式でデータを保存するフォーマットです。基本的には JSON を使うケースでは MessagePack を代わりに使うことが可能です。JSON に比べて高速でサイズの小さいフォーマットとなっています。JSON は human-readable ですが、MessagePack は machine-readable と言えます(バイナリなので当然ですね)。
最適化のテクニックとして、以下のようなものが紹介されていました。
- Zero-copy read optimization
- Reserved memory pool
Zero-copy read optimization は、長い文字列のコピー時に copy-on-write を使うようにするというものでした。実際にコピーされるのは書き換えられた時のみとするので高速になります。
Reserved memory pool は、自前で global にメモリを確保しておくことで、いちいちメモリを確保する必要がなくなり高速になるということのようです。Ruby での実装では、gem をインストールする時点で 4KB 区切りで一定数のメモリチャンクを確保するようになっているとのことでした。
その後、ベンチマークが紹介されました。複数のデータ形式ごとにシリアライズ/デシリアライズの速度を計測しており、上記最適化が効いていることがわかりました。
このセッションを聞いて、最適化という仕事にとても興味を持ちました。ファクトから何が原因か予想し、その原因を取り除くアイデアを考えて実装し、また計測する。これを繰り返すことで少しずつスループットが改善されていく。本セッションを聞くことで、そのチャレンジングな問題解決の過程をとてもワクワクしながら追体験することができました。
Timezone API
Ruby 2.6からTime
クラスでタイムゾーンがサポートされていますが、その経緯やどのように実現したか等のお話がありました。
RubyのTime
クラスは、C言語の標準ライブラリであるtime_t
をラップすることで実現されています。また、Ruby 1.9.2からはFixed offset modeに対応しています。Fixed offset modeとはTime
クラスに対して時間に加えてoffsetを指定できます。
(例:Time.new(2019, 4, 18, 10, 0, 0, "+09:00")
という形で"+09:00"
を指定できる)
しかし、特定地域の時間をTime
クラスを使って表現する場合は、タイムゾーンにサポートした他のgem(tzinfoやtimezone等)で一度offsetを求めてからTime
クラスへ指定する必要があり、タイムゾーンのサポートができているとは言い難い状態でした。
そこで、Time
クラスでもタイムゾーンのサポートをという声が上がり、Ruby 2.6でタイムゾーンのサポートをすることになりました。
具体的にだと、Ruby 2.6以降のTime
クラスには、Fixed offset modeもしくはlocal_to_utc
やutc_to_local
等のメソッドが利用可能なtimezoneオブジェクトを指定できるようになっています。特定のメソッドを持ったtimezoneオブジェクトにすることで、定期的に更新されるタイムゾーンの情報と依存を無くすことができます。
上記のtimezoneオブジェクトは、tzinfoやtimezoneのgem等で実装されているとのことでした。
Ruby 2.5系(2.5.5)の場合
Ruby 2.5時点ではTime
クラスでタイムゾーンをサポートしていないので、timezoneオブジェクトを指定するとエラーになってしまいます。
irb(main):001:0> RUBY_VERSION => "2.5.5" irb(main):002:0> require "tzinfo" => true irb(main):003:0> timezone = TZInfo::Timezone.get("Asia/Tokyo") => #<TZInfo::DataTimezone: Asia/Tokyo> irb(main):004:0> time = Time.new(2019, 4, 18, 10, 0, 0, timezone) Traceback (most recent call last): . . TypeError (can't convert TZInfo::DataTimezone into an exact number)
Ruby 2.6系(2.6.2)の場合
Ruby 2.6ではTime
クラスでタイムゾーンをサポートしているので、timezoneオブジェクトを指定することが可能になっています。
irb(main):001:0> RUBY_VERSION => "2.6.2" irb(main):002:0> require "tzinfo" => true irb(main):003:0> timezone = TZInfo::Timezone.get("Asia/Tokyo") => #<TZInfo::DataTimezone: Asia/Tokyo> irb(main):004:0> time = Time.new(2019, 4, 18, 10, 0, 0, timezone) => 2019-04-18 10:00:00 +0900 irb(main):005:0> time.to_s => "2019-04-18 10:00:00 +0900"
ちなみに、Time
クラスのfind_timezone
というフックメソッドを実装することで、Time.new
等に特定地域のタイムゾーンの名前(例:"Asia/Tokyo")を指定することも可能になるとのことでした。
Ruby 1.9.3以降はFixed offset modeに対応している等、Time
クラスに関する歴史を本セッションから知ることができ、また一つ、Rubyに詳しくなったなと感じました。
Red Chainer and Cumo: Practical Deep Learning in Ruby
Rubyのための深層学習のツールであるRedChainerと数値計算ライブラリであるCumoについての発表でした。
Rubyにおける深層学習のライブラリ群はPythonと比較しても、揃いつつあります。Pythonで深層学習を行うライブラリの一つとしてChainerがあるのですが、RedChainerはRubyでChainerを扱うためのライブラリです。
発表中に、「Rubyの場合、プログラミングを楽しみながら深層学習が行えるのではないか?と思ったのが、RedChainerに取り組んだきっかけ」と話されていました。サンプルコードを見ると、Chainer::Chain
を継承することで"便利メソッド"を使用しながら深層学習を行えそうでした。
RedChainerの課題は実行速度と、他のFWとの連携にあるようです。
例えば、Chainerの学習モデルと結果をRedChainerでそのまま使用することはできず、RedChainerで使用するために、一旦DBなどに保存してRubyで扱えるような形式にする必要があります。
この問題を解決するのがONNXであると紹介がありました。ONNXは言語、プラットフォームに依存せず、ONNX-Chainerを使用し、ChainerからONNXファイルを作成し、onnx-red-chainerでONNXファイルを読み込むことで、学習済みのモデルやパラメーターをRubyで扱うことが可能になるようです。
今後の展望は、RedChainerで学習済みのパラメーターやモデルを生成することのようです。
おわりに
ピクスタの開発部では3日間を通して、RubyKaigiに参加することができなかった方でもイベントの内容が楽しめるように、実況ツイートをしてきました。 微力ではありますが、スポンサーになったことや、実況ツイートで、Rubyの取り組みに寄与できたのではないかと思います。
* * * * *
ピクスタではエンジニアを募集しています!