KLabGames Tech Blog

KLabは、多くのスマートフォン向けゲームを開発・提供しています。 スマートフォン向けゲームの開発と運用は、Webソーシャルゲームと比べて格段に複雑で、またコンシューマゲーム開発とも異なったノウハウが求められる領域もあります。 このブログでは、KLabのゲーム開発・運用の中で培われた様々な技術や挑戦とそのノウハウについて、広く紹介していきます。

カテゴリ: git

こんにちは。@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のブランチ補完が遅い原因が分かった。

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

こんにちは。@kokukumaです。

みなさんは画像や音声データなど(以下、アセット)をどのように管理しているでしょうか?

簡単なversion管理ツールを使っている人もいれば、専用のアセット管理ツールを導入している人もいるでしょう。

私がいま在籍しているプロジェクトでは、他のクライアントコードと同様にgitで管理していました。

しかし、プロジェクトが進むにつれてアセットも増え、gitでは辛い場面も数多くでてきました。

  • cloneが遅すぎる。実行して一晩寝かせるレベル。
  • 容量がでかすぎる。12GBとかある...。消したい。
  • ふざけるな。git fetchが遅すぎる。

というような状態になり、やっぱりアセットをgitで管理するのは無理があるよねとなりました。

しかし、今から別のツールに移行するのも結構辛い。。

という事で、gitを使いつつ、もっと簡単にアセット管理をできる仕組みを考えてみました。

こんなのを作ることにした

  • クライアント-アセットの対応情報を取得できる

    クライアントとアセットの整合性が取れていなければ、動かない可能性があります。

    そのため、自分が開発しているクライアントに対応するアセットがどれかを知る必要があります。

    それを保存するためにDBとかを準備するのが面倒なので、gitレポジトリの中に保存してしまうことにしました。

    そして、この情報のみ頻繁にfetchするようにします。

  • 必要最小限のアセットだけfetchする

    一度に使うのは、特定のversion, 特定のplatform用のアセットです。

    そのため、上記の対応情報でわかったアセットだけを取得します。

    もちろん履歴はfetchしません。加えて、必要なplatformのディレクトリだけfetchする形にしました。

    これによって、実際のアセットを取得する際も、それほど時間をかけずに取得できます。

全体像を書くとこんな感じです。

図1. アセット管理の流れ

image

これで、fetchが重いとかローカルの容量が大きくなりすぎるといった問題を回避できそうです。

これらをどうやってgit上で行っているかを説明するために、gitの内部構造について軽く触れておきます。

gitの内部構造

『Pro Git』Chapter 10「Gitの内側」を読むと、Gitの中でどのように変更履歴やディレクトリ構造が保存されているのかがよくわかります。

要点をまとめると以下になります。

  • 変更履歴・ディレクトリ構造は、commit object, tree object, blob objectのグラフで表現されている。
  • tree objectには、ディレクトリ中にある、ディレクトリ名やファイル名が保存される。
  • blob objectには、ファイルのコンテンツが保存される。

つまり、dir1/hellodir1/worldfile2と3つのファイルが保存されている場合、以下の様に保存されることになります。

図2. gitの構造

image

クライアント-アセットの対応情報を取得する

gitの内部がこのような形であると分かれば、レポジトリに保存されているディレクトリ構造と関係なく、データを保存して置けることがわかります。

gitの配管コマンドを利用すれば、ファイルを作成・編集しなくても、indexファイルにデータを書き込みcommit objectを作成することが出来ます。

そして、tree構造を作ってしまえば、gitのコマンドを使って簡単に保存した値を取得することができます。

つまり、tree objectの構造をkeyとして、blob objectにvalueを保存する、簡単なkey value storeとしてgitレポジトリを利用することができるわけです。

以下、bashでこの操作をやるとこんな感じです。

# set_value (レコード名) (key) (value) .gitディレクトリで実行。
set_value refs/heads/ppack_index/record1 assets/iphone ppack_assets_3dd0_iphone

# get_value (レコード名) (key) .gitディレクトリで実行。
get_value refs/heads/ppack_index/record1 assets/iphone
# => ppack_assets_3dd0_iphone

function set_value(){
    export GIT_INDEX_FILE='tmp_index'; # まっさらなindex

    local RECORD=$1
    local KEY=$2
    local VALUE=$3
    local PARENT=""

    # 指定したレコードが既にあればそれをindexに読み込む。
    if [[ ! -z $(git show-ref $RECORD) ]]; then
        git read-tree $RECORD^{tree}
        PARENT="-p $RECORD"
    fi

    BLOB_HASH=$(echo ${VALUE} | git hash-object -w --stdin)       # valueを保存したblob objectを作成。
    git update-index --add --cacheinfo 100644 $BLOB_HASH $KEY     # keyをファイル名としてindexに登録。
    TREE_HASH=$(git write-tree)                                   # 今のindex内容でtree objesctを作成する。
    COMMIT_HASH=$(git commit-tree $TREE_HASH -m $RECORD $PARENT)  # treeにcommit objectを紐付ける。
    git cat-file -p $COMMIT_HASH^{tree}

    # RECORDブランチを作成
    git update-ref $RECORD $COMMIT_HASH

    rm tmp_index
    unset GIT_INDEX_FILE

}
function get_value(){
    local PPACK_INDEX=$1
    local KEY=$2
    VALUE=$(git cat-file -p $PPACK_INDEX:$KEY 2> /dev/null)  # あるcommitのファイルの中身を
    echo $VALUE
}

この中に、clientレポジトリのコミットと、それに対応する必要最小限のアセットを指定するコミットを紐づけるデータを保存しておきます。

1つのレコードには、以下の情報を登録しておきました。

key value
client_branch クライアントのブランチ名
client_commit クライアントのコミットhash
asset_commit assetのコミットhash
assets/iphone iphoneのアセットだけ取得するためのコミット
assets/android androidのアセットだけ取得するためのコミット

次に、iphone/androidだけを取得するコミットをどうやって作るかを説明します。

必要最小限のアセットだけfetchする

私の案件では、platform毎にディレクトリが分けられ、アセットが保存されていました。

そのため、必要最小限のアセットだけをfetchするためには、特定のディレクトリだけfetchする方法が必要です。

それを実現するために、clone/fetchするobjectがどのようにして選ばれるかを調べ、利用しました。

まず、通常cloneすると、図2のようにブランチが示すcommit objectから辿れる全てのオブジェクトを取得します。

一方、shallow cloneを実行すると、図3のように、履歴をたどらずに取得することができます。

図3. shallow cloneしたとき(赤黄緑だけcloneする)

image

ここから、clone/fetchする対象は、指定されたcommit objectとから辿ることが出来る範囲にしぼられることが分かります。

そのため、ディレクトリ単位でfetchするためには、cloneしたいディレクトリのtree objectを指すcommit objectを作ってやり、それをcloneの対象として指定すればよいことになります。

図4. ディレクトリ単位でgit cloneする時の図(赤黄緑だけcloneする)

image

以下、bashでこの操作をやるとこんな感じです。

# clone元レポジトリで事前にやっておくこと。
declare BASE_HASH="692c6c9"
declare TARGET_DIR="directory_name"

TREE_HASH=$(git rev-parse $BASE_HASH:$TARGET_DIR)
COMMIT_HASH=$(git commit-tree $TREE_HASH -m 'clone')
git tag -a clone_tag -m 'clone_tag' $COMMIT_HASH

# cloneするときの操作
git clone -b clone_tag (clone元レポジトリのurl)

このアセットfetch用のcommit objectを、クライアントのコードに対応するアセットとして情報を持っておけば、必要最小限のアセットを取得することができるようになります。

まとめ

gitの構造を利用してやることで、ファイル数が多すぎて重いレポジトリでも、負荷なく使える環境を作ることができました。

今後、もう少し汎用的なツールにまとめていきたいです。

※この記事は8/10に書かれたものです。現在では最新のGit2.5のWindows用バイナリも公式に公開されていますので、
 自前ビルドせずともGit2.5が利用できますが、次期バージョンでも同様の手順でビルドできると思われます。

@makki_dです。
普段はLinuxを使っていますが、WindowsでGitをビルドしてみたというお話です。
とても簡単にビルドすることができたので紹介したいと思います。

Git 2.5 を使いたいんです。

先日Git2.5が公開されました。
様々な修正のほか機能も多数追加されましたが、個人的には git worktree コマンドに注目しています。

git worktreeとは

あるブランチで作業をしているときそれを中断して、急ぎで別のブランチの修正をしなければならないことってよくありますよね。
そんな時皆さんはどうしていますか?

  1. 今の作業をとりあえずcommitstashしてブランチを切り替える
  2. もうひとつcloneしてきてそこで作業する
  3. git new-workdirで別ディレクトリに作業ツリーを作る
  4. git worktreeで別ディレクトリに作業ツリーを作る ← New!!

git worktreegit new-workdirと同じく、別ディレクトリに作業ツリーを作ることができるコマンドです。
作業ツリーを別に作ることで、今進めている作業をそのまま置いておきながら、同じリポジトリの別ブランチの作業を別ディレクトリで行うことができます。

具体的な使い方はドキュメントの例がわかりやすいです。

これまでもgit new-workdirがありましたが、このコマンドはシンボリックリンクを作成するため、Windowsの場合一般ユーザ権限では利用できませんでした
一方、git worktreeではシンボリックリンクではなく、ファイル中にパスを記録する形で作業ツリーと親リポジトリの関係が管理されるので、一般ユーザでも利用することができます。

公式のWindows用バイナリは1.9.5 (※8/10時点)

早速 Git 2.5 をWindowsでも使ってみたいところですが、公式のDownloadページには、Windows向けバイナリは1.9.5までしか用意されていません。
そして「新しいバージョンが使いたかったら、ソースからビルドしてね」とさらっと書かれています。

Windows上でOSSのビルド環境を整えるというと多くの場合苦行となるような印象がありますが、Gitの場合は驚くほど簡単に開発環境がセットアップできました。

WindowsでのGitビルド環境

Windows用のバイナリと開発環境は、Git for Windowsで入手できます。
紛らわしいですが、「git for windows」等で検索すると上位に出てくる msysGit は 1.9.5 までしかありません。
特に2系を使いたい場合は、間違えずに後継プロジェクトであるGit for Windowsを見てください。

Git for Windows SDKのダウンロード・インストール

インストーラを実行すると、指定したインストールディレクトリ以下に gcc を始めとした開発ツール・環境・ライブラリ一式がダウンロード・インストールされます。
64bit版SDKのデフォルトのインストールパスは C:\git-sdk-64 なのでそのまま記載しますが、変更した場合は適宜読み替えてください。

インストールパス
ネットワークインストール

インストールが終わるとそのまま Git のビルドが始まりますが、私の環境では gettext 関連のビルドでエラーとなりました。

ビルドエラー

エラーとなったコマンドプロンプトを一旦終了し、msysのシェル (C:\git-sdk-64\mingw64_shell.bat) で改めて開き直します。
gitのソースディレクトリ (/usr/src/git) でmakeを実行すると、先ほどのエラーに引っかからずにビルドできます。

ビルド成功

ビルドできたらmake instalすると C:\git-sdk-64\mingw64\bin に実行ファイル一式がインストールされます。
ここにPATHを通すことで、gitコマンドがコマンドプロンプトやcygwinから使えるようになります。

バージョン2.4.6??

執筆時点のmasterブランチでビルドすると、git のバージョンが2.4.6になっていました。

version

これではgit worktreeが使えない!?と一瞬焦りましたが、よくよくコミットグラフを確認すると、v2.5.0タグのブランチがまるごとmergeされてるため、使いたかったgit worktreeコマンドも使えました。

worktree

ちょっとパッチを当ててみる

git worktreeで作った作業ツリーのサブディレクトリの中でalias登録したgitコマンドを呼び出すと、次のようなエラーが出てしまいます。

makiuchi-d@PC-1034 MINGW64 ~/Projects/test/work1/a (work1)
$ git br
fatal: internal error: work tree has already been set
Current worktree: C:/Users/makiuchi-d/Projects/test/work1
New worktree: C:/Users/makiuchi-d/Projects/test/work1/a

これでは不便なので、エラーを出している部分に即席パッチをあててみようと思います。

diff --git a/environment.c b/environment.c
index fb4eda7..8cf1442 100644
--- a/environment.c
+++ b/environment.c
@@ -226,7 +226,7 @@ void set_git_work_tree(const char *new_work_tree)
 {
        if (git_work_tree_initialized) {
                new_work_tree = real_path(new_work_tree);
-               if (strcmp(new_work_tree, work_tree))
+               if (strncmp(new_work_tree, work_tree, strlen(work_tree)))
                        die("internal error: work tree has already been set\n"
                            "Current worktree: %s\nNew worktree: %s",
                            work_tree, new_work_tree);

※あくまで応急措置です

この修正を加えた上でmakemake installすることで、動くようになりました。

動作確認

まとめ

GitはWindowsでも簡単に開発環境をセットアップできます。
いち早く新機能を試したいときや、ちょっとした修正をしたい場合など、ぜひ試してみてください。

P.S.
8/18、Git 2.5のWndows用バイナリが公式に公開されました。

読者の皆さまが普段使っているバージョン管理システムは何でしょうか?多くの会社さんと同様、KLabでは大多数のプロジェクトでGitを利用しています。

Gitでは全てのcommitについて名前とメールアドレスが記録されます。ところで、Git管理しているリポジトリ上で会社のメールアドレスと個人のメールアドレスが混ざることがありませんか?

KLab社内では大半のプロジェクトでGitHub Enterpriseを利用している一方、一部プロジェクトや公開用のリポジトリについてはgithub.comも併用しており、それぞれで登録メールアドレスが異なっていたりするため、間違いが起こりやすい状況になっています。

本稿では、そんなときでもリポジトリごとに適切なメールアドレスでcommitできるような~/.gitconfigの書き方を紹介します。

具体的な手順

今回紹介する手順は、リポジトリをgit cloneするタイミングでuser.nameおよびuser.emailを自動的にgit config --localで設定するようにした、というものです。また、その際の設定値をスクリプト側に書かず、~/.gitconfigに書けるようにしました。

手順としては、まず~/.gitconfigを次のように変更または追記します。

[init]
        templatedir = ~/.git_templates/
[user]
        name = Git Hanako
        email = git@example.com
[user "ssh://git@ghe.example.jp/"]
        email = hanako@ghe.example.jp
[user "https://ghe.example.jp/"]
        email = hanako@ghe.example.jp

ただし、[user]ブロックに書くメールアドレスはデフォルトのメールアドレス、[user <URL>]ブロックに書くメールアドレスはリポジトリのURLに対応するメールアドレスです。

次に、この設定通りにGitのローカル設定を変更するためのフックスクリプトを設置します。次のように~/.git_templates/hooks/post-checkoutを作成します。

#!/bin/sh

PREVIOUS_HEAD=$1
NEW_HEAD=$2
BRANCH_SWITCH=$3
Z40="0000000000000000000000000000000000000000"

# Continue only when "git clone" has been executed.
if [ "$PREVIOUS_HEAD" != "$Z40" -o "$BRANCH_SWITCH" != "1" ]; then
  exit
fi

origin_name="$(git remote | head -1)"
current_remote_url="$(git config --get --local remote.$origin_name.url)"
default_name="$(git config --get user.name)"
default_email="$(git config --get user.email)"
local_name="$(git config --local --get user.name)"
local_email="$(git config --local --get user.email)"

if [ "$current_remote_url" ]; then
    case $current_remote_url in
        *://*)
            # Normalize URL: remove leading "git+"
            #   e.g. "git+ssh://user@host/path/" ==> "ssh://user@host/path/"
            current_remote_url=$(echo $current_remote_url | sed 's/^git\+//')
            ;;
        *:*)
            # Convert scp-style URL to normal-form
            #   e.g. "user@host:path/" ==> "ssh://user@host/path/"
            current_remote_url=$(echo $current_remote_url | sed 's/\(.*\):/ssh:\/\/\1\//')
            ;;
    esac
    if [ -z "$local_name" ]; then
        name="$(git config --get-urlmatch user.name $current_remote_url)"
        if [ "$name" != "$default_name" ]; then
            git config --local user.name "$name"
        fi
    fi
    if [ -z "$local_email" ]; then
        email="$(git config --get-urlmatch user.email $current_remote_url)"
        if [ "$email" != "$default_email" ]; then
            git config --local user.email "$email"
        fi
    fi
else
    echo "No remote URL"
fi

こうすることで、git cloneのタイミングでuser.nameuser.emailが必要に応じて設定されるというわけです。

ただし、git initしてgit remote addしたような場合には対応できないので、注意が必要です。

~/.gitconfig にはURLごとの設定が書ける

スクリプトだけ紹介しても寂しいので、仕組みも紹介します。

Gitの設定は~/.gitconfigに記述できますが、 Git 1.8.5からはリポジトリURLごとの設定が書けるようになりました。git-configのman pageには次のような例が掲載されています。

; HTTP
[http]
    sslVerify
[http "https://weak.example.com"]
    sslVerify = false
    cookieFile = /tmp/cookie.txt

これはリポジトリのホスト名によってhttp.sslVerifyを勝手に切り替えるような例です。設定値によっては、このように書くだけで勝手に設定を使い分けることができます。

一方で、user.emailなどの設定はURLに紐付いているわけではないため、~/.gitconfigを設定しただけでは反映されません。とはいえ、同じ枠組みを利用すればシンプルに設定が記述できますし、git config--get-urlmatchオプションを使えば簡単に値が取り出せますから、今回はこれをgit cloneのフックスクリプトから利用したというわけです。

この仕組みを使えば、特定オーガナイゼーションのみ、もしくは特定リポジトリのみといった設定も可能ですので、他にも応用ができそうですよね。便利な使い方を思いついた方はぜひ教えてください!


@hnw

↑このページのトップヘ