ParameterFilterの正規表現フィルタをハッシュルックアップで最適化
アンカー付き完全一致パターン(/^email$/ や /\Atoken\z/ など)の正規表現フィルタを、O(1)のハッシュルックアップに置き換えることで、ParameterFilterのパフォーマンスを大幅に改善しました。
背景
Railsアプリケーションでは、特定のパラメータ名だけをフィルタしたいケースで /^email$/ のようなアンカー付き完全一致正規表現がよく使われます。しかし、これらは実質的に「exact match」であるにもかかわらず、他の正規表現と同列に .any? で全件走査されていました。
Shopifyのアプリケーションを調査した作者は、このパターンが広く使われていることに着目しました。ParameterFilter はリクエストごとに呼び出されるため、ここでのオーバーヘッドはアプリケーション全体のパフォーマンスに積み重なる影響を持ちます。PR の説明によれば、フィルタがすべて完全一致パターンの場合、改善後は最大 4.5倍 の高速化が得られます。
技術的な変更
完全一致正規表現を識別して文字列として取り出す extract_exact_key メソッドが追加され、その結果をハッシュに格納する @exact_keys インスタンス変数が導入されました。
extract_exact_key は以下の条件をすべて満たす正規表現からリテラル文字列を抽出します:
-
casefold?がfalse(大文字小文字を区別する) -
^または\Aで始まり、$または\zで終わる - アンカーを除いた本体が
/\A[a-zA-Z0-9_]+\z/にマッチする(英数字とアンダースコアのみ)
def extract_exact_key(regexp) # :nodoc:
return if regexp.casefold?
source = regexp.source
return unless source.start_with?("^", "\\A") && source.end_with?("$", "\\z")
literal = source.delete_prefix("^").delete_prefix("\\A")
.delete_suffix("$").delete_suffix("\\z")
literal if literal.match?(/\A[a-zA-Z0-9_]+\z/)
end
compile_filters! では、@deep_regexps の分岐の後に完全一致判定の分岐が追加されました。
変更前:
when Regexp
if item.to_s.include?("\\.")
(@deep_regexps ||= []) << item
else
@regexps << item
end
変更後:
when Regexp
if item.to_s.include?("\\.")
(@deep_regexps ||= []) << item
elsif (literal = extract_exact_key(item))
(@exact_keys ||= {})[literal] = true
else
@regexps << item
end
value_for_key での照合では、@exact_keys が存在する場合にまずハッシュルックアップを試みるようになりました。また、key.to_s の呼び出しを key_s に事前キャッシュすることで、同一キーに対する重複変換も排除されています。
def value_for_key(key, value, full_parent_key = nil, original_params = nil)
key_s = key.to_s
if @deep_regexps
full_key = full_parent_key ? "#{full_parent_key}.#{key_s}" : key_s
end
if @exact_keys && @exact_keys[key_s]
value = @mask
elsif @regexps.any? { |r| r.match?(key_s) }
# ...
end
end
テストでは /^token$/(^...$ 形式)と /\Astate\z/(\A...\z 形式)の両アンカースタイルが完全一致として扱われ、/password/(アンカーなし、部分一致)は従来通り @regexps で処理されることが確認されています。
設計判断
extract_exact_key の抽出条件を 英数字とアンダースコアのみ に限定したことが、この最適化の重要な設計判断です。正規表現のソース文字列をそのまま「リテラル」として扱うには、メタ文字(.、*、[ など)が含まれていないことを確認する必要があります。/\A[a-zA-Z0-9_]+\z/ によるバリデーションはその保証として機能しており、誤ったフィルタリング(本来フィルタすべきでないキーがマスクされる、またはその逆)を防いでいます。
casefold? チェックにより、大文字小文字を区別しない正規表現(/^Token$/i など)はハッシュルックアップの対象外となります。ハッシュルックアップでは大文字小文字を無視した照合が行えないため、これも正確性を担保するための判断です。
@exact_keys を nil 初期化し ||= で遅延生成する実装は、@deep_regexps と同じイディオムに揃えたものです。完全一致パターンが1件もない場合はオブジェクトが生成されず、メモリ上のオーバーヘッドを最小限に抑えています。
まとめ
本PRは、Railsアプリケーションで広く使われるパターンを識別し、データ構造の選択(配列の線形走査 → ハッシュのO(1)ルックアップ)を変えることで実用的な高速化を実現しました。既存の @regexps パスへのフォールバックを保持しているため、完全一致以外のパターンや大文字小文字を区別しないフィルタに対する互換性は完全に維持されています。