Skip to content

Commit bff1064

Browse files
webmaster128mergify[bot]
authored andcommitted
Store engine together with Module to fix memory increase issue
(cherry picked from commit a344468) # Conflicts: # packages/vm/src/cache.rs # packages/vm/src/modules/in_memory_cache.rs
1 parent e2ad213 commit bff1064

File tree

6 files changed

+239
-121
lines changed

6 files changed

+239
-121
lines changed

packages/vm/src/cache.rs

Lines changed: 57 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::io::{Read, Write};
44
use std::marker::PhantomData;
55
use std::path::{Path, PathBuf};
66
use std::sync::Mutex;
7-
use wasmer::{Engine, Store};
7+
use wasmer::{Module, Store};
88

99
use crate::backend::{Backend, BackendApi, Querier, Storage};
1010
use crate::capabilities::required_capabilities_from_module;
@@ -16,8 +16,13 @@ use crate::instance::{Instance, InstanceOptions};
1616
use crate::modules::{CachedModule, FileSystemCache, InMemoryCache, PinnedMemoryCache};
1717
use crate::parsed_wasm::ParsedWasm;
1818
use crate::size::Size;
19+
<<<<<<< HEAD
1920
use crate::static_analysis::has_ibc_entry_points;
2021
use crate::wasm_backend::{compile, make_compiling_engine, make_runtime_engine};
22+
=======
23+
use crate::static_analysis::{Entrypoint, ExportInfo, REQUIRED_IBC_EXPORTS};
24+
use crate::wasm_backend::{compile, make_compiling_engine};
25+
>>>>>>> a34446891 (Store engine together with Module to fix memory increase issue)
2126

2227
const STATE_DIR: &str = "state";
2328
// Things related to the state of the blockchain.
@@ -88,24 +93,14 @@ pub struct CacheInner {
8893
memory_cache: InMemoryCache,
8994
fs_cache: FileSystemCache,
9095
stats: Stats,
91-
/// A single engine to execute all contracts in this cache instance (usually
92-
/// this means all contracts in the process).
93-
///
94-
/// This engine is headless, i.e. does not contain a Singlepass compiler.
95-
/// It only executes modules compiled with other engines.
96-
///
97-
/// The engine has one memory limit set which is the same for all contracts
98-
/// running with it. If different memory limits would be needed for different
99-
/// contracts at some point, we'd need multiple engines. This is because the tunables
100-
/// that control the limit are attached to the engine.
101-
runtime_engine: Engine,
10296
}
10397

10498
pub struct Cache<A: BackendApi, S: Storage, Q: Querier> {
10599
/// Available capabilities are immutable for the lifetime of the cache,
106100
/// i.e. any number of read-only references is allowed to access it concurrently.
107101
available_capabilities: HashSet<String>,
108102
inner: Mutex<CacheInner>,
103+
instance_memory_limit: Size,
109104
// Those two don't store data but only fix type information
110105
type_api: PhantomData<A>,
111106
type_storage: PhantomData<S>,
@@ -161,8 +156,8 @@ where
161156
memory_cache: InMemoryCache::new(memory_cache_size),
162157
fs_cache,
163158
stats: Stats::default(),
164-
runtime_engine: make_runtime_engine(Some(instance_memory_limit)),
165159
}),
160+
instance_memory_limit,
166161
type_storage: PhantomData::<S>,
167162
type_api: PhantomData::<A>,
168163
type_querier: PhantomData::<Q>,
@@ -298,11 +293,12 @@ where
298293
// for a not-so-relevant use case.
299294

300295
// Try to get module from file system cache
301-
if let Some((module, module_size)) = cache.fs_cache.load(checksum, &cache.runtime_engine)? {
296+
if let Some(cached_module) = cache
297+
.fs_cache
298+
.load(checksum, Some(self.instance_memory_limit))?
299+
{
302300
cache.stats.hits_fs_cache = cache.stats.hits_fs_cache.saturating_add(1);
303-
return cache
304-
.pinned_memory_cache
305-
.store(checksum, module, module_size);
301+
return cache.pinned_memory_cache.store(checksum, cached_module);
306302
}
307303

308304
// Re-compile from original Wasm bytecode
@@ -317,16 +313,16 @@ where
317313
}
318314

319315
// This time we'll hit the file-system cache.
320-
let Some((module, module_size)) = cache.fs_cache.load(checksum, &cache.runtime_engine)?
316+
let Some(cached_module) = cache
317+
.fs_cache
318+
.load(checksum, Some(self.instance_memory_limit))?
321319
else {
322320
return Err(VmError::generic_err(
323321
"Can't load module from file system cache after storing it to file system cache (pin)",
324322
));
325323
};
326324

327-
cache
328-
.pinned_memory_cache
329-
.store(checksum, module, module_size)
325+
cache.pinned_memory_cache.store(checksum, cached_module)
330326
}
331327

332328
/// Unpins a Module, i.e. removes it from the pinned memory cache.
@@ -350,10 +346,10 @@ where
350346
backend: Backend<A, S, Q>,
351347
options: InstanceOptions,
352348
) -> VmResult<Instance<A, S, Q>> {
353-
let (cached, store) = self.get_module(checksum)?;
349+
let (module, store) = self.get_module(checksum)?;
354350
let instance = Instance::from_module(
355351
store,
356-
&cached.module,
352+
&module,
357353
backend,
358354
options.gas_limit,
359355
options.print_debug,
@@ -366,36 +362,49 @@ where
366362
/// Returns a module tied to a previously saved Wasm.
367363
/// Depending on availability, this is either generated from a memory cache, file system cache or Wasm code.
368364
/// This is part of `get_instance` but pulled out to reduce the locking time.
369-
fn get_module(&self, checksum: &Checksum) -> VmResult<(CachedModule, Store)> {
365+
fn get_module(&self, checksum: &Checksum) -> VmResult<(Module, Store)> {
370366
let mut cache = self.inner.lock().unwrap();
371367
// Try to get module from the pinned memory cache
372368
if let Some(element) = cache.pinned_memory_cache.load(checksum)? {
373369
cache.stats.hits_pinned_memory_cache =
374370
cache.stats.hits_pinned_memory_cache.saturating_add(1);
375-
let store = Store::new(cache.runtime_engine.clone());
376-
return Ok((element, store));
371+
let CachedModule {
372+
module,
373+
engine,
374+
size_estimate: _,
375+
} = element;
376+
let store = Store::new(engine);
377+
return Ok((module, store));
377378
}
378379

379380
// Get module from memory cache
380381
if let Some(element) = cache.memory_cache.load(checksum)? {
381382
cache.stats.hits_memory_cache = cache.stats.hits_memory_cache.saturating_add(1);
382-
let store = Store::new(cache.runtime_engine.clone());
383-
return Ok((element, store));
383+
let CachedModule {
384+
module,
385+
engine,
386+
size_estimate: _,
387+
} = element;
388+
let store = Store::new(engine);
389+
return Ok((module, store));
384390
}
385391

386392
// Get module from file system cache
387-
if let Some((module, module_size)) = cache.fs_cache.load(checksum, &cache.runtime_engine)? {
393+
if let Some(cached_module) = cache
394+
.fs_cache
395+
.load(checksum, Some(self.instance_memory_limit))?
396+
{
388397
cache.stats.hits_fs_cache = cache.stats.hits_fs_cache.saturating_add(1);
389398

390-
cache
391-
.memory_cache
392-
.store(checksum, module.clone(), module_size)?;
393-
let cached = CachedModule {
399+
cache.memory_cache.store(checksum, cached_module.clone())?;
400+
401+
let CachedModule {
394402
module,
395-
size_estimate: module_size,
396-
};
397-
let store = Store::new(cache.runtime_engine.clone());
398-
return Ok((cached, store));
403+
engine,
404+
size_estimate: _,
405+
} = cached_module;
406+
let store = Store::new(engine);
407+
return Ok((module, store));
399408
}
400409

401410
// Re-compile module from wasm
@@ -414,21 +423,23 @@ where
414423
}
415424

416425
// This time we'll hit the file-system cache.
417-
let Some((module, module_size)) = cache.fs_cache.load(checksum, &cache.runtime_engine)?
426+
let Some(cached_module) = cache
427+
.fs_cache
428+
.load(checksum, Some(self.instance_memory_limit))?
418429
else {
419430
return Err(VmError::generic_err(
420431
"Can't load module from file system cache after storing it to file system cache (get_module)",
421432
));
422433
};
423-
cache
424-
.memory_cache
425-
.store(checksum, module.clone(), module_size)?;
426-
let cached = CachedModule {
434+
cache.memory_cache.store(checksum, cached_module.clone())?;
435+
436+
let CachedModule {
427437
module,
428-
size_estimate: module_size,
429-
};
430-
let store = Store::new(cache.runtime_engine.clone());
431-
Ok((cached, store))
438+
engine,
439+
size_estimate: _,
440+
} = cached_module;
441+
let store = Store::new(engine);
442+
Ok((module, store))
432443
}
433444
}
434445

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,30 @@
1-
use wasmer::Module;
1+
use wasmer::{Engine, Module};
2+
3+
/// Some manual tests on Simon's machine showed that Engine is roughly 3-5 KB big,
4+
/// so give it a constant 10 KiB estimate.
5+
#[inline]
6+
pub fn engine_size_estimate() -> usize {
7+
10 * 1024
8+
}
29

310
#[derive(Debug, Clone)]
411
pub struct CachedModule {
512
pub module: Module,
13+
/// The runtime engine to run this module. Ideally we could use a single engine
14+
/// for all modules but the memory issue described in <https://github.com/wasmerio/wasmer/issues/4377>
15+
/// requires using one engine per module as a workaround.
16+
pub engine: Engine,
617
/// The estimated size of this element in memory.
718
/// Since the cached modules are just [rkyv](https://rkyv.org/) dumps of the Module
819
/// instances, we use the file size of the module on disk (not the Wasm!)
920
/// as an estimate for this.
10-
/// Note: Since CosmWasm 1.4 (Wasmer 4), Store/Engine are not cached anymore.
11-
/// The majority of the Module size is the Artifact.
21+
///
22+
/// Between CosmWasm 1.4 (Wasmer 4) and 1.5.2, Store/Engine were not cached. This lead to a
23+
/// memory consumption problem. From 1.5.2 on, Module and Engine are cached and Store is created
24+
/// from Engine on demand.
25+
///
26+
/// The majority of the Module size is the Artifact which is why we use the module filesize as the estimate.
27+
/// Some manual tests on Simon's machine showed that Engine is roughly 3-5 KB big, so give it a constant
28+
/// estimate: [`engine_size_estimate`].
1229
pub size_estimate: usize,
1330
}

packages/vm/src/modules/file_system_cache.rs

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@ use std::panic::catch_unwind;
55
use std::path::{Path, PathBuf};
66
use thiserror::Error;
77

8-
use wasmer::{AsEngineRef, DeserializeError, Module, Target};
8+
use wasmer::{DeserializeError, Module, Target};
99

1010
use crate::checksum::Checksum;
1111
use crate::errors::{VmError, VmResult};
1212

1313
use crate::filesystem::mkdir_p;
1414
use crate::modules::current_wasmer_module_version;
15+
use crate::wasm_backend::make_runtime_engine;
16+
use crate::Size;
17+
18+
use super::cached_module::engine_size_estimate;
19+
use super::CachedModule;
1520

1621
/// Bump this version whenever the module system changes in a way
1722
/// that old stored modules would be corrupt when loaded in the new system.
@@ -130,24 +135,29 @@ impl FileSystemCache {
130135
path
131136
}
132137

133-
/// Loads a serialized module from the file system and returns a module (i.e. artifact + store),
134-
/// along with the size of the serialized module.
138+
/// Loads a serialized module from the file system and returns a Module + Engine,
139+
/// along with a size estimation for the pair.
135140
pub fn load(
136141
&self,
137142
checksum: &Checksum,
138-
engine: &impl AsEngineRef,
139-
) -> VmResult<Option<(Module, usize)>> {
143+
memory_limit: Option<Size>,
144+
) -> VmResult<Option<CachedModule>> {
140145
let file_path = self.module_file(checksum);
141146

147+
let engine = make_runtime_engine(memory_limit);
142148
let result = if self.unchecked_modules {
143-
unsafe { Module::deserialize_from_file_unchecked(engine, &file_path) }
149+
unsafe { Module::deserialize_from_file_unchecked(&engine, &file_path) }
144150
} else {
145-
unsafe { Module::deserialize_from_file(engine, &file_path) }
151+
unsafe { Module::deserialize_from_file(&engine, &file_path) }
146152
};
147153
match result {
148154
Ok(module) => {
149155
let module_size = module_size(&file_path)?;
150-
Ok(Some((module, module_size)))
156+
Ok(Some(CachedModule {
157+
module,
158+
engine,
159+
size_estimate: module_size + engine_size_estimate(),
160+
}))
151161
}
152162
Err(DeserializeError::Io(err)) => match err.kind() {
153163
io::ErrorKind::NotFound => Ok(None),
@@ -229,7 +239,7 @@ mod tests {
229239
use super::*;
230240
use crate::{
231241
size::Size,
232-
wasm_backend::{compile, make_compiling_engine, make_runtime_engine},
242+
wasm_backend::{compile, make_compiling_engine},
233243
};
234244
use tempfile::TempDir;
235245
use wasmer::{imports, Instance as WasmerInstance, Store};
@@ -256,8 +266,7 @@ mod tests {
256266
let checksum = Checksum::generate(&wasm);
257267

258268
// Module does not exist
259-
let runtime_engine = make_runtime_engine(TESTING_MEMORY_LIMIT);
260-
let cached = cache.load(&checksum, &runtime_engine).unwrap();
269+
let cached = cache.load(&checksum, TESTING_MEMORY_LIMIT).unwrap();
261270
assert!(cached.is_none());
262271

263272
// Store module
@@ -266,14 +275,21 @@ mod tests {
266275
cache.store(&checksum, &module).unwrap();
267276

268277
// Load module
269-
let cached = cache.load(&checksum, &runtime_engine).unwrap();
278+
let cached = cache.load(&checksum, TESTING_MEMORY_LIMIT).unwrap();
270279
assert!(cached.is_some());
271280

272281
// Check the returned module is functional.
273282
// This is not really testing the cache API but better safe than sorry.
274283
{
275-
let (cached_module, module_size) = cached.unwrap();
276-
assert_eq!(module_size, module.serialize().unwrap().len());
284+
let CachedModule {
285+
module: cached_module,
286+
engine: runtime_engine,
287+
size_estimate,
288+
} = cached.unwrap();
289+
assert_eq!(
290+
size_estimate,
291+
module.serialize().unwrap().len() + 10240 /* engine size estimate */
292+
);
277293
let import_object = imports! {};
278294
let mut store = Store::new(runtime_engine);
279295
let instance = WasmerInstance::new(&mut store, &cached_module, &import_object).unwrap();
@@ -318,20 +334,25 @@ mod tests {
318334
let checksum = Checksum::generate(&wasm);
319335

320336
// Store module
321-
let engine1 = make_compiling_engine(TESTING_MEMORY_LIMIT);
322-
let module = compile(&engine1, &wasm).unwrap();
337+
let compiling_engine = make_compiling_engine(TESTING_MEMORY_LIMIT);
338+
let module = compile(&compiling_engine, &wasm).unwrap();
323339
cache.store(&checksum, &module).unwrap();
324340

325341
// It's there
326-
let engine2 = make_runtime_engine(TESTING_MEMORY_LIMIT);
327-
assert!(cache.load(&checksum, &engine2).unwrap().is_some());
342+
assert!(cache
343+
.load(&checksum, TESTING_MEMORY_LIMIT)
344+
.unwrap()
345+
.is_some());
328346

329347
// Remove module
330348
let existed = cache.remove(&checksum).unwrap();
331349
assert!(existed);
332350

333351
// it's gone now
334-
assert!(cache.load(&checksum, &engine2).unwrap().is_none());
352+
assert!(cache
353+
.load(&checksum, TESTING_MEMORY_LIMIT)
354+
.unwrap()
355+
.is_none());
335356

336357
// Remove again
337358
let existed = cache.remove(&checksum).unwrap();

0 commit comments

Comments
 (0)