Skip to content

btc(stratum): memoize tx_data across work-gen calls (H5 leak fix) - #119

Merged
frstrtr merged 1 commit into
masterfrom
btc/h5-tx-data-fix
Jun 17, 2026
Merged

btc(stratum): memoize tx_data across work-gen calls (H5 leak fix)#119
frstrtr merged 1 commit into
masterfrom
btc/h5-tx-data-fix

Conversation

@frstrtr

@frstrtr frstrtr commented Jun 16, 2026

Copy link
Copy Markdown
Owner

BTC H5 leak fix — memoize tx_data across work-gen calls

Leak site: src/impl/btc/stratum/work_source.cpp:634build_connection_coinbase re-materialized the per-job transaction hex vector on every call, even when the mempool tx set was unchanged.

Before (2026-06-02 fixture, print-2026-06-02.txt)

  • work_source.cpp:634 leaked 768.18 MB / 1.2M calls = ~96% of all leaked string bytes, 78% of total leaked bytes, linear/unbounded over the ~1h50m trace.
  • This is the per-job tx_data duplication, NOT collapsed by a1.

Fix

Single-slot memo keyed to a fingerprint over wd->m_hashes (the merkle leaf set). On a repeat call against the same tx set, the cached shared_ptr is returned (refcount bump) instead of re-serializing the whole mempool.

Two confirmations integrator asked for

  1. Deterministic key: the fingerprint is CHash256 (double-SHA256) over wd->m_hashes iterated in merkle-leaf order (the vector order), each leaf fed as its 32 raw bytes. Same tx set => same leaf order => same fingerprint, across calls. Because a txid IS the hash of its tx, identical m_hashes => identical tx_data, so the key uniquely determines the cached value (no collision-driven mismatch).
  2. Bounded eviction: the memo is a single slot (tx_data_fp_ + tx_data_memo_). A new fingerprint overwrites the prior entry, dropping its refcount — at most one tx set is retained. No unbounded map; we never need more than the latest tx set.

Atomic-with-merkle

The key is the exact leaf set that produced this job's merkle / witness_commitment (captured at stratum_server.cpp:1395). A memo hit therefore implies an identical merkle — no stale-tx-vs-fresh-merkle mismatch on submit. This is why the key is the tx-set fingerprint, not work_generation_: on BTC work_generation_ bumps tip-only (main_btc.cpp:1212/1215, no mempool-arrival subscriber) while build_template reads live mempool_ per call, so a work-gen-keyed memo could serve stale tx_data against a fresh merkle.

a504426 / a1 reconciliation

  • a504426b is an ancestor of this branch base (1040edec, merge of [dash][S4-pre] payout-credit foundation — advance block.m_txs (leaf, single-coin) #114).
  • a1 is already landed: the shared_ptr conversion at work_source.cpp:637 (per-job COPIES -> refcount bumps) and the active_jobs_ MAX_ACTIVE_JOBS=256 bound at stratum_server.cpp:1375 are both present on the base.
  • This memo lands as an additive refinement on top of a1 — it removes the rebuild-every-call churn that a1 did not address. No re-fix, no conflict. Prod (.53 PID 14308, sha 2f2e8282) is running a1; this is the next layer.

Tests

ctest: 60/60 BTC stratum / template-builder / mining-interface tests pass (100%). (The 9 Dash X11/DGW KAT entries reported by a full ctest run are ***Not Run — their executables are not built in this incremental build dir; out of the BTC lane and not affected by this BTC-local change.)

Acceptance gate — post-patch heaptrack (IN FLIGHT)

Per the task, the acceptance gate is a post-patch heaptrack on the 2026-06-02 fixture: the retention curve at work_source.cpp:634 must flatten. Queued next heartbeat (30–60 min run); before/after curve will be appended to this PR before merge.

Scope / safety

BTC-local: only src/impl/btc/stratum/work_source.{cpp,hpp}. Zero core edits, zero cross-coin footprint (tx_data populated at this one impl site; all other coins carry null). Commit signed b000c323. Push held — merge operator-gated.

build_connection_coinbase rebuilt the per-job tx hex vector on every call
even when the mempool tx set was unchanged. The a1 fix collapsed the
per-job COPIES into shared_ptr refcount bumps, but the vector itself was
still freshly materialized each call -- the dominant churn/leak site in
the 2026-06-02 heaptrack (work_source.cpp:634, 768.18 MB / 1.2M calls,
~78%% of total leaked bytes, linear/unbounded over the trace).

Add a single-slot memo keyed to a fingerprint computed over wd->m_hashes
(the merkle leaf set, in merkle-leaf order) so a repeat call against the
same tx set returns the cached shared_ptr instead of re-serializing the
whole mempool. The fingerprint keys on the exact leaves that built this
job merkle, so a memo hit is atomic-with-merkle by construction (no
stale-tx-vs-fresh-merkle mismatch on submit). Single slot => bounded
memory (one tx set retained), self-evicting on tx-set roll.

BTC-local: touches only src/impl/btc/stratum/work_source.{cpp,hpp}; tx_data
is populated at this one impl site, core only plumbs it, all other coins
carry null -- no cross-coin entanglement.

ctest: 60/60 BTC stratum/template/mining-interface tests green.
@frstrtr

frstrtr commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

Methodology correction (supersedes the f929f9e1 arena-frag / churn-not-retention read — do not close this as an instrumentation artifact):

heaptrack_print on a live-growing .zst only emits the MOST-CALLS-TO-ALLOCATION section; the PEAK and LEAKED rankings require a second pass over a closed/static stream. The earlier 0B-peak top sites were an artifact of reading the growing stream, not evidence of churn.

A static-copy pass over the 2026-06-02 trace shows leaked ~1.03 GB ≈ peak ~1.05 GB, with arena-fragmentation residual only ~20 MB. That is retention, not churn — a real tracked-heap leak. Dominant site is work_source.cpp:634 (build_connection_coinbase tx-hex vector, 768.18 MB / 1.2M calls, ~78%% of leaked bytes, linear/unbounded over the trace), which is exactly what the memoize fix in b000c32 targets. So this fix is warranted and should not be withdrawn as arena-frag.

Open question being characterized this heartbeat: whether the remaining ~22%% is a second retention site (mining_submit json get<>) needing its own patch, or also collapses under the b000c32 memo. Will update with the PEAK/LEAKED #2-site ranking.

@frstrtr

frstrtr commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

Correction — withdraw the arena-fragmentation read (was msgid f929f9e1).

Methodology note for the record: heaptrack_print on a live-growing .zst emits only the MOST-CALLS section; PEAK/LEAKED rankings require a second pass over a closed/static stream. Running that second pass on a static cp of the stream gives leaked ~1.03G ≈ peak ~1.05G — arena/mmap-fragmentation residual is only ~20M (noise). So this is retained tracked-heap, not glibc churn → hypothesis-c (allocator undercounting) is refuted, do not close as arena-frag.

The retention concentrates at the per-call tx_data rebuild in build_connection_coinbase (src/impl/btc/stratum/work_source.cpp:634, ~768MB / 1.2M calls, ~78% of leaked; 809M std::string::_M_assign + 539M json get<> are the push-back/get consumers at that site). The mempool tx set is re-serialized into a fresh vector on every call even when unchanged → this is the a2 "share tx cache by ref" class, which the single-slot fingerprint memo on btc/h5-tx-data-fix (tx_data_memo.hpp) already implements.

@frstrtr

frstrtr commented Jun 17, 2026

Copy link
Copy Markdown
Owner Author

Review verdict (integrator): APPROVE for merge.

Verified at b000c32:

  • Scope: single-coin BTC only (src/impl/btc/stratum/work_source.{cpp,hpp}). Clean per-coin isolation, single-coin smoke gate sufficient.
  • CI: all green — Linux x86_64, macOS arm64, Windows x86_64, ASan+UBSan, CodeQL (c-cpp + python), Web-static, Qt leak gate, all six coin smokes. Embedded smoke SKIPPED is expected (branch-gated, not a failure). mergeable=MERGEABLE / CLEAN.
  • Logic: the a2 single-slot memo fingerprints over wd->m_hashes (the merkle leaf set, merkle-leaf order), so a memo hit is atomic-with-merkle by construction — no stale-tx-vs-fresh-merkle submit mismatch. tx-hash collision-resistance means identical fingerprint implies identical tx data, so no false-hit hazard. Single slot bounds memory to one retained tx set and self-evicts on tx-set roll. Both read+write of tx_data_fp_/tx_data_memo_ are guarded by template_mutex_; std::move out happens on a local copy after lock release. reserve() is a clean add.

Supersedes #117 a2 on the live path. Merge gated on operator tap (live-path fix, not SAFE-COSMETIC) — surfacing now. Merge with NO --delete-branch per current rule.

@frstrtr
frstrtr merged commit 152ef20 into master Jun 17, 2026
16 checks passed
@frstrtr
frstrtr deleted the btc/h5-tx-data-fix branch June 17, 2026 09:18
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.

1 participant