@@ -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
649649void
650650GlobalParallelApplyLedgerState::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, <x]() {
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
10881155std::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 ()));
0 commit comments