LoginSignup
0
0

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

Last updated at Posted at 2023-12-22

デスクトップアプリケーションとして提供されている 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

動作要件

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

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です。

todo-app.png

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

flowstorm.png

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

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

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

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

1st-tracing.png

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

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

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

add.png

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

switch.png

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

code-tree.png

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

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

value.png

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

exp1.png

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

スクリーンショット 2023-12-22 17.09.30.png

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

スクリーンショット 2023-12-22 17.13.00.png

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

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

tab.png

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 関数にブレイクポイントを設定することで、どのワーカースレッドからから呼ばれているのかすぐに分かって便利だったりします。

break.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. さっきまでの表示は Call tree tool と言います。ちなみに1番右のタブは Function list tool と言い、各関数の呼び出し階数とそれぞれの呼び出しにおける引数違い(変化)を見比べることができる画面です。

  3. https://http-kit.github.io/server.html:thread:worker-name-prefix のオプションの説明を参照。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0