FlowStorm/ClojureStorm による タイム・トラベル・デバッグ

デスクトップアプリケーションとして提供されている Clojure のデバッガ [FlowStrom](https://github.com/flow-storm/flow-storm-debugger) の紹介です。 日本語の記事をまったく見かけないのですが Clojure の他のデバッガより便利な点がいくつかあります。 time travel debugging という、処理のステップを精緻に全て記録し状態を行ったり来たりして確認が可能なコンセプトは Clojure の他のデバッガとは異なる特長なのではないかと思います（他のデバッガをそんなに知らないので実はできるのかもしれないですが）。

## FlowStorm の特徴

1つ目は ClojureScript もサポートしていることです。 ブラウザのデバッガ使えばいいよ、というのはあります。しかし Clojure も ClojureScript も Babashka も同様のツールとやり方でデバッガを使えるのはやはり優位な点でしょう。

2つ目は 一度全ての状態遷移をトレースしたうえで各状態を行き来して確認できるです。Break Point を置いてステップ実行しながら状態を確認する方法だと「あれ、この１つ前の状態はどんなだっけ？今どう変わった？よし、もう一回最初から」みたいなことをやりがちです。また、Break Point を設定するのはバグなどの調べたい箇所のあたりが付いている場合です。FlowStorm はあたりが付いていない場合に全体を広く見ていく使い方に合います。

FlowStrom は、自身のこのようなデバッグ方式を **time travel debugger** と呼んでいます。この名前が一般的なのか自分は知らないですが、ググってみると Microsoft VisualStudio で ASP.NET を対象に 2019 年に機能追加されているのを発見しました。

https://www.publickey1.jp/blog/19/time_travel_debuggingvisual_studio.html

他には Rust 用の VSCode 拡張もヒットします。

https://firedbg.sea-ql.org/

３つ目は expression and value oriented という点です。デバッガは通常ソースコードにおける line oriented なことが多いですが、 FlowStorm では式の評価１つ１つを詳細に追うことができます。これは実際に見たほうが分かりやすいポイントなので後ほど実際に説明します。

より詳細な FlowStorm の特長については README の FAQ のこの節を参照ください。 → https://github.com/flow-storm/flow-storm-debugger?tab=readme-ov-file#how-does-it-compare-to-other-clojure-debuggers


## 動作要件

1. jdk17+
2. 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 ウェブアプリケーションを使わせてもらいます。

https://github.com/prestancedesign/babashka-htmx-todoapp

### Sample Web Application

次のように Babashka スクリプトをダウンロードしておいてください。

wget があれば：

```sh
wget https://raw.githubusercontent.com/babashka/babashka/master/examples/htmx_todoapp.clj
```

curl しかなければ：

```sh
curl -OL https://raw.githubusercontent.com/babashka/babashka/master/examples/htmx_todoapp.clj
```

あるいはせっかくなので Babashka でダウンロードするなら[^1]：

```sh
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 から起動してみてください。

```sh
bb ./htmx_todoapp.clj
```

localhost:3000 で TODO ウェブアプリケーションが立ち上がればOKです。

![todo-app.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37466/c11d7f0e-446c-4a1f-6941-c19232da351f.png)

FlowStorm を使う際は Babashka から直接は起動しないため、`ctrl + c` などで一度停止してしまってください。 

### `deps.edn`

つづいて `deps.edn` を作成します。 `bb` は組み込みの依存ライブラリ一覧を deps.edn として出力する `print-deps` サブコマンドを提供しているので、これをベースにします。

```sh
bb print-deps > deps.edn
```

生成された `deps.edn` に次の内容を追記します。

```clojure
: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 デバッガ本体の２つの依存を追加しています。

- `:jvm-opts` では、デバッグの有効化と有効にする範囲を名前空間の接頭辞（ここでは `user` ）で指定しています。

全体としては例えば次のような内容になります。依存ライブラリの構成やバージョンは Babashka のバージョンが変われば変わるかもしれません。

```clojure
{: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 行目あたりの

```clojure
(when (= *file* (System/getProperty "babashka.file"))
```

を

```clojure
(when true
```

のように書き換えてください。


そして、 `clj -A:dev` で Clojure から、まずは Repl を起動します。

```sh
$ 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 が出てますがとりあえず無視で。

```sh
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 製です。

![flowstorm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37466/ac4f4796-49c6-951c-5159-f75be3699bc6.png)

さらにその状態で次のように Babashka スクリプトを eval します。

```clojure
(load-string (slurp "htmx_todoapp.clj"))
```

http://localhost:3000/ がブラウザで自動で立ち上がって TODO アプリケーションの画面がまた表示されると思います。 Flowstrom debugger のほうは `worker-1` というスレッドが表示されたと思います。 `worker-1` をダブルクリックするとメインのペインにトレーシングが表示されています。この画面におけるトレーシングの表示は関数の呼び出しネストした階層として見せるのでよくあるスタックトレースに近い見た目です。

横向きの `▲` をクリックして全部開いてみてください。

![1st-tracing.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37466/a856abb3-59ec-d80d-907e-22eb9f98bcdc.png)

先のステップで、`deps.edn` にて `"-Dclojure.storm.instrumentOnlyPrefixes=user"` という JVM オプションを設定していたのを覚えていると思います。そのため `user` 名前空間のみがデバッガによってトレーシング対象となって表示されています。いま、初回の画面表示の際にはパスとしては `/` に対して GET リクエストが発生した状態になっているので、このとき `user` 名前空間、つまり今回であれば `htmx_todoapp.clj` スクリプトの中の処理としては `routes` 関数（リクエストの振り分け処理を行っている）が最初に通るエントリポイントとなっています。結果として、デバッガ上でもそれが一番上の行に Root として表示されています。

各行をクリックすると、画面下部の `Args:` ペインや `Ret:` ペインに引数や返り値の型と値が表示されます。各関数呼び出しが階層表示され値が見れるこの画面はこれだけでも十分便利です。

TODOアプリケーションの画面から TODO を追加してみます。「あいうえお」などなんでもいいので入力して Enter します。

![add.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37466/8974df2a-adda-e716-a1d1-f3b13fc45af4.png)

デバッガのほうで `worker-2` のスレッドが増えたはずです。それをダブルクリックして開き、今度は画面下部のタブ切り替えで `()` をクリックして `Code tool` を開きます[^3]。

![switch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37466/07e37015-5c95-9fc3-7850-4b047f827cd3.png)

`Code tool` では `routes` 関数のソースコードが表示されています。

![code-tree.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37466/ee24bc9a-6274-0bd3-d60c-8390295536b4.png)

ピンク色の部分は、この実行において実際に評価された 値 または 式 です。

上部のボタンの羅列から `›` をクリックするとピンクのところが2行目の `uri` から順にグリーンに変化していきます。式や値が評価された順にその評価結果（＝中身）がどうなっているか右のペインに表示されます。

![value.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37466/9cedaa6f-15b6-bd3e-a616-78596ef228c8.png)

`uri` シンボルにバインドされた値は `String` 型の　"/todos" となっています。 `›` をも一回クリックすると次の式 `(str/split uri #"/")` （のカッコ）がグリーン色になり、右ペインはその評価結果に切り替わります。 `PersistentVector` 型で `["", "todos"]` という値です。

![exp1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37466/b20d18a4-bd1f-961d-d6c5-d112435181c8.png)

さらに `›` をくりっくしていけば、その後の式（や値）の評価結果が順次表示されます。

![スクリーンショット 2023-12-22 17.09.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37466/fbc71778-1ada-5355-5090-84738f6b280e.png)

`‹` をクリックすれば、逆に１つずつ前の評価結果の表示に戻っていけます。 `›` でずっと先に進めていくと、 `route` 関数を抜けてそこから呼び出している `add-item` 関数、さらにそこで呼び出されている `parse-body` 関数と順次ソースコードが表示され式や値の評価結果を見ていくことができます。

![スクリーンショット 2023-12-22 17.13.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37466/14505608-adfd-2eb4-3df1-56dddb2101c1.png)

基本的な使い方はこのような感じで以上です。説明がちょっと冗長になってしまったわりに面白い例で説明できなかった気もするのですが、ポイントとしてはブレイクポイントを使わず式や値の評価を詳細にトレースした結果全体を保持しその状態遷移を自由に行ったり来たりしながら調査していくことができるというのが伝えたいところでした。

この後は、各画面についての簡単な捕捉です。

![tab.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37466/6035545b-ae4f-685e-51a1-9ed339633b54.png)

## `Flows` タブ

FlowStorm の `Flows` タブではスレッドごとにトレーシングが分かれて表示されます。 例えば `pmap` のようなスレッドを複数作成する可能性のある処理では画面にたくさんのスレッドが表示されます。今回はサーバを起ち上げるために [httpkit](https://github.com/http-kit/http-kit) を使っているため、 http-kit のデフォルト設定により最大で４つのスレッドが `worker-*` という名前で表示されます[^2]。

処理が複数スレッドに振り分けられると、自分の画面操作がどこのスレッドで起きているか探すのが少し面倒になります。今回の利用でいえば、ダウンロードしたスクリプトの 237 行目あたり、

```clojure
        (srv/run-server #'routes {:port port})
```

のオプションの引数に `:thread: 1` などを追加する修正をするとデバッグとしてはやりやすくなります。

また、この記事では式と値の評価を順に追うやり方した説明しなかったですが、 Power Step という機能を使うと、ある値と同一の値が次に評価されるステップまで進む（`identity`）、ある値と等しい値が次に登場するステップまで進む（`qualtily`）、任意の述語を満たす値が次に登場するステップまで進む、のような追い方ができます。その他に多様なジャンプの方法が提供されています。

## `Browser` タブ

名前空間と変数に対して、詳細な instrument とその解除を設定できます。また、関数に対して Break Point の設定も可能です。今回の場合だと例えば `routes` 関数かあるいはそこから呼ばれていた `add-item` 関数にブレイクポイントを設定することで、どのワーカースレッドからから呼ばれているのかすぐに分かって便利だったりします。

![break.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37466/fdefb465-e1d4-0578-1204-881db36b0e0c.png)

このスクショの場合 `worker-3` でブレイクしていることが伺えます。

## `Taps` タブ

`tap>` を視覚化できるそうです。使ったことないのであまりわかりません、すみません。

## `Docs` タブ

関数の実行をサンプリングしてドキュメントを生成できるそうです。これも使ったことないのであまりわかりません、すみません。

## `Timeline` タブ

スレッドを超えてグローバルな時系列での処理ステップを追うことができます。例えば、マルチスレッドの処理をデバッグするのに使えます。

## `Printer` タブ

コードを再実行することなく、 print をコード中に複数差し込んでコンソール出力させつつ実行を再現することができます。 print の追加自体は `Flows` ツールから設定します。

## その他の補足

FlowStrom とは Flowstrom debugger のコア機能の名称です。
ClojureStrom (ClojureScriptStorm) とは本家 Clojure Compiler をフォークして instrument を差し込んだものです。

FlowStrom でデバッグを行うことも可能ですが ClojureStrom (ClojureScriptStorm) を使う方式のほうが推奨されています。

## まとめ

Time Travel Debugging は、デバッグ意外にも学習用途でコードの処理を追うのにも役にたちます。導入はやや面倒で分かりづらいところがありますが、初学者や入門者の方もぜひ使ってほしいツールです。多彩な機能を自分もぜんぜん使い切れてないので、日本語の情報が増えると嬉しいなと思います。


[^1]: 本題とまったく関係ないですが、インプットを String で受け取れたら `(str *input*)` を2回しているのがキレイになるのになーと思っていますが方法が分かりません。できるんでしょうか。
[^2]: https://http-kit.github.io/server.html の `:thread` と `:worker-name-prefix` のオプションの説明を参照。
[^3]: さっきまでの表示は `Call tree tool` と言います。ちなみに1番右のタブは `Function list tool` と言い、各関数の呼び出し階数とそれぞれの呼び出しにおける引数違い（変化）を見比べることができる画面です。