Skip to content

Commit 931c62c

Browse files
Copilotclaude
andcommitted
parallelize classic-entry collection and TTL extraction, instrument post-tx apply
Three mechanical wins on the apply path (60-ledger SAC benchmark mean close 117.1 -> 112.3ms, tracy-less build): - collectModifiedClassicEntries now runs in two parallel phases on the apply pool: tx bundles are chunked across workers that bin classic footprint keys by global-map shard (hashing each key once), then one worker per shard dedups and copies the modified entries out of the ltx (pure read: getNewestVersionBelowRoot terminates at the root without touching its caches). setup_collect_classic 5.6 -> 0.8ms. - extractDirtyTTLShardEntries extracts the 16 global-map shards on parallel workers; the extracted vector is shared between the early in-memory updater and the TTL shard writer, which now makes its own sorted copy off the apply thread. par gap 7.1 -> 5.5ms. - post_tx_set_apply split into post_tx_refunds (8.1ms: per-tx fee refund writes through the ltx) and post_tx_results (1.6ms: result XDR serialization), exposing refunds as part of the serial-ltx cost pool for the next optimization round. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 8eb9ecf commit 931c62c

5 files changed

Lines changed: 177 additions & 52 deletions

File tree

src/ledger/LedgerManagerImpl.cpp

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ LedgerManagerImpl::ApplyState::updateInMemorySorobanState(
341341
void
342342
LedgerManagerImpl::ApplyState::startEarlyInMemorySorobanStateUpdate(
343343
std::vector<std::shared_future<std::shared_ptr<LiveBucket>>> threadShards,
344-
std::vector<BucketEntry> ttlEntries,
344+
std::shared_ptr<std::vector<BucketEntry> const> ttlEntries,
345345
SorobanNetworkConfig const& sorobanConfig, uint32_t ledgerVersion)
346346
{
347347
threadInvariant();
@@ -356,8 +356,7 @@ LedgerManagerImpl::ApplyState::startEarlyInMemorySorobanStateUpdate(
356356

357357
// The TTL entries are shared (read-only) between the byproduct scan and
358358
// the state application.
359-
auto sharedTtl = std::make_shared<std::vector<BucketEntry> const>(
360-
std::move(ttlEntries));
359+
auto sharedTtl = std::move(ttlEntries);
361360

362361
// Byproduct scan: modified-key set (for eviction) and new contract code
363362
// (for the module cache). Fast; runs in parallel with the state
@@ -3016,21 +3015,26 @@ LedgerManagerImpl::applySorobanStages(AppConnector& app, AbstractLedgerTxn& ltx,
30163015
// so read-only TTL bumps are fully reconciled: write the TTL shard
30173016
// optimistically (overlapping the ltx commit and post-apply work).
30183017
// The thread-shard futures are snapshotted first: the early
3019-
// in-memory updater consumes the TTL changes directly (by value)
3018+
// in-memory updater shares the extracted TTL entries with the shard
3019+
// writer (which makes its own sorted copy off the apply thread)
30203020
// rather than waiting on the TTL shard's file write.
30213021
auto threadShardFutures = mPendingLedgerShards;
3022-
auto ttlEntries = globalParState.extractDirtyTTLShardEntries();
3023-
auto ttlEntriesForUpdater = ttlEntries;
3024-
if (!ttlEntries.empty())
3022+
auto ttlEntries = std::make_shared<std::vector<BucketEntry> const>(
3023+
globalParState.extractDirtyTTLShardEntries(app));
3024+
if (!ttlEntries->empty())
30253025
{
30263026
auto& bm = mApp.getBucketManager();
30273027
auto& workerCtx = mApp.getWorkerIOContext();
30283028
bool doFsync = !mApp.getConfig().DISABLE_XDR_FSYNC;
30293029
uint32_t protocol = header.ledgerVersion;
30303030
mPendingLedgerShards.emplace_back(
30313031
std::async(std::launch::async,
3032-
[&bm, &workerCtx, protocol, doFsync,
3033-
entries = std::move(ttlEntries)]() mutable {
3032+
[&bm, &workerCtx, protocol, doFsync, ttlEntries]() {
3033+
// Copy + sort off the apply thread; the shared
3034+
// vector itself stays unsorted for the early
3035+
// updater (TTL keys are unique, so apply order
3036+
// doesn't matter there).
3037+
auto entries = *ttlEntries;
30343038
std::sort(entries.begin(), entries.end(),
30353039
BucketEntryIdCmp<LiveBucket>{});
30363040
return LiveBucket::freshShard(
@@ -3051,10 +3055,10 @@ LedgerManagerImpl::applySorobanStages(AppConnector& app, AbstractLedgerTxn& ltx,
30513055
// legacy seal-time updateState instead. Also skipped when this
30523056
// ledger produced no soroban changes (e.g. empty tx sets in tests).
30533057
if (bypassLtxForSorobanEntries(mApp.getConfig()) &&
3054-
(!threadShardFutures.empty() || !ttlEntriesForUpdater.empty()))
3058+
(!threadShardFutures.empty() || !ttlEntries->empty()))
30553059
{
30563060
mApplyState.startEarlyInMemorySorobanStateUpdate(
3057-
std::move(threadShardFutures), std::move(ttlEntriesForUpdater),
3061+
std::move(threadShardFutures), std::move(ttlEntries),
30583062
sorobanConfig, header.ledgerVersion);
30593063
}
30603064

@@ -3232,6 +3236,8 @@ LedgerManagerImpl::applyTransactions(
32323236
std::chrono::duration<double, std::milli>(txSubEnd - txSubStart)
32333237
.count();
32343238
mLastPhaseTimings.applySeqClassicMs = 0;
3239+
mLastPhaseTimings.postTxRefundsMs = 0;
3240+
mLastPhaseTimings.postTxResultsMs = 0;
32353241
#endif
32363242
std::vector<ApplyStage> applyStages;
32373243
for (auto const& phase : phases)
@@ -3471,6 +3477,9 @@ LedgerManagerImpl::processPostTxSetApply(
34713477
{
34723478
for (auto const& txBundle : stage)
34733479
{
3480+
#ifdef BUILD_TESTS
3481+
auto refundStart = std::chrono::steady_clock::now();
3482+
#endif
34743483
if (ledgerCloseMeta)
34753484
{
34763485
// Use child LTX for meta change tracking.
@@ -3496,13 +3505,26 @@ LedgerManagerImpl::processPostTxSetApply(
34963505
.getMeta()
34973506
.getTxEventManager());
34983507
}
3508+
#ifdef BUILD_TESTS
3509+
auto refundEnd = std::chrono::steady_clock::now();
3510+
mLastPhaseTimings.postTxRefundsMs +=
3511+
std::chrono::duration<double, std::milli>(refundEnd -
3512+
refundStart)
3513+
.count();
3514+
#endif
34993515

35003516
// setPostTxApplyFeeProcessing can update the feeCharged in
35013517
// the result, so this needs to be done after
35023518
processResultAndMeta(ledgerCloseMeta, txBundle.getTxNum(),
35033519
txBundle.getEffects().getMeta(),
35043520
*txBundle.getTx(),
35053521
txBundle.getResPayload(), txResultSet);
3522+
#ifdef BUILD_TESTS
3523+
mLastPhaseTimings.postTxResultsMs +=
3524+
std::chrono::duration<double, std::milli>(
3525+
std::chrono::steady_clock::now() - refundEnd)
3526+
.count();
3527+
#endif
35063528
}
35073529
}
35083530
}

src/ledger/LedgerManagerImpl.h

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,12 +276,13 @@ class LedgerManagerImpl : public LedgerManager
276276
// runs at seal via joinEarlyInMemorySorobanStateUpdate.
277277
// threadShards: the per-cluster shard futures (long since written
278278
// by updater start). ttlEntries: this ledger's reconciled TTL
279-
// changes, fed directly (by value) so the updater need not wait for
280-
// the TTL shard's file write.
279+
// changes, fed directly (shared with the TTL shard writer, which
280+
// makes its own sorted copy off the apply thread) so the updater
281+
// need not wait for the TTL shard's file write.
281282
void startEarlyInMemorySorobanStateUpdate(
282283
std::vector<std::shared_future<std::shared_ptr<LiveBucket>>>
283284
threadShards,
284-
std::vector<BucketEntry> ttlEntries,
285+
std::shared_ptr<std::vector<BucketEntry> const> ttlEntries,
285286
SorobanNetworkConfig const& sorobanConfig, uint32_t ledgerVersion);
286287

287288
bool
@@ -659,6 +660,11 @@ class LedgerManagerImpl : public LedgerManager
659660
double applyParallelPhaseTotalMs = 0;
660661
double applySeqClassicMs = 0;
661662
double postTxSetApplyMs = 0;
663+
// Sub-timings of postTxSetApplyMs: the per-tx post-apply fee
664+
// processing (refunds, written through the ltx) and the result/meta
665+
// collection (result XDR serialization + result set append).
666+
double postTxRefundsMs = 0;
667+
double postTxResultsMs = 0;
662668
double applyTxTailMs = 0;
663669
double destroyApplyStagesMs = 0;
664670
double applyUpgradesMs = 0;

src/simulation/ApplyLoad.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,10 @@ logPhaseTimingsTable(
284284
extract(&LedgerManagerImpl::LedgerClosePhaseTimings::applySeqClassicMs);
285285
auto postTxSetApply =
286286
extract(&LedgerManagerImpl::LedgerClosePhaseTimings::postTxSetApplyMs);
287+
auto postTxRefunds =
288+
extract(&LedgerManagerImpl::LedgerClosePhaseTimings::postTxRefundsMs);
289+
auto postTxResults =
290+
extract(&LedgerManagerImpl::LedgerClosePhaseTimings::postTxResultsMs);
287291
auto applyTxTail =
288292
extract(&LedgerManagerImpl::LedgerClosePhaseTimings::applyTxTailMs);
289293
auto destroyApplyStages = extract(
@@ -410,6 +414,8 @@ logPhaseTimingsTable(
410414
{"| *** par gap ***", computePhaseStats(parGap)},
411415
{"| apply_seq_classic", computePhaseStats(applySeqClassic)},
412416
{"| post_tx_set_apply", computePhaseStats(postTxSetApply)},
417+
{"| post_tx_refunds", computePhaseStats(postTxRefunds)},
418+
{"| post_tx_results", computePhaseStats(postTxResults)},
413419
{"| tail", computePhaseStats(applyTxTail)},
414420
{"| ~apply_stages", computePhaseStats(destroyApplyStages)},
415421
{"| *** tx gap ***", computePhaseStats(txGap)},

src/transactions/ParallelApplyUtils.cpp

Lines changed: 124 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ GlobalParallelApplyLedgerState::
503503
mSetupCommitWritesMs += setupLap();
504504
mSetupSeqWriteMs += gSeqPreApplyWriteMs;
505505
#endif
506-
collectModifiedClassicEntries(ltx, stages);
506+
collectModifiedClassicEntries(app, ltx, stages);
507507
return;
508508
}
509509

@@ -648,53 +648,120 @@ GlobalParallelApplyLedgerState::commitBufferedPreParallelApplyWrites(
648648

649649
void
650650
GlobalParallelApplyLedgerState::collectModifiedClassicEntries(
651-
AbstractLedgerTxn& ltx, std::vector<ApplyStage> const& stages)
651+
AppConnector& app, AbstractLedgerTxn& ltx,
652+
std::vector<ApplyStage> const& stages)
652653
{
653654
ZoneScoped;
654655
#ifdef BUILD_TESTS
655656
auto _collectT0 = std::chrono::steady_clock::now();
656657
#endif
657658

658-
std::unordered_set<LedgerKey> classicKeys;
659+
// Collect the classic footprint keys and copy their modified entries out
660+
// of the ltx, in two parallel phases on the (currently idle) apply pool:
661+
// first the tx bundles are chunked across workers, each binning classic
662+
// keys by target global-map shard (computing each key's hash once);
663+
// then one worker per shard deduplicates its keys and copies the
664+
// modified entries into that shard. The ltx reads are pure lookups
665+
// (getNewestVersionBelowRoot walks per-level entry maps and terminates
666+
// at the root without touching its caches), and distinct shard workers
667+
// touch disjoint submaps, so no synchronization is needed.
668+
std::vector<TxBundle const*> bundles;
659669
for (auto const& stage : stages)
660670
{
661671
for (auto const& txBundle : stage)
662672
{
663-
auto const& footprint =
664-
txBundle.getTx()->sorobanResources().footprint;
665-
for (auto const& key : footprint.readWrite)
666-
{
667-
if (!isSorobanEntry(key))
668-
{
669-
classicKeys.emplace(key);
670-
}
671-
}
672-
for (auto const& key : footprint.readOnly)
673-
{
674-
if (!isSorobanEntry(key))
675-
{
676-
classicKeys.emplace(key);
677-
}
678-
}
673+
bundles.push_back(&txBundle);
679674
}
680675
}
681676

682-
for (auto const& lk : classicKeys)
677+
auto& threadPool = app.getApplyThreadPool();
678+
threadPool.ensureWorkerCount(1);
679+
size_t const numChunks = std::max<size_t>(
680+
1, std::min<size_t>(kGlobalEntryMapShards, bundles.size()));
681+
std::vector<
682+
std::array<std::vector<ParallelApplyLedgerKey>, kGlobalEntryMapShards>>
683+
bins(numChunks);
683684
{
684-
auto entryPair = ltx.getNewestVersionBelowRoot(lk);
685-
if (!entryPair.first)
685+
std::vector<std::future<void>> futures;
686+
futures.reserve(numChunks);
687+
size_t const chunkSize = (bundles.size() + numChunks - 1) / numChunks;
688+
for (size_t c = 0; c < numChunks; ++c)
686689
{
687-
continue;
690+
futures.emplace_back(
691+
threadPool.submit([c, chunkSize, &bundles, &bins]() {
692+
size_t const begin = c * chunkSize;
693+
size_t const end =
694+
std::min(begin + chunkSize, bundles.size());
695+
auto binKey = [&](LedgerKey const& key) {
696+
if (isSorobanEntry(key))
697+
{
698+
return;
699+
}
700+
ParallelApplyLedgerKey pk(key);
701+
bins[c][globalMapShardOf(pk)].push_back(std::move(pk));
702+
};
703+
for (size_t i = begin; i < end; ++i)
704+
{
705+
auto const& fp =
706+
bundles[i]->getTx()->sorobanResources().footprint;
707+
for (auto const& key : fp.readWrite)
708+
{
709+
binKey(key);
710+
}
711+
for (auto const& key : fp.readOnly)
712+
{
713+
binKey(key);
714+
}
715+
}
716+
}));
688717
}
718+
for (auto& f : futures)
719+
{
720+
releaseAssert(f.valid());
721+
f.get();
722+
}
723+
}
689724

690-
GlobalParApplyLedgerEntryOpt entry = scopeAdoptEntryOpt(
691-
entryPair.second
692-
? std::make_optional(entryPair.second->ledgerEntry())
693-
: std::nullopt);
694-
695-
ParallelApplyLedgerKey pk(lk);
696-
globalMapShardFor(pk).emplace(std::move(pk),
697-
GlobalParallelApplyEntry{entry, false});
725+
{
726+
std::vector<std::future<void>> futures;
727+
futures.reserve(kGlobalEntryMapShards);
728+
for (size_t s = 0; s < kGlobalEntryMapShards; ++s)
729+
{
730+
futures.emplace_back(threadPool.submit([this, s, &bins, &ltx]() {
731+
auto& shard = mGlobalEntryMapShards[s];
732+
for (auto& bin : bins)
733+
{
734+
for (auto& pk : bin[s])
735+
{
736+
// Skip duplicates (and keys already collected by the
737+
// pre-apply phases), matching the no-op-on-duplicate
738+
// emplace semantics of the serial version.
739+
if (shard.find(pk) != shard.end())
740+
{
741+
continue;
742+
}
743+
auto entryPair =
744+
ltx.getNewestVersionBelowRoot(pk.ledgerKey());
745+
if (!entryPair.first)
746+
{
747+
continue;
748+
}
749+
GlobalParApplyLedgerEntryOpt entry = scopeAdoptEntryOpt(
750+
entryPair.second
751+
? std::make_optional(
752+
entryPair.second->ledgerEntry())
753+
: std::nullopt);
754+
shard.emplace(std::move(pk), GlobalParallelApplyEntry{
755+
entry, false});
756+
}
757+
}
758+
}));
759+
}
760+
for (auto& f : futures)
761+
{
762+
releaseAssert(f.valid());
763+
f.get();
764+
}
698765
}
699766

700767
#ifdef BUILD_TESTS
@@ -1086,13 +1153,35 @@ ThreadParallelApplyLedgerState::extractDirtySorobanShardEntries() const
10861153
}
10871154

10881155
std::vector<BucketEntry>
1089-
GlobalParallelApplyLedgerState::extractDirtyTTLShardEntries() const
1156+
GlobalParallelApplyLedgerState::extractDirtyTTLShardEntries(
1157+
AppConnector& app) const
10901158
{
1159+
ZoneScoped;
1160+
// Extract the shards on parallel workers (the apply pool is idle at this
1161+
// point: all stages have joined), then concatenate.
1162+
auto& threadPool = app.getApplyThreadPool();
1163+
threadPool.ensureWorkerCount(1);
1164+
std::array<std::vector<BucketEntry>, kGlobalEntryMapShards> perShard;
1165+
std::vector<std::future<void>> futures;
1166+
futures.reserve(kGlobalEntryMapShards);
1167+
for (size_t i = 0; i < kGlobalEntryMapShards; ++i)
1168+
{
1169+
futures.emplace_back(threadPool.submit([this, i, &perShard]() {
1170+
perShard[i] = extractDirtyEntriesForShard(mGlobalEntryMapShards[i],
1171+
*this, /*ttlOnly=*/true);
1172+
}));
1173+
}
1174+
size_t total = 0;
1175+
for (size_t i = 0; i < kGlobalEntryMapShards; ++i)
1176+
{
1177+
releaseAssert(futures[i].valid());
1178+
futures[i].get();
1179+
total += perShard[i].size();
1180+
}
10911181
std::vector<BucketEntry> entries;
1092-
for (auto const& shard : mGlobalEntryMapShards)
1182+
entries.reserve(total);
1183+
for (auto& shardEntries : perShard)
10931184
{
1094-
auto shardEntries =
1095-
extractDirtyEntriesForShard(shard, *this, /*ttlOnly=*/true);
10961185
entries.insert(entries.end(),
10971186
std::make_move_iterator(shardEntries.begin()),
10981187
std::make_move_iterator(shardEntries.end()));

src/transactions/ParallelApplyUtils.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,8 @@ class GlobalParallelApplyLedgerState
284284
AppConnector& app, AbstractLedgerTxn& ltx,
285285
std::vector<TxBundle const*> const& txBundles);
286286

287-
void collectModifiedClassicEntries(AbstractLedgerTxn& ltx,
287+
void collectModifiedClassicEntries(AppConnector& app,
288+
AbstractLedgerTxn& ltx,
288289
std::vector<ApplyStage> const& stages);
289290

290291
bool maybeMergeRoTTLBumps(ParallelApplyLedgerKey const& key,
@@ -343,8 +344,9 @@ class GlobalParallelApplyLedgerState
343344
// stages) as (unsorted) BucketEntries for a level-0 shard write. Must
344345
// be called after the last stage's commitChangesFromThreads (so
345346
// read-only TTL bumps have been max-merged) and before
346-
// commitChangesToLedgerTxn (which moves entries out).
347-
std::vector<BucketEntry> extractDirtyTTLShardEntries() const;
347+
// commitChangesToLedgerTxn (which moves entries out). Extracts the map
348+
// shards on the (idle) apply pool in parallel.
349+
std::vector<BucketEntry> extractDirtyTTLShardEntries(AppConnector& app) const;
348350

349351
// Consumes the global entry map: moves entries into the LedgerTxn
350352
// instead of copying. Must only be called once, as the final operation

0 commit comments

Comments
 (0)