gpu_allocator/allocator/free_list_allocator/
mod.rs1#![deny(unsafe_code, clippy::unwrap_used)]
2#[cfg(feature = "std")]
3use alloc::sync::Arc;
4use alloc::{
5 borrow::ToOwned,
6 string::{String, ToString},
7 vec::Vec,
8};
9#[cfg(feature = "std")]
10use std::backtrace::Backtrace;
11#[cfg(all(feature = "std", not(feature = "hashbrown")))]
12use std::collections::{HashMap, HashSet};
13
14#[cfg(feature = "hashbrown")]
15use hashbrown::{HashMap, HashSet};
16use log::{log, Level};
17
18#[cfg(feature = "visualizer")]
19pub(crate) mod visualizer;
20
21use super::{AllocationReport, AllocationType, SubAllocator, SubAllocatorBase};
22use crate::{AllocationError, Result};
23
24const USE_BEST_FIT: bool = true;
25
26fn align_down(val: u64, alignment: u64) -> u64 {
27 val & !(alignment - 1u64)
28}
29
30fn align_up(val: u64, alignment: u64) -> u64 {
31 align_down(val + alignment - 1u64, alignment)
32}
33
34#[derive(Debug)]
35pub(crate) struct MemoryChunk {
36 pub(crate) chunk_id: core::num::NonZeroU64,
37 pub(crate) size: u64,
38 pub(crate) offset: u64,
39 pub(crate) allocation_type: AllocationType,
40 pub(crate) name: Option<String>,
41 #[cfg(feature = "std")]
43 pub(crate) backtrace: Arc<Backtrace>,
44 next: Option<core::num::NonZeroU64>,
45 prev: Option<core::num::NonZeroU64>,
46}
47
48#[derive(Debug)]
49pub(crate) struct FreeListAllocator {
50 size: u64,
51 allocated: u64,
52 pub(crate) chunk_id_counter: u64,
53 pub(crate) chunks: HashMap<core::num::NonZeroU64, MemoryChunk>,
54 free_chunks: HashSet<core::num::NonZeroU64>,
55}
56
57fn is_on_same_page(offset_a: u64, size_a: u64, offset_b: u64, page_size: u64) -> bool {
59 let end_a = offset_a + size_a - 1;
60 let end_page_a = align_down(end_a, page_size);
61 let start_b = offset_b;
62 let start_page_b = align_down(start_b, page_size);
63
64 end_page_a == start_page_b
65}
66
67fn has_granularity_conflict(type0: AllocationType, type1: AllocationType) -> bool {
69 if type0 == AllocationType::Free || type1 == AllocationType::Free {
70 return false;
71 }
72
73 type0 != type1
74}
75
76impl FreeListAllocator {
77 pub(crate) fn new(size: u64) -> Self {
78 #[allow(clippy::unwrap_used)]
79 let initial_chunk_id = core::num::NonZeroU64::new(1).unwrap();
80
81 let mut chunks = HashMap::default();
82 chunks.insert(
83 initial_chunk_id,
84 MemoryChunk {
85 chunk_id: initial_chunk_id,
86 size,
87 offset: 0,
88 allocation_type: AllocationType::Free,
89 name: None,
90 #[cfg(feature = "std")]
91 backtrace: Arc::new(Backtrace::disabled()),
92 prev: None,
93 next: None,
94 },
95 );
96
97 let mut free_chunks = HashSet::default();
98 free_chunks.insert(initial_chunk_id);
99
100 Self {
101 size,
102 allocated: 0,
103 chunk_id_counter: 2,
106 chunks,
107 free_chunks,
108 }
109 }
110
111 fn get_new_chunk_id(&mut self) -> Result<core::num::NonZeroU64> {
113 if self.chunk_id_counter == u64::MAX {
114 return Err(AllocationError::OutOfMemory);
116 }
117
118 let id = self.chunk_id_counter;
119 self.chunk_id_counter += 1;
120 core::num::NonZeroU64::new(id).ok_or_else(|| {
121 AllocationError::Internal("New chunk id was 0, which is not allowed.".into())
122 })
123 }
124 fn remove_id_from_free_list(&mut self, chunk_id: core::num::NonZeroU64) {
126 self.free_chunks.remove(&chunk_id);
127 }
128 fn merge_free_chunks(
130 &mut self,
131 chunk_left: core::num::NonZeroU64,
132 chunk_right: core::num::NonZeroU64,
133 ) -> Result<()> {
134 let (right_size, right_next) = {
136 let chunk = self.chunks.remove(&chunk_right).ok_or_else(|| {
137 AllocationError::Internal("Chunk ID not present in chunk list.".into())
138 })?;
139 self.remove_id_from_free_list(chunk.chunk_id);
140
141 (chunk.size, chunk.next)
142 };
143
144 {
146 let chunk = self.chunks.get_mut(&chunk_left).ok_or_else(|| {
147 AllocationError::Internal("Chunk ID not present in chunk list.".into())
148 })?;
149 chunk.next = right_next;
150 chunk.size += right_size;
151 }
152
153 if let Some(right_next) = right_next {
155 let chunk = self.chunks.get_mut(&right_next).ok_or_else(|| {
156 AllocationError::Internal("Chunk ID not present in chunk list.".into())
157 })?;
158 chunk.prev = Some(chunk_left);
159 }
160
161 Ok(())
162 }
163}
164
165impl SubAllocatorBase for FreeListAllocator {}
166impl SubAllocator for FreeListAllocator {
167 fn allocate(
168 &mut self,
169 size: u64,
170 alignment: u64,
171 allocation_type: AllocationType,
172 granularity: u64,
173 name: &str,
174 #[cfg(feature = "std")] backtrace: Arc<Backtrace>,
175 ) -> Result<(u64, core::num::NonZeroU64)> {
176 let free_size = self.size - self.allocated;
177 if size > free_size {
178 return Err(AllocationError::OutOfMemory);
179 }
180
181 let mut best_fit_id: Option<core::num::NonZeroU64> = None;
182 let mut best_offset = 0u64;
183 let mut best_aligned_size = 0u64;
184 let mut best_chunk_size = 0u64;
185
186 for current_chunk_id in self.free_chunks.iter() {
187 let current_chunk = self.chunks.get(current_chunk_id).ok_or_else(|| {
188 AllocationError::Internal(
189 "Chunk ID in free list is not present in chunk list.".into(),
190 )
191 })?;
192
193 if current_chunk.size < size {
194 continue;
195 }
196
197 let mut offset = align_up(current_chunk.offset, alignment);
198
199 if let Some(prev_idx) = current_chunk.prev {
200 let previous = self.chunks.get(&prev_idx).ok_or_else(|| {
201 AllocationError::Internal("Invalid previous chunk reference.".into())
202 })?;
203 if is_on_same_page(previous.offset, previous.size, offset, granularity)
204 && has_granularity_conflict(previous.allocation_type, allocation_type)
205 {
206 offset = align_up(offset, granularity);
207 }
208 }
209
210 let padding = offset - current_chunk.offset;
211 let aligned_size = padding + size;
212
213 if aligned_size > current_chunk.size {
214 continue;
215 }
216
217 if let Some(next_idx) = current_chunk.next {
218 let next = self.chunks.get(&next_idx).ok_or_else(|| {
219 AllocationError::Internal("Invalid next chunk reference.".into())
220 })?;
221 if is_on_same_page(offset, size, next.offset, granularity)
222 && has_granularity_conflict(allocation_type, next.allocation_type)
223 {
224 continue;
225 }
226 }
227
228 if USE_BEST_FIT {
229 if best_fit_id.is_none() || current_chunk.size < best_chunk_size {
230 best_fit_id = Some(*current_chunk_id);
231 best_aligned_size = aligned_size;
232 best_offset = offset;
233
234 best_chunk_size = current_chunk.size;
235 };
236 } else {
237 best_fit_id = Some(*current_chunk_id);
238 best_aligned_size = aligned_size;
239 best_offset = offset;
240
241 best_chunk_size = current_chunk.size;
242 break;
243 }
244 }
245
246 let first_fit_id = best_fit_id.ok_or(AllocationError::OutOfMemory)?;
247
248 let chunk_id = if best_chunk_size > best_aligned_size {
249 let new_chunk_id = self.get_new_chunk_id()?;
250
251 let new_chunk = {
252 let free_chunk = self.chunks.get_mut(&first_fit_id).ok_or_else(|| {
253 AllocationError::Internal("Chunk ID must be in chunk list.".into())
254 })?;
255 let new_chunk = MemoryChunk {
256 chunk_id: new_chunk_id,
257 size: best_aligned_size,
258 offset: free_chunk.offset,
259 allocation_type,
260 name: Some(name.to_string()),
261 #[cfg(feature = "std")]
262 backtrace,
263 prev: free_chunk.prev,
264 next: Some(first_fit_id),
265 };
266
267 free_chunk.prev = Some(new_chunk.chunk_id);
268 free_chunk.offset += best_aligned_size;
269 free_chunk.size -= best_aligned_size;
270 new_chunk
271 };
272
273 if let Some(prev_id) = new_chunk.prev {
274 let prev_chunk = self.chunks.get_mut(&prev_id).ok_or_else(|| {
275 AllocationError::Internal("Invalid previous chunk reference.".into())
276 })?;
277 prev_chunk.next = Some(new_chunk.chunk_id);
278 }
279
280 self.chunks.insert(new_chunk_id, new_chunk);
281
282 new_chunk_id
283 } else {
284 let chunk = self
285 .chunks
286 .get_mut(&first_fit_id)
287 .ok_or_else(|| AllocationError::Internal("Invalid chunk reference.".into()))?;
288
289 chunk.allocation_type = allocation_type;
290 chunk.name = Some(name.to_string());
291 #[cfg(feature = "std")]
292 {
293 chunk.backtrace = backtrace;
294 }
295
296 self.remove_id_from_free_list(first_fit_id);
297
298 first_fit_id
299 };
300
301 self.allocated += best_aligned_size;
302
303 Ok((best_offset, chunk_id))
304 }
305
306 fn free(&mut self, chunk_id: Option<core::num::NonZeroU64>) -> Result<()> {
307 let chunk_id = chunk_id
308 .ok_or_else(|| AllocationError::Internal("Chunk ID must be a valid value.".into()))?;
309
310 let (next_id, prev_id) = {
311 let chunk = self.chunks.get_mut(&chunk_id).ok_or_else(|| {
312 AllocationError::Internal(
313 "Attempting to free chunk that is not in chunk list.".into(),
314 )
315 })?;
316 chunk.allocation_type = AllocationType::Free;
317 chunk.name = None;
318 #[cfg(feature = "std")]
319 {
320 chunk.backtrace = Arc::new(Backtrace::disabled());
321 }
322
323 self.allocated -= chunk.size;
324
325 self.free_chunks.insert(chunk.chunk_id);
326
327 (chunk.next, chunk.prev)
328 };
329
330 if let Some(next_id) = next_id {
331 if self.chunks[&next_id].allocation_type == AllocationType::Free {
332 self.merge_free_chunks(chunk_id, next_id)?;
333 }
334 }
335
336 if let Some(prev_id) = prev_id {
337 if self.chunks[&prev_id].allocation_type == AllocationType::Free {
338 self.merge_free_chunks(prev_id, chunk_id)?;
339 }
340 }
341 Ok(())
342 }
343
344 fn rename_allocation(
345 &mut self,
346 chunk_id: Option<core::num::NonZeroU64>,
347 name: &str,
348 ) -> Result<()> {
349 let chunk_id = chunk_id
350 .ok_or_else(|| AllocationError::Internal("Chunk ID must be a valid value.".into()))?;
351
352 let chunk = self.chunks.get_mut(&chunk_id).ok_or_else(|| {
353 AllocationError::Internal(
354 "Attempting to rename chunk that is not in chunk list.".into(),
355 )
356 })?;
357
358 if chunk.allocation_type == AllocationType::Free {
359 return Err(AllocationError::Internal(
360 "Attempting to rename a freed allocation.".into(),
361 ));
362 }
363
364 chunk.name = Some(name.into());
365
366 Ok(())
367 }
368
369 fn report_memory_leaks(
370 &self,
371 log_level: Level,
372 memory_type_index: usize,
373 memory_block_index: usize,
374 ) {
375 for (chunk_id, chunk) in self.chunks.iter() {
376 if chunk.allocation_type == AllocationType::Free {
377 continue;
378 }
379 let empty = "".to_string();
380 let name = chunk.name.as_ref().unwrap_or(&empty);
381 let backtrace_info;
382 #[cfg(feature = "std")]
383 {
384 backtrace_info = format!(
386 ",
387 backtrace: {}",
388 chunk.backtrace
389 )
390 }
391 #[cfg(not(feature = "std"))]
392 {
393 backtrace_info = ""
394 }
395 log!(
396 log_level,
397 r#"leak detected: {{
398 memory type: {}
399 memory block: {}
400 chunk: {{
401 chunk_id: {},
402 size: 0x{:x},
403 offset: 0x{:x},
404 allocation_type: {:?},
405 name: {}{backtrace_info}
406 }}
407}}"#,
408 memory_type_index,
409 memory_block_index,
410 chunk_id,
411 chunk.size,
412 chunk.offset,
413 chunk.allocation_type,
414 name,
415 );
416 }
417 }
418
419 fn report_allocations(&self) -> Vec<AllocationReport> {
420 self.chunks
421 .iter()
422 .filter(|(_key, chunk)| chunk.allocation_type != AllocationType::Free)
423 .map(|(_key, chunk)| AllocationReport {
424 name: chunk
425 .name
426 .clone()
427 .unwrap_or_else(|| "<Unnamed FreeList allocation>".to_owned()),
428 offset: chunk.offset,
429 size: chunk.size,
430 #[cfg(feature = "visualizer")]
431 backtrace: chunk.backtrace.clone(),
432 })
433 .collect::<Vec<_>>()
434 }
435
436 fn allocated(&self) -> u64 {
437 self.allocated
438 }
439
440 fn supports_general_allocations(&self) -> bool {
441 true
442 }
443}