Files
wehub-resource-sync 0ef5fcb1c5
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:03:20 +08:00

389 lines
12 KiB
Python

"""Memory tracking infrastructure for headroom.
This module provides centralized memory tracking across all components,
enabling observability into memory usage patterns and budget enforcement.
"""
from __future__ import annotations
import sys
import threading
import time
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any
# Try to import psutil for process memory tracking
try:
import psutil
PSUTIL_AVAILABLE = True
except ImportError:
PSUTIL_AVAILABLE = False
@dataclass
class ComponentStats:
"""Statistics for a single memory component."""
name: str
entry_count: int
size_bytes: int
budget_bytes: int | None = None
hits: int = 0
misses: int = 0
evictions: int = 0
last_updated: float = field(default_factory=time.time)
@property
def size_mb(self) -> float:
"""Size in megabytes."""
return self.size_bytes / (1024 * 1024)
@property
def budget_mb(self) -> float | None:
"""Budget in megabytes."""
return self.budget_bytes / (1024 * 1024) if self.budget_bytes else None
@property
def budget_used_percent(self) -> float | None:
"""Percentage of budget used."""
if self.budget_bytes and self.budget_bytes > 0:
return (self.size_bytes / self.budget_bytes) * 100
return None
@property
def hit_rate(self) -> float | None:
"""Cache hit rate as percentage."""
total = self.hits + self.misses
if total > 0:
return (self.hits / total) * 100
return None
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for JSON serialization."""
return {
"name": self.name,
"entry_count": self.entry_count,
"size_bytes": self.size_bytes,
"size_mb": round(self.size_mb, 2),
"budget_bytes": self.budget_bytes,
"budget_mb": round(self.budget_mb, 2) if self.budget_mb else None,
"budget_used_percent": round(self.budget_used_percent, 2)
if self.budget_used_percent
else None,
"hits": self.hits,
"misses": self.misses,
"evictions": self.evictions,
"hit_rate": round(self.hit_rate, 2) if self.hit_rate else None,
"last_updated": self.last_updated,
}
@dataclass
class ProcessStats:
"""Process-level memory statistics."""
rss_bytes: int
vms_bytes: int
percent: float
available_bytes: int
total_bytes: int
@property
def rss_mb(self) -> float:
"""Resident set size in MB."""
return self.rss_bytes / (1024 * 1024)
@property
def vms_mb(self) -> float:
"""Virtual memory size in MB."""
return self.vms_bytes / (1024 * 1024)
@property
def available_mb(self) -> float:
"""Available system memory in MB."""
return self.available_bytes / (1024 * 1024)
@property
def total_mb(self) -> float:
"""Total system memory in MB."""
return self.total_bytes / (1024 * 1024)
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for JSON serialization."""
return {
"rss_bytes": self.rss_bytes,
"rss_mb": round(self.rss_mb, 2),
"vms_bytes": self.vms_bytes,
"vms_mb": round(self.vms_mb, 2),
"percent": round(self.percent, 2),
"available_mb": round(self.available_mb, 2),
"total_mb": round(self.total_mb, 2),
}
@dataclass
class MemoryReport:
"""Complete memory report including process and component stats."""
process: ProcessStats
components: dict[str, ComponentStats]
total_tracked_bytes: int
target_budget_bytes: int | None
timestamp: float = field(default_factory=time.time)
@property
def total_tracked_mb(self) -> float:
"""Total tracked memory in MB."""
return self.total_tracked_bytes / (1024 * 1024)
@property
def target_budget_mb(self) -> float | None:
"""Target budget in MB."""
return self.target_budget_bytes / (1024 * 1024) if self.target_budget_bytes else None
@property
def is_over_budget(self) -> bool:
"""Check if tracked memory exceeds target budget."""
if self.target_budget_bytes:
return self.total_tracked_bytes > self.target_budget_bytes
return False
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for JSON serialization."""
return {
"process": self.process.to_dict(),
"components": {name: stats.to_dict() for name, stats in self.components.items()},
"total_tracked_bytes": self.total_tracked_bytes,
"total_tracked_mb": round(self.total_tracked_mb, 2),
"target_budget_bytes": self.target_budget_bytes,
"target_budget_mb": round(self.target_budget_mb, 2) if self.target_budget_mb else None,
"is_over_budget": self.is_over_budget,
"timestamp": self.timestamp,
}
class MemoryTracker:
"""Singleton that tracks memory usage across all components.
Usage:
# Register a component
tracker = MemoryTracker.get()
tracker.register("my_store", my_store.get_memory_stats)
# Get all stats
report = tracker.get_report()
# Get specific component
stats = tracker.get_component_stats("my_store")
"""
_instance: MemoryTracker | None = None
_lock: threading.Lock = threading.Lock()
def __init__(self, target_budget_mb: float | None = None):
"""Initialize the tracker.
Args:
target_budget_mb: Target memory budget in MB for all tracked components.
"""
self._components: dict[str, Callable[[], ComponentStats]] = {}
self._target_budget_bytes: int | None = (
int(target_budget_mb * 1024 * 1024) if target_budget_mb else None
)
self._component_lock = threading.Lock()
@classmethod
def get(cls, target_budget_mb: float | None = None) -> MemoryTracker:
"""Get or create the singleton instance.
Args:
target_budget_mb: Target memory budget (only used on first call).
Returns:
The singleton MemoryTracker instance.
"""
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = cls(target_budget_mb=target_budget_mb)
return cls._instance
@classmethod
def reset(cls) -> None:
"""Reset the singleton instance. Useful for testing."""
with cls._lock:
cls._instance = None
def set_target_budget(self, budget_mb: float) -> None:
"""Set the target memory budget.
Args:
budget_mb: Target budget in megabytes.
"""
self._target_budget_bytes = int(budget_mb * 1024 * 1024)
def register(self, name: str, stats_fn: Callable[[], ComponentStats]) -> None:
"""Register a component's stats function.
Args:
name: Unique name for the component.
stats_fn: Function that returns ComponentStats for this component.
"""
with self._component_lock:
self._components[name] = stats_fn
def unregister(self, name: str) -> bool:
"""Unregister a component.
Args:
name: Name of the component to unregister.
Returns:
True if component was unregistered, False if not found.
"""
with self._component_lock:
if name in self._components:
del self._components[name]
return True
return False
def get_component_stats(self, name: str) -> ComponentStats | None:
"""Get stats for a specific component.
Args:
name: Name of the component.
Returns:
ComponentStats or None if component not found.
"""
with self._component_lock:
if name in self._components:
try:
return self._components[name]()
except Exception:
return None
return None
def get_all_component_stats(self) -> dict[str, ComponentStats]:
"""Get stats for all registered components.
Returns:
Dictionary mapping component names to their stats.
"""
stats: dict[str, ComponentStats] = {}
with self._component_lock:
for name, fn in self._components.items():
try:
stats[name] = fn()
except Exception:
# Skip components that fail to report stats
pass
return stats
def get_process_stats(self) -> ProcessStats:
"""Get process-level memory statistics.
Returns:
ProcessStats with current memory usage.
"""
if PSUTIL_AVAILABLE:
process = psutil.Process()
mem_info = process.memory_info()
sys_mem = psutil.virtual_memory()
return ProcessStats(
rss_bytes=mem_info.rss,
vms_bytes=mem_info.vms,
percent=process.memory_percent(),
available_bytes=sys_mem.available,
total_bytes=sys_mem.total,
)
else:
# Fallback when psutil not available
return ProcessStats(
rss_bytes=0,
vms_bytes=0,
percent=0.0,
available_bytes=0,
total_bytes=0,
)
def get_total_tracked_bytes(self) -> int:
"""Get total memory used by all tracked components.
Returns:
Total bytes used by tracked components.
"""
stats = self.get_all_component_stats()
return sum(s.size_bytes for s in stats.values())
def get_report(self) -> MemoryReport:
"""Get a complete memory report.
Returns:
MemoryReport with process and component statistics.
"""
process_stats = self.get_process_stats()
component_stats = self.get_all_component_stats()
total_tracked = sum(s.size_bytes for s in component_stats.values())
return MemoryReport(
process=process_stats,
components=component_stats,
total_tracked_bytes=total_tracked,
target_budget_bytes=self._target_budget_bytes,
)
@property
def registered_components(self) -> list[str]:
"""Get list of registered component names."""
with self._component_lock:
return list(self._components.keys())
@property
def target_budget_mb(self) -> float | None:
"""Get target budget in MB."""
return self._target_budget_bytes / (1024 * 1024) if self._target_budget_bytes else None
def estimate_object_size(obj: Any, seen: set | None = None) -> int:
"""Estimate the memory size of a Python object recursively.
This provides a rough estimate by traversing the object graph.
For more accurate measurements, use tracemalloc or memory_profiler.
Args:
obj: Object to measure.
seen: Set of already-seen object ids (for cycle detection).
Returns:
Estimated size in bytes.
"""
if seen is None:
seen = set()
obj_id = id(obj)
if obj_id in seen:
return 0
seen.add(obj_id)
size = sys.getsizeof(obj)
if isinstance(obj, dict):
size += sum(
estimate_object_size(k, seen) + estimate_object_size(v, seen) for k, v in obj.items()
)
elif isinstance(obj, (list, tuple, set, frozenset)):
size += sum(estimate_object_size(item, seen) for item in obj)
elif hasattr(obj, "__dict__"):
size += estimate_object_size(obj.__dict__, seen)
elif hasattr(obj, "__slots__"):
size += sum(
estimate_object_size(getattr(obj, slot, None), seen)
for slot in obj.__slots__
if hasattr(obj, slot)
)
return size