awscliのコマンドの補完の遅延にイラッとしたので速くすることを考えてみた

はじめに

awsCLIとしてawscliがある。version 2を使ってくれと書かれていたりはするものの、この記事はversion 1。まぁそこまで大差はないと思う。

補完を有効にしなかった場合には、どのコマンドを使えば良いかの確認に aws help を使い、どのサブコマンドを使えば良いかの確認に aws <command> help を使い必須の引数をhelpで確認して、ようやく実行ができる。この種の作業にしばしばイライラさせられる1

補完の設定

awscliを利用するときに以下のような設定を追加すると補完が有効になる。これはbashの例2$(which aws_completer)と書くこともあるかもしれない。

complete -C '/usr/local/aws/bin/aws_completer' aws

これで補完が効いて満足となるかと思ったがそうでもなかった。

補完にかかる時間

TABを入力すれば補完される。この補完にはaws_completerが使われるが、補完が機能するまでにちょっとした遅延が存在する。その遅延の時間を測ってみると、手元の環境では0.6sから0.7s程度掛かっていた。

ちなみに今書いている環境が貧弱なのでこれくらい時間がかかるが、もう少しまともな環境なら0.2sから0.3sくらいになるのではないか。別の環境で測ったときにはその程度の時間だったような気がする。補完などの機能においては、これくらいの時間でさえもイライラする遅延として感じるようだ。

ほとんどの時間は awscli.completer モジュールのimport時間のようなので、もはや補完などの特定の機能については、深いところに置いたモジュールを読み込んだら負け、もっと言えばインタプリタを立ち上げたら負けなのかもしれない。

$ time python -c 'import awscli.completer'

real    0m0.421s
user    0m0.363s
sys     0m0.055s

$ time python -c 'import awscli.completer'

real    0m0.430s
user    0m0.369s
sys     0m0.055s

ちなみにimport後の処理で0.2sから0.3程度かかるようだ。

キャッシュして読み込む

この遅延をどうにかしたい。ということでテキトーな補完を書いてみた。どうやら、awscli.completerを使って補完の候補を生成しているようだ。そんなわけで候補を ~/.config/compgen/<profile>/<command>/<command name> あたりの場所に事前に生成しておき、補完時にはこれをcatで読み込むだけにしてみることにする。

以下のようなコードを書く。

import sys
import pathlib
import queue
import awscli.completer

dirpath = pathlib.Path("~/.config/compgen/default/aws").expanduser()
dirpath.mkdir(parents=True, exist_ok=True)

q = queue.Queue()
q.put(["aws"])

c = awscli.completer.Completer()
while not q.empty():
    path = q.get()
    if len(path) > 2:
        continue
    fpath = dirpath / ".".join(path)
    print(f"write {fpath}", file=sys.stderr)
    with open(fpath, "w") as wf:
        for line in c.complete(" ".join(path), point=None):
            print(line, file=wf)
            q.put([*path, line.strip()])

以下のようなファイルが生成される

$ ls ~/.config/compgen/default/aws/*
~/.config/compgen/default/aws/aws
~/.config/compgen/default/aws/aws.accessanalyzer
~/.config/compgen/default/aws/aws.acm
~/.config/compgen/default/aws/aws.acm-pca
~/.config/compgen/default/aws/aws.alexaforbusiness
~/.config/compgen/default/aws/aws.amplify
...
~/.config/compgen/default/aws/aws.workspaces
~/.config/compgen/default/aws/aws.xray

$ ls ~/.config/compgen/default/aws/* | wc
     236     236   12774

そして以下のような補完関数を書いてあげる3

# complete -C "$(which aws_completer)" aws

function _aws_completion(){
    local first profile datapath
    profile="default"
    cmdname="${COMP_WORDS[0]}"
    datapath="~/.config/compgen/${profile}/${cmdname}"
    local filepath
    case ${COMP_CWORD} in
        1)
            filepath="${datapath}/${cmdname}"
            if [ -f ${filepath} ]; then
                COMPREPLY=( $(compgen -W "`cat ${filepath}`" -- ${COMP_WORDS[COMP_CWORD]}) )
            else
                COMPREPLY=( $(compgen -f -- ${COMP_WORDS[COMP_CWORD]}))
            fi
            ;;
        2)
            filepath="${datapath}/${cmdname}.${COMP_WORDS[1]}"
            if [ -f ${filepath} ]; then
                COMPREPLY=( $(compgen -W "`cat ${filepath}`" -- ${COMP_WORDS[COMP_CWORD]}) )
            else
                COMPREPLY=( $(compgen -f -- ${COMP_WORDS[COMP_CWORD]}))
            fi
            ;;
        *)
            COMPREPLY=( $(compgen -f -- ${COMP_WORDS[COMP_CWORD]}))
            ;;
    esac
}
complete -o nosort -F _aws_completion aws
# complete -F _aws_completion aws

少し細工をして時間を測るようにしたところ、0.02sから0.03s程度になった。快適。 flagなどにも対応させたかったりするなーとは思ったりもした。

付録: 補完にかかる時間の計測方法

ちなみにどうやって時間を測ったかと言うと以下のようなコードに書き換えた。version 2はバイナリのようなのでこのようなことはできないかもしれないし。もう少し早いかもしれない。ただ、pythonで書かれていることは変わらないようだ。https://github.com/aws/aws-cli/tree/v2 がブランチなようなので。

--- ~/venvs/my/bin/aws_completer 2020-10-24 12:53:23.000000000 +0900
+++ /tmp/after    2020-10-24 12:54:07.000000000 +0900
@@ -13,6 +13,9 @@
 # language governing permissions and limitations under the License.
 
 import os
+import time
+
+st = time.time()
 if os.environ.get('LC_CTYPE', '') == 'UTF-8':
     os.environ['LC_CTYPE'] = 'en_US.UTF-8'
 import awscli.completer
@@ -27,3 +30,6 @@
         # If the user hits Ctrl+C, we don't want to print
         # a traceback to the user.
         pass
+    with open('/tmp/aws_completer', 'a') as wf:
+        import sys
+        print(time.time() - st, sys.argv, file=wf)

このときの結果が以下の様なものだった。

0.8783209323883057 ['~/venvs/my/bin/aws_completer', 'aws', '', 's3']
0.570117712020874 ['~/venvs/my/bin/aws_completer', 'aws', '', 's3']
0.6095981597900391 ['~/venvs/my/bin/aws_completer', 'aws', '', 'ecs']
0.637779951095581 ['~/venvs/my/bin/aws_completer', 'aws', '', 'ecs']
0.6307311058044434 ['~/venvs/my/bin/aws_completer', 'aws', '', 'ecs']
0.5969369411468506 ['~/venvs/my/bin/aws_completer', 'aws', '', 'ecs']
0.6082282066345215 ['~/venvs/my/bin/aws_completer', 'aws', '', 'ecs']
0.5688650608062744 ['~/venvs/my/bin/aws_completer', 'aws', 'servicedi', 'aws']
0.613184928894043 ['~/venvs/my/bin/aws_completer', 'aws', '', 'servicediscovery']
0.6116440296173096 ['~/venvs/my/bin/aws_completer', 'aws', '', 'servicediscovery']
0.6048803329467773 ['~/venvs/my/bin/aws_completer', 'aws', '', 'servicediscovery']
0.6156458854675293 ['~/venvs/my/bin/aws_completer', 'aws', '', 'servicediscovery']
0.6055679321289062 ['~/venvs/my/bin/aws_completer', 'aws', '', 'servicediscovery']
0.6923770904541016 ['~/venvs/my/bin/aws_completer', 'aws', '', 'servicediscovery']

キャッシュした方の時間の計測方法

以下のような形でナノ秒の表現でのdiffを出力していた。

local st
st="$(gdate +%N)"

echo "$(echo $(gdate +%N) - $st | bc -l)":${filepath} >> /tmp/aws_compgen

ここで 29666000 * (10 ** -9) = 0.029666

29666000:~.config/compgen/default/aws/aws
18947000:~.config/compgen/default/aws/aws
16910000:~.config/compgen/default/aws/aws
16431000:~.config/compgen/default/aws/aws
18454000:~.config/compgen/default/aws/aws.ecs
15494000:~.config/compgen/default/aws/aws.s3
15465000:~.config/compgen/default/aws/aws.s3
15542000:~.config/compgen/default/aws/aws.s3
21761000:~.config/compgen/default/aws/aws
16756000:~.config/compgen/default/aws/aws
16119000:~.config/compgen/default/aws/aws
16808000:~.config/compgen/default/aws/aws
16162000:~.config/compgen/default/aws/aws.servicediscovery
15574000:~.config/compgen/default/aws/aws.servicediscovery

  1. 他にもaws用のインタラクティブシェルとして立ち上がるaws-shellというパッケージもあるが、これはuiが自分には合わず途中でやめてしまった。

  2. 設定方法自体も公式のドキュメントに書かれている。 https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-completion.html#cli-command-completion-configure

  3. この補完のコード自体は洗練されたものでもなんでもない。