Skip to content

Commit eb94c1f

Browse files
Merge pull request #2 from ACCESS-NRI/cice5-profiling
Add cice5 profiling parser
2 parents 85bdea2 + 361ac44 commit eb94c1f

File tree

3 files changed

+177
-5
lines changed

3 files changed

+177
-5
lines changed

src/access/profiling/__init__.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414
from access.profiling.parser import ProfilingParser
1515
from access.profiling.fms_parser import FMSProfilingParser
1616
from access.profiling.payujson_parser import PayuJSONProfilingParser
17+
from access.profiling.cice5_parser import CICE5ProfilingParser
1718

18-
__all__ = [
19-
"ProfilingParser",
20-
"FMSProfilingParser",
21-
"PayuJSONProfilingParser",
22-
]
19+
__all__ = ["ProfilingParser", "FMSProfilingParser", "PayuJSONProfilingParser", "CICE5ProfilingParser"]
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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 CICE5 profiling data.
5+
The data to be parsed is written in the following form, where block stats are discarded:
6+
7+
Timer 1: Total 8133.37 seconds
8+
Timer stats (node): min = 8133.36 seconds
9+
max = 8133.37 seconds
10+
mean= 8133.36 seconds
11+
Timer stats(block): min = 0.00 seconds
12+
max = 0.00 seconds
13+
mean= 0.00 seconds
14+
Timer 2: TimeLoop 8133.00 seconds
15+
Timer stats (node): min = 8132.99 seconds
16+
max = 8133.00 seconds
17+
mean= 8132.99 seconds
18+
Timer stats(block): min = 0.00 seconds
19+
max = 0.00 seconds
20+
mean= 0.00 seconds
21+
22+
These timers are printed at the end of the CICE5 run and can be an arbitrary number of timers.
23+
For example, ESM1.6 has 17 timers printed at the end of ice_diag.d output log.
24+
"""
25+
26+
from access.profiling.parser import ProfilingParser
27+
import re
28+
29+
30+
class CICE5ProfilingParser(ProfilingParser):
31+
"""CICE5 profiling output parser."""
32+
33+
def __init__(self):
34+
super().__init__()
35+
self._metrics = ["min", "max", "mean"]
36+
37+
@property
38+
def metrics(self) -> list:
39+
"""Implements "metrics" abstract method/property.
40+
41+
Returns:
42+
list: the metric names captured by this parser.
43+
"""
44+
return self._metrics
45+
46+
def read(self, stream: str) -> dict:
47+
"""Implements "read" abstract method to parse profiling data in CICE5 log output.
48+
49+
Args:
50+
stream (str): String containing the CICE5 log to be parsed.
51+
52+
Returns:
53+
dict: Parsed timing information.
54+
55+
Raises:
56+
ValueError: If matching timings aren't found.
57+
"""
58+
# Initialize result dictionary
59+
result = {"region": [], "min": [], "max": [], "mean": []}
60+
61+
# Regex pattern to match timer blocks
62+
# This captures the region name and the three node timing values
63+
pattern = r"Timer\s+\d+:\s+(\w+)\s+[\d.]+\s+seconds\s+Timer stats \(node\): min =\s+([\d.]+) seconds\s+max =\s+([\d.]+) seconds\s+mean=\s+([\d.]+) seconds"
64+
65+
# Find all matches
66+
matches = re.findall(pattern, stream, re.MULTILINE | re.DOTALL)
67+
68+
if not matches:
69+
raise ValueError("No CICE5 profiling data found")
70+
71+
# Extract data from matches
72+
for match in matches:
73+
region, min_time, max_time, mean_time = match
74+
result["region"].append(region)
75+
result["min"].append(float(min_time))
76+
result["max"].append(float(max_time))
77+
result["mean"].append(float(mean_time))
78+
79+
return result

tests/test_cice5_profiling.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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 pytest
5+
6+
from access.profiling import CICE5ProfilingParser
7+
8+
9+
@pytest.fixture(scope="module")
10+
def cice5_required_metrics():
11+
return ("min", "max", "mean")
12+
13+
14+
@pytest.fixture(scope="module")
15+
def cice5_parser():
16+
"""Fixture instantiating the CICE5 parser."""
17+
return CICE5ProfilingParser()
18+
19+
20+
@pytest.fixture(scope="module")
21+
def cice5_profiling():
22+
"""Fixture returning a dict holding the parsed CICE5 timing content."""
23+
return {
24+
"region": ["Total", "TimeLoop"],
25+
"min": [16197.42, 16197.14],
26+
"max": [16197.47, 16197.19],
27+
"mean": [16197.44, 16197.16],
28+
}
29+
30+
31+
@pytest.fixture(scope="module")
32+
def cice5_log_file():
33+
"""Fixture returning the CICE5 timing content."""
34+
return """ --------------------------------
35+
CICE model diagnostic output
36+
--------------------------------
37+
38+
Document ice_in namelist parameters:
39+
====================================
40+
41+
runtype = continue
42+
Restart read/written 17520 58159382400.0000
43+
0.000000000000000E+000
44+
45+
Timing information:
46+
47+
Timer 1: Total 16197.47 seconds
48+
Timer stats (node): min = 16197.42 seconds
49+
max = 16197.47 seconds
50+
mean= 16197.44 seconds
51+
Timer stats(block): min = 0.00 seconds
52+
max = 0.00 seconds
53+
mean= 0.00 seconds
54+
Timer 2: TimeLoop 16197.19 seconds
55+
Timer stats (node): min = 16197.14 seconds
56+
max = 16197.19 seconds
57+
mean= 16197.16 seconds
58+
Timer stats(block): min = 0.00 seconds
59+
max = 0.00 seconds
60+
mean= 0.00 seconds
61+
"""
62+
63+
64+
@pytest.fixture(scope="module")
65+
def cice5_incorrect_log_file():
66+
"""Fixture returning an incorrect CICE5 timing content."""
67+
return """Timer stats (node): min = 16197.42 seconds
68+
max = 16197.47 seconds
69+
mean= 16197.44 seconds
70+
Timer stats(block): min = 0.00 seconds
71+
max = 0.00 seconds
72+
mean= 0.00 seconds"""
73+
74+
75+
def test_cice5_profiling(cice5_required_metrics, cice5_parser, cice5_log_file, cice5_profiling):
76+
"""Test the correct parsing of CICE5 timing information."""
77+
parsed_log = cice5_parser.read(cice5_log_file)
78+
79+
# check metrics are present in parser and parsed output
80+
for metric in cice5_required_metrics:
81+
assert metric in cice5_parser.metrics, f"{metric} metric not found in CICE5 parser metrics."
82+
assert metric in parsed_log, f"{metric} metric not found in CICE5 parsed log."
83+
84+
# check content for each metric is correct
85+
for idx, region in enumerate(cice5_profiling["region"]):
86+
assert region in parsed_log["region"], f"{region} not found in CICE5 parsed log"
87+
for metric in cice5_required_metrics:
88+
assert (
89+
cice5_profiling[metric][idx] == parsed_log[metric][idx]
90+
), f"Incorrect {metric} for region {region} (idx: {idx})."
91+
92+
93+
def test_cice5_incorrect_profiling(cice5_parser, cice5_incorrect_log_file):
94+
"""Test the parsing of incirrect CICE5 timing information."""
95+
with pytest.raises(ValueError):
96+
cice5_parser.read(cice5_incorrect_log_file)

0 commit comments

Comments
 (0)