wasmtime_runtime/instance/allocator/pooling/
index_allocator.rs1use crate::CompiledModuleId;
4use std::collections::hash_map::{Entry, HashMap};
5use std::mem;
6use std::sync::Mutex;
7
8#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq)]
11pub struct SlotId(pub u32);
12impl SlotId {
13 pub fn index(self) -> usize {
15 self.0 as usize
16 }
17}
18
19#[derive(Debug)]
20pub struct IndexAllocator(Mutex<Inner>);
21
22#[derive(Debug)]
23struct Inner {
24 max_unused_warm_slots: u32,
31
32 unused_warm_slots: u32,
37
38 warm: List,
41
42 last_cold: u32,
47
48 slot_state: Vec<SlotState>,
53
54 module_affine: HashMap<CompiledModuleId, List>,
60}
61
62#[derive(Default, Debug)]
64struct List {
65 head: Option<SlotId>,
66 tail: Option<SlotId>,
67}
68
69#[derive(Default, Debug, Copy, Clone)]
72struct Link {
73 prev: Option<SlotId>,
74 next: Option<SlotId>,
75}
76
77#[derive(Clone, Debug)]
78enum SlotState {
79 Used(Option<CompiledModuleId>),
81
82 UnusedCold,
84
85 UnusedWarm(Unused),
90}
91
92impl SlotState {
93 fn unwrap_unused(&mut self) -> &mut Unused {
94 match self {
95 SlotState::UnusedWarm(u) => u,
96 _ => unreachable!(),
97 }
98 }
99}
100
101#[derive(Default, Copy, Clone, Debug)]
102struct Unused {
103 affinity: Option<CompiledModuleId>,
105
106 affine_list_link: Link,
108
109 unused_list_link: Link,
111}
112
113enum AllocMode {
114 ForceAffineAndClear,
115 AnySlot,
116}
117
118impl IndexAllocator {
119 pub fn new(max_instances: u32, max_unused_warm_slots: u32) -> Self {
121 IndexAllocator(Mutex::new(Inner {
122 last_cold: 0,
123 max_unused_warm_slots,
124 unused_warm_slots: 0,
125 module_affine: HashMap::new(),
126 slot_state: (0..max_instances).map(|_| SlotState::UnusedCold).collect(),
127 warm: List::default(),
128 }))
129 }
130
131 pub fn alloc(&self, module_id: Option<CompiledModuleId>) -> Option<SlotId> {
136 self._alloc(module_id, AllocMode::AnySlot)
137 }
138
139 pub fn alloc_affine_and_clear_affinity(&self, module_id: CompiledModuleId) -> Option<SlotId> {
147 self._alloc(Some(module_id), AllocMode::ForceAffineAndClear)
148 }
149
150 fn _alloc(&self, module_id: Option<CompiledModuleId>, mode: AllocMode) -> Option<SlotId> {
151 let mut inner = self.0.lock().unwrap();
152 let inner = &mut *inner;
153
154 let slot_id = inner.pick_affine(module_id).or_else(|| {
158 match mode {
159 AllocMode::AnySlot => {
180 if inner.unused_warm_slots < inner.max_unused_warm_slots {
181 inner.pick_cold().or_else(|| inner.pick_warm())
182 } else {
183 inner.pick_warm().or_else(|| {
184 debug_assert!(inner.max_unused_warm_slots == 0);
185 inner.pick_cold()
186 })
187 }
188 }
189
190 AllocMode::ForceAffineAndClear => None,
195 }
196 })?;
197
198 inner.slot_state[slot_id.index()] = SlotState::Used(match mode {
199 AllocMode::ForceAffineAndClear => None,
200 AllocMode::AnySlot => module_id,
201 });
202
203 Some(slot_id)
204 }
205
206 pub(crate) fn free(&self, index: SlotId) {
207 let mut inner = self.0.lock().unwrap();
208 let inner = &mut *inner;
209 let module = match inner.slot_state[index.index()] {
210 SlotState::Used(module) => module,
211 _ => unreachable!(),
212 };
213
214 inner.unused_warm_slots += 1;
218 let unused_list_link = inner
219 .warm
220 .append(index, &mut inner.slot_state, |s| &mut s.unused_list_link);
221
222 let affine_list_link = match module {
223 Some(module) => match inner.module_affine.entry(module) {
227 Entry::Occupied(mut e) => e
228 .get_mut()
229 .append(index, &mut inner.slot_state, |s| &mut s.affine_list_link),
230 Entry::Vacant(v) => {
231 v.insert(List::new(index));
232 Link::default()
233 }
234 },
235
236 None => Link::default(),
238 };
239
240 inner.slot_state[index.index()] = SlotState::UnusedWarm(Unused {
241 affinity: module,
242 affine_list_link,
243 unused_list_link,
244 });
245 }
246
247 #[cfg(test)]
250 pub(crate) fn testing_freelist(&self) -> Vec<SlotId> {
251 let inner = self.0.lock().unwrap();
252 inner
253 .warm
254 .iter(&inner.slot_state, |s| &s.unused_list_link)
255 .collect()
256 }
257
258 #[cfg(test)]
261 pub(crate) fn testing_module_affinity_list(&self) -> Vec<CompiledModuleId> {
262 let inner = self.0.lock().unwrap();
263 inner.module_affine.keys().copied().collect()
264 }
265}
266
267impl Inner {
268 fn pick_affine(&mut self, module_id: Option<CompiledModuleId>) -> Option<SlotId> {
271 let ret = self.module_affine.get(&module_id?)?.tail?;
275 self.remove(ret);
276 Some(ret)
277 }
278
279 fn pick_warm(&mut self) -> Option<SlotId> {
280 let head = self.warm.head?;
285 self.remove(head);
286 Some(head)
287 }
288
289 fn remove(&mut self, slot: SlotId) {
290 self.unused_warm_slots -= 1;
293 self.warm
294 .remove(slot, &mut self.slot_state, |u| &mut u.unused_list_link);
295
296 let module = self.slot_state[slot.index()].unwrap_unused().affinity;
301 if let Some(module) = module {
302 let mut list = match self.module_affine.entry(module) {
303 Entry::Occupied(e) => e,
304 Entry::Vacant(_) => unreachable!(),
305 };
306 list.get_mut()
307 .remove(slot, &mut self.slot_state, |u| &mut u.affine_list_link);
308
309 if list.get_mut().head.is_none() {
310 list.remove();
311 }
312 }
313 }
314
315 fn pick_cold(&mut self) -> Option<SlotId> {
316 if (self.last_cold as usize) == self.slot_state.len() {
317 None
318 } else {
319 let ret = Some(SlotId(self.last_cold));
320 self.last_cold += 1;
321 ret
322 }
323 }
324}
325
326impl List {
327 fn new(id: SlotId) -> List {
329 List {
330 head: Some(id),
331 tail: Some(id),
332 }
333 }
334
335 fn append(
337 &mut self,
338 id: SlotId,
339 states: &mut [SlotState],
340 link: fn(&mut Unused) -> &mut Link,
341 ) -> Link {
342 let tail = mem::replace(&mut self.tail, Some(id));
344
345 match tail {
349 Some(tail) => link(states[tail.index()].unwrap_unused()).next = Some(id),
350 None => self.head = Some(id),
351 }
352 Link {
353 prev: tail,
354 next: None,
355 }
356 }
357
358 fn remove(
360 &mut self,
361 id: SlotId,
362 slot_state: &mut [SlotState],
363 link: fn(&mut Unused) -> &mut Link,
364 ) -> Unused {
365 let mut state = *slot_state[id.index()].unwrap_unused();
366 let next = link(&mut state).next;
367 let prev = link(&mut state).prev;
368
369 match next {
373 Some(next) => link(slot_state[next.index()].unwrap_unused()).prev = prev,
374 None => self.tail = prev,
375 }
376
377 match prev {
379 Some(prev) => link(slot_state[prev.index()].unwrap_unused()).next = next,
380 None => self.head = next,
381 }
382 state
383 }
384
385 #[cfg(test)]
386 fn iter<'a>(
387 &'a self,
388 states: &'a [SlotState],
389 link: fn(&Unused) -> &Link,
390 ) -> impl Iterator<Item = SlotId> + 'a {
391 let mut cur = self.head;
392 let mut prev = None;
393 std::iter::from_fn(move || {
394 if cur.is_none() {
395 assert_eq!(prev, self.tail);
396 }
397 let ret = cur?;
398 match &states[ret.index()] {
399 SlotState::UnusedWarm(u) => {
400 assert_eq!(link(u).prev, prev);
401 prev = Some(ret);
402 cur = link(u).next
403 }
404 _ => unreachable!(),
405 }
406 Some(ret)
407 })
408 }
409}
410
411#[cfg(test)]
412mod test {
413 use super::{IndexAllocator, SlotId};
414 use crate::CompiledModuleIdAllocator;
415
416 #[test]
417 fn test_next_available_allocation_strategy() {
418 for size in 0..20 {
419 let state = IndexAllocator::new(size, 0);
420 for i in 0..size {
421 assert_eq!(state.alloc(None).unwrap().index(), i as usize);
422 }
423 assert!(state.alloc(None).is_none());
424 }
425 }
426
427 #[test]
428 fn test_affinity_allocation_strategy() {
429 let id_alloc = CompiledModuleIdAllocator::new();
430 let id1 = id_alloc.alloc();
431 let id2 = id_alloc.alloc();
432 let state = IndexAllocator::new(100, 100);
433
434 let index1 = state.alloc(Some(id1)).unwrap();
435 assert_eq!(index1.index(), 0);
436 let index2 = state.alloc(Some(id2)).unwrap();
437 assert_eq!(index2.index(), 1);
438 assert_ne!(index1, index2);
439
440 state.free(index1);
441 let index3 = state.alloc(Some(id1)).unwrap();
442 assert_eq!(index3, index1);
443 state.free(index3);
444
445 state.free(index2);
446
447 let affinity_modules = state.testing_module_affinity_list();
449 assert_eq!(2, affinity_modules.len());
450 assert!(affinity_modules.contains(&id1));
451 assert!(affinity_modules.contains(&id2));
452
453 let mut indices = vec![];
459 for _ in 0..100 {
460 indices.push(state.alloc(Some(id2)).unwrap());
461 }
462 assert!(state.alloc(None).is_none());
463 assert_eq!(indices[0], index2);
464
465 for i in indices {
466 state.free(i);
467 }
468
469 let affinity_modules = state.testing_module_affinity_list();
471 assert_eq!(1, affinity_modules.len());
472 assert!(affinity_modules.contains(&id2));
473
474 let index = state.alloc(Some(id1)).unwrap();
477 state.free(index);
478 }
479
480 #[test]
481 fn clear_affine() {
482 let id_alloc = CompiledModuleIdAllocator::new();
483 let id = id_alloc.alloc();
484
485 for max_unused_warm_slots in [0, 1, 2] {
486 let state = IndexAllocator::new(100, max_unused_warm_slots);
487
488 let index1 = state.alloc(Some(id)).unwrap();
489 let index2 = state.alloc(Some(id)).unwrap();
490 state.free(index2);
491 state.free(index1);
492 assert!(state.alloc_affine_and_clear_affinity(id).is_some());
493 assert!(state.alloc_affine_and_clear_affinity(id).is_some());
494 assert_eq!(state.alloc_affine_and_clear_affinity(id), None);
495 }
496 }
497
498 #[test]
499 fn test_affinity_allocation_strategy_random() {
500 use rand::Rng;
501 let mut rng = rand::thread_rng();
502
503 let id_alloc = CompiledModuleIdAllocator::new();
504 let ids = std::iter::repeat_with(|| id_alloc.alloc())
505 .take(10)
506 .collect::<Vec<_>>();
507 let state = IndexAllocator::new(1000, 1000);
508 let mut allocated: Vec<SlotId> = vec![];
509 let mut last_id = vec![None; 1000];
510
511 let mut hits = 0;
512 for _ in 0..100_000 {
513 loop {
514 if !allocated.is_empty() && rng.gen_bool(0.5) {
515 let i = rng.gen_range(0..allocated.len());
516 let to_free_idx = allocated.swap_remove(i);
517 state.free(to_free_idx);
518 } else {
519 let id = ids[rng.gen_range(0..ids.len())];
520 let index = match state.alloc(Some(id)) {
521 Some(id) => id,
522 None => continue,
523 };
524 if last_id[index.index()] == Some(id) {
525 hits += 1;
526 }
527 last_id[index.index()] = Some(id);
528 allocated.push(index);
529 }
530 break;
531 }
532 }
533
534 assert!(
538 hits > 20000,
539 "expected at least 20000 (20%) ID-reuses but got {}",
540 hits
541 );
542 }
543
544 #[test]
545 fn test_affinity_threshold() {
546 let id_alloc = CompiledModuleIdAllocator::new();
547 let id1 = id_alloc.alloc();
548 let id2 = id_alloc.alloc();
549 let id3 = id_alloc.alloc();
550 let state = IndexAllocator::new(10, 2);
551
552 assert_eq!(state.alloc(Some(id1)), Some(SlotId(0)));
554 state.free(SlotId(0));
555 assert_eq!(state.alloc(Some(id2)), Some(SlotId(1)));
556 state.free(SlotId(1));
557
558 assert_eq!(state.alloc(Some(id3)), Some(SlotId(0)));
561 state.free(SlotId(0));
562
563 assert_eq!(state.alloc(Some(id2)), Some(SlotId(1)));
565
566 assert_eq!(state.alloc(Some(id1)), Some(SlotId(2)));
569
570 state.free(SlotId(1));
571 state.free(SlotId(2));
572
573 assert_eq!(state.alloc(Some(id1)), Some(SlotId(2)));
575 assert_eq!(state.alloc(Some(id2)), Some(SlotId(1)));
576 assert_eq!(state.alloc(Some(id3)), Some(SlotId(0)));
577
578 state.free(SlotId(1));
579 state.free(SlotId(2));
580 state.free(SlotId(0));
581
582 assert_eq!(state.alloc(Some(id_alloc.alloc())), Some(SlotId(1)));
584
585 assert_eq!(state.alloc(Some(id_alloc.alloc())), Some(SlotId(2)));
587
588 assert_eq!(state.alloc(Some(id_alloc.alloc())), Some(SlotId(3)));
590
591 state.free(SlotId(1));
592 state.free(SlotId(2));
593 state.free(SlotId(3));
594
595 assert_eq!(state.alloc(Some(id3)), Some(SlotId(0)));
597 }
598}