Skip to content

実行可能サンプル(RUN)を Web Worker 化して停止・タイムアウトに対応#240

Merged
znz merged 5 commits into
rurema:masterfrom
znz:run-web-worker
Jul 13, 2026
Merged

実行可能サンプル(RUN)を Web Worker 化して停止・タイムアウトに対応#240
znz merged 5 commits into
rurema:masterfrom
znz:run-web-worker

Conversation

@znz

@znz znz commented Jul 13, 2026

Copy link
Copy Markdown
Member

概要

RUN ボタンの実行を、ページのメインスレッドでの同期 vm.eval から Web Worker へ移します(fix #239)。無限ループや長い sleep を含むサンプルでタブの UI が完全に固まる問題を解消し、STOP ボタンでの中断・30 秒タイムアウト・出力の逐次表示を追加します。

あわせて、レビューでのブラウザ確認を受けてサンプルブロックの UI を再設計しました。RUN/COPY ボタンは pre の中(float)から pre の外のヘッダー行に移り、「例」の caption や新設の「実行結果」タブと同じ行に並びます。

変更内容

実行の Web Worker 化

  • 新規 theme/default/js/run-worker.js(module Worker)を追加。1 回の実行につき 1 Worker を生成し、完了または中断で破棄します。VM は Worker 内で毎回作り直すため、実行同士の状態リークはありません
  • wasm のコンパイルはこれまでどおりメインスレッド側で一度だけ行ってキャッシュし(createOnceLoader)、WebAssembly.Module を構造化クローンで Worker へ postMessage します。STOP・タイムアウト後の再実行でもコンパイルのやり直しは不要です
  • 実行中は RUN ボタンが STOP に変わり、クリックで Worker.terminate() して中断できます。30 秒(RUN_TIMEOUT_MS)で自動 terminate します
  • run-worker.js.mjs ではなく .js 命名です(Rename js/run.mjs to js/run.js to survive strict MIME checking #217 と同じ MIME 判定の理由)

出力の逐次表示と注記

  • 旧実装の「$stdoutStringIO に溜めて eval 後に一括読み出し」をやめ、Worker 内の PRELUDE が書き込みのたびに JS.global.call(:postOutput, text) でメインスレッドへ送ります。出力し続けるループで wasm メモリ内の文字列が膨らみ続ける問題も解消します
  • $stdout/$stderr に入る JSStreamIOwrite/flush だけでなく putsprintprintfputc<<sync/sync=tty? も実装しています。Kernel#puts/p#write フォールバックで動きますが、マニュアルのサンプルには $stderr.puts などの直接呼び出しが多数あるためです(旧 StringIO はこれらを備えていました)
  • 表示は 64KB で打ち切り(accumulateOutput)。打ち切り後のチャンクは即捨てるので暴走出力でも O(1) です
  • 終わり方は必ず注記でわかるようにしました: 正常終了 (正常終了しました)・STOP (停止しました)・タイムアウト (30秒でタイムアウトしました)・例外はエラーメッセージ(赤字)。これらの注記は 1 回きりで長さが有界なので、64KB 打ち切り済みでも必ず末尾に表示されます(出力ゼロのサンプルでも完了がわかります)

編集まわり

  • 編集開始時のプレーンテキスト化で、コンパイラが <code> 直後に置く改行も除去します(editableText。編集開始時に先頭の空行として見えていたもの)
  • 改行入力・貼り付けが <br>/<div> にならないよう contentEditable = 'plaintext-only' を優先し(Chrome/Safari/Firefox 136+)、未対応ブラウザは従来の true + paste イベントで text/plain をキャレット位置に挿入します。貼り付けた内容が前の行にくっつく問題の解消です
  • pre 自身のクリック(padding 部分や 1 行サンプルの下の余白)でも code にフォーカスして末尾にキャレットを置くので、pre 内のどこをクリックしても編集に入れます。Ctrl/Cmd+Enter での再実行は従来どおりです

サンプルブロックの UI 再設計(レビュー指摘反映)

  • script.js が全 pre の直前にヘッダー行 <div class="highlight__toolbar"> を生成し、COPY(と run.js の RUN)をその右端のボタン置き場に入れます。pre 内への float 配置は、カーソル形状のちらつき・編集時のレイアウト崩れ・pre の上 padding を 0 にするハックなど無理が多いためやめました
  • ヘッダー行はコード部分よりひと段暗い #e6e6e6 でコードブロックと一体に見せます。caption(「例」)はコード部分と同じ #f2f2f2・上だけ角丸のタブとしてヘッダー左端に取り込み、RUN/COPY は白背景+枠線+角丸のボタンにして埋没を防いでいます
  • pre の padding は四辺均等の 1.1em に戻しました(上だけ 0 のハック廃止。中身が枠の上端に張り付く問題の解消)。ヘッダー行直後の pre は上 padding を 0.25em に抑えます
  • RUN の出力欄は「2px の白い境」+「実行結果」タブ付きヘッダー+出力 pre の構成で、入力欄に密着した 1 ブロックに見せます(highlight--with-output/highlight__toolbar--attached)
  • ツールバーは JS 生成なので、静的 HTML やコンパイラ出力に変更はありません

ビルド・型

  • lib/bitclust/subcommands/statichtml_command.rbcopy_run_ruby_wasm_scriptRUN_RUBY_WASM_JS_FILES = %w[run.js run-worker.js] の一覧コピーに拡張しました。この一覧への追加漏れは RUN 機能が本番で無言で壊れる原因になるため要注意です。片方だけ欠けたテーマも警告してスキップします
  • sig/bitclust/subcommands/statichtml_command.rbs に型を追加

実装メモ

  • worker URL の解決: new URL('run-worker.js', import.meta.url)run.js 自身の URL から解決します。ページ階層や custom_js_url() の実装に依存せず、テンプレートは無変更です(URL が無い QuickJS でのテスト実行を妨げないよう遅延評価)
  • プロトコル: main→worker { module, code }、worker→main { type: 'output', text }(0 回以上)→ { type: 'done' } または { type: 'error', message }(必ず最後に 1 回)。STOP/タイムアウトは main 側が terminate() するだけです
  • Ruby→JS の受け渡し: JSStreamIO#write は worker 内の小さなヘルパー postOutput() を呼び、{ type: ... } の封筒詰めは run-worker.js の 1 箇所に閉じています
  • ライフサイクル: 旧実装も VM は実行ごとに使い捨てだったため、Worker 単位に持ち替えても意味論上の後退はありません。ボタン状態はブロックごとの current で管理し、done/error/STOP/タイムアウトのどの経路も同じ finish() に収束させています

検証

  • qjs: test/js/test_run.mjs(純粋関数: accumulateOutput・各注記・editableTextPRELUDE の形・formatRunError)、test/js/test_script.mjs(ヘッダー行の構造・caption の取り込み・ruremaAddCopyButton フック・COPY の動作)全て pass
  • Ruby: 新規 test/test_run_worker_prelude.rbPRELUDE を実 Ruby(JS ブリッジをスタブ)で eval し、Kernel#puts/p のフォールバック経路と $stderr.puts 等の直接呼び出しの両方が旧 StringIO と同じ出力になることを検証。test_statichtml_command.rb は 2 ファイルコピー+片方欠けテーマのテストを追加。フルスイート ruby test/run_test.rb 17596 tests 100% pass
  • ブラウザ(レビューで確認済み): RUN→STOP→再実行、無限ループの 30 秒タイムアウト(タブは固まらない)、暴走出力の 64KB 打ち切りと打ち切り後の注記表示、例外時の赤字+RETRY、ヘッダー行・タブ・ボタンの見た目
  • wasm 実行の実挙動(CDN の @ruby/wasm-wasi を module Worker 内で dynamic import する部分を含む)はレビューでのブラウザ確認に依っています。ローカルでの再現手順:
bundle exec bitclust --database=/tmp/db-3.4 statichtml \
  --outputdir=/tmp/run-html --templatedir=data/bitclust/template.offline \
  --catalog=data/bitclust/catalog \
  --run-ruby-wasm='https://cdn.jsdelivr.net/npm/@ruby/3.4-wasm-wasi@2.9.3-2.9.4/dist/ruby+stdlib.wasm'
ruby -run -e httpd /tmp/run-html -p 8000

fix #239

🤖 Generated with Claude Code

znz and others added 2 commits July 13, 2026 10:48
RUN ボタンは ruby.wasm をメインスレッドで同期実行しており、無限ループや
長い sleep を含むサンプルではタブの UI が完全に固まっていた(fix rurema#239)。

- 実行を新設の theme/default/js/run-worker.js (module Worker) に移動。
  wasm のコンパイルはこれまでどおりメインスレッド側で一度だけ行いキャッシュ
  し、WebAssembly.Module を実行ごとに使い捨ての Worker へ postMessage で
  渡す。VM は実行のたびに作り直すので、terminate() 後の再実行やコンパイル
  やり直しの問題は生じない
- 実行中は RUN ボタンを STOP に切り替え、クリックで Worker.terminate()
  して中断できるようにした。30 秒(RUN_TIMEOUT_MS)経過でも自動的に
  terminate し、出力欄にその旨を追記する
- $stdout/$stderr を StringIO に溜めて eval 完了後に一括取得していたのを
  やめ、Worker 内で書き込みのたびに JS.global.call(:postOutput, text) で
  メインスレッドへ逐次 postMessage するようにした。表示側の 64KB 打ち切り
  (旧 truncateOutput 相当)は accumulateOutput として維持しつつ、打ち切り
  後の追加チャンクは即座に捨てるようにして無限出力ループでも O(1) で済む
  ようにした
- Worker スクリプトの URL は new URL('run-worker.js', import.meta.url) で
  run.js 自身の URL から解決し、ページの階層やテンプレート側の
  custom_js_url() の実装に依存しないようにした
- run-worker.js も run.js 同様に .mjs ではなく .js 命名にした(rurema#217 と同じ
  MIME 判定の理由)。statichtml のテーマファイルコピー処理
  (copy_run_ruby_wasm_script)を RUN_RUBY_WASM_JS_FILES 経由で2ファイルとも
  コピーするよう拡張し、片方だけ欠けているテーマも許容する
- test/js/test_run.mjs に純粋関数のテスト(accumulateOutput・
  STOPPED_NOTE・timeoutNote・run-worker.js の PRELUDE/formatRunError)を
  追加。Worker/DOM は QuickJS では扱えないため、テストはこれまでどおり
  純粋関数部分のみを対象にしている

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- RUN/COPY を1つの float コンテナ(.highlight__button-group)にまとめ、
  ボタン間・周囲の余白で pointer と text のカーソル形状がちらつかない
  ようにする(隙間のカーソルと選択不可はコンテナが引き受ける)
- 編集可能にしたときに先頭へ空行が入るのを解消。コンパイラが <code>
  直後に置く改行をプレーンテキスト化時に除去(editableText)し、編集時の
  code を inline-block + width:100%(float と同じ行に入れずボタンの下に
  落ちる)から block に変更
- 貼り付けで改行が <br>/<div> になって前の行にくっつくのを解消。
  contentEditable=plaintext-only を優先し、未対応ブラウザは paste で
  text/plain をキャレット位置に挿入するフォールバック
@znz

znz commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

Claude Code へのレビュー結果入力:

#240 の以下を確認しました。

  • RUN → 実行中に STOP → 出力欄に (停止しました) → RUN で再実行できる
  • 無限ループのサンプル(例: loop { })で 30 秒後に自動 terminate され
    (30秒でタイムアウトしました) が出る。その間タブの他の操作が固まらない
  • loop { puts "x"; }のような出力し続けるサンプルで、64KB 到達後に... (truncated)` が 1 回だけ表示され、その後は静かに打ち切られる
    (メモリ使用量が張り付いたままにならない)。この状態で STOP/タイムアウト
    した場合も、打ち切り済み出力の末尾に注記が表示される
  • 例外を投げるサンプルは従来どおりエラー表示(赤字・RETRY)になる

以下の問題が気になりました。

  • 実行可能な例の RUN ボタンや COPY ボタンでマウスカーソルの形状がクリック可能と入力欄でちらついてクリックしにくい
  • 編集可能になったときに最初に空行が入っている
  • 出力欄や入力欄に貼り付けたときの内容が上にくっついている

RUN や COPY のボタンは調整が難しければ pre の外の方が良いかもしれません。

以下、Claude Code によるコメント:

ブラウザでの確認と指摘ありがとうございます。指摘の3点に対応するコミットを追加しました。

  • カーソル形状のちらつき: RUN/COPY を個別に float: right させていたため、ボタン間・周囲の余白が pre のテキストカーソル領域のままで、pointertext がちらついていました。両ボタンを1つの float コンテナ .highlight__button-group(script.js が生成し、run.js も同じものに RUN を入れる)にまとめ、隙間のカーソル形状(cursor: default)と選択不可はコンテナが引き受けるようにしました。
  • 編集可能時の先頭の空行: 原因は2つの複合でした。①コンパイラが <code> の直後に改行を出力していて code.textContent"\n" で始まる(COPY は以前から同じ理由で先頭改行を strip 済み)②編集時の display: inline-block; width: 100% が float のボタン群と同じ行に入れず、ボタンの下へ押し下げられる。①はプレーンテキスト化時に先頭の改行を除去(editableText、qjs テスト追加)、②は display: block へ変更(block なら行ボックスが float を避けるので、通常表示と同じ回り込みになります)で解消しました。
  • 貼り付けた内容が上の行にくっつく: contenteditable の既定のリッチペーストが改行を <br>/<div> に変えたり先頭の改行を落としたりするためでした。contentEditable = 'plaintext-only' を優先し(Chrome/Safari/Firefox 136+)、未対応ブラウザでは従来どおり true にして paste イベントで text/plain をキャレット位置に挿入するフォールバックを入れました。plaintext-only だと Enter の改行も \n になるため、再実行時の code.textContent も編集内容と一致します。

「ボタンを pre の外に出す」案も検討しましたが、COPY は全ページの全 pre(RUN 対象外も含む)に付くため、外出しはサイト全体のレイアウト変更になります。まずは上記で解消を狙い、それでもちらつき等が残るようなら caption(pre 上部のタブ)と同様の外置きツールバー化を別 PR で検討させてください。

検証: qjs の2スイート(test_run.mjs / test_script.mjs、グループ構造と editableText のテストを追加)とフルスイート ruby test/run_test.rb(17596 tests)が全て pass。ブラウザでの再確認をお願いしたい観点は以下です。

  • ボタン群にマウスを載せたとき、ボタン間の隙間でもカーソルがちらつかない
  • 初回 RUN 後の編集開始時に先頭に空行が入らない(コードがボタンの左に回り込む)
  • 複数行テキストの貼り付けで改行が保持され、前の行にくっつかない(Ctrl+Enter での再実行も貼り付けどおりに動く)
  • COPY の位置・動作が従来どおり(全 pre で右上、クリックで COPIED 表示)

pre 内への float 配置は、カーソル形状のちらつき・編集時のレイアウト
崩れ・pre の上 padding を 0 にするハックなど無理が多かったため、
pre の直前のツールバー行 <div class="highlight__toolbar"> に移す。

- script.js: 全 pre の直前にツールバー行を生成し、右端のボタン置き場に
  COPY を入れる。コンパイラが pre の直前に置く caption(タブ)があれば
  ツールバー左端に取り込み、pre への密着を保つ
- run.js: RUN ボタンも同じボタン置き場の先頭に入れる。RUN の出力欄は
  highlight--with-output / highlight__toolbar--attached で入力欄の pre に
  密着させて1ブロックに見せる
- pre の上 padding を復活(1.1em)。出力・編集テキストが枠の上端に
  張り付いていたのはこれが 0 だったため
- 編集時に pre の padding 部分(1行サンプルの下半分など)をクリック
  しても入力欄に入れるよう、pre へのクリックで code にフォーカスして
  末尾にキャレットを置く
- RUN/COPY ボタンは角丸(4px)でボタンらしく、caption は上だけ角丸
  (6px)でタブらしく
@znz

znz commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

Claude Code へのレビュー結果入力:

出力や編集可能なテキストが上にくっついているのは変化がありません。1行しかないサンプルコードで下半分でのクリックでは入力欄に入れなくなって不便になっています。
pre 中への追加は無理している部分が多そうなので、外に移動してください。
ボタンはもっとボタンっぽく見えるように角丸にしてください。
caption はタブっぽく見えるように上だけ角丸にしてください。

Claude Code によるコメント:

再確認ありがとうございます。ご提案どおり、RUN/COPY ボタンを pre の外に移動するコミットを追加しました。

  • ボタンの外出し: 全 pre の直前にツールバー行 <div class="highlight__toolbar"> を script.js が生成し、右端のボタン置き場に COPY(と run.js の RUN)を入れる構成にしました。pre 内への float 配置に伴う無理(カーソルちらつき・編集時のレイアウト崩れ・上 padding 0 のハック)をまとめて解消しています
  • 「出力や編集テキストが上にくっつく」: 正体は pre の上 padding が 0 だったことでした(ボタンを pre 内の上端に密着させるためのハック)。外出しに伴い padding: 1.1em の四辺均等に戻したので、入力欄・出力欄とも中身が枠の上端に張り付かなくなります
  • 1行サンプルの下半分をクリックしても入力欄に入れない件: 編集時に pre 自身へのクリックで code にフォーカスし、末尾にキャレットを置くようにしました(code の外側= padding 部分のクリックでも入力欄に入れます)
  • ボタンの角丸: RUN/COPY とも border-radius: 4px でボタンらしく
  • caption のタブ化: 上だけ角丸(border-radius: 6px 6px 0 0)。コンパイラが pre の直前に置く caption はツールバー行の左端に取り込み、pre への密着を保っています(左に caption タブ・右にボタン群の1行)
  • RUN の出力欄は highlight--with-output/highlight__toolbar--attached で入力欄の pre に密着させ、従来どおり1つのブロックに見えるようにしています

検証: qjs 2スイート(ツールバー構造・caption 取り込み・フックのテストを追加、計23+21件)とフルスイート ruby test/run_test.rb が全て pass。ツールバーは JS 生成なので静的 HTML・Ruby 側の変更はありません。

ブラウザでの再確認をお願いしたい観点:

  • 入力欄・出力欄とも中身が上端に張り付いていない(上 padding がある)
  • ボタン行が pre の右上外側にあり、カーソルがちらつかない
  • 1行サンプルでも pre 内のどこをクリックしても編集に入れる
  • caption 付きサンプルで「左にタブ・右にボタン」の1行になっており、タブが pre に密着している
  • 出力欄が入力欄に密着して1ブロックに見える(間に隙間ができていない)
  • COPY が全 pre(RUN 対象外の言語含む)で従来どおり動く

- ツールバー行が白い帯として浮き、コードの上の隙間が広く見えていた
  ため、pre と同じ背景色を与えてコードブロックと一体のヘッダー行に
  する。ヘッダーが上の余白を兼ねるので、直後の pre の上 padding は
  0.25em に抑える
- RUN の出力欄のヘッダーが入力欄の枠に貼り付いて見えていたため、
  境に 2px の白を挟んで区切る
- 出力欄だとわかるように、出力欄ヘッダーの左端へ caption と同じ
  タブ様式の「実行結果」ラベルを付ける
@znz

znz commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

Claude Code へのレビュー結果入力:

実行前のコードの上の隙間が広い気がします。
実行結果の COPY ボタンが上のコードの枠にくっついています。
出力の方は出力とわかるタブがつくと良さそうです。

Claude Code によるコメント:

確認ありがとうございます。指摘の3点に対応するコミットを追加しました。

  • コードの上の隙間が広い: ツールバー行が白い帯として浮いていたのが原因です。pre と同じ背景色を与えてコードブロックと一体のヘッダー行にしました(ブロックの開始位置は従来どおり本文から 1em)。ヘッダーが上の余白を兼ねるので、直後の pre の上 padding も 0.25em に抑えています
  • 実行結果の COPY ボタンが上のコードの枠にくっつく: 入力欄と出力欄ヘッダーの境に 2px の白を挟んで区切るようにしました。ヘッダー行自体が灰色になったことで、COPY は「出力欄のヘッダーの中のボタン」として見えるようになります
  • 出力とわかるタブ: 出力欄ヘッダーの左端に、caption と同じタブ様式(上だけ角丸)の「実行結果」ラベルを付けました

検証: qjs 2スイート+フルスイート(17596 tests)全て pass。変更は CSS と run.js の出力欄生成部のみです。

ブラウザでの再確認をお願いしたい観点:

  • サンプルの見た目が「灰色ヘッダー行(右上にボタン)+コード」の一体ブロックになっており、上の隙間が広く見えない
  • caption 付きサンプルはヘッダー左端にタブ、右端にボタン
  • RUN 後は「入力欄|(2px の白)|実行結果タブ+COPY のヘッダー|出力欄」の並びで、境目が識別できる
  • RUN 対象外の言語や素の pre でも「ヘッダー+COPY」の見た目が破綻していない

- ヘッダー行・caption・ボタンがどれも似た薄い灰色で埋没していたため、
  ヘッダー行をコード部分よりひと段暗く(#e6e6e6)し、caption タブは
  コード部分と同じ色(#f2f2f2)で「下とつながったタブ」に、RUN/COPY は
  白背景+枠線(opacity 廃止)でボタンとして浮き上がらせる
- 出力が空のサンプルだと実行が終わったのかわからないため、正常終了時も
  「(正常終了しました)」を出力欄の末尾に追記する(DONE_NOTE)
@znz

znz commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

Claude Code へのレビュー結果入力:

例や RUN, COPY ボタンも薄い灰色の中に入ってしまっています。
出力がないときに実行が終わったのかどうかわかりにくいので、正常終了のときもメッセージがほしいです。

Claude Code によるコメント:

確認ありがとうございます。指摘の2点に対応するコミットを追加しました。

  • caption やボタンが薄い灰色に埋没する件: ヘッダー行・caption・ボタンがどれも似た明度の灰色だったのが原因です。3段階に分けました。
    • ヘッダー行: コード部分よりひと段暗い #e6e6e6
    • caption(「例」「実行結果」のタブ): コード部分と同じ #f2f2f2 — 下のコードと「つながったタブ」に見え、暗いヘッダーの上で浮き上がります
    • RUN/COPY ボタン: 白背景+枠線(1px solid #999)+角丸で明確にボタンに。埋没の一因だった opacity: 0.75 は廃止しました(LOADING 中の緑・COPIED の緑はそのまま)
  • 出力がないときに実行が終わったのかわからない件: 正常終了時も (正常終了しました) を出力欄の末尾に必ず追記するようにしました(DONE_NOTE)。出力ゼロのサンプルでは出力欄にこの注記だけが表示され、完了がわかります。停止・タイムアウト・エラーの注記と同じ扱い(64KB 打ち切りの対象外)です

検証: qjs 2スイート+フルスイート(17596 tests)全て pass。

ブラウザでの再確認をお願いしたい観点:

  • ヘッダー行(暗め)の上で「例」タブと RUN/COPY ボタンがはっきり見分けられる
  • 出力なしのサンプル(代入だけ等)を RUN すると「(正常終了しました)」が出る
  • 出力ありのサンプルでは出力の末尾に注記が付く(邪魔になりすぎないか、感触を見ていただけると)
  • COPIED・LOADING(緑)の表示が枠線付きボタンでも崩れていない

@znz znz merged commit 7c077fc into rurema:master Jul 13, 2026
10 checks passed
@znz znz deleted the run-web-worker branch July 13, 2026 04:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

実行可能サンプル(RUN)を Web Worker 化して停止・タイムアウトできるようにする

1 participant