Skip to content

Commit

Permalink
Support for Intel code-coverage analysis (#91)
Browse files Browse the repository at this point in the history
  • Loading branch information
abhaasgoyal committed Jul 11, 2024
1 parent fc625c0 commit f4c3c19
Show file tree
Hide file tree
Showing 18 changed files with 430 additions and 44 deletions.
9 changes: 9 additions & 0 deletions docs/user_guide/config_options.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,15 @@ science_configurations: [
]
```

## codecov

: **Default:** False, _optional key. :octicons-dash-24: Specifies whether to build `benchcab` with code-coverage flags, which can then be used in post-run analysis (`benchcab gen_codecov`).

```yaml
codecov:
true
```

[meorg]: https://modelevaluation.org/
[forty-two-me]: https://modelevaluation.org/experiment/display/s6k22L3WajmiS9uGv
[five-me]: https://modelevaluation.org/experiment/display/Nb37QxkAz3FczWDd7
Expand Down
37 changes: 36 additions & 1 deletion src/benchcab/benchcab.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
from benchcab import fluxsite, internal, spatial
from benchcab.comparison import run_comparisons, run_comparisons_in_parallel
from benchcab.config import read_config
from benchcab.coverage import (
get_coverage_tasks_default,
run_coverage_tasks,
run_coverages_in_parallel,
)
from benchcab.environment_modules import EnvironmentModules, EnvironmentModulesInterface
from benchcab.internal import get_met_forcing_file_names
from benchcab.model import Model
Expand Down Expand Up @@ -203,6 +208,7 @@ def fluxsite_submit_job(self, config_path: str, skip: list[str]) -> None:
modules=config["modules"],
pbs_config=config["fluxsite"]["pbs"],
skip_bitwise_cmp="fluxsite-bitwise-cmp" in skip,
skip_codecov="gen_codecov" in skip or not config["codecov"],
verbose=is_verbose(),
benchcab_path=str(self.benchcab_exe_path),
)
Expand All @@ -226,6 +232,29 @@ def fluxsite_submit_job(self, config_path: str, skip: list[str]) -> None:
logger.info("The NetCDF output for each task is written to:")
logger.info(f"{internal.FLUXSITE_DIRS['OUTPUT']}/<task_name>_out.nc")

def gen_codecov(self, config_path: str):

Check warning on line 235 in src/benchcab/benchcab.py

View check run for this annotation

Codecov / codecov/patch

src/benchcab/benchcab.py#L235

Added line #L235 was not covered by tests
"""Endpoint for `benchcab codecov`."""
logger = self._get_logger()
config = self._get_config(config_path)
self._validate_environment(project=config["project"], modules=config["modules"])

coverage_tasks = get_coverage_tasks_default(
models=self._get_models(config=config)
)

Check warning on line 243 in src/benchcab/benchcab.py

View check run for this annotation

Codecov / codecov/patch

src/benchcab/benchcab.py#L237-L243

Added lines #L237 - L243 were not covered by tests

if not config["codecov"]:
msg = """`config.yaml` should have set `codecov: true` before building and
running `gen_codecov`."""
raise ValueError(msg)

Check warning on line 249 in src/benchcab/benchcab.py

View check run for this annotation

Codecov / codecov/patch

src/benchcab/benchcab.py#L247-L249

Added lines #L247 - L249 were not covered by tests
logger.info("Running coverage tasks...")
if config["fluxsite"]["multiprocess"]:

Check warning on line 251 in src/benchcab/benchcab.py

View check run for this annotation

Codecov / codecov/patch

src/benchcab/benchcab.py#L251

Added line #L251 was not covered by tests
ncpus = config["fluxsite"]["pbs"]["ncpus"]
run_coverages_in_parallel(coverage_tasks, n_processes=ncpus)
else:
run_coverage_tasks(coverage_tasks)
logger.info("Successfully ran coverage tasks")

Check warning on line 256 in src/benchcab/benchcab.py

View check run for this annotation

Codecov / codecov/patch

src/benchcab/benchcab.py#L255-L256

Added lines #L255 - L256 were not covered by tests

def checkout(self, config_path: str):
"""Endpoint for `benchcab checkout`."""
logger = self._get_logger()
Expand Down Expand Up @@ -271,7 +300,11 @@ def build(self, config_path: str, mpi=False):
logger.info(
f"Compiling CABLE {build_mode} for realisation {repo.name}..."
)
repo.build(modules=config["modules"], mpi=mpi)
repo.build(
modules=config["modules"],
mpi=mpi,
coverage=config["codecov"],

Check warning on line 306 in src/benchcab/benchcab.py

View check run for this annotation

Codecov / codecov/patch

src/benchcab/benchcab.py#L304-L306

Added lines #L304 - L306 were not covered by tests
)
logger.info(f"Successfully compiled CABLE for realisation {repo.name}")

def fluxsite_setup_work_directory(self, config_path: str):
Expand Down Expand Up @@ -334,6 +367,8 @@ def fluxsite(self, config_path: str, no_submit: bool, skip: list[str]):
self.fluxsite_run_tasks(config_path)
if "fluxsite-bitwise-cmp" not in skip:
self.fluxsite_bitwise_cmp(config_path)
if "codecov" not in skip:

Check warning on line 370 in src/benchcab/benchcab.py

View check run for this annotation

Codecov / codecov/patch

src/benchcab/benchcab.py#L370

Added line #L370 was not covered by tests
self.gen_codecov(config_path)
else:
self.fluxsite_submit_job(config_path, skip)

Expand Down
13 changes: 12 additions & 1 deletion src/benchcab/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,17 @@ def generate_parser(app: Benchcab) -> argparse.ArgumentParser:
submissions: deletes runs/ and benchmark submission files
all: deletes in both stages of submissions and realisations""",
)

parser_clean.set_defaults(func=app.clean)

# subcommand: 'benchcab gen_codecov"
parser_codecov = subparsers.add_parser(
"gen_codecov",
parents=[args_help, args_subcommand],
help="Runs code coverage tasks when runs are finised.",
description="""Uses profmerge and codecov utilties to do code coverage
analysis. Note: All sources must be built using Intel compiler.
""",
add_help=False,
)
parser_codecov.set_defaults(func=app.gen_codecov)
return main_parser
2 changes: 2 additions & 0 deletions src/benchcab/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ def read_optional_key(config: dict):
"pbs", {}
)

config["codecov"] = config.get("codecov", False)


def read_config_file(config_path: str) -> dict:
"""Load the config file in a dict.
Expand Down
95 changes: 95 additions & 0 deletions src/benchcab/coverage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Copyright 2024 ACCESS-NRI and contributors. See the top-level COPYRIGHT file for details.
# SPDX-License-Identifier: Apache-2.0

"""A module containing functions and data structures for running coverage tasks."""

import multiprocessing
import operator
from contextlib import nullcontext
from pathlib import Path
from typing import Optional

from benchcab import internal
from benchcab.environment_modules import EnvironmentModules, EnvironmentModulesInterface
from benchcab.model import Model
from benchcab.utils import get_logger
from benchcab.utils.fs import chdir
from benchcab.utils.subprocess import SubprocessWrapper, SubprocessWrapperInterface


class CoverageTask:
"""A class used to represent a single coverage report generation task."""

subprocess_handler: SubprocessWrapperInterface = SubprocessWrapper()
modules_handler: EnvironmentModulesInterface = EnvironmentModules()

def __init__(
self,
coverage_dir: str,
project_name: Optional[str] = "CABLE",
dpi_file: Optional[str] = "pgopti.dpi",
spi_file: Optional[str] = "pgopti.spi",
) -> None:
"""Constructor.
Parameters
----------
coverage_dir:
Name of directory where coverage analysis is to be done
project_name:
Name of project on which codecov is run
dpi_file:
name of DPI file created after merging .dyn files created after all runs
spi_file:
Static profile information on compilation
"""
self.logger = get_logger()
self.coverage_dir = coverage_dir
self.project_name = project_name
self.dpi_file = dpi_file
self.spi_file = spi_file

def run(self) -> None:
"""Executes `profmerge` and `codecov` to run codecov analysis for a given realisation."""
if not Path(self.coverage_dir).is_dir():
msg = f"""The coverage directory: {self.coverage_dir}

Check warning on line 57 in src/benchcab/coverage.py

View check run for this annotation

Codecov / codecov/patch

src/benchcab/coverage.py#L57

Added line #L57 was not covered by tests
does not exist. Did you run the jobs and/or set `coverage: true` in `config.yaml`
before the building stage"""
raise OSError(msg)

Check warning on line 60 in src/benchcab/coverage.py

View check run for this annotation

Codecov / codecov/patch

src/benchcab/coverage.py#L60

Added line #L60 was not covered by tests

self.logger.info(f"Generating coverage report in {self.coverage_dir}")

# Load intel-compiler in case we run from CLI, otherwise assuming
# PBS jobscript loads
with chdir(self.coverage_dir), (
nullcontext()
if self.modules_handler.module_is_loaded("intel-compiler")
else self.modules_handler.load([internal.DEFAULT_MODULES["intel-compiler"]])
):
self.subprocess_handler.run_cmd(f"profmerge -prof-dpi {self.dpi_file}")
self.subprocess_handler.run_cmd(
f"codecov -prj {self.project_name} -dpi {self.dpi_file} -spi {self.spi_file}"
)


def run_coverage_tasks(coverage_tasks: list[CoverageTask]) -> None:
"""Runs coverage tasks serially."""
for task in coverage_tasks:
task.run()

Check warning on line 80 in src/benchcab/coverage.py

View check run for this annotation

Codecov / codecov/patch

src/benchcab/coverage.py#L79-L80

Added lines #L79 - L80 were not covered by tests


def get_coverage_tasks_default(models: list[Model]) -> list[CoverageTask]:
"""Returns list of Coveragee Tasks setting default values for optional parameters."""
return [CoverageTask(model.get_coverage_dir()) for model in models]

Check warning on line 85 in src/benchcab/coverage.py

View check run for this annotation

Codecov / codecov/patch

src/benchcab/coverage.py#L85

Added line #L85 was not covered by tests


def run_coverages_in_parallel(
coverage_tasks: list[CoverageTask],
n_processes=internal.FLUXSITE_DEFAULT_PBS["ncpus"],
) -> None:
"""Runs coverage tasks in parallel across multiple processes."""
run_task = operator.methodcaller("run")
with multiprocessing.Pool(n_processes) as pool:
pool.map(run_task, coverage_tasks, chunksize=1)

Check warning on line 95 in src/benchcab/coverage.py

View check run for this annotation

Codecov / codecov/patch

src/benchcab/coverage.py#L93-L95

Added lines #L93 - L95 were not covered by tests
6 changes: 5 additions & 1 deletion src/benchcab/data/config-schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,8 @@ spatial:
args:
nullable: true
type: "string"
required: false
required: false

codecov:
type: "boolean"
required: false
7 changes: 6 additions & 1 deletion src/benchcab/data/pbs_jobscript.j2
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ module load {{module}}
set -ev

{{benchcab_path}} fluxsite-run-tasks --config={{config_path}}{{verbose_flag}}
{% if skip_bitwise_cmp == False %}{{benchcab_path}} fluxsite-bitwise-cmp --config={{config_path}}{{verbose_flag}}{% endif %}
{%- if skip_bitwise_cmp == False %}
{{benchcab_path}} fluxsite-bitwise-cmp --config={{config_path}}{{verbose_flag}}
{%- endif %}
{%- if skip_codecov == False %}
{{benchcab_path}} gen_codecov --config={{config_path}}{{verbose_flag}}
{%- endif %}
2 changes: 2 additions & 0 deletions src/benchcab/data/test/config-optional.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ realisations:
branch_path: branches/Users/ccc561/v3.0-YP-changes
name: git_branch

codecov:
true

modules: [
intel-compiler/2021.1.1,
Expand Down
21 changes: 21 additions & 0 deletions src/benchcab/data/test/pbs_jobscript_no_skip_codecov.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash
#PBS -l wd
#PBS -l ncpus=18
#PBS -l mem=30GB
#PBS -l walltime=6:00:00
#PBS -q normal
#PBS -P tm70
#PBS -j oe
#PBS -m e
#PBS -l storage=gdata/ks32+gdata/hh5+gdata/wd9

module purge
module load foo
module load bar
module load baz

set -ev

/absolute/path/to/benchcab fluxsite-run-tasks --config=/path/to/config.yaml
/absolute/path/to/benchcab fluxsite-bitwise-cmp --config=/path/to/config.yaml
/absolute/path/to/benchcab gen_codecov --config=/path/to/config.yaml
2 changes: 1 addition & 1 deletion src/benchcab/data/test/pbs_jobscript_skip_bitwise.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ module load baz

set -ev

/absolute/path/to/benchcab fluxsite-run-tasks --config=/path/to/config.yaml
/absolute/path/to/benchcab fluxsite-run-tasks --config=/path/to/config.yaml
10 changes: 8 additions & 2 deletions src/benchcab/internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
CONFIG_REQUIRED_KEYS = ["realisations", "modules"]

# CMake module used for compilation:
CMAKE_MODULE = "cmake/3.24.2"
DEFAULT_MODULES = {
"cmake": "cmake/3.24.2",
"intel-compiler": "intel-compiler/2021.10.0",
}

# Number of parallel jobs used when compiling with CMake:
CMAKE_BUILD_PARALLEL_LEVEL = 4
Expand Down Expand Up @@ -51,6 +54,9 @@
# Path CABLE grid info file
GRID_FILE = CABLE_AUX_DIR / "offline" / "gridinfo_CSIRO_1x1.nc"

# Relative path to directory that stores codecov files
CODECOV_DIR = RUN_DIR / "coverage"

# Fluxsite directory tree
FLUXSITE_DIRS: dict[str, Path] = {}

Expand Down Expand Up @@ -243,7 +249,7 @@

FLUXSITE_DEFAULT_EXPERIMENT = "forty-two-site-test"

OPTIONAL_COMMANDS = ["fluxsite-bitwise-cmp"]
OPTIONAL_COMMANDS = ["fluxsite-bitwise-cmp", "gen_codecov"]


def get_met_forcing_file_names(experiment: str) -> list[str]:
Expand Down
Loading

0 comments on commit f4c3c19

Please sign in to comment.