mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-06 05:17:42 +00:00
perf(observability): avoid sampler stat allocations (#5708)
* perf(observability): avoid cgroup stat key allocations Replace the memory.stat HashMap parser with a fixed-field parser so the memory observability sampler does not allocate String keys or hash every cgroup field on each interval. Co-Authored-By: heihutu <heihutu@gmail.com> * perf(observability): parse mimalloc stats without copying Parse the mimalloc stats JSON while the mimalloc-owned buffer is still alive, then free it immediately. This avoids allocating an owned String on each allocator memory sample. Co-Authored-By: heihutu <heihutu@gmail.com> --------- Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
@@ -19,7 +19,6 @@ use rustfs_io_metrics::{
|
||||
use serde::Serialize;
|
||||
#[cfg(any(test, not(target_os = "windows")))]
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use std::ffi::CStr;
|
||||
use std::path::Path;
|
||||
@@ -131,6 +130,20 @@ struct CgroupMemorySnapshot {
|
||||
inactive_file_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
struct CgroupMemoryStatFields {
|
||||
anon: Option<u64>,
|
||||
file: Option<u64>,
|
||||
active_file: Option<u64>,
|
||||
inactive_file: Option<u64>,
|
||||
rss: Option<u64>,
|
||||
cache: Option<u64>,
|
||||
total_rss: Option<u64>,
|
||||
total_cache: Option<u64>,
|
||||
total_active_file: Option<u64>,
|
||||
total_inactive_file: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
struct AllocatorMemorySnapshot {
|
||||
backend: &'static str,
|
||||
@@ -156,16 +169,32 @@ fn read_optional_u64(path: &Path) -> Option<u64> {
|
||||
trimmed.parse::<u64>().ok()
|
||||
}
|
||||
|
||||
fn parse_kv_stats(content: &str) -> HashMap<String, u64> {
|
||||
content
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let mut parts = line.split_whitespace();
|
||||
let key = parts.next()?;
|
||||
let value = parts.next()?.parse::<u64>().ok()?;
|
||||
Some((key.to_string(), value))
|
||||
})
|
||||
.collect()
|
||||
fn parse_cgroup_memory_stat(content: &str) -> CgroupMemoryStatFields {
|
||||
let mut fields = CgroupMemoryStatFields::default();
|
||||
for line in content.lines() {
|
||||
let mut parts = line.split_whitespace();
|
||||
let Some(key) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
let Some(value) = parts.next().and_then(|value| value.parse::<u64>().ok()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match key {
|
||||
"anon" => fields.anon = Some(value),
|
||||
"file" => fields.file = Some(value),
|
||||
"active_file" => fields.active_file = Some(value),
|
||||
"inactive_file" => fields.inactive_file = Some(value),
|
||||
"rss" => fields.rss = Some(value),
|
||||
"cache" => fields.cache = Some(value),
|
||||
"total_rss" => fields.total_rss = Some(value),
|
||||
"total_cache" => fields.total_cache = Some(value),
|
||||
"total_active_file" => fields.total_active_file = Some(value),
|
||||
"total_inactive_file" => fields.total_inactive_file = Some(value),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
fields
|
||||
}
|
||||
|
||||
fn read_cgroup_v2() -> Option<CgroupMemorySnapshot> {
|
||||
@@ -175,14 +204,14 @@ fn read_cgroup_v2() -> Option<CgroupMemorySnapshot> {
|
||||
return None;
|
||||
}
|
||||
|
||||
let stats = parse_kv_stats(&std::fs::read_to_string(&stat_path).ok()?);
|
||||
let stats = parse_cgroup_memory_stat(&std::fs::read_to_string(&stat_path).ok()?);
|
||||
Some(CgroupMemorySnapshot {
|
||||
current_bytes: read_optional_u64(&root.join("memory.current")),
|
||||
limit_bytes: read_optional_u64(&root.join("memory.max")),
|
||||
anon_bytes: stats.get("anon").copied(),
|
||||
file_bytes: stats.get("file").copied(),
|
||||
active_file_bytes: stats.get("active_file").copied(),
|
||||
inactive_file_bytes: stats.get("inactive_file").copied(),
|
||||
anon_bytes: stats.anon,
|
||||
file_bytes: stats.file,
|
||||
active_file_bytes: stats.active_file,
|
||||
inactive_file_bytes: stats.inactive_file,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -193,20 +222,14 @@ fn read_cgroup_v1() -> Option<CgroupMemorySnapshot> {
|
||||
return None;
|
||||
}
|
||||
|
||||
let stats = parse_kv_stats(&std::fs::read_to_string(&stat_path).ok()?);
|
||||
let stats = parse_cgroup_memory_stat(&std::fs::read_to_string(&stat_path).ok()?);
|
||||
Some(CgroupMemorySnapshot {
|
||||
current_bytes: read_optional_u64(&root.join("memory.usage_in_bytes")),
|
||||
limit_bytes: read_optional_u64(&root.join("memory.limit_in_bytes")),
|
||||
anon_bytes: stats.get("total_rss").copied().or_else(|| stats.get("rss").copied()),
|
||||
file_bytes: stats.get("total_cache").copied().or_else(|| stats.get("cache").copied()),
|
||||
active_file_bytes: stats
|
||||
.get("total_active_file")
|
||||
.copied()
|
||||
.or_else(|| stats.get("active_file").copied()),
|
||||
inactive_file_bytes: stats
|
||||
.get("total_inactive_file")
|
||||
.copied()
|
||||
.or_else(|| stats.get("inactive_file").copied()),
|
||||
anon_bytes: stats.total_rss.or(stats.rss),
|
||||
file_bytes: stats.total_cache.or(stats.cache),
|
||||
active_file_bytes: stats.total_active_file.or(stats.active_file),
|
||||
inactive_file_bytes: stats.total_inactive_file.or(stats.inactive_file),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -300,18 +323,17 @@ fn parse_mimalloc_stats_json(stats_json: &str) -> Option<AllocatorMemoryObservat
|
||||
fn read_allocator_memory_snapshot() -> Option<AllocatorMemorySnapshot> {
|
||||
// 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`, which is done before returning.
|
||||
let stats = unsafe {
|
||||
// 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 stats = CStr::from_ptr(stats_ptr).to_str().ok().map(str::to_owned);
|
||||
let observation = CStr::from_ptr(stats_ptr).to_str().ok().and_then(parse_mimalloc_stats_json);
|
||||
libmimalloc_sys::mi_free(stats_ptr.cast());
|
||||
stats?
|
||||
observation?
|
||||
};
|
||||
let observation = parse_mimalloc_stats_json(&stats)?;
|
||||
Some(AllocatorMemorySnapshot {
|
||||
backend: crate::allocator_reclaim::allocator_backend(),
|
||||
observation,
|
||||
@@ -456,17 +478,18 @@ mod tests {
|
||||
CgroupMemorySnapshot, MEMORY_OBSERVABILITY_SERVICE_NAME, MemoryObservabilityCancellationSource,
|
||||
MemoryObservabilityController, MemoryObservabilityDesiredState, MemoryObservabilityServiceState,
|
||||
MemoryObservabilityShutdownHandle, MemoryObservabilityWorkerMutation, build_memory_observability_controller_snapshot,
|
||||
build_memory_observability_status_snapshot, parse_kv_stats, parse_mimalloc_stats_json, read_optional_u64,
|
||||
build_memory_observability_status_snapshot, parse_cgroup_memory_stat, parse_mimalloc_stats_json, read_optional_u64,
|
||||
};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn parse_kv_stats_extracts_numeric_pairs() {
|
||||
let parsed = parse_kv_stats("anon 12\nfile 34\nactive_file 56\n");
|
||||
assert_eq!(parsed.get("anon").copied(), Some(12));
|
||||
assert_eq!(parsed.get("file").copied(), Some(34));
|
||||
assert_eq!(parsed.get("active_file").copied(), Some(56));
|
||||
fn parse_cgroup_memory_stat_extracts_tracked_numeric_fields() {
|
||||
let parsed = parse_cgroup_memory_stat("anon 12\nfile 34\nactive_file 56\nignored 78\nmalformed nope\n");
|
||||
assert_eq!(parsed.anon, Some(12));
|
||||
assert_eq!(parsed.file, Some(34));
|
||||
assert_eq!(parsed.active_file, Some(56));
|
||||
assert_eq!(parsed.inactive_file, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user