ぶていのログでぶログ

思い出したが吉日

rfコマンドv1.23.0をリリースした

前回の記事はこちら。 前回に続いてついにYAMLの色つけ出力に対応した!やったぜ。 また、JSONの色つけ出力も改善し、階層ごとにObject(HASH)のキーの色を変えるようにした。 これで視認性が上がるはず。

以下、変更点の詳細

🏕 Features

mruby-yamlに変わってmruby-rapidyamlを使うようにした

YAMLの色つけ出力を行うために、mruby-rapidyamlという新しいmgemを作った。

github.com

mruby-yamlをforkしてもよかったのだが、せっかくなので1から作ろうと思いたちこれを作った。 …が、結構苦労したのでそれは別の記事で書く予定。。

色つけ出力ができるようになったという効果もあったのだが、rapidyamlは名前の通り高速性も売りにしていて、その恩恵にあやかり、rfも高速でYAMLをパースできるようになった! 手元の環境で、73MB程度のYAML(後述)の読み込みをrf v1.23.0、rf v1.22.0と参考にyq v4.43.1、CRuby v3.3.4のYAMLライブラリで比較してみた。

❯ ls -lah large_file.yaml
-rw-r--r-- 1 buty4649 buty4649 74M Aug 18 23:15 large_file.yaml

❯ hyperfine --warmup 3 --input large_file.yaml 'mise exec rf@1.23.0 -- rf -y -qs _' 'mise exec rf@1.22.0 -- rf -y -qs _' 'yq null' 'ruby -ryaml -e YAML.load\(\$stdin.read\)'
Benchmark 1: mise exec rf@1.23.0 -- rf -y -qs _
  Time (mean ± σ):     983.9 ms ±   4.9 ms    [User: 776.4 ms, System: 139.6 ms]
  Range (min … max):   977.5 ms … 991.5 ms    10 runs

Benchmark 2: mise exec rf@1.22.0 -- rf -y -qs _
  Time (mean ± σ):      2.382 s ±  0.029 s    [User: 1.852 s, System: 0.335 s]
  Range (min … max):    2.308 s …  2.422 s    10 runs

Benchmark 3: yq null
  Time (mean ± σ):      2.984 s ±  0.054 s    [User: 3.766 s, System: 0.267 s]
  Range (min … max):    2.920 s …  3.080 s    10 runs

Benchmark 4: ruby -ryaml -e YAML.load\(\$stdin.read\)
  Time (mean ± σ):      8.496 s ±  0.060 s    [User: 7.350 s, System: 0.373 s]
  Range (min … max):    8.424 s …  8.617 s    10 runs

Summary
  mise exec rf@1.23.0 -- rf -y -qs _ ran
    2.42 ± 0.03 times faster than mise exec rf@1.22.0 -- rf -y -qs _
    3.03 ± 0.06 times faster than yq null
    8.63 ± 0.07 times faster than ruby -ryaml -e YAML.load\(\$stdin.read\)

rf v1.22.0は2.42sだったのが、v1.23.0では0.983s(983ms)になった!約2.46倍早くなったすごい! これだけでもmruby-rapidyamlを実装してよかったとおもう。

おまけ: 巨大なYAMLファイルを生成するスクリプト

テストにつかった巨大なYAMLファイルを生成するスクリプト。 ChatGPTに作ってもらったのだが、出力サイズを10MBと指定しても誤差がひどくて74MBのファイルが生成された。

require 'yaml'

# 3階層のダミーデータを生成するための関数
def generate_large_data(entry_count)
  data = {}
  entry_count.times do |i|
    nested_data = {}
    10.times do |j|
      inner_data = {}
      10.times do |k|
        inner_data["key_#{i}_#{j}_#{k}"] = "value_" + ("a" * 50) # さらに内側のキーと値
      end
      nested_data["key_#{i}_#{j}"] = inner_data
    end
    data["key_#{i}"] = nested_data
  end
  data
end

# 大きなYAMLファイルを生成する
def create_large_yaml_file(filename, size_in_mb)
  entry_count = (size_in_mb * 1024 * 1024) / 1040 # 1エントリのサイズが約1040バイトと仮定
  data = generate_large_data(entry_count)
  File.open(filename, 'w') do |file|
    file.write(YAML.dump(data))
  end
end

# 10MBのYAMLファイルを作成
create_large_yaml_file('large_file.yaml', 10)

rfのテストを厳格化した

rfではrfコマンドの実行してその出力が意図した通りかをチェックしている。 この出力のマッチャーの指定が今までmatchだったのをeqにした。 該当のコードは以下のような感じ。

# 変更前
expect(last_command_started.output).to match expect_output

# 変更後
expect(last_command_started.output).to eq expect_output

matchマッチャーを使うと正規表現を使って判定するようで意図しないテストになっていた。 そのため、厳格にチェックするためにeqに変更した。 ただしこれだとバージョンごとに出力が変わる項目などのテストがとても煩雑になるので、expect_outputがRegexpのインスタンスの場合は今まで通りmatchを使うようにした。