diff --git a/Cargo.lock b/Cargo.lock index b7b1ec220..517983884 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1858,9 +1858,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.3" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", "jobserver", @@ -2522,12 +2522,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "cty" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" - [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -5988,15 +5982,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" -[[package]] -name = "libmimalloc-sys" -version = "0.1.49" -source = "git+https://github.com/xonatius/mimalloc_rust.git?rev=6d4c41bb10c6d9da1d1b6f07b38c4cc051667f11#6d4c41bb10c6d9da1d1b6f07b38c4cc051667f11" -dependencies = [ - "cc", - "cty", -] - [[package]] name = "libredox" version = "0.1.20" @@ -6397,14 +6382,6 @@ dependencies = [ "synstructure 0.13.2", ] -[[package]] -name = "mimalloc" -version = "0.1.52" -source = "git+https://github.com/xonatius/mimalloc_rust.git?rev=6d4c41bb10c6d9da1d1b6f07b38c4cc051667f11#6d4c41bb10c6d9da1d1b6f07b38c4cc051667f11" -dependencies = [ - "libmimalloc-sys", -] - [[package]] name = "mime" version = "0.3.17" @@ -9162,13 +9139,11 @@ dependencies = [ "insta", "jiff", "libc", - "libmimalloc-sys", "libsystemd", "matchit 0.9.2", "md-5 0.11.0", "metrics", "metrics-util", - "mimalloc", "mime_guess", "opentelemetry", "opentelemetry_sdk", @@ -9204,6 +9179,8 @@ dependencies = [ "rustfs-lock", "rustfs-log-analyzer", "rustfs-madmin", + "rustfs-mimalloc", + "rustfs-mimalloc-sys", "rustfs-notify", "rustfs-object-capacity", "rustfs-object-data-cache", @@ -9875,6 +9852,24 @@ dependencies = [ "tokio", ] +[[package]] +name = "rustfs-mimalloc" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a406f4aa07084301d485beec873af6dccc8e3f8762da244743df92038b1db1a6" +dependencies = [ + "rustfs-mimalloc-sys", +] + +[[package]] +name = "rustfs-mimalloc-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3051b819175f58445d4c369a72f0ab88149f3885ba8bea2aff3be01f53fe7cd" +dependencies = [ + "cc", +] + [[package]] name = "rustfs-notify" version = "1.0.0-rc.3" diff --git a/Cargo.toml b/Cargo.toml index 918af3cce..717a0c6e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -350,8 +350,8 @@ russh-sftp = "2.4.0" dav-server = "0.11.0" # Performance Analysis and Memory Profiling -mimalloc = { version = "0.1.52", git = "https://github.com/xonatius/mimalloc_rust.git", rev = "6d4c41bb10c6d9da1d1b6f07b38c4cc051667f11" } -libmimalloc-sys = { version = "0.1.49", git = "https://github.com/xonatius/mimalloc_rust.git", rev = "6d4c41bb10c6d9da1d1b6f07b38c4cc051667f11", features = ["extended"] } +rustfs-mimalloc = { version = "0.5.0" } +rustfs-mimalloc-sys = { version = "0.5.0" } hotpath = { version = "0.23.3", default-features = false } # Snapshot testing for output format regression detection insta = { version = "1.48" } diff --git a/deny.toml b/deny.toml index 296b229e4..c6fb8facc 100644 --- a/deny.toml +++ b/deny.toml @@ -43,9 +43,6 @@ allow-git = [ # RustFS fork carrying presigned expiry and constant-time authentication fixes. # owner: rustfs-maintainers review: 2026-10 "https://github.com/rustfs/s3s.git", - # MiMalloc fork pinned for hotpath allocation counting support. - # owner: houseme review: 2026-10 - "https://github.com/xonatius/mimalloc_rust.git", ] [bans] diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index af5514870..03afb5628 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -336,13 +336,13 @@ opentelemetry = { workspace = true } tracing-opentelemetry = { workspace = true } # Data structures hashbrown = { workspace = true, features = ["serde", "rayon"] } -mimalloc = { workspace = true } +rustfs-mimalloc = { workspace = true } [target.'cfg(target_os = "linux")'.dependencies] libsystemd.workspace = true [target.'cfg(not(target_os = "windows"))'.dependencies] -libmimalloc-sys.workspace = true +rustfs-mimalloc-sys.workspace = true [dev-dependencies] uuid = { workspace = true, features = ["v4", "v5", "fast-rng", "macro-diagnostics"] } diff --git a/rustfs/src/allocator_reclaim.rs b/rustfs/src/allocator_reclaim.rs index 0c31390c3..eba1a8948 100644 --- a/rustfs/src/allocator_reclaim.rs +++ b/rustfs/src/allocator_reclaim.rs @@ -369,14 +369,8 @@ pub fn allocator_reclaim_controller_snapshot(ctx: &CancellationToken) -> Allocat } #[cfg(not(target_os = "windows"))] -#[allow(unsafe_code)] fn collect_allocator_memory(force: bool) -> Result<(), String> { - // SAFETY: `mi_collect` is provided by the active global allocator backend - // on this target family. It is explicitly intended to reclaim retained - // pages/segments and does not require additional invariants from the caller. - unsafe { - libmimalloc_sys::mi_collect(force); - } + rustfs_mimalloc::MiMalloc::collect(force); Ok(()) } diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index 5e9d5075b..7f4bea1d2 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -26,22 +26,22 @@ struct MiMallocAllocator; unsafe impl GlobalAlloc for MiMallocAllocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { // SAFETY: the caller upholds GlobalAlloc's contract for layout. - unsafe { mimalloc::MiMalloc.alloc(layout) } + unsafe { rustfs_mimalloc::MiMalloc.alloc(layout) } } unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { // SAFETY: the caller upholds GlobalAlloc's contract for layout. - unsafe { mimalloc::MiMalloc.alloc_zeroed(layout) } + unsafe { rustfs_mimalloc::MiMalloc.alloc_zeroed(layout) } } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { // SAFETY: ptr and layout came from this allocator and are forwarded unchanged. - unsafe { mimalloc::MiMalloc.dealloc(ptr, layout) } + unsafe { rustfs_mimalloc::MiMalloc.dealloc(ptr, layout) } } unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { // SAFETY: ptr and layout came from this allocator and are forwarded unchanged. - unsafe { mimalloc::MiMalloc.realloc(ptr, layout, new_size) } + unsafe { rustfs_mimalloc::MiMalloc.realloc(ptr, layout, new_size) } } } @@ -51,7 +51,7 @@ static GLOBAL: hotpath::CountingAllocator = hotpath::Counting #[cfg(not(all(feature = "hotpath", feature = "hotpath-alloc")))] #[global_allocator] -static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; +static GLOBAL: rustfs_mimalloc::MiMalloc = rustfs_mimalloc::MiMalloc; fn main() { let _hotpath_guard = hotpath::HotpathGuardBuilder::new("main").build(); @@ -71,8 +71,9 @@ mod tests { allocation.extend_from_slice(&[7_u8; 64]); assert_eq!(allocation.len(), 64); + let heap = rustfs_mimalloc::heap::Heap::main(); // SAFETY: the live Vec pointer is valid to inspect for heap ownership. - assert!(unsafe { libmimalloc_sys::mi_is_in_heap_region(allocation.as_ptr().cast()) }); + assert!(unsafe { heap.contains(allocation.as_ptr()) }); } #[test] @@ -85,12 +86,13 @@ mod tests { let layout = Layout::from_size_align(32, 8).expect("valid test allocation layout"); let grown_layout = Layout::from_size_align(64, 8).expect("valid grown test allocation layout"); let allocator = super::MiMallocAllocator; + let heap = rustfs_mimalloc::heap::Heap::main(); // SAFETY: The pointer is checked for null before use and later released // through the same allocator with the corresponding layout. let ptr = unsafe { allocator.alloc_zeroed(layout) }; assert!(!ptr.is_null()); - assert!(unsafe { libmimalloc_sys::mi_is_in_heap_region(ptr.cast()) }); + assert!(unsafe { heap.contains(ptr) }); assert!(unsafe { std::slice::from_raw_parts(ptr, 32).iter().all(|byte| *byte == 0) }); // SAFETY: `ptr` was allocated by `allocator` with `layout`; on failure @@ -102,7 +104,7 @@ mod tests { panic!("mimalloc realloc failed in allocator smoke test"); } - assert!(unsafe { libmimalloc_sys::mi_is_in_heap_region(grown_ptr.cast()) }); + assert!(unsafe { heap.contains(grown_ptr) }); // SAFETY: `grown_ptr` was reallocated by `allocator` and is released // with the matching grown layout. unsafe { allocator.dealloc(grown_ptr, grown_layout) }; diff --git a/rustfs/src/memory_observability.rs b/rustfs/src/memory_observability.rs index 3e29d24a0..12bd4b3c5 100644 --- a/rustfs/src/memory_observability.rs +++ b/rustfs/src/memory_observability.rs @@ -17,10 +17,7 @@ use rustfs_io_metrics::{ record_cpu_usage, record_memory_usage, record_process_memory_split, }; use serde::Serialize; -#[cfg(any(test, not(target_os = "windows")))] use serde_json::Value; -#[cfg(not(target_os = "windows"))] -use std::ffi::CStr; use std::path::Path; use std::sync::{Arc, Mutex, OnceLock}; use std::time::Duration; @@ -231,7 +228,18 @@ fn read_cgroup_memory_snapshot() -> Option { read_cgroup_v2().or_else(read_cgroup_v1) } -#[cfg(any(test, not(target_os = "windows")))] +fn read_allocator_memory_snapshot() -> Option { + let json = rustfs_mimalloc::MiMalloc::stats_json(); + if json.is_empty() { + return None; + } + let observation = parse_mimalloc_stats_json(&json)?; + Some(AllocatorMemorySnapshot { + backend: crate::allocator_reclaim::allocator_backend(), + observation, + }) +} + fn numeric_json_value(value: &Value) -> Option { match value { Value::Number(number) => number @@ -242,7 +250,6 @@ fn numeric_json_value(value: &Value) -> Option { } } -#[cfg(any(test, not(target_os = "windows")))] fn numeric_json_field(value: &Value, field: &str) -> Option { match value { Value::Object(fields) => fields @@ -254,7 +261,6 @@ fn numeric_json_field(value: &Value, field: &str) -> Option { } } -#[cfg(any(test, not(target_os = "windows")))] fn mimalloc_stat_field(value: &Value, metric: &str, field: &str) -> Option { match value { Value::Object(fields) => { @@ -271,12 +277,10 @@ fn mimalloc_stat_field(value: &Value, metric: &str, field: &str) -> Option } } -#[cfg(any(test, not(target_os = "windows")))] fn mimalloc_stat_current(value: &Value, metric: &str) -> Option { mimalloc_stat_field(value, metric, "current") } -#[cfg(any(test, not(target_os = "windows")))] fn mimalloc_stat_sum(value: &Value, metrics: &[&str], field: &str) -> Option { metrics .iter() @@ -285,7 +289,6 @@ fn mimalloc_stat_sum(value: &Value, metrics: &[&str], field: &str) -> Option 0) } -#[cfg(any(test, not(target_os = "windows")))] fn parse_mimalloc_stats_json(stats_json: &str) -> Option { let value = serde_json::from_str::(stats_json).ok()?; let malloc_metrics = ["malloc_normal", "malloc_huge"]; @@ -312,33 +315,6 @@ fn parse_mimalloc_stats_json(stats_json: &str) -> Option Option { - // SAFETY: `mi_stats_get_json` returns a null-terminated JSON buffer owned by - // mimalloc when called with a null input buffer. The mimalloc API requires - // freeing that buffer with `mi_free`; parsing finishes before the buffer is freed. - let observation = unsafe { - let stats_ptr = libmimalloc_sys::mi_stats_get_json(0, std::ptr::null_mut()); - if stats_ptr.is_null() { - return None; - } - - let observation = CStr::from_ptr(stats_ptr).to_str().ok().and_then(parse_mimalloc_stats_json); - libmimalloc_sys::mi_free(stats_ptr.cast()); - observation? - }; - Some(AllocatorMemorySnapshot { - backend: crate::allocator_reclaim::allocator_backend(), - observation, - }) -} - -#[cfg(target_os = "windows")] -fn read_allocator_memory_snapshot() -> Option { - None -} - fn configured_memory_observability_interval_secs() -> u64 { rustfs_utils::get_env_u64(ENV_MEMORY_OBSERVABILITY_INTERVAL_SECS, DEFAULT_MEMORY_OBSERVABILITY_INTERVAL_SECS).max(1) } @@ -566,6 +542,13 @@ mod tests { assert_eq!(parse_mimalloc_stats_json(r#"{ "allocator": "unknown" }"#), None); } + #[test] + fn read_allocator_memory_snapshot_uses_mimalloc_stats_json() { + let snapshot = super::read_allocator_memory_snapshot(); + #[cfg(not(target_os = "windows"))] + assert!(snapshot.is_some(), "allocator snapshot should be available on non-Windows"); + } + #[test] fn memory_observability_snapshot_reports_disabled_when_metrics_are_disabled() { let snapshot = build_memory_observability_status_snapshot(false, 15, false);