Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 22 additions & 23 deletions codecarbon/core/powermetrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def __init__(
self._system = sys.platform.lower()
self._n_points = n_points
self._interval = interval
self._warned_missing_samples = set()
self._setup_cli()

def _setup_cli(self) -> None:
Expand Down Expand Up @@ -175,29 +176,27 @@ def get_details(self) -> Dict:
try:
with open(self._log_file_path) as f:
logfile = f.read()
cpu_pattern = r"CPU Power: (\d+) mW"
cpu_power_list = re.findall(cpu_pattern, logfile)

details["CPU Power"] = np.mean(
[float(power) / 1000 for power in cpu_power_list]
)
details["CPU Energy Delta"] = np.sum(
[
(self._interval / 1000) * (float(power) / 1000)
for power in cpu_power_list
]
)
gpu_pattern = r"GPU Power: (\d+) mW"
gpu_power_list = re.findall(gpu_pattern, logfile)
details["GPU Power"] = np.mean(
[float(power) / 1000 for power in gpu_power_list]
)
details["GPU Energy Delta"] = np.sum(
[
(self._interval / 1000) * (float(power) / 1000)
for power in gpu_power_list
]
)
for chip_part in ("CPU", "GPU"):
power_list = re.findall(rf"{chip_part} Power: (\d+) mW", logfile)
if not power_list:
# np.mean([]) is NaN, and NaN poisons every downstream total,
# so report 0 W instead and make the situation visible.
# get_details() runs every measurement cycle, so warn only
# once per chip part to avoid flooding the log.
if chip_part not in self._warned_missing_samples:
self._warned_missing_samples.add(chip_part)
logger.warning(
f"Powermetrics returned no '{chip_part} Power' sample in "
+ f"{self._log_file_path}, reporting 0 W (warned once)."
)
details[f"{chip_part} Power"] = 0.0
details[f"{chip_part} Energy Delta"] = 0.0
continue
watts = [float(power) / 1000 for power in power_list]
details[f"{chip_part} Power"] = np.mean(watts)
details[f"{chip_part} Energy Delta"] = np.sum(
[(self._interval / 1000) * watt for watt in watts]
)
except Exception as e:
logger.info(
f"Unable to read Powermetrics logged file at {self._log_file_path}\n \
Expand Down
53 changes: 53 additions & 0 deletions tests/test_powermetrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,59 @@ def test_get_details(self, mock_setup, mock_log_values):

assert cpu_details == expected_details

@mock.patch("codecarbon.core.powermetrics.ApplePowermetrics._log_values")
@mock.patch("codecarbon.core.powermetrics.ApplePowermetrics._setup_cli")
def test_get_details_without_samples(self, mock_setup, mock_log_values, tmp_path):
"""An empty log must report 0 W, not NaN, which would poison all totals."""
(tmp_path / "empty_powermetrics_log.txt").write_text("")
powermetrics = ApplePowermetrics(
output_dir=str(tmp_path),
log_file_name="empty_powermetrics_log.txt",
)

assert powermetrics.get_details() == {
"CPU Power": 0.0,
"CPU Energy Delta": 0.0,
"GPU Power": 0.0,
"GPU Energy Delta": 0.0,
}

@mock.patch("codecarbon.core.powermetrics.ApplePowermetrics._log_values")
@mock.patch("codecarbon.core.powermetrics.ApplePowermetrics._setup_cli")
def test_get_details_without_gpu_samples(
self, mock_setup, mock_log_values, tmp_path
):
"""A log with no GPU line must report 0 W for the GPU, not NaN."""
(tmp_path / "cpu_only_log.txt").write_text("CPU Power: 500 mW\n")
powermetrics = ApplePowermetrics(
output_dir=str(tmp_path),
log_file_name="cpu_only_log.txt",
)

details = powermetrics.get_details()

assert details["CPU Power"] == 0.5
assert details["GPU Power"] == 0.0
assert details["GPU Energy Delta"] == 0.0

@mock.patch("codecarbon.core.powermetrics.ApplePowermetrics._log_values")
@mock.patch("codecarbon.core.powermetrics.ApplePowermetrics._setup_cli")
def test_missing_samples_warns_only_once(
self, mock_setup, mock_log_values, tmp_path
):
"""get_details() runs every cycle, so the warning must not flood the log."""
(tmp_path / "cpu_only_log.txt").write_text("CPU Power: 500 mW\n")
powermetrics = ApplePowermetrics(
output_dir=str(tmp_path),
log_file_name="cpu_only_log.txt",
)

with mock.patch("codecarbon.core.powermetrics.logger.warning") as mock_warning:
for _ in range(3):
assert powermetrics.get_details()["GPU Power"] == 0.0

mock_warning.assert_called_once()

def test_is_powermetrics_available_returns_false_on_instantiation_error(self):
from codecarbon.core.powermetrics import clear_powermetrics_cache

Expand Down
Loading