Skip to main content

cubecl_server/memory_management/
memory_manage.rs

1use super::{
2    DEDICATED_POOL_POS, ManagedMemoryBinding, ManagedMemoryDescriptor, ManagedMemoryHandle,
3    MemoryAllocationMode, MemoryConfiguration, MemoryLocation, MemoryPoolsReport,
4    PERSISTENT_POOL_POS, PageGuard,
5    memory_pool::{DirectPool, MemoryPool, PageMapping, PersistentPool},
6};
7use crate::{
8    config::{
9        CubeClRuntimeConfig, RuntimeConfig,
10        memory::{MemoryLogLevel, PersistentMemory},
11    },
12    logging::ServerLogger,
13    memory_management::{BytesFormat, DynamicMemory, ErrorGraph, FailureId, memory_pool::Slice},
14    server::IoError,
15    storage::{ComputeStorage, ManagedResource, StorageHandle},
16};
17
18use crate::memory_management::relocation::{CopyQueue, RelocationNeed, RelocationReason};
19use alloc::format;
20use alloc::string::{String, ToString};
21use alloc::vec::Vec;
22use core::ops::Range;
23use cubecl_environment::backtrace::BackTrace;
24use cubecl_environment::sync::Arc;
25use cubecl_ir::MemoryDeviceProperties;
26
27/// Which pool a slice's location routes to: the two fixed sentinels, or a
28/// dynamic pool by index.
29#[derive(Clone, Copy)]
30enum PoolPosition {
31    Persistent,
32    Dedicated,
33    Dynamic(u8),
34}
35
36impl PoolPosition {
37    fn new(pool: u8) -> Self {
38        match pool {
39            PERSISTENT_POOL_POS => PoolPosition::Persistent,
40            DEDICATED_POOL_POS => PoolPosition::Dedicated,
41            index => PoolPosition::Dynamic(index),
42        }
43    }
44
45    /// The error for a location naming a dynamic pool the layout does not have.
46    fn missing(index: u8) -> IoError {
47        IoError::NotFound {
48            backtrace: BackTrace::capture(),
49            reason: format!("Memory pool {index} doesn't exist").into(),
50        }
51    }
52}
53
54/// What a reservation may do to the pages the memory holds to find room.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum PageUpdate {
57    /// Anything: the periodic cleanup runs first and may release or renumber
58    /// pages, and a page is allocated when no room is held.
59    Allow,
60    /// Only add: no page is released or renumbered, so every page keeps its
61    /// number and its address, but a page is allocated when no room is held.
62    AddOnly,
63    /// Nothing: only room the pages already hold.
64    Forbidden,
65}
66
67/// Why a cleanup runs, which decides how much it gives back.
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum Cleanup {
70    /// On the way to a reservation: each pool gives back what its own policy
71    /// says is unused, such as a page past its deallocation period or an
72    /// outdated page that emptied.
73    Periodic,
74    /// Asked for: everything nothing holds goes back.
75    Explicit,
76}
77
78/// Reserves and keeps track of chunks of memory in the storage, and slices upon these chunks.
79pub struct MemoryManagement<Storage> {
80    name: String,
81    persistent: PersistentPool,
82    /// Allocations made under [`MemoryAllocationMode::Dedicated`]: each its own
83    /// device allocation, returned to the driver on the tick after it is freed.
84    dedicated: DirectPool,
85    pools: DynamicMemory,
86    storage: Storage,
87    alloc_reserve_count: u64,
88    mode: MemoryAllocationMode,
89    /// The mode no window overrides: the configured or provided one.
90    base_mode: MemoryAllocationMode,
91    /// Open allocation windows (see [`mode`](Self::mode)), innermost last,
92    /// each holding the mode it put in force.
93    windows: Vec<MemoryAllocationMode>,
94    config: PersistentMemory,
95    logger: Arc<ServerLogger>,
96}
97
98/// The options for creating a new [`MemoryManagement`] instance.
99#[derive(Debug)]
100pub struct MemoryManagementOptions {
101    /// The name of the memory management.
102    name: String,
103    /// The [`MemoryAllocationOption`] used by this instance.
104    memory: MemoryAllocationOption,
105}
106
107impl MemoryManagementOptions {
108    /// Creates a new [`MemoryManagementOptions`].
109    pub fn new<S: Into<String>>(name: S) -> Self {
110        Self {
111            name: name.into(),
112            memory: MemoryAllocationOption::FromConfig,
113        }
114    }
115
116    /// Forces the [`MemoryAllocationMode`] during execution to always be the provided one.
117    pub fn mode(mut self, mode: MemoryAllocationMode) -> Self {
118        self.memory = MemoryAllocationOption::Provided(mode);
119        self
120    }
121}
122
123#[derive(Default, Debug)]
124/// Determines which [`MemoryAllocationMode`] is used during allocations.
125enum MemoryAllocationOption {
126    #[default]
127    /// Uses the [`GlobalConfig`] to determine the mode of allocation.
128    FromConfig,
129    /// Use the provided [`MemoryAllocationMode`].
130    Provided(MemoryAllocationMode),
131}
132
133impl<Storage: ComputeStorage> MemoryManagement<Storage> {
134    /// Creates the options from device limits.
135    pub fn from_configuration(
136        storage: Storage,
137        properties: &MemoryDeviceProperties,
138        config: MemoryConfiguration,
139        logger: Arc<ServerLogger>,
140        options: MemoryManagementOptions,
141    ) -> Self {
142        let pools = DynamicMemory::new(properties, config, logger.clone(), options.name.clone());
143
144        let config = CubeClRuntimeConfig::get().memory.persistent_memory.clone();
145
146        let mode = match options.memory {
147            MemoryAllocationOption::Provided(mode) => mode,
148            MemoryAllocationOption::FromConfig => match config {
149                PersistentMemory::Enabled | PersistentMemory::SizeMatch => {
150                    MemoryAllocationMode::Auto
151                }
152                PersistentMemory::Disabled => MemoryAllocationMode::Auto,
153                PersistentMemory::Enforced => MemoryAllocationMode::Persistent,
154            },
155        };
156
157        Self {
158            name: options.name,
159            persistent: PersistentPool::new(
160                properties.max_page_size,
161                properties.alignment,
162                PERSISTENT_POOL_POS,
163            ),
164            // A watermark of zero: every free slice goes back on the next tick.
165            dedicated: DirectPool::new(properties.alignment, DEDICATED_POOL_POS, Some(0)),
166            pools,
167            storage,
168            alloc_reserve_count: 0,
169            mode,
170            base_mode: mode,
171            windows: Vec::new(),
172            config,
173            logger,
174        }
175    }
176
177    /// Change the mode of allocation.
178    ///
179    /// Windows **nest**: a `Persistent` or `Dedicated` call opens one, an
180    /// `Auto` call closes the innermost, and the innermost open window decides
181    /// the effective mode. Callers routinely nest without knowing it — a
182    /// module load opens a persistent window around the whole load while the
183    /// parameter machinery underneath opens one per parameter — and without
184    /// the stack, the first inner window's exit would flip the rest of the
185    /// outer window back to `Auto`: weights landing in the dynamic pools,
186    /// where every page they sit on is held for the model's whole life.
187    ///
188    /// The persistent-memory config decides what a `Persistent` window puts in
189    /// force (`Disabled` and `Enforced` keep the configured mode); a
190    /// `Dedicated` window is honored whatever the config, since it exists to
191    /// keep a buffer out of every pool.
192    pub fn mode(&mut self, mode: MemoryAllocationMode) {
193        match mode {
194            MemoryAllocationMode::Auto => {
195                self.windows.pop();
196            }
197            MemoryAllocationMode::Persistent => self.windows.push(match self.config {
198                PersistentMemory::Enabled | PersistentMemory::SizeMatch => mode,
199                PersistentMemory::Disabled | PersistentMemory::Enforced => self.base_mode,
200            }),
201            MemoryAllocationMode::Dedicated => self.windows.push(mode),
202        }
203        let mode = self.windows.last().copied().unwrap_or(self.base_mode);
204
205        self.logger.log_memory(
206            |level| !matches!(level, MemoryLogLevel::Disabled),
207            || {
208                format!(
209                    "[{}] Setting memory allocation mode: from {:?} => {mode:?}",
210                    self.name, self.mode
211                )
212            },
213        );
214
215        self.mode = mode;
216    }
217
218    /// Cleanup allocations in pools that are deemed unnecessary.
219    pub fn cleanup(&mut self, cleanup: Cleanup, failures: &mut ErrorGraph) {
220        self.logger.log_memory(
221            |level| !matches!(level, MemoryLogLevel::Disabled) && cleanup == Cleanup::Explicit,
222            || "Manual memory cleanup ...".to_string(),
223        );
224
225        self.persistent.cleanup(
226            &mut self.storage,
227            self.alloc_reserve_count,
228            cleanup,
229            failures,
230        );
231
232        // Dedicated buffers never wait for an explicit cleanup: freed is done.
233        if self.dedicated.reclaim(&mut self.storage, failures) {
234            self.storage.flush();
235        }
236
237        self.pools.cleanup(
238            &mut self.storage,
239            self.alloc_reserve_count,
240            cleanup,
241            failures,
242        );
243
244        // The pools only queue their page deallocations in the storage; an
245        // explicit cleanup means "release the memory now", so push them to the
246        // driver instead of leaving them pending.
247        if cleanup == Cleanup::Explicit {
248            self.storage.flush();
249        }
250    }
251
252    /// Returns the storage from the specified binding
253    pub fn get_cursor(&self, binding: ManagedMemoryBinding) -> Result<u64, IoError> {
254        let slice = self.find(&binding)?;
255        Ok(slice.cursor)
256    }
257
258    /// The allocation behind `binding`.
259    fn find(&self, binding: &ManagedMemoryBinding) -> Result<&Slice, IoError> {
260        let slice = self.pool(allocated(binding.descriptor())?)?.find(binding)?;
261        owned_by(slice, binding)?;
262        Ok(slice)
263    }
264
265    /// [`find`](Self::find), mutably — the path [`taint`](Self::taint) and
266    /// [`written`](Self::written) take to reach the slice.
267    fn find_mut(&mut self, binding: &ManagedMemoryBinding) -> Result<&mut Slice, IoError> {
268        let slice = self
269            .pool_mut(allocated(binding.descriptor())?)?
270            .find_mut(binding)?;
271        owned_by(slice, binding)?;
272        Ok(slice)
273    }
274
275    /// The pool `location` names.
276    fn pool(&self, location: MemoryLocation) -> Result<&dyn MemoryPool, IoError> {
277        match PoolPosition::new(location.pool) {
278            PoolPosition::Persistent => Ok(&self.persistent),
279            PoolPosition::Dedicated => Ok(&self.dedicated),
280            PoolPosition::Dynamic(index) => self
281                .pools
282                .pool(index)
283                .ok_or_else(|| PoolPosition::missing(index)),
284        }
285    }
286
287    /// The pool `location` names, mutably.
288    fn pool_mut(&mut self, location: MemoryLocation) -> Result<&mut dyn MemoryPool, IoError> {
289        match PoolPosition::new(location.pool) {
290            PoolPosition::Persistent => Ok(&mut self.persistent),
291            PoolPosition::Dedicated => Ok(&mut self.dedicated),
292            PoolPosition::Dynamic(index) => self
293                .pools
294                .pool_mut(index)
295                .ok_or_else(|| PoolPosition::missing(index)),
296        }
297    }
298
299    /// Returns the storage from the specified binding.
300    ///
301    /// This is the funnel every buffer dereference passes through
302    /// ([`get_resource`](Self::get_resource) delegates here), so it is where
303    /// a lazily-carved allocation gets its real device backing: the handle
304    /// returned always refers to mapped memory.
305    pub fn get_storage(&mut self, binding: ManagedMemoryBinding) -> Result<StorageHandle, IoError> {
306        self.materialize(&binding)?;
307        let slice = self.find(&binding)?;
308        Ok(slice.storage.clone())
309    }
310
311    /// The bytes this memory holds from the device right now, whatever pool
312    /// holds them.
313    pub fn bytes_allocated(&self) -> u64 {
314        self.storage.bytes_allocated()
315    }
316
317    /// Whether emptying the outdated pools is wanted, before the bytes the
318    /// device holds are known: only while something is outdated, when the
319    /// arena is full or the device is short of room, and not again while the
320    /// pools look as they did when the last relocation moved nothing.
321    pub fn relocation_need(&self) -> RelocationNeed {
322        self.pools.relocation_need()
323    }
324
325    /// Whether a growth left pages behind for a relocation to empty.
326    pub fn has_outdated(&self) -> bool {
327        self.pools.has_outdated()
328    }
329
330    /// Empty what the outdated pools hold into the current pages, so the
331    /// pages they held go back to the driver. `reason` decides whether a
332    /// target may take a new page.
333    pub fn relocate(
334        &mut self,
335        copier: &mut dyn CopyQueue<Storage>,
336        reason: RelocationReason,
337        failures: &mut ErrorGraph,
338    ) {
339        self.pools
340            .relocate(&mut self.storage, copier, reason, failures);
341    }
342
343    /// Keep the page `location` names as it is for as long as the guard lives
344    /// (see [`PageGuard`]). `None` when no pool holds such a page.
345    pub fn guard(&mut self, location: MemoryLocation) -> Option<PageGuard> {
346        self.pool_mut(location).ok()?.guard(location)
347    }
348
349    /// Install real backing behind `binding` when its allocation was carved
350    /// lazily under a dry run. Lookup errors are left for
351    /// [`find`](Self::find) to report with its usual diagnostics.
352    fn materialize(&mut self, binding: &ManagedMemoryBinding) -> Result<(), IoError> {
353        let location = binding.descriptor().location();
354        if location.init == 0 {
355            return Ok(());
356        }
357        match PoolPosition::new(location.pool) {
358            PoolPosition::Persistent => self.persistent.materialize(&mut self.storage, binding),
359            PoolPosition::Dedicated => self.dedicated.materialize(&mut self.storage, binding),
360            PoolPosition::Dynamic(_) => self.pools.materialize(&mut self.storage, binding),
361        }
362    }
363
364    /// The resource behind `binding`, holding the allocation's binding for as
365    /// long as it lives: what a caller that keeps the raw address past this
366    /// call is handed. The binding keeps the allocation in place, and leaves
367    /// the rest of its page to other allocations.
368    pub fn managed_resource(
369        &mut self,
370        binding: ManagedMemoryBinding,
371        offset_start: Option<u64>,
372        offset_end: Option<u64>,
373    ) -> Result<ManagedResource<Storage::Resource>, IoError> {
374        let resource = self.get_resource(binding.clone(), offset_start, offset_end)?;
375        Ok(ManagedResource::new(binding, resource))
376    }
377
378    /// Returns the resource from the storage at the specified handle
379    pub fn get_resource(
380        &mut self,
381        binding: ManagedMemoryBinding,
382        offset_start: Option<u64>,
383        offset_end: Option<u64>,
384    ) -> Result<Storage::Resource, IoError> {
385        let handle = self.get_storage(binding)?;
386
387        let handle = match offset_start {
388            Some(offset) => handle.offset_start(offset),
389            None => handle,
390        };
391        let handle = match offset_end {
392            Some(offset) => handle.offset_end(offset),
393            None => handle,
394        };
395        self.storage().get(&handle)
396    }
397
398    /// Reserve `size` bytes, changing the pages held only as far as `update`
399    /// allows.
400    ///
401    /// # Errors
402    ///
403    /// [`IoError::PageUpdateForbidden`] when `update` forbids a new page and
404    /// the room held cannot serve it, and whatever the pools or the device
405    /// refused otherwise.
406    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
407    pub fn reserve(
408        &mut self,
409        size: u64,
410        update: PageUpdate,
411        failures: &mut ErrorGraph,
412    ) -> Result<ManagedMemoryHandle, IoError> {
413        match update {
414            PageUpdate::Allow => {
415                // Drive the pools' periodic deallocation. Each pool gates
416                // itself on its own `dealloc_period` (pools without one no-op),
417                // so this is a few comparisons per reservation — without it,
418                // pages freed long ago are never returned to the driver until
419                // an explicit cleanup, which on long-running processes lets
420                // every stream's pools grow monotonically.
421                self.cleanup(Cleanup::Periodic, failures);
422                self.reserve_adding(size, failures)
423            }
424            PageUpdate::AddOnly => self.reserve_adding(size, failures),
425            PageUpdate::Forbidden => {
426                self.reserve_held(size, failures)
427                    .ok_or_else(|| IoError::PageUpdateForbidden {
428                        size,
429                        backtrace: BackTrace::capture(),
430                    })
431            }
432        }
433    }
434
435    /// Reserve `size` bytes, allocating a page when the room held has none,
436    /// without releasing or renumbering any page.
437    fn reserve_adding(
438        &mut self,
439        size: u64,
440        failures: &mut ErrorGraph,
441    ) -> Result<ManagedMemoryHandle, IoError> {
442        // If this happens every nanosecond, counts overflows after 585 years, so not worth thinking too
443        // hard about overflow here.
444        self.alloc_reserve_count += 1;
445
446        let mapping = PageMapping::current();
447
448        if matches!(self.mode, MemoryAllocationMode::Dedicated) {
449            return self
450                .dedicated
451                .alloc(&mut self.storage, size, mapping, failures);
452        }
453
454        // In an explicit persistent window the pool always serves the
455        // allocation (reusing a freed same-size slice when one exists).
456        // Outside a window, the pool participates only under the `size-match`
457        // config: recurring weight-shaped allocations reuse the buckets — a
458        // training-friendly heuristic that would otherwise pull inference
459        // activations into exact-sized persistent slices.
460        let persistent_mode = matches!(self.mode, MemoryAllocationMode::Persistent);
461        let size_match = matches!(self.config, PersistentMemory::SizeMatch);
462
463        if (persistent_mode || size_match)
464            && let Some(val) = self.persistent.try_reserve(size, failures)
465        {
466            self.logger.log_memory(
467                |level| matches!(level, MemoryLogLevel::Full),
468                || {
469                    format!(
470                        "[{}] Reserved memory {size} using persistent memory",
471                        self.name
472                    )
473                },
474            );
475            return Ok(val);
476        }
477
478        if persistent_mode || (size_match && self.persistent.has_size(size)) {
479            let allocated = self
480                .persistent
481                .alloc(&mut self.storage, size, mapping, failures);
482
483            self.logger.log_memory(
484                |level| !matches!(level, MemoryLogLevel::Disabled),
485                || {
486                    format!(
487                        "[{}] Allocated a new memory page using persistent memory, \n{}",
488                        self.name, self,
489                    )
490                },
491            );
492            return allocated;
493        }
494
495        self.logger.log_memory(
496            |level| matches!(level, MemoryLogLevel::Full),
497            || {
498                format!(
499                    "[{}] Reserved memory {} using dynamic pool",
500                    self.name,
501                    BytesFormat::new(size)
502                )
503            },
504        );
505
506        let reserved = self
507            .pools
508            .reserve(&mut self.storage, size, mapping, failures)?;
509
510        self.logger.log_memory(
511            |level| matches!(level, MemoryLogLevel::Full),
512            || {
513                format!(
514                    "[{}], Allocated a new memory page, current usage: \n{}",
515                    self.name, self
516                )
517            },
518        );
519
520        Ok(reserved)
521    }
522
523    /// Reserve `size` bytes in the room the pools already hold, the way
524    /// [`reserve_adding`](Self::reserve_adding) would route them: no cleanup,
525    /// no new page, nothing moved. `None` when that room cannot serve it.
526    fn reserve_held(
527        &mut self,
528        size: u64,
529        failures: &mut ErrorGraph,
530    ) -> Option<ManagedMemoryHandle> {
531        match self.mode {
532            MemoryAllocationMode::Dedicated => self.dedicated.try_reserve(size, failures),
533            MemoryAllocationMode::Persistent => self.persistent.try_reserve(size, failures),
534            MemoryAllocationMode::Auto => {
535                if matches!(self.config, PersistentMemory::SizeMatch)
536                    && let Some(handle) = self.persistent.try_reserve(size, failures)
537                {
538                    return Some(handle);
539                }
540                self.pools.try_reserve(size, failures)
541            }
542        }
543    }
544
545    /// Fetch the storage used by the memory manager.
546    ///
547    /// # Notes
548    ///
549    /// The storage should probably not be used for allocations since the handles won't be
550    /// compatible with the ones provided by the current trait. Prefer using the
551    /// [alloc](ComputeStorage::alloc) and [dealloc](ComputeStorage::dealloc) functions.
552    ///
553    /// This is useful if you need to time the deallocations based on async computation, or to
554    /// change the mode of storage for different reasons.
555    pub fn storage(&mut self) -> &mut Storage {
556        &mut self.storage
557    }
558
559    /// Everything this memory holds, pool by pool: each pool's shape, usage
560    /// and high-water marks, the dynamic ones in allocation-routing order.
561    /// [`MemoryPoolsReport::usage`] sums them.
562    pub fn memory_report(&self) -> MemoryPoolsReport {
563        MemoryPoolsReport {
564            dynamic: self.pools.report(),
565            persistent: self.persistent.report(),
566            dedicated: self.dedicated.report(),
567        }
568    }
569
570    /// Print out a report of the current memory usage.
571    pub fn print_memory_usage(&self) {
572        #[cfg(feature = "std")]
573        log::info!("{}", self.memory_report().usage());
574    }
575
576    /// Binds the given [handle](HandleId) to a [`MemorySlot`].
577    pub fn bind(
578        &mut self,
579        reserved: ManagedMemoryHandle,
580        assigned: ManagedMemoryHandle,
581        cursor: u64,
582        failures: &mut ErrorGraph,
583    ) -> Result<(), IoError> {
584        let location = allocated(reserved.descriptor())?;
585        self.pool_mut(location)?
586            .bind(reserved, assigned, cursor, failures)
587    }
588
589    /// The failure claiming any byte of `range` in the allocation behind
590    /// `binding`, if one does.
591    ///
592    /// This is the read path's whole check: a field on a slice the resolution
593    /// walks anyway. A binding this manager does not hold answers `None` —
594    /// lookup errors are [`find`](Self::find)'s to report, and a failed lookup
595    /// carries no failure to name.
596    pub fn failure(&self, binding: &ManagedMemoryBinding, range: Range<u64>) -> Option<FailureId> {
597        self.find(binding)
598            .ok()
599            .and_then(|slice| slice.tainted.failure(&range))
600    }
601
602    /// Point `range` of the allocation behind `binding` at `failure`.
603    ///
604    /// A binding this manager does not hold is left alone, for the same
605    /// reason [`failure`](Self::failure) answers `None` for one.
606    pub fn taint(
607        &mut self,
608        binding: &ManagedMemoryBinding,
609        range: Range<u64>,
610        failure: FailureId,
611        failures: &mut ErrorGraph,
612    ) {
613        if let Ok(slice) = self.find_mut(binding) {
614            slice.tainted.taint(range, failure, failures);
615        }
616    }
617
618    /// `range` of the allocation behind `binding` has a writer again: release
619    /// every claim on those bytes — and only those bytes, since a partial
620    /// write says nothing about the rest of the buffer.
621    pub fn written(
622        &mut self,
623        binding: &ManagedMemoryBinding,
624        range: Range<u64>,
625        failures: &mut ErrorGraph,
626    ) {
627        if let Ok(slice) = self.find_mut(binding) {
628            slice.tainted.written(range, failures);
629        }
630    }
631}
632
633/// Where the allocation `descriptor` names sits, once a reservation gave it
634/// a place.
635fn allocated(descriptor: &ManagedMemoryDescriptor) -> Result<MemoryLocation, IoError> {
636    if !descriptor.is_allocated() {
637        return Err(IoError::NotFound {
638            backtrace: BackTrace::capture(),
639            reason: "Memory location was never initialized".into(),
640        });
641    }
642    Ok(descriptor.location())
643}
644
645/// Whether `slice` still holds the allocation behind `binding`. A stale
646/// location (a page deallocated since, whose index a later cleanup reassigned)
647/// must surface as `NotFound`, never as another allocation's slice.
648fn owned_by(slice: &Slice, binding: &ManagedMemoryBinding) -> Result<(), IoError> {
649    if slice.handle.descriptor() != binding.descriptor() {
650        return Err(IoError::NotFound {
651            backtrace: BackTrace::capture(),
652            reason: "Memory location points to a different allocation".into(),
653        });
654    }
655    Ok(())
656}
657
658impl<Storage: ComputeStorage> core::fmt::Display for MemoryManagement<Storage> {
659    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
660        f.write_str("\n# MemoryManagement\n\n")?;
661        f.write_fmt(format_args!(" - name: {:?}\n", self.name))?;
662        f.write_fmt(format_args!("\n## Persistent\n\n{}", self.persistent))?;
663        f.write_str("\n## Dynamic\n\n")?;
664
665        f.write_fmt(format_args!("{}", self.pools))?;
666        let memory_usage = self.memory_report().usage();
667        f.write_fmt(format_args!("\n## Summary\n\n{memory_usage}"))?;
668
669        Ok(())
670    }
671}
672
673impl<Storage> core::fmt::Debug for MemoryManagement<Storage> {
674    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
675        f.write_str(
676            alloc::format!(
677                "DynamicMemoryManagement {:?}",
678                core::any::type_name::<Storage>(),
679            )
680            .as_str(),
681        )
682    }
683}
684
685#[cfg(test)]
686mod tests {
687    use super::*;
688    #[cfg(not(exclusive_memory_only))]
689    use crate::memory_management::MemoryPoolKind;
690    use crate::{memory_management::MemoryManagement, storage::BytesStorage};
691
692    const MIB: u64 = 1024 * 1024;
693    const DUMMY_MEM_PROPS: MemoryDeviceProperties = MemoryDeviceProperties::new(128 * MIB, 32);
694    /// The page the `Adaptive` preset's metadata pool carves.
695    #[cfg(not(exclusive_memory_only))]
696    const METADATA_PAGE: u64 = 8 * MIB;
697
698    fn options() -> MemoryManagementOptions {
699        MemoryManagementOptions {
700            name: "test".into(),
701            memory: MemoryAllocationOption::FromConfig,
702        }
703    }
704
705    /// The adaptive preset's small pool is for metadata: an allocation close
706    /// to its page size still goes to the adaptive pool, which sizes pages to
707    /// what it serves, rather than onto the metadata pages.
708    #[test_log::test]
709    #[cfg(not(exclusive_memory_only))]
710    fn adaptive_preset_routes_near_page_sizes_to_the_adaptive_pool() {
711        let mut memory = MemoryManagement::from_configuration(
712            BytesStorage::default(),
713            &DUMMY_MEM_PROPS,
714            MemoryConfiguration::Adaptive,
715            Arc::new(ServerLogger::default()),
716            options(),
717        );
718        let _near = memory
719            .reserve(7 * MIB, PageUpdate::Allow, &mut ErrorGraph::default())
720            .unwrap();
721
722        let served = memory
723            .memory_report()
724            .dynamic
725            .into_iter()
726            .find(|pool| pool.largest_alloc == 7 * MIB)
727            .expect("some pool served the allocation");
728        assert!(
729            matches!(served.kind, MemoryPoolKind::Adaptive { .. }),
730            "served by {:?}",
731            served.kind
732        );
733    }
734
735    // Test pools with slices.
736    #[test_log::test]
737    #[cfg(not(exclusive_memory_only))]
738    fn test_handle_mutability() {
739        let mut memory_management = MemoryManagement::from_configuration(
740            BytesStorage::default(),
741            &DUMMY_MEM_PROPS,
742            MemoryConfiguration::Adaptive,
743            Arc::new(ServerLogger::default()),
744            options(),
745        );
746        let handle = memory_management
747            .reserve(10, PageUpdate::Allow, &mut ErrorGraph::default())
748            .unwrap();
749        let other_ref = handle.clone();
750        assert!(!handle.can_mut(), "Handle can't be mut when multiple ref.");
751        drop(other_ref);
752        assert!(handle.can_mut(), "Handle should be mut when only one ref.");
753    }
754
755    // Test pools with slices.
756    #[test_log::test]
757    #[cfg(not(exclusive_memory_only))]
758    fn test_memory_usage() {
759        let mut memory_management = MemoryManagement::from_configuration(
760            BytesStorage::default(),
761            &DUMMY_MEM_PROPS,
762            MemoryConfiguration::Adaptive,
763            Arc::new(ServerLogger::default()),
764            options(),
765        );
766        let handle = memory_management.reserve(100, PageUpdate::Allow, &mut ErrorGraph::default());
767        let usage = memory_management.memory_report().usage();
768
769        assert_eq!(usage.bytes_in_use, 100);
770        // A metadata-sized allocation is carved from one metadata page.
771        assert_eq!(usage.bytes_reserved, METADATA_PAGE);
772
773        // Drop and re-alloc.
774        drop(handle);
775        let _handle = memory_management.reserve(100, PageUpdate::Allow, &mut ErrorGraph::default());
776        let usage_new = memory_management.memory_report().usage();
777        assert_eq!(usage, usage_new);
778    }
779
780    #[test_log::test]
781    #[cfg(not(exclusive_memory_only))]
782    fn find_uninit_binding_returns_not_found() {
783        let mut memory_management = MemoryManagement::from_configuration(
784            BytesStorage::default(),
785            &DUMMY_MEM_PROPS,
786            MemoryConfiguration::Adaptive,
787            Arc::new(ServerLogger::default()),
788            options(),
789        );
790
791        // Even with a live page at index 0, a never-initialized descriptor must
792        // not resolve to it.
793        let _live = memory_management
794            .reserve(512, PageUpdate::Allow, &mut ErrorGraph::default())
795            .unwrap();
796
797        let binding = ManagedMemoryHandle::new().binding();
798        assert!(matches!(
799            memory_management.get_cursor(binding),
800            Err(IoError::NotFound { .. })
801        ));
802    }
803
804    #[test_log::test]
805    #[cfg(not(exclusive_memory_only))]
806    fn find_stale_descriptor_returns_not_found() {
807        let mut memory_management = MemoryManagement::from_configuration(
808            BytesStorage::default(),
809            &DUMMY_MEM_PROPS,
810            MemoryConfiguration::Adaptive,
811            Arc::new(ServerLogger::default()),
812            options(),
813        );
814
815        let reserved = memory_management
816            .reserve(512, PageUpdate::Allow, &mut ErrorGraph::default())
817            .unwrap();
818        let stale = reserved.clone();
819        let assigned = ManagedMemoryHandle::new();
820        let assigned_binding = assigned.clone().binding();
821
822        memory_management
823            .bind(reserved, assigned, 0, &mut ErrorGraph::default())
824            .unwrap();
825
826        // The slice's identity is now `assigned`; the stale reserved descriptor
827        // must surface as `NotFound`, not as the new allocation's data.
828        assert!(matches!(
829            memory_management.get_cursor(stale.binding()),
830            Err(IoError::NotFound { .. })
831        ));
832        assert!(memory_management.get_cursor(assigned_binding).is_ok());
833    }
834
835    #[test_log::test]
836    fn held_binding_survives_explicit_cleanup_renumber() {
837        // Exclusive pages: one page per allocation, so freeing two of three
838        // leaves the pool with pages to drop and one to renumber.
839        let mut memory_management = MemoryManagement::from_configuration(
840            BytesStorage::default(),
841            &DUMMY_MEM_PROPS,
842            MemoryConfiguration::ExclusivePages,
843            Arc::new(ServerLogger::default()),
844            options(),
845        );
846
847        let handle_a = memory_management
848            .reserve(1024, PageUpdate::Allow, &mut ErrorGraph::default())
849            .unwrap();
850        let handle_b = memory_management
851            .reserve(1024, PageUpdate::Allow, &mut ErrorGraph::default())
852            .unwrap();
853        let handle_c = memory_management
854            .reserve(1024, PageUpdate::Allow, &mut ErrorGraph::default())
855            .unwrap();
856
857        let binding_b = handle_b.binding();
858        let reserved = memory_management.memory_report().usage().bytes_reserved;
859        drop(handle_a);
860        drop(handle_c);
861
862        // Deallocates the two free pages and renumbers the surviving one.
863        memory_management.cleanup(Cleanup::Explicit, &mut ErrorGraph::default());
864
865        assert!(memory_management.get_cursor(binding_b.clone()).is_ok());
866        assert!(memory_management.get_storage(binding_b).is_ok());
867        assert!(
868            memory_management.memory_report().usage().bytes_reserved < reserved,
869            "the two freed pages must have gone back"
870        );
871    }
872
873    /// Persistent windows nest: a module load arms one window around the whole
874    /// load while the parameter machinery arms one per parameter inside it —
875    /// an inner window closing must not flip the rest of the outer one back to
876    /// `Auto`, or most of the load's weights land in the dynamic pools.
877    #[test_log::test]
878    #[cfg(not(exclusive_memory_only))]
879    fn persistent_windows_nest() {
880        let mut memory_management = MemoryManagement::from_configuration(
881            BytesStorage::default(),
882            &DUMMY_MEM_PROPS,
883            MemoryConfiguration::Adaptive,
884            Arc::new(ServerLogger::default()),
885            options(),
886        );
887
888        memory_management.mode(MemoryAllocationMode::Persistent); // the load's window
889        memory_management.mode(MemoryAllocationMode::Persistent); // one parameter's window
890        memory_management.mode(MemoryAllocationMode::Auto); // that parameter is done
891
892        // Still inside the load's window: the allocation must be persistent.
893        let weight = memory_management
894            .reserve(1024, PageUpdate::Allow, &mut ErrorGraph::default())
895            .unwrap();
896        let report = memory_management.memory_report();
897        assert_eq!(
898            report.persistent.usage.bytes_in_use, 1024,
899            "an allocation inside the outer window is persistent"
900        );
901        assert_eq!(
902            report
903                .dynamic
904                .iter()
905                .map(|pool| pool.pages_peak)
906                .sum::<u64>(),
907            0,
908            "nothing leaked into the dynamic pools"
909        );
910
911        // The outer window closes; ordinary allocations are dynamic again.
912        memory_management.mode(MemoryAllocationMode::Auto);
913        let _transient = memory_management
914            .reserve(1024, PageUpdate::Allow, &mut ErrorGraph::default())
915            .unwrap();
916        assert_eq!(
917            memory_management
918                .memory_report()
919                .dynamic
920                .iter()
921                .map(|pool| pool.pages_peak)
922                .sum::<u64>(),
923            1
924        );
925
926        drop(weight);
927    }
928
929    #[test_log::test]
930    #[cfg(not(exclusive_memory_only))]
931    fn alloc_two_chunks_on_one_page() {
932        let mut memory_management = MemoryManagement::from_configuration(
933            BytesStorage::default(),
934            &DUMMY_MEM_PROPS,
935            MemoryConfiguration::Adaptive,
936            Arc::new(ServerLogger::default()),
937            options(),
938        );
939
940        let alloc_size = 512;
941        let _handle =
942            memory_management.reserve(alloc_size, PageUpdate::Allow, &mut ErrorGraph::default());
943        let _new_handle =
944            memory_management.reserve(alloc_size, PageUpdate::Allow, &mut ErrorGraph::default());
945
946        let usage = memory_management.memory_report().usage();
947        assert_eq!(usage.number_allocs, 2);
948        assert_eq!(usage.bytes_in_use, alloc_size * 2);
949        // One page, not two: both slices were carved from the same one.
950        assert_eq!(usage.bytes_reserved, METADATA_PAGE);
951    }
952
953    #[test_log::test]
954    #[cfg(not(exclusive_memory_only))]
955    fn alloc_reuses_storage() {
956        // If no storage is re-used, this will allocate two pages.
957        let mut memory_management = MemoryManagement::from_configuration(
958            BytesStorage::default(),
959            &DUMMY_MEM_PROPS,
960            MemoryConfiguration::Adaptive,
961            Arc::new(ServerLogger::default()),
962            options(),
963        );
964
965        let alloc_size = 512;
966        let _handle =
967            memory_management.reserve(alloc_size, PageUpdate::Allow, &mut ErrorGraph::default());
968        drop(_handle);
969        let _new_handle =
970            memory_management.reserve(alloc_size, PageUpdate::Allow, &mut ErrorGraph::default());
971
972        let usage = memory_management.memory_report().usage();
973        assert_eq!(usage.number_allocs, 1);
974        assert_eq!(usage.bytes_in_use, alloc_size);
975        assert_eq!(usage.bytes_reserved, METADATA_PAGE);
976    }
977
978    #[test_log::test]
979    #[cfg(not(exclusive_memory_only))]
980    fn alloc_allocs_new_storage() {
981        let mut memory_management = MemoryManagement::from_configuration(
982            BytesStorage::default(),
983            &DUMMY_MEM_PROPS,
984            MemoryConfiguration::Adaptive,
985            Arc::new(ServerLogger::default()),
986            options(),
987        );
988
989        // Three quarters of the adaptive pool's smallest page each: the second
990        // one does not fit beside the first, so the pool has to grow a page.
991        let page_size = 2 * MIB;
992        let alloc_size = page_size / 4 * 3;
993        let _handle =
994            memory_management.reserve(alloc_size, PageUpdate::Allow, &mut ErrorGraph::default());
995        let _new_handle =
996            memory_management.reserve(alloc_size, PageUpdate::Allow, &mut ErrorGraph::default());
997
998        let usage = memory_management.memory_report().usage();
999        assert_eq!(usage.number_allocs, 2);
1000        assert_eq!(usage.bytes_in_use, alloc_size * 2);
1001        assert_eq!(usage.bytes_reserved, page_size * 2);
1002    }
1003
1004    #[test_log::test]
1005    #[cfg(not(exclusive_memory_only))]
1006    fn alloc_respects_alignment_size() {
1007        let mut memory_management = MemoryManagement::from_configuration(
1008            BytesStorage::default(),
1009            &MemoryDeviceProperties::new(DUMMY_MEM_PROPS.max_page_size, 50),
1010            MemoryConfiguration::Adaptive,
1011            Arc::new(ServerLogger::default()),
1012            options(),
1013        );
1014        let alloc_size = 40;
1015        let _handle =
1016            memory_management.reserve(alloc_size, PageUpdate::Allow, &mut ErrorGraph::default());
1017        let _new_handle =
1018            memory_management.reserve(alloc_size, PageUpdate::Allow, &mut ErrorGraph::default());
1019        let usage = memory_management.memory_report().usage();
1020        // Each slice should be aligned to 50 bytes, so 10 padding bytes.
1021        assert_eq!(usage.bytes_padding, 10 * 2);
1022    }
1023
1024    /// Each size lands on the pool that serves it: metadata churn stays on the
1025    /// metadata pages, and the workload gets pages sized to what it asks for.
1026    #[test_log::test]
1027    #[cfg(not(exclusive_memory_only))]
1028    fn allocs_on_correct_page() {
1029        let mut memory_management = MemoryManagement::from_configuration(
1030            BytesStorage::default(),
1031            &DUMMY_MEM_PROPS,
1032            MemoryConfiguration::Adaptive,
1033            Arc::new(ServerLogger::default()),
1034            options(),
1035        );
1036
1037        let alloc_sizes = [4096, 4 * MIB];
1038        let handles = alloc_sizes.map(|size| {
1039            memory_management
1040                .reserve(size, PageUpdate::Allow, &mut ErrorGraph::default())
1041                .unwrap()
1042        });
1043
1044        assert_ne!(
1045            handles[0].descriptor().location().pool,
1046            handles[1].descriptor().location().pool,
1047            "metadata and the workload must not share a pool"
1048        );
1049
1050        let usage = memory_management.memory_report().usage();
1051        // Total memory should be size of all pages, and no more: one metadata
1052        // page, and one adaptive page grown to the 4 MiB allocation.
1053        assert_eq!(usage.bytes_in_use, alloc_sizes.iter().sum::<u64>());
1054        assert_eq!(usage.bytes_reserved, METADATA_PAGE + 5 * MIB);
1055    }
1056
1057    // The motivating use case: allocations from different "sequence-length
1058    // ranges" reuse the same arena instead of each landing in its own
1059    // size-bucketed pool that keeps a separate reservation.
1060    #[test_log::test]
1061    #[cfg(not(exclusive_memory_only))]
1062    fn single_arena_reuses_across_sizes() {
1063        let mut memory_management = MemoryManagement::from_configuration(
1064            BytesStorage::default(),
1065            &DUMMY_MEM_PROPS,
1066            MemoryConfiguration::Adaptive,
1067            Arc::new(ServerLogger::default()),
1068            options(),
1069        );
1070
1071        // A "small seq" allocation, then freed.
1072        let small = memory_management
1073            .reserve(128 * 1024, PageUpdate::Allow, &mut ErrorGraph::default())
1074            .unwrap();
1075        drop(small);
1076        // A "large seq" allocation must reuse the same arena page.
1077        let large = memory_management
1078            .reserve(512 * 1024, PageUpdate::Allow, &mut ErrorGraph::default())
1079            .unwrap();
1080
1081        let usage = memory_management.memory_report().usage();
1082        assert_eq!(
1083            usage.bytes_reserved,
1084            2 * MIB,
1085            "both sizes must share a single arena page"
1086        );
1087        assert_eq!(usage.number_allocs, 1);
1088        drop(large);
1089    }
1090
1091    #[test_log::test]
1092    #[cfg(not(exclusive_memory_only))]
1093    fn allocate_deallocate_reallocate() {
1094        let mut memory_management = MemoryManagement::from_configuration(
1095            BytesStorage::default(),
1096            &DUMMY_MEM_PROPS,
1097            MemoryConfiguration::Adaptive,
1098            Arc::new(ServerLogger::default()),
1099            options(),
1100        );
1101        // Allocate a bunch
1102        let handles: Vec<_> = (0..5)
1103            .map(|i| {
1104                memory_management.reserve(
1105                    1000 * (i + 1),
1106                    PageUpdate::Allow,
1107                    &mut ErrorGraph::default(),
1108                )
1109            })
1110            .collect();
1111        let usage_before = memory_management.memory_report().usage();
1112        // Deallocate
1113        drop(handles);
1114        // Reallocate
1115        let _new_handles: Vec<_> = (0..5)
1116            .map(|i| {
1117                memory_management.reserve(
1118                    1000 * (i + 1),
1119                    PageUpdate::Allow,
1120                    &mut ErrorGraph::default(),
1121                )
1122            })
1123            .collect();
1124        let usage_after = memory_management.memory_report().usage();
1125        assert_eq!(usage_before.number_allocs, usage_after.number_allocs);
1126        assert_eq!(usage_before.bytes_in_use, usage_after.bytes_in_use);
1127        // Usage after can actually be _less_ because of defragging.
1128        assert!(usage_before.bytes_reserved >= usage_after.bytes_reserved);
1129    }
1130
1131    #[test_log::test]
1132    #[cfg(not(exclusive_memory_only))]
1133    fn test_fragmentation_resistance() {
1134        let mut memory_management = MemoryManagement::from_configuration(
1135            BytesStorage::default(),
1136            &DUMMY_MEM_PROPS,
1137            MemoryConfiguration::Adaptive,
1138            Arc::new(ServerLogger::default()),
1139            options(),
1140        );
1141        // Allocate a mix of small and large chunks
1142        let sizes = [50, 1000, 100, 5000, 200, 10000, 300];
1143        let handles: Vec<_> = sizes
1144            .iter()
1145            .map(|&size| {
1146                memory_management
1147                    .reserve(size, PageUpdate::Allow, &mut ErrorGraph::default())
1148                    .unwrap()
1149            })
1150            .collect();
1151        let usage_before = memory_management.memory_report().usage();
1152        // Deallocate every other allocation
1153        for i in (0..handles.len()).step_by(2) {
1154            drop(handles[i].clone());
1155        }
1156        // Reallocate similar sizes
1157        for &size in &sizes[0..sizes.len() / 2] {
1158            memory_management
1159                .reserve(size, PageUpdate::Allow, &mut ErrorGraph::default())
1160                .unwrap();
1161        }
1162        let usage_after = memory_management.memory_report().usage();
1163        // Check that we haven't increased our memory usage significantly
1164        assert!(usage_after.bytes_reserved <= (usage_before.bytes_reserved as f64 * 1.1) as u64);
1165    }
1166
1167    // Test pools without slices. More or less same as tests above.
1168    #[test_log::test]
1169    fn noslice_test_handle_mutability() {
1170        let mut memory_management = MemoryManagement::from_configuration(
1171            BytesStorage::default(),
1172            &DUMMY_MEM_PROPS,
1173            MemoryConfiguration::ExclusivePages,
1174            Arc::new(ServerLogger::default()),
1175            options(),
1176        );
1177        let handle = memory_management
1178            .reserve(10, PageUpdate::Allow, &mut ErrorGraph::default())
1179            .unwrap();
1180        let other_ref = handle.clone();
1181        assert!(!handle.can_mut(), "Handle can't be mut when multiple ref.");
1182        drop(other_ref);
1183        assert!(handle.can_mut(), "Handle should be mut when only one ref.");
1184    }
1185
1186    #[test_log::test]
1187    fn noslice_alloc_two_chunk() {
1188        let mut memory_management = MemoryManagement::from_configuration(
1189            BytesStorage::default(),
1190            &DUMMY_MEM_PROPS,
1191            MemoryConfiguration::ExclusivePages,
1192            Arc::new(ServerLogger::default()),
1193            options(),
1194        );
1195
1196        let alloc_size = 512;
1197        let _handle =
1198            memory_management.reserve(alloc_size, PageUpdate::Allow, &mut ErrorGraph::default());
1199        let _new_handle =
1200            memory_management.reserve(alloc_size, PageUpdate::Allow, &mut ErrorGraph::default());
1201
1202        let usage = memory_management.memory_report().usage();
1203        assert_eq!(usage.number_allocs, 2);
1204        assert_eq!(usage.bytes_in_use, alloc_size * 2);
1205        assert!(usage.bytes_reserved >= alloc_size * 2);
1206    }
1207
1208    #[test_log::test]
1209    fn noslice_alloc_reuses_storage() {
1210        // If no storage is re-used, this will allocate two pages.
1211        let mut memory_management = MemoryManagement::from_configuration(
1212            BytesStorage::default(),
1213            &DUMMY_MEM_PROPS,
1214            MemoryConfiguration::ExclusivePages,
1215            Arc::new(ServerLogger::default()),
1216            options(),
1217        );
1218
1219        let alloc_size = 512;
1220        let _handle =
1221            memory_management.reserve(alloc_size, PageUpdate::Allow, &mut ErrorGraph::default());
1222        drop(_handle);
1223        let _new_handle =
1224            memory_management.reserve(alloc_size, PageUpdate::Allow, &mut ErrorGraph::default());
1225
1226        let usage = memory_management.memory_report().usage();
1227        assert_eq!(usage.number_allocs, 1);
1228        assert_eq!(usage.bytes_in_use, alloc_size);
1229        assert!(usage.bytes_reserved >= alloc_size);
1230    }
1231
1232    #[test_log::test]
1233    fn noslice_alloc_allocs_new_storage() {
1234        let mut memory_management = MemoryManagement::from_configuration(
1235            BytesStorage::default(),
1236            &DUMMY_MEM_PROPS,
1237            MemoryConfiguration::ExclusivePages,
1238            Arc::new(ServerLogger::default()),
1239            options(),
1240        );
1241
1242        let alloc_size = 768;
1243        let _handle =
1244            memory_management.reserve(alloc_size, PageUpdate::Allow, &mut ErrorGraph::default());
1245        let _new_handle =
1246            memory_management.reserve(alloc_size, PageUpdate::Allow, &mut ErrorGraph::default());
1247        let usage = memory_management.memory_report().usage();
1248        assert_eq!(usage.number_allocs, 2);
1249        assert_eq!(usage.bytes_in_use, alloc_size * 2);
1250        assert!(usage.bytes_reserved >= alloc_size * 2);
1251    }
1252
1253    #[test_log::test]
1254    fn noslice_alloc_respects_alignment_size() {
1255        let mut memory_management = MemoryManagement::from_configuration(
1256            BytesStorage::default(),
1257            &MemoryDeviceProperties::new(DUMMY_MEM_PROPS.max_page_size, 50),
1258            MemoryConfiguration::ExclusivePages,
1259            Arc::new(ServerLogger::default()),
1260            options(),
1261        );
1262        let alloc_size = 40;
1263        let _handle =
1264            memory_management.reserve(alloc_size, PageUpdate::Allow, &mut ErrorGraph::default());
1265        let _new_handle =
1266            memory_management.reserve(alloc_size, PageUpdate::Allow, &mut ErrorGraph::default());
1267        let usage = memory_management.memory_report().usage();
1268        // Each slice should be aligned to 50 bytes, so 10 padding bytes.
1269        assert_eq!(usage.bytes_padding, 10 * 2);
1270    }
1271
1272    #[test_log::test]
1273    fn noslice_allocs_on_correct_page() {
1274        let mut memory_management = MemoryManagement::from_configuration(
1275            BytesStorage::default(),
1276            &MemoryDeviceProperties::new(DUMMY_MEM_PROPS.max_page_size, 10),
1277            MemoryConfiguration::ExclusivePages,
1278            Arc::new(ServerLogger::default()),
1279            options(),
1280        );
1281        // Allocate one thing on each page.
1282        let alloc_sizes = [50, 150, 250, 350];
1283        let _handles = alloc_sizes
1284            .map(|s| memory_management.reserve(s, PageUpdate::Allow, &mut ErrorGraph::default()));
1285        let usage = memory_management.memory_report().usage();
1286        // Total memory should be size of all pages, and no more.
1287        assert_eq!(usage.bytes_in_use, alloc_sizes.iter().sum::<u64>());
1288    }
1289
1290    #[test_log::test]
1291    fn a_guarded_allocation_is_never_handed_out_again() {
1292        let mut memory_management = MemoryManagement::from_configuration(
1293            BytesStorage::default(),
1294            &DUMMY_MEM_PROPS,
1295            MemoryConfiguration::ExclusivePages,
1296            Arc::new(ServerLogger::default()),
1297            options(),
1298        );
1299
1300        let recorded = memory_management
1301            .reserve(1024, PageUpdate::Allow, &mut ErrorGraph::default())
1302            .unwrap();
1303        let guard = memory_management
1304            .guard(recorded.descriptor().location())
1305            .expect("the pool holds the page");
1306        drop(recorded);
1307
1308        // Freed, but guarded: the next allocation of that size needs a page
1309        // of its own, and a cleanup leaves the guarded one where it is.
1310        let before = memory_management.memory_report().usage().bytes_reserved;
1311        let other = memory_management
1312            .reserve(1024, PageUpdate::Allow, &mut ErrorGraph::default())
1313            .unwrap();
1314        let after = memory_management.memory_report().usage().bytes_reserved;
1315        assert!(after > before, "the guarded page was handed out again");
1316        memory_management.cleanup(Cleanup::Explicit, &mut ErrorGraph::default());
1317        assert_eq!(
1318            memory_management.memory_report().usage().bytes_reserved,
1319            after,
1320            "the guarded page was released"
1321        );
1322
1323        drop(guard);
1324        drop(other);
1325        memory_management.cleanup(Cleanup::Explicit, &mut ErrorGraph::default());
1326        assert_eq!(memory_management.memory_report().usage().bytes_reserved, 0);
1327    }
1328
1329    #[test_log::test]
1330    fn a_guard_leaves_in_place_updates_alone() {
1331        let mut memory_management = MemoryManagement::from_configuration(
1332            BytesStorage::default(),
1333            &DUMMY_MEM_PROPS,
1334            MemoryConfiguration::ExclusivePages,
1335            Arc::new(ServerLogger::default()),
1336            options(),
1337        );
1338
1339        let cache = memory_management
1340            .reserve(1024, PageUpdate::Allow, &mut ErrorGraph::default())
1341            .unwrap();
1342        let _guard = memory_management.guard(cache.descriptor().location());
1343
1344        assert!(
1345            cache.can_mut(),
1346            "a graph replaying against a buffer must not stop it being updated in place"
1347        );
1348    }
1349
1350    #[test_log::test]
1351    fn noslice_allocate_deallocate_reallocate() {
1352        let mut memory_management = MemoryManagement::from_configuration(
1353            BytesStorage::default(),
1354            &DUMMY_MEM_PROPS,
1355            MemoryConfiguration::ExclusivePages,
1356            Arc::new(ServerLogger::default()),
1357            options(),
1358        );
1359        // Allocate a bunch
1360        let handles: Vec<_> = (0..5)
1361            .map(|i| {
1362                memory_management.reserve(
1363                    1000 * (i + 1),
1364                    PageUpdate::Allow,
1365                    &mut ErrorGraph::default(),
1366                )
1367            })
1368            .collect();
1369        let usage_before = memory_management.memory_report().usage();
1370        // Deallocate
1371        drop(handles);
1372        // Reallocate
1373        let _new_handles: Vec<_> = (0..5)
1374            .map(|i| {
1375                memory_management.reserve(
1376                    1000 * (i + 1),
1377                    PageUpdate::Allow,
1378                    &mut ErrorGraph::default(),
1379                )
1380            })
1381            .collect();
1382        let usage_after = memory_management.memory_report().usage();
1383        assert_eq!(usage_before.number_allocs, usage_after.number_allocs);
1384        assert_eq!(usage_before.bytes_in_use, usage_after.bytes_in_use);
1385        assert_eq!(usage_before.bytes_reserved, usage_after.bytes_reserved);
1386    }
1387}