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
19 changes: 12 additions & 7 deletions codecarbon/external/hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import math
import re
import threading
import time
from abc import ABC, abstractmethod
from dataclasses import dataclass
Expand Down Expand Up @@ -207,6 +208,9 @@ def __init__(
):
assert tracking_mode in ["machine", "process"]
self._power_history: List[Power] = []
# The monitor scheduler thread appends samples while the measurement
# thread drains them; the lock is only held for the O(1) swap.
self._power_history_lock = threading.Lock()
self._output_dir = output_dir
self._mode = mode
self._model = model
Expand Down Expand Up @@ -393,12 +397,12 @@ def _get_energy_from_cpus(self, delay: Time) -> Energy:
return Energy.from_energy(energy)

def total_power(self) -> Power:
self._power_history.append(self._get_power_from_cpus())
power_history_in_W = [power.W for power in self._power_history]
self._power_history = []
if not power_history_in_W:
logger.warning("No power samples collected, returning 0 W")
return Power.from_watts(0)
latest_sample = self._get_power_from_cpus()
with self._power_history_lock:
power_history = self._power_history
self._power_history = []
power_history.append(latest_sample)
power_history_in_W = [power.W for power in power_history]
cpu_power = sum(power_history_in_W) / len(power_history_in_W)
return Power.from_watts(cpu_power)

Expand Down Expand Up @@ -432,7 +436,8 @@ def start(self):

def monitor_power(self):
cpu_power = self._get_power_from_cpus()
self._power_history.append(cpu_power)
with self._power_history_lock:
self._power_history.append(cpu_power)

def get_model(self):
return self._model
Expand Down
52 changes: 52 additions & 0 deletions tests/test_cpu_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,58 @@ def test_cpu_total_power_fetches_sample_when_history_is_empty(
mocked_get_power_from_cpus.assert_called_once()
self.assertEqual(cpu._power_history, [])

@mock.patch(
"codecarbon.external.hardware.CPU._get_power_from_cpus",
return_value=Power.from_watts(1),
)
def test_cpu_total_power_keeps_samples_added_while_draining(
self,
mocked_get_power_from_cpus,
mocked_is_psutil_available,
mocked_is_powergadget_available,
mocked_is_rapl_available,
):
"""A sample appended by the monitor thread while total_power drains the
history must not be lost (see issue #1315)."""
cpu = CPU.from_utils(
None, MODE_CPU_LOAD, "Intel(R) Core(TM) i7-7600U CPU @ 2.80GHz", 100
)

class ProbedHistory(list):
"""Runs the monitor thread's write the first time `total_power`
touches the history, whichever way it reads it."""

fired = False

def _fire(inner):
if not inner.fired:
inner.fired = True
cpu.monitor_power()

def __iter__(inner):
inner._fire()
return list.__iter__(inner)

def __len__(inner):
inner._fire()
return list.__len__(inner)

def __getitem__(inner, index):
inner._fire()
return list.__getitem__(inner, index)

def copy(inner):
inner._fire()
return list.copy(inner)

cpu._power_history = ProbedHistory([Power.from_watts(1)])

cpu.total_power()

# The interleaved sample must survive the drain, whatever the drain
# is implemented with.
self.assertEqual(len(cpu._power_history), 1)

@mock.patch(
"codecarbon.external.hardware.CPU._get_power_from_cpus",
return_value=Power.from_watts(30),
Expand Down
Loading