debug: add lightweight one-shot /api/debug/memory/objects (gc, no tracemalloc)
tracemalloc's continuous tracing locks up a loaded app, so add a one-shot gc-based memory breakdown: top object types by total size AND by count, plus the biggest individual containers (>1MB). a runaway 'count' points at an unbounded cache; a big bytes/str total points at blob retention. lets us pinpoint the RSS growth (300MB -> 1.8GB after browsing) without tracing.
This commit is contained in:
parent
5a54ffe14a
commit
de9e78cf95
1 changed files with 55 additions and 0 deletions
|
|
@ -2985,6 +2985,61 @@ def debug_memory_stop():
|
|||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/debug/memory/objects')
|
||||
def debug_memory_objects():
|
||||
"""One-shot memory breakdown by live object type (plain gc — NO tracemalloc, so it
|
||||
won't lock up a loaded app). Hit this once when RSS is high to see which type/cache
|
||||
dominates: a big 'count' reveals accumulation (a cache that never evicts); a big
|
||||
'mb' under bytes/str reveals blob retention. Pinpoints the leak without tracing."""
|
||||
import gc
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
try:
|
||||
gc.collect()
|
||||
objs = gc.get_objects()
|
||||
counts = defaultdict(int)
|
||||
sizes = defaultdict(int)
|
||||
biggest = []
|
||||
for o in objs:
|
||||
tn = type(o).__name__
|
||||
counts[tn] += 1
|
||||
try:
|
||||
sz = sys.getsizeof(o)
|
||||
except Exception:
|
||||
sz = 0
|
||||
sizes[tn] += sz
|
||||
if sz > 1_000_000 and isinstance(o, (dict, list, set, frozenset, bytes, bytearray, str, tuple)):
|
||||
try:
|
||||
biggest.append((sz, tn, len(o)))
|
||||
except Exception:
|
||||
pass
|
||||
biggest.sort(reverse=True)
|
||||
rss = None
|
||||
try:
|
||||
import psutil
|
||||
rss = round(psutil.Process(os.getpid()).memory_info().rss / (1024 * 1024), 1)
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify({
|
||||
'rss_mb': rss,
|
||||
'total_objects': len(objs),
|
||||
'top_by_size_mb': [
|
||||
{'type': t, 'count': counts[t], 'mb': round(sizes[t] / (1024 * 1024), 1)}
|
||||
for t, _ in sorted(sizes.items(), key=lambda x: x[1], reverse=True)[:20]
|
||||
],
|
||||
'top_by_count': [
|
||||
{'type': t, 'count': c, 'mb': round(sizes[t] / (1024 * 1024), 1)}
|
||||
for t, c in sorted(counts.items(), key=lambda x: x[1], reverse=True)[:20]
|
||||
],
|
||||
'biggest_containers': [
|
||||
{'mb': round(s / (1024 * 1024), 1), 'type': t, 'len': ln}
|
||||
for s, t, ln in biggest[:15]
|
||||
],
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/activity/feed')
|
||||
def get_activity_feed():
|
||||
"""Get recent activity feed for dashboard"""
|
||||
|
|
|
|||
Loading…
Reference in a new issue