Summary
Follow-up to #2490 / #2525 after the idle-thread release, parser-stack drop and graph owner heaps. With both graph owners resident, a settled daemon's uncharged anonymous memory now levels off at about 300 MB. Two things are still uncharged and grow with the corpus or the refresh, so a fixed runtime allowance and the #2490 invariant test cannot be added yet.
Evidence
Release production build, isolated HOME/TRACEDECAY_DATA_DIR, systemd-run --user --scope -p MemoryMax=6G -p MemorySwapMax=1G, 96-core host. Gap = anon + swap - charged owners.
1. The refresh increment's retained parse pool and build residue. SharedRetainedParsePool keeps trees, extraction artifacts and sources for every document an increment re-extracted (up to 256 docs / 64 MiB of source). No owner charges them.
| corpus |
cold gap |
gap after a 40-file refresh commit |
| every 20th tracked file (293 files) |
212,824,510 |
279,094,351 |
| every 5th tracked file (1,164 files) |
266,709,683 |
318,949,300 |
| synthetic, 300 Rust files x 160 fns |
178,809,782 |
375,914,470 |
The retained tree bytes have no live measure. tree-sitter allocates through glibc malloc. One way forward: route tree-sitter to mimalloc with ts_set_allocator, build and reparse retained documents inside an OwnerHeapV1 scope, and register the pool as a resident owner charged by its heap pages. Another: stop retaining across builds, measuring the reuse rate first.
2. The no-owner residual. With both graph owners idle-released, what stays resident:
| corpus |
anon + swap, no owners |
[anon:mimalloc] |
glibc / unnamed anon |
| 293 files |
227,074,048 |
165.6 MB |
51.5 MB |
| 1,164 files |
294,797,312 |
205.3 MB |
78.0 MB |
| full clone (5,855), after a refresh |
435,204,096 |
343.6 MB |
76.8 MB |
The glibc part levels off (SQLite page caches filling to their per-connection bound). The mimalloc part keeps growing, about 34-45 KB per file. #2661's heaptrack puts non-owner live Rust at about 5 KB per file, so most of this is pages pinned by a few long-lived blocks from index builds and activation. Next step: an allocation-site profile of the no-owner state (heaptrack on a system-allocator build, or an MI_STAT build) to find which long-lived allocations pin build pages. Then either allocate those results after the build scratch is freed, or build them in their own owner heap.
Acceptance
Anon + swap stays within charged owners plus one fixed allowance on a cold index and after a refresh, from 300 to 5,855 files, including a heavy-file corpus. A daemon test then asserts it and fails on today's master.
Summary
Follow-up to #2490 / #2525 after the idle-thread release, parser-stack drop and graph owner heaps. With both graph owners resident, a settled daemon's uncharged anonymous memory now levels off at about 300 MB. Two things are still uncharged and grow with the corpus or the refresh, so a fixed runtime allowance and the #2490 invariant test cannot be added yet.
Evidence
Release
productionbuild, isolatedHOME/TRACEDECAY_DATA_DIR,systemd-run --user --scope -p MemoryMax=6G -p MemorySwapMax=1G, 96-core host. Gap = anon + swap - charged owners.1. The refresh increment's retained parse pool and build residue.
SharedRetainedParsePoolkeeps trees, extraction artifacts and sources for every document an increment re-extracted (up to 256 docs / 64 MiB of source). No owner charges them.The retained tree bytes have no live measure. tree-sitter allocates through glibc
malloc. One way forward: route tree-sitter to mimalloc withts_set_allocator, build and reparse retained documents inside anOwnerHeapV1scope, and register the pool as a resident owner charged by its heap pages. Another: stop retaining across builds, measuring the reuse rate first.2. The no-owner residual. With both graph owners idle-released, what stays resident:
[anon:mimalloc]The glibc part levels off (SQLite page caches filling to their per-connection bound). The mimalloc part keeps growing, about 34-45 KB per file. #2661's heaptrack puts non-owner live Rust at about 5 KB per file, so most of this is pages pinned by a few long-lived blocks from index builds and activation. Next step: an allocation-site profile of the no-owner state (heaptrack on a system-allocator build, or an
MI_STATbuild) to find which long-lived allocations pin build pages. Then either allocate those results after the build scratch is freed, or build them in their own owner heap.Acceptance
Anon + swap stays within charged owners plus one fixed allowance on a cold index and after a refresh, from 300 to 5,855 files, including a heavy-file corpus. A daemon test then asserts it and fails on today's master.