From f262f3330109a5e8fb053f9bc7148345f3faf94a Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Fri, 13 Sep 2024 17:13:35 -0700 Subject: [PATCH 1/6] Refactor LoadGen to pull out tx generation code --- src/main/CommandHandler.cpp | 3 +- src/simulation/LoadGenerator.cpp | 1047 +++----------------- src/simulation/LoadGenerator.h | 180 +--- src/simulation/TxGenerator.cpp | 886 +++++++++++++++++ src/simulation/TxGenerator.h | 167 ++++ src/simulation/test/LoadGeneratorTests.cpp | 9 +- 6 files changed, 1237 insertions(+), 1055 deletions(-) create mode 100644 src/simulation/TxGenerator.cpp create mode 100644 src/simulation/TxGenerator.h diff --git a/src/main/CommandHandler.cpp b/src/main/CommandHandler.cpp index b438a65fa6..fd8d9e3034 100644 --- a/src/main/CommandHandler.cpp +++ b/src/main/CommandHandler.cpp @@ -1405,7 +1405,8 @@ CommandHandler::generateLoad(std::string const& params, std::string& retStr) if (cfg.mode == LoadGenMode::SOROBAN_CREATE_UPGRADE) { auto configUpgradeKey = - mApp.getLoadGenerator().getConfigUpgradeSetKey(cfg); + mApp.getLoadGenerator().getConfigUpgradeSetKey( + cfg.getSorobanUpgradeConfig()); auto configUpgradeKeyStr = stellar::decoder::encode_b64( xdr::xdr_to_opaque(configUpgradeKey)); res["config_upgrade_set_key"] = configUpgradeKeyStr; diff --git a/src/simulation/LoadGenerator.cpp b/src/simulation/LoadGenerator.cpp index ef65b67826..38ede5358d 100644 --- a/src/simulation/LoadGenerator.cpp +++ b/src/simulation/LoadGenerator.cpp @@ -45,12 +45,6 @@ namespace { // Default distribution settings, largely based on averages seen on testnet constexpr unsigned short DEFAULT_OP_COUNT = 1; -constexpr uint32_t DEFAULT_WASM_BYTES = 35 * 1024; -constexpr uint32_t DEFAULT_NUM_DATA_ENTRIES = 2; -constexpr uint32_t DEFAULT_IO_KILOBYTES = 1; -constexpr uint32_t DEFAULT_TX_SIZE_BYTES = 256; -constexpr uint64_t DEFAULT_INSTRUCTIONS = 28'000'000; - // Sample from a discrete distribution of `values` with weights `weights`. // Returns `defaultValue` if `values` is empty. template @@ -90,18 +84,14 @@ const uint32_t LoadGenerator::COMPLETION_TIMEOUT_WITHOUT_CHECKS = 4; const uint32_t LoadGenerator::MIN_UNIQUE_ACCOUNT_MULTIPLIER = 3; LoadGenerator::LoadGenerator(Application& app) - : mMinBalance(0) - , mLastSecond(0) + : mTxGenerator(app) , mApp(app) + , mLastSecond(0) , mTotalSubmitted(0) , mLoadgenComplete( mApp.getMetrics().NewMeter({"loadgen", "run", "complete"}, "run")) , mLoadgenFail( mApp.getMetrics().NewMeter({"loadgen", "run", "failed"}, "run")) - , mApplySorobanSuccess( - mApp.getMetrics().NewCounter({"ledger", "apply-soroban", "success"})) - , mApplySorobanFailure( - mApp.getMetrics().NewCounter({"ledger", "apply-soroban", "failure"})) , mStepTimer(mApp.getMetrics().NewTimer({"loadgen", "step", "submit"})) , mStepMeter( mApp.getMetrics().NewMeter({"loadgen", "step", "count"}, "step")) @@ -163,44 +153,13 @@ LoadGenerator::getMode(std::string const& mode) } } -int -generateFee(std::optional maxGeneratedFeeRate, Application& app, - size_t opsCnt) -{ - int fee = 0; - auto baseFee = app.getLedgerManager().getLastTxFee(); - - if (maxGeneratedFeeRate) - { - auto feeRateDistr = - uniform_int_distribution(baseFee, *maxGeneratedFeeRate); - // Add a bit more fee to get non-integer fee rates, such that - // `floor(fee / opsCnt) == feeRate`, but - // `fee / opsCnt >= feeRate`. - // This is to create a bit more realistic fee structure: in reality not - // every transaction would necessarily have the `fee == ops_count * - // some_int`. This also would exercise more code paths/logic during the - // transaction comparisons. - auto fractionalFeeDistr = uniform_int_distribution( - 0, static_cast(opsCnt) - 1); - fee = static_cast(opsCnt) * feeRateDistr(gRandomEngine) + - fractionalFeeDistr(gRandomEngine); - } - else - { - fee = static_cast(opsCnt * baseFee); - } - - return fee; -} - void LoadGenerator::createRootAccount() { releaseAssert(!mRoot); auto rootTestAccount = TestAccount::createRoot(mApp); mRoot = make_shared(rootTestAccount); - if (!loadAccount(mRoot, mApp)) + if (!mTxGenerator.loadAccount(mRoot)) { CLOG_ERROR(LoadGen, "Could not retrieve root account!"); } @@ -256,7 +215,7 @@ LoadGenerator::cleanupAccounts() for (auto it = mCreationSourceAccounts.begin(); it != mCreationSourceAccounts.end();) { - if (loadAccount(it->second, mApp)) + if (mTxGenerator.loadAccount(it->second)) { mAccountsAvailable.insert(it->first); it = mCreationSourceAccounts.erase(it); @@ -270,8 +229,9 @@ LoadGenerator::cleanupAccounts() // "Free" any accounts that aren't used by the tx queue anymore for (auto it = mAccountsInUse.begin(); it != mAccountsInUse.end();) { - auto accIt = mAccounts.find(*it); - releaseAssert(accIt != mAccounts.end()); + auto const& accounts = mTxGenerator.getAccounts(); + auto accIt = accounts.find(*it); + releaseAssert(accIt != accounts.end()); if (!mApp.getHerder().sourceAccountPending( accIt->second->getPublicKey())) { @@ -289,7 +249,7 @@ LoadGenerator::cleanupAccounts() void LoadGenerator::reset() { - mAccounts.clear(); + mTxGenerator.reset(); mAccountsInUse.clear(); mAccountsAvailable.clear(); mCreationSourceAccounts.clear(); @@ -402,7 +362,7 @@ LoadGenerator::start(GeneratedLoadConfig& cfg) auto instanceKeyIter = mContractInstanceKeys.begin(); std::advance(instanceKeyIter, i % sorobanLoadCfg.nInstances); - ContractInstance instance; + TxGenerator::ContractInstance instance; instance.readOnlyKeys.emplace_back(*mCodeKey); instance.readOnlyKeys.emplace_back(*instanceKeyIter); instance.contractID = instanceKeyIter->contractData().contract; @@ -421,12 +381,21 @@ LoadGenerator::start(GeneratedLoadConfig& cfg) releaseAssert(mPreLoadgenApplySorobanSuccess == 0); releaseAssert(mPreLoadgenApplySorobanFailure == 0); - mPreLoadgenApplySorobanSuccess = mApplySorobanSuccess.count(); - mPreLoadgenApplySorobanFailure = mApplySorobanFailure.count(); + mPreLoadgenApplySorobanSuccess = + mTxGenerator.GetApplySorobanSuccess().count(); + mPreLoadgenApplySorobanFailure = + mTxGenerator.GetApplySorobanFailure().count(); mStarted = true; } +ConfigUpgradeSetKey +LoadGenerator::getConfigUpgradeSetKey(SorobanUpgradeConfig const& upgradeCfg, + Hash const& contractId) const +{ + return mTxGenerator.getConfigUpgradeSetKey(upgradeCfg, contractId); +} + // Schedule a callback to generateLoad() STEP_MSECS milliseconds from now. void LoadGenerator::scheduleLoadGeneration(GeneratedLoadConfig cfg) @@ -643,8 +612,6 @@ LoadGenerator::generateLoad(GeneratedLoadConfig cfg) return; } - updateMinBalance(); - auto txPerStep = getTxPerStep(cfg.txRate, cfg.spikeInterval, cfg.spikeSize); if (cfg.mode == LoadGenMode::CREATE) { @@ -688,7 +655,7 @@ LoadGenerator::generateLoad(GeneratedLoadConfig cfg) uint64_t sourceAccountId = getNextAvailableAccount(ledgerNum); - std::function()> generateTx; @@ -699,18 +666,18 @@ LoadGenerator::generateLoad(GeneratedLoadConfig cfg) break; case LoadGenMode::PAY: generateTx = [&]() { - return paymentTransaction(cfg.nAccounts, cfg.offset, - ledgerNum, sourceAccountId, 1, - cfg.maxGeneratedFeeRate); + return mTxGenerator.paymentTransaction( + cfg.nAccounts, cfg.offset, ledgerNum, sourceAccountId, + 1, cfg.maxGeneratedFeeRate); }; break; case LoadGenMode::PRETEND: { auto opCount = chooseOpCount(mApp.getConfig()); generateTx = [&, opCount]() { - return pretendTransaction(cfg.nAccounts, cfg.offset, - ledgerNum, sourceAccountId, - opCount, cfg.maxGeneratedFeeRate); + return mTxGenerator.pretendTransaction( + cfg.nAccounts, cfg.offset, ledgerNum, sourceAccountId, + opCount, cfg.maxGeneratedFeeRate); }; } break; @@ -722,13 +689,13 @@ LoadGenerator::generateLoad(GeneratedLoadConfig cfg) generateTx = [&, opCount, isDex]() { if (isDex) { - return manageOfferTransaction(ledgerNum, - sourceAccountId, opCount, - cfg.maxGeneratedFeeRate); + return mTxGenerator.manageOfferTransaction( + ledgerNum, sourceAccountId, opCount, + cfg.maxGeneratedFeeRate); } else { - return paymentTransaction( + return mTxGenerator.paymentTransaction( cfg.nAccounts, cfg.offset, ledgerNum, sourceAccountId, opCount, cfg.maxGeneratedFeeRate); } @@ -738,10 +705,10 @@ LoadGenerator::generateLoad(GeneratedLoadConfig cfg) case LoadGenMode::SOROBAN_UPLOAD: { generateTx = [&]() { - return sorobanRandomWasmTransaction( + return mTxGenerator.sorobanRandomWasmTransaction( ledgerNum, sourceAccountId, - generateFee(cfg.maxGeneratedFeeRate, mApp, - /* opsCnt */ 1)); + mTxGenerator.generateFee(cfg.maxGeneratedFeeRate, + /* opsCnt */ 1)); }; } break; @@ -752,27 +719,76 @@ LoadGenerator::generateLoad(GeneratedLoadConfig cfg) if (sorobanCfg.nWasms != 0) { --sorobanCfg.nWasms; - return createUploadWasmTransaction( - ledgerNum, sourceAccountId, cfg); + + auto wasm = cfg.modeSetsUpInvoke() + ? rust_bridge::get_test_wasm_loadgen() + : rust_bridge::get_write_bytes(); + xdr::opaque_vec<> wasmBytes; + wasmBytes.assign(wasm.data.begin(), wasm.data.end()); + + LedgerKey contractCodeLedgerKey; + contractCodeLedgerKey.type(CONTRACT_CODE); + contractCodeLedgerKey.contractCode().hash = + sha256(wasmBytes); + + releaseAssert(!mCodeKey); + mCodeKey = contractCodeLedgerKey; + + // Wasm blob + approximate overhead for contract + // instance and ContractCode LE overhead + mContactOverheadBytes = wasmBytes.size() + 160; + + return mTxGenerator.createUploadWasmTransaction( + ledgerNum, sourceAccountId, wasmBytes, *mCodeKey, + cfg.maxGeneratedFeeRate); } else { --sorobanCfg.nInstances; - return createContractTransaction(ledgerNum, - sourceAccountId, cfg); + + auto salt = + sha256("upgrade" + + std::to_string( + ++mNumCreateContractTransactionCalls)); + + auto txPair = mTxGenerator.createContractTransaction( + ledgerNum, sourceAccountId, *mCodeKey, + mContactOverheadBytes, salt, + cfg.maxGeneratedFeeRate); + + auto const& instanceLk = + txPair.second->sorobanResources() + .footprint.readWrite.back(); + + mContractInstanceKeys.emplace(instanceLk); + + return txPair; } }; break; case LoadGenMode::SOROBAN_INVOKE: generateTx = [&]() { - return invokeSorobanLoadTransaction(ledgerNum, - sourceAccountId, cfg); + auto instanceIter = + mContractInstances.find(sourceAccountId); + releaseAssert(instanceIter != mContractInstances.end()); + auto const& instance = instanceIter->second; + return mTxGenerator.invokeSorobanLoadTransaction( + ledgerNum, sourceAccountId, instance, + mContactOverheadBytes, cfg.maxGeneratedFeeRate); }; break; case LoadGenMode::SOROBAN_CREATE_UPGRADE: generateTx = [&]() { - return invokeSorobanCreateUpgradeTransaction( - ledgerNum, sourceAccountId, cfg); + releaseAssert(mCodeKey); + releaseAssert(mContractInstanceKeys.size() == 1); + + auto upgradeBytes = + mTxGenerator.getConfigUpgradeSetFromLoadConfig( + cfg.getSorobanUpgradeConfig()); + return mTxGenerator.invokeSorobanCreateUpgradeTransaction( + ledgerNum, sourceAccountId, upgradeBytes, *mCodeKey, + *mContractInstanceKeys.begin(), + cfg.maxGeneratedFeeRate); }; break; case LoadGenMode::MIXED_CLASSIC_SOROBAN: @@ -820,8 +836,8 @@ LoadGenerator::submitCreationTx(uint32_t nAccounts, uint32_t offset, { uint32_t numToProcess = nAccounts < MAX_OPS_PER_TX ? nAccounts : MAX_OPS_PER_TX; - auto [from, tx] = - creationTransaction(mAccounts.size() + offset, numToProcess, ledgerNum); + auto [from, tx] = creationTransaction( + mTxGenerator.getAccounts().size() + offset, numToProcess, ledgerNum); TransactionResultCode code; TransactionQueue::AddResultCode status; bool createDuplicate = false; @@ -859,7 +875,7 @@ LoadGenerator::submitCreationTx(uint32_t nAccounts, uint32_t offset, bool LoadGenerator::submitTx(GeneratedLoadConfig const& cfg, - std::function()> generateTx) { @@ -941,7 +957,7 @@ LoadGenerator::getNextAvailableAccount(uint32_t ledgerNum) // later. To resolve this, we resample a new account by simply looping // here. } while (mApp.getHerder().sourceAccountPending( - findAccount(sourceAccountId, ledgerNum)->getPublicKey())); + mTxGenerator.findAccount(sourceAccountId, ledgerNum)->getPublicKey())); return sourceAccountId; } @@ -1010,803 +1026,32 @@ LoadGenerator::logProgress(std::chrono::nanoseconds submitTimer, mTxMetrics.report(); } -std::pair +std::pair LoadGenerator::creationTransaction(uint64_t startAccount, uint64_t numItems, uint32_t ledgerNum) { - TestAccountPtr sourceAcc = + TxGenerator::TestAccountPtr sourceAcc = mInitialAccountsCreated - ? findAccount(getNextAvailableAccount(ledgerNum), ledgerNum) + ? mTxGenerator.findAccount(getNextAvailableAccount(ledgerNum), + ledgerNum) : mRoot; - vector creationOps = createAccounts( + vector creationOps = mTxGenerator.createAccounts( startAccount, numItems, ledgerNum, !mInitialAccountsCreated); - mInitialAccountsCreated = true; - return std::make_pair( - sourceAcc, createTransactionFrame(sourceAcc, creationOps, - LoadGenMode::CREATE, std::nullopt)); -} - -void -LoadGenerator::updateMinBalance() -{ - auto b = mApp.getLedgerManager().getLastMinBalance(0); - if (b > mMinBalance) - { - mMinBalance = b; - } -} - -std::vector -LoadGenerator::createAccounts(uint64_t start, uint64_t count, - uint32_t ledgerNum, bool initialAccounts) -{ - vector ops; - SequenceNumber sn = static_cast(ledgerNum) << 32; - auto balance = initialAccounts ? mMinBalance * 10000000 : mMinBalance * 100; - for (uint64_t i = start; i < start + count; i++) - { - auto name = "TestAccount-" + to_string(i); - auto account = TestAccount{mApp, txtest::getAccount(name.c_str()), sn}; - ops.push_back(txtest::createAccount(account.getPublicKey(), balance)); - - // Cache newly created account - auto acc = make_shared(account); - mAccounts.emplace(i, acc); - if (initialAccounts) - { - mCreationSourceAccounts.emplace(i, acc); - } - } - return ops; -} - -bool -LoadGenerator::loadAccount(TestAccount& account, Application& app) -{ - LedgerSnapshot lsg(mApp); - auto const entry = lsg.getAccount(account.getPublicKey()); - if (!entry) - { - return false; - } - account.setSequenceNumber(entry.current().data.account().seqNum); - return true; -} - -bool -LoadGenerator::loadAccount(TestAccountPtr acc, Application& app) -{ - if (acc) - { - return loadAccount(*acc, app); - } - return false; -} - -std::pair -LoadGenerator::pickAccountPair(uint32_t numAccounts, uint32_t offset, - uint32_t ledgerNum, uint64_t sourceAccountId) -{ - auto sourceAccount = findAccount(sourceAccountId, ledgerNum); - releaseAssert( - !mApp.getHerder().sourceAccountPending(sourceAccount->getPublicKey())); - - auto destAccountId = rand_uniform(0, numAccounts - 1) + offset; - - auto destAccount = findAccount(destAccountId, ledgerNum); - - CLOG_DEBUG(LoadGen, "Generated pair for payment tx - {} and {}", - sourceAccountId, destAccountId); - return std::pair(sourceAccount, - destAccount); -} - -LoadGenerator::TestAccountPtr -LoadGenerator::findAccount(uint64_t accountId, uint32_t ledgerNum) -{ - // Load account and cache it. - TestAccountPtr newAccountPtr; - - auto res = mAccounts.find(accountId); - if (res == mAccounts.end()) - { - SequenceNumber sn = static_cast(ledgerNum) << 32; - auto name = "TestAccount-" + std::to_string(accountId); - newAccountPtr = - std::make_shared(mApp, txtest::getAccount(name), sn); - - if (!loadAccount(newAccountPtr, mApp)) - { - throw std::runtime_error( - fmt::format("Account {0} must exist in the DB.", accountId)); - } - mAccounts.insert( - std::pair(accountId, newAccountPtr)); - } - else - { - newAccountPtr = res->second; - } - - return newAccountPtr; -} - -std::pair -LoadGenerator::paymentTransaction(uint32_t numAccounts, uint32_t offset, - uint32_t ledgerNum, uint64_t sourceAccount, - uint32_t opCount, - std::optional maxGeneratedFeeRate) -{ - TestAccountPtr to, from; - uint64_t amount = 1; - std::tie(from, to) = - pickAccountPair(numAccounts, offset, ledgerNum, sourceAccount); - vector paymentOps; - paymentOps.reserve(opCount); - for (uint32_t i = 0; i < opCount; ++i) - { - paymentOps.emplace_back(txtest::payment(to->getPublicKey(), amount)); - } - - return std::make_pair(from, createTransactionFrame(from, paymentOps, - LoadGenMode::PAY, - maxGeneratedFeeRate)); -} - -std::pair -LoadGenerator::manageOfferTransaction( - uint32_t ledgerNum, uint64_t accountId, uint32_t opCount, - std::optional maxGeneratedFeeRate) -{ - auto account = findAccount(accountId, ledgerNum); - Asset selling(ASSET_TYPE_NATIVE); - Asset buying(ASSET_TYPE_CREDIT_ALPHANUM4); - strToAssetCode(buying.alphaNum4().assetCode, "USD"); - vector ops; - for (uint32_t i = 0; i < opCount; ++i) - { - ops.emplace_back(txtest::manageBuyOffer( - rand_uniform(1, 10000000), selling, buying, - Price{rand_uniform(1, 100), rand_uniform(1, 100)}, - 100)); - } - return std::make_pair(account, createTransactionFrame( - account, ops, LoadGenMode::MIXED_CLASSIC, - maxGeneratedFeeRate)); -} - -static void -increaseOpSize(Operation& op, uint32_t increaseUpToBytes) -{ - if (increaseUpToBytes == 0) - { - return; - } - - SorobanAuthorizationEntry auth; - auth.credentials.type(SOROBAN_CREDENTIALS_SOURCE_ACCOUNT); - auth.rootInvocation.function.type( - SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN); - SCVal val(SCV_BYTES); - - auto const overheadBytes = xdr::xdr_size(auth) + xdr::xdr_size(val); - if (overheadBytes > increaseUpToBytes) - { - increaseUpToBytes = 0; - } - else - { - increaseUpToBytes -= overheadBytes; - } - - val.bytes().resize(increaseUpToBytes); - auth.rootInvocation.function.contractFn().args = {val}; - op.body.invokeHostFunctionOp().auth = {auth}; -} - -std::pair -LoadGenerator::createUploadWasmTransaction(uint32_t ledgerNum, - uint64_t accountId, - GeneratedLoadConfig const& cfg) -{ - releaseAssert(cfg.isSorobanSetup()); - releaseAssert(!mCodeKey); - auto wasm = cfg.modeSetsUpInvoke() ? rust_bridge::get_test_wasm_loadgen() - : rust_bridge::get_write_bytes(); - auto account = findAccount(accountId, ledgerNum); - - SorobanResources uploadResources{}; - uploadResources.instructions = 2'500'000; - uploadResources.readBytes = wasm.data.size() + 500; - uploadResources.writeBytes = wasm.data.size() + 500; - - Operation uploadOp; - uploadOp.body.type(INVOKE_HOST_FUNCTION); - auto& uploadHF = uploadOp.body.invokeHostFunctionOp().hostFunction; - uploadHF.type(HOST_FUNCTION_TYPE_UPLOAD_CONTRACT_WASM); - uploadHF.wasm().assign(wasm.data.begin(), wasm.data.end()); - - LedgerKey contractCodeLedgerKey; - contractCodeLedgerKey.type(CONTRACT_CODE); - contractCodeLedgerKey.contractCode().hash = sha256(uploadHF.wasm()); - uploadResources.footprint.readWrite = {contractCodeLedgerKey}; - - int64_t resourceFee = - sorobanResourceFee(mApp, uploadResources, 5000 + wasm.data.size(), 100); - resourceFee += 1'000'000; - auto tx = sorobanTransactionFrameFromOps( - mApp.getNetworkID(), *account, {uploadOp}, {}, uploadResources, - generateFee(cfg.maxGeneratedFeeRate, mApp, - /* opsCnt */ 1), - resourceFee); - mCodeKey = contractCodeLedgerKey; - - // Wasm blob + approximate overhead for contract instance and ContractCode - // LE overhead - mContactOverheadBytes = wasm.data.size() + 160; - return std::make_pair(account, tx); -} - -std::pair -LoadGenerator::createContractTransaction(uint32_t ledgerNum, uint64_t accountId, - GeneratedLoadConfig const& cfg) -{ - releaseAssert(mCodeKey); - - auto account = findAccount(accountId, ledgerNum); - SorobanResources createResources{}; - createResources.instructions = 1'000'000; - createResources.readBytes = mContactOverheadBytes; - createResources.writeBytes = 300; - - auto salt = sha256("upgrade" + - std::to_string(++mNumCreateContractTransactionCalls)); - auto contractIDPreimage = makeContractIDPreimage(*account, salt); - - auto tx = makeSorobanCreateContractTx( - mApp, *account, contractIDPreimage, - makeWasmExecutable(mCodeKey->contractCode().hash), createResources, - generateFee(cfg.maxGeneratedFeeRate, mApp, - /* opsCnt */ 1)); - - auto const& instanceLk = createResources.footprint.readWrite.back(); - mContractInstanceKeys.emplace(instanceLk); - - return std::make_pair(account, tx); -} - -std::pair -LoadGenerator::invokeSorobanLoadTransaction(uint32_t ledgerNum, - uint64_t accountId, - GeneratedLoadConfig const& cfg) -{ - auto const& appCfg = mApp.getConfig(); - - auto account = findAccount(accountId, ledgerNum); - auto instanceIter = mContractInstances.find(accountId); - releaseAssert(instanceIter != mContractInstances.end()); - auto const& instance = instanceIter->second; - - auto const& networkCfg = mApp.getLedgerManager().getSorobanNetworkConfig(); - - // Approximate instruction measurements from loadgen contract. While the - // guest and host cycle counts are exact, and we can predict the cost of the - // guest and host loops correctly, it is difficult to estimate the CPU cost - // of storage given that the number and size of keys is variable. - // baseInstructionCount is a rough estimate for storage cost, but might be - // too small if a given invocation writes many or large entries. This means - // some TXs will fail due to exceeding resource limitations. However these - // should fail at apply time, so will still generate siginificant load - uint64_t const baseInstructionCount = 3'000'000; - uint64_t const instructionsPerGuestCycle = 80; - uint64_t const instructionsPerHostCycle = 5030; - - // Pick random number of cycles between bounds - uint64_t targetInstructions = - sampleDiscrete(appCfg.LOADGEN_INSTRUCTIONS_FOR_TESTING, - appCfg.LOADGEN_INSTRUCTIONS_DISTRIBUTION_FOR_TESTING, - DEFAULT_INSTRUCTIONS); - - // Factor in instructions for storage - targetInstructions = baseInstructionCount >= targetInstructions - ? 0 - : targetInstructions - baseInstructionCount; - - // Randomly select a number of guest cycles - uint64_t guestCyclesMax = targetInstructions / instructionsPerGuestCycle; - uint64_t guestCycles = rand_uniform(0, guestCyclesMax); - - // Rest of instructions consumed by host cycles - targetInstructions -= guestCycles * instructionsPerGuestCycle; - uint64_t hostCycles = targetInstructions / instructionsPerHostCycle; - - SorobanResources resources; - resources.footprint.readOnly = instance.readOnlyKeys; - - auto numEntries = - sampleDiscrete(appCfg.LOADGEN_NUM_DATA_ENTRIES_FOR_TESTING, - appCfg.LOADGEN_NUM_DATA_ENTRIES_DISTRIBUTION_FOR_TESTING, - DEFAULT_NUM_DATA_ENTRIES); - for (uint32_t i = 0; i < numEntries; ++i) - { - auto lk = contractDataKey(instance.contractID, makeU32(i), - ContractDataDurability::PERSISTENT); - resources.footprint.readWrite.emplace_back(lk); - } - - std::vector const& ioKilobytesValues = - appCfg.LOADGEN_IO_KILOBYTES_FOR_TESTING; - auto totalWriteBytes = - sampleDiscrete(ioKilobytesValues, - appCfg.LOADGEN_IO_KILOBYTES_DISTRIBUTION_FOR_TESTING, - DEFAULT_IO_KILOBYTES) * - 1024; - - if (totalWriteBytes < mContactOverheadBytes) - { - totalWriteBytes = mContactOverheadBytes; - numEntries = 0; - } - - uint32_t kiloBytesPerEntry = 0; - if (numEntries > 0) - { - kiloBytesPerEntry = - (totalWriteBytes - mContactOverheadBytes) / numEntries / 1024; - - // If numEntries > 0, we can't write a 0 byte entry - if (kiloBytesPerEntry == 0) - { - kiloBytesPerEntry = 1; - } - } - - auto guestCyclesU64 = makeU64(guestCycles); - auto hostCyclesU64 = makeU64(hostCycles); - auto numEntriesU32 = makeU32(numEntries); - auto kiloBytesPerEntryU32 = makeU32(kiloBytesPerEntry); - - Operation op; - op.body.type(INVOKE_HOST_FUNCTION); - auto& ihf = op.body.invokeHostFunctionOp().hostFunction; - ihf.type(HOST_FUNCTION_TYPE_INVOKE_CONTRACT); - ihf.invokeContract().contractAddress = instance.contractID; - ihf.invokeContract().functionName = "do_work"; - ihf.invokeContract().args = {guestCyclesU64, hostCyclesU64, numEntriesU32, - kiloBytesPerEntryU32}; - - // baseInstructionCount is a very rough estimate and may be a significant - // underestimation based on the IO load used, so use max instructions - resources.instructions = networkCfg.mTxMaxInstructions; - - // We don't have a good way of knowing how many bytes we will need to read - // since the previous invocation writes a random number of bytes, so use - // upper bound - uint32_t const maxReadKilobytes = - ioKilobytesValues.empty() ? DEFAULT_IO_KILOBYTES - : *std::max_element(ioKilobytesValues.begin(), - ioKilobytesValues.end()); - resources.readBytes = maxReadKilobytes * 1024; - resources.writeBytes = totalWriteBytes; - - // Approximate TX size before padding and footprint, slightly over estimated - // by `baselineTxOverheadBytes` so we stay below limits, plus footprint size - uint32_t constexpr baselineTxOverheadBytes = 260; - uint32_t const txOverheadBytes = - baselineTxOverheadBytes + xdr::xdr_size(resources); - uint32_t desiredTxBytes = - sampleDiscrete(appCfg.LOADGEN_TX_SIZE_BYTES_FOR_TESTING, - appCfg.LOADGEN_TX_SIZE_BYTES_DISTRIBUTION_FOR_TESTING, - DEFAULT_TX_SIZE_BYTES); - auto paddingBytes = - txOverheadBytes > desiredTxBytes ? 0 : desiredTxBytes - txOverheadBytes; - increaseOpSize(op, paddingBytes); - - auto resourceFee = - sorobanResourceFee(mApp, resources, txOverheadBytes + paddingBytes, 40); - resourceFee += 1'000'000; - - auto tx = sorobanTransactionFrameFromOps( - mApp.getNetworkID(), *account, {op}, {}, resources, - generateFee(cfg.maxGeneratedFeeRate, mApp, - /* opsCnt */ 1), - resourceFee); - - return std::make_pair(account, tx); -} - -ConfigUpgradeSetKey -LoadGenerator::getConfigUpgradeSetKey(GeneratedLoadConfig const& cfg) const -{ - releaseAssertOrThrow(mCodeKey.has_value()); - releaseAssertOrThrow(mContractInstanceKeys.size() == 1); - auto const& instanceLK = *mContractInstanceKeys.begin(); - - SCBytes upgradeBytes = getConfigUpgradeSetFromLoadConfig(cfg); - auto upgradeHash = sha256(upgradeBytes); - - ConfigUpgradeSetKey upgradeSetKey; - upgradeSetKey.contentHash = upgradeHash; - upgradeSetKey.contractID = instanceLK.contractData().contract.contractId(); - return upgradeSetKey; -} - -SCBytes -LoadGenerator::getConfigUpgradeSetFromLoadConfig( - GeneratedLoadConfig const& cfg) const -{ - xdr::xvector updatedEntries; - auto const& upgradeCfg = cfg.getSorobanUpgradeConfig(); - - LedgerSnapshot lsg(mApp); - - for (uint32_t i = 0; - i < static_cast(CONFIG_SETTING_BUCKETLIST_SIZE_WINDOW); ++i) + if (!mInitialAccountsCreated) { - auto entry = lsg.load(configSettingKey(static_cast(i))) - .current(); - - auto& setting = entry.data.configSetting(); - switch (static_cast(i)) - { - case CONFIG_SETTING_CONTRACT_MAX_SIZE_BYTES: - if (upgradeCfg.maxContractSizeBytes > 0) - { - setting.contractMaxSizeBytes() = - upgradeCfg.maxContractSizeBytes; - } - break; - case CONFIG_SETTING_CONTRACT_COMPUTE_V0: - if (upgradeCfg.ledgerMaxInstructions > 0) - { - setting.contractCompute().ledgerMaxInstructions = - upgradeCfg.ledgerMaxInstructions; - } - - if (upgradeCfg.txMaxInstructions > 0) - { - setting.contractCompute().txMaxInstructions = - upgradeCfg.txMaxInstructions; - } - - if (upgradeCfg.txMemoryLimit > 0) - { - setting.contractCompute().txMemoryLimit = - upgradeCfg.txMemoryLimit; - } - break; - case CONFIG_SETTING_CONTRACT_LEDGER_COST_V0: - if (upgradeCfg.ledgerMaxReadLedgerEntries > 0) - { - setting.contractLedgerCost().ledgerMaxReadLedgerEntries = - upgradeCfg.ledgerMaxReadLedgerEntries; - } - - if (upgradeCfg.ledgerMaxReadBytes > 0) - { - setting.contractLedgerCost().ledgerMaxReadBytes = - upgradeCfg.ledgerMaxReadBytes; - } - - if (upgradeCfg.ledgerMaxWriteLedgerEntries > 0) - { - setting.contractLedgerCost().ledgerMaxWriteLedgerEntries = - upgradeCfg.ledgerMaxWriteLedgerEntries; - } - - if (upgradeCfg.ledgerMaxWriteBytes > 0) - { - setting.contractLedgerCost().ledgerMaxWriteBytes = - upgradeCfg.ledgerMaxWriteBytes; - } - - if (upgradeCfg.txMaxReadLedgerEntries > 0) - { - setting.contractLedgerCost().txMaxReadLedgerEntries = - upgradeCfg.txMaxReadLedgerEntries; - } - - if (upgradeCfg.txMaxReadBytes > 0) - { - setting.contractLedgerCost().txMaxReadBytes = - upgradeCfg.txMaxReadBytes; - } - - if (upgradeCfg.txMaxWriteLedgerEntries > 0) - { - setting.contractLedgerCost().txMaxWriteLedgerEntries = - upgradeCfg.txMaxWriteLedgerEntries; - } - - if (upgradeCfg.txMaxWriteBytes > 0) - { - setting.contractLedgerCost().txMaxWriteBytes = - upgradeCfg.txMaxWriteBytes; - } - break; - case CONFIG_SETTING_CONTRACT_HISTORICAL_DATA_V0: - break; - case CONFIG_SETTING_CONTRACT_EVENTS_V0: - if (upgradeCfg.txMaxContractEventsSizeBytes > 0) - { - setting.contractEvents().txMaxContractEventsSizeBytes = - upgradeCfg.txMaxContractEventsSizeBytes; - } - break; - case CONFIG_SETTING_CONTRACT_BANDWIDTH_V0: - if (upgradeCfg.ledgerMaxTransactionsSizeBytes > 0) - { - setting.contractBandwidth().ledgerMaxTxsSizeBytes = - upgradeCfg.ledgerMaxTransactionsSizeBytes; - } - - if (upgradeCfg.txMaxSizeBytes > 0) - { - setting.contractBandwidth().txMaxSizeBytes = - upgradeCfg.txMaxSizeBytes; - } - break; - case CONFIG_SETTING_CONTRACT_COST_PARAMS_CPU_INSTRUCTIONS: - case CONFIG_SETTING_CONTRACT_COST_PARAMS_MEMORY_BYTES: - break; - case CONFIG_SETTING_CONTRACT_DATA_KEY_SIZE_BYTES: - if (upgradeCfg.maxContractDataKeySizeBytes > 0) - { - setting.contractDataKeySizeBytes() = - upgradeCfg.maxContractDataKeySizeBytes; - } - break; - case CONFIG_SETTING_CONTRACT_DATA_ENTRY_SIZE_BYTES: - if (upgradeCfg.maxContractDataEntrySizeBytes > 0) - { - setting.contractDataEntrySizeBytes() = - upgradeCfg.maxContractDataEntrySizeBytes; - } - break; - case CONFIG_SETTING_STATE_ARCHIVAL: - { - auto& ses = setting.stateArchivalSettings(); - if (upgradeCfg.maxEntryTTL > 0) - { - ses.maxEntryTTL = upgradeCfg.maxEntryTTL; - } - - if (upgradeCfg.minTemporaryTTL > 0) - { - ses.minTemporaryTTL = upgradeCfg.minTemporaryTTL; - } - - if (upgradeCfg.minPersistentTTL > 0) - { - ses.minPersistentTTL = upgradeCfg.minPersistentTTL; - } - - if (upgradeCfg.persistentRentRateDenominator > 0) - { - ses.persistentRentRateDenominator = - upgradeCfg.persistentRentRateDenominator; - } - - if (upgradeCfg.tempRentRateDenominator > 0) - { - ses.tempRentRateDenominator = - upgradeCfg.tempRentRateDenominator; - } - - if (upgradeCfg.maxEntriesToArchive > 0) - { - ses.maxEntriesToArchive = upgradeCfg.maxEntriesToArchive; - } - - if (upgradeCfg.bucketListSizeWindowSampleSize > 0) - { - ses.bucketListSizeWindowSampleSize = - upgradeCfg.bucketListSizeWindowSampleSize; - } - - if (upgradeCfg.bucketListWindowSamplePeriod > 0) - { - ses.bucketListWindowSamplePeriod = - upgradeCfg.bucketListWindowSamplePeriod; - } - - if (upgradeCfg.evictionScanSize > 0) - { - ses.evictionScanSize = upgradeCfg.evictionScanSize; - } - - if (upgradeCfg.startingEvictionScanLevel > 0) - { - ses.startingEvictionScanLevel = - upgradeCfg.startingEvictionScanLevel; - } - } - break; - case CONFIG_SETTING_CONTRACT_EXECUTION_LANES: - if (upgradeCfg.ledgerMaxTxCount > 0) - { - setting.contractExecutionLanes().ledgerMaxTxCount = - upgradeCfg.ledgerMaxTxCount; - } - break; - default: - releaseAssert(false); - break; - } - - // These two definitely aren't changing, and including both will hit the - // contractDataEntrySizeBytes limit - if (entry.data.configSetting().configSettingID() != - CONFIG_SETTING_CONTRACT_COST_PARAMS_CPU_INSTRUCTIONS && - entry.data.configSetting().configSettingID() != - CONFIG_SETTING_CONTRACT_COST_PARAMS_MEMORY_BYTES) + auto const& initialAccounts = mTxGenerator.getAccounts(); + for (auto const& kvp : initialAccounts) { - updatedEntries.emplace_back(entry.data.configSetting()); + mCreationSourceAccounts.emplace(kvp.first, kvp.second); } } - - ConfigUpgradeSet upgradeSet; - upgradeSet.updatedEntry = updatedEntries; - - return xdr::xdr_to_opaque(upgradeSet); -} - -std::pair -LoadGenerator::invokeSorobanCreateUpgradeTransaction( - uint32_t ledgerNum, uint64_t accountId, GeneratedLoadConfig const& cfg) -{ - releaseAssert(mCodeKey); - releaseAssert(mContractInstanceKeys.size() == 1); - - auto account = findAccount(accountId, ledgerNum); - auto const& instanceLK = *mContractInstanceKeys.begin(); - auto const& contractID = instanceLK.contractData().contract; - - SCBytes upgradeBytes = getConfigUpgradeSetFromLoadConfig(cfg); - - LedgerKey upgradeLK(CONTRACT_DATA); - upgradeLK.contractData().durability = TEMPORARY; - upgradeLK.contractData().contract = contractID; - - SCVal upgradeHashBytes(SCV_BYTES); - auto upgradeHash = sha256(upgradeBytes); - upgradeHashBytes.bytes() = xdr::xdr_to_opaque(upgradeHash); - upgradeLK.contractData().key = upgradeHashBytes; - - ConfigUpgradeSetKey upgradeSetKey; - upgradeSetKey.contentHash = upgradeHash; - upgradeSetKey.contractID = contractID.contractId(); - - SorobanResources resources; - resources.footprint.readOnly = {instanceLK, *mCodeKey}; - resources.footprint.readWrite = {upgradeLK}; - resources.instructions = 2'500'000; - resources.readBytes = 3'100; - resources.writeBytes = 3'100; - - SCVal b(SCV_BYTES); - b.bytes() = upgradeBytes; - - Operation op; - op.body.type(INVOKE_HOST_FUNCTION); - auto& ihf = op.body.invokeHostFunctionOp().hostFunction; - ihf.type(HOST_FUNCTION_TYPE_INVOKE_CONTRACT); - ihf.invokeContract().contractAddress = contractID; - ihf.invokeContract().functionName = "write"; - ihf.invokeContract().args.emplace_back(b); - - auto resourceFee = sorobanResourceFee(mApp, resources, 1'000, 40); - resourceFee += 1'000'000; - - auto tx = sorobanTransactionFrameFromOps( - mApp.getNetworkID(), *account, {op}, {}, resources, - generateFee(cfg.maxGeneratedFeeRate, mApp, - /* opsCnt */ 1), - resourceFee); - - return std::make_pair(account, tx); -} - -std::pair -LoadGenerator::sorobanRandomWasmTransaction(uint32_t ledgerNum, - uint64_t accountId, - uint32_t inclusionFee) -{ - auto [resources, wasmSize] = sorobanRandomUploadResources(); - - auto account = findAccount(accountId, ledgerNum); - Operation uploadOp = createUploadWasmOperation(wasmSize); - LedgerKey contractCodeLedgerKey; - contractCodeLedgerKey.type(CONTRACT_CODE); - contractCodeLedgerKey.contractCode().hash = - sha256(uploadOp.body.invokeHostFunctionOp().hostFunction.wasm()); - resources.footprint.readWrite.push_back(contractCodeLedgerKey); - - int64_t resourceFee = sorobanResourceFee( - mApp, resources, 5000 + static_cast(wasmSize), 100); - // Roughly cover the rent fee. - resourceFee += 100000; - auto tx = sorobanTransactionFrameFromOps(mApp.getNetworkID(), *account, - {uploadOp}, {}, resources, - inclusionFee, resourceFee); - return std::make_pair(account, tx); -} - -std::pair -LoadGenerator::sorobanRandomUploadResources() -{ - auto const& cfg = mApp.getConfig(); - SorobanResources resources{}; - - // Sample a random Wasm size - uint32_t wasmSize = sampleDiscrete( - cfg.LOADGEN_WASM_BYTES_FOR_TESTING, - cfg.LOADGEN_WASM_BYTES_DISTRIBUTION_FOR_TESTING, DEFAULT_WASM_BYTES); - - // Estimate VM instantiation cost, with some additional buffer to increase - // the chance that this instruction count is sufficient. - ContractCostParamEntry const& vmInstantiationCosts = - mApp.getLedgerManager() - .getSorobanNetworkConfig() - .cpuCostParams()[VmInstantiation]; - // Amount to right shift `vmInstantiationCosts.linearTerm * wasmSize` by - uint32_t constexpr vmShiftTerm = 7; - // Additional buffer per byte to increase the chance that this instruction - // count is sufficient - uint32_t constexpr vmBufferPerByte = 100; - // Perform multiplication as int64_t to avoid overflow - uint32_t linearResult = static_cast( - (vmInstantiationCosts.linearTerm * static_cast(wasmSize)) >> - vmShiftTerm); - resources.instructions = vmInstantiationCosts.constTerm + linearResult + - (vmBufferPerByte * wasmSize); - - // Double instruction estimate because wasm parse price is charged twice (as - // of protocol 21). - resources.instructions *= 2; - - // Allocate enough write bytes to write the whole Wasm plus the 40 bytes of - // the key with some additional buffer to increase the chance that this - // write size is sufficient - uint32_t constexpr keyOverhead = 40; - uint32_t constexpr writeBuffer = 128; - resources.writeBytes = wasmSize + keyOverhead + writeBuffer; - - return {resources, wasmSize}; -} - -std::pair -LoadGenerator::pretendTransaction(uint32_t numAccounts, uint32_t offset, - uint32_t ledgerNum, uint64_t sourceAccount, - uint32_t opCount, - std::optional maxGeneratedFeeRate) -{ - vector ops; - ops.reserve(opCount); - auto acc = findAccount(sourceAccount, ledgerNum); - for (uint32 i = 0; i < opCount; i++) - { - auto args = SetOptionsArguments{}; - - // We make SetOptionsOps such that we end up - // with a n-op transaction that is exactly 100n + 240 bytes. - args.inflationDest = std::make_optional(acc->getPublicKey()); - args.homeDomain = std::make_optional(std::string(16, '*')); - if (i == 0) - { - // The first operation needs to be bigger to achieve - // 100n + 240 bytes. - args.homeDomain->append(std::string(8, '*')); - args.signer = std::make_optional(Signer{}); - } - ops.push_back(txtest::setOptions(args)); - } - return std::make_pair(acc, - createTransactionFrame(acc, ops, LoadGenMode::PRETEND, - maxGeneratedFeeRate)); + mInitialAccountsCreated = true; + return std::make_pair(sourceAcc, + mTxGenerator.createTransactionTestFramePtr( + sourceAcc, creationOps, false, std::nullopt)); } -std::pair +std::pair LoadGenerator::createMixedClassicSorobanTransaction( uint32_t ledgerNum, uint64_t sourceAccountId, GeneratedLoadConfig const& cfg) @@ -1821,23 +1066,28 @@ LoadGenerator::createMixedClassicSorobanTransaction( { // Create a payment transaction mLastMixedMode = LoadGenMode::PAY; - return paymentTransaction(cfg.nAccounts, cfg.offset, ledgerNum, - sourceAccountId, 1, cfg.maxGeneratedFeeRate); + return mTxGenerator.paymentTransaction(cfg.nAccounts, cfg.offset, + ledgerNum, sourceAccountId, 1, + cfg.maxGeneratedFeeRate); } case 1: { // Create a soroban upload transaction mLastMixedMode = LoadGenMode::SOROBAN_UPLOAD; - return sorobanRandomWasmTransaction(ledgerNum, sourceAccountId, - generateFee(cfg.maxGeneratedFeeRate, - mApp, - /* opsCnt */ 1)); + return mTxGenerator.sorobanRandomWasmTransaction( + ledgerNum, sourceAccountId, + mTxGenerator.generateFee(cfg.maxGeneratedFeeRate, /* opsCnt */ 1)); } case 2: { // Create a soroban invoke transaction mLastMixedMode = LoadGenMode::SOROBAN_INVOKE; - return invokeSorobanLoadTransaction(ledgerNum, sourceAccountId, cfg); + + auto instanceIter = mContractInstances.find(sourceAccountId); + releaseAssert(instanceIter != mContractInstances.end()); + return mTxGenerator.invokeSorobanLoadTransaction( + ledgerNum, sourceAccountId, instanceIter->second, + mContactOverheadBytes, cfg.maxGeneratedFeeRate); } default: releaseAssert(false); @@ -1846,7 +1096,7 @@ LoadGenerator::createMixedClassicSorobanTransaction( void LoadGenerator::maybeHandleFailedTx(TransactionFrameBaseConstPtr tx, - TestAccountPtr sourceAccount, + TxGenerator::TestAccountPtr sourceAccount, TransactionQueue::AddResultCode status, TransactionResultCode code) { @@ -1867,7 +1117,7 @@ LoadGenerator::maybeHandleFailedTx(TransactionFrameBaseConstPtr tx, sourceAccount->setSequenceNumber(*txQueueSeqNum); return; } - if (!loadAccount(sourceAccount, mApp)) + if (!mTxGenerator.loadAccount(sourceAccount)) { CLOG_ERROR(LoadGen, "Unable to reload account {}", sourceAccount->getAccountId()); @@ -1903,16 +1153,16 @@ LoadGenerator::checkSorobanStateSynced(Application& app, return result; } -std::vector +std::vector LoadGenerator::checkAccountSynced(Application& app, bool isCreate) { - std::vector result; - for (auto const& acc : mAccounts) + std::vector result; + for (auto const& acc : mTxGenerator.getAccounts()) { - TestAccountPtr account = acc.second; + TxGenerator::TestAccountPtr account = acc.second; auto accountFromDB = *account; - auto reloadRes = loadAccount(accountFromDB, app); + auto reloadRes = mTxGenerator.loadAccount(accountFromDB); // For account creation, reload accounts from the DB // For payments, ensure that the sequence number matches expected // seqnum. Timeout after 20 ledgers. @@ -1956,9 +1206,10 @@ LoadGenerator::checkMinimumSorobanSuccess(GeneratedLoadConfig const& cfg) return true; } - int64_t nTxns = - mApplySorobanSuccess.count() + mApplySorobanFailure.count() - - mPreLoadgenApplySorobanSuccess - mPreLoadgenApplySorobanFailure; + int64_t nTxns = mTxGenerator.GetApplySorobanSuccess().count() + + mTxGenerator.GetApplySorobanFailure().count() - + mPreLoadgenApplySorobanSuccess - + mPreLoadgenApplySorobanFailure; if (nTxns == 0) { @@ -1966,8 +1217,8 @@ LoadGenerator::checkMinimumSorobanSuccess(GeneratedLoadConfig const& cfg) return true; } - int64_t nSuccessful = - mApplySorobanSuccess.count() - mPreLoadgenApplySorobanSuccess; + int64_t nSuccessful = mTxGenerator.GetApplySorobanSuccess().count() - + mPreLoadgenApplySorobanSuccess; return (nSuccessful * 100) / nTxns >= cfg.getMinSorobanPercentSuccess(); } @@ -1978,7 +1229,7 @@ LoadGenerator::waitTillComplete(GeneratedLoadConfig cfg) { mLoadTimer = std::make_unique(mApp.getClock()); } - vector inconsistencies; + vector inconsistencies; inconsistencies = checkAccountSynced(mApp, cfg.isCreate()); auto sorobanInconsistencies = checkSorobanStateSynced(mApp, cfg); @@ -2121,30 +1372,6 @@ LoadGenerator::TxMetrics::report() mSorobanCreateUpgradeTxs.one_minute_rate()); } -TransactionFrameBaseConstPtr -LoadGenerator::createTransactionFrame( - TestAccountPtr from, std::vector ops, LoadGenMode mode, - std::optional maxGeneratedFeeRate) -{ - - auto txf = transactionFromOperations( - mApp, from->getSecretKey(), from->nextSequenceNumber(), ops, - generateFee(maxGeneratedFeeRate, mApp, ops.size())); - if (mode == LoadGenMode::PRETEND) - { - Memo memo(MEMO_TEXT); - memo.text() = std::string(28, ' '); - txbridge::setMemo(txf, memo); - - txbridge::setMinTime(txf, 0); - txbridge::setMaxTime(txf, UINT64_MAX); - } - - txbridge::getSignatures(txf).clear(); - txf->addSignature(from->getSecretKey()); - return txf; -} - TransactionQueue::AddResultCode LoadGenerator::execute(TransactionFrameBasePtr txf, LoadGenMode mode, TransactionResultCode& code) @@ -2355,14 +1582,14 @@ GeneratedLoadConfig::getSorobanConfig() const return sorobanConfig; } -GeneratedLoadConfig::SorobanUpgradeConfig& +SorobanUpgradeConfig& GeneratedLoadConfig::getMutSorobanUpgradeConfig() { releaseAssert(mode == LoadGenMode::SOROBAN_CREATE_UPGRADE); return sorobanUpgradeConfig; } -GeneratedLoadConfig::SorobanUpgradeConfig const& +SorobanUpgradeConfig const& GeneratedLoadConfig::getSorobanUpgradeConfig() const { releaseAssert(mode == LoadGenMode::SOROBAN_CREATE_UPGRADE); diff --git a/src/simulation/LoadGenerator.h b/src/simulation/LoadGenerator.h index a15bb41052..ae49899aa4 100644 --- a/src/simulation/LoadGenerator.h +++ b/src/simulation/LoadGenerator.h @@ -7,8 +7,10 @@ #include "crypto/SecretKey.h" #include "herder/Herder.h" #include "main/Application.h" +#include "simulation/TxGenerator.h" #include "test/TestAccount.h" #include "test/TxTests.h" +#include "util/NonCopyable.h" #include "xdr/Stellar-types.h" #include @@ -54,56 +56,11 @@ struct GeneratedLoadConfig struct SorobanConfig { uint32_t nInstances = 0; - // For now, this value is automatically set to one. A future update will // enable multiple Wasm entries uint32_t nWasms = 0; }; - // Config settings for SOROBAN_CREATE_UPGRADE - struct SorobanUpgradeConfig - { - // Network Upgrade Parameters - uint32_t maxContractSizeBytes{}; - uint32_t maxContractDataKeySizeBytes{}; - uint32_t maxContractDataEntrySizeBytes{}; - - // Compute settings for contracts (instructions and memory). - int64_t ledgerMaxInstructions{}; - int64_t txMaxInstructions{}; - uint32_t txMemoryLimit{}; - - // Ledger access settings for contracts. - uint32_t ledgerMaxReadLedgerEntries{}; - uint32_t ledgerMaxReadBytes{}; - uint32_t ledgerMaxWriteLedgerEntries{}; - uint32_t ledgerMaxWriteBytes{}; - uint32_t ledgerMaxTxCount{}; - uint32_t txMaxReadLedgerEntries{}; - uint32_t txMaxReadBytes{}; - uint32_t txMaxWriteLedgerEntries{}; - uint32_t txMaxWriteBytes{}; - - // Contract events settings. - uint32_t txMaxContractEventsSizeBytes{}; - - // Bandwidth related data settings for contracts - uint32_t ledgerMaxTransactionsSizeBytes{}; - uint32_t txMaxSizeBytes{}; - - // State Archival Settings - uint32_t maxEntryTTL{}; - uint32_t minTemporaryTTL{}; - uint32_t minPersistentTTL{}; - int64_t persistentRentRateDenominator{}; - int64_t tempRentRateDenominator{}; - uint32_t maxEntriesToArchive{}; - uint32_t bucketListSizeWindowSampleSize{}; - uint32_t bucketListWindowSamplePeriod{}; - uint32_t evictionScanSize{}; - uint32_t startingEvictionScanLevel{}; - }; - // Config settings for MIXED_CLASSIC_SOROBAN struct MixClassicSorobanConfig { @@ -197,7 +154,6 @@ struct GeneratedLoadConfig class LoadGenerator { public: - using TestAccountPtr = std::shared_ptr; LoadGenerator(Application& app); static LoadGenMode getMode(std::string const& mode); @@ -222,22 +178,16 @@ class LoadGenerator void generateLoad(GeneratedLoadConfig cfg); ConfigUpgradeSetKey - getConfigUpgradeSetKey(GeneratedLoadConfig const& cfg) const; + getConfigUpgradeSetKey(SorobanUpgradeConfig const& upgradeCfg, + Hash const& contractId) const; // Verify cached accounts are properly reflected in the database // return any accounts that are inconsistent. - std::vector checkAccountSynced(Application& app, - bool isCreate); + std::vector + checkAccountSynced(Application& app, bool isCreate); std::vector checkSorobanStateSynced(Application& app, GeneratedLoadConfig const& cfg); - struct ContractInstance - { - // [wasm, instance] - xdr::xvector readOnlyKeys; - SCAddress contractID; - }; - UnorderedSet const& getContractInstanceKeysForTesting() const { @@ -289,10 +239,6 @@ class LoadGenerator TransactionQueue::AddResultCode execute(TransactionFrameBasePtr txf, LoadGenMode mode, TransactionResultCode& code); - TransactionFrameBaseConstPtr - createTransactionFrame(TestAccountPtr from, std::vector ops, - LoadGenMode mode, - std::optional maxGeneratedFeeRate); static const uint32_t STEP_MSECS; static const uint32_t TX_SUBMIT_MAX_TRIES; @@ -300,18 +246,15 @@ class LoadGenerator static const uint32_t COMPLETION_TIMEOUT_WITHOUT_CHECKS; static const uint32_t MIN_UNIQUE_ACCOUNT_MULTIPLIER; + TxGenerator mTxGenerator; + Application& mApp; + std::unique_ptr mLoadTimer; - int64 mMinBalance; uint64_t mLastSecond; - Application& mApp; int64_t mTotalSubmitted; // Set when load generation actually begins std::unique_ptr mStartTime; - TestAccountPtr mRoot; - // Accounts cache - std::map mAccounts; - // Track account IDs that are currently being referenced by the transaction // queue (to avoid source account collisions during tx submission) std::unordered_set mAccountsInUse; @@ -320,33 +263,15 @@ class LoadGenerator // Get an account ID not currently in use. uint64_t getNextAvailableAccount(uint32_t ledgerNum); - // For account creation only: allocate a few accounts for creation purposes - // (with sufficient balance to create new accounts) to avoid source account - // collisions. - std::unordered_map mCreationSourceAccounts; - - medida::Meter& mLoadgenComplete; - medida::Meter& mLoadgenFail; - - // Counts of soroban transactions that succeeded or failed at apply time - medida::Counter const& mApplySorobanSuccess; - medida::Counter const& mApplySorobanFailure; - - // Counts of successful and failed soroban transactions prior to running - // loadgen - int64_t mPreLoadgenApplySorobanSuccess = 0; - int64_t mPreLoadgenApplySorobanFailure = 0; - // Number of times `createContractTransaction` has been called. Used to // ensure unique preimages for all `SOROBAN_UPGRADE_SETUP` runs. uint32_t mNumCreateContractTransactionCalls = 0; - bool mFailed{false}; - bool mStarted{false}; - bool mInitialAccountsCreated{false}; - - uint32_t mWaitTillCompleteForLedgers{0}; - uint32_t mSorobanWasmWaitTillLedgers{0}; + // For account creation only: allocate a few accounts for creation purposes + // (with sufficient balance to create new accounts) to avoid source account + // collisions. + std::unordered_map + mCreationSourceAccounts; medida::Timer& mStepTimer; medida::Meter& mStepMeter; @@ -364,67 +289,40 @@ class LoadGenerator // Maps account ID to it's contract instance, where each account has a // unique instance - UnorderedMap mContractInstances; + UnorderedMap mContractInstances; + + TxGenerator::TestAccountPtr mRoot; + + medida::Meter& mLoadgenComplete; + medida::Meter& mLoadgenFail; + + // Counts of successful and failed soroban transactions prior to running + // loadgen + int64_t mPreLoadgenApplySorobanSuccess = 0; + int64_t mPreLoadgenApplySorobanFailure = 0; + + bool mFailed{false}; + bool mStarted{false}; + bool mInitialAccountsCreated{false}; + + uint32_t mWaitTillCompleteForLedgers{0}; + uint32_t mSorobanWasmWaitTillLedgers{0}; // Mode used for last mixed transaction in MIX_CLASSIC_SOROBAN mode LoadGenMode mLastMixedMode; + void createRootAccount(); + void reset(); void resetSorobanState(); - void createRootAccount(); int64_t getTxPerStep(uint32_t txRate, std::chrono::seconds spikeInterval, uint32_t spikeSize); // Schedule a callback to generateLoad() STEP_MSECS milliseconds from now. void scheduleLoadGeneration(GeneratedLoadConfig cfg); - std::vector createAccounts(uint64_t i, uint64_t batchSize, - uint32_t ledgerNum, - bool initialAccounts); - bool loadAccount(TestAccount& account, Application& app); - bool loadAccount(TestAccountPtr account, Application& app); - - SCBytes - getConfigUpgradeSetFromLoadConfig(GeneratedLoadConfig const& cfg) const; - - std::pair - pickAccountPair(uint32_t numAccounts, uint32_t offset, uint32_t ledgerNum, - uint64_t sourceAccountId); - TestAccountPtr findAccount(uint64_t accountId, uint32_t ledgerNum); - - std::pair - paymentTransaction(uint32_t numAccounts, uint32_t offset, - uint32_t ledgerNum, uint64_t sourceAccount, - uint32_t opCount, - std::optional maxGeneratedFeeRate); - std::pair - pretendTransaction(uint32_t numAccounts, uint32_t offset, - uint32_t ledgerNum, uint64_t sourceAccount, - uint32_t opCount, - std::optional maxGeneratedFeeRate); - std::pair - manageOfferTransaction(uint32_t ledgerNum, uint64_t accountId, - uint32_t opCount, - std::optional maxGeneratedFeeRate); - std::pair - createUploadWasmTransaction(uint32_t ledgerNum, uint64_t accountId, - GeneratedLoadConfig const& cfg); - std::pair - createContractTransaction(uint32_t ledgerNum, uint64_t accountId, - GeneratedLoadConfig const& cfg); - std::pair - invokeSorobanLoadTransaction(uint32_t ledgerNum, uint64_t accountId, - GeneratedLoadConfig const& cfg); - std::pair - invokeSorobanCreateUpgradeTransaction(uint32_t ledgerNum, - uint64_t accountId, - GeneratedLoadConfig const& cfg); - std::pair - sorobanRandomWasmTransaction(uint32_t ledgerNum, uint64_t accountId, - uint32_t inclusionFee); - // Create a transaction in MIXED_CLASSIC_SOROBAN mode - std::pair + std::pair createMixedClassicSorobanTransaction(uint32_t ledgerNum, uint64_t sourceAccountId, GeneratedLoadConfig const& cfg); @@ -433,10 +331,10 @@ class LoadGenerator // wasm of that size as well as the size itself. std::pair sorobanRandomUploadResources(); void maybeHandleFailedTx(TransactionFrameBaseConstPtr tx, - TestAccountPtr sourceAccount, + TxGenerator::TestAccountPtr sourceAccount, TransactionQueue::AddResultCode status, TransactionResultCode code); - std::pair + std::pair creationTransaction(uint64_t startAccount, uint64_t numItems, uint32_t ledgerNum); void logProgress(std::chrono::nanoseconds submitTimer, @@ -445,14 +343,12 @@ class LoadGenerator uint32_t submitCreationTx(uint32_t nAccounts, uint32_t offset, uint32_t ledgerNum); bool submitTx(GeneratedLoadConfig const& cfg, - std::function()> generateTx); void waitTillComplete(GeneratedLoadConfig cfg); void waitTillCompleteWithoutChecks(); - void updateMinBalance(); - unsigned short chooseOpCount(Config const& cfg) const; void cleanupAccounts(); diff --git a/src/simulation/TxGenerator.cpp b/src/simulation/TxGenerator.cpp new file mode 100644 index 0000000000..c3b9b862c7 --- /dev/null +++ b/src/simulation/TxGenerator.cpp @@ -0,0 +1,886 @@ +#include "simulation/TxGenerator.h" +#include "herder/Herder.h" +#include "ledger/LedgerManager.h" +#include "transactions/TransactionBridge.h" +#include "transactions/test/SorobanTxTestUtils.h" +#include + +namespace stellar +{ + +using namespace std; +using namespace txtest; + +namespace +{ +// Default distribution settings, largely based on averages seen on testnet +constexpr uint32_t DEFAULT_WASM_BYTES = 35 * 1024; +constexpr uint32_t DEFAULT_NUM_DATA_ENTRIES = 2; +constexpr uint32_t DEFAULT_IO_KILOBYTES = 1; +constexpr uint32_t DEFAULT_TX_SIZE_BYTES = 256; +constexpr uint64_t DEFAULT_INSTRUCTIONS = 28'000'000; + +// Sample from a discrete distribution of `values` with weights `weights`. +// Returns `defaultValue` if `values` is empty. +template +T +sampleDiscrete(std::vector const& values, + std::vector const& weights, T defaultValue) +{ + if (values.empty()) + { + return defaultValue; + } + + std::discrete_distribution distribution(weights.begin(), + weights.end()); + return values.at(distribution(gRandomEngine)); +} +} // namespace + +TxGenerator::TxGenerator(Application& app) + : mApp(app) + , mMinBalance(0) + , mApplySorobanSuccess( + mApp.getMetrics().NewCounter({"ledger", "apply-soroban", "success"})) + , mApplySorobanFailure( + mApp.getMetrics().NewCounter({"ledger", "apply-soroban", "failure"})) +{ + updateMinBalance(); +} + +void +TxGenerator::updateMinBalance() +{ + auto b = mApp.getLedgerManager().getLastMinBalance(0); + if (b > mMinBalance) + { + mMinBalance = b; + } +} + +int +TxGenerator::generateFee(std::optional maxGeneratedFeeRate, + size_t opsCnt) +{ + int fee = 0; + auto baseFee = mApp.getLedgerManager().getLastTxFee(); + + if (maxGeneratedFeeRate) + { + auto feeRateDistr = + uniform_int_distribution(baseFee, *maxGeneratedFeeRate); + // Add a bit more fee to get non-integer fee rates, such that + // `floor(fee / opsCnt) == feeRate`, but + // `fee / opsCnt >= feeRate`. + // This is to create a bit more realistic fee structure: in reality not + // every transaction would necessarily have the `fee == ops_count * + // some_int`. This also would exercise more code paths/logic during the + // transaction comparisons. + auto fractionalFeeDistr = uniform_int_distribution( + 0, static_cast(opsCnt) - 1); + fee = static_cast(opsCnt) * feeRateDistr(gRandomEngine) + + fractionalFeeDistr(gRandomEngine); + } + else + { + fee = static_cast(opsCnt * baseFee); + } + + return fee; +} + +uint64_t +TxGenerator::bytesToRead(xdr::xvector const& keys) +{ + LedgerTxn ltx(mApp.getLedgerTxnRoot()); + uint64_t total = 0; + for (auto const& key : keys) + { + auto ltxe = ltx.loadWithoutRecord(key); + if (ltxe) + { + total += xdr::xdr_size(key); + total += xdr::xdr_size(ltxe.current()); + } + } + return total; +} + +bool +TxGenerator::loadAccount(TestAccount& account) +{ + LedgerTxn ltx(mApp.getLedgerTxnRoot()); + auto entry = stellar::loadAccount(ltx, account.getPublicKey()); + if (!entry) + { + return false; + } + account.setSequenceNumber(entry.current().data.account().seqNum); + return true; +} + +bool +TxGenerator::loadAccount(TxGenerator::TestAccountPtr acc) +{ + if (acc) + { + return loadAccount(*acc); + } + return false; +} + +std::pair +TxGenerator::pickAccountPair(uint32_t numAccounts, uint32_t offset, + uint32_t ledgerNum, uint64_t sourceAccountId) +{ + auto sourceAccount = findAccount(sourceAccountId, ledgerNum); + releaseAssert( + !mApp.getHerder().sourceAccountPending(sourceAccount->getPublicKey())); + + auto destAccountId = rand_uniform(0, numAccounts - 1) + offset; + + auto destAccount = findAccount(destAccountId, ledgerNum); + + CLOG_DEBUG(LoadGen, "Generated pair for payment tx - {} and {}", + sourceAccountId, destAccountId); + return std::pair( + sourceAccount, destAccount); +} + +TxGenerator::TestAccountPtr +TxGenerator::findAccount(uint64_t accountId, uint32_t ledgerNum) +{ + // Load account and cache it. + TxGenerator::TestAccountPtr newAccountPtr; + + auto res = mAccounts.find(accountId); + if (res == mAccounts.end()) + { + SequenceNumber sn = static_cast(ledgerNum) << 32; + auto name = "TestAccount-" + std::to_string(accountId); + newAccountPtr = + std::make_shared(mApp, txtest::getAccount(name), sn); + + if (!loadAccount(newAccountPtr)) + { + throw std::runtime_error( + fmt::format("Account {0} must exist in the DB.", accountId)); + } + mAccounts.insert(std::pair( + accountId, newAccountPtr)); + } + else + { + newAccountPtr = res->second; + } + + return newAccountPtr; +} + +std::vector +TxGenerator::createAccounts(uint64_t start, uint64_t count, uint32_t ledgerNum, + bool initialAccounts) +{ + vector ops; + SequenceNumber sn = static_cast(ledgerNum) << 32; + auto balance = initialAccounts ? mMinBalance * 10000000 : mMinBalance * 100; + for (uint64_t i = start; i < start + count; i++) + { + auto name = "TestAccount-" + to_string(i); + auto account = TestAccount{mApp, txtest::getAccount(name.c_str()), sn}; + ops.push_back(txtest::createAccount(account.getPublicKey(), balance)); + + // Cache newly created account + auto acc = make_shared(account); + mAccounts.emplace(i, acc); + } + return ops; +} + +TransactionTestFramePtr +TxGenerator::createTransactionTestFramePtr( + TxGenerator::TestAccountPtr from, std::vector ops, bool pretend, + std::optional maxGeneratedFeeRate) +{ + auto txf = transactionFromOperations( + mApp, from->getSecretKey(), from->nextSequenceNumber(), ops, + generateFee(maxGeneratedFeeRate, ops.size())); + if (pretend) + { + Memo memo(MEMO_TEXT); + memo.text() = std::string(28, ' '); + txbridge::setMemo(txf, memo); + + txbridge::setMinTime(txf, 0); + txbridge::setMaxTime(txf, UINT64_MAX); + } + + txbridge::getSignatures(txf).clear(); + txf->addSignature(from->getSecretKey()); + return txf; +} + +std::pair +TxGenerator::paymentTransaction(uint32_t numAccounts, uint32_t offset, + uint32_t ledgerNum, uint64_t sourceAccount, + uint32_t opCount, + std::optional maxGeneratedFeeRate) +{ + TxGenerator::TestAccountPtr to, from; + uint64_t amount = 1; + std::tie(from, to) = + pickAccountPair(numAccounts, offset, ledgerNum, sourceAccount); + vector paymentOps; + paymentOps.reserve(opCount); + for (uint32_t i = 0; i < opCount; ++i) + { + paymentOps.emplace_back(txtest::payment(to->getPublicKey(), amount)); + } + + return std::make_pair(from, + createTransactionTestFramePtr(from, paymentOps, false, + maxGeneratedFeeRate)); +} + +std::pair +TxGenerator::manageOfferTransaction(uint32_t ledgerNum, uint64_t accountId, + uint32_t opCount, + std::optional maxGeneratedFeeRate) +{ + auto account = findAccount(accountId, ledgerNum); + Asset selling(ASSET_TYPE_NATIVE); + Asset buying(ASSET_TYPE_CREDIT_ALPHANUM4); + strToAssetCode(buying.alphaNum4().assetCode, "USD"); + vector ops; + for (uint32_t i = 0; i < opCount; ++i) + { + ops.emplace_back(txtest::manageBuyOffer( + rand_uniform(1, 10000000), selling, buying, + Price{rand_uniform(1, 100), rand_uniform(1, 100)}, + 100)); + } + return std::make_pair(account, + createTransactionTestFramePtr(account, ops, false, + maxGeneratedFeeRate)); +} + +// TODO:: passing in wasm and maxGeneratedFeeRate? +std::pair +TxGenerator::createUploadWasmTransaction( + uint32_t ledgerNum, uint64_t accountId, xdr::opaque_vec<> const& wasm, + LedgerKey const& contractCodeLedgerKey, + std::optional maxGeneratedFeeRate) +{ + auto account = findAccount(accountId, ledgerNum); + + SorobanResources uploadResources{}; + uploadResources.instructions = 2'500'000; + uploadResources.readBytes = wasm.size() + 500; + uploadResources.writeBytes = wasm.size() + 500; + + Operation uploadOp; + uploadOp.body.type(INVOKE_HOST_FUNCTION); + auto& uploadHF = uploadOp.body.invokeHostFunctionOp().hostFunction; + uploadHF.type(HOST_FUNCTION_TYPE_UPLOAD_CONTRACT_WASM); + uploadHF.wasm() = wasm; + + uploadResources.footprint.readWrite = {contractCodeLedgerKey}; + + int64_t resourceFee = + sorobanResourceFee(mApp, uploadResources, 5000 + wasm.size(), 100); + resourceFee += 1'000'000; + auto tx = sorobanTransactionFrameFromOps(mApp.getNetworkID(), *account, + {uploadOp}, {}, uploadResources, + generateFee(maxGeneratedFeeRate, + /* opsCnt */ 1), + resourceFee); + + return std::make_pair(account, tx); +} + +std::pair +TxGenerator::createContractTransaction( + uint32_t ledgerNum, uint64_t accountId, LedgerKey const& codeKey, + uint64_t contractOverheadBytes, uint256 const& salt, + std::optional maxGeneratedFeeRate) +{ + auto account = findAccount(accountId, ledgerNum); + SorobanResources createResources{}; + createResources.instructions = 500'000; + createResources.readBytes = contractOverheadBytes; + createResources.writeBytes = 300; + + auto contractIDPreimage = makeContractIDPreimage(*account, salt); + + auto tx = makeSorobanCreateContractTx( + mApp, *account, contractIDPreimage, + makeWasmExecutable(codeKey.contractCode().hash), createResources, + generateFee(maxGeneratedFeeRate, + /* opsCnt */ 1)); + + return std::make_pair(account, tx); +} + +static void +increaseOpSize(Operation& op, uint32_t increaseUpToBytes) +{ + if (increaseUpToBytes == 0) + { + return; + } + + SorobanAuthorizationEntry auth; + auth.credentials.type(SOROBAN_CREDENTIALS_SOURCE_ACCOUNT); + auth.rootInvocation.function.type( + SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN); + SCVal val(SCV_BYTES); + + auto const overheadBytes = xdr::xdr_size(auth) + xdr::xdr_size(val); + if (overheadBytes > increaseUpToBytes) + { + increaseUpToBytes = 0; + } + else + { + increaseUpToBytes -= overheadBytes; + } + + val.bytes().resize(increaseUpToBytes); + auth.rootInvocation.function.contractFn().args = {val}; + op.body.invokeHostFunctionOp().auth = {auth}; +} + +std::pair +TxGenerator::invokeSorobanLoadTransaction( + uint32_t ledgerNum, uint64_t accountId, ContractInstance const& instance, + uint64_t contractOverheadBytes, std::optional maxGeneratedFeeRate) +{ + auto const& appCfg = mApp.getConfig(); + + auto account = findAccount(accountId, ledgerNum); + + auto const& networkCfg = mApp.getLedgerManager().getSorobanNetworkConfig(); + + // Approximate instruction measurements from loadgen contract. While the + // guest and host cycle counts are exact, and we can predict the cost of the + // guest and host loops correctly, it is difficult to estimate the CPU cost + // of storage given that the number and size of keys is variable. We have + // rough estimates for storage and padding (because it uses an auth + // payload). baseInstructionCount is for vm instantiation and additional + // cushion to make sure transactions will succeed, but this means that the + // instruction count is not perfect. Some TXs will fail due to exceeding + // resource limitations. However these should fail at apply time, so will + // still generate significant load + uint64_t const baseInstructionCount = 1'500'000; + uint64_t const instructionsPerGuestCycle = 80; + uint64_t const instructionsPerHostCycle = 5030; + + // Very rough estimates. + uint64_t const instructionsPerKbWritten = 8000; + + // instructionsPerPaddingByte is just a value we know works. We use an auth + // payload as padding, so it consumes instructions on the host side. + uint64_t const instructionsPerPaddingByte = 100; + + SorobanResources resources; + resources.footprint.readOnly = instance.readOnlyKeys; + + auto numEntries = + sampleDiscrete(appCfg.LOADGEN_NUM_DATA_ENTRIES_FOR_TESTING, + appCfg.LOADGEN_NUM_DATA_ENTRIES_DISTRIBUTION_FOR_TESTING, + DEFAULT_NUM_DATA_ENTRIES); + for (uint32_t i = 0; i < numEntries; ++i) + { + auto lk = contractDataKey(instance.contractID, makeU32(i), + ContractDataDurability::PERSISTENT); + resources.footprint.readWrite.emplace_back(lk); + } + + std::vector const& ioKilobytesValues = + appCfg.LOADGEN_IO_KILOBYTES_FOR_TESTING; + auto totalKbWriteBytes = sampleDiscrete( + ioKilobytesValues, appCfg.LOADGEN_IO_KILOBYTES_DISTRIBUTION_FOR_TESTING, + DEFAULT_IO_KILOBYTES); + + // Make sure write bytes is sufficient for number of entries written + if (totalKbWriteBytes < numEntries) + { + totalKbWriteBytes = numEntries; + } + + auto totalWriteBytes = totalKbWriteBytes * 1024; + + totalWriteBytes += numEntries * 100 /*Entry overhead*/; + + if (numEntries == 0) + { + totalWriteBytes = 0; + numEntries = 0; + } + + uint32_t kiloBytesPerEntry = 0; + + if (numEntries > 0) + { + kiloBytesPerEntry = + (totalWriteBytes - contractOverheadBytes) / numEntries / 1024; + + // If numEntries > 0, we can't write a 0 byte entry + if (kiloBytesPerEntry == 0) + { + kiloBytesPerEntry = 1; + } + } + + // Approximate TX size before padding and footprint, slightly over estimated + // by `baselineTxOverheadBytes` so we stay below limits, plus footprint size + uint32_t constexpr baselineTxOverheadBytes = 260; + uint32_t const txOverheadBytes = + baselineTxOverheadBytes + xdr::xdr_size(resources); + uint32_t desiredTxBytes = + sampleDiscrete(appCfg.LOADGEN_TX_SIZE_BYTES_FOR_TESTING, + appCfg.LOADGEN_TX_SIZE_BYTES_DISTRIBUTION_FOR_TESTING, + DEFAULT_TX_SIZE_BYTES); + auto paddingBytes = + txOverheadBytes > desiredTxBytes ? 0 : desiredTxBytes - txOverheadBytes; + + auto instructionsForStorageAndAuth = + ((numEntries + kiloBytesPerEntry) * instructionsPerKbWritten) + + instructionsPerPaddingByte * paddingBytes; + + // Pick random number of cycles between bounds + uint64_t targetInstructions = + sampleDiscrete(appCfg.LOADGEN_INSTRUCTIONS_FOR_TESTING, + appCfg.LOADGEN_INSTRUCTIONS_DISTRIBUTION_FOR_TESTING, + DEFAULT_INSTRUCTIONS); + + // Factor in instructions for storage + targetInstructions = baseInstructionCount + instructionsForStorageAndAuth >= + targetInstructions + ? 0 + : targetInstructions - baseInstructionCount - + instructionsForStorageAndAuth; + + // Randomly select a number of guest cycles + uint64_t guestCyclesMax = targetInstructions / instructionsPerGuestCycle; + uint64_t guestCycles = rand_uniform(0, guestCyclesMax); + + // Rest of instructions consumed by host cycles + targetInstructions -= guestCycles * instructionsPerGuestCycle; + uint64_t hostCycles = targetInstructions / instructionsPerHostCycle; + + auto guestCyclesU64 = makeU64(guestCycles); + auto hostCyclesU64 = makeU64(hostCycles); + auto numEntriesU32 = makeU32(numEntries); + auto kiloBytesPerEntryU32 = makeU32(kiloBytesPerEntry); + + Operation op; + op.body.type(INVOKE_HOST_FUNCTION); + auto& ihf = op.body.invokeHostFunctionOp().hostFunction; + ihf.type(HOST_FUNCTION_TYPE_INVOKE_CONTRACT); + ihf.invokeContract().contractAddress = instance.contractID; + ihf.invokeContract().functionName = "do_work"; + ihf.invokeContract().args = {guestCyclesU64, hostCyclesU64, numEntriesU32, + kiloBytesPerEntryU32}; + + resources.readBytes = bytesToRead(resources.footprint.readOnly) + + bytesToRead(resources.footprint.readWrite); + resources.writeBytes = totalWriteBytes; + + increaseOpSize(op, paddingBytes); + + int64_t instructionCount = + baseInstructionCount + hostCycles * instructionsPerHostCycle + + guestCycles * instructionsPerGuestCycle + instructionsForStorageAndAuth; + resources.instructions = instructionCount; + + auto resourceFee = + sorobanResourceFee(mApp, resources, txOverheadBytes + paddingBytes, 40); + resourceFee += 1'000'000; + + // A tx created using this method may be discarded when creating the txSet, + // so we need to refresh the TestAccount sequence number to avoid a + // txBAD_SEQ. + account->loadSequenceNumber(); + + auto tx = sorobanTransactionFrameFromOps(mApp.getNetworkID(), *account, + {op}, {}, resources, + generateFee(maxGeneratedFeeRate, + /* opsCnt */ 1), + resourceFee); + + return std::make_pair(account, tx); +} + +std::map const& +TxGenerator::getAccounts() +{ + return mAccounts; +} + +medida::Counter const& +TxGenerator::GetApplySorobanSuccess() +{ + return mApplySorobanSuccess; +} + +medida::Counter const& +TxGenerator::GetApplySorobanFailure() +{ + return mApplySorobanFailure; +} + +void +TxGenerator::reset() +{ + mAccounts.clear(); +} + +ConfigUpgradeSetKey +TxGenerator::getConfigUpgradeSetKey(SorobanUpgradeConfig const& upgradeCfg, + Hash const& contractId) const +{ + SCBytes upgradeBytes = getConfigUpgradeSetFromLoadConfig(upgradeCfg); + auto upgradeHash = sha256(upgradeBytes); + + ConfigUpgradeSetKey upgradeSetKey; + upgradeSetKey.contentHash = upgradeHash; + upgradeSetKey.contractID = contractId; + return upgradeSetKey; +} + +SCBytes +TxGenerator::getConfigUpgradeSetFromLoadConfig( + SorobanUpgradeConfig const& upgradeCfg) const +{ + xdr::xvector updatedEntries; + + LedgerTxn ltx(mApp.getLedgerTxnRoot()); + for (uint32_t i = 0; + i < static_cast(CONFIG_SETTING_BUCKETLIST_SIZE_WINDOW); ++i) + { + auto entry = + ltx.load(configSettingKey(static_cast(i))); + auto& setting = entry.current().data.configSetting(); + switch (static_cast(i)) + { + case CONFIG_SETTING_CONTRACT_MAX_SIZE_BYTES: + if (upgradeCfg.maxContractSizeBytes > 0) + { + setting.contractMaxSizeBytes() = + upgradeCfg.maxContractSizeBytes; + } + break; + case CONFIG_SETTING_CONTRACT_COMPUTE_V0: + if (upgradeCfg.ledgerMaxInstructions > 0) + { + setting.contractCompute().ledgerMaxInstructions = + upgradeCfg.ledgerMaxInstructions; + } + + if (upgradeCfg.txMaxInstructions > 0) + { + setting.contractCompute().txMaxInstructions = + upgradeCfg.txMaxInstructions; + } + + if (upgradeCfg.txMemoryLimit > 0) + { + setting.contractCompute().txMemoryLimit = + upgradeCfg.txMemoryLimit; + } + break; + case CONFIG_SETTING_CONTRACT_LEDGER_COST_V0: + if (upgradeCfg.ledgerMaxReadLedgerEntries > 0) + { + setting.contractLedgerCost().ledgerMaxReadLedgerEntries = + upgradeCfg.ledgerMaxReadLedgerEntries; + } + + if (upgradeCfg.ledgerMaxReadBytes > 0) + { + setting.contractLedgerCost().ledgerMaxReadBytes = + upgradeCfg.ledgerMaxReadBytes; + } + + if (upgradeCfg.ledgerMaxWriteLedgerEntries > 0) + { + setting.contractLedgerCost().ledgerMaxWriteLedgerEntries = + upgradeCfg.ledgerMaxWriteLedgerEntries; + } + + if (upgradeCfg.ledgerMaxWriteBytes > 0) + { + setting.contractLedgerCost().ledgerMaxWriteBytes = + upgradeCfg.ledgerMaxWriteBytes; + } + + if (upgradeCfg.txMaxReadLedgerEntries > 0) + { + setting.contractLedgerCost().txMaxReadLedgerEntries = + upgradeCfg.txMaxReadLedgerEntries; + } + + if (upgradeCfg.txMaxReadBytes > 0) + { + setting.contractLedgerCost().txMaxReadBytes = + upgradeCfg.txMaxReadBytes; + } + + if (upgradeCfg.txMaxWriteLedgerEntries > 0) + { + setting.contractLedgerCost().txMaxWriteLedgerEntries = + upgradeCfg.txMaxWriteLedgerEntries; + } + + if (upgradeCfg.txMaxWriteBytes > 0) + { + setting.contractLedgerCost().txMaxWriteBytes = + upgradeCfg.txMaxWriteBytes; + } + break; + case CONFIG_SETTING_CONTRACT_HISTORICAL_DATA_V0: + break; + case CONFIG_SETTING_CONTRACT_EVENTS_V0: + if (upgradeCfg.txMaxContractEventsSizeBytes > 0) + { + setting.contractEvents().txMaxContractEventsSizeBytes = + upgradeCfg.txMaxContractEventsSizeBytes; + } + break; + case CONFIG_SETTING_CONTRACT_BANDWIDTH_V0: + if (upgradeCfg.ledgerMaxTransactionsSizeBytes > 0) + { + setting.contractBandwidth().ledgerMaxTxsSizeBytes = + upgradeCfg.ledgerMaxTransactionsSizeBytes; + } + + if (upgradeCfg.txMaxSizeBytes > 0) + { + setting.contractBandwidth().txMaxSizeBytes = + upgradeCfg.txMaxSizeBytes; + } + break; + case CONFIG_SETTING_CONTRACT_COST_PARAMS_CPU_INSTRUCTIONS: + case CONFIG_SETTING_CONTRACT_COST_PARAMS_MEMORY_BYTES: + break; + case CONFIG_SETTING_CONTRACT_DATA_KEY_SIZE_BYTES: + if (upgradeCfg.maxContractDataKeySizeBytes > 0) + { + setting.contractDataKeySizeBytes() = + upgradeCfg.maxContractDataKeySizeBytes; + } + break; + case CONFIG_SETTING_CONTRACT_DATA_ENTRY_SIZE_BYTES: + if (upgradeCfg.maxContractDataEntrySizeBytes > 0) + { + setting.contractDataEntrySizeBytes() = + upgradeCfg.maxContractDataEntrySizeBytes; + } + break; + case CONFIG_SETTING_STATE_ARCHIVAL: + { + auto& ses = setting.stateArchivalSettings(); + if (upgradeCfg.bucketListSizeWindowSampleSize > 0) + { + ses.bucketListSizeWindowSampleSize = + upgradeCfg.bucketListSizeWindowSampleSize; + } + + if (upgradeCfg.evictionScanSize > 0) + { + ses.evictionScanSize = upgradeCfg.evictionScanSize; + } + + if (upgradeCfg.startingEvictionScanLevel > 0) + { + ses.startingEvictionScanLevel = + upgradeCfg.startingEvictionScanLevel; + } + } + break; + case CONFIG_SETTING_CONTRACT_EXECUTION_LANES: + if (upgradeCfg.ledgerMaxTxCount > 0) + { + setting.contractExecutionLanes().ledgerMaxTxCount = + upgradeCfg.ledgerMaxTxCount; + } + break; + default: + releaseAssert(false); + break; + } + + // These two definitely aren't changing, and including both will hit the + // contractDataEntrySizeBytes limit + if (entry.current().data.configSetting().configSettingID() != + CONFIG_SETTING_CONTRACT_COST_PARAMS_CPU_INSTRUCTIONS && + entry.current().data.configSetting().configSettingID() != + CONFIG_SETTING_CONTRACT_COST_PARAMS_MEMORY_BYTES) + { + updatedEntries.emplace_back(entry.current().data.configSetting()); + } + } + + ConfigUpgradeSet upgradeSet; + upgradeSet.updatedEntry = updatedEntries; + + return xdr::xdr_to_opaque(upgradeSet); +} + +std::pair +TxGenerator::invokeSorobanCreateUpgradeTransaction( + uint32_t ledgerNum, uint64_t accountId, SCBytes const& upgradeBytes, + LedgerKey const& codeKey, LedgerKey const& instanceKey, + std::optional maxGeneratedFeeRate) +{ + auto account = findAccount(accountId, ledgerNum); + auto const& contractID = instanceKey.contractData().contract; + + // TODO: Change this method to take SorobanUpgradeConfig as a parameter + // instead of upgradeBytes. SCBytes upgradeBytes = + // getConfigUpgradeSetFromLoadConfig(cfg); + + LedgerKey upgradeLK(CONTRACT_DATA); + upgradeLK.contractData().durability = TEMPORARY; + upgradeLK.contractData().contract = contractID; + + SCVal upgradeHashBytes(SCV_BYTES); + auto upgradeHash = sha256(upgradeBytes); + upgradeHashBytes.bytes() = xdr::xdr_to_opaque(upgradeHash); + upgradeLK.contractData().key = upgradeHashBytes; + + ConfigUpgradeSetKey upgradeSetKey; + upgradeSetKey.contentHash = upgradeHash; + upgradeSetKey.contractID = contractID.contractId(); + + SorobanResources resources; + resources.footprint.readOnly = {instanceKey, codeKey}; + resources.footprint.readWrite = {upgradeLK}; + resources.instructions = 2'500'000; + resources.readBytes = 3'100; + resources.writeBytes = 3'100; + + SCVal b(SCV_BYTES); + b.bytes() = upgradeBytes; + + Operation op; + op.body.type(INVOKE_HOST_FUNCTION); + auto& ihf = op.body.invokeHostFunctionOp().hostFunction; + ihf.type(HOST_FUNCTION_TYPE_INVOKE_CONTRACT); + ihf.invokeContract().contractAddress = contractID; + ihf.invokeContract().functionName = "write"; + ihf.invokeContract().args.emplace_back(b); + + auto resourceFee = sorobanResourceFee(mApp, resources, 1'000, 40); + resourceFee += 1'000'000; + + auto tx = sorobanTransactionFrameFromOps(mApp.getNetworkID(), *account, + {op}, {}, resources, + generateFee(maxGeneratedFeeRate, + /* opsCnt */ 1), + resourceFee); + + return std::make_pair(account, tx); +} + +std::pair +TxGenerator::sorobanRandomWasmTransaction(uint32_t ledgerNum, + uint64_t accountId, + uint32_t inclusionFee) +{ + auto [resources, wasmSize] = sorobanRandomUploadResources(); + + auto account = findAccount(accountId, ledgerNum); + Operation uploadOp = createUploadWasmOperation(wasmSize); + LedgerKey contractCodeLedgerKey; + contractCodeLedgerKey.type(CONTRACT_CODE); + contractCodeLedgerKey.contractCode().hash = + sha256(uploadOp.body.invokeHostFunctionOp().hostFunction.wasm()); + resources.footprint.readWrite.push_back(contractCodeLedgerKey); + + int64_t resourceFee = sorobanResourceFee( + mApp, resources, 5000 + static_cast(wasmSize), 100); + // Roughly cover the rent fee. + resourceFee += 100000; + auto tx = sorobanTransactionFrameFromOps(mApp.getNetworkID(), *account, + {uploadOp}, {}, resources, + inclusionFee, resourceFee); + return std::make_pair(account, tx); +} + +std::pair +TxGenerator::sorobanRandomUploadResources() +{ + auto const& cfg = mApp.getConfig(); + SorobanResources resources{}; + + // Sample a random Wasm size + uint32_t wasmSize = sampleDiscrete( + cfg.LOADGEN_WASM_BYTES_FOR_TESTING, + cfg.LOADGEN_WASM_BYTES_DISTRIBUTION_FOR_TESTING, DEFAULT_WASM_BYTES); + + // Estimate VM instantiation cost, with some additional buffer to increase + // the chance that this instruction count is sufficient. + ContractCostParamEntry const& vmInstantiationCosts = + mApp.getLedgerManager() + .getSorobanNetworkConfig() + .cpuCostParams()[VmInstantiation]; + // Amount to right shift `vmInstantiationCosts.linearTerm * wasmSize` by + uint32_t constexpr vmShiftTerm = 7; + // Additional buffer per byte to increase the chance that this instruction + // count is sufficient + uint32_t constexpr vmBufferPerByte = 100; + // Perform multiplication as int64_t to avoid overflow + uint32_t linearResult = static_cast( + (vmInstantiationCosts.linearTerm * static_cast(wasmSize)) >> + vmShiftTerm); + resources.instructions = vmInstantiationCosts.constTerm + linearResult + + (vmBufferPerByte * wasmSize); + + // Double instruction estimate because wasm parse price is charged twice (as + // of protocol 21). + resources.instructions *= 2; + + // Allocate enough write bytes to write the whole Wasm plus the 40 bytes of + // the key with some additional buffer to increase the chance that this + // write size is sufficient + uint32_t constexpr keyOverhead = 40; + uint32_t constexpr writeBuffer = 128; + resources.writeBytes = wasmSize + keyOverhead + writeBuffer; + + return {resources, wasmSize}; +} + +std::pair +TxGenerator::pretendTransaction(uint32_t numAccounts, uint32_t offset, + uint32_t ledgerNum, uint64_t sourceAccount, + uint32_t opCount, + std::optional maxGeneratedFeeRate) +{ + vector ops; + ops.reserve(opCount); + auto acc = findAccount(sourceAccount, ledgerNum); + for (uint32 i = 0; i < opCount; i++) + { + auto args = SetOptionsArguments{}; + + // We make SetOptionsOps such that we end up + // with a n-op transaction that is exactly 100n + 240 bytes. + args.inflationDest = std::make_optional(acc->getPublicKey()); + args.homeDomain = std::make_optional(std::string(16, '*')); + if (i == 0) + { + // The first operation needs to be bigger to achieve + // 100n + 240 bytes. + args.homeDomain->append(std::string(8, '*')); + args.signer = std::make_optional(Signer{}); + } + ops.push_back(txtest::setOptions(args)); + } + return std::make_pair(acc, createTransactionTestFramePtr( + acc, ops, true, maxGeneratedFeeRate)); +} + +} \ No newline at end of file diff --git a/src/simulation/TxGenerator.h b/src/simulation/TxGenerator.h new file mode 100644 index 0000000000..b6ba4643ae --- /dev/null +++ b/src/simulation/TxGenerator.h @@ -0,0 +1,167 @@ +#pragma once + +#include "main/Application.h" +#include "test/TestAccount.h" +#include "test/TxTests.h" + +namespace medida +{ +class Counter; +} +namespace stellar +{ +// Config settings for SOROBAN_CREATE_UPGRADE +struct SorobanUpgradeConfig +{ + // Network Upgrade Parameters + uint32_t maxContractSizeBytes{}; + uint32_t maxContractDataKeySizeBytes{}; + uint32_t maxContractDataEntrySizeBytes{}; + + // Compute settings for contracts (instructions and memory). + int64_t ledgerMaxInstructions{}; + int64_t txMaxInstructions{}; + uint32_t txMemoryLimit{}; + + // Ledger access settings for contracts. + uint32_t ledgerMaxReadLedgerEntries{}; + uint32_t ledgerMaxReadBytes{}; + uint32_t ledgerMaxWriteLedgerEntries{}; + uint32_t ledgerMaxWriteBytes{}; + uint32_t ledgerMaxTxCount{}; + uint32_t txMaxReadLedgerEntries{}; + uint32_t txMaxReadBytes{}; + uint32_t txMaxWriteLedgerEntries{}; + uint32_t txMaxWriteBytes{}; + + // Contract events settings. + uint32_t txMaxContractEventsSizeBytes{}; + + // Bandwidth related data settings for contracts + uint32_t ledgerMaxTransactionsSizeBytes{}; + uint32_t txMaxSizeBytes{}; + + // State Archival Settings + uint32_t maxEntryTTL{}; + uint32_t minTemporaryTTL{}; + uint32_t minPersistentTTL{}; + int64_t persistentRentRateDenominator{}; + int64_t tempRentRateDenominator{}; + uint32_t maxEntriesToArchive{}; + uint32_t bucketListSizeWindowSampleSize{}; + uint32_t bucketListWindowSamplePeriod{}; + uint32_t evictionScanSize{}; + uint32_t startingEvictionScanLevel{}; +}; + +class TxGenerator +{ + public: + struct ContractInstance + { + // [wasm, instance] + xdr::xvector readOnlyKeys; + SCAddress contractID; + }; + + using TestAccountPtr = std::shared_ptr; + TxGenerator(Application& app); + + bool loadAccount(TestAccount& account); + bool loadAccount(TestAccountPtr account); + + TestAccountPtr findAccount(uint64_t accountId, uint32_t ledgerNum); + + std::vector createAccounts(uint64_t start, uint64_t count, + uint32_t ledgerNum, + bool initialAccounts); + + TransactionTestFramePtr + createTransactionTestFramePtr(TestAccountPtr from, + std::vector ops, bool pretend, + std::optional maxGeneratedFeeRate); + + std::pair + paymentTransaction(uint32_t numAccounts, uint32_t offset, + uint32_t ledgerNum, uint64_t sourceAccount, + uint32_t opCount, + std::optional maxGeneratedFeeRate); + + std::pair + manageOfferTransaction(uint32_t ledgerNum, uint64_t accountId, + uint32_t opCount, + std::optional maxGeneratedFeeRate); + + std::pair + createUploadWasmTransaction(uint32_t ledgerNum, uint64_t accountId, + xdr::opaque_vec<> const& wasm, + LedgerKey const& contractCodeLedgerKey, + std::optional maxGeneratedFeeRate); + std::pair + createContractTransaction(uint32_t ledgerNum, uint64_t accountId, + LedgerKey const& codeKey, + uint64_t contractOverheadBytes, + uint256 const& salt, + std::optional maxGeneratedFeeRate); + + std::pair + invokeSorobanLoadTransaction(uint32_t ledgerNum, uint64_t accountId, + TxGenerator::ContractInstance const& instance, + uint64_t contractOverheadBytes, + std::optional maxGeneratedFeeRate); + std::pair + invokeSorobanCreateUpgradeTransaction( + uint32_t ledgerNum, uint64_t accountId, SCBytes const& upgradeBytes, + LedgerKey const& codeKey, LedgerKey const& instanceKey, + std::optional maxGeneratedFeeRate); + std::pair + sorobanRandomWasmTransaction(uint32_t ledgerNum, uint64_t accountId, + uint32_t inclusionFee); + + std::pair + pretendTransaction(uint32_t numAccounts, uint32_t offset, + uint32_t ledgerNum, uint64_t sourceAccount, + uint32_t opCount, + std::optional maxGeneratedFeeRate); + + int generateFee(std::optional maxGeneratedFeeRate, size_t opsCnt); + + std::pair + pickAccountPair(uint32_t numAccounts, uint32_t offset, uint32_t ledgerNum, + uint64_t sourceAccountId); + + ConfigUpgradeSetKey + getConfigUpgradeSetKey(SorobanUpgradeConfig const& upgradeCfg, + Hash const& contractId) const; + + SCBytes getConfigUpgradeSetFromLoadConfig( + SorobanUpgradeConfig const& upgradeCfg) const; + + std::map const& getAccounts(); + + medida::Counter const& GetApplySorobanSuccess(); + medida::Counter const& GetApplySorobanFailure(); + + void reset(); + + private: + std::pair sorobanRandomUploadResources(); + + // Calculates total size we'll need to read for all specified keys + uint64_t bytesToRead(xdr::xvector const& keys); + + void updateMinBalance(); + + Application& mApp; + + // Accounts cache + std::map mAccounts; + + int64 mMinBalance; + + // Counts of soroban transactions that succeeded or failed at apply time + medida::Counter const& mApplySorobanSuccess; + medida::Counter const& mApplySorobanFailure; +}; + +} \ No newline at end of file diff --git a/src/simulation/test/LoadGeneratorTests.cpp b/src/simulation/test/LoadGeneratorTests.cpp index 1d55ff9a53..b5d2c00cee 100644 --- a/src/simulation/test/LoadGeneratorTests.cpp +++ b/src/simulation/test/LoadGeneratorTests.cpp @@ -287,8 +287,13 @@ TEST_CASE("generate soroban load", "[loadgen][soroban]") rand_uniform(INT64_MAX - 10'000, INT64_MAX); upgradeCfg.startingEvictionScanLevel = rand_uniform(4, 8); - auto upgradeSetKey = - loadGen.getConfigUpgradeSetKey(createUpgradeLoadGenConfig); + auto testingKeys = + app.getLoadGenerator().getContractInstanceKeysForTesting(); + REQUIRE(testingKeys.size() == 1); + auto contractId = testingKeys.begin()->contractData().contract.contractId(); + + auto upgradeSetKey = loadGen.getConfigUpgradeSetKey( + createUpgradeLoadGenConfig.getSorobanUpgradeConfig(), contractId); numTxsBefore = getSuccessfulTxCount(); loadGen.generateLoad(createUpgradeLoadGenConfig); From 9e0b149de5544fcbb103865615739b043e68838d Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Mon, 12 Aug 2024 17:03:59 -0700 Subject: [PATCH 2/6] Add applyLoad command --- docs/stellar-core_example.cfg | 2 +- src/herder/HerderImpl.cpp | 1 - src/herder/HerderImpl.h | 2 + src/main/CommandLine.cpp | 135 +++++++++++++++++ src/simulation/ApplyLoad.cpp | 266 ++++++++++++++++++++++++++++++++++ src/simulation/ApplyLoad.h | 47 ++++++ src/util/TxResource.h | 23 +++ 7 files changed, 474 insertions(+), 2 deletions(-) create mode 100644 src/simulation/ApplyLoad.cpp create mode 100644 src/simulation/ApplyLoad.h diff --git a/docs/stellar-core_example.cfg b/docs/stellar-core_example.cfg index 9ff121a664..103c115cf4 100644 --- a/docs/stellar-core_example.cfg +++ b/docs/stellar-core_example.cfg @@ -494,7 +494,7 @@ MANUAL_CLOSE=false # ARTIFICIALLY_GENERATE_LOAD_FOR_TESTING (true or false) defaults to false # Enables synthetic load generation on demand. -# The load is triggered by the `generateload` runtime command. +# The load is triggered by the `generateload` runtime command or the `apply-load` command line command. # This option only exists for stress-testing and should not be enabled in # production networks. ARTIFICIALLY_GENERATE_LOAD_FOR_TESTING=false diff --git a/src/herder/HerderImpl.cpp b/src/herder/HerderImpl.cpp index 977270a712..51c1c039f0 100644 --- a/src/herder/HerderImpl.cpp +++ b/src/herder/HerderImpl.cpp @@ -60,7 +60,6 @@ constexpr uint32 const CLOSE_TIME_DRIFT_SECONDS_THRESHOLD = 10; constexpr uint32 const TRANSACTION_QUEUE_TIMEOUT_LEDGERS = 4; constexpr uint32 const TRANSACTION_QUEUE_BAN_LEDGERS = 10; -constexpr uint32 const TRANSACTION_QUEUE_SIZE_MULTIPLIER = 2; constexpr uint32 const SOROBAN_TRANSACTION_QUEUE_SIZE_MULTIPLIER = 2; std::unique_ptr diff --git a/src/herder/HerderImpl.h b/src/herder/HerderImpl.h index 7c54fd7eb2..eaea78200e 100644 --- a/src/herder/HerderImpl.h +++ b/src/herder/HerderImpl.h @@ -25,6 +25,8 @@ class Timer; namespace stellar { +constexpr uint32 const TRANSACTION_QUEUE_SIZE_MULTIPLIER = 2; + class Application; class LedgerManager; class HerderSCPDriver; diff --git a/src/main/CommandLine.cpp b/src/main/CommandLine.cpp index 5616201e62..170e8ec90c 100644 --- a/src/main/CommandLine.cpp +++ b/src/main/CommandLine.cpp @@ -27,9 +27,11 @@ #include "main/SettingsUpgradeUtils.h" #include "main/StellarCoreVersion.h" #include "main/dumpxdr.h" +#include "medida/metrics_registry.h" #include "overlay/OverlayManager.h" #include "rust/RustBridge.h" #include "scp/QuorumSetUtils.h" +#include "simulation/ApplyLoad.h" #include "transactions/TransactionUtils.h" #include "util/Logging.h" #include "util/types.h" @@ -1799,6 +1801,138 @@ runGenFuzz(CommandLineArgs const& args) return 0; }); } + +int +runApplyLoad(CommandLineArgs const& args) +{ + CommandLine::ConfigOption configOption; + + uint32_t numAccounts = 0; + + uint64_t ledgerMaxInstructions = 0; + uint64_t ledgerMaxReadLedgerEntries = 0; + uint64_t ledgerMaxReadBytes = 0; + uint64_t ledgerMaxWriteLedgerEntries = 0; + uint64_t ledgerMaxWriteBytes = 0; + uint64_t ledgerMaxTxCount = 0; + uint64_t ledgerMaxTransactionsSizeBytes = 0; + + ParserWithValidation numAccountsParser{ + clara::Arg(numAccounts, "NumAccounts").required(), + [&] { return numAccounts > 0 ? "" : "NumAccounts must be > 0"; }}; + + ParserWithValidation ledgerMaxInstructionsParser{ + clara::Opt(ledgerMaxInstructions, + "LedgerMaxInstructions")["--ledger-max-instructions"] + .required(), + [&] { + return ledgerMaxInstructions > 0 + ? "" + : "ledgerMaxInstructions must be > 0"; + }}; + + ParserWithValidation ledgerMaxReadLedgerEntriesParser{ + clara::Opt(ledgerMaxReadLedgerEntries, + "LedgerMaxReadLedgerEntries")["--ledger-max-read-entries"] + .required(), + [&] { + return ledgerMaxReadLedgerEntries > 0 + ? "" + : "ledgerMaxReadLedgerEntries must be > 0"; + }}; + + ParserWithValidation ledgerMaxReadBytesParser{ + clara::Opt(ledgerMaxReadBytes, + "LedgerMaxReadBytes")["--ledger-max-read-bytes"] + .required(), + [&] { + return ledgerMaxReadBytes > 0 ? "" + : "ledgerMaxReadBytes must be > 0"; + }}; + + ParserWithValidation ledgerMaxWriteLedgerEntriesParser{ + clara::Opt(ledgerMaxWriteLedgerEntries, + "LedgerMaxWriteLedgerEntries")["--ledger-max-write-entries"] + .required(), + [&] { + return ledgerMaxWriteLedgerEntries > 0 + ? "" + : "ledgerMaxWriteLedgerEntries must be > 0"; + }}; + + ParserWithValidation ledgerMaxWriteBytesParser{ + clara::Opt(ledgerMaxWriteBytes, + "LedgerMaxWriteBytes")["--ledger-max-write-bytes"] + .required(), + [&] { + return ledgerMaxWriteBytes > 0 ? "" + : "ledgerMaxWriteBytes must be > 0"; + }}; + + ParserWithValidation ledgerMaxTxCountParser{ + clara::Opt(ledgerMaxTxCount, + "LedgerMaxTxCount")["--ledger-max-tx-count"] + .required(), + [&] { + return ledgerMaxTxCount > 0 ? "" : "ledgerMaxTxCount must be > 0"; + }}; + + ParserWithValidation ledgerMaxTransactionsSizeBytesParser{ + clara::Opt(ledgerMaxTransactionsSizeBytes, + "LedgerMaxTransactionsSizeBytes")["--ledger-max-tx-size"] + .required(), + [&] { + return ledgerMaxTransactionsSizeBytes > 0 + ? "" + : "ledgerMaxTransactionsSizeBytes must be > 0"; + }}; + + return runWithHelp( + args, + {configurationParser(configOption), numAccountsParser, + ledgerMaxInstructionsParser, ledgerMaxReadLedgerEntriesParser, + ledgerMaxReadBytesParser, ledgerMaxWriteLedgerEntriesParser, + ledgerMaxWriteBytesParser, ledgerMaxTxCountParser, + ledgerMaxTransactionsSizeBytesParser}, + [&] { + auto config = configOption.getConfig(); + config.RUN_STANDALONE = true; + + VirtualClock clock(VirtualClock::REAL_TIME); + int result; + auto appPtr = Application::create(clock, config); + + auto& app = *appPtr; + { + auto& lm = app.getLedgerManager(); + app.start(); + + ApplyLoad al(app, numAccounts, ledgerMaxInstructions, + ledgerMaxReadLedgerEntries, ledgerMaxReadBytes, + ledgerMaxWriteLedgerEntries, ledgerMaxWriteBytes, + ledgerMaxTxCount, ledgerMaxTransactionsSizeBytes); + + auto& ledgerClose = + app.getMetrics().NewTimer({"ledger", "ledger", "close"}); + ledgerClose.Clear(); + + for (size_t i = 0; i < 20; ++i) + { + al.benchmark(); + } + + CLOG_INFO(Perf, "Max ledger close: {} milliseconds", + ledgerClose.max()); + CLOG_INFO(Perf, "Mean ledger close: {} milliseconds", + ledgerClose.mean()); + + CLOG_INFO(Perf, "Tx Success Rate: {:f}%", + al.successRate() * 100); + } + + return result; + }); +} #endif int @@ -1871,6 +2005,7 @@ handleCommandLine(int argc, char* const* argv) {"fuzz", "run a single fuzz input and exit", runFuzz}, {"gen-fuzz", "generate a random fuzzer input file", runGenFuzz}, {"test", "execute test suite", runTest}, + {"apply-load", "run apply time load test", runApplyLoad}, #endif {"version", "print version information", runVersion}}}; diff --git a/src/simulation/ApplyLoad.cpp b/src/simulation/ApplyLoad.cpp new file mode 100644 index 0000000000..9d82acff00 --- /dev/null +++ b/src/simulation/ApplyLoad.cpp @@ -0,0 +1,266 @@ +#include "simulation/ApplyLoad.h" +#include "herder/Herder.h" +#include "ledger/LedgerManager.h" +#include "test/TxTests.h" +#include "transactions/TransactionBridge.h" +#include "transactions/TransactionUtils.h" + +#include "herder/HerderImpl.h" + +#include "medida/meter.h" +#include "medida/metrics_registry.h" + +#include "util/XDRCereal.h" +#include + +namespace stellar +{ + +ApplyLoad::ApplyLoad(Application& app, uint32_t numAccounts, + uint64_t ledgerMaxInstructions, + uint64_t ledgerMaxReadLedgerEntries, + uint64_t ledgerMaxReadBytes, + uint64_t ledgerMaxWriteLedgerEntries, + uint64_t ledgerMaxWriteBytes, uint64_t ledgerMaxTxCount, + uint64_t ledgerMaxTransactionsSizeBytes) + : mTxGenerator(app), mApp(app), mNumAccounts(numAccounts) +{ + + auto rootTestAccount = TestAccount::createRoot(mApp); + mRoot = std::make_shared(rootTestAccount); + releaseAssert(mTxGenerator.loadAccount(mRoot)); + + mUpgradeConfig.maxContractSizeBytes = 65536; + mUpgradeConfig.maxContractDataKeySizeBytes = 250; + mUpgradeConfig.maxContractDataEntrySizeBytes = 65536; + mUpgradeConfig.ledgerMaxInstructions = ledgerMaxInstructions; + mUpgradeConfig.txMaxInstructions = 100000000; + mUpgradeConfig.txMemoryLimit = 41943040; + mUpgradeConfig.ledgerMaxReadLedgerEntries = ledgerMaxReadLedgerEntries; + mUpgradeConfig.ledgerMaxReadBytes = ledgerMaxReadBytes; + mUpgradeConfig.ledgerMaxWriteLedgerEntries = ledgerMaxWriteLedgerEntries; + mUpgradeConfig.ledgerMaxWriteBytes = ledgerMaxWriteBytes; + mUpgradeConfig.ledgerMaxTxCount = ledgerMaxTxCount; + mUpgradeConfig.txMaxReadLedgerEntries = 40; + mUpgradeConfig.txMaxReadBytes = 200000; + mUpgradeConfig.txMaxWriteLedgerEntries = 25; + mUpgradeConfig.txMaxWriteBytes = 66560; + mUpgradeConfig.txMaxContractEventsSizeBytes = 8198; + mUpgradeConfig.ledgerMaxTransactionsSizeBytes = + ledgerMaxTransactionsSizeBytes; + mUpgradeConfig.txMaxSizeBytes = 71680; + mUpgradeConfig.bucketListSizeWindowSampleSize = 30; + mUpgradeConfig.evictionScanSize = 100000; + mUpgradeConfig.startingEvictionScanLevel = 7; + + setupAccountsAndUpgradeProtocol(); + + setupUpgradeContract(); + + upgradeSettings(); + + setupLoadContracts(); + + // One contract per account + releaseAssert(mTxGenerator.GetApplySorobanSuccess().count() == + numAccounts + 4); + releaseAssert(mTxGenerator.GetApplySorobanFailure().count() == 0); +} + +void +ApplyLoad::closeLedger(std::vector const& txs, + xdr::xvector const& upgrades) +{ + auto txSet = makeTxSetFromTransactions(txs, mApp, 0, UINT64_MAX); + + auto sv = + mApp.getHerder().makeStellarValue(txSet.first->getContentsHash(), 1, + upgrades, mApp.getConfig().NODE_SEED); + + auto& lm = mApp.getLedgerManager(); + LedgerCloseData lcd(lm.getLastClosedLedgerNum() + 1, txSet.first, sv); + lm.closeLedger(lcd); +} + +void +ApplyLoad::setupAccountsAndUpgradeProtocol() +{ + auto const& lm = mApp.getLedgerManager(); + // pass in false for initialAccounts so we fund new account with a lower + // balance, allowing the creation of more accounts. + std::vector creationOps = mTxGenerator.createAccounts( + 0, mNumAccounts, lm.getLastClosedLedgerNum() + 1, false); + + auto initTx = mTxGenerator.createTransactionFramePtr(mRoot, creationOps, + false, std::nullopt); + + // Upgrade to latest protocol as well + auto upgrade = xdr::xvector{}; + auto ledgerUpgrade = LedgerUpgrade{LEDGER_UPGRADE_VERSION}; + ledgerUpgrade.newLedgerVersion() = Config::CURRENT_LEDGER_PROTOCOL_VERSION; + auto v = xdr::xdr_to_opaque(ledgerUpgrade); + upgrade.push_back(UpgradeType{v.begin(), v.end()}); + + closeLedger({initTx}, upgrade); +} + +void +ApplyLoad::setupUpgradeContract() +{ + auto wasm = rust_bridge::get_write_bytes(); + xdr::opaque_vec<> wasmBytes; + wasmBytes.assign(wasm.data.begin(), wasm.data.end()); + + LedgerKey contractCodeLedgerKey; + contractCodeLedgerKey.type(CONTRACT_CODE); + contractCodeLedgerKey.contractCode().hash = sha256(wasmBytes); + + mUpgradeCodeKey = contractCodeLedgerKey; + + auto const& lm = mApp.getLedgerManager(); + auto uploadTx = mTxGenerator.createUploadWasmTransaction( + lm.getLastClosedLedgerNum() + 1, 0, wasmBytes, contractCodeLedgerKey, + std::nullopt); + + closeLedger({uploadTx.second}); + + auto salt = sha256("upgrade contract salt preimage"); + + auto createTx = mTxGenerator.createContractTransaction( + lm.getLastClosedLedgerNum() + 1, 0, contractCodeLedgerKey, + wasmBytes.size() + 160, salt, std::nullopt); + closeLedger({createTx.second}); + + mUpgradeInstanceKey = + createTx.second->sorobanResources().footprint.readWrite.back(); +} + +// To upgrade settings, just modify mUpgradeConfig and then call +// upgradeSettings() +void +ApplyLoad::upgradeSettings() +{ + auto const& lm = mApp.getLedgerManager(); + auto upgradeBytes = + mTxGenerator.getConfigUpgradeSetFromLoadConfig(mUpgradeConfig); + + auto invokeTx = mTxGenerator.invokeSorobanCreateUpgradeTransaction( + lm.getLastClosedLedgerNum() + 1, 0, upgradeBytes, mUpgradeCodeKey, + mUpgradeInstanceKey, std::nullopt); + + auto upgradeSetKey = mTxGenerator.getConfigUpgradeSetKey( + mUpgradeConfig, + mUpgradeInstanceKey.contractData().contract.contractId()); + + auto upgrade = xdr::xvector{}; + auto ledgerUpgrade = LedgerUpgrade{LEDGER_UPGRADE_CONFIG}; + ledgerUpgrade.newConfig() = upgradeSetKey; + auto v = xdr::xdr_to_opaque(ledgerUpgrade); + upgrade.push_back(UpgradeType{v.begin(), v.end()}); + + closeLedger({invokeTx.second}, upgrade); +} + +void +ApplyLoad::setupLoadContracts() +{ + auto wasm = rust_bridge::get_test_wasm_loadgen(); + xdr::opaque_vec<> wasmBytes; + wasmBytes.assign(wasm.data.begin(), wasm.data.end()); + + LedgerKey contractCodeLedgerKey; + contractCodeLedgerKey.type(CONTRACT_CODE); + contractCodeLedgerKey.contractCode().hash = sha256(wasmBytes); + + mLoadCodeKey = contractCodeLedgerKey; + + auto const& lm = mApp.getLedgerManager(); + auto uploadTx = mTxGenerator.createUploadWasmTransaction( + lm.getLastClosedLedgerNum() + 1, 0, wasmBytes, contractCodeLedgerKey, + std::nullopt); + + closeLedger({uploadTx.second}); + + for (auto kvp : mTxGenerator.getAccounts()) + { + auto salt = sha256("Load contract " + std::to_string(kvp.first)); + + auto createTx = mTxGenerator.createContractTransaction( + lm.getLastClosedLedgerNum() + 1, 0, contractCodeLedgerKey, + wasmBytes.size() + 160, salt, std::nullopt); + closeLedger({createTx.second}); + + auto instanceKey = + createTx.second->sorobanResources().footprint.readWrite.back(); + + TxGenerator::ContractInstance instance; + instance.readOnlyKeys.emplace_back(mLoadCodeKey); + instance.readOnlyKeys.emplace_back(instanceKey); + instance.contractID = instanceKey.contractData().contract; + mLoadInstances.emplace(kvp.first, instance); + } +} + +void +ApplyLoad::benchmark() +{ + auto& lm = mApp.getLedgerManager(); + std::vector txs; + + auto resources = multiplyByDouble(lm.maxLedgerResources(true), + TRANSACTION_QUEUE_SIZE_MULTIPLIER); + + auto const& accounts = mTxGenerator.getAccounts(); + std::vector shuffledAccounts(accounts.size()); + std::iota(shuffledAccounts.begin(), shuffledAccounts.end(), 0); + stellar::shuffle(std::begin(shuffledAccounts), std::end(shuffledAccounts), + gRandomEngine); + + for (auto accountIndex : shuffledAccounts) + { + auto it = accounts.find(accountIndex); + releaseAssert(it != accounts.end()); + + auto instanceIter = mLoadInstances.find(it->first); + releaseAssert(instanceIter != mLoadInstances.end()); + auto const& instance = instanceIter->second; + auto tx = mTxGenerator.invokeSorobanLoadTransaction( + lm.getLastClosedLedgerNum() + 1, it->first, instance, + rust_bridge::get_write_bytes().data.size() + 160, std::nullopt); + + if (!anyGreater(tx.second->getResources(false), resources)) + { + resources -= tx.second->getResources(false); + } + else + { + for (size_t i = static_cast(Resource::Type::OPERATIONS); + i <= static_cast(Resource::Type::WRITE_LEDGER_ENTRIES); + ++i) + { + auto type = static_cast(i); + if (tx.second->getResources(false).getVal(type) > + resources.getVal(type)) + { + CLOG_INFO(Perf, "Ledger {} limit hit during tx generation", + Resource::getStringFromType(type)); + } + } + break; + } + + txs.emplace_back(tx.second); + } + + closeLedger(txs); +} + +double +ApplyLoad::successRate() +{ + return mTxGenerator.GetApplySorobanSuccess().count() * 1.0 / + (mTxGenerator.GetApplySorobanSuccess().count() + + mTxGenerator.GetApplySorobanFailure().count()); +} + +} \ No newline at end of file diff --git a/src/simulation/ApplyLoad.h b/src/simulation/ApplyLoad.h new file mode 100644 index 0000000000..e7e62880ac --- /dev/null +++ b/src/simulation/ApplyLoad.h @@ -0,0 +1,47 @@ +#include "main/Application.h" +#include "simulation/TxGenerator.h" +#include "test/TestAccount.h" + +namespace stellar +{ +class ApplyLoad +{ + public: + ApplyLoad(Application& app, uint32_t numAccounts, + uint64_t ledgerMaxInstructions, + uint64_t ledgerMaxReadLedgerEntries, uint64_t ledgerMaxReadBytes, + uint64_t ledgerMaxWriteLedgerEntries, + uint64_t ledgerMaxWriteBytes, uint64_t ledgerMaxTxCount, + uint64_t ledgerMaxTransactionsSizeBytes); + + void benchmark(); + + double successRate(); + + private: + void closeLedger(std::vector const& txs, + xdr::xvector const& upgrades = {}); + + void setupAccountsAndUpgradeProtocol(); + void setupUpgradeContract(); + void setupLoadContracts(); + + // Upgrades using mUpgradeConfig + void upgradeSettings(); + + LedgerKey mUpgradeCodeKey; + LedgerKey mUpgradeInstanceKey; + + LedgerKey mLoadCodeKey; + UnorderedMap mLoadInstances; + + SorobanUpgradeConfig mUpgradeConfig; + + TxGenerator mTxGenerator; + Application& mApp; + TxGenerator::TestAccountPtr mRoot; + + uint32_t mNumAccounts; +}; + +} \ No newline at end of file diff --git a/src/util/TxResource.h b/src/util/TxResource.h index 314161735d..9d7416cfdc 100644 --- a/src/util/TxResource.h +++ b/src/util/TxResource.h @@ -33,6 +33,29 @@ class Resource WRITE_LEDGER_ENTRIES = 6 }; + static std::string + getStringFromType(Type type) + { + switch (type) + { + case Type::OPERATIONS: + return "Operations"; + case Type::INSTRUCTIONS: + return "Instructions"; + case Type::TX_BYTE_SIZE: + return "TxByteSize"; + case Type::READ_BYTES: + return "ReadBytes"; + case Type::WRITE_BYTES: + return "WriteBytes"; + case Type::READ_LEDGER_ENTRIES: + return "ReadLedgerEntries"; + case Type::WRITE_LEDGER_ENTRIES: + return "WriteLedgerEntries"; + } + return "Unknown"; + } + Resource(std::vector args) { if (args.size() != NUM_CLASSIC_TX_RESOURCES && From cbbf78a671d222133017489bbc1fb4965511ec65 Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Wed, 11 Sep 2024 14:50:26 -0700 Subject: [PATCH 3/6] Review update --- src/herder/HerderImpl.cpp | 2 +- src/herder/HerderImpl.h | 2 +- src/main/CommandLine.cpp | 42 ++++++++----- src/simulation/ApplyLoad.cpp | 43 ++++++++----- src/simulation/ApplyLoad.h | 8 ++- src/simulation/LoadGenerator.cpp | 100 +++++++++++++++++-------------- src/simulation/LoadGenerator.h | 9 +++ src/simulation/TxGenerator.cpp | 16 ++--- src/simulation/TxGenerator.h | 4 +- 9 files changed, 134 insertions(+), 92 deletions(-) diff --git a/src/herder/HerderImpl.cpp b/src/herder/HerderImpl.cpp index 51c1c039f0..e3e1eeb601 100644 --- a/src/herder/HerderImpl.cpp +++ b/src/herder/HerderImpl.cpp @@ -60,7 +60,7 @@ constexpr uint32 const CLOSE_TIME_DRIFT_SECONDS_THRESHOLD = 10; constexpr uint32 const TRANSACTION_QUEUE_TIMEOUT_LEDGERS = 4; constexpr uint32 const TRANSACTION_QUEUE_BAN_LEDGERS = 10; -constexpr uint32 const SOROBAN_TRANSACTION_QUEUE_SIZE_MULTIPLIER = 2; +constexpr uint32 const TRANSACTION_QUEUE_SIZE_MULTIPLIER = 2; std::unique_ptr Herder::create(Application& app) diff --git a/src/herder/HerderImpl.h b/src/herder/HerderImpl.h index eaea78200e..be1d3d8e12 100644 --- a/src/herder/HerderImpl.h +++ b/src/herder/HerderImpl.h @@ -25,7 +25,7 @@ class Timer; namespace stellar { -constexpr uint32 const TRANSACTION_QUEUE_SIZE_MULTIPLIER = 2; +constexpr uint32 const SOROBAN_TRANSACTION_QUEUE_SIZE_MULTIPLIER = 2; class Application; class LedgerManager; diff --git a/src/main/CommandLine.cpp b/src/main/CommandLine.cpp index 170e8ec90c..c165139513 100644 --- a/src/main/CommandLine.cpp +++ b/src/main/CommandLine.cpp @@ -1807,8 +1807,6 @@ runApplyLoad(CommandLineArgs const& args) { CommandLine::ConfigOption configOption; - uint32_t numAccounts = 0; - uint64_t ledgerMaxInstructions = 0; uint64_t ledgerMaxReadLedgerEntries = 0; uint64_t ledgerMaxReadBytes = 0; @@ -1817,10 +1815,6 @@ runApplyLoad(CommandLineArgs const& args) uint64_t ledgerMaxTxCount = 0; uint64_t ledgerMaxTransactionsSizeBytes = 0; - ParserWithValidation numAccountsParser{ - clara::Arg(numAccounts, "NumAccounts").required(), - [&] { return numAccounts > 0 ? "" : "NumAccounts must be > 0"; }}; - ParserWithValidation ledgerMaxInstructionsParser{ clara::Opt(ledgerMaxInstructions, "LedgerMaxInstructions")["--ledger-max-instructions"] @@ -1889,17 +1883,15 @@ runApplyLoad(CommandLineArgs const& args) return runWithHelp( args, - {configurationParser(configOption), numAccountsParser, - ledgerMaxInstructionsParser, ledgerMaxReadLedgerEntriesParser, - ledgerMaxReadBytesParser, ledgerMaxWriteLedgerEntriesParser, - ledgerMaxWriteBytesParser, ledgerMaxTxCountParser, - ledgerMaxTransactionsSizeBytesParser}, + {configurationParser(configOption), ledgerMaxInstructionsParser, + ledgerMaxReadLedgerEntriesParser, ledgerMaxReadBytesParser, + ledgerMaxWriteLedgerEntriesParser, ledgerMaxWriteBytesParser, + ledgerMaxTxCountParser, ledgerMaxTransactionsSizeBytesParser}, [&] { auto config = configOption.getConfig(); config.RUN_STANDALONE = true; VirtualClock clock(VirtualClock::REAL_TIME); - int result; auto appPtr = Application::create(clock, config); auto& app = *appPtr; @@ -1907,7 +1899,7 @@ runApplyLoad(CommandLineArgs const& args) auto& lm = app.getLedgerManager(); app.start(); - ApplyLoad al(app, numAccounts, ledgerMaxInstructions, + ApplyLoad al(app, ledgerMaxInstructions, ledgerMaxReadLedgerEntries, ledgerMaxReadBytes, ledgerMaxWriteLedgerEntries, ledgerMaxWriteBytes, ledgerMaxTxCount, ledgerMaxTransactionsSizeBytes); @@ -1916,6 +1908,16 @@ runApplyLoad(CommandLineArgs const& args) app.getMetrics().NewTimer({"ledger", "ledger", "close"}); ledgerClose.Clear(); + auto& cpuInsRatio = app.getMetrics().NewHistogram( + {"soroban", "host-fn-op", + "invoke-time-fsecs-cpu-insn-ratio"}); + cpuInsRatio.Clear(); + + auto& cpuInsRatioExclVm = app.getMetrics().NewHistogram( + {"soroban", "host-fn-op", + "invoke-time-fsecs-cpu-insn-ratio-excl-vm"}); + cpuInsRatioExclVm.Clear(); + for (size_t i = 0; i < 20; ++i) { al.benchmark(); @@ -1923,14 +1925,26 @@ runApplyLoad(CommandLineArgs const& args) CLOG_INFO(Perf, "Max ledger close: {} milliseconds", ledgerClose.max()); + CLOG_INFO(Perf, "Min ledger close: {} milliseconds", + ledgerClose.min()); CLOG_INFO(Perf, "Mean ledger close: {} milliseconds", ledgerClose.mean()); + CLOG_INFO(Perf, "Max CPU ins ratio: {}", + cpuInsRatio.max() / 1000000); + CLOG_INFO(Perf, "Mean CPU ins ratio: {}", + cpuInsRatio.mean() / 1000000); + + CLOG_INFO(Perf, "Max CPU ins ratio excl VM: {}", + cpuInsRatioExclVm.max() / 1000000); + CLOG_INFO(Perf, "Mean CPU ins ratio excl VM: {}", + cpuInsRatioExclVm.mean() / 1000000); + CLOG_INFO(Perf, "Tx Success Rate: {:f}%", al.successRate() * 100); } - return result; + return 0; }); } #endif diff --git a/src/simulation/ApplyLoad.cpp b/src/simulation/ApplyLoad.cpp index 9d82acff00..e8cb6fc69a 100644 --- a/src/simulation/ApplyLoad.cpp +++ b/src/simulation/ApplyLoad.cpp @@ -2,6 +2,7 @@ #include "herder/Herder.h" #include "ledger/LedgerManager.h" #include "test/TxTests.h" +#include "transactions/MutableTransactionResult.h" #include "transactions/TransactionBridge.h" #include "transactions/TransactionUtils.h" @@ -16,14 +17,16 @@ namespace stellar { -ApplyLoad::ApplyLoad(Application& app, uint32_t numAccounts, - uint64_t ledgerMaxInstructions, +ApplyLoad::ApplyLoad(Application& app, uint64_t ledgerMaxInstructions, uint64_t ledgerMaxReadLedgerEntries, uint64_t ledgerMaxReadBytes, uint64_t ledgerMaxWriteLedgerEntries, uint64_t ledgerMaxWriteBytes, uint64_t ledgerMaxTxCount, uint64_t ledgerMaxTransactionsSizeBytes) - : mTxGenerator(app), mApp(app), mNumAccounts(numAccounts) + : mTxGenerator(app) + , mApp(app) + , mNumAccounts( + ledgerMaxTxCount * SOROBAN_TRANSACTION_QUEUE_SIZE_MULTIPLIER + 1) { auto rootTestAccount = TestAccount::createRoot(mApp); @@ -62,9 +65,9 @@ ApplyLoad::ApplyLoad(Application& app, uint32_t numAccounts, setupLoadContracts(); // One contract per account - releaseAssert(mTxGenerator.GetApplySorobanSuccess().count() == - numAccounts + 4); - releaseAssert(mTxGenerator.GetApplySorobanFailure().count() == 0); + releaseAssert(mTxGenerator.getApplySorobanSuccess().count() == + mNumAccounts + 4); + releaseAssert(mTxGenerator.getApplySorobanFailure().count() == 0); } void @@ -181,7 +184,7 @@ ApplyLoad::setupLoadContracts() closeLedger({uploadTx.second}); - for (auto kvp : mTxGenerator.getAccounts()) + for (auto const& kvp : mTxGenerator.getAccounts()) { auto salt = sha256("Load contract " + std::to_string(kvp.first)); @@ -207,8 +210,8 @@ ApplyLoad::benchmark() auto& lm = mApp.getLedgerManager(); std::vector txs; - auto resources = multiplyByDouble(lm.maxLedgerResources(true), - TRANSACTION_QUEUE_SIZE_MULTIPLIER); + auto resources = multiplyByDouble( + lm.maxLedgerResources(true), SOROBAN_TRANSACTION_QUEUE_SIZE_MULTIPLIER); auto const& accounts = mTxGenerator.getAccounts(); std::vector shuffledAccounts(accounts.size()); @@ -228,15 +231,20 @@ ApplyLoad::benchmark() lm.getLastClosedLedgerNum() + 1, it->first, instance, rust_bridge::get_write_bytes().data.size() + 160, std::nullopt); + { + LedgerTxn ltx(mApp.getLedgerTxnRoot()); + auto res = tx.second->checkValid(mApp, ltx, 0, 0, UINT64_MAX); + releaseAssert((res && res->isSuccess())); + } + if (!anyGreater(tx.second->getResources(false), resources)) { resources -= tx.second->getResources(false); } else { - for (size_t i = static_cast(Resource::Type::OPERATIONS); - i <= static_cast(Resource::Type::WRITE_LEDGER_ENTRIES); - ++i) + bool limitHit = false; + for (size_t i = 0; i < resources.size(); ++i) { auto type = static_cast(i); if (tx.second->getResources(false).getVal(type) > @@ -244,8 +252,13 @@ ApplyLoad::benchmark() { CLOG_INFO(Perf, "Ledger {} limit hit during tx generation", Resource::getStringFromType(type)); + limitHit = true; } } + + // If this assert fails, it most likely means that we ran out of + // accounts, which should not happen. + releaseAssert(limitHit); break; } @@ -258,9 +271,9 @@ ApplyLoad::benchmark() double ApplyLoad::successRate() { - return mTxGenerator.GetApplySorobanSuccess().count() * 1.0 / - (mTxGenerator.GetApplySorobanSuccess().count() + - mTxGenerator.GetApplySorobanFailure().count()); + return mTxGenerator.getApplySorobanSuccess().count() * 1.0 / + (mTxGenerator.getApplySorobanSuccess().count() + + mTxGenerator.getApplySorobanFailure().count()); } } \ No newline at end of file diff --git a/src/simulation/ApplyLoad.h b/src/simulation/ApplyLoad.h index e7e62880ac..a3254b3245 100644 --- a/src/simulation/ApplyLoad.h +++ b/src/simulation/ApplyLoad.h @@ -1,3 +1,8 @@ +// Copyright 2024 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#pragma once #include "main/Application.h" #include "simulation/TxGenerator.h" #include "test/TestAccount.h" @@ -7,8 +12,7 @@ namespace stellar class ApplyLoad { public: - ApplyLoad(Application& app, uint32_t numAccounts, - uint64_t ledgerMaxInstructions, + ApplyLoad(Application& app, uint64_t ledgerMaxInstructions, uint64_t ledgerMaxReadLedgerEntries, uint64_t ledgerMaxReadBytes, uint64_t ledgerMaxWriteLedgerEntries, uint64_t ledgerMaxWriteBytes, uint64_t ledgerMaxTxCount, diff --git a/src/simulation/LoadGenerator.cpp b/src/simulation/LoadGenerator.cpp index 38ede5358d..cd9e24cade 100644 --- a/src/simulation/LoadGenerator.cpp +++ b/src/simulation/LoadGenerator.cpp @@ -382,9 +382,9 @@ LoadGenerator::start(GeneratedLoadConfig& cfg) releaseAssert(mPreLoadgenApplySorobanSuccess == 0); releaseAssert(mPreLoadgenApplySorobanFailure == 0); mPreLoadgenApplySorobanSuccess = - mTxGenerator.GetApplySorobanSuccess().count(); + mTxGenerator.getApplySorobanSuccess().count(); mPreLoadgenApplySorobanFailure = - mTxGenerator.GetApplySorobanFailure().count(); + mTxGenerator.getApplySorobanFailure().count(); mStarted = true; } @@ -719,50 +719,14 @@ LoadGenerator::generateLoad(GeneratedLoadConfig cfg) if (sorobanCfg.nWasms != 0) { --sorobanCfg.nWasms; - - auto wasm = cfg.modeSetsUpInvoke() - ? rust_bridge::get_test_wasm_loadgen() - : rust_bridge::get_write_bytes(); - xdr::opaque_vec<> wasmBytes; - wasmBytes.assign(wasm.data.begin(), wasm.data.end()); - - LedgerKey contractCodeLedgerKey; - contractCodeLedgerKey.type(CONTRACT_CODE); - contractCodeLedgerKey.contractCode().hash = - sha256(wasmBytes); - - releaseAssert(!mCodeKey); - mCodeKey = contractCodeLedgerKey; - - // Wasm blob + approximate overhead for contract - // instance and ContractCode LE overhead - mContactOverheadBytes = wasmBytes.size() + 160; - - return mTxGenerator.createUploadWasmTransaction( - ledgerNum, sourceAccountId, wasmBytes, *mCodeKey, - cfg.maxGeneratedFeeRate); + return createUploadWasmTransaction(cfg, ledgerNum, + sourceAccountId); } else { --sorobanCfg.nInstances; - - auto salt = - sha256("upgrade" + - std::to_string( - ++mNumCreateContractTransactionCalls)); - - auto txPair = mTxGenerator.createContractTransaction( - ledgerNum, sourceAccountId, *mCodeKey, - mContactOverheadBytes, salt, - cfg.maxGeneratedFeeRate); - - auto const& instanceLk = - txPair.second->sorobanResources() - .footprint.readWrite.back(); - - mContractInstanceKeys.emplace(instanceLk); - - return txPair; + return createInstanceTransaction(cfg, ledgerNum, + sourceAccountId); } }; break; @@ -1094,6 +1058,52 @@ LoadGenerator::createMixedClassicSorobanTransaction( } } +std::pair +LoadGenerator::createUploadWasmTransaction(GeneratedLoadConfig const& cfg, + uint32_t ledgerNum, + uint64_t sourceAccountId) +{ + auto wasm = cfg.modeSetsUpInvoke() ? rust_bridge::get_test_wasm_loadgen() + : rust_bridge::get_write_bytes(); + xdr::opaque_vec<> wasmBytes; + wasmBytes.assign(wasm.data.begin(), wasm.data.end()); + + LedgerKey contractCodeLedgerKey; + contractCodeLedgerKey.type(CONTRACT_CODE); + contractCodeLedgerKey.contractCode().hash = sha256(wasmBytes); + + releaseAssert(!mCodeKey); + mCodeKey = contractCodeLedgerKey; + + // Wasm blob + approximate overhead for contract + // instance and ContractCode LE overhead + mContactOverheadBytes = wasmBytes.size() + 160; + + return mTxGenerator.createUploadWasmTransaction(ledgerNum, sourceAccountId, + wasmBytes, *mCodeKey, + cfg.maxGeneratedFeeRate); +} + +std::pair +LoadGenerator::createInstanceTransaction(GeneratedLoadConfig const& cfg, + uint32_t ledgerNum, + uint64_t sourceAccountId) +{ + auto salt = sha256("upgrade" + + std::to_string(++mNumCreateContractTransactionCalls)); + + auto txPair = mTxGenerator.createContractTransaction( + ledgerNum, sourceAccountId, *mCodeKey, mContactOverheadBytes, salt, + cfg.maxGeneratedFeeRate); + + auto const& instanceLk = + txPair.second->sorobanResources().footprint.readWrite.back(); + + mContractInstanceKeys.emplace(instanceLk); + + return txPair; +} + void LoadGenerator::maybeHandleFailedTx(TransactionFrameBaseConstPtr tx, TxGenerator::TestAccountPtr sourceAccount, @@ -1206,8 +1216,8 @@ LoadGenerator::checkMinimumSorobanSuccess(GeneratedLoadConfig const& cfg) return true; } - int64_t nTxns = mTxGenerator.GetApplySorobanSuccess().count() + - mTxGenerator.GetApplySorobanFailure().count() - + int64_t nTxns = mTxGenerator.getApplySorobanSuccess().count() + + mTxGenerator.getApplySorobanFailure().count() - mPreLoadgenApplySorobanSuccess - mPreLoadgenApplySorobanFailure; @@ -1217,7 +1227,7 @@ LoadGenerator::checkMinimumSorobanSuccess(GeneratedLoadConfig const& cfg) return true; } - int64_t nSuccessful = mTxGenerator.GetApplySorobanSuccess().count() - + int64_t nSuccessful = mTxGenerator.getApplySorobanSuccess().count() - mPreLoadgenApplySorobanSuccess; return (nSuccessful * 100) / nTxns >= cfg.getMinSorobanPercentSuccess(); } diff --git a/src/simulation/LoadGenerator.h b/src/simulation/LoadGenerator.h index ae49899aa4..85e651691f 100644 --- a/src/simulation/LoadGenerator.h +++ b/src/simulation/LoadGenerator.h @@ -326,6 +326,15 @@ class LoadGenerator createMixedClassicSorobanTransaction(uint32_t ledgerNum, uint64_t sourceAccountId, GeneratedLoadConfig const& cfg); + + std::pair + createUploadWasmTransaction(GeneratedLoadConfig const& cfg, + uint32_t ledgerNum, uint64_t sourceAccountId); + + std::pair + createInstanceTransaction(GeneratedLoadConfig const& cfg, + uint32_t ledgerNum, uint64_t sourceAccountId); + // Samples a random wasm size from the `LOADGEN_WASM_BYTES_FOR_TESTING` // distribution. Returns a pair containing the appropriate resources for a // wasm of that size as well as the size itself. diff --git a/src/simulation/TxGenerator.cpp b/src/simulation/TxGenerator.cpp index c3b9b862c7..e024a082b7 100644 --- a/src/simulation/TxGenerator.cpp +++ b/src/simulation/TxGenerator.cpp @@ -100,7 +100,6 @@ TxGenerator::bytesToRead(xdr::xvector const& keys) auto ltxe = ltx.loadWithoutRecord(key); if (ltxe) { - total += xdr::xdr_size(key); total += xdr::xdr_size(ltxe.current()); } } @@ -265,7 +264,6 @@ TxGenerator::manageOfferTransaction(uint32_t ledgerNum, uint64_t accountId, maxGeneratedFeeRate)); } -// TODO:: passing in wasm and maxGeneratedFeeRate? std::pair TxGenerator::createUploadWasmTransaction( uint32_t ledgerNum, uint64_t accountId, xdr::opaque_vec<> const& wasm, @@ -360,8 +358,6 @@ TxGenerator::invokeSorobanLoadTransaction( auto account = findAccount(accountId, ledgerNum); - auto const& networkCfg = mApp.getLedgerManager().getSorobanNetworkConfig(); - // Approximate instruction measurements from loadgen contract. While the // guest and host cycle counts are exact, and we can predict the cost of the // guest and host loops correctly, it is difficult to estimate the CPU cost @@ -370,8 +366,8 @@ TxGenerator::invokeSorobanLoadTransaction( // payload). baseInstructionCount is for vm instantiation and additional // cushion to make sure transactions will succeed, but this means that the // instruction count is not perfect. Some TXs will fail due to exceeding - // resource limitations. However these should fail at apply time, so will - // still generate significant load + // resource limitations, but failures will be rare and those failures + // will happen at apply time, so they will still generate significant load. uint64_t const baseInstructionCount = 1'500'000; uint64_t const instructionsPerGuestCycle = 80; uint64_t const instructionsPerHostCycle = 5030; @@ -520,13 +516,13 @@ TxGenerator::getAccounts() } medida::Counter const& -TxGenerator::GetApplySorobanSuccess() +TxGenerator::getApplySorobanSuccess() { return mApplySorobanSuccess; } medida::Counter const& -TxGenerator::GetApplySorobanFailure() +TxGenerator::getApplySorobanFailure() { return mApplySorobanFailure; } @@ -738,10 +734,6 @@ TxGenerator::invokeSorobanCreateUpgradeTransaction( auto account = findAccount(accountId, ledgerNum); auto const& contractID = instanceKey.contractData().contract; - // TODO: Change this method to take SorobanUpgradeConfig as a parameter - // instead of upgradeBytes. SCBytes upgradeBytes = - // getConfigUpgradeSetFromLoadConfig(cfg); - LedgerKey upgradeLK(CONTRACT_DATA); upgradeLK.contractData().durability = TEMPORARY; upgradeLK.contractData().contract = contractID; diff --git a/src/simulation/TxGenerator.h b/src/simulation/TxGenerator.h index b6ba4643ae..d24a8aaa9e 100644 --- a/src/simulation/TxGenerator.h +++ b/src/simulation/TxGenerator.h @@ -139,8 +139,8 @@ class TxGenerator std::map const& getAccounts(); - medida::Counter const& GetApplySorobanSuccess(); - medida::Counter const& GetApplySorobanFailure(); + medida::Counter const& getApplySorobanSuccess(); + medida::Counter const& getApplySorobanFailure(); void reset(); From b6fbe26f53bd4220d37fec23ec48d8ec45479226 Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Mon, 16 Sep 2024 09:28:01 -0700 Subject: [PATCH 4/6] Rebase fixes --- src/simulation/LoadGenerator.cpp | 16 ++-- src/simulation/LoadGenerator.h | 7 +- src/simulation/TxGenerator.cpp | 98 +++++++++++++++------- src/simulation/TxGenerator.h | 24 +++--- src/simulation/test/LoadGeneratorTests.cpp | 7 +- src/test/TestUtils.cpp | 3 +- 6 files changed, 96 insertions(+), 59 deletions(-) diff --git a/src/simulation/LoadGenerator.cpp b/src/simulation/LoadGenerator.cpp index cd9e24cade..9a8a4991d2 100644 --- a/src/simulation/LoadGenerator.cpp +++ b/src/simulation/LoadGenerator.cpp @@ -390,9 +390,13 @@ LoadGenerator::start(GeneratedLoadConfig& cfg) } ConfigUpgradeSetKey -LoadGenerator::getConfigUpgradeSetKey(SorobanUpgradeConfig const& upgradeCfg, - Hash const& contractId) const +LoadGenerator::getConfigUpgradeSetKey( + SorobanUpgradeConfig const& upgradeCfg) const { + auto testingKeys = getContractInstanceKeysForTesting(); + releaseAssert(testingKeys.size() == 1); + auto contractId = testingKeys.begin()->contractData().contract.contractId(); + return mTxGenerator.getConfigUpgradeSetKey(upgradeCfg, contractId); } @@ -1011,11 +1015,11 @@ LoadGenerator::creationTransaction(uint64_t startAccount, uint64_t numItems, } mInitialAccountsCreated = true; return std::make_pair(sourceAcc, - mTxGenerator.createTransactionTestFramePtr( + mTxGenerator.createTransactionFramePtr( sourceAcc, creationOps, false, std::nullopt)); } -std::pair +std::pair LoadGenerator::createMixedClassicSorobanTransaction( uint32_t ledgerNum, uint64_t sourceAccountId, GeneratedLoadConfig const& cfg) @@ -1058,7 +1062,7 @@ LoadGenerator::createMixedClassicSorobanTransaction( } } -std::pair +std::pair LoadGenerator::createUploadWasmTransaction(GeneratedLoadConfig const& cfg, uint32_t ledgerNum, uint64_t sourceAccountId) @@ -1084,7 +1088,7 @@ LoadGenerator::createUploadWasmTransaction(GeneratedLoadConfig const& cfg, cfg.maxGeneratedFeeRate); } -std::pair +std::pair LoadGenerator::createInstanceTransaction(GeneratedLoadConfig const& cfg, uint32_t ledgerNum, uint64_t sourceAccountId) diff --git a/src/simulation/LoadGenerator.h b/src/simulation/LoadGenerator.h index 85e651691f..0fd898176a 100644 --- a/src/simulation/LoadGenerator.h +++ b/src/simulation/LoadGenerator.h @@ -178,8 +178,7 @@ class LoadGenerator void generateLoad(GeneratedLoadConfig cfg); ConfigUpgradeSetKey - getConfigUpgradeSetKey(SorobanUpgradeConfig const& upgradeCfg, - Hash const& contractId) const; + getConfigUpgradeSetKey(SorobanUpgradeConfig const& upgradeCfg) const; // Verify cached accounts are properly reflected in the database // return any accounts that are inconsistent. @@ -327,11 +326,11 @@ class LoadGenerator uint64_t sourceAccountId, GeneratedLoadConfig const& cfg); - std::pair + std::pair createUploadWasmTransaction(GeneratedLoadConfig const& cfg, uint32_t ledgerNum, uint64_t sourceAccountId); - std::pair + std::pair createInstanceTransaction(GeneratedLoadConfig const& cfg, uint32_t ledgerNum, uint64_t sourceAccountId); diff --git a/src/simulation/TxGenerator.cpp b/src/simulation/TxGenerator.cpp index e024a082b7..4b03d5bb06 100644 --- a/src/simulation/TxGenerator.cpp +++ b/src/simulation/TxGenerator.cpp @@ -93,14 +93,14 @@ TxGenerator::generateFee(std::optional maxGeneratedFeeRate, uint64_t TxGenerator::bytesToRead(xdr::xvector const& keys) { - LedgerTxn ltx(mApp.getLedgerTxnRoot()); + LedgerSnapshot lsg(mApp); uint64_t total = 0; for (auto const& key : keys) { - auto ltxe = ltx.loadWithoutRecord(key); - if (ltxe) + auto entry = lsg.load(key); + if (entry) { - total += xdr::xdr_size(ltxe.current()); + total += xdr::xdr_size(entry.current()); } } return total; @@ -109,8 +109,8 @@ TxGenerator::bytesToRead(xdr::xvector const& keys) bool TxGenerator::loadAccount(TestAccount& account) { - LedgerTxn ltx(mApp.getLedgerTxnRoot()); - auto entry = stellar::loadAccount(ltx, account.getPublicKey()); + LedgerSnapshot lsg(mApp); + auto const entry = lsg.getAccount(account.getPublicKey()); if (!entry) { return false; @@ -197,8 +197,8 @@ TxGenerator::createAccounts(uint64_t start, uint64_t count, uint32_t ledgerNum, return ops; } -TransactionTestFramePtr -TxGenerator::createTransactionTestFramePtr( +TransactionFrameBasePtr +TxGenerator::createTransactionFramePtr( TxGenerator::TestAccountPtr from, std::vector ops, bool pretend, std::optional maxGeneratedFeeRate) { @@ -220,7 +220,7 @@ TxGenerator::createTransactionTestFramePtr( return txf; } -std::pair +std::pair TxGenerator::paymentTransaction(uint32_t numAccounts, uint32_t offset, uint32_t ledgerNum, uint64_t sourceAccount, uint32_t opCount, @@ -238,11 +238,11 @@ TxGenerator::paymentTransaction(uint32_t numAccounts, uint32_t offset, } return std::make_pair(from, - createTransactionTestFramePtr(from, paymentOps, false, - maxGeneratedFeeRate)); + createTransactionFramePtr(from, paymentOps, false, + maxGeneratedFeeRate)); } -std::pair +std::pair TxGenerator::manageOfferTransaction(uint32_t ledgerNum, uint64_t accountId, uint32_t opCount, std::optional maxGeneratedFeeRate) @@ -259,12 +259,12 @@ TxGenerator::manageOfferTransaction(uint32_t ledgerNum, uint64_t accountId, Price{rand_uniform(1, 100), rand_uniform(1, 100)}, 100)); } - return std::make_pair(account, - createTransactionTestFramePtr(account, ops, false, - maxGeneratedFeeRate)); + return std::make_pair( + account, + createTransactionFramePtr(account, ops, false, maxGeneratedFeeRate)); } -std::pair +std::pair TxGenerator::createUploadWasmTransaction( uint32_t ledgerNum, uint64_t accountId, xdr::opaque_vec<> const& wasm, LedgerKey const& contractCodeLedgerKey, @@ -297,7 +297,7 @@ TxGenerator::createUploadWasmTransaction( return std::make_pair(account, tx); } -std::pair +std::pair TxGenerator::createContractTransaction( uint32_t ledgerNum, uint64_t accountId, LedgerKey const& codeKey, uint64_t contractOverheadBytes, uint256 const& salt, @@ -349,7 +349,7 @@ increaseOpSize(Operation& op, uint32_t increaseUpToBytes) op.body.invokeHostFunctionOp().auth = {auth}; } -std::pair +std::pair TxGenerator::invokeSorobanLoadTransaction( uint32_t ledgerNum, uint64_t accountId, ContractInstance const& instance, uint64_t contractOverheadBytes, std::optional maxGeneratedFeeRate) @@ -552,13 +552,13 @@ TxGenerator::getConfigUpgradeSetFromLoadConfig( { xdr::xvector updatedEntries; - LedgerTxn ltx(mApp.getLedgerTxnRoot()); + LedgerSnapshot lsg(mApp); for (uint32_t i = 0; i < static_cast(CONFIG_SETTING_BUCKETLIST_SIZE_WINDOW); ++i) { - auto entry = - ltx.load(configSettingKey(static_cast(i))); - auto& setting = entry.current().data.configSetting(); + auto entry = lsg.load(configSettingKey(static_cast(i))) + .current(); + auto& setting = entry.data.configSetting(); switch (static_cast(i)) { case CONFIG_SETTING_CONTRACT_MAX_SIZE_BYTES: @@ -678,12 +678,50 @@ TxGenerator::getConfigUpgradeSetFromLoadConfig( case CONFIG_SETTING_STATE_ARCHIVAL: { auto& ses = setting.stateArchivalSettings(); + if (upgradeCfg.maxEntryTTL > 0) + { + ses.maxEntryTTL = upgradeCfg.maxEntryTTL; + } + + if (upgradeCfg.minTemporaryTTL > 0) + { + ses.minTemporaryTTL = upgradeCfg.minTemporaryTTL; + } + + if (upgradeCfg.minPersistentTTL > 0) + { + ses.minPersistentTTL = upgradeCfg.minPersistentTTL; + } + + if (upgradeCfg.persistentRentRateDenominator > 0) + { + ses.persistentRentRateDenominator = + upgradeCfg.persistentRentRateDenominator; + } + + if (upgradeCfg.tempRentRateDenominator > 0) + { + ses.tempRentRateDenominator = + upgradeCfg.tempRentRateDenominator; + } + + if (upgradeCfg.maxEntriesToArchive > 0) + { + ses.maxEntriesToArchive = upgradeCfg.maxEntriesToArchive; + } + if (upgradeCfg.bucketListSizeWindowSampleSize > 0) { ses.bucketListSizeWindowSampleSize = upgradeCfg.bucketListSizeWindowSampleSize; } + if (upgradeCfg.bucketListWindowSamplePeriod > 0) + { + ses.bucketListWindowSamplePeriod = + upgradeCfg.bucketListWindowSamplePeriod; + } + if (upgradeCfg.evictionScanSize > 0) { ses.evictionScanSize = upgradeCfg.evictionScanSize; @@ -710,12 +748,12 @@ TxGenerator::getConfigUpgradeSetFromLoadConfig( // These two definitely aren't changing, and including both will hit the // contractDataEntrySizeBytes limit - if (entry.current().data.configSetting().configSettingID() != + if (entry.data.configSetting().configSettingID() != CONFIG_SETTING_CONTRACT_COST_PARAMS_CPU_INSTRUCTIONS && - entry.current().data.configSetting().configSettingID() != + entry.data.configSetting().configSettingID() != CONFIG_SETTING_CONTRACT_COST_PARAMS_MEMORY_BYTES) { - updatedEntries.emplace_back(entry.current().data.configSetting()); + updatedEntries.emplace_back(entry.data.configSetting()); } } @@ -725,7 +763,7 @@ TxGenerator::getConfigUpgradeSetFromLoadConfig( return xdr::xdr_to_opaque(upgradeSet); } -std::pair +std::pair TxGenerator::invokeSorobanCreateUpgradeTransaction( uint32_t ledgerNum, uint64_t accountId, SCBytes const& upgradeBytes, LedgerKey const& codeKey, LedgerKey const& instanceKey, @@ -777,7 +815,7 @@ TxGenerator::invokeSorobanCreateUpgradeTransaction( return std::make_pair(account, tx); } -std::pair +std::pair TxGenerator::sorobanRandomWasmTransaction(uint32_t ledgerNum, uint64_t accountId, uint32_t inclusionFee) @@ -845,7 +883,7 @@ TxGenerator::sorobanRandomUploadResources() return {resources, wasmSize}; } -std::pair +std::pair TxGenerator::pretendTransaction(uint32_t numAccounts, uint32_t offset, uint32_t ledgerNum, uint64_t sourceAccount, uint32_t opCount, @@ -871,8 +909,8 @@ TxGenerator::pretendTransaction(uint32_t numAccounts, uint32_t offset, } ops.push_back(txtest::setOptions(args)); } - return std::make_pair(acc, createTransactionTestFramePtr( - acc, ops, true, maxGeneratedFeeRate)); + return std::make_pair( + acc, createTransactionFramePtr(acc, ops, true, maxGeneratedFeeRate)); } } \ No newline at end of file diff --git a/src/simulation/TxGenerator.h b/src/simulation/TxGenerator.h index d24a8aaa9e..4ae04c6a09 100644 --- a/src/simulation/TxGenerator.h +++ b/src/simulation/TxGenerator.h @@ -76,49 +76,49 @@ class TxGenerator uint32_t ledgerNum, bool initialAccounts); - TransactionTestFramePtr - createTransactionTestFramePtr(TestAccountPtr from, - std::vector ops, bool pretend, - std::optional maxGeneratedFeeRate); + TransactionFrameBaseConstPtr + createTransactionFramePtr(TestAccountPtr from, std::vector ops, + bool pretend, + std::optional maxGeneratedFeeRate); - std::pair + std::pair paymentTransaction(uint32_t numAccounts, uint32_t offset, uint32_t ledgerNum, uint64_t sourceAccount, uint32_t opCount, std::optional maxGeneratedFeeRate); - std::pair + std::pair manageOfferTransaction(uint32_t ledgerNum, uint64_t accountId, uint32_t opCount, std::optional maxGeneratedFeeRate); - std::pair + std::pair createUploadWasmTransaction(uint32_t ledgerNum, uint64_t accountId, xdr::opaque_vec<> const& wasm, LedgerKey const& contractCodeLedgerKey, std::optional maxGeneratedFeeRate); - std::pair + std::pair createContractTransaction(uint32_t ledgerNum, uint64_t accountId, LedgerKey const& codeKey, uint64_t contractOverheadBytes, uint256 const& salt, std::optional maxGeneratedFeeRate); - std::pair + std::pair invokeSorobanLoadTransaction(uint32_t ledgerNum, uint64_t accountId, TxGenerator::ContractInstance const& instance, uint64_t contractOverheadBytes, std::optional maxGeneratedFeeRate); - std::pair + std::pair invokeSorobanCreateUpgradeTransaction( uint32_t ledgerNum, uint64_t accountId, SCBytes const& upgradeBytes, LedgerKey const& codeKey, LedgerKey const& instanceKey, std::optional maxGeneratedFeeRate); - std::pair + std::pair sorobanRandomWasmTransaction(uint32_t ledgerNum, uint64_t accountId, uint32_t inclusionFee); - std::pair + std::pair pretendTransaction(uint32_t numAccounts, uint32_t offset, uint32_t ledgerNum, uint64_t sourceAccount, uint32_t opCount, diff --git a/src/simulation/test/LoadGeneratorTests.cpp b/src/simulation/test/LoadGeneratorTests.cpp index b5d2c00cee..fd31de9b05 100644 --- a/src/simulation/test/LoadGeneratorTests.cpp +++ b/src/simulation/test/LoadGeneratorTests.cpp @@ -287,13 +287,8 @@ TEST_CASE("generate soroban load", "[loadgen][soroban]") rand_uniform(INT64_MAX - 10'000, INT64_MAX); upgradeCfg.startingEvictionScanLevel = rand_uniform(4, 8); - auto testingKeys = - app.getLoadGenerator().getContractInstanceKeysForTesting(); - REQUIRE(testingKeys.size() == 1); - auto contractId = testingKeys.begin()->contractData().contract.contractId(); - auto upgradeSetKey = loadGen.getConfigUpgradeSetKey( - createUpgradeLoadGenConfig.getSorobanUpgradeConfig(), contractId); + createUpgradeLoadGenConfig.getSorobanUpgradeConfig()); numTxsBefore = getSuccessfulTxCount(); loadGen.generateLoad(createUpgradeLoadGenConfig); diff --git a/src/test/TestUtils.cpp b/src/test/TestUtils.cpp index cc666cf78f..7750fe345a 100644 --- a/src/test/TestUtils.cpp +++ b/src/test/TestUtils.cpp @@ -240,7 +240,8 @@ upgradeSorobanNetworkConfig(std::function modifyFn, auto cfg = nodes[0]->getLedgerManager().getSorobanNetworkConfig(); modifyFn(cfg); createUpgradeLoadGenConfig.copySorobanNetworkConfigToUpgradeConfig(cfg); - auto upgradeSetKey = lg.getConfigUpgradeSetKey(createUpgradeLoadGenConfig); + auto upgradeSetKey = lg.getConfigUpgradeSetKey( + createUpgradeLoadGenConfig.getSorobanUpgradeConfig()); lg.generateLoad(createUpgradeLoadGenConfig); completeCount = complete.count(); simulation->crankUntil( From e62cfca51d3360104d60820867b412db384ca6e0 Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Mon, 16 Sep 2024 14:58:54 -0700 Subject: [PATCH 5/6] Add utilization metrics and comments --- src/main/CommandLine.cpp | 15 ++++++ src/simulation/ApplyLoad.cpp | 93 ++++++++++++++++++++++++++++++++++-- src/simulation/ApplyLoad.h | 30 ++++++++++++ 3 files changed, 134 insertions(+), 4 deletions(-) diff --git a/src/main/CommandLine.cpp b/src/main/CommandLine.cpp index c165139513..5210bb8fb1 100644 --- a/src/main/CommandLine.cpp +++ b/src/main/CommandLine.cpp @@ -1940,6 +1940,21 @@ runApplyLoad(CommandLineArgs const& args) CLOG_INFO(Perf, "Mean CPU ins ratio excl VM: {}", cpuInsRatioExclVm.mean() / 1000000); + CLOG_INFO(Perf, "Tx count utilization {}%", + al.getTxCountUtilization().mean() / 1000.0); + CLOG_INFO(Perf, "Instruction utilization {}%", + al.getInstructionUtilization().mean() / 1000.0); + CLOG_INFO(Perf, "Tx size utilization {}%", + al.getTxSizeUtilization().mean() / 1000.0); + CLOG_INFO(Perf, "Read bytes utilization {}%", + al.getReadByteUtilization().mean() / 1000.0); + CLOG_INFO(Perf, "Write bytes utilization {}%", + al.getWriteByteUtilization().mean() / 1000.0); + CLOG_INFO(Perf, "Read entry utilization {}%", + al.getReadEntryUtilization().mean() / 1000.0); + CLOG_INFO(Perf, "Write entry utilization {}%", + al.getWriteEntryUtilization().mean() / 1000.0); + CLOG_INFO(Perf, "Tx Success Rate: {:f}%", al.successRate() * 100); } diff --git a/src/simulation/ApplyLoad.cpp b/src/simulation/ApplyLoad.cpp index e8cb6fc69a..b4d40fd2b6 100644 --- a/src/simulation/ApplyLoad.cpp +++ b/src/simulation/ApplyLoad.cpp @@ -27,6 +27,20 @@ ApplyLoad::ApplyLoad(Application& app, uint64_t ledgerMaxInstructions, , mApp(app) , mNumAccounts( ledgerMaxTxCount * SOROBAN_TRANSACTION_QUEUE_SIZE_MULTIPLIER + 1) + , mTxCountUtilization( + mApp.getMetrics().NewHistogram({"soroban", "benchmark", "tx-count"})) + , mInstructionUtilization( + mApp.getMetrics().NewHistogram({"soroban", "benchmark", "ins"})) + , mTxSizeUtilization( + mApp.getMetrics().NewHistogram({"soroban", "benchmark", "tx-size"})) + , mReadByteUtilization( + mApp.getMetrics().NewHistogram({"soroban", "benchmark", "read-byte"})) + , mWriteByteUtilization(mApp.getMetrics().NewHistogram( + {"soroban", "benchmark", "write-byte"})) + , mReadEntryUtilization(mApp.getMetrics().NewHistogram( + {"soroban", "benchmark", "read-entry"})) + , mWriteEntryUtilization(mApp.getMetrics().NewHistogram( + {"soroban", "benchmark", "write-entry"})) { auto rootTestAccount = TestAccount::createRoot(mApp); @@ -213,12 +227,16 @@ ApplyLoad::benchmark() auto resources = multiplyByDouble( lm.maxLedgerResources(true), SOROBAN_TRANSACTION_QUEUE_SIZE_MULTIPLIER); + // Save a snapshot so we can calculate what % we used up. + auto const resourcesSnapshot = resources; + auto const& accounts = mTxGenerator.getAccounts(); std::vector shuffledAccounts(accounts.size()); std::iota(shuffledAccounts.begin(), shuffledAccounts.end(), 0); stellar::shuffle(std::begin(shuffledAccounts), std::end(shuffledAccounts), gRandomEngine); + bool limitHit = false; for (auto accountIndex : shuffledAccounts) { auto it = accounts.find(accountIndex); @@ -243,7 +261,6 @@ ApplyLoad::benchmark() } else { - bool limitHit = false; for (size_t i = 0; i < resources.size(); ++i) { auto type = static_cast(i); @@ -256,15 +273,47 @@ ApplyLoad::benchmark() } } - // If this assert fails, it most likely means that we ran out of - // accounts, which should not happen. - releaseAssert(limitHit); break; } txs.emplace_back(tx.second); } + // If this assert fails, it most likely means that we ran out of + // accounts, which should not happen. + releaseAssert(limitHit); + + mTxCountUtilization.Update( + (1.0 - (resources.getVal(Resource::Type::OPERATIONS) * 1.0 / + resourcesSnapshot.getVal(Resource::Type::OPERATIONS))) * + 100000.0); + mInstructionUtilization.Update( + (1.0 - (resources.getVal(Resource::Type::INSTRUCTIONS) * 1.0 / + resourcesSnapshot.getVal(Resource::Type::INSTRUCTIONS))) * + 100000.0); + mTxSizeUtilization.Update( + (1.0 - (resources.getVal(Resource::Type::TX_BYTE_SIZE) * 1.0 / + resourcesSnapshot.getVal(Resource::Type::TX_BYTE_SIZE))) * + 100000.0); + mReadByteUtilization.Update( + (1.0 - (resources.getVal(Resource::Type::READ_BYTES) * 1.0 / + resourcesSnapshot.getVal(Resource::Type::READ_BYTES))) * + 100000.0); + mWriteByteUtilization.Update( + (1.0 - (resources.getVal(Resource::Type::WRITE_BYTES) * 1.0 / + resourcesSnapshot.getVal(Resource::Type::WRITE_BYTES))) * + 100000.0); + mReadEntryUtilization.Update( + (1.0 - + (resources.getVal(Resource::Type::READ_LEDGER_ENTRIES) * 1.0 / + resourcesSnapshot.getVal(Resource::Type::READ_LEDGER_ENTRIES))) * + 100000.0); + mWriteEntryUtilization.Update( + (1.0 - + (resources.getVal(Resource::Type::WRITE_LEDGER_ENTRIES) * 1.0 / + resourcesSnapshot.getVal(Resource::Type::WRITE_LEDGER_ENTRIES))) * + 100000.0); + closeLedger(txs); } @@ -276,4 +325,40 @@ ApplyLoad::successRate() mTxGenerator.getApplySorobanFailure().count()); } +medida::Histogram const& +ApplyLoad::getTxCountUtilization() +{ + return mTxCountUtilization; +} +medida::Histogram const& +ApplyLoad::getInstructionUtilization() +{ + return mInstructionUtilization; +} +medida::Histogram const& +ApplyLoad::getTxSizeUtilization() +{ + return mTxSizeUtilization; +} +medida::Histogram const& +ApplyLoad::getReadByteUtilization() +{ + return mReadByteUtilization; +} +medida::Histogram const& +ApplyLoad::getWriteByteUtilization() +{ + return mWriteByteUtilization; +} +medida::Histogram const& +ApplyLoad::getReadEntryUtilization() +{ + return mReadEntryUtilization; +} +medida::Histogram const& +ApplyLoad::getWriteEntryUtilization() +{ + return mWriteEntryUtilization; +} + } \ No newline at end of file diff --git a/src/simulation/ApplyLoad.h b/src/simulation/ApplyLoad.h index a3254b3245..9630bca290 100644 --- a/src/simulation/ApplyLoad.h +++ b/src/simulation/ApplyLoad.h @@ -7,6 +7,8 @@ #include "simulation/TxGenerator.h" #include "test/TestAccount.h" +#include "medida/meter.h" + namespace stellar { class ApplyLoad @@ -18,10 +20,30 @@ class ApplyLoad uint64_t ledgerMaxWriteBytes, uint64_t ledgerMaxTxCount, uint64_t ledgerMaxTransactionsSizeBytes); + // Fills up a list of transactions with + // SOROBAN_TRANSACTION_QUEUE_SIZE_MULTIPLIER * the max ledger resources + // specified in the ApplyLoad constructor, create a TransactionSet out of + // those transactions, and then close a ledger with that TransactionSet. The + // generated transactions are generated using the LOADGEN_* config + // parameters. void benchmark(); + // Returns the % of transactions that succeeded during apply time. The range + // of values is [0,1.0]. double successRate(); + // These metrics track what percentage of available resources were used when + // creating the list of transactions in benchmark(). + // Histogram uses integers, so the values are scaled up by 100,000 + // Ex. We store 18000 for .18 (or 18%) + medida::Histogram const& getTxCountUtilization(); + medida::Histogram const& getInstructionUtilization(); + medida::Histogram const& getTxSizeUtilization(); + medida::Histogram const& getReadByteUtilization(); + medida::Histogram const& getWriteByteUtilization(); + medida::Histogram const& getReadEntryUtilization(); + medida::Histogram const& getWriteEntryUtilization(); + private: void closeLedger(std::vector const& txs, xdr::xvector const& upgrades = {}); @@ -46,6 +68,14 @@ class ApplyLoad TxGenerator::TestAccountPtr mRoot; uint32_t mNumAccounts; + + medida::Histogram& mTxCountUtilization; + medida::Histogram& mInstructionUtilization; + medida::Histogram& mTxSizeUtilization; + medida::Histogram& mReadByteUtilization; + medida::Histogram& mWriteByteUtilization; + medida::Histogram& mReadEntryUtilization; + medida::Histogram& mWriteEntryUtilization; }; } \ No newline at end of file From 0a1c2f7f39d8a1d428f6d442feb8ab455227a0b9 Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Mon, 16 Sep 2024 17:26:56 -0700 Subject: [PATCH 6/6] Add docs and fix rebase issue --- docs/software/commands.md | 16 ++++++++++++++++ src/simulation/TxGenerator.cpp | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/software/commands.md b/docs/software/commands.md index 28b1436122..46fac346e1 100644 --- a/docs/software/commands.md +++ b/docs/software/commands.md @@ -19,6 +19,22 @@ Common options can be placed at any place in the command line. ## Command line options Command options can only by placed after command. +* **apply-load**: Applies Soroban transactions by repeatedly generating transactions and closing +them directly through the LedgerManager. The parameters specified below configure the network limits, and +they're all required - **--ledger-max-instructions N**, **--ledger-max-read-entries N**, **--ledger-max-write-entries N**, **--ledger-max-read-byte N**, **--ledger-max-write-bytes N**, **--ledger-max-tx-size N**, **--ledger-max-tx-count N**. 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. + +* At the moment, the Soroban transactions are generated using some of the same config parameters as the **generateload** command. Specifically, + `ARTIFICIALLY_GENERATE_LOAD_FOR_TESTING=true`, + `LOADGEN_NUM_DATA_ENTRIES_FOR_TESTING`, + `LOADGEN_NUM_DATA_ENTRIES_DISTRIBUTION_FOR_TESTING`, + `LOADGEN_IO_KILOBYTES_FOR_TESTING`, + `LOADGEN_IO_KILOBYTES_DISTRIBUTION_FOR_TESTING`, + `LOADGEN_TX_SIZE_BYTES_FOR_TESTING`, + `LOADGEN_TX_SIZE_BYTES_DISTRIBUTION_FOR_TESTING`, + `LOADGEN_INSTRUCTIONS_FOR_TESTING`, and + `LOADGEN_INSTRUCTIONS_DISTRIBUTION_FOR_TESTING`. + * **catchup **: Perform catchup from history archives without connecting to network. For new instances (with empty history tables - only ledger 1 present in the database) it will respect LEDGER-COUNT diff --git a/src/simulation/TxGenerator.cpp b/src/simulation/TxGenerator.cpp index 4b03d5bb06..d07380f64d 100644 --- a/src/simulation/TxGenerator.cpp +++ b/src/simulation/TxGenerator.cpp @@ -305,7 +305,7 @@ TxGenerator::createContractTransaction( { auto account = findAccount(accountId, ledgerNum); SorobanResources createResources{}; - createResources.instructions = 500'000; + createResources.instructions = 1'000'000; createResources.readBytes = contractOverheadBytes; createResources.writeBytes = 300;