記事の一覧ページを直していた朝、途中から補完が返ってこなくなりました。型情報のツールチップも空のままです。言語サーバーを再起動すると数分だけ戻り、また同じところで沈みます。三度目の再起動を押したところで手が止まりました。
原因を設定ファイルの中に探していたのですが、私が一度も見ていなかったのは、もっと手前にある数でした。エディタが抱えているファイルが何本あるのか、その中でコードが何本なのかを、私は一度も数えていなかったのです。
その朝に走らせたコマンドと、出てきた数字をそのまま書き残します。
再起動が効かない朝は、遅い場所を取り違えています
補完が返らないとき、私たちはつい「言語サーバーが重い」と一括りにしてしまいます。ところがエディタの中では、少なくとも三つの層が別々の母数を見ています。
| 層 | 対象を決めているもの | 再起動で母数が変わるか |
|---|---|---|
| 言語サーバーのプロジェクト | tsconfig.json の include / exclude | 変わりません(設定が同じなら同じ範囲を読み直します) |
| ファイル監視とワークスペース索引 | ワークスペースのルートと除外設定 | 変わりません |
| エージェントのコード検索 | 追跡ファイルと .antigravityignore | 変わりません |
どの層が詰まっているのかは、症状の出方で見当がつきます。補完だけが返らず検索は普通に動くなら一番目、ファイルを保存した直後に全体が固まるなら二番目、エージェントに調べ物を頼んだときだけ長く待たされるなら三番目です。三つが同時に重いときは、たいてい共通の母数のほうに原因があります。
三つとも、再起動では母数が動きません。動くのは状態だけです。ですから再起動で数分だけ戻るという症状は、「状態は作り直せているのに、抱えている量が多すぎて同じ場所へ戻ってしまう」と読むほうが素直でした。
再起動は症状に、除外は母数に。 この線引きを置いてから、私は朝いちばんに数を取るようになりました。
まず、リポジトリを数えます
数えるのはひと言で終わります。追跡ファイルの総数と、その中でコードが何本かを並べるだけです。
# 追跡ファイルの総数
git ls-files | wc -l
# 拡張子の内訳(上位だけ)
git ls-files | sed 's/.*\.//' | sort | uniq -c | sort -rn | head -8私が触っているのは、記事を MDX で持っている Next.js のサイトです。出てきたのが次の表です。
| 区分 | ファイル数 | バイト数 | 全体に占める割合(バイト) |
|---|---|---|---|
| 追跡ファイル全体 | 2,300 | 34,383,649 | 100% |
.ts と .tsx | 71 | 470,510 | 1.4% |
content/ 配下(MDX) | 2,183 | 31,874,978 | 92.7% |
content/ と public/content/ を除いた残り | 117 | 2,523,152 | 7.3% |
ファイル数とバイト数は、必ず並べて見るようにしています。どちらか一方だけでは、重さの出どころを取り違えるからです。
| 区分 | ディレクトリ数 | ファイル数 | 1 ファイルの平均 |
|---|---|---|---|
content/ | 22 | 2,183 | 約 14,600 バイト |
src/ | 41 | 74 | 約 6,600 バイト |
content/ は 22 のディレクトリに 2,183 ファイルが詰まっています。ディレクトリ単位で張られる監視から見れば軽く、ファイル単位で回る索引から見れば重い、という偏った形です。「監視は問題ないのに索引だけ終わらない」という症状は、こういう形から出てきます。
ここで気持ちが切り替わりました。言語サーバーのプロジェクトは 71 ファイルしかありません。tsconfig.json の include は **/*.ts と **/*.tsx なので、MDX は一本も読まれていないのです。
つまり、私が三度も再起動していた相手は、そもそもいちばん軽い層でした。重かったのは、拡張子を選ばずに全部を抱える二番目と三番目の層です。
読み切るだけで何ミリ秒かかるのかを、床として持っておきます
母数が効いているかどうかは、いちばん素朴な指標で足ります。全ファイルを開いて読むだけの時間です。解析も索引も何もしない、下限の数字です。
import subprocess, time
out = subprocess.run(["git", "ls-files", "-z"], capture_output=True).stdout
files = [f.decode() for f in out.split(b"\0") if f]
code = [f for f in files if f.endswith((".ts", ".tsx", ".mjs", ".js"))]
def read_all(paths):
start = time.perf_counter()
for p in paths:
with open(p, "rb") as h:
h.read()
return time.perf_counter() - start
for label, paths in (("all", files), ("code-only", code)):
runs = sorted(read_all(paths) for _ in range(5))
print(f"{label:10s} n={len(paths):5d} best={runs[0]*1000:.1f}ms median={runs[2]*1000:.1f}ms")私の環境での出力です。
all n= 2300 best=28.8ms median=29.4ms
code-only n= 76 best=0.8ms median=0.8ms三十六倍でした。ただし、この数字の読み方には注意が要ります。これはページキャッシュが温まった状態で、開いて読むだけの床です。構文解析・索引・埋め込みの生成は、すべてこの上に積み上がります。初回起動や監視イベントが連続する場面では、この床の何十倍にもなります。
ですから私は、この数字を速度の証明としては使いません。体感で速くなった気がする、という判断を、私はあまり信用できなくなったと感じています。除外を書く前と後で同じコマンドを走らせて、母数がどれだけ落ちたかだけを見ています。効いたか効かなかったかが、体感ではなく差分で分かるからです。
除外を書いたのに効かなかった理由は、角括弧でした
さて、除外を書きます。.antigravityignore は gitignore と同じ書式ですから、書いたパターンが本当に当たるかどうかは git 自身に聞けます。判定器に聞くのがいちばん早いのです。
# 一時ファイルにパターンを 1 行だけ書いて、当たり判定を確かめる
printf '%s\n' 'src/app/[locale]/' > /tmp/probe-ignore
git -c core.excludesFile=/tmp/probe-ignore \
check-ignore --no-index -v 'src/app/[locale]/HomeClient.tsx'何も返ってきませんでした。当たっていません。念のため、当たらないはずのパスも並べて確かめた結果が次の表です。
| パターン | src/app/[locale]/HomeClient.tsx | src/app/l/HomeClient.tsx | src/app/x/page.tsx |
|---|---|---|---|
src/app/[locale]/ | 当たりません | 当たります | 当たりません |
src/app/\[locale\]/ | 当たります | 当たりません | 当たりません |
**/[locale]/** | 当たりません | 当たります | 当たりません |
角括弧が文字クラスとして解釈されていたのです。原因がパターンの書式ではなく記号の意味にあると気づいたのは、判定器へ一行ずつ聞き直したあとでした。[locale] は「l・o・c・a・e のいずれか一文字」を意味しますから、src/app/l/ や src/app/c/ には当たり、本物の src/app/[locale]/ には当たりません。除外したかったものは残り、意図していなかったディレクトリのほうが消えていた、という順序でした。
App Router を使っていると、この角括弧はあちこちに現れます。私のリポジトリでは、追跡ファイルのうち 21 本のパスに角括弧が含まれていました。[category] や [slug] も同じ扱いです。
書き直すなら、バックスラッシュでエスケープするか、角括弧を含まない上位のディレクトリを指定します。そして書いたあとは、残った側を一度数えます。
# 除外を適用したあとに何本残るかを数える
TOTAL=$(git ls-files | wc -l)
IGN=$(git ls-files -z \
| git -c core.excludesFile=/tmp/probe-ignore check-ignore --no-index --stdin -z \
| tr -cd '\0' | wc -c)
echo "tracked=$TOTAL ignored=$IGN remaining=$((TOTAL-IGN))"content/ と public/content/ の二行だけを書いたとき、私の手元では tracked=2300 ignored=2183 remaining=117 になりました。落ちた量が想定と合っているかは、この一行で確かめられます。
パターンの当たり判定そのもので詰まったときは、「.antigravityignore が効いていないときに疑う4か所」に、スラッシュの位置や否定パターンの順序をまとめてあります。あわせて読んでいただけると、原因の切り分けが早くなるはずです。
測り方も一言だけ添えておきます。私は五回走らせて最良値と中央値の両方を出しています。一回だけでは、ちょうど別のプロセスが走っていた瞬間を掴んでしまうことがあるからです。二つの値が離れているときは、母数ではなく同時に動いている何かを先に疑います。
そして、全体と code のみの差がほとんど無いのなら、母数は原因ではありません。そのときは一本の巨大なファイルや、生成された長い行を含むファイルを探すほうへ切り替えます。同じ 30 ミリ秒でも、二千本に分散しているのか一本に偏っているのかで、打つ手はまったく変わってきます。
除外してよい場所と、あえて残す場所
母数が分かると、次は「どこまで外すか」を決めることになります。私は三段に分けて考えています。
| 区分 | 例 | 扱い |
|---|---|---|
| 生成物 | .next/、out/、ビルド出力、カバレッジ | 迷わず外します(消えても書き直せます) |
| 依存 | node_modules/ | 多くの設定で既定から外れています。二重に書く必要はありません |
| 自分で書いた資産 | content/ の MDX、設計メモ | 外す前に、検索できなくなって困らないかを確かめます |
三段目でいちど失敗しました。content/ をまとめて外したところ、エージェントに「同じ題材の記事が既にないか」を探させたときに、一本も見つからなくなったのです。母数は軽くなりましたが、私がいちばん頻繁に頼んでいた仕事のほうが動かなくなりました。
外す前と後で、実際に使う検索をひとつ走らせておくと、この取り違えを避けられます。
# 除外を書く前に、当てにしている検索が何件返るかを控えておく
git grep -l 'antigravityignore' -- content/articles/ja | wc -l私の手元では 26 件でした。除外を入れたあとに同じ数が返らなければ、外しすぎです。
もうひとつ気をつけているのが、除外設定を役割ごとに分けておくことです。ファイル監視を軽くしたいのか、エージェントの検索母数を絞りたいのかで、書くべき場所は違います。同じ数行を両方へそのままコピーすると、片方では効きすぎ、片方では足りない状態になりがちでした。
いま置いている線引き
数え直してから、私の朝の手順は短くなりました。補完が返らないときは、まず追跡ファイルの総数とコードの本数を並べます。二桁の開きがあれば、原因は言語サーバーではなく、その周りで全部を抱えている層のほうにあります。
一方で、コードそのものが数千本ある本物の大規模リポジトリでは、話が逆になります。同じ症状に見えても、原因は真逆かもしれません。そのときは除外ではなく tsconfig.json の include を絞る側の問題です。どちらなのかを決めるためにも、最初の一回だけは数える価値があります。
今日できることを一つだけ挙げるなら、git ls-files | wc -l と、.ts と .tsx の本数を、同じ画面に並べて書き出すことです。エディタを離れずに走らせたい場合は、「Antigravity 2.10.0 の埋め込みターミナルで、エージェントの変更を検証するループを組む」で触れた内蔵ターミナルが便利です。
私はこの二つの数字を書き留めてから、除外を一行書くようにしています。最後までお読みくださり、ありがとうございました。