Skip to content

Commit 3242f27

Browse files
authored
Merge pull request #1054 from arno-hc/experiment
Fix issues for Windows hosts
2 parents 53eec49 + abcaf04 commit 3242f27

3 files changed

Lines changed: 104 additions & 8 deletions

File tree

codecarbon/core/cpu.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -208,14 +208,29 @@ def is_rapl_available(rapl_dir: Optional[str] = None) -> bool:
208208

209209
def is_psutil_available():
210210
try:
211-
nice = psutil.cpu_times().nice
212-
if nice > 0.0001:
213-
return True
211+
cpu_times = psutil.cpu_times()
212+
213+
# platforms like Windows do not have 'nice' attribute
214+
if hasattr(cpu_times, "nice"):
215+
nice = cpu_times.nice
216+
if nice > 0.0001:
217+
return True
218+
else:
219+
logger.debug(
220+
f"is_psutil_available(): psutil.cpu_times().nice is too small: {nice}"
221+
)
222+
return False
223+
214224
else:
225+
# Fallback: check if psutil works by calling cpu_percent
215226
logger.debug(
216-
f"is_psutil_available() : psutil.cpu_times().nice is too small : {nice} !"
227+
"is_psutil_available(): no 'nice' attribute, using fallback check."
217228
)
218-
return False
229+
230+
# check CPU utilization usable
231+
psutil.cpu_percent(interval=0.0, percpu=False)
232+
return True
233+
219234
except Exception as e:
220235
logger.debug(
221236
"Not using the psutil interface, an exception occurred while instantiating "

codecarbon/core/util.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def count_physical_cpus():
103103
import subprocess
104104

105105
if platform.system() == "Windows":
106-
return int(os.environ.get("NUMBER_OF_PROCESSORS", 1))
106+
return _windows_get_physical_sockets()
107107
else:
108108
try:
109109
output = subprocess.check_output(["lscpu"], text=True)
@@ -119,6 +119,28 @@ def count_physical_cpus():
119119
return 1
120120

121121

122+
def _windows_get_physical_sockets():
123+
try:
124+
# use PowerShell to count number of objects of class Win32_Processor
125+
cmd = [
126+
"powershell",
127+
"-NoProfile",
128+
"-Command",
129+
"(Get-CimInstance -ClassName Win32_Processor).Count",
130+
]
131+
result = subprocess.run(
132+
cmd, capture_output=True, text=True, timeout=10, check=True
133+
)
134+
135+
output = result.stdout.strip() or "1"
136+
logger.debug(f"Detected {output} physical sockets on Windows.")
137+
return int(output)
138+
139+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, ValueError) as e:
140+
logger.error(f"Error detecting physical sockets on Windows: {e}")
141+
return 1 # Fallback:at least one socket
142+
143+
122144
def count_cpus() -> int:
123145
if SLURM_JOB_ID is None:
124146
return psutil.cpu_count()

tests/test_cpu.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,45 @@
1111
IntelPowerGadget,
1212
IntelRAPL,
1313
is_powergadget_available,
14+
is_psutil_available,
1415
)
1516
from codecarbon.core.units import Energy, Power, Time
1617
from codecarbon.core.util import count_physical_cpus
1718
from codecarbon.external.hardware import CPU
1819
from codecarbon.input import DataSource
1920

2021

22+
class TestCPU(unittest.TestCase):
23+
@mock.patch("psutil.cpu_times")
24+
def test_is_psutil_available_with_nice(self, mock_cpu_times):
25+
# Create a mock with 'nice' attribute
26+
mock_times = mock.Mock()
27+
mock_times.nice = 0.1
28+
mock_cpu_times.return_value = mock_times
29+
self.assertTrue(is_psutil_available())
30+
31+
@mock.patch("psutil.cpu_times")
32+
def test_is_psutil_available_with_small_nice(self, mock_cpu_times):
33+
# Test when nice attribute is too small
34+
mock_times = mock.Mock()
35+
mock_times.nice = 0.00001
36+
mock_cpu_times.return_value = mock_times
37+
self.assertFalse(is_psutil_available())
38+
39+
@mock.patch("psutil.cpu_times")
40+
def test_is_psutil_available_without_nice(self, mock_cpu_times):
41+
# Create a mock without 'nice' attribute (like Windows)
42+
mock_times = mock.Mock(spec=[]) # Empty spec = no attributes
43+
mock_cpu_times.return_value = mock_times
44+
with mock.patch("psutil.cpu_percent") as mock_cpu_percent:
45+
self.assertTrue(is_psutil_available())
46+
mock_cpu_percent.assert_called_once_with(interval=0.0, percpu=False)
47+
48+
@mock.patch("psutil.cpu_times", side_effect=Exception("Test error"))
49+
def test_is_psutil_not_available_on_exception(self, mock_cpu_times):
50+
self.assertFalse(is_psutil_available())
51+
52+
2153
class TestIntelPowerGadget(unittest.TestCase):
2254
@pytest.mark.integ_test
2355
def test_intel_power_gadget(self):
@@ -306,10 +338,37 @@ def test_get_matching_cpu(self):
306338
class TestPhysicalCPU(unittest.TestCase):
307339
def test_count_physical_cpus_windows(self):
308340
with mock.patch("platform.system", return_value="Windows"):
309-
with mock.patch.dict(os.environ, {"NUMBER_OF_PROCESSORS": "4"}):
341+
342+
with mock.patch(
343+
"subprocess.run", return_value=mock.Mock(returncode=0, stdout="4")
344+
):
310345
assert count_physical_cpus() == 4
311346

312-
with mock.patch.dict(os.environ, {}, clear=True):
347+
with mock.patch(
348+
"subprocess.run", return_value=mock.Mock(returncode=0, stdout="")
349+
):
350+
assert count_physical_cpus() == 1
351+
352+
def test_count_physical_cpus_windows_with_error(self):
353+
with mock.patch("platform.system", return_value="Windows"):
354+
# Test CalledProcessError
355+
with mock.patch(
356+
"subprocess.run",
357+
side_effect=subprocess.CalledProcessError(1, "powershell"),
358+
):
359+
assert count_physical_cpus() == 1
360+
361+
# Test TimeoutExpired
362+
with mock.patch(
363+
"subprocess.run",
364+
side_effect=subprocess.TimeoutExpired("powershell", 10),
365+
):
366+
assert count_physical_cpus() == 1
367+
368+
# Test ValueError when converting invalid output
369+
with mock.patch(
370+
"subprocess.run", return_value=mock.Mock(returncode=0, stdout="invalid")
371+
):
313372
assert count_physical_cpus() == 1
314373

315374
def test_count_physical_cpus_linux(self):

0 commit comments

Comments
 (0)