- Ruby / Rails関連
週刊Railsウォッチ: ArelでCAST関数サポート、webdrivers依存を解消、YJIT高速化ほか(20230824後編)
こんにちは、hachi8833です。5日ぐらい前から、iPhoneアプリのボタンでツイートリンクをコピーしたときのドメインがx.comに変わりましたね。
🔗Rails: 先週の改修(Rails公式ニュースより)
🔗 ArelでSQLのCAST
をサポート
例:
product_table = Product.arel_table product_table.cast(product_table[:position], "integer")
Produces SQL:
CAST("products"."position" as integer)
動機/背景
CAST(field as type)
は広くサポートされているSQL関数。
このプルリクは、cast(field, type)
というヘルパーで、この名前付き関数に対するネイティブなarelサポートを追加する。同PRより
つっつきボイス:「arel_table
を取ってきてcast
するとSQLのCAST
を使えるようになるのか」「Arel大好きな私としては、Arelでできるようになったのは嬉しいですね😋」
参考: PostgreSQL: Documentation: 15: 4.2. Value Expressions
「ただCAST
はDBMS側で実行されるのでインデックスが効かなくなりますよ」「ありゃ、そうなったら遅くなっちゃうか」「CAST
は便利なんですが、念のためみたいなノリでカジュアルに使うとインデックスが効かなくなって遅くなるので、乱用注意ですね」「気をつけないといかんヤツ」「というようなことを昔経験しました」
🔗 to_json
のパフォーマンスを改善
- PR: Improve performance of JSON HTML entity escaping by jhawthorn · Pull Request #48669 · rails/rails
このプルリクは、
.to_json
のパフォーマンスを現在よりもさらに2倍高速にする(現在はRails 7.0よりも約4.5倍高速)。この場合、文字列引数を用いて
gsub!
を5回実行する方が、正規表現とハッシュで1回実行するよりも速いらしい(他の場合はそうでもなさそう)。正規表現にマッチするものがある場合(エスケープすべき文字がある場合)は、部分的に高速(CRubyは新しいマッチオブジェクトと文字列をアロケーションして、提供された
map
のハッシュ内で検索キーとして利用するため)。より上流で最適化が行われる可能性もあるが、現時点ではこの方法はそれらのアロケーションを回避している。この方法は驚くべきことに(少なくとも私は驚いた)、置換が不要な場合でも非常に高速である。テストでは、短い約200バイトの文字列では約3倍速く、事前エスケープ済み約600kのtwitter.jsonでは約5倍速くなった。
ベンチマーク
▶JSONエスケープ(クリックすると展開します)
- スクリプト
$ FILE=twitter.json be ruby benchmark_escaping2.rb Warming up -------------------------------------- original 9.000 i/100ms original (noop) 10.000 i/100ms gsub(str) x 5 44.000 i/100ms gsub(str) x 5 (noop) 229.000 i/100ms gsub(ascii_re) + gsub(utf8) 25.000 i/100ms gsub(ascii_re) + gsub(utf8) (noop) 32.000 i/100ms json_escape gem 919.000 i/100ms Calculating ------------------------------------- original 94.573 (± 0.0%) i/s - 477.000 in 5.043877s original (noop) 100.715 (± 0.0%) i/s - 510.000 in 5.063885s gsub(str) x 5 496.012 (± 7.9%) i/s - 2.464k in 5.002329s gsub(str) x 5 (noop) 2.302k (± 0.7%) i/s - 11.679k in 5.074632s gsub(ascii_re) + gsub(utf8) 257.878 (± 0.8%) i/s - 1.300k in 5.041332s gsub(ascii_re) + gsub(utf8) (noop) 321.248 (± 0.6%) i/s - 1.632k in 5.080333s json_escape gem 8.958k (± 0.3%) i/s - 45.031k in 5.026965s Comparison: json_escape gem: 8958.0 i/s gsub(str) x 5 (noop): 2301.5 i/s - 3.89x slower gsub(str) x 5: 496.0 i/s - 18.06x slower gsub(ascii_re) + gsub(utf8) (noop): 321.2 i/s - 27.88x slower gsub(ascii_re) + gsub(utf8): 257.9 i/s - 34.74x slower original (noop): 100.7 i/s - 88.94x slower original: 94.6 i/s - 94.72x slower
short.json
{"locale":"en","featureFlags":["redactedredactedredact","redactedredactedredac","redactedredactedre","redactedredactedred","redactedredact","redactedredactedredacted","redactedredactedredactedr"]}
$ FILE=twitter.json be ruby benchmark_escaping2.rb Warming up -------------------------------------- original 9.000 i/100ms original (noop) 10.000 i/100ms gsub(str) x 5 44.000 i/100ms gsub(str) x 5 (noop) 229.000 i/100ms gsub(ascii_re) + gsub(utf8) 25.000 i/100ms gsub(ascii_re) + gsub(utf8) (noop) 32.000 i/100ms json_escape gem 919.000 i/100ms Calculating ------------------------------------- original 94.573 (± 0.0%) i/s - 477.000 in 5.043877s original (noop) 100.715 (± 0.0%) i/s - 510.000 in 5.063885s gsub(str) x 5 496.012 (± 7.9%) i/s - 2.464k in 5.002329s gsub(str) x 5 (noop) 2.302k (± 0.7%) i/s - 11.679k in 5.074632s gsub(ascii_re) + gsub(utf8) 257.878 (± 0.8%) i/s - 1.300k in 5.041332s gsub(ascii_re) + gsub(utf8) (noop) 321.248 (± 0.6%) i/s - 1.632k in 5.080333s json_escape gem 8.958k (± 0.3%) i/s - 45.031k in 5.026965s Comparison: json_escape gem: 8958.0 i/s gsub(str) x 5 (noop): 2301.5 i/s - 3.89x slower gsub(str) x 5: 496.0 i/s - 18.06x slower gsub(ascii_re) + gsub(utf8) (noop): 321.2 i/s - 27.88x slower gsub(ascii_re) + gsub(utf8): 257.9 i/s - 34.74x slower original (noop): 100.7 i/s - 88.94x slower original: 94.6 i/s - 94.72x slower
$ FILE=short.json be ruby benchmark_escaping2.rb Warming up -------------------------------------- original 26.846k i/100ms original (noop) 27.998k i/100ms gsub(str) x 5 82.923k i/100ms gsub(str) x 5 (noop) 83.683k i/100ms gsub(ascii_re) + gsub(utf8) 103.509k i/100ms gsub(ascii_re) + gsub(utf8) (noop) 103.445k i/100ms json_escape gem 1.424M i/100ms Calculating ------------------------------------- original 279.761k (± 1.6%) i/s - 1.423M in 5.087410s original (noop) 279.118k (± 2.1%) i/s - 1.400M in 5.017966s gsub(str) x 5 835.196k (± 0.5%) i/s - 4.229M in 5.063709s gsub(str) x 5 (noop) 832.362k (± 0.9%) i/s - 4.184M in 5.027225s gsub(ascii_re) + gsub(utf8) 1.028M (± 0.7%) i/s - 5.175M in 5.035134s gsub(ascii_re) + gsub(utf8) (noop) 1.032M (± 0.5%) i/s - 5.172M in 5.014163s json_escape gem 14.476M (± 0.7%) i/s - 72.617M in 5.016768s Comparison: json_escape gem: 14475588.1 i/s gsub(ascii_re) + gsub(utf8) (noop): 1031551.3 i/s - 14.03x slower gsub(ascii_re) + gsub(utf8): 1027913.4 i/s - 14.08x slower gsub(str) x 5: 835196.3 i/s - 17.33x slower gsub(str) x 5 (noop): 832361.5 i/s - 17.39x slower original: 279761.3 i/s - 51.74x slower original (noop): 279117.6 i/s - 51.86x slower
json_escape
は同じエスケープ処理をCで実装している(このgemは使わないこと: 必要に思えても別の使い所を見つけるべき)。短い文字列では、ASCII部分のエスケープを正規表現として実行し、その後Unicode部分のエスケープを文字列として実行する方法が最適に見えるが、
gsub!
を5回実行する方法よりもわずかに速い程度なので、シンプルにするため、そして長い文字列でパフォーマンスを大幅に向上させるため、この方法にこだわった。
to_json
#48614と同じベンチマークを使う。
JSON.generate
とRapidJSON.generate
はスケール用であり、ここで説明している余分なエスケープ処理は行わない。▶
to_json
(クリックすると展開します)
- 改修前
Calculating ------------------------------------- source_data.to_json 74.671 (± 1.3%) i/s - 378.000 in 5.062521s source_data_sym.to_json 76.969 (± 0.0%) i/s - 385.000 in 5.002087s JSON.generate(source_data) 576.164 (± 1.2%) i/s - 2.916k in 5.061908s RapidJSON.generate(source_data) 852.714 (± 3.2%) i/s - 4.316k in 5.067053s
- 改修後
Calculating ------------------------------------- source_data.to_json 142.762 (± 2.1%) i/s - 715.000 in 5.010974s source_data_sym.to_json 150.613 (± 2.0%) i/s - 756.000 in 5.021137s JSON.generate(source_data) 575.412 (± 1.0%) i/s - 2.915k in 5.066603s RapidJSON.generate(source_data) 873.174 (± 2.9%) i/s - 4.368k in 5.006853s
#48614の約2倍高速で、Rails 7.0 よりも4.5倍高速。
次はどうする?
これまでよりはずっと高速になったが、エスケープ処理はまだパフォーマンスに影響する(ただし、もはや主要な原因ではない)。私たちにできることとして、エスケープ処理をC拡張で書き直す方法が考えられる(理想としては標準ライブラリにアップストリームしたい)。しかし、2023年以降も本当にこの方法でエスケープ処理を行うべきかどうか確認する必要があると思われる。
そのあたりを@matthewdと私で調査したが、おそらく必要なのは
<script
、</script>
、および<!--
のエスケープだけでよい可能性がある(ただし、おそらくXHTMLを対象にせずHTML5のみを対象にすることが前提)。ES2019以降は、JSON/JavaScriptの相互運用性に関する"クセ"である
U+2028
とU+2029
の処理を気にかける必要はなくなった。現在このバージョンは広く利用可能になっている。ただし今の話はどれもこのプルリクの範疇ではない。このプルリクはエスケープ動作を変更せずにパフォーマンスを向上できる。
同PRより
つっつきボイス:「これは最適化ですね」「to_json
をさらに速くしたとは凄い」
参考: § 2.11 JSONのサポート -- Active Support コア拡張機能 - Railsガイド
# activesupport/lib/active_support/json/encoding.rb#L37
def encode(value)
unless options.empty?
value = value.as_json(options.dup)
end
json = stringify(jsonify(value))
+
+ # Rails does more escaping than the JSON gem natively does (we
+ # escape \u2028 and \u2029 and optionally >, <, & to work around
+ # certain browser problems).
- if Encoding.escape_html_entities_in_json
- json.gsub! ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS
- else
- json.gsub! ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS
+ json.gsub!(">", '\u003e')
+ json.gsub!("<", '\u003c')
+ json.gsub!("&", '\u0026')
end
+ json.gsub!("\u2028", '\u2028')
+ json.gsub!("\u2029", '\u2029')
json
end
「この改修では、複雑な正規表現1回よりシンプルなgsub!
を5回実行する方が速かった、なるほど」「ありそうな話」「ベンチマーク回してみないとわからないヤツ」「そういえば専用メソッドの方が正規表現より速いことも多いですよね↓」
「今後Rubyのコードが進化したらまた変わってくるかも」「プルリクメッセージの"次はどうする"でもそのあたりを心配しているっぽいですね」
🔗 webdrivers
への依存を解消
動機/背景
Selenium 4.6以降、Selenium ManagerでChrome Driverのインストールと統合を管理できるようになった。
Selenium 4.11以降、Selenium Managerでテスト用のChromeのインストールパスを解決可能になった。詳細
gem
宣言をGemfile.tt
からなくすことで、新しく生成されたアプリケーションや最新バージョンのRailsに合わせてGemfile
を更新しているアプリケーションで、依存関係を取り除き、新しくリリースされたChromeのバージョンが導入されたときのテストの失敗を回避できる(例: titusfortner/webdrivers#247)。同PRより
つっつきボイス:「webdriversが不要になるのは嬉しい👍」「今までテスト用のChromeが自動アップデートされるとテストが失敗するようになったりすることがありましたよね」
🔗Rails
🔗 rspec-sidekiq: RSpecでSidekiqテストをサポート(Ruby Weeklyより)
つっつきボイス:「名前の通り、RSpecでSidekiqのテストを書きやすくするgemですね」「★多いですね」「複数ジョブを組み合わせたテストがやりやすくなりそう👍」
# 同リポジトリより
# 基本
expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job
# ジョブクラスを指定
expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job(AwesomeJob)
# 引数を指定
expect { AwesomeJob.perform_async "Awesome!" }.to enqueue_sidekiq_job.with("Awesome!")
# キュー名を指定
expect { AwesomeJob.set(queue: "high").perform_async }.to enqueue_sidekiq_job.on("high")
# 日時を指定
specific_time = 1.hour.from_now
expect { AwesomeJob.perform_at(specific_time) }.to enqueue_sidekiq_job.at(specific_time)
# インターバルを指定(freezeや時間の管理に注意すること)
freeze_time do
expect { AwesomeJob.perform_in(1.hour) }.to enqueue_sidekiq_job.in(1.hour)
end
# 組み合わせやチェインも自由に行える
expect { AwesomeJob.perform_at(specific_time, "Awesome!") }.to(
enqueue_sidekiq_job(AwesomeJob)
.with("Awesome!")
.on("default")
.at(specific_time)
)
「on
でSidekiqのキューオプションも渡せるんですね」
# 同リポジトリより
class AwesomeJob
include Sidekiq::Job
sidekiq_options queue: :low
end
AwesomeJob.perform_async("a little awesome")
# test with..
expect(AwesomeJob).to have_enqueued_sidekiq_job("a little awesome").on("low")
# Setting the queue when enqueuing
AwesomeJob.set(queue: "high").perform_async("Very Awesome!")
expect(AwesomeJob).to have_enqueued_sidekiq_job("Very Awesome!").on("high")
🔗Ruby
🔗 YJITの例外ハンドラを高速化(Ruby Weeklyより)
Yeah we've just had a huge speedup on that benchmark yesterday :) https://t.co/qKBGOtaa0K
— k0kubun (@k0kubun) August 9, 2023
つっつきボイス:「YJITは継続的に高速化と改良が進んでいて頼もしい🎉」「speed.yjit.orgでも成果がわかりますね↓」
参考: speed.yjit.org
🔗 Short Ruby Newsletter: Rubyの技術情報ニュースレター
I’m super impressed with the @shortrubynews newsletter (and @lucianghinda). This newsletter solves my Ruby tweet FOMO. I now am a paid subscriber.
Feature request: create a searchable archive of the content (keyword, author, etc) 🙏https://t.co/y9vo54kzxQ
— Eric Berry (@coderberry) February 28, 2023
つっつきボイス:「YassLabの安川さんに教わったサイトです」「サイトを開いたけどニュースレター登録のフォームしか見えない?」「あ、右上の✗で閉じてください」
「なるほど、いわゆるRubyの技術情報をニュースレターで配布するサービスなのね」「本編は有料コンテンツですが、1年前にスタートしてから#55号も出していて、かなり熱心に運営しているみたいです」「週1.6ドルまたは月6.5ドルなのね」「有料で運営する理由の説明もありますね: 運営を持続可能にする、読者が学習費用を経費で落とせるなど」「興味のある方は購読してみてもいいかも👍」
訂正(2023/08/25)
以下のご指摘をいただきました🙇。ありがとうございます!
今回の号についてですが、Short Ruby Newsは本編は無料です。もちろんお金を払うことはできますが、任意です。
— 大倉雅史(OKURA Masafumi) (@okuramasafumi) August 24, 2023
「有料ニュースレターは昔よく見かけたな〜」「英語圏だと今でもニュースレター形式の技術情報が割りと多いですね」
🔗 設計・セキュリティ
🔗 ソルト付きハッシュのソルトはどこに保存するのが一般的か
バランスのとれた良い記事。個人的にはpepperの前に、DBのパスワードカラムを読み込み禁止にすべきだと思う(SQLインジェクション耐性できるしストアドで認証出来る) / https://t.co/kTFpKhLgLy
— Kazuho Oku (@kazuho) August 17, 2023
つっつきボイス:「例のpictLandとpictSQUAREの件をきっかけに徳丸先生がソルトの保存場所についてクイズを出して↓、その後で上の記事を書いていました」
ソルト化ハッシュが話題になっているので、ソルトに対する理解度測定試験問題を作りました。ふるってご参加ください。現在の知識で「これかな?」で答えていただけると幸いです。
【問題】パスワードをハッシュ値で保存する際のソルトはどこに保存するのが一般的な実装ですか?— 徳丸 浩 (@ockeghem) August 17, 2023
「記事はまさに定番の内容ですね: ソルトはユーザーのパスワードハッシュとともにデータベースに保存するのが基本で、ソルトを環境変数やハードウェアセキュリティモジュール(HSM)に保存する方法は"ユーザーのパスワードハッシュごとにユニークである"という要件を満たせなくなるので普通やらない」「クイズの方は思ったより正答率低いですね」「ソルトとペッパーの違いをこの記事で思い出しました😅」
参考: Hardware security module - Wikipedia
「記事にもあるように、Unixのパスワードにはハッシュとともにソルトも保存されていますね」「Modular Crypt Format(MCF)っていう形式なのか: 以前パスワード周りを調べたときに、Deviseなどもたしかこういうフォーマットになっていたのを覚えてます↓」
$ openssl passwd -6 -salt=SALT pokemon
$6$SALT$ULXzhDaWogf6Q3KHTtpYdqKKEIaFPML8gl5wpHpvPJVkGgiKGubqkogwvqoVn3eDsrJuRB22w.RPWzAdEu1xD.
参考: Modular Crypt Format — Passlib v1.7.4 Documentation
「既存のパスワードハッシュを別フレームワークに移行しなければならなくなったことがあったんですが、そのときに現行のパスワードハッシュがどのフォーマットなのかを自力で解析しました」「フレームワークが変わるのは大変そう...」
以下はつっつき後に見つけたツイートです。
敢えて別々に保存する動機がないということです。分けて保存しても、侵入されていることを前提にすると大して安全にならないのに、トランザクションの一貫性を保つのに工夫がいるなど面倒くさいだけ。 https://t.co/LUiOYlncLB
— 徳丸 浩 (@ockeghem) August 21, 2023
後編は以上です。
バックナンバー(2023年度第3四半期)
週刊Railsウォッチ: Rails 7.0.7に含まれているRails 7.0.6のバグ修正ほか(20230823前編)
- 20230809 Rails 7.0.5のcreate_association挙動変更取り消し、YJITの性能を最大限引き出す方法ほか
- 20230803後編 Railsフラグメントキャッシュ経由の情報漏洩に注意ほか
- 20230802前編 Active Storageバリアントの事前変換、Linkヘッダープリロードのオプトアウトほか
- 20230727後編 Rubyにdefp導入の提案、IRB 1.7.3リリースほか
- 20230725前編 config.autoload_libとconfig.autoload_lib_onceが追加ほか
- 20230721後編 Kaigi on Rails 2023プロポーザル募集、rubocop-magic_numbersほか
- 20230719前編 複合主キー関連の実装進む、Action TextでHTML5サニタイザほか
- 20230705後編 AWS LambdaでRailsをRackで動かすLambyほか
- 20230704前編 productionのforce_ssl=trueがデフォルトで有効に、rakeタスクをthorで書くほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)