Skip to content

Commit adea1aa

Browse files
authored
Merge pull request #1127 from agodianel/fix/832-total-power-zero-division
fix: handle empty power history in CPU.total_power() (#832)
2 parents ce16f64 + b10a1ff commit adea1aa

2 files changed

Lines changed: 69 additions & 4 deletions

File tree

codecarbon/external/hardware.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -377,12 +377,12 @@ def _get_energy_from_cpus(self, delay: Time) -> Energy:
377377

378378
def total_power(self) -> Power:
379379
self._power_history.append(self._get_power_from_cpus())
380-
if len(self._power_history) == 0:
381-
logger.warning("Power history is empty, returning 0 W")
382-
return Power.from_watts(0)
383380
power_history_in_W = [power.W for power in self._power_history]
384-
cpu_power = sum(power_history_in_W) / len(power_history_in_W)
385381
self._power_history = []
382+
if not power_history_in_W:
383+
logger.warning("No power samples collected, returning 0 W")
384+
return Power.from_watts(0)
385+
cpu_power = sum(power_history_in_W) / len(power_history_in_W)
386386
return Power.from_watts(cpu_power)
387387

388388
def measure_power_and_energy(self, last_duration: float) -> Tuple[Power, Energy]:

tests/test_cpu_load.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,68 @@ def test_cpu_calculate_power_from_cpu_load_linear(
121121
for test in tests_values:
122122
power = cpu._calculate_power_from_cpu_load(tdp, test["cpu_load"], cpu_model)
123123
self.assertEqual(power, test["expected_power"])
124+
125+
@mock.patch(
126+
"codecarbon.external.hardware.CPU._get_power_from_cpus",
127+
return_value=Power.from_watts(0),
128+
)
129+
def test_cpu_total_power_empty_history(
130+
self,
131+
mocked_is_psutil_available,
132+
mocked_is_powergadget_available,
133+
mocked_is_rapl_available,
134+
mocked_get_power_from_cpus,
135+
):
136+
"""Regression test for issue #832: ZeroDivisionError when no power samples collected."""
137+
cpu = CPU.from_utils(
138+
None, MODE_CPU_LOAD, "Intel(R) Core(TM) i7-7600U CPU @ 2.80GHz", 100
139+
)
140+
# Simulate an empty power history before calling total_power
141+
cpu._power_history = []
142+
result = cpu.total_power()
143+
# Should NOT raise ZeroDivisionError, should return a valid Power object
144+
self.assertIsInstance(result, Power)
145+
self.assertEqual(result.W, 0)
146+
147+
@mock.patch(
148+
"codecarbon.external.hardware.CPU._get_power_from_cpus",
149+
return_value=Power.from_watts(42),
150+
)
151+
def test_cpu_total_power_fetches_sample_when_history_is_empty(
152+
self,
153+
mocked_get_power_from_cpus,
154+
mocked_is_psutil_available,
155+
mocked_is_powergadget_available,
156+
mocked_is_rapl_available,
157+
):
158+
"""Calling total_power should always fetch the latest sample before averaging."""
159+
cpu = CPU.from_utils(
160+
None, MODE_CPU_LOAD, "Intel(R) Core(TM) i7-7600U CPU @ 2.80GHz", 100
161+
)
162+
cpu._power_history = []
163+
result = cpu.total_power()
164+
self.assertEqual(result.W, 42)
165+
mocked_get_power_from_cpus.assert_called_once()
166+
self.assertEqual(cpu._power_history, [])
167+
168+
@mock.patch(
169+
"codecarbon.external.hardware.CPU._get_power_from_cpus",
170+
return_value=Power.from_watts(30),
171+
)
172+
def test_cpu_total_power_averages_buffered_and_latest_sample(
173+
self,
174+
mocked_get_power_from_cpus,
175+
mocked_is_psutil_available,
176+
mocked_is_powergadget_available,
177+
mocked_is_rapl_available,
178+
):
179+
cpu = CPU.from_utils(
180+
None, MODE_CPU_LOAD, "Intel(R) Core(TM) i7-7600U CPU @ 2.80GHz", 100
181+
)
182+
cpu._power_history = [Power.from_watts(10), Power.from_watts(20)]
183+
184+
result = cpu.total_power()
185+
186+
self.assertEqual(result.W, 20)
187+
self.assertEqual(cpu._power_history, [])
188+
mocked_get_power_from_cpus.assert_called_once()

0 commit comments

Comments
 (0)