Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions Source/WTF/wtf/uv_get_constrained_memory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,51 @@ uint64_t uv_get_constrained_memory()
return uv__get_cgroup_constrained_memory(buf);
}

static uint64_t uv__get_cgroup1_current_memory(char buf[1024])
{
char filename[4097];
char* p;
int n;

p = uv__cgroup1_find_memory_controller(buf, &n);
if (p != NULL) {
snprintf(filename, sizeof(filename),
"/sys/fs/cgroup/memory/%.*s/memory.usage_in_bytes", n, p);
uint64_t current = uv__read_uint64(filename);
if (current != 0)
return current;
}

return uv__read_uint64("/sys/fs/cgroup/memory/memory.usage_in_bytes");
}

static uint64_t uv__get_cgroup2_current_memory(char buf[1024])
{
char filename[4097];
char* p;
int n;

p = buf + strlen("0::/");
n = (int)strcspn(p, "\n");

snprintf(filename, sizeof(filename),
"/sys/fs/cgroup/%.*s/memory.current", n, p);
return uv__read_uint64(filename);
}
Comment on lines +280 to +292

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor asymmetry with this fork's limit reader: uv__get_cgroup2_memory_limits() walks leaf→root and returns the tightest ancestor memory.max/memory.high, but this reads memory.current only at the leaf. When the effective limit comes from an ancestor (e.g. a K8s pod-level limit with sidecar containers), ancestor.max - leaf.current will overestimate process.availableMemory() by whatever siblings have charged against the ancestor. Fine to land as-is since it matches upstream libuv and is still strictly better than today, but a follow-up that computes min(limit_i - current_i) across the hierarchy (or reads memory.current at the same level whose limit was selected) would make the two consistent.

Extended reasoning...

What the bug is

This fork's uv__get_cgroup2_memory_limits() (Source/WTF/wtf/uv_get_constrained_memory.cpp:186-224) diverges from upstream libuv: it walks from the leaf cgroup up to /sys/fs/cgroup and returns the tightest memory.max/memory.high found at any ancestor. The comment at line 203 is explicit: "cgroup v2 limits are hierarchical: walk from the leaf to the root, taking the tightest limit observed at any level."

The new uv__get_cgroup2_current_memory() copies upstream libuv verbatim and reads memory.current only at the leaf. It does not walk the hierarchy.

The PR description states these two values will be subtracted to implement process.availableMemory() (Node's uv_get_available_memory() semantics: cgroup limit - cgroup current usage). Pairing an ancestor-level limit with a leaf-level usage is not apples-to-apples, because in cgroup v2 an ancestor's memory.current aggregates the usage of all its descendants — including the leaf's siblings — and it is that aggregate that is enforced against the ancestor's memory.max.

Step-by-step example

Kubernetes pod with a pod-level memory limit and two containers (app + sidecar):

/sys/fs/cgroup/kubepods/pod-X/            memory.max = 1Gi   memory.current = 900Mi
/sys/fs/cgroup/kubepods/pod-X/container-A memory.max = max   memory.current = 450Mi  ← this process
/sys/fs/cgroup/kubepods/pod-X/container-B memory.max = max   memory.current = 450Mi
  1. uv_get_constrained_memory() walks the hierarchy: leaf container-A has memory.max = max, so it climbs to pod-X and returns 1Gi. ✅ Correct.
  2. uv_get_cgroup_current_memory() reads only leaf container-A/memory.current and returns 450Mi.
  3. Bun computes availableMemory = 1Gi - 450Mi ≈ 574Mi.
  4. Reality: the pod's memory.current is already 900Mi, so only ~100Mi is allocatable before the pod is OOM-killed. The result overestimates headroom by ~5.7×.

Why existing code doesn't prevent it

Upstream libuv is internally consistent because it reads both the limit and the current usage at the leaf only — so even if it misses an ancestor limit, the two numbers are at least from the same accounting scope. This fork enhanced the limit reader to be hierarchy-aware but the new usage reader is leaf-only, creating the asymmetry. Nothing on the Bun side can reconcile the two scalars after the fact, because the caller doesn't know which ancestor supplied the limit.

Impact

process.availableMemory() is advisory/best-effort, so this won't crash anything, and the single-container-per-pod case (very common) is unaffected because the leaf and the constraining ancestor coincide. But multi-container pods with a pod-level (rather than per-container) memory limit are a normal K8s deployment pattern (sidecars, service meshes), and there the reported headroom will be inflated by the siblings' usage — which somewhat undermines the PR's stated goal.

How to fix

The cleanest fix is a single hierarchy walk that computes min(memory.max_i - memory.current_i) across levels and exposes that directly as "available", since the two independent scalars can't be correctly combined by the caller. A lighter fix is to have uv__get_cgroup2_current_memory() walk the same hierarchy and return memory.current from the level whose memory.max was tightest (though that requires the limit reader to communicate which level that was). Either is reasonable as a follow-up; this PR is still a strict improvement over the status quo.


uint64_t uv_get_cgroup_current_memory()
{
char buf[1024];

if (uv__slurp("/proc/self/cgroup", buf, sizeof(buf)))
return 0;

if (strncmp(buf, "0::/", 4))
return uv__get_cgroup1_current_memory(buf);

return uv__get_cgroup2_current_memory(buf);
}

/* Find the cgroup v1 line whose controller list contains "cpu" as a whole
* token (it may appear as "cpu", "cpu,cpuacct", or "cpuacct,cpu"). Returns
* a pointer to the path component (after the leading "/") and its length. */
Expand Down Expand Up @@ -376,6 +421,11 @@ uint64_t uv_get_constrained_memory()
return 0;
}

uint64_t uv_get_cgroup_current_memory()
{
return 0;
}

int uv_get_constrained_cpu()
{
return 0;
Expand Down
1 change: 1 addition & 0 deletions Source/WTF/wtf/uv_get_constrained_memory.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#pragma once

uint64_t uv_get_constrained_memory();
uint64_t uv_get_cgroup_current_memory();
int uv_get_constrained_cpu();
Loading