scope_profiler.results
Post-processing API over a set of profiling regions.
This is the analysis layer, deliberately independent of where the data came
from: :meth:ProfilingResults.from_h5 builds one of these from a merged HDF5
file, while :meth:ProfileManager.finalize(return_results=True) <scope_profiler.profile_manager.ProfileManager.finalize> builds the same thing
straight out of the in-memory buffers, without a round trip through disk. Both
give back the same type, and everything downstream (summaries, dataframes, the
plotting functions, the exporters) cannot tell them apart.
Classes
| Name | Description |
|---|---|
ProfilingResults | The profiling data of one run, as an ordered mapping of region name to |
Functions
| Name | Description |
|---|---|
merge_results | Combine several result sets into one. |
ProfilingResultsclass#
class ProfilingResults(regions: dict[str, MPIRegion], metadata: dict | None = None, num_ranks: int | None = None, likwid: dict[int, dict[str, LikwidRegionResult]] | None = None, perf_events: dict[int, dict] | None = None, line_profile: dict[int, list] | None = None, file_path: str | Path = '', is_root: bool = True, exclusive_totals: dict[str, dict[int, int]] | None = None, event_data_available: bool = True, threads: dict[int, list] | None = None, tasks: dict[int, list] | None = None)The profiling data of one run, as an ordered mapping of region name to
:class:~scope_profiler.mpi_region.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 secondsThe same object comes back from a merged output file, via
:meth:from_h5 (or its module-level twin
:func:~scope_profiler.h5reader.read_h5)::
results = ProfilingResults.from_h5("profiling_data.h5")All durations are reported in seconds.
display_labelproperty#
display_label: strWhat to call this run in charts, tables and exports.
The :attr:label when 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
str A non-empty name for the run.
file_pathproperty#
file_path: PathThe run’s output file.
Returns
Path
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.
finalize_timeproperty#
finalize_time: float | NoneWhen finalize() was called, in seconds on the recording clock.
This is the finalize_time_ns metadata field, read as the first
thing ProfileManager.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
float or None The registered finalize time, or None for runs without one (older files, or a run that never reached finalize()).
has_event_dataproperty#
has_event_data: boolWhether per-call timestamps are available to event-based APIs.
has_likwidproperty#
has_likwid: boolWhether the run recorded LIKWID hardware counter results.
has_perf_eventsproperty#
has_perf_events: boolWhether this run recorded built-in Linux perf-event counters.
is_rootproperty#
is_root: boolWhether 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 - :meth:print_summary, the plot_*
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
bool True unless this is a non-root rank’s share of an MPI run.
labelpropertywritable#
label: str | NoneThe run’s label, as given to ProfileManager.setup(label=...).
Returns
str or None
The label, or None for a run that was not given one. Use
:attr:display_label to name the run in output regardless.
likwid_ranksproperty#
likwid_ranks: list[int]Ranks that recorded LIKWID results, in ascending order.
line_profileproperty#
line_profile: dict[int, list]Persisted line-profiler records keyed by rank.
Each record contains region, filename, function,
first_lineno, line_numbers, hits, times and unit.
The elapsed seconds for a line are times * unit.
maximum_end_timeproperty#
maximum_end_time: floatGet the maximum end time across all regions and ranks.
Returns
float Maximum end time in seconds, or 0.0 if no region recorded timing.
metadataproperty#
metadata: dictGet environment metadata for the run (gathered from rank 0).
Returns
dict Metadata dict (hostname, OpenMP thread count, platform, versions, etc.), or an empty dict if the run recorded none.
minimum_start_timeproperty#
minimum_start_time: floatGet 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
float Minimum start time in seconds.
num_nodesproperty#
num_nodes: int | NoneNumber of distinct hosts the run’s ranks ran on, or None if unknown.
Read from the num_nodes metadata 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.
num_ranksproperty#
num_ranks: intGet the number of ranks recorded in the profiling data.
Returns
int Number of ranks.
region_namesproperty#
region_names: list[str]Names of all regions, in order of appearance.
run_start_timeproperty#
run_start_time: float | NoneWhen the run started, in seconds on the recording clock.
This is the start_time_ns metadata field, written by
ProfileManager.setup() — by default the moment setup() was called,
or an earlier instant if one was passed to it.
Returns
float or None
The registered start time, or None for runs without one (older
files, or runs that never called setup()). Use
:attr:startup_time for the elapsed time before the first region,
which stays defined either way.
startup_timeproperty#
startup_time: floatSeconds 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
(:attr:run_start_time is None), since the run is then only known
from its first region onwards.
Returns
float Elapsed time before the first region entry, in seconds.
tasksproperty#
tasks: dict[int, list]Rank -> the asyncio tasks and greenlets the run followed.
Each entry is a :class:~scope_profiler.concurrency.TaskInfo, whose
running_time and awaiting_time split the lane’s life into the
part it held a thread and the part it was suspended. Empty unless the
run profiled with track_async=True.
threadsproperty#
threads: dict[int, list]Rank -> the threads that recorded calls, in registration order.
Each entry is a :class:~scope_profiler.concurrency.ThreadInfo: name,
OS ids, when the thread started and ended, and the CPU time it burned.
Empty unless the run profiled with track_threads=True::
for rank, threads in results.threads.items(): for thread in threads: print(rank, thread.name, thread.cpu_time)The per-call thread_ids column of every region indexes into this
rank’s list, so results["solve"][0].for_thread(2) is the part of
solve that ran on results.threads[0][2].
time_originproperty#
time_origin: floatZero point of the relative timeline, in seconds.
The registered :attr:run_start_time when there is one, and otherwise
the first region entry (:attr:minimum_start_time). This is what
:meth:events and :meth:call_stack measure from.
Note the plot_* functions instead frame their x axis on the first
region entry, so that a long gap between setup() and the first
region does not fill a chart with empty space. Pass
origin=results.minimum_start_time to :meth:events to reproduce
the numbers on a chart’s axis.
Returns
float Origin timestamp on the recording clock.
time_spanproperty#
time_span: floatWall-clock seconds between the first region entry and the last exit.
Returns
float Duration of the profiled window in seconds, or 0.0 if no region recorded timing.
total_timeproperty#
total_time: float | NoneWall-clock seconds from setup() to finalize().
Unlike :attr: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
before finalize() is called - the number to report as “how long
did the run take” alongside the region breakdown.
Returns
float or None
:attr:finalize_time minus :attr:run_start_time, or None if
either is missing - an older file, or a run that set up
profiling (or called finalize) some other way than
ProfileManager.setup()/finalize().
call_graphmethod#
def call_graph(rank: int = 0, include=None, exclude=None) -> list[dict]Return call relationships without timestamps or durations.
New profiles persist call_id and parent_id for every Python
event. Legacy profiles fall back to the timestamp-based call stack.
The returned nodes contain only call_id, parent_id, name,
call_index and depth.
Filtering renests: a call whose parent was excluded is reported under
its nearest surviving ancestor, with the depth that implies, exactly
as :meth:call_stack does. Leaving the excluded id in parent_id
would hand back a graph with edges pointing at nodes that are not in
it.
call_id is unique within this rank, not across the file - each
rank numbers its own calls.
call_stackmethod#
def call_stack(rank: int = 0, include: list[str] | str | None = None, exclude: list[str] | str | None = None, relative: bool = True, origin: float | None = None) -> list[dict]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 .prof
export and the speedscope export use.
Parameters
rank : int, optional
Rank whose calls to reconstruct (default: 0).
include, exclude : list of str or str, optional
Regex patterns selecting which regions to include, matched as in
:meth:get_regions.
relative : bool, optional
If True (default), timestamps start at zero; see :meth:events.
origin : float, optional
Measure from this timestamp instead; see :meth:events.
Returns
List[dict]
One entry per call, parents before children, with keys call_id,
name, start, end, duration, depth and
parent (the enclosing call’s id, or None). See
:func:~scope_profiler.call_stack.build_call_stack.
default_titlemethod#
def default_title() -> strHeading 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
str
e.g. "results/run_a.h5 (128 ranks)".
eventsmethod#
def events(include: list[str] | str | None = None, exclude: list[str] | str | None = None, ranks: list[int] | int | None = None, relative: bool = True, origin: float | None = None) -> list[dict]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, exclude : list of str or str, optional
Regex patterns selecting which regions to include, matched as in
:meth: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
:attr:time_origin — the start time registered by
ProfileManager.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; pass
origin=results.minimum_start_time to zero the timeline on the
first region entry regardless of what the run registered.
Returns
List[dict]
Entries with keys name, rank, call_index, start,
end and duration, in seconds, ordered by region (as in
:meth:get_regions) then rank then call order. Entries also carry
gpu_duration when CUDA-event timing was enabled for that
region.
Examples
for event in results.events(include=“solve”): # doctest: +SKIP … print(event[“rank”], event[“start”], event[“duration”])
from_h5methodclassmethod#
def from_h5(file_path: str | Path, verbose: bool = False) -> ProfilingResultsLoad a merged profiling file written by
:meth:ProfileManager.finalize <scope_profiler.profile_manager.ProfileManager.finalize>::
results = ProfilingResults.from_h5("profiling_data.h5")results.print_summary():func:scope_profiler.read_h5 is the same thing under a shorter name.
Parameters
file_path : str | Path Path to the merged HDF5 file containing profiling data. verbose : bool, optional Print each rank group as it is read (default: False).
Returns
ProfilingResults The run’s profiling data, of whichever class this was called on.
Raises
FileNotFoundError If the specified HDF5 file does not exist.
get_likwid_regionmethod#
def get_likwid_region(tag: str, rank: int = 0) -> LikwidRegionResultGet one region’s LIKWID results for a single rank.
Parameters
tag : str LIKWID marker region tag (the profiled region’s name). rank : int, optional Rank to read from (default: 0).
Returns
LikwidRegionResult The region’s counters, event names and derived metrics.
Raises
KeyError If the rank recorded no LIKWID data or has no such region.
get_likwid_regionsmethod#
def get_likwid_regions(rank: int | None = None) -> dictGet 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
dict
With rank given, a mapping of region tag to
:class:~scope_profiler.likwid_data.LikwidRegionResult; otherwise a
mapping of rank to such a dict. Empty when the run did not use
LIKWID.
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_eventsmethod#
def get_perf_events(rank: int | None = None) -> dictReturn aggregated Linux perf_event_open counts by region.
get_regionmethod#
def get_region(region_name: str) -> MPIRegionRetrieve profiling data for a specific region.
Parameters
region_name : str Name of the region to retrieve.
Returns
Region Region object containing profiling data for all ranks.
Raises
KeyError If the specified region name does not exist.
get_regionsmethod#
def get_regions(include: list[str] | str | None = None, exclude: list[str] | str | None = None) -> list[MPIRegion]Get a list of all regions in order of appearance.
Returns
List[Region] List of Region objects.
lane_labelmethod#
def lane_label(lane: int, rank: int = 0) -> strA human-readable name for one lane of rank.
Lanes are the stacks a rank’s calls were reconstructed into (see
:func:~scope_profiler.concurrency.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.
likwid_to_dataframemethod#
def 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
pandas.DataFrame Empty if there is no LIKWID data.
Raises
ImportError If pandas is not installed.
print_likwid_summarymethod#
def print_likwid_summary(stream=None) -> NonePrint every LIKWID region’s events and derived metrics.
Parameters
stream : file-like, optional Destination (default: stdout).
print_summarymethod#
def print_summary(include: list[str] | str | None = None, exclude: list[str] | str | None = None, ranks: list[int] | None = None, sort: str = 'start', title: str | None = None, stream=None, suppress_notes: bool = False, columns: list[str] | str | None = None, percentage_mode: str = 'coverage') -> NonePrint a region summary table, aggregated over ranks.
Renders the same table as scope-profiler inspect and the summary
printed by ProfileManager.finalize().
Parameters
include, exclude : list of str or str, optional
Regex patterns selecting which regions to print, matched as in
:meth: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, std or name.
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, total and
avg. percent is relative to scope_profiler.session;
parent_percent is relative to the enclosing call path. Use
region for the region-name column.
percentage_mode : {“coverage”, “exclusive”}, optional
Quantity used for % session. Defaults to wall-clock coverage;
use exclusive to attribute time after nested regions.
Notes
Does nothing on a non-root rank (see :attr:is_root), so a parallel
script can call it unguarded and print the table once.
summarymethod#
def summary(include: list[str] | str | None = None, exclude: list[str] | str | None = None) -> list[dict]Summarize every region, aggregated over ranks.
Parameters
include, exclude : list of str or str, optional
Regex patterns selecting which regions to summarize, matched as in
:meth:get_regions.
Returns
List[dict]
One dict per region (see
:meth:~scope_profiler.mpi_region.MPIRegion.get_summary), ordered
by first start time. inclusive_duration includes nested
regions; exclusive_duration excludes them. Durations are in
seconds.
thread_summarymethod#
def thread_summary(rank: int = 0) -> list[dict]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
list of dict
One entry per thread with keys index, name, alive,
wall_time, cpu_time, num_calls and
region_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.
to_dataframemethod#
def to_dataframe(include: list[str] | str | None = None, exclude: list[str] | str | None = None, per_rank: bool = False)Return the region summaries as a pandas DataFrame.
Parameters
include, exclude : list of str or str, optional
Regex patterns selecting which regions to include, matched as in
:meth:get_regions.
per_rank : bool, optional
If True, emit one row per (region, rank) with a rank column
instead of one aggregated row per region (default: False).
Returns
pandas.DataFrame Region statistics, with durations in seconds.
Raises
ImportError If pandas is not installed.
to_events_dataframemethod#
def to_events_dataframe(include: list[str] | str | None = None, exclude: list[str] | str | None = None, ranks: list[int] | int | None = None, relative: bool = True, origin: float | None = None)Return every recorded call as a pandas DataFrame (one row per call).
Parameters
include, exclude, ranks, relative, origin
As in :meth:events.
Returns
pandas.DataFrame
Columns name, rank, call_index, start, end and
duration, with times in seconds. A gpu_duration column is
included when any selected event has CUDA-event timing.
Raises
ImportError If pandas is not installed.
merge_resultsfunction#
def merge_results(*result_sets, label: str | None = 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
ProfilingResults One result set holding every region of every input.
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.