現象
在本倉庫(2463 個文件、4068 個提交)執行 libra branch -l 需要 25 秒以上,遠超 5 秒預期:
$ time ./target/debug/libra branch -l
* main
real 0m27.061s
user 0m8.641s
sys 0m15.951s
/usr/bin/time -v 顯示 Maximum resident set size: 232MB,Voluntary context switches: 75163。
對照測量(同一 debug 二進制)
| 命令 |
分類 |
耗時 |
rev-parse HEAD |
ReadOnly |
37ms |
cat-file -p HEAD |
ReadOnly |
41ms |
config user.name |
ReadOnly |
68ms |
ls-files |
ReadOnly |
0.37s |
status / status --porcelain |
ReadOnly |
~0.9s |
diff --stat |
ReadOnly |
~1.1s |
show-ref / for-each-ref(240 個 ref) |
ReadOnly |
~2.5–2.8s |
show --stat HEAD --oneline(單提交) |
ReadOnly |
0.367s |
branch -l / branch --show-current / branch -r |
Repository |
~25–26s |
tag -l / remote -v / reflog / notes list |
Repository |
~25–28s |
log --oneline -1 / rev-list --max-count=5 HEAD |
ReadOnly |
~14s |
小倉庫(1 文件 1 提交)branch -l |
Repository |
119ms |
小倉庫 log -1 |
ReadOnly |
45ms |
關鍵分叉點:
- 所有 Repository 域命令都慢(
remote -v 25.5s、reflog 25.9s、notes list 25.8s),ReadOnly 命令都快(config user.name 68ms)。branch 自身邏輯只有一次 reference 表查詢,不可能是 25s。
log 是例外:它是 ReadOnly 但也慢(另一個獨立 bug,見下)。
根因 1(branch -l 慢的主因):只讀查詢走了寫邊界
src/cli.rs:1917:Commands::Branch(_) => Repository,無條件——-l、-r、--show-current 也一樣。
src/cli.rs:2094-2098:Repository => RepoMutation;唯一的讀例外是 src/cli.rs:2066 的 set_upstream_is_idempotent,-l 命中不了。
src/cli.rs:3630-3651:非 ReadOnly 且不在 command_has_existing_operation_boundary(src/cli.rs:2106)中的命令,一律包 run_with_operation 持久事務。
src/internal/operation/middleware.rs:334 的 run_with_persistent_operation 做:取 lease(middleware/lease.rs:119,189)→ read_heads_view → WorkspaceStatePointer::load → pre 快照(snapshot.rs:290)→ capture_reference_state(全表 SELECT ... FROM reference)→ 寫 operation/journal/view → 跑業務 → post 快照 → 寫 view/journal/CAS/pointer;src/cli.rs:3661 出邊界後還要 BackgroundIndexDrainGuard::finish 等最多 60s。
- 快照最貴:
snapshot.rs:221-271 對 list_visible_files 返回的每個文件無條件 hash_file,無 index/mtime 短路(status 有批處理+跳過未改動文件);hash_file(snapshot.rs:526-563)每個文件一次 WorktreeIo::submit_absolute(executor.rs:269),經 dispatcher 線程 + worker 子進程管道往返。本倉庫 2463 文件 × pre/post 兩次 ≈ 5000 次 IPC。
/proc 採樣佐證:branch --show-current 運行期間單進程 78 線程,rchar 16 秒內漲到 256MB、syscr 漲到 2000 萬次(平均每次 ~12B),read_bytes 長期為 0(全是 page-cache + IPC,不是磁盤)。一個工作線程以 ~130 萬次/秒做 8B 小讀,符合「逐文件 IPC 任務風暴」特徵。FD 快照顯示進程持有 maintenance.lock、operation-v2-repository.lock、info/operation-v2.lock、libra.db、objects/pack/*.idx——正是操作邊界的持有物。
- 排除 lease:
LIBRA_INTERNAL_REPOSITORY_REF_LEASE_HELD=1 LIBRA_INTERNAL_OPERATION_SCOPE_LEASE_HELD=1 下耗時不變(26.1s vs 26.2s)。
根因 2(log/rev-list 慢,同倉庫的另一個獨立 bug,供對照)
src/command/log.rs:1473-1474:get_reachable_commits_excluding(start, excludes, None, ...) 的 depth 寫死 None,即全量遍歷(本倉庫 rev-list --count HEAD = 4068 提交),之後才在 src/command/log.rs:1485-1495 按 number/skip 截斷。log -1 也要加載全部 4068 個 commit(14s),而 show HEAD(單點加載)只要 42ms。
- 這與
branch 無關(branch -l 無 filter 時不走 commit_contains/resolve_reachable_for_merge,branch.rs:3146 在 spec=None 時直接回 None),但解釋了為什麼同為 ReadOnly 的 log 也慢。
次要 hygiene:22k 個殘留鎖文件
.libra/object-index-repair-locks/ 有 22784 個 *.lock(du -sb 僅 2.6MB 實際字節,但佔 ~88M 磁盤塊;object-index-repair/ 本體已空、僅剩 1.2M 目錄項)。本次 preflight 重放讀的是 marker 目錄(已空,很快),所以不是本次 25s 的主因;但每次取 shard 鎖 / 列目錄時都要掃 2 萬文件的目錄項,是潛在長尾風險,建議做殘留清理。
復現步驟
# 大倉庫(本倉庫,2463 文件 / 4068 提交)
time ./target/debug/libra branch -l # ~25s
time ./target/debug/libra branch --show-current # ~25s
time ./target/debug/libra remote -v # ~25s(同為 Repository 域)
time ./target/debug/libra config user.name # 68ms(ReadOnly 對照)
time ./target/debug/libra log --oneline -1 # ~14s(獨立的全量遍歷 bug)
# 小倉庫對照
rm -rf /tmp/tinyrepo && mkdir -p /tmp/tinyrepo
/target/debug/libra init /tmp/tinyrepo # 在 /tmp/tinyrepo 內執行
# cd /tmp/tinyrepo && libra add file.txt && libra commit -m init
# branch -l → ~119ms,log -1 → ~45ms
/proc 採樣方法(無 strace/perf 環境下):
./target/debug/libra branch --show-current &
PID=$!
for i in $(seq 1 8); do sleep 2; cat /proc/$PID/io; grep -E "State|Threads" /proc/$PID/status; done
# 觀察 rchar/syscr 暴漲、read_bytes 長期為 0、Threads ~78
修復方向(最小改動建議)
- 給
branch 加只讀判定,仿 config_command_is_read_only(src/cli.rs:2215)和 Stash(List|Show) => ReadOnly(src/cli.rs:1820)先例:當 new_branch/delete/rename/copy/set_upstream_to/... 全空且僅是 -l/-r/-a/--show-current/--contains/--sort/--format/--column/-v 查詢時,command_scope 走 ReadOnly / operation_class_for_command 回 ReadOnly,跳過雙快照。tag -l、remote -v、reflog、notes list 同理审计。
log/rev-list 下推 limit:無 path/grep/pickaxe 等需全量過濾時,depth = skip + number 或改懶惰迭代提前終止,避免 -1 加載 4068 提交。
- 快照對未改動文件加 mtime+index 短路(復用 status 的跳過邏輯),或對 ReadOnly 邊界跳過快照;現狀是
branch -l 這種不碰工作區的命令也要哈希全部文件兩遍。
- 清理
object-index-repair-locks 殘留,並給鎖文件加 TTL/後台回收。
環境
libra 0.23.65(debug 構建,target/debug/libra 1.1G)
- Linux,倉庫
.libra:libra.db 12M、objects 676M(28052 個 loose object + pack)、reference 240 行(Branch 26 / Head 2 / Tag 212)、object_index ~28k 行、operation/operation_journal 各 1103 行
- 工作區(排除
.libra/target):119M、2463 文件;target/ 1.4T(已被 .libraignore 排除)
現象
在本倉庫(2463 個文件、4068 個提交)執行
libra branch -l需要 25 秒以上,遠超 5 秒預期:/usr/bin/time -v顯示Maximum resident set size: 232MB,Voluntary context switches: 75163。對照測量(同一 debug 二進制)
rev-parse HEADcat-file -p HEADconfig user.namels-filesstatus/status --porcelaindiff --statshow-ref/for-each-ref(240 個 ref)show --stat HEAD --oneline(單提交)branch -l/branch --show-current/branch -rtag -l/remote -v/reflog/notes listlog --oneline -1/rev-list --max-count=5 HEADbranch -llog -1關鍵分叉點:
remote -v25.5s、reflog25.9s、notes list25.8s),ReadOnly 命令都快(config user.name68ms)。branch自身邏輯只有一次reference表查詢,不可能是 25s。log是例外:它是 ReadOnly 但也慢(另一個獨立 bug,見下)。根因 1(
branch -l慢的主因):只讀查詢走了寫邊界src/cli.rs:1917:Commands::Branch(_) => Repository,無條件——-l、-r、--show-current也一樣。src/cli.rs:2094-2098:Repository => RepoMutation;唯一的讀例外是src/cli.rs:2066的set_upstream_is_idempotent,-l命中不了。src/cli.rs:3630-3651:非 ReadOnly 且不在command_has_existing_operation_boundary(src/cli.rs:2106)中的命令,一律包run_with_operation持久事務。src/internal/operation/middleware.rs:334的run_with_persistent_operation做:取 lease(middleware/lease.rs:119,189)→read_heads_view→WorkspaceStatePointer::load→ pre 快照(snapshot.rs:290)→capture_reference_state(全表SELECT ... FROM reference)→ 寫 operation/journal/view → 跑業務 → post 快照 → 寫 view/journal/CAS/pointer;src/cli.rs:3661出邊界後還要BackgroundIndexDrainGuard::finish等最多 60s。snapshot.rs:221-271對list_visible_files返回的每個文件無條件hash_file,無 index/mtime 短路(status有批處理+跳過未改動文件);hash_file(snapshot.rs:526-563)每個文件一次WorktreeIo::submit_absolute(executor.rs:269),經 dispatcher 線程 + worker 子進程管道往返。本倉庫 2463 文件 × pre/post 兩次 ≈ 5000 次 IPC。/proc採樣佐證:branch --show-current運行期間單進程 78 線程,rchar16 秒內漲到 256MB、syscr漲到 2000 萬次(平均每次 ~12B),read_bytes長期為 0(全是 page-cache + IPC,不是磁盤)。一個工作線程以 ~130 萬次/秒做 8B 小讀,符合「逐文件 IPC 任務風暴」特徵。FD 快照顯示進程持有maintenance.lock、operation-v2-repository.lock、info/operation-v2.lock、libra.db、objects/pack/*.idx——正是操作邊界的持有物。LIBRA_INTERNAL_REPOSITORY_REF_LEASE_HELD=1 LIBRA_INTERNAL_OPERATION_SCOPE_LEASE_HELD=1下耗時不變(26.1s vs 26.2s)。根因 2(
log/rev-list慢,同倉庫的另一個獨立 bug,供對照)src/command/log.rs:1473-1474:get_reachable_commits_excluding(start, excludes, None, ...)的depth寫死None,即全量遍歷(本倉庫rev-list --count HEAD= 4068 提交),之後才在src/command/log.rs:1485-1495按number/skip截斷。log -1也要加載全部 4068 個 commit(14s),而show HEAD(單點加載)只要 42ms。branch無關(branch -l無 filter 時不走commit_contains/resolve_reachable_for_merge,branch.rs:3146在spec=None時直接回None),但解釋了為什麼同為 ReadOnly 的log也慢。次要 hygiene:22k 個殘留鎖文件
.libra/object-index-repair-locks/有 22784 個*.lock(du -sb僅 2.6MB 實際字節,但佔 ~88M 磁盤塊;object-index-repair/本體已空、僅剩 1.2M 目錄項)。本次 preflight 重放讀的是 marker 目錄(已空,很快),所以不是本次 25s 的主因;但每次取 shard 鎖 / 列目錄時都要掃 2 萬文件的目錄項,是潛在長尾風險,建議做殘留清理。復現步驟
/proc採樣方法(無 strace/perf 環境下):修復方向(最小改動建議)
branch加只讀判定,仿config_command_is_read_only(src/cli.rs:2215)和Stash(List|Show) => ReadOnly(src/cli.rs:1820)先例:當new_branch/delete/rename/copy/set_upstream_to/...全空且僅是-l/-r/-a/--show-current/--contains/--sort/--format/--column/-v查詢時,command_scope走 ReadOnly /operation_class_for_command回ReadOnly,跳過雙快照。tag -l、remote -v、reflog、notes list同理审计。log/rev-list下推 limit:無 path/grep/pickaxe 等需全量過濾時,depth = skip + number或改懶惰迭代提前終止,避免-1加載 4068 提交。branch -l這種不碰工作區的命令也要哈希全部文件兩遍。object-index-repair-locks殘留,並給鎖文件加 TTL/後台回收。環境
libra 0.23.65(debug 構建,target/debug/libra1.1G).libra:libra.db12M、objects676M(28052 個 loose object + pack)、reference240 行(Branch 26 / Head 2 / Tag 212)、object_index~28k 行、operation/operation_journal各 1103 行.libra/target):119M、2463 文件;target/1.4T(已被.libraignore排除)