Skip to content
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

Module instances are leaked #3930

Closed
vapourismo opened this issue May 30, 2023 · 11 comments
Closed

Module instances are leaked #3930

vapourismo opened this issue May 30, 2023 · 11 comments
Assignees
Labels
priority-medium Medium priority issue
Milestone

Comments

@vapourismo
Copy link
Contributor

vapourismo commented May 30, 2023

Describe the bug

Created module instances are leaked.

See culprits in Instruments:

 173.16 KB     100.0%	683	 	start
 173.16 KB     100.0%	683	 	 main
 173.16 KB     100.0%	683	 	  wasm_instance_new
 173.16 KB     100.0%	683	 	   wasmer::sys::instance::Instance::new_by_index::h3f778f08f235f2e5
 167.16 KB      96.5%	682	 	    wasmer::sys::module::Module::instantiate::h578fd9e49215478a
 167.16 KB      96.5%	682	 	     wasmer_compiler::engine::artifact::Artifact::instantiate::h8700c6ef8eb7c0a3
 138.33 KB      79.8%	227	 	      wasmer_vm::instance::allocator::InstanceAllocator::new::hbded1a5c0a954d87
  28.83 KB      16.6%	455	 	      wasmer_compiler::engine::tunables::Tunables::create_memories::h040daa7098df58ec
  21.28 KB      12.2%	227	 	       wasmer_vm::memory::VMMemory::from_definition::hb61ab796d08ad056
   4.00 KB       2.3%	1	 	       wasmer_vm::store::InternalStoreHandle$LT$T$GT$::new::hdfab1d2e9f056009
   4.00 KB       2.3%	1	 	        alloc::raw_vec::RawVec$LT$T$C$A$GT$::reserve_for_push::h119af399b1a83ede
   4.00 KB       2.3%	1	 	         alloc::raw_vec::finish_grow::hfeb597ee13fa8ad2
   4.00 KB       2.3%	1	 	          realloc
   6.00 KB       3.4%	1	 	    alloc::raw_vec::RawVec$LT$T$C$A$GT$::reserve_for_push::h9ed7b6522f5c4f3d
   6.00 KB       3.4%	1	 	     alloc::raw_vec::finish_grow::hb394925605ecddb5
   6.00 KB       3.4%	1	 	      realloc

Wasmer: 3.3.0
Rust: 1.66.0
Platform: macOS 13.4

Steps to reproduce

Minimal C program to try with:

#include <wasm.h>
#include <wasmer.h>
#include <stdio.h>
#include <unistd.h>

int main(void) {
  wasm_engine_t* engine = wasm_engine_new();
  wasm_store_t* store = wasm_store_new(engine);

  wasm_byte_vec_t text_code;
  wasm_name_new_from_string(&text_code, "(module (memory 100))");

  wasm_byte_vec_t binary_code;
  wat2wasm(&text_code, &binary_code);

  wasm_module_t* module = wasm_module_new(store, &binary_code);
  assert(module != NULL);

  while (true) {
    puts("Start");

    wasm_trap_t* trap = NULL;
    wasm_extern_vec_t imports = WASM_EMPTY_VEC;
    wasm_instance_t* instance = wasm_instance_new(store, module, &imports, &trap);
    assert(instance != NULL);
    assert(trap == NULL);
    wasm_instance_delete(instance);

    puts("Stop");
    sleep(1);
  }

  return 0;
}

Compile and run

cc -Wall test-wasmer.c -lwasmer -o test-wasmer && ./test-wasmer

Minimal Rust program:

use std::{error::Error, thread::sleep, time::Duration};
use wasmer::{Imports, Instance, Module, Store};

fn main() -> Result<(), Box<dyn Error>> {
    let mut store = Store::default();
    let module = Module::new(
        &store,
        r#"
            (module
                (memory 100))
        "#,
    )?;

    let imports = Imports::new();

    loop {
        eprintln!("Start");
        let _instance = Instance::new(&mut store, &module, &imports)?;
        eprintln!("Stop");
        sleep(Duration::from_secs(1));
    }
}

Observe memory growing over time.

Expected behavior

Memory usage does not grow over time.

Actual behavior

Memory grows unboundedly.

@ptitSeb ptitSeb self-assigned this May 30, 2023
@ptitSeb ptitSeb added this to the v4.0 milestone May 30, 2023
@vapourismo vapourismo changed the title Module instances are leaked in C API Module instances are leaked May 31, 2023
@vapourismo
Copy link
Contributor Author

The same program in the equivalent Rust also leaks the WebAssembly module instance.

use std::{error::Error, thread::sleep, time::Duration};
use wasmer::{Imports, Instance, Module, Store};

fn main() -> Result<(), Box<dyn Error>> {
    let mut store = Store::default();
    let module = Module::new(
        &store,
        r#"
            (module
                (memory 100))
        "#,
    )?;

    let imports = Imports::new();

    loop {
        eprintln!("Start");
        let _instance = Instance::new(&mut store, &module, &imports)?;
        eprintln!("Stop");
        sleep(Duration::from_secs(1));
    }
}

Retained allocations:

  94.70 KB     100.0%	354	 	start
  94.70 KB     100.0%	354	 	 main
  94.70 KB     100.0%	354	 	  std::rt::lang_start::h8a864340956ea73e
  94.70 KB     100.0%	354	 	   std::rt::lang_start_internal::h659a783147314d97
  94.70 KB     100.0%	354	 	    std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::h32830ebb33b87c49
  94.70 KB     100.0%	354	 	     std::sys_common::backtrace::__rust_begin_short_backtrace::hbbc90449e1076fda
  94.70 KB     100.0%	354	 	      core::ops::function::FnOnce::call_once::h41c3b0b17f3523ef
  94.70 KB     100.0%	354	 	       rust_playground::main::hbbbd94805847f966
  94.70 KB     100.0%	354	 	        wasmer::instance::Instance::new::h6bdd1061175f1d44
  94.70 KB     100.0%	354	 	         wasmer::sys::instance::Instance::new::h76bd49b143a05a02
  88.70 KB      93.6%	353	 	          wasmer::sys::module::Module::instantiate::hf18419641ac1372c
  88.70 KB      93.6%	353	 	           wasmer_compiler::engine::artifact::Artifact::instantiate::h7f532addf0921d41
  71.91 KB      75.9%	118	 	            wasmer_vm::instance::allocator::InstanceAllocator::new::h62c8077ab4e852b1
  71.91 KB      75.9%	118	 	             alloc::alloc::alloc::h4f7fb60f769e3933
  16.80 KB      17.7%	235	 	            wasmer_compiler::engine::tunables::Tunables::create_memories::hd6ece2df5d9f30ed
  10.97 KB      11.5%	117	 	             _$LT$wasmer_compiler..engine..tunables..BaseTunables$u20$as$u20$wasmer_compiler..engine..tunables..Tunables$GT$::create_vm_memory::h538753d2d3dc66c1
  10.97 KB      11.5%	117	 	              wasmer_vm::memory::VMMemory::from_definition::h88f1066ce9ceb522
  10.97 KB      11.5%	117	 	               alloc::alloc::exchange_malloc::h269ed50e23f313fb
  10.97 KB      11.5%	117	 	                alloc::alloc::Global::alloc_impl::h4844f7025e303f0e
   4.00 KB       4.2%	1	 	             wasmer_vm::store::InternalStoreHandle$LT$T$GT$::new::h9065e52632f8844f
   4.00 KB       4.2%	1	 	              alloc::vec::Vec$LT$T$C$A$GT$::push::h41fcf55556e50ad5
   4.00 KB       4.2%	1	 	               alloc::raw_vec::RawVec$LT$T$C$A$GT$::reserve_for_push::h47bc85bf1fe6befc
   4.00 KB       4.2%	1	 	                alloc::raw_vec::RawVec$LT$T$C$A$GT$::grow_amortized::hb5f415c4f3e622c8
   4.00 KB       4.2%	1	 	                 alloc::raw_vec::finish_grow::h5071d316d85d9cdb
   4.00 KB       4.2%	1	 	                  _$LT$alloc..alloc..Global$u20$as$u20$core..alloc..Allocator$GT$::grow::hda0c0c0d7d10c23c
   4.00 KB       4.2%	1	 	                   alloc::alloc::Global::grow_impl::h81c276b1dcbef28f
   4.00 KB       4.2%	1	 	                    realloc
   1.83 KB       1.9%	117	 	             wasmer_types::entity::primary_map::PrimaryMap$LT$K$C$V$GT$::with_capacity::h6b102908a7d1534a
   1.83 KB       1.9%	117	 	              alloc::vec::Vec$LT$T$GT$::with_capacity::h1f60ebba99e42f60
   1.83 KB       1.9%	117	 	               alloc::raw_vec::RawVec$LT$T$C$A$GT$::allocate_in::hf5cce045a50fa59c
   1.83 KB       1.9%	117	 	                _$LT$alloc..alloc..Global$u20$as$u20$core..alloc..Allocator$GT$::allocate::he0ae021f29788cb9
   1.83 KB       1.9%	117	 	                 alloc::alloc::Global::alloc_impl::h5bf2748ed16e505d
   6.00 KB       6.3%	1	 	          wasmer_vm::store::StoreHandle$LT$T$GT$::new::hefd74e1832b76bde
   6.00 KB       6.3%	1	 	           wasmer_vm::store::InternalStoreHandle$LT$T$GT$::new::hf24e63a1361d33d4
   6.00 KB       6.3%	1	 	            alloc::vec::Vec$LT$T$C$A$GT$::push::h622e523569e78c19
   6.00 KB       6.3%	1	 	             alloc::raw_vec::RawVec$LT$T$C$A$GT$::reserve_for_push::h058125264ac10b91
   6.00 KB       6.3%	1	 	              alloc::raw_vec::RawVec$LT$T$C$A$GT$::grow_amortized::hfdae73102e1a6b4e
   6.00 KB       6.3%	1	 	               alloc::raw_vec::finish_grow::hc8d59e69bc12b7a8
   6.00 KB       6.3%	1	 	                _$LT$alloc..alloc..Global$u20$as$u20$core..alloc..Allocator$GT$::grow::hc35b145c71eb3dad
   6.00 KB       6.3%	1	 	                 alloc::alloc::Global::grow_impl::h49a156dda5e36f25
   6.00 KB       6.3%	1	 	                  realloc

@ptitSeb
Copy link
Contributor

ptitSeb commented May 31, 2023

Oh, so it's not c-api specific! I'll try to do some valgrind analysis, see if I can identify the source of the leak

@vapourismo
Copy link
Contributor Author

I believe this is somewhat unsurprising, but re-allocating the Store with a cloned Engine gets rid of the leak.

use std::{error::Error, thread::sleep, time::Duration};
use wasmer::{Engine, Imports, Instance, Module, Store};

fn main() -> Result<(), Box<dyn Error>> {
    let engine = Engine::default();
    let module = Module::new(
        &engine,
        r#"
            (module
                (memory 100))
        "#,
    )?;

    let imports = Imports::new();

    loop {
        {
            eprintln!("Start");
            let mut store = Store::new(engine.clone());
            let _instance = Instance::new(&mut store, &module, &imports)?;
            eprintln!("Stop");
        }

        sleep(Duration::from_secs(1));
    }
}

Purely guessing, but maybe there is a cyclic reference between the VM Store and VM Instance's objects.

@vapourismo
Copy link
Contributor Author

Actually if it were properly cyclic, it would most likely leak the memory anyway, despite re-creating the store.

@theduke
Copy link
Contributor

theduke commented Jun 1, 2023

So, the problem here is that Instance is essentially just an index pointing to some state in the Store.

Store holds all the actual data.

In a certain sense this is by design.

Stores are really cheap to construct, and there is currently no real motivation for using a single Store with multiple instances, that I'd be aware of.

So this is expected behaviour, not a leak.
The memory will be freed when Store is dropped.

I do understand that this might be unexpected behaviour though, that also isn't clearly documented.

There also is no way to clean up the relevant data from a store if you would want to re-use it, but offering such a cleanup would come with complications, due to Instance being clone.

@theduke
Copy link
Contributor

theduke commented Jun 1, 2023

So I'm leaning towards "this is expected behaviour", and thus primarily a documentation issue.

This might change in the future though when we want to utilize stores to share code and internal state between instances (like in the component model), but this isn't currently the case.

@vapourismo
Copy link
Contributor Author

I figured as much. Yet, I am still surprised at this flaw.

There are major problems with this, if it is really by design.

C API

While creating a Module in Wasmer doesn't require the Store itself, the standard C API enforces this:

WASM_API_EXTERN own wasm_module_t* wasm_module_new(
  wasm_store_t*, const wasm_byte_vec_t* binary);

From a C perspective this makes it really awkward when you need to re-create the Module for each Store while the Module hasn't actually changed.
In reality of course you can keep the resulting Module around even after destroying the Store given it doesn't store anything in it and only accesses the Engine. However, this may change in the future, right?

Re-creating the Store isn't always possible

Imagine a scenario where I have a few "library" instances, that I need to keep around for their state hence need to stay in the store, and many short-lived instances that depend on the "library" instances. I would be paying a massive memory price if these short-lived instances allocate a few gigabytes of memory every time.

I feel like there are options that would improve the situation significantly:

  1. Purge store objects when they become inaccessible. For example: when a VMMemory object becomes inaccessible, reset its size to 0 and free the underlying space. This way you pay the price for the entry in Vec<VMMemory> but not the price of the underlying object.
  2. Remove the inaccessible tail of entries from the vector of objects when the object at the end of the vector becomes inaccessible. This is most likely only useful for 1-instance scenarios or when the instance dependencies are entirely linear.

@theduke
Copy link
Contributor

theduke commented Jun 1, 2023

We have had similar discussions, but there are some design changes that would be needed and it would potentially complicate the Rust API quite a bit, or introduce some performance tradeoffs, so that's part of a larger discussion.

Types like Instance and others currently are really just indexes, not ref counts or similar, so cloning them is basically free, but that also means there is no way to determine if something has become inaccessible. Introducing some (atomic) ref counting here wouldn't be terribly significant, but there are some intricacies to get this right without significantly complicating the Rust API. It would probably also require something like a wasmer_store_garbage_collect() function in C to trigger a cleanup.

In reality of course you can keep the resulting Module around even after destroying the Store given it doesn't store anything in it and only accesses the Engine. However, this may change in the future, right?

We inherited the C API from the upstream standardized https://github.com/WebAssembly/wasm-c-api .
For Wasmer a Module is basically a compilation artifact , with no runtime state whatsoever, and is just tied to the Engine, not to a Store at all, and I don't see this changing in the future.

@vapourismo
Copy link
Contributor Author

vapourismo commented Jun 1, 2023

Understand that the standardised API constraints you, but would you consider a wasmer_module_new which only needs the engine - to manifest that Wasmer doesn't require the store for module creation?

WASM_API_EXTERN own wasm_module_t* wasmer_module_new(
  wasm_engine_t*, const wasm_byte_vec_t* binary);

@ptitSeb
Copy link
Contributor

ptitSeb commented Jun 6, 2023

Looks like a good idea, if you can do a Pull Request about that, we will merged it.

@ptitSeb
Copy link
Contributor

ptitSeb commented Aug 17, 2023

Was merged. Closing.

@ptitSeb ptitSeb closed this as completed Aug 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
priority-medium Medium priority issue
Projects
None yet
Development

No branches or pull requests

3 participants