デスクトップアプリケーションとして提供されている Clojure のデバッガ FlowStrom の紹介です。 日本語の記事をまったく見かけないのですが Clojure の他のデバッガより便利な点がいくつかあります。 time travel debugging という、処理のステップを精緻に全て記録し状態を行ったり来たりして確認が可能なコンセプトは Clojure の他のデバッガとは異なる特長なのではないかと思います(他のデバッガをそんなに知らないので実はできるのかもしれないですが)。
FlowStorm の特徴
1つ目は ClojureScript もサポートしていることです。 ブラウザのデバッガ使えばいいよ、というのはあります。しかし Clojure も ClojureScript も Babashka も同様のツールとやり方でデバッガを使えるのはやはり優位な点でしょう。
2つ目は 一度全ての状態遷移をトレースしたうえで各状態を行き来して確認できるです。Break Point を置いてステップ実行しながら状態を確認する方法だと「あれ、この1つ前の状態はどんなだっけ?今どう変わった?よし、もう一回最初から」みたいなことをやりがちです。また、Break Point を設定するのはバグなどの調べたい箇所のあたりが付いている場合です。FlowStorm はあたりが付いていない場合に全体を広く見ていく使い方に合います。
FlowStrom は、自身のこのようなデバッグ方式を time travel debugger と呼んでいます。この名前が一般的なのか自分は知らないですが、ググってみると Microsoft VisualStudio で ASP.NET を対象に 2019 年に機能追加されているのを発見しました。
他には Rust 用の VSCode 拡張もヒットします。
3つ目は expression and value oriented という点です。デバッガは通常ソースコードにおける line oriented なことが多いですが、 FlowStorm では式の評価1つ1つを詳細に追うことができます。これは実際に見たほうが分かりやすいポイントなので後ほど実際に説明します。
より詳細な FlowStorm の特長については README の FAQ のこの節を参照ください。 → https://github.com/flow-storm/flow-storm-debugger?tab=readme-ov-file#how-does-it-compare-to-other-clojure-debuggers
動作要件
- jdk17+
- clojure 1.11.0+
どちらもけっこう新しめのバージョンを要求されるのでご注意ください。それよりも下位のバージョンで動かす方法も載っていますが、バージョンを上げるほうが大抵は楽(とくに試すだけなら)なはずです。
下位バージョンでの動かし方はこちらから → https://github.com/flow-storm/flow-storm-debugger?tab=readme-ov-file#prerequisites
また、今回はそれ以外にそもそもの実行環境としていかが必要です。
- Clojure (i.e.
brew install clojure
on MacOS) - Babashka (i.e.
brew install babashka
on MacOS)
Tracing with Babashka Script
FlowStrom 自体は Clojure, ClojureScript で使えますが、少し工夫すると Babashka でも一応使えます。今回は手軽さを理由に Babashka の Single Script を扱います。サンプルアプリケーションとして次の TODO ウェブアプリケーションを使わせてもらいます。
Sample Web Application
次のように Babashka スクリプトをダウンロードしておいてください。
wget があれば:
wget https://raw.githubusercontent.com/babashka/babashka/master/examples/htmx_todoapp.clj
curl しかなければ:
curl -OL https://raw.githubusercontent.com/babashka/babashka/master/examples/htmx_todoapp.clj
あるいはせっかくなので Babashka でダウンロードするなら1:
bb -e '(->> (str *input*) babashka.http-client/get :body (spit (last (clojure.string/split (str *input*) #"/"))))' <<< "https://raw.githubusercontent.com/babashka/babashka/master/examples/htmx_todoapp.clj"
ダウンロードできたら次のように Babashka から起動してみてください。
bb ./htmx_todoapp.clj
localhost:3000 で TODO ウェブアプリケーションが立ち上がればOKです。
FlowStorm を使う際は Babashka から直接は起動しないため、ctrl + c
などで一度停止してしまってください。
deps.edn
つづいて deps.edn
を作成します。 bb
は組み込みの依存ライブラリ一覧を deps.edn として出力する print-deps
サブコマンドを提供しているので、これをベースにします。
bb print-deps > deps.edn
生成された deps.edn
に次の内容を追記します。
:aliases {:dev {:classpath-overrides {org.clojure/clojure nil}
:extra-deps {com.github.flow-storm/clojure {:mvn/version "RELEASE"}
com.github.flow-storm/flow-storm-dbg {:mvn/version "RELEASE"}}
:jvm-opts ["-Dclojure.storm.instrumentEnable=true"
"-Dclojure.storm.instrumentOnlyPrefixes=user"]}}
-
:classpath-overrides {org.clojure/clojure nil}
はデフォルトの Clojure コンパイラを使用しないようにする設定です。 FlowStorm は通常の Clojure コンパイラを「デバッグ機能を差し込んで改変した FlowStorm のコンパイラ」に差し替えることで機能を提供する仕組みになっています。 -
:extra-deps
はデフォルトのコンパイラの代わりに差し込まれる FlowStorm 改変版の Clojure コンパイラ と FlowStorm デバッガ本体の2つの依存を追加しています。 -
:jvm-opts
では、デバッグの有効化と有効にする範囲を名前空間の接頭辞(ここではuser
)で指定しています。
全体としては例えば次のような内容になります。依存ライブラリの構成やバージョンは Babashka のバージョンが変われば変わるかもしれません。
{:aliases {:dev {:classpath-overrides {org.clojure/clojure nil}
:extra-deps {com.github.flow-storm/clojure {:mvn/version "RELEASE"}
com.github.flow-storm/flow-storm-dbg {:mvn/version "RELEASE"}}
:jvm-opts ["-Dclojure.storm.instrumentEnable=true"
"-Dclojure.storm.instrumentOnlyPrefixes=user"]}}
:deps
{babashka/babashka.core
{:git/url "https://github.com/babashka/babashka.core",
:git/sha "52a6037bd4b632bffffb04394fb4efd0cdab6b1e"},
babashka/babashka.curl {:mvn/version "0.1.2"},
babashka/fs {:mvn/version "0.4.19"},
babashka/process {:mvn/version "0.5.21"},
cheshire/cheshire {:mvn/version "5.12.0"},
clj-commons/clj-yaml {:mvn/version "1.0.27"},
com.cognitect/transit-clj {:mvn/version "1.0.333"},
com.taoensso/timbre {:mvn/version "6.0.1"},
hiccup/hiccup {:mvn/version "2.0.0-RC1"},
http-kit/http-kit {:mvn/version "2.7.0-RC1"},
insn/insn {:mvn/version "0.5.2"},
nrepl/bencode {:mvn/version "1.1.0"},
org.babashka/babashka.impl.java {:mvn/version "0.1.8"},
org.babashka/cli {:mvn/version "0.7.53"},
org.babashka/http-client {:mvn/version "0.4.15"},
org.babashka/sci.impl.types {:mvn/version "0.0.2"},
org.clojure/clojure {:mvn/version "1.11.1"},
org.clojure/core.async {:mvn/version "1.6.673"},
org.clojure/core.match {:mvn/version "1.0.0"},
org.clojure/core.rrb-vector {:mvn/version "0.1.2"},
org.clojure/data.csv {:mvn/version "1.0.0"},
org.clojure/data.priority-map {:mvn/version "1.1.0"},
org.clojure/data.xml {:mvn/version "0.2.0-alpha8"},
org.clojure/test.check {:mvn/version "1.1.1"},
org.clojure/tools.cli {:mvn/version "1.0.214"},
org.clojure/tools.logging {:mvn/version "1.1.0"},
org.flatland/ordered {:mvn/version "1.5.9"},
rewrite-clj/rewrite-clj {:mvn/version "1.1.47"},
selmer/selmer {:mvn/version "1.12.59"}}}
clj
から起動
スクリプトは Babashka から起動されているかをチェックする処理が入っているため、そこを書き換えます。 235 行目あたりの
(when (= *file* (System/getProperty "babashka.file"))
を
(when true
のように書き換えてください。
そして、 clj -A:dev
で Clojure から、まずは Repl を起動します。
$ clj -A:dev
ClojureStorm 1.11.1-16
Evaluate the :help keyword for more info or :tut/basics for a beginners tour.
Storm functions plugged in
Storm instrumentation set to: true
Runtime index system started
user=>
続いてデバッガを起動。 Warning が出てますがとりあえず無視で。
user=> :dbg
12月 22, 2023 1:12:08 午後 com.sun.javafx.application.PlatformImpl startup
警告: Unsupported JavaFX configuration: classes were loaded from 'unnamed module @6e111aeb'
Waiting for dispatch-fn before dispatching events
user=> Runtime config retrieved :{:env-kind :clj, :storm? true, :recording? true, :total-order-recording? false, :breakpoints #{}}
こんな感じのデスクトップアプリケーションが自動で立ち上がるはずです。自分の環境では1秒くらいでスッと意外とスムーズに立ち上がります。この Flowstrom debugger は JavaFX 製です。
さらにその状態で次のように Babashka スクリプトを eval します。
(load-string (slurp "htmx_todoapp.clj"))
http://localhost:3000/ がブラウザで自動で立ち上がって TODO アプリケーションの画面がまた表示されると思います。 Flowstrom debugger のほうは worker-1
というスレッドが表示されたと思います。 worker-1
をダブルクリックするとメインのペインにトレーシングが表示されています。この画面におけるトレーシングの表示は関数の呼び出しネストした階層として見せるのでよくあるスタックトレースに近い見た目です。
横向きの ▲
をクリックして全部開いてみてください。
先のステップで、deps.edn
にて "-Dclojure.storm.instrumentOnlyPrefixes=user"
という JVM オプションを設定していたのを覚えていると思います。そのため user
名前空間のみがデバッガによってトレーシング対象となって表示されています。いま、初回の画面表示の際にはパスとしては /
に対して GET リクエストが発生した状態になっているので、このとき user
名前空間、つまり今回であれば htmx_todoapp.clj
スクリプトの中の処理としては routes
関数(リクエストの振り分け処理を行っている)が最初に通るエントリポイントとなっています。結果として、デバッガ上でもそれが一番上の行に Root として表示されています。
各行をクリックすると、画面下部の Args:
ペインや Ret:
ペインに引数や返り値の型と値が表示されます。各関数呼び出しが階層表示され値が見れるこの画面はこれだけでも十分便利です。
TODOアプリケーションの画面から TODO を追加してみます。「あいうえお」などなんでもいいので入力して Enter します。
デバッガのほうで worker-2
のスレッドが増えたはずです。それをダブルクリックして開き、今度は画面下部のタブ切り替えで ()
をクリックして Code tool
を開きます2。
Code tool
では routes
関数のソースコードが表示されています。
ピンク色の部分は、この実行において実際に評価された 値 または 式 です。
上部のボタンの羅列から ›
をクリックするとピンクのところが2行目の uri
から順にグリーンに変化していきます。式や値が評価された順にその評価結果(=中身)がどうなっているか右のペインに表示されます。
uri
シンボルにバインドされた値は String
型の "/todos" となっています。 ›
をも一回クリックすると次の式 (str/split uri #"/")
(のカッコ)がグリーン色になり、右ペインはその評価結果に切り替わります。 PersistentVector
型で ["", "todos"]
という値です。
さらに ›
をくりっくしていけば、その後の式(や値)の評価結果が順次表示されます。
‹
をクリックすれば、逆に1つずつ前の評価結果の表示に戻っていけます。 ›
でずっと先に進めていくと、 route
関数を抜けてそこから呼び出している add-item
関数、さらにそこで呼び出されている parse-body
関数と順次ソースコードが表示され式や値の評価結果を見ていくことができます。
基本的な使い方はこのような感じで以上です。説明がちょっと冗長になってしまったわりに面白い例で説明できなかった気もするのですが、ポイントとしてはブレイクポイントを使わず式や値の評価を詳細にトレースした結果全体を保持しその状態遷移を自由に行ったり来たりしながら調査していくことができるというのが伝えたいところでした。
この後は、各画面についての簡単な捕捉です。
Flows
タブ
FlowStorm の Flows
タブではスレッドごとにトレーシングが分かれて表示されます。 例えば pmap
のようなスレッドを複数作成する可能性のある処理では画面にたくさんのスレッドが表示されます。今回はサーバを起ち上げるために httpkit を使っているため、 http-kit のデフォルト設定により最大で4つのスレッドが worker-*
という名前で表示されます3。
処理が複数スレッドに振り分けられると、自分の画面操作がどこのスレッドで起きているか探すのが少し面倒になります。今回の利用でいえば、ダウンロードしたスクリプトの 237 行目あたり、
(srv/run-server #'routes {:port port})
のオプションの引数に :thread: 1
などを追加する修正をするとデバッグとしてはやりやすくなります。
また、この記事では式と値の評価を順に追うやり方した説明しなかったですが、 Power Step という機能を使うと、ある値と同一の値が次に評価されるステップまで進む(identity
)、ある値と等しい値が次に登場するステップまで進む(qualtily
)、任意の述語を満たす値が次に登場するステップまで進む、のような追い方ができます。その他に多様なジャンプの方法が提供されています。
Browser
タブ
名前空間と変数に対して、詳細な instrument とその解除を設定できます。また、関数に対して Break Point の設定も可能です。今回の場合だと例えば routes
関数かあるいはそこから呼ばれていた add-item
関数にブレイクポイントを設定することで、どのワーカースレッドからから呼ばれているのかすぐに分かって便利だったりします。
このスクショの場合 worker-3
でブレイクしていることが伺えます。
Taps
タブ
tap>
を視覚化できるそうです。使ったことないのであまりわかりません、すみません。
Docs
タブ
関数の実行をサンプリングしてドキュメントを生成できるそうです。これも使ったことないのであまりわかりません、すみません。
Timeline
タブ
スレッドを超えてグローバルな時系列での処理ステップを追うことができます。例えば、マルチスレッドの処理をデバッグするのに使えます。
Printer
タブ
コードを再実行することなく、 print をコード中に複数差し込んでコンソール出力させつつ実行を再現することができます。 print の追加自体は Flows
ツールから設定します。
その他の補足
FlowStrom とは Flowstrom debugger のコア機能の名称です。
ClojureStrom (ClojureScriptStorm) とは本家 Clojure Compiler をフォークして instrument を差し込んだものです。
FlowStrom でデバッグを行うことも可能ですが ClojureStrom (ClojureScriptStorm) を使う方式のほうが推奨されています。
まとめ
Time Travel Debugging は、デバッグ意外にも学習用途でコードの処理を追うのにも役にたちます。導入はやや面倒で分かりづらいところがありますが、初学者や入門者の方もぜひ使ってほしいツールです。多彩な機能を自分もぜんぜん使い切れてないので、日本語の情報が増えると嬉しいなと思います。
-
本題とまったく関係ないですが、インプットを String で受け取れたら
(str *input*)
を2回しているのがキレイになるのになーと思っていますが方法が分かりません。できるんでしょうか。 ↩ -
さっきまでの表示は
Call tree tool
と言います。ちなみに1番右のタブはFunction list tool
と言い、各関数の呼び出し階数とそれぞれの呼び出しにおける引数違い(変化)を見比べることができる画面です。 ↩ -
https://http-kit.github.io/server.html の
:thread
と:worker-name-prefix
のオプションの説明を参照。 ↩