自作Zshプロンプトを非同期対応した

自作のシェルプロンプト、almelを非同期対応させることで巨大なリポジトリ内での硬直を軽減し、使用感を向上させました。

github.com

Almel とは

almel はRust製の高速に動作するシェルプロンプトです。 現在はBash, Zsh, fishに対応しています*1

以前の記事:

ryooooooga.hateblo.jp ryooooooga.hateblo.jp

Almelはlibgit2を用いることで高速にGitステータスを表示しています。

ですがこれまでは常に同期的にGitステータスを取得していたため、巨大なリポジトリ内では場合によって描画に〜1秒程度の時間がかかることがあり、発生する待ち時間がストレスになることがありました。

既存の非同期プロンプト

Powerlevel10kagkozak Zsh Promptなどの既存のZshプロンプトの中には、非同期に描画することでスループットを向上させているものがあります。

github.com github.com

これらは、Gitステータスなどの実行に時間の掛かるセグメントを除外した"仮のプロンプト"を同期的に表示し、その後Gitステータスを含む完全なプロンプトを非同期的に描画することでコマンド実行後の硬直を軽減し、利用体験を向上させています。

Almelの非同期対応

今回、Almelを非同期対応させることでこれらと同様に利用体験を向上させました*2

非同期描画にはzsh-asyncを利用しているため、あらかじめなんらかの方法でzsh-asyncをインストールしておく必要があります*3

github.com

初期化方法:

# .zshrc
# zsh-asyncを読み込む
source "${zsh-asyncのインストールディレクトリ}/async.zsh"

# Almelを初期化する
eval "$(almel init zsh --async)"

Zinitを用いた初期化方法:

# .zshrc
zinit light-mode from'gh-r' as'program' for \
    atload'eval "$(almel init zsh --async)"' @'Ryooooooga/almel'

zinit wait lucid blockf light-mode for \
    @'mafredri/zsh-async'

パフォーマンスについて

では、もともとGitに関する情報の取得にどの程度の時間がかかっていたのか測定してみます。

小さいリポジトリでの計測

まず比較的小規模であるAlmel自身のリポジトリを用いて、Gitリポジトリに関する情報取得の有無による実行速度の差を確認します。

# Gitステータスあり
$ time (for i in {1..100}; do almel prompt -s0 -j0 -d1 zsh >/dev/null; done)
( for i in {1..100}; do; almel prompt -s0 -j0 -d1 zsh > /dev/null; done; )  1.16s user 0.71s system 83% cpu 2.248 total
# 平均 22.48ms (N=100)

# Gitステータスなし
$ time (for i in {1..100}; do almel prompt -s0 -j0 -d1 zsh --no-git >/dev/null; done)
( for i in {1..100}; do; almel prompt -s0 -j0 -d1 zsh --no-git > /dev/null; ;   0.83s user 0.46s system 78% cpu 1.629 total
# 平均 16.29ms (N=100)

ここで利用している almel prompt コマンドはAlmelのプロンプトの実際の表示を行っているコマンドで、--no-git オプションを渡すことでGit関連の機能を全て無効化できます。

差はわずかではありますが、当然ながら--no-gitオプションありのほうが速いことがわかります。

Profiling Rust - macOS で DTrace を使って FlameGraph を描画する - Qiita を参考にプロファイリングを行ってみると、Gitステータスの取得 (下図 右 緑矩形) に最も時間が掛かり、次いで Gitリポジトリの初期化 (下図 左 青矩形) に時間が掛かっていることがわかります。

f:id:Ryooooooga:20220408185040p:plain
Almelのプロファイル結果 (青: Gitリポジトリの初期化, 緑: Gitステータスの取得)

Gitステータスの原理からして、リポジトリに含まれるファイルの数が多いほどより多くの時間が必要になります。

やや大きいリポジトリでの計測

続いて、手元にあったリポジトリの中で最も体感の描画時間が長かった lazygit を用いて計測を行います。

# Gitステータスあり
$ time (for i in {1..100}; do almel prompt -s0 -j0 -d1 zsh >/dev/null; done)
( for i in {1..100}; do; almel prompt -s0 -j0 -d1 zsh > /dev/null; done; )  10.24s user 15.13s system 95% cpu 26.438 total
# 平均 264.38ms (N=100)

# Gitステータスなし
$ time (for i in {1..100}; do almel prompt -s0 -j0 -d1 zsh --no-git >/dev/null; done)
( for i in {1..100}; do; almel prompt -s0 -j0 -d1 zsh --no-git > /dev/null; ;   0.47s user 0.42s system 73% cpu 1.207 total
# 平均 12.07ms (N=100)

Gitステータスありの場合、260ms以上も掛かっていることがわかります。 これほどになると同期描画の硬直はストレスに感じます。

まとめ

Almelのプロンプトの描画を非同期化することで利用体験を向上させました。

以前からGitステータス取得のパフォーマンスの低下は懸念点であったため、それが解消できたことは喜ばしいです。

また、almel init zsh --asyncで出力されるスクリプト(下記)は非常に短く簡単であるため非同期プロンプトを実装する際の参考になるでしょう。

github.com

*1:fish用の出力を利用することで、がんばればPowerShellでも動かせることを確認しています。

*2:現在、非同期描画に対応しているのはZshのみです。

*3:zsh-asyncが読み込まれていない場合は通常通り同期的に描画されます。