soulsync/tests/test_memory_tracker.py
BoulderBadgeDad ab33d8cf2e #802: on-demand memory-growth diagnostic (tracemalloc, browser-drivable)
A user reports ~0.7 MiB/s RSS growth; the one theory offered so far
(connection leak) was debunked, so instead of guessing: measure. New
core/diagnostics/memory_tracker wraps tracemalloc behind three GET endpoints
the user can drive from a browser:

  /api/debug/memory/start   begin tracing + baseline snapshot (idempotent)
  /api/debug/memory/report  top allocation sites by GROWTH since the baseline
                            (?top=N), with traced totals + process RSS so we
                            can see how much of the real growth tracing
                            accounts for; 15-frame tracebacks name the caller
  /api/debug/memory/stop    end tracing, free trace bookkeeping

Opt-in by design — tracemalloc shadows every allocation while active, so it
never runs by default. RSS via psutil with a /proc fallback.

Tests: report-without-tracking returns a hint (not an error); a real
start->hog->report->stop roundtrip attributes a genuine 5MB allocation to the
test file (fun fact encoded in the test: 'x'*1000 constant-folds into ONE
shared string and traces as ~40KB — the hog must allocate at runtime); the
stat formatter is duck-typed and unit-tested.
2026-06-06 18:31:14 -07:00

60 lines
2.1 KiB
Python

"""Seam tests for the #802 memory-growth diagnostic (core/diagnostics)."""
from __future__ import annotations
from types import SimpleNamespace
import core.diagnostics.memory_tracker as mt
def teardown_function(_fn):
# Never leave tracemalloc running across tests — it shadows every
# allocation in the process.
mt.stop_tracking()
def test_report_without_tracking_is_a_hint_not_an_error():
mt.stop_tracking()
out = mt.report()
assert out['tracking'] is False
assert 'start' in out['hint']
def test_start_report_stop_roundtrip_captures_growth():
assert mt.start_tracking()['tracking'] is True
# Idempotent start
assert mt.start_tracking()['already_running'] is True
# Allocate something measurable after the baseline.
# bytearray(1000) allocates at RUNTIME — a constant expression like
# 'x' * 1000 gets folded into ONE shared string and traces as ~40KB.
hog = [bytearray(1000) for _ in range(5000)] # ~5 MB, genuinely allocated
out = mt.report(top=10)
assert out['tracking'] is True
assert out['elapsed_seconds'] is not None
assert out['traced_current_mb'] > 0
assert isinstance(out['top_growth'], list) and out['top_growth']
# The hog must show up as growth attributed to THIS file.
top_locations = ' '.join(s['location'] for s in out['top_growth'])
assert 'test_memory_tracker.py' in top_locations
assert any(s['size_diff_mb'] > 1 for s in out['top_growth'])
del hog
stopped = mt.stop_tracking()
assert stopped == {'tracking': False, 'was_tracking': True}
assert mt.is_tracking() is False
def test_format_stat_projects_duck_typed_stat():
frame = SimpleNamespace(filename='core/foo.py', lineno=42)
stat = SimpleNamespace(
size=2 * 1024 * 1024, size_diff=1024 * 1024,
count=10, count_diff=4,
traceback=[frame, SimpleNamespace(filename='core/bar.py', lineno=7)],
)
out = mt.format_stat(stat)
assert out['location'] == 'core/bar.py:7'
assert out['trace'] == ['core/foo.py:42', 'core/bar.py:7']
assert out['size_mb'] == 2.0 and out['size_diff_mb'] == 1.0
assert out['count'] == 10 and out['count_diff'] == 4