Zsh では、たいていのものは補完できるのだが、場合によって補完が行われるまでに時間が掛かるという問題に遭遇する。たとえば補完候補リストを生成するのに、ネットワークを通じてリモートのリソースにアクセスする場合など、コストのかかる処理が必要なときだ。
補完の重要な目的のひとつはコマンドライン編集を加速させることだが、補完自体に時間が掛かって編集が待たされてしまっては本末転倒である。そこで Zsh には、コストのかかる補完データをキャッシングして、その後の補完を高速に行うための機能が用意されている。今回は、私が補完スクリプトを実装する際に、キャッシュ機能を利用する必要が生じた実際例に基づいて、その使い方と得られた知見を説明したい。
実際に使った例
キャッシュ機能を利用する必要が生じたのは、[jhbuild] link-jhbuild コマンド用の[補完スクリプト] link-jhbuild-completionを作成したときだ。jhbuild とは、[GNOME] link-gnome デスクトップの各種プログラムをビルドするためのツールである。jhbuild コマンドには、ビルドしたいプログラム (モジュールと呼ばれる) の名前を引数として指定する。モジュール数は数百件あり、覚えていられないので補完する必要が生じた。補完候補を得るまでのポイントは以下のとおりで別段難しくはない。
- jhbuildには引数としてビルドしたいのプログラム(モジュールと呼ばれる)名を指定する
- ビルド可能なモジュール一覧は、
% jhbuild list
コマンドで取得できる -
jhbuild list
で得られたモジュール一覧を補完候補として利用すればよい
問題は、jhbuild list
は結果を返すまでに数十秒も掛かり、その結果を補完候補として利用するには遅すぎるということだった (リモートにあるGNOMEの最新のGitリポジトリを参照しに行ったり、モジュール間の依存関係を計算したりと非常にコストの高い処理となっている)。キャッシュ機能でこの問題を解決できそうであることを知り、活用してみた。実際にキャッシュの有無で性能差を比較してみたところ、以下のとおり約2000倍の性能向上が得られた(補完スクリプトのトップ関数をサブシェル化してtimeで測定)。
キャッシュなし:
2.24s user 0.17s system 4% cpu 49.010 totalキャッシュあり:
0.02s user 0.00s system 98% cpu 0.024 total
実際に作成したスクリプトの抜粋
参考までに、以下に補完スクリプトの抜粋をキャッシュ関連の箇所だけ抜き出しておく。スクリプト全体は以下のURLから参照できる:
https://github.com/jmatsuzawa/zsh-comp-jhbuild/blob/master/_jhbuild
(( $+functions[_jhbuild_modules] )) || _jhbuild_modules() {
local update_policy
zstyle -s ":completion:${curcontext}:" cache-policy update_policy
if [[ -z "$update_policy" ]]; then
zstyle ":completion:${curcontext}:" cache-policy _jhbuild_caching_policy
fi
if ( [[ ${+_jhbuild_module_list} -eq 0 ]] || _cache_invalid _jhbuild_modules ) &&
! _retrieve_cache _jhbuild_modules; then
_jhbuild_module_list=(${(@f)"$(jhbuild list -a 2>/dev/null)"})
_store_cache _jhbuild_modules _jhbuild_module_list
fi
_describe -t modules 'modules' _jhbuild_module_list
}
(( $+functions[_jhbuild_caching_policy] )) || _jhbuild_caching_policy() {
local -a oldp
oldp=( "$1"(Nm+7) )
(( $#oldp ))
}
キーとなる関数
キャシュ機能を使用するうえで、ポイントとなるZshが提供する関数が三つある。
-
_store_cache
-
_retrieve_cache
-
_cache_invalid
_store_cache
データをキャッシュとして保存するために使用する。_store_cache <キャッシュ識別子> <パラメーター>...
という構文を取る。キャッシュデータの保存先は、デフォルトでは、${ZDOTDIR}/.zcompcache
ディレクトリ、ZDOTDIRが未設定であれば${HOME}/.zcompcache
ディレクトリの配下となり、<キャッシュ識別子>に指定した名称でキャッシュファイルが保存される。保存先を変更したい場合は、cache-pathスタイルに保存場所のディレクトリ名を設定する。jhbuild用スクリプトの実装例 (`_store_cache _jhbuild_modules _jhbuild_module_list` _jhbuild_module_listは配列変数)では、`${ZDOTDIR}/.zcompcache/_jhbuild_modules`というファイルが生成される。ファイルの中身は、_store_cache に指定した`_jhbuild_module_list`という配列変数が、その要素も含めてダンプされている。正常に保存できた場合は、0が返る。(なお、キャッシュファイルの形式は、そのままzshスクリプトとして解釈可能になっており、sourceや.(ドット)コマンドでキャッシュファイルを読み込ませれば、<パラメーター>に指定した変数を起動中のzshセッションに復元することもできる。そのため、キャッシングレイヤーの外部(メタ的な処理など)でキャッシュの中身を参照したい場合は、sourceやドットコマンドを利用して簡単に復元できる)。
_retrieve_cache
保存したキャッシュを参照するために使用する。_retrieve_cache <キャッシュ識別子>
という構文を取る。<キャッシュ識別子>で指定されたキャッシュファイルがキャッシュディレクトリに生成済みであれば、それを読み込み、`_store_cache` に指定した<パラメーター>変数が現在のzshセッションに復元される。正常に復元出来た場合は0が返る。jhbuild用スクリプトの実装例(_retrieve_cache _jhbuild_modules
)では、$ZDOTDIR/.zcompcache/_jhbuild_modules
が読み込まれ、_jhbuild_module_list 配列変数が復元される。なお、上記_store_cache
の説明の末尾で、キャッシュファイルはそのままドットコマンドで復元できると述べたが、_retrieve_cache
は内部的にドットコマンドでキャッシュを復元している。
_cache_invalid
キャッシュの更新が必要かどうかを判断するのに使用する。キャッシュの中身が補完候補リストとして古すぎる場合には、キャッシュを更新する必要がある。
_cache_invalid <キャッシュ識別子>
という構文を取る。_cache_invalid
の返す値は0(真)、または1(偽)である。真が返る場合はキャッシュが無効(invalid)であると判断されたとき、偽が返る場合はキャッシュが有効である(invalidではない)と判断されたときである。つまり、 _cache_invalid
が真を返す場合には、キャッシュされるべき補完リストデータを再生成して、そのデータを再度 _store_cache
することでキャッシュの再構築ができる。(個人的には、_cache_invalid
の真偽値の意味が少し混乱してしまうので、関数名を _cache_valid
として真偽値の意味を逆にした方が直感的になったのではないかと思う...)
では、有効/無効は具体的にどう判定されるのか? _cache_invalid
関数は、内部的にcache-policyスタイルで指定した関数(以下、ポリシー関数と呼ぶ)を呼び出し、ポリシー関数の返した真偽結果を返す。ポリシー関数には、キャッシュファイルの絶対パスが第一引数かつ唯一の引数として渡される。補完スクリプト作成者は、以下の処理を実装しておくことで、ユーザーが補完を実行した時にキャッシュの更新を自動的に行うことができる:
-
ポリシー関数を実装する
-
ポリシー関数の名前を文字列としてcache-policyスタイルに設定する
-
_cache_invalid が真を返す場合にキャッシュの再構築を行う
なお、ポリシー関数の実装例が、zshcompsysの[公式マニュアル] link-zshcompsys-docに以下のとおり紹介されており、zsh標準の補完スクリプトを含め、多くの補完スクリプトがこれに近いポリシー関数を実装している。
_example_caching_policy () {
# rebuild if cache is more than a week old
local -a oldp
oldp=( "$1"(Nm+7) )
(( $#oldp ))
}
多くの場合、ユーザーは、与えられたポリシーに満足できない場合、各補完スクリプト用の自前のポリシーを適用することができる。どういうことかというと、基本的に、zsh標準の補完スクリプトは、cache-policyがすでに設定済みであればそれを上書きしない。要するに、補完スクリプトユーザーが自前のポリシー関数を実装してそれをcache-policyに設定しておくことで、自分オリジナルのキャッシュ更新ポリシーを適用することができる。自分が補完スクリプトを実装する場合にもこの点を考慮に入れておくと汎用性を高めることができるだろう。
スタイル
キャッシュ関連の重要なスタイルを紹介する。キャッシュ関数の説明ですでに紹介済みだが、整理のために改めて個別に説明する。もちろんスタイルとはzstyleで設定するスタイルのことである。
use-cache
キャッシュ機能の有効/無効を切り替えるブール値を指定する。有効にする場合はtrue。補完スクリプト自身ではなく、そのユーザー側で、キャッシュを利用したいコンテキストに応じて設定する。use-cacheでキャッシュを有効にしていない場合は、補完スクリプトがキャッシング処理を実装していてもキャッシュ機能が働かない。要するに、_store_cache
、_retrieve_cache
、_cache_invalid
の各関数は何もせず、即座に1を返す。
cache-policy
キャッシュの更新が必要かどうかを判断する関数名を文字列で指定する。cache-policyに指定した関数が、_cache_invalid
起動時に呼ばれる。
cache-path
キャッシュファイルを保存するディレクトリを文字列として指定する。cache-pathが指定されていない場合は、${ZDOTDIR}/.zcompcache
ディレクトリが使用される。ZDOTDIRも未設定であれば、${HOME}/.zcompcache
ディレクトリが使用される。