23 Commits

Author SHA1 Message Date
Martin Vogel 3491a8e83b fix(mem): restore mimalloc's Linux arena-commit default (#1654)
Since #1360 routed ordinary malloc/new through mimalloc on Linux, the
arena policy governs every allocation in the process rather than just the
bound sqlite/tree_sitter populations. cbm sets arena_eager_commit=0, so
mimalloc commits sub-ranges with mprotect(PROT_READ|PROT_WRITE) over a
PROT_NONE reservation, and each partial commit SPLITS the reserved VMA.

Measured on the Go corpus, Linux arm64, shipped binaries:

  v0.9.0   10 mappings, at ANY worker count
  v0.10.5  ~22k mappings, peak; the count tracks CONCURRENCY
           (999 at 1 worker, 8460 at 4, 11965 at 18)

Two consequences, both of which #1654 reported from a 96-CPU/376 GB host:
the mmap/mprotect churn serialises on the kernel's per-process mmap_lock,
and the VMA count climbs toward vm.max_map_count, after which mmap fails
for ANY size -- so mimalloc reported it could not allocate 10 KB while
`free -g` still showed 246 GB available.

mimalloc's own default for this option is 2, meaning "eager-commit arenas
only on an OS that overcommits (i.e. linux)", precisely because commit is
free there until pages are touched. Overriding it to 0 opted Linux out of
the default written for Linux. Restore it on Linux only; every other
platform keeps the lazy setting, where commit is NOT free and the
upfront-memory reason still holds (Windows especially, #581).

Measured effect, same corpus and host, baseline build vs this build:

  mappings  22450 -> 17312  (-23%)
  wall       92.4s -> 92.6s (unchanged)
  peak RSS  19.14 -> 19.22 GB (unchanged)

This is a partial mitigation, not a cure: the remaining ~17k mappings are
individual 64 KB-3 MB extraction buffers, each taking its own mmap (the
worker reserves ~40 GB of address space for ~19 GB of RSS). Pooling those
is the durable fix and is deliberately left out of this change.

Guard: mem_arena_eager_commit_follows_platform_commit_cost pins the
platform split so the Linux default cannot be silently opted out again.

Reproduction and controlled 2x2 (only vm.max_map_count varied) are
recorded on #1654.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-16 03:23:05 +02:00
Martin Vogel 5d97f82f23 test(mem): assert the map contract the allocator can actually honour
The probe assumed the walk would attribute over half of a 12 MB allocation.
mimalloc v3 exposes only the main heap, abandoned pages and the calling
thread's theap -- there is no API to enumerate every theap -- so on Windows the
probe's blocks are unreachable through all three and the walk saw ~190 KB.

Asserts the triple documented in mem.h instead: what the walk cannot see, the
residual must carry. Either attribution covers the probe or the committed total
grew by at least as much, so memory can never go missing from the map
unnoticed. Bucket attribution is checked where the walk did reach the probe.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-26 00:55:24 +02:00
Martin Vogel 73a15f93ac diag: add a memory map that reports what it cannot see
Growth investigations were guessing, because the diagnostics snapshot only
carried process totals: RSS and committed bytes say memory grew, never
where. The map adds allocator-live bytes and a size-class histogram to each
snapshot, so a distinctive class identifies the allocation without
per-callsite instrumentation.

The design constraint is honesty about coverage. Walking the allocator
reaches the aggregate heap plus this thread's heap and its abandoned pages,
which is not the whole process, and in a build where malloc does not route
through the allocator at all it reaches nothing. A per-subsystem tally
would report zeros there and read as a clean bill of health. So every
snapshot carries three independent totals and an explicit residual --
os_committed, allocator-live, residual = the difference -- and the triple
localises growth instead of implying it:

  live grows            leak on a walked heap; buckets say which size
  residual grows, flat  another thread, retention, or non-allocator memory
  both flat, RSS grows  mappings or stacks, not the heap

That guard paid for itself immediately. Pointed at the open Windows growth
in #581 it reported live=0 with zero blocks and the residual carrying all
of it -- not 'the heap is innocent' but 'this walk saw nothing', which is
the correct reading for a process demonstrably holding tens of megabytes.
Together with the growth surviving a build with the allocator override
compiled out, that says the Windows allocations under investigation never
reach the allocator, so an allocator-based map cannot localise them and the
next instrument has to sit lower down.

Covered by a test that allocates a known volume in a known size class and
requires the map to attribute it there, so the map can never regress into
silently reporting zeros.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-25 09:53:59 +02:00
Martin Vogel 568e613141 test-infra: overlap the local ladder, open the serial tail, keep container I/O off virtiofs
Local runs left most of the machine idle: the serial tail ran sixteen
suites one at a time on otherwise-idle cores, the three platform legs
were launched by hand (usually sequentially), and every container-leg
object write crossed the virtiofs bind mount.

- run-tests-parallel.sh tail scheduling in two phases: the FLEX suites
  (timing-shaped but free of the shared per-account daemon runtime
  namespace) run CBM_TAIL_JOBS-wide (default 2), then the EXCL group —
  daemon-family plus the suites that drive daemon one-shots or
  supervisor rendezvous — runs strictly sequentially on a machine
  exactly as quiet as the old fully-serial tail gave it. The wave was
  already fed longest-first by the shard dealing order, so the drain-out
  no longer ends on a heavy straggler.
- ladder.sh: one maintained entry point for the full local push gate
  with the legs overlapped — lint, the Linux container suite, and the
  Windows VM suite in the background, the macOS suite in the
  foreground, one verdict per leg, logs kept per leg. A missing
  prerequisite fails its leg loudly instead of silently skipping.
- win.sh test-par now runs through vm-run-tests.sh (--par mode): the
  full parallel harness under the CI-shaped protected temp root with
  complete output. It previously ran under the MSYS-shared /tmp and
  piped through `tail -25` — the same truncated-blindness class that
  hid 40 Windows failures from the `test` command.
- docker-compose: build artifacts and the incremental fixture cache
  move to named volumes on the container VM's native filesystem. Object
  writes over the virtiofs bind mount are the container legs' largest
  avoidable I/O cost, and the fixture cache now survives across
  container runs.
- test_mem(win): the first valid full-parallel VM run proved working-set
  trimming beats the re-touch mitigation (19 MB resident of a 256 MB
  double-touch at 18 parallel suites). The RSS probe now VirtualLocks a
  64 MB span — locked pages are exempt from trimming, making the
  measurement pressure-immune — with bounded touch-and-sample retries
  when the lock is unavailable. Red-to-green under the same 18-job load.
- cli: the portable install's staging error now appends the activation
  refusal note (predicate, SID, object) like the managed path already
  does — a bare "activation transaction I/O failed" on a CI-only
  failure is undiagnosable without it.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-23 01:33:58 +02:00
Martin Vogel 83c137d2a5 feat: complete shared daemon lifecycle
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-18 01:26:08 +02:00
Martin Vogel 0e00ef5702 feat: coordinate concurrent CBM sessions
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-16 19:20:46 +02:00
Sam Li 04f95ce7b8 feat(mem): pure budget resolver with strict parse + clamp for CBM_MEM_BUDGET_MB
Fold the inline CBM_MEM_BUDGET_MB override logic in cbm_mem_init into a pure,
testable cbm_mem_resolve_budget() that returns a result struct
(budget/source/clamped/invalid), so the parse + clamp lives in exactly one
place and cbm_mem_init only surfaces the outcome as log lines.

Parsing now matches the strict src/foundation/limits.c convention (errno/ERANGE,
reject trailing garbage, positive-only): a fat-fingered value like "8GB" or a
20-digit typo becomes a warning + safe ram_fraction fallback instead of a
silently wrong budget. A valid-but-huge value clamps to detected total RAM
(logged mem.budget.clamped) rather than overflowing the MiB->bytes multiply and
wrapping to a near-zero budget (which would pin cbm_mem_over_budget() true and
stall indexing). mem.init now logs source= on both the override and fraction
paths, and the README gains the CBM_MEM_BUDGET_MB row it lacked.

Hardening of the existing #363 knob.

Signed-off-by: Sam Li <yangsec888@gmail.com>
2026-07-08 06:03:54 -04:00
Martin Vogel 563a663a6a fix(test): harden mem_rss WorkingSetSize check for windows-11-arm
mem_rss_reflects_external_resident_memory asserted cbm_mem_rss() >= 128MB
after touching a 256MB region, but on Windows cbm_mem_rss() reads
WorkingSetSize (GetProcessMemoryInfo), which the OS trims under memory
pressure — a stressed windows-11-arm runner kept only ~97MB resident and
the leg (a required gate) flaked. Re-touch the region immediately before
measuring and assert a 32MB Windows threshold that survives aggressive
trimming while staying far above the ~1MB mimalloc warm buffer, so it still
guards the real regression (a broken small-counter RSS). The Linux #else
branch keeps its 128MB assertion (the actual undercount it was written for).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-05 04:14:32 +02:00
Martin Vogel cbd41533d3 fix(mem): reconcile peak RSS with live current RSS (Linux statm vs ru_maxrss)
DCO / dco (push) Has been cancelled
cbm_mem_peak_rss() returned mimalloc's peak_rss (from getrusage ru_maxrss,
KB-granular) while on Linux cbm_mem_rss() reads the live /proc/self/statm
value (page-granular, the RSS-undercount fallback). The two sources can
disagree by a few pages, so a live current read could momentarily exceed the
reported peak and break the definitional peak >= current invariant — flaking
the mem_peak_rss_gte_rss test on the Linux/ARM CI leg (observed as peak 172KB
under current). Reconcile the sources so the reported peak is at least the
current RSS. Not observable on macOS, where both come from mimalloc.

The mem_peak_rss_gte_rss guard now faults in a 32MB buffer before asserting so
the invariant is checked against a non-trivial live current read.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-05 01:50:46 +02:00
Martin Vogel 74b27f3e02 Merge pull request #859 from DeusData/distill/782-slab-aligned-pages
fix(slab): O(1) aligned-page cross-thread frees + retire-on-live-count (closes #852)
2026-07-05 00:20:05 +02:00
Martin Vogel 19296d75b9 Merge remote-tracking branch 'origin/main' into distill/776-linux-rss-undercount
DCO / dco (push) Has been cancelled
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 21:36:27 +02:00
Martin Vogel 26cd4678a8 fix(mem): use real /proc RSS on Linux (mimalloc current_rss undercounts, blinding backpressure)
Distilled from #776's 132460f5. This project sets arena_eager_commit=0 +
purge_decommits=1 + purge_delay=0 in cbm_mem_init to reduce upfront memory, so
mimalloc's committed-page counter reads low. On Linux mi_process_info() never
sets current_rss (vendored/mimalloc/src/prim/unix/prim.c only fills peak_rss),
so current_rss defaults to that low committed counter — cbm_mem_rss() returned a
few MB while true RSS was multiple GB, leaving cbm_mem_over_budget() backpressure,
the memory ceiling, and the host RAM tier blind on Linux.

Prefer os_rss() (/proc/self/statm) as the primary source on Linux; macOS and
Windows are unchanged (their mi_process_info current_rss is accurate via
task_info / GetProcessMemoryInfo). cbm_mem_peak_rss() is untouched. Complements
#752 by making the RAM tiers actually bite on Linux.

Reproduce-first: mem_rss_reflects_external_resident_memory pins mimalloc's
committed counter low with a live mi_malloc, then grows true RSS via a raw 256MB
mmap (invisible to mimalloc); unfixed Linux returns the ~few-MB committed counter
(RED), fixed Linux returns /proc RSS (GREEN). macOS/Windows pass either way, so
the RED manifests on the Linux CI leg.

Co-authored-by: petercoxphoto <info@petercox.ie>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 21:33:58 +02:00
Martin Vogel a960d3d255 Merge pull request #752 from Andy11-cpu/fix/ram-tier-budget
Tier RAM budget by host size
2026-07-04 20:43:23 +02:00
Martin Vogel c095a4a82e Merge remote-tracking branch 'origin/main' into distill/782-slab-aligned-pages
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 20:38:00 +02:00
Martin Vogel 487377459f fix(slab): O(1) aligned-page cross-thread frees + retire-on-live-count (no global hot-path lock)
Distilled from #782 keeping its retire-on-live_count correctness, replacing the
global-lock+linear-scan with aligned-page O(1) lookup + per-page lock-free
remote-free queue (no hot-path contention). Each 64KB slab page is allocated
aligned to its own size, so the owning page of any chunk is recovered by masking
the pointer; a lock-free 3-level radix page map (cold-path writes only) tells
slab_free whether a masked base is a real slab page or a plain heap pointer
without dereferencing an unrelated address. Owner frees hit the thread-local
free list; foreign frees CAS onto that page's lock-free MPSC remote-free stack.
Pages carry refcount = handed-out chunks + owner guard; reclaim/destroy retire a
page that still holds foreign-live chunks and free it when the final chunk
returns, so a page holding a live tree-sitter lexer chunk is never freed under
it. Keeps #782's resolve-worker terminal slab teardown.

Closes the #852 use-after-free and the cross-thread invalid free. Reproduce-first
guards in test_mem.c are RED on main (heap-use-after-free / bad-free under ASan)
and GREEN with this fix.

Closes #852
Supersedes #782

Co-authored-by: SS-42 <47749027+SS-42@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 20:35:33 +02:00
Martin Vogel e85c7d69f0 fix(index): bound + source-cap parallel retention with re-read fallback (low-RAM peak RSS)
DCO / dco (push) Has been cancelled
Distilled from #685 (nguyentamdat) rebased onto current main, plus two
research-driven refinements and a genuine reproduce-first guard.

The parallel extract retains each file's source text so the fused cross-file
LSP resolve can re-parse it. That retention is transient but a peak-RSS driver.
On main the caps are flat (100 MiB/file, 2 GiB total) and a file over the cap is
silently unretained AND its cross-file resolution is skipped -- a graph-quality
gap. This change bounds retention AND keeps every cross-file edge.

- Source-text cap as a FLOOR, not just a ceiling: retention total defaults to
  min(cbm_mem_budget()/8, 1 GiB), per-file min(32 MiB, total). Following the
  rust-analyzer memory model, the RAM-derived default is clamped to a small
  absolute ceiling so a huge-RAM host does not hold tens of GB it would re-read
  cheaply. Both caps env-overridable via CBM_RETAIN_TOTAL_MB /
  CBM_RETAIN_PER_FILE_MB (limits.c convention); ceilings bound only the
  auto-derived default, never an explicit operator/caller choice. A dropped file
  emits one index.retain_capped WARN per run.
- Bounded re-read fallback (the correctness guarantee): resolve_worker re-reads
  an unretained file's source on demand (bounded, freed immediately) instead of
  skipping resolution, wired at every cross-LSP site that consumes source. The
  cap now only trades retained RAM for a bounded re-read, never a lost edge.
- cbm_parallel_extract_ex + opts struct (cbm_parallel_extract is now a wrapper
  passing NULL -> env-derived defaults); malloc/calloc NULL-check hardening.

Reproduce-first: parallel_cross_file_reread_preserves_unretained_edges uses a
Java<->Kotlin pair whose cross-file lsp edges are genuinely source-dependent;
the edges are lost when the caller is unretained and the fallback is absent
(RED), present with it (GREEN), with a retained CONTROL scenario proving
non-vacuity. #685's original Python red test was a false guard (per-file py_lsp
already resolves those calls) and is replaced. Peak-bound guards
(retained_bytes <= total_cap; retain_sources=false retains nothing) in test_mem.c.

Verify: make -f Makefile.cbm cbm && make -f Makefile.cbm lint-ci; test-runner
parallel pipeline incremental py_lsp ts_lsp java_lsp kotlin_lsp c_lsp cs_lsp
go_lsp rust_lsp mem -> 2323 passed.

Co-authored-by: nguyentamdat <nguyentamdat@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 18:38:58 +02:00
Andy11-cpu 2839dc03f1 Tier RAM budget by host size
Use 25% of physical RAM on hosts with 16GB or less, 35% on 32GB or less, and 50% above. Expose cbm_mem_ram_fraction_for_total() for unit tests and call it from MCP/CLI startup instead of a fixed 50% fraction.

Signed-off-by: Andy11-cpu <canada11@duck.com>
2026-07-01 21:49:53 -04:00
Martin Vogel eed87fd372 Forbid test skips and convert existing skips to hard failures
Add scripts/check-no-test-skips.sh (run from lint) which fails the lint phase on any plain SKIP() or direct tf_skip_count manipulation; only SKIP_PLATFORM() (for genuinely platform-specific tests) is tolerated. Add FAIL() and SKIP_PLATFORM() helpers to the test framework and convert the remaining SKIP()/perf-gated skips across the suite into pass-or-fail assertions, so a suite that cannot meet its preconditions reports a red failure instead of a silent skip.
2026-06-05 21:53:25 +02:00
Martin Vogel 7fa3acd0c6 WIP: strict linting + RAM-first pipeline (lint fixes pending) 2026-04-01 23:22:44 +02:00
Martin Vogel 7f579f91ad Add 480+ tests, fix 3 bugs discovered by new test coverage
New tests (6481 lines across 23 files):
- Arena: reset invariants, growth, sized init, destroy safety
- Dynamic array: capacity, remove, clear, grow patterns
- FQN: module/package/class name computation (592 lines)
- Graph buffer: node/edge CRUD, dedup, merge, concurrent access
- Hash table: collision, resize, iteration, edge cases
- MCP: tool dispatch, argument parsing, error handling
- Memory: allocation tracking, collection, limits
- Pipeline: test detection (path + func name patterns)
- Store: node/edge CRUD, search, batch ops, persistence
- String: intern dedup, util edge cases, YAML parsing (1105 lines)
- Traces: ingestion, dedup, query
- Watcher: baseline, polling, change detection
- Worker pool: task distribution, completion

Bugs found and fixed:
1. Arena reset overflow: block_size not reset to block_sizes[0]
   after growth — subsequent allocations could overflow blocks[0]
2. Test path detection: missing __tests__/, tests/, spec/ directory
   patterns and _spec.rb suffix
3. Test func detection: "Testable" falsely matched as Go test —
   now requires uppercase or end-of-string after "Test" prefix
2026-03-24 23:48:23 +01:00
Martin Vogel fdb83f83f7 Fix Windows test compilation: mkdtemp, mkdir, setenv, pipe compat
- cbm_mkdtemp: _mktemp + _mkdir on Windows
- cbm_setenv/cbm_unsetenv: _putenv_s on Windows
- cbm_pipe: _pipe on Windows
- Replace bare POSIX calls in 12 test files with compat wrappers
- All test files now include compat.h
2026-03-18 19:19:42 +01:00
Martin Vogel ef512c3508 Remove macOS-internal headers inserted by clang-tidy (breaks Linux/GCC) 2026-03-18 01:52:05 +01:00
Martin Vogel 18fa9979ee Replace vmem with mimalloc global allocator, add extraction-phase prescan, fix __init__.py QN collision
Memory management:
- Vendor mimalloc v2.1.9 as global allocator (MI_OVERRIDE=1 in prod)
- New mem.h/mem.c: RSS-based budget tracking via mi_process_info()
- Remove vmem.c/vmem.h (mmap-based budget tracking)
- Remove slab tier2 bump allocator (~300 LOC); >64B goes to mimalloc
- Slab tier1 pages from malloc (= mimalloc) instead of vmem
- Arena blocks from malloc instead of vmem
- Budget raised from 35% to 50% RAM (no more untracked C++ heap)

Extraction-phase prescan (eliminates disk re-reads):
- HTTP call sites: keyword check + URL extraction during extraction
- HTTP routes: decorator + source-based extraction during extraction
- Config file refs: regex scan during extraction
- httplinks: 41.8s → 13ms on Linux kernel (3,212x faster)
- configlink: 41.4s → 0.8s on Linux kernel (54x faster)
- Linux kernel fast-mode total: 2m38s → 1m18s

Bug fixes:
- __init__.py Module QN no longer collides with Folder QN
- index.ts same fix for JS/TS packages
- 13 regression tests for QN collision at FQN + extraction layers
- search_graph/search_code default limit raised from 10 to 500k
- Resolve all clang-tidy, cppcheck, and clang-format warnings

Repo cleanup:
- tree-sitter-form, tree-sitter-magma moved to tools/
- .gitignore: build/, node_modules/, graph-ui/dist/, TEST_PLAN.md
2026-03-17 22:05:57 +01:00