Feat/ingest memory instrumentation - #54
Merged
xerj-org merged 3 commits intoJul 27, 2026
Merged
Conversation
Sustained bulk ingest can grow process RSS far beyond the raw corpus, but RSS and aggregate memtable counters cannot identify which lifecycle still owns the bytes. Policy changes made without that attribution risk moving memory rather than bounding it. Add an opt-in `xerj.ingest_memory.v1` ownership ledger and sampler. The default mode is fully off. `XERJ_INGEST_MEMORY_TRACE=summary` enables bounded snapshots containing logical lifecycle gauges, jemalloc allocated/active/resident, RSS, process CPU time, accounting underflows, and dropped-output events. Wire real owners for HTTP bodies and rewritten buffers, shared raw semantic sources, parsed documents and caller vectors, prepared documents and derived vectors, observed active memtables, and drained flush snapshots. Semantic document/vector transitions are atomic, raw Arc ownership outlives every clone, prepared ownership ends after each real publication, and flush ownership spans the shard drain through the blocking finalizer/publisher. Keep merge and cache names reserved and explicitly unavailable rather than reporting false zero measurements. Document that sampled MemtableActive can briefly overlap authoritative FlushDrained and that logical totals are diagnostic attribution, not allocator identities. The summary sink is non-blocking and bounded to 256 lines. Newly created NDJSON files are mode 0600. Output failures increment dropped-event accounting and stop the failed writer. Shutdown synchronously refreshes memtable, jemalloc, RSS, and CPU state before trace_stop, so a completed final flush is not represented by a stale sampled memtable value. Disabled-mode structural tests prove no semantic schema reads, sizing walks, owner maps, or guard vectors. The lifecycle harness exercises real embedding and publication barriers, mixed per-item embedding and publication failures, caller and custom vectors, malformed input, multiple targets, index-creation failure, panic cleanup, sequential prepared-memory decline, and target/task isolation. Concurrent success and failure flushes prove distinct task-local ledgers remain live through their publishers and refund to zero. Verification: - Default-parallel engine library suite: 187 passed. - Server suite: 11 passed before sink hardening; seven focused trace tests pass afterward, including 60-second shutdown interruption, mode-0600 output, preserved existing permissions, /dev/full loss accounting, and final refresh. - Formatting, diff checks, and scoped engine/server checks pass. - Immediate-SIGINT release smoke: 2,000 documents, no explicit flush, 60-second sample period; trace_stop reports every logical current as zero with refreshed physical probes, zero accounting errors, and zero dropped events. - Broader live smoke: 2,600 ordinary, caller-vector, and semantic documents; all writes succeeded, restart counts and representative vectors matched, and the off control created no trace output. - ES-YAML hard gate on final server e759e9e09dd168d43f4cad76a611beaf2a96977ac02f26e96b84fdccd064e889: 1360 passed, 0 failed, 3 skipped. This commit adds diagnostics, not a heap bound, cache budget, merge rewrite, or performance claim. It makes those follow-up changes measurable.
Full FinanceBench runs are too slow and noisy to use as the first detector for ingest ownership leaks, allocator retention, durability regressions, or restart corruption. RSS alone also cannot distinguish live logical owners from jemalloc retention, file-backed mappings, or uninstrumented memory. Add a deterministic two-layer diagnostic suite. Engine tests exercise exact JSON and vector sizing, real semantic bulk ownership, malformed input, injected embedding/publication failures, repeated same-ID overwrites, flushes, cache warming, and force-merge churn. All engine seams are cfg(test)-only. Add a Linux shipped-path runner that creates a typed semantic_text index and drives real HTTP bulk, refresh, explicit flush, force merge, graceful shutdown, restart, exact counts, and three exact sentinel lookups. The default lexical feature-hash embedder exercises the embedding lifecycle without claiming neural semantics. Each cell uses a fresh process and data directory. Capture independent internal xerj.ingest_memory.v1 and external /proc telemetry. Bind successful runs to binary, corpus, checkout, configuration, host, artifact sizes, and SHA-256 hashes. The analyzer rejects schema/category/measurement drift, missing or reordered trace events, accounting errors, dropped output, stop-time retained owners, missing RSS, artifact changes, missing durability phases, and count/sentinel corruption. It reports relational RSS slopes without inventing a host-independent threshold; optional budgets require an explicit headroom multiplier. Failed phases clean up server processes without masking the original exception. Sampling intervals must be positive, generated TOML paths are escaped, output directories are never overwritten, and raw plus derived artifacts are private. The runner caps individual HTTP operations at five minutes. Verification: - Engine library suite: 191 passed, 0 failed. - Focused bounded engine suite: 4 passed, 0 failed. - Python harness suite: 8 passed, 0 failed. - cargo check -p xerj-engine --tests passed. - cargo fmt --all --check, Python compilation, and git diff --check passed. - Live semantic_text smoke at 32 and 64 documents passed every refresh, flush, merge, shutdown, and restart boundary. Prepared-vector ownership peaked at 27,648 bytes and returned to zero. - Independent hostile-review smoke passed at 32 and 64 documents. - ES-YAML hard gate: 1360 passed, 0 failed, 3 skipped. This suite is a fast diagnostic and regression instrument. It is not a FinanceBench result, a production memory bound, or a performance claim. Cache and merge attribution remains explicitly unavailable until those owners are wired.
CI runs cargo clippy --workspace --all-targets with warnings denied. The instrumentation branch introduced four findings: the flush helper's argument allowance was attached to the test-hook type instead of the helper, Ledger exposed new() without Default, and two sink receive loops used loop-plus-match forms. Attach the narrowly documented argument-count exception to do_flush_shard itself, implement Default for Ledger through its const constructor, and express both blocking receiver loops as while-let loops without changing shutdown or sink-error behavior. Verified after rebasing onto origin/main 64a83a9: cargo fmt --all --check; cargo clippy --workspace --all-targets -- -D warnings; full xerj-engine tests; seven ingest-memory server tests; and eight bounded diagnostic Python tests.
buger
force-pushed
the
feat/ingest-memory-instrumentation
branch
from
July 27, 2026 16:51
93a0f1d to
e29aa85
Compare
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.
Summary
This PR adds opt-in ingest-memory attribution and a bounded, deterministic shipped-path diagnostic suite.
It does not claim to fix the unbounded-ingest heap problem by itself. It makes that problem measurable: a developer can now separate live logical ownership from allocator retention, RSS, file-backed memory, and still-uninstrumented memory, then reproduce lifecycle regressions in seconds or minutes instead of repeatedly indexing the full FinanceBench corpus.
The default runtime remains unchanged. Instrumentation is disabled unless
XERJ_INGEST_MEMORY_TRACE=summaryis set.Why this is useful
Our large-PDF and FinanceBench investigations showed process RSS growing far beyond the raw corpus, but the existing RSS and memtable counters could not answer which phase still owned the bytes. Without owner-level attribution, a memory optimization can appear successful while merely moving retention from one subsystem to another.
This PR provides two independent views:
xerj.ingest_memory.v1: logical ingest owners plus jemalloc allocated/active/resident, process RSS, process CPU time, accounting errors, and dropped output.xerj.process_sample.v1: an external Linux/procview of RSS/HWM, anonymous/file-backed RSS, PSS, private pages, swap, I/O, CPU ticks, threads, and file descriptors.Those streams are intentionally separate so one instrument can expose disagreement or observer error in the other.
What is instrumented
The engine records lifecycle ownership for:
Merge and read-cache category names are present but explicitly report
unavailable. They are not reported as zero, because those owners are not wired yet.Semantic ownership follows the real asynchronous lifecycle. Raw shared ownership outlives every
Arcclone, document/vector transitions are emitted atomically, prepared ownership ends after each actual publication, and drained ownership remains charged through the blocking finalizer and publisher. Success, partial failure, cancellation-style cleanup, publication failure, malformed input, and flush failure paths are covered.Runtime behavior and safety
Enable summary tracing with:
XERJ_INGEST_MEMORY_SAMPLE_MSis clamped to 25–60000 ms. The output can be a file path ortracing. Newly created files are mode0600.The sink is non-blocking and bounded to 256 queued lines. Output failures increment dropped-event accounting and stop the failed writer instead of blocking ingest. Shutdown synchronously refreshes memtable, allocator, RSS, and CPU state before writing
trace_stop, so the final record does not rely on a stale periodic sample.With tracing disabled, structural tests prove that the semantic path does not perform instrumentation schema reads, JSON sizing walks, owner-map allocation, or guard-vector construction.
Bounded diagnostic suite
The new suite lives at
demo/usecases/autoindex/scale/bounded/.It generates a deterministic typed corpus, creates a real
semantic_textmapping, and drives XERJ through the ES-compatible HTTP path:The default
--embed-mode lexicalexercises XERJ's deterministic feature-hash embedding lifecycle. It does not claim neural semantics.Example:
The runner refuses to overwrite an output directory. Every cell uses a fresh server process and data directory. Failed phases clean up the server without masking the original exception. Individual HTTP operations time out after five minutes.
The run manifest records the binary hash and size, binary version, suite checkout and dirty state, corpus hash, host, configuration, correctness results, and every raw artifact's size and SHA-256. Raw and derived artifacts are mode
0600.The analyzer rejects:
trace_stop.It reports per-cell peaks, RSS per document, and incremental RSS slopes without inventing an absolute host-independent threshold. Optional versioned budgets are supported, but their headroom multiplier must be chosen explicitly from repeated runs on a pinned host.
The README also includes ready-made prompts for an AI reviewer to validate one run, compare two runs, and diagnose the first phase where RSS separates from allocator and logical ownership.
Verified results
Deterministic tests
The engine regression tests cover exact JSON/vector capacity formulas, real semantic bulk ownership, caller vectors, malformed input, injected embedding and publication failures, repeated same-ID overwrites, three flush cycles, cache warming, force merge, zero retained owners, and zero accounting errors.
Live shipped-path smoke
A release binary was exercised with real
semantic_textHTTP ingestion at 32 and 64 documents.analysis.jsonandreport.mdwere mode0600.This small smoke is a harness and lifecycle proof, not a throughput benchmark.
Broader instrumentation smoke
A separate release smoke ingested 2,600 ordinary, caller-vector, and semantic documents. All writes succeeded, restart counts and representative vectors matched, and an instrumentation-off control created no trace output.
An immediate-SIGINT smoke ingested 2,000 documents with no explicit flush and a 60-second sample period. The synchronous final record reported every logical current at zero, fresh physical probes, zero accounting errors, and zero dropped events.
Compatibility and build gates
cargo fmt --all --check: passed.cargo check -p xerj-engine --tests: passed.git diff --check: passed.Overhead and limitations
Relationship to the profiling-tooling PR
This PR is independent of
feat/debug-profiling-tooling. It does not usepprof,jemalloc_pprof, or the profiling build profiles. Either PR can merge first.The tools are complementary:
Follow-up enabled by this PR
The immediate follow-up is to run the bounded suite across increasing cells, capture heap/CPU profiles at the first divergence, wire merge/cache owners, and then implement the smallest evidence-backed memory-bound fix. Each fix can be proven on the deterministic suite before paying the cost of the full 368-PDF FinanceBench run.