Skip to content

perf(bench): profiler RAM + workflow đo memory trên CI - #38

Merged
hungpham10 merged 15 commits into
mainfrom
feat/memory-profile
Oct 5, 2026
Merged

hungpham10 merged 15 commits into
mainfrom
feat/memory-profile

Conversation

@hungpham10

Copy link
Copy Markdown
Owner

Vấn đề

GraphIndex là in-memory-first: rebuild() (lib.rs:706) gọi load_all_symbols / all_chains / all_call_name_indexes / all_call_records và 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

File Nội dung
crates/codegraph-graph/src/meminfo.rs Đọc RSS std-only: Linux /proc/self/statm + VmHWM, macOS fallback ps -o rss=. MemTracker lấy mẫu theo phase. Không thêm dependency.
crates/codegraph-bench/src/bin/mem.rs Đo RSS theo extract → 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 N sinh index deterministic, không phụ thuộc network.
.github/workflows/memory.yml Chạy profiler, in bảng vào job summary, upload artifact.

Vì sao cần --synthetic

Repo 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ên edges + 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 do ingest), và pred symbols / pred edges. Phần predicted là 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 Storage trait 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:

  1. 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.
  2. call_names — đọc ở 3 chỗ (callers_by_call_name ×2, dependencies_report), PK (repo_id, name) đã sẵn trong sg_call_names.
  3. 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ủa rebuild_edges), nên chains_map là 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/rustc không tồn tại). CI của PR này là lần chạy đầu để xác nhận compile + format + clippy.

`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.
@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 858c6afe-0081-4739-9714-0c2c82fe74bb
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

hungpham10 and others added 3 commits October 5, 2026 00:33
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.
@codspeed

codspeed Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 38 untouched benchmarks


Comparing feat/memory-profile (25a4bdb) with main (c37b0fa)

Open in CodSpeed

Hung Pham and others added 10 commits October 5, 2026 08:40
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-commenter

codecov-commenter commented Oct 5, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 37.79412% with 423 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.13%. Comparing base (c37b0fa) to head (25a4bdb).

Files with missing lines Patch % Lines
crates/codegraph-bench/src/bin/mem.rs 0.00% 330 Missing ⚠️
crates/codegraph-graph/src/memtrack.rs 75.00% 44 Missing ⚠️
crates/codegraph-graph/src/storage/cached.rs 0.00% 23 Missing ⚠️
crates/codegraph-graph/src/lib.rs 78.46% 14 Missing ⚠️
crates/codegraph-graph/src/meminfo.rs 92.50% 6 Missing ⚠️
crates/codegraph-graph/src/lru.rs 0.00% 3 Missing ⚠️
crates/codegraph-graph/src/storage.rs 0.00% 3 Missing ⚠️

❌ 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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hungpham10
hungpham10 merged commit 3df7b6f into main Oct 5, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants