Skip to content
Projects

Profiling modes

Download notebookView on GitHubRun when the site was built.

Install for this page: pip install scope-profiler (base install only).

The quickstart covers the shortest path from a few regions to a profile. This page collects the options for choosing how much code to measure and how much data to retain. Start with explicit regions; enable broader modes only when the extra detail answers a specific question.

Show code
%%writefile app.py
import time
import scope_profiler as sp
def solve():
with sp.region("linear_solve", tags=["solver", "expensive"]):
time.sleep(0.002)
def write_output():
with sp.region("write_output", tags=["io"]):
time.sleep(0.001)
def setup_mesh():
with sp.region("setup_mesh"):
time.sleep(0.001)
def main():
setup_mesh()
for _ in range(3):
solve()
write_output()
if __name__ == "__main__":
main()
Writing app.py

The cells below profile this small app.py, written into a scratch directory as the working directory. The command-line cells pass -q to suppress the per-region summary, and the Python cells pass verbose=False.

The default CLI mode activates regions already present in the application:

%%bash
scope-profiler run -q -o profile.h5 app.py
region_names("profile.h5")
['linear_solve', 'setup_mesh', 'write_output']

Use tags to select a subset of regions. A repeated --tag uses OR semantics:

def solve():
time.sleep(0.002)
with sp.session(output=None, verbose=False):
with sp.region("linear_solve", tags=["solver", "expensive"]):
solve()
%%bash
scope-profiler run -q -o tagged.h5 --tag solver --tag io app.py
region_names("tagged.h5")
['linear_solve', 'write_output']

For code without explicit regions, or when function-level attribution is needed, opt in to recursive tracing:

%%bash
scope-profiler run -q -o recursive.h5 --recursive app.py
region_names("recursive.h5")
['__main__.<module>',
'__main__.main',
'__main__.setup_mesh',
'__main__.solve',
'__main__.write_output',
'app.py',
'linear_solve',
'setup_mesh',
'write_output']

--include and --exclude accept shell-style globs for recursively traced function names. --all additionally includes installed packages and the standard library:

%%bash
scope-profiler run -q -o filtered.h5 --recursive \
--include 'my_app.solver.*' \
--exclude '*logging*' \
app.py
region_names("filtered.h5")
['app.py', 'linear_solve', 'setup_mesh', 'write_output']

Use --entrypoint main to name the traced application entrypoint. Recursive tracing has more overhead than explicit regions, so filters are recommended for large applications.

%%bash
scope-profiler run -q -o entrypoint.h5 --recursive --entrypoint main app.py
region_names("entrypoint.h5")
['__main__.<module>',
'__main__.setup_mesh',
'__main__.solve',
'__main__.write_output',
'app.py',
'linear_solve',
'main',
'setup_mesh',
'write_output']

The CLI is equivalent to a managed Python session. For a callable entrypoint, use sp.run():

def main():
with sp.region("step"):
time.sleep(0.001)
sp.run(main, output="profile.h5", verbose=False)

Use the session decorator when the application should own its entrypoint:

def run_application():
with sp.region("step"):
time.sleep(0.001)
@sp.session(file_path="profile.h5", verbose=False)
def main():
run_application()
main()

For a script that cannot use a context manager, opt in to a guarded exit hook:

run = sp.setup(file_path="profile.h5", auto_finalize=True)

The exit hook finalizes when the interpreter exits. This notebook finalizes explicitly instead, which cancels the pending hook:

sp.finalize(verbose=False)

The output extension selects the format. --no-output is useful for checking regions or printing a summary without leaving a file behind:

%%bash
scope-profiler run -q -o profile.h5 app.py
scope-profiler run -q -o profile.json app.py
scope-profiler run -q -o report.html app.py
scope-profiler run -q --no-output app.py
ls
app.py
entrypoint.h5
filtered.h5
profile.h5
profile.json
recursive.h5
report.html
tagged.h5

Use --label to identify a run in comparisons:

%%bash
scope-profiler run -q --label baseline app.py
sp.read_h5("profiling_data.h5").display_label
'baseline'

Thread and asynchronous task tracking are opt-in Python API modes:

from concurrent.futures import ThreadPoolExecutor
def worker(index):
with sp.region("worker"):
time.sleep(0.001)
def run_parallel_work():
with ThreadPoolExecutor(max_workers=2) as pool:
list(pool.map(worker, range(4)))
async def main():
with sp.region("task"):
await asyncio.sleep(0.001)
with sp.session(track_threads=True, verbose=False, output=None):
run_parallel_work()
# A notebook already runs an event loop: in a script this is `asyncio.run(main())`.
with sp.session(track_async=True, verbose=False, output=None):
await main()

track_async=True also tracks task suspension time, so a region held across an await can be separated into running and awaiting time.

Aggregation mode stores counts and timing totals without per-call events:

%%bash
scope-profiler run -q -o aggregated.h5 --aggregation-mode app.py

For periodic workloads, pause profiling between phases or use sample_every():

class Simulation:
def step(self):
with sp.region("step"):
pass
simulation = Simulation()
steps = 100
with sp.session(file_path="profile.h5", verbose=False):
with ProfileManager.sample_every(10) as profile_step:
for step in range(steps):
with profile_step(step):
simulation.step()
{region.name: region.num_calls for region in sp.read_h5("profile.h5").get_regions()}
{'scope_profiler.session': 1, 'step': 10}

Only every tenth step is profiled: 10 of the 100 calls of step are recorded.

See Configuration for line profiling, GPU timing, NVTX, MPI, LIKWID, memory profiling, and HDF5 tuning options.

Load the IPython magics when profiling from a notebook:

%load_ext scope_profiler.ipython_magics

Use %%scope for a measured cell, %%scope_recursive when nothing is instrumented, %%scope_line for line timings, and %%scope_agg for very hot regions. %scope_compare, %scope_load, and %scope_export support the follow-up analysis workflow. See Jupyter/IPython magics for the complete reference.