Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Par apply improve#5312

Draft
dmkozh wants to merge 110 commits into
stellar:master from
dmkozh:par_apply_improve
Draft

Par apply improve #5312
dmkozh wants to merge 110 commits into
stellar:master from
dmkozh:par_apply_improve

Conversation

@dmkozh

@dmkozh dmkozh commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

No description provided.

dmkozh and others added 30 commits May 28, 2026 15:13
Replace xdrSha256(success) with streaming SHA256 calculation to avoid
XDR re-serialization of InvokeHostFunctionSuccessPreImage. The return
value and events are already available as XDR-encoded bytes, so we can
hash them directly without round-trip serialization.
Adds parallel processing to transaction set handling:
1. Parallel TxFrame creation: Creates TxFrames from XDR envelopes in
 parallel during transaction set deserialization. Uses work-stealing
 via std::async with even distribution across available threads.
2. Parallel transaction validation: Validates transactions in parallel
 in txsAreValid() when there are 2+ transactions.
3. Hash precomputation: Precomputes content and full hashes before
 parallel operations to avoid race conditions.
4. Test coverage: Adds StreamingShaTest for InvokeHostFunctionSuccessPreImage
 verification.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add sizeBytes field to ContractDataMapEntryT to cache the XDR serialized
size of ledger entries. This avoids repeated xdr_size() calls during
state updates, reducing CPU overhead in the hot path.
Also adds Tracy zone to updateState() for profiling visibility.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
During ledger close, three independent operations are now parallelized:
- addHotArchiveBatch (modifies mHotArchiveBucketList)
- addLiveBatch (modifies mLiveBucketList) - runs on main thread
- updateInMemorySorobanState (modifies mInMemorySorobanState)
These operations modify completely independent data structures and can
safely run concurrently. Added getInMemorySorobanStateForUpdate() to
allow direct access to mInMemorySorobanState during COMMITTING phase.
This reduces ledger close latency by overlapping CPU-bound operations.
# Conflicts:
#	src/ledger/LedgerManagerImpl.cpp
-5ms for 6400 SAC transfers scenario
libsodium uses a portable C SHA256 implementation, missing SHA-NI hardware
instructions available on Intel Xeon Platinum. OpenSSL automatically uses
SHA-NI, providing 4.6x speedup for streaming add() (893ns->193ns/call) and
56% total SHA256 self-time reduction (3,744ms->1,659ms per 30s trace).
Use opaque aligned storage for SHA256_CTX in the header to avoid naming
conflict between OpenSSL's ::SHA256 function and stellar::SHA256 class.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
...5ms/ledger)
Run LiveBucketIndex construction on async worker thread in parallel with
the put loop in mergeInMemory. Both read mergedEntries as const — fully
independent. Tracy confirms full overlap: index future wait averages 2.2μs.
finalizeLedgerTxnChanges drops from 164ms to 136ms per ledger.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When ledgerCloseMeta is null (meta tracking disabled), operate directly
on the parent LTX in processFeesSeqNums and processPostTxSetApply instead
of creating a child LTX per-transaction. The child LTX was only needed
for getChanges() meta tracking.
Saves ~41ms/ledger from eliminating ~10.6K child LTX create/commit
cycles. Combined with experiment 011 (meta tracking), TPS improves
from 10,688 to 12,736 (+19.2%).
Also raises APPLY_LOAD_MAX_SAC_TPS_MAX_TPS from 12000 to 15000.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
#	docs/apply-load-max-sac-tps.cfg
In commitChangesToLedgerTxn, determining whether an entry is INIT (new)
vs LIVE (existing) required calling mInMemorySorobanState.get() which
computes sha256(xdr_to_opaque(key)) for every CONTRACT_DATA entry.
With ~40K entries per ledger, this added ~16ms of SHA256 per ledger.
Track existence via a bool mIsNew flag in ParallelApplyEntry, set when
a TX creates an entry that didn't previously exist. This replaces the
expensive SHA256-based existence check with a simple boolean.
commitChangesToLedgerTxn: 72.6ms -> 44.2ms (-39%)
TPS: 16,640 -> 16,960 (+1.9%)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
#	src/transactions/ParallelApplyUtils.cpp
Add move overloads for createWithoutLoading/updateWithoutLoading and
ScopedLedgerEntryOpt::moveFromScope to eliminate two deep copies per
entry when committing parallel apply state to LedgerTxn. Reduces
commitChangesToLedgerTxn from 44ms to 39ms per ledger (-12.8%).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pre-load Soroban read-only entries (contract instance, code, TTL) into
the global parallel apply state during setup, so per-TX lookups hit
thread-local maps instead of traversing to InMemorySorobanState. Also
cache protocol version and skip Soroban merge tracking in
processFeesSeqNums, and use std::move for mLatestTxResultSet.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
#	docs/success/049-skip-child-ltx-processFeesSeqNums.md
Use bitset instead of maps and relax invariants a bit.
This is pretty impactful - -10ms apply time for SAC, -20ms apply time for soroswap
Pre-compute expected entry counts from footprint sizes and call reserve()
on ParallelApplyEntryMap containers before they accumulate entries.
Eliminates log2(N) rehash operations during parallel apply, yielding
-26% commitChangesFromThread and -27% commitChangesToLedgerTxn self-time.
+576 TPS (+3.1%): 18,368 → 18,944
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
#	src/transactions/ParallelApplyUtils.cpp
resolveBackgroundEvictionScan previously received an UnorderedSet<LedgerKey>
built by getAllKeysWithoutSealing() containing ~128K entries (~20ms to build),
but only performed ~10-100 lookups. Added isModifiedKey() to LedgerTxn for
direct O(1) lookups in the existing EntryMap, eliminating the set construction.
resolveEviction zone: 20ms -> 0.116ms per ledger (99.4% reduction).
TPS: 18,944 -> 19,328 avg (+2.0%).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace single global mutex + RandomEvictionCache with 16 sharded caches,
each with its own mutex. This eliminates contention when 4 parallel threads
verify signatures simultaneously. Also use maybeGet() instead of exists()+get()
double-lookup, fix ZoneText string heap allocations, make counters atomic,
and remove unused liveSnapshot copy in applySorobanStageClustersInParallel.
Sort lightweight 24-byte EntryRef structs (type tag + pointer) instead of
full BucketEntry objects (200-500 bytes) in convertToBucketEntry. Reduces
sort swap cost by ~12x and materializes final vector in one cache-friendly
sequential pass. Cuts convertToBucketEntry from 31.9ms to 25.4ms per ledger.
Benchmark: 13,760 -> 14,144 TPS (+384 TPS, +2.8%)
Skip building LedgerTxnDelta in setEffectsDeltaFromSuccessfulTx when
INVARIANT_CHECKS is empty. The delta is consumed exclusively by
checkOnOperationApply which iterates an empty list when no invariants
are configured. This eliminates ~285ms of shared_ptr allocations and
entry copies across 4 worker threads per ledger.
Benchmark: 12,736 -> 13,760 TPS (+1,024 TPS, +8.0%)
Copilot AI and others added 3 commits June 11, 2026 17:22
Two more serial-ltx phases converted to parallel-compute + serial-insert
(60-ledger SAC benchmark: 112.3 -> 101.2ms):
- commitBufferedPreParallelApplyWrites: the common case (plain soroban
 tx, no meta) computes the seqnum-bumped account entries on workers,
 reading the post-fee state below the root (a pure lookup); the
 one-time-signer scan also runs in the workers, falling back to the
 legacy path on the rare hit (sponsorship bookkeeping can touch other
 accounts). setup_commit_writes 12.7 -> 2.9ms.
- processFeesSeqNums: plain soroban txs whose source appears exactly
 once in the set have their fee math and account reads (from the LCL
 view; fee processing is the first writer of accounts in a ledger)
 staged on the apply pool; the serial loop inserts the charged
 accounts, collects results in order, and bumps the fee pool by the
 staged sum once. Classic txs, fee bumps, repeated sources and
 meta-enabled configurations keep the legacy per-tx path.
 process_fees_seqnums 13.7 -> 8.3ms.
Also widens a BL-size tolerance in the upgrade state-size test (the
sharded level-0 buckets add per-shard METAENTRY overhead) and makes
TransactionFrame::updateSorobanMetrics public for the staged path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Final batch of serial-ltx staging (60-ledger SAC benchmark:
101.2 -> 87.6ms, reaching the sub-90ms target):
- processPostTxSetApply (no-meta): each soroban tx's fee refund touches
 only its source account, so the refunded account states are computed
 on parallel workers, replicating refundSorobanFee's semantics on raw
 entries (balance add with buying-liabilities check, result
 finalization, fee events); the serial loop inserts the refunded
 accounts via flat per-bundle slots and adjusts the fee pool once.
 Fee bumps and merged accounts fall back to the legacy path.
 post_tx_set_apply 9.2 -> 5.9ms.
- buildPreApplyAccountOverlay builds per-chunk partial overlays on the
 apply pool and merges them serially (below-root reads are pure
 lookups; the per-key entry copies dominate). setup_seq_check
 7.4 -> 2.7ms.
- commitChangesToLedgerTxn writes directly into the apply ltx instead
 of through a nested child whose commit re-merged every entry (no
 atomicity benefit: failures abort). commit_to_ltx 5.0 -> 3.5ms.
- processFeesSeqNums replaces the full per-source occurrence count with
 an exclusion set built only from classic txs and fee bumps (soroban
 txs are limited to one per source per ledger by protocol).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dmkozh and others added 26 commits June 11, 2026 14:07
Pulls in three medida-side performance fixes for hot metrics shared by the
parallel apply threads:
- Meter::Mark no longer takes a mutex (tick election via CAS);
- CKMS sample compaction is a single-pass O(n+m) merge instead of
 mid-vector emplace/erase, eliminating multi-ms latency spikes under the
 histogram lock (the periodic apply-time spikes seen with metrics on);
- Histogram uses a plain mutex, and Histogram/Timer/Sample gain UpdateMany
 bulk-record APIs (one lock + one clock read per batch) used by the
 per-ledger metrics flush.
All behavior-preserving: CKMS output verified bit-exact against the old
implementation, and meter/EWMA semantics are unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SimpleTimer::Update is called from the parallel apply threads (notably the
bucket point-load timers), where the per-update mutex around max tracking
was a cross-thread contention point. The sum/count counters were already
atomic; with a compare-exchange max the whole timer is now lock-free.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With metrics enabled, every InvokeHostFunction op marked ~20 process-wide
meters and updated ~7 histograms/timers from the apply threads (~25
contended lock/cache-line operations per op), and the parallel path also
performed a registry lookup of the ledger.operation.apply timer per
transaction. On the apply-load benchmark (sac, TX=6000, T=8) this made
close time 282ms with metrics vs 107ms without, with p99 spikes from CKMS
compaction; DISABLE_SOROBAN_METRICS_FOR_TESTING existed to hide this.
Instead, record all per-op/per-tx metric updates into a per-thread
SorobanMetrics::ApplyMetricsBatch (meter increments as plain sums, sample
streams for percentile-bearing histograms/timers as raw value vectors,
one brief uncontended lock per record) and drain all batches into the
underlying medida metrics once per ledger in
publishAndResetLedgerWideMetrics(), using the new UpdateMany bulk APIs.
Observable metric values are preserved: meter counts are identical sums,
EWMA rates tick at 5s granularity either way, and histograms receive the
exact same sample stream, just at close time.
The sequential apply path still updates medida directly (main thread,
cheap after the medida fixes). Combined with those fixes, metrics-on
close time drops to ~106.5ms vs ~104.1ms metrics-off (200-ledger run),
i.e. metric overhead goes from ~164% to ~2%.
Also adds a regression test asserting the batched metrics are visible
after the ledger close that applied the transaction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Make the shard-hash lists (currShards/snapShards on HistoryStateBucket,
snapShards on FutureBucket) optional in text (JSON) archives: emitted only
when non-empty, tolerated when absent. Legacy archive states (and all
non-sharded states) keep their format byte-for-byte, fixing the
"Serialization round trip" test against the checked-in testdata.
This requires split load/save members rather than a const/non-const
serialize pair: cereal const_casts nested objects before dispatching, so
the non-const overload would be used for both directions.
Binary archives (the publish-queue checkpoint files read by
loadCheckpointHAS) have positional fields, so they always carry the lists;
skipping fields there corrupts the stream (this was the "Change ordering
of buffered ledgers" abort).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Make checkOnBucketApply per-slot: it now receives all buckets of a
(level, isCurr) slot in application order (newest first) once the slot is
fully applied, instead of one bucket at a time. For composite (sharded)
level-0 slots the per-shard offer-count check compared a single shard's
offers against the DB count over the whole slot's ledger range, tripping
BucketListIsConsistentWithDatabase with 'Incorrect OFFER count'.
The invariant threads intra-slot shadowing across the slot's buckets
itself (mirroring BucketApplicator's seen-keys behavior, including dead
keys), and runs the count check once per slot. ApplyBucketsWork snapshots
the seen-keys set per slot rather than per bucket. Includes the
InvariantTests TestInvariant signature update for the new interface.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Teach remaining test-side consumers of level-0 buckets to expand
composites to their shards, per the design rule that file-access APIs
assert on composites:
- BucketListIsConsistentWithDatabaseTests: writeBucketFile (copies shard
 files into the apply app) and doesBucketContain (iterates shards);
- BucketIndexTests: 'serialize bucket indexes' and 'bucket entry counters'
 check shard files/indexes instead of the file-less composite;
- ApplicationUtilsTests: damage a shard file (composites have none) in the
 offline self-check test.
Also regenerate the ledger-close-meta golden files (via
GENERATE_TEST_LEDGER_CLOSE_META=1): they had been stale since CAP-77 and
this branch changes bucketListHash (sharded level-0) and the parallel
tx-set clustering (see 'parallel apply scheduling and merge
optimizations'), which cascade into all header hashes and reorder the
same transactions within the generated sets (verified: same tx count and
result codes before/after).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fix a TSan-reported data race in the early in-memory soroban state
update: the async task launched by finalizeLedgerTxnChanges joins the
early update (consuming/moving its future) while the apply thread
re-queried hasEarlyInMemorySorobanStateUpdate() for the module-cache step
-- which was also a logic bug, since the new-contract-code addition was
silently skipped whenever the async join won the race. Capture the flag
before launching the task.
Also make the byproducts future a shared_future waited via per-thread
local copies: both the apply thread and the async task wait on it
concurrently, which is only allowed through distinct shared_future
objects.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fix a TSan-reported race in the 'invariant check concurrent with state
advance' test: the futureKeys bookkeeping set is inserted into by the main
thread while the scanner thread reads it; guard it with a mutex. (The
matchedKeys/unexpectedEntry bookkeeping is scanner-thread-only until the
join, so it needs no synchronization.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fix a cross-process file race: two loadgen tests write and then delete
LOADGEN_PREGENERATED_TRANSACTIONS_FILE under its default fixed name in the
shared working directory, so when they land in concurrently-running make
check partitions, one process's cleanup deletes the file under the other's
read ('failed to open XDR file: stellar-load-transactions.xdr'). Point the
test config at the per-instance test directory, like the other test
artifacts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 'TTL mismatch between cache and BL' section read entryData.sizeBytes
from a hash node after erasing it from the set (TSan heap-use-after-free).
Copy the size out before the erase.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TSan reported a lock-order inversion (potential deadlock) between
VirtualClock's mPendingActionQueueMutex and the peer recursive mutex:
overlay threads post actions to the main thread while holding peer locks
(peer mutex -> queue mutex), and the main thread destroyed queued action
closures while holding the queue mutex -- closures can own the last
reference to a CapacityTrackedMessage, whose destructor takes the peer
mutex (queue mutex -> peer mutex).
Swap the pending queue out under the lock and destroy (shutdown path) or
enqueue into the scheduler (crank path, where Scheduler::enqueue can drop
and thus destroy actions) outside it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The memory tracking in the underlying quorum checker library changed, so
the memory-limit interruption no longer triggers at these tests'
thresholds. Hide them with the '[.]' tag until the limits are re-tuned
against the new tracking; not critical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gate the rust-side tcmalloc global allocator behind a new 'tcmalloc'
cargo feature wired to the USE_TCMALLOC automake conditional. Sanitizer
builds (asan/tsan/memcheck) and non-Linux builds do not link the tcmalloc
implementation on the C++ side, so the unconditional rust dependency left
tc_memalign etc. undefined at link time (and would break sanitizer malloc
interception even if linked).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Make the unified (asan/tsan) rust build work with the locally-forked p26
host: the fork carries the output-buffer pooling API (and module-cache
optimizations) that the bridge calls for p26, so the unified build's
upstream git pin no longer compiles. Point the soroban-env-host-p26
dependency at the local submodule path, exclude the soroban submodule
workspaces from the root workspace, de-inherit workspace keys in the p26
member manifests (submodule commit; cargo cannot resolve nested-workspace
inheritance for path dependencies), and re-resolve the unified lockfile
(also bumping num-bigint 0.4.4 -> 0.4.6, required by ark-ff 0.5's
TryFrom<BigInt> usage under -Zbuild-std).
Also add a tsan.supp entry for vendored asio's signal self-pipe
(asio_signal_handler write vs close-at-shutdown), the linux analogue of
the existing kqueue entries.
With this (and the tcmalloc feature gating), the full test suite builds
and passes under --enable-threadsanitizer
--enable-unified-rust-unsafe-for-production; run it from src/ (the
libsodium guard-page self-test is sanitizer-incompatible) with
SKIP_SOROBAN_TESTS=1 (check-sorobans re-runs host tests on the nightly
channel, where old ethnum in p21/p22 does not compile).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Run the per-transaction checkValid calls of
TxSetUtils::getInvalidTxListWithErrors on the apply thread pool instead
of sequentially on the main thread. Every check is an independent
read-only query against the immutable LCL snapshot; each worker gets its
own CheckValidLedgerViewWrapper (per-view bucket stream caches are not
thread-safe) and results are merged on the main thread in input order,
so the outcome is identical to the sequential loop.
The blocker for off-main-thread validation was
getLastClosedSorobanNetworkConfig() (main-thread-only, returns a
reference that can dangle across LCL replacement). The ledger view
snapshot already carries its own SorobanNetworkConfig copy, so expose it
through AbstractLedgerView/CheckValidLedgerViewWrapper and prefer it in
checkValid; the SQL-backed test views return nullptr and fall back to
the LedgerManager accessor on the main thread as before.
Benchmark (apply-load SAC model, 6000 txs/ledger, 8 worker threads,
50 ledgers): soroban-phase trim_invalid mean 33.9ms -> 18.6ms, total tx
set build 81.8ms -> 68.4ms. The remaining trim cost is dominated by the
sequential second-pass per-tx fee-source balance check, addressed
separately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The second pass of getInvalidTxListWithErrors re-loaded every fee
source account from the ledger snapshot to compare its balance against
the accumulated fees of all candidate transactions. checkValid has
already verified that the fee source can pay the full fee of each
individual transaction, so the cumulative check only adds information
when several candidate transactions share a fee source (only possible
via fee bumps, since the queue enforces one tx per source account).
Track the per-account candidate tx count in the (renamed) AccountFeeMap
and only re-load accounts with more than one candidate, eliminating
~one account load per transaction from the nomination hot path.
Benchmark (apply-load SAC model, 6000 txs/ledger, 8 worker threads,
50 ledgers): soroban-phase trim_invalid mean 18.6ms -> 15.3ms, total
tx set build 68.4ms -> 62.3ms.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TxSetXDRFrame::makeFromWire copied the entire (Generalized)TransactionSet
into the frame, then traversed it twice more: once for xdr_argpack_size
and once for the streaming contents hash. Add move overloads of
makeFromWire (used by toWireTxSetFrame/makeEmpty/history, where the XDR
is a freshly-built local) to eliminate the deep copy of every envelope,
and compute the encoded size during the hash traversal with a combined
XDR hasher, eliminating the separate size pass for generalized tx sets.
Benchmark (apply-load SAC model, 6000 txs/ledger, 8 worker threads,
50 ledgers): to_wire mean 15.2ms -> 11.5ms, total tx set build
62.3ms -> 59.2ms.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
prepareBuilderTxs hashed every footprint key, sorted the full entry
list and marked the conflict pairs sequentially on the main thread; in
the builder benchmark this flat cost dominated (a zero-conflict run of
10k txs with ~40-key footprints spent ~60ms of its 73ms there).
Parallelize all three passes across worker threads:
- footprint key hashing fills disjoint ranges of a preallocated entry
 array, chunked by key count;
- the single large sort is replaced by a scatter into 256 buckets keyed
 by the top hash bits (uniform thanks to siphash) followed by
 independent per-bucket sorts;
- conflict marking first collects directed half-edges binned by the tx
 id to update (txId % nThreads), then applies them with each thread
 exclusively owning its tx ids, so the BitSet writes race-free.
Also overlap the whole conflict discovery with the fee-order sort and
the per-tx resource precomputation, which don't depend on it.
The builder outputs are bit-identical to the sequential version
(verified by identical included-tx counts and instruction utilization
across all benchmark scenarios and seeds).
"parallel tx set building benchmark" (10k txs, mean ms/build):
 conflicts/tx=0: 73.3 -> 30.6
 0.1 (5 RO, 1 RW per key): 72.8 -> 33.3
 0.5 (2 RO, 2 RW): 94.8 -> 50.6
 1 (1000 RO, 1 RW): 71.7 -> 35.4
 5 (0 RO, 3 RW): 84.1 -> 40.2
 10 (40 RO, 1 RW): 109.4 -> 64.6
 20 (40 RO, 1 RW): 101.4 -> 57.0
 10 (10 RO, 10 RW): 141.7 -> 92.2
 50 (50 RO, 5 RW): 148.0 -> 69.1
Apply-load SAC model (6000 txs/ledger, 8 threads, small footprints):
parallel_build mean 17.7ms -> 14.9ms, total tx set build
59.2ms -> 55.5ms.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three changes to ApplicableTxSetFrame validation:
- Replace the per-stage cluster footprint conflict check (which copied
 every LedgerKey into per-stage hash sets) with a sorted-hash group
 scan: hash every footprint key (in parallel across worker threads),
 group entries by hash, and only run exact key comparisons on groups
 that span several clusters and contain a read-write key. A real
 conflict confirms immediately; a spurious 64-bit collision is
 rejected by the exact comparison, so the accept/reject semantics are
 unchanged.
- Return the transaction by const reference from the phase iterator
 instead of by value; every full-phase iteration (tx type checks, fee
 checks, resource sums) was paying a shared_ptr refcount bump per tx.
- Call getResources once instead of twice per tx in getTotalResources.
Benchmark (apply-load SAC model, 6000 txs/ledger, 8 worker threads,
50 ledgers): validate_txset mean 9.6ms -> 3.9ms, total tx set build
55.5ms -> 46.9ms.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a stage is at capacity, every further conflict-free tryAdd
allocated a merged cluster, mutated the cluster list, scanned all the
bins and then rolled everything back just to conclude that the
transaction doesn't fit. Since a conflict-free transaction forms a
singleton cluster, it fits in-place iff it fits into the least loaded
bin, so track the minimum bin load and reject such transactions with a
single comparison (the one-off full-repacking attempt is preserved).
Successful additions share the bookkeeping via finalizeAddedCluster.
The packing decisions are unchanged: first-fit bin selection and the
repacking heuristics are exactly as before (verified by identical
included-tx counts and instruction utilization across all benchmark
scenarios).
"parallel tx set building benchmark" (10k txs, mean ms/build):
 conflicts/tx=0: 30.6 -> 26.3
 0.1 (5 RO, 1 RW per key): 33.3 -> 30.8
 0.5 (2 RO, 2 RW): 46.3 (from 50.6)
 1 (1000 RO, 1 RW): 35.4 -> 30.2
 5 (0 RO, 3 RW): 40.2 -> 38.5
 10 (40 RO, 1 RW): 64.6 -> 62.8
 20 (40 RO, 1 RW): 57.0 -> 53.3
 10 (10 RO, 10 RW): 92.2 -> 82.8
 50 (50 RO, 5 RW): 69.1 -> 67.8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both are pure functions of the immutable transaction envelope but were
recomputed as full XDR traversals on every use: check_xdr_depth on every
checkValid call (~12% of the parallel trim CPU in the apply-load
profile) and xdr_size on every fee/resource computation (getResources in
the tx set builder, Soroban resource fee in checkValid, etc). Cache them
in relaxed atomics (concurrent validation threads may compute them
independently but always store the same value); clearCached() resets
them for the test paths that mutate envelopes in place.
The wins compound across the nomination pipeline since the same frames
flow through queue admission, trimming, surge pricing and validation:
in the apply-load SAC benchmark (6000 txs/ledger, 8 threads) the
builder's resource precompute reuses the sizes cached by trim,
parallel_build mean 11.4ms -> 9.8ms. Repeat checkValid calls (queue
admission, then trim, then SCP tx set validation) skip the depth
traversal entirely.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Building the GeneralizedTransactionSet deep-copies every transaction
envelope into the XDR structure; this dominated the remaining to_wire
cost. Pre-size the stage/cluster structure, then run the copies as
independent ~256-tx chunks across worker threads (each chunk covers a
disjoint index range of a single cluster, so there are no writer
races). The thread budget flows from makeTxSetFromTransactions
(LEDGER_CLOSE_WORKER_THREADS); all other callers keep the sequential
default.
Benchmark (apply-load SAC model, 6000 txs/ledger, 8 worker threads,
50 ledgers): to_wire mean 10.3ms -> 6.2ms; total tx set build
46.1ms -> 40.3ms together with the previous commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

Copilot code review Copilot Awaiting requested review from Copilot Copilot will automatically review once the pull request is marked ready for review

At least 1 approving review is required to merge this pull request.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

AltStyle によって変換されたページ (->オリジナル) /