Skip to content

Commit 89d85c2

Browse files
committed
Support reusable module cache with multithreaded pre-compilation
1 parent 06adc32 commit 89d85c2

13 files changed

+663
-14
lines changed

src/ledger/LedgerManager.h

+7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "catchup/LedgerApplyManager.h"
88
#include "history/HistoryManager.h"
99
#include "ledger/NetworkConfig.h"
10+
#include "rust/RustBridge.h"
1011
#include <memory>
1112

1213
namespace stellar
@@ -200,6 +201,12 @@ class LedgerManager
200201
virtual void manuallyAdvanceLedgerHeader(LedgerHeader const& header) = 0;
201202

202203
virtual SorobanMetrics& getSorobanMetrics() = 0;
204+
virtual rust_bridge::SorobanModuleCache& getModuleCache() = 0;
205+
206+
// Compiles all contracts in the current ledger, for ledger protocols
207+
// starting at minLedgerVersion and running through to
208+
// Config::CURRENT_LEDGER_PROTOCOL_VERSION (to enable upgrades).
209+
virtual void compileAllContractsInLedger(uint32_t minLedgerVersion) = 0;
203210

204211
virtual ~LedgerManager()
205212
{

src/ledger/LedgerManagerImpl.cpp

+27
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include "ledger/LedgerTxn.h"
2323
#include "ledger/LedgerTxnEntry.h"
2424
#include "ledger/LedgerTxnHeader.h"
25+
#include "ledger/SharedModuleCacheCompiler.h"
2526
#include "main/Application.h"
2627
#include "main/Config.h"
2728
#include "main/ErrorMessages.h"
@@ -42,6 +43,7 @@
4243
#include "work/WorkScheduler.h"
4344
#include "xdrpp/printer.h"
4445

46+
#include <cstdint>
4547
#include <fmt/format.h>
4648

4749
#include "xdr/Stellar-ledger.h"
@@ -155,6 +157,7 @@ LedgerManagerImpl::LedgerManagerImpl(Application& app)
155157
, mCatchupDuration(
156158
app.getMetrics().NewTimer({"ledger", "catchup", "duration"}))
157159
, mState(LM_BOOTING_STATE)
160+
, mModuleCache(::rust_bridge::new_module_cache())
158161

159162
{
160163
setupLedgerCloseMetaStream();
@@ -403,6 +406,9 @@ LedgerManagerImpl::loadLastKnownLedger(bool restoreBucketlist)
403406
updateNetworkConfig(ltx);
404407
mSorobanNetworkConfigReadOnly = mSorobanNetworkConfigForApply;
405408
}
409+
410+
// Prime module cache with ledger content.
411+
compileAllContractsInLedger(latestLedgerHeader->ledgerVersion);
406412
}
407413

408414
Database&
@@ -566,6 +572,27 @@ LedgerManagerImpl::getSorobanMetrics()
566572
return mSorobanMetrics;
567573
}
568574

575+
rust_bridge::SorobanModuleCache&
576+
LedgerManagerImpl::getModuleCache()
577+
{
578+
return *mModuleCache;
579+
}
580+
581+
void
582+
LedgerManagerImpl::compileAllContractsInLedger(uint32_t minLedgerVersion)
583+
{
584+
auto& moduleCache = getModuleCache();
585+
std::vector<uint32_t> ledgerVersions;
586+
for (uint32_t i = minLedgerVersion;
587+
i <= Config::CURRENT_LEDGER_PROTOCOL_VERSION; i++)
588+
{
589+
ledgerVersions.push_back(i);
590+
}
591+
auto compiler = std::make_shared<SharedModuleCacheCompiler>(
592+
mApp, moduleCache, ledgerVersions);
593+
compiler->run();
594+
}
595+
569596
void
570597
LedgerManagerImpl::publishSorobanMetrics()
571598
{

src/ledger/LedgerManagerImpl.h

+6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "ledger/NetworkConfig.h"
1212
#include "ledger/SorobanMetrics.h"
1313
#include "main/PersistentState.h"
14+
#include "rust/RustBridge.h"
1415
#include "transactions/TransactionFrame.h"
1516
#include "util/XDRStream.h"
1617
#include "xdr/Stellar-ledger.h"
@@ -152,6 +153,9 @@ class LedgerManagerImpl : public LedgerManager
152153
// Update cached ledger state values managed by this class.
153154
void advanceLedgerPointers(CloseLedgerOutput const& output);
154155

156+
// The reusable / inter-ledger soroban module cache.
157+
::rust::Box<rust_bridge::SorobanModuleCache> mModuleCache;
158+
155159
protected:
156160
// initialLedgerVers must be the ledger version at the start of the ledger
157161
// and currLedgerVers is the ledger version in the current ltx header. These
@@ -251,5 +255,7 @@ class LedgerManagerImpl : public LedgerManager
251255
{
252256
return mCurrentlyApplyingLedger;
253257
}
258+
rust_bridge::SorobanModuleCache& getModuleCache() override;
259+
void compileAllContractsInLedger(uint32_t minLedgerVersion) override;
254260
};
255261
}
+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#include "ledger/SharedModuleCacheCompiler.h"
2+
#include "bucket/SearchableBucketList.h"
3+
#include "crypto/Hex.h"
4+
#include "crypto/SHA.h"
5+
#include "main/Application.h"
6+
#include "rust/RustBridge.h"
7+
#include "util/Logging.h"
8+
#include <chrono>
9+
10+
using namespace stellar;
11+
12+
SharedModuleCacheCompiler::SharedModuleCacheCompiler(
13+
Application& app, rust_bridge::SorobanModuleCache& moduleCache,
14+
std::vector<uint32_t> const& ledgerVersions)
15+
: mApp(app)
16+
, mModuleCache(moduleCache)
17+
, mSnap(app.getBucketManager()
18+
.getBucketSnapshotManager()
19+
.copySearchableLiveBucketListSnapshot())
20+
, mNumThreads(
21+
static_cast<size_t>(std::max(2, app.getConfig().WORKER_THREADS) - 1))
22+
, mLedgerVersions(ledgerVersions)
23+
{
24+
}
25+
26+
void
27+
SharedModuleCacheCompiler::pushWasm(xdr::xvector<uint8_t> const& vec)
28+
{
29+
std::unique_lock<std::mutex> lock(mMutex);
30+
mHaveSpace.wait(
31+
lock, [&] { return mBytesLoaded - mBytesCompiled < MAX_MEM_BYTES; });
32+
xdr::xvector<uint8_t> buf(vec);
33+
auto size = buf.size();
34+
mWasms.emplace_back(std::move(buf));
35+
mBytesLoaded += size;
36+
lock.unlock();
37+
mHaveContracts.notify_all();
38+
LOG_INFO(DEFAULT_LOG, "Loaded contract with {} bytes of wasm code", size);
39+
}
40+
41+
bool
42+
SharedModuleCacheCompiler::isFinishedCompiling(
43+
std::unique_lock<std::mutex>& lock)
44+
{
45+
releaseAssert(lock.owns_lock());
46+
return mLoadedAll && mBytesCompiled == mBytesLoaded;
47+
}
48+
49+
void
50+
SharedModuleCacheCompiler::setFinishedLoading()
51+
{
52+
std::unique_lock lock(mMutex);
53+
mLoadedAll = true;
54+
lock.unlock();
55+
mHaveContracts.notify_all();
56+
}
57+
58+
bool
59+
SharedModuleCacheCompiler::popAndCompileWasm(size_t thread,
60+
std::unique_lock<std::mutex>& lock)
61+
{
62+
63+
releaseAssert(lock.owns_lock());
64+
65+
// Wait for a new contract to compile (or being done).
66+
mHaveContracts.wait(
67+
lock, [&] { return !mWasms.empty() || isFinishedCompiling(lock); });
68+
69+
// Check to see if we were woken up due to end-of-compilation.
70+
if (isFinishedCompiling(lock))
71+
{
72+
return false;
73+
}
74+
75+
xdr::xvector<uint8_t> wasm = std::move(mWasms.front());
76+
mWasms.pop_front();
77+
78+
// Make a local shallow copy of the cache, so we don't race on the
79+
// shared host.
80+
auto cache = mModuleCache.shallow_clone();
81+
82+
lock.unlock();
83+
84+
auto start = std::chrono::steady_clock::now();
85+
auto slice = rust::Slice<const uint8_t>(wasm.data(), wasm.size());
86+
try
87+
{
88+
for (auto ledgerVersion : mLedgerVersions)
89+
{
90+
cache->compile(ledgerVersion, slice);
91+
}
92+
}
93+
catch (std::exception const& e)
94+
{
95+
LOG_ERROR(DEFAULT_LOG, "Thread {} failed to compile wasm code: {}",
96+
thread, e.what());
97+
}
98+
auto end = std::chrono::steady_clock::now();
99+
auto dur_us =
100+
std::chrono::duration_cast<std::chrono::microseconds>(end - start);
101+
LOG_INFO(DEFAULT_LOG, "Thread {} compiled {} byte wasm contract {} in {}us",
102+
thread, wasm.size(), binToHex(sha256(wasm)), dur_us.count());
103+
lock.lock();
104+
mTotalCompileTime += dur_us;
105+
mBytesCompiled += wasm.size();
106+
wasm.clear();
107+
mHaveSpace.notify_all();
108+
mHaveContracts.notify_all();
109+
return true;
110+
}
111+
112+
void
113+
SharedModuleCacheCompiler::run()
114+
{
115+
auto self = shared_from_this();
116+
auto start = std::chrono::steady_clock::now();
117+
LOG_INFO(DEFAULT_LOG,
118+
"Launching 1 loading and {} compiling background threads",
119+
mNumThreads);
120+
mApp.postOnBackgroundThread(
121+
[self]() {
122+
self->mSnap->scanForContractCode([&](LedgerEntry const& entry) {
123+
self->pushWasm(entry.data.contractCode().code);
124+
return Loop::INCOMPLETE;
125+
});
126+
self->setFinishedLoading();
127+
},
128+
"contract loading thread");
129+
130+
for (auto thread = 0; thread < self->mNumThreads; ++thread)
131+
{
132+
mApp.postOnBackgroundThread(
133+
[self, thread]() {
134+
size_t nContractsCompiled = 0;
135+
std::unique_lock<std::mutex> lock(self->mMutex);
136+
while (!self->isFinishedCompiling(lock))
137+
{
138+
if (self->popAndCompileWasm(thread, lock))
139+
{
140+
++nContractsCompiled;
141+
}
142+
}
143+
LOG_INFO(DEFAULT_LOG, "Thread {} compiled {} contracts", thread,
144+
nContractsCompiled);
145+
},
146+
fmt::format("compilation thread {}", thread));
147+
}
148+
149+
std::unique_lock lock(self->mMutex);
150+
self->mHaveContracts.wait(
151+
lock, [self, &lock] { return self->isFinishedCompiling(lock); });
152+
153+
auto end = std::chrono::steady_clock::now();
154+
LOG_INFO(DEFAULT_LOG,
155+
"All contracts compiled in {}ms real time, {}ms CPU time",
156+
std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
157+
.count(),
158+
std::chrono::duration_cast<std::chrono::milliseconds>(
159+
self->mTotalCompileTime)
160+
.count());
161+
}
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#pragma once
2+
// Copyright 2024 Stellar Development Foundation and contributors. Licensed
3+
// under the Apache License, Version 2.0. See the COPYING file at the root
4+
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0
5+
6+
#include "bucket/BucketSnapshotManager.h"
7+
#include "rust/RustBridge.h"
8+
#include "xdrpp/types.h"
9+
10+
#include <cstdint>
11+
#include <deque>
12+
13+
#include <condition_variable>
14+
#include <memory>
15+
#include <mutex>
16+
17+
namespace stellar
18+
{
19+
class Application;
20+
}
21+
22+
// This class encapsulates a multithreaded strategy for loading contracts
23+
// out of the database (on one thread) and compiling them (on N-1 others).
24+
class SharedModuleCacheCompiler
25+
: public std::enable_shared_from_this<SharedModuleCacheCompiler>
26+
{
27+
stellar::Application& mApp;
28+
stellar::rust_bridge::SorobanModuleCache& mModuleCache;
29+
stellar::SearchableSnapshotConstPtr mSnap;
30+
std::deque<xdr::xvector<uint8_t>> mWasms;
31+
32+
const size_t mNumThreads;
33+
const size_t MAX_MEM_BYTES = 10 * 1024 * 1024;
34+
bool mLoadedAll{false};
35+
size_t mBytesLoaded{0};
36+
size_t mBytesCompiled{0};
37+
std::vector<uint32_t> mLedgerVersions;
38+
39+
std::mutex mMutex;
40+
std::condition_variable mHaveSpace;
41+
std::condition_variable mHaveContracts;
42+
43+
std::chrono::microseconds mTotalCompileTime{0};
44+
45+
void setFinishedLoading();
46+
bool isFinishedCompiling(std::unique_lock<std::mutex>& lock);
47+
// This gets called in a loop on the loader/producer thread.
48+
void pushWasm(xdr::xvector<uint8_t> const& vec);
49+
// This gets called in a loop on the compiler/consumer threads. It returns
50+
// true if anything was actually compiled.
51+
bool popAndCompileWasm(size_t thread, std::unique_lock<std::mutex>& lock);
52+
53+
public:
54+
SharedModuleCacheCompiler(
55+
stellar::Application& app,
56+
stellar::rust_bridge::SorobanModuleCache& moduleCache,
57+
std::vector<uint32_t> const& ledgerVersions);
58+
void run();
59+
};

src/main/ApplicationUtils.cpp

+15
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "main/PersistentState.h"
2424
#include "main/StellarCoreVersion.h"
2525
#include "overlay/OverlayManager.h"
26+
#include "rust/RustBridge.h"
2627
#include "scp/LocalNode.h"
2728
#include "util/GlobalChecks.h"
2829
#include "util/Logging.h"
@@ -1034,4 +1035,18 @@ listContracts(Config const& cfg)
10341035
return 0;
10351036
}
10361037

1038+
int
1039+
compileContracts(Config const& cfg)
1040+
{
1041+
VirtualClock clock(VirtualClock::REAL_TIME);
1042+
auto config = cfg;
1043+
config.setNoListen();
1044+
auto app = Application::create(clock, config, /* newDB */ false);
1045+
// Initializing the ledgerManager will, in restoring the last known ledger,
1046+
// also cause all contracts to be compiled. All we need to do is start and
1047+
// stop the application.
1048+
app->start();
1049+
return 0;
1050+
}
1051+
10371052
}

src/main/ApplicationUtils.h

+1
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,5 @@ std::optional<uint32_t>
6666
getStellarCoreMajorReleaseVersion(std::string const& vstr);
6767

6868
int listContracts(Config const& cfg);
69+
int compileContracts(Config const& cfg);
6970
}

src/main/CommandLine.cpp

+9
Original file line numberDiff line numberDiff line change
@@ -1478,8 +1478,15 @@ runListContracts(CommandLineArgs const& args)
14781478

14791479
return runWithHelp(args, {configurationParser(configOption)},
14801480
[&] { return listContracts(configOption.getConfig()); });
1481+
}
14811482

1483+
int
1484+
runCompileContracts(CommandLineArgs const& args)
1485+
{
1486+
CommandLine::ConfigOption configOption;
14821487

1488+
return runWithHelp(args, {configurationParser(configOption)}, [&] {
1489+
return compileContracts(configOption.getConfig());
14831490
});
14841491
}
14851492

@@ -1960,6 +1967,8 @@ handleCommandLine(int argc, char* const* argv)
19601967
{"list-contracts",
19611968
"List sha256 hashes of all contract code entries in the bucket list",
19621969
runListContracts},
1970+
{"compile-contracts", "Compile wasm files and cache them",
1971+
runCompileContracts},
19631972
{"version", "print version information", runVersion}}};
19641973

19651974
auto adjustedCommandLine = commandLine.adjustCommandLine({argc, argv});

0 commit comments

Comments
 (0)