test_memory_profile.py raw
1 """Memory profiling tests for the smesh WASM web app.
2
3 Requires an instrumented build:
4 MOXIEROOT=../moxie moxie build -target wasm -print-allocs "web/wasm/app" \
5 -o web/static/app.wasm ./web/wasm/app/
6
7 Run:
8 pytest test/test_memory_profile.py --headed
9 """
10
11 import time
12 import json
13 import pytest
14
15
16 def _trigger_stats(h):
17 h.js("window.__moxie_trigger_mem_stats && window.__moxie_trigger_mem_stats()")
18 time.sleep(0.3)
19
20
21 def _mem(h):
22 _trigger_stats(h)
23 return h.js("return window.__moxie_mem_stats ? window.__moxie_mem_stats() : null") or {}
24
25
26 def _counters(h):
27 _trigger_stats(h)
28 return h.js("return window.__moxie_read_alloc_counters ? window.__moxie_read_alloc_counters() : null") or {}
29
30
31 @pytest.fixture(scope="module")
32 def app(browser, relay, h):
33 """Navigate to the app and wait until WASM is ready."""
34 browser.get(relay["url"])
35 h.wait_wasm_ready(timeout=20)
36 # Allow initial render to settle.
37 time.sleep(2)
38 return browser
39
40
41 @pytest.mark.memory
42 class TestMemoryProfile:
43
44 def test_mem_stats_available(self, app, h):
45 """Verify instrumented WASM exports are accessible."""
46 stats = _mem(h)
47 assert stats is not None, "mem_stats returned None — is this an instrumented build?"
48 assert "totalAlloc" in stats or "heapPtr" in stats, f"unexpected stats shape: {stats}"
49
50 def test_baseline(self, app, h):
51 """Record initial heap state."""
52 stats = _mem(h)
53 heap_mb = stats.get("heapPtr", 0) / (1024 * 1024)
54 total_mb = stats.get("totalAlloc", 0) / (1024 * 1024)
55 print(f"\n[baseline] heapPtr={heap_mb:.1f}MB totalAlloc={total_mb:.1f}MB mallocs={stats.get('mallocs', 0)}")
56 # Baseline should be well under 64MB.
57 assert heap_mb < 64, f"baseline heap {heap_mb:.1f}MB exceeds 64MB"
58
59 def test_dom_handles_bounded(self, app, h):
60 """DOM handle count must stay under 3000."""
61 count = h.js("return window.__moxie_alloc_log ? 'logging' : 'no_log'")
62 # If no instrumentation log, skip quantitative check.
63 if count == "no_log":
64 pytest.skip("__moxie_alloc_log not initialised — set globalThis.__moxie_instrument=true")
65
66 # Enable instrumentation and wait for threshold events.
67 h.js("globalThis.__moxie_instrument = true;")
68 time.sleep(1)
69 log = h.js("return (globalThis.__moxie_alloc_log || []).slice(-10)")
70 if log:
71 max_dom = max((e.get("value", 0) for e in log if e.get("source") == "dom"), default=0)
72 assert max_dom < 3000, f"DOM handle count reached {max_dom} (limit 3000)"
73
74 def test_idle_alloc_rate(self, app, h):
75 """After subscriptions are closed, allocation rate must be < 1KB/s.
76
77 This tests that the rotate-map eviction is working: a live session
78 with no incoming events should allocate nearly nothing.
79 """
80 before = _mem(h)
81 if not before.get("totalAlloc"):
82 pytest.skip("totalAlloc not available in this build")
83
84 # Close the feed subscription to achieve idle state.
85 h.js("window.__moxie_trigger_mem_stats && window.__moxie_trigger_mem_stats()")
86 time.sleep(30)
87
88 after = _mem(h)
89 delta_bytes = after.get("totalAlloc", 0) - before.get("totalAlloc", 0)
90 rate_bytes_per_sec = delta_bytes / 30
91
92 print(f"\n[idle_rate] delta={delta_bytes}B over 30s = {rate_bytes_per_sec:.0f}B/s")
93 assert rate_bytes_per_sec < 1024, (
94 f"idle alloc rate {rate_bytes_per_sec:.0f}B/s exceeds 1KB/s — "
95 f"rotate-map may not be active or relay not quiesced"
96 )
97
98 def test_alloc_counters_present(self, app, h):
99 """Verify per-site counters are populated when -print-allocs was used."""
100 counters = _counters(h)
101 if not counters or counters.get("n", 0) == 0:
102 pytest.skip("Alloc counters not available — build without -print-allocs?")
103
104 n = counters["n"]
105 counts = counters.get("counts", [])
106 sizes = counters.get("sizes", [])
107
108 assert n > 0, "n should be positive"
109 assert len(counts) >= n
110 assert len(sizes) >= n
111
112 total_allocs = sum(counts[:n])
113 total_bytes = sum(sizes[:n])
114 print(f"\n[counters] {n} sites, {total_allocs} allocs, {total_bytes}B total")
115 # At least some allocs should have happened.
116 assert total_allocs > 0, "no allocations recorded"
117
118 def test_heap_under_restart_threshold(self, app, h):
119 """After normal use heapPtr should be well under the 128MB restart threshold."""
120 stats = _mem(h)
121 heap_bytes = stats.get("heapPtr", 0)
122 heap_mb = heap_bytes / (1024 * 1024)
123 print(f"\n[heap_check] heapPtr={heap_mb:.1f}MB")
124 assert heap_mb < 64, (
125 f"heap {heap_mb:.1f}MB exceeds 64MB after normal use — "
126 f"rotate-map may not be reducing growth"
127 )
128