webrender/
bump_allocator.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 use std::{
6    alloc::Layout,
7    ptr::{self, NonNull}, sync::{Arc, Mutex},
8};
9
10use allocator_api2::alloc::{AllocError, Allocator, Global};
11
12const CHUNK_ALIGNMENT: usize = 32;
13const DEFAULT_CHUNK_SIZE: usize = 128 * 1024;
14
15/// A simple bump allocator, sub-allocating from fixed size chunks that are provided
16/// by a parent allocator.
17///
18/// If an allocation is larger than the chunk size, a chunk sufficiently large to contain
19/// the allocation is added.
20pub struct BumpAllocator {
21    /// The chunk we are currently allocating from.
22    current_chunk: NonNull<Chunk>,
23    /// For debugging.
24    allocation_count: i32,
25
26    chunk_pool: Arc<ChunkPool>,
27
28    stats: Stats,
29}
30
31impl BumpAllocator {
32    pub fn new(chunk_pool: Arc<ChunkPool>) -> Self {
33        let mut stats = Stats::default();
34
35        let first_chunk = chunk_pool.allocate_chunk(DEFAULT_CHUNK_SIZE).unwrap();
36        stats.chunks = 1;
37        stats.reserved_bytes += DEFAULT_CHUNK_SIZE;
38
39        BumpAllocator {
40            current_chunk: first_chunk,
41            chunk_pool,
42            allocation_count: 0,
43            stats,
44        }
45    }
46
47    pub fn get_stats(&mut self) -> Stats {
48        self.stats.chunk_utilization = self.stats.chunks as f32 - 1.0 + Chunk::utilization(self.current_chunk);
49        self.stats
50    }
51
52    pub fn allocate_item(&mut self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
53        self.stats.allocations += 1;
54        self.stats.allocated_bytes += layout.size();
55
56        if let Ok(alloc) = Chunk::allocate_item(self.current_chunk, layout) {
57            self.allocation_count += 1;
58            return Ok(alloc);
59        }
60
61        self.alloc_chunk(layout.size())?;
62
63        match Chunk::allocate_item(self.current_chunk, layout) {
64            Ok(alloc) => {
65                self.allocation_count += 1;
66                    return Ok(alloc);
67            }
68            Err(_) => {
69                return Err(AllocError);
70            }
71        }
72    }
73
74    pub fn deallocate_item(&mut self, ptr: NonNull<u8>, layout: Layout) {
75        self.stats.deallocations += 1;
76
77        if Chunk::contains_item(self.current_chunk, ptr) {
78            unsafe { Chunk::deallocate_item(self.current_chunk, ptr, layout); }
79        }
80
81        self.allocation_count -= 1;
82        debug_assert!(self.allocation_count >= 0);
83    }
84
85    pub unsafe fn grow_item(&mut self, ptr: NonNull<u8>, old_layout: Layout, new_layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
86        debug_assert!(
87            new_layout.size() >= old_layout.size(),
88            "`new_layout.size()` must be greater than or equal to `old_layout.size()`"
89        );
90
91        self.stats.reallocations += 1;
92
93        if Chunk::contains_item(self.current_chunk, ptr) {
94            if let Ok(alloc) = Chunk::grow_item(self.current_chunk, ptr, old_layout, new_layout) {
95                self.stats.in_place_reallocations += 1;
96                return Ok(alloc);
97            }
98        }
99
100        let new_alloc = if let Ok(alloc) = Chunk::allocate_item(self.current_chunk, new_layout) {
101            alloc
102        } else {
103            self.alloc_chunk(new_layout.size())?;
104            Chunk::allocate_item(self.current_chunk, new_layout).map_err(|_| AllocError)?
105        };
106
107        self.stats.reallocated_bytes += old_layout.size();
108
109        unsafe {
110            ptr::copy_nonoverlapping(ptr.as_ptr(), new_alloc.as_ptr().cast(), old_layout.size());
111        }
112
113        Ok(new_alloc)
114    }
115
116    pub unsafe fn shrink_item(&mut self, ptr: NonNull<u8>, old_layout: Layout, new_layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
117        debug_assert!(
118            new_layout.size() <= old_layout.size(),
119            "`new_layout.size()` must be smaller than or equal to `old_layout.size()`"
120        );
121
122        if Chunk::contains_item(self.current_chunk, ptr) {
123            return unsafe { Ok(Chunk::shrink_item(self.current_chunk, ptr, old_layout, new_layout)) };
124        }
125
126        // Can't actually shrink, so return the full range of the previous allocation.
127        Ok(NonNull::slice_from_raw_parts(ptr, old_layout.size()))
128    }
129
130    fn alloc_chunk(&mut self, item_size: usize) -> Result<(), AllocError> {
131        let chunk_size = DEFAULT_CHUNK_SIZE.max(align(item_size, CHUNK_ALIGNMENT) + CHUNK_ALIGNMENT);
132        self.stats.reserved_bytes += chunk_size;
133
134        let chunk = self.chunk_pool.allocate_chunk(chunk_size)?;
135
136        unsafe {
137            (*chunk.as_ptr()).previous = Some(self.current_chunk);
138        }
139        self.current_chunk = chunk;
140
141        self.stats.chunks += 1;
142
143        Ok(())
144    }
145}
146
147impl Drop for BumpAllocator {
148    fn drop(&mut self) {
149        assert!(self.allocation_count == 0);
150
151        unsafe {
152            self.chunk_pool.recycle_chunks(self.current_chunk);
153        }
154    }
155}
156
157/// A Contiguous buffer of memory holding multiple sub-allocaions.
158pub struct Chunk {
159    previous: Option<NonNull<Chunk>>,
160    /// Offset of the next allocation
161    cursor: *mut u8,
162    /// Points to the first byte after the chunk's buffer.
163    chunk_end: *mut u8,
164    /// Size of the chunk
165    size: usize,
166}
167
168impl Chunk {
169    pub fn allocate_item(this: NonNull<Chunk>, layout: Layout) -> Result<NonNull<[u8]>, ()> {
170        // Common wisdom would be to always bump address downward (https://fitzgeraldnick.com/2019/11/01/always-bump-downwards.html).
171        // However, bump allocation does not show up in profiles with the current workloads
172        // so we can keep things simple for now.
173        debug_assert!(CHUNK_ALIGNMENT % layout.align() == 0);
174        debug_assert!(layout.align() > 0);
175        debug_assert!(layout.align().is_power_of_two());
176
177        let size = align(layout.size(), CHUNK_ALIGNMENT);
178
179        unsafe {
180            let cursor = (*this.as_ptr()).cursor;
181            let end = (*this.as_ptr()).chunk_end;
182            let available_size = end.offset_from(cursor);
183
184            if size as isize > available_size {
185                return Err(());
186            }
187
188            let next = cursor.add(size);
189
190            (*this.as_ptr()).cursor = next;
191
192            let cursor = NonNull::new(cursor).unwrap();
193            let suballocation: NonNull<[u8]> = NonNull::slice_from_raw_parts(cursor, size);
194
195            Ok(suballocation)
196        }
197    }
198
199    pub unsafe fn deallocate_item(this: NonNull<Chunk>, item: NonNull<u8>, layout: Layout) {
200        debug_assert!(Chunk::contains_item(this, item));
201
202        unsafe {
203            let size = align(layout.size(), CHUNK_ALIGNMENT);
204            let item_end = item.as_ptr().add(size);
205
206            // If the item is the last allocation, then move the cursor back
207            // to reuse its memory.
208            if item_end == (*this.as_ptr()).cursor {
209                (*this.as_ptr()).cursor = item.as_ptr();
210            }
211
212            // Otherwise, deallocation is a no-op
213        }
214    }
215
216    pub unsafe fn grow_item(this: NonNull<Chunk>, item: NonNull<u8>, old_layout: Layout, new_layout: Layout) -> Result<NonNull<[u8]>, ()> {
217        debug_assert!(Chunk::contains_item(this, item));
218
219        let old_size = align(old_layout.size(), CHUNK_ALIGNMENT);
220        let new_size = align(new_layout.size(), CHUNK_ALIGNMENT);
221        let old_item_end = item.as_ptr().add(old_size);
222
223        if old_item_end != (*this.as_ptr()).cursor {
224            return Err(());
225        }
226
227        // The item is the last allocation. we can attempt to just move
228        // the cursor if the new size fits.
229
230        let chunk_end = (*this.as_ptr()).chunk_end;
231        let available_size = chunk_end.offset_from(item.as_ptr());
232
233        if new_size as isize > available_size {
234            // Does not fit.
235            return Err(());
236        }
237
238        let new_item_end = item.as_ptr().add(new_size);
239        (*this.as_ptr()).cursor = new_item_end;
240
241        Ok(NonNull::slice_from_raw_parts(item, new_size))
242    }
243
244    pub unsafe fn shrink_item(this: NonNull<Chunk>, item: NonNull<u8>, old_layout: Layout, new_layout: Layout) -> NonNull<[u8]> {
245        debug_assert!(Chunk::contains_item(this, item));
246
247        let old_size = align(old_layout.size(), CHUNK_ALIGNMENT);
248        let new_size = align(new_layout.size(), CHUNK_ALIGNMENT);
249        let old_item_end = item.as_ptr().add(old_size);
250
251        // The item is the last allocation. we can attempt to just move
252        // the cursor if the new size fits.
253
254        if old_item_end == (*this.as_ptr()).cursor {
255            let new_item_end = item.as_ptr().add(new_size);
256            (*this.as_ptr()).cursor = new_item_end;
257        }
258
259        NonNull::slice_from_raw_parts(item, new_size)
260    }
261
262    pub fn contains_item(this: NonNull<Chunk>, item: NonNull<u8>) -> bool {
263        unsafe {
264            let start: *mut u8 = this.cast::<u8>().as_ptr().add(CHUNK_ALIGNMENT);
265            let end: *mut u8 = (*this.as_ptr()).chunk_end;
266            let item = item.as_ptr();
267
268            start <= item && item < end
269        }
270    }
271
272    fn available_size(this: NonNull<Chunk>) -> usize {
273        unsafe {
274            let this = this.as_ptr();
275            (*this).chunk_end.offset_from((*this).cursor) as usize
276        }
277    }
278
279    fn utilization(this: NonNull<Chunk>) -> f32 {
280        let size = unsafe { (*this.as_ptr()).size } as f32;
281        (size - Chunk::available_size(this) as f32) / size
282    }
283}
284
285fn align(val: usize, alignment: usize) -> usize {
286    let rem = val % alignment;
287    if rem == 0 {
288        return val;
289    }
290
291    val.checked_add(alignment).unwrap() - rem
292}
293
294#[derive(Copy, Clone, Debug, Default)]
295pub struct Stats {
296    pub chunks: u32,
297    pub chunk_utilization: f32,
298    pub allocations: u32,
299    pub deallocations: u32,
300    pub reallocations: u32,
301    pub in_place_reallocations: u32,
302
303    pub reallocated_bytes: usize,
304    pub allocated_bytes: usize,
305    pub reserved_bytes: usize,
306}
307
308/// A simple pool for allocating and recycling memory chunks of a fixed size,
309/// protected by a mutex.
310///
311/// Chunks in the pool are stored as a linked list using a pointer to the next
312/// element at the beginning of the chunk.
313pub struct ChunkPool {
314    inner: Mutex<ChunkPoolInner>,
315}
316
317struct ChunkPoolInner {
318    first: Option<NonNull<RecycledChunk>>,
319    count: i32,
320}
321
322/// Header at the beginning of recycled memory chunk.
323struct RecycledChunk {
324    next: Option<NonNull<RecycledChunk>>,
325}
326
327impl ChunkPool {
328    pub fn new() -> Self {
329        ChunkPool {
330            inner: Mutex::new(ChunkPoolInner {
331                first: None,
332                count: 0,
333            }),
334        }
335    }
336
337    /// Pop a chunk from the pool or allocate a new one.
338    ///
339    /// If the requested size is not equal to the default chunk size,
340    /// a new chunk is allocated.
341    pub fn allocate_chunk(&self, size: usize) -> Result<NonNull<Chunk>, AllocError> {
342        let chunk: Option<NonNull<RecycledChunk>> = if size == DEFAULT_CHUNK_SIZE {
343            // Try to reuse a chunk.
344            let mut inner = self.inner.lock().unwrap();
345            let mut chunk = inner.first.take();
346            inner.first = chunk.as_mut().and_then(|chunk| unsafe { chunk.as_mut().next.take() });
347
348            if chunk.is_some() {
349                inner.count -= 1;
350                debug_assert!(inner.count >= 0);
351            }
352
353            chunk
354        } else {
355            // Always allocate a new chunk if it is not the standard size.
356            None
357        };
358
359        let chunk: NonNull<Chunk> = match chunk {
360            Some(chunk) => chunk.cast(),
361            None => {
362                // Allocate a new one.
363                let layout = match Layout::from_size_align(size, CHUNK_ALIGNMENT) {
364                    Ok(layout) => layout,
365                    Err(_) => {
366                        return Err(AllocError);
367                    }
368                };
369
370                let alloc = Global.allocate(layout)?;
371
372                alloc.cast()
373            }
374        };
375
376        let chunk_start: *mut u8 = chunk.cast().as_ptr();
377
378        unsafe {
379            let chunk_end = chunk_start.add(size);
380            let cursor = chunk_start.add(CHUNK_ALIGNMENT);
381            ptr::write(
382                chunk.as_ptr(),
383                Chunk {
384                    previous: None,
385                    chunk_end,
386                    cursor,
387                    size,
388                },
389            );
390        }
391
392        Ok(chunk)
393    }
394
395    /// Put the provided list of chunks into the pool.
396    ///
397    /// Chunks with size different from the default chunk size are deallocated
398    /// immediately.
399    ///
400    /// # Safety
401    ///
402    /// Ownership of the provided chunks is transfered to the pool, nothing
403    /// else can access them after this function runs.
404    unsafe fn recycle_chunks(&self, chunk: NonNull<Chunk>) {
405        let mut inner = self.inner.lock().unwrap();
406        let mut iter = Some(chunk);
407        // Go through the provided linked list of chunks, and insert each
408        // of them at the beginning of our linked list of recycled chunks.
409        while let Some(mut chunk) = iter {
410            // Advance the iterator.
411            iter = unsafe { chunk.as_mut().previous.take() };
412
413            unsafe {
414                // Don't recycle chunks with a non-standard size.
415                let size = chunk.as_ref().size;
416                if size != DEFAULT_CHUNK_SIZE {
417                    let layout = Layout::from_size_align(size, CHUNK_ALIGNMENT).unwrap();
418                    Global.deallocate(chunk.cast(), layout);
419                    continue;
420                }
421            }
422
423            // Turn the chunk into a recycled chunk.
424            let recycled: NonNull<RecycledChunk> = chunk.cast();
425
426            // Insert into the recycled list.
427            unsafe {
428                ptr::write(recycled.as_ptr(), RecycledChunk {
429                    next: inner.first,
430                });
431            }
432            inner.first = Some(recycled);
433
434            inner.count += 1;
435        }
436    }
437
438    /// Deallocate chunks until the pool contains at most `target` items, or
439    /// `count` chunks have been deallocated.
440    ///
441    /// Returns `true` if the target number of chunks in the pool was reached,
442    /// `false` if this method stopped before reaching the target.
443    ///
444    /// Purging chunks can be expensive so it is preferable to perform this
445    /// operation outside of the critical path. Specifying a lower `count`
446    /// allows the caller to split the work and spread it over time.
447    #[inline(never)]
448    pub fn purge_chunks(&self, target: u32, mut count: u32) -> bool {
449        let mut inner = self.inner.lock().unwrap();
450        assert!(inner.count >= 0);
451
452        while inner.count as u32 > target {
453            unsafe {
454                // First can't be None because inner.count > 0.
455                let chunk = inner.first.unwrap();
456
457                // Pop chunk off the list.
458                inner.first = chunk.as_ref().next;
459
460                // Deallocate chunk.
461                let layout = Layout::from_size_align(
462                    DEFAULT_CHUNK_SIZE,
463                    CHUNK_ALIGNMENT
464                ).unwrap();
465                Global.deallocate(chunk.cast(), layout);
466            }
467
468            inner.count -= 1;
469            count -= 1;
470
471            if count == 0 {
472                return false;
473            }
474        }
475
476        return true;
477    }
478
479    /// Deallocate all of the chunks.
480    pub fn purge_all_chunks(&self) {
481        self.purge_chunks(0, u32::MAX);
482    }
483}
484
485impl Drop for ChunkPool {
486    fn drop(&mut self) {
487        self.purge_all_chunks();
488    }
489}
490
491unsafe impl Send for ChunkPoolInner {}