try
Some checks failed
CI / Check (push) Failing after 3m22s
Build and publish Docker image / build-and-push (push) Successful in 7m25s

This commit is contained in:
Andras Schmelczer 2026-06-04 22:34:26 +01:00
parent 843d14b7ba
commit c938b71904
13 changed files with 698 additions and 109 deletions

1
server-rs/Cargo.lock generated
View file

@ -3901,6 +3901,7 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.11.0",
"tikv-jemalloc-sys",
"tikv-jemallocator",
"tokio",
"tower",

View file

@ -41,6 +41,10 @@ sentry = { version = "0.46.0", default-features = false, features = ["backtrace"
# steady-state RSS). Decay is configured via `malloc_conf` in main.rs.
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemallocator = { version = "0.6", features = ["unprefixed_malloc_on_supported_platforms"] }
# Direct mallctl access so we can force-purge dirty pages back to the OS after the
# data load (jemalloc's idle decay/background_thread doesn't reliably return the
# ~30GB of load-time transient buffers without subsequent allocation activity).
tikv-jemalloc-sys = "0.6"
[lints.clippy]
min_ident_chars = "warn"

View file

@ -136,9 +136,74 @@ fn resident_memory_kib() -> Option<u64> {
})
}
/// Force jemalloc to return all dirty/muzzy pages to the OS immediately.
///
/// jemalloc keeps freed pages "dirty" for fast reuse and only hands them back to
/// the OS via decay. Under the bursty allocate/free of request handling (and the
/// huge startup-load transients) that decay lags badly, so RSS balloons far above
/// the live working set. `arena.4096.purge` (4096 == `MALLCTL_ARENAS_ALL`) purges
/// every arena synchronously, dropping RSS back down.
#[cfg(target_os = "linux")]
fn jemalloc_purge() {
const PURGE: &[u8] = b"arena.4096.purge\0";
// Safety: a write-only mallctl with no input/output buffers; the name is a
// valid NUL-terminated jemalloc control string.
unsafe {
tikv_jemalloc_sys::mallctl(
PURGE.as_ptr().cast(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
0,
);
}
}
#[cfg(not(target_os = "linux"))]
fn jemalloc_purge() {}
/// Enable jemalloc's background purge thread at runtime. Setting it via
/// `background_thread:true` in `malloc_conf` is unreliable (it can be silently
/// dropped depending on init ordering), so we also flip it explicitly here.
#[cfg(target_os = "linux")]
fn enable_jemalloc_background_thread() {
let enabled: bool = true;
// Safety: write-only mallctl of a bool to a valid control name.
unsafe {
tikv_jemalloc_sys::mallctl(
b"background_thread\0".as_ptr().cast(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::addr_of!(enabled) as *mut core::ffi::c_void,
std::mem::size_of::<bool>(),
);
}
}
#[cfg(not(target_os = "linux"))]
fn enable_jemalloc_background_thread() {}
/// Periodically purge jemalloc arenas so request-handling transients are returned
/// to the OS instead of accumulating as dirty pages. A plain OS thread (not a
/// tokio task) keeps the madvise sweep off the async runtime.
#[cfg(target_os = "linux")]
fn spawn_jemalloc_purger() {
std::thread::Builder::new()
.name("jemalloc-purge".to_string())
.spawn(|| loop {
std::thread::sleep(Duration::from_secs(10));
jemalloc_purge();
})
.ok();
}
#[cfg(not(target_os = "linux"))]
fn spawn_jemalloc_purger() {}
#[cfg(target_os = "linux")]
fn trim_allocator(label: &'static str) {
let before = resident_memory_kib();
jemalloc_purge();
let trimmed = unsafe { libc::malloc_trim(0) };
let after = resident_memory_kib();
if let (Some(before), Some(after)) = (before, after) {
@ -401,6 +466,12 @@ async fn main() -> anyhow::Result<()> {
)
.init();
// Keep jemalloc from hoarding freed memory: run its background purge thread
// and a periodic explicit purge so load-time and request-time transients are
// returned to the OS instead of inflating RSS.
enable_jemalloc_background_thread();
spawn_jemalloc_purger();
// Initialize Prometheus metrics
let metrics_handle = metrics::init_metrics();
info!("Prometheus metrics initialized");
@ -1016,17 +1087,13 @@ async fn main() -> anyhow::Result<()> {
.layer(sentry::integrations::tower::SentryHttpLayer::new()),
);
// Lock all current and future memory pages to prevent swapping
unsafe {
if libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) != 0 {
let err = std::io::Error::last_os_error();
tracing::warn!(
"mlockall failed (need CAP_IPC_LOCK or sufficient RLIMIT_MEMLOCK): {err}"
);
} else {
info!("All memory pages locked (mlockall)");
}
}
// NOTE: we deliberately do NOT mlockall() here. Locking MCL_CURRENT|MCL_FUTURE
// pinned the allocator's entire mapped heap — including jemalloc's freed/dirty
// pages — resident and non-reclaimable, inflating RSS from the ~10GB working
// set to ~40GB and defeating the allocator's page-return entirely. The hot
// working set stays resident naturally; freed pages are returned to the OS.
trim_allocator("startup complete");
let addr = consts::SERVER_ADDRESS;
let listener = tokio::net::TcpListener::bind(addr)

View file

@ -312,6 +312,7 @@ fn build_style(is_dark: bool, layers: &[serde_json::Value], tile_url: &str) -> s
pub async fn init_tile_reader(path: &std::path::Path) -> anyhow::Result<TileReader> {
let backend = FileBackend::open(path)?;
let reader = AsyncPmTilesReader::try_from_cached_source(backend, HashMapCache::default()).await?;
let reader =
AsyncPmTilesReader::try_from_cached_source(backend, HashMapCache::default()).await?;
Ok(reader)
}