Files
wehub-resource-sync a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:34 +08:00

170 lines
6.4 KiB
Rust

//! Memory-bound probe for the Layout-tier cache (see
//! `mermaid_cache_render.rs`: `LayoutCache`, `LAYOUT_CACHE_MAX`,
//! `approx_layout_bytes`, `layout_cache_usage`).
//!
//! Renders 50 distinct diagrams through the sized-render path and checks that
//! the layout tier stays bounded: entries <= LAYOUT_CACHE_MAX (32), approx
//! resident bytes <= ~2.5 MB (the documented worst-case estimate), LRU
//! eviction actually kicks in (50 misses but only 32 resident), and process
//! RSS growth over the run is sane (< 50 MB; note the raw RSS delta also
//! includes PNG-tier metadata, rasterizer/fontdb allocations, and allocator
//! slack, so the cache's own approx_bytes is reported separately to keep the
//! cache-tier contribution attributable).
//!
//! `#[ignore]`-d: run explicitly with
//! `cargo test -p jcode-tui-mermaid --test layout_cache_memory_probe -- --ignored --nocapture`
#![cfg(feature = "renderer")]
/// VmRSS from /proc/self/status, in bytes (Linux only).
fn vm_rss_bytes() -> Option<u64> {
let status = std::fs::read_to_string("/proc/self/status").ok()?;
for line in status.lines() {
if let Some(rest) = line.strip_prefix("VmRSS:") {
let kb = rest
.trim()
.trim_end_matches("kB")
.trim()
.parse::<u64>()
.ok()?;
return Some(kb * 1024);
}
}
None
}
/// A distinct small flowchart per index: node count varies (3..=10 chain
/// links) and every label embeds the index plus a per-run nonce so source
/// hashes differ across probe iterations *and* across repeated runs (a stale
/// PNG disk cache from a previous run cannot satisfy the render, but even if
/// the PNG tier hit, the layout tier misses because the hash is new).
fn probe_diagram(idx: usize, nonce: u128) -> String {
let links = 3 + (idx % 8);
let mut out = String::from("flowchart TD\n");
for step in 0..links {
out.push_str(&format!(
" P{idx}S{step}N{nonce}[probe {idx} step {step}] --> P{idx}S{s}N{nonce}[probe {idx} step {s}]\n",
s = step + 1
));
}
out
}
#[test]
#[ignore = "memory probe: run explicitly with --ignored --nocapture"]
fn layout_cache_stays_bounded_under_50_distinct_renders() {
const RENDERS: usize = 50;
/// Documented worst case: 32 entries x ~75 KB (100-node cap) ~= 2.4 MB.
const APPROX_BYTES_CAP: u64 = 2_500_000;
/// Generous cap for whole-process growth over 50 small renders. The raw
/// delta includes non-cache allocations (rasterizer, fontdb, PNG-tier).
const RSS_DELTA_CAP: u64 = 50 * 1024 * 1024;
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
jcode_tui_mermaid::reset_debug_stats();
let stats_before = jcode_tui_mermaid::debug_stats();
let limit = stats_before.layout_cache_limit;
assert_eq!(limit, 32, "probe assumes LAYOUT_CACHE_MAX == 32");
assert!(
RENDERS > limit,
"probe must overflow the cache to exercise LRU eviction"
);
let rss_before = vm_rss_bytes().expect("VmRSS readable on Linux");
let mut render_errors = 0usize;
for idx in 0..RENDERS {
let content = probe_diagram(idx, nonce);
match jcode_tui_mermaid::render_mermaid_untracked(&content, Some(90)) {
jcode_tui_mermaid::RenderResult::Image { .. } => {}
jcode_tui_mermaid::RenderResult::Error(error) => {
render_errors += 1;
eprintln!("render {idx} failed: {error}");
}
}
}
assert_eq!(render_errors, 0, "all probe renders must succeed");
let rss_after = vm_rss_bytes().expect("VmRSS readable on Linux");
let stats = jcode_tui_mermaid::debug_stats();
let memory = jcode_tui_mermaid::debug_memory_profile();
let rss_delta = rss_after.saturating_sub(rss_before);
eprintln!("--- layout cache memory probe ---");
eprintln!("renders: {RENDERS} distinct diagrams");
eprintln!(
"layout_cache entries: {} / {} (limit)",
stats.layout_cache_entries, stats.layout_cache_limit
);
eprintln!(
"layout_cache approx_bytes:{} ({:.1} KB)",
stats.layout_cache_approx_bytes,
stats.layout_cache_approx_bytes as f64 / 1024.0
);
eprintln!(
"layout hits/misses: {} / {}",
stats.layout_cache_hits, stats.layout_cache_misses
);
eprintln!(
"RSS before/after/delta: {:.1} MB / {:.1} MB / {:.1} MB \
(delta includes PNG-tier + rasterizer allocations, not just the layout cache)",
rss_before as f64 / 1048576.0,
rss_after as f64 / 1048576.0,
rss_delta as f64 / 1048576.0
);
// (a) Entry count bounded, consistently reported by stats and memory profile.
assert!(
stats.layout_cache_entries <= limit,
"entries {} exceed LAYOUT_CACHE_MAX {limit}",
stats.layout_cache_entries
);
assert_eq!(
memory.layout_cache_entries, stats.layout_cache_entries,
"MermaidMemoryProfile and MermaidDebugStats disagree on entry count"
);
assert_eq!(memory.layout_cache_limit, limit);
// (b) Approximate resident bytes bounded by the documented worst case.
assert!(
stats.layout_cache_approx_bytes <= APPROX_BYTES_CAP,
"layout cache approx bytes {} exceed the ~2.5MB worst-case bound",
stats.layout_cache_approx_bytes
);
assert_eq!(
memory.layout_cache_approx_bytes,
stats.layout_cache_approx_bytes
);
assert!(
stats.layout_cache_approx_bytes > 0,
"cache should hold real layouts after 50 renders"
);
// LRU eviction actually kicked in: every distinct source is a layout-tier
// miss (this integration-test binary runs the probe alone, so the global
// counters are not shared with other tests), and only `limit` entries
// stay resident.
assert_eq!(
stats.layout_cache_misses, RENDERS as u64,
"each distinct diagram must miss the layout tier once"
);
assert_eq!(
stats.layout_cache_hits, 0,
"distinct sources must not hit the layout tier"
);
assert_eq!(
stats.layout_cache_entries, limit,
"LRU eviction must cap residency at LAYOUT_CACHE_MAX"
);
// (c) Whole-process growth stays sane.
assert!(
rss_delta < RSS_DELTA_CAP,
"RSS delta {rss_delta} bytes exceeds {RSS_DELTA_CAP} bytes"
);
}