Skip to content

Commit 0c8f7dc

Browse files
Add metrics for tracking tx e2e latency (#5356)
Adds metrics for tracking e2e latency in testing. Same work as #5330.
2 parents f8b9c6e + 06cefdf commit 0c8f7dc

7 files changed

Lines changed: 231 additions & 0 deletions

File tree

src/herder/HerderImpl.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,12 @@ HerderImpl::recvTransaction(TransactionFrameBasePtr tx, bool submittedFromSelf,
644644
ZoneScoped;
645645
TransactionQueue::AddResult result(
646646
TransactionQueue::AddResultCode::ADD_STATUS_COUNT);
647+
#ifdef BUILD_TESTS
648+
if (submittedFromSelf)
649+
{
650+
mLedgerManager.recordTxSubmission(tx->getContentsHash());
651+
}
652+
#endif
647653

648654
// Allow txs of the same kind to reach the tx queue in case it can be
649655
// replaced by fee

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: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,10 @@
7575
#include <Tracy.hpp>
7676

7777
#include "LedgerManagerImpl.h"
78+
#include <algorithm>
7879
#include <chrono>
7980
#include <future>
81+
#include <limits>
8082
#include <memory>
8183
#include <optional>
8284
#include <regex>
@@ -228,6 +230,32 @@ LedgerManagerImpl::LedgerApplyMetrics::LedgerApplyMetrics(
228230
{
229231
}
230232

233+
#ifdef BUILD_TESTS
234+
// For simulations of e2e latency, we use these metrics instead of the V1
235+
// self-delay metric for a few reasons. First, the self-delay metric doesn't
236+
// exist on the overlay-v2 branch. Second, it is hard to unify what the
237+
// self-delay window is measuring between v1 and v2 (since it measures time from
238+
// mempool addition to removal). Third, the self-delay metric is a slightly
239+
// different window from the e2e number we want to report (we want the time from
240+
// tx sub to meta emission, but the existing metric, for example, includes an
241+
// extra delay for a postOnMainThread when background apply is enabled). Fourth,
242+
// as of writing, the metric is a SimpleTimer, so it is lacking most of the
243+
// percentile data we want, but even if we switch it to be a full Timer, the
244+
// 30-second medida windowing makes interpretation more difficult.
245+
LedgerManagerImpl::TxLatencyMetrics::TxLatencyMetrics(MetricsRegistry& registry)
246+
: mTxsSubmitted(registry.NewCounter({"loadgen", "tx-latency", "submitted"}))
247+
, mTxsExternalized(
248+
registry.NewCounter({"loadgen", "tx-latency", "externalized"}))
249+
, mRunMin(registry.NewCounter({"loadgen", "tx-latency-run", "min-ms"}))
250+
, mRunMax(registry.NewCounter({"loadgen", "tx-latency-run", "max-ms"}))
251+
, mRunMean(registry.NewCounter({"loadgen", "tx-latency-run", "mean-ms"}))
252+
, mRunP50(registry.NewCounter({"loadgen", "tx-latency-run", "p50-ms"}))
253+
, mRunP75(registry.NewCounter({"loadgen", "tx-latency-run", "p75-ms"}))
254+
, mRunP99(registry.NewCounter({"loadgen", "tx-latency-run", "p99-ms"}))
255+
{
256+
}
257+
#endif
258+
231259
LedgerManagerImpl::ApplyState::ApplyState(Application& app)
232260
: mMetrics(app.getMetrics())
233261
, mAppConnector(app.getAppConnector())
@@ -351,6 +379,9 @@ LedgerManagerImpl::LedgerManagerImpl(Application& app)
351379
, mCatchupDuration(
352380
app.getMetrics().NewTimer({"ledger", "catchup", "duration"}))
353381
, mState(LM_BOOTING_STATE)
382+
#ifdef BUILD_TESTS
383+
, mTxLatencyMetrics(app.getMetrics())
384+
#endif
354385
{
355386
// At this point, we haven't called assumeState yet, so the BucketLists are
356387
// empty. We will create an "empty" snapshot that is not null, but
@@ -1307,6 +1338,143 @@ LedgerManagerImpl::emitNextMeta()
13071338
mNextMetaToEmit.reset();
13081339
}
13091340

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

src/ledger/LedgerManagerImpl.h

Lines changed: 32 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>
@@ -437,6 +438,34 @@ class LedgerManagerImpl : public LedgerManager
437438
std::optional<LedgerCloseMetaFrame> mLastLedgerCloseMeta;
438439
// Local prng for OP_APPLY_SLEEP_TIME_*_FOR_TESTING.
439440
stellar_default_random_engine mApplySleepRng;
441+
442+
// Metrics for measuring tx e2e latency. Active only when
443+
// Config::LOADGEN_MEASURE_TX_E2E_LATENCY_FOR_TESTING is set.
444+
struct TxLatencyMetrics
445+
{
446+
// Lifetime totals (cumulative; not reset between runs).
447+
medida::Counter& mTxsSubmitted;
448+
medida::Counter& mTxsExternalized;
449+
// Per-run "loadgen.tx-latency-run.*" statistics (ms), reset by
450+
// beginTxLatencyMeasurement.
451+
medida::Counter& mRunMin;
452+
medida::Counter& mRunMax;
453+
medida::Counter& mRunMean;
454+
medida::Counter& mRunP50;
455+
medida::Counter& mRunP75;
456+
medida::Counter& mRunP99;
457+
ANNOTATED_MUTEX(mMutex);
458+
UnorderedMap<Hash, VirtualClock::time_point>
459+
mTxSubmitTimes GUARDED_BY(mMutex);
460+
// Each recorded submission -> meta-emission latency in ms
461+
std::vector<uint32_t> mSamples GUARDED_BY(mMutex);
462+
463+
TxLatencyMetrics(MetricsRegistry& registry);
464+
} mTxLatencyMetrics;
465+
466+
// End point of the tx-latency metric: matches the ledger-close meta's
467+
// txProcessing entries against mTxSubmitTimes and records each latency.
468+
void recordTxMetaEmissionLatency(LedgerCloseMeta const& lcm);
440469
#endif
441470

442471
void setState(State s);
@@ -537,6 +566,9 @@ class LedgerManagerImpl : public LedgerManager
537566
getModuleCacheForTesting() override;
538567
void rebuildInMemorySorobanStateForTesting(uint32_t ledgerVersion) override;
539568
uint64_t getSorobanInMemoryStateSizeForTesting() override;
569+
void recordTxSubmission(Hash const& contentsHash) override;
570+
void beginTxLatencyMeasurement(uint32_t expectedTxCount) override;
571+
void finalizeTxLatencyMeasurement() override;
540572
#endif
541573

542574
uint64_t secondsSinceLastLedgerClose() const override;

src/main/Config.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ static std::unordered_set<std::string> const TESTING_ONLY_OPTIONS = {
6464
"LOADGEN_TX_SIZE_BYTES_DISTRIBUTION_FOR_TESTING",
6565
"LOADGEN_INSTRUCTIONS_FOR_TESTING",
6666
"LOADGEN_INSTRUCTIONS_DISTRIBUTION_FOR_TESTING",
67+
"LOADGEN_MEASURE_TX_E2E_LATENCY_FOR_TESTING",
6768
"CATCHUP_WAIT_MERGES_TX_APPLY_FOR_TESTING",
6869
"ARTIFICIALLY_SET_SURVEY_PHASE_DURATION_FOR_TESTING",
6970
"ARTIFICIALLY_DELAY_BUCKET_APPLICATION_FOR_TESTING",
@@ -140,6 +141,7 @@ Config::Config() : NODE_SEED(SecretKey::random())
140141
LOADGEN_TX_SIZE_BYTES_DISTRIBUTION_FOR_TESTING = {};
141142
LOADGEN_INSTRUCTIONS_FOR_TESTING = {};
142143
LOADGEN_INSTRUCTIONS_DISTRIBUTION_FOR_TESTING = {};
144+
LOADGEN_MEASURE_TX_E2E_LATENCY_FOR_TESTING = false;
143145
CATCHUP_WAIT_MERGES_TX_APPLY_FOR_TESTING = false;
144146
ARTIFICIALLY_SET_SURVEY_PHASE_DURATION_FOR_TESTING =
145147
std::chrono::minutes::zero();
@@ -1693,6 +1695,11 @@ Config::processConfig(std::shared_ptr<cpptoml::table> t)
16931695
LOADGEN_INSTRUCTIONS_DISTRIBUTION_FOR_TESTING =
16941696
readIntArray<uint32_t>(item);
16951697
}},
1698+
{"LOADGEN_MEASURE_TX_E2E_LATENCY_FOR_TESTING",
1699+
[&]() {
1700+
LOADGEN_MEASURE_TX_E2E_LATENCY_FOR_TESTING =
1701+
readBool(item);
1702+
}},
16961703
#ifdef BUILD_TESTS
16971704
{"OP_APPLY_SLEEP_TIME_DURATION_FOR_TESTING",
16981705
[&]() {

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_E2E_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
@@ -470,6 +470,7 @@ LoadGenerator::start(GeneratedLoadConfig& cfg)
470470
0));
471471
}
472472
}
473+
mApp.getLedgerManager().beginTxLatencyMeasurement(cfg.nTxs);
473474
mStarted = true;
474475
}
475476

@@ -1381,6 +1382,7 @@ LoadGenerator::waitTillComplete(GeneratedLoadConfig cfg)
13811382
if (checkMinimumSorobanSuccess(cfg))
13821383
{
13831384
CLOG_INFO(LoadGen, "Load generation complete.");
1385+
mApp.getLedgerManager().finalizeTxLatencyMeasurement();
13841386
mLoadgenComplete.Mark();
13851387
reset();
13861388
}
@@ -1457,6 +1459,7 @@ LoadGenerator::waitTillCompleteWithoutChecks()
14571459
"for high traffic due to tx queue limiter evictions.",
14581460
inconsistencies.size());
14591461
}
1462+
mApp.getLedgerManager().finalizeTxLatencyMeasurement();
14601463
mLoadgenComplete.Mark();
14611464
reset();
14621465
return;

0 commit comments

Comments
 (0)