-
Notifications
You must be signed in to change notification settings - Fork 39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Parachain runtime instance cache #1687
Conversation
…achain-runtime-instance-cache
Codecov Report
@@ Coverage Diff @@
## master #1687 +/- ##
==========================================
+ Coverage 21.56% 23.36% +1.79%
==========================================
Files 745 708 -37
Lines 31935 29284 -2651
Branches 16586 15090 -1496
==========================================
- Hits 6887 6841 -46
+ Misses 19286 16722 -2564
+ Partials 5762 5721 -41
... and 110 files with indirect coverage changes 📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
logger_->error( | ||
"Memory size exceeded when growing it on {} bytes, offset was 0x{:x}", | ||
chunk_sz, | ||
offset_); | ||
return 0; | ||
} | ||
resize(offset_ + chunk_sz); | ||
auto new_size = offset_ + chunk_sz; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
auto new_size = offset_ + chunk_sz; | |
auto new_size = new_pages_num * kMemoryPageSize; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What for? resize() aligns the new size anyway.
core/runtime/runtime_context.hpp
Outdated
class RuntimeContext { | ||
public: | ||
// should be created from runtime contex factory | ||
RuntimeContext() = delete; | ||
RuntimeContext(const RuntimeContext &) = delete; | ||
RuntimeContext &operator=(const RuntimeContext &) = delete; | ||
|
||
RuntimeContext(RuntimeContext &&) = default; | ||
|
||
// constructor for tests | ||
static RuntimeContext create_TEST( | ||
std::shared_ptr<ModuleInstance> module_instance) { | ||
return RuntimeContext{module_instance}; | ||
} | ||
|
||
struct ContextParams { | ||
ContextParams() = delete; | ||
|
||
MemoryLimits memory_limits; | ||
}; | ||
|
||
const std::shared_ptr<ModuleInstance> module_instance; | ||
|
||
private: | ||
friend class RuntimeContextFactoryImpl; | ||
friend class RuntimeContextFactory; | ||
RuntimeContext(std::shared_ptr<ModuleInstance> module_instance); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use ModuleInstance
directly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, this should be noted in comments. ModuleInstances are reused, but before every call there should be some setup. RuntimeContext enforces this setup. Resetting memory, setting the correct batch.
core/runtime/executor.hpp
Outdated
virtual outcome::result<Buffer> callWithCtx(RuntimeContext &ctx, | ||
std::string_view name, | ||
const Buffer &encoded_args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this wrapper method to ModuleInstance
core/parachain/pvf/pvf_impl.cpp
Outdated
OUTCOME_TRY( | ||
parent_hash, | ||
block_header_repository_->getHashByNumber(params.relay_parent_number)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OUTCOME_TRY( | |
parent_hash, | |
block_header_repository_->getHashByNumber(params.relay_parent_number)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not used in rust, also num -> hash
is ambigous
std::unordered_map<ParachainId, Entry> instance_cache_; | ||
std::map<uint64_t, ParachainId> last_usage_time_; | ||
const uint32_t instances_limit_ = 43; | ||
std::atomic<uint64_t> time_ = 0; | ||
|
||
void erase(decltype(instance_cache_)::iterator it); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lru
.- Keep 2 codes for para near upgrade, also there are less codes then paras.
std::unordered_map<ParachainId, Entry> instance_cache_; | |
std::map<uint64_t, ParachainId> last_usage_time_; | |
const uint32_t instances_limit_ = 43; | |
std::atomic<uint64_t> time_ = 0; | |
void erase(decltype(instance_cache_)::iterator it); | |
Lru<ValidationCodeHash, Entry> instance_cache_; |
using SafeInstance = SafeObject<std::shared_ptr<runtime::ModuleInstance>>; | ||
using SafeInstanceRef = std::reference_wrapper<SafeInstance>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
using SafeInstance = SafeObject<std::shared_ptr<runtime::ModuleInstance>>; | |
using SafeInstanceRef = std::reference_wrapper<SafeInstance>; |
outcome::result<SafeInstanceRef> requestInstance( | ||
ParachainId para_id, | ||
const common::Hash256 &code_hash, | ||
const ParachainRuntime &code_zstd); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
~BorrowedInstance()
will callpool(self.borrowed)
outcome::result<SafeInstanceRef> requestInstance( | |
ParachainId para_id, | |
const common::Hash256 &code_hash, | |
const ParachainRuntime &code_zstd); | |
outcome::result<std::shared_ptr<runtime::ModuleInstance>> requestInstance( | |
const common::Hash256 &code_hash, | |
const ParachainRuntime &code_zstd); | |
pool(std::shared_ptr<runtime::ModuleInstance>); |
PvfRuntimeCache::requestInstance(ParachainId para_id, | ||
const common::Hash256 &code_hash, | ||
const ParachainRuntime &code_zstd) { | ||
++time_; | ||
std::unique_lock lock{instance_cache_mutex_}; | ||
auto it = instance_cache_.find(para_id); | ||
|
||
bool it_found = it != instance_cache_.end(); | ||
|
||
bool same_hash = | ||
it_found && it->second.instance.sharedAccess([](const auto &instance) { | ||
return instance->getCodeHash(); | ||
}) == code_hash; | ||
|
||
if (!(it_found && same_hash)) { | ||
ParachainRuntime code; | ||
OUTCOME_TRY(runtime::uncompressCodeIfNeeded(code_zstd, code)); | ||
OUTCOME_TRY(runtime_module, module_factory_->make(code)); | ||
OUTCOME_TRY(instance, runtime_module->instantiate()); | ||
if (it_found) { | ||
erase(it); | ||
} | ||
SafeObject safe_instance{instance}; | ||
|
||
auto [new_it, inserted] = | ||
instance_cache_.emplace(std::piecewise_construct, | ||
std::forward_as_tuple(para_id), | ||
std::forward_as_tuple(instance, time_)); | ||
BOOST_ASSERT(inserted); | ||
last_usage_time_.emplace(time_, para_id); | ||
it = new_it; | ||
if (instance_cache_.size() > instances_limit_) { | ||
cleanup(lock); | ||
} | ||
} else { | ||
auto lru_it = last_usage_time_.find(it->second.last_used); | ||
BOOST_ASSERT(lru_it != last_usage_time_.end()); | ||
last_usage_time_.erase(lru_it); | ||
last_usage_time_.emplace(time_, para_id); | ||
it->second.last_used = time_; | ||
} | ||
BOOST_ASSERT(it != instance_cache_.end()); | ||
return it->second.instance; | ||
} | ||
|
||
void PvfRuntimeCache::cleanup(const std::unique_lock<std::mutex> &lock) { | ||
BOOST_ASSERT(lock.owns_lock()); | ||
BOOST_ASSERT(lock.mutex() == &instance_cache_mutex_); | ||
|
||
for (auto it = last_usage_time_.begin(); | ||
it != last_usage_time_.end() | ||
&& last_usage_time_.size() > instances_limit_;) { | ||
instance_cache_.erase(it->second); | ||
it = last_usage_time_.erase(it); | ||
} | ||
} | ||
|
||
void PvfRuntimeCache::erase(decltype(instance_cache_)::iterator it) { | ||
// instance can be used at this point, so we wait for exclusive access | ||
// and then extract it from the map (we cannot erase it from inside | ||
// the exclusive access callback because destroying a locked mutex is | ||
// UB) | ||
it->second.instance.exclusiveAccess( | ||
[this, it](auto) { | ||
last_usage_time_.erase(it->second.last_used); | ||
return instance_cache_.extract(it); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PvfRuntimeCache::requestInstance(ParachainId para_id, | |
const common::Hash256 &code_hash, | |
const ParachainRuntime &code_zstd) { | |
++time_; | |
std::unique_lock lock{instance_cache_mutex_}; | |
auto it = instance_cache_.find(para_id); | |
bool it_found = it != instance_cache_.end(); | |
bool same_hash = | |
it_found && it->second.instance.sharedAccess([](const auto &instance) { | |
return instance->getCodeHash(); | |
}) == code_hash; | |
if (!(it_found && same_hash)) { | |
ParachainRuntime code; | |
OUTCOME_TRY(runtime::uncompressCodeIfNeeded(code_zstd, code)); | |
OUTCOME_TRY(runtime_module, module_factory_->make(code)); | |
OUTCOME_TRY(instance, runtime_module->instantiate()); | |
if (it_found) { | |
erase(it); | |
} | |
SafeObject safe_instance{instance}; | |
auto [new_it, inserted] = | |
instance_cache_.emplace(std::piecewise_construct, | |
std::forward_as_tuple(para_id), | |
std::forward_as_tuple(instance, time_)); | |
BOOST_ASSERT(inserted); | |
last_usage_time_.emplace(time_, para_id); | |
it = new_it; | |
if (instance_cache_.size() > instances_limit_) { | |
cleanup(lock); | |
} | |
} else { | |
auto lru_it = last_usage_time_.find(it->second.last_used); | |
BOOST_ASSERT(lru_it != last_usage_time_.end()); | |
last_usage_time_.erase(lru_it); | |
last_usage_time_.emplace(time_, para_id); | |
it->second.last_used = time_; | |
} | |
BOOST_ASSERT(it != instance_cache_.end()); | |
return it->second.instance; | |
} | |
void PvfRuntimeCache::cleanup(const std::unique_lock<std::mutex> &lock) { | |
BOOST_ASSERT(lock.owns_lock()); | |
BOOST_ASSERT(lock.mutex() == &instance_cache_mutex_); | |
for (auto it = last_usage_time_.begin(); | |
it != last_usage_time_.end() | |
&& last_usage_time_.size() > instances_limit_;) { | |
instance_cache_.erase(it->second); | |
it = last_usage_time_.erase(it); | |
} | |
} | |
void PvfRuntimeCache::erase(decltype(instance_cache_)::iterator it) { | |
// instance can be used at this point, so we wait for exclusive access | |
// and then extract it from the map (we cannot erase it from inside | |
// the exclusive access callback because destroying a locked mutex is | |
// UB) | |
it->second.instance.exclusiveAccess( | |
[this, it](auto) { | |
last_usage_time_.erase(it->second.last_used); | |
return instance_cache_.extract(it); | |
}); | |
} | |
PvfRuntimeCache::requestInstance(const common::Hash256 &code_hash, | |
const ParachainRuntime &code_zstd) { | |
std::unique_lock lock{instance_cache_mutex_}; | |
std::shared_ptr<runtime::ModuleInstance> instance; | |
if (auto entry = instance_cache_.get(code_hash)) { | |
if (entry.instances.empty()) { | |
BOOST_OUTCOME_TRY(instance, entry.module->instantiate()); | |
} else { | |
instance = entry->instances.back(); | |
entry->instances.pop_back(); | |
} | |
} else { | |
ParachainRuntime code; | |
OUTCOME_TRY(runtime::uncompressCodeIfNeeded(code_zstd, code)); | |
OUTCOME_TRY(runtime_module, module_factory_->make(code)); | |
BOOST_OUTCOME_TRY(instance, runtime_module->instantiate()); | |
cache_.put(code_hash, {module, {}}); | |
} | |
return std::make_shared<runtime::BorrowedInstance>(weak_from_this(), instance); | |
} | |
void PvfRuntimeCache::pool(std::shared_ptr<runtime::ModuleInstance> instance) { | |
std::unique_lock lock{instance_cache_mutex_}; | |
auto module = instance->getModule(); | |
auto hash = module->getHash(); | |
if (auto entry = instance_cache_.get(hash)) { | |
entry->instances.emplace_back(instance); | |
} else { | |
instance_cache_.put(hash, {module, {instance}}); | |
} | |
} |
…achain-runtime-instance-cache
…:soramitsu/kagome into feature/parachain-runtime-instance-cache
Signed-off-by: Ruslan Tushov <[email protected]>
Signed-off-by: Ruslan Tushov <[email protected]>
Signed-off-by: Ruslan Tushov <[email protected]>
Signed-off-by: Ruslan Tushov <[email protected]>
…:soramitsu/kagome into feature/parachain-runtime-instance-cache
outcome::result<primitives::Version> CoreImpl::version( | ||
RuntimeEnvironment &env) { | ||
return executor_->call<primitives::Version>(env, "Core_version"); | ||
std::shared_ptr<ModuleInstance> instance) { | ||
OUTCOME_TRY(genesis_hash, header_repo_->getHashByNumber(0)); | ||
OUTCOME_TRY(genesis_header, header_repo_->getBlockHeader(genesis_hash)); | ||
OUTCOME_TRY(ctx, | ||
ctx_factory_->ephemeral(instance, genesis_header.state_root)); | ||
return executor_->decodedCallWithCtx<primitives::Version>(ctx, | ||
"Core_version"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this method, if only used by calculate_genesis_state
, or remove ambigous argument
class PvfRuntimeCache { | ||
public: | ||
using SafeInstance = SafeObject<std::shared_ptr<runtime::ModuleInstance>>; | ||
using SafeInstanceRef = std::reference_wrapper<SafeInstance>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SafeInstanceRef
will become invalid when lru evicts it
* Add cache for runtime instances in pvf_impl * Refactor runtime environment factory * Executor refactoring for testability Co-authored-by: Ruslan Tushov <[email protected]> Co-authored-by: Ruslan Tushov <[email protected]>
* Add cache for runtime instances in pvf_impl * Refactor runtime environment factory * Executor refactoring for testability Co-authored-by: Ruslan Tushov <[email protected]> Co-authored-by: Ruslan Tushov <[email protected]>
Referenced issues
closes #1670
Description of the Change
Adds a parachain runtime instance cache, so that parachains don't instantiate a WASM module every time they want to call a runtime method. Also, respect the memory page limit returned by session_executor_params runtime call.
Benefits
Faster PVF instantiation.
Possible Drawbacks
Increased memory consumption.