wasmtime_environ/tunables.rs
1use crate::prelude::*;
2use crate::{IndexType, Limits, Memory, TripleExt};
3use core::{fmt, str::FromStr};
4use serde_derive::{Deserialize, Serialize};
5use target_lexicon::{PointerWidth, Triple};
6use wasmparser::Operator;
7
8macro_rules! define_tunables {
9 (
10 $(#[$outer_attr:meta])*
11 pub struct $tunables:ident {
12 $(
13 $(#[$field_attr:meta])*
14 pub $field:ident : $field_ty:ty,
15 )*
16 }
17
18 pub struct $config_tunables:ident {
19 ...
20 }
21 ) => {
22 $(#[$outer_attr])*
23 pub struct $tunables {
24 $(
25 $(#[$field_attr])*
26 pub $field: $field_ty,
27 )*
28 }
29
30 /// Optional tunable configuration options used in `wasmtime::Config`
31 #[derive(Default, Clone)]
32 #[expect(missing_docs, reason = "macro-generated fields")]
33 pub struct $config_tunables {
34 $(pub $field: Option<$field_ty>,)*
35 }
36
37 impl $config_tunables {
38 /// Formats configured fields into `f`.
39 pub fn format(&self, f: &mut fmt::DebugStruct<'_,'_>) {
40 $(
41 if let Some(val) = &self.$field {
42 f.field(stringify!($field), val);
43 }
44 )*
45 }
46
47 /// Configure the `Tunables` provided.
48 pub fn configure(&self, tunables: &mut Tunables) {
49 $(
50 if let Some(val) = &self.$field {
51 tunables.$field = val.clone();
52 }
53 )*
54 }
55 }
56 };
57}
58
59define_tunables! {
60 /// Tunable parameters for WebAssembly compilation.
61 #[derive(Clone, Hash, Serialize, Deserialize, Debug)]
62 pub struct Tunables {
63 /// The garbage collector implementation to use, which implies the layout of
64 /// GC objects and barriers that must be emitted in Wasm code.
65 pub collector: Option<Collector>,
66
67 /// Initial size, in bytes, to be allocated for linear memories.
68 pub memory_reservation: u64,
69
70 /// The size, in bytes, of the guard page region for linear memories.
71 pub memory_guard_size: u64,
72
73 /// The size, in bytes, to allocate at the end of a relocated linear
74 /// memory for growth.
75 pub memory_reservation_for_growth: u64,
76
77 /// Whether or not to generate native DWARF debug information.
78 pub debug_native: bool,
79
80 /// Whether we are enabling precise Wasm-level debugging in
81 /// the guest.
82 pub debug_guest: bool,
83
84 /// Whether or not to retain DWARF sections in compiled modules.
85 pub parse_wasm_debuginfo: bool,
86
87 /// Whether or not fuel is enabled for generated code, meaning that fuel
88 /// will be consumed every time a wasm instruction is executed.
89 pub consume_fuel: bool,
90
91 /// The cost of each operator. If fuel is not enabled, this is ignored.
92 pub operator_cost: OperatorCostStrategy,
93
94 /// Whether or not we use epoch-based interruption.
95 pub epoch_interruption: bool,
96
97 /// Whether or not linear memories are allowed to be reallocated after
98 /// initial allocation at runtime.
99 pub memory_may_move: bool,
100
101 /// Whether or not linear memory allocations will have a guard region at the
102 /// beginning of the allocation in addition to the end.
103 pub guard_before_linear_memory: bool,
104
105 /// Whether to initialize tables lazily, so that instantiation is fast but
106 /// indirect calls are a little slower. If false, tables are initialized
107 /// eagerly from any active element segments that apply to them during
108 /// instantiation.
109 pub table_lazy_init: bool,
110
111 /// Indicates whether an address map from compiled native code back to wasm
112 /// offsets in the original file is generated.
113 pub generate_address_map: bool,
114
115 /// Flag for the component module whether adapter modules have debug
116 /// assertions baked into them.
117 pub debug_adapter_modules: bool,
118
119 /// Whether or not lowerings for relaxed simd instructions are forced to
120 /// be deterministic.
121 pub relaxed_simd_deterministic: bool,
122
123 /// Whether or not Wasm functions target the winch abi.
124 pub winch_callable: bool,
125
126 /// Whether or not the host will be using native signals (e.g. SIGILL,
127 /// SIGSEGV, etc) to implement traps.
128 pub signals_based_traps: bool,
129
130 /// Whether CoW images might be used to initialize linear memories.
131 pub memory_init_cow: bool,
132
133 /// Whether to enable inlining in Wasmtime's compilation orchestration
134 /// or not.
135 pub inlining: bool,
136
137 /// Whether to inline calls within the same core Wasm module or not.
138 pub inlining_intra_module: IntraModuleInlining,
139
140 /// The size of "small callees" that can be inlined regardless of the
141 /// caller's size.
142 pub inlining_small_callee_size: u32,
143
144 /// The general size threshold for the sum of the caller's and callee's
145 /// sizes, past which we will generally not inline calls anymore.
146 pub inlining_sum_size_threshold: u32,
147
148 /// Whether any component model feature related to concurrency is
149 /// enabled.
150 pub concurrency_support: bool,
151
152 /// Whether recording in RR is enabled or not. This is used primarily
153 /// to signal checksum computation for compiled artifacts.
154 pub recording: bool,
155 }
156
157 pub struct ConfigTunables {
158 ...
159 }
160}
161
162impl Tunables {
163 /// Returns a `Tunables` configuration assumed for running code on the host.
164 pub fn default_host() -> Self {
165 if cfg!(miri) {
166 Tunables::default_miri()
167 } else if cfg!(target_pointer_width = "32") {
168 Tunables::default_u32()
169 } else if cfg!(target_pointer_width = "64") {
170 Tunables::default_u64()
171 } else {
172 panic!("unsupported target_pointer_width");
173 }
174 }
175
176 /// Returns the default set of tunables for the given target triple.
177 pub fn default_for_target(target: &Triple) -> Result<Self> {
178 if cfg!(miri) {
179 return Ok(Tunables::default_miri());
180 }
181 let mut ret = match target
182 .pointer_width()
183 .map_err(|_| format_err!("failed to retrieve target pointer width"))?
184 {
185 PointerWidth::U32 => Tunables::default_u32(),
186 PointerWidth::U64 => Tunables::default_u64(),
187 _ => bail!("unsupported target pointer width"),
188 };
189
190 // Pulley targets never use signals-based-traps and also can't benefit
191 // from guard pages, so disable them.
192 if target.is_pulley() {
193 ret.signals_based_traps = false;
194 ret.memory_guard_size = 0;
195 }
196 Ok(ret)
197 }
198
199 /// Returns the default set of tunables for running under MIRI.
200 pub fn default_miri() -> Tunables {
201 Tunables {
202 collector: None,
203
204 // No virtual memory tricks are available on miri so make these
205 // limits quite conservative.
206 memory_reservation: 1 << 20,
207 memory_guard_size: 0,
208 memory_reservation_for_growth: 0,
209
210 // General options which have the same defaults regardless of
211 // architecture.
212 debug_native: false,
213 parse_wasm_debuginfo: true,
214 consume_fuel: false,
215 operator_cost: OperatorCostStrategy::Default,
216 epoch_interruption: false,
217 memory_may_move: true,
218 guard_before_linear_memory: true,
219 table_lazy_init: true,
220 generate_address_map: true,
221 debug_adapter_modules: false,
222 relaxed_simd_deterministic: false,
223 winch_callable: false,
224 signals_based_traps: false,
225 memory_init_cow: true,
226 inlining: false,
227 inlining_intra_module: IntraModuleInlining::WhenUsingGc,
228 inlining_small_callee_size: 50,
229 inlining_sum_size_threshold: 2000,
230 debug_guest: false,
231 concurrency_support: true,
232 recording: false,
233 }
234 }
235
236 /// Returns the default set of tunables for running under a 32-bit host.
237 pub fn default_u32() -> Tunables {
238 Tunables {
239 // For 32-bit we scale way down to 10MB of reserved memory. This
240 // impacts performance severely but allows us to have more than a
241 // few instances running around.
242 memory_reservation: 10 * (1 << 20),
243 memory_guard_size: 0x1_0000,
244 memory_reservation_for_growth: 1 << 20, // 1MB
245 signals_based_traps: true,
246
247 ..Tunables::default_miri()
248 }
249 }
250
251 /// Returns the default set of tunables for running under a 64-bit host.
252 pub fn default_u64() -> Tunables {
253 Tunables {
254 // 64-bit has tons of address space to static memories can have 4gb
255 // address space reservations liberally by default, allowing us to
256 // help eliminate bounds checks.
257 //
258 // A 32MiB default guard size is then allocated so we can remove
259 // explicit bounds checks if any static offset is less than this
260 // value. SpiderMonkey found, for example, that in a large corpus of
261 // wasm modules 20MiB was the maximum offset so this is the
262 // power-of-two-rounded up from that and matches SpiderMonkey.
263 memory_reservation: 1 << 32,
264 memory_guard_size: 32 << 20,
265
266 // We've got lots of address space on 64-bit so use a larger
267 // grow-into-this area, but on 32-bit we aren't as lucky. Miri is
268 // not exactly fast so reduce memory consumption instead of trying
269 // to avoid memory movement.
270 memory_reservation_for_growth: 2 << 30, // 2GB
271
272 signals_based_traps: true,
273 ..Tunables::default_miri()
274 }
275 }
276
277 /// Get the GC heap's memory type, given our configured tunables.
278 pub fn gc_heap_memory_type(&self) -> Memory {
279 Memory {
280 idx_type: IndexType::I32,
281 limits: Limits { min: 0, max: None },
282 shared: false,
283 // We *could* try to match the target architecture's page size, but that
284 // would require exercising a page size for memories that we don't
285 // otherwise support for Wasm; we conservatively avoid that, and just
286 // use the default Wasm page size, for now.
287 page_size_log2: 16,
288 }
289 }
290}
291
292/// The garbage collector implementation to use.
293#[derive(Clone, Copy, Hash, Serialize, Deserialize, Debug, PartialEq, Eq)]
294pub enum Collector {
295 /// The deferred reference-counting collector.
296 DeferredReferenceCounting,
297 /// The null collector.
298 Null,
299}
300
301impl fmt::Display for Collector {
302 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
303 match self {
304 Collector::DeferredReferenceCounting => write!(f, "deferred reference-counting"),
305 Collector::Null => write!(f, "null"),
306 }
307 }
308}
309
310/// Whether to inline function calls within the same module.
311#[derive(Clone, Copy, Hash, Serialize, Deserialize, Debug, PartialEq, Eq)]
312#[expect(missing_docs, reason = "self-describing variants")]
313pub enum IntraModuleInlining {
314 Yes,
315 No,
316 WhenUsingGc,
317}
318
319impl FromStr for IntraModuleInlining {
320 type Err = Error;
321
322 fn from_str(s: &str) -> Result<Self, Self::Err> {
323 match s {
324 "y" | "yes" | "true" => Ok(Self::Yes),
325 "n" | "no" | "false" => Ok(Self::No),
326 "gc" => Ok(Self::WhenUsingGc),
327 _ => bail!(
328 "invalid intra-module inlining option string: `{s}`, \
329 only yes,no,gc accepted"
330 ),
331 }
332 }
333}
334
335/// The cost of each operator.
336///
337/// Note: a more dynamic approach (e.g. a user-supplied callback) can be
338/// added as a variant in the future if needed.
339#[derive(Clone, Hash, Serialize, Deserialize, Debug, PartialEq, Eq, Default)]
340pub enum OperatorCostStrategy {
341 /// A table of operator costs.
342 Table(Box<OperatorCost>),
343
344 /// Each cost defaults to 1 fuel unit, except `Nop`, `Drop` and
345 /// a few control flow operators.
346 #[default]
347 Default,
348}
349
350impl OperatorCostStrategy {
351 /// Create a new operator cost strategy with a table of costs.
352 pub fn table(cost: OperatorCost) -> Self {
353 OperatorCostStrategy::Table(Box::new(cost))
354 }
355
356 /// Get the cost of an operator.
357 pub fn cost(&self, op: &Operator) -> i64 {
358 match self {
359 OperatorCostStrategy::Table(cost) => cost.cost(op),
360 OperatorCostStrategy::Default => default_operator_cost(op),
361 }
362 }
363}
364
365const fn default_operator_cost(op: &Operator) -> i64 {
366 match op {
367 // Nop and drop generate no code, so don't consume fuel for them.
368 Operator::Nop | Operator::Drop => 0,
369
370 // Control flow may create branches, but is generally cheap and
371 // free, so don't consume fuel. Note the lack of `if` since some
372 // cost is incurred with the conditional check.
373 Operator::Block { .. }
374 | Operator::Loop { .. }
375 | Operator::Unreachable
376 | Operator::Return
377 | Operator::Else
378 | Operator::End => 0,
379
380 // Everything else, just call it one operation.
381 _ => 1,
382 }
383}
384
385macro_rules! default_cost {
386 // Nop and drop generate no code, so don't consume fuel for them.
387 (Nop) => {
388 0
389 };
390 (Drop) => {
391 0
392 };
393
394 // Control flow may create branches, but is generally cheap and
395 // free, so don't consume fuel. Note the lack of `if` since some
396 // cost is incurred with the conditional check.
397 (Block) => {
398 0
399 };
400 (Loop) => {
401 0
402 };
403 (Unreachable) => {
404 0
405 };
406 (Return) => {
407 0
408 };
409 (Else) => {
410 0
411 };
412 (End) => {
413 0
414 };
415
416 // Everything else, just call it one operation.
417 ($op:ident) => {
418 1
419 };
420}
421
422macro_rules! define_operator_cost {
423 ($(@$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident ($($ann:tt)*) )*) => {
424 /// The fuel cost of each operator in a table.
425 #[derive(Clone, Hash, Serialize, Deserialize, Debug, PartialEq, Eq)]
426 #[allow(missing_docs, non_snake_case, reason = "to avoid triggering clippy lints")]
427 pub struct OperatorCost {
428 $(
429 pub $op: u8,
430 )*
431 }
432
433 impl OperatorCost {
434 /// Returns the cost of the given operator.
435 pub fn cost(&self, op: &Operator) -> i64 {
436 match op {
437 $(
438 Operator::$op $({ $($arg: _),* })? => self.$op as i64,
439 )*
440 unknown => panic!("unknown op: {unknown:?}"),
441 }
442 }
443 }
444
445 impl OperatorCost {
446 /// Creates a new `OperatorCost` table with default costs for each operator.
447 pub const fn new() -> Self {
448 Self {
449 $(
450 $op: default_cost!($op),
451 )*
452 }
453 }
454 }
455
456 impl Default for OperatorCost {
457 fn default() -> Self {
458 Self::new()
459 }
460 }
461 }
462}
463
464wasmparser::for_each_operator!(define_operator_cost);