From d26b1a3b0bb5fe2a3c762ed07ee520fbe36b569b Mon Sep 17 00:00:00 2001 From: Garand Tyson Date: Tue, 8 Sep 2026 11:39:37 -0700 Subject: [PATCH] Add txset construction mode to apply load --- docs/apply-load-benchmark-sac.cfg | 16 +- docs/apply-load-benchmark-token.cfg | 17 +- docs/apply-load-for-meta.cfg | 16 +- docs/apply-load-ledger-limits.cfg | 16 +- docs/metrics.md | 1 + docs/software/commands.md | 29 +- src/herder/HerderImpl.cpp | 9 + src/herder/HerderImpl.h | 3 + src/simulation/ApplyLoad.cpp | 540 +++++++++------------ src/simulation/ApplyLoad.h | 50 +- src/simulation/test/LoadGeneratorTests.cpp | 22 + 11 files changed, 368 insertions(+), 351 deletions(-) diff --git a/docs/apply-load-benchmark-sac.cfg b/docs/apply-load-benchmark-sac.cfg index 2adcfd9458..f859c44df7 100644 --- a/docs/apply-load-benchmark-sac.cfg +++ b/docs/apply-load-benchmark-sac.cfg @@ -8,10 +8,22 @@ APPLY_LOAD_MODE="benchmark" APPLY_LOAD_MODEL_TX="sac" # Which timing path to use: "apply" times only transaction application, while -# "txset-validation-and-apply" also simulates a non-leader receiving and -# validating a tx set before applying it. Tx-set creation is not measured. +# "txset-validation-and-apply" also times tx-set construction from an +# overfilled candidate backlog and validation before application. APPLY_LOAD_TIMING_PHASES = "apply" +# Purposefully overfill the candidate backlog to simulate a busy production +# validator's queue. In "txset-validation-and-apply", these multipliers default +# to 2 and must be at least 2 (the classic multiplier is checked only when +# APPLY_LOAD_CLASSIC_TXS_PER_LEDGER > 0). Raise them, e.g. to 4, to increase +# the backlog without raising ledger limits. More candidates need more +# accounts; increase GENESIS_TEST_ACCOUNT_COUNT if needed. +# These flags scale candidate counts. The Soroban multiplier applies after +# SAC batching, i.e. to +# APPLY_LOAD_MAX_SOROBAN_TX_COUNT / APPLY_LOAD_BATCH_SAC_COUNT. +SOROBAN_TRANSACTION_QUEUE_SIZE_MULTIPLIER_FOR_TESTING = 2 +TRANSACTION_QUEUE_SIZE_MULTIPLIER_FOR_TESTING = 2 + # Whether to time the write part of the apply stage. This can be # disabled to get less noisy results for non-write related changes, # but should be enabled to get more comprehensive e2e numbers. diff --git a/docs/apply-load-benchmark-token.cfg b/docs/apply-load-benchmark-token.cfg index ae02b67a4e..68089920eb 100644 --- a/docs/apply-load-benchmark-token.cfg +++ b/docs/apply-load-benchmark-token.cfg @@ -8,10 +8,21 @@ APPLY_LOAD_MODE="benchmark" APPLY_LOAD_MODEL_TX="custom_token" # Which timing path to use: "apply" times only transaction application, while -# "txset-validation-and-apply" also simulates a non-leader receiving and -# validating a tx set before applying it. Tx-set creation is not measured. +# "txset-validation-and-apply" also times tx-set construction from an +# overfilled candidate backlog and validation before application. APPLY_LOAD_TIMING_PHASES = "apply" +# Purposefully overfill the candidate backlog to simulate a busy production +# validator's queue. In "txset-validation-and-apply", these multipliers default +# to 2 and must be at least 2 (the classic multiplier is checked only when +# APPLY_LOAD_CLASSIC_TXS_PER_LEDGER > 0). Raise them, e.g. to 4, to increase +# the backlog without raising ledger limits. More candidates need more +# accounts; increase GENESIS_TEST_ACCOUNT_COUNT if needed. +# These flags scale candidate counts. Token transfers need two accounts +# per candidate. +SOROBAN_TRANSACTION_QUEUE_SIZE_MULTIPLIER_FOR_TESTING = 2 +TRANSACTION_QUEUE_SIZE_MULTIPLIER_FOR_TESTING = 2 + # Whether to time the write part of the apply stage. This can be # disabled to get less noisy results for non-write related changes, # but should be enabled to get more comprehensive e2e numbers. @@ -61,4 +72,4 @@ NODE_SEED="SDQVDISRYN2JXBS7ICL7QJAEKB3HWBJFP2QECXG7GZICAHBK4UNJCWK2 self" [QUORUM_SET] THRESHOLD_PERCENT=100 -VALIDATORS=["$self"] \ No newline at end of file +VALIDATORS=["$self"] diff --git a/docs/apply-load-for-meta.cfg b/docs/apply-load-for-meta.cfg index 36c268d7df..21f45e8855 100644 --- a/docs/apply-load-for-meta.cfg +++ b/docs/apply-load-for-meta.cfg @@ -10,10 +10,22 @@ APPLY_LOAD_MODE="ledger-limits" # Which timing path to use: "apply" times only transaction application, while -# "txset-validation-and-apply" also simulates a non-leader receiving and -# validating a tx set before applying it. Tx-set creation is not measured. +# "txset-validation-and-apply" also times tx-set construction from an +# overfilled candidate backlog and validation before application. APPLY_LOAD_TIMING_PHASES = "apply" +# Purposefully overfill the candidate backlog to simulate a busy production +# validator's queue. In "txset-validation-and-apply", these multipliers default +# to 2 and must be at least 2 (the classic multiplier is checked only when +# APPLY_LOAD_CLASSIC_TXS_PER_LEDGER > 0). Raise them, e.g. to 4, to increase +# the backlog without raising ledger limits. More candidates need more +# accounts; increase GENESIS_TEST_ACCOUNT_COUNT if needed. +# The classic multiplier scales the candidate count in the tx-set timing path. +# The Soroban multiplier scales the candidate resource budget in both timing +# paths, so 2 means a budget of 2x ledger capacity. +SOROBAN_TRANSACTION_QUEUE_SIZE_MULTIPLIER_FOR_TESTING = 2 +TRANSACTION_QUEUE_SIZE_MULTIPLIER_FOR_TESTING = 2 + # Custom meta path - if not set it will be written to a temp directory and # cleaned up after running the benchmark METADATA_OUTPUT_STREAM='meta.xdr' diff --git a/docs/apply-load-ledger-limits.cfg b/docs/apply-load-ledger-limits.cfg index 3dae22fb87..09c96f0dd8 100644 --- a/docs/apply-load-ledger-limits.cfg +++ b/docs/apply-load-ledger-limits.cfg @@ -8,10 +8,22 @@ APPLY_LOAD_MODE="ledger-limits" # Which timing path to use: "apply" times only transaction application, while -# "txset-validation-and-apply" also simulates a non-leader receiving and -# validating a tx set before applying it. Tx-set creation is not measured. +# "txset-validation-and-apply" also times tx-set construction from an +# overfilled candidate backlog and validation before application. APPLY_LOAD_TIMING_PHASES = "apply" +# Purposefully overfill the candidate backlog to simulate a busy production +# validator's queue. In "txset-validation-and-apply", these multipliers default +# to 2 and must be at least 2 (the classic multiplier is checked only when +# APPLY_LOAD_CLASSIC_TXS_PER_LEDGER > 0). Raise them, e.g. to 4, to increase +# the backlog without raising ledger limits. More candidates need more +# accounts; increase GENESIS_TEST_ACCOUNT_COUNT if needed. +# The classic multiplier scales the candidate count in the tx-set timing path. +# The Soroban multiplier scales the candidate resource budget in both timing +# paths, so 2 means a budget of 2x ledger capacity. +SOROBAN_TRANSACTION_QUEUE_SIZE_MULTIPLIER_FOR_TESTING = 2 +TRANSACTION_QUEUE_SIZE_MULTIPLIER_FOR_TESTING = 2 + # Medida metrics (histograms in particular) in apply path cause severe and # non-deterministic performance degradation. While this has to be addressed # eventually, it is useful to disable these when optimizing anything besides diff --git a/docs/metrics.md b/docs/metrics.md index 09b92f5bb5..d2ef15b05d 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -62,6 +62,7 @@ crypto.verify.miss | meter | number of signature cach crypto.verify.total | meter | sum of both hits and misses crypto.verify.tx-valid-hit | meter | signature cache hits that occurred while validating transactions (outside of background signature validation) crypto.verify.tx-valid-total | meter | sum of both hits and misses during transaction validation (outside of background signature validation) +herder.txset.build | timer | time spent building the tx set proposed at nomination from the transaction queues herder.txset.validate | timer | time spent turning a received tx set into an applicable tx set and validating it on a validity-cache miss herder.pending[-soroban]-txs.age0 | counter | number of gen0 pending transactions herder.pending[-soroban]-txs.age1 | counter | number of gen1 pending transactions diff --git a/docs/software/commands.md b/docs/software/commands.md index 8edf80399b..9d6eeee106 100644 --- a/docs/software/commands.md +++ b/docs/software/commands.md @@ -23,8 +23,8 @@ Command options can only by placed after command. synthetic transactions. By default the benchmark is isolated to mostly just executing the transactions and thus it omits a lot of the supporting mechanisms (such as overlay, SCP, mempool etc). It may also measure tx-set - validation and consensus processing via `APPLY_LOAD_TIMING_PHASES` (see - below). This command will generate enough + construction, validation and consensus processing via + `APPLY_LOAD_TIMING_PHASES` (see below). This command will generate enough transactions to fill up a synthetic transaction queue (it's just a list of transactions with the same limits as the real queue), and then create a transaction set off of that to apply. This can also be used to record the @@ -42,15 +42,22 @@ Command options can only by placed after command. - `"apply"`: the default apply-only benchmark. Its close helper still calls `checkValid`, but that happens before the recorded ledger-close timer and leaves the caches warm, as consensus validation would on a live node. - - `"txset-validation-and-apply"`: simulates a non-leader receiving the tx - set over the wire, validating it through local consensus (with the node - as its own single-validator quorum), and then applying it. It reports - validation, ledger close, and end-to-end time in addition to the - apply-only output. It does not simulate network transport, peer fetching, - or multi-node timing. Leader-side tx-set creation and signing happen - before the measured span. The signature verification cache is cleared - before validation, then retained so apply sees the warm cache produced by - validation. + - `"txset-validation-and-apply"`: times tx-set construction, decoding, + validation and application through local consensus with a + single-validator quorum. Construction + uses a synthetic candidate backlog of at least 2x ledger capacity to + exercise trimming, surge pricing and parallel partitioning as a busy + validator would. The queue size multipliers + (`SOROBAN_TRANSACTION_QUEUE_SIZE_MULTIPLIER_FOR_TESTING` and + `TRANSACTION_QUEUE_SIZE_MULTIPLIER_FOR_TESTING`) control this backlog; + both default to 2 and must be at least 2 for enabled transaction types. + Raise them to increase the backlog without raising ledger limits. They + scale model and classic transaction counts; in `ledger-limits` mode the + Soroban multiplier scales the candidate resource budget in both timing + paths. Construction measures the builder itself, + the output reports construction, validation, and ledger close + timings. Network transport, peer fetching, tx queue submission, and + multi-node timing are not simulated. `"txset-validation-and-apply"` is not supported with `APPLY_LOAD_MODE="max-sac-tps"`; that search targets apply-only close time. * Load generation is configured in the Core config file. The relevant settings diff --git a/src/herder/HerderImpl.cpp b/src/herder/HerderImpl.cpp index e04b93bc11..7aaa002224 100644 --- a/src/herder/HerderImpl.cpp +++ b/src/herder/HerderImpl.cpp @@ -47,6 +47,7 @@ #include "util/GlobalChecks.h" #include +#include #include #include @@ -82,6 +83,7 @@ HerderImpl::SCPMetrics::SCPMetrics(Application& app) {"scp", "envelope", "invalidsig"}, "envelope")) , mTriggerPrepareStartFallback(app.getMetrics().NewMeter( {"scp", "trigger", "prepare-start-fallback"}, "trigger")) + , mTxSetBuild(app.getMetrics().NewTimer({"herder", "txset", "build"})) { } @@ -1664,6 +1666,7 @@ HerderImpl::triggerNextLedger(uint32_t ledgerSeqToTrigger, // Since we are not currently applying, it is safe to use read-only LCL, as // it's guaranteed to be up-to-date auto lcl = mLedgerManager.getLastClosedLedgerHeader(); + auto const txSetBuildStart = std::chrono::steady_clock::now(); PerPhaseTransactionList txPhases; txPhases.emplace_back(mTransactionQueue.getTransactions(lcl.header)); @@ -1752,6 +1755,12 @@ HerderImpl::triggerNextLedger(uint32_t ledgerSeqToTrigger, mTransactionQueue.ban( invalidTxPhases[static_cast(TxSetPhase::CLASSIC)]); + // Stop before addTxSet below: its side effects can include SCP callbacks + // and even externalizing a ledger, which are not tx set building. + mSCPMetrics.mTxSetBuild.Update( + std::chrono::duration_cast( + std::chrono::steady_clock::now() - txSetBuildStart)); + auto txSetHash = proposedSet->getContentsHash(); // Inform the item fetcher so queries from other peers about his txSet diff --git a/src/herder/HerderImpl.h b/src/herder/HerderImpl.h index 155726ea15..25681e9247 100644 --- a/src/herder/HerderImpl.h +++ b/src/herder/HerderImpl.h @@ -362,6 +362,9 @@ class HerderImpl : public Herder // network-close-time anchor to the local prepare-start anchor. medida::Meter& mTriggerPrepareStartFallback; + // Time spent building the tx set proposed at nomination. + medida::Timer& mTxSetBuild; + SCPMetrics(Application& app); }; diff --git a/src/simulation/ApplyLoad.cpp b/src/simulation/ApplyLoad.cpp index 2637b8a161..3dd3451372 100644 --- a/src/simulation/ApplyLoad.cpp +++ b/src/simulation/ApplyLoad.cpp @@ -79,11 +79,9 @@ interpolatePercentile(std::vector const& sortedValues, return sortedValues[lo] * (1.0 - weight) + sortedValues[hi] * weight; } -// Logs the distribution of per-ledger samples for one timing phase, expressed -// in `unit`. +// Logs the distribution of per-ledger timing samples in milliseconds. void -logPhaseStats(std::string const& label, std::vector const& samples, - std::string const& unit = "ms") +logPhaseStats(std::string const& label, std::vector const& samples) { releaseAssert(!samples.empty()); @@ -101,52 +99,18 @@ logPhaseStats(std::string const& label, std::vector const& samples, std::vector sortedSamples = samples; std::sort(sortedSamples.begin(), sortedSamples.end()); - CLOG_WARNING(Perf, "mean {}: {} {}", label, mean, unit); - CLOG_WARNING(Perf, "p25 {}: {} {}", label, - interpolatePercentile(sortedSamples, 25.0), unit); - CLOG_WARNING(Perf, "p50 {}: {} {}", label, - interpolatePercentile(sortedSamples, 50.0), unit); - CLOG_WARNING(Perf, "p75 {}: {} {}", label, - interpolatePercentile(sortedSamples, 75.0), unit); - CLOG_WARNING(Perf, "p95 {}: {} {}", label, - interpolatePercentile(sortedSamples, 95.0), unit); - CLOG_WARNING(Perf, "p99 {}: {} {}", label, - interpolatePercentile(sortedSamples, 99.0), unit); - CLOG_WARNING(Perf, "{} stddev: {} {}", label, std::sqrt(variance), unit); -} - -void -nominateAndClose(Application& app, TxSetXDRFrameConstPtr txSet, - StellarValue const& value) -{ - auto& herder = static_cast(app.getHerder()); - auto const& lcl = app.getLedgerManager().getLastClosedLedgerHeader(); - auto const ledgerSeq = lcl.header.ledgerSeq + 1; - herder.getPendingEnvelopes().putTxSet(txSet->getContentsHash(), ledgerSeq, - txSet); - herder.getHerderSCPDriver().nominate(ledgerSeq, value, txSet, - lcl.header.scpValue); - - auto const deadline = - std::chrono::steady_clock::now() + std::chrono::seconds(60); - size_t cranks = 0; - while (app.getLedgerManager().getLastClosedLedgerNum() < ledgerSeq && - std::chrono::steady_clock::now() < deadline) - { - app.getClock().crank(true); - ++cranks; - } - if (app.getLedgerManager().getLastClosedLedgerNum() < ledgerSeq) - { - throw std::runtime_error(fmt::format( - FMT_STRING("nominateAndClose: SCP did not externalize ledger {} " - "within 60s ({} cranks); close time {} is {}s from the " - "wall clock (slip limit {}s)"), - ledgerSeq, cranks, value.closeTime, - static_cast(value.closeTime) - - static_cast(app.timeNow()), - Herder::MAX_TIME_SLIP_SECONDS.count())); - } + CLOG_WARNING(Perf, "mean {}: {} ms", label, mean); + CLOG_WARNING(Perf, "p25 {}: {} ms", label, + interpolatePercentile(sortedSamples, 25.0)); + CLOG_WARNING(Perf, "p50 {}: {} ms", label, + interpolatePercentile(sortedSamples, 50.0)); + CLOG_WARNING(Perf, "p75 {}: {} ms", label, + interpolatePercentile(sortedSamples, 75.0)); + CLOG_WARNING(Perf, "p95 {}: {} ms", label, + interpolatePercentile(sortedSamples, 95.0)); + CLOG_WARNING(Perf, "p99 {}: {} ms", label, + interpolatePercentile(sortedSamples, 99.0)); + CLOG_WARNING(Perf, "{} stddev: {} ms", label, std::sqrt(variance)); } template @@ -585,42 +549,65 @@ uint32_t ApplyLoad::calculateBenchmarkModelTxCount() const { auto const& config = mApp.getConfig(); - releaseAssertOrThrow(config.APPLY_LOAD_BATCH_SAC_COUNT > 0); switch (mModelTx) { case ApplyLoadModelTx::SAC: + { // In benchmark mode APPLY_LOAD_MAX_SOROBAN_TX_COUNT means modeled SAC // transfers, while generation expects number of tx envelopes. - releaseAssertOrThrow(config.APPLY_LOAD_MAX_SOROBAN_TX_COUNT % - config.APPLY_LOAD_BATCH_SAC_COUNT == - 0); + releaseAssertOrThrow(config.APPLY_LOAD_BATCH_SAC_COUNT > 0); + if (config.APPLY_LOAD_MAX_SOROBAN_TX_COUNT % + config.APPLY_LOAD_BATCH_SAC_COUNT != + 0) { - auto benchmarkTxCount = config.APPLY_LOAD_MAX_SOROBAN_TX_COUNT / - config.APPLY_LOAD_BATCH_SAC_COUNT; - if (benchmarkTxCount < - config.APPLY_LOAD_LEDGER_MAX_DEPENDENT_TX_CLUSTERS) - { - throw std::runtime_error( - "For benchmark SAC mode, " - "APPLY_LOAD_MAX_SOROBAN_TX_COUNT / " - "APPLY_LOAD_BATCH_SAC_COUNT must be at least " - "APPLY_LOAD_LEDGER_MAX_DEPENDENT_TX_CLUSTERS to satisfy " - "requested parallelism"); - } - return benchmarkTxCount; + throw std::runtime_error( + "For benchmark APPLY_LOAD_MODEL_TX=sac, " + "APPLY_LOAD_MAX_SOROBAN_TX_COUNT must be divisible by " + "APPLY_LOAD_BATCH_SAC_COUNT"); } + auto benchmarkTxCount = config.APPLY_LOAD_MAX_SOROBAN_TX_COUNT / + config.APPLY_LOAD_BATCH_SAC_COUNT; + if (benchmarkTxCount < + config.APPLY_LOAD_LEDGER_MAX_DEPENDENT_TX_CLUSTERS) + { + throw std::runtime_error( + "For benchmark APPLY_LOAD_MODEL_TX=sac, " + "APPLY_LOAD_MAX_SOROBAN_TX_COUNT / " + "APPLY_LOAD_BATCH_SAC_COUNT must be at least " + "APPLY_LOAD_LEDGER_MAX_DEPENDENT_TX_CLUSTERS to satisfy " + "requested parallelism"); + } + return benchmarkTxCount; + } case ApplyLoadModelTx::CUSTOM_TOKEN: - // No batching for custom token, one transfer per tx envelope - return config.APPLY_LOAD_MAX_SOROBAN_TX_COUNT; case ApplyLoadModelTx::SOROSWAP: - // No batching for Soroswap, one swap per tx envelope + // These models perform one transfer or swap per tx envelope. return config.APPLY_LOAD_MAX_SOROBAN_TX_COUNT; } releaseAssertOrThrow(false); return 0; } +uint32_t +ApplyLoad::txQueueMultiplier(bool isSoroban) const +{ + if (!measuresTxSetPhases()) + { + return 1; + } + auto const& cfg = mApp.getConfig(); + return isSoroban ? cfg.SOROBAN_TRANSACTION_QUEUE_SIZE_MULTIPLIER + : cfg.TRANSACTION_QUEUE_SIZE_MULTIPLIER; +} + +uint32_t +ApplyLoad::classicTxCount() const +{ + return mApp.getConfig().APPLY_LOAD_CLASSIC_TXS_PER_LEDGER * + txQueueMultiplier(false); +} + void ApplyLoad::upgradeSettingsForMaxTPS(uint32_t txsToGenerate) { @@ -722,32 +709,14 @@ ApplyLoad::ApplyLoad(Application& app) // Basic input parameter validation - it's not comprehensive, but should // catch some simple misconfiguration cases. - if (mMode == ApplyLoadMode::BENCHMARK_MODEL_TX) + if (measuresTxSetPhases() && + (txQueueMultiplier(true) < 2 || + (config.APPLY_LOAD_CLASSIC_TXS_PER_LEDGER > 0 && + txQueueMultiplier(false) < 2))) { - if (mModelTx == ApplyLoadModelTx::SAC) - { - if (config.APPLY_LOAD_MAX_SOROBAN_TX_COUNT % - config.APPLY_LOAD_BATCH_SAC_COUNT != - 0) - { - throw std::runtime_error( - "For benchmark APPLY_LOAD_MODEL_TX=sac, " - "APPLY_LOAD_MAX_SOROBAN_TX_COUNT must be divisible by " - "APPLY_LOAD_BATCH_SAC_COUNT"); - } - auto benchmarkTxCount = config.APPLY_LOAD_MAX_SOROBAN_TX_COUNT / - config.APPLY_LOAD_BATCH_SAC_COUNT; - if (benchmarkTxCount < - config.APPLY_LOAD_LEDGER_MAX_DEPENDENT_TX_CLUSTERS) - { - throw std::runtime_error( - "For benchmark APPLY_LOAD_MODEL_TX=sac, " - "APPLY_LOAD_MAX_SOROBAN_TX_COUNT / " - "APPLY_LOAD_BATCH_SAC_COUNT must be at least " - "APPLY_LOAD_LEDGER_MAX_DEPENDENT_TX_CLUSTERS to satisfy " - "requested parallelism"); - } - } + throw std::runtime_error( + "Tx-set timing requires transaction queue size multipliers " + "of at least 2 for each enabled transaction type"); } // Noisy binary search-based modes require at least 30 ledgers to have // enough samples for statistics to be meaningful. @@ -781,9 +750,7 @@ ApplyLoad::ApplyLoad(Application& app) case ApplyLoadMode::LIMIT_BASED: mNumAccounts = config.APPLY_LOAD_MAX_SOROBAN_TX_COUNT * config.SOROBAN_TRANSACTION_QUEUE_SIZE_MULTIPLIER + - config.APPLY_LOAD_CLASSIC_TXS_PER_LEDGER * - config.TRANSACTION_QUEUE_SIZE_MULTIPLIER + - 2; + classicTxCount() + 2; break; case ApplyLoadMode::MAX_SAC_TPS: mNumAccounts = convertTPStoTPL(config.APPLY_LOAD_MAX_SAC_TPS_MAX_TPS, @@ -792,25 +759,12 @@ ApplyLoad::ApplyLoad(Application& app) config.APPLY_LOAD_CLASSIC_TXS_PER_LEDGER; break; case ApplyLoadMode::BENCHMARK_MODEL_TX: - if (mModelTx == ApplyLoadModelTx::CUSTOM_TOKEN) - { - // Need 2 unique accounts per transfer to avoid conflicts - mNumAccounts = config.APPLY_LOAD_MAX_SOROBAN_TX_COUNT * 2 + - config.APPLY_LOAD_CLASSIC_TXS_PER_LEDGER; - } - else if (mModelTx == ApplyLoadModelTx::SOROSWAP) - { - // Need 1 unique account per swap + classic accounts + root - mNumAccounts = config.APPLY_LOAD_MAX_SOROBAN_TX_COUNT + 1 + - config.APPLY_LOAD_CLASSIC_TXS_PER_LEDGER; - } - else - { - mNumAccounts = - config.APPLY_LOAD_MAX_SOROBAN_TX_COUNT * - config.SOROBAN_TRANSACTION_QUEUE_SIZE_MULTIPLIER + - config.APPLY_LOAD_CLASSIC_TXS_PER_LEDGER + 2; - } + // Token transfers need disjoint source/destination pairs; the other + // models need one source per candidate. Reserve setup accounts too. + mNumAccounts = + calculateBenchmarkModelTxCount() * txQueueMultiplier(true) * + (mModelTx == ApplyLoadModelTx::CUSTOM_TOKEN ? 2 : 1) + + classicTxCount() + 2; break; } if (config.APPLY_LOAD_LEDGER_MAX_DEPENDENT_TX_CLUSTERS == 0) @@ -854,22 +808,6 @@ ApplyLoad::setup() mTxGenerator.addAccount(i, acc); } - if (mApp.getLedgerManager() - .getLastClosedLedgerHeader() - .header.maxTxSetSize < - mApp.getConfig().APPLY_LOAD_CLASSIC_TXS_PER_LEDGER) - { - auto upgrade = xdr::xvector{}; - - LedgerUpgrade ledgerUpgrade; - ledgerUpgrade.type(LEDGER_UPGRADE_MAX_TX_SET_SIZE); - ledgerUpgrade.newMaxTxSetSize() = - mApp.getConfig().APPLY_LOAD_CLASSIC_TXS_PER_LEDGER; - auto v = xdr::xdr_to_opaque(ledgerUpgrade); - upgrade.push_back(UpgradeType{v.begin(), v.end()}); - closeLedger({}, upgrade); - } - setupUpgradeContract(); // Set large resources for initial setup @@ -918,6 +856,24 @@ ApplyLoad::setup() break; } + // Make sure the classic ledger capacity fits the generated payments. Do + // this after contract setup, which may temporarily raise the classic + // limit. The tx-set timing path overfills the classic candidates and needs + // the builder to trim them to exactly APPLY_LOAD_CLASSIC_TXS_PER_LEDGER, so + // it pins the limit rather than just raising it. + auto classicLimit = cfg.APPLY_LOAD_CLASSIC_TXS_PER_LEDGER; + auto currentLimit = + mApp.getLedgerManager().getLastClosedLedgerHeader().header.maxTxSetSize; + if (classicLimit > 0 && + (currentLimit < classicLimit || + (measuresTxSetPhases() && currentLimit != classicLimit))) + { + LedgerUpgrade upgrade(LEDGER_UPGRADE_MAX_TX_SET_SIZE); + upgrade.newMaxTxSetSize() = classicLimit; + auto bytes = xdr::xdr_to_opaque(upgrade); + closeLedger({}, {UpgradeType{bytes.begin(), bytes.end()}}); + } + // Setup initial bucket list for modes that support it. if (mMode == ApplyLoadMode::LIMIT_BASED) { @@ -926,7 +882,7 @@ ApplyLoad::setup() } bool -ApplyLoad::measuresTxSetValidation() const +ApplyLoad::measuresTxSetPhases() const { switch (mTimingPhases) { @@ -940,85 +896,52 @@ ApplyLoad::measuresTxSetValidation() const } void -ApplyLoad::logTxSetValidationPhaseStats() const +ApplyLoad::logTxSetPhaseStats() const { - releaseAssert(measuresTxSetValidation()); + releaseAssert(measuresTxSetPhases()); + auto const ledgerCount = mPhaseReceiveToCloseMs.size(); + releaseAssert(mPhaseConstructionMs.size() == ledgerCount && + mPhaseValidationMs.size() == ledgerCount && + mPhaseLedgerCloseMs.size() == ledgerCount); CLOG_WARNING(Perf, "================================================"); + logPhaseStats("txset construction", mPhaseConstructionMs); logPhaseStats("txset validation", mPhaseValidationMs); + logPhaseStats("ledger close", mPhaseLedgerCloseMs); + logPhaseStats("receive-to-close", mPhaseReceiveToCloseMs); + + CLOG_WARNING(Perf, + "candidate txs per ledger: {:.1f}, included txs per ledger: " + "{:.1f}", + static_cast(mBenchmarkCandidateTxCount) / ledgerCount, + static_cast(mBenchmarkTxCount) / ledgerCount); // Expect one cold check per tx + one StellarValue check per ledger. - auto const ledgerCount = static_cast(mPhaseValidationMs.size()); - auto const txCount = static_cast(mBenchmarkTxCount); CLOG_WARNING( Perf, "sig cache hits/misses: {}/{} (expected {} misses = {} tx sigs + {} " "value sigs)", - mLedgerSigCacheHits, mLedgerSigCacheMisses, txCount + ledgerCount, - txCount, ledgerCount); - if (txCount > 0) - { - CLOG_WARNING( - Perf, - "tx signature cache misses per transaction: {:.4f} (expect 1.0)", - static_cast(static_cast(mLedgerSigCacheMisses) - - ledgerCount) / - static_cast(txCount)); - } + mLedgerSigCacheHits, mLedgerSigCacheMisses, + mBenchmarkTxCount + ledgerCount, mBenchmarkTxCount, ledgerCount); // Expect one cold tx set validation per ledger. auto const validations = mApp.getMetrics().NewTimer({"herder", "txset", "validate"}).count(); CLOG_WARNING(Perf, "txset validations per ledger: {:.2f}", - static_cast(validations) / - static_cast(ledgerCount)); - - logPhaseStats("ledger close", mPhaseLedgerCloseMs); - logPhaseStats("end-to-end txset+apply", mPhaseEndToEndMs); - - // Report each phase's share of its own ledger's end-to-end time as a - // distribution over ledgers, so per-ledger variance in the shares is - // visible rather than just the ratio of the totals. - auto const sampleCount = mPhaseEndToEndMs.size(); - releaseAssert(mPhaseValidationMs.size() == sampleCount && - mPhaseLedgerCloseMs.size() == sampleCount); - std::vector validationShares; - std::vector ledgerCloseShares; - std::vector otherShares; - for (size_t i = 0; i < sampleCount; ++i) - { - if (mPhaseEndToEndMs[i] <= 0.0) - { - continue; - } - double validationShare = - mPhaseValidationMs[i] / mPhaseEndToEndMs[i] * 100.0; - double ledgerCloseShare = - mPhaseLedgerCloseMs[i] / mPhaseEndToEndMs[i] * 100.0; - validationShares.emplace_back(validationShare); - ledgerCloseShares.emplace_back(ledgerCloseShare); - // Everything outside validation and close: wire decoding, SCP - // processing, and externalization overhead. - otherShares.emplace_back(100.0 - validationShare - ledgerCloseShare); - } - if (!validationShares.empty()) - { - CLOG_WARNING(Perf, "per-ledger phase shares of end-to-end time:"); - logPhaseStats("txset validation share", validationShares, "%"); - logPhaseStats("ledger close share", ledgerCloseShares, "%"); - logPhaseStats("other consensus overhead share", otherShares, "%"); - } + static_cast(validations) / ledgerCount); CLOG_WARNING(Perf, "================================================"); } void -ApplyLoad::recordSorobanUtilization(ApplicableTxSetFrame const& txSet, - uint32_t ledgerVersion) +ApplyLoad::recordSorobanUtilization(ApplicableTxSetFrame const& txSet) { - auto ledgerResources = mApp.getLedgerManager().maxLedgerResources(true); - auto txSetResources = txSet.getPhases() - .at(static_cast(TxSetPhase::SOROBAN)) - .getTotalResources(ledgerVersion) - .value(); + auto& lm = mApp.getLedgerManager(); + auto ledgerResources = lm.maxLedgerResources(true); + auto txSetResources = + txSet.getPhases() + .at(static_cast(TxSetPhase::SOROBAN)) + .getTotalResources( + lm.getLastClosedLedgerHeader().header.ledgerVersion) + .value(); auto updateUtilization = [&](medida::Histogram& histogram, Resource::Type resource) { histogram.Update(txSetResources.getVal(resource) * 1.0 / @@ -1043,49 +966,63 @@ ApplyLoad::closeLedger(std::vector const& txs, xdr::xvector const& upgrades, bool recordUtilization) { - auto txSet = makeTxSetFromTransactions(txs, mApp, 0, 0); - if (recordUtilization) { - recordSorobanUtilization(*txSet.second, mApp.getLedgerManager() - .getLastClosedLedgerHeader() - .header.ledgerVersion); + auto txSet = makeTxSetFromTransactions(txs, mApp, 0, 0); + recordSorobanUtilization(*txSet.second); } - auto sv = - mApp.getHerder().makeStellarValue(txSet.first->getContentsHash(), 1, - upgrades, mApp.getConfig().NODE_SEED); - stellar::txtest::closeLedger(mApp, txs, /* strictOrder */ false, upgrades); } void -ApplyLoad::closeLedgerViaConsensus( - std::vector const& txs, bool recordUtilization) +ApplyLoad::closeBenchmarkLedger(std::vector const& txs, + bool recordUtilization) { - releaseAssert(!txs.empty()); - auto& herder = mApp.getHerder(); - auto const& lcl = mApp.getLedgerManager().getLastClosedLedgerHeader(); + if (!measuresTxSetPhases()) + { + closeLedger(txs, {}, recordUtilization); + return; + } + releaseAssert(!txs.empty()); + auto& herder = static_cast(mApp.getHerder()); + auto const lcl = mApp.getLedgerManager().getLastClosedLedgerHeader(); + auto const ledgerSeq = lcl.header.ledgerSeq + 1; uint64_t const closeTime = lcl.header.scpValue.closeTime + 1; - auto txSet = makeTxSetFromTransactions(txs, mApp, 1, 1); - if (recordUtilization) - { - recordSorobanUtilization(*txSet.second, lcl.header.ledgerVersion); - } + xdr::opaque_vec<> wireBytes; + StellarValue nominatedValue; + size_t included; + double constructionMs; + { + // Leader side: time trimming, surge pricing and parallel partitioning + // of an overfilled candidate list. Generation validated the candidates, + // warming caches as queue admission would. Queue extraction, validity + // caching and banning are only covered by the live herder.txset.build + // timer. + auto const buildStart = std::chrono::steady_clock::now(); + auto [txSet, applicableTxSet] = + makeTxSetFromTransactions(txs, mApp, 1, 1); + constructionMs = std::chrono::duration( + std::chrono::steady_clock::now() - buildStart) + .count(); + included = txSet->sizeTxTotal(); + releaseAssert(included > 0); + + if (recordUtilization) + { + recordSorobanUtilization(*applicableTxSet); + } - // We want to simulate a non-leader node receiving a TX set off the wire, - // so we build and sign outside the measured receiver-side span. - GeneralizedTransactionSet xdrTxSet; - txSet.first->toXDR(xdrTxSet); - auto const wireBytes = xdr::xdr_to_opaque(xdrTxSet); - auto const nominatedValue = - herder.makeStellarValue(txSet.first->getContentsHash(), closeTime, {}, - mApp.getConfig().NODE_SEED); - // Do not retain leader-side frames during the measured work. - xdrTxSet = GeneralizedTransactionSet{}; - txSet.first.reset(); - txSet.second.reset(); + // Serialize and sign outside the timed spans. Drop the leader's frames + // before simulating receipt and cold validation on a non-leader. + GeneralizedTransactionSet xdrTxSet; + txSet->toXDR(xdrTxSet); + wireBytes = xdr::xdr_to_opaque(xdrTxSet); + nominatedValue = + herder.makeStellarValue(txSet->getContentsHash(), closeTime, {}, + mApp.getConfig().NODE_SEED); + } // Validation should see cold signatures and leave them warm for apply. PubKeyUtils::clearVerifySigCache(); @@ -1105,7 +1042,7 @@ ApplyLoad::closeLedgerViaConsensus( double const validationBefore = validationTimer.sum(); double const ledgerCloseBefore = ledgerCloseTimer.sum(); - auto const e2eStart = std::chrono::steady_clock::now(); + auto const receiveStart = std::chrono::steady_clock::now(); // Decode into a fresh frame as the overlay receive path does. GeneralizedTransactionSet receivedXdr; @@ -1113,47 +1050,69 @@ ApplyLoad::closeLedgerViaConsensus( auto receivedTxSet = TxSetXDRFrame::makeFromWire(receivedXdr); // Nomination through externalization and apply use production SCP paths. - nominateAndClose(mApp, receivedTxSet, nominatedValue); + herder.getPendingEnvelopes().putTxSet(receivedTxSet->getContentsHash(), + ledgerSeq, receivedTxSet); + herder.getHerderSCPDriver().nominate(ledgerSeq, nominatedValue, + receivedTxSet, lcl.header.scpValue); + + auto const deadline = + std::chrono::steady_clock::now() + std::chrono::seconds(60); + size_t cranks = 0; + while (mApp.getLedgerManager().getLastClosedLedgerNum() < ledgerSeq && + std::chrono::steady_clock::now() < deadline) + { + mApp.getClock().crank(true); + ++cranks; + } + if (mApp.getLedgerManager().getLastClosedLedgerNum() < ledgerSeq) + { + throw std::runtime_error(fmt::format( + FMT_STRING( + "SCP did not externalize ledger {} within 60s ({} cranks); " + "close time {} is {}s from the wall clock (slip limit {}s)"), + ledgerSeq, cranks, closeTime, + static_cast(closeTime) - + static_cast(mApp.timeNow()), + Herder::MAX_TIME_SLIP_SECONDS.count())); + } - auto const e2eEnd = std::chrono::steady_clock::now(); - double const ledgerCloseMs = ledgerCloseTimer.sum() - ledgerCloseBefore; + auto const closeEnd = std::chrono::steady_clock::now(); + mPhaseConstructionMs.emplace_back(constructionMs); mPhaseValidationMs.emplace_back(validationTimer.sum() - validationBefore); - mPhaseLedgerCloseMs.emplace_back(ledgerCloseMs); - mPhaseEndToEndMs.emplace_back( - std::chrono::duration(e2eEnd - e2eStart).count()); + mPhaseLedgerCloseMs.emplace_back(ledgerCloseTimer.sum() - + ledgerCloseBefore); + mPhaseReceiveToCloseMs.emplace_back( + std::chrono::duration(closeEnd - receiveStart) + .count()); mApp.syncOwnMetrics(); mLedgerSigCacheHits += sigHitMeter.count() - sigHitsBefore; mLedgerSigCacheMisses += sigMissMeter.count() - sigMissesBefore; - mBenchmarkTxCount += txs.size(); -} + mBenchmarkCandidateTxCount += txs.size(); + mBenchmarkTxCount += included; -void -ApplyLoad::closeBenchmarkLedger(std::vector const& txs, - bool recordUtilization) -{ - switch (mTimingPhases) - { - case ApplyLoadTimingPhases::APPLY_ONLY: - closeLedger(txs, {}, recordUtilization); - break; - case ApplyLoadTimingPhases::TX_SET_VALIDATION_AND_APPLY: - closeLedgerViaConsensus(txs, recordUtilization); - break; - } + CLOG_INFO(Perf, + "Consensus close: ledger={} candidates={} included={} " + "construction_ms={:.3f} validation_ms={:.3f} close_ms={:.3f} " + "receive_to_close_ms={:.3f}", + ledgerSeq, txs.size(), included, mPhaseConstructionMs.back(), + mPhaseValidationMs.back(), mPhaseLedgerCloseMs.back(), + mPhaseReceiveToCloseMs.back()); } void ApplyLoad::execute() { logExecutionEnvironmentSnapshot(mApp.getConfig()); - if (measuresTxSetValidation()) + if (measuresTxSetPhases()) { mApp.getMetrics().NewTimer({"herder", "txset", "validate"}).Clear(); + mPhaseConstructionMs.clear(); mPhaseValidationMs.clear(); mPhaseLedgerCloseMs.clear(); - mPhaseEndToEndMs.clear(); + mPhaseReceiveToCloseMs.clear(); mLedgerSigCacheHits = 0; mLedgerSigCacheMisses = 0; + mBenchmarkCandidateTxCount = 0; mBenchmarkTxCount = 0; } @@ -1169,6 +1128,11 @@ ApplyLoad::execute() benchmarkModelTx(); break; } + + if (measuresTxSetPhases()) + { + logTxSetPhaseStats(); + } } void @@ -1706,11 +1670,6 @@ ApplyLoad::benchmarkLimits() getWriteEntryUtilization().max() / 1000.0); CLOG_INFO(Perf, "Tx Success Rate: {:f}%", successRate() * 100); - - if (measuresTxSetValidation()) - { - logTxSetValidationPhaseStats(); - } } double @@ -1727,32 +1686,21 @@ ApplyLoad::benchmarkLimitsIteration() auto const& config = mApp.getConfig(); std::vector txs; - auto maxResourcesToGenerate = lm.maxLedgerResources(true); - // The TxSet validation will compare the ledger instruction limit - // against the sum of the instructions of the slowest cluster in each - // stage, so we just multiply the instructions limit by the max number - // of clusters. - maxResourcesToGenerate.setVal( - Resource::Type::INSTRUCTIONS, - maxResourcesToGenerate.getVal(Resource::Type::INSTRUCTIONS) * - config.APPLY_LOAD_LEDGER_MAX_DEPENDENT_TX_CLUSTERS); - // Scale the resources by the tx queue multipler to emulate filled - // mempool. - maxResourcesToGenerate = - multiplyByDouble(maxResourcesToGenerate, + // maxLedgerResources already accounts for parallel instruction capacity. + auto maxResourcesToGenerate = + multiplyByDouble(lm.maxLedgerResources(true), config.SOROBAN_TRANSACTION_QUEUE_SIZE_MULTIPLIER); CLOG_INFO(Perf, "benchmark max generation resources: {}", maxResourcesToGenerate.toString()); auto resourcesLeft = maxResourcesToGenerate; - // Generate classic payments using the first - // APPLY_LOAD_CLASSIC_TXS_PER_LEDGER accounts. + // Generate classic payments using the first classicTxCount() accounts. generateClassicPayments(txs, 0); // Use remaining accounts (after classic) for soroban transactions auto const& accounts = mTxGenerator.getAccounts(); - uint32_t sorobanStartIdx = config.APPLY_LOAD_CLASSIC_TXS_PER_LEDGER; + uint32_t sorobanStartIdx = classicTxCount(); // Omit root account std::vector shuffledAccounts(accounts.size() - 1 - sorobanStartIdx); @@ -1992,24 +1940,8 @@ ApplyLoad::benchmarkModelTx() for (size_t i = 0; i < config.APPLY_LOAD_NUM_LEDGERS; ++i) { - double closeTimeMs = 0.0; - switch (mModelTx) - { - case ApplyLoadModelTx::SAC: - closeTimeMs = benchmarkModelTxTpsSingleLedger( - ApplyLoadModelTx::SAC, calculateBenchmarkModelTxCount()); - break; - case ApplyLoadModelTx::CUSTOM_TOKEN: - closeTimeMs = benchmarkModelTxTpsSingleLedger( - ApplyLoadModelTx::CUSTOM_TOKEN, - calculateBenchmarkModelTxCount()); - break; - case ApplyLoadModelTx::SOROSWAP: - closeTimeMs = benchmarkModelTxTpsSingleLedger( - ApplyLoadModelTx::SOROSWAP, calculateBenchmarkModelTxCount()); - break; - } - closeTimes.emplace_back(closeTimeMs); + closeTimes.emplace_back(benchmarkModelTxTpsSingleLedger( + mModelTx, calculateBenchmarkModelTxCount())); } releaseAssert(!closeTimes.empty()); @@ -2020,11 +1952,6 @@ ApplyLoad::benchmarkModelTx() config.APPLY_LOAD_NUM_LEDGERS, config.APPLY_LOAD_MAX_SOROBAN_TX_COUNT); logPhaseStats("close time", closeTimes); CLOG_WARNING(Perf, "================================================"); - - if (measuresTxSetValidation()) - { - logTxSetValidationPhaseStats(); - } } double @@ -2043,29 +1970,26 @@ ApplyLoad::benchmarkModelTxTpsSingleLedger(ApplyLoadModelTx modelTx, // Generate classic payments using accounts at the end of the range, // so they don't overlap with soroban accounts. + uint32_t candidates = txsPerLedger * txQueueMultiplier(true); std::vector txs; - txs.reserve(txsPerLedger + - mApp.getConfig().APPLY_LOAD_CLASSIC_TXS_PER_LEDGER); - uint32_t classicStartIdx = - mNumAccounts - mApp.getConfig().APPLY_LOAD_CLASSIC_TXS_PER_LEDGER; + txs.reserve(candidates + classicTxCount()); + uint32_t classicStartIdx = mNumAccounts - classicTxCount(); generateClassicPayments(txs, classicStartIdx); // Generate soroban model transactions switch (modelTx) { case ApplyLoadModelTx::SAC: - generateSacPayments(txs, txsPerLedger); + generateSacPayments(txs, candidates); break; case ApplyLoadModelTx::CUSTOM_TOKEN: - generateTokenTransfers(txs, txsPerLedger); + generateTokenTransfers(txs, candidates); break; case ApplyLoadModelTx::SOROSWAP: - generateSoroswapSwaps(txs, txsPerLedger); + generateSoroswapSwaps(txs, candidates); break; } - releaseAssertOrThrow( - txs.size() == - txsPerLedger + mApp.getConfig().APPLY_LOAD_CLASSIC_TXS_PER_LEDGER); + releaseAssertOrThrow(txs.size() == candidates + classicTxCount()); mApp.getBucketManager().getLiveBucketList().resolveAllFutures(); releaseAssert( @@ -2082,7 +2006,9 @@ ApplyLoad::benchmarkModelTxTpsSingleLedger(ApplyLoadModelTx modelTx, CLOG_INFO(Perf, "Model tx benchmark: {:.2f}ms", closeTime); // Check transaction success rate. We should never have any failures, - // and all TXs should have been executed. + // and all TXs should have been executed. When the candidate list is + // overfilled, the tx-set builder must trim it down to exactly one full + // ledger so that the timings stay comparable across timing paths. int64_t newSuccessCount = mTxGenerator.getApplySorobanSuccess().count() - initialSuccessCount; @@ -2107,18 +2033,16 @@ void ApplyLoad::generateClassicPayments(std::vector& txs, uint32_t startAccountIdx) { - auto const& config = mApp.getConfig(); auto const& accounts = mTxGenerator.getAccounts(); auto& lm = mApp.getLedgerManager(); - releaseAssert(accounts.size() >= - startAccountIdx + config.APPLY_LOAD_CLASSIC_TXS_PER_LEDGER); + releaseAssert(accounts.size() >= startAccountIdx + classicTxCount()); CheckValidLedgerViewWrapper ledgerView(mApp); auto appConnector = mApp.getAppConnector(); auto diagnostics = DiagnosticEventManager::createDisabled(); - for (uint32_t i = 0; i < config.APPLY_LOAD_CLASSIC_TXS_PER_LEDGER; ++i) + for (uint32_t i = 0; i < classicTxCount(); ++i) { uint64_t accountIdx = startAccountIdx + i; auto it = accounts.find(accountIdx); diff --git a/src/simulation/ApplyLoad.h b/src/simulation/ApplyLoad.h index a81af0f2ee..794f5fb4ed 100644 --- a/src/simulation/ApplyLoad.h +++ b/src/simulation/ApplyLoad.h @@ -50,25 +50,20 @@ class ApplyLoad uint32_t getTotalHotArchiveEntries() const; private: - // Whether this run records tx-set validation phase timings (i.e. runs in - // the TX_SET_VALIDATION_AND_APPLY timing path). - bool measuresTxSetValidation() const; - - // Simulates a non-leader receiving a tx set over the wire, then closes it - // through local consensus. Tx-set creation is outside the measured span. - void - closeLedgerViaConsensus(std::vector const& txs, - bool recordUtilization); + // Whether this run records tx-set phase timings (i.e. runs in the + // TX_SET_VALIDATION_AND_APPLY timing path). + bool measuresTxSetPhases() const; + // Closes a benchmark ledger through the path selected by - // APPLY_LOAD_TIMING_PHASES. + // APPLY_LOAD_TIMING_PHASES. Tx-set timing covers leader construction, + // followed by cold receiver validation and application via local consensus. void closeBenchmarkLedger(std::vector const& txs, bool recordUtilization); - void recordSorobanUtilization(ApplicableTxSetFrame const& txSet, - uint32_t ledgerVersion); + void recordSorobanUtilization(ApplicableTxSetFrame const& txSet); - // Logs the phase timings recorded by closeLedgerViaConsensus. Must only - // be called when measuresTxSetValidation() is true. - void logTxSetValidationPhaseStats() const; + // Logs the phase timings recorded by closeBenchmarkLedger. Must only + // be called when measuresTxSetPhases() is true. + void logTxSetPhaseStats() const; uint32_t calculateRequiredHotArchiveEntries(Config const& cfg); @@ -117,8 +112,7 @@ class ApplyLoad // parameters. double benchmarkLimitsIteration(); - // Generates APPLY_LOAD_CLASSIC_TXS_PER_LEDGER classic payment TXs - // using accounts starting at startAccountIdx. + // Generate classic payment candidates from accounts at startAccountIdx. void generateClassicPayments(std::vector& txs, uint32_t startAccountIdx); @@ -144,6 +138,14 @@ class ApplyLoad // to execute, taking APPLY_LOAD_BATCH_SAC_COUNT into account. uint32_t calculateBenchmarkModelTxCount() const; + // Scales model and classic candidate counts by the queue size multiplier + // when measuring tx-set phases; otherwise returns 1. Limit-based Soroban + // generation always scales its resource budget by the configured + // multiplier. + uint32_t txQueueMultiplier(bool isSoroban) const; + // Number of classic payment candidates generated per ledger. + uint32_t classicTxCount() const; + // Iterate over all available accounts to make sure they are loaded into the // BucketListDB cache. Note that this should be run every time an account // entry is modified. @@ -164,17 +166,19 @@ class ApplyLoad ApplyLoadTxProfile mLimitsBasedTxProfile; ApplyLoadTimingPhases mTimingPhases; - // A phase is a timed portion of one ledger's receiver-side processing. We - // track cold tx-set validation, ledger close/application, and end-to-end - // time from wire decoding through the completed ledger close. Ledger close - // includes apply-side prepareForApply. + // Construction is timed separately from receiver-side decoding, cold + // validation and ledger close. Ledger close includes prepareForApply. + std::vector mPhaseConstructionMs; std::vector mPhaseValidationMs; std::vector mPhaseLedgerCloseMs; - std::vector mPhaseEndToEndMs; + std::vector mPhaseReceiveToCloseMs; - // Signature cache totals and the transaction count used to interpret them. + // Signature cache totals and the transaction counts used to interpret + // them: candidates offered to the tx-set builder, and transactions it + // included in the built sets. uint64_t mLedgerSigCacheHits = 0; uint64_t mLedgerSigCacheMisses = 0; + uint64_t mBenchmarkCandidateTxCount = 0; uint64_t mBenchmarkTxCount = 0; uint32_t mTotalHotArchiveEntries; diff --git a/src/simulation/test/LoadGeneratorTests.cpp b/src/simulation/test/LoadGeneratorTests.cpp index b4d66c69ce..929241e119 100644 --- a/src/simulation/test/LoadGeneratorTests.cpp +++ b/src/simulation/test/LoadGeneratorTests.cpp @@ -1164,9 +1164,13 @@ TEST_CASE("apply load benchmark model tx", TEST_CASE("apply load benchmark custom token", "[loadgen][applyload][soroban][acceptance]") { + auto const timingPhases = + GENERATE(ApplyLoadTimingPhases::APPLY_ONLY, + ApplyLoadTimingPhases::TX_SET_VALIDATION_AND_APPLY); auto cfg = getTestConfig(); cfg.APPLY_LOAD_MODE = ApplyLoadMode::BENCHMARK_MODEL_TX; cfg.APPLY_LOAD_MODEL_TX = ApplyLoadModelTx::CUSTOM_TOKEN; + cfg.APPLY_LOAD_TIMING_PHASES = timingPhases; cfg.TESTING_UPGRADE_MAX_TX_SET_SIZE = 1000; cfg.USE_CONFIG_FOR_GENESIS = true; cfg.LEDGER_PROTOCOL_VERSION = Config::CURRENT_LEDGER_PROTOCOL_VERSION; @@ -1192,14 +1196,25 @@ TEST_CASE("apply load benchmark custom token", auto& successCountMetric = app->getMetrics().NewCounter({"ledger", "apply-soroban", "success"}); REQUIRE(successCountMetric.count() > 0); + if (timingPhases == ApplyLoadTimingPhases::TX_SET_VALIDATION_AND_APPLY) + { + // Each benchmark ledger runs at least one cold tx set validation. + REQUIRE(app->getMetrics() + .NewTimer({"herder", "txset", "validate"}) + .count() >= cfg.APPLY_LOAD_NUM_LEDGERS); + } } TEST_CASE("apply load benchmark soroswap", "[loadgen][applyload][soroban][acceptance]") { + auto const timingPhases = + GENERATE(ApplyLoadTimingPhases::APPLY_ONLY, + ApplyLoadTimingPhases::TX_SET_VALIDATION_AND_APPLY); auto cfg = getTestConfig(); cfg.APPLY_LOAD_MODE = ApplyLoadMode::BENCHMARK_MODEL_TX; cfg.APPLY_LOAD_MODEL_TX = ApplyLoadModelTx::SOROSWAP; + cfg.APPLY_LOAD_TIMING_PHASES = timingPhases; cfg.USE_CONFIG_FOR_GENESIS = true; cfg.LEDGER_PROTOCOL_VERSION = Config::CURRENT_LEDGER_PROTOCOL_VERSION; cfg.MANUAL_CLOSE = true; @@ -1224,6 +1239,13 @@ TEST_CASE("apply load benchmark soroswap", auto& successCountMetric = app->getMetrics().NewCounter({"ledger", "apply-soroban", "success"}); REQUIRE(successCountMetric.count() > 0); + if (timingPhases == ApplyLoadTimingPhases::TX_SET_VALIDATION_AND_APPLY) + { + // Each benchmark ledger runs at least one cold tx set validation. + REQUIRE(app->getMetrics() + .NewTimer({"herder", "txset", "validate"}) + .count() >= cfg.APPLY_LOAD_NUM_LEDGERS); + } } TEST_CASE("noisy binary search", "[applyload]")