access-profiling 0.1__py3-none-any.whl
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.
- access/profiling/__init__.py +32 -0
- access/profiling/access_models.py +84 -0
- access/profiling/cice5_parser.py +80 -0
- access/profiling/cylc_manager.py +209 -0
- access/profiling/cylc_parser.py +146 -0
- access/profiling/esmf_parser.py +183 -0
- access/profiling/experiment.py +263 -0
- access/profiling/fms_parser.py +80 -0
- access/profiling/manager.py +559 -0
- access/profiling/metrics.py +85 -0
- access/profiling/parser.py +263 -0
- access/profiling/payu_manager.py +338 -0
- access/profiling/payujson_parser.py +75 -0
- access/profiling/plotting_utils.py +116 -0
- access/profiling/scaling.py +128 -0
- access/profiling/um_parser.py +242 -0
- access_profiling-0.1.dist-info/METADATA +100 -0
- access_profiling-0.1.dist-info/RECORD +21 -0
- access_profiling-0.1.dist-info/WHEEL +5 -0
- access_profiling-0.1.dist-info/licenses/LICENSE +201 -0
- access_profiling-0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Copyright 2025 ACCESS-NRI and contributors. See the top-level COPYRIGHT file for details.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
from matplotlib.figure import Figure
|
|
6
|
+
|
|
7
|
+
from access.profiling.metrics import ProfilingMetric
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def calculate_column_widths(table_data: list[list], first_col_fraction: float = None) -> list:
|
|
11
|
+
"""Calculate column widths based on content character length and required width for first column.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
table_data (list[list]): Table data including headers. e.g.
|
|
15
|
+
[[ "ncpus", "col1", "col2", "col3"],
|
|
16
|
+
["region1", 0.1, 0.2, 0.3],
|
|
17
|
+
["region2", 1. , 2. , 3. ]]
|
|
18
|
+
first_col_fraction (float): If provided, controls the fraction of the table width
|
|
19
|
+
assigned to the first column. Default None.
|
|
20
|
+
If set to 0.0 or None, all columns have the same width.
|
|
21
|
+
Must be between 0.0 (inclusive) and 1.0 (exclusive).
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
list : Column width fractions, adding up to 1.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ValueError: If table_data has fewer than 2 rows or 2 columns.
|
|
28
|
+
ValueError: If table_data shape is not rectangular, i.e., all rows do not have the same number of columns.
|
|
29
|
+
"""
|
|
30
|
+
if not table_data:
|
|
31
|
+
return []
|
|
32
|
+
|
|
33
|
+
# Check that table has a header row and row-label column, and no missing elements.
|
|
34
|
+
if len(table_data) > 1:
|
|
35
|
+
for row in table_data[1:]:
|
|
36
|
+
if len(row) != len(table_data[0]):
|
|
37
|
+
raise ValueError("Table rows must have the same number of elements")
|
|
38
|
+
else:
|
|
39
|
+
raise ValueError("Table must have at least 2 rows (first row is table header)")
|
|
40
|
+
if len(table_data[0]) < 2:
|
|
41
|
+
raise ValueError("Table must have at least 2 columns (first column is row label)")
|
|
42
|
+
|
|
43
|
+
if first_col_fraction is not None and not (0 <= first_col_fraction < 1):
|
|
44
|
+
raise ValueError("first_col_fraction must be between 0 and 1 (exclusive)")
|
|
45
|
+
|
|
46
|
+
n_cols = len(table_data[0])
|
|
47
|
+
|
|
48
|
+
# Calculate max content length for each column based on no. of chars
|
|
49
|
+
max_lengths = []
|
|
50
|
+
for col in range(n_cols):
|
|
51
|
+
col_lengths = [len(str(row[col])) for row in table_data]
|
|
52
|
+
max_lengths.append(max(col_lengths))
|
|
53
|
+
|
|
54
|
+
if first_col_fraction and first_col_fraction > 0:
|
|
55
|
+
# Set data columns to proportional widths based on content and first_col_fraction
|
|
56
|
+
data_cols_total = sum(max_lengths[1:])
|
|
57
|
+
base_width = (1 - first_col_fraction) / data_cols_total
|
|
58
|
+
|
|
59
|
+
col_widths = [first_col_fraction]
|
|
60
|
+
for length in max_lengths[1:]:
|
|
61
|
+
col_widths.append(length * base_width)
|
|
62
|
+
|
|
63
|
+
else:
|
|
64
|
+
# Equal column width
|
|
65
|
+
total_length = sum(max_lengths)
|
|
66
|
+
col_widths = [length / total_length for length in max_lengths]
|
|
67
|
+
|
|
68
|
+
return col_widths
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def plot_bar_metrics(
|
|
72
|
+
data: dict[str, list[float]],
|
|
73
|
+
region_labels: list[str],
|
|
74
|
+
metric: ProfilingMetric,
|
|
75
|
+
show: bool = True,
|
|
76
|
+
) -> Figure:
|
|
77
|
+
"""Plots a grouped bar chart of a profiling metric over regions.
|
|
78
|
+
|
|
79
|
+
Regions are placed along the x-axis. Within each region group, there is one bar per
|
|
80
|
+
experiment, coloured by experiment name.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
data (dict[str, list[float]]): Mapping of experiment name to a list of metric values,
|
|
84
|
+
one per region (in the same order as ``region_labels``).
|
|
85
|
+
region_labels (list[str]): Ordered list of region display labels for the x-axis.
|
|
86
|
+
metric (ProfilingMetric): The metric being plotted (used for axis labels and title).
|
|
87
|
+
show (bool): Whether to call ``plt.show()``. Default: True.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Figure: The Matplotlib figure containing the bar chart.
|
|
91
|
+
"""
|
|
92
|
+
exp_names = list(data.keys())
|
|
93
|
+
n_experiments = len(exp_names)
|
|
94
|
+
n_regions = len(region_labels)
|
|
95
|
+
|
|
96
|
+
fig, ax = plt.subplots(figsize=(max(8, n_experiments * n_regions * 0.8), 6))
|
|
97
|
+
bar_width = 0.8 / n_experiments
|
|
98
|
+
group_positions = list(range(n_regions))
|
|
99
|
+
|
|
100
|
+
for i, exp_name in enumerate(exp_names):
|
|
101
|
+
offsets = [pos + (i - (n_experiments - 1) / 2) * bar_width for pos in group_positions]
|
|
102
|
+
ax.bar(offsets, data[exp_name], width=bar_width, label=exp_name)
|
|
103
|
+
|
|
104
|
+
ax.set_xticks(group_positions)
|
|
105
|
+
ax.set_xticklabels(region_labels)
|
|
106
|
+
ax.set_xlabel("Region")
|
|
107
|
+
ax.set_ylabel(f"{metric.name} ({metric.units})")
|
|
108
|
+
ax.set_title(f"{metric.description}")
|
|
109
|
+
ax.legend(title="Experiment")
|
|
110
|
+
ax.grid(axis="y", linestyle="--", alpha=0.7)
|
|
111
|
+
fig.tight_layout()
|
|
112
|
+
|
|
113
|
+
if show:
|
|
114
|
+
plt.show()
|
|
115
|
+
|
|
116
|
+
return fig
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Copyright 2025 ACCESS-NRI and contributors. See the top-level COPYRIGHT file for details.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Functions to calculate metrics related to parallel scaling of applications."""
|
|
5
|
+
|
|
6
|
+
import matplotlib.gridspec as gridspec
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import xarray as xr
|
|
9
|
+
from matplotlib.figure import Figure
|
|
10
|
+
|
|
11
|
+
from access.profiling.metrics import ProfilingMetric
|
|
12
|
+
from access.profiling.plotting_utils import calculate_column_widths
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parallel_speedup(stats: xr.Dataset, metric: ProfilingMetric) -> xr.DataArray:
|
|
16
|
+
"""Calculates the parallel speedup from scaling data.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
stats (Dataset): Scaling data, stored as a xarray dataset.
|
|
20
|
+
metric (ProfilingMetric): Metric to use for the speedup calculation.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
DataArray: Parallel speedup.
|
|
24
|
+
Raises:
|
|
25
|
+
ValueError: If metric units are not time (e.g., seconds).
|
|
26
|
+
"""
|
|
27
|
+
if stats[metric].pint.dimensionality != "[time]":
|
|
28
|
+
raise ValueError("Metric units must be time (e.g., seconds)!")
|
|
29
|
+
speedup = stats[metric].sel(ncpus=stats["ncpus"].min()) / stats[metric]
|
|
30
|
+
speedup.name = "speedup"
|
|
31
|
+
return speedup
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parallel_efficiency(stats: xr.Dataset, metric: ProfilingMetric) -> xr.DataArray:
|
|
35
|
+
"""Calculates the parallel efficiency from scaling data.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
stats (Dataset): Scaling data, stored as a xarray dataset.
|
|
39
|
+
metric (ProfilingMetric): Metric to use for the efficiency calculation.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
DataArray: Parallel efficiency.
|
|
43
|
+
"""
|
|
44
|
+
speedup = parallel_speedup(stats, metric)
|
|
45
|
+
eff = speedup * (speedup.ncpus.min() / speedup.ncpus)
|
|
46
|
+
eff = eff.pint.to("percent")
|
|
47
|
+
eff.name = "parallel efficiency"
|
|
48
|
+
return eff
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def plot_scaling_metrics(
|
|
52
|
+
stats: list[xr.Dataset],
|
|
53
|
+
metric: ProfilingMetric,
|
|
54
|
+
xcoordinate: str = "ncpus",
|
|
55
|
+
first_col_fraction: float = 0.4,
|
|
56
|
+
show: bool = True,
|
|
57
|
+
) -> Figure:
|
|
58
|
+
"""Plots parallel speedup and efficiency from a list of datasets
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
stats (list[xr.Dataset]): The raw times to plot.
|
|
62
|
+
metric (ProfilinMetric): The metric to plot for each stat.
|
|
63
|
+
xcoordinate (str): The x-axis variable e.g. "ncpus".
|
|
64
|
+
first_col_fraction (float): The fraction of table width to assign to the row labels. Default 0.4.
|
|
65
|
+
show (bool): Whether to show the generated plot. Default: True.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Figure: The Matplotlib figure on which the scaling plots and table are plotted on.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If region_labels is non-empty
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
# setup plots
|
|
75
|
+
fig = plt.figure(figsize=(15, 6))
|
|
76
|
+
# using gridspec so table can be added
|
|
77
|
+
gs = gridspec.GridSpec(2, 2, height_ratios=[3, 1], hspace=0.3)
|
|
78
|
+
ax1, ax2 = fig.add_subplot(gs[0, 0]), fig.add_subplot(gs[0, 1])
|
|
79
|
+
ax_tbl = fig.add_subplot(gs[1, :])
|
|
80
|
+
|
|
81
|
+
# add table of raw timings
|
|
82
|
+
tbl = [[xcoordinate] + list(stats[0][xcoordinate].values)] # first row
|
|
83
|
+
for stat in stats:
|
|
84
|
+
# calculate efficiency and speedup
|
|
85
|
+
efficiency = parallel_efficiency(stat, metric)
|
|
86
|
+
speedup = parallel_speedup(stat, metric)
|
|
87
|
+
|
|
88
|
+
# plots speedup and efficiency on their respective axes.
|
|
89
|
+
max_eff = 100
|
|
90
|
+
for region in stat.region.values:
|
|
91
|
+
speedup.loc[region, :].plot.line(x=xcoordinate, ax=ax1, marker="o", label=region)
|
|
92
|
+
efficiency.loc[region, :].plot.line(x=xcoordinate, ax=ax2, marker="o", label=region)
|
|
93
|
+
# find max efficiency for setting efficiency axis
|
|
94
|
+
max_eff = max(max_eff, efficiency.loc[region, :].max())
|
|
95
|
+
|
|
96
|
+
tbl.append([region] + [f"{val:.2f}" for val in stat[metric].loc[:, region].pint.dequantify().values])
|
|
97
|
+
|
|
98
|
+
# ideal speedup/scaling
|
|
99
|
+
minx = stat[xcoordinate].values.min()
|
|
100
|
+
nx = len(stat[xcoordinate].values)
|
|
101
|
+
ideal_speedups = [i / minx for i in stat[xcoordinate].values]
|
|
102
|
+
ax1.plot(stat[xcoordinate].values, ideal_speedups, "k:", label="ideal")
|
|
103
|
+
ax2.plot(stat[xcoordinate].values, [100] * nx, "k:", label="ideal")
|
|
104
|
+
|
|
105
|
+
# formatting
|
|
106
|
+
ax1.legend()
|
|
107
|
+
ax1.grid()
|
|
108
|
+
ax2.grid()
|
|
109
|
+
ax2.set_ylim((0, 1.1 * max_eff))
|
|
110
|
+
ax1.set_title("Parallel Speedup")
|
|
111
|
+
ax2.set_title("Parallel Efficiency")
|
|
112
|
+
ax_tbl.axis("off")
|
|
113
|
+
tbl_chart = ax_tbl.table(
|
|
114
|
+
tbl,
|
|
115
|
+
bbox=(0.05, 0, 0.9, 1),
|
|
116
|
+
cellLoc="center",
|
|
117
|
+
colWidths=calculate_column_widths(tbl, first_col_fraction),
|
|
118
|
+
)
|
|
119
|
+
ax_tbl.set_title(f"Timings ({stat[metric].pint.units})")
|
|
120
|
+
for i in range(len(tbl[0])):
|
|
121
|
+
tbl_chart[(0, i)].set_text_props(weight="bold")
|
|
122
|
+
for i in range(len(tbl)):
|
|
123
|
+
tbl_chart[(i, 0)].set_text_props(weight="bold")
|
|
124
|
+
|
|
125
|
+
if show:
|
|
126
|
+
plt.show()
|
|
127
|
+
|
|
128
|
+
return fig
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# Copyright 2025 ACCESS-NRI and contributors. See the top-level COPYRIGHT file for details.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Parser for UM profiling data.
|
|
5
|
+
This routine parses the inclusive timers from the UM output log
|
|
6
|
+
(e.g. ``atm.fort6.pe0`` for UM7) and returns a dictionary of the
|
|
7
|
+
profiling data. Since UM7 and UM13 provides multiple sections with timer
|
|
8
|
+
output - we have chosen to use the 'Wallclock times' sub-section
|
|
9
|
+
within the Inclusive Timer Summary section.
|
|
10
|
+
|
|
11
|
+
The profiling data is assumed to have the following
|
|
12
|
+
format:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
MPP : Inclusive timer summary
|
|
16
|
+
|
|
17
|
+
WALLCLOCK TIMES
|
|
18
|
+
<N> ROUTINE MEAN MEDIAN SD % of mean MAX (PE) MIN (PE)
|
|
19
|
+
1 AS3 Atmos_Phys2 1308.30 1308.30 0.02 0.00% 1308.33 ( 118) 1308.26 ( 221)
|
|
20
|
+
2 AP2 Boundary Layer 956.50 956.13 3.26 0.34% 981.27 ( 136) 953.28 ( 43)
|
|
21
|
+
3 AS5-8 Updates 884.62 885.52 2.89 0.33% 889.49 ( 48) 879.36 ( 212)
|
|
22
|
+
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
CPU TIMES (sorted by wallclock times)
|
|
26
|
+
<N> ROUTINE MEAN MEDIAN SD % of mean MAX (PE) MIN (PE)
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
All columns in the first sub-section, except for the numeric index and the `% of mean`, are parsed and returned.
|
|
32
|
+
For UM versions 13.x, there is an extra 'N' column name that appears to the left of 'ROUTINE'; this 'N' is
|
|
33
|
+
not present in the output from UM v7.x .
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
import logging
|
|
38
|
+
import os
|
|
39
|
+
import re
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
|
|
42
|
+
from access.profiling.metrics import pemax, pemin, tavg, tmax, tmed, tmin, tstd
|
|
43
|
+
from access.profiling.parser import ProfilingParser, _convert_from_string, _read_text_file
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class UMProfilingParser(ProfilingParser):
|
|
49
|
+
"""UM profiling output parser."""
|
|
50
|
+
|
|
51
|
+
# The parsed column names that will be kept. The order needs to match
|
|
52
|
+
# the order of the column names in the input data (defined as ``raw_headers``
|
|
53
|
+
# in the ``read``` method), after discarding the ignored columns.
|
|
54
|
+
_metrics = [tavg, tmed, tstd, tmax, pemax, tmin, pemin]
|
|
55
|
+
|
|
56
|
+
def parse(self, file_path: str | Path | os.PathLike) -> dict:
|
|
57
|
+
"""Parse UM profiling data from a file path.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
file_path (str | Path | os.PathLike): file to parse.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
stats (dict): dictionary of parsed profiling data.
|
|
64
|
+
Ignores two columns ``N``, and ``% over mean`` columns.
|
|
65
|
+
|
|
66
|
+
To keep consistent column names across all parsers, the following
|
|
67
|
+
mapping is used:
|
|
68
|
+
================== ==================
|
|
69
|
+
UM column name Standard metric
|
|
70
|
+
================== ==================
|
|
71
|
+
N - (ignored)
|
|
72
|
+
ROUTINE region
|
|
73
|
+
MEAN tavg
|
|
74
|
+
MEDIAN tmed
|
|
75
|
+
SD tstd
|
|
76
|
+
% of mean - (ignored)
|
|
77
|
+
MAX tmax
|
|
78
|
+
(PE) pemax
|
|
79
|
+
MIN tmin
|
|
80
|
+
(PE) pemin
|
|
81
|
+
================== ==================
|
|
82
|
+
Each key returns a list of values, one for each region. For
|
|
83
|
+
example, if there are 20 regions, ``stats['tavg']`` will
|
|
84
|
+
return a list with 20 values, one each for each of the regions.
|
|
85
|
+
|
|
86
|
+
The assumption is that people will want to look at the same metric
|
|
87
|
+
for *all* regions at a time; if you want to look at all metrics for
|
|
88
|
+
a single region, then you will have to first find the index for the
|
|
89
|
+
``region``, and then extract that index from *each* of the 'metric'
|
|
90
|
+
lists.
|
|
91
|
+
|
|
92
|
+
Any number of column headers can be present at the beginning, i.e., before
|
|
93
|
+
``ROUTINE`` (including ``N`` for UM v13+) and will be ignored when the
|
|
94
|
+
header is being parsed. Such columns must contain integer values for the
|
|
95
|
+
profiling information parsing to work, otherwise (e.g., new columns with float
|
|
96
|
+
data), an ``AssertionError`` is raised.
|
|
97
|
+
|
|
98
|
+
Parsing will fail if there are any unexpected columns at the end, either
|
|
99
|
+
at the end of the expected header, or the end of the expected columns with
|
|
100
|
+
profiling data (regardless of type of the column value)
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
ValueError: If the UM version number can not be found in the input string data.
|
|
104
|
+
ValueError: If a match for any of header, footer or section (i.e., empty section)
|
|
105
|
+
is not found.
|
|
106
|
+
AssertionError: If the expected format is not found in *all* of the lines within the
|
|
107
|
+
profiling section.
|
|
108
|
+
TypeError: If file_path cannot be converted to a valid Path object.
|
|
109
|
+
FileNotFoundError: If file_path doesn't exist or isn't a file.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
stream = _read_text_file(file_path)
|
|
113
|
+
|
|
114
|
+
# First create the local variable with the metrics list
|
|
115
|
+
metrics = self.metrics
|
|
116
|
+
raw_headers = ["ROUTINE", "MEAN", "MEDIAN", "SD", r"\% of mean", "MAX", r"\(PE\)", "MIN", r"\(PE\)"]
|
|
117
|
+
|
|
118
|
+
header = r"MPP : Inclusive timer summary\s+WALLCLOCK TIMES\s*"
|
|
119
|
+
# UM 13 has an extra header 'N' for the numeric row index (that UM7 does not)
|
|
120
|
+
# Writing the pattern this way avoids having to code in UM version dependent patterns
|
|
121
|
+
header += r"\S*\s+"
|
|
122
|
+
# Then skip over white-space-separated header names.
|
|
123
|
+
header += r"\s*".join(raw_headers) + r"\s*"
|
|
124
|
+
header_pattern = re.compile(header, re.MULTILINE)
|
|
125
|
+
header_match = header_pattern.search(stream)
|
|
126
|
+
if not header_match:
|
|
127
|
+
logger.debug("Header pattern: %s", header)
|
|
128
|
+
logger.debug("Input string: %s", stream)
|
|
129
|
+
raise ValueError(f"No matching header found in {file_path}.")
|
|
130
|
+
logger.debug("Found header: %s", header_match.group(0))
|
|
131
|
+
|
|
132
|
+
# This line (and any preceeding whitespace) indicates
|
|
133
|
+
# the end of the profiling data that we want to parse
|
|
134
|
+
footer = r"CPU TIMES \(sorted by wallclock times\)\s*"
|
|
135
|
+
footer_pattern = re.compile(footer, re.MULTILINE)
|
|
136
|
+
footer_match = footer_pattern.search(stream)
|
|
137
|
+
if not footer_match:
|
|
138
|
+
logger.debug("Footer pattern: %s", footer)
|
|
139
|
+
logger.debug("Input string: %s", stream)
|
|
140
|
+
raise ValueError("No matching footer found.")
|
|
141
|
+
logger.debug("Found footer: %s", footer_match.group(0))
|
|
142
|
+
|
|
143
|
+
# Match *everything* between the header and footer (the match could be 0 characters)
|
|
144
|
+
profiling_section_p = re.compile(header + r"(.*)" + footer, re.MULTILINE | re.DOTALL)
|
|
145
|
+
profiling_section = profiling_section_p.search(stream)
|
|
146
|
+
|
|
147
|
+
profiling_section = profiling_section.group(1)
|
|
148
|
+
logger.debug("Found section: %s", profiling_section)
|
|
149
|
+
|
|
150
|
+
# This is regex dark arts - seems to work, I roughly understood when I
|
|
151
|
+
# was refining this named capture group, but I might not be able to in
|
|
152
|
+
# the future. Made heavy use of the regex debugger at regex101.com :) - MS 19/9/2025
|
|
153
|
+
profile_line = r"^\s*[\d\s]+\s+(?P<region>[a-zA-Z][a-zA-Z:()_/\-*&0-9\s\.]+(?<!\s))"
|
|
154
|
+
for metric in metrics:
|
|
155
|
+
logger.debug(f"Adding {metric.name=}")
|
|
156
|
+
group_name = "".join(metric.name.split()) # remove any white-space from metric name to create group name
|
|
157
|
+
if metric in [pemax, pemin]:
|
|
158
|
+
# the pemax and pemin values are enclosed within brackets '()',
|
|
159
|
+
# so we need to ignore both the opening and closing brackets
|
|
160
|
+
add_pattern = r"\s+\(\s*(?P<" + group_name + r">[0-9.]+)\s*\)"
|
|
161
|
+
elif metric == tstd:
|
|
162
|
+
add_pattern = (
|
|
163
|
+
r"\s+(?P<" + group_name + r">[0-9.]+)\s+[\S]+"
|
|
164
|
+
) # SD is followed by % of mean -> ignore that column
|
|
165
|
+
else:
|
|
166
|
+
add_pattern = (
|
|
167
|
+
r"\s+(?P<" + group_name + r">[0-9.]+)"
|
|
168
|
+
) # standard white-space followed by a sequence of digits or '.'
|
|
169
|
+
|
|
170
|
+
logger.debug(f"{add_pattern=} for {metric.name=}")
|
|
171
|
+
profile_line += add_pattern
|
|
172
|
+
logger.debug(f"{profile_line=} after {metric.name=}")
|
|
173
|
+
|
|
174
|
+
profile_line += r"$" # the regex should match till the end of line.
|
|
175
|
+
profiling_region_p = re.compile(profile_line, re.MULTILINE)
|
|
176
|
+
|
|
177
|
+
stats = {"region": []}
|
|
178
|
+
stats.update({m: [] for m in self.metrics})
|
|
179
|
+
for line in profiling_region_p.finditer(profiling_section):
|
|
180
|
+
logger.debug(f"Matched line: {line.group(0)}")
|
|
181
|
+
stats["region"].append(line.group("region"))
|
|
182
|
+
for metric in metrics:
|
|
183
|
+
group_name = "".join(metric.name.split())
|
|
184
|
+
stats[metric].append(_convert_from_string(line.group(group_name)))
|
|
185
|
+
|
|
186
|
+
# Parsing is done - let's run some checks
|
|
187
|
+
num_lines = len(profiling_section.strip().split("\n"))
|
|
188
|
+
logger.debug(f"Found {num_lines} lines in profiling section")
|
|
189
|
+
if len(stats["region"]) != num_lines:
|
|
190
|
+
raise AssertionError(f"Expected {num_lines} regions, found {len(stats['region'])}.")
|
|
191
|
+
|
|
192
|
+
logger.info(f"Found {len(stats['region'])} regions with profiling info")
|
|
193
|
+
return stats
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
"""Example UM7 runtime log snippet to be parsed for total wallclock runtime:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
END OF RUN - TIMER OUTPUT
|
|
200
|
+
Timer information is for whole run
|
|
201
|
+
PE 0 Elapsed CPU Time: 3943.63426200007
|
|
202
|
+
PE 0 Elapsed Wallclock Time: 3943.80157899974
|
|
203
|
+
|
|
204
|
+
Total Elapsed CPU Time: 820297.910506003
|
|
205
|
+
Maximum Elapsed Wallclock Time: 3944.07699399998
|
|
206
|
+
Speedup: 207.982225436750
|
|
207
|
+
```
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class UMTotalRuntimeParser(ProfilingParser):
|
|
212
|
+
"""Parser for UM total runtime from the UM log file."""
|
|
213
|
+
|
|
214
|
+
_metrics = [tmax]
|
|
215
|
+
|
|
216
|
+
def parse(self, file_path: str | Path | os.PathLike) -> dict:
|
|
217
|
+
"""Parse UM total runtime from a file.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
file_path (str | Path | os.PathLike): input string to parse.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
dict: dictionary of parsed profiling data.
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
ValueError: If no matching total runtime line is found.
|
|
227
|
+
"""
|
|
228
|
+
stream = _read_text_file(file_path)
|
|
229
|
+
total_runtime_pattern = re.compile(
|
|
230
|
+
r"Maximum\s+Elapsed\s+Wallclock\s+Time\s*:\s*(?P<total_time>[0-9.]+)\s*",
|
|
231
|
+
re.MULTILINE,
|
|
232
|
+
)
|
|
233
|
+
total_runtime_match = total_runtime_pattern.search(stream)
|
|
234
|
+
if not total_runtime_match:
|
|
235
|
+
logger.debug("Total runtime pattern: %s", total_runtime_pattern)
|
|
236
|
+
logger.debug("Input string: %s", stream)
|
|
237
|
+
raise ValueError("No matching total runtime line found.")
|
|
238
|
+
|
|
239
|
+
total_time = float(total_runtime_match.group("total_time"))
|
|
240
|
+
logger.debug(f"Found total UM runtime: {total_time} seconds")
|
|
241
|
+
|
|
242
|
+
return {"region": ["um_total_walltime"], tmax: [total_time]}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: access-profiling
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Tools and utilities to profile the ACCESS climate models.
|
|
5
|
+
Author-email: Micael Oliveira <micael.oliveira@anu.edu.au>, Manodeep Sinha <manodeep.sinha@anu.edu.au>, Edward Yang <edward.yang@anu.edu.au>
|
|
6
|
+
Keywords: profiling,access
|
|
7
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
8
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Topic :: Utilities
|
|
11
|
+
Requires-Python: >=3.11.4
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: xarray
|
|
15
|
+
Requires-Dist: pint
|
|
16
|
+
Requires-Dist: pint-xarray
|
|
17
|
+
Requires-Dist: matplotlib
|
|
18
|
+
Requires-Dist: access-config-utils
|
|
19
|
+
Requires-Dist: experiment-runner
|
|
20
|
+
Requires-Dist: experiment-generator
|
|
21
|
+
Requires-Dist: payu
|
|
22
|
+
Provides-Extra: devel
|
|
23
|
+
Requires-Dist: pre-commit; extra == "devel"
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: pytest; extra == "test"
|
|
26
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# access-profiling
|
|
30
|
+
|
|
31
|
+
 [](https://codecov.io/github/ACCESS-NRI/access-profiling) [](https://opensource.org/license/apache-2-0) [](https://github.com/psf/black)
|
|
32
|
+
|
|
33
|
+
## About
|
|
34
|
+
|
|
35
|
+
A Python package providing various tools and utilities to profile the models developed at [ACCESS-NRI](https://github.com/ACCESS-NRI).
|
|
36
|
+
|
|
37
|
+
## Key Features
|
|
38
|
+
|
|
39
|
+
- Parsers to extract profiling data from model output.
|
|
40
|
+
- Functions to plot key profiling metrics.
|
|
41
|
+
|
|
42
|
+
## Documentation
|
|
43
|
+
|
|
44
|
+
Coming soon.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
### Using pip
|
|
49
|
+
|
|
50
|
+
You can install the latest release directly from PyPI:
|
|
51
|
+
```shell
|
|
52
|
+
pip install access-profiling
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### From source
|
|
56
|
+
|
|
57
|
+
If you prefer to install from source:
|
|
58
|
+
```shell
|
|
59
|
+
git clone https://github.com/ACCESS-NRI/access-profiling.git
|
|
60
|
+
cd access-profiling
|
|
61
|
+
pip install .
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
Coming soon.
|
|
67
|
+
|
|
68
|
+
## Development installation
|
|
69
|
+
|
|
70
|
+
If you intend to contribute or modify the package, it is recommended to work inside a virtual environment.
|
|
71
|
+
|
|
72
|
+
1. Create and activate a virtual environment
|
|
73
|
+
```shell
|
|
74
|
+
# Create a virtual environment
|
|
75
|
+
python3 -m venv .venv
|
|
76
|
+
|
|
77
|
+
# Activate the virtual environment
|
|
78
|
+
source .venv/bin/activate
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
2. Install in editable mode with development and test dependencies
|
|
82
|
+
```shell
|
|
83
|
+
pip install -e ".[devel,test]"
|
|
84
|
+
```
|
|
85
|
+
This will install the package in editable mode, meaning changes to the source code are reflected immediately without reinstallation. Development dependencies such as testing tools will also be installed.
|
|
86
|
+
|
|
87
|
+
3. Run the test suite
|
|
88
|
+
```shell
|
|
89
|
+
pytest
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Contributing
|
|
93
|
+
|
|
94
|
+
Contributions are welcome! Please open an issue or submit a pull request if you’d like to add features, fix bugs, or improve documentation.
|
|
95
|
+
|
|
96
|
+
For significant contributions, we recommend discussing proposed changes in an issue before opening a pull request.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
This project is licensed under the Apache 2.0 License.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
access/profiling/__init__.py,sha256=PZZoZy6rHr5221EaJsI3YhuLzgKFvD2fMf2FmCWvl1Y,1025
|
|
2
|
+
access/profiling/access_models.py,sha256=GAXfaiid9psHYOyXBAXyt2YfZJs1cjVXCkIlRx6mNGU,3186
|
|
3
|
+
access/profiling/cice5_parser.py,sha256=tBhRYZhMupade3ALuvNPzYEC5nzfMOeAfRTIEi0lVYU,2926
|
|
4
|
+
access/profiling/cylc_manager.py,sha256=YNzW4KtXhFzKWx47VrtguqroBH4O7QtTleZbaKOofsU,9777
|
|
5
|
+
access/profiling/cylc_parser.py,sha256=2oNJVUWXiGxiYTmGjL0jd3KBlGulTMCjI0kCaXGrVEk,5332
|
|
6
|
+
access/profiling/esmf_parser.py,sha256=-wIwJmgZWPIUmUeNLXXAaxXweLVbPaDC2u7K_AAfGzo,7948
|
|
7
|
+
access/profiling/experiment.py,sha256=K16h-a_X3V-68gkmHXRL162WsZocZf4DrIay5xhah64,11512
|
|
8
|
+
access/profiling/fms_parser.py,sha256=GjsKpSj6-gYLHa-go0kljdO5L_7QCKa98cOgWs6YJio,3481
|
|
9
|
+
access/profiling/manager.py,sha256=w-xiQRxGfp4k0i0bVKyyW-jzFrHMX040h6CeA8FG880,27825
|
|
10
|
+
access/profiling/metrics.py,sha256=1iDPSDcczq9sCI2ULYKI8W5opvHHLn4QR40pmw5Bu68,3300
|
|
11
|
+
access/profiling/parser.py,sha256=8YwNhIAVjRvJhZvA6hl7_BK118HBbG5VAY5Jp0BV7zw,9102
|
|
12
|
+
access/profiling/payu_manager.py,sha256=hlVLleN20-VFb1i3CrBbQYib_PhMlkeTioupDDjioRw,14421
|
|
13
|
+
access/profiling/payujson_parser.py,sha256=jTqtXKkoMacKB2Yc6Hoi6xcMkcrZEJs_nGv73zFFByM,2287
|
|
14
|
+
access/profiling/plotting_utils.py,sha256=io8-BIFZGhCkHlDeEsPxTnlR0iegCv27sxeZTUFdutU,4607
|
|
15
|
+
access/profiling/scaling.py,sha256=GruGTn5y3no-QxbT-kHpR9Q6RrPzpVtrkglRLSGrwbs,4620
|
|
16
|
+
access/profiling/um_parser.py,sha256=E8VJ7Tq5VIrTRbzrNcPfQkSnHoaNdOS_nId4G8StLDo,11202
|
|
17
|
+
access_profiling-0.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
18
|
+
access_profiling-0.1.dist-info/METADATA,sha256=tTDn4iq7SJQJUq2PyOt60ss6rvYxXPzeQMkuKQJbQeU,3115
|
|
19
|
+
access_profiling-0.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
20
|
+
access_profiling-0.1.dist-info/top_level.txt,sha256=dKU9Xvk9JgcB3ufviuSVfTY6KZ2eihlcvYerY_-00OQ,7
|
|
21
|
+
access_profiling-0.1.dist-info/RECORD,,
|