gabriel / musehub public
memory.py python
135 lines 4.2 KB
Raw
sha256:20b27796ed512236119feef48b4577a8fd9ded05a15267dd4e6da23604ad1f1a docs: update Suggested Ownership Split table to reflect ful… Sonnet 5 28 days ago
1 """Process memory profiling utilities.
2
3 Three instruments:
4
5 1. ``rss_mb()`` — current RSS in MiB (cheap, single call).
6
7 2. ``profile_task(name)`` — async context manager that logs RSS delta before
8 and after a block. Use it to wrap every background task so we can see
9 exactly which one is the memory hog.
10
11 3. ``top_allocations(limit)`` — returns the top tracemalloc frames by
12 cumulative memory. Requires tracemalloc to be started at boot
13 (``tracemalloc.start()`` in ``main.py``).
14
15 4. ``MemoryLogMiddleware`` — ASGI middleware that logs RSS after every
16 request above a configurable threshold.
17
18 Usage in background tasks::
19
20 from musehub.debug.memory import profile_task
21
22 async with profile_task("symbol-index repo=abc123"):
23 await build_symbol_index(session, repo_id, head)
24
25 Usage in route handlers for a one-off snapshot::
26
27 from musehub.debug.memory import rss_mb
28 logger.info("rss after push: %.1f MiB", rss_mb())
29 """
30
31 import logging
32 import os
33 import time
34 import tracemalloc
35 from contextlib import asynccontextmanager
36 from typing import AsyncGenerator
37
38 from starlette.types import ASGIApp, Receive, Scope, Send
39 from musehub.types.json_types import MemoryFrame
40
41 logger = logging.getLogger(__name__)
42
43 try:
44 import psutil as _psutil
45 _PROC = _psutil.Process(os.getpid())
46
47 def rss_mb() -> float:
48 """Return current RSS in MiB."""
49 return float(_PROC.memory_info().rss) / 1024 / 1024
50
51 except ImportError: # pragma: no cover — psutil optional in test envs
52 def rss_mb() -> float: # noqa: F811
53 return -1.0
54
55 @asynccontextmanager
56 async def profile_task(name: str) -> AsyncGenerator[None, None]:
57 """Log RSS before and after an async block.
58
59 Logs at INFO so it always appears in production logs. If the delta
60 exceeds 50 MiB it is also logged at WARNING so it's easy to grep.
61
62 Example log output::
63
64 [memory] START symbol-index repo=abc rss=210.3 MiB
65 [memory] END symbol-index repo=abc rss=310.7 MiB delta=+100.4 MiB elapsed=2.14s ← WARNING
66 """
67 rss_before = rss_mb()
68 t0 = time.monotonic()
69 try:
70 yield
71 finally:
72 elapsed = time.monotonic() - t0
73 rss_after = rss_mb()
74 delta = rss_after - rss_before
75 sign = "+" if delta >= 0 else ""
76 msg = (
77 f"[memory] END {name} "
78 f"rss={rss_after:.1f} MiB delta={sign}{delta:.1f} MiB elapsed={elapsed:.2f}s"
79 )
80 if abs(delta) >= 50:
81 logger.warning(msg)
82 else:
83 logger.info(msg)
84 logger.info(
85 "[memory] START %s rss=%.1f MiB",
86 name, rss_before,
87 )
88
89 def top_allocations(limit: int = 20) -> list[MemoryFrame]:
90 """Return the top *limit* tracemalloc frames by cumulative size.
91
92 Returns an empty list if tracemalloc was not started.
93 """
94 if not tracemalloc.is_tracing():
95 return []
96 snapshot = tracemalloc.take_snapshot()
97 stats = snapshot.statistics("lineno")
98 result = []
99 for stat in stats[:limit]:
100 frame = stat.traceback[0]
101 result.append({
102 "file": frame.filename,
103 "line": frame.lineno,
104 "size_kb": round(stat.size / 1024, 1),
105 "count": stat.count,
106 })
107 return result
108
109 class MemoryLogMiddleware:
110 """ASGI middleware that logs RSS after every response.
111
112 Only logs when RSS exceeds ``warn_above_mb`` (default 400 MiB) so that
113 normal traffic doesn't flood the logs. Set ``warn_above_mb=0`` to log
114 every request (noisy but useful during a profiling session).
115 """
116
117 def __init__(self, app: ASGIApp, warn_above_mb: float = 400.0) -> None:
118 self.app = app
119 self.warn_above_mb = warn_above_mb
120
121 async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
122 if scope["type"] not in ("http", "websocket"):
123 await self.app(scope, receive, send)
124 return
125
126 await self.app(scope, receive, send)
127
128 rss = rss_mb()
129 if rss >= self.warn_above_mb:
130 path = scope.get("path", "?")
131 method = scope.get("method", "?")
132 logger.warning(
133 "[memory] HIGH RSS %.1f MiB after %s %s",
134 rss, method, path,
135 )
File History 3 commits
sha256:20b27796ed512236119feef48b4577a8fd9ded05a15267dd4e6da23604ad1f1a docs: update Suggested Ownership Split table to reflect ful… Sonnet 5 28 days ago
sha256:8ea2f75226d5834770ccaddbaa6efda1cc337446fd481bb385de8bec4555ae79 docs: Section 9 CI pipeline — confirm job queue is idempote… Sonnet 5 28 days ago
sha256:80e1a60a39562f6e616aaafbb27487f03abbec7d8ffc6164302b7a0c0bfc63ee docs: check off Section 0 items verified in inventory doc, … Sonnet 5 28 days ago