perf(bench): profiler RAM + workflow đo memory trên CI - #38
Merged
Merged
Conversation
`GraphIndex` là in-memory-first: `rebuild()` nạp toàn bộ symbols/chains/ call_names/edges vào HashMap. CodSpeed chỉ đo thời gian nên cái giá RAM này không ai thấy — thêm đường đo riêng trước khi tối ưu. - `codegraph-graph/src/meminfo.rs`: đọc RSS std-only (Linux `/proc/self/statm` + `VmHWM`, macOS fallback `ps -o rss=`), kèm `MemTracker` lấy mẫu theo phase. Không thêm dependency. - `codegraph-bench/src/bin/mem`: đo RSS theo extract → index → query trên repo thật, đồng thời dự đoán RAM theo cấu trúc (`count × size_of::<T>()`) để quy kết quả về đúng HashMap nào. Chế độ `--synthetic N` sinh index deterministic (không phụ thuộc network) với quy mô đủ để `edges`/`call_names` chiếm RAM đáng kể. - `.github/workflows/memory.yml`: chạy profiler, in bảng vào job summary, upload artifact. Không chặn merge — GitHub runner là VM dùng chung nên RSS chỉ dùng so sánh tương đối, phần `predicted` mới là số ổn định. Còn lại: tối ưu `edges`/`call_names`/`symbols` ra khỏi RAM (storage-backed lazy). PR này mới phần đo, chưa đụng storage trait.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Báo qua CI của PR #38: - `meminfo.rs`: tách `rss_linux()` riêng để `rss_bytes()` không có `return` (clippy `needless_return`, fail cả job clippy lẫn slim build). - `meminfo.rs`: `FALLBACK_PAGE_SIZE` chỉ dùng trong nhánh linux → gate `#[cfg(target_os = "linux")]` (dead_code trên Windows). - `bin/mem.rs`: bổ sung import `Annotation`, `CallRecord`, `EffectType`, `ScopeLevel`, `SymbolKind` — patch import đầu tiên không áp dụng nên các kiểu này chưa có tên trong scope.
Chạy `mem` với nhiều repo trong cùng process làm RSS cộng dồn: repo sau đo trên nền của repo trước nên `rss_after_extract` không còn đọc được (synthetic thì không dính vì chỉ đo 1 index). Tách từng repo ra process riêng — chậm hơn vài giây nhưng số đo đúng.
Baseline đo được trên CI cho thấy 320 MiB `Δ index` với 50k symbol / 200k edge, nhưng `symbols + edges` chỉ quy được 29.7 MiB — 91% nằm ở nơi khác. Không có cách nào chọn đúng cấu trúc cần tối ưu khi chỉ có tổng RSS. - `memtrack.rs`: deep size từng cấu trúc in-memory của `GraphIndex` (`symbols`, `chains_map`, `call_names`, `edges`, `name_index`, `scope_index`, `name_keys`, `files`). Heap của `String`/`Vec` đo exact bằng `capacity()`; bucket `HashMap` ước lượng theo công thức hashbrown. Ghi rõ trong doc phần nào exact / ước lượng / không tính. - `LruCache::len` / `is_empty` (DashMap::len O(1)) + `Storage::cache_occupancy` (default `Vec::new()`, chỉ `CachedStorage` override) → biết LLR đang giữ bao nhiêu entry. - `GraphIndex::mem_breakdown()` gom tất cả lại, `ranked()` sort giảm dần để thứ tự tối ưu là thứ tự xếp hạng. - `bin/mem`: in bảng breakdown + JSON (`breakdown`, `accounted_total`, `caches`). Chưa đổi hành vi production: các method mới đều chỉ đọc, không vào hot path.
CI báo 4 lỗi E0599 + 1 E0631: helper deep-size nhận `&str`/`&[T]` nhưng gọi `.capacity()` — method đó chỉ có trên `String`/`Vec`. Đổi helper sang `&String`/`&Vec<T>` và thêm `#[allow(clippy::ptr_arg)]` kèm lý do: cần bytes **đã cấp** (`capacity`), slice không đọc được — dùng `len` sẽ lệch tới ~2× do `Vec` over-allocate.
…ort_by - `LruCache::is_empty` không nơi nào gọi → bỏ (chỉ cần `len` cho occupancy). - `.map(|x| f(x))` → `.map(f)` ở 4 chỗ trong `memtrack`. - `ranked()` dùng `sort_unstable_by` + tiebreaker theo tên: vừa tránh `stable_sort_primitive`, vừa giữ thứ tự **deterministic** khi hai cấu trúc trùng bytes (sort thuần tuỳ thứ tự ban đầu → báo cáo khác nhau giữa 2 lần chạy).
Breakdown hiện tại chỉ tính HashMap của `GraphIndex` (97.5 MiB / 320 MiB Δ). ~223 MiB còn lại nằm ở hai radix engine — sống trong `InMemoryStorage` chứ không phải HashMap nên `mem_breakdown` không thấy. Cách đo: dựng cùng 50k symbol nhưng bỏ dần từng thành phần, lấy hiệu RSS. - `symbols` — chỉ symbol → `symbols` + `name_index` + **name engine** - `chains` — + chain, không call record → **chain engine** (trie + rt_shortcuts) - `full` — + call record → `call_names` + `edges` Không đụng `Storage` trait nên không có rủi ro 6 backend; đổi hoàn toàn ở tầng benchmark. Kết quả quyết định có nên tối ưu `call_names` (30 MiB) hay không.
… peak Sửa lỗi phương pháp: `rss_after_index` đo **high-water của allocator**, không phải live memory. Trong `ingest` tồn tại 3 bản sao call song song — `results` (borrow, `lib.rs:983`) → `all_calls` (clone, `:1054`) → `recs_by_caller` (clone, `:1389`) — rồi cả hai bản sao cuối bị drop nhưng allocator không trả arena về OS. Nên "137 MiB do call records" ở lượt trước là đỉnh lúc ingest, không phải chi phí giữ lại sau restart. `--mode open`: ingest vào sqlite → `drop(index)` → **`drop(parsed)`** → `open` lại. `open` chỉ chạy `rebuild()` (nạp blob + dựng HashMap) nên không có bản sao tạm nào; `rss_after_open − rss_before_open` là chi phí thường trực thật. Chạy cả `--mode ingest` và `--mode open` cho 3 shape trong CI để so trực tiếp: hiệu giữa hai mode = phần transient, còn mode open = phần phải giữ.
`ingest` chỉ cần sửa đúng một field của `CallRecord` là `caller_id` (sang id global) nhưng lại clone toàn bộ struct (176 B + heap) hai lần: - `all_calls: Vec<CallRecord>` → `Vec<CallRef>` (16 B, mượn từ `ParseResult` của caller vốn sống suốt `ingest` + `caller_id` đã remap). - `recs_by_caller: HashMap<u64, Vec<CallRecord>>` → `Vec<u32>` (index vào `calls`), tra lại qua `calls[i].rec`. 200k call record: 2 × ~43.6 MiB → ~4 MiB, tiết kiệm ~83 MiB / peak 319.7 MiB (~26%) theo số đo `--mode ingest` shape `full`. Tiền lệ có sẵn: `resolve_calls` đã dùng `Vec<&CallRecord>` để group. Ghi chú: `caller_id` trong blob persist giờ mang id **local** thay vì global. Vô hại — `flow`, `rebuild_edges`, `bingraph` đều lấy caller id từ key của blob, không đọc field đó; đã ghi chú tại chỗ ghi. Test mới `call_records_grouped_by_global_caller_id`: hai file cùng dùng `SYMBOL_BASE` làm caller local, chặn bug gom nhầm theo id local. Kèm: `--mode open` trong CI hạ 50k → 5k (rebuild() còn nạp `all_call_records()` rồi deserialize lần hai, 50k mất ~1h40m/shape ≈ 5h/job), và sửa 3 comment mô tả "3 bản sao call" đã lỗi thời.
Codecov Report❌ Patch coverage is ❌ Your patch status has failed because the patch coverage (37.79%) is below the target coverage (100.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #38 +/- ##
==========================================
- Coverage 78.44% 77.13% -1.31%
==========================================
Files 88 91 +3
Lines 20819 21481 +662
==========================================
+ Hits 16331 16570 +239
- Misses 4488 4911 +423 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Vấn đề
GraphIndexlà in-memory-first:rebuild()(lib.rs:706) gọiload_all_symbols/all_chains/all_call_name_indexes/all_call_recordsvà nạp hết vào HashMap. CodSpeed đo thời gian nên cái giá RAM này không ai thấy — cần đường đo riêng trước khi tối ưu.Thay đổi
crates/codegraph-graph/src/meminfo.rs/proc/self/statm+VmHWM, macOS fallbackps -o rss=.MemTrackerlấy mẫu theo phase. Không thêm dependency.crates/codegraph-bench/src/bin/mem.rsextract → index → query, kèm dự đoán RAM theo cấu trúc (count × size_of::<T>()) để quy kết quả về đúng HashMap. Chế độ--synthetic Nsinh index deterministic, không phụ thuộc network..github/workflows/memory.ymlVì sao cần
--syntheticRepo trong
.github/benches/repos/sources.txt(hello / serde-json / flask / express) đều rất nhỏ — vài nghìn symbol, không đủ để lộ khác biệt RAM giữa các cấu trúc. Chế độ synthetic sinh 50k function với fan-out cố định nênedges+call_names(hai cấu trúc to nhất) có quy mô đo được.Đo được gì
Bảng in ra gồm RSS từng phase,
Δ index(RAM doingest), vàpred symbols/pred edges. Phầnpredictedlà số deterministic — đó là thứ đáng tin để theo dõi hồi quy. RSS thô trên GitHub-hosted runner là VM dùng chung, chỉ dùng so sánh tương đối.Phạm vi
PR này chỉ thêm phần đo, chưa tối ưu gì. Chưa đụng
Storagetrait nên không ảnh hưởng backend nào.Bước kế tiếp (chưa làm ở đây), theo thứ tự tỉ lệ/công suất:
edges—HashMap<(u64,u64), EdgeMeta>được dựng bằng full pass toàn chains nhưng chỉ được đọc ở đúng một chỗ (flow(),lib.rs:1816). Chuyển sang storage-backed, đổi 1 dòng.call_names— đọc ở 3 chỗ (callers_by_call_name×2,dependencies_report), PK(repo_id, name)đã sẵn trongsg_call_names.symbols— 57 chỗ dùng, phải tách point-lookup (lazy) khỏi full-scan (SQL riêng).Lưu ý: 1 và 2 đều đứng trên
chains_map(nguồn củarebuild_edges), nênchains_maplà nút thắt kiến trúc — đó là lý do nó xếp cuối.Rủi ro
Code viết mà chưa compile được trên máy này (máy không có Rust toolchain —
cargo/rustckhông tồn tại). CI của PR này là lần chạy đầu để xác nhận compile + format + clippy.