Skip to content

Commit 3cfb625

Browse files
authored
Bucket query asserts and perf fix (#5323)
# Description The first commit resolves #5067. The 2nd commit addresses a perf issue we saw with the BucketList refactor. We build snapshot objects in parallel contents. Currently, the constructor gets a fresh set of metric references for each snapshot, acquiring a lock to do so. This changes it such that we use a single shared metrics object across all snapshots. Metrics are thread safe, so there should be no safety issue with this, and it makes the snapshot constructor much cheaper. # Checklist - [x] Reviewed the [contributing](https://github.com/stellar/stellar-core/blob/master/CONTRIBUTING.md#submitting-changes) document - [x] Rebased on top of master (no merge commits) - [x] Ran `clang-format` v8.0.0 (via `make format` or the Visual Studio extension) - [x] Compiles - [x] Ran all tests - [x] If change impacts performance, include supporting evidence per the [performance document](https://github.com/stellar/stellar-core/blob/master/performance-eval/performance-eval.md)
2 parents 8dbfbf4 + a8d0c5f commit 3cfb625

5 files changed

Lines changed: 156 additions & 35 deletions

File tree

src/bucket/BucketListSnapshot.cpp

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -53,27 +53,44 @@ BucketListSnapshotData<BucketT>::BucketListSnapshotData(
5353
{
5454
}
5555

56+
//
57+
// BucketSnapshotMetrics
58+
//
59+
60+
template <class BucketT>
61+
BucketSnapshotMetrics<BucketT>::BucketSnapshotMetrics(MetricsRegistry& metrics)
62+
: mPointTimers([&metrics]() {
63+
UnorderedMap<LedgerEntryType, std::reference_wrapper<SimpleTimer>>
64+
timers;
65+
for (auto t : xdr::xdr_traits<LedgerEntryType>::enum_values())
66+
{
67+
auto const& label = xdr::xdr_traits<LedgerEntryType>::enum_name(
68+
static_cast<LedgerEntryType>(t));
69+
auto& metric = metrics.NewSimpleTimer(
70+
{BucketT::METRIC_STRING, label}, std::chrono::microseconds{1});
71+
timers.emplace(static_cast<LedgerEntryType>(t), metric);
72+
}
73+
return timers;
74+
}())
75+
, mBulkLoadMeter(
76+
metrics.NewMeter({BucketT::METRIC_STRING, "query", "loads"}, "query"))
77+
{
78+
}
79+
5680
//
5781
// SearchableBucketListSnapshot
5882
//
5983

6084
template <class BucketT>
6185
SearchableBucketListSnapshot<BucketT>::SearchableBucketListSnapshot(
6286
MetricsRegistry& metrics,
87+
std::shared_ptr<BucketSnapshotMetrics<BucketT> const> snapshotMetrics,
6388
std::shared_ptr<BucketListSnapshotData<BucketT> const> data)
6489
: mData(std::move(data))
6590
, mMetrics(metrics)
66-
, mBulkLoadMeter(
67-
metrics.NewMeter({BucketT::METRIC_STRING, "query", "loads"}, "query"))
91+
, mSnapshotMetrics(std::move(snapshotMetrics))
6892
{
69-
for (auto t : xdr::xdr_traits<LedgerEntryType>::enum_values())
70-
{
71-
auto const& label = xdr::xdr_traits<LedgerEntryType>::enum_name(
72-
static_cast<LedgerEntryType>(t));
73-
auto& metric = metrics.NewSimpleTimer({BucketT::METRIC_STRING, label},
74-
std::chrono::microseconds{1});
75-
mPointTimers.emplace(static_cast<LedgerEntryType>(t), metric);
76-
}
93+
releaseAssert(mSnapshotMetrics);
7794
}
7895

7996
template <class BucketT>
@@ -82,9 +99,8 @@ SearchableBucketListSnapshot<BucketT>::SearchableBucketListSnapshot(
8299
: mData(other.mData)
83100
// mStreams intentionally left empty — each copy gets its own stream cache
84101
, mMetrics(other.mMetrics)
85-
, mPointTimers(other.mPointTimers)
102+
, mSnapshotMetrics(other.mSnapshotMetrics)
86103
, mBulkTimers(other.mBulkTimers)
87-
, mBulkLoadMeter(other.mBulkLoadMeter)
88104
{
89105
}
90106

@@ -98,20 +114,45 @@ SearchableBucketListSnapshot<BucketT>::operator=(
98114
mData = other.mData;
99115
mStreams.clear();
100116
mMetrics = other.mMetrics;
101-
mPointTimers = other.mPointTimers;
117+
mSnapshotMetrics = other.mSnapshotMetrics;
102118
mBulkTimers = other.mBulkTimers;
103-
mBulkLoadMeter = other.mBulkLoadMeter;
119+
#ifdef BUILD_TESTS
120+
// Reset thread ownership so the copy can be claimed by another thread.
121+
mThreadId.store(std::thread::id{});
122+
#endif
104123
}
105124
return *this;
106125
}
107126

127+
// Bucket loads are not thread safe and a single snapshot instance should only
128+
// be queried by one thread. We cache the initial caller's thread id and assert
129+
// following queries are from the same thread. Note: this only guards the
130+
// bucket-loading query entry points; access to the immutable underlying
131+
// snapshot data is thread safe.
132+
template <class BucketT>
133+
void
134+
SearchableBucketListSnapshot<BucketT>::threadInvariant() const
135+
{
136+
#ifdef BUILD_TESTS
137+
auto const current = std::this_thread::get_id();
138+
std::thread::id unclaimed{};
139+
// Atomically claim ownership on first use, so any concurrent claimant sees
140+
// the CAS fail with `unclaimed` set to the owner's id and asserts.
141+
if (!mThreadId.compare_exchange_strong(unclaimed, current))
142+
{
143+
releaseAssert(unclaimed == current);
144+
}
145+
#endif
146+
}
147+
108148
// File streams are fairly expensive to create, so they are lazily created and
109149
// stored in mStreams.
110150
template <class BucketT>
111151
XDRInputFileStream&
112152
SearchableBucketListSnapshot<BucketT>::getStream(
113153
std::shared_ptr<BucketT const> const& bucket) const
114154
{
155+
threadInvariant();
115156
BucketT const* key = bucket.get();
116157
auto it = mStreams.find(key);
117158
if (it == mStreams.end())
@@ -307,9 +348,10 @@ SearchableBucketListSnapshot<BucketT>::load(LedgerKey const& k) const
307348
{
308349
ZoneScoped;
309350
releaseAssert(mData);
351+
threadInvariant();
310352

311-
auto timerIter = mPointTimers.find(k.type());
312-
releaseAssert(timerIter != mPointTimers.end());
353+
auto timerIter = mSnapshotMetrics->mPointTimers.find(k.type());
354+
releaseAssert(timerIter != mSnapshotMetrics->mPointTimers.end());
313355
auto timer = timerIter->second.get().TimeScope();
314356

315357
std::shared_ptr<typename BucketT::LoadT const> result{};
@@ -341,9 +383,13 @@ medida::Timer&
341383
SearchableBucketListSnapshot<BucketT>::getBulkLoadTimer(
342384
std::string const& label, size_t numEntries) const
343385
{
386+
// mBulkTimers is per-snapshot mutable state lazily populated here, so this
387+
// must be single-threaded. Enforced here as well as at the public query
388+
// entry points.
389+
threadInvariant();
344390
if (numEntries != 0)
345391
{
346-
mBulkLoadMeter.get().Mark(numEntries);
392+
mSnapshotMetrics->mBulkLoadMeter.get().Mark(numEntries);
347393
}
348394

349395
auto iter = mBulkTimers.find(label);
@@ -370,8 +416,10 @@ SearchableBucketListSnapshot<BucketT>::getSnapshotData() const
370416

371417
SearchableLiveBucketListSnapshot::SearchableLiveBucketListSnapshot(
372418
MetricsRegistry& metrics,
419+
std::shared_ptr<BucketSnapshotMetrics<LiveBucket> const> snapshotMetrics,
373420
std::shared_ptr<BucketListSnapshotData<LiveBucket> const> data)
374-
: SearchableBucketListSnapshot<LiveBucket>(metrics, std::move(data))
421+
: SearchableBucketListSnapshot<LiveBucket>(
422+
metrics, std::move(snapshotMetrics), std::move(data))
375423
{
376424
}
377425

@@ -383,6 +431,7 @@ SearchableBucketListSnapshot<BucketT>::loadKeys(
383431
{
384432
ZoneScoped;
385433
releaseAssert(mData);
434+
threadInvariant();
386435
auto timer = getBulkLoadTimer(label, inKeys.size()).TimeScope();
387436

388437
auto keys = inKeys;
@@ -406,6 +455,7 @@ SearchableLiveBucketListSnapshot::loadPoolShareTrustLinesByAccountAndAsset(
406455
{
407456
ZoneScoped;
408457
releaseAssert(mData);
458+
threadInvariant();
409459

410460
LedgerKeySet trustlinesToLoad;
411461

@@ -448,6 +498,7 @@ SearchableLiveBucketListSnapshot::loadInflationWinners(size_t maxWinners,
448498
{
449499
ZoneScoped;
450500
releaseAssert(mData);
501+
threadInvariant();
451502

452503
auto timer = getBulkLoadTimer("inflationWinners", 0).TimeScope();
453504

@@ -548,6 +599,7 @@ SearchableLiveBucketListSnapshot::scanForEviction(
548599
ZoneScoped;
549600
releaseAssert(mData);
550601
releaseAssert(stats);
602+
threadInvariant();
551603

552604
auto getBucketFromIter =
553605
[&levels = mData->levels](
@@ -599,6 +651,7 @@ SearchableLiveBucketListSnapshot::scanForEntriesOfType(
599651
{
600652
ZoneScoped;
601653
releaseAssert(mData);
654+
threadInvariant();
602655

603656
auto scanBucket = [&](std::shared_ptr<LiveBucket const> const& bucket) {
604657
if (bucket->isEmpty())
@@ -806,8 +859,11 @@ SearchableLiveBucketListSnapshot::scanForEvictionInBucket(
806859

807860
SearchableHotArchiveBucketListSnapshot::SearchableHotArchiveBucketListSnapshot(
808861
MetricsRegistry& metrics,
862+
std::shared_ptr<BucketSnapshotMetrics<HotArchiveBucket> const>
863+
snapshotMetrics,
809864
std::shared_ptr<BucketListSnapshotData<HotArchiveBucket> const> data)
810-
: SearchableBucketListSnapshot<HotArchiveBucket>(metrics, std::move(data))
865+
: SearchableBucketListSnapshot<HotArchiveBucket>(
866+
metrics, std::move(snapshotMetrics), std::move(data))
811867
{
812868
}
813869

@@ -817,6 +873,7 @@ SearchableHotArchiveBucketListSnapshot::scanAllEntries(
817873
{
818874
ZoneScoped;
819875
releaseAssert(mData);
876+
threadInvariant();
820877

821878
auto scanBucket =
822879
[&](std::shared_ptr<HotArchiveBucket const> const& bucket) {
@@ -841,6 +898,8 @@ SearchableHotArchiveBucketListSnapshot::scanAllEntries(
841898
// Explicit template instantiations
842899
template struct BucketListSnapshotData<LiveBucket>;
843900
template struct BucketListSnapshotData<HotArchiveBucket>;
901+
template struct BucketSnapshotMetrics<LiveBucket>;
902+
template struct BucketSnapshotMetrics<HotArchiveBucket>;
844903
template class SearchableBucketListSnapshot<LiveBucket>;
845904
template class SearchableBucketListSnapshot<HotArchiveBucket>;
846905

src/bucket/BucketListSnapshot.h

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
#include "util/XDRStream.h"
1515
#include "xdr/Stellar-ledger-entries.h"
1616

17+
#include <atomic>
1718
#include <functional>
1819
#include <list>
1920
#include <memory>
2021
#include <set>
2122
#include <string>
23+
#include <thread>
2224
#include <vector>
2325

2426
namespace medida
@@ -64,14 +66,38 @@ template <class BucketT> struct BucketListSnapshotData
6466
explicit BucketListSnapshotData(BucketListBase<BucketT> const& bl);
6567
};
6668

69+
// Pre-resolved metric references for snapshot queries. Resolving a metric
70+
// takes the global MetricsRegistry lock, so metrics are resolved once and
71+
// shared across snapshots rather than re-resolved in every
72+
// SearchableBucketListSnapshot constructor.
73+
template <class BucketT> struct BucketSnapshotMetrics
74+
{
75+
BUCKET_TYPE_ASSERT(BucketT);
76+
77+
// Tracks load times for each LedgerEntryType. We use
78+
// SimpleTimer since medida Timer overhead is too expensive for point
79+
// loads.
80+
UnorderedMap<LedgerEntryType, std::reference_wrapper<SimpleTimer>> const
81+
mPointTimers;
82+
std::reference_wrapper<medida::Meter> const mBulkLoadMeter;
83+
84+
explicit BucketSnapshotMetrics(MetricsRegistry& metrics);
85+
};
86+
6787
// SearchableBucketListSnapshot provides BucketList lookup functionality.
6888
// Each snapshot maintains its own stream cache for file I/O and a pointer to
6989
// immutable snapshot data (ledger header, list of referenced buckets, etc).
7090
//
7191
// Thread-safety:
72-
// - A single snapshot instance must only be used by one thread at a time.
92+
// - A single snapshot instance must only be used by one thread at a time. In
93+
// test builds this is enforced by threadInvariant(): the first thread to
94+
// issue a query claims the snapshot, and subsequent queries from a different
95+
// thread trigger an assertion.
7396
// - The underlying snapshot data (shared via shared_ptr) is immutable and
7497
// can be safely shared.
98+
// - To query from another thread, make a copy: copies reset the cached stream
99+
// cache and (in test builds) the owning-thread id, so each copy can be
100+
// claimed by a different thread.
75101
template <class BucketT> class SearchableBucketListSnapshot
76102
{
77103
BUCKET_TYPE_ASSERT(BucketT);
@@ -84,18 +110,30 @@ template <class BucketT> class SearchableBucketListSnapshot
84110
mutable UnorderedMap<BucketT const*, std::unique_ptr<XDRInputFileStream>>
85111
mStreams;
86112

113+
#ifdef BUILD_TESTS
114+
// Thread id of the first thread to issue a query on this snapshot instance.
115+
// Used by threadInvariant() to assert that a single snapshot is not queried
116+
// concurrently from multiple threads. Reset on copy so each copy can be
117+
// claimed by a different thread.
118+
mutable std::atomic<std::thread::id> mThreadId{};
119+
#endif
120+
121+
// Bucket loads are not thread safe and a single snapshot instance should
122+
// only be queried by one thread. We cache the initial caller's thread id
123+
// and assert following queries are from the same thread (test builds only).
124+
void threadInvariant() const;
125+
87126
std::reference_wrapper<MetricsRegistry> mMetrics;
88127

89-
// Tracks load times for each LedgerEntryType. We use
90-
// SimpleTimer since medida Timer overhead is too expensive for point loads.
91-
UnorderedMap<LedgerEntryType, std::reference_wrapper<SimpleTimer>>
92-
mPointTimers;
128+
// Pre-resolved point load timers and bulk load meter, shared across
129+
// snapshots (see BucketSnapshotMetrics).
130+
std::shared_ptr<BucketSnapshotMetrics<BucketT> const> mSnapshotMetrics;
93131

94132
// Bulk load timers take significantly longer, so the timer overhead is
95-
// comparatively negligible.
133+
// comparatively negligible. Resolved lazily per label, so kept
134+
// per-instance rather than in BucketSnapshotMetrics.
96135
mutable UnorderedMap<std::string, std::reference_wrapper<medida::Timer>>
97136
mBulkTimers;
98-
std::reference_wrapper<medida::Meter> mBulkLoadMeter;
99137

100138
// Returns (lazily-constructed) file stream for bucket file. Note
101139
// this might be in some random position left over from a previous read --
@@ -136,6 +174,7 @@ template <class BucketT> class SearchableBucketListSnapshot
136174

137175
SearchableBucketListSnapshot(
138176
MetricsRegistry& metrics,
177+
std::shared_ptr<BucketSnapshotMetrics<BucketT> const> snapshotMetrics,
139178
std::shared_ptr<BucketListSnapshotData<BucketT> const> data);
140179

141180
public:
@@ -170,6 +209,8 @@ class SearchableLiveBucketListSnapshot
170209
{
171210
SearchableLiveBucketListSnapshot(
172211
MetricsRegistry& metrics,
212+
std::shared_ptr<BucketSnapshotMetrics<LiveBucket> const>
213+
snapshotMetrics,
173214
std::shared_ptr<BucketListSnapshotData<LiveBucket> const> data);
174215

175216
Loop scanForEvictionInBucket(
@@ -210,6 +251,8 @@ class SearchableHotArchiveBucketListSnapshot
210251
{
211252
SearchableHotArchiveBucketListSnapshot(
212253
MetricsRegistry& metrics,
254+
std::shared_ptr<BucketSnapshotMetrics<HotArchiveBucket> const>
255+
snapshotMetrics,
213256
std::shared_ptr<BucketListSnapshotData<HotArchiveBucket> const> data);
214257

215258
public:
@@ -226,6 +269,8 @@ class SearchableHotArchiveBucketListSnapshot
226269

227270
extern template struct BucketListSnapshotData<LiveBucket>;
228271
extern template struct BucketListSnapshotData<HotArchiveBucket>;
272+
extern template struct BucketSnapshotMetrics<LiveBucket>;
273+
extern template struct BucketSnapshotMetrics<HotArchiveBucket>;
229274
extern template class SearchableBucketListSnapshot<LiveBucket>;
230275
extern template class SearchableBucketListSnapshot<HotArchiveBucket>;
231276

src/ledger/ImmutableLedgerView.cpp

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -225,12 +225,16 @@ ImmutableLedgerData::checkInvariant() const
225225
ImmutableLedgerData::ImmutableLedgerData(
226226
LiveBucketList const& liveBL, HotArchiveBucketList const& hotArchiveBL,
227227
LedgerHeaderHistoryEntry const& lcl, HistoryArchiveState const& has,
228-
std::optional<SorobanNetworkConfig> sorobanConfig)
228+
std::optional<SorobanNetworkConfig> sorobanConfig, MetricsRegistry& metrics)
229229
: mLiveBucketData(
230230
std::make_shared<BucketListSnapshotData<LiveBucket>>(liveBL))
231231
, mHotArchiveBucketData(
232232
std::make_shared<BucketListSnapshotData<HotArchiveBucket>>(
233233
hotArchiveBL))
234+
, mLiveSnapshotMetrics(
235+
std::make_shared<BucketSnapshotMetrics<LiveBucket>>(metrics))
236+
, mHotArchiveSnapshotMetrics(
237+
std::make_shared<BucketSnapshotMetrics<HotArchiveBucket>>(metrics))
234238
, mSorobanConfig(std::move(sorobanConfig))
235239
, mLastClosedLedgerHeader(lcl)
236240
, mLastClosedHistoryArchiveState(has)
@@ -275,19 +279,22 @@ ImmutableLedgerData::createAndMaybeLoadConfig(
275279
// Bootstrap: build a lightweight temporary state just to load config
276280
// from the current live bucket list.
277281
auto tempState = std::make_shared<ImmutableLedgerData>(
278-
liveBL, hotArchiveBL, lcl, has, /*sorobanConfig*/ std::nullopt);
282+
liveBL, hotArchiveBL, lcl, has, /*sorobanConfig*/ std::nullopt,
283+
metrics);
279284
ImmutableLedgerView tempView(tempState, metrics);
280285
sorobanConfig = SorobanNetworkConfig::loadFromLedger(tempView);
281286
}
282-
return std::make_shared<ImmutableLedgerData>(liveBL, hotArchiveBL, lcl, has,
283-
std::move(sorobanConfig));
287+
return std::make_shared<ImmutableLedgerData>(
288+
liveBL, hotArchiveBL, lcl, has, std::move(sorobanConfig), metrics);
284289
}
285290

286291
ImmutableLedgerView::ImmutableLedgerView(ImmutableLedgerDataPtr state,
287292
MetricsRegistry& metrics)
288293
: mState(state)
289-
, mLiveSnapshot(metrics, state->mLiveBucketData)
290-
, mHotArchiveSnapshot(metrics, state->mHotArchiveBucketData)
294+
, mLiveSnapshot(metrics, state->mLiveSnapshotMetrics,
295+
state->mLiveBucketData)
296+
, mHotArchiveSnapshot(metrics, state->mHotArchiveSnapshotMetrics,
297+
state->mHotArchiveBucketData)
291298
, mMetrics(metrics)
292299
{
293300
}

0 commit comments

Comments
 (0)