Skip to main content

burn_backend/backend/
memory_pools.rs

1//! The layout of a device's dynamic memory pools, and what one reports.
2//!
3//! An allocator that only grows keeps whatever page it ever needed, so a
4//! long-running workload reserves its worst moment for life. These types let a
5//! caller install a layout of its own — a pool per size class, each capped at a
6//! number of pages — and read back what the workload actually held, so the caps
7//! are a measurement rather than a guess.
8//!
9//! The vocabulary is the backend's own rather than any runtime's, since
10//! [`Backend`](super::Backend) is also implemented by backends with no pools at
11//! all.
12
13use alloc::string::String;
14use alloc::vec::Vec;
15
16/// A layout for a device's dynamic memory pools, applied with
17/// [`Backend::memory_install_pools`](super::Backend::memory_install_pools).
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub enum MemoryPoolLayout {
20    /// An ordered list of pools that sub-slice fixed-size pages. An allocation
21    /// lands in the first pool whose [`max_slice`](SlicedPool::max_slice)
22    /// accepts its size, so a small pool listed before a large one captures the
23    /// small-allocation traffic. Pages are allocated on first use.
24    Sliced(Vec<SlicedPool>),
25    /// One direct pool for everything: every allocation is its own, sized to
26    /// the request and reused by exact size — no pages, no sub-slicing, no
27    /// padding beyond alignment. With no page size chosen in advance, this is
28    /// what a workload runs on for its largest allocation to be read back as it
29    /// was asked for.
30    Direct,
31    /// The runtime's default: a ladder of size-bucketed pools that sub-slice
32    /// large pages.
33    SubSlices,
34    /// One page per allocation, in exponentially spaced size buckets.
35    ExclusivePages,
36}
37
38/// One pool of a [`MemoryPoolLayout::Sliced`] layout: allocations are slices of
39/// fixed-size pages, capped at `pages` pages. An allocation that no longer fits
40/// goes to the next pool that accepts it, and fails when none does.
41///
42/// Sizes are rounded up to the device's alignment, so a pool holds at least
43/// what it was asked for. A layout that cannot be honoured at all — a zero
44/// size, `max_slice` past `page_size`, `pages` outside `1..=65535` — is refused
45/// with [`InvalidLayout`](InstallMemoryPoolsError::InvalidLayout).
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub struct SlicedPool {
48    /// Size of each page in bytes; also the largest single allocation this pool
49    /// can serve. Rounded up to the device's alignment.
50    pub page_size: u64,
51    /// How many pages the pool may hold. `None` grows without a cap, which is
52    /// what a workload is measured on before its caps are known.
53    pub pages: Option<u64>,
54    /// Largest allocation routed to this pool; `None` accepts anything up to
55    /// `page_size`. Later pools see only what this one declines.
56    pub max_slice: Option<u64>,
57}
58
59/// One dynamic pool's measured state, in the order the pools were installed.
60///
61/// The read side of a measured layout: install a growable one, run the
62/// workload, read these, re-install capped at `pages_peak`. Pool placement is
63/// deterministic, so the same allocations fit the capped layout by
64/// construction.
65#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
66pub struct SlicedPoolReport {
67    /// Size of each page in bytes; `0` for a [direct](MemoryPoolLayout::Direct)
68    /// pool, which has no pages to size.
69    pub page_size: u64,
70    /// Pages currently held — for a direct pool, live allocations.
71    pub pages: u64,
72    /// The most pages ever held at once.
73    pub pages_peak: u64,
74    /// The largest single allocation served, in requested bytes.
75    pub largest_alloc: u64,
76}
77
78/// One reading of a device allocator's state, across the runtime's streams and
79/// pools.
80#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
81pub struct MemoryPoolUsage {
82    /// Live allocations, not pages.
83    pub number_allocs: u64,
84    /// Bytes those allocations use, excluding padding.
85    pub bytes_in_use: u64,
86    /// Bytes of padding inside them.
87    pub bytes_padding: u64,
88    /// Total bytes reserved on the device: at least `bytes_in_use`, plus pages
89    /// held for reuse.
90    pub bytes_reserved: u64,
91}
92
93/// Why installing a pool layout did not take effect.
94///
95/// The distinction a caller needs is **transient or permanent**: a refused
96/// rebuild is worth retrying once whatever holds the pools drains, while a
97/// backend with no configurable pools refuses forever. Treating the two alike
98/// either gives up on a layout that would have installed, or repeats an
99/// expensive measurement that can never succeed.
100#[derive(Clone, Debug, PartialEq, Eq)]
101pub enum InstallMemoryPoolsError {
102    /// The pools being rebuilt still hold live allocations, so the previous
103    /// layout was kept. Transient.
104    PoolsInUse {
105        /// Bytes still live in those pools.
106        bytes_in_use: u64,
107    },
108    /// The calling stream is already in an error state, so its pools were not
109    /// rebuilt. The layout still applies to streams created afterwards, and the
110    /// underlying failure surfaces at the next flush or sync.
111    StreamUnavailable,
112    /// This backend has no configurable dynamic pools. Permanent.
113    Unsupported,
114    /// The layout itself cannot be honoured, so the previous one was kept: an
115    /// empty pool list, a zero size, a slice larger than its page, an
116    /// unusable cap, or a pool shape this build has none of. Permanent — what
117    /// has to change is the layout, not the moment it is installed at.
118    InvalidLayout {
119        /// What the backend objected to, in its own words.
120        reason: String,
121    },
122}
123
124impl core::fmt::Display for InstallMemoryPoolsError {
125    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
126        match self {
127            Self::PoolsInUse { bytes_in_use } => {
128                write!(formatter, "{bytes_in_use} B still live in the pools")
129            }
130            Self::StreamUnavailable => {
131                write!(formatter, "the calling stream is in an error state")
132            }
133            Self::Unsupported => {
134                write!(formatter, "this backend has no configurable memory pools")
135            }
136            Self::InvalidLayout { reason } => {
137                write!(formatter, "the pool layout cannot be honoured: {reason}")
138            }
139        }
140    }
141}
142
143impl core::error::Error for InstallMemoryPoolsError {}