Skip to content

branch -l 在中等規模倉庫需 25s:只讀查詢走了寫操作邊界(雙快照逐文件 IPC) #574

Description

@genedna

現象

在本倉庫(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 慢的主因):只讀查詢走了寫邊界

  1. src/cli.rs:1917:Commands::Branch(_) => Repository,無條件——-l、-r、--show-current 也一樣。
  2. src/cli.rs:2094-2098:Repository => RepoMutation;唯一的讀例外是 src/cli.rs:2066 的 set_upstream_is_idempotent,-l 命中不了。
  3. src/cli.rs:3630-3651:非 ReadOnly 且不在 command_has_existing_operation_boundary(src/cli.rs:2106)中的命令,一律包 run_with_operation 持久事務。
  4. 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。
  5. 快照最貴: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。
  6. /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——正是操作邊界的持有物。
  7. 排除 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

修復方向(最小改動建議)

  1. 給 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 同理审计。
  2. log/rev-list 下推 limit:無 path/grep/pickaxe 等需全量過濾時,depth = skip + number 或改懶惰迭代提前終止,避免 -1 加載 4068 提交。
  3. 快照對未改動文件加 mtime+index 短路(復用 status 的跳過邏輯),或對 ReadOnly 邊界跳過快照;現狀是 branch -l 這種不碰工作區的命令也要哈希全部文件兩遍。
  4. 清理 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 排除)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingvcs

Type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions