API reference#
Install for this page:
pip install "scope-profiler[pproc]"covers the complete reference. The core profiling and file APIs need onlypip install scope-profiler; plotting/DataFrames needpproc, and LIKWID collection needsscope-profiler[likwid]plus a system LIKWID installation.
Module-level shortcuts#
The everyday lifecycle, instrumentation, and state-query calls are importable from the package root, so the common case needs no class name:
import scope_profiler as sp
with sp.session(file_path="run.h5", return_results=True) as run:
with sp.region("solve"):
solve()
results = run.results
sp.setup, sp.finalize, sp.session, sp.region, sp.profile,
sp.is_active, sp.is_configured, sp.registered_regions, and
sp.recorded_regions are the corresponding ProfileManager class
methods rather than wrappers around them, and act on the same
process-wide default manager. Use ProfileManager directly for
independent manager instances.
ProfileManager#
The main entry point for all profiling operations. Class methods provide
the process-wide default manager, so the common case does not require an
instance. Create ProfileManager() instances when independent profiling
sessions need to coexist; each instance owns its configuration, regions,
decorators, and output.
- class scope_profiler.profile_manager.ProfileManager#
Manage and track a set of profiling regions.
The class methods remain the process-wide default API. Instantiating the class creates an independent manager with its own configuration, regions, decorators, and call-id space, allowing multiple profiling sessions to be active at the same time:
cpu = ProfileManager() io = ProfileManager() with cpu.session(file_path="cpu.h5"): with io.session(file_path="io.h5"): with cpu.profile_region("compute"), io.profile_region("write"): work()
- classmethod region(region_name, functions=None, tags=None)#
Get the profiling region named
region_name, creating it if needed.The returned region is a context manager, which is how a block of code is timed:
with ProfileManager.region("solve"): solve()
- Parameters:
region_name (str) – The name of the profiling region.
functions (list of callable, optional) –
Functions to register for line-by-line profiling. Only has an effect when
use_line_profiler=True. Useful when using the context manager form, since the decorator form (wrap) registers functions automatically:with ProfileManager.region("my_region", functions=[my_func]): my_func()
tags (iterable of str, optional) – User-defined labels persisted with the region. Reusing a region name with a different non-None tag set raises
ValueError.
- Returns:
ProfileRegion
- Return type:
The ProfileRegion instance.
Notes
profile_regionis the original name for this method and remains available as an alias; the two are the same object.
- classmethod profile_region(region_name, functions=None, tags=None)#
Original name for
region(), kept so existing instrumentation keeps working unchanged. Same object, not a forwarding wrapper: this is on the per-event hot path.- Return type:
- classmethod profile(region_name=None, recursive=None, metadata=None, record_outcome=False)#
Decorator factory for profiling a function.
- Parameters:
region_name (str, optional) – Name for the profiling region. If not provided, uses the decorated function’s name. Supports being used with or without parentheses.
recursive (bool, optional) – If True, also profiles Python function calls made by the decorated function (excluding scope-profiler internals). If None, falls back to
ProfileManager.setup(recursive_profile=...).metadata (callable or dict, optional) – Metadata to attach to each call. A callable receives the decorated function’s
(*args, **kwargs)and must return a mapping.record_outcome (bool, optional) – Add
statusand, for failures,exceptionmetadata to each decorated call (default: False).
- Returns:
Decorated function wrapped with profiling instrumentation.
- Return type:
Callable
Notes
The decorated function is registered so that calling
ProfileManager.setup()after decoration re-binds the wrapper to the new region class at zero per-call cost. This means@ProfileManager.profilecan be applied at class-definition time even whensetup()is called later.
- classmethod run(func, *args, output=None, **kwargs)#
Run a callable inside a managed profiling session.
- classmethod profile_script(script_path, *, script_args=None, output=None, **kwargs)#
Profile a script from Python using the CLI runner semantics.
- classmethod profile_command(command, *, output=None, recursive=False, all=False, mpi_calls=False)#
Run a command under
scope-profiler runand return its result.
- classmethod instrument(module, *, include=('*',), exclude=(), prefix=None)#
Decorate matching module-level functions without editing source.
- classmethod region_factory(region_name, *, tags=None)#
Create reusable regions whose calls may carry dynamic metadata.
- Return type:
RegionFactory
- classmethod setup_from_env(**overrides)#
Configure profiling from
SCOPE_PROFILER_*environment variables.
- classmethod finalize(verbose=True, return_results=False, native_traces=None, verbose_line_profiler=False)#
Finalize profiling and write the run’s data to a single output file.
Copies each region’s buffered timestamps out, moves every rank’s copy to rank 0, and has rank 0 write them into one HDF5 file. Nothing is staged on the filesystem, so no shared
$TMPDIRis needed. Optionally prints profiling statistics for each region.Under MPI this is collective: every rank must call it, with the same arguments, or the job hangs - rank 0 waits for a payload from every other rank. A rank that dies before reaching it therefore leaves the job waiting rather than silently dropping that rank’s data.
With
use_likwid=Truethis is also where the LIKWID markers are closed and every marker region of the run is read back and stored in the output file underrank<r>/likwid/regions/<tag>; seeget_likwid_regions()for reading it back.- Parameters:
verbose (bool, optional) – If True, prints the concise profiling summary (default: True).
verbose_line_profiler (bool, optional) – If True, prints detailed line-profiler tables when line profiling is enabled (default: False).
return_results (bool, optional) –
If True, return the run’s data as a
ProfilingResults- the same post-processing APIread_h5()gives back, built straight from the in-memory buffers instead of by reading the output file back:results = ProfileManager.finalize(return_results=True) results.print_summary() df = results.to_dataframe()
This works with
deactivate_file_output=True, where no file is written at all. Under MPI the per-rank data is gathered on rank 0, which is collective: every rank must pass the same value.native_traces (path or sequence of paths, optional) –
Files (or directories of them) written by the C or Fortran region API in this same process, to fold into this run’s output - either native format, a
.spttrace or the.h5anSP_USE_HDF5C build writes. Each rank picks up the file matching its own rank, so a mixed-language MPI run still produces one file:kernels.stop_profiling() # native sp_finalize() ProfileManager.finalize(native_traces=".")
Call the native side’s
sp_finalize()first: its output has to exist by the time this reads it. A region name recorded on both sides raises, rather than silently double-counting.
- Returns:
The run’s profiling data when
return_results=True, and None otherwise. Under MPI rank 0 gets the whole run, like the merged output file; the other ranks get an empty result set for whichprint_summary(), theplot_*functions and the exporters do nothing, so the script above needs no rank guard. Seeis_root.- Return type:
ProfilingResults or None
- classmethod read_results()#
Open the merged profiling file this run wrote, for post-processing.
Convenience for analysing results in the same script that produced them:
ProfileManager.finalize() results = ProfileManager.read_results() results.print_summary()
- Returns:
The data in the file at
config.file_path.- Return type:
- Raises:
FileNotFoundError – If the merged file does not exist yet. It is written by
finalize(), and only on rank 0 - guard the call withif ProfileManager.get_config()._rank == 0under MPI.
- classmethod get_region(region_name)#
Get a registered ProfileRegion by name.
- Parameters:
region_name (str) – The name of the profiling region.
- Returns:
ProfileRegion or None
- Return type:
The registered ProfileRegion instance or None if not found.
- classmethod get_all_regions()#
Get all registered ProfileRegion instances.
- Returns:
dict
- Return type:
Dictionary of all registered ProfileRegion instances.
- classmethod define_region(region_name, functions=None, tags=None)#
Define a reusable region handle that survives later setup calls.
- Return type:
RegionHandle
- classmethod metadata(**values)#
Attach key/value metadata to calls completed inside this scope.
- classmethod registered_regions()#
Names of all instrumentation points known to this manager.
- classmethod recorded_regions()#
Names of regions that have recorded at least one call.
- classmethod last_results()#
Most recent in-memory results requested from this manager, if any.
- classmethod setup(options=None, *, auto_finalize=False, output=<object object>, replace=False, region_tags=None, config_path=None, **overrides)#
Initialize and configure the profiling system.
- Parameters:
options (ProfilingOptions, optional) –
A
ProfilingOptionsbag holding any of the settings below, for reuse across calls or construction away from the call site:options = ProfilingOptions(use_likwid=True, file_path="run.h5") ProfileManager.setup(options=options)
An explicit keyword argument passed alongside
optionswins over the same field onoptions, which in turn wins overconfig_pathand the defaults below.config_path (str or os.PathLike, optional) – TOML file containing a
[profiling]table with these settings. Values passed directly tosetup()take precedence. Seeload_profiling_config().auto_finalize (bool, optional) – Finalize once at normal interpreter exit, unless
finalize()is called explicitly first (default: False). This is convenient for scripts that want setup without a surrounding session. Avoid it when MPI ranks may not exit together.output (str, os.PathLike or None, optional) – Direct spelling of the output destination.
Nonekeeps results in memory and writes no file. This is an alias forfile_pathanddeactivate_file_output; do not combine those spellings.replace (bool, optional) – Replace an active configuration (default: False). Without this, accidental setup of an already active manager raises.
**overrides –
Any of the settings below, passed as keyword arguments:
ProfileManager.setup(file_path="run.h5", use_likwid=True)
They are the fields of
ProfilingOptions, which is where they are declared once and typed; an unrecognised name raisesTypeErrornaming the closest match. Prefixed settings can also be given as groups onoptions- seeMemrayOptions,GPUOptionsandHDF5Options.file_path (str, optional) – Path to the output profiling data file (default: “profiling_data.h5”).
label (str or None, optional) –
Short name for this run (default: None, i.e. the output file’s stem). Post-processing uses it wherever a run has to be named – chart legends, the summary heading,
scope-profiler inspect, the JSON statistics - which is what makes several runs distinguishable when they are compared:ProfileManager.setup(file_path="run_a.h5", label="128 ranks")
It is stored in the output file as the
labelmetadata field, so it survives into every later post-processing step.use_likwid (bool, optional) – Enable LIKWID hardware counter collection (default: False).
use_line_profiler (bool, optional) – Enable line-by-line profiling via line_profiler (default: False).
use_memray (bool, optional) – Record process-wide allocations with Memray (default: False). The capture is a separate Memray
.binfile and requires the separately installedmemraypackage.memory_profile_path (str, optional) – Memray capture path (default:
<file-stem>.memray.bin).memray_native_traces (bool, optional) – Memray capture options. Python allocator tracing can create much larger traces and has materially higher overhead.
memray_trace_python_allocators (bool, optional) – Memray capture options. Python allocator tracing can create much larger traces and has materially higher overhead.
memray_follow_fork (bool, optional) – Memray capture options. Python allocator tracing can create much larger traces and has materially higher overhead.
deactivate_profiling (bool, optional) – Turn profiling off entirely (default: False). Every region becomes a no-op, so the instrumentation can stay in the code at near-zero cost instead of being removed.
use_nvtx (bool, optional) – Add NVTX ranges to profiled regions for NVIDIA Nsight tools (default: False). Requires
scope-profiler[nvtx].use_gpu_timing (bool, optional) – Record CUDA-event elapsed device time for each profiled region (default: False). CPU timestamps are still recorded, so the normal timeline remains enqueue-side timing.
gpu_sync_on_exit (bool, optional) – Wait for the current GPU device before recording CPU end time. Requires
use_gpu_timing=True; defaults to False. Applies to contexts and decorators and removes CPU/GPU overlap. Custom backends must providesynchronize()when enabled.gpu_timing_backend (str or object, optional) – CUDA-event backend for
use_gpu_timing:"auto","torch","cupy", or a custom object implementingrecord_event()andelapsed_time_ns(start_event, end_event).deactivate_file_output (bool, optional) –
Write no HDF5 file at all (default: False), not even the run metadata. Use it with
finalize(return_results=True)to analyse a run entirely in memory:ProfileManager.setup(deactivate_file_output=True) ... results = ProfileManager.finalize(return_results=True)
recursive_profile (bool, optional) – Enable recursive profiling for all decorated functions by default (default: False). This can be overridden per decorator with
@ProfileManager.profile(..., recursive=...).aggregation_mode (bool, optional) – Record only count, inclusive total, minimum, maximum, and exclusive total per region. Timeline events are unavailable in this mode; it cannot be combined with line, GPU, NVTX, or LIKWID profiling.
profile_mpi_calls (bool, optional) – Profile mpi4py operations made through predefined and derived communicators (default: False). mpi4py is imported only if the application imports it. With
session(), obtainMPI.COMM_WORLDinside the session so it receives the proxy.track_threads (bool, optional) –
Profile every thread (default: False). Each thread gets its own buffers and scope stack, so regions entered concurrently no longer overwrite one another, and every recorded call carries the thread it ran on. The run also reports each thread’s name, OS ids, lifetime and CPU time - see
threads:with ProfileManager.session(track_threads=True): ...
Cannot be combined with line, GPU, NVTX, LIKWID or aggregation profiling.
track_async (bool, optional) – Follow asyncio tasks and greenlets as well (default: False, and implies
track_threads). Each call additionally carries the task it ran in and the time that task spent suspended inside the call, so awithblock held across anawaitreports its await time rather than charging it to the region. Per-task running and awaiting totals are available fromtasks.metadata_detail ({"minimal", "full"}, optional) – How much of the run’s environment is recorded (default:
"minimal"): versions, platform, CPU model and thread/rank counts, with source files relative to the working directory."full"adds the user name, host name, working directory, loaded modules, environment variables,SLURM_*variables and absolute source paths. Region source text is controlled separately bycapture_region_source.capture_region_source (bool, optional) –
Record where each region is defined - the
withblock or the decorated function - once per distinct source file, the first time any of its regions is created (default: False). Seesource_text. Off by default because the cost, while cheap for a typical file, is not always: it is oneast.parse+ tree walk of that file, so it tracks the file’s total size, not the size or number of the regions in it - under a millisecond for a typical few-hundred- line file, but tenths of a second per rank for one containing thousands of lines across many regions. Every rank pays that independently, so it can compound to whole seconds under contention on a job with more ranks than idle cores (measured: ~0.3s/rank at 8 ranks, ~2.9s/rank at 64, for a single ~10,000-line file, on a shared/oversubscribed node):ProfileManager.setup(capture_region_source=True)
buffer_limit (int, optional) – Initial number of profiling events preallocated per region (default: 1024). Buffers grow on demand, so this is a starting size rather than a limit; raise it for very hot regions to avoid repeated reallocation.
output_mode ({"auto", "direct", "parallel"}, optional) – MPI file writer.
autoprefers MPI-enabled h5py when compatible with the active instrumentation and otherwise lets ranks append directly to one serial-HDF5 file in token order.hdf5_compression ({"gzip", "lzf", "zstd"} or None, optional) – Compression filter for timestamp and GPU-duration datasets.
hdf5_compression_level (int or None, optional) – GZIP level 0–9 or Zstandard level 1–22.
hdf5_chunk_size (int or None, optional) – Maximum events per dataset chunk. Enables chunked partial reads even without compression.
Notes
The run’s start time is the moment
setup()is called; it is stored as thestart_time_nsmetadata field and is the origin of the relative timeline in post-processing. MPI rank detection is separate fromprofile_mpi_calls: collectives are used exactly when the process was started by an MPI launcher. Seescope_profiler.mpi_launchfor detection and itsSCOPE_PROFILER_MPIoverride.
- classmethod session(options=None, *, verbose=True, verbose_line_profiler=False, return_results=False, native_traces=None, output=<object object>, replace=False, config_path=None, **overrides)#
Return a context manager that sets up and finalizes profiling.
Every argument other than
verbose,verbose_line_profiler,return_resultsandnative_tracesis passed tosetup(), with the same meaning and precedence there -options(aProfilingOptions),config_path, and any setting as a keyword:with ProfileManager.session(options=options) as run: ... with ProfileManager.session(file_path="run.h5") as run: ...
Finalization runs even when the profiled block raises; the original exception is preserved.
native_tracesis handed tofinalize(), which folds the output of a C or Fortran library profiled in this same process into this run’s file - in either native format. See its documentation for the two rules that mixing languages imposes.When
return_results=True, the context object exposes the finalizedProfilingResultsasresults:with ProfileManager.session(return_results=True, verbose=False) as run: with ProfileManager.profile_region("solve"): solve() results = run.results
- classmethod set_config(config)#
Set a new profiling configuration and update the region class.
- Parameters:
config (ProfilingConfig) – The new profiling configuration to apply.
- Return type:
None
- classmethod get_config()#
Get the current profiling configuration, creating a default one if
setup()has not been called.This is the only place a configuration comes into being outside
setup(), and it is deliberately lazy: constructing one resolves the MPI communicator, which imports mpi4py and therefore callsMPI_Initin any process the launcher marked as a rank. Doing that at import time would meanimport scope_profilersilently joins the MPI job - fatal in a process forked from a rank, which is exactly what the LIKWID counter read-back does.Resolving defaults is not itself setup. Regions remain disabled until
setup()(directly or throughsession()) installs a config withset_config(). This lets applications declare decorators at import time without accidentally starting a profiling run.- Returns:
The current profiling configuration.
- Return type:
ProfilingOptions#
The settings setup() and session() accept, as an object that can be
built away from the call site and reused across runs. It is also where
each setting is declared once: the keyword arguments of setup() are
its fields.
Settings that share a prefix can be given as groups instead, which spell the same thing without the prefix:
from scope_profiler import HDF5Options, ProfilingOptions
options = ProfilingOptions(
file_path="run.h5",
hdf5=HDF5Options(compression="gzip", compression_level=4),
)
- class scope_profiler.profile_config.ProfilingOptions(file_path=None, label=None, use_likwid=None, perf_events=None, use_line_profiler=None, use_memray=None, memory_profile_path=None, memray_native_traces=None, memray_trace_python_allocators=None, memray_follow_fork=None, deactivate_profiling=None, use_nvtx=None, use_gpu_timing=None, gpu_timing_backend=None, deactivate_file_output=None, recursive_profile=None, aggregation_mode=None, profile_mpi_calls=None, track_threads=None, track_async=None, capture_region_source=None, metadata_detail=None, buffer_limit=None, output_mode=None, hdf5_compression=None, hdf5_compression_level=None, hdf5_chunk_size=None, memray=None, gpu=None, hdf5=None, gpu_sync_on_exit=None)#
A bag of
ProfileManager.setup()settings, to reuse or pass around.Every field mirrors a
setup()/session()keyword argument and defaults toNone, meaning “unset - letsetup()’s own default, or itsconfig_pathfile, decide”. Pass one tosetup()orsession()instead of repeating the same handful of keyword arguments at every call site:options = ProfilingOptions(use_likwid=True, file_path="run.h5") ProfileManager.setup(options=options) # ... or, equivalently: with ProfileManager.session(options=options): ...
An explicit keyword argument passed alongside
optionsstill wins over the same field onoptions, so a sharedProfilingOptionscan be reused across runs with one-off overrides:ProfileManager.setup(options=options, file_path="run_b.h5")
Settings that share a prefix can also be given as groups –
MemrayOptions,GPUOptionsandHDF5Options– which spell the same settings without the prefix:options = ProfilingOptions( file_path="run.h5", hdf5=HDF5Options(compression="gzip", compression_level=4), )
Setting one thing both ways raises
ValueErrorrather than picking a winner.- Parameters:
file_path (str | None)
label (str | None)
use_likwid (bool | None)
use_line_profiler (bool | None)
use_memray (bool | None)
memory_profile_path (str | None)
memray_native_traces (bool | None)
memray_trace_python_allocators (bool | None)
memray_follow_fork (bool | None)
deactivate_profiling (bool | None)
use_nvtx (bool | None)
use_gpu_timing (bool | None)
gpu_timing_backend (Any)
deactivate_file_output (bool | None)
recursive_profile (bool | None)
aggregation_mode (bool | None)
profile_mpi_calls (bool | None)
track_threads (bool | None)
track_async (bool | None)
capture_region_source (bool | None)
metadata_detail (str | None)
buffer_limit (int | None)
output_mode (str | None)
hdf5_compression (str | None)
hdf5_compression_level (int | None)
hdf5_chunk_size (int | None)
memray (MemrayOptions | None)
gpu (GPUOptions | None)
hdf5 (HDF5Options | None)
gpu_sync_on_exit (bool | None)
- label#
Short name for this run (default: None, i.e. the output file’s stem). Post-processing uses it wherever a run has to be named – chart legends, the summary heading,
scope-profiler inspect, the JSON statistics - and it is stored in the output file as thelabelmetadata field, so it survives into every later post-processing step.- Type:
str or None
- perf_events#
Linux
perf_event_openevents to collect per region (default: None).- Type:
sequence of str or None
- use_line_profiler#
Enable line-by-line profiling via line_profiler (default: False).
- Type:
bool or None
- deactivate_profiling#
Turn profiling off entirely (
setup()default: False). Every region becomes a no-op, so the instrumentation can stay in the code at near-zero cost instead of being removed.- Type:
bool or None
- use_nvtx#
Add NVTX ranges to profiled regions for NVIDIA Nsight tools (default: False). Requires
scope-profiler[nvtx].- Type:
bool or None
- use_gpu_timing#
Record CUDA-event elapsed device time for each profiled region (default: False). CPU timestamps are still recorded, so the normal timeline remains enqueue-side timing.
- Type:
bool or None
- gpu_sync_on_exit#
Wait for the GPU device at region exit. Requires
use_gpu_timingand removes CPU/GPU overlap; disabled by default.- Type:
bool or None
- gpu_timing_backend#
CUDA-event backend for
use_gpu_timing:"auto","torch","cupy", or a custom object implementingrecord_event()andelapsed_time_ns(start_event, end_event)(default:"auto").
- deactivate_file_output#
Write no HDF5 file at all (default: False), not even the run metadata. Pair it with
finalize(return_results=True)to analyse a run entirely in memory.- Type:
bool or None
- recursive_profile#
Enable recursive profiling for all decorated functions by default (default: False). Overridable per decorator with
@ProfileManager.profile(..., recursive=...).- Type:
bool or None
- aggregation_mode#
Record only count, inclusive total, minimum, maximum, and exclusive total per region (default: False). Timeline events are unavailable in this mode; it cannot be combined with line, GPU, NVTX, or LIKWID profiling.
- Type:
bool or None
- profile_mpi_calls#
Profile mpi4py operations made through predefined or derived communicators (default: False). mpi4py remains lazily imported.
- Type:
bool or None
- track_threads#
Record which thread each call ran on, and describe every thread the run touched (default: False). Required for correct results from concurrently used regions; see
ThreadedProfileRegion.- Type:
bool or None
- track_async#
Also record which asyncio task or greenlet each call ran in, and how much of the call was spent awaiting (default: False). Implies
track_threads.- Type:
bool or None
- capture_region_source#
Record where each region is defined - the
withblock or the decorated function - once per distinct source file, the first time any of its regions is created (default: False). Seesource_text. Off by default: the cost tracks that file’s total size (oneast.parse+ tree walk), so it stays under a millisecond for a typical file but can reach tenths of a second per rank for a single file with thousands of lines, paid independently by every rank.- Type:
bool or None
- metadata_detail#
How much of the run’s environment is recorded (default:
"minimal")."minimal"stores versions, platform, CPU model and thread/rank counts, and each region’s and line profile’s source file relative to the working directory (or by name alone, outside it)."full"adds the user name, host name, working directory, loaded modules, environment variables such asPATHandVIRTUAL_ENV,SLURM_*variables and absolute source paths; opt into it for runs that stay on the machine they were recorded on.- Type:
{“minimal”, “full”} or None
- buffer_limit#
Initial number of profiling events preallocated per region (default: 1024). Buffers grow on demand, so this is a starting size rather than a limit; raise it for very hot regions to avoid repeated reallocation.
- Type:
int or None
- output_mode#
MPI file writer:
"auto","direct", or"parallel"(default:"auto").autoprefers MPI-enabled h5py when compatible with the active instrumentation and otherwise lets ranks append directly to one serial-HDF5 file in token order.- Type:
str or None
- hdf5_compression#
Compression filter for timestamp and GPU-duration datasets:
None,"auto","gzip","lzf", or"zstd"(default: None)."auto"compresses at publication time, and only when the run is large enough for the saving to repay the write CPU.- Type:
str or None
- hdf5_chunk_size#
Maximum events per dataset chunk (default: None). Enables chunked partial reads even without compression.
- Type:
int or None
- memray#
The
use_memray/memory_profile_path/memray_*settings as one group.- Type:
MemrayOptions or None
- gpu#
The
use_gpu_timing/gpu_timing_backend/use_nvtxsettings as one group.- Type:
GPUOptions or None
- hdf5#
The
hdf5_*settings as one group.- Type:
HDF5Options or None
- class scope_profiler.profile_config.MemrayOptions(enabled=None, path=None, native_traces=None, trace_python_allocators=None, follow_fork=None)#
Memray capture settings, as a group.
ProfilingOptions(memray=MemrayOptions(enabled=True, native_traces=True))configures the same run asProfilingOptions(use_memray=True, memray_native_traces=True); use whichever reads better at the call site. In a TOML config file the group is the[profiling.memray]table.- Parameters:
- class scope_profiler.profile_config.GPUOptions(timing=None, backend=None, nvtx=None, sync_on_exit=None)#
GPU timing and NVTX settings, as a group.
In a TOML config file this is the
[profiling.gpu]table.- backend#
CUDA-event backend (
gpu_timing_backend):"auto","torch","cupy", or an object implementingrecord_event()andelapsed_time_ns(start_event, end_event).
- class scope_profiler.profile_config.HDF5Options(compression=None, compression_level=None, chunk_size=None)#
HDF5 storage settings, as a group.
In a TOML config file this is the
[profiling.hdf5]table.- compression#
Compression filter for timestamp and GPU-duration datasets (
hdf5_compression):"gzip","lzf","zstd", or"auto"to compress only runs large enough to repay the write CPU.- Type:
str or None
- scope_profiler.profile_config.load_profiling_config(path)#
Load the
[profiling]table from a TOML settings file.A top-level table is also accepted for small files. Paths in the file retain TOML’s normal meaning and are interpreted relative to the current working directory, just like paths passed to
ProfileManager.setup().Settings may be written flat, or grouped into the
memray,gpuandhdf5sub-tables that mirrorMemrayOptions,GPUOptionsandHDF5Options:[profiling] file_path = "run.h5" [profiling.hdf5] compression = "gzip" compression_level = 4
Setting the same thing both ways is an error, not a silent precedence rule.
ProfilingConfig#
Holds one manager’s profiling configuration. Normally you interact with
it through ProfileManager.setup(), but you can also construct one
directly for advanced use cases.
- class scope_profiler.profile_config.ProfilingConfig(file_path='profiling_data.h5', label=None, use_likwid=False, perf_events=None, use_line_profiler=False, use_memray=False, memory_profile_path=None, memray_native_traces=False, memray_trace_python_allocators=False, memray_follow_fork=False, deactivate_profiling=False, use_nvtx=False, use_gpu_timing=False, gpu_timing_backend='auto', deactivate_file_output=False, recursive_profile=False, aggregation_mode=False, profile_mpi_calls=False, track_threads=False, track_async=False, capture_region_source=False, metadata_detail='minimal', buffer_limit=1024, output_mode='auto', hdf5_compression=None, hdf5_compression_level=None, hdf5_chunk_size=None, gpu_sync_on_exit=False)#
Configuration for one profiling manager.
This class centralizes configuration for LIKWID performance counters, buffer limits, and file paths. Each manager owns a separate instance, so independently configured profiling sessions can coexist. Constructing it is purely local: it reads the communicator for rank and size but issues no MPI call of its own, so
setup()does not have to be collective.- Parameters:
file_path (str)
label (str | None)
use_likwid (bool)
use_line_profiler (bool)
use_memray (bool)
memory_profile_path (str | None)
memray_native_traces (bool)
memray_trace_python_allocators (bool)
memray_follow_fork (bool)
deactivate_profiling (bool)
use_nvtx (bool)
use_gpu_timing (bool)
deactivate_file_output (bool)
recursive_profile (bool)
aggregation_mode (bool)
profile_mpi_calls (bool)
track_threads (bool)
track_async (bool)
capture_region_source (bool)
metadata_detail (str)
buffer_limit (int)
output_mode (str)
hdf5_compression (str | None)
hdf5_compression_level (int | None)
hdf5_chunk_size (int | None)
gpu_sync_on_exit (bool)
- classmethod reset()#
Compatibility no-op retained for callers of older releases.
Configurations are ordinary per-manager objects now, so there is no process-wide instance to reset.
- pylikwid_markerinit()#
Initialize LIKWID markers if LIKWID is enabled.
- stop_memory_profiling()#
Finish Memray’s process-wide allocation capture, if enabled.
- Return type:
None
- pylikwid_markerclose()#
Close LIKWID markers to finalize measurement regions.
Idempotent: repeated calls (a second
finalize(), say) do nothing.
- collect_likwid_results(region_names)#
Close the LIKWID markers and return the run’s counter results.
Tries three sources, richest first, so a host where LIKWID cannot do the fancy parts still ends up with real numbers in the HDF5 file:
the perfmon read-back, run in a subprocess because it can crash the interpreter outright on hosts that cannot really count;
LIKWID’s marker file, parsed directly — real values, but no event names or derived metrics;
a marker-API snapshot taken before the markers were closed, for when there is no marker file at all.
See
scope_profiler.likwid_datafor why the first one is fenced off.- Parameters:
region_names (iterable of str) – Region names to snapshot via the marker API.
- Returns:
Empty when LIKWID is disabled, the process was not started under
likwid-perfctr -m/likwid-mpirun -marker, or the markers were already collected.- Return type:
list of scope_profiler.likwid_data.LikwidRegionResult
Notes
A process can only do this once. Closing the markers tears the marker API down, and querying it afterwards crashes the interpreter rather than raising, so a second
finalize()returns nothing instead of reading again. The counters of the whole process therefore end up in the file written by the firstfinalize().
- property perf_events: tuple[str, ...]#
Requested Linux perf event names, or an empty tuple when disabled.
- property memory_profile_path: Path | None#
Memray capture path, or None when allocation profiling is off.
- property gpu_sync_on_exit: bool#
Whether GPU regions wait for the device before recording CPU end time.
- property gpu_timing_backend#
CUDA-event timing backend selector or backend object.
- property recursive_profile: bool#
Return whether recursive decorator profiling is enabled by default.
- property capture_region_source: bool#
Return whether a region’s defining source is captured at creation.
- property tracker#
This run’s
ConcurrencyTracker.None unless
track_threadsis set, which is what keeps the default configuration free of any thread, asyncio or greenlet hook.
Region classes#
BaseProfileRegion#
- class scope_profiler.region_profiler.BaseProfileRegion(region_name, config, tags=())#
Base class providing shared profiling logic.
Handles start/end time buffering and call counting. The buffers grow on demand and are copied out once, at the end of the run, by
ProfileManager.finalize()- regions never touch HDF5 themselves.- Parameters:
region_name (str)
config (ProfilingConfig)
- region_name#
- config#
- tags#
- ptr#
- buffer_limit#
- capacity#
- start_times#
- end_times#
- source_file#
- source_lineno#
- source_text#
- set_source(filename, lineno, text)#
Record where this region is defined, the first time it is called.
First writer wins: a region name reused at more than one call site keeps only the first location, matching how their timings are already pooled together under one name (see issue #161).
- Return type:
None
- wrap(func)#
Wrap a function for profiling.
Subclasses must override this method to implement the appropriate profiling behavior.
- append(start, end)#
Append a start/end time pair to the buffer, growing it if needed.
- property num_calls: int#
Times this region was entered, for the lifetime of the process.
Derived rather than counted: the slots in use (
ptr) plus whatever earlier finalize() calls already copied out. Keeping it out of__enter__removes an attribute write from every recorded call.
- get_durations_numpy()#
Return durations (end - start) for buffered entries as a NumPy array.
- Return type:
- open_slots()#
Buffer slots whose call is still running, in ascending order.
A slot is open until its end time is written, which is what distinguishes a call in flight from one that has returned. Both region forms are covered: the context manager writes its start on entry, the decorator writes nothing until the call returns.
- Return type:
- closed_slots()#
Mask of buffered slots this finalize() should copy out.
A slot qualifies once its end time is written and it has not already gone out with an earlier finalize(). None - the usual case - means every slot qualifies and the caller can skip the mask entirely.
- Return type:
ndarray | None
- mark_written()#
Record that everything buffered so far has been handed to finalize().
Called by
finalize()once the data has been copied out, so that a second run in the same process reports only its own events instead of re-reporting the first run’s. The timestamp buffer rewinds (the arrays are reused; anything pastptris unread scratch), whilenum_callskeeps counting for the lifetime of the process — it is the in-memory view of the region, which callers inspect afterfinalize().A call still running has a slot reserved that finalize() did not copy out, so it must survive the rewind. It is moved to the front of the buffer rather than pinning everything behind it: leaving the whole buffer in place would make the next finalize() re-report every completed call sitting below it, with a second set of call ids.
The one case that still cannot rewind is a call entered through the decorator form, which keeps its slot index in the wrapper’s own frame where nothing can remap it. Recognisable because such a slot is open without appearing in
_scope_ptr_stack. There the buffer stays put and the slots already copied out are recorded instead, so the next finalize() skips them rather than reporting those calls again.- Return type:
None
- add_function(func)#
Register a function for profiling. No-op except in LineProfilerRegion.
- Return type:
None
DisabledProfileRegion#
- class scope_profiler.region_profiler.DisabledProfileRegion(region_name, config, tags=())#
Profiling region that performs no measurements.
Used when profiling is disabled but code paths must remain valid.
- Parameters:
region_name (str)
config (ProfilingConfig)
- wrap(func)#
Return the original function unchanged — no wrapper, no overhead.
- append(start, end)#
Ignored: no data recorded.
- get_durations_numpy()#
Return an empty array since nothing is recorded.
- buffer_limit#
- capacity#
- config#
- end_times#
- ptr#
- region_name#
- source_file#
- source_lineno#
- source_text#
- start_times#
- tags#
TimeOnlyProfileRegion#
- class scope_profiler.region_profiler.TimeOnlyProfileRegion(region_name, config, tags=())#
Region that records timing, collected once at the end of the run.
- Parameters:
region_name (str)
config (ProfilingConfig)
- wrap(func)#
Wrap a function to measure its execution time.
- buffer_limit#
- capacity#
- config#
- end_times#
- ptr#
- region_name#
- source_file#
- source_lineno#
- source_text#
- start_times#
- tags#
FullProfileRegion#
- class scope_profiler.region_profiler.FullProfileRegion(region_name, config, tags=())#
Region that records both timing and LIKWID metrics, and writes to HDF5.
This is the most complete profiling mode: users obtain LIKWID markers and nanosecond-resolution timing.
- Parameters:
region_name (str)
config (ProfilingConfig)
- likwid_marker_start#
- likwid_marker_stop#
- wrap(func)#
Wrap a function to measure time and collect LIKWID metrics.
LineProfilerRegion#
- class scope_profiler.region_profiler.LineProfilerRegion(region_name, config, tags=())#
Region that records timing and line-by-line profiling via line_profiler.
Uses line_profiler to collect per-line execution statistics for decorated functions. Also records nanosecond timestamps.
Line-by-line profiling is most useful with the decorator (
wrap) path, which automatically registers the function with the line profiler. When used as a context manager, the profiler is enabled/disabled around the block - any functions previously added via the decorator path will be profiled while the context is active.- Parameters:
region_name (str)
config (ProfilingConfig)
- wrap(func)#
Wrap a function to measure execution time and collect line-by-line stats.
- enter_frame(frame)#
Enter this region while registering
framewith line_profiler.
- enter_timing_only()#
Enter this region without registering or enabling line_profiler.
- record_line_timing(frame, lineno, duration_ns)#
Record one line timing sample from recursive CLI tracing.
- manual_line_records(unit=1e-09)#
Return manually traced line timings in the persisted record shape.
- add_function(func)#
Register a function for line-by-line profiling.
- Return type:
None
- print_stats()#
Print line-by-line profiling statistics.
- get_stats()#
Return the line_profiler stats object.
ThreadedProfileRegion#
Selected by track_threads=True; see Threads, tasks and
coroutines.
- class scope_profiler.region_profiler.ThreadedProfileRegion(region_name, config, tags=())#
Region that records which thread - and which task - each call ran on.
Selected by
track_threads=True. Two things change againstTimeOnlyProfileRegion:every thread gets its own buffers and its own scope stack, so concurrent calls no longer overwrite each other’s reserved slots or close each other’s scopes;
each recorded call carries the lane it ran on, so the nesting reconstruction can group calls into stacks that really are stacks (see
lane_ids()).
With
track_async=Truea call additionally carries the id of the asyncio task or greenlet it ran in, and the time that task spent suspended inside the call - the await time of that one call, as opposed to the task’s total.The per-call cost over
TimeOnlyProfileRegionis one thread-local lookup plus, in async mode, two integer reads off the current task record. Nothing here allocates or locks per call.- Parameters:
region_name (str)
config (ProfilingConfig)
- wrap(func)#
Wrap a function so its calls are recorded on the caller’s thread.
- snapshot_arrays()#
This run’s completed calls, concatenated over every thread.
- Returns:
(start_times, end_times, None, None, None, thread_ids, task_ids, await_ns)in nanoseconds, laid out on the snapshot tupleProfileManager._snapshot_regions()documents, or None when this region recorded nothing since the lastfinalize().task_idsandawait_nsare None unless async tracking is on.- Return type:
tuple or None
- closed_slots()#
Not meaningful per lane;
snapshot_arrays()applies the mask.None, as the base class means it: every slot qualifies, and there is no mask for a caller to apply. A threaded region keeps no buffers of its own, so there is nothing here to mask in the first place.
- mark_written()#
Rewind every lane, keeping calls that are still running.
- Return type:
None
Threads, tasks and coroutines#
A run profiled with track_threads describes the lanes its calls ran
on. The records below are what ProfilingResults.threads and
ProfilingResults.tasks hand back.
- class scope_profiler.concurrency.ThreadInfo(index, rank, name, ident, native_id, daemon, start_ns, end_ns, cpu_ns)#
One row of a run’s thread table, as post-processing sees it.
The read-back counterpart of
ThreadRecord: same fields, but durations in seconds and timestamps relative to the start of the run, matching every other time reported byProfilingResults.- Parameters:
- class scope_profiler.concurrency.TaskInfo(index, rank, kind, name, coro_name, thread_index, created_ns, done_ns, steps, running_ns, suspended_ns)#
One row of a run’s task table: an asyncio task or a greenlet.
running_timeandawaiting_timesplit the lane’s wall time into the part it held its thread and the part it did not, which is the numberawait-heavy code is usually looking for.- Parameters:
- class scope_profiler.concurrency.ConcurrencyTracker(track_async=False)#
Per-run registry of threads, asyncio tasks and greenlets.
One tracker belongs to one
ProfilingConfig, so two simultaneously active managers never share lane numbering. Creating it is free;install()is what puts the (few, cheap) hooks in place, anduninstall()takes them out again.- Parameters:
track_async (bool)
- current_thread()#
This thread’s record, registering it on first use.
On the region hot path. The common case is one failed-then-cached attribute lookup on a
threading.local, which is why the registry itself is never consulted here.- Return type:
ThreadRecord
- install()#
Put the thread-start, asyncio and greenlet hooks in place.
Idempotent, so a second
setup()on the same configuration does not stack wrappers.- Return type:
None
- uninstall()#
Remove every hook and restore what was there before.
- Return type:
None
- instrument_loop(loop)#
Route
loop’s task creation through the step timer.Public because a loop implementation that never reaches
BaseEventLoop.run_forever(uvloop, say) can be handed here directly. Chains onto whatever task factory the application already installed instead of replacing it.Instrumented once per loop, and keyed on the loop rather than on whichever factory is currently in place: an application that installs its own factory after the loop has started displaces ours, and re-wrapping to get back on top would make the two call each other forever, since its captured “previous” is us. Such a loop keeps recording regions and thread ids; the tasks created after the swap simply have no lane of their own.
- Return type:
None
- snapshot(origin_ns=0)#
The lane tables, as columns, ready to be written or returned.
Timestamps are shifted by
origin_ns(the run’s start), matching what the event columns store, so a thread’s lifetime and the calls on it share one timeline.UNKNOWNentries are left alone.
- scope_profiler.concurrency.lane_ids(thread_ids, task_ids)#
One id per call identifying the stack it belongs to.
A call belongs to its task when it ran inside one and to its bare thread otherwise. The two id spaces are disjoint by construction: thread lanes are mapped to
-2 - thread, task lanes stay non-negative, and-1is left for a call whose thread is unknown - a region recorded by the Fortran API, say, folded into an otherwise thread-aware run. This is the keybuild_call_arrays()groups by, since only calls sharing a lane can nest.
Post-processing#
Everything in this section is importable from the package root:
from scope_profiler import (
build_call_arrays,
build_call_stack,
load,
read_h5,
read_h5_summary,
read_json,
read_profile,
plot_flame_chart,
plot_gantt,
write_region_statistics_json,
)
All durations and timestamps exposed here are in seconds; the HDF5 file stores nanoseconds.
ProfilingResults#
The post-processing API itself, and the only type the analysis layer
has. ProfilingResults.from_h5() builds one from a merged file, and
ProfileManager.finalize(return_results=True) builds the same thing
straight from memory.
- class scope_profiler.results.ProfilingResults(regions, metadata=None, num_ranks=None, likwid=None, perf_events=None, line_profile=None, file_path='', is_root=True, exclusive_totals=None, event_data_available=True, threads=None, tasks=None)#
The profiling data of one run, as an ordered mapping of region name to
MPIRegion:results = ProfileManager.finalize(return_results=True) for region in results: print(region.name, region.total_duration) solve = results["solve"] # same as results.get_region("solve") solve[0].average_duration # rank 0, in seconds
The same object comes back from a merged output file, via
from_h5()(or its module-level twinread_h5()):results = ProfilingResults.from_h5("profiling_data.h5")
All durations are reported in seconds.
- Parameters:
- classmethod from_h5(file_path, verbose=False)#
Load a merged profiling file written by
ProfileManager.finalize:results = ProfilingResults.from_h5("profiling_data.h5") results.print_summary()
scope_profiler.read_h5()is the same thing under a shorter name.- Parameters:
- Returns:
The run’s profiling data, of whichever class this was called on.
- Return type:
- Raises:
FileNotFoundError – If the specified HDF5 file does not exist.
- get_region(region_name)#
Retrieve profiling data for a specific region.
- summary(include=None, exclude=None)#
Summarize every region, aggregated over ranks.
- Parameters:
include (list of str or str, optional) – Regex patterns selecting which regions to summarize, matched as in
get_regions().exclude (list of str or str, optional) – Regex patterns selecting which regions to summarize, matched as in
get_regions().
- Returns:
One dict per region (see
get_summary()), ordered by first start time.inclusive_durationincludes nested regions;exclusive_durationexcludes them. Durations are in seconds.- Return type:
List[dict]
- to_dataframe(include=None, exclude=None, per_rank=False)#
Return the region summaries as a pandas DataFrame.
- Parameters:
include (list of str or str, optional) – Regex patterns selecting which regions to include, matched as in
get_regions().exclude (list of str or str, optional) – Regex patterns selecting which regions to include, matched as in
get_regions().per_rank (bool, optional) – If True, emit one row per (region, rank) with a
rankcolumn instead of one aggregated row per region (default: False).
- Returns:
Region statistics, with durations in seconds.
- Return type:
pandas.DataFrame
- Raises:
ImportError – If pandas is not installed.
- events(include=None, exclude=None, ranks=None, relative=True, origin=None)#
Return one dict per recorded call, across all regions and ranks.
This is the long-form (“tidy”) view to build custom plots from: one row per call rather than one per region.
- Parameters:
include (list of str or str, optional) – Regex patterns selecting which regions to include, matched as in
get_regions().exclude (list of str or str, optional) – Regex patterns selecting which regions to include, matched as in
get_regions().ranks (list of int or int, optional) – Restrict to these ranks (default: all).
relative (bool, optional) – If True (default), timestamps are measured from
time_origin— the start time registered byProfileManager.setup(), or the first region entry for runs without one — so the timeline starts at zero. If False, the raw monotonic-clock timestamps are returned; those are only comparable within a single run.origin (float, optional) – Measure from this timestamp instead, in seconds on the recording clock. Overrides
relative; passorigin=results.minimum_start_timeto zero the timeline on the first region entry regardless of what the run registered.
- Returns:
Entries with keys
name,rank,call_index,start,endandduration, in seconds, ordered by region (as inget_regions()) then rank then call order. Entries also carrygpu_durationwhen CUDA-event timing was enabled for that region.- Return type:
List[dict]
Examples
>>> for event in results.events(include="solve"): ... print(event["rank"], event["start"], event["duration"])
- to_events_dataframe(include=None, exclude=None, ranks=None, relative=True, origin=None)#
Return every recorded call as a pandas DataFrame (one row per call).
- Parameters:
- Returns:
Columns
name,rank,call_index,start,endandduration, with times in seconds. Agpu_durationcolumn is included when any selected event has CUDA-event timing.- Return type:
pandas.DataFrame
- Raises:
ImportError – If pandas is not installed.
- call_stack(rank=0, include=None, exclude=None, relative=True, origin=None)#
Reconstruct the nested call stack for one rank.
Regions record no call graph, so nesting is recovered from timestamp containment - the same reconstruction the flame chart, the
.profexport and the speedscope export use.- Parameters:
rank (int, optional) – Rank whose calls to reconstruct (default: 0).
include (list of str or str, optional) – Regex patterns selecting which regions to include, matched as in
get_regions().exclude (list of str or str, optional) – Regex patterns selecting which regions to include, matched as in
get_regions().relative (bool, optional) – If True (default), timestamps start at zero; see
events().origin (float, optional) – Measure from this timestamp instead; see
events().
- Returns:
One entry per call, parents before children, with keys
call_id,name,start,end,duration,depthandparent(the enclosing call’s id, or None). Seebuild_call_stack().- Return type:
List[dict]
- call_graph(rank=0, include=None, exclude=None)#
Return call relationships without timestamps or durations.
New profiles persist
call_idandparent_idfor every Python event. Legacy profiles fall back to the timestamp-based call stack. The returned nodes contain onlycall_id,parent_id,name,call_indexanddepth.Filtering renests: a call whose parent was excluded is reported under its nearest surviving ancestor, with the depth that implies, exactly as
call_stack()does. Leaving the excluded id inparent_idwould hand back a graph with edges pointing at nodes that are not in it.call_idis unique within this rank, not across the file - each rank numbers its own calls.
- print_summary(include=None, exclude=None, ranks=None, sort='start', title=None, stream=None, suppress_notes=False, columns=None, percentage_mode='coverage')#
Print a region summary table, aggregated over ranks.
Renders the same table as
scope-profiler inspectand the summary printed byProfileManager.finalize().- Parameters:
include (list of str or str, optional) – Regex patterns selecting which regions to print, matched as in
get_regions().exclude (list of str or str, optional) – Regex patterns selecting which regions to print, matched as in
get_regions().ranks (list of int, optional) – Restrict the statistics to these ranks (default: all).
sort (str, optional) – Column to order by:
start(default),total,calls,avg,min,max,stdorname.title (str, optional) – Heading above the table (default: the file path and rank count).
stream (file-like, optional) – Where to write (default: stdout).
columns (list of str or str, optional) – Region summary columns to print. Defaults to
region,calls,percent,parent_percent,totalandavg.percentis relative toscope_profiler.session;parent_percentis relative to the enclosing call path. Useregionfor the region-name column.percentage_mode ({"coverage", "exclusive"}, optional) – Quantity used for
% session. Defaults to wall-clock coverage; useexclusiveto attribute time after nested regions.suppress_notes (bool)
- Return type:
None
Notes
Does nothing on a non-root rank (see
is_root), so a parallel script can call it unguarded and print the table once.
- default_title()#
Heading naming this run, for a summary table.
The label leads when the run has one, since that is the name the user chose; the file path follows either way, because when several runs are being compared it is what tells them apart on disk.
- Returns:
e.g.
"results/run_a.h5 (128 ranks)".- Return type:
- property file_path: Path#
The run’s output file.
- Returns:
The file path as a pathlib.Path object. For results taken straight from memory this is the path
setup()was configured with, which need not exist on disk.- Return type:
Path
- property label: str | None#
The run’s label, as given to
ProfileManager.setup(label=...).- Returns:
The label, or None for a run that was not given one. Use
display_labelto name the run in output regardless.- Return type:
str or None
- property display_label: str#
What to call this run in charts, tables and exports.
The
labelwhen the run has one, and otherwise the stem of its output file — which is what post-processing named runs by before labels existed, and remains the default.- Returns:
A non-empty name for the run.
- Return type:
- property is_root: bool#
Whether this rank holds the run’s data.
Always True for a file that was read back, and for serial runs. Under MPI,
finalize(return_results=True)gathers everything on rank 0, so only rank 0’s results are the root ones; the others come back empty and with this False.Everything that produces output -
print_summary(), theplot_*functions, the exporters - does nothing for non-root results. That is what lets a parallel script call them unguarded and still write each figure exactly once, from rank 0. Read it when a script needs to make the same distinction for output of its own.- Returns:
True unless this is a non-root rank’s share of an MPI run.
- Return type:
- property metadata: dict#
Get environment metadata for the run (gathered from rank 0).
- Returns:
Metadata dict (hostname, OpenMP thread count, platform, versions, etc.), or an empty dict if the run recorded none.
- Return type:
- property threads: dict[int, list]#
Rank -> the threads that recorded calls, in registration order.
Each entry is a
ThreadInfo: name, OS ids, when the thread started and ended, and the CPU time it burned. Empty unless the run profiled withtrack_threads=True:for rank, threads in results.threads.items(): for thread in threads: print(rank, thread.name, thread.cpu_time)
The per-call
thread_idscolumn of every region indexes into this rank’s list, soresults["solve"][0].for_thread(2)is the part ofsolvethat ran onresults.threads[0][2].
- property tasks: dict[int, list]#
Rank -> the asyncio tasks and greenlets the run followed.
Each entry is a
TaskInfo, whoserunning_timeandawaiting_timesplit the lane’s life into the part it held a thread and the part it was suspended. Empty unless the run profiled withtrack_async=True.
- lane_label(lane, rank=0)#
A human-readable name for one lane of
rank.Lanes are the stacks a rank’s calls were reconstructed into (see
lane_ids()). This turns one back into something worth putting on a chart or a speedscope profile selector: a task’s own name where the lane is a task, the thread’s name where it is a bare thread.
- thread_summary(rank=0)#
Per-thread wall time, CPU time and call count for one rank.
Wall and CPU time come from the thread table; the call count and the time attributed to regions come from the events, so a thread that burned CPU outside every profiled region shows the gap directly.
- Returns:
One entry per thread with keys
index,name,alive,wall_time,cpu_time,num_callsandregion_time(the summed inclusive duration of that thread’s top-level calls, so nested regions are not counted twice), in thread-table order. Empty when the run did not track threads.- Return type:
- Parameters:
rank (int)
- property line_profile: dict[int, list]#
Persisted line-profiler records keyed by rank.
Each record contains
region,filename,function,first_lineno,line_numbers,hits,timesandunit. The elapsed seconds for a line aretimes * unit.
- property num_ranks: int#
Get the number of ranks recorded in the profiling data.
- Returns:
Number of ranks.
- Return type:
- property num_nodes: int | None#
Number of distinct hosts the run’s ranks ran on, or None if unknown.
Read from the
num_nodesmetadata field. Files written before it was recorded still answer for a single-rank run, which can only have used one node; a multi-rank run without the field returns None.
- get_likwid_regions(rank=None)#
Get the LIKWID marker results of the run.
- Parameters:
rank (int, optional) – Return only this rank’s regions. By default every rank is included, keyed by rank.
- Returns:
With
rankgiven, a mapping of region tag toLikwidRegionResult; otherwise a mapping of rank to such a dict. Empty when the run did not use LIKWID.- Return type:
Examples
results = read_h5("profiling_data.h5") for rank, regions in results.get_likwid_regions().items(): for tag, result in regions.items(): for name, values in zip(result.metric_names, result.metrics): print(rank, tag, name, values)
- get_perf_events(rank=None)#
Return aggregated Linux
perf_event_opencounts by region.
- get_likwid_region(tag, rank=0)#
Get one region’s LIKWID results for a single rank.
- Parameters:
- Returns:
The region’s counters, event names and derived metrics.
- Return type:
LikwidRegionResult
- Raises:
KeyError – If the rank recorded no LIKWID data or has no such region.
- likwid_to_dataframe()#
Return every LIKWID event and metric as a tidy pandas DataFrame.
One row per (rank, region, hardware thread), with a column per event and per derived metric, plus the region’s LIKWID runtime and call count. Regions measured with different event groups simply leave the other group’s columns empty.
- Returns:
Empty if there is no LIKWID data.
- Return type:
pandas.DataFrame
- Raises:
ImportError – If pandas is not installed.
- print_likwid_summary(stream=None)#
Print every LIKWID region’s events and derived metrics.
- Parameters:
stream (file-like, optional) – Destination (default: stdout).
- Return type:
None
- property minimum_start_time: float#
Get the minimum start time across all regions and ranks.
This is the origin of the timeline: subtract it from any timestamp to get seconds since the first region entry. Regions with no recorded calls are ignored; the result is 0.0 if no region recorded any.
- Returns:
Minimum start time in seconds.
- Return type:
- property run_start_time: float | None#
When the run started, in seconds on the recording clock.
This is the
start_time_nsmetadata field, written byProfileManager.setup()— by default the moment setup() was called, or an earlier instant if one was passed to it.- Returns:
The registered start time, or None for runs without one (older files, or runs that never called setup()). Use
startup_timefor the elapsed time before the first region, which stays defined either way.- Return type:
float or None
- property time_origin: float#
Zero point of the relative timeline, in seconds.
The registered
run_start_timewhen there is one, and otherwise the first region entry (minimum_start_time). This is whatevents()andcall_stack()measure from.Note the
plot_*functions instead frame their x axis on the first region entry, so that a long gap betweensetup()and the first region does not fill a chart with empty space. Passorigin=results.minimum_start_timetoevents()to reproduce the numbers on a chart’s axis.- Returns:
Origin timestamp on the recording clock.
- Return type:
- property startup_time: float#
Seconds between the start of the run and the first profiled region.
Time the instrumentation never saw: imports, reading input, building a mesh. Zero when there is no registered start time (
run_start_timeis None), since the run is then only known from its first region onwards.- Returns:
Elapsed time before the first region entry, in seconds.
- Return type:
- property maximum_end_time: float#
Get the maximum end time across all regions and ranks.
- Returns:
Maximum end time in seconds, or 0.0 if no region recorded timing.
- Return type:
- property time_span: float#
Wall-clock seconds between the first region entry and the last exit.
- Returns:
Duration of the profiled window in seconds, or 0.0 if no region recorded timing.
- Return type:
- property finalize_time: float | None#
When
finalize()was called, in seconds on the recording clock.This is the
finalize_time_nsmetadata field, read as the first thingProfileManager.finalize()does - before it spends any time collecting or writing the run’s data, so it marks the moment finalize() was reached rather than the moment it returned.- Returns:
The registered finalize time, or None for runs without one (older files, or a run that never reached finalize()).
- Return type:
float or None
- property total_time: float | None#
Wall-clock seconds from
setup()tofinalize().Unlike
time_span(first region entry to last exit), this covers the whole instrumented program: startup work before the first region, gaps between regions, and any teardown after the last one but beforefinalize()is called - the number to report as “how long did the run take” alongside the region breakdown.- Returns:
finalize_timeminusrun_start_time, or None if either is missing - an older file, or a run that set up profiling (or called finalize) some other way thanProfileManager.setup()/finalize().- Return type:
float or None
load#
One entry point for reading a run back, whatever format it is in. The format comes from the file’s contents rather than its name, so a caller that just has a path does not have to know which reader it wanted.
- scope_profiler.profile_io.load(path, verbose=False)#
Load a profile for post-processing, whatever format it is in.
The one entry point for reading a run back:
import scope_profiler results = scope_profiler.load("profiling_data.h5") results.print_summary()
HDF5 and JSON profiles are both accepted, and the format is taken from the file’s contents rather than its name (see
sniff_profile_format()), so the caller does not have to know which ofread_h5()andread_json()it wanted. Those remain available for reading a file whose format is already known.- Parameters:
- Returns:
The run’s profiling data, with all durations in seconds.
- Return type:
- Raises:
FileNotFoundError – If
pathdoes not exist.ValueError – If
pathis an HTML report. A report is a rendering of a run, not a copy of it, so there is nothing to read back.
- scope_profiler.profile_io.sniff_profile_format(path)#
Which format the contents of
pathare in.profile_format()goes by the file name, which is all that can be known before a file exists. For a file that does exist the contents are the better authority: a run written asprofile.dat, or a JSON profile someone renamed to.h5, still reads correctly.Falls back to
profile_format()when the leading bytes match nothing known, so an unrecognisable file is still treated as HDF5 and produces h5py’s own error rather than one invented here.- Raises:
FileNotFoundError – If
pathdoes not exist.- Return type:
read_h5#
The module-level spelling of ProfilingResults.from_h5(); the two are
interchangeable.
- scope_profiler.h5reader.read_h5(file_path, verbose=False)#
Load a merged profiling file for post-processing.
The discoverable spelling of
ProfilingResults.from_h5; the two are interchangeable:from scope_profiler import read_h5 results = read_h5("profiling_data.h5") results.print_summary() solve = results["solve"] # same as results.get_region("solve") solve[0].average_duration # rank 0, in seconds
- Parameters:
- Returns:
The run’s profiling data. All durations are reported in seconds.
- Return type:
- Raises:
FileNotFoundError – If the specified HDF5 file does not exist.
read_h5_summary#
Load fixed-size scalar statistics without reading per-call event columns.
- scope_profiler.h5reader.read_h5_summary(file_path, verbose=False, *, fallback=True, include_likwid=True, include_line_profile=True, regions=None, ranks=None)#
Load fixed-size statistics, optionally falling back for older files.
The returned object supports metadata and scalar region statistics. Event, timeline, call-stack and percentile APIs intentionally have no per-call data; use
read_h5()when those are required.
read_json and write_json#
The same run as a JSON document: write_json() writes one,
read_json() reads it back into the very same ProfilingResults, and
read_profile() / write_profile() pick between JSON and HDF5 by the
file name — the rule scope-profiler run -o follows. See CLI reference for
when to prefer which.
- scope_profiler.json_export.read_json(file_path, verbose=False)#
Read a JSON profile back into
ProfilingResults.The JSON twin of
read_h5():results = read_json("profiling_data.json") results.print_summary()
- Parameters:
- Return type:
- scope_profiler.json_export.write_json(results, filepath, *, include=None, exclude=None, ranks=None, indent=None)#
Write one run to
filepathas JSON, and return the path written.
- scope_profiler.json_export.export_json(profiling_data, filepath, ranks=None, include=None, exclude=None, verbose=True, indent=None)#
Write one JSON document per run, exactly as
export_speedscopedoes.- Parameters:
profiling_data (ProfilingResults | Sequence[ProfilingResults]) – The run(s) to export: file runs, in-memory results, or a mix.
filepath (str | Path) – Base output path, e.g.
figures/profile.json. Ending it in.gzcompresses the output. The run’s label is appended to the stem when more than one run is exported.ranks (list[int] | int, optional) – Ranks to export (default: every rank).
include (list[str] | str, optional) – Region name filters, as for the plotting functions.
exclude (list[str] | str, optional) – Region name filters, as for the plotting functions.
indent (int, optional) – Passed to
json.dumps; the default writes the document on one line.verbose (bool)
- Returns:
The files written, in the order they were written.
- Return type:
list[Path]
- scope_profiler.profile_io.read_profile(file_path, verbose=False)#
Read a profile from HDF5 or JSON, whichever the name says it is.
Dispatches on the file name;
load()dispatches on the contents and is the better default for a file that already exists.- Parameters:
verbose (bool)
- Return type:
- scope_profiler.profile_io.write_profile(results, file_path, **kwargs)#
Write
resultstofile_pathin the format its name asks for.- Parameters:
results (ProfilingResults) – The run to write.
file_path (str | Path) – Destination.
.json/.json.gzwrites a JSON profile,.htmlan HTML report, and anything else an HDF5 file.**kwargs – Passed to the format’s own writer.
- Returns:
The file written.
- Return type:
Path
Native (C and Fortran) traces#
Reading what the C and Fortran region APIs write - they share one format; see Profiling C and C++ code and Profiling Fortran code.
- scope_profiler.native_trace.load_traces(inputs, label=None)#
Read native output into the standard post-processing API.
Both formats are accepted, and mix:
.spttraces from a Fortran or plain C build, and the per-rank.h5profiles anSP_USE_HDF5C build writes. Every rank of a run lands in one result either way.- Parameters:
inputs (path or sequence of paths) – Files and/or directories containing them (see
find_traces()).label (str, optional) – Name for the run in summaries, charts and exports.
- Returns:
The same object a Python run produces, so every summary, plot and exporter works on it unchanged.
- Return type:
- Raises:
TraceFormatError – If two files claim the same rank.
- scope_profiler.native_trace.convert_traces(inputs, output_path, label=None)#
Convert Fortran traces into a standard scope-profiler HDF5 file.
The result is indistinguishable from one a Python run wrote, so
scope-profiler plot/inspectandread_h5()work on it directly.
- scope_profiler.native_trace.read_trace(path)#
Read one rank’s trace file.
- Parameters:
path (str or Path) – A
.sptfile written bysp_finalize().- Returns:
(rank, regions), whereregionsmaps a region name to(start_times, end_times)int64 arrays in nanoseconds - exactly the shapeRankPayloadcarries. Each value also carries.source_file/.source_linenoattributes (both None on a version-1 trace, or a region registered withoutsp_region_at()), without changing how it unpacks.- Return type:
- Raises:
TraceFormatError – If the file is not a trace, is truncated, or was written by a newer format version.
- scope_profiler.native_trace.find_traces(inputs)#
Collect native output files from paths, directories, or a mix of both.
- Parameters:
inputs (path or sequence of paths) – Files to read, and/or directories to search (non-recursively) for
*.spttraces and*_rank<NNNNN>.h5per-rank profiles.- Returns:
The files, sorted, with duplicates removed.
- Return type:
list of Path
- Raises:
FileNotFoundError – If an input does not exist, or a directory holds neither kind of file.
- scope_profiler.native_trace.write_results(results, output_path)#
Write any
ProfilingResultsout as a standard HDF5 file.Goes through
ProfilingWriter, so the result has exactly the layout a Python run produces - which is what lets an imported (or merged) run be read back byread_h5()and fed to every plot and exporter.- Parameters:
results (ProfilingResults) – The run to write.
output_path (str or Path) – HDF5 file to create.
- Returns:
The file that was written.
- Return type:
Path
- scope_profiler.native_trace.fortran_source_path()#
Path to
scope_profiler.f90, the module to compile into your program.It ships with the package, so this works from an installed wheel:
gfortran -c $(python -c "import scope_profiler.native_trace as t; print(t.fortran_source_path())")
- Returns:
The Fortran module source.
- Return type:
Path
- scope_profiler.native_trace.c_source_path()#
Path to
scope_profiler.c, the implementation entry point to compile.Its public and private implementation headers sit next to it;
c_include_dir()is what to put on the compiler’s include path:cc -c $(python -c "import scope_profiler.native_trace as t; print(t.c_source_path())") -I$(python -c "import scope_profiler.native_trace as t; print(t.c_include_dir())")
- Returns:
The C source file.
- Return type:
Path
- scope_profiler.native_trace.c_include_dir()#
Directory holding
scope_profiler.h, for the compiler’s-I.- Returns:
The include directory.
- Return type:
Path
Combining runs#
- scope_profiler.results.merge_results(*result_sets, label=None, file_path=None)#
Combine several result sets into one.
The case this exists for is a mixed-language run: a Python driver records its own regions while the Fortran (or other) code it calls records theirs, and what the user wants at the end is one profile covering both. Ranks line up by number, so rank 3’s Python regions and rank 3’s Fortran regions end up side by side.
- Parameters:
*result_sets (ProfilingResults) – The sets to combine. Non-root sets (the empty ones every rank but 0 gets back under MPI) are ignored, so this can be called unguarded in a parallel script.
label (str, optional) – Name for the combined run. Defaults to the first set’s label.
file_path (str or Path, optional) – Output path to attribute the combined set to. Defaults to the first set’s.
- Returns:
One result set holding every region of every input.
- Return type:
- Raises:
ValueError – If no result sets were given, or if a region name appears in more than one of them. Merging same-named regions would silently double-count a Python wrapper and the native region inside it, so the collision has to be resolved by the caller - name the regions apart, for instance with a
"fortran:"prefix.
Region#
- class scope_profiler.region.Region(start_times, end_times, gpu_durations=None, call_ids=None, parent_ids=None, thread_ids=None, task_ids=None, await_times=None, event_metadata=None, source_file=None, source_lineno=None, source_text=None, tags=(), aggregate=None, event_data_available=True)#
Timing data for one region on one rank.
All duration and timestamp properties are reported in seconds; the underlying HDF5 data is stored in nanoseconds.
- Parameters:
start_times (ndarray)
end_times (ndarray)
gpu_durations (ndarray | None)
call_ids (ndarray | None)
parent_ids (ndarray | None)
thread_ids (ndarray | None)
task_ids (ndarray | None)
await_times (ndarray | None)
source_file (str | None)
source_lineno (int | None)
source_text (str | None)
aggregate (dict | None)
event_data_available (bool)
- get_summary()#
Return a summary of the region’s statistics as a dictionary.
- Returns:
Dictionary containing statistics: num_calls, total_duration, average_duration, min_duration, max_duration, first_duration, last_duration, and std_duration. Durations are in seconds.
- Return type:
Dict[str, Any]
- events(origin=0.0)#
Return one dict per recorded call.
- Parameters:
origin (float, optional) – Seconds subtracted from every timestamp, so passing
results.minimum_start_timeyields a timeline starting at zero (default: 0.0, i.e. raw timestamps).- Returns:
One entry per call with keys
call_index,start,endandduration, in seconds and in recorded order.- Return type:
- property thread_ids: ndarray | None#
Thread-table index of every call, or None if threads were not tracked.
- property task_ids: ndarray | None#
Task-table index of every call,
-1outside a task.None unless the run used
track_async.
- property await_times_ns: ndarray | None#
Nanoseconds each call spent with its task suspended, or None.
- property await_times: ndarray | None#
Seconds each call spent with its task suspended, or None.
The wall time inside the
withblock during which the task was not running: awaiting, or switched away from. Subtracting it from the call’s duration leaves the time the lane actually held its thread.
- property total_await_duration: float | None#
Total awaited seconds over every call, or None if not tracked.
- for_thread(thread_index)#
A view of this region holding only the calls of one thread.
Every column is sliced with the same mask, so the returned region is an ordinary
Region- summaries, durations and percentiles all describe that thread alone.- Raises:
ValueError – If this region has no thread column (the run did not use
track_threads).- Parameters:
thread_index (int)
- Return type:
- property call_ids#
Explicit call ids, or None for legacy profiles.
- property parent_ids#
Explicit parent ids, or None for legacy profiles.
- property stored_summary: dict | None#
Fixed-size statistics used by aggregate and summary-only results.
- property source_file: str | None#
Path of the file this region is defined in, or None if not captured.
- property source_text: str | None#
Source text of the region’s
withblock or decorated function.None if it was not captured - either the file it came from is no longer readable, or the file predates this being recorded.
- property gpu_durations_ns: ndarray | None#
CUDA-event elapsed device times in nanoseconds, or None if absent.
- property gpu_durations: ndarray | None#
CUDA-event elapsed device times in seconds, or None if absent.
- property gpu_total_duration: float | None#
Total CUDA-event elapsed device time in seconds, or None if absent.
- property exclusive_duration: float#
Alias for
total_exclusive_duration.
- property gpu_average_duration: float | None#
Average CUDA-event elapsed device time in seconds, or None if absent.
- percentile_duration(percentile)#
Return a duration percentile in seconds.
percentilefollowsnumpy.percentile()and must be between 0 and 100. Empty regions return0.0for consistency with the other duration statistics.
MPIRegion#
- class scope_profiler.mpi_region.MPIRegion(name, regions)#
One named region across all ranks that recorded it.
Indexing gives the per-rank
Region(region[0]), while the properties on this class aggregate over every rank. All durations are in seconds.- property source_file: str | None#
Path of the file this region is defined in, or None if not captured.
- get_summary()#
Return statistics aggregated over every rank.
- Returns:
Dictionary with name, num_ranks, num_calls (summed over ranks) and duration statistics in seconds, pooled over all calls on all ranks.
- Return type:
Dict[str, Any]
- events(ranks=None, origin=0.0)#
Return one dict per recorded call, on every rank.
This is the long-form view custom plots and dataframes want: each entry is a single call rather than a per-region aggregate.
- Parameters:
ranks (list of int or int, optional) – Restrict to these ranks (default: all ranks that recorded the region). Ranks without data for this region are skipped.
origin (float, optional) – Seconds subtracted from every timestamp, so passing
results.minimum_start_timeyields a timeline starting at zero (default: 0.0, i.e. raw timestamps).
- Returns:
Entries with keys
name,rank,call_index,start,endandduration, in seconds, ordered by rank and then by call order.- Return type:
- property durations: ndarray#
Get every recorded call duration on every rank, in seconds.
- Returns:
Pooled durations. Empty if no rank recorded timing.
- Return type:
np.ndarray
- property gpu_durations: ndarray | None#
CUDA-event elapsed device times pooled across ranks, or None if absent.
- property num_calls: int#
Total number of calls summed over all ranks.
- Returns:
Summed call count.
- Return type:
- num_calls_per_rank()#
Get the call count for each rank.
- average_durations()#
Get the average duration for each rank.
- min_durations()#
Get the minimum duration for each rank.
- max_durations()#
Get the maximum duration for each rank.
- total_durations()#
Get the total duration for each rank.
- property total_duration: float#
Get the total duration summed over all ranks and calls.
- Returns:
Total duration in seconds.
- Return type:
- property gpu_total_duration: float | None#
Total CUDA-event elapsed device time across ranks, or None if absent.
- property total_exclusive_duration: float#
Total time excluding nested regions, across ranks, in seconds.
- property exclusive_duration: float#
Alias for
total_exclusive_duration.
- property average_duration: float#
Get the mean duration over every call on every rank.
Note this pools all calls, so ranks with more calls weigh more heavily; use
average_durations()for the per-rank breakdown.- Returns:
Average duration in seconds, or 0.0 if no timing was recorded.
- Return type:
- property gpu_average_duration: float | None#
Average CUDA-event elapsed device time across ranks, or None if absent.
- property std_duration: float#
Get the standard deviation over every call on every rank.
- Returns:
Standard deviation in seconds, or 0.0 if no timing was recorded.
- Return type:
- percentile_duration(percentile)#
Return a pooled duration percentile across all ranks.
- property rank_imbalance: float#
Maximum per-rank total divided by the mean per-rank total.
A value of 1.0 means perfectly balanced ranks. Values are
0.0when no calls were recorded or only one rank has timing data.
- property min_duration: float#
Get the minimum duration across all ranks.
- Returns:
The minimum duration among all ranks, in seconds.
- Return type:
- property max_duration: float#
Get the maximum duration across all ranks.
- Returns:
The maximum duration among all ranks, in seconds.
- Return type:
- property first_duration: float#
Get the duration of the call that started earliest across all ranks.
- Returns:
Duration in seconds of the chronologically first call, or 0.0 if no rank recorded timing.
- Return type:
- property last_duration: float#
Get the duration of the call that ended latest across all ranks.
- Returns:
Duration in seconds of the chronologically last call, or 0.0 if no rank recorded timing.
- Return type:
MPI call wrappers#
Opt-in profiling wrappers for mpi4py communicators and requests.
Importing this module never imports mpi4py. Pass an existing communicator to
profile_mpi_comm, or explicitly call profile_mpi4py to patch predefined
communicators for the duration of a run. Importing scope-profiler therefore
cannot initialize MPI in a serial process.
- class scope_profiler.mpi_wrappers.MPIRegionMetadata(operation, kind, bytes, peer, root, tag, communicator, request=None)#
Structured metadata decoded from a Python or native MPI region name.
- class scope_profiler.mpi_wrappers.ProfiledMPIComm(communicator, *, pickle_object_sizes=False)#
A transparent communicator proxy for common mpi4py operations.
- Parameters:
communicator (Any)
pickle_object_sizes (bool)
- class scope_profiler.mpi_wrappers.ProfiledMPIRequest(request, *, operation, bytes, peer, tag, communicator)#
A request proxy that attributes blocking completion time to MPI wait.
- scope_profiler.mpi_wrappers.format_mpi_region(operation, *, bytes=-1, peer=-1, root=-1, tag=-1, communicator=-1, request=None)#
Return the canonical region name shared by Python and C++ wrappers.
- scope_profiler.mpi_wrappers.message_nbytes(value, *, pickle_objects=True)#
Estimate payload bytes for an mpi4py object or buffer specification.
Buffer-mode calls are exact when their datatype exposes
Get_size. Object-mode values use their buffer size when possible. Withpickle_objects=True(the default for this explicit helper), other objects use the size of their protocol-5 pickle.-1means unknown.
- scope_profiler.mpi_wrappers.parse_mpi_region(name)#
Decode a canonical MPI region name, or return
Nonefor another region.- Parameters:
name (str)
- Return type:
MPIRegionMetadata | None
- scope_profiler.mpi_wrappers.profile_mpi4py(mpi_module=None, *, pickle_object_sizes=False, lazy=False)#
Temporarily profile mpi4py’s predefined communicators.
When lazy is true, mpi4py is wrapped only if the target imports it. This lets
scope-profiler run --mpi-callsenable MPI-call profiling without importing or initializing MPI for an ordinary serial program. Both globals and Python’s import function are restored even when the program raises.
- scope_profiler.mpi_wrappers.profile_mpi_comm(communicator, *, pickle_object_sizes=False)#
Wrap a communicator without importing mpi4py.
Set
pickle_object_sizes=Trueto measure serialized object-mode payloads at the cost of one extra serialization per call. It is off by default to avoid overhead and repeated__reduce__side effects.- Parameters:
- Return type:
Call stack reconstruction#
Regions record no call graph, so nesting is recovered from timestamp
containment. ProfilingResults.call_stack() is the usual entry point;
the functions below operate on its result and let you walk the
reconstructed tree when building your own nested visualisation.
- scope_profiler.call_stack.build_call_stack(regions, rank, origin=0.0)#
Reconstruct per-call nesting for one rank, one dict per call.
A convenience wrapper over
build_call_arrays()for callers that want to iterate calls rather than columns. It allocates a dict per call (~700 bytes), so prefer the arrays for anything that has to scale with a long run.- Parameters:
regions (iterable of MPIRegion) – Regions to include, e.g.
results.get_regions().rank (int) – Rank whose calls to reconstruct. Regions with no data for this rank are skipped.
origin (float, optional) – Seconds subtracted from every timestamp, so passing
results.minimum_start_timeyields a timeline starting at zero (default: 0.0, i.e. raw timestamps).
- Returns:
One dict per call, ordered by start time (parents before children), with keys
call_id,name,call_path,lane,start,end,duration(the inclusive duration),inclusive_duration,exclusive_duration(seconds),depth(0 for a top-level call) andparent(thecall_idof the enclosing call in this same list, or None).call_pathjoins a call and each of its ancestors with" > ", keeping same-named regions at different call sites distinct. Acolorkey carries whatever the plotting code assigned to the region.call_idis stable for the returned list and is unique within this rank/stack reconstruction.- Return type:
- Raises:
NestingError – If the intervals are not properly nested.
Notes
Calls are indexed by position in the returned list rather than by name, because a region called more than once - or recursively - contributes several calls under one name.
- scope_profiler.call_stack.call_stack_roots(calls)#
Indices of the top-level calls in a
build_call_stack()result.
- scope_profiler.call_stack.call_stack_children(calls)#
Child indices per call, for walking a
build_call_stack()result.Returns a list parallel to
calls: entry i holds the indices of the calls directly nested inside call i, in start-time order.
Intervals must be properly nested, within a lane#
Two calls recorded on one lane — one thread, or one asyncio task or
greenlet running on it — either nest completely or do not overlap at
all. A call that starts inside another on the same lane and ends after
it raises NestingError rather than being reconstructed under a guessed
parent — there is no call stack that describes it, and every consumer
(flame chart, .prof, speedscope, exclusive time) would otherwise have
to invent one.
Calls on different lanes overlap freely and are reconstructed as
separate stacks. A run profiled with track_threads records a lane per
call, which is what makes that possible; without it a whole rank is one
lane, so profiling concurrent threads raises here instead. See Threads,
tasks and coroutines.
A stack of with blocks or decorated functions on one lane always
satisfies this. The other realistic way to violate it is a mismatched
sp_begin/sp_end pair in C or Fortran; see the C and
Fortran guides.
finalize() is deliberately more forgiving than the readers: a rank
whose intervals do not nest keeps its timings and loses only the call
graph, with a warning, rather than throwing away a run that has already
finished computing.
- exception scope_profiler.call_stack.NestingError#
Raised when recorded intervals are not properly nested.
A lane’s calls must form a forest: any two intervals on one lane are either disjoint or one contains the other. Partial overlap has no call stack to reconstruct, and every consumer of this module (flame chart,
.prof, speedscope, exclusive time) would have to invent one.In practice this means a mismatched
sp_begin/sp_endpair in C or Fortran, or regions driven from several threads in a run that did not profile withtrack_threads=True- without that, every thread’s calls land in one lane and interleave.
Reconstruction at scale#
build_call_stack allocates a dict per call (~700 bytes), which is fine
for inspecting a run and not fine for a long one. build_call_arrays
returns the same reconstruction as numpy columns and is what the
exporters and finalize() use — a run with ten million events per rank
reconstructs in a couple of seconds instead of a couple of minutes.
- scope_profiler.call_stack.build_call_arrays(regions, rank)#
Reconstruct one rank’s nesting as numpy arrays.
The workhorse behind
build_call_stack(), and what anything processing a whole run should call: it never builds a dict per call, so it stays usable on the tens of millions of events a long simulation produces.- Parameters:
- Return type:
- Raises:
NestingError – If any interval ends before it starts, or two intervals overlap without one containing the other.
- class scope_profiler.call_stack.CallArrays(names, source_files, source_lines, region_index, call_index, start_ns, end_ns, depth, parent, exclusive_ns, lane=array([], dtype=int64))#
One rank’s reconstructed nesting, in start-sorted (
call_id) order.Every array is indexed by
call_id, soparentindexes straight back into the same arrays.region_indexandcall_indexpoint at where a call came from: regionnames[region_index[i]], call numbercall_index[i]within that region’s buffers.- Parameters:
- lane: ndarray#
Which stack each call belongs to; see
scope_profiler.concurrency.lane_ids().Empty for a run that did not track threads, meaning the whole rank is one stack - rather than a full-length array of
-1, which would cost three more passes over the events of every single-threaded run to say nothing. Consumers must therefore checklane.sizebefore indexing it. Defaulted so the field can be omitted by older callers constructing this tuple themselves.
Plotting#
- scope_profiler.plotting_scripts.plot_gantt(profiling_data, ranks=None, include=None, exclude=None, filepath=None, show=False, verbose=True, cmap='tab20', data_filepath=None, data_format='csv', backend='matplotlib', return_fig=False, min_duration=0.0, start_time=None, end_time=None, aggregate_calls=1, collapse_depth=None)#
Plot a Gantt chart of all (or selected) regions with per-rank lanes using maxplotlib.
- Parameters:
backend (str) – Backend to use for rendering: “matplotlib” (default) or “plotly”.
return_fig (bool) – Return the rendered figure instead of the default
None. Matplotlib returns(fig, axes); Plotly returns its figure object.profiling_data (ProfilingResults | Sequence[ProfilingResults])
filepath (str | None)
show (bool)
verbose (bool)
cmap (str)
data_format (str)
min_duration (float)
start_time (float | None)
end_time (float | None)
aggregate_calls (int)
collapse_depth (int | None)
- Return type:
object | None
- scope_profiler.plotting_scripts.plot_flame_chart(profiling_data, ranks=None, include=None, exclude=None, filepath=None, show=False, verbose=True, cmap='inferno', data_filepath=None, data_format='csv', backend='matplotlib', return_fig=False)#
Plot a flame chart reconstructing the call stack from region timings.
- Parameters:
backend (str) – Backend to use for rendering: “matplotlib” (default) or “plotly”.
return_fig (bool) – Return the rendered figure instead of the default
None. Matplotlib returns(fig, axes); Plotly returns its figure object.profiling_data (ProfilingResults | Sequence[ProfilingResults])
filepath (str | None)
show (bool)
verbose (bool)
cmap (str)
data_format (str)
- Return type:
object | None
- scope_profiler.plotting_scripts.plot_flame_graph(profiling_data, ranks=None, include=None, exclude=None, filepath=None, show=False, verbose=True, cmap='inferno', data_filepath=None, data_format='csv', backend='matplotlib', return_fig=False)#
Plot an aggregated flame graph whose x-axis represents total time.
- scope_profiler.plotting_scripts.plot_durations(profiling_data, ranks=None, include=None, exclude=None, labels=None, metric='total', metrics=None, sort_by=None, top_n=None, combine_regions=None, stack_children=False, filepath=None, show=False, verbose=True, cmap='tab20', log_scale=False, data_filepath=None, data_format='csv', backend='matplotlib', return_fig=False)#
Plot duration bar charts for one or more profiling files using maxplotlib.
- Parameters:
combine_regions (dict[str, list[str] | str], optional) – Merge several regions into a single bar, e.g.
{"setup": ["setup: .*"]}combines everysetup: ...region into one bar named “setup”, pooling their calls the same waysort_byand the other duration statistics pool a single region’s calls. Each value is one or more regex patterns (matched likeinclude); a region matching several groups is claimed by whichever group is listed first.metric (str) – Duration metric to render (
avg,min,max,total,firstorlast).first/lastare the durations of the chronologically first and last call, which show how much of a region’s cost is one-off warmup rather than steady state.metrics (Sequence[str], optional) – Render several metrics instead of one. Each gets its own figure –
filepathis suffixed with the metric name, as it always was for multiple metrics - but a singledata_filepathholds them all, with the metric named per row, so one export can back a chart whose metric the viewer switches. Overridesmetricwhen given.stack_children (bool) – Split each bar into the region’s own (exclusive) time plus one segment per region called directly from it, stacked on top of each other, so a bar shows where its time went instead of only how much there was. The nesting comes from timestamp containment, the same reconstruction the flame chart uses, over every region in the run – a child that is filtered out of the bars still gets its own segment. Only
totalandavgdecompose this way;min/maxare rejected. Colors then identify segments rather than runs, so with several runs the bars are still grouped per region in run order but the legend names the segments.backend (str) – Backend to use for rendering: “matplotlib” (default) or “plotly”.
return_fig (bool) – Return the rendered figure instead of the saved filepath list.
profiling_data (ProfilingResults | Sequence[ProfilingResults])
sort_by (str | None)
top_n (int | None)
filepath (str | None)
show (bool)
verbose (bool)
cmap (str)
log_scale (bool)
data_format (str)
- Returns:
List of filepaths that were written (empty if filepath is None). With
return_fig, the rendered figure, or the list of figures when several metrics were requested.- Return type:
- scope_profiler.plotting_scripts.plot_duration_timeseries(profiling_data, ranks=None, include=None, exclude=None, filepath=None, show=False, verbose=True, cmap='tab20', log_scale=False, data_filepath=None, data_format='csv', backend='matplotlib', return_fig=False)#
Plot each region’s call duration over wall-clock time, with a min-max band.
One line per region tracks the mean duration over the ranks that recorded each call, shaded between the minimum and maximum duration seen across those ranks, so rank imbalance shows up as a widening band.
- Parameters:
backend (str) – Backend to use for rendering: “matplotlib” (default) or “plotly”.
return_fig (bool) – Return the rendered figure instead of the default
None. Matplotlib returns(fig, axes); Plotly returns its figure object.profiling_data (ProfilingResults | Sequence[ProfilingResults])
filepath (str | None)
show (bool)
verbose (bool)
cmap (str)
log_scale (bool)
data_format (str)
- Return type:
None
- scope_profiler.plotting_scripts.plot_speedup(profiling_data, x_field='num_ranks', ranks=None, include=None, exclude=None, filepath=None, show=False, verbose=True, cmap='tab20', data_filepath=None, data_format='csv', backend='matplotlib', return_fig=False)#
Plot scope speedup versus a chosen parallelism/metadata field using maxplotlib.
Statistics export#
- scope_profiler.plotting_scripts.collect_region_statistics(profiling_data, ranks=None, include=None, exclude=None, labels=None)#
Collect aggregate region-duration statistics for one or more profiling files.
- scope_profiler.plotting_scripts.write_region_statistics_json(profiling_data, filepath, ranks=None, include=None, exclude=None, labels=None)#
Write aggregate region-duration statistics to a JSON file.
Exporting to other tools#
- scope_profiler.speedscope_export.export_speedscope(profiling_data, filepath, ranks=None, include=None, exclude=None, verbose=True)#
Write speedscope JSON files for the selected ranks.
One file is written per input HDF5 file, holding one profile per rank — or, for a run profiled with
track_threads, one per thread and task. Unlike.prof, the format carries several profiles per file, and speedscope switches between them from its profile selector.- Parameters:
profiling_data (ProfilingResults | Sequence[ProfilingResults]) – The run(s) to export: file runs, in-memory results from
ProfileManager.finalize(return_results=True), or a mix.filepath (str | Path) – Base output path, e.g.
figures/profile.speedscope.json. The input file’s stem is appended when more than one file is exported.ranks (list[int] | int, optional) – Ranks to export (default: rank 0 only).
include (list[str] | str, optional) – Region name filters, as for the plotting functions.
exclude (list[str] | str, optional) – Region name filters, as for the plotting functions.
verbose (bool)
- Returns:
The files written, in the order they were written.
- Return type:
list[Path]
- scope_profiler.chrome_trace_export.export_chrome_trace(profiling_data, filepath, ranks=None, include=None, exclude=None, verbose=True)#
Write Chrome Trace JSON files, one per input profiling run.
- scope_profiler.prof_export.export_prof(profiling_data, filepath, ranks=None, include=None, exclude=None, call_paths=True, verbose=True)#
Write per-rank
.proffiles readable bypstats/snakeviz.- Parameters:
profiling_data (ProfilingResults | Sequence[ProfilingResults]) – The run(s) to export: file runs, in-memory results from
ProfileManager.finalize(return_results=True), or a mix.filepath (str | Path) – Base output path, e.g.
figures/profile.prof. A_rank<N>suffix is appended per rank (and the input file’s stem too, when more than one file is exported), since.profhas no notion of ranks or runs.ranks (list[int] | int, optional) – Ranks to export (default: rank 0 only).
include (list[str] | str, optional) – Region name filters, as for the plotting functions.
exclude (list[str] | str, optional) – Region name filters, as for the plotting functions.
call_paths (bool, optional) – Preserve each reconstructed parent/child path as a separate node in the exported pstats tree. This makes SnakeViz distinguish same-named regions called below different parents. Set to
Falsefor a compact, name-aggregated export.verbose (bool, optional) – Print each file written, with the
snakevizcommand that opens it.
- Returns:
The files written, in the order they were written.
- Return type:
list[Path]
- scope_profiler.prof_export.to_pstats(profiling_data, ranks=None, include=None, exclude=None, call_paths=True)#
Build a
pstats.Statsper (run label, rank), without disk I/O.The in-memory twin of
export_prof(): same call reconstruction and filtering, but returns realpstats.Statsobjects keyed by(run_label, rank)instead of writing.proffiles.- Parameters:
profiling_data (ProfilingResults | Sequence[ProfilingResults]) – The run(s) to export: file runs, in-memory results from
ProfileManager.finalize(return_results=True), or a mix.ranks (list[int] | int, optional) – Ranks to export (default: rank 0 only).
include (list[str] | str, optional) – Region name filters, as for the plotting functions.
exclude (list[str] | str, optional) – Region name filters, as for the plotting functions.
call_paths (bool, optional) – Preserve each reconstructed parent/child path as a separate node in the resulting tree; see
build_pstats_dict().
- Returns:
Maps
(run_label, rank)to itspstats.Stats, empty on ranks that own no files.- Return type:
dict[tuple[str, int], pstats.Stats]
- scope_profiler.prof_export.build_pstats(calls, root_name=None, call_paths=True)#
Build reconstructed calls straight into a
pstats.Stats.The in-memory twin of
build_pstats_dict(), for callers that want tosort_stats/print_statsimmediately instead of writing a.proffile first. A run with no calls yields an emptyStats, whichpstatswould otherwise refuse to construct from an empty dict.- Parameters:
calls (CallArrays)
root_name (str | None)
call_paths (bool)
- Return type:
- scope_profiler.prof_export.load_prof(filepath)#
Read a
.proffile written byexport_prof()back as a Stats.The
.proftwin ofread_json().
- class scope_profiler.prof_export.ScopeProfile(stats)#
A
cProfile.Profilecarrying reconstructed region stats.pstats.Statsonly accepts a filename or an object with acreate_stats()method that leaves the finished pstats dict in.stats(seepstats.Stats.load_stats) -cProfile.Profileis the real class satisfying that contract. Subclassing it, instead of merely duck-typing it, letsbuild_pstats()handpstats.Statsa type it actually declares support for, and skipsProfile.__init__(which would start a C-level profiler this class never uses).Inherited
Profilemethods that only need.statswork as usual, soScopeProfile(stats).dump_stats(path)writes the same.proffilewrite_prof_file()does. Like a realProfile, an instance is consumed bypstats.Stats: it takes.statsand leaves the profile empty, so build a new one perStats.
- scope_profiler.json_export.export_json(profiling_data, filepath, ranks=None, include=None, exclude=None, verbose=True, indent=None)
Write one JSON document per run, exactly as
export_speedscopedoes.- Parameters:
profiling_data (ProfilingResults | Sequence[ProfilingResults]) – The run(s) to export: file runs, in-memory results, or a mix.
filepath (str | Path) – Base output path, e.g.
figures/profile.json. Ending it in.gzcompresses the output. The run’s label is appended to the stem when more than one run is exported.ranks (list[int] | int, optional) – Ranks to export (default: every rank).
include (list[str] | str, optional) – Region name filters, as for the plotting functions.
exclude (list[str] | str, optional) – Region name filters, as for the plotting functions.
indent (int, optional) – Passed to
json.dumps; the default writes the document on one line.verbose (bool)
- Returns:
The files written, in the order they were written.
- Return type:
list[Path]
LIKWID#
See LIKWID hardware counters for the workflow and the HDF5 layout.
LikwidRegionResult#
- class scope_profiler.likwid_data.LikwidRegionResult(tag, group_id=-1, group_name='', cpus=<factory>, times=<factory>, call_counts=<factory>, event_names=<factory>, counter_names=<factory>, events=<factory>, metric_names=<factory>, metrics=<factory>, source='full_api')
Counter results for a single LIKWID marker region.
The per-thread arrays are all indexed by the same thread axis, so
events[e, t]andmetrics[m, t]refer to the thread whose runtime istimes[t]and whose CPU iscpus[t].- Parameters:
- tag
Region name as passed to
markerstartregion.- Type:
- group_id
Index of the LIKWID event group this region was measured with.
- Type:
- group_name
Name of that group (e.g.
"CLOCK"), empty if unknown.- Type:
- times
Per-thread accumulated runtime in seconds, shape
(nthreads,).- Type:
- call_counts
Per-thread number of times the region was entered, shape
(nthreads,).- Type:
- event_names
Names of the raw hardware events, length
nevents. Not unique: a group may program one event on several counters (seeevent_labels).
- counter_names
Hardware counter register each event was programmed on (
FIXC0,PMC1,MBOX3C0, …), lengthnevents. Empty for files written before counter names were recorded.
- events
Raw counter values, shape
(nevents, nthreads).- Type:
- metrics
Derived metric values, shape
(nmetrics, nthreads).- Type:
- source
Which collection path produced this result:
"full_api"(perfmon read-back, the only one with real event names and metrics),"marker_file"(LIKWID’s marker file parsed directly — real values, placeholder event names, no metrics) or"marker_api"(amarkergetregionsnapshot of the calling thread only).- Type:
- tag: str
- group_id: int = -1
- group_name: str = ''
- times: ndarray
- call_counts: ndarray
- events: ndarray
- metrics: ndarray
- source: str = 'full_api'
- property event_labels: list[str]
Unique per-event labels, safe to use as dict or column keys.
event_namesalone is not unique. A group such asMEM_DPprograms the same event on one counter per memory channel, soCAS_COUNT_RDlegitimately appears eight times on a socket with eight channels (the ones reading zero are unpopulated). Keying anything by the bare name silently keeps only the last channel.Names that occur once are returned unchanged; repeated ones get the hardware counter appended —
CAS_COUNT_RD:MBOX0C0,CAS_COUNT_RD:MBOX1C0, … — or a positional suffix if the file predates counter names being recorded.
- as_dict()
Return the region’s results as a plain dictionary.
Convenience for callers that want to build a DataFrame or dump the numbers without touching the array layout.
- Return type:
Summary tables#
- scope_profiler.summary.likwid_tables(results, include=None, exclude=None, ranks=None)#
Build one LIKWID counter table per (rank, event group).
Regions become columns and counters become rows: a run typically has a handful of regions but a few dozen counters, so this orientation stays readable where the transpose would not. Splitting per event group keeps every column of a table comparable, since a group fixes which events and metrics exist.
Columns are ordered by descending LIKWID runtime (ties alphabetically), so the costliest regions lead, as in the region table.
- Parameters:
results (ProfilingResults) – Source of the LIKWID results.
include (list of str or str, optional) – Regex patterns selecting which regions to report, matched as for the region table.
exclude (list of str or str, optional) – Regex patterns selecting which regions to report, matched as for the region table.
ranks (list of int, optional) – Restrict to these ranks (default: all).
- Returns:
One entry per table, each with
rank,group,columns(the region labels) andsections((title, rows)pairs, where a row is(name, values)). Empty when the file holds no LIKWID data.- Return type:
- scope_profiler.summary.print_likwid_table(table, title=None, stream=None)#
Print one LIKWID counter table from
likwid_tables().- Return type:
None
- scope_profiler.summary.print_likwid_tables(results, include=None, exclude=None, ranks=None, stream=None)#
Print every LIKWID counter table a result set exposes.
A no-op for files recorded without LIKWID, so callers can invoke it unconditionally.
Collection and storage#
- scope_profiler.likwid_data.collect_marker_results_isolated(timeout=120.0)#
Run the perfmon read-back in a child process and return its results.
collect_marker_results()has to re-initialize LIKWID’s perfmon module, and on hosts where the counters are not really usable that can abort the process outright instead of raising — which atfinalize()time would destroy a completed run’s output. Running it behind a process boundary turns that worst case into a missing enrichment.- Parameters:
timeout (float, optional) – Seconds to wait for the child before giving up (default: 120).
- Returns:
Nonewhen the child could not deliver results (crashed, timed out, or LIKWID refused to re-open the counters), which tells the caller to fall back toparse_marker_file().- Return type:
list of LikwidRegionResult or None
- scope_profiler.likwid_data.parse_marker_file(path=None)#
Read LIKWID’s marker file directly, without calling into LIKWID.
The file
markerclose()writes is plain text:<nthreads> <nregions> <ngroups> <region_id>:<tag>-<group_id> (one line per region) <region_id> <group_id> <cpu> <call_count> <time> <nevents> <values...>
Parsing it here is the crash-proof path: it touches no counters and calls no LIKWID function, so it cannot take the interpreter down the way re-initializing perfmon can. The cost is that event names, counter registers and derived metrics are not in the file — those exist only inside LIKWID’s group definitions — so events come back positionally named and the metric list is empty.
- scope_profiler.likwid_data.collect_marker_results(pylikwid)#
Read every region of the run back from LIKWID’s marker file.
Must be called after
markerclose(), which is what writes the file. Becausemarkerclose()also tears the perfmon module down, the event sets named inLIKWID_EVENTSare re-registered first; that is what makes event and metric names available for the values in the file.- Parameters:
pylikwid (module) – The imported
pylikwidmodule.- Returns:
One entry per region recorded by LIKWID, in file order. Empty when the process is not running under the marker API, or when the performance counters cannot be re-opened.
- Return type:
list of LikwidRegionResult
- scope_profiler.likwid_data.collect_region_snapshots(pylikwid, region_names)#
Read the current counter values for each named region.
Uses the marker API’s
markergetregion, so it must be called beforemarkerclose()and reports only the calling thread. Regions that LIKWID does not know about (never entered, or measured under a different name) are skipped.
- scope_profiler.likwid_data.snapshots_to_results(snapshots)#
Convert marker-API snapshots into the common result structure.
Used as the fallback when the full API cannot re-open the counters, so the snapshots taken before
markerclose()can still be written to HDF5.
- scope_profiler.likwid_data.write_likwid_results(h5file, results, environment=None)#
Write collected LIKWID results into an open HDF5 file.
Creates (replacing any previous copy) a
likwidgroup holding one subgroup per region underlikwid/regions/<tag>.- Parameters:
h5file (h5py.File or h5py.Group) – Destination, typically the per-rank profiling file.
results (iterable of LikwidRegionResult) – Regions to store.
environment (dict, optional) – LIKWID environment variables to record as attributes on the group, so the event set a file was measured with stays with the data.
- Return type:
None
- scope_profiler.likwid_data.markers_available()#
Whether this process runs under the LIKWID marker API.
markerinit()silently degrades to a no-op when LIKWID’s environment is absent, so this is what distinguishes “there will be counter data” from “the script was started as a plainpython script.py”.- Return type: