Skip to content

Commit ae00eda

Browse files
committed
feat: add physical RSS memory tracking alongside tracemalloc
- Read /proc/self/statm inside worker before and after import - Exposes pure C-extension memory allocations (e.g. grpc/protobuf) missed by Python's internal tracemalloc - Requires zero dependencies, bypassing the need for psutil
1 parent 40ac359 commit ae00eda

1 file changed

Lines changed: 37 additions & 6 deletions

File tree

scripts/import_profiler/profiler.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@
1010
import os
1111
import logging
1212

13+
def get_rss_mb():
14+
"""Gets current Resident Set Size (physical memory) in MB. Linux only."""
15+
try:
16+
with open('/proc/self/statm', 'r') as f:
17+
rss_pages = int(f.read().split()[1])
18+
page_size = os.sysconf('SC_PAGE_SIZE')
19+
return (rss_pages * page_size) / (1024 * 1024)
20+
except Exception:
21+
return 0.0
22+
1323
def run_worker(target_module):
1424
"""Performs ONE import and returns metrics."""
1525
tracemalloc.start()
@@ -19,6 +29,7 @@ def run_worker(target_module):
1929
# This explicitly isolates the pure import latency and entirely omits the
2030
# 10ms-50ms Python VM interpreter startup overhead that would skew the metrics.
2131
start_time = time.perf_counter()
32+
rss_before = get_rss_mb()
2233

2334
modules_before = set(sys.modules.keys())
2435

@@ -27,6 +38,7 @@ def run_worker(target_module):
2738
# ---------------------
2839

2940
end_time = time.perf_counter()
41+
rss_after = get_rss_mb()
3042
_, peak = tracemalloc.get_traced_memory()
3143
tracemalloc.stop()
3244

@@ -54,6 +66,7 @@ def run_worker(target_module):
5466
metrics = {
5567
"time_ms": (end_time - start_time) * 1000,
5668
"peak_ram_mb": peak / (1024 * 1024),
69+
"rss_ram_mb": rss_after - rss_before,
5770
"loaded_modules": len(new_modules),
5871
"loaded_lines": loaded_lines
5972
}
@@ -70,7 +83,7 @@ def _run_worker_and_parse(cmd):
7083
break
7184
if data is None:
7285
raise ValueError("Worker did not output metrics JSON.")
73-
for key in ("time_ms", "peak_ram_mb", "loaded_modules", "loaded_lines"):
86+
for key in ("time_ms", "peak_ram_mb", "rss_ram_mb", "loaded_modules", "loaded_lines"):
7487
if key not in data:
7588
raise KeyError(f"Missing key '{key}' in worker output")
7689
return data
@@ -84,7 +97,7 @@ def run_master(iterations, target_module, cpu="0", csv_path=None):
8497
"""Orchestrates the benchmark."""
8598
if iterations < 1:
8699
raise ValueError("Number of iterations must be at least 1.")
87-
times, memories = [], []
100+
times, memories, rss_memories = [], [], []
88101
loaded_modules_val, loaded_lines_val = 0, 0
89102

90103
print(f"Profiling start... Running {iterations} cold-start iterations for {target_module}.")
@@ -105,6 +118,7 @@ def run_master(iterations, target_module, cpu="0", csv_path=None):
105118
data = _run_worker_and_parse(cmd)
106119
times.append(data["time_ms"])
107120
memories.append(data["peak_ram_mb"])
121+
rss_memories.append(data["rss_ram_mb"])
108122
loaded_modules_val = data["loaded_modules"]
109123
loaded_lines_val = data["loaded_lines"]
110124
except FileNotFoundError as e:
@@ -116,6 +130,7 @@ def run_master(iterations, target_module, cpu="0", csv_path=None):
116130
data = _run_worker_and_parse(cmd)
117131
times.append(data["time_ms"])
118132
memories.append(data["peak_ram_mb"])
133+
rss_memories.append(data["rss_ram_mb"])
119134
loaded_modules_val = data["loaded_modules"]
120135
loaded_lines_val = data["loaded_lines"]
121136
except subprocess.CalledProcessError as err:
@@ -131,9 +146,9 @@ def run_master(iterations, target_module, cpu="0", csv_path=None):
131146
if csv_path:
132147
with open(csv_path, "w", newline="", encoding="utf-8") as f:
133148
writer = csv.writer(f)
134-
writer.writerow(["Iteration", "Time (ms)", "Peak RAM (MB)"])
135-
for idx, (t, m) in enumerate(zip(times, memories)):
136-
writer.writerow([idx + 1, f"{t:.2f}", f"{m:.4f}"])
149+
writer.writerow(["Iteration", "Time (ms)", "Tracemalloc RAM (MB)", "RSS Physical RAM (MB)"])
150+
for idx, (t, m, r) in enumerate(zip(times, memories, rss_memories)):
151+
writer.writerow([idx + 1, f"{t:.2f}", f"{m:.4f}", f"{r:.4f}"])
137152
print(f"Raw metrics successfully exported to CSV: {csv_path}")
138153

139154
# Compute percentiles (P50, P90, P99)
@@ -150,6 +165,12 @@ def run_master(iterations, target_module, cpu="0", csv_path=None):
150165
else:
151166
p50_mem = p90_mem = p99_mem = memories[0] if memories else 0.0
152167

168+
if len(rss_memories) > 1:
169+
q_rss = statistics.quantiles(rss_memories, n=100)
170+
p50_rss, p90_rss, p99_rss = q_rss[49], q_rss[89], q_rss[98]
171+
else:
172+
p50_rss = p90_rss = p99_rss = rss_memories[0] if rss_memories else 0.0
173+
153174
print(f"\n--- Results for {target_module} ({iterations} iterations) ---")
154175
print(f"Code Volume (Deterministic):")
155176
print(f" Loaded Modules: {loaded_modules_val}")
@@ -164,7 +185,7 @@ def run_master(iterations, target_module, cpu="0", csv_path=None):
164185
if len(times) > 1:
165186
print(f" StdDev: {statistics.stdev(times):.2f}")
166187

167-
print(f"RAM (MB):")
188+
print(f"Tracemalloc RAM (MB):")
168189
print(f" P50 (Median): {p50_mem:.4f}")
169190
print(f" P90: {p90_mem:.4f}")
170191
print(f" P99: {p99_mem:.4f}")
@@ -174,6 +195,16 @@ def run_master(iterations, target_module, cpu="0", csv_path=None):
174195
if len(memories) > 1:
175196
print(f" StdDev: {statistics.stdev(memories):.4f}")
176197

198+
print(f"Physical RSS RAM (MB):")
199+
print(f" P50 (Median): {p50_rss:.4f}")
200+
print(f" P90: {p90_rss:.4f}")
201+
print(f" P99: {p99_rss:.4f}")
202+
print(f" Mean: {statistics.mean(rss_memories):.4f}")
203+
print(f" Min: {min(rss_memories):.4f}")
204+
print(f" Max: {max(rss_memories):.4f}")
205+
if len(rss_memories) > 1:
206+
print(f" StdDev: {statistics.stdev(rss_memories):.4f}")
207+
177208
def run_trace(target_module):
178209
"""Generates importtime trace log and writes it to a file."""
179210
trace_file = f"import_trace_{target_module.replace('.', '_')}.log"

0 commit comments

Comments
 (0)