stoolap-jemalloc 0.1.5

A jemalloc-style memory allocator written in pure Rust, with a built-in pprof heap profiler
Documentation
// Copyright 2026 Stoolap Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Small workloads that Miri can run in reasonable time, to check the
//! allocator against Rust's aliasing and data race rules:
//!
//! ```sh
//! MIRIFLAGS=-Zmiri-ignore-leaks cargo +nightly miri test --test miri
//! ```
//!
//! They run as ordinary tests too.

use std::alloc::{GlobalAlloc, Layout};
use std::thread;
use stoolap_jemalloc::Jemalloc;

#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc::new();

/// A freed block is the next one the thread's cache hands out
#[test]
fn thread_cache_round_trip() {
    let layout = Layout::from_size_align(8, 8).unwrap();
    unsafe {
        let p = GLOBAL.alloc(layout);
        p.write(1);
        GLOBAL.dealloc(p, layout);
        let q = GLOBAL.alloc(layout);
        assert_eq!(q, p);
        q.write(2);
        GLOBAL.dealloc(q, layout);
    }
}

/// Threads freeing different size classes into one arena's chunk at once,
/// each holding only its class's lock. The blocks are all made on this
/// thread, so they share its arena whichever arenas the workers get.
#[test]
fn threads_on_different_classes() {
    let classes = [16usize, 48, 200, 3000];
    // More than a cache holds, so the workers flush to the arena
    let blocks: Vec<Vec<usize>> = classes
        .iter()
        .map(|&size| {
            let layout = Layout::from_size_align(size, 8).unwrap();
            (0..300)
                .map(|_| unsafe {
                    let p = GLOBAL.alloc(layout);
                    p.write(size as u8);
                    p as usize
                })
                .collect()
        })
        .collect();
    let handles: Vec<_> = classes
        .into_iter()
        .zip(blocks)
        .map(|(size, ptrs)| {
            thread::spawn(move || {
                let layout = Layout::from_size_align(size, 8).unwrap();
                for p in ptrs {
                    let p = p as *mut u8;
                    unsafe {
                        assert_eq!(*p, size as u8);
                        GLOBAL.dealloc(p, layout);
                    }
                }
            })
        })
        .collect();
    for h in handles {
        h.join().unwrap();
    }
}

/// Large runs and huge mappings from several threads at once
#[test]
fn threads_on_large_and_huge() {
    let handles: Vec<_> = [40_000usize, 300_000, 2 << 20]
        .into_iter()
        .map(|size| {
            thread::spawn(move || {
                let layout = Layout::from_size_align(size, 8).unwrap();
                for _ in 0..4 {
                    unsafe {
                        let p = GLOBAL.alloc(layout);
                        p.write(7);
                        let q = GLOBAL.realloc(p, layout, size * 2);
                        assert_eq!(*q, 7);
                        GLOBAL.dealloc(q, Layout::from_size_align(size * 2, 8).unwrap());
                    }
                }
            })
        })
        .collect();
    for h in handles {
        h.join().unwrap();
    }
}

/// Allocates and frees from its destructor, after the thread's cache is gone
struct LateUser;

impl Drop for LateUser {
    fn drop(&mut self) {
        let small: Vec<Box<u64>> = (0..20).map(Box::new).collect();
        let large = vec![1u8; 40_000];
        assert_eq!(*small[19] + u64::from(large[39_999]), 20);
    }
}

thread_local! {
    static LATE: LateUser = const { LateUser };
}

#[test]
fn frees_while_a_thread_exits() {
    thread::spawn(|| {
        LATE.with(|_| {});
        drop(Box::new(5u32));
    })
    .join()
    .unwrap();
}

/// Blocks freed by another thread than the one that allocated them, while
/// that one keeps allocating: the freeing thread's flushes write free list
/// links into slabs the other refills from, and its frees merge runs that
/// the other's arena split
#[test]
fn frees_on_another_thread() {
    let (tx, rx) = std::sync::mpsc::sync_channel::<(Vec<Box<[u64; 4]>>, Vec<Vec<u8>>)>(2);
    let producer = thread::spawn(move || {
        for round in 0..4u64 {
            // More small blocks than a cache holds, so the consumer flushes
            let small = (0..300).map(|i| Box::new([round, i, 1, 2])).collect();
            let large = (0..2)
                .map(|i| vec![round as u8; 40_000 + 4096 * i])
                .collect();
            tx.send((small, large)).unwrap();
        }
    });
    let consumer = thread::spawn(move || {
        for (small, large) in rx {
            for b in small {
                assert_eq!(b[2] + b[3], 3);
            }
            for v in large {
                assert_eq!(v[0], v[v.len() - 1]);
            }
        }
    });
    producer.join().unwrap();
    consumer.join().unwrap();
    stoolap_jemalloc::purge();
}