前後期で一貫して取り組んだことは Chrome でのデバッグ機能の実装を通した利用体験・フレームワーク開発による開発効率の改善である。
前期では、Chrome を使ったデバッグを問題なくできるところを目指し、外部ツール(Chrome、VSCode) とのインターフェースをテストするフレームワーク導入に向けて実装していった。後期では、Chrome DevTools の強みを活かしたリッチな体験、 プロトタイプを通して得られた知見をもとによりよいフレームワーク開発による開発効率の向上を目標に取り組んだ。
前期では本プロジェクト開始前にプロトタイプとして導入されていたものを改善する形で主にバグ修正や Chrome DevTools 上の UI として提供されている機能をサポートしていった。 バグ修正については手元の環境で Chrome を用いてデバッグ機能を行いそれを元に見つかったバグを中心に修正を行っていった。提供されている機能のサポートについては、例外で止まる機能やブレークポイントなしでスクリプト実行を強制する機能などを実装した。他にもデバッガー起動時に Chrome を自動で開いて Chrome DevTools とデバッガーの接続を自動で行う機能や Chrome のコンソールで式評価をした際に標準出力に出力されるものをターミナルではなくコンソール上に出力されるものを開発した。
後期では Chrome DevTools 独自の機能を用いた利用体験向上への試みや前期で挙がってきた課題の解決などを中心に行なった。体験向上の取り組みとして、多次元配列の評価結果のプレビュー機能や配列を表に変換させたものの表示機能、コンソール上に画像を表示させる機能などを実装した。前期では必須機能のサポートが中心だった為、 Pull Request はほとんどマージされたが、後期では体験向上のアイディアの模索が中心だった為、プロトタイプを作り検証 -> クローズする Pull Request が多かった。前期で挙がってきた課題としてはデバッガー起動時にユーザーが毎回 ScreenCast 画面を閉じなければならないということがあった。Chrome DevTools Frontend のコードを参考にしながら課題解決に取り組んだ。
現在、"debug.gem"では Chrome デベロッパーツール、VSCode と連携してデバッグが可能であり、それぞれ独自のプロトコルを用いている。Chrome デベロッパーツールでは CDP(Chrome DevTools Protocol)、VSCode では DAP(Debug Adapter Protocol)が用いられている。これらのプロトコルに従って外部ツールと通信する部分が "debug.gem" には実装されていてその部分のテストができるフレームワーク開発に本プロジェクトで取り組んだ。
本プロジェクトでは2種類のテストフレームワークを開発した。一つ目は以下のようにプロトコルベースの低レベルのテストケースを書くもので、テストケース作成時にはジェネレーターを用いる。実際の動作により近いテストが実現できる、テストジェネレーターを使うので作る際の負担が少ないなどの長所を持っているが、プロトコルを知らない開発者はどのような動作をしているのか分かりにくい、ジェネレーターを作成する際に誤った動作をするともう一度やり直さなくてはならない、などの問題もある。前期ではこの形のテストフレームワーク開発に取り組み、DAP サーバーのプロトコルベースでのテストを行うことができるものをマージするところまで完了した。
def test_1639301775
run_dap_scenario PROGRAM do
[
*INITIALIZE_DAP_MSGS,
{
seq: 7,
type: "event",
event: "stopped",
body: {
reason: "pause",
...
二つ目は以下のように抽象化された高レベルのテストケースを書くものである。こちらはプロトコルを意識することなくテストケースを書くことができる、一つのメソッドで CDP と DAP 両方のテストができる、などの長所を持っている一方で、実際のやりとりを完全には再現できない、それぞれのプロトコルにおける細かい違いに応じたテストができない、などの短所も含んでいる。一つのメソッドで CDP と DAP 両方のテストを行うような設計にしている。後期ではこのテストフレームワークの開発に取り組み、マージするところまで完了した。
def test_break1
run_protocol_scenario PROGRAM do
req_add_breakpoints 5
req_add_breakpoints 8
req_continue
...
その他の取り組みとして、デバッガーの統合テストをするテストフレームワークやテストケースジェネレーターの改善・バグ修正などを行った。具体的にはデバッギーに出力 されるものが多いとタイムアウトになってしまうテストフレームワークのバグの修正や、テストケースジェネレーターではデバッグをしたソースコードが出力先のファイルに含まれていないかチェックしてなければ出力できるようになった。
- Fix #329 by getting frame's path correctly
- Continue the debuggee after detaching to Chrome
- 繋いでいる状態で Chrome DevTools ページを閉じると、エラーが出て終了するバグ修正の Pull Request 。閉じた時に Chrome は Opcode に8(8は接続を閉じるという意味)を入れて送ってくるが、それに対応できていないのが原因だった。
- この時、対応方法は以下の3通りだった。
- デバッギーを
exit
で終了する。 - デバッギーに
continue
コマンドを実行させる。 - rdbg REPL に戻る。
- デバッギーを
- gdb や リモートデバッグに合わせるのが良いと考え、デバッギーに
continue
コマンドを実行させる方法を採用した。
- Fix bug that error occurs after detaching to Chrome
- デタッチ後にエラーが出て終了するバグ修正の Pull Request 。スレッド終了後に
readline
メソッドが呼ばれてエラーが出てしまうということが原因だった。
- デタッチ後にエラーが出て終了するバグ修正の Pull Request 。スレッド終了後に
- Ensure that the correct line number of the breakpoint is returned
- Chrome がブレークポイントをセットするよう要求した行番号が正しいかどうか確認するようにした Pull Request。
ブレークポイントをセット後、それらを全て削除したがまだブレークポイントが残っているバグ修正に関する Pull Request 群
- Make sure to get break numbers correctly in CDP server
- Fix bug that breakpoints still left even though they are removed in Chrome
- Fix bug that line numbers can not be retrieved from Chrome's requests
- Fix bug that number of lines in a file are not counted correctly
- Add missing methods in Chrome DevTools Protocol
マウスオーバーをした時に表示するオブジェクトに関するもの。当時はマウスオーバーされたオブジェクトを eval
して返ってきた値を返すようにしていたが、グローバル変数など値が変わってしまうという問題があった。
- Do not evaluate the method when "objectGroup" is "popover"
- メソッドの時以外
eval
するようにした。マージされなかった。
- メソッドの時以外
- Update several parts in CDP to support for the current change
- DAP サーバーの処理では
eval
を使わないで評価していたので DAP サーバーの方に合わせるようにしたもの。上記の Do not evaluate the method when "objectGroup" is "popover" と比較して読みやすさやオブジェクトの表示範囲が広かったのでこちらを採用することにした。 - バックトレースの部分も DAP の方に合わせてそれぞれの変数の継承元のオブジェクトなどを表示できるようにした。
- DAP サーバーの処理では
- Make sure to match all constant variables in Chrome
- ピリオド以降の文字(
ABC.new
のうち.new
の部分)にマッチしないようにした。 CDP サーバーに関する変更。
- ピリオド以降の文字(
- Do not match any characters after the dot in an expression
- ピリオド以降の文字(
ABC.new
のうち.new
の部分)にマッチしないようにした。DAP サーバーに関する変更。
- ピリオド以降の文字(
- Match not only top level namespaces but also inside of them
A::B::C
と言ったコードがあった場合B
やC
にマウスオーバーをしてもそれらのオブジェクトが表示されないというバグがあったのでその修正をしたもの。
Chromeが自動で起動した後、 Chrome DevTools ページに飛ばないというバグの修正関連。1.4.0リリース直前にこのバグを発見した。
- Fix bug that debugger sometimes doesn't open Chrome automatically
- ワークアラウンドとして出したPull Request。Chrome からのレスポンスの取得回数を増やした結果、筆者の環境でバグが修正されたように見えたことを根拠とした。
- Correct changes in #445
- ワークアラウンドのPull Requestの変更を書き直したもの。Page Domain を有効にする"Page.enable"メソッドを送信することで指定したURLへと移動する"Page.navigate"メソッドを確実に実行できるようにした。
- Send current page's frame id when debugger sends 'Page.navigate' method
- "Page.navigate"メソッドの中にFrame Id をパラメーターとして送ることにしたもの。
- 感覚ではあるが、上記の Correct changes in #445 でバグはだいぶ改善された。しかし、デバッガーを使い始める1回目にバグがまだ観測された(一度終了して再起動するとこのバグはほとんど観測されない)。ドキュメントを見たところ、"Page.navigate"メソッドを送る際に Frame Id のフィールドをパラメーターとして送らない場合トップフレームを Frame Id として取得するという内容があったの遷移するフレームがトップフレームではなかった場合失敗するのではないかという推測を立て Fame Id をパラメーターとして送るようにした。その結果、現在に至るまでバグは筆者の環境で観測されていないので、バグを修正することができたと判断している。
- Show objects when a hovered expression is a specific class in Chrome
- Fix bug that several types doesn't show their classes
-
Fix bug that debugger doesn't stop at the final line in Chrome
- ファイルの最終行に止まらないバグを修正するPull Request。デバッグコマンドを入力する順番が間違っていたことが原因だった。
-
- Chrome を自動で開くために作ったテンポラリーディレクトリを削除する処理に止まってしまうというバグを修正する Pull Request 。
-
Call "cleanup_reader" method in "UI_ServerBase" class when CDP exits
- 継承元のメソッドを呼び出す Pull Request 。 Chrome のプロセスを終了しない方針になっていてこのメソッドは削除予定なのでクローズした。
-
Give correct arguments to "respond_fail" method
- メソッド呼び出しの際にハッシュを展開して引数として渡す。
-
Use "DEBUGGER__.safe_inspect" when objects are inspected
- Object#inspect の代わりに DEBUGGER__#safe_inspect を使う。
-
Fix bug that debugger raise an error when breakpoints are set in script snippets
- Snippets ペインにブレークポイントをセットしている状態でデバッガーを起動しようとすると落ちてしまうバグの修正 Pull Request 。
-
Call "cleanup_reader" method in "UI_ServerBase" class when CDP exits
- Refactor processing of WebSocket in
cdp_server.rb
- WebSocket の処理を WebSocket クラスとして切り出したもの。
- Fix processing not related to WebSocket
- WebSocket の処理に関係ないものを WebSocket クラスから切り離した。
- Remove useless if statement
- Refactor the logic in WebSocket class
-
- 当初、以下の機能が実装されていたが断念した。
- ポートなどの情報をファイルとして持たせる
- なんらかの方法でそのファイルを書き換えることができた場合意図しない接続先にデバッガーが接続しようとしてしまうというセキュリティリスクがあり断念。
- DevToolsActivePort ファイルからポートなどの情報を読み取る
- 実験してみたところ、ポート番号が変わっても DevToolsActivePort ファイルが更新されなかったり、 DevToolsActivePort ファイルを用いたいくつかのプロジェクトでうまく動かないといった Issue が観測されたので断念。
- ポートなどの情報をファイルとして持たせる
- 当初、以下の機能が実装されていたが断念した。
-
Do not open Chrome automatically if "CONFIG[:chrome_path]" is an empty string
- Chrome を自動で開きたくないケースがあると考え、環境変数で設定できるようにした。
- Output stdout to Console instead of the terminal
- Make sure that $stdout returns to its original value after evaluation in CDP
- Support "Pause on exceptions" feature in CDP
- 捕捉された例外でも止まるのか、捕捉されなかった場合のみとまるのか2つのオプションがあったため、以下のようにした。
- 捕捉された例外でも止まる ->
catch Exception
- 捕捉されなかった場合のみ止まる -> postmoterm モードで実行
- 捕捉された例外でも止まる ->
- 捕捉された例外でも止まるのか、捕捉されなかった場合のみとまるのか2つのオプションがあったため、以下のようにした。
- Show exception messages in Chrome when debugger paused on an exception
- 例外で止まった時、メッセージを表示するようにした。
- Show variables in the target frame in Chrome
- funcitonLocation パラメーターを"Debugger .paused"メソッドの中に追加することで実現。 functionLocation フィールドの中の lineNumber フィールドにはワークアラウンドとして0を固定値で入れていたが、筆者の試した範囲では正常に表示することを確認できたのでこの変更で良いと判断した。
- Set lineNumber of functionLocation correctly
- lineNumber フィールドに正しい行数を入れるようにしたもの。意図しない場所に変数が表示されるというバグが見つかったためワークアラウンドの部分を修正することにした。
- Improve error messages when it evaluates expressions in Console
- Improve error messages in the Chrome DevTools Console
- Make multidimensional hashes and arrays easier to read in Chrome
- 配列やハッシュの中身を以下のようにクリックして見れるようにしたもの。
- Support several features in Chrome DevTools
- 以下の機能をサポートした。
- ブレークポイントをアクティベートする機能
- スクリプトをブレークポイントなしで強制実行
- 以下の機能をサポートした。
- support completion on CDP
- コンソールの補完機能を実装した Pull Request
- 以下の理由から Close した。
- Chrome DevTools Frontend 内でJavaScript の補完機能が実装されている為、
(123).a~
のように括弧をつけないと候補が表示されないなど意図した結果が出ない時があった。
- Chrome DevTools Frontend 内でJavaScript の補完機能が実装されている為、
- 以下の理由から Close した。
- コンソールの補完機能を実装した Pull Request
- Add query parameters to URL to use the debugger easily
- Query parameter をつけることで ScreenCast 画面を開かないようにした。
- Show tables when results are Array or Hash
- Table を表示するようにしたもの。あまり効果的なものではないと判断した為クローズした。
- Prototype of showing image and colored texts
- JavaScript のコンソールに画像を表示するプロトタイプの Pull Request
- Set abbreviated property values in the Chrome DevTools Console
- Prototype of showing image
- JavaScript のコンソールに SVG 形式の画像を表示するプロトタイプの Pull Request
- webrick でサーバーを立ち上げて任意の SVG 要素を生成して返すようにすることで画像を表示するようにしている
- あまり使い道がなくクローズした。
- JavaScript のコンソールに SVG 形式の画像を表示するプロトタイプの Pull Request
- Use file scheme instead of http scheme in Page pane
-
Add explanations on how to maximize Chrome DevTools
- Chrome DevTools ページを最大化する為の方法をドキュメントに記載。
-
Remove explanations about how to disable ScreenCast displays
-
- 実験で作ったフレームワーク。イメージができたのでクローズした。
-
[WIP] 1. Add the test framework for CDP
-
テストフレームワークの1パターン目。以下のように
server_cdp.rb
と同じように書くような形にした。 -
def test_sample run_cdp_scenario PROGRAM do res1 = cdp_request 'Debugger.setBreakpointByUrl', condition: '', lineNumber: 7, url: "http://debuggee#{temp_file_path}" res2 = cdp_request 'Debugger.setBreakpointByUrl', condition: '', lineNumber: 5, url: "http://debuggee#{temp_file_path}" res3 = cdp_request 'Debugger.setBreakpointByUrl', condition: '', lineNumber: 3, url: "http://debuggee#{temp_file_path}" ...
-
メリット
- 最も柔軟に書くことができる。
- 行数が少なくて済む
-
デメリット
- 可読性が低く CDP をよく知っている人にしか分からないものになっている。
-
-
[WIP] 2. Add the test framework for CDP
-
テストフレームワークの2パターン目。以下のようにJSON 形式でリクエスト・レスポンスを書くような形にした。テストジェネレーターも併せて作った。
-
def test_sample run_cdp_scenario RROGRAM do cdp_req({ "id": 1, "method": "Page.getResourceTree", "params": { } }) ...
-
メリット
- パターン1と比べ可読性が高い。
-
デメリット
- 毎回メソッドを通して呼んでいるので余計なカッコがついたりして可読性が下がる。
- 行数が長くなる。
-
-
3. Add the test framework for CDP
-
テストフレームワークの3パターン目。以下のようにプロトコルをそのまま書くような形にした。テストジェネレーターも併せて作った。
-
def test_sample run_cdp_scenario PROGRAM do { "id": 1, "type": "request" "method": "Page.getResourceTree", "params": { } ...
-
メリット
- DAP と同じ形式なので読みやすい。
-
デメリット
- 返り値を使ってリクエストを送ることができないなど柔軟性が低い。
- 行数が長くなる。
- 返り値を使ってリクエストを送ることができないなど柔軟性が低い。
-
-
-
Add the test framework for DAP
- DAP をテストする為のフレームワーク。
-
Call "recv_request" method with correct number of arguments
- DAP のテストを実行する際に時々落ちるバグを修正。メソッドを呼び出す際に正しい引数を与えていなかったことが原因だった。
-
Add a new workflow to test for DAP
- DAP テスト用の新しいワークフローを追加
- ランダムなタイミングでテストが失敗するという問題を抱えていて
rake test
では実行されないようになっている。
-
- CI が落ちていたのでその修正。継承元オブジェクトの表示される順番が筆者の環境と CI 環境で違ったことが原因だった。
-
Make sure to fail DAP's tests when a debuggee does not finish after all scenarios
- Add the test case for Call Stack feature
- Add the test case for eval
- Add the test case for watch
- Add the test case for catch
- Add the test case for "Run To Line"
- Add the test case for "Restart"
- Add DAP's test helper methods for unit tests
- Add the new test framework for DAP and CDP
- Fix some parts in test/support/protocol_utils.rb to run tests correctly
Debuggeeの出力が多いときテストフレームワークが動かないというバグを修正する為のPull Request 群
- Read data from the I/O stream of the remote debuggee every commands are typed
- Read data from the I/O stream of the remote debuggee when Timeout Error occurs
- Timeout Errorが起こったタイミングでリトライするようにしたもの。バグが治ったことは筆者の環境で確認できたが、この変更によってどうしてこのバグが直ったのか原因がわからず、たまたまの可能性が高くマージされなかった。
- Fix bug that test framework gets stuck when debuggee outputs many lines
- デバッギー側の出力を常に読み取る新しくスレッドを作るようにした。マージされた。
- Fix test
- CIが落ちていたのでその修正。
assert_finish
メソッドをAPIから削除後、まだ消し忘れが残っていたことが原因だった。
- CIが落ちていたのでその修正。
- Remove the useless method call
- 必要のないメソッドが呼ばれていたのでそのメソッドの削除。
- Remove some unused code
- 使われてないコードの削除
- Fix unused block arguments
- Fix small parts in lib/debug/thread_client.rb
- Remove redundant "begin" block
- Use snake case instead of camel case
- キャメルケースで変数が定義されていたのでスネークケースに変数を書き直したもの。VS Code のフィールド名であることを強調する為意図的にキャメルケースで書かれていたものだった為、マージされなかった。
- Refactor the logic by removing an intermediate variable
- 中間変数を削除するもの。テストが通らずクローズした。
- Verify that the debugger and debuggee program have exited
assert_finish
メソッドをAPIから削除しテストフレームワークの内部でデバッガー・デバッギープログラムが終了しているかどうか確認するようにした。
- Add the
program
method if the source of the script file doesn't match the source of allprogram
methods in the file- テストケースジェネレーターの改善。デバッグ対象のソースコードがテストケース製成先のファイルに含まれていなければシナリオメソッドと一緒にソースコードを追加するようにした。
- Update the guide for the test generator
- テストケースジェネレーターの使い方を記載。
- Add descriptions about assert methods to CONTRIBUTING.md
- テストの API の使い方を記載。
- Fix typo
本プロジェクト開始時のゴールに対する進捗は以下のようになっている。概ね、目標を達成することができた。
- Chrome でのデバッグ機能を実現する CDP サーバーの開発
- vscode-rdbg と遜色ない機能を持ったデバッグができる。
- Chrome DevTools 上の UI として提供されている機能のサポートをする。
- Chrome DevTools の強みを活かしたリッチな開発体験向上
- CDPサーバー と DAPサーバー のテストフレームワークの開発
- プロトコルベースでのテストフレームワーク
- CDP サーバー
- テストフレームワークがマージされる。
- テストフレームワークが安定して動く。
- DAP サーバー
- テストフレームワークがマージされる。
- テストフレームワークが安定して動く。
- CDP サーバー
- 抽象化されたテストフレームワーク
- テストフレームワークがマージされる。
- テストフレームワークが安定して動く。
- プロトコルベースでのテストフレームワーク
本プロジェクトにおける社会的インパクトは2つの点から言うことができる。
1点目は利用体験の向上である。デバッガー起動時に Chrome DevTools の画面を自動で開く機能や変数のクラス情報を表示する機能などの VS Code を用いたデバッグと同様の機能を実現することができた。 Chrome を用いた JavaScript のデバッグに慣れ親しんでいるユーザーが “debug.gem” を使いやすくなった。
2点目は“debug.gem” の開発効率の向上である。テストフレームワークが開発されたことで、 CDP や DAP サーバーの開発は変更が加わったとしてもバグが出にくくなり、レビュアーのレビューの負担を減らすことができた。
また、Chrome DevTools Frontend に対応したデバッガーの事例は少なくノウハウもあまり世に出回っていない。今後 Chrome DevTools Frontend に対応したデバッガーを開発する開発者にとって “debug.gem” を参考になる代表例の一つということができるだろう。
本プロジェクトを進めるに当たって笹田耕一さんには Pull Request のレビューを始め沢山の助言をいただきました。笹田さんのレビューからはいつも気付かない視点を与えてくださり自分一人ではここまで来ることはできなかったと思っています。本当にありがとうございました。
また、このような素晴らしい機会を与えてくださった Ruby アソシエーション関係者の皆様に感謝します。