Skip to content

Conversation

@Ddfulton
Copy link

📝 Summary

When running marimo in containers with resource limits (e.g., docker run --memory=16g), the machine stats footer shows host resources instead of container resources. This PR auto-detects containerized environments with cgroup limits and displays container resources instead.

Before: Shows host memory (512GB) even in a 16GB container
image

After: Shows actual container limits with "container memory" label
image

This is something we've been doing at Carolina Cloud for a while and have found very useful. Not sure whether it belongs in the main source code but figured I'd share to see what the community thinks!

🔍 Description of Changes

Backend:

  • Added cgroup detection functions (has_cgroup_limits(), get_container_resources()) supporting both v1 and v2
  • Fixed missing cgroup v1 CPU detection bug
  • Modified /api/usage endpoint to return container stats when limits detected, with is_container flag
    Frontend:
  • Updated memory stats to show "container memory" vs "computer memory" based on is_container field
    Compatibility:
  • Only activates on Linux with cgroups (Docker, Kubernetes, Podman)
  • Safely falls back to host stats on macOS/Windows or containers without limits

📋 Checklist

  • I have read the contributor guidelines.
  • For large changes, or changes that affect the public API: this change was discussed or approved through an issue, on Discord, or the community discussions (Please provide a link if applicable).
  • I have added tests for the changes made.
  • I have run the code and verified that it works as expected.

@vercel
Copy link

vercel bot commented Dec 12, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
marimo-docs Ready Ready Preview, Comment Dec 12, 2025 8:45pm

@github-actions
Copy link

github-actions bot commented Dec 12, 2025

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@github-actions github-actions bot added the bash-focus Area to focus on during release bug bash label Dec 12, 2025
@Ddfulton
Copy link
Author

I have read the CLA Document and I hereby sign the CLA

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds automatic detection of containerized environments (Docker, Kubernetes, Podman) to display accurate resource statistics when running marimo in containers with resource limits. Instead of showing misleading host-level metrics, the footer now displays container-specific memory limits when cgroup constraints are detected, with an updated label to indicate "container memory" vs "computer memory".

Key Changes:

  • Added cgroup v1 and v2 detection functions to identify and read container resource limits from Linux cgroups
  • Modified the /api/usage endpoint to return container statistics when limits are detected, adding an is_container boolean flag
  • Updated the frontend machine stats component to display appropriate memory labels based on container context

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
marimo/_utils/health.py Added has_cgroup_limits() and get_container_resources() functions to detect and read cgroup v1/v2 resource limits
marimo/_server/api/endpoints/health.py Modified /api/usage endpoint to use container resources when available and include is_container flag
tests/_server/api/endpoints/test_health.py Added assertions to verify is_container field is present and boolean in API response
packages/openapi/src/api.ts Added optional is_container boolean field to memory response type
packages/openapi/api.yaml Added is_container boolean field to memory schema definition
frontend/src/components/editor/chrome/wrapper/footer-items/machine-stats.tsx Updated memory stats tooltip to show "container memory" vs "computer memory" based on is_container flag
Comments suppressed due to low confidence (11)

marimo/_utils/health.py:204

  • File is opened but is not closed.
                open("/sys/fs/cgroup/memory.max", encoding="utf-8")

marimo/_utils/health.py:213

  • File is opened but is not closed.
                open("/sys/fs/cgroup/cpu.max", encoding="utf-8").read().strip()

marimo/_utils/health.py:226

  • File is opened but is not closed.
                open(
                    "/sys/fs/cgroup/memory/memory.limit_in_bytes",
                    encoding="utf-8",
                )

marimo/_utils/health.py:238

  • File is opened but is not closed.
                open("/sys/fs/cgroup/cpu/cpu.cfs_quota_us", encoding="utf-8")

marimo/_utils/health.py:345

  • File is opened but is not closed.
                    open("/sys/fs/cgroup/cpu.max", encoding="utf-8")

marimo/_utils/health.py:366

  • File is opened but is not closed.
                    open(
                        "/sys/fs/cgroup/cpu/cpu.cfs_quota_us", encoding="utf-8"
                    )

marimo/_utils/health.py:374

  • File is opened but is not closed.
                    open(
                        "/sys/fs/cgroup/cpu/cpu.cfs_period_us",
                        encoding="utf-8",
                    )

marimo/_utils/health.py:323

  • File is opened but is not closed.
                    open(
                        "/sys/fs/cgroup/memory/memory.usage_in_bytes",
                        encoding="utf-8",
                    )

marimo/_utils/health.py:287

  • File is opened but is not closed.
                    open("/sys/fs/cgroup/memory.max", encoding="utf-8")

marimo/_utils/health.py:315

  • File is opened but is not closed.
                    open(
                        "/sys/fs/cgroup/memory/memory.limit_in_bytes",
                        encoding="utf-8",
                    )

marimo/_utils/health.py:292

  • File is opened but is not closed.
                    open("/sys/fs/cgroup/memory.current", encoding="utf-8")

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +363 to +377
quota = int(
open(
"/sys/fs/cgroup/cpu/cpu.cfs_quota_us", encoding="utf-8"
)
.read()
.strip()
)
period = int(
open(
"/sys/fs/cgroup/cpu/cpu.cfs_period_us",
encoding="utf-8",
)
.read()
.strip()
)
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

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

File handles are not being closed after reading cgroup files, which can lead to resource leaks. Each open() call should use a context manager (with statement) or explicitly close the file handle.

Copilot uses AI. Check for mistakes.
Comment on lines +203 to +207
memory_max = (
open("/sys/fs/cgroup/memory.max", encoding="utf-8")
.read()
.strip()
)
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

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

File handles are not being closed after reading cgroup files, which can lead to resource leaks. Each open() call should use a context manager (with statement) or explicitly close the file handle. This issue occurs multiple times throughout both functions where files are opened but never closed.

Copilot uses AI. Check for mistakes.
Comment on lines +212 to +214
cpu_max = (
open("/sys/fs/cgroup/cpu.max", encoding="utf-8").read().strip()
)
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

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

File handles are not being closed after reading cgroup files, which can lead to resource leaks. Each open() call should use a context manager (with statement) or explicitly close the file handle.

Copilot uses AI. Check for mistakes.
Comment on lines +222 to +229
limit = int(
open(
"/sys/fs/cgroup/memory/memory.limit_in_bytes",
encoding="utf-8",
)
.read()
.strip()
)
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

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

File handles are not being closed after reading cgroup files, which can lead to resource leaks. Each open() call should use a context manager (with statement) or explicitly close the file handle.

Copilot uses AI. Check for mistakes.
Comment on lines +189 to +388
def has_cgroup_limits() -> tuple[bool, bool]:
"""
Check if cgroup resource limits are explicitly set.
Returns:
(has_memory_limit, has_cpu_limit): Tuple of booleans indicating
whether memory and CPU limits are set.
"""
has_memory = False
has_cpu = False

try:
# Check cgroup v2 (modern containers)
if os.path.exists("/sys/fs/cgroup/memory.max"):
memory_max = (
open("/sys/fs/cgroup/memory.max", encoding="utf-8")
.read()
.strip()
)
# 'max' means unlimited, any number means limited
has_memory = memory_max != "max"

if os.path.exists("/sys/fs/cgroup/cpu.max"):
cpu_max = (
open("/sys/fs/cgroup/cpu.max", encoding="utf-8").read().strip()
)
# 'max' means unlimited
has_cpu = cpu_max != "max"

# Fallback to cgroup v1 (legacy)
if not has_memory and os.path.exists(
"/sys/fs/cgroup/memory/memory.limit_in_bytes"
):
limit = int(
open(
"/sys/fs/cgroup/memory/memory.limit_in_bytes",
encoding="utf-8",
)
.read()
.strip()
)
# Very large number (typically > 2^62) indicates unlimited
# This is the default "unlimited" value in cgroup v1
has_memory = limit < (1 << 62)

if not has_cpu and os.path.exists(
"/sys/fs/cgroup/cpu/cpu.cfs_quota_us"
):
quota = int(
open("/sys/fs/cgroup/cpu/cpu.cfs_quota_us", encoding="utf-8")
.read()
.strip()
)
# In cgroup v1, -1 means unlimited
has_cpu = quota > 0

except (FileNotFoundError, PermissionError, ValueError) as e:
LOGGER.debug(f"Error checking cgroup limits: {e}")

return has_memory, has_cpu


def get_container_resources() -> Optional[dict[str, Any]]:
"""
Get container resource limits if running in a resource-restricted container.
Returns:
Dictionary with 'memory' and/or 'cpu' keys if limits are set,
None if not in a container or no limits are configured.
Example return value:
{
'memory': {
'total': 2147483648, # bytes
'used': 1073741824, # bytes
'available': 1073741824, # bytes
'percent': 50.0 # percentage
},
'cpu': {
'quota': 200000, # microseconds
'period': 100000, # microseconds
'cores': 2.0 # effective number of cores
}
}
"""
has_memory_limit, has_cpu_limit = has_cgroup_limits()

if not (has_memory_limit or has_cpu_limit):
return None

resources: dict[str, Any] = {}

# Get memory stats if limited
if has_memory_limit:
try:
# Try cgroup v2 first
if os.path.exists("/sys/fs/cgroup/memory.max"):
memory_max = (
open("/sys/fs/cgroup/memory.max", encoding="utf-8")
.read()
.strip()
)
memory_current = (
open("/sys/fs/cgroup/memory.current", encoding="utf-8")
.read()
.strip()
)

if memory_max != "max":
total = int(memory_max)
used = int(memory_current)
available = total - used
percent = (used / total) * 100 if total > 0 else 0

resources["memory"] = {
"total": total,
"used": used,
"available": available,
"percent": percent,
}
# Fallback to cgroup v1
elif os.path.exists("/sys/fs/cgroup/memory/memory.limit_in_bytes"):
total = int(
open(
"/sys/fs/cgroup/memory/memory.limit_in_bytes",
encoding="utf-8",
)
.read()
.strip()
)
used = int(
open(
"/sys/fs/cgroup/memory/memory.usage_in_bytes",
encoding="utf-8",
)
.read()
.strip()
)
available = total - used
percent = (used / total) * 100 if total > 0 else 0

resources["memory"] = {
"total": total,
"used": used,
"available": available,
"percent": percent,
}
except (FileNotFoundError, PermissionError, ValueError) as e:
LOGGER.debug(f"Error reading container memory stats: {e}")

# Get CPU stats if limited
if has_cpu_limit:
try:
# cgroup v2
if os.path.exists("/sys/fs/cgroup/cpu.max"):
cpu_max_line = (
open("/sys/fs/cgroup/cpu.max", encoding="utf-8")
.read()
.strip()
)
if cpu_max_line != "max":
parts = cpu_max_line.split()
if len(parts) == 2:
quota = int(parts[0])
period = int(parts[1])
cores = quota / period

resources["cpu"] = {
"quota": quota,
"period": period,
"cores": cores,
}
# cgroup v1
elif os.path.exists("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"):
quota = int(
open(
"/sys/fs/cgroup/cpu/cpu.cfs_quota_us", encoding="utf-8"
)
.read()
.strip()
)
period = int(
open(
"/sys/fs/cgroup/cpu/cpu.cfs_period_us",
encoding="utf-8",
)
.read()
.strip()
)
if quota > 0: # -1 means unlimited
cores = quota / period
resources["cpu"] = {
"quota": quota,
"period": period,
"cores": cores,
}
except (FileNotFoundError, PermissionError, ValueError) as e:
LOGGER.debug(f"Error reading container CPU stats: {e}")

return resources if resources else None
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

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

The new cgroup detection functions has_cgroup_limits() and get_container_resources() lack unit tests. Given that other utility functions in marimo/_utils/health.py have corresponding tests in tests/_utils/test_health_utils.py, these functions should also have unit tests covering different scenarios (cgroup v1, v2, no limits, file not found, etc.).

Copilot uses AI. Check for mistakes.
Comment on lines +237 to +241
quota = int(
open("/sys/fs/cgroup/cpu/cpu.cfs_quota_us", encoding="utf-8")
.read()
.strip()
)
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

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

File handles are not being closed after reading cgroup files, which can lead to resource leaks. Each open() call should use a context manager (with statement) or explicitly close the file handle.

Copilot uses AI. Check for mistakes.
Comment on lines +286 to +295
memory_max = (
open("/sys/fs/cgroup/memory.max", encoding="utf-8")
.read()
.strip()
)
memory_current = (
open("/sys/fs/cgroup/memory.current", encoding="utf-8")
.read()
.strip()
)
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

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

File handles are not being closed after reading cgroup files, which can lead to resource leaks. Each open() call should use a context manager (with statement) or explicitly close the file handle.

Copilot uses AI. Check for mistakes.
Comment on lines +311 to +326
total = int(
open(
"/sys/fs/cgroup/memory/memory.limit_in_bytes",
encoding="utf-8",
)
.read()
.strip()
)
used = int(
open(
"/sys/fs/cgroup/memory/memory.usage_in_bytes",
encoding="utf-8",
)
.read()
.strip()
)
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

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

File handles are not being closed after reading cgroup files, which can lead to resource leaks. Each open() call should use a context manager (with statement) or explicitly close the file handle.

Copilot uses AI. Check for mistakes.
Comment on lines +344 to +348
cpu_max_line = (
open("/sys/fs/cgroup/cpu.max", encoding="utf-8")
.read()
.strip()
)
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

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

File handles are not being closed after reading cgroup files, which can lead to resource leaks. Each open() call should use a context manager (with statement) or explicitly close the file handle.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@Light2Dark Light2Dark left a comment

Choose a reason for hiding this comment

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

I think it's a great improvement :). The frontend looks good to me.

Curious if you have tried this solution on the backend? giampaolo/psutil#1011. api docs: https://psutil.readthedocs.io/en/latest/index.html#psutil.PROCFS_PATH

Comment on lines +59 to +61
const isContainer = is_container === true;
const roundedPercent = Math.round(percent);
const memoryLabel = isContainer ? "container memory" : "computer memory";
Copy link
Contributor

Choose a reason for hiding this comment

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

nit

Suggested change
const isContainer = is_container === true;
const roundedPercent = Math.round(percent);
const memoryLabel = isContainer ? "container memory" : "computer memory";
const roundedPercent = Math.round(percent);
const memoryLabel = is_container ? "container memory" : "computer memory";

@mscolnick
Copy link
Contributor

mscolnick commented Dec 15, 2025

this is a good find @Light2Dark . @Ddfulton using https://psutil.readthedocs.io/en/latest/index.html#psutil.PROCFS_PATH should hopefully clean up some code, if you are open to make the change

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bash-focus Area to focus on during release bug bash

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants