1#![deny(missing_docs)]
21
22use std::cell::RefCell;
23use std::collections::HashMap;
24use std::fs::File;
25use std::marker::PhantomData;
26use std::mem::{take, ManuallyDrop, MaybeUninit};
27use std::ops::{Deref, Range};
28use std::os::fd::{AsFd, AsRawFd};
29use std::path::PathBuf;
30use std::ptr::NonNull;
31use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
32use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender};
33use std::sync::{Arc, Mutex, OnceLock, RwLock};
34use std::thread::{JoinHandle, ThreadId};
35use std::time::{Duration, Instant};
36
37use crossbeam_deque::{Injector, Steal, Stealer, Worker};
38use memmap2::MmapMut;
39use numa_maps::NumaMap;
40use thiserror::Error;
41
42mod readme {
43 #![doc = include_str!("../README.md")]
44}
45
46pub struct Handle {
51 ptr: NonNull<u8>,
53 len: usize,
55}
56
57unsafe impl Send for Handle {}
58unsafe impl Sync for Handle {}
59
60#[allow(clippy::len_without_is_empty)]
61impl Handle {
62 fn new(ptr: NonNull<u8>, len: usize) -> Self {
64 Self { ptr, len }
65 }
66
67 fn dangling() -> Self {
69 Self {
70 ptr: NonNull::dangling(),
71 len: 0,
72 }
73 }
74
75 fn is_dangling(&self) -> bool {
76 self.ptr == NonNull::dangling()
77 }
78
79 fn len(&self) -> usize {
81 self.len
82 }
83
84 fn as_non_null(&self) -> NonNull<u8> {
86 self.ptr
87 }
88
89 fn clear(&mut self) -> std::io::Result<()> {
91 unsafe { self.madvise(MADV_DONTNEED_STRATEGY) }
93 }
94
95 fn fast_clear(&mut self) -> std::io::Result<()> {
97 unsafe { self.madvise(libc::MADV_DONTNEED) }
99 }
100
101 unsafe fn madvise(&self, advice: libc::c_int) -> std::io::Result<()> {
103 let ptr = self.as_non_null().as_ptr().cast();
110 let ret = unsafe { libc::madvise(ptr, self.len, advice) };
111 if ret != 0 {
112 let err = std::io::Error::last_os_error();
113 return Err(err);
114 }
115 Ok(())
116 }
117}
118
119const INITIAL_SIZE: usize = 32 << 20;
121
122pub const VALID_SIZE_CLASS: Range<usize> = 10..37;
124
125#[cfg(target_os = "linux")]
130const MADV_DONTNEED_STRATEGY: libc::c_int = libc::MADV_REMOVE;
131
132#[cfg(not(target_os = "linux"))]
133const MADV_DONTNEED_STRATEGY: libc::c_int = libc::MADV_DONTNEED;
134
135type PhantomUnsyncUnsend<T> = PhantomData<*mut T>;
136
137#[derive(Error, Debug)]
139pub enum AllocError {
140 #[error("I/O error")]
142 Io(#[from] std::io::Error),
143 #[error("Out of memory")]
145 OutOfMemory,
146 #[error("Invalid size class")]
148 InvalidSizeClass(usize),
149 #[error("Disabled by configuration")]
151 Disabled,
152 #[error("Memory unsuitable for requested alignment")]
154 UnalignedMemory,
155}
156
157impl AllocError {
158 #[must_use]
160 pub fn is_disabled(&self) -> bool {
161 matches!(self, AllocError::Disabled)
162 }
163}
164
165#[derive(Clone, Copy)]
167struct SizeClass(usize);
168
169impl SizeClass {
170 const fn new_unchecked(value: usize) -> Self {
171 Self(value)
172 }
173
174 const fn index(self) -> usize {
175 self.0 - VALID_SIZE_CLASS.start
176 }
177
178 const fn byte_size(self) -> usize {
180 1 << self.0
181 }
182
183 const fn from_index(index: usize) -> Self {
184 Self(index + VALID_SIZE_CLASS.start)
185 }
186
187 fn from_byte_size(byte_size: usize) -> Result<Self, AllocError> {
189 let class = byte_size.next_power_of_two().trailing_zeros() as usize;
190 class.try_into()
191 }
192
193 const fn from_byte_size_unchecked(byte_size: usize) -> Self {
194 Self::new_unchecked(byte_size.next_power_of_two().trailing_zeros() as usize)
195 }
196}
197
198impl TryFrom<usize> for SizeClass {
199 type Error = AllocError;
200
201 fn try_from(value: usize) -> Result<Self, Self::Error> {
202 if VALID_SIZE_CLASS.contains(&value) {
203 Ok(SizeClass(value))
204 } else {
205 Err(AllocError::InvalidSizeClass(value))
206 }
207 }
208}
209
210#[derive(Default, Debug)]
211struct AllocStats {
212 allocations: AtomicU64,
213 slow_path: AtomicU64,
214 refill: AtomicU64,
215 deallocations: AtomicU64,
216 clear_eager: AtomicU64,
217 clear_slow: AtomicU64,
218}
219
220static INJECTOR: OnceLock<GlobalStealer> = OnceLock::new();
222
223static LGALLOC_ENABLED: AtomicBool = AtomicBool::new(false);
225
226static LGALLOC_EAGER_RETURN: AtomicBool = AtomicBool::new(false);
228
229static LGALLOC_FILE_GROWTH_DAMPENER: AtomicUsize = AtomicUsize::new(0);
234
235static LOCAL_BUFFER_BYTES: AtomicUsize = AtomicUsize::new(32 << 20);
237
238struct GlobalStealer {
240 size_classes: Vec<SizeClassState>,
243 path: RwLock<Option<PathBuf>>,
245 background_sender: Mutex<Option<(JoinHandle<()>, Sender<BackgroundWorkerConfig>)>>,
247}
248
249#[derive(Default)]
251struct SizeClassState {
252 areas: RwLock<Vec<ManuallyDrop<(File, MmapMut)>>>,
256 injector: Injector<Handle>,
258 clean_injector: Injector<Handle>,
260 lock: Mutex<()>,
262 stealers: RwLock<HashMap<ThreadId, PerThreadState<Handle>>>,
264 alloc_stats: AllocStats,
266 total_bytes: AtomicUsize,
268 area_count: AtomicUsize,
270}
271
272impl GlobalStealer {
273 fn get_static() -> &'static Self {
275 INJECTOR.get_or_init(Self::new)
276 }
277
278 fn get_size_class(&self, size_class: SizeClass) -> &SizeClassState {
280 &self.size_classes[size_class.index()]
281 }
282
283 fn new() -> Self {
284 let mut size_classes = Vec::with_capacity(VALID_SIZE_CLASS.len());
285
286 for _ in VALID_SIZE_CLASS {
287 size_classes.push(SizeClassState::default());
288 }
289
290 Self {
291 size_classes,
292 path: RwLock::default(),
293 background_sender: Mutex::default(),
294 }
295 }
296}
297
298impl Drop for GlobalStealer {
299 fn drop(&mut self) {
300 take(&mut self.size_classes);
301 }
302}
303
304struct PerThreadState<T> {
305 stealer: Stealer<T>,
306 alloc_stats: Arc<AllocStats>,
307}
308
309struct ThreadLocalStealer {
311 size_classes: Vec<LocalSizeClass>,
313 _phantom: PhantomUnsyncUnsend<Self>,
314}
315
316impl ThreadLocalStealer {
317 fn new() -> Self {
318 let thread_id = std::thread::current().id();
319 let size_classes = VALID_SIZE_CLASS
320 .map(|size_class| LocalSizeClass::new(SizeClass::new_unchecked(size_class), thread_id))
321 .collect();
322 Self {
323 size_classes,
324 _phantom: PhantomData,
325 }
326 }
327
328 fn allocate(&self, size_class: SizeClass) -> Result<Handle, AllocError> {
333 if !LGALLOC_ENABLED.load(Ordering::Relaxed) {
334 return Err(AllocError::Disabled);
335 }
336 self.size_classes[size_class.index()].get_with_refill()
337 }
338
339 fn deallocate(&self, mem: Handle) {
341 let size_class = SizeClass::from_byte_size_unchecked(mem.len());
342
343 self.size_classes[size_class.index()].push(mem);
344 }
345}
346
347thread_local! {
348 static WORKER: RefCell<ThreadLocalStealer> = RefCell::new(ThreadLocalStealer::new());
349}
350
351struct LocalSizeClass {
358 worker: Worker<Handle>,
360 size_class: SizeClass,
362 size_class_state: &'static SizeClassState,
364 thread_id: ThreadId,
366 stats: Arc<AllocStats>,
368 _phantom: PhantomUnsyncUnsend<Self>,
370}
371
372impl LocalSizeClass {
373 fn new(size_class: SizeClass, thread_id: ThreadId) -> Self {
375 let worker = Worker::new_lifo();
376 let stealer = GlobalStealer::get_static();
377 let size_class_state = stealer.get_size_class(size_class);
378
379 let stats = Arc::new(AllocStats::default());
380
381 let mut lock = size_class_state.stealers.write().expect("lock poisoned");
382 lock.insert(
383 thread_id,
384 PerThreadState {
385 stealer: worker.stealer(),
386 alloc_stats: Arc::clone(&stats),
387 },
388 );
389
390 Self {
391 worker,
392 size_class,
393 size_class_state,
394 thread_id,
395 stats,
396 _phantom: PhantomData,
397 }
398 }
399
400 #[inline]
405 fn get(&self) -> Result<Handle, AllocError> {
406 self.worker
407 .pop()
408 .or_else(|| {
409 std::iter::repeat_with(|| {
410 let limit = 1.max(
415 LOCAL_BUFFER_BYTES.load(Ordering::Relaxed)
416 / self.size_class.byte_size()
417 / 2,
418 );
419
420 self.size_class_state
421 .injector
422 .steal_batch_with_limit_and_pop(&self.worker, limit)
423 .or_else(|| {
424 self.size_class_state
425 .clean_injector
426 .steal_batch_with_limit_and_pop(&self.worker, limit)
427 })
428 .or_else(|| {
429 self.size_class_state
430 .stealers
431 .read()
432 .expect("lock poisoned")
433 .values()
434 .map(|state| state.stealer.steal())
435 .collect()
436 })
437 })
438 .find(|s| !s.is_retry())
439 .and_then(Steal::success)
440 })
441 .ok_or(AllocError::OutOfMemory)
442 }
443
444 fn get_with_refill(&self) -> Result<Handle, AllocError> {
446 self.stats.allocations.fetch_add(1, Ordering::Relaxed);
447 match self.get() {
449 Err(AllocError::OutOfMemory) => {
450 self.stats.slow_path.fetch_add(1, Ordering::Relaxed);
451 let _lock = self.size_class_state.lock.lock().expect("lock poisoned");
453 if let Ok(mem) = self.get() {
455 return Ok(mem);
456 }
457 self.try_refill_and_get()
458 }
459 r => r,
460 }
461 }
462
463 fn push(&self, mut mem: Handle) {
465 debug_assert_eq!(mem.len(), self.size_class.byte_size());
466 self.stats.deallocations.fetch_add(1, Ordering::Relaxed);
467 if self.worker.len()
468 >= LOCAL_BUFFER_BYTES.load(Ordering::Relaxed) / self.size_class.byte_size()
469 {
470 if LGALLOC_EAGER_RETURN.load(Ordering::Relaxed) {
471 self.stats.clear_eager.fetch_add(1, Ordering::Relaxed);
472 mem.fast_clear().expect("clearing successful");
473 }
474 self.size_class_state.injector.push(mem);
475 } else {
476 self.worker.push(mem);
477 }
478 }
479
480 fn try_refill_and_get(&self) -> Result<Handle, AllocError> {
484 self.stats.refill.fetch_add(1, Ordering::Relaxed);
485 let mut stash = self.size_class_state.areas.write().expect("lock poisoned");
486
487 let initial_capacity = std::cmp::max(1, INITIAL_SIZE / self.size_class.byte_size());
488
489 let last_capacity =
490 stash.iter().last().map_or(0, |mmap| mmap.1.len()) / self.size_class.byte_size();
491 let growth_dampener = LGALLOC_FILE_GROWTH_DAMPENER.load(Ordering::Relaxed);
492 let next_capacity = last_capacity
495 + std::cmp::max(
496 initial_capacity,
497 last_capacity / (growth_dampener.saturating_add(1)),
498 );
499
500 let next_byte_len = next_capacity * self.size_class.byte_size();
501 let (file, mut mmap) = Self::init_file(next_byte_len)?;
502
503 self.size_class_state
504 .total_bytes
505 .fetch_add(next_byte_len, Ordering::Relaxed);
506 self.size_class_state
507 .area_count
508 .fetch_add(1, Ordering::Relaxed);
509
510 let mut chunks = mmap
512 .as_mut()
513 .chunks_exact_mut(self.size_class.byte_size())
514 .map(|chunk| NonNull::new(chunk.as_mut_ptr()).expect("non-null"));
515
516 let ptr = chunks.next().expect("At least once chunk allocated.");
518 let mem = Handle::new(ptr, self.size_class.byte_size());
519
520 for ptr in chunks {
522 self.size_class_state
523 .clean_injector
524 .push(Handle::new(ptr, self.size_class.byte_size()));
525 }
526
527 stash.push(ManuallyDrop::new((file, mmap)));
528 Ok(mem)
529 }
530
531 fn init_file(byte_len: usize) -> Result<(File, MmapMut), AllocError> {
534 let file = {
535 let path = GlobalStealer::get_static()
536 .path
537 .read()
538 .expect("lock poisoned");
539 let Some(path) = &*path else {
540 return Err(AllocError::Io(std::io::Error::from(
541 std::io::ErrorKind::NotFound,
542 )));
543 };
544 tempfile::tempfile_in(path)?
545 };
546 let fd = file.as_fd().as_raw_fd();
547 let length = libc::off_t::try_from(byte_len).expect("Must fit");
548 let ret = unsafe { libc::ftruncate(fd, length) };
550 if ret != 0 {
551 return Err(std::io::Error::last_os_error().into());
553 }
554 let mmap = unsafe { memmap2::MmapOptions::new().map_mut(&file)? };
556 assert_eq!(mmap.len(), byte_len);
557 Ok((file, mmap))
558 }
559}
560
561impl Drop for LocalSizeClass {
562 fn drop(&mut self) {
563 if let Ok(mut lock) = self.size_class_state.stealers.write() {
565 lock.remove(&self.thread_id);
566 }
567
568 while let Some(mem) = self.worker.pop() {
570 self.size_class_state.injector.push(mem);
571 }
572
573 let ordering = Ordering::Relaxed;
574
575 self.size_class_state
577 .alloc_stats
578 .allocations
579 .fetch_add(self.stats.allocations.load(ordering), ordering);
580 let global_stats = &self.size_class_state.alloc_stats;
581 global_stats
582 .refill
583 .fetch_add(self.stats.refill.load(ordering), ordering);
584 global_stats
585 .slow_path
586 .fetch_add(self.stats.slow_path.load(ordering), ordering);
587 global_stats
588 .deallocations
589 .fetch_add(self.stats.deallocations.load(ordering), ordering);
590 global_stats
591 .clear_slow
592 .fetch_add(self.stats.clear_slow.load(ordering), ordering);
593 global_stats
594 .clear_eager
595 .fetch_add(self.stats.clear_eager.load(ordering), ordering);
596 }
597}
598
599fn thread_context<R, F: FnOnce(&ThreadLocalStealer) -> R>(f: F) -> R {
601 WORKER.with(|cell| f(&cell.borrow()))
602}
603
604pub fn allocate<T>(capacity: usize) -> Result<(NonNull<T>, usize, Handle), AllocError> {
631 if std::mem::size_of::<T>() == 0 {
632 return Ok((NonNull::dangling(), usize::MAX, Handle::dangling()));
633 } else if capacity == 0 {
634 return Ok((NonNull::dangling(), 0, Handle::dangling()));
635 }
636
637 let byte_len = std::cmp::max(page_size::get(), std::mem::size_of::<T>() * capacity);
639 let size_class = SizeClass::from_byte_size(byte_len)?;
642
643 let handle = thread_context(|s| s.allocate(size_class))?;
644 debug_assert_eq!(handle.len(), size_class.byte_size());
645 let ptr: NonNull<T> = handle.as_non_null().cast();
646 if ptr.as_ptr().align_offset(std::mem::align_of::<T>()) != 0 {
649 thread_context(move |s| s.deallocate(handle));
650 return Err(AllocError::UnalignedMemory);
651 }
652 let actual_capacity = handle.len() / std::mem::size_of::<T>();
653 Ok((ptr, actual_capacity, handle))
654}
655
656pub fn deallocate(handle: Handle) {
666 if handle.is_dangling() {
667 return;
668 }
669 thread_context(|s| s.deallocate(handle));
670}
671
672struct BackgroundWorker {
674 config: BackgroundWorkerConfig,
675 receiver: Receiver<BackgroundWorkerConfig>,
676 global_stealer: &'static GlobalStealer,
677 worker: Worker<Handle>,
678}
679
680impl BackgroundWorker {
681 fn new(receiver: Receiver<BackgroundWorkerConfig>) -> Self {
682 let config = BackgroundWorkerConfig {
683 interval: Duration::MAX,
684 ..Default::default()
685 };
686 let global_stealer = GlobalStealer::get_static();
687 let worker = Worker::new_fifo();
688 Self {
689 config,
690 receiver,
691 global_stealer,
692 worker,
693 }
694 }
695
696 fn run(&mut self) {
697 let mut next_cleanup: Option<Instant> = None;
698 loop {
699 let timeout = next_cleanup.map_or(Duration::MAX, |next_cleanup| {
700 next_cleanup.saturating_duration_since(Instant::now())
701 });
702 match self.receiver.recv_timeout(timeout) {
703 Ok(config) => {
704 self.config = config;
705 next_cleanup = None;
706 }
707 Err(RecvTimeoutError::Disconnected) => break,
708 Err(RecvTimeoutError::Timeout) => {
709 self.maintenance();
710 }
711 }
712 next_cleanup = next_cleanup
713 .unwrap_or_else(Instant::now)
714 .checked_add(self.config.interval);
715 }
716 }
717
718 fn maintenance(&self) {
719 for (index, size_class_state) in self.global_stealer.size_classes.iter().enumerate() {
720 let size_class = SizeClass::from_index(index);
721 let count = self.clear(size_class, size_class_state, &self.worker);
722 size_class_state
723 .alloc_stats
724 .clear_slow
725 .fetch_add(count.try_into().expect("must fit"), Ordering::Relaxed);
726 }
727 }
728
729 fn clear(
730 &self,
731 size_class: SizeClass,
732 state: &SizeClassState,
733 worker: &Worker<Handle>,
734 ) -> usize {
735 let byte_size = size_class.byte_size();
737 let mut limit = (self.config.clear_bytes + byte_size - 1) / byte_size;
738 let mut count = 0;
739 let mut steal = Steal::Retry;
740 while limit > 0 && !steal.is_empty() {
741 steal = std::iter::repeat_with(|| state.injector.steal_batch_with_limit(worker, limit))
742 .find(|s| !s.is_retry())
743 .unwrap_or(Steal::Empty);
744 while let Some(mut mem) = worker.pop() {
745 match mem.clear() {
746 Ok(()) => count += 1,
747 Err(e) => panic!("Syscall failed: {e:?}"),
748 }
749 state.clean_injector.push(mem);
750 limit -= 1;
751 }
752 }
753 count
754 }
755}
756
757pub fn lgalloc_set_config(config: &LgAlloc) {
772 let stealer = GlobalStealer::get_static();
773
774 if let Some(enabled) = &config.enabled {
775 LGALLOC_ENABLED.store(*enabled, Ordering::Relaxed);
776 }
777
778 if let Some(eager_return) = &config.eager_return {
779 LGALLOC_EAGER_RETURN.store(*eager_return, Ordering::Relaxed);
780 }
781
782 if let Some(path) = &config.path {
783 *stealer.path.write().expect("lock poisoned") = Some(path.clone());
784 }
785
786 if let Some(file_growth_dampener) = &config.file_growth_dampener {
787 LGALLOC_FILE_GROWTH_DAMPENER.store(*file_growth_dampener, Ordering::Relaxed);
788 }
789
790 if let Some(local_buffer_bytes) = &config.local_buffer_bytes {
791 LOCAL_BUFFER_BYTES.store(*local_buffer_bytes, Ordering::Relaxed);
792 }
793
794 if let Some(config) = config.background_config.clone() {
795 let mut lock = stealer.background_sender.lock().expect("lock poisoned");
796
797 let config = if let Some((_, sender)) = &*lock {
798 match sender.send(config) {
799 Ok(()) => None,
800 Err(err) => Some(err.0),
801 }
802 } else {
803 Some(config)
804 };
805 if let Some(config) = config {
806 let (sender, receiver) = std::sync::mpsc::channel();
807 let mut worker = BackgroundWorker::new(receiver);
808 let join_handle = std::thread::Builder::new()
809 .name("lgalloc-0".to_string())
810 .spawn(move || worker.run())
811 .expect("thread started successfully");
812 sender.send(config).expect("Receiver exists");
813 *lock = Some((join_handle, sender));
814 }
815 }
816}
817
818#[derive(Default, Debug, Clone, Eq, PartialEq)]
820pub struct BackgroundWorkerConfig {
821 pub interval: Duration,
823 pub clear_bytes: usize,
825}
826
827#[derive(Default, Clone, Eq, PartialEq)]
829pub struct LgAlloc {
830 pub enabled: Option<bool>,
832 pub path: Option<PathBuf>,
834 pub background_config: Option<BackgroundWorkerConfig>,
836 pub eager_return: Option<bool>,
838 pub file_growth_dampener: Option<usize>,
840 pub local_buffer_bytes: Option<usize>,
842}
843
844impl LgAlloc {
845 #[must_use]
847 pub fn new() -> Self {
848 Self::default()
849 }
850
851 pub fn enable(&mut self) -> &mut Self {
853 self.enabled = Some(true);
854 self
855 }
856
857 pub fn disable(&mut self) -> &mut Self {
859 self.enabled = Some(false);
860 self
861 }
862
863 pub fn with_background_config(&mut self, config: BackgroundWorkerConfig) -> &mut Self {
865 self.background_config = Some(config);
866 self
867 }
868
869 pub fn with_path(&mut self, path: PathBuf) -> &mut Self {
871 self.path = Some(path);
872 self
873 }
874
875 pub fn eager_return(&mut self, eager_return: bool) -> &mut Self {
877 self.eager_return = Some(eager_return);
878 self
879 }
880
881 pub fn file_growth_dampener(&mut self, file_growth_dapener: usize) -> &mut Self {
883 self.file_growth_dampener = Some(file_growth_dapener);
884 self
885 }
886
887 pub fn local_buffer_bytes(&mut self, local_buffer_bytes: usize) -> &mut Self {
889 self.local_buffer_bytes = Some(local_buffer_bytes);
890 self
891 }
892}
893
894pub fn lgalloc_stats() -> LgAllocStats {
907 LgAllocStats::read(None)
908}
909
910pub fn lgalloc_stats_with_mapping() -> std::io::Result<LgAllocStats> {
924 let mut numa_map = NumaMap::from_file("/proc/self/numa_maps")?;
925 Ok(LgAllocStats::read(Some(&mut numa_map)))
926}
927
928#[derive(Debug)]
930pub struct LgAllocStats {
931 pub size_class: Vec<(usize, SizeClassStats)>,
933 pub file: Vec<(usize, std::io::Result<FileStats>)>,
936 pub map: Option<Vec<(usize, MapStats)>>,
939}
940
941impl LgAllocStats {
942 fn read(mut numa_map: Option<&mut NumaMap>) -> Self {
946 let global = GlobalStealer::get_static();
947
948 if let Some(numa_map) = numa_map.as_mut() {
949 for entry in &mut numa_map.ranges {
951 entry.normalize();
952 }
953 numa_map.ranges.sort();
954 }
955
956 let mut size_class_stats = Vec::with_capacity(VALID_SIZE_CLASS.len());
957 let mut file_stats = Vec::default();
958 let mut map_stats = Vec::default();
959 for (index, state) in global.size_classes.iter().enumerate() {
960 let size_class = SizeClass::from_index(index);
961 let size_class_bytes = size_class.byte_size();
962
963 size_class_stats.push((size_class_bytes, SizeClassStats::from(state)));
964
965 let areas = state.areas.read().expect("lock poisoned");
966 for (file, mmap) in areas.iter().map(Deref::deref) {
967 file_stats.push((size_class_bytes, FileStats::extract_from(file)));
968 if let Some(numa_map) = numa_map.as_deref() {
969 map_stats.push((size_class_bytes, MapStats::extract_from(mmap, numa_map)));
970 }
971 }
972 }
973
974 Self {
975 size_class: size_class_stats,
976 file: file_stats,
977 map: numa_map.map(|_| map_stats),
978 }
979 }
980}
981
982#[derive(Debug)]
984pub struct SizeClassStats {
985 pub areas: usize,
987 pub area_total_bytes: usize,
989 pub free_regions: usize,
991 pub clean_regions: usize,
993 pub global_regions: usize,
995 pub thread_regions: usize,
997 pub allocations: u64,
999 pub slow_path: u64,
1001 pub refill: u64,
1003 pub deallocations: u64,
1005 pub clear_eager_total: u64,
1007 pub clear_slow_total: u64,
1009}
1010
1011impl From<&SizeClassState> for SizeClassStats {
1012 fn from(size_class_state: &SizeClassState) -> Self {
1013 let areas = size_class_state.area_count.load(Ordering::Relaxed);
1014 let area_total_bytes = size_class_state.total_bytes.load(Ordering::Relaxed);
1015 let global_regions = size_class_state.injector.len();
1016 let clean_regions = size_class_state.clean_injector.len();
1017 let stealers = size_class_state.stealers.read().expect("lock poisoned");
1018 let mut thread_regions = 0;
1019 let mut allocations = 0;
1020 let mut deallocations = 0;
1021 let mut refill = 0;
1022 let mut slow_path = 0;
1023 let mut clear_eager_total = 0;
1024 let mut clear_slow_total = 0;
1025 for thread_state in stealers.values() {
1026 thread_regions += thread_state.stealer.len();
1027 let thread_stats = &*thread_state.alloc_stats;
1028 allocations += thread_stats.allocations.load(Ordering::Relaxed);
1029 deallocations += thread_stats.deallocations.load(Ordering::Relaxed);
1030 refill += thread_stats.refill.load(Ordering::Relaxed);
1031 slow_path += thread_stats.slow_path.load(Ordering::Relaxed);
1032 clear_eager_total += thread_stats.clear_eager.load(Ordering::Relaxed);
1033 clear_slow_total += thread_stats.clear_slow.load(Ordering::Relaxed);
1034 }
1035
1036 let free_regions = thread_regions + global_regions + clean_regions;
1037
1038 let global_stats = &size_class_state.alloc_stats;
1039 allocations += global_stats.allocations.load(Ordering::Relaxed);
1040 deallocations += global_stats.deallocations.load(Ordering::Relaxed);
1041 refill += global_stats.refill.load(Ordering::Relaxed);
1042 slow_path += global_stats.slow_path.load(Ordering::Relaxed);
1043 clear_eager_total += global_stats.clear_eager.load(Ordering::Relaxed);
1044 clear_slow_total += global_stats.clear_slow.load(Ordering::Relaxed);
1045 Self {
1046 areas,
1047 area_total_bytes,
1048 free_regions,
1049 global_regions,
1050 clean_regions,
1051 thread_regions,
1052 allocations,
1053 deallocations,
1054 refill,
1055 slow_path,
1056 clear_eager_total,
1057 clear_slow_total,
1058 }
1059 }
1060}
1061
1062#[derive(Debug)]
1064pub struct FileStats {
1065 pub file_size: usize,
1067 pub allocated_size: usize,
1069}
1070
1071impl FileStats {
1072 fn extract_from(file: &File) -> std::io::Result<Self> {
1075 let mut stat: MaybeUninit<libc::stat> = MaybeUninit::uninit();
1076 let ret = unsafe { libc::fstat(file.as_raw_fd(), stat.as_mut_ptr()) };
1078 if ret == -1 {
1079 Err(std::io::Error::last_os_error())
1080 } else {
1081 let stat = unsafe { stat.assume_init_ref() };
1083 let blocks = stat.st_blocks.try_into().unwrap_or(0);
1084 let file_size = stat.st_size.try_into().unwrap_or(0);
1085 Ok(FileStats {
1086 file_size,
1087 allocated_size: blocks * 512,
1089 })
1090 }
1091 }
1092}
1093
1094#[derive(Debug)]
1096pub struct MapStats {
1097 pub mapped: usize,
1099 pub active: usize,
1101 pub dirty: usize,
1103}
1104
1105impl MapStats {
1106 fn extract_from(mmap: &MmapMut, numa_map: &NumaMap) -> Self {
1110 let base = mmap.as_ptr().cast::<()>() as usize;
1112 let range = match numa_map
1113 .ranges
1114 .binary_search_by(|range| range.address.cmp(&base))
1115 {
1116 Ok(pos) => Some(&numa_map.ranges[pos]),
1117 Err(_pos) => None,
1120 };
1121
1122 let mut mapped = 0;
1123 let mut active = 0;
1124 let mut dirty = 0;
1125 for property in range.iter().flat_map(|e| e.properties.iter()) {
1126 match property {
1127 numa_maps::Property::Dirty(d) => dirty = *d,
1128 numa_maps::Property::Mapped(m) => mapped = *m,
1129 numa_maps::Property::Active(a) => active = *a,
1130 _ => {}
1131 }
1132 }
1133
1134 Self {
1135 mapped,
1136 active,
1137 dirty,
1138 }
1139 }
1140}
1141
1142#[cfg(test)]
1143mod test {
1144 use std::mem::{ManuallyDrop, MaybeUninit};
1145 use std::ptr::NonNull;
1146 use std::sync::atomic::{AtomicBool, Ordering};
1147 use std::sync::Arc;
1148 use std::time::Duration;
1149
1150 use serial_test::serial;
1151
1152 use super::*;
1153
1154 fn initialize() {
1155 lgalloc_set_config(
1156 LgAlloc::new()
1157 .enable()
1158 .with_background_config(BackgroundWorkerConfig {
1159 interval: Duration::from_secs(1),
1160 clear_bytes: 4 << 20,
1161 })
1162 .with_path(std::env::temp_dir())
1163 .file_growth_dampener(1),
1164 );
1165 }
1166
1167 struct Wrapper<T> {
1168 handle: MaybeUninit<Handle>,
1169 ptr: NonNull<MaybeUninit<T>>,
1170 cap: usize,
1171 }
1172
1173 unsafe impl<T: Send> Send for Wrapper<T> {}
1174 unsafe impl<T: Sync> Sync for Wrapper<T> {}
1175
1176 impl<T> Wrapper<T> {
1177 fn allocate(capacity: usize) -> Result<Self, AllocError> {
1178 let (ptr, cap, handle) = allocate(capacity)?;
1179 assert!(cap > 0);
1180 let handle = MaybeUninit::new(handle);
1181 Ok(Self { ptr, cap, handle })
1182 }
1183
1184 fn as_slice(&mut self) -> &mut [MaybeUninit<T>] {
1185 unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.cap) }
1186 }
1187 }
1188
1189 impl<T> Drop for Wrapper<T> {
1190 fn drop(&mut self) {
1191 unsafe { deallocate(self.handle.assume_init_read()) };
1192 }
1193 }
1194
1195 #[test]
1196 #[serial]
1197 fn test_readme() -> Result<(), AllocError> {
1198 initialize();
1199
1200 let (ptr, cap, handle) = allocate::<u8>(2 << 20)?;
1202 let mut vec = ManuallyDrop::new(unsafe { Vec::from_raw_parts(ptr.as_ptr(), 0, cap) });
1204
1205 vec.extend_from_slice(&[1, 2, 3, 4]);
1207
1208 assert_eq!(&*vec, &[1, 2, 3, 4]);
1210
1211 deallocate(handle);
1213 Ok(())
1214 }
1215
1216 #[test]
1217 #[serial]
1218 fn test_1() -> Result<(), AllocError> {
1219 initialize();
1220 <Wrapper<u8>>::allocate(4 << 20)?.as_slice()[0] = MaybeUninit::new(1);
1221 Ok(())
1222 }
1223
1224 #[test]
1225 #[serial]
1226 fn test_3() -> Result<(), AllocError> {
1227 initialize();
1228 let until = Arc::new(AtomicBool::new(true));
1229
1230 let inner = || {
1231 let until = Arc::clone(&until);
1232 move || {
1233 let mut i = 0;
1234 let until = &*until;
1235 while until.load(Ordering::Relaxed) {
1236 i += 1;
1237 let mut r = <Wrapper<u8>>::allocate(4 << 20).unwrap();
1238 r.as_slice()[0] = MaybeUninit::new(1);
1239 }
1240 println!("repetitions: {i}");
1241 }
1242 };
1243 let handles = [
1244 std::thread::spawn(inner()),
1245 std::thread::spawn(inner()),
1246 std::thread::spawn(inner()),
1247 std::thread::spawn(inner()),
1248 ];
1249 std::thread::sleep(Duration::from_secs(4));
1250 until.store(false, Ordering::Relaxed);
1251 for handle in handles {
1252 handle.join().unwrap();
1253 }
1254 Ok(())
1256 }
1257
1258 #[test]
1259 #[serial]
1260 fn test_4() -> Result<(), AllocError> {
1261 initialize();
1262 let until = Arc::new(AtomicBool::new(true));
1263
1264 let inner = || {
1265 let until = Arc::clone(&until);
1266 move || {
1267 let mut i = 0;
1268 let until = &*until;
1269 let batch = 64;
1270 let mut buffer = Vec::with_capacity(batch);
1271 while until.load(Ordering::Relaxed) {
1272 i += 64;
1273 buffer.extend((0..batch).map(|_| {
1274 let mut r = <Wrapper<u8>>::allocate(2 << 20).unwrap();
1275 r.as_slice()[0] = MaybeUninit::new(1);
1276 r
1277 }));
1278 buffer.clear();
1279 }
1280 println!("repetitions vec: {i}");
1281 }
1282 };
1283 let handles = [
1284 std::thread::spawn(inner()),
1285 std::thread::spawn(inner()),
1286 std::thread::spawn(inner()),
1287 std::thread::spawn(inner()),
1288 ];
1289 std::thread::sleep(Duration::from_secs(4));
1290 until.store(false, Ordering::Relaxed);
1291 for handle in handles {
1292 handle.join().unwrap();
1293 }
1294 std::thread::sleep(Duration::from_secs(1));
1295 let stats = lgalloc_stats();
1296 for size_class in &stats.size_class {
1297 println!("size_class {:?}", size_class);
1298 }
1299 for (size_class, file_stats) in &stats.file {
1300 match file_stats {
1301 Ok(file_stats) => println!("file_stats {size_class} {file_stats:?}"),
1302 Err(e) => eprintln!("error: {e}"),
1303 }
1304 }
1305 Ok(())
1306 }
1307
1308 #[test]
1309 #[serial]
1310 fn leak() -> Result<(), AllocError> {
1311 lgalloc_set_config(&LgAlloc {
1312 enabled: Some(true),
1313 path: Some(std::env::temp_dir()),
1314 ..Default::default()
1315 });
1316 let r = <Wrapper<u8>>::allocate(1000)?;
1317
1318 let thread = std::thread::spawn(move || drop(r));
1319
1320 thread.join().unwrap();
1321 Ok(())
1322 }
1323
1324 #[test]
1325 #[serial]
1326 fn test_zst() -> Result<(), AllocError> {
1327 initialize();
1328 <Wrapper<()>>::allocate(10)?;
1329 Ok(())
1330 }
1331
1332 #[test]
1333 #[serial]
1334 fn test_zero_capacity_zst() -> Result<(), AllocError> {
1335 initialize();
1336 <Wrapper<()>>::allocate(0)?;
1337 Ok(())
1338 }
1339
1340 #[test]
1341 #[serial]
1342 fn test_zero_capacity_nonzst() -> Result<(), AllocError> {
1343 initialize();
1344 <Wrapper<()>>::allocate(0)?;
1345 Ok(())
1346 }
1347
1348 #[test]
1349 #[serial]
1350 fn test_stats() -> Result<(), AllocError> {
1351 initialize();
1352 let (_ptr, _cap, handle) = allocate::<usize>(1024)?;
1353 deallocate(handle);
1354
1355 let stats = lgalloc_stats();
1356
1357 assert!(!stats.size_class.is_empty());
1358
1359 Ok(())
1360 }
1361
1362 #[test]
1363 #[serial]
1364 fn test_disable() {
1365 lgalloc_set_config(&*LgAlloc::new().disable());
1366 assert!(matches!(allocate::<u8>(1024), Err(AllocError::Disabled)));
1367 }
1368}