scope-profiler 0.2.4__tar.gz → 0.2.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/PKG-INFO +1 -1
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/pyproject.toml +1 -1
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/plotting_scripts.py +277 -65
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/post_processing.py +107 -52
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/test_post_processing.py +162 -9
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler.egg-info/PKG-INFO +1 -1
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/README.md +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/setup.cfg +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/__init__.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/__main__.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/h5reader.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/inspection.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/metadata.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/mpi_launch.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/mpi_region.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/prof_export.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/profile_config.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/profile_manager.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/region.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/region_profiler.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/speedscope_export.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/summary.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/__init__.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/check_mpi_launch.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/examples.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/examples_pylikwid.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/pylikwid_readme.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/test_app.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/test_buffer_growth.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/test_inspection.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/test_metadata.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/test_mpi.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/test_overhead.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/test_prof_export.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/test_reader_api.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/test_readme.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/test_repeated_finalize.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/test_speedscope_export.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/unit/__init__.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/unit/test_mpi_launch.py +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler.egg-info/SOURCES.txt +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler.egg-info/dependency_links.txt +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler.egg-info/entry_points.txt +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler.egg-info/requires.txt +0 -0
- {scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler.egg-info/top_level.txt +0 -0
|
@@ -99,32 +99,44 @@ def _get_cmap_colors(cmap: str, n_colors: int) -> list[str]:
|
|
|
99
99
|
def _add_gantt_bars(
|
|
100
100
|
canvas,
|
|
101
101
|
row: int | None,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
durations: Sequence[float],
|
|
105
|
-
colors: Sequence[str],
|
|
102
|
+
lanes: Sequence[str],
|
|
103
|
+
bars: Sequence[tuple[int, float, float, str]],
|
|
106
104
|
alpha: float = 0.7,
|
|
107
105
|
) -> None:
|
|
108
|
-
"""Draw ``Canvas.gantt`` bars,
|
|
109
|
-
|
|
110
|
-
``Canvas.gantt``
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
106
|
+
"""Draw ``Canvas.gantt`` bars, every call of a lane on that lane's row.
|
|
107
|
+
|
|
108
|
+
``Canvas.gantt`` puts bar *i* of a call on row *i* (``y =
|
|
109
|
+
arange(len(tasks))``, on both backends) and takes a single color for the
|
|
110
|
+
whole call, since maxplotlib's Plotly backend reads ``color`` as one
|
|
111
|
+
RGB(A) value. Each drawing call therefore passes one entry per lane --- so
|
|
112
|
+
the entry index is the lane's row --- and only fills in the bars sharing
|
|
113
|
+
one color; the rest are NaN, which draws nothing on either backend.
|
|
114
|
+
|
|
115
|
+
Lanes hold several calls, so bars are grouped by their position within the
|
|
116
|
+
lane as well: the *k*-th call of every lane goes into the same group,
|
|
117
|
+
leaving at most one bar per lane per drawing call.
|
|
116
118
|
"""
|
|
117
119
|
col = None if row is None else 0
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
n_lanes = len(lanes)
|
|
121
|
+
|
|
122
|
+
# (color, index within the lane) -> the bars drawn together.
|
|
123
|
+
groups: dict[tuple[str, int], list[tuple[int, float, float]]] = defaultdict(list)
|
|
124
|
+
calls_per_lane: dict[int, int] = defaultdict(int)
|
|
125
|
+
for lane, start, duration, color in bars:
|
|
126
|
+
slot = calls_per_lane[lane]
|
|
127
|
+
calls_per_lane[lane] += 1
|
|
128
|
+
groups[(color, slot)].append((lane, start, duration))
|
|
129
|
+
|
|
130
|
+
for (color, _), group in groups.items():
|
|
131
|
+
starts = np.full(n_lanes, np.nan)
|
|
132
|
+
durations = np.full(n_lanes, np.nan)
|
|
133
|
+
for lane, start, duration in group:
|
|
134
|
+
starts[lane] = start
|
|
135
|
+
durations[lane] = duration
|
|
124
136
|
canvas.gantt(
|
|
125
|
-
list(
|
|
126
|
-
|
|
127
|
-
|
|
137
|
+
list(lanes),
|
|
138
|
+
starts,
|
|
139
|
+
durations,
|
|
128
140
|
row=row,
|
|
129
141
|
col=col,
|
|
130
142
|
color=color,
|
|
@@ -169,19 +181,6 @@ def _render(
|
|
|
169
181
|
_close_matplotlib_figure(canvas)
|
|
170
182
|
|
|
171
183
|
|
|
172
|
-
def _block_ticks(tasks: Sequence[str]) -> tuple[list[float], list[str]]:
|
|
173
|
-
"""Return one tick per run of identical task names, at the run's centre."""
|
|
174
|
-
ticks: list[float] = []
|
|
175
|
-
labels: list[str] = []
|
|
176
|
-
start = 0
|
|
177
|
-
for index in range(1, len(tasks) + 1):
|
|
178
|
-
if index == len(tasks) or tasks[index] != tasks[start]:
|
|
179
|
-
ticks.append((start + index - 1) / 2)
|
|
180
|
-
labels.append(tasks[start])
|
|
181
|
-
start = index
|
|
182
|
-
return ticks, labels
|
|
183
|
-
|
|
184
|
-
|
|
185
184
|
def _panel_gridspec(
|
|
186
185
|
fig_width: float, fig_height: float, label_chars: int, multi_panel: bool
|
|
187
186
|
) -> dict:
|
|
@@ -575,30 +574,43 @@ def plot_gantt(
|
|
|
575
574
|
else:
|
|
576
575
|
print(f"Plotting combined Gantt chart for files: {', '.join(labels)}")
|
|
577
576
|
|
|
578
|
-
#
|
|
579
|
-
# panel is as tall as the number of
|
|
580
|
-
# region form a contiguous block labelled at its centre.
|
|
577
|
+
# One lane per (region, rank): every call of a region lands on the same
|
|
578
|
+
# row, so a panel is as tall as the number of lanes it draws.
|
|
581
579
|
single_panel = len(prepared) == 1
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
580
|
+
panel_lanes: list[list[str]] = []
|
|
581
|
+
panel_bars: list[list[tuple[int, float, float, str]]] = []
|
|
582
|
+
for _, regions, selected_ranks, first_start_time in prepared:
|
|
583
|
+
lanes: list[str] = []
|
|
584
|
+
bars: list[tuple[int, float, float, str]] = []
|
|
585
|
+
for region in regions:
|
|
586
|
+
for rank in selected_ranks:
|
|
587
|
+
# A region need not have been entered on every rank.
|
|
588
|
+
if rank not in region:
|
|
589
|
+
continue
|
|
590
|
+
region_data = region[rank]
|
|
591
|
+
if not len(region_data.start_times):
|
|
592
|
+
continue
|
|
593
|
+
lane = len(lanes)
|
|
594
|
+
lanes.append(f"{region.name} (rank {rank})")
|
|
595
|
+
color = _to_hex(region.color)
|
|
596
|
+
for start, end in zip(region_data.start_times, region_data.end_times):
|
|
597
|
+
bars.append(
|
|
598
|
+
(
|
|
599
|
+
lane,
|
|
600
|
+
float(start - first_start_time),
|
|
601
|
+
float(end - start),
|
|
602
|
+
color,
|
|
603
|
+
)
|
|
604
|
+
)
|
|
605
|
+
panel_lanes.append(lanes)
|
|
606
|
+
panel_bars.append(bars)
|
|
607
|
+
|
|
596
608
|
if not any(panel_bars):
|
|
597
609
|
raise ValueError("No calls recorded for the requested ranks.")
|
|
598
610
|
|
|
599
|
-
panel_heights = [max(2.5, 0.
|
|
611
|
+
panel_heights = [max(2.5, 0.35 * len(lanes)) for lanes in panel_lanes]
|
|
600
612
|
fig_width, fig_height = 12.0, 1.0 + sum(panel_heights)
|
|
601
|
-
lane_label_chars = max(len(
|
|
613
|
+
lane_label_chars = max(len(lane) for lanes in panel_lanes for lane in lanes)
|
|
602
614
|
canvas = Canvas(
|
|
603
615
|
nrows=len(prepared),
|
|
604
616
|
ncols=1,
|
|
@@ -608,27 +620,20 @@ def plot_gantt(
|
|
|
608
620
|
),
|
|
609
621
|
)
|
|
610
622
|
|
|
611
|
-
for idx, (label, bars) in enumerate(zip(labels, panel_bars)):
|
|
623
|
+
for idx, (label, lanes, bars) in enumerate(zip(labels, panel_lanes, panel_bars)):
|
|
612
624
|
row = None if single_panel else idx
|
|
613
625
|
col = None if single_panel else 0
|
|
614
626
|
|
|
615
|
-
|
|
616
|
-
starts = [bar[1] for bar in bars]
|
|
617
|
-
durations = [bar[2] for bar in bars]
|
|
618
|
-
colors = [bar[3] for bar in bars]
|
|
619
|
-
_add_gantt_bars(canvas, row, tasks, starts, durations, colors)
|
|
627
|
+
_add_gantt_bars(canvas, row, lanes, bars)
|
|
620
628
|
|
|
621
|
-
|
|
622
|
-
# repeating the region name on every call's row.
|
|
623
|
-
yticks, yticklabels = _block_ticks(tasks)
|
|
624
|
-
canvas.set_yticks(yticks, labels=yticklabels, row=row, col=col)
|
|
629
|
+
canvas.set_yticks(list(range(len(lanes))), labels=lanes, row=row, col=col)
|
|
625
630
|
canvas.set_xlim(
|
|
626
631
|
0,
|
|
627
|
-
max(start + duration for start, duration in
|
|
632
|
+
max(start + duration for _, start, duration, _ in bars),
|
|
628
633
|
row=row,
|
|
629
634
|
col=col,
|
|
630
635
|
)
|
|
631
|
-
canvas.set_ylim(-0.6, len(
|
|
636
|
+
canvas.set_ylim(-0.6, len(lanes) - 0.4, row=row, col=col)
|
|
632
637
|
canvas.set_xlabel("Time (seconds)", row=row, col=col)
|
|
633
638
|
canvas.set_title(
|
|
634
639
|
"Profiling Gantt Chart" if single_panel else label, row=row, col=col
|
|
@@ -1005,6 +1010,213 @@ def plot_durations(
|
|
|
1005
1010
|
return saved_paths
|
|
1006
1011
|
|
|
1007
1012
|
|
|
1013
|
+
def _duration_timeseries(
|
|
1014
|
+
region,
|
|
1015
|
+
ranks: list[int] | None,
|
|
1016
|
+
first_start_time: float,
|
|
1017
|
+
) -> dict[str, np.ndarray] | None:
|
|
1018
|
+
"""Aggregate one region's calls over ranks into a duration-versus-time series.
|
|
1019
|
+
|
|
1020
|
+
Calls are matched across ranks by call index, so point ``i`` describes the
|
|
1021
|
+
i-th call of the region on every rank that got that far. Ranks that stopped
|
|
1022
|
+
calling the region earlier simply drop out of the later points, which keeps
|
|
1023
|
+
ragged call counts (e.g. a rank-dependent number of iterations) usable.
|
|
1024
|
+
|
|
1025
|
+
Returns ``None`` if no selected rank recorded any call.
|
|
1026
|
+
"""
|
|
1027
|
+
if ranks is None:
|
|
1028
|
+
selected_ranks = sorted(region.regions)
|
|
1029
|
+
else:
|
|
1030
|
+
selected_ranks = [rank for rank in ranks if rank in region.regions]
|
|
1031
|
+
|
|
1032
|
+
per_rank = [
|
|
1033
|
+
(rank, region.regions[rank])
|
|
1034
|
+
for rank in selected_ranks
|
|
1035
|
+
if region.regions[rank].durations.size
|
|
1036
|
+
]
|
|
1037
|
+
if not per_rank:
|
|
1038
|
+
return None
|
|
1039
|
+
|
|
1040
|
+
max_calls = max(len(region_data.durations) for _, region_data in per_rank)
|
|
1041
|
+
times, means, minima, maxima, counts = [], [], [], [], []
|
|
1042
|
+
for index in range(max_calls):
|
|
1043
|
+
starts = [
|
|
1044
|
+
float(region_data.start_times[index]) - first_start_time
|
|
1045
|
+
for _, region_data in per_rank
|
|
1046
|
+
if len(region_data.durations) > index
|
|
1047
|
+
]
|
|
1048
|
+
durations = [
|
|
1049
|
+
float(region_data.durations[index])
|
|
1050
|
+
for _, region_data in per_rank
|
|
1051
|
+
if len(region_data.durations) > index
|
|
1052
|
+
]
|
|
1053
|
+
times.append(float(np.mean(starts)))
|
|
1054
|
+
means.append(float(np.mean(durations)))
|
|
1055
|
+
minima.append(float(np.min(durations)))
|
|
1056
|
+
maxima.append(float(np.max(durations)))
|
|
1057
|
+
counts.append(len(durations))
|
|
1058
|
+
|
|
1059
|
+
return {
|
|
1060
|
+
"time": np.asarray(times, dtype=float),
|
|
1061
|
+
"mean": np.asarray(means, dtype=float),
|
|
1062
|
+
"min": np.asarray(minima, dtype=float),
|
|
1063
|
+
"max": np.asarray(maxima, dtype=float),
|
|
1064
|
+
"num_ranks": np.asarray(counts, dtype=int),
|
|
1065
|
+
"ranks": np.asarray([rank for rank, _ in per_rank], dtype=int),
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
def plot_duration_timeseries(
|
|
1070
|
+
profiling_data: ProfilingH5Reader | Sequence[ProfilingH5Reader],
|
|
1071
|
+
ranks: list[int] | int | None = None,
|
|
1072
|
+
include: list[str] | str | None = None,
|
|
1073
|
+
exclude: list[str] | str | None = None,
|
|
1074
|
+
filepath: str | None = None,
|
|
1075
|
+
show: bool = False,
|
|
1076
|
+
verbose: bool = True,
|
|
1077
|
+
cmap: str = DEFAULT_CMAP,
|
|
1078
|
+
data_filepath: str | Path | None = None,
|
|
1079
|
+
data_format: str = "csv",
|
|
1080
|
+
backend: str = "matplotlib",
|
|
1081
|
+
) -> None:
|
|
1082
|
+
"""Plot each region's call duration over wall-clock time, with a min-max band.
|
|
1083
|
+
|
|
1084
|
+
One line per region tracks the mean duration over the ranks that recorded
|
|
1085
|
+
each call, shaded between the minimum and maximum duration seen across
|
|
1086
|
+
those ranks, so rank imbalance shows up as a widening band.
|
|
1087
|
+
|
|
1088
|
+
Parameters
|
|
1089
|
+
----------
|
|
1090
|
+
backend : str
|
|
1091
|
+
Backend to use for rendering: "matplotlib" (default) or "plotly".
|
|
1092
|
+
"""
|
|
1093
|
+
Canvas = _get_canvas()
|
|
1094
|
+
readers = _as_readers(profiling_data)
|
|
1095
|
+
if not readers:
|
|
1096
|
+
raise ValueError("No profiling data provided.")
|
|
1097
|
+
|
|
1098
|
+
normalized_ranks = _normalize_ranks(ranks)
|
|
1099
|
+
|
|
1100
|
+
reader_regions = []
|
|
1101
|
+
all_region_names: set[str] = set()
|
|
1102
|
+
for reader in readers:
|
|
1103
|
+
regions = reader.get_regions(include=include, exclude=exclude)
|
|
1104
|
+
if not regions:
|
|
1105
|
+
raise ValueError("No regions matched the selected filters.")
|
|
1106
|
+
all_region_names.update(region.name for region in regions)
|
|
1107
|
+
reader_regions.append((reader, regions))
|
|
1108
|
+
|
|
1109
|
+
color_map = _region_color_map(all_region_names, cmap=cmap)
|
|
1110
|
+
|
|
1111
|
+
prepared = []
|
|
1112
|
+
for reader, regions in reader_regions:
|
|
1113
|
+
first_start_time = reader.minimum_start_time
|
|
1114
|
+
series = [
|
|
1115
|
+
(
|
|
1116
|
+
region.name,
|
|
1117
|
+
_duration_timeseries(region, normalized_ranks, first_start_time),
|
|
1118
|
+
)
|
|
1119
|
+
for region in regions
|
|
1120
|
+
]
|
|
1121
|
+
series = [(name, values) for name, values in series if values is not None]
|
|
1122
|
+
if series:
|
|
1123
|
+
prepared.append((reader, series))
|
|
1124
|
+
|
|
1125
|
+
if not prepared:
|
|
1126
|
+
raise ValueError("No calls recorded for the requested ranks.")
|
|
1127
|
+
|
|
1128
|
+
labels = _unique_labels([reader.file_path.stem for reader, _ in prepared])
|
|
1129
|
+
|
|
1130
|
+
if data_filepath:
|
|
1131
|
+
records = []
|
|
1132
|
+
for label, (_, series) in zip(labels, prepared):
|
|
1133
|
+
for region_name, values in series:
|
|
1134
|
+
for index in range(values["time"].size):
|
|
1135
|
+
records.append(
|
|
1136
|
+
[
|
|
1137
|
+
label,
|
|
1138
|
+
region_name,
|
|
1139
|
+
index,
|
|
1140
|
+
float(values["time"][index]),
|
|
1141
|
+
float(values["mean"][index]),
|
|
1142
|
+
float(values["min"][index]),
|
|
1143
|
+
float(values["max"][index]),
|
|
1144
|
+
int(values["num_ranks"][index]),
|
|
1145
|
+
]
|
|
1146
|
+
)
|
|
1147
|
+
header = [
|
|
1148
|
+
"file",
|
|
1149
|
+
"region",
|
|
1150
|
+
"call_index",
|
|
1151
|
+
"time_seconds",
|
|
1152
|
+
"mean_duration_seconds",
|
|
1153
|
+
"min_duration_seconds",
|
|
1154
|
+
"max_duration_seconds",
|
|
1155
|
+
"num_ranks",
|
|
1156
|
+
]
|
|
1157
|
+
if data_format == "json":
|
|
1158
|
+
points = [dict(zip(header, record)) for record in records]
|
|
1159
|
+
colors_map = {
|
|
1160
|
+
name: _to_hex(color) for name, color in sorted(color_map.items())
|
|
1161
|
+
}
|
|
1162
|
+
_write_json(data_filepath, {"points": points, "colors": colors_map})
|
|
1163
|
+
else:
|
|
1164
|
+
_write_csv(data_filepath, header, records)
|
|
1165
|
+
|
|
1166
|
+
if verbose:
|
|
1167
|
+
print("Plotting duration over time for files: " + ", ".join(labels))
|
|
1168
|
+
|
|
1169
|
+
single_panel = len(prepared) == 1
|
|
1170
|
+
fig_width, fig_height = 12.0, 1.0 + 4.0 * len(prepared)
|
|
1171
|
+
canvas = Canvas(
|
|
1172
|
+
nrows=len(prepared),
|
|
1173
|
+
ncols=1,
|
|
1174
|
+
figsize=(fig_width, fig_height),
|
|
1175
|
+
# Duration tick labels plus the "Duration (seconds)" axis label.
|
|
1176
|
+
gridspec_kw=_panel_gridspec(fig_width, fig_height, 12, not single_panel),
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
for idx, (reader, series) in enumerate(prepared):
|
|
1180
|
+
row = None if single_panel else idx
|
|
1181
|
+
col = None if single_panel else 0
|
|
1182
|
+
|
|
1183
|
+
for region_name, values in series:
|
|
1184
|
+
color = _to_hex(color_map[region_name])
|
|
1185
|
+
canvas.fill_between(
|
|
1186
|
+
values["time"],
|
|
1187
|
+
values["min"],
|
|
1188
|
+
values["max"],
|
|
1189
|
+
row=row,
|
|
1190
|
+
col=col,
|
|
1191
|
+
color=color,
|
|
1192
|
+
alpha=0.25,
|
|
1193
|
+
)
|
|
1194
|
+
canvas.add_line(
|
|
1195
|
+
values["time"],
|
|
1196
|
+
values["mean"],
|
|
1197
|
+
row=row,
|
|
1198
|
+
col=col,
|
|
1199
|
+
linewidth=1.8,
|
|
1200
|
+
color=color,
|
|
1201
|
+
label=region_name,
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
canvas.set_xlabel("Time (seconds)", row=row, col=col)
|
|
1205
|
+
canvas.set_ylabel("Duration per call (seconds)", row=row, col=col)
|
|
1206
|
+
canvas.set_title(
|
|
1207
|
+
"Region duration over time" if single_panel else reader.file_path.stem,
|
|
1208
|
+
row=row,
|
|
1209
|
+
col=col,
|
|
1210
|
+
)
|
|
1211
|
+
canvas.set_grid(True, row=row, col=col)
|
|
1212
|
+
canvas.set_legend(row=row, col=col)
|
|
1213
|
+
|
|
1214
|
+
if not single_panel:
|
|
1215
|
+
canvas.suptitle("Region duration over time")
|
|
1216
|
+
|
|
1217
|
+
_render(canvas, filepath, show, backend)
|
|
1218
|
+
|
|
1219
|
+
|
|
1008
1220
|
def plot_speedup(
|
|
1009
1221
|
profiling_data: ProfilingH5Reader | Sequence[ProfilingH5Reader],
|
|
1010
1222
|
x_field: str = "num_ranks",
|
|
@@ -7,6 +7,7 @@ import os
|
|
|
7
7
|
from scope_profiler.h5reader import ProfilingH5Reader
|
|
8
8
|
from scope_profiler.plotting_scripts import (
|
|
9
9
|
DEFAULT_CMAP,
|
|
10
|
+
plot_duration_timeseries,
|
|
10
11
|
plot_durations,
|
|
11
12
|
plot_flame,
|
|
12
13
|
plot_gantt,
|
|
@@ -60,7 +61,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
60
61
|
help=(
|
|
61
62
|
"Directory where outputs are saved "
|
|
62
63
|
"(gantt_plot.png, flame_plot.png, durations_plot.png, "
|
|
63
|
-
"optional speedup_plot.png,
|
|
64
|
+
"duration_timeseries_plot.png, optional speedup_plot.png, "
|
|
65
|
+
"and region_statistics.json)"
|
|
64
66
|
),
|
|
65
67
|
)
|
|
66
68
|
parser.add_argument(
|
|
@@ -90,16 +92,30 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
90
92
|
"Supports comma-separated values and ranges (e.g., 1-3,5)."
|
|
91
93
|
),
|
|
92
94
|
)
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"--plots",
|
|
97
|
+
"-p",
|
|
98
|
+
nargs="*",
|
|
99
|
+
type=str,
|
|
100
|
+
choices=["gantt", "flame", "durations", "timeseries", "speedup"],
|
|
101
|
+
default=None,
|
|
102
|
+
help=(
|
|
103
|
+
"Which plots to generate (default: all). "
|
|
104
|
+
"Choices: gantt, flame, durations, timeseries, speedup. "
|
|
105
|
+
"Example: --plots gantt durations"
|
|
106
|
+
),
|
|
107
|
+
)
|
|
93
108
|
parser.add_argument(
|
|
94
109
|
"--metrics",
|
|
95
110
|
"-m",
|
|
96
111
|
nargs="*",
|
|
97
112
|
type=str,
|
|
98
113
|
choices=["avg", "min", "max", "total"],
|
|
99
|
-
default=
|
|
114
|
+
default=["total"],
|
|
100
115
|
help=(
|
|
101
116
|
"Which duration statistics to include in the durations bar plot "
|
|
102
|
-
"(default:
|
|
117
|
+
"(default: total). "
|
|
118
|
+
"Example: --metrics avg min max total"
|
|
103
119
|
),
|
|
104
120
|
)
|
|
105
121
|
parser.add_argument(
|
|
@@ -140,7 +156,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
140
156
|
action="store_true",
|
|
141
157
|
help=(
|
|
142
158
|
"Also write the exact data behind each plot as a data file "
|
|
143
|
-
"(gantt_data, flame_data, durations_data,
|
|
159
|
+
"(gantt_data, flame_data, durations_data, "
|
|
160
|
+
"duration_timeseries_data, optional speedup_data; "
|
|
144
161
|
"see --export-data-format for the file extension/content), so "
|
|
145
162
|
"charts can be reconstructed later without the original HDF5 "
|
|
146
163
|
"files. Requires -o/--output."
|
|
@@ -243,6 +260,9 @@ def main(argv: list[str] | None = None):
|
|
|
243
260
|
"--export-speedscope."
|
|
244
261
|
)
|
|
245
262
|
|
|
263
|
+
_ALL_PLOTS = {"gantt", "flame", "durations", "timeseries", "speedup"}
|
|
264
|
+
selected_plots: set[str] = set(args.plots) if args.plots is not None else _ALL_PLOTS
|
|
265
|
+
|
|
246
266
|
if args.ranks:
|
|
247
267
|
ranks = []
|
|
248
268
|
for spec in args.ranks:
|
|
@@ -274,11 +294,13 @@ def main(argv: list[str] | None = None):
|
|
|
274
294
|
gantt_path = None
|
|
275
295
|
flame_path = None
|
|
276
296
|
durations_path = None
|
|
297
|
+
timeseries_path = None
|
|
277
298
|
speedup_path = None
|
|
278
299
|
statistics_path = None
|
|
279
300
|
gantt_data_path = None
|
|
280
301
|
flame_data_path = None
|
|
281
302
|
durations_data_path = None
|
|
303
|
+
timeseries_data_path = None
|
|
282
304
|
speedup_data_path = None
|
|
283
305
|
prof_path = None
|
|
284
306
|
prof_paths: list = []
|
|
@@ -291,20 +313,34 @@ def main(argv: list[str] | None = None):
|
|
|
291
313
|
# Plotly's native output is a self-contained interactive page;
|
|
292
314
|
# writing .png from it would additionally require kaleido.
|
|
293
315
|
ext = "html" if args.backend == "plotly" else "png"
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
316
|
+
if "gantt" in selected_plots:
|
|
317
|
+
gantt_path = os.path.join(args.output, f"gantt_plot.{ext}")
|
|
318
|
+
if "flame" in selected_plots:
|
|
319
|
+
flame_path = os.path.join(args.output, f"flame_plot.{ext}")
|
|
320
|
+
if "durations" in selected_plots:
|
|
321
|
+
durations_path = os.path.join(args.output, f"durations_plot.{ext}")
|
|
322
|
+
if "timeseries" in selected_plots:
|
|
323
|
+
timeseries_path = os.path.join(
|
|
324
|
+
args.output, f"duration_timeseries_plot.{ext}"
|
|
325
|
+
)
|
|
326
|
+
if len(readers) > 1 and "speedup" in selected_plots:
|
|
298
327
|
speedup_path = os.path.join(args.output, f"speedup_plot.{ext}")
|
|
299
328
|
statistics_path = os.path.join(args.output, "region_statistics.json")
|
|
300
329
|
if args.export_data:
|
|
301
330
|
data_ext = args.export_data_format
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
args.output, f"
|
|
306
|
-
|
|
307
|
-
|
|
331
|
+
if "gantt" in selected_plots:
|
|
332
|
+
gantt_data_path = os.path.join(args.output, f"gantt_data.{data_ext}")
|
|
333
|
+
if "flame" in selected_plots:
|
|
334
|
+
flame_data_path = os.path.join(args.output, f"flame_data.{data_ext}")
|
|
335
|
+
if "durations" in selected_plots:
|
|
336
|
+
durations_data_path = os.path.join(
|
|
337
|
+
args.output, f"durations_data.{data_ext}"
|
|
338
|
+
)
|
|
339
|
+
if "timeseries" in selected_plots:
|
|
340
|
+
timeseries_data_path = os.path.join(
|
|
341
|
+
args.output, f"duration_timeseries_data.{data_ext}"
|
|
342
|
+
)
|
|
343
|
+
if len(readers) > 1 and "speedup" in selected_plots:
|
|
308
344
|
speedup_data_path = os.path.join(
|
|
309
345
|
args.output, f"speedup_data.{data_ext}"
|
|
310
346
|
)
|
|
@@ -339,47 +375,64 @@ def main(argv: list[str] | None = None):
|
|
|
339
375
|
)
|
|
340
376
|
|
|
341
377
|
if render_plots:
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
378
|
+
if "gantt" in selected_plots:
|
|
379
|
+
plot_gantt(
|
|
380
|
+
profiling_data=readers,
|
|
381
|
+
filepath=gantt_path,
|
|
382
|
+
show=args.show,
|
|
383
|
+
include=args.include,
|
|
384
|
+
exclude=args.exclude,
|
|
385
|
+
ranks=args.ranks,
|
|
386
|
+
cmap=args.cmap,
|
|
387
|
+
data_filepath=gantt_data_path,
|
|
388
|
+
data_format=args.export_data_format,
|
|
389
|
+
backend=args.backend,
|
|
390
|
+
)
|
|
354
391
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
392
|
+
if "flame" in selected_plots:
|
|
393
|
+
plot_flame(
|
|
394
|
+
profiling_data=readers,
|
|
395
|
+
filepath=flame_path,
|
|
396
|
+
show=args.show,
|
|
397
|
+
include=args.include,
|
|
398
|
+
exclude=args.exclude,
|
|
399
|
+
ranks=args.ranks,
|
|
400
|
+
cmap=args.cmap,
|
|
401
|
+
data_filepath=flame_data_path,
|
|
402
|
+
data_format=args.export_data_format,
|
|
403
|
+
backend=args.backend,
|
|
404
|
+
)
|
|
367
405
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
406
|
+
if "durations" in selected_plots:
|
|
407
|
+
durations_paths = plot_durations(
|
|
408
|
+
profiling_data=readers,
|
|
409
|
+
filepath=durations_path,
|
|
410
|
+
show=args.show,
|
|
411
|
+
include=args.include,
|
|
412
|
+
exclude=args.exclude,
|
|
413
|
+
ranks=args.ranks,
|
|
414
|
+
metrics=args.metrics,
|
|
415
|
+
cmap=args.cmap,
|
|
416
|
+
data_filepath=durations_data_path,
|
|
417
|
+
data_format=args.export_data_format,
|
|
418
|
+
backend=args.backend,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
if "timeseries" in selected_plots:
|
|
422
|
+
plot_duration_timeseries(
|
|
423
|
+
profiling_data=readers,
|
|
424
|
+
filepath=timeseries_path,
|
|
425
|
+
show=args.show,
|
|
426
|
+
include=args.include,
|
|
427
|
+
exclude=args.exclude,
|
|
428
|
+
ranks=args.ranks,
|
|
429
|
+
cmap=args.cmap,
|
|
430
|
+
data_filepath=timeseries_data_path,
|
|
431
|
+
data_format=args.export_data_format,
|
|
432
|
+
backend=args.backend,
|
|
433
|
+
)
|
|
381
434
|
|
|
382
|
-
if len(readers) > 1:
|
|
435
|
+
if len(readers) > 1 and "speedup" in selected_plots:
|
|
383
436
|
plot_speedup(
|
|
384
437
|
profiling_data=readers,
|
|
385
438
|
x_field=args.x_field,
|
|
@@ -410,11 +463,13 @@ def main(argv: list[str] | None = None):
|
|
|
410
463
|
gantt_path,
|
|
411
464
|
flame_path,
|
|
412
465
|
*durations_paths,
|
|
466
|
+
timeseries_path,
|
|
413
467
|
speedup_path,
|
|
414
468
|
statistics_path,
|
|
415
469
|
gantt_data_path,
|
|
416
470
|
flame_data_path,
|
|
417
471
|
durations_data_path,
|
|
472
|
+
timeseries_data_path,
|
|
418
473
|
speedup_data_path,
|
|
419
474
|
*prof_paths,
|
|
420
475
|
*speedscope_paths,
|
{scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/test_post_processing.py
RENAMED
|
@@ -7,6 +7,8 @@ import pytest
|
|
|
7
7
|
from scope_profiler.h5reader import ProfilingH5Reader
|
|
8
8
|
from scope_profiler.plotting_scripts import (
|
|
9
9
|
_build_call_stack_intervals,
|
|
10
|
+
_duration_timeseries,
|
|
11
|
+
plot_duration_timeseries,
|
|
10
12
|
plot_durations,
|
|
11
13
|
plot_flame,
|
|
12
14
|
plot_gantt,
|
|
@@ -36,6 +38,11 @@ def _write_sample_h5(path, rank_regions, metadata=None):
|
|
|
36
38
|
)
|
|
37
39
|
|
|
38
40
|
|
|
41
|
+
def _seconds(nanoseconds):
|
|
42
|
+
"""Timestamps are written in nanoseconds; the reader reports seconds."""
|
|
43
|
+
return [value / 1e9 for value in nanoseconds]
|
|
44
|
+
|
|
45
|
+
|
|
39
46
|
def _sample_file_data(rank_count, setup_duration, solve_duration):
|
|
40
47
|
return {
|
|
41
48
|
rank: {
|
|
@@ -56,7 +63,13 @@ def test_plot_durations_comparison(tmp_path):
|
|
|
56
63
|
|
|
57
64
|
readers = [ProfilingH5Reader(file_one), ProfilingH5Reader(file_two)]
|
|
58
65
|
|
|
59
|
-
saved_paths = plot_durations(
|
|
66
|
+
saved_paths = plot_durations(
|
|
67
|
+
readers,
|
|
68
|
+
filepath=out_file,
|
|
69
|
+
show=False,
|
|
70
|
+
verbose=False,
|
|
71
|
+
metrics=["avg", "min", "max", "total"],
|
|
72
|
+
)
|
|
60
73
|
|
|
61
74
|
assert len(saved_paths) == 4
|
|
62
75
|
for metric in ("avg", "min", "max", "total"):
|
|
@@ -65,6 +78,91 @@ def test_plot_durations_comparison(tmp_path):
|
|
|
65
78
|
assert metric_file.stat().st_size > 0
|
|
66
79
|
|
|
67
80
|
|
|
81
|
+
def test_duration_timeseries_bands_span_ranks(tmp_path):
|
|
82
|
+
file_path = tmp_path / "run.h5"
|
|
83
|
+
# Two calls of "solve" per rank, with rank 1 slower on the second call, so
|
|
84
|
+
# the band has to widen between the two points.
|
|
85
|
+
_write_sample_h5(
|
|
86
|
+
file_path,
|
|
87
|
+
{
|
|
88
|
+
0: {"solve": ([0, 100], [10, 110])},
|
|
89
|
+
1: {"solve": ([0, 100], [20, 140])},
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
reader = ProfilingH5Reader(file_path)
|
|
93
|
+
|
|
94
|
+
series = _duration_timeseries(reader.get_region("solve"), None, 0.0)
|
|
95
|
+
|
|
96
|
+
assert list(series["num_ranks"]) == [2, 2]
|
|
97
|
+
assert series["min"] == pytest.approx(_seconds([10, 10]))
|
|
98
|
+
assert series["max"] == pytest.approx(_seconds([20, 40]))
|
|
99
|
+
assert series["mean"] == pytest.approx(_seconds([15, 25]))
|
|
100
|
+
assert series["time"] == pytest.approx(_seconds([0, 100]))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_duration_timeseries_handles_ragged_call_counts(tmp_path):
|
|
104
|
+
file_path = tmp_path / "run.h5"
|
|
105
|
+
_write_sample_h5(
|
|
106
|
+
file_path,
|
|
107
|
+
{
|
|
108
|
+
0: {"solve": ([0, 100], [10, 130])},
|
|
109
|
+
1: {"solve": ([0], [20])},
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
reader = ProfilingH5Reader(file_path)
|
|
113
|
+
|
|
114
|
+
series = _duration_timeseries(reader.get_region("solve"), None, 0.0)
|
|
115
|
+
|
|
116
|
+
# The second call exists on rank 0 only, so its band collapses to a point.
|
|
117
|
+
assert list(series["num_ranks"]) == [2, 1]
|
|
118
|
+
assert series["min"] == pytest.approx(_seconds([10, 30]))
|
|
119
|
+
assert series["max"] == pytest.approx(_seconds([20, 30]))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_duration_timeseries_respects_rank_selection(tmp_path):
|
|
123
|
+
file_path = tmp_path / "run.h5"
|
|
124
|
+
_write_sample_h5(
|
|
125
|
+
file_path,
|
|
126
|
+
{
|
|
127
|
+
0: {"solve": ([0], [10])},
|
|
128
|
+
1: {"solve": ([0], [50])},
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
reader = ProfilingH5Reader(file_path)
|
|
132
|
+
|
|
133
|
+
series = _duration_timeseries(reader.get_region("solve"), [0], 0.0)
|
|
134
|
+
|
|
135
|
+
assert list(series["num_ranks"]) == [1]
|
|
136
|
+
assert series["min"] == pytest.approx(series["max"])
|
|
137
|
+
assert series["max"] == pytest.approx(_seconds([10]))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_plot_duration_timeseries_export_data_json(tmp_path):
|
|
141
|
+
file_path = tmp_path / "run.h5"
|
|
142
|
+
data_file = tmp_path / "duration_timeseries_data.json"
|
|
143
|
+
|
|
144
|
+
_write_sample_h5(file_path, _sample_file_data(2, 10, 20))
|
|
145
|
+
reader = ProfilingH5Reader(file_path)
|
|
146
|
+
|
|
147
|
+
plot_duration_timeseries(
|
|
148
|
+
reader,
|
|
149
|
+
filepath=tmp_path / "duration_timeseries_plot.png",
|
|
150
|
+
show=False,
|
|
151
|
+
verbose=False,
|
|
152
|
+
data_filepath=data_file,
|
|
153
|
+
data_format="json",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
assert (tmp_path / "duration_timeseries_plot.png").stat().st_size > 0
|
|
157
|
+
payload = json.loads(data_file.read_text(encoding="utf-8"))
|
|
158
|
+
assert {point["region"] for point in payload["points"]} == {"setup", "solve"}
|
|
159
|
+
for point in payload["points"]:
|
|
160
|
+
assert point["min_duration_seconds"] <= point["mean_duration_seconds"]
|
|
161
|
+
assert point["mean_duration_seconds"] <= point["max_duration_seconds"]
|
|
162
|
+
assert point["num_ranks"] == 2
|
|
163
|
+
assert set(payload["colors"]) == {"setup", "solve"}
|
|
164
|
+
|
|
165
|
+
|
|
68
166
|
def test_plot_gantt_combined(tmp_path):
|
|
69
167
|
file_one = tmp_path / "run_one.h5"
|
|
70
168
|
file_two = tmp_path / "run_two.h5"
|
|
@@ -81,6 +179,59 @@ def test_plot_gantt_combined(tmp_path):
|
|
|
81
179
|
assert out_file.stat().st_size > 0
|
|
82
180
|
|
|
83
181
|
|
|
182
|
+
def test_plot_gantt_puts_every_call_of_a_region_on_one_lane(tmp_path, monkeypatch):
|
|
183
|
+
"""Repeated calls of a region share one row per rank, not a row each."""
|
|
184
|
+
h5_path = tmp_path / "run.h5"
|
|
185
|
+
_write_sample_h5(
|
|
186
|
+
h5_path,
|
|
187
|
+
{
|
|
188
|
+
rank: {
|
|
189
|
+
"setup": ([0], [10]),
|
|
190
|
+
"solve": ([20, 40, 60], [30, 50, 70]),
|
|
191
|
+
}
|
|
192
|
+
for rank in range(2)
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
bars_by_lane = {}
|
|
197
|
+
|
|
198
|
+
class _RecordingCanvas:
|
|
199
|
+
"""Collect what plot_gantt draws instead of rendering it."""
|
|
200
|
+
|
|
201
|
+
def __init__(self, *args, **kwargs):
|
|
202
|
+
self.lanes = None
|
|
203
|
+
|
|
204
|
+
def gantt(self, tasks, start_times, durations, **kwargs):
|
|
205
|
+
self.lanes = tasks
|
|
206
|
+
for lane, start in enumerate(start_times):
|
|
207
|
+
if not np.isnan(start):
|
|
208
|
+
bars_by_lane.setdefault(lane, []).append(float(start))
|
|
209
|
+
|
|
210
|
+
def __getattr__(self, name):
|
|
211
|
+
# set_yticks, set_xlim, set_title, ... are all no-ops here.
|
|
212
|
+
return lambda *args, **kwargs: None
|
|
213
|
+
|
|
214
|
+
import scope_profiler.plotting_scripts as plotting_scripts
|
|
215
|
+
|
|
216
|
+
canvas = _RecordingCanvas()
|
|
217
|
+
monkeypatch.setattr(
|
|
218
|
+
plotting_scripts, "_get_canvas", lambda: lambda *a, **kw: canvas
|
|
219
|
+
)
|
|
220
|
+
monkeypatch.setattr(plotting_scripts, "_render", lambda *a, **kw: None)
|
|
221
|
+
|
|
222
|
+
plot_gantt(ProfilingH5Reader(h5_path), show=False, verbose=False)
|
|
223
|
+
|
|
224
|
+
# One lane per (region, rank), and solve's three calls share their lane.
|
|
225
|
+
assert canvas.lanes == [
|
|
226
|
+
"setup (rank 0)",
|
|
227
|
+
"setup (rank 1)",
|
|
228
|
+
"solve (rank 0)",
|
|
229
|
+
"solve (rank 1)",
|
|
230
|
+
]
|
|
231
|
+
assert sorted(bars_by_lane) == [0, 1, 2, 3]
|
|
232
|
+
assert [len(bars_by_lane[lane]) for lane in sorted(bars_by_lane)] == [1, 1, 3, 3]
|
|
233
|
+
|
|
234
|
+
|
|
84
235
|
def test_build_call_stack_intervals_reconstructs_nesting(tmp_path):
|
|
85
236
|
# "outer" [0, 100) encloses two sequential "inner" calls, [10, 40) and
|
|
86
237
|
# [50, 90), which in turn each enclose a "leaf" call.
|
|
@@ -294,6 +445,7 @@ def test_post_processing_cli_supports_multiple_files(tmp_path):
|
|
|
294
445
|
|
|
295
446
|
gantt_plot = output_dir / "gantt_plot.png"
|
|
296
447
|
flame_plot = output_dir / "flame_plot.png"
|
|
448
|
+
timeseries_plot = output_dir / "duration_timeseries_plot.png"
|
|
297
449
|
speedup_plot = output_dir / "speedup_plot.png"
|
|
298
450
|
stats_json = output_dir / "region_statistics.json"
|
|
299
451
|
|
|
@@ -301,10 +453,11 @@ def test_post_processing_cli_supports_multiple_files(tmp_path):
|
|
|
301
453
|
assert gantt_plot.stat().st_size > 0
|
|
302
454
|
assert flame_plot.exists()
|
|
303
455
|
assert flame_plot.stat().st_size > 0
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
456
|
+
durations_plot = output_dir / "durations_plot.png"
|
|
457
|
+
assert durations_plot.exists()
|
|
458
|
+
assert durations_plot.stat().st_size > 0
|
|
459
|
+
assert timeseries_plot.exists()
|
|
460
|
+
assert timeseries_plot.stat().st_size > 0
|
|
308
461
|
assert speedup_plot.exists()
|
|
309
462
|
assert speedup_plot.stat().st_size > 0
|
|
310
463
|
assert stats_json.exists()
|
|
@@ -338,10 +491,9 @@ def test_post_processing_cli_supports_wildcard_file_patterns(tmp_path):
|
|
|
338
491
|
assert gantt_plot.stat().st_size > 0
|
|
339
492
|
assert flame_plot.exists()
|
|
340
493
|
assert flame_plot.stat().st_size > 0
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
assert metric_file.stat().st_size > 0
|
|
494
|
+
durations_plot = output_dir / "durations_plot.png"
|
|
495
|
+
assert durations_plot.exists()
|
|
496
|
+
assert durations_plot.stat().st_size > 0
|
|
345
497
|
assert speedup_plot.exists()
|
|
346
498
|
assert speedup_plot.stat().st_size > 0
|
|
347
499
|
assert stats_json.exists()
|
|
@@ -410,6 +562,7 @@ def test_plot_durations_export_data_json(tmp_path):
|
|
|
410
562
|
verbose=False,
|
|
411
563
|
data_filepath=data_file,
|
|
412
564
|
data_format="json",
|
|
565
|
+
metrics=["avg", "min", "max", "total"],
|
|
413
566
|
)
|
|
414
567
|
|
|
415
568
|
payload = json.loads(data_file.read_text(encoding="utf-8"))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/test_buffer_growth.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/test_repeated_finalize.py
RENAMED
|
File without changes
|
{scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/test_speedscope_export.py
RENAMED
|
File without changes
|
|
File without changes
|
{scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler/tests/unit/test_mpi_launch.py
RENAMED
|
File without changes
|
|
File without changes
|
{scope_profiler-0.2.4 → scope_profiler-0.2.6}/src/scope_profiler.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|