こんにちは。@kokukumaです。

昨日ふと調べたgitのブランチ名補完が異常に遅い原因について書きたいと思います。

現象

僕のwindows環境としては、conemuを入れて、その中でgitつかっています。

非常に鬱陶しいのが、ブランチの補完が異常に遅いことです。

git checkout ori TAB! TAB! ...................... 遅い。。。

git-complete.bashの中を確認する

とりあえず、git-complete.bashの中でどこが遅いのか読んでみます。

雰囲気で、git for-each-refが遅いと察することができます。

for-each-refとは、.git内にあるrefsを出力するためのgitコマンドです。詳しくは、for-each-refを参照のこと。

とりあえず、時間を測ってみます。

※ 以下、gitの挙動を知りたいだけなので、linux上で実施。

vagrant@debian-jessie:~/projects/lovelive$ time git for-each-ref --format="%(refname)" refs/tags refs/heads refs/remotes >/dev/null

real    0m0.064s
user    0m0.004s
sys     0m0.032s
vagrant@debian-jessie:~/projects/lovelive$ time git for-each-ref --format="%(refname:short)" refs/tags refs/heads refs/remotes >/dev/null

real    0m1.107s
user    0m0.008s
sys     0m0.496s

なぜか、shortつけただけで、すごく時間が伸びています。

システムコールを見てみる

原因を調べるため、straceを使って実行されているシステムコールのsummaryをとってみます。

vagrant@debian-jessie:~/projects/lovelive$ strace -cf git for-each-ref --format="%(refname)" refs/tags refs/heads refs/remotes > /dev/null
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.000907          17        54           openat
  0.00    0.000000           0       145           read
vagrant@debian-jessie:~/projects/lovelive$ strace -cf git for-each-ref --format="%(refname:short)" refs/tags refs/heads refs/remotes > /dev/null
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 87.62    0.015765           1     13131     13047 lstat
  6.27    0.001128          21        54           openat

でました。

ちょくちょく遅い原因となっているlstat。

参考: VirtualBoxのファイルシステムを10倍速くする ~ find編 ~

gitのコードを確認する

次に、なぜshortをつけただけでlstatが大量発行されるのかを、gitのコードを追って調べてみます。

// git-2.1.4/builtin/for-each-ref.c

static void populate_value(struct refinfo *ref)
{
(省略)
            if (!strcmp(formatp, "short"))
                refname = shorten_unambiguous_ref(refname,      // <- ここでrefnameを短くしているぽい。
                              warn_ambiguous_refs);
// git-2.1.4/refs.c
char *shorten_unambiguous_ref(const char *refname, int strict)
{
(省略)

    /* skip first rule, it will always match */
    for (i = nr_rules - 1; i > 0 ; --i) {
        (省略)
        /*
         * check if the short name resolves to a valid ref,
         * but use only rules prior to the matched one
         */
        for (j = 0; j < rules_to_fail; j++) {
            (省略)
            mksnpath(refname, sizeof(refname),
                 rule, short_name_len, short_name);
            if (ref_exists(refname))                    // <- ここの中でlstatしてる。
                break;
        }
        (省略)
}

わかったこと

shorten_unambiguous_refがやっていることは、refsをshort nameに変換する作業です。

例えば、refs/heads/branch_name を branch_name に変換する感じ。

やり方は単に、refs/headsとかに引っかかったら、その部分を削るだけです。

しかし短くすると、ブランチ名を一意にsha1に変換できなくなる可能性がでてきます。

  • refs/heads/branch_name -> f62a336
  • refs/tags/branch_name -> 865fe2e
  • git checkout branch_name => f62a336 ? or 865fe2e ?

そこでshorten_unambiguous_refでは、同じshort_nameで表すことができるrefsがあるか確認して、あったら短くしないと動いています。

これによって、for-each-refで出力される結果がすべて、一意にsha1に変換できることを保証しているわけです。

ただ、その存在確認のやり方が、

  • branch_nameにrefs/remotesとかrefs/tagsとかつけてpathをつくる
  • lstatして、そのpathにファイルがあるか確認する
  • open -> read -> sha1を確認する

となっているので、1つのrefsに対して、4、5回lstatを実行しています。

そのためレポジトリのブランチが増加すると、lstatの回数が増加して、1TABにつき1万回のlstat発行!とかになります。

そして、gitbashのlstatや、VM(over vboxsf)のlstatが凄く遅いせいで、windowsでブランチ補完が遅くなるという感じ。

対応

for-each-refを上手く直す方が正しいでしょうが、ちょっと大変そうなので、雑な対応で乗り切ることにしました。

:shortつけるのが遅い原因なら、:shortやめてsedでrefs/headを削る作戦。

zatu-git-complete.bash

これをすることで、一意にsha1に変換出来ないブランチ名が補完される可能性があります...

でもまぁ、remotesには基本originがつくでしょうし、ブランチ名と同じ名前のタグとかつけない(たぶん)ので大丈夫。

効果は体感で5倍。

zatu-git-complete.bashを手元に保存して、.bashrcとかに以下みたいに書いておけば使えます。

source (保存したpath)/zatu-git-complete.bash

まとめ

なんでwindowsのgitのブランチ補完が遅い原因が分かった。

雑対応で雰囲気速くなった。