Repository: bytecodealliance/wasmtime
Pull request #8038 (2024-03-01 18:48:33)

Add commentary on advantages/disadvantages of the pooling allocator
Inspired by discussion on #8034

<!--
Please make sure you include the following information:

- If this work has been discussed elsewhere, please include a link to that
  conversation. If it was discussed in an issue, just mention "issue #...".

- Explain why this change is needed. If the details are in an issue already,
  this can be brief.

Our development process is documented in the Wasmtime book:
https://docs.wasmtime.dev/contributing-development-process.html

Please ensure all communication follows the code of conduct:
https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md
-->


alexcrichton: I also added a commit here for commentary on the disadvantages of `memory_init_cow` with respect to extra IPIs it may generate.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 1699a0744..def959fc7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -333,9 +333,9 @@ dependencies = [
 
 [[package]]
 name = "capstone"
-version = "0.9.0"
+version = "0.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "178b3c80b4ce3939792d1dd6600575e012da806a10e81abe812dc29cbfe629ec"
+checksum = "b08ca438d9585a2b216b0c2e88ea51e096286c5f197f7be2526bb515ef775b6c"
 dependencies = [
  "capstone-sys",
  "libc",
@@ -343,9 +343,9 @@ dependencies = [
 
 [[package]]
 name = "capstone-sys"
-version = "0.13.0"
+version = "0.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf2f3d97b88fa4a9a6464c7c08b8c4588aaea8c18d0651eca11f2ca15f50f1f6"
+checksum = "fe7183271711ffb7c63a6480e4baf480e0140da59eeba9b18fcc8bf3478950e3"
 dependencies = [
  "cc",
  "libc",
diff --git a/Cargo.toml b/Cargo.toml
index a3f26a5f1..6412d17a7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -251,7 +251,7 @@ env_logger = "0.10"
 log = { version = "0.4.8", default-features = false }
 clap = { version = "4.3.12", default-features = false, features = ["std", "derive"] }
 hashbrown = { version = "0.14", default-features = false }
-capstone = "0.9.0"
+capstone = "0.12.0"
 once_cell = "1.12.0"
 smallvec = { version = "1.6.1", features = ["union"] }
 tracing = "0.1.26"
diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs
index 2b03d21b7..821636a53 100644
--- a/crates/cli-flags/src/lib.rs
+++ b/crates/cli-flags/src/lib.rs
@@ -81,6 +81,26 @@ wasmtime_option_group! {
         /// Configure attempting to initialize linear memory via a
         /// copy-on-write mapping (default: yes)
         pub memory_init_cow: Option<bool>,
+
+        /// The maximum number of WebAssembly instances which can be created
+        /// with the pooling allocator.
+        pub pooling_total_core_instances: Option<u32>,
+
+        /// The maximum number of WebAssembly components which can be created
+        /// with the pooling allocator.
+        pub pooling_total_component_instances: Option<u32>,
+
+        /// The maximum number of WebAssembly memories which can be created with
+        /// the pooling allocator.
+        pub pooling_total_memories: Option<u32>,
+
+        /// The maximum number of WebAssembly tables which can be created with
+        /// the pooling allocator.
+        pub pooling_total_tables: Option<u32>,
+
+        /// The maximum number of WebAssembly stacks which can be created with
+        /// the pooling allocator.
+        pub pooling_total_stacks: Option<u32>,
     }
 
     enum Optimize {
@@ -528,6 +548,21 @@ impl CommonOptions {
                     if let Some(size) = self.opts.pooling_table_keep_resident {
                         cfg.table_keep_resident(size);
                     }
+                    if let Some(limit) = self.opts.pooling_total_core_instances {
+                        cfg.total_core_instances(limit);
+                    }
+                    if let Some(limit) = self.opts.pooling_total_component_instances {
+                        cfg.total_component_instances(limit);
+                    }
+                    if let Some(limit) = self.opts.pooling_total_memories {
+                        cfg.total_memories(limit);
+                    }
+                    if let Some(limit) = self.opts.pooling_total_tables {
+                        cfg.total_tables(limit);
+                    }
+                    if let Some(limit) = self.opts.pooling_total_stacks {
+                        cfg.total_stacks(limit);
+                    }
                     if let Some(enable) = self.opts.memory_protection_keys {
                         if enable {
                             cfg.memory_protection_keys(wasmtime::MpkEnabled::Enable);
diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs
index 51d8258c1..b9a22efdf 100644
--- a/crates/wasmtime/src/config.rs
+++ b/crates/wasmtime/src/config.rs
@@ -1526,6 +1526,15 @@ impl Config {
     /// if a module is instantiated many times this can lower the overall memory
     /// required needed to run that module.
     ///
+    /// The main disadvantage of copy-on-write initialization, however, is that
+    /// it may be possible for highly-parallel scenarios to be less scalable. If
+    /// a page is read initially by a WebAssembly module then that page will be
+    /// mapped to a read-only copy shared between all WebAssembly instances. If
+    /// the same page is then written, however, then a private copy is created
+    /// and swapped out from the read-only version. This also requires an [IPI],
+    /// however, which can be a significant bottleneck in high-parallelism
+    /// situations.
+    ///
     /// This feature is only applicable when a WebAssembly module meets specific
     /// criteria to be initialized in this fashion, such as:
     ///
@@ -1554,6 +1563,7 @@ impl Config {
     ///
     /// [`Module::deserialize_file`]: crate::Module::deserialize_file
     /// [`Module`]: crate::Module
+    /// [IPI]: https://en.wikipedia.org/wiki/Inter-processor_interrupt
     pub fn memory_init_cow(&mut self, enable: bool) -> &mut Self {
         self.memory_init_cow = enable;
         self
@@ -2076,6 +2086,91 @@ pub enum WasmBacktraceDetails {
 ///
 /// This structure has a builder-style API in the same manner as [`Config`] and
 /// is configured with [`Config::allocation_strategy`].
+///
+/// Note that usage of the pooling allocator does not affect compiled
+/// WebAssembly code. Compiled `*.cwasm` files, for example, are usable both
+/// with and without the pooling allocator.
+///
+/// ## Advantages of Pooled Allocation
+///
+/// The main benefit of the pooling allocator is to make WebAssembly
+/// instantiation both faster and more scalable in terms of parallelism.
+/// Allocation is faster because virtual memory is already configured and ready
+/// to go within the pool, there's no need to [`mmap`] (for example on Unix) a
+/// new region and configure it with guard pages. By avoiding [`mmap`] this
+/// avoids whole-process virtual memory locks which can improve scalability and
+/// performance through avoiding this.
+///
+/// Additionally with pooled allocation it's possible to create "affine slots"
+/// to a particular WebAssembly module or component over time. For example if
+/// the same module is multiple times over time the pooling allocator will, by
+/// default, attempt to reuse the same slot. This mean that the slot has been
+/// pre-configured and can retain virtual memory mappings for a copy-on-write
+/// image, for example (see [`Config::memory_init_cow`] for more information.
+/// This means that in a steady state instance deallocation is a single
+/// [`madvise`] to reset linear memory to its original contents followed by a
+/// single (optional) [`mprotect`] during the next instantiation to shrink
+/// memory back to its original size. Compared to non-pooled allocation this
+/// avoids the need to [`mmap`] a new region of memory, [`munmap`] it, and
+/// [`mprotect`] regions too.
+///
+/// Another benefit of pooled allocation is that it's possible to configure
+/// things such that no virtual memory management is required at all in a steady
+/// state. For example a pooling allocator can be configured with
+/// [`Config::memory_init_cow`] disabledd, dynamic bounds checks enabled
+/// through
+/// [`Config::static_memory_maximum_size(0)`](Config::static_memory_maximum_size),
+/// and sufficient space through
+/// [`PoolingAllocationConfig::table_keep_resident`] /
+/// [`PoolingAllocationConfig::linear_memory_keep_resident`]. With all these
+/// options in place no virtual memory tricks are used at all and everything is
+/// manually managed by Wasmtime (for example resetting memory is a
+/// `memset(0)`). This is not as fast in a single-threaded scenario but can
+/// provide benefits in high-parallelism situations as no virtual memory locks
+/// or IPIs need happen.
+///
+/// ## Disadvantages of Pooled Allocation
+///
+/// Despite the above advantages to instantiation performance the pooling
+/// allocator is not enabled by default in Wasmtime. One reason is that the
+/// performance advantages are not necessarily portable, for example while the
+/// pooling allocator works on Windows it has not been tuned for performance on
+/// Windows in the same way it has on Linux.
+///
+/// Additionally the main cost of the pooling allocator is that it requires a
+/// very large reservation of virtual memory (on the order of most of the
+/// addressable virtual address space). WebAssembly 32-bit linear memories in
+/// Wasmtime are, by default 4G address space reservations with a 2G guard
+/// region both before and after the linear memory. Memories in the pooling
+/// allocator are contiguous which means that we only need a guard after linear
+/// memory because the previous linear memory's slot post-guard is our own
+/// pre-guard. This means that, by default, the pooling allocator uses 6G of
+/// virtual memory per WebAssembly linear memory slot. 6G of virtual memory is
+/// 32.5 bits of a 64-bit address. Many 64-bit systems can only actually use
+/// 48-bit addresses by default (although this can be extended on architectures
+/// nowadays too), and of those 48 bits one of them is reserved to indicate
+/// kernel-vs-userspace. This leaves 47-32.5=14.5 bits left, meaning you can
+/// only have at most 64k slots of linear memories on many systems by default.
+/// This is a relatively small number and shows how the pooling allocator can
+/// quickly exhaust all of virtual memory.
+///
+/// Another disadvantage of the pooling allocator is that it may keep memory
+/// alive when nothing is using it. A previously used slot for an instance might
+/// have paged-in memory that will not get paged out until the
+/// [`Engine`](crate::Engine) owning the pooling allocator is dropped. While
+/// suitable for some applications this behavior may not be suitable for all
+/// applications.
+///
+/// Finally the last disadvantage of the pooling allocator is that the
+/// configuration values for the maximum number of instances, memories, tables,
+/// etc, must all be fixed up-front. There's not always a clear answer as to
+/// what these values should be so not all applications may be able to work
+/// with this constraint.
+///
+/// [`madvise`]: https://man7.org/linux/man-pages/man2/madvise.2.html
+/// [`mprotect`]: https://man7.org/linux/man-pages/man2/mprotect.2.html
+/// [`mmap`]: https://man7.org/linux/man-pages/man2/mmap.2.html
+/// [`munmap`]: https://man7.org/linux/man-pages/man2/munmap.2.html
 #[cfg(feature = "pooling-allocator")]
 #[derive(Debug, Clone, Default)]
 pub struct PoolingAllocationConfig {
diff --git a/src/commands/serve.rs b/src/commands/serve.rs
index f11f6bc73..85e2a40c5 100644
--- a/src/commands/serve.rs
+++ b/src/commands/serve.rs
@@ -385,8 +385,7 @@ async fn handle_request(
 
     let (sender, receiver) = tokio::sync::oneshot::channel();
 
-    // TODO: need to track the join handle, but don't want to block the response on it
-    tokio::task::spawn(async move {
+    let task = tokio::task::spawn(async move {
         let req_id = inner.next_req_id();
         let (mut parts, body) = req.into_parts();
 
@@ -450,7 +449,20 @@ async fn handle_request(
     match receiver.await {
         Ok(Ok(resp)) => Ok(resp),
         Ok(Err(e)) => Err(e.into()),
-        Err(_) => bail!("guest never invoked `response-outparam::set` method"),
+        Err(_) => {
+            // An error in the receiver (`RecvError`) only indicates that the
+            // task exited before a response was sent (i.e., the sender was
+            // dropped); it does not describe the underlying cause of failure.
+            // Instead we retrieve and propagate the error from inside the task
+            // which should more clearly tell the user what went wrong. Note
+            // that we assume the task has already exited at this point so the
+            // `await` should resolve immediately.
+            let e = match task.await {
+                Ok(r) => r.expect_err("if the receiver has an error, the task must have failed"),
+                Err(e) => e.into(),
+            };
+            bail!("guest never invoked `response-outparam::set` method: {e:?}")
+        }
     }
 }
 
diff --git a/supply-chain/config.toml b/supply-chain/config.toml
index 685b1ca91..455588585 100644
--- a/supply-chain/config.toml
+++ b/supply-chain/config.toml
@@ -205,11 +205,11 @@ version = "1.1.0"
 criteria = "safe-to-deploy"
 
 [[exemptions.capstone]]
-version = "0.9.0"
+version = "0.12.0"
 criteria = "safe-to-deploy"
 
 [[exemptions.capstone-sys]]
-version = "0.13.0"
+version = "0.16.0"
 criteria = "safe-to-deploy"
 
 [[exemptions.cast]]


Test patch:
diff --git a/winch/filetests/src/disasm.rs b/winch/filetests/src/disasm.rs
index 5c921d041..a140f2b8e 100644
--- a/winch/filetests/src/disasm.rs
+++ b/winch/filetests/src/disasm.rs
@@ -33,6 +33,7 @@ pub fn disasm(
 
             let is_jump = detail
                 .groups()
+                .iter()
                 .find(|g| g.0 as u32 == CS_GRP_JUMP)
                 .is_some();
 
@@ -67,8 +68,12 @@ pub fn disasm(
 
             // Flip write_offsets to true once we've seen a `ret`, as instructions that follow the
             // return are often related to trap tables.
-            write_offsets =
-                write_offsets || detail.groups().find(|g| g.0 as u32 == CS_GRP_RET).is_some();
+            write_offsets = write_offsets
+                || detail
+                    .groups()
+                    .iter()
+                    .find(|g| g.0 as u32 == CS_GRP_RET)
+                    .is_some();
 
             line
         })