From a6ce94bb539eb485b9fdd58d84ef2e5008943f97 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Thu, 18 Sep 2025 13:43:23 +1000 Subject: [PATCH 1/3] add cice5 profiler --- src/access/parsers/cice5_profiling.py | 57 +++++++++++++++++++ tests/test_cice5_profiling.py | 79 +++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 src/access/parsers/cice5_profiling.py create mode 100644 tests/test_cice5_profiling.py diff --git a/src/access/parsers/cice5_profiling.py b/src/access/parsers/cice5_profiling.py new file mode 100644 index 0000000..745708e --- /dev/null +++ b/src/access/parsers/cice5_profiling.py @@ -0,0 +1,57 @@ +# Copyright 2025 ACCESS-NRI and contributors. See the top-level COPYRIGHT file for details. +# SPDX-License-Identifier: Apache-2.0 + +"""Parser for CICE5 profiling data. +The data to be parsed is written in the following form, where bloc stats are discarded: + +Timer 1: Total 8133.37 seconds + Timer stats (node): min = 8133.36 seconds + max = 8133.37 seconds + mean= 8133.36 seconds + Timer stats(block): min = 0.00 seconds + max = 0.00 seconds + mean= 0.00 seconds +Timer 2: TimeLoop 8133.00 seconds + Timer stats (node): min = 8132.99 seconds + max = 8133.00 seconds + mean= 8132.99 seconds + Timer stats(block): min = 0.00 seconds + max = 0.00 seconds + mean= 0.00 seconds +""" + +from access.parsers.profiling import ProfilingParser +import re + + +class CICE5ProfilingParser(ProfilingParser): + """CICE5 profiling output parser.""" + + def __init__(self): + super().__init__() + self._metrics = ["min", "max", "mean"] + + @property + def metrics(self) -> list: + return self._metrics + + def read(self, stream: str) -> dict: + # Initialize result dictionary + result = {"region": [], "min": [], "max": [], "mean": []} + + # Regex pattern to match timer blocks + # This captures the region name and the three node timing values + 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" + + # Find all matches + matches = re.findall(pattern, stream, re.MULTILINE | re.DOTALL) + + # Extract data from matches + for match in matches: + region, min_time, max_time, mean_time = match + result["region"].append(region) + result["min"].append(float(min_time)) + result["max"].append(float(max_time)) + result["mean"].append(float(mean_time)) + + return result diff --git a/tests/test_cice5_profiling.py b/tests/test_cice5_profiling.py new file mode 100644 index 0000000..5700767 --- /dev/null +++ b/tests/test_cice5_profiling.py @@ -0,0 +1,79 @@ +# Copyright 2025 ACCESS-NRI and contributors. See the top-level COPYRIGHT file for details. +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +from access.parsers.cice5_profiling import CICE5ProfilingParser + + +@pytest.fixture(scope="module") +def cice5_required_metrics(): + return ("min", "max", "mean") + + +@pytest.fixture(scope="module") +def cice5_parser(): + """Fixture instantiating the CICE5 parser.""" + return CICE5ProfilingParser() + + +@pytest.fixture(scope="module") +def cice5_profiling(): + """Fixture returning a dict holding the parsed FMS timing content without hits.""" + return { + "region": ["Total", "TimeLoop"], + "min": [16197.42, 16197.14], + "max": [16197.47, 16197.19], + "mean": [16197.44, 16197.16], + } + + +@pytest.fixture(scope="module") +def cice5_log_file(): + """Fixture returning the CICE5 timing content.""" + return """ -------------------------------- + CICE model diagnostic output + -------------------------------- + + Document ice_in namelist parameters: + ==================================== + + runtype = continue + Restart read/written 17520 58159382400.0000 + 0.000000000000000E+000 + +Timing information: + +Timer 1: Total 16197.47 seconds + Timer stats (node): min = 16197.42 seconds + max = 16197.47 seconds + mean= 16197.44 seconds + Timer stats(block): min = 0.00 seconds + max = 0.00 seconds + mean= 0.00 seconds +Timer 2: TimeLoop 16197.19 seconds + Timer stats (node): min = 16197.14 seconds + max = 16197.19 seconds + mean= 16197.16 seconds + Timer stats(block): min = 0.00 seconds + max = 0.00 seconds + mean= 0.00 seconds +""" + + +def test_cice5_profiling(cice5_required_metrics, cice5_parser, cice5_log_file, cice5_profiling): + """Test the correct parsing of CICE5 timing information.""" + parsed_log = cice5_parser.read(cice5_log_file) + + # check metrics are present in parser and parsed output + for metric in cice5_required_metrics: + assert metric in cice5_parser.metrics, f"{metric} metric not found in CICE5 parser metrics." + assert metric in parsed_log, f"{metric} metric not found in CICE5 parsed log." + + # check content for each metric is correct + for idx, region in enumerate(cice5_profiling["region"]): + assert region in parsed_log["region"], f"{region} not found in CICE5 parsed log" + for metric in cice5_required_metrics: + assert ( + cice5_profiling[metric][idx] == parsed_log[metric][idx] + ), f"Incorrect {metric} for region {region} (idx: {idx})." From 62902cc55a14a94332f8258930309dab313dc6f3 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Thu, 18 Sep 2025 16:34:18 +1000 Subject: [PATCH 2/3] add doc strings for implemented methods --- src/access/parsers/cice5_profiling.py | 13 +++++++++++++ tests/test_cice5_profiling.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/access/parsers/cice5_profiling.py b/src/access/parsers/cice5_profiling.py index 745708e..7deedf6 100644 --- a/src/access/parsers/cice5_profiling.py +++ b/src/access/parsers/cice5_profiling.py @@ -33,9 +33,22 @@ def __init__(self): @property def metrics(self) -> list: + """Implements "metrics" abstract method/property. + + Returns: + list: the metric names captured by this parser. + """ return self._metrics def read(self, stream: str) -> dict: + """Implements "read" abstract method to parse profiling data in CICE5 log output. + + Args: + stream (str): String containing the CICE5 log to be parsed. + + Returns: + dict: Parsed timing information. + """ # Initialize result dictionary result = {"region": [], "min": [], "max": [], "mean": []} diff --git a/tests/test_cice5_profiling.py b/tests/test_cice5_profiling.py index 5700767..afe921c 100644 --- a/tests/test_cice5_profiling.py +++ b/tests/test_cice5_profiling.py @@ -19,7 +19,7 @@ def cice5_parser(): @pytest.fixture(scope="module") def cice5_profiling(): - """Fixture returning a dict holding the parsed FMS timing content without hits.""" + """Fixture returning a dict holding the parsed CICE5 timing content.""" return { "region": ["Total", "TimeLoop"], "min": [16197.42, 16197.14], From 41fcbfb18198d47c067899aa66c2cc4904ffbf3e Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Fri, 19 Sep 2025 14:11:18 +1000 Subject: [PATCH 3/3] raise error when no cice5 profiling data found --- src/access/parsers/cice5_profiling.py | 6 ++++++ tests/test_cice5_profiling.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/access/parsers/cice5_profiling.py b/src/access/parsers/cice5_profiling.py index 7deedf6..8ed5d04 100644 --- a/src/access/parsers/cice5_profiling.py +++ b/src/access/parsers/cice5_profiling.py @@ -48,6 +48,9 @@ def read(self, stream: str) -> dict: Returns: dict: Parsed timing information. + + Raises: + ValueError: If matching timings aren't found. """ # Initialize result dictionary result = {"region": [], "min": [], "max": [], "mean": []} @@ -59,6 +62,9 @@ def read(self, stream: str) -> dict: # Find all matches matches = re.findall(pattern, stream, re.MULTILINE | re.DOTALL) + if not matches: + raise ValueError("No CICE5 profiling data found") + # Extract data from matches for match in matches: region, min_time, max_time, mean_time = match diff --git a/tests/test_cice5_profiling.py b/tests/test_cice5_profiling.py index afe921c..a7608a8 100644 --- a/tests/test_cice5_profiling.py +++ b/tests/test_cice5_profiling.py @@ -61,6 +61,17 @@ def cice5_log_file(): """ +@pytest.fixture(scope="module") +def cice5_incorrect_log_file(): + """Fixture returning an incorrect CICE5 timing content.""" + return """Timer stats (node): min = 16197.42 seconds + max = 16197.47 seconds + mean= 16197.44 seconds + Timer stats(block): min = 0.00 seconds + max = 0.00 seconds + mean= 0.00 seconds""" + + def test_cice5_profiling(cice5_required_metrics, cice5_parser, cice5_log_file, cice5_profiling): """Test the correct parsing of CICE5 timing information.""" parsed_log = cice5_parser.read(cice5_log_file) @@ -77,3 +88,9 @@ def test_cice5_profiling(cice5_required_metrics, cice5_parser, cice5_log_file, c assert ( cice5_profiling[metric][idx] == parsed_log[metric][idx] ), f"Incorrect {metric} for region {region} (idx: {idx})." + + +def test_cice5_incorrect_profiling(cice5_parser, cice5_incorrect_log_file): + """Test the parsing of incirrect CICE5 timing information.""" + with pytest.raises(ValueError): + cice5_parser.read(cice5_incorrect_log_file)