RubyKaigi 2024 Note
ohbarye.icon 個人メモ
Principles
shrinkingのロジック
generatorごとに実装する必要がある
Integerにおけるshrinkの例
これ以上縮小できないbase caseを0とする
これを中立点という
縮小前の値を20とする
縮小前の値から自身を引き (20-20=0)、半分の値を引き (20-10=10)、1/4を引き (20-5=15)、1/8を引き (20-1.25=18.75)...と繰り返す
整数ジェネレーターの場合はtruncateするので18.75 -> 18でよい
すると[0, 10, 15, 18, 19, 20]のような配列が得られる
TODOs
PBTの箇所
デフォルトでは100件のデータを生成
並行/並列処理に関して
論文読む
1994 Property-based testing of privileged programs
1997 Property-based testing: a new approach to testing for assurance
property based testingの実績調査
2000年ではQuickCheck論文の例
✅ generatorを組み合わせられるようにする
✅ array + integer
✅ generatorを一通りつくる
✅ shrinkingのサポート
✅ 基礎機能
arbitraryごとの実装は必要だが呼ばれるようにできた
✅ 並列実行
shrinkのイテレーションを毎回並列実行するようにした
✅ 生成したい値が複数あるユースケースに対応
✅ generatorを可変長引数で渡せる
✅ generatorをキーワード引数で渡せる
✅ 並列化のうまみを得る方法を考える
アイデア
✅ 複数ケース(properties)を並列実行する。100個 x 100個で10,000 Ractorsを作るみたいな
❌ 複数ファイルを並列実行する
結局test executorに手を入れないといけない。rspecやminitestの既存のフローに組み込みたい
❌ ケースの実行ではなくインプットデータの生成も並列化すべき?
やりすぎ。ruby-prop_checkのようにlazyするぐらいでいい
✅ かっこいいsyntax
Pbt.も短いからいいのかも
fast-checkは魔術的じゃなくて真似しやすそう
✅ 並列実行
もしかしてRactorじゃなくてもいい?parallel gemなどRactor以外を指定して使えるようにし、比較するのはあり breakできればさらに効率的だが、同じことをractorでできない
✅ リッチレポート verbose mode
✅ ドキュメンテーション
✅ ベンチマーク
itに相当するprop do ... endみたいなsyntaxが実現できるか?
新しいsyntaxをユーザーに覚えさせるのはいまいちなのでやめる
魔術によって実現はできそう
実装しながら探す
3.3.0では、例外が発生したRactorをRactor.selectに渡すとハングする
isolationに失敗したときにどのオブジェクトなのかを知りたい
Thread.killみたいに実行中のRactorを止める方法は?
キーワード引数をブロックに渡したいができない?
code:ruby
Ractor.new(x: 1, y: 2) { |x:, y:| }
# unknown keyword: :x, :y (ArgumentError)
登壇に向けて
スライド作成
発表練習
日誌
2024/02/23
TODOs整理
2024/02/25
ruby-prop_checkのコード読む
lazy treeの抽象化について理解した
あらゆるところでLazyTreeでラップするのでコードが読みにくい...
generator#generateの際にshrinkingした値まで生成している
テストケースが1件でも失敗したら、そのケースの値のshrunken valuesを用いて
generatorsの結合でモナドのbind演算が出てきたところがよくわからない array generatorを実装
2024/03/02
実践プロパティベーステスト7章
shrinking
実践プロパティベーステスト8章
実行時間が長くなる例があり、並列化の題材として良さそう
2024/03/03
第3部 ステートフルプロパティテスト
モデリングが必要
2024/03/08
elixirのpbt関連実装を見た
stream dataでもlazy treeを使っていた
そもそもlazy treeって有名なデータ構造なのか?
lazy segtreeはあるぽい
2024/03/11
exhaustedの意味がわかった
実行したいケース数に対し、利用可能なテストケースがなくなってしまったとき
これで十分かどうかはプログラマ側で判断する
5.4でshrinkingの元ネタが出てくる
2024/03/12
論文読み切った
2024/03/14
並列度、というかRactorの生成数を増やすためにproperty構文を追加した
これでN properties x M casesのRactorができる
これはRSpecでいえばitに相当するものなので、it抜きで書ける構文を追加したい が、それをやるとRSpec専用実装になるのであとまわし
複数のpropertyをどこかで待ち合わせてレポートする箇所が必要なのでwait_for_all_propertiesを明示的に呼ばせる必要がある...これはあまりかっこよくない
2024/03/16
ケース数などを制御するためにconfigを導入した
configオブジェクトをRactorでシェアできないつらみ
Structをセットして#configureとかで操作できるような定番な操作をしたいが....
Singleton
Configuration.instanceを呼ぶとエラー
code:ruby
can not get unshareable values from instance variables of classes/modules from non-main Ractors (Ractor::IsolationError)
クラス変数
code:ruby
can not access class variables from non-main Ractors (Ractor::IsolationError)
定数
セットするオブジェクトがsharableであればRactor内から参照はできる。frozen Hashとか
いったんこれでいく
もっと良い実装が思いついたら変えたい
2024/03/18
def property内ではRactorでないのでconfigを参照できるが...
configを明示的に引数でバケツリレーすればよい
見た目はかなりRubyishでないが...GoのContextみたいなものと思えば許せなくもない
迷うのでとりあえず定数を使う方向でいったんいく
そろそろparallel shrinkも実装していきたい
syntaxも良い感じにしたい...
2024/03/23
チュートリアル的に書いてみたがとても優れている。並列云々抜きにこういうツールが普通にRubyにほしい
クラスが多用されておらず関数の組み合わせメインなら、構造を真似しても並列化しやすいのではと考えた
やってみたらそんなことなかった...
結果、複数のproperty based testを並列実行できなくなった
isolationの制約が厳しい...
一方、configで定数セットというトリッキーな技を使わなくてすむようにはなった
runnerやreporterを分離でき、機能を追加しやすい構造にはなったのでまずはこの方向で機能拡充する
syntaxもちょっと短くできるようにした(Pbt.から必要なものを全て呼べる
Ractor.selectがhangするときがある。既知の問題かどうかあとで調べる
次こそはshrink実装する
2024/03/24
shrinkの並列化について
通常のshrinkでは、失敗した値を起点としてgeneratorにさらに値を生成させる。その値のうち失敗したものを起点として...という再帰で最小値を探索する
このイテレーションの1回1回をRactorでガッと並列処理するワイルドなつくり
たぶん無駄は多いがトータルで見れば失敗値に辿り着くまでにユーザー時間は少ないという発想
streamの代替としてEnumerator
fast-checkをみていると内部で本当にいろいろ頑張っているなという感じ
足りない機能がわかる
次はarray shrinkをもっとよくする
arrayを短くするだけでなく要素(item)もshrinkする
2024/03/25
osyoyuさんにruby/rubyのbuild方法を教えてもらって実行した
master (3b4dacf2ede0dafbcf942ac696439237f8b31dc6) でもRactorがhangする問題は起きた
./ruby -e "r = Ractor.new{1/0}; Ractor.select(*[r])"
2024/03/30
arbitraryの書き方がだいぶわかってきた
RactorにわたすブロックでHashをdestructuringしたいのだけどArgumentErrorになってしまった
Ractorを使わない場合だけ許容するようなのも考えたが使い方が変わるのはイマイチなので一貫性のある挙動を採用した
2024/03/31
Ractorに渡すことになるblock内が、RSpec exampleインスタンスで実行されるかのようにできないか挑戦した
が、ダメっ...
やったこと
block引数の中身をprism parserで文字列として取得。文字列はRactorに渡せるので渡す
渡した文字列をRSpec exampleインスタンスのinstance_evalで評価してProcインスタンス化すればいける
と思ったがRSpec exampleインスタンスをObjectSpaceから取得するところで失敗
RactorはObejectSpaceもシェアしていないので取得できない
回避するには...?
❌ RSpec exampleインスタンスをRactor内でnewする -> ブロック内で宣言されたローカル変数にはアクセスできない
code:ruby
it "passes a value that the given single arbitrary generates" do
x = 1
Pbt.assert do
Pbt.property(self, Pbt.integer) do |n|
raise unless n.is_a?(Integer)
end
end
end
🧐 itに変わるexample生成のsyntaxをつくる?
RSpec専用の実装をつくりたくない...
解決の糸口がないので後回しにする
調べたメモ
pryはmethod_sourceを使っているのか
node配下のコードをnode.sliceで取得できるのを見つけるのに時間かかった
自前のvisitorを書き、nodeをtree walkする過程にフックしていろいろinspectできるの面白い
autoloadが指定された定数をRactor内で初めて読み込むとエラーになる
code:ruby
RSpec.describe Pbt do
describe "arguments" do
it "passes a value that the given single arbitrary generates" do
r = Ractor.new do
loop do
v, msg = Ractor.receive
v.instance_eval msg
end
end
end
end
code:ruby
# ./spec/e2e/prism_spec.rb:28:in `rescue in block (3 levels) in <top (required)>'
# ./spec/e2e/prism_spec.rb:25:in `block (3 levels) in <top (required)>'
# ./spec/e2e/prism_spec.rb:6:in `block (2 levels) in <top (required)>'
# ------------------
# --- Caused by: ---
# Ractor::UnsafeError:
# require by autoload on non-main Ractor is not supported (Eq)
# (eval at ./spec/e2e/prism_spec.rb:19):1:in `block (5 levels) in <top (required)>'
RSpec::Matchers::BuiltIn.constants.each { |c| RSpec::Matchers::BuiltIn.const_get(c) }のように自前でeager_loadすれば回避できるが... autoloadしているモジュールを全部知るのはだるい
インスタンスをsendしたあとにexpectをinstance_evalすると
どこかでclassかmoduleのインスタンス変数を触るようでIsolationErrorが起きる
code:ruby
# ./spec/e2e/prism_spec.rb:20:in `rescue in block (3 levels) in <top (required)>'
# ./spec/e2e/prism_spec.rb:17:in `block (3 levels) in <top (required)>'
# ------------------
# --- Caused by: ---
# Ractor::IsolationError:
# can not set instance variables of classes/modules by non-main Ractors
sendでcopyじゃなくてmoveするといいのかも?
と思いr.send([self, "expect(1).to eq 1"], move: true)すると
code:ruby
TypeError:
can't create instance of singleton class
ractor-tvarだと
code:ruby
require "ractor/tvar"
tv = Ractor::TVar.new(self)
code:ruby
ArgumentError:
only shareable object are allowed
既存資産がRactor readyじゃなさすぎる...!
つぎ
parallel gemなど別の並行処理ライブラリをオプションとして選べるようにしてみるテストを優先する
それらならexpectやassertionは使えるのか?もしそうならRactorよりもそっちのほうがいいということになるな...
2024/04/01 - 2024/04/02
文字列のarbitrariesを追加した
arbitraryを組み合わせてarbitraryを作れるようになってきて面白い
arbitrary.mapでMapArbitraryを返すようにするとより汎用性が高まって良さそう
継承で複雑にはしたくない気持ちはあるが...
将来的にユーザーにも任意のgeneratorを作らせるデザインなら抽象クラスがあってもいいかも
抽象的なArbitraryでありえるもの (fast-checkにあるやつ)
MapArbitrary
別のarbitraryが生成する値をmapして使える
FilterArbitrary
別のarbitraryが生成する値をfilterして使える
ChainArbitrary
別のarbitraryが生成する値をさらに別のarbitraryに変換できる。当面はいらないかな
NoShrinkArbitrary
よくわかってないけど不要そう
NoBiasArbitrary
bias factorを無視できるやつ。今はbias factorを渡せるつくりになっていないので不要
MapArbitraryとFilterArbitraryを追加
filterを使う場合はかなりexampleが減ってしまい、マッチする値が生成できないことがある
max_consecutive_attemptsみたいなオプションで「N回試みたが値が生成できなかったのでraiseする」というruby prop checkのような仕組みがあってもいい
2024/04/02 - 2024/04/06
毎日ちまちまと基本的なarbitrariesを追加した
parallel gemを真面目にみる
オプションでプロセス / スレッド / Ractorを選択できる
Ractorを選択する場合はクラスとメソッド名を固定で渡すことになる
起動したRactorに対してクラス、メソッド、引数を渡して実行させる
完了したらRactorは終了する
引数はRactor sharableなものに限る
code:ruby
# 3 Processes -> finished after 1 run
results = Parallel.map('a','b','c', in_processes: 3) { |one_letter| SomeClass.expensive_calculation(one_letter) } # 3 Threads -> finished after 1 run
results = Parallel.map('a','b','c', in_threads: 3) { |one_letter| SomeClass.expensive_calculation(one_letter) } # 3 Ractors -> finished after 1 run
2024/04/06
parallel gemを使ってmulti process, multi threadでも実行できるようにした
parallel gemのRactor modeはsyntaxを大きく変えないと使えない
ユーザーが書いたブロックをdefine_methodしてやって外から呼べるようにするみたいな
モチベーション
ベンチマーカー。これで複数手段の比較がしやすくなる
expectとかのアサーションがprocess, threadなら使えるかも
Pbt.assertを並列化できるかもしれない
次こと
逐次実行の以外ケースではnum_runsが無駄に多くなっている。直せたらなおす
arrayのshrinkを賢くする
ドキュメントを充実させる
本当にRactor優位なのかベンチマークとる
expectなどがプロセス、スレッドモードなら使えるのかどうか
さらなら並列化が可能かどうか見切りをつける
リッチレポート
2024/04/07
✅ 逐次実行の以外ケースではnum_runsが無駄に多くなっている。直せたらなおす
✅ arrayのshrinkを賢くする
2024/04/10
RactorのCコードを読もうとしたが何もわからない
さらなら並列化が可能かどうか見切りをつける
Ractor内でrequireができないことが発覚
code:ruby
irb(main):001> Ractor.new{require 'parallel'}.take
(irb):1: warning: Ractor is experimental, and the behavior may change in future versions of Ruby! Also there are many implementation issues.
<internal:/Users/ohbarye/.rbenv/versions/3.3.0/lib/ruby/3.3.0/rubygems/core_ext/kernel_require.rb>:39:in `require': can not access non-shareable objects in constant Kernel::RUBYGEMS_ACTIVATION_MONITOR by non-main ractor. (Ractor::IsolationError)
from (irb):1:in `block in <top (required)>'
<internal:ractor>:711:in `take': thrown by remote Ractor. (Ractor::RemoteError)
from (irb):1:in `<main>'
from <internal:kernel>:187:in `loop'
from /Users/ohbarye/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/irb-1.11.0/exe/irb:9:in `<top (required)>'
from /Users/ohbarye/.rbenv/versions/3.3.0/bin/irb:25:in `load'
from /Users/ohbarye/.rbenv/versions/3.3.0/bin/irb:25:in `<main>'
<internal:/Users/ohbarye/.rbenv/versions/3.3.0/lib/ruby/3.3.0/rubygems/core_ext/kernel_require.rb>:39:in `require': can not access non-shareable objects in constant Kernel::RUBYGEMS_ACTIVATION_MONITOR by non-main ractor. (Ractor::IsolationError)
from (irb):1:in `block in <top (required)>'
assert自体をRactorでラップして実行するとすぐdefined with an un-shareable Proc in a different Ractor (RuntimeError)が起きる...
見切りをつけてもいいかも
2024/04/11 - 2024/04/13
expectやeqなどのマッチャーを使う方法を編み出した...が危険な匂いがすごい
procの中身を取得する
新しいクラスを定義してclass_evalでメソッドにする
定数とメソッド名ならRactorに渡せる
このクラスでexpectなどRSpec DSLが使えるようにするためinclude ::RSpec::Matchersする
Ractorは渡されたクラス名とメソッド名を用いて__send__で実行する
マッチャーのautoloadに失敗したり、クラス変数への代入が発生するところで死ぬ
発見次第prependしてオーバーライドする
main Ractorでの処理を壊さないようif Ractor.current == Ractor.main のような分岐を入れて回避する
これをやるならさらなる並列化はいったん諦めたほうがよい
Pbt.assertをnon-main Ractorでやるのはむずいので
ZeitwerkでRSpecのコードを全部ロードできるか?と思ったが、rspecのコードがZeitwerkの規約に従っているわけではなかったのでうまくいかなかった
✅ expectなどがプロセス、スレッドモードなら使えるのかどうか
使える。便利.....
✅ ドキュメンテーション
がんばった
2024/04/14
✅ ベンチマーク
だいたいのシナリオではworkerがいらないことがわかってしまった...
CPU-boundなシナリオでのみRactorが勝つのがわかったのはよかった
IO-boundシナリオでnoneが勝つのはなぜ?
ローカルのファイルreadぐらいじゃ大したことないからぽい
Networkリクエストを試してみた
code:ruby
require 'net/http'
require 'uri'
response = Net::HTTP.get_response(uri)
やはりprocess, threadで同じぐらいの成果でnoneだけ遅い
code:shell
$ bundle exec rake benchmark:success:io_bound
### Benchmark success:io_bound
This runs a script that does IO bound work.
ruby benchmark/success_io_bound.rb
Warming up --------------------------------------
process 1.000 i/100ms
thread 1.000 i/100ms
none 1.000 i/100ms
Calculating -------------------------------------
process 0.351 (± 0.0%) i/s - 2.000 in 5.693587s
thread 0.352 (± 0.0%) i/s - 2.000 in 5.678491s
none 0.036 (± 0.0%) i/s - 1.000 in 27.668218s
Ractorで動かない問題があるので比較できず...
✅ リッチレポート
fast-checkの実装をほぼそのまま使えた
2024/04/17
0.2.0リリース
rspecのexpectationを使える方法としてrefinementを模索していた
結論ダメ
変更できるスコープがレキシカルで狭いので、RSpec内部のコードをオーバーライドすることはできなかった
直接内部のコードを呼び出すシーンではできるけど、制御がrspecコードにわたってしまうとrefinementが無効になる
prependするパターンしか今のところ見つかってない
prependしたあとに継承ツリーから消せるんだっけ?できない気がするが要調査
2024/04/20
ファイルreadのIOベンチマークではいまいち差が出なかったので追加を試みた。が、結果はどれもイマイチ
ネットワークリクエスト
Net::HTTPによるHTTPリクエスト
Ractorではisolation errorが起きる為ベンチマークできず
TCP
ローカルホストへのTCPリクエストはRactorが早いときもあればworkerなしが早いことも...
UNIXドメインソケット
workerなしが最も早いという結果に。ファイルreadと同じようなもの
DB
sqlite3ですらRactor内では使えない...のでベンチマークできず
当初の期待通りCPUバウンドな処理でしか活躍できていない
スライドにて語りたいことをリストアップしていく
まずはモリモリで作り切る。削るのは後
2024/04/21
RSpec integrationを入れてみた
RUBY_MN_THREADS=1でベンチマークを回す
0.3.0リリース
スライド作成
とりあえず字だけで50ページぐらい一気に書き殴った
話したいことを概ね詰め込んだ
これでざっくり喋ってみてフォーカスすべき点を決めるのはありかも
いらないところも削らずにスライドには残しておく
スライド提出まであと15日。休日はあと7日... 計画していかないとやばい
2024/05/05
ベンチマーク更新
~2024/05/06
ひたすらスライドづくり。96ページ
通し練習してスキップする対象を決めたら提出する
2024/05/07
デモコード実装
2024/05/16
いろんな人からフィードバックもらったり議論できた
ffu
HaskellだとQuickCheckがテストツールのfirst choiceになるのでけっこうPBTに馴染みがある
一方、何やっているのか初見でわからないテストも多い
そもそもテストエコシステムが充実していないという課題があった。今はマシかも
ohbarye.icon 補完的な関係にあるExample basedとProperty basedのうち、Haskellは後者に偏っている。Rubyではほぼ前者なので文化圏によって偏りがあるのは面白い hsbt
Ractor対応させる系のPRは実はそれなりに来ている。10件ぐらいは見た
実行速度の問題があったりするがパッチに問題なければマージしている
構造的な変更を伴うものもある
Ruby本体が使っているtest-unitではテストでRactor対応しているかどうかを判別できるようにしている def宣言の前にractorと書く
Ruby本体のコードではこの機能ではなくassert_ractorを使っている
どっちかに寄せていくべき、どっちがいいか、という議論がここ最近起きていた
内部的には違うことをやっている気がするがまだよくわかってないのでコードを見てみると良さそう
ko1
Thread safetyについて
今でこそいろいろなgemがスレッドセーフということになっているが確実にセーフである保証はしてない
見つかったバグが修正されているにすぎない
ujm) 問題が難しくなるのでは?
DBへの書き込みとかみんなやっているし理解できるはず
forkは意外と遅くない
Copy on writeで生成コストが低くできる
mtsmfm
Ractorを使うようなコードをユーザーが書くのではなく、キラーアプリやツールみたいなのが出てほしさ
ProcessやThreadがユーザーに露出することはあまりない
Links
実例
This is where the unit testing paradigm, which depends primarily on equality assertions, reaches its limits when dealing with varied responses from an LLM.
創造性の高いLLMアプリケーションからの出力を、等価アサーションでテストするのは困難 対象の特性を満たすことを検証する
ksssさんによる、RBSの情報をもとに任意の値でテストを呼び出すツール 他言語
Quixir
outdatedだがコードがものすごく少ない
ExCheck
outdated