ingest: size each element once on the extract path, and return element views (+ stress-density benchmark) - #5972
Draft
tamirms wants to merge 4 commits into
Draft
ingest: size each element once on the extract path, and return element views (+ stress-density benchmark)#5972tamirms wants to merge 4 commits into
tamirms wants to merge 4 commits into
Conversation
The walk's element count is knowable before the walk starts — the array's length prefix is an O(1) read — so the parts slice no longer grows through append reallocations. On a malformed prefix the count stays 0 and the walk itself reports the error, exactly as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zyTXU8wkocJgN6mBafnou
collectRaws consumed the generated Iter(), which yields UNTRIMMED suffix views and sizes each element to advance — then MustRaw() sized the same element again to trim it. Since these element views are plain byte views, the trimmed element IS its raw bytes: collect by trimming (Raw()) and stepping past the trimmed extent — one size pass per element instead of two — into an output presized from the element count. The V4 per-operation spine gets the same treatment (opEventRaws): the ops Iter() sized every operation's whole interior to advance, then MustEvents() re-sized its Changes to locate the events. Events is the LAST OperationMetaV2 field, so the events offset (one locate) plus the events extent (measured by the same walk that collects them) positions the next operation — each operation's interior is now sized exactly once. The separate MustCount() + MustIter() double count-read collapses into the same helper. Same outputs (empty-not-nil contract preserved), same *xdr.ViewError surface on malformed input, recovered by the same TryVoid boundaries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zyTXU8wkocJgN6mBafnou
The pubnet fixture (249 txs) is representative but sparse; stellar-rpc's hot ingest sees ~6,000-tx stress ledgers where the per-element size passes are the whole cost. Add a crafted ledger at that density — V3-soroban / V4-soroban / V4-classic metas cycling so every extractor arm runs, LCM V2 (the modern TransactionResultMetaV1 element), per-op ledger-entry changes and per-tx diagnostics for a realistic spine — measuring parts, the parts+events composition, and all three products. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zyTXU8wkocJgN6mBafnou
tamirms
force-pushed
the
tamirms/extract-easy-wins
branch
from
August 5, 2026 05:40
11e0c4e to
7912f5b
Compare
tamirms
pushed a commit
to stellar/stellar-rpc
that referenced
this pull request
Aug 5, 2026
Pins the ingest easy-wins branch (stellar/go-stellar-sdk#5972) and adapts the one call site the branch's API change touches: TxEvents now hands back element views rather than [][]byte, so the stage filter reads tev.Stage() directly instead of re-wrapping each raw, and the per-op payload writes []byte(evView) — a free retype, same backing array. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TxEvents and LedgerTransactionView handed back raw byte slices, so the consumer that wanted to READ a field re-wrapped every element into a view (stellar-rpc's stage filter did exactly xdr.TransactionEventView(raw)), and the producer could not simply use the generated MustAll(): it returns []E, and Go cannot retype the outer slice to [][]byte even though E is ~[]byte. That forced a hand-rolled walk here purely to build the [][]byte directly. Element views ARE their wire bytes — trimmed to the element's extent — so []byte(elem) and View(bytes) are both free; only the outer slice allocates. Typing the fields as element views therefore costs a consumer that wants bytes nothing, saves a consumer that wants fields the re-wrap, and lets collection be the generated MustAll(). TxEvents.TransactionEvents [][]byte -> []xdr.TransactionEventView TxEvents.OperationEvents [][][]byte -> [][]xdr.ContractEventView LedgerTransactionView.DiagnosticEvents -> []xdr.DiagnosticEventView LedgerTransactionView.TransactionEvents -> []xdr.TransactionEventView LedgerTransactionView.ContractEvents -> [][]xdr.ContractEventView Every collection site is now MustAll(); collectRaws/collectRawsExtent and their two constraint interfaces are deleted. opEventRaws keeps stepping the OPERATIONS array by hand — Iter()/All() there would size each operation's whole interior to advance, then MustEvents() would re-walk Ext+Changes and collection would re-walk Events, about double — but its inner events array uses MustAll() like everything else, recovering the array's wire extent by summing the trimmed element lengths MustAll already produced. (An earlier draft of this commit claimed the extent could not be recovered from trimmed elements. It can: extent = count prefix + sum of element lengths.) BenchmarkLedgerTxPartsStressDensity vs the [][]byte form: parts_events -3.39%, all_products -2.82% (p=0.000, n=10); B/op and allocs/op unchanged. Net -26 lines. The win is the API and the deleted code; the time comes from generated concrete methods replacing a generic helper's dictionary dispatch. BREAKING: the two struct field types change. Callers wanting bytes write []byte(elem); callers wanting fields drop their wrapping step. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tamirms
force-pushed
the
tamirms/extract-easy-wins
branch
from
August 5, 2026 06:08
7912f5b to
46c4f74
Compare
tamirms
pushed a commit
to stellar/stellar-rpc
that referenced
this pull request
Aug 5, 2026
Pins the ingest easy-wins branch (stellar/go-stellar-sdk#5972) and adapts the one call site the branch's API change touches: TxEvents now hands back element views rather than [][]byte, so the stage filter reads tev.Stage() directly instead of re-wrapping each raw, and the per-op payload writes []byte(evView) — a free retype, same backing array. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tamirms
pushed a commit
to stellar/stellar-rpc
that referenced
this pull request
Aug 5, 2026
Pins the ingest easy-wins branch (stellar/go-stellar-sdk#5972) and adapts the one call site the branch's API change touches: TxEvents now hands back element views rather than [][]byte, so the stage filter reads tev.Stage() directly instead of re-wrapping each raw, and the per-op payload writes []byte(evView) — a free retype, same backing array. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tamirms
pushed a commit
to stellar/stellar-rpc
that referenced
this pull request
Aug 16, 2026
Pins the ingest easy-wins branch (stellar/go-stellar-sdk#5972) and adapts the one call site the branch's API change touches: TxEvents now hands back element views rather than [][]byte, so the stage filter reads tev.Stage() directly instead of re-wrapping each raw, and the per-op payload writes []byte(evView) — a free retype, same backing array. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Four self-contained follow-ups to #5966, all inside
ingest/. Three are allocation/size-pass removals on the extract path plus the benchmark that makes them measurable; the fourth is a deliberate, narrow breaking change to the events product types.The motivation is stellar-rpc's hot ingest, which runs
ExtractLedgerTxParts+ the events product on every closed ledger under a hard real-time budget. Its stress fixture is ~6,000 transactions per ledger, where the per-element size passes — not the parsing — are essentially the whole cost.The commits
1.
presize ExtractLedgerTxParts output from the TxProcessing countThe walk's element count is knowable before the walk starts: the array's length prefix is an O(1) read. The parts slice was growing through
appendreallocations anyway. Now it's presized.On a malformed prefix the count stays 0 and the walk itself reports the error, exactly as before — the presize is not a new validation point.
2.
size each event exactly once when collecting rawsTwo places were sizing the same bytes twice.
collectRawsconsumed the generatedIter(), which yields untrimmed suffix views and sizes each element to advance — thenMustRaw()sized that same element again to trim it. Since element views are plain byte views, the trimmed element is its raw bytes, so collecting by trimming and stepping past the trimmed extent gets both from one pass.The V4 per-operation spine (
opEventRaws) had the same shape one level up: the opsIter()sized every operation's whole interior to advance, thenMustEvents()re-sized itsChangesto locate the events. Sinceeventsis the lastOperationMetaV2field, the events offset plus the events extent is exactly what positions the next operation, so each operation's interior is sized once.3.
stress-density benchmark — 6,000 txs, ~10 events eachThe existing pubnet fixture (249 txs) is representative but sparse — too sparse for the per-element size passes to show up. This adds a crafted ledger at stress density, with V3-soroban / V4-soroban / V4-classic metas cycling so every extractor arm runs, LCM V2 (the modern
TransactionResultMetaV1element), plus per-op ledger-entry changes and per-tx diagnostics for a realistic spine. It measures parts, the parts+events composition, and all three products.4.
return element views, not [][]byte, from the events products— breakingTxEventsandLedgerTransactionViewreturned raw byte slices. That meant a consumer wanting to read a field re-wrapped every element back into a view (stellar-rpc's stage filter did literallyxdr.TransactionEventView(raw)), while the producer could not use the generatedMustAll()at all — it returns[]E, and Go cannot retype the outer slice to[][]byteeven thoughEis~[]byte.Element views ARE their wire bytes, so
[]byte(elem)andView(bytes)are both free; only the outer slice ever allocates. Typing the fields as element views costs a bytes-wanting consumer nothing, saves a field-reading consumer the re-wrap, and lets every collection site beMustAll().collectRaws/collectRawsExtentand their two constraint interfaces are deleted — net −26 lines. Callers wanting bytes write[]byte(elem); callers wanting fields drop their wrapping step.opEventRawsstill steps the operations array by hand, and that is the one place it's warranted:Iter()/All()there would size each operation's whole interior to advance, thenMustEvents()would re-walk Ext+Changes and collection would re-walk Events — about double. Its inner events array usesMustAll()like everything else, recovering the array's wire extent by summing the trimmed element lengthsMustAllalready produced.Measurements
BenchmarkLedgerTxPartsStressDensity, this branch vsmain. Both arms run the identical benchmark code — the base arm ismainwith commit 3's test file cherry-picked onto it, so the comparison isn't confounded by a differently-shaped benchmark on each side.-count=10, c6id.8xlarge (Xeon 8375C).partsparts_eventsall_productspartsB/opparts_eventsB/oppartsallocs/opparts_eventsallocs/opAll p=0.000 at n=10, run-to-run variance ±0–1%.
Within the branch, commit 4 contributes −3.39% on
parts_eventsover commit 2's form (−1.48% from the type change, a further −1.94% from dropping the generic helper for the generated method); allocations are unchanged by it at 16,010/op. Its value is the API and the deleted code, not the allocation count.Downstream check: pinned into stellar-rpc's hot ingest at 6,000 tx/ledger, this branch holds per-ledger
ingest_totalat p50 29.7 / p99 37.6 ms against a 100 ms budget.Why these four and not more
This branch is deliberately the low-risk subset. A larger redesign of the views walk was prototyped and measured separately and is not proposed here: it required either a generated API surface that grows quadratically in type count, or runtime ordering knowledge from every caller, and it carried a hand-mirrored locate layer duplicating generated code. These four need none of that — they're local to
ingest/, they don't touch the generated views, and each is independently revertible.🤖 Generated with Claude Code