#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct HostHeapState {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_bytes: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allocated_bytes: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resident_bytes: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mapped_bytes: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub narenas: Option<usize>,
}
impl HostHeapState {
pub fn test_fixture() -> HostHeapState {
HostHeapState {
active_bytes: Some(1 << 20),
allocated_bytes: Some(512 * 1024),
resident_bytes: Some(2 << 20),
mapped_bytes: Some(4 << 20),
narenas: Some(64),
}
}
pub fn format_human(&self) -> String {
use std::fmt::Write;
let HostHeapState {
active_bytes,
allocated_bytes,
resident_bytes,
mapped_bytes,
narenas,
} = self;
fn row<T: std::fmt::Display>(out: &mut String, key: &str, value: Option<&T>) {
match value {
Some(v) => {
let _ = writeln!(out, "{key}: {v}");
}
None => {
let _ = writeln!(out, "{key}: (unknown)");
}
}
}
let mut out = String::new();
row(&mut out, "allocated_bytes", allocated_bytes.as_ref());
row(&mut out, "active_bytes", active_bytes.as_ref());
row(&mut out, "resident_bytes", resident_bytes.as_ref());
row(&mut out, "mapped_bytes", mapped_bytes.as_ref());
row(&mut out, "narenas", narenas.as_ref());
out
}
pub fn diff(&self, other: &HostHeapState) -> String {
use std::fmt::Write;
let HostHeapState {
active_bytes: a_active,
allocated_bytes: a_allocated,
resident_bytes: a_resident,
mapped_bytes: a_mapped,
narenas: a_narenas,
} = self;
let HostHeapState {
active_bytes: b_active,
allocated_bytes: b_allocated,
resident_bytes: b_resident,
mapped_bytes: b_mapped,
narenas: b_narenas,
} = other;
let mut out = String::new();
fn row_opt<T: std::fmt::Display + PartialEq>(
out: &mut String,
key: &str,
a: Option<&T>,
b: Option<&T>,
) {
if a == b {
return;
}
let render = |v: Option<&T>| match v {
Some(x) => format!("{x}"),
None => "(unknown)".to_string(),
};
let _ = writeln!(out, "{key}: {} → {}", render(a), render(b));
}
row_opt(
&mut out,
"allocated_bytes",
a_allocated.as_ref(),
b_allocated.as_ref(),
);
row_opt(
&mut out,
"active_bytes",
a_active.as_ref(),
b_active.as_ref(),
);
row_opt(
&mut out,
"resident_bytes",
a_resident.as_ref(),
b_resident.as_ref(),
);
row_opt(
&mut out,
"mapped_bytes",
a_mapped.as_ref(),
b_mapped.as_ref(),
);
row_opt(&mut out, "narenas", a_narenas.as_ref(), b_narenas.as_ref());
out
}
}
pub fn collect() -> HostHeapState {
if tikv_jemalloc_ctl::epoch::advance().is_err() {
return HostHeapState::default();
}
let active_bytes = tikv_jemalloc_ctl::stats::active::read()
.ok()
.map(|v| v as u64);
let allocated_bytes = tikv_jemalloc_ctl::stats::allocated::read()
.ok()
.map(|v| v as u64);
let resident_bytes = tikv_jemalloc_ctl::stats::resident::read()
.ok()
.map(|v| v as u64);
let mapped_bytes = tikv_jemalloc_ctl::stats::mapped::read()
.ok()
.map(|v| v as u64);
let narenas = tikv_jemalloc_ctl::arenas::narenas::read()
.ok()
.map(|v| v as usize);
HostHeapState {
active_bytes,
allocated_bytes,
resident_bytes,
mapped_bytes,
narenas,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_lands_all_none() {
let h = HostHeapState::default();
assert!(h.active_bytes.is_none());
assert!(h.allocated_bytes.is_none());
assert!(h.resident_bytes.is_none());
assert!(h.mapped_bytes.is_none());
assert!(h.narenas.is_none());
}
#[test]
fn test_fixture_populates_every_field() {
let h = HostHeapState::test_fixture();
assert!(h.active_bytes.is_some());
assert!(h.allocated_bytes.is_some());
assert!(h.resident_bytes.is_some());
assert!(h.mapped_bytes.is_some());
assert!(h.narenas.is_some());
}
#[test]
fn format_human_lists_every_field() {
let out = HostHeapState::test_fixture().format_human();
assert!(out.contains("allocated_bytes:"));
assert!(out.contains("active_bytes:"));
assert!(out.contains("resident_bytes:"));
assert!(out.contains("mapped_bytes:"));
assert!(out.contains("narenas:"));
assert!(out.ends_with('\n'));
}
#[test]
fn format_human_field_order_is_stable() {
let out = HostHeapState::default().format_human();
let labels: Vec<&str> = out
.lines()
.filter_map(|l| l.split(':').next())
.filter(|s| !s.starts_with(' '))
.collect();
assert_eq!(
labels,
vec![
"allocated_bytes",
"active_bytes",
"resident_bytes",
"mapped_bytes",
"narenas",
],
"format_human field order drifted — if intentional, update \
the expected vector and verify the HostContext heap_state \
sub-block still reads in the expected top-to-bottom order",
);
}
#[test]
fn format_human_renders_none_as_unknown() {
let out = HostHeapState::default().format_human();
for line in out.lines() {
assert!(
line.ends_with(": (unknown)"),
"expected unknown, got {line:?}"
);
}
}
#[test]
fn diff_is_empty_on_equal_snapshots() {
let a = HostHeapState::test_fixture();
let b = HostHeapState::test_fixture();
assert_eq!(a.diff(&b), "");
}
#[test]
fn diff_reports_only_changed_fields() {
let a = HostHeapState::test_fixture();
let mut b = a.clone();
b.allocated_bytes = Some(9 * 1024 * 1024);
let d = a.diff(&b);
assert!(d.contains("allocated_bytes:"));
assert!(!d.contains("active_bytes:"));
assert!(!d.contains("resident_bytes:"));
assert!(!d.contains("mapped_bytes:"));
assert!(!d.contains("narenas:"));
assert!(d.contains("→"));
}
#[test]
fn diff_renders_none_transitions() {
let a = HostHeapState::default();
let b = HostHeapState::test_fixture();
let d = a.diff(&b);
assert!(d.contains("allocated_bytes: (unknown) →"));
assert!(d.contains("narenas: (unknown) →"));
}
#[test]
fn diff_renders_some_to_none_transitions() {
let a = HostHeapState::test_fixture();
let b = HostHeapState::default();
let d = a.diff(&b);
assert!(
d.contains("allocated_bytes:") && d.contains("→ (unknown)"),
"expected allocated_bytes → (unknown), got:\n{d}",
);
assert!(d.contains("active_bytes:"));
assert!(d.contains("resident_bytes:"));
assert!(d.contains("mapped_bytes:"));
assert!(d.contains("narenas:"));
}
#[test]
fn serde_round_trip_preserves_fields() {
let h = HostHeapState::test_fixture();
let s = serde_json::to_string(&h).expect("serialize");
let back: HostHeapState = serde_json::from_str(&s).expect("deserialize");
assert_eq!(back, h);
}
#[test]
fn serde_skips_none_fields() {
let h = HostHeapState::default();
let s = serde_json::to_string(&h).expect("serialize");
assert_eq!(s, "{}");
}
#[test]
fn serde_accepts_missing_fields_via_defaults() {
let back: HostHeapState = serde_json::from_str("{}").expect("deserialize");
assert_eq!(back, HostHeapState::default());
}
#[test]
fn collect_returns_populated_snapshot_under_jemallocator() {
let h = collect();
assert!(
h.narenas.is_some(),
"narenas must populate on a libjemalloc build"
);
assert!(
h.narenas.unwrap() > 0,
"narenas must be > 0; jemalloc computes 4*ncpus at init",
);
assert!(h.allocated_bytes.is_some());
assert!(h.active_bytes.is_some());
assert!(h.resident_bytes.is_some());
assert!(h.mapped_bytes.is_some());
}
}