Native profiling¶
mew profile profiles benchmarks out of process, so it captures native
(C/C++) stack frames — the ones a compiled extension spends its time in, which
the in-process mew run --sample (CPU profiling) cannot see.
It launches a fresh worker process per benchmark case and lets a system profiler sample the whole process while that case runs. The deliverable is an artifact you open in the profiler’s own viewer, not a column in the results table.
Backends¶
mew profile selects a native-frame profiler with --profiler (default auto):
Backend |
Platform |
Native frames |
Artifact / viewer |
|---|---|---|---|
|
macOS |
✓ |
|
|
Linux, Windows |
✓ (not macOS) |
speedscope JSON → speedscope.app |
|
Linux |
✓ |
|
auto picks the platform’s native backend. If none is available — for example
macOS with only the Command Line Tools and no full Xcode — it tells you so and
points you to mew run --sample for in-process Python sampling.
The py-spy and perf backends both emit a speedscope-readable
artifact, so a single viewer covers them. py-spy needs an extra
uv pip install py-spy for installation and, in containers, CAP_SYS_PTRACE. perf is a
system package whose version must match the running kernel, and recording usually
needs kernel.perf_event_paranoid lowered.
Basics (xctrace / macOS)¶
xctrace ships with the full Xcode (not the Command Line Tools alone). If you see
an “xctrace needs the full Xcode” error, select it:
$ sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
Then record and open in Instruments:
$ mew profile --open
$ mew profile -k bench_sort --open # filter benchmarks like `mew run`
By default all cases land in one mew.trace bundle with one run per case
(navigate them with Instruments’ run picker). Pass --separate for one
<case>.trace file each.
Templates¶
--template chooses which Instruments instruments record and how they’re
configured; mew profile defaults to Time Profiler (CPU sampling). Pass any
other template name to record something else:
$ xctrace list templates # authoritative list for your Xcode version
$ mew profile --template Allocations
$ mew profile --template "System Trace" # quote names with spaces
The exact set is Xcode-version-dependent, but the standard templates relevant to mew are:
Template |
Records |
|---|---|
|
CPU call-stack sampling (the default). |
|
Heap allocations over time, incl. C-extension |
|
Allocations plus periodic leak detection. |
|
Syscalls, VM faults, thread scheduling — off-CPU insight. |
|
Hardware performance counters (instructions, cache misses). |
--template also accepts a path to a .tracetemplate, so a custom template
works with no change to mew:
$ mew profile --template ~/Library/Application\ Support/Instruments/Templates/My.tracetemplate
Do you need a custom one? Almost never. The built-ins cover the axes mew
cares about, and xctrace record can only use a template, not author one. You’d
make your own only for a specific combination of instruments (e.g. Time Profiler
Allocations in one recording) or non-default settings (e.g. a higher sampling rate) — built in the Instruments GUI via File → Save As Template, which writes a
.tracetemplateinto~/Library/Application Support/Instruments/Templates/. Pass that path to--template. There’s no CLI authoring path, and no reason for mew to add one: pass-through already covers it.
Native memory and leaks¶
--template Allocations / Leaks turn mew profile into a native memory tool,
catching C-extension malloc that mew run --profile-memory (memray, Python-level)
misses. For a quick leak check without Instruments, the base-system /usr/bin/leaks
— available with just the Command Line Tools, unlike xctrace — inspects a process
for unreferenced blocks; run the target with MallocStackLogging=1 for allocation
backtraces (atos symbolicates them). Caveat: a single snapshot conflates one-time
setup (lazy imports, interned strings, allocator arenas) with real leaks, so treat
the count as a starting point, not a gate.
Basics (py-spy / perf, Linux)¶
Both write one artifact per case into --output-dir (default ./.mew-traces)
and load into speedscope.app:
$ mew profile -p py-spy # *.speedscope.json per case
$ mew profile -p perf # *.perf.txt per case (raw *.data kept alongside)
$ mew profile -p py-spy --open # opens via the `speedscope` CLI if installed
If npx speedscope / the speedscope CLI isn’t installed, --open prints the
artifact path to drag onto speedscope.app instead.
Why a separate command¶
In-process samplers (pyinstrument, memray) run inside the benchmark interpreter
and return a small summary that mew run staples onto each timed row. A native
profiler must sample the process from the outside, so it can’t enrich those
rows — it produces a trace file instead. Keeping it under mew profile makes the
trade-off explicit: mew run --sample for a quick Python-level summary in the
table, mew profile when you need to see into the C.
Symbol resolution¶
Native frames are only readable if the extension carries debug symbols. Build it
RelWithDebInfo (or at least with -g) and keep the .dSYM (macOS) /
unstripped .so next to the module. A stripped release build shows
address-only frames in the native stack.
Tuning¶
Flag |
Default |
Notes |
|---|---|---|
|
|
Body reps under the sampler. Out-of-process samplers run at ~1 kHz, so fast benchmarks need many reps to accumulate stacks. |
|
|
(py-spy/perf) Sampling frequency in Hz. Ignored by xctrace. |
|
none |
Hard cap per recording, e.g. |
Like the in-process passes, profiling runs separately from timing — don’t read timings out of a profile.
What native profiles include (and don’t)¶
Two differences from mew run --sample to keep in mind when reading a native
profile:
state.pause()regions are not excluded. The in-process sampler suspends sampling for the duration of apause()block, so setup excluded from timing is also excluded from the profile. A native profiler samples the whole process from the outside and has no hook intopause(), so everything in the benchmark body — setup outside thefor _ in stateloop and paused regions included — shows up in the samples. If a hot frame in the flame graph is really one-time setup, that’s why. Move setup out of the profiled body, or read the profile knowing it’s there.On-CPU only. Sampling profilers count where the program is running, not where it’s blocked — time spent waiting on the GIL, locks, or I/O is largely invisible. If a benchmark looks fast on-CPU but slow on the clock, the gap is off-CPU time.
py-spy --idlesurfaces some of it; perf can approximate it via scheduler tracepoints. Neither is wired up by default.