@ledsun blog

無味の味は佳境に入らざればすなわち知れず

bootstrap(セルフホスト)時のspinel_codegen.rb実行時のGC回数増

bootstrap(セルフホスト)時のspinel_codegen.rb実行時間の変わり方 - @ledsun blog で、Nodeテーブルを初期化するクラスを抽出すると、実効時間が160秒 => 344秒と伸びることが分かりました。 この原因を探りたいです。

まず、stackprof の CPU profile で計測したところGCに掛かる時間が増えていることが分かりました。 そこでGCに注目して計測してみました。

GC.stat による GC 回数の測定

GC.stat で GC 回数と allocation の差を確認します。

目的

ruby spinel_codegen.rb ast output.c の実行前後で GC.stat を取り、次の値の差分を比較します。

  • total_allocated_objects
  • total_freed_objects
  • heap_live_slots
  • heap_free_slots
  • minor_gc_count
  • major_gc_count
  • old_objects

測定コマンド

抽出前の base と抽出後の extract-node-init を同じ入力 AST で測ります。 こんな感じです。

ruby -e '
ast, out, codegen = ARGV
ARGV.replace([ast, out])
before = GC.stat
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
load codegen
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
after = GC.stat

keys = [
  :total_allocated_objects,
  :total_freed_objects,
  :heap_live_slots,
  :heap_free_slots,
  :minor_gc_count,
  :major_gc_count,
  :old_objects,
]

puts "elapsed=#{(t1 - t0).round(3)}"
keys.each do |key|
  puts "#{key}=#{after[key]} delta=#{after[key].to_i - before[key].to_i}"
end
' AST_PATH OUT_C_PATH SPINEL_CODEGEN_RB_PATH

測定対象

対象 コミット 内容
base 0d10645 extract-node-init の親
extract d94f5d6 refactor(codegen): extract node table loader

結果

指標 base extract
elapsed 189.826s 356.201s +166.375s
total_allocated_objects delta 2,149,566,411 2,064,184,970 -85,381,441
total_freed_objects delta 2,144,512,398 2,063,651,005 -80,861,393
heap_live_slots delta 5,054,013 533,965 -4,520,048
heap_free_slots delta 2,416,646 2,339,788 -76,858
minor_gc_count delta 2,082 19,438 +17,356
major_gc_count delta 19 33 +14
old_objects delta 3,393,053 526,672 -2,866,381

読み取り

extract-node-init では総 allocation 数は少し減っています。 minor_gc_count2,082 から 19,438 に増えています。約 9.3x の増加です。 推論ロジックよりGCに原因がありそうです。

さて、なぜGC回数が増えたのでしょうか? またGCを切ったらこの差は縮まるのでしょうか?

bootstrap(セルフホスト)時のspinel_codegen.rb実行時間の変わり方

spinel_codegen.rbのbootstrap(セルフホスト)時の性能評価手順を整理 - @ledsun blog をつかってどのくらい性能が変わるかを試しに測ってみます。

refactor(codegen): extract node table loader · ledsun/spinel@d94f5d6 · GitHub のようにNodeテーブルを初期化するクラスを抽出します。すると?

変更前 。 コミットで言うと https://github.com/matz/spinel/commit/f6b593711e8100d0ff0fbd77243b8e1678e39d7e です。

{
  "results": [
    {
      "command": "ruby spinel_codegen.rb /tmp/spinel_master_codegen.ast /tmp/spinel_master_bench_gen1.c",
      "mean": 160.00851559026,
      "stddev": 11.138275974757432,
      "median": 154.14327744726,
      "user": 158.99329034,
      "system": 1.0015925266666665,
      "min": 153.02849486026,
      "max": 172.85377446326,
      "times": [
        153.02849486026,
        154.14327744726,
        172.85377446326
      ],
      "exit_codes": [
        0,
        0,
        0
      ]
    }
  ]
}

変更後

{
  "results": [
    {
      "command": "ruby spinel_codegen.rb build/codegen.ast /tmp/spinel_codegen_bench_gen1.c",
      "mean": 344.58640011078,
      "stddev": 9.74270517215269,
      "median": 347.07773677278,
      "user": 343.53394030666664,
      "system": 1.0031182066666666,
      "min": 333.83992995578,
      "max": 352.84153360378,
      "times": [
        333.83992995578,
        352.84153360378,
        347.07773677278
      ],
      "exit_codes": [
        0,
        0,
        0
      ]
    }
  ]
}

雑に中央値を見ると 160秒 => 344秒。 軽く倍になっています。 「こんなに変わるんだ」と、驚きがあります。

ここから、性能に大きく影響を与える変更を絞り込んで行きたいです。

spinel_codegen.rbのbootstrap(セルフホスト)時の性能評価手順を整理

ビルド済みのspinelコマンドの実効時間の話ではありません。 spinelコマンドを作成するためにspinel_codegen.rbを実行する時間を計測します。

背景

[codex] Extract NodeStore from codegen by ledsun · Pull Request #284 · matz/spinel · GitHub にてリファクタリングしてみたところ、僕の予想以上に実効時間が伸びたことが始まりです。 いくらか憶測で修正してみましたが、どういう修正が実効時間に影響を与えるのか特定できていません。

基本に立ち返ってまずは計測方法を用意します。

目的

spinel_codegen.rb の分割や require 追加が、ruby spinel_codegen.rb の実行速度に与える影響を切り分けるための計測手順です。

計測対象

計測対象はコード生成フェーズだけに固定します。

ruby spinel_codegen.rb build/codegen.ast /tmp/spinel_codegen_bench_gen1.c

build/codegen.ast は計測前に一度だけ作り直します。

./spinel_parse spinel_codegen.rb build/codegen.ast

理由

make spinel_codegen は次の処理を含みます。

  1. spinel_parse による AST 生成
  2. ruby spinel_codegen.rb ... による C コード生成
  3. C コンパイラによる spinel_codegen バイナリ生成

今回見たいのは Ruby スクリプトの分割による影響なので、AST 生成と C コンパイルは計測から外します。これにより、Ruby のファイルロード、初期化、AST 読み込み、コード生成処理にかかる時間を比較しやすくします。

出力先は /tmp/spinel_codegen_bench_gen1.c に固定します。リポジトリ内の build/gen1.c を更新しないため、計測で作業ツリーを汚しません。

方法

hyperfine を使い、ウォームアップ 1 回、計測 5 回を標準条件にします。

#!/usr/bin/env bash
set -euo pipefail

RUNS="${RUNS:-5}"
WARMUP="${WARMUP:-1}"
AST="${AST:-build/codegen.ast}"
OUT="${OUT:-/tmp/spinel_codegen_bench_gen1.c}"
JSON_OUT="${JSON_OUT:-/tmp/spinel_codegen_hyperfine.json}"

if ! command -v hyperfine >/dev/null 2>&1; then
  echo "error: hyperfine is required" >&2
  exit 1
fi

if ! command -v ruby >/dev/null 2>&1; then
  echo "error: ruby is required" >&2
  exit 1
fi

if [ ! -x ./spinel_parse ]; then
  echo "error: ./spinel_parse is required; build it before measuring" >&2
  exit 1
fi

mkdir -p "$(dirname "$AST")"

echo "ruby: $(ruby -v)"
echo "hyperfine: $(hyperfine --version)"
echo "commit: $(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
echo "ast: $AST"
echo "out: $OUT"
echo "runs: $RUNS"
echo "warmup: $WARMUP"

./spinel_parse spinel_codegen.rb "$AST"

hyperfine \
  --warmup "$WARMUP" \
  --runs "$RUNS" \
  --export-json "$JSON_OUT" \
  "ruby spinel_codegen.rb $AST $OUT"

wc -l "$AST" "$OUT"
echo "json: $JSON_OUT"
benchmark/measure_codegen.sh

実行回数は環境変数で変更できます。

RUNS=10 WARMUP=2 benchmark/measure_codegen.sh

結果の JSON は標準では /tmp/spinel_codegen_hyperfine.json に出力します。

JSON_OUT=/tmp/codegen-master.json benchmark/measure_codegen.sh

比較時は同じ Ruby、同じマシン、同じ RUNS / WARMUP で測ります。ブランチを切り替えた後は毎回このスクリプトを実行し、各ブランチの平均値、中央値、標準偏差を比較します。

Ubuntu + llama.cppでローカルLLM環境構築メモ

llama.cpp のビルド

依存ツールインストール

sudo apt update
sudo apt install -y build-essential cmake git

ソース取得

git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp

ビルド(CMake)

cmake -B build
cmake --build build --config Release -j

動作確認

モデル実行

./build/bin/llama-cli -hf Qwen/Qwen2.5-Coder-3B-Instruct-GGUF

今のところ効果がありそうな設定

-t 8
-c 4096

下のパラメーターを指定したら遅くなりました。

-t 12
--cache-type-k q8_0
--cache-type-v q8_0

性能目安

  • Prompt: ~60 tok/s
  • Generation: ~10 tok/s

ノートPCのCPUで動かしているので、これくらい

WSLのrbenv経由のRubyの起動を速くする

結論

mkdir -p ~/.local/bin
ln -sf /usr/bin/readlink ~/.local/bin/greadlink
fish_add_path -p ~/.local/bin

問題

https://github.com/matz/spinel のテストをローカルで実行すると、229回 Ruby が実行されます。 Ruby の起動に時間が掛かると、それだけでテスト実行に時間が掛かります。

原因

rbenv はまず /usr/bin/rbenv がシンボリックリンクなので、それをたどって /usr/lib/rbenv/libexec/rbenv を見つけます。 このときに readlink コマンドを使います。

ただし macOS の標準 readlink は BSD 系で、GNU readlink とはオプションや挙動が違います。 macOS で GNU coreutils を入れると、GNU 版 readlinkgreadlink という名前で提供されることがあります。

この差を吸収するため、rbenv は type -p greadlink readlink を実行し、まず greadlink コマンドを探します。

WSL で PATH/mnt/c/... の Windows 側パスが多く含まれていると、Linux 側に存在しない greadlink を探すために Windows 側パスまで探索します。 この探索に時間が掛かります。

対策

greadlink コマンドを PATH の上流に置くと探索が即座に終わり、rbenv shim 経由の Ruby 起動が速くなります。

RubyKaigi 2026

0日目

移動トラブル

今回のRubyKaigiは、かなり印象的なスタートになりました。

もともとは羽田空港から函館空港へ飛行機で移動する予定でした。 管制機器のトラブルによって、飛行機の出発が危ぶまれる状況になったのです。 飛行機は諦めて新幹線で移動することにしました。

移動には4時間ほどかかり、函館駅に到着したのは10時過ぎでした。

ただ、このトラブルのおかげで良い体験もありました。 同じように移動トラブルに巻き込まれていた rhiroe さんが、Slackで「ご飯行く人いませんか?」と募集していたのを見かけて、それに乗っかる形で一緒にジンギスカンを食べに行きました。 Slack上ではIDを見かけていて認識はしていたのですが、実際にお会いするのは初めてでした。

ジンギスカン

移動トラブルが良い思い出になりました。

1日目

キーノート

The Journey of Box Building - RubyKaigi 2026

Tagomorisさん

1日目で一番印象に残ったのは、Tagomorisさんのキーノートでした。

3年間、Ruby::Boxを作り続けてきたのもすごいのですが、2023年のshioyama先生の発表がきっかけでRubyにコミットするモチベーションに気がついた話がエモくて良かったです。

自分の過去のことを思い出しました。 そういえば自分も2022年のkateinoigakukunさんの発表をみて、自分の「ブラウザでRubyを動かしたい」というモチベーションに思い出したのでした。

こういうRubyKaigiの発表を見ていた人が刺激を受けて次の発表者になるのが、とてもRubyKaigiで、エモいです。

PicoRuby.WASM

Funicular: A Browser App Framework Powered by PicoRuby.WASM - RubyKaigi 2026

はすみさんの PicoRuby.WASM の発表も印象に残っています。

スリープ処理を自前で実装しているのがかっこよかったです。 ruby.wasm は、WASIに依存する作戦をとったため、スリープの実装は諦めています。 必要であればアドホックにブラウザのsetTimeoutを使う方針です。 ここを諦めずに、自前でタスクスケジューラから実装しているのがかっこよかったです。

さらにブラウザでデバッガも動いていてかっこいいです。

はすみさんは継続的にアウトプットを出し続けている点もすごいです。

お昼ご飯とちょっとした雑談

1日目のお昼は、ANDPADさんが配られていたラッキーピエロのチャイニーズチキンバーガーを食べました。

杉野さんと大塚さんと一緒に食べていました。 RubyKaigiで毎年恒例になってきました。 美容院の話なんかしました。

杉野さんが血糖値の計測装置をつけていて、その話で盛り上がっていました。 ご飯を食べると本当に数値が上がるので面白かったです。

レッドブルを飲むと一気に上がって下がるという話を聞いて、へーって思いました。

はるかさんの発表

Building a Standalone Ruby Programming Environment - RubyKaigi 2026

はるかさん

はるかさんの発表は、また違った意味で印象に残っています。 内容自体も面白かったのですが、それ以上に説明をはっしょった苦労ポイントがありそうでした。 DVI出力の速度が足りないときにどうやってボトルネックを特定したのか不思議でした。

詳しいことは、やんちゃバーで話を聞くことができました。 しかし残念なことに、聞いた内容は、僕の脳みその中のアルコールで蒸留されました。

後日、Boothで販売されていたマイコンも購入しました。

ライトニングトーク

今年は銅鑼が一杯聞けて良かったです。 去年の懸田さんのプチ炎上したメッセージは伝わっていたみたいです。

LT発表者の方々

懇親会

移動は最初徒歩で進んでいたのですが、途中から市電が空いてきたため、市電に乗って駅前まで移動し、そこから歩いてホテルに向かいました。

Bashさん、高橋さん、nagachikaさんと話した記憶はあります。 翌日に発表があるので、ここで宿に帰りました。

発表資料修正

オチで悩んでいました。 そもそもこの時点で、デモの案はありましたが動いていませんでした。 代わりにひねったオチを用意してありました。 ただしRubyKaigiでコードやデモ見せないで口先だけでなんかいうのかっこ悪すぎるんですよね。

デモをつくってみました。 意外とちゃんと動いたので、オチは「乞うご期待」とシンプルに終わらせることにしました。

完成してみると、自分は思っていたよりピリピリしてたなと気がつきました。

やんちゃハウスとやんちゃバーという拠点

今回の滞在でかなり重要だったのが、やんちゃハウスの存在でした。

函館アリーナのすぐ近くにあり、徒歩ですぐに行き来できる距離にあったため、イベントの合間に戻ることができました。 これが想像以上に便利で、セッションの合間に休憩したり、お風呂に入ったり、ちょっと横になったりすることができました。

また、やんちゃバーが毎日20時頃から深夜2時頃まで開かれていて、「常に戻れる場所がある」感がありました。

2日目

2日目の朝は、近くの日帰り温泉に入りに行きました。 このあとあわせて、3日で5回入ります。

Astroの話が気になった

The AST Galaxy to the Virtual Machine Blues - RubyKaigi 2026

笹田さん

笹田さんのAstroの発表でした。

事前コンパイルすることでツリーウォークインタプリタ方式で実装を簡単にしつつ、YJIT並の実行速度を出すアイデアでした。 AIの力で2週間で新しいアイデアを試せるのすごい時代ですよね。

なんで「事前コンパイルすることでツリーウォークインタプリタ方式で実装を簡単にしつつ、YJIT並の実行速度が出る」のかはちんぷんかんぷんでした。

Matzの発表で出てきたSpinelの話とも対応してて少し面白いです。 AstroはフルのRubyをどう簡単(?)に実装するかという話で、Spinelは割り切ったRubyサブセットの高速化を狙うもので、同じようなアプローチでも目指すゴールは違いそうです。

関さん

Programming with a DJ Controller - not vibe coding - RubyKaigi 2026

関さん

見てるときは気がつかなかったけど、エディタが同時に二つ動くの異常なんだよなあ。 「エディタがRuby製でOSの機能を通さずに直接動かしているから」なんだけど、その場では気がつかなかった。 咳マニアとして不覚です。

「ログを音楽にするのは、直感に反して多分実用的なんじゃないかなあ?」と、思いました。 ログを目grepするときって文字じゃなくて、色の濃さとか波とか見ているので、慣れれば音楽からも同じことができる気がします。

発表前の準備と初めての通し練習

五島軒のお弁当をゲットして早めに食べて発表練習をしていました。 このタイミングで、初めて30分通しのリハーサルを行いました。

デモ込みでも、大体時間丁度だったので良かったです。 本番では、早めに進んで、30秒くらい時間が余りました。

nagachikaさん

Pure Intonation on Browser: Building a Sequencer with Ruby - RubyKaigi 2026

nagachikaさん

nagachikaさんの発表も、結構印象に残っています。 音楽理論の話で、純正律と平均律の違いについて説明されていました。

普段自分たちが聞いている音楽は平均律で作られていて、これはオクターブを均等に分割したものです。均等に割ると無理数になるのでプログラマー的にはちょっと気持ち悪い数値になっています。 一方で純正律は、きれいな比率で音を作るので、理屈としてとても美しいです。

デモで ruby.wasm を使って、ブラウザのAPI経由で音を鳴らしていたのも印象的でした。 実際に音を聞いてみると、単音では逆に違和感がありました。普段から平均律に慣れているので、「あれ、なんか変だな」と感じるみたいです。 和音で聞くと綺麗に聞こえました。

ruby.wasm 経由でブラウザのWeb Audio APIが使えることで、Rubyから音楽プログラミングができるようになりました。 実際に発表で使っているのを見ると、胸が熱くなります。

しかも、あのnagachika新聞のnagachikaさんが使っているのです。

発表本番

ruby.wasm also enables JavaScript to call Ruby libraries. - RubyKaigi 2026

自分の発表を行いました。

メインテーマは「JavaScriptからRubyを呼び出す方向をがんばるぞい!」です。 裏テーマは「これまでのruby.wasm総復習」です。 裏番組が両方英語だったので、ruby.wasmに初めて触れる人も迷い込んできそうと読みました。

函館アリーナのGLAY関連展示

発表後の完全オフモード

発表が終わった後は、アフタヌーンブレイク以降のセッションには参加せず、やんちゃハウスに戻りました。 お風呂に入り横になったら、そのまま寝てしまったようです。

目覚めたら、少し時間があったので函館山周辺を散歩しました。 アリーナ周辺は平らなのに、函館山周辺だけめちゃ坂でした。

函館山の坂観光

転職ドラフトの懇親会に行った

2日目の夜は、転職ドラフト主催の懇親会に参加しました。 ビールは4杯飲みました。APA、IPA、もものヴァイツェンと黒いのを飲みました。

「RubyKaigiで発表している人として見られている」感覚が新鮮でした。 過去3年RubyKaigiで発表しているので、別に不思議なことは何もないのですが。

このあと流れでタクシーで函館山に登りました。

函館山展望台からの夜景

めちゃくちゃ寒かったです。

函館山から五稜郭へ

函館山で夜景を見たあと、そのままタクシーで五稜郭の方へ移動しました。

五稜郭のあたりで、会社の人たちと合流したら、なぜかないさるろーさんとも再会しました。 前に会ったのは沖縄でのRubyKaigiのときで、公式懇親会のあとに、麦汁さんたちと急に飲みに行きました。

それ以来だったので、久しぶりに会えて嬉しかったです。 こういうふうに、「前のRubyKaigiで会った人と、また次のRubyKaigiで会う」というのが続いていくのも、RubyKaigiの面白いところです。

今は横須賀.rb方面で活動されているらしいです。 横須賀.rbに行く理由が増えました。

このあと初やんちゃバーです。 さすがに一杯飲んで寝ました。

3日目

3日目の朝は、近くの日帰り温泉に入りに行きました。 3回目です。

Ruby Committers and the World

Ruby Committers and the World - RubyKaigi 2026

Rubyコミッターのみなさん

RubyKaigiの目玉イベントですね。 After Matz体制はみんな気になりますよね。

お昼

早めに会場を抜けて昼食はラーメンを食べました。

塩ラーメン

偶然、横須賀.rbの記念撮影現場に出くわして、ないさるろーさんに代わって撮影係をやりました。

国分さん

Lightning-Fast Method Calls with Ruby 4.1 ZJIT - RubyKaigi 2026

いまやJIT第一人者ですね。 言ってることは分かったつもりなのですが、なぜJITフレームに情報を逃がすと速くなるかはよくわかりませんでした。 保持する情報量は変わっていないように思えます。

最後の「AIで誰でもZJIT書けるようになったので、みんなもJIT書きましょう」って言ってたのが面白かったです。 RJITの時も「RUBYで誰でもRJIT書けるようになったので、みんなもJIT書きましょう」と言ってたのを思い出しました。

Matzのキーノートが良かった

Matzさん

最終日のMatzのキーノートも、今回かなり印象に残っています。

正直なところ、これまでのRubyKaigiのキーノートは、どちらかというとRubyの大枠の方針の話で技術の詳細に踏み込まない「締めの儀式」みたいな感覚で聞いていることが多かったです。 もちろん毎回面白いのですが、いわゆるテックトークとは少し違うものとして聞いていました。 今回はかなり印象が違いました。

Spinelの話が出てきて、「新しいことをやっているな」と感じました。 RubyをAOTでCにトランスパイルして、コンパイルしてから実行します。 Rubyのサブセットとして割り切って設計されているのが、mrubyの経験とかが生きているのかな?と思いました。つまり「RubyというのはサブセットであってもRubyである」という経験知がありそうです。 上手く発展すると、GoやRustが主戦場にしているCLIツール系にRubyの利用領域が広がるので夢があります。

Matzから新しいプレゼントがもらえて、自分は盛り上がりました。 あとで気がついたのですが、笹田さんのAstroとも少し繋がっていますね。

それと同時に、AIの使い方の話も印象に残っています。

Emacsで直接編集するのをやめて、Claude Codeだけでmrubyを実装している、という話をしていて、「そこまで振り切るのか」と思いました。自分だと「手で直した方が早い」と思ってしまうのですが、あえてそうしないのが、“縛りプレイ”的で面白かったです。

夜の懇親会

3日目の夜はTwoGateさんの懇親会に参加しました。 懇親会の前にお風呂に入りました。 4回目ともなると慣れた物で、RTAっぽくなってきました。

会場はモデルルームみたいなとてもおしゃれな一戸建ての家で、出張シェフの方がその場で肉を焼いて提供してくれるスタイルでした。 豚肉や鹿肉をいただきながら、ワインを飲みつつ、AOTコンパイラの話などで盛り上がりました。

やんちゃバー

その後、やんちゃバーに戻り、katsuyoshiさんとSpinelのデバッグなどをしていました。

Open class definitions emit duplicate C definitions · Issue #5 · matz/spinel · GitHub

記憶にないのですが、記録によるとやんちゃバーでバグ報告までやったみたいです。 バグ報告としては最速だったみたいです。

翌日にはMatzに直してもらえました。 SpinelにIssueを出すと、Matzが(ClaudeCodeをつかって)直してくれます。まるでMatzと会話しているような感じがあって楽しいです。

4日目

最終日は、少し観光をしてから帰ることにしました。

やんちゃハウスチェックアウトまえが最後のお風呂でした。 RTA記録はおおよそドア・ツー・ドアで20分くらいです。

そのあと、丸井今井に寄ってお土産を一通り買いました。 観光も兼ねて、五稜郭タワーに上ろうかなと思っていたのですが、これがかなり混んでいて驚きました。 金曜日までとは明らかに人の多さが違っていて、「これが花見&GW初日の威力か・・・」とおののきました。 タワーに登るのは諦めて、写真を撮りました。

五稜郭タワー

周りのお店もどこも混んでいて少し困りましたが、バス乗り場の前にあるスープカレー屋さんは空いていたので、そこでお昼を食べることにしました。

スープカレー

バスで函館空港へ向かいました。 荷物が結構増えていたので預け入れをしたのですが、そのときに航空会社の方から「座席をエコノミーに変更してもらえませんか」という相談を受けました。 もともと少し良い席を取っていたのですが、特に問題もなかったので、そのままオファーを受けることにしました。 搭乗口の前で呼び出されてチケットを変更しました。 たまに見る呼び出されている人はこういう感じなのかと、実績解除できました。

帰りは無事に羽田空港に到着して、そのままスムーズに帰宅しました。

帰宅後Spinelで遊んでバグ報告しました。

Struct definition order can produce incomplete type errors for value fields · Issue #14 · matz/spinel · GitHub

WSL2のディスクサイズを縮小する手順

WSL2のディスク(.vhdx)は動的拡張です。使っていると段々大きくなっていきます。 WSL内のファイル削除しても、Windowsからは空き領域が検知できずvhdxファイルの物理サイズは縮まりません。 exportで実データをtarに書き出す。importで再作成すると、未使用領域を除いた状態でvhdxを再作成できます。

事前準備

WSL内を掃除

sudo apt clean
sudo apt autoremove
docker system prune -a

TRIM

sudo fstrim -av

縮小手順

WSLを停止

wsl --shutdown

export

wsl --export Ubuntu D:\backup\ubuntu.tar

補足

  • pax format cannot archive sockets無視してOK(正常)
  • 実データ量だけtarになる

既存WSL削除

wsl --unregister Ubuntu

新しい保存先を作成

Microsoft StoreからインストールしたUbuntuはC:\Users\<ユーザー>\AppData\Local\Packages\...を使っています。 importするときは、使用ディレクトリを指定します。

mkdir C:\wsl\ubuntu

import

wsl --import Ubuntu C:\wsl\ubuntu D:\backup\ubuntu.tar --version 2

新しい ext4.vhdx が作られる

デフォルトユーザー復元

import後はrootになるので修正

sudo nano /etc/wsl.conf

内容:

[user]
default=ledsun
wsl --shutdown

Gem.ruby_api_versionについて

ruby.wasmの怪現象 - @ledsun blog についてrubygemsにissueつくるかー、と思って調べていたら・・・迷宮入りしました。 現状をメモしておきます。

背景

ruby.wasm では、Ruby VM 初期化直後に次のような状態で返ることがあります。

  • Gem は定義されている
  • rubygems.rb は未ロード
  • Gem.ruby_api_version / Gem.extension_api_version は未定義

WASI向けにbundle install --standalone で生成される bundler/setup.rb を使うときに問題になります。k

ruby.wasm 側ではワークアラウンドを PR #622 で入れてあります。

Bundler に立てようと思っているIssueの概要

Bundler の standalone generator は generated setup.rb の中で:

  • require "rbconfig" は行う
  • unless defined?(Gem) のときだけ Gem.ruby_api_version / Gem.extension_api_version の簡易実装を定義する

そのため、今回のように

  • Gem は defined
  • しかし RubyGems API surface は未完成

という状態だと、フォールバック定義が入らず、 Gem.ruby_api_version を前提にしているソースコードが壊れます。

関連コード: https://github.com/ruby/rubygems/blob/4758fb59f8f83695aac41e7dd1034c9042424a66/bundler/lib/bundler/installer/standalone.rb#L76

次のように個別にチェックすればいいのかな?とイメージしています。

def self.ruby_api_version
   RbConfig::CONFIG["ruby_version"]
 end unless respond_to?(:ruby_api_version)

def self.extension_api_version
  if "no" == RbConfig::CONFIG["ENABLE_SHARED"]
    "\#{ruby_api_version}-static"
  else
    ruby_api_version
  end
end unless respond_to?(:extension_api_version)

Bundlerの既存 Issue

Standalone bundle's setup.rb should implement Gem.try_activate to avoid breaking binding.irb · Issue #7545 · ruby/rubygems · GitHub が近いです。 standalone の setup.rb がpartial な Gem surface が問題を起こしています。

また、コメントで次のような設計案が出ています。

  • Gem.ruby_api_versionRbConfig.ruby_api_version に移せないか
  • Gem.extension_api_versionRbConfig 側に移せないか
  • Gem.* は将来的に deprecated にできないか

もし、これが実現すればアドホックにruby_api_versionを定義する必要が無くなります。 今回の問題の根本的な解決になりそうです。

bugs.ruby-lang.orgの既存議論

RbConfig.ruby_api_version の提案はありませんでした。

関連しそうな議論:

特に #6648 はとても長くて、議論に追いつけていません。 RbConfig.ruby_api_version を提案するなら、それなりに準備が必要そうです。

Gem.ruby_api_versionRbConfig::CONFIG["ruby_version"] の違い

Gem.ruby_api_versionGem.target_rbconfig["ruby_version"]を見ているようです。 これはクロスコンパイルと関係があるようですが、僕にはよくわかりません。

今のところ考えていること

とりあえず rubygems/rubygems にIssueを作るのは良さそうです。 あとは、せっかくRubyKaigiが近いので、RbConfigについての理解を深めておいて、誰かに直接相談したいところです。

Fiber initialization failure on WASI/ruby.wasm raises TypeError instead of FiberError

Rubyの不具合を再現するために - @ledsun blog でエラーの取り方を工夫しようと思いましたが、結局諦めました。 仕方が無いので、現状のややこしい再現コードのままバグ報告しようと思います。

というわけで英語で書いた説明の下書きです。 十分にややこしい説明になりました。 今日は見直す気力が尽きたので、後日見直します。

Summary

When Ruby is built for WASI and used through ruby.wasm, a Fiber initialization failure can surface as:

TypeError: wrong argument type false (expected Class)

instead of the expected FiberError.

This seems to happen because fiber_pool_expand() can call:

rb_raise(rb_eFiberError, ...)

during Init_Cont(), before rb_eFiberError has been initialized.

Reproduction environment

I observed this with ruby.wasm built against a Ruby revision older than:

For example, the parent revision reproduced the issue:

  • 4f7dfbe58ee2915b0724251c6464c9b4e0c34245

The issue was observed during ruby.wasm VM initialization, before running user Ruby code.

Reproduction steps

Build ruby.wasm with a Ruby revision older than eac35edfd1101e8f7c34dbdd7b595fdac8f0ad4c.

For example, create build_manifest.json at the ruby.wasm repository root:

{"ruby_revisions":{"head":"4f7dfbe58ee2915b0724251c6464c9b4e0c34245"}}

Then rebuild from a clean state:

rm -rf build rubies
rake npm:ruby-head-wasm-wasi

Add temporary instrumentation around ruby_setup() in ruby.wasm's initialization code.

In packages/gems/js/ext/js/witapi-core.c, replace ruby_init() with ruby_setup() and print rb_errinfo() when it fails:

int state = ruby_setup();
if (state) {
  fprintf(stderr, "ruby_setup state=%d\n", state);

  VALUE errinfo = rb_errinfo();
  if (!NIL_P(errinfo)) {
    VALUE klass_name = rb_class_name(CLASS_OF(errinfo));
    VALUE message = rb_funcall(errinfo, rb_intern("message"), 0);
    fprintf(stderr, "errinfo class=%s\n", StringValueCStr(klass_name));
    fprintf(stderr, "errinfo message=%s\n", StringValueCStr(message));
  }

  exit(EXIT_FAILURE);
}

The actual failure happens before user Ruby code runs, so I used temporary instrumentation around ruby_setup() to inspect rb_errinfo().

Run a minimal JS driver that only initializes the VM:

import fs from "node:fs/promises";
import { WASI } from "node:wasi";
import { RubyVM, consolePrinter } from "@ruby/wasm-wasi";

const bytes = await fs.readFile("./packages/npm-packages/ruby-head-wasm-wasi/dist/ruby.wasm");
const module = await WebAssembly.compile(bytes);

const wasi = new WASI({
  version: "preview1",
  returnOnExit: false,
  args: ["ruby.wasm"],
  env: {
    RUBY_FIBER_MACHINE_STACK_SIZE: String(1024 * 1024),
    RUBY_FIBER_VM_STACK_SIZE: String(1024 * 1024),
  },
});

const vm = new RubyVM();
const imports = { wasi_snapshot_preview1: wasi.wasiImport };
vm.addToImports(imports);

const printer = consolePrinter({
  stdout: (s) => process.stdout.write(s),
  stderr: (s) => process.stderr.write(s),
});
printer.addToImports(imports);

const instance = await WebAssembly.instantiate(module, imports);
printer.setMemory(instance.exports.memory);

await vm.setInstance(instance);
wasi.initialize(instance);
vm.initialize();

Observed result

With instrumentation around ruby_setup() in ruby.wasm, initialization fails with TAG_RAISE:

ruby_setup state=6
errinfo class=TypeError
errinfo message=wrong argument type false (expected Class)

The failure happens during vm.initialize() on the JavaScript side.

Expected result

If Fiber initialization fails, the raised exception should be FiberError, not TypeError.

Suspected cause

There seem to be two related code shapes: the revision where I reproduced the error, and current master.

In the reproduced revision

In that revision, fiber_pool_expand() could directly raise with rb_eFiberError:

https://github.com/ruby/ruby/blob/4f7dfbe58ee2915b0724251c6464c9b4e0c34245/cont.c#L557

rb_raise(rb_eFiberError, "can't set a guard page: %s", ERRNOMSG);

But this could happen while Init_Cont() was still running, before rb_eFiberError was initialized:

https://github.com/ruby/ruby/blob/4f7dfbe58ee2915b0724251c6464c9b4e0c34245/cont.c#L3387-L3409

fiber_pool_initialize(&shared_fiber_pool, stack_size, FIBER_POOL_INITIAL_SIZE, vm_stack_size);

rb_cFiber = rb_define_class("Fiber", rb_cObject);
rb_define_alloc_func(rb_cFiber, fiber_alloc);
rb_eFiberError = rb_define_class("FiberError", rb_eStandardError);

So rb_raise() received an uninitialized rb_eFiberError, which appears to explain the secondary error:

TypeError: wrong argument type false (expected Class)

On current master

On current master, fiber_pool_expand() no longer directly raises on the guard page setup failure. It now returns NULL with errno set:

https://github.com/ruby/ruby/blob/7209523ffd909ed1914f4ec2544d327a950b19d2/cont.c#L600-L607

However, the initialization order in Init_Cont() still appears to be the same:

https://github.com/ruby/ruby/blob/7209523ffd909ed1914f4ec2544d327a950b19d2/cont.c#L3656-L3678

fiber_pool_initialize() is still called before rb_eFiberError is initialized.

So if fiber_pool_initialize() fails during Init_Cont(), it may still raise with rb_eFiberError before rb_eFiberError is initialized.

Relation to eac35edf

This was exposed by a WASI-specific guard page setup failure. That specific failure path was later avoided by:

However, I think the observed TypeError is a separate secondary issue. It seems to be caused by fiber_pool_expand() raising with rb_eFiberError before rb_eFiberError is initialized in Init_Cont().

So even though eac35edf avoids the specific WASI mprotect(PROT_NONE) failure, the initialization-order issue may still be worth fixing independently.

Rubyの不具合を再現するために

ruby.wasmのデバッグ - @ledsun blog で見つけたRuby側の不具合を再現するコードを作成中です。

まず、Rubyの初期化に失敗するコミットを指定してruby.wasmをビルドします。 build_manifest.jsonを作ります。

{"ruby_revisions":{"head":"4f7dfbe58ee2915b0724251c6464c9b4e0c34245"}}

ruby.wasmをビルドします。

rake npm:ruby-head-wasm-wasi

次に、Node.jsからruby.wasmを初期化するとエラーを再現できそうです。

import fs from "node:fs/promises";
import { WASI } from "node:wasi";
import { RubyVM, consolePrinter } from "@ruby/wasm-wasi";

const bytes = await fs.readFile("./packages/npm-packages/ruby-head-wasm-wasi/dist/ruby.debug+stdlib.wasm");
const module = await WebAssembly.compile(bytes);
const wasi = new WASI({
  version: "preview1",
  returnOnExit: false,
  args: ["ruby.wasm"],
  env: {
    RUBY_FIBER_MACHINE_STACK_SIZE: String(1024 * 1024),
    RUBY_FIBER_VM_STACK_SIZE: String(1024 * 1024),
  },
});

const vm = new RubyVM();
const imports = {
  wasi_snapshot_preview1: wasi.wasiImport,
};
vm.addToImports(imports);

const printer = consolePrinter({
  stderr: (s) => process.stderr.write(`[ruby stderr] ${s}`),
});
printer.addToImports(imports);

const instance = await WebAssembly.instantiate(module, imports);
printer.setMemory(instance.exports.memory);
await vm.setInstance(instance);
wasi.initialize(instance);

vm.initialize();

こんな感じです。

►node repro.mjs
(node:183986) ExperimentalWarning: WASI is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
[ruby stderr] ruby_setup state=6
[ruby stderr] errinfo class=TypeError
[ruby stderr] errinfo message=wrong argument type false (expected Class)

エラーが起きることは期待通りです。 「ここで出るエラーがTypeErrorじゃなくてFiberErrorになってほしい」が、私が主張したいRubyの不具合です。

まだ、ちょっと問題があります。 https://github.com/ruby/ruby.wasm/blob/fb4a4e89184f306a4d1d802d0ac60e5b6c3cd136/packages/gems/js/ext/js/witapi-core.c#L205 に次のパッチを当てないといけません。

  int state = ruby_setup();
  if (state) {
    VALUE errinfo = rb_errinfo();
    fprintf(stderr, "ruby_setup state=%d\n", state);
    if (!NIL_P(errinfo)) {
      VALUE klass_name = rb_class_name(CLASS_OF(errinfo));
      VALUE message = rb_funcall(errinfo, rb_intern("message"), 0);
      fprintf(stderr, "errinfo class=%s\n", StringValueCStr(klass_name));
      fprintf(stderr, "errinfo message=%s\n", StringValueCStr(message));
    }
    exit(EXIT_FAILURE);
  }

元ネタはRubyのソースコードにあります。 https://github.com/ruby/ruby/blob/d95afb737e55dca56851ec5933637d217deec805/eval.c#L98-L108

ruby_init(void)
{
    int state = ruby_setup();
    if (state) {
        if (RTEST(ruby_debug)) {
            rb_execution_context_t *ec = GET_EC();
            rb_ec_error_print(ec, ec->errinfo);
        }
        exit(EXIT_FAILURE);
    }
}

そもそもデバッグ情報を出力してそうです。 でも、上手く出力できないんですよ。 ここをクリアして、スパッと再現コード作りたいなーって頑張っているところです。

fish shell 4を入れる

Release fish 4.6.0 · fish-shell/fish-shell · GitHub がリリースされています。 手元の環境を見ると

►fish --version
fish, version 3.7.1

随分古いです。 WSLにfish shellをいれる - @ledsun blogみると、aptのリポジトリに3系を指定していました。

fish shell - 4.x release series : “Fish shell maintainers” team の情報を元に4系リポジトリを追加しました。

sudo add-apt-repository ppa:fish-shell/release-4
sudo apt update
sudo apt upgrade

無事4.5になりました。

►fish --version
fish, version 4.5.0

4.6にならないのはリリースが1時間前だからだと思います。

fish-shell 4.6.0のリリースが1時間前であることを照明するスクリーンショット

ruby.wasmのデバッグ

ruby.wasmでテストが落ちる現象を発見しIssueを報告しました。

test:unit fails with UnhandledPromiseRejection "Symbol(kExitCode)" on Node.js 20/22/24 · Issue #628 · ruby/ruby.wasm · GitHub

それから、Codex CLIとデバッグをしました。 状況が複雑過ぎて、僕の理解可能なコンテキストを超えています。 一度、ここにまとめます。

というか、Codex CLIにまとめてもらいました。

現状はこう整理できます。

最初に見えていたのは Node 側のUnhandledPromiseRejection ... "Symbol(kExitCode)"でした。これは Node WASI の returnOnExit: true による sentinel で、Ruby 側の exitproc_exit がそのまま Symbol(kExitCode) として JS 側へ飛んできていただけでした。packages/npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs 側でこれを吸収するようにした結果、ノイズな UnhandledPromiseRejection は消え、単純な exit code だけが見えるようになりました。

そこから追うと、test-unit 実行以前に vm.initialize() の段階で Ruby 本体が落ちていました。さらに掘ると ruby_setup()TAG_RAISE で失敗していて、当初見えていた例外はTypeError: wrong argument type false (expected Class)でした。これは本当の原因ではなく、Ruby 本体の Init_Contrb_eFiberError を定義する前に rb_raise(rb_eFiberError, ...) していたため、未初期化の rb_eFiberErrorfalse 扱いされて壊れた例外になっていた、という二次バグでした。

Ruby 本体側に診断を入れて追った結果、失敗地点は build/checkouts/head/cont.cInit_Cont 内、fiber_pool_initialize(...) から呼ばれるfiber_pool_expand(...) だと分かりました。そこでは fiber stack 用 guard page を張るために mprotect(page, RB_PAGE_SIZE, PROT_NONE) を使っていますが、Node 上の wasip1 実行ではここが失敗していました。rb_eFiberError の代わりに一時的に rb_eRuntimeError で投げるようにすると、本来のエラーが見えました。

最終的に見えている実際の失敗はこれです。RuntimeError: can't set a guard page: Invalid argument

要するに、問題は 2 層あります。

  • 本質的な失敗 wasip1 上で fiber guard page 用の mprotect(PROT_NONE) が成立せず、Ruby 初期化が Init_Cont で止まる
  • Ruby 本体の別バグ その失敗を c未初期化のまま raise していたため、最初は TypeError: wrong argument type false (expected Class) に化けていた

なので、Symbol(kExitCode) は根本原因ではなく、最初に表面に出ていた Node 側の見え方でした。実際の本丸は Ruby 本体 cont.c の fiber guard page 初期化失敗です。

次の対応方針は自然に 2 つです。

  • ruby.wasm 側では downstream patch で wasm32-wasi の guard page 処理を無効化するか、mprotect 失敗を許容して起動できるようにする
  • Ruby upstream 側には
    1. wasm32-wasi で guard page 前提が破綻している件
    2. rb_eFiberError 未初期化で壊れた例外を出す件 の両方を報告する

問題を整理します。

問題1

Codexは見落としているけど、「Node WASIでruby.wasmを動かしたときにRuby内でexitするとJS側でSymbol(kExitCode)が飛ぶ」があります。 https://github.com/nodejs/node/blob/052aec7127f40f8b70fc185bf6091f10a259d376/lib/wasi.js#L102 の動作をruby.wasm側で見落としていそうです。

問題2

「wasip1 上で fiber guard page 用の mprotect(PROT_NONE) が成立しない」 これは僕にとって一番難解です。 https://github.com/ruby/ruby/blob/d95afb737e55dca56851ec5933637d217deec805/cont.c#L559の分岐がwasip1の時に漏れているのでしょうか? Codexとパッチを書きながら、実際に動かして理解を深めていくと良さそうです。

問題3

「rb_eFiberError 未初期化で壊れた例外を出す」はhttps://github.com/ruby/ruby/blob/d95afb737e55dca56851ec5933637d217deec805/cont.c#L565 に到達したときにrb_eFiberErrorが未初期化という、そのまんまではないかと思います。 再現コードを作ってhttps://bugs.ruby-lang.org/に、報告するのが良さそうです。

https://github.com/ruby/ruby/blob/d95afb737e55dca56851ec5933637d217deec805/cont.c#L3502 でFiberの初期化に入るけど、rb_eFiberErrorの初期化はhttps://github.com/ruby/ruby/blob/d95afb737e55dca56851ec5933637d217deec805/cont.c#L3524」という単純な話に思えます。

20260322追記

問題2の原因は僕がPCに残っていた、古いRubyのソースコードを使っていることでした。

ruby.wasmの怪現象

ruby.wasmでは、CRubyと同様にRubyのVMを初期化し、その上でRubyスクリプトを動かします。

CRubyでRubyのVMを初期化が完了すると定数Gemが定義されてかつrubygems.rbが読み込まれています。 Ruby WASM上でRuby VMを初期化すると、Gemは定義されているのに、rubygems.rbは読み込まれていない状態で返ってくることがあります*1

通常は、問題になりません。 Bundlerをスタンドアローン形式でインストールした際、つまりbundle install --standaloneで作成されるbundler/setup.rbコマンドを使うと問題になります。

ruby.wasmではWASIのコンポーネントモデルを使う場合に、Bundlerをスタンドアローン形式で使っています。 このとき問題が起きます。

理由は以下の通りです:

  1. bundler/setup.rbのフォールバック処理において、Gemが定義されているかどうかをチェックしている
  2. rubygems.rbで定義されているメソッドが存在するかどうかは見ていない

ソースコードは https://github.com/ruby/rubygems/blob/884d80fd158fead18bb4f021796a2af062a45ac9/bundler/lib/bundler/installer/standalone.rb#L76 です。

ruby.wasm側は https://github.com/ruby/ruby.wasm/pull/622 でパッチを当てて修正しました。 Bundler側にも修正を入れた方がよい気もします。一方ruby.wasm特有の問題な気もします。 Bundler側に説明 or 相談するとして、どう説明したものかわかりません。

考えをまとめるために、とりあえずこの文章を書いています。

*1:私にはなぜこれが起きるのかわかっていません

Ubuntuのrustupをsnapをやめる

AIエージェントが動くサンドボックスから、snapアプリケーションを動かすのは制約があるみたいです。 CUIアプリケーションのインストールにはsnapは使わない方が良さそうです。 rustをsanpで入れていたので、入れ直します。

アンインストール

sudo snap remove rustup

再インストール

curl https://sh.rustup.rs -sSf | sh

1) Proceed with installation (default)

を選択。

確認

►which rustc
/home/ledsun/.cargo/bin/rustc

調査中: ruby.wasm CIでのBundler standalone の Gem.extension_api_version 未定義問題

背景

ruby.wasmのCIのrake npm:ruby-head-wasm-wasip2:checkで次のエラーが発生しました。

undefined method `extension_api_version' for Gem:Module

ログ: https://github.com/ruby/ruby.wasm/actions/runs/22755141423/job/65997955482#step:16:146

この問題は https://github.com/ruby/ruby.wasm/pull/622 で回避できています。 この修正では、Bundler側が生成するコードとほぼ同じパッチを自前で入れています。

        if defined?(Gem)
          module Gem
            def self.ruby_api_version
              RbConfig::CONFIG["ruby_version"]
            end unless respond_to?(:ruby_api_version)

            def self.extension_api_version
              if "no" == RbConfig::CONFIG["ENABLE_SHARED"]
                "\#{ruby_api_version}-static"
              else
                ruby_api_version
              end
            end unless respond_to?(:extension_api_version)
          end
        end

パッチ適用条件を「Gemが定義されている」に、変えています。

このパッチをつくった時点ではBundler側の実装は確認していません。 Bundler側に修正の余地があるかもしれません。 あらためてBundlerの実装をみて原因を絞り込んでいきます。

調査

bundle install --standalone で生成される setup.rb

bundle install --standalone で生成される setup.rb は、
Gem 定数が未定義のときだけ RubyGems 互換 API を定義します。

対象コード: https://github.com/ruby/rubygems/blob/884d80fd158fead18bb4f021796a2af062a45ac9/bundler/lib/bundler/installer/standalone.rb#L76-L94

        unless defined?(Gem)
          module Gem
            def self.ruby_api_version
              RbConfig::CONFIG["ruby_version"]
            end

            def self.extension_api_version
              if 'no' == RbConfig::CONFIG['ENABLE_SHARED']
                "#{ruby_api_version}-static"
              else
                ruby_api_version
              end
            end
          end
        end

適用条件が「Gemが定義されていない」です。

Gem.ruby_api_version / Gem.extension_api_version の定義場所

これらは RubyGems の Gem モジュールに定義されています。

例:

つまり 通常の RubyGems 環境では必ず存在する API です。 「Gemが定義されているがGem.extension_api_versionが定義されていない」状況は考えにくそうです。 Bundlerが生成するコードの適用条件が「Gemが定義されていない」なのも妥当に思えます。

ただ、実際には起きています。 なぜでしょう?

仮説

rake npm:ruby-head-wasm-wasip2:check で、次の状態が発生しているのでしょうか?

  1. Gem 定数が定義されている
  2. lib/rubygems.rb が読み込まれていない
  3. bundle install --standalonesetup.rb を実行