Skip to content

Commit 42bb677

Browse files
authored
Improve errors with pooling-by-default serve command (#10483)
* Improve errors with pooling-by-default `serve` command This commit is intended to address #10482 where the defaults of the `wasmtime serve` subcommand produced a confusing and surprising error. Specifically the `-Wmax-table-elements` option, prior to this change, only affected the store limiter used and didn't actually affect the pooling allocator settings. That meant that if a module exceeded the limits of the pooling allocator it would produce an error message that seemed like `-Wmax-table-elements` would fix but it wouldn't actually. Two changes in this commit are meant to address this: * Errors from the pooling allocator have been update to mention "pooling allocator" within them somewhere to surface where the error is coming from. * The `-Wmax-memory-size` and `-Wmax-table-elements` configuration are now applied to the pooling allocator automatically if the corresponding `-Opooling-*` option isn't passed. That should mean that the original error should be a bit easier to debug while the attempted solution will also work. * Fix test expectations * Get new test passing on 32-bit
1 parent 9547f2f commit 42bb677

File tree

6 files changed

+110
-85
lines changed

6 files changed

+110
-85
lines changed

crates/cli-flags/src/lib.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,9 @@ impl CommonOptions {
826826
if let Some(limit) = self.opts.pooling_total_tables {
827827
cfg.total_tables(limit);
828828
}
829-
if let Some(limit) = self.opts.pooling_table_elements {
829+
if let Some(limit) = self.opts.pooling_table_elements
830+
.or(self.wasm.max_table_elements)
831+
{
830832
cfg.table_elements(limit);
831833
}
832834
if let Some(limit) = self.opts.pooling_max_core_instance_size {
@@ -837,7 +839,9 @@ impl CommonOptions {
837839
limit => cfg.total_stacks(limit),
838840
_ => err,
839841
}
840-
if let Some(max) = self.opts.pooling_max_memory_size {
842+
if let Some(max) = self.opts.pooling_max_memory_size
843+
.or(self.wasm.max_memory_size)
844+
{
841845
cfg.max_memory_size(max);
842846
}
843847
if let Some(size) = self.opts.pooling_decommit_batch_size {

crates/wasmtime/src/runtime/vm/instance/allocator/pooling.rs

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -147,19 +147,24 @@ pub struct InstanceLimits {
147147

148148
impl Default for InstanceLimits {
149149
fn default() -> Self {
150+
let total = if cfg!(target_pointer_width = "32") {
151+
100
152+
} else {
153+
1000
154+
};
150155
// See doc comments for `wasmtime::PoolingAllocationConfig` for these
151156
// default values
152157
Self {
153-
total_component_instances: 1000,
158+
total_component_instances: total,
154159
component_instance_size: 1 << 20, // 1 MiB
155-
total_core_instances: 1000,
160+
total_core_instances: total,
156161
max_core_instances_per_component: u32::MAX,
157162
max_memories_per_component: u32::MAX,
158163
max_tables_per_component: u32::MAX,
159-
total_memories: 1000,
160-
total_tables: 1000,
164+
total_memories: total,
165+
total_tables: total,
161166
#[cfg(feature = "async")]
162-
total_stacks: 1000,
167+
total_stacks: total,
163168
core_instance_size: 1 << 20, // 1 MiB
164169
max_tables_per_module: 1,
165170
// NB: in #8504 it was seen that a C# module in debug module can
@@ -169,9 +174,9 @@ impl Default for InstanceLimits {
169174
#[cfg(target_pointer_width = "64")]
170175
max_memory_size: 1 << 32, // 4G,
171176
#[cfg(target_pointer_width = "32")]
172-
max_memory_size: usize::MAX,
177+
max_memory_size: 10 << 20, // 10 MiB
173178
#[cfg(feature = "gc")]
174-
total_gc_heaps: 1000,
179+
total_gc_heaps: total,
175180
}
176181
}
177182
}
@@ -497,7 +502,8 @@ unsafe impl InstanceAllocatorImpl for PoolingInstanceAllocator {
497502
offsets: &VMComponentOffsets<HostPtr>,
498503
get_module: &'a dyn Fn(StaticModuleIndex) -> &'a Module,
499504
) -> Result<()> {
500-
self.validate_component_instance_size(offsets)?;
505+
self.validate_component_instance_size(offsets)
506+
.context("component instance size does not fit in pooling allocator requirements")?;
501507

502508
let mut num_core_instances = 0;
503509
let mut num_memories = 0;
@@ -533,23 +539,23 @@ unsafe impl InstanceAllocatorImpl for PoolingInstanceAllocator {
533539
{
534540
bail!(
535541
"The component transitively contains {num_core_instances} core module instances, \
536-
which exceeds the configured maximum of {}",
542+
which exceeds the configured maximum of {} in the pooling allocator",
537543
self.limits.max_core_instances_per_component
538544
);
539545
}
540546

541547
if num_memories > usize::try_from(self.limits.max_memories_per_component).unwrap() {
542548
bail!(
543549
"The component transitively contains {num_memories} Wasm linear memories, which \
544-
exceeds the configured maximum of {}",
550+
exceeds the configured maximum of {} in the pooling allocator",
545551
self.limits.max_memories_per_component
546552
);
547553
}
548554

549555
if num_tables > usize::try_from(self.limits.max_tables_per_component).unwrap() {
550556
bail!(
551557
"The component transitively contains {num_tables} tables, which exceeds the \
552-
configured maximum of {}",
558+
configured maximum of {} in the pooling allocator",
553559
self.limits.max_tables_per_component
554560
);
555561
}
@@ -558,9 +564,12 @@ unsafe impl InstanceAllocatorImpl for PoolingInstanceAllocator {
558564
}
559565

560566
fn validate_module_impl(&self, module: &Module, offsets: &VMOffsets<HostPtr>) -> Result<()> {
561-
self.validate_memory_plans(module)?;
562-
self.validate_table_plans(module)?;
563-
self.validate_core_instance_size(offsets)?;
567+
self.validate_memory_plans(module)
568+
.context("module memory does not fit in pooling allocator requirements")?;
569+
self.validate_table_plans(module)
570+
.context("module table does not fit in pooling allocator requirements")?;
571+
self.validate_core_instance_size(offsets)
572+
.context("module instance size does not fit in pooling allocator requirements")?;
564573
Ok(())
565574
}
566575

tests/all/cli_tests.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2405,3 +2405,33 @@ fn compilation_logs() -> Result<()> {
24052405
}
24062406
Ok(())
24072407
}
2408+
2409+
#[test]
2410+
fn big_table_in_pooling_allocator() -> Result<()> {
2411+
// Works by default
2412+
run_wasmtime(&["tests/all/cli_tests/big_table.wat"])?;
2413+
2414+
// Does not work by default in the pooling allocator, and the error message
2415+
// should mention something about the pooling allocator.
2416+
let output = run_wasmtime_for_output(
2417+
&["-Opooling-allocator", "tests/all/cli_tests/big_table.wat"],
2418+
None,
2419+
)?;
2420+
assert!(!output.status.success());
2421+
println!("{}", String::from_utf8_lossy(&output.stderr));
2422+
assert!(String::from_utf8_lossy(&output.stderr).contains("pooling allocator"));
2423+
2424+
// Does work with `-Wmax-table-elements`
2425+
run_wasmtime(&[
2426+
"-Opooling-allocator",
2427+
"-Wmax-table-elements=25000",
2428+
"tests/all/cli_tests/big_table.wat",
2429+
])?;
2430+
// Also works with `-Opooling-table-elements`
2431+
run_wasmtime(&[
2432+
"-Opooling-allocator",
2433+
"-Opooling-table-elements=25000",
2434+
"tests/all/cli_tests/big_table.wat",
2435+
])?;
2436+
Ok(())
2437+
}

tests/all/cli_tests/big_table.wat

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
(module
2+
(table 25000 funcref)
3+
)

tests/all/main.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,17 @@ pub(crate) fn gc_store() -> wasmtime::Result<wasmtime::Store<()>> {
118118
let engine = wasmtime::Engine::new(&config)?;
119119
Ok(wasmtime::Store::new(&engine, ()))
120120
}
121+
122+
trait ErrorExt {
123+
fn assert_contains(&self, msg: &str);
124+
}
125+
126+
impl ErrorExt for anyhow::Error {
127+
fn assert_contains(&self, msg: &str) {
128+
if self.chain().any(|e| e.to_string().contains(msg)) {
129+
return;
130+
}
131+
132+
panic!("failed to find {msg:?} within error message {self:?}")
133+
}
134+
}

tests/all/pooling_allocator.rs

Lines changed: 34 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::skip_pooling_allocator_tests;
1+
use super::{skip_pooling_allocator_tests, ErrorExt};
22
use wasmtime::*;
33

44
#[test]
@@ -35,18 +35,17 @@ fn memory_limit() -> Result<()> {
3535
// Module should fail to instantiate because it has too many memories
3636
match Module::new(&engine, r#"(module (memory 1) (memory 1))"#) {
3737
Ok(_) => panic!("module instantiation should fail"),
38-
Err(e) => assert_eq!(
39-
e.to_string(),
40-
"defined memories count of 2 exceeds the per-instance limit of 1",
41-
),
38+
Err(e) => {
39+
e.assert_contains("defined memories count of 2 exceeds the per-instance limit of 1")
40+
}
4241
}
4342

4443
// Module should fail to instantiate because the minimum is greater than
4544
// the configured limit
4645
match Module::new(&engine, r#"(module (memory 4))"#) {
4746
Ok(_) => panic!("module instantiation should fail"),
48-
Err(e) => assert_eq!(
49-
e.to_string(),
47+
Err(e) =>
48+
e.assert_contains(
5049
"memory index 0 has a minimum byte size of 262144 which exceeds the limit of 0x30000 bytes",
5150
),
5251
}
@@ -244,18 +243,16 @@ fn table_limit() -> Result<()> {
244243
// Module should fail to instantiate because it has too many tables
245244
match Module::new(&engine, r#"(module (table 1 funcref) (table 1 funcref))"#) {
246245
Ok(_) => panic!("module compilation should fail"),
247-
Err(e) => assert_eq!(
248-
e.to_string(),
249-
"defined tables count of 2 exceeds the per-instance limit of 1",
250-
),
246+
Err(e) => {
247+
e.assert_contains("defined tables count of 2 exceeds the per-instance limit of 1")
248+
}
251249
}
252250

253251
// Module should fail to instantiate because the minimum is greater than
254252
// the configured limit
255253
match Module::new(&engine, r#"(module (table 31 funcref))"#) {
256254
Ok(_) => panic!("module compilation should fail"),
257-
Err(e) => assert_eq!(
258-
e.to_string(),
255+
Err(e) => e.assert_contains(
259256
"table index 0 has a minimum element size of 31 which exceeds the limit of 10",
260257
),
261258
}
@@ -634,26 +631,14 @@ fn instance_too_large() -> Result<()> {
634631
config.allocation_strategy(pool);
635632

636633
let engine = Engine::new(&config)?;
637-
let expected = if cfg!(feature = "wmemcheck") {
638-
"\
639-
instance allocation for this module requires 336 bytes which exceeds the \
640-
configured maximum of 16 bytes; breakdown of allocation requirement:
641-
642-
* 76.19% - 256 bytes - instance state management
643-
* 21.43% - 72 bytes - static vmctx data
644-
"
645-
} else {
646-
"\
647-
instance allocation for this module requires 240 bytes which exceeds the \
648-
configured maximum of 16 bytes; breakdown of allocation requirement:
649-
650-
* 66.67% - 160 bytes - instance state management
651-
* 30.00% - 72 bytes - static vmctx data
652-
"
653-
};
654634
match Module::new(&engine, "(module)") {
655635
Ok(_) => panic!("should have failed to compile"),
656-
Err(e) => assert_eq!(e.to_string(), expected),
636+
Err(e) => {
637+
e.assert_contains("exceeds the configured maximum of 16 bytes");
638+
e.assert_contains("breakdown of allocation requirement");
639+
e.assert_contains("instance state management");
640+
e.assert_contains("static vmctx data");
641+
}
657642
}
658643

659644
let mut lots_of_globals = format!("(module");
@@ -662,26 +647,14 @@ configured maximum of 16 bytes; breakdown of allocation requirement:
662647
}
663648
lots_of_globals.push_str(")");
664649

665-
let expected = if cfg!(feature = "wmemcheck") {
666-
"\
667-
instance allocation for this module requires 1936 bytes which exceeds the \
668-
configured maximum of 16 bytes; breakdown of allocation requirement:
669-
670-
* 13.22% - 256 bytes - instance state management
671-
* 82.64% - 1600 bytes - defined globals
672-
"
673-
} else {
674-
"\
675-
instance allocation for this module requires 1840 bytes which exceeds the \
676-
configured maximum of 16 bytes; breakdown of allocation requirement:
677-
678-
* 8.70% - 160 bytes - instance state management
679-
* 86.96% - 1600 bytes - defined globals
680-
"
681-
};
682650
match Module::new(&engine, &lots_of_globals) {
683651
Ok(_) => panic!("should have failed to compile"),
684-
Err(e) => assert_eq!(e.to_string(), expected),
652+
Err(e) => {
653+
e.assert_contains("exceeds the configured maximum of 16 bytes");
654+
e.assert_contains("breakdown of allocation requirement");
655+
e.assert_contains("defined globals");
656+
e.assert_contains("instance state management");
657+
}
685658
}
686659

687660
Ok(())
@@ -879,10 +852,9 @@ fn component_instance_size_limit() -> Result<()> {
879852

880853
match wasmtime::component::Component::new(&engine, "(component)") {
881854
Ok(_) => panic!("should have hit limit"),
882-
Err(e) => assert_eq!(
883-
e.to_string(),
884-
"instance allocation for this component requires 48 bytes of `VMComponentContext` space \
885-
which exceeds the configured maximum of 1 bytes"
855+
Err(e) => e.assert_contains(
856+
"instance allocation for this component requires 48 bytes of \
857+
`VMComponentContext` space which exceeds the configured maximum of 1 bytes",
886858
),
887859
}
888860

@@ -1047,10 +1019,9 @@ fn component_core_instances_limit() -> Result<()> {
10471019
"#,
10481020
) {
10491021
Ok(_) => panic!("should have hit limit"),
1050-
Err(e) => assert_eq!(
1051-
e.to_string(),
1022+
Err(e) => e.assert_contains(
10521023
"The component transitively contains 2 core module instances, which exceeds the \
1053-
configured maximum of 1"
1024+
configured maximum of 1",
10541025
),
10551026
}
10561027

@@ -1090,10 +1061,9 @@ fn component_memories_limit() -> Result<()> {
10901061
"#,
10911062
) {
10921063
Ok(_) => panic!("should have hit limit"),
1093-
Err(e) => assert_eq!(
1094-
e.to_string(),
1064+
Err(e) => e.assert_contains(
10951065
"The component transitively contains 2 Wasm linear memories, which exceeds the \
1096-
configured maximum of 1"
1066+
configured maximum of 1",
10971067
),
10981068
}
10991069

@@ -1133,10 +1103,9 @@ fn component_tables_limit() -> Result<()> {
11331103
"#,
11341104
) {
11351105
Ok(_) => panic!("should have hit limit"),
1136-
Err(e) => assert_eq!(
1137-
e.to_string(),
1106+
Err(e) => e.assert_contains(
11381107
"The component transitively contains 2 tables, which exceeds the \
1139-
configured maximum of 1"
1108+
configured maximum of 1",
11401109
),
11411110
}
11421111

@@ -1281,13 +1250,9 @@ fn shared_memory_unsupported() -> Result<()> {
12811250
"#,
12821251
)
12831252
.unwrap_err();
1284-
let err = err.to_string();
1285-
assert!(
1286-
err.contains(
1287-
"memory index 0 is shared which is not supported \
1288-
in the pooling allocator"
1289-
),
1290-
"bad error: {err}"
1253+
err.assert_contains(
1254+
"memory index 0 is shared which is not supported \
1255+
in the pooling allocator",
12911256
);
12921257
Ok(())
12931258
}

0 commit comments

Comments
 (0)