Jupyter/IPython magics#
scope_profiler.ipython_magics adds eleven magics to a notebook or
IPython session, so a quick “how long is this cell/line taking, and
where” doesn’t need the ProfileManager.session()/profile_region()
boilerplate spelled out by hand each time. It is a thin adapter, not a
second implementation: every magic calls straight into the same API the
HDF5 output & post-processing from Python page describes –
ProfileManager.session/profile_region,
ProfilingResults.print_summary/to_dataframe, read_h5, the
diff_rows/print_diff_table functions behind scope-profiler diff,
and prof_export/speedscope_export.
%%scope / %scope_timeit / %%scope_line / %%scope_recursive / %%scope_agg
| ProfileManager.session(return_results=True)
v
ProfilingResults ---kept in memory, keyed by name--- <-- %scope_load (read_h5)
|
+-- print_summary() (the recording magics, %scope_last)
+-- to_dataframe() / to_events_dataframe() (%scope_df)
+-- plot_durations() (-p / --plot)
+-- diff_rows() + print_diff_table() (%scope_compare)
+-- export_prof() / export_speedscope() (%scope_export)
A rule of thumb for which recording magic to reach for:
You want |
Magic |
|---|---|
Time this cell as one block |
|
Time one statement, repeatedly |
|
Find the slow line in a function |
|
Find the slow function, with nothing instrumented |
|
Time a region entered millions of times |
|
Installing and loading#
pip install "scope-profiler[notebook]"
This is a separate extra from the base install (it pulls in IPython);
pip install scope-profiler alone is unaffected. -p/--plot
additionally needs the pproc extra, exactly as scope-profiler plot
does.
Load the extension once per kernel:
%load_ext scope_profiler.ipython_magics
%%scope – profile a cell#
%%scope solve
result = solve(problem)
Runs the cell inside a profiling region named solve (the name is
optional; it defaults to "cell") and prints the region summary table
immediately after. Nothing is written to disk – the result lives in the
kernel’s memory for the rest of the session, stored under that name for
%scope_last and %scope_compare to refer back to.
Options:
-q/--quiet– suppress the printed table (still records the run).-p/--plot– also show a duration bar chart (plot_durations).--include PATTERN– regex restricting which regions the table/plot show, same matchingProfilingResults.get_regionsuses.
To profile sub-parts of the cell separately, nest
ProfileManager.profile_region calls inside it as usual – %%scope
only adds the outer region and the in-memory session, everything nested
inside works exactly as it does in a script.
%scope_timeit – time a statement#
%scope_timeit -n 20 solve(problem)
Runs the given statement n times (default 7) inside a profiling region
named "timeit" and prints calls/total/avg/min/max. It answers the same
question as the built-in %timeit, but through scope-profiler’s own
region timer and table instead of the timeit module – useful when you
want the result to be comparable with %%scope/%scope_compare output,
or plan to follow up with -p to see run-to-run variance as a bar
chart.
-q suppresses the printed table, same as for %%scope.
%%scope_line – line-by-line profiling for a cell#
%%scope_line
from scope_profiler import ProfileManager
@ProfileManager.profile("compute")
def compute(n):
total = 0
for i in range(n):
total += i * i
return total
compute(1_000_000)
Runs the cell with use_line_profiler=True and prints per-line hit
counts/time for whatever the cell itself hands to line_profiler, the
same as running the cell as a script (see guide/line_profiler and
examples/ex_line_profiling.py). Needs the line-profiler extra:
pip install "scope-profiler[line-profiler]"
Prefer decorating the function you care about with
@ProfileManager.profile, as above – it registers cleanly regardless of
where it’s called from. A bare
with ProfileManager.profile_region(...): at the cell’s top level also
works, but line_profiler’s own handling of a module-level code object
containing a nested def can cut the printed table off at that def –
the same limitation a plain script hits, not something specific to the
magic.
-q suppresses the region summary table, -Q suppresses the
line-by-line tables; both can be combined.
%%scope_recursive – profile a cell with nothing instrumented#
%%scope_recursive
result = solve(problem)
Records every Python call the cell makes as its own region, named
<module>.<qualname>, with no decorators and no profile_region blocks
– recursive_profile=True, the same thing scope-profiler run does to
a script. The summary comes back sorted by total time, so the answer to
“where did this cell’s time actually go?” is the top of the table:
│ scope_profiler.session │ 1 │ 100.00% │ 5.0e-03 │
│ └─ cell │ 1 │ 94.22% │ 4.7e-03 │
│ │ └─ __main__.<module> │ 1 │ 93.90% │ 4.7e-03 │
│ │ │ └─ __main__.outer │ 1 │ 93.63% │ 4.7e-03 │
│ │ │ │ └─ __main__.inner │ 2 │ 93.47% │ 4.7e-03 │
│ │ │ │ │ └─ __main__.inner.<locals>.<genexpr> │ 4k │ 21.46% │ 1.1e-03 │
This is the magic to reach for first, before you know what to
instrument: use it to find the hot function, then wrap that one in
%%scope/%%scope_line for a cheap, focused measurement.
Two things to know, both inherited from how recursive profiling works in a script rather than from the magic:
Tracing is not limited to your own functions – library internals called from the cell are recorded too, and each traced call carries per-call overhead. Keep the cell small and use
--includeto filter the table.The cell is run with
execrather than IPython’s own execution, so a trailing expression is not echoed asOut[n]. Assignments land in the notebook namespace exactly as usual.
A cell that raises is reported the way any other failing cell is, and
the profile of everything up to the failure is still recorded and
printed – which is often exactly what you want to see when a run dies
halfway. That holds for %%scope and %%scope_agg too.
%%scope_agg – aggregation mode for very hot regions#
%%scope_agg
for step in range(1_000_000):
with ProfileManager.profile_region("step"):
advance(step)
Runs the cell with aggregation_mode=True: each region keeps its count,
inclusive total, min/max and exclusive total, and no per-call timeline
at all. A region entered a million times therefore costs a few numbers
instead of a million timestamps, which is what makes such a cell
measurable at all.
The trade-off is that the per-call event data is gone: the Gantt chart,
%scope_df --events and the timeline exports have nothing to show. The
summary table, %scope_compare and %scope_df (region rows) work as
usual. Aggregation mode cannot be combined with line, GPU, NVTX or
LIKWID timing.
%scope_load – bring in a run from outside the notebook#
%scope_load results/run_128ranks.h5
%scope_load -n baseline results/before.h5
Reads an HDF5 file with read_h5 and stores it under the same names the
other magics use – so a run from an MPI job, a scope-profiler run, or
a colleague becomes directly comparable with what you just measured in a
cell:
%%scope candidate
solve(problem)
%scope_compare run_128ranks candidate
The name defaults to the file’s stem; -n/--name overrides it and
-q skips the summary table.
%scope_df – a run as a pandas DataFrame#
df = %scope_df
df.nlargest(5, "total_duration")
Returns the recorded run as a DataFrame (to_dataframe), so it both
renders as a table on its own and can be captured for analysis, grouping
or plotting in the notebook – everything
2. Post-processing profiling data does with a file, on the run you
just measured. Needs pandas (the pproc extra).
-n/--name– which recorded run (default: the most recent).--per-rank– one row per (region, rank) instead of one aggregated row.--events– one row per recorded call (to_events_dataframe) instead of one per region. An aggregation-mode run records no per-call events, so this comes back empty with a note saying why.--include/--exclude– regex region filters.
%scope_last – reprint a previous run#
%scope_last # the most recently recorded run
%scope_last solve # a specific one, by name
Reprints the summary table for a run recorded earlier by %%scope or
%scope_timeit, without re-running anything. Takes the same
-p/--plot and --include options as %%scope. Useful after
scrolling past a result, or to re-render it with a different --include
filter.
%scope_compare – compare two runs#
%%scope baseline
solve_naive(problem)
%%scope candidate
solve_optimized(problem)
%scope_compare baseline candidate
Aligns the two runs’ regions by name and prints one row per region with
the change in a chosen metric – the same table scope-profiler diff
prints for two HDF5 files, here for two in-notebook runs. With no
arguments, it compares the two most recently recorded runs, so
%scope_compare right after the two cells above works without naming
them.
--metric– one oftotal,avg,min,max,p50,p95,p99,imbalance,calls(defaulttotal).--sort–delta,pct, orname(defaultdelta).
%scope_export – export a run to a file#
%scope_export candidate.prof
%scope_export -n baseline baseline.speedscope.json
Writes a previously recorded run to a .prof file (openable in
snakeviz or any pstats viewer) or a speedscope JSON file, via
scope_profiler.prof_export.export_prof/
scope_profiler.speedscope_export.export_speedscope – the same
functions scope-profiler export uses on an HDF5 file, here applied
directly to an in-memory ProfilingResults. Format is inferred from
filepath’s extension (--format prof/--format speedscope to force
one); -n/--name selects which recorded run to export (default: the
most recent one); --include/ --exclude restrict which regions are
written.
%scope_reset – drop recorded runs#
%scope_reset # clear every recorded run
%scope_reset baseline # drop just one, by name
Recorded runs otherwise accumulate in memory for the life of the kernel.
Useful once %scope_last/%scope_compare have several names to choose
from and you want a clean slate, or to free a large run’s timeline once
you are done with it.
Tutorial#
7. Notebook magics walks through all eleven magics against a small toy workload, including comparing a “before” and “after” version of a function.