Android の世界には FCM (Firebase Cloud Messaing) という、Android 端末に push notification を送るためのプロトコルがある。そして、Ruby の世界には fcm gem という、そのプロトコルを簡単に叩くためのライブラリが存在する。
この fcm という、まるで Google 公式 gem であるかのような名前なのだが、全然公式ではないし、コードを読んでみるとこれがかなりやばい。具体的に何がやばいかというと、
- 必要もなく httparty gem と json gem に依存している。
- Persistent connections を使っていないので、とてつもなく遅い。
- 単にコードの質が低い。
このレベルだとpull request を送って改善するのも無理そうだし(json gem を依存関係から削除するというのは PR 送ったけど)、新しいものを作った方が世界が幸せになれそうだと思ったので、andpush という gem を作った。
The andpush gem
例によって gem 'andpush'
するか、gem i andpush
すればすぐに使える。
require 'andpush'
server_key = "..." # Your server key
device_token = "..." # The device token of the device you'd like to push a message to
client = Andpush.new(server_key)
payload = {
to: device_token,
notification: {
title: "Update",
body: "Your weekly summary is ready"
},
data: {
extra: "data"
}
}
response = client.push(payload)
json = response.json
json[:canonical_ids] # => 0
json[:failure] # => 0
json[:multicast_id] # => 8478364278516813477
と、使い方は fcm gem と大差ないが、ベンチマークを取ってみるとその差は歴然:
require 'benchmark/ips'
require 'andpush'
require 'fcm'
server_key = ENV.fetch('FCM_TEST_SERVER_KEY')
DEVICE_TOKEN = ENV.fetch('FCM_TEST_REGISTRATION_TOKEN')
FCM_CLIENT = FCM.new(server_key)
PAYLOAD_FOR_FCM = { dry_run: true, notification: { title: "u", body: "y" } }
ANDPUSH_CLIENT = Andpush.build(server_key)
PAYLOAD_FOR_ANDPUSH = PAYLOAD_FOR_FCM.merge(to: DEVICE_TOKEN)
Benchmark.ips do |x|
x.report("andpush") { ANDPUSH_CLIENT.push(PAYLOAD_FOR_ANDPUSH) }
x.report("fcm") { FCM_CLIENT.send(DEVICE_TOKEN, PAYLOAD_FOR_FCM) }
x.compare!
end
結果:
$ ruby bench.rb
Warming up --------------------------------------
andpush 2.000 i/100ms
fcm 1.000 i/100ms
Calculating -------------------------------------
andpush 27.903 (± 7.2%) i/s - 140.000 in 5.063236s
fcm 7.232 (±13.8%) i/s - 35.000 in 5.020404s
Comparison:
andpush: 27.9 i/s
fcm: 7.2 i/s - 3.86x slower
(benchmark のために dry_run: true にしているが、Google さんごめんなさい。)
ご覧の通り、4倍近い throughput が出た。これは、短時間に大量の push notification を送りたい場合に非常に重要である。
なぜ速いの?
net-http-persistent gem で提供される persistent connections を使っているので、一番最初のリクエストだけで SSL のハンドシェイク等が行われ、それ以降は実際のリクエストだけがやりとりされるので速い。たぶん。実は HTTP にそこまで詳しくないので詳しい人教えて下さい。
net-http-persistent gem だって依存の一つではないか?
その通りであるが、作者の Eric Hodel は Ruby committer だし、個人的には Net::HTTP と同じような感覚で使っている。中身もざっくり読んで問題無さそうなので、自分で書くよりはこれを使う方がいいだろうと判断した。さらに、net-http-persistent は connection_pool gem というまた別の gem に依存しているが、これは sidekiq の作者 Mike Perham によって作られたものなので、(そこまでじっくりコードは読んでないものの、ある程度)信頼できる。これ以外の依存は無い。
結論
- 遅い実装には遅い理由があり、速い実装には速い理由がある。
- Pipelining を使えばもっと速くなるだろうが、レスポンスや例外の処理が悩ましくな流のでそれはまた今度。
- コードを読まずに名前やスターの数だけで判断して gem を使うのはやめよう。
- fcm gem よりも andpush gem を使おう。
- andpush gem のフィードバックお待ちしています。