1
use std::alloc::GlobalAlloc;
2
use std::alloc::Layout;
3
use std::alloc::System;
4
use std::fmt;
5
use std::ptr::NonNull;
6
use std::sync::atomic::AtomicUsize;
7
use std::sync::atomic::Ordering;
8

            
9
use allocator_api2::alloc::AllocError;
10
use allocator_api2::alloc::Allocator;
11

            
12
use merc_io::BytesFormatter;
13

            
14
/// An allocator that can be used to count performance metrics
15
/// on the allocations performed.
16
pub struct AllocCounter {
17
    number_of_allocations: AtomicUsize,
18
    size_of_allocations: AtomicUsize,
19

            
20
    total_number_of_allocations: AtomicUsize,
21
    total_size_of_allocations: AtomicUsize,
22

            
23
    max_number_of_allocations: AtomicUsize,
24
    max_size_of_allocations: AtomicUsize,
25
}
26

            
27
pub struct AllocMetrics {
28
    number_of_allocations: usize,
29
    size_of_allocations: usize,
30

            
31
    total_number_of_allocations: usize,
32
    total_size_of_allocations: usize,
33

            
34
    max_number_of_allocations: usize,
35
    max_size_of_allocations: usize,
36
}
37

            
38
impl fmt::Display for AllocMetrics {
39
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40
        writeln!(
41
            f,
42
            "Current allocations: {} (size: {} bytes)",
43
            self.number_of_allocations,
44
            BytesFormatter(self.size_of_allocations)
45
        )?;
46
        writeln!(
47
            f,
48
            "Total allocations: {} (size: {} bytes)",
49
            self.total_number_of_allocations,
50
            BytesFormatter(self.total_size_of_allocations)
51
        )?;
52
        write!(
53
            f,
54
            "Peak allocations: {} (size: {} bytes)",
55
            self.max_number_of_allocations,
56
            BytesFormatter(self.max_size_of_allocations)
57
        )
58
    }
59
}
60

            
61
impl Default for AllocCounter {
62
    /// Creates a new allocation counter with all metrics initialized to zero
63
    fn default() -> Self {
64
        Self::new()
65
    }
66
}
67

            
68
impl AllocCounter {
69
    /// Creates a new allocation counter with all metrics initialized to zero
70
3
    pub const fn new() -> Self {
71
3
        Self {
72
3
            number_of_allocations: AtomicUsize::new(0),
73
3
            size_of_allocations: AtomicUsize::new(0),
74
3
            total_number_of_allocations: AtomicUsize::new(0),
75
3
            total_size_of_allocations: AtomicUsize::new(0),
76
3
            max_number_of_allocations: AtomicUsize::new(0),
77
3
            max_size_of_allocations: AtomicUsize::new(0),
78
3
        }
79
3
    }
80

            
81
    /// Returns the performance metrics of the allocator
82
4
    pub fn get_metrics(&self) -> AllocMetrics {
83
4
        AllocMetrics {
84
4
            number_of_allocations: self.number_of_allocations.load(Ordering::Relaxed),
85
4
            size_of_allocations: self.size_of_allocations.load(Ordering::Relaxed),
86
4

            
87
4
            total_number_of_allocations: self.total_number_of_allocations.load(Ordering::Relaxed),
88
4
            total_size_of_allocations: self.total_size_of_allocations.load(Ordering::Relaxed),
89
4

            
90
4
            max_number_of_allocations: self.max_number_of_allocations.load(Ordering::Relaxed),
91
4
            max_size_of_allocations: self.max_size_of_allocations.load(Ordering::Relaxed),
92
4
        }
93
4
    }
94

            
95
    /// Resets all current allocation metrics (but preserves total and max metrics)
96
1
    pub fn reset(&self) {
97
1
        self.number_of_allocations.store(0, Ordering::Relaxed);
98
1
        self.size_of_allocations.store(0, Ordering::Relaxed);
99
1
    }
100

            
101
4001
    fn alloc(&self, layout: Layout) -> *mut u8 {
102
        // SAFETY: callers of this method (the `GlobalAlloc`/`Allocator` impls)
103
        // only pass non-zero layouts, which is `System.alloc`'s requirement.
104
4001
        let ret = unsafe { System.alloc(layout) };
105

            
106
4001
        if !ret.is_null() {
107
            // Update allocation counters atomically
108
4001
            self.number_of_allocations.fetch_add(1, Ordering::Relaxed);
109
4001
            self.size_of_allocations.fetch_add(layout.size(), Ordering::Relaxed);
110

            
111
4001
            self.total_number_of_allocations.fetch_add(1, Ordering::Relaxed);
112
4001
            self.total_size_of_allocations
113
4001
                .fetch_add(layout.size(), Ordering::Relaxed);
114

            
115
            // Update max counters using compare-and-swap loops. All counters use
116
            // `Relaxed`, so under contention a recorded peak may lag the true peak
117
            // by a few concurrent allocations; the peaks are best-effort metrics.
118
4001
            let current_allocs = self.number_of_allocations.load(Ordering::Relaxed);
119
4001
            let mut max_allocs = self.max_number_of_allocations.load(Ordering::Relaxed);
120
4002
            while current_allocs > max_allocs {
121
4
                match self.max_number_of_allocations.compare_exchange_weak(
122
4
                    max_allocs,
123
4
                    current_allocs,
124
4
                    Ordering::Relaxed,
125
4
                    Ordering::Relaxed,
126
4
                ) {
127
3
                    Ok(_) => break,
128
1
                    Err(val) => max_allocs = val,
129
                }
130
            }
131

            
132
4001
            let current_size = self.size_of_allocations.load(Ordering::Relaxed);
133
4001
            let mut max_size = self.max_size_of_allocations.load(Ordering::Relaxed);
134
4001
            while current_size > max_size {
135
3
                match self.max_size_of_allocations.compare_exchange_weak(
136
3
                    max_size,
137
3
                    current_size,
138
3
                    Ordering::Relaxed,
139
3
                    Ordering::Relaxed,
140
3
                ) {
141
3
                    Ok(_) => break,
142
                    Err(val) => max_size = val,
143
                }
144
            }
145
        }
146

            
147
4001
        ret
148
4001
    }
149

            
150
4001
    fn dealloc(&self, ptr: *mut u8, layout: Layout) {
151
4001
        unsafe {
152
4001
            System.dealloc(ptr, layout);
153
4001
        }
154

            
155
        // Update allocation counters atomically
156
4001
        self.number_of_allocations.fetch_sub(1, Ordering::Relaxed);
157
4001
        self.size_of_allocations.fetch_sub(layout.size(), Ordering::Relaxed);
158
4001
    }
159
}
160

            
161
unsafe impl GlobalAlloc for AllocCounter {
162
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
163
        self.alloc(layout)
164
    }
165

            
166
    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
167
        self.dealloc(ptr, layout)
168
    }
169
}
170

            
171
unsafe impl Allocator for AllocCounter {
172
    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
173
        if layout.size() == 0 {
174
            // `Allocator` must support zero-sized layouts, but passing them to
175
            // `GlobalAlloc::alloc` is undefined behaviour. Return a dangling,
176
            // well-aligned pointer instead, like the standard allocators do.
177
            let ptr = unsafe { NonNull::new_unchecked(std::ptr::without_provenance_mut::<u8>(layout.align())) };
178
            return Ok(NonNull::slice_from_raw_parts(ptr, 0));
179
        }
180

            
181
        let ptr = self.alloc(layout);
182

            
183
        if ptr.is_null() {
184
            return Err(AllocError);
185
        }
186

            
187
        let slice_ptr = std::ptr::slice_from_raw_parts_mut(ptr, layout.size());
188
        Ok(NonNull::new(slice_ptr).expect("The resulting ptr will never be null"))
189
    }
190

            
191
    unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
192
        if layout.size() == 0 {
193
            // Zero-sized allocations hand out a dangling pointer that was never allocated.
194
            return;
195
        }
196

            
197
        self.dealloc(ptr.as_ptr(), layout)
198
    }
199
}
200

            
201
#[cfg(test)]
202
mod tests {
203
    use super::*;
204
    use std::sync::Arc;
205
    use std::thread;
206

            
207
    #[test]
208
1
    fn test_basic_allocation_tracking() {
209
1
        let counter = AllocCounter::new();
210
1
        let metrics = counter.get_metrics();
211

            
212
        // Initially all metrics should be zero
213
1
        assert_eq!(metrics.number_of_allocations, 0);
214
1
        assert_eq!(metrics.size_of_allocations, 0);
215
1
        assert_eq!(metrics.total_number_of_allocations, 0);
216
1
        assert_eq!(metrics.total_size_of_allocations, 0);
217
1
        assert_eq!(metrics.max_number_of_allocations, 0);
218
1
        assert_eq!(metrics.max_size_of_allocations, 0);
219
1
    }
220

            
221
    #[test]
222
1
    fn test_thread_safety() {
223
1
        let counter = Arc::new(AllocCounter::new());
224
1
        let num_threads = 4;
225
1
        let allocations_per_thread = 1000;
226

            
227
1
        let handles: Vec<_> = (0..num_threads)
228
4
            .map(|_| {
229
4
                let counter = Arc::clone(&counter);
230
4
                thread::spawn(move || {
231
4
                    for _ in 0..allocations_per_thread {
232
4000
                        let layout = Layout::from_size_align(64, 8).unwrap();
233
4000
                        let ptr = counter.alloc(layout);
234
4000
                        if !ptr.is_null() {
235
4000
                            counter.dealloc(ptr, layout);
236
4000
                        }
237
                    }
238
4
                })
239
4
            })
240
1
            .collect();
241

            
242
4
        for handle in handles {
243
4
            handle.join().unwrap();
244
4
        }
245

            
246
1
        let metrics = counter.get_metrics();
247

            
248
        // After all threads complete, current allocations should be 0
249
1
        assert_eq!(metrics.number_of_allocations, 0);
250
1
        assert_eq!(metrics.size_of_allocations, 0);
251

            
252
        // Total allocations should equal num_threads * allocations_per_thread
253
1
        assert_eq!(
254
            metrics.total_number_of_allocations,
255
1
            num_threads * allocations_per_thread
256
        );
257
1
        assert_eq!(
258
            metrics.total_size_of_allocations,
259
1
            num_threads * allocations_per_thread * 64
260
        );
261
1
    }
262

            
263
    #[test]
264
1
    fn test_reset_functionality() {
265
1
        let counter = AllocCounter::new();
266

            
267
        // Simulate some allocations
268
1
        let layout = Layout::from_size_align(32, 8).unwrap();
269
1
        let ptr = counter.alloc(layout);
270

            
271
1
        let metrics_before = counter.get_metrics();
272
1
        assert!(metrics_before.number_of_allocations > 0);
273

            
274
1
        counter.reset();
275
1
        let metrics_after = counter.get_metrics();
276

            
277
        // Current metrics should be reset
278
1
        assert_eq!(metrics_after.number_of_allocations, 0);
279
1
        assert_eq!(metrics_after.size_of_allocations, 0);
280

            
281
        // Total and max metrics should be preserved
282
1
        assert_eq!(
283
            metrics_after.total_number_of_allocations,
284
            metrics_before.total_number_of_allocations
285
        );
286
1
        assert_eq!(
287
            metrics_after.max_number_of_allocations,
288
            metrics_before.max_number_of_allocations
289
        );
290

            
291
        // Clean up
292
1
        if !ptr.is_null() {
293
1
            counter.dealloc(ptr, layout);
294
1
        }
295
1
    }
296
}