Skip to content

Commit ba7d330

Browse files
committed
Add metrics for tracking tx e2e latency
1 parent 78e4ccd commit ba7d330

7 files changed

Lines changed: 220 additions & 0 deletions

File tree

src/herder/HerderImpl.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,12 @@ HerderImpl::recvTransaction(TransactionFrameBasePtr tx, bool submittedFromSelf,
651651
CLOG_TRACE(Herder, "recv transaction {} for {}",
652652
hexAbbrev(tx->getFullHash()),
653653
KeyUtils::toShortString(tx->getSourceID()));
654+
#ifdef BUILD_TESTS
655+
if (submittedFromSelf)
656+
{
657+
mLedgerManager.recordTxSubmission(tx->getContentsHash());
658+
}
659+
#endif
654660

655661
auto const& env = tx->getEnvelope();
656662
mApp.getOverlayManager().broadcastTransaction(env, tx->getFullFee(),

src/ledger/LedgerManager.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,16 @@ class LedgerManager
299299
virtual ::rust::Box<rust_bridge::SorobanModuleCache>
300300
getModuleCacheForTesting() = 0;
301301
virtual uint64_t getSorobanInMemoryStateSizeForTesting() = 0;
302+
303+
// Records the submission time of a self-submitted transaction for the
304+
// tx-latency metrics. No-op unless
305+
// Config::LOADGEN_MEASURE_TX_LATENCY_FOR_TESTING is set.
306+
virtual void recordTxSubmission(Hash const& contentsHash) = 0;
307+
308+
// Begins/ends a load-generation latency measurement window. No-op unless
309+
// Config::LOADGEN_MEASURE_TX_LATENCY_FOR_TESTING is set.
310+
virtual void beginTxLatencyMeasurement(uint32_t expectedTxCount) = 0;
311+
virtual void finalizeTxLatencyMeasurement() = 0;
302312
#endif
303313

304314
// Return the (changing) number of seconds since the LCL closed.

src/ledger/LedgerManagerImpl.cpp

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@
7474
#include <Tracy.hpp>
7575

7676
#include "LedgerManagerImpl.h"
77+
#include <algorithm>
7778
#include <chrono>
79+
#include <limits>
7880
#include <memory>
7981
#include <optional>
8082
#include <regex>
@@ -226,6 +228,22 @@ LedgerManagerImpl::LedgerApplyMetrics::LedgerApplyMetrics(
226228
{
227229
}
228230

231+
#ifdef BUILD_TESTS
232+
LedgerManagerImpl::TxLatencyMetrics::TxLatencyMetrics(MetricsRegistry& registry)
233+
: mTxsSubmitted(registry.NewCounter({"loadgen", "tx-latency", "submitted"}))
234+
, mTxsExternalized(
235+
registry.NewCounter({"loadgen", "tx-latency", "externalized"}))
236+
, mLatencyTimer(registry.NewTimer({"loadgen", "tx-latency", "duration"}))
237+
, mRunMin(registry.NewCounter({"loadgen", "tx-latency-run", "min-ms"}))
238+
, mRunMax(registry.NewCounter({"loadgen", "tx-latency-run", "max-ms"}))
239+
, mRunMean(registry.NewCounter({"loadgen", "tx-latency-run", "mean-ms"}))
240+
, mRunP50(registry.NewCounter({"loadgen", "tx-latency-run", "p50-ms"}))
241+
, mRunP75(registry.NewCounter({"loadgen", "tx-latency-run", "p75-ms"}))
242+
, mRunP99(registry.NewCounter({"loadgen", "tx-latency-run", "p99-ms"}))
243+
{
244+
}
245+
#endif
246+
229247
LedgerManagerImpl::ApplyState::ApplyState(Application& app)
230248
: mMetrics(app.getMetrics())
231249
, mAppConnector(app.getAppConnector())
@@ -349,6 +367,9 @@ LedgerManagerImpl::LedgerManagerImpl(Application& app)
349367
, mCatchupDuration(
350368
app.getMetrics().NewTimer({"ledger", "catchup", "duration"}))
351369
, mState(LM_BOOTING_STATE)
370+
#ifdef BUILD_TESTS
371+
, mTxLatencyMetrics(app.getMetrics())
372+
#endif
352373
{
353374
// At this point, we haven't called assumeState yet, so the BucketLists are
354375
// empty. We will create an "empty" snapshot that is not null, but
@@ -1308,6 +1329,142 @@ LedgerManagerImpl::emitNextMeta()
13081329
mNextMetaToEmit.reset();
13091330
}
13101331

1332+
#ifdef BUILD_TESTS
1333+
void
1334+
LedgerManagerImpl::recordTxSubmission(Hash const& contentsHash)
1335+
{
1336+
if (!mApp.getConfig().LOADGEN_MEASURE_TX_LATENCY_FOR_TESTING)
1337+
{
1338+
return;
1339+
}
1340+
MutexLocker guard(mTxLatencyMetrics.mMutex);
1341+
if (mTxLatencyMetrics.mTxSubmitTimes
1342+
.try_emplace(contentsHash, mApp.getClock().now())
1343+
.second)
1344+
{
1345+
mTxLatencyMetrics.mTxsSubmitted.inc();
1346+
}
1347+
else
1348+
{
1349+
CLOG_WARNING(Ledger, "Duplicate tx submission recorded for hash {}.",
1350+
binToHex(contentsHash));
1351+
}
1352+
}
1353+
1354+
void
1355+
LedgerManagerImpl::recordTxMetaEmissionLatency(LedgerCloseMeta const& lcm)
1356+
{
1357+
if (!mApp.getConfig().LOADGEN_MEASURE_TX_LATENCY_FOR_TESTING)
1358+
{
1359+
return;
1360+
}
1361+
VirtualClock::time_point const emitTime = mApp.getClock().now();
1362+
MutexLocker guard(mTxLatencyMetrics.mMutex);
1363+
auto probe = [&](auto const& txProcessing) {
1364+
for (auto const& txp : txProcessing)
1365+
{
1366+
if (auto submitted = mTxLatencyMetrics.mTxSubmitTimes.find(
1367+
txp.result.transactionHash);
1368+
submitted != mTxLatencyMetrics.mTxSubmitTimes.end())
1369+
{
1370+
auto const latency = emitTime - submitted->second;
1371+
mTxLatencyMetrics.mTxsExternalized.inc();
1372+
mTxLatencyMetrics.mLatencyTimer.Update(latency);
1373+
int64_t const ms =
1374+
std::chrono::duration_cast<std::chrono::milliseconds>(
1375+
latency)
1376+
.count();
1377+
mTxLatencyMetrics.mSamples.push_back(std::clamp<int64_t>(
1378+
ms, 0, std::numeric_limits<uint32_t>::max()));
1379+
mTxLatencyMetrics.mTxSubmitTimes.erase(submitted);
1380+
}
1381+
}
1382+
};
1383+
switch (lcm.v())
1384+
{
1385+
case 0:
1386+
probe(lcm.v0().txProcessing);
1387+
break;
1388+
case 1:
1389+
probe(lcm.v1().txProcessing);
1390+
break;
1391+
case 2:
1392+
probe(lcm.v2().txProcessing);
1393+
break;
1394+
default:
1395+
releaseAssert(false);
1396+
}
1397+
}
1398+
1399+
void
1400+
LedgerManagerImpl::beginTxLatencyMeasurement(uint32_t expectedTxCount)
1401+
{
1402+
if (!mApp.getConfig().LOADGEN_MEASURE_TX_LATENCY_FOR_TESTING)
1403+
{
1404+
return;
1405+
}
1406+
MutexLocker guard(mTxLatencyMetrics.mMutex);
1407+
if (!mTxLatencyMetrics.mTxSubmitTimes.empty())
1408+
{
1409+
CLOG_WARNING(Ledger,
1410+
"Starting tx latency measurement with {} unmatched "
1411+
"submissions from prior run",
1412+
mTxLatencyMetrics.mTxSubmitTimes.size());
1413+
mTxLatencyMetrics.mTxSubmitTimes.clear();
1414+
}
1415+
mTxLatencyMetrics.mSamples.clear();
1416+
mTxLatencyMetrics.mSamples.reserve(expectedTxCount);
1417+
1418+
// Clear the per-run latency metrics.
1419+
mTxLatencyMetrics.mRunMin.clear();
1420+
mTxLatencyMetrics.mRunMax.clear();
1421+
mTxLatencyMetrics.mRunMean.clear();
1422+
mTxLatencyMetrics.mRunP50.clear();
1423+
mTxLatencyMetrics.mRunP75.clear();
1424+
mTxLatencyMetrics.mRunP99.clear();
1425+
}
1426+
1427+
void
1428+
LedgerManagerImpl::finalizeTxLatencyMeasurement()
1429+
{
1430+
if (!mApp.getConfig().LOADGEN_MEASURE_TX_LATENCY_FOR_TESTING)
1431+
{
1432+
return;
1433+
}
1434+
MutexLocker guard(mTxLatencyMetrics.mMutex);
1435+
auto& samples = mTxLatencyMetrics.mSamples;
1436+
if (samples.empty())
1437+
{
1438+
CLOG_WARNING(Ledger,
1439+
"Finalizing tx latency measurement with no samples");
1440+
return;
1441+
}
1442+
std::sort(samples.begin(), samples.end());
1443+
size_t const n = samples.size();
1444+
1445+
auto percentile = [&](size_t p) -> uint32_t {
1446+
// Calculate ceiling of rank
1447+
size_t rank = (p * n + 99) / 100;
1448+
rank = std::clamp<size_t>(rank, 1, n);
1449+
return samples[rank - 1];
1450+
};
1451+
1452+
uint64_t sum = 0;
1453+
for (uint32_t s : samples)
1454+
{
1455+
sum += s;
1456+
}
1457+
1458+
mTxLatencyMetrics.mRunMin.set_count(samples.front());
1459+
mTxLatencyMetrics.mRunMax.set_count(samples.back());
1460+
// Mean rounded to the nearest millisecond.
1461+
mTxLatencyMetrics.mRunMean.set_count((sum + n / 2) / n);
1462+
mTxLatencyMetrics.mRunP50.set_count(percentile(50));
1463+
mTxLatencyMetrics.mRunP75.set_count(percentile(75));
1464+
mTxLatencyMetrics.mRunP99.set_count(percentile(99));
1465+
}
1466+
#endif
1467+
13111468
namespace
13121469
{
13131470
#ifdef BUILD_TESTS
@@ -1773,6 +1930,7 @@ LedgerManagerImpl::applyLedger(LedgerCloseData const& ledgerData,
17731930
appliedLedgerState->getLastClosedLedgerHeader();
17741931
// Copy this before we move it into mNextMetaToEmit below
17751932
mLastLedgerCloseMeta = *ledgerCloseMeta;
1933+
recordTxMetaEmissionLatency(ledgerCloseMeta->getXDR());
17761934
}
17771935
#endif
17781936

src/ledger/LedgerManagerImpl.h

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "transactions/ParallelApplyUtils.h"
1919
#include "transactions/TransactionFrame.h"
2020
#include "util/Math.h"
21+
#include "util/UnorderedMap.h"
2122
#include "util/XDRStream.h"
2223
#include "xdr/Stellar-ledger.h"
2324
#include <atomic>
@@ -431,6 +432,35 @@ class LedgerManagerImpl : public LedgerManager
431432
std::optional<LedgerCloseMetaFrame> mLastLedgerCloseMeta;
432433
// Local prng for OP_APPLY_SLEEP_TIME_*_FOR_TESTING.
433434
stellar_default_random_engine mApplySleepRng;
435+
436+
// Metrics for measuring tx e2e latency. Active only when
437+
// Config::LOADGEN_MEASURE_TX_LATENCY_FOR_TESTING is set.
438+
struct TxLatencyMetrics
439+
{
440+
// Lifetime totals (cumulative; not reset between runs).
441+
medida::Counter& mTxsSubmitted;
442+
medida::Counter& mTxsExternalized;
443+
medida::Timer& mLatencyTimer;
444+
// Per-run "loadgen.tx-latency-run.*" statistics (ms), reset by
445+
// beginTxLatencyMeasurement.
446+
medida::Counter& mRunMin;
447+
medida::Counter& mRunMax;
448+
medida::Counter& mRunMean;
449+
medida::Counter& mRunP50;
450+
medida::Counter& mRunP75;
451+
medida::Counter& mRunP99;
452+
ANNOTATED_MUTEX(mMutex);
453+
UnorderedMap<Hash, VirtualClock::time_point>
454+
mTxSubmitTimes GUARDED_BY(mMutex);
455+
// Each recorded submission -> meta-emission latency in ms
456+
std::vector<uint32_t> mSamples GUARDED_BY(mMutex);
457+
458+
TxLatencyMetrics(MetricsRegistry& registry);
459+
} mTxLatencyMetrics;
460+
461+
// End point of the tx-latency metric: matches the ledger-close meta's
462+
// txProcessing entries against mTxSubmitTimes and records each latency.
463+
void recordTxMetaEmissionLatency(LedgerCloseMeta const& lcm);
434464
#endif
435465

436466
void setState(State s);
@@ -533,6 +563,9 @@ class LedgerManagerImpl : public LedgerManager
533563
getModuleCacheForTesting() override;
534564
void rebuildInMemorySorobanStateForTesting(uint32_t ledgerVersion) override;
535565
uint64_t getSorobanInMemoryStateSizeForTesting() override;
566+
void recordTxSubmission(Hash const& contentsHash) override;
567+
void beginTxLatencyMeasurement(uint32_t expectedTxCount) override;
568+
void finalizeTxLatencyMeasurement() override;
536569
#endif
537570

538571
uint64_t secondsSinceLastLedgerClose() const override;

src/main/Config.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ static std::unordered_set<std::string> const TESTING_ONLY_OPTIONS = {
6565
"LOADGEN_TX_SIZE_BYTES_DISTRIBUTION_FOR_TESTING",
6666
"LOADGEN_INSTRUCTIONS_FOR_TESTING",
6767
"LOADGEN_INSTRUCTIONS_DISTRIBUTION_FOR_TESTING",
68+
"LOADGEN_MEASURE_TX_LATENCY_FOR_TESTING",
6869
"CATCHUP_WAIT_MERGES_TX_APPLY_FOR_TESTING",
6970
"ARTIFICIALLY_SET_SURVEY_PHASE_DURATION_FOR_TESTING",
7071
"ARTIFICIALLY_DELAY_BUCKET_APPLICATION_FOR_TESTING",
@@ -141,6 +142,7 @@ Config::Config() : NODE_SEED(SecretKey::random())
141142
LOADGEN_TX_SIZE_BYTES_DISTRIBUTION_FOR_TESTING = {};
142143
LOADGEN_INSTRUCTIONS_FOR_TESTING = {};
143144
LOADGEN_INSTRUCTIONS_DISTRIBUTION_FOR_TESTING = {};
145+
LOADGEN_MEASURE_TX_LATENCY_FOR_TESTING = false;
144146
CATCHUP_WAIT_MERGES_TX_APPLY_FOR_TESTING = false;
145147
ARTIFICIALLY_SET_SURVEY_PHASE_DURATION_FOR_TESTING =
146148
std::chrono::minutes::zero();
@@ -1676,6 +1678,10 @@ Config::processConfig(std::shared_ptr<cpptoml::table> t)
16761678
LOADGEN_INSTRUCTIONS_DISTRIBUTION_FOR_TESTING =
16771679
readIntArray<uint32_t>(item);
16781680
}},
1681+
{"LOADGEN_MEASURE_TX_LATENCY_FOR_TESTING",
1682+
[&]() {
1683+
LOADGEN_MEASURE_TX_LATENCY_FOR_TESTING = readBool(item);
1684+
}},
16791685
#ifdef BUILD_TESTS
16801686
{"OP_APPLY_SLEEP_TIME_DURATION_FOR_TESTING",
16811687
[&]() {

src/main/Config.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,10 @@ class Config : public std::enable_shared_from_this<Config>
330330
std::vector<uint32_t> LOADGEN_INSTRUCTIONS_FOR_TESTING;
331331
std::vector<uint32_t> LOADGEN_INSTRUCTIONS_DISTRIBUTION_FOR_TESTING;
332332

333+
// Defaults to false. When set, measure the latency of self-submitted
334+
// transactions from submission to meta generation.
335+
bool LOADGEN_MEASURE_TX_LATENCY_FOR_TESTING;
336+
333337
#ifdef BUILD_TESTS
334338
// Config parameters that force transaction application during ledger
335339
// close to sleep for a certain amount of time.

src/simulation/LoadGenerator.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ LoadGenerator::start(GeneratedLoadConfig& cfg)
478478
0));
479479
}
480480
}
481+
mApp.getLedgerManager().beginTxLatencyMeasurement(cfg.nTxs);
481482
mStarted = true;
482483
}
483484

@@ -1342,6 +1343,7 @@ LoadGenerator::waitTillComplete(GeneratedLoadConfig cfg)
13421343
if (checkMinimumSorobanSuccess(cfg))
13431344
{
13441345
CLOG_INFO(LoadGen, "Load generation complete.");
1346+
mApp.getLedgerManager().finalizeTxLatencyMeasurement();
13451347
mLoadgenComplete.Mark();
13461348
reset();
13471349
}
@@ -1423,6 +1425,7 @@ LoadGenerator::waitTillCompleteWithoutChecks()
14231425
"for high traffic due to tx queue limiter evictions.",
14241426
inconsistencies.size());
14251427
}
1428+
mApp.getLedgerManager().finalizeTxLatencyMeasurement();
14261429
mLoadgenComplete.Mark();
14271430
reset();
14281431
return;

0 commit comments

Comments
 (0)