From 91eee4a0fd7c6acefa35d501b63ccfbd2cd8f438 Mon Sep 17 00:00:00 2001 From: Arno Date: Thu, 5 Feb 2026 17:51:47 +0100 Subject: [PATCH 1/6] fix: improve CPU detection on Windows and enhance psutil availability check to work on Windows as well --- codecarbon/core/cpu.py | 27 ++++++++++++++++++++------- codecarbon/core/util.py | 24 +++++++++++++++++++++++- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/codecarbon/core/cpu.py b/codecarbon/core/cpu.py index 3ab71cfab..c88d9d7b6 100644 --- a/codecarbon/core/cpu.py +++ b/codecarbon/core/cpu.py @@ -208,14 +208,27 @@ def is_rapl_available(rapl_dir: Optional[str] = None) -> bool: def is_psutil_available(): try: - nice = psutil.cpu_times().nice - if nice > 0.0001: - return True + cpu_times = psutil.cpu_times() + + # platforms like Windows do not have 'nice' attribute + if hasattr(cpu_times, "nice"): + nice = cpu_times.nice + if nice > 0.0001: + return True + else: + logger.debug( + f"is_psutil_available(): psutil.cpu_times().nice is too small: {nice}" + ) + return False + else: - logger.debug( - f"is_psutil_available() : psutil.cpu_times().nice is too small : {nice} !" - ) - return False + #Fallback: check is psutil 'working' by calling cpu_percent + logger.debug("is_psutil_available(): no 'nice' attribute, using fallback check.") + + # check CPU utilization usable + psutil.cpu_percent(interval=0.0, percpu=False) + return True + except Exception as e: logger.debug( "Not using the psutil interface, an exception occurred while instantiating " diff --git a/codecarbon/core/util.py b/codecarbon/core/util.py index facb79ab5..0fc69aa07 100644 --- a/codecarbon/core/util.py +++ b/codecarbon/core/util.py @@ -103,7 +103,10 @@ def count_physical_cpus(): import subprocess if platform.system() == "Windows": - return int(os.environ.get("NUMBER_OF_PROCESSORS", 1)) + # Windows does not have a straightforward way to count physical CPUs (sockets). + # Env variable NUMBER_OF_PROCESSORS gives logical processors, not physical sockets. + # return int(os.environ.get("NUMBER_OF_PROCESSORS", 1)) + return _windows_get_physical_sockets() else: try: output = subprocess.check_output(["lscpu"], text=True) @@ -118,7 +121,26 @@ def count_physical_cpus(): ) return 1 +def _windows_get_physical_sockets(): + try: + # use PowerShell to count number of objects of class Win32_Processor + cmd = ["powershell", "-NoProfile", "-Command", "(Get-CimInstance -ClassName Win32_Processor).Count"] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10, check=True) + + output = result.stdout.strip() + + # Fallback: if Count is empty, at least one socket exists + if not output: + logger.debug("PowerShell command returned empty output for socket count. Defaulting to 1.") + output = 1 + + logger.debug(f"Detected {output} physical sockets on Windows.") + return int(output) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, ValueError) as e: + logger.error(f"Error detecting physical sockets on Windows: {e}") + return 1 # Fallback:at least one socket + def count_cpus() -> int: if SLURM_JOB_ID is None: return psutil.cpu_count() From 489afe5463ba39b6af5595da5c5f577cb458ce35 Mon Sep 17 00:00:00 2001 From: Arno Date: Fri, 6 Feb 2026 16:59:04 +0100 Subject: [PATCH 2/6] fix: add tests for psutil availability and physical cpu count --- codecarbon/core/util.py | 23 +++++++++++++++++------ tests/test_cpu.py | 24 ++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/codecarbon/core/util.py b/codecarbon/core/util.py index 0fc69aa07..eaf3ea7a1 100644 --- a/codecarbon/core/util.py +++ b/codecarbon/core/util.py @@ -121,18 +121,28 @@ def count_physical_cpus(): ) return 1 + def _windows_get_physical_sockets(): try: # use PowerShell to count number of objects of class Win32_Processor - cmd = ["powershell", "-NoProfile", "-Command", "(Get-CimInstance -ClassName Win32_Processor).Count"] - result = subprocess.run(cmd, capture_output=True, text=True, timeout=10, check=True) + cmd = [ + "powershell", + "-NoProfile", + "-Command", + "(Get-CimInstance -ClassName Win32_Processor).Count", + ] + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=10, check=True + ) output = result.stdout.strip() - # Fallback: if Count is empty, at least one socket exists + # Fallback: if empty, at least one socket exists if not output: - logger.debug("PowerShell command returned empty output for socket count. Defaulting to 1.") - output = 1 + logger.debug( + "PowerShell command returned empty output for socket count. Defaulting to 1." + ) + output = 1 logger.debug(f"Detected {output} physical sockets on Windows.") return int(output) @@ -140,7 +150,8 @@ def _windows_get_physical_sockets(): except (subprocess.CalledProcessError, subprocess.TimeoutExpired, ValueError) as e: logger.error(f"Error detecting physical sockets on Windows: {e}") return 1 # Fallback:at least one socket - + + def count_cpus() -> int: if SLURM_JOB_ID is None: return psutil.cpu_count() diff --git a/tests/test_cpu.py b/tests/test_cpu.py index 5453ac4ef..edbcb4ab4 100644 --- a/tests/test_cpu.py +++ b/tests/test_cpu.py @@ -11,6 +11,7 @@ IntelPowerGadget, IntelRAPL, is_powergadget_available, + is_psutil_available, ) from codecarbon.core.units import Energy, Power, Time from codecarbon.core.util import count_physical_cpus @@ -18,6 +19,20 @@ from codecarbon.input import DataSource +class TestCPU(unittest.TestCase): + def test_is_psutil_available(self): + @mock.patch("psutil.cpu_times") + def test_is_psutil_available(self, mock_cpu_times): + # Test when psutil is available + mock_cpu_times.return_value = mock.Mock() + self.assertTrue(is_psutil_available()) # noqa: E712 + + @mock.patch("psutil.cpu_times", side_effect=AttributeError) + def test_is_psutil_not_available(self, mock_cpu_times): + # Test when psutil is not available + self.assertFalse(is_psutil_available()) # noqa: E712 + + class TestIntelPowerGadget(unittest.TestCase): @pytest.mark.integ_test def test_intel_power_gadget(self): @@ -306,10 +321,15 @@ def test_get_matching_cpu(self): class TestPhysicalCPU(unittest.TestCase): def test_count_physical_cpus_windows(self): with mock.patch("platform.system", return_value="Windows"): - with mock.patch.dict(os.environ, {"NUMBER_OF_PROCESSORS": "4"}): + + with mock.patch( + "subprocess.run", return_value=mock.Mock(returncode=0, stdout="4") + ): assert count_physical_cpus() == 4 - with mock.patch.dict(os.environ, {}, clear=True): + with mock.patch( + "subprocess.run", return_value=mock.Mock(returncode=0, stdout="") + ): assert count_physical_cpus() == 1 def test_count_physical_cpus_linux(self): From 6020c6d1d0bdfed218d338c9ebe0893d16f01e6d Mon Sep 17 00:00:00 2001 From: benoit-cty Date: Sun, 8 Feb 2026 19:31:35 +0100 Subject: [PATCH 3/6] lint --- codecarbon/core/cpu.py | 6 ++++-- codecarbon/core/util.py | 3 --- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/codecarbon/core/cpu.py b/codecarbon/core/cpu.py index c88d9d7b6..9ed09d20a 100644 --- a/codecarbon/core/cpu.py +++ b/codecarbon/core/cpu.py @@ -222,8 +222,10 @@ def is_psutil_available(): return False else: - #Fallback: check is psutil 'working' by calling cpu_percent - logger.debug("is_psutil_available(): no 'nice' attribute, using fallback check.") + # Fallback: check if psutil works by calling cpu_percent + logger.debug( + "is_psutil_available(): no 'nice' attribute, using fallback check." + ) # check CPU utilization usable psutil.cpu_percent(interval=0.0, percpu=False) diff --git a/codecarbon/core/util.py b/codecarbon/core/util.py index eaf3ea7a1..f94e016f6 100644 --- a/codecarbon/core/util.py +++ b/codecarbon/core/util.py @@ -103,9 +103,6 @@ def count_physical_cpus(): import subprocess if platform.system() == "Windows": - # Windows does not have a straightforward way to count physical CPUs (sockets). - # Env variable NUMBER_OF_PROCESSORS gives logical processors, not physical sockets. - # return int(os.environ.get("NUMBER_OF_PROCESSORS", 1)) return _windows_get_physical_sockets() else: try: From 31c3f4c5d0969c4fd925c32534dc656d1afbc1b4 Mon Sep 17 00:00:00 2001 From: benoit-cty Date: Sun, 8 Feb 2026 19:46:56 +0100 Subject: [PATCH 4/6] Fix type --- codecarbon/core/util.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/codecarbon/core/util.py b/codecarbon/core/util.py index f94e016f6..b9ec93b7b 100644 --- a/codecarbon/core/util.py +++ b/codecarbon/core/util.py @@ -132,15 +132,7 @@ def _windows_get_physical_sockets(): cmd, capture_output=True, text=True, timeout=10, check=True ) - output = result.stdout.strip() - - # Fallback: if empty, at least one socket exists - if not output: - logger.debug( - "PowerShell command returned empty output for socket count. Defaulting to 1." - ) - output = 1 - + output = result.stdout.strip() or "1" logger.debug(f"Detected {output} physical sockets on Windows.") return int(output) From 9e72978a601a3d58ce0a2f1db62192bcd6c354f2 Mon Sep 17 00:00:00 2001 From: benoit-cty Date: Sun, 8 Feb 2026 19:47:24 +0100 Subject: [PATCH 5/6] Better test --- tests/test_cpu.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/tests/test_cpu.py b/tests/test_cpu.py index edbcb4ab4..e1f70e1d6 100644 --- a/tests/test_cpu.py +++ b/tests/test_cpu.py @@ -20,17 +20,26 @@ class TestCPU(unittest.TestCase): - def test_is_psutil_available(self): - @mock.patch("psutil.cpu_times") - def test_is_psutil_available(self, mock_cpu_times): - # Test when psutil is available - mock_cpu_times.return_value = mock.Mock() - self.assertTrue(is_psutil_available()) # noqa: E712 - - @mock.patch("psutil.cpu_times", side_effect=AttributeError) - def test_is_psutil_not_available(self, mock_cpu_times): - # Test when psutil is not available - self.assertFalse(is_psutil_available()) # noqa: E712 + @mock.patch("psutil.cpu_times") + def test_is_psutil_available_with_nice(self, mock_cpu_times): + # Create a mock with 'nice' attribute + mock_times = mock.Mock() + mock_times.nice = 0.1 + mock_cpu_times.return_value = mock_times + self.assertTrue(is_psutil_available()) + + @mock.patch("psutil.cpu_times") + def test_is_psutil_available_without_nice(self, mock_cpu_times): + # Create a mock without 'nice' attribute (like Windows) + mock_times = mock.Mock(spec=[]) # Empty spec = no attributes + mock_cpu_times.return_value = mock_times + with mock.patch("psutil.cpu_percent") as mock_cpu_percent: + self.assertTrue(is_psutil_available()) + mock_cpu_percent.assert_called_once_with(interval=0.0, percpu=False) + + @mock.patch("psutil.cpu_times", side_effect=Exception("Test error")) + def test_is_psutil_not_available_on_exception(self, mock_cpu_times): + self.assertFalse(is_psutil_available()) class TestIntelPowerGadget(unittest.TestCase): From abcaf046353e0a39dfe15c9a9a65b8bb43b976d2 Mon Sep 17 00:00:00 2001 From: benoit-cty Date: Sun, 8 Feb 2026 20:17:47 +0100 Subject: [PATCH 6/6] test: improve coverage for Windows platform detection - Add test for is_psutil_available() with small nice value - Add comprehensive error handling tests for count_physical_cpus() on Windows: - CalledProcessError - TimeoutExpired - ValueError for invalid output This brings test coverage to 100% for the Windows-specific code paths added in the PR. --- tests/test_cpu.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_cpu.py b/tests/test_cpu.py index e1f70e1d6..f7d34aca7 100644 --- a/tests/test_cpu.py +++ b/tests/test_cpu.py @@ -28,6 +28,14 @@ def test_is_psutil_available_with_nice(self, mock_cpu_times): mock_cpu_times.return_value = mock_times self.assertTrue(is_psutil_available()) + @mock.patch("psutil.cpu_times") + def test_is_psutil_available_with_small_nice(self, mock_cpu_times): + # Test when nice attribute is too small + mock_times = mock.Mock() + mock_times.nice = 0.00001 + mock_cpu_times.return_value = mock_times + self.assertFalse(is_psutil_available()) + @mock.patch("psutil.cpu_times") def test_is_psutil_available_without_nice(self, mock_cpu_times): # Create a mock without 'nice' attribute (like Windows) @@ -341,6 +349,28 @@ def test_count_physical_cpus_windows(self): ): assert count_physical_cpus() == 1 + def test_count_physical_cpus_windows_with_error(self): + with mock.patch("platform.system", return_value="Windows"): + # Test CalledProcessError + with mock.patch( + "subprocess.run", + side_effect=subprocess.CalledProcessError(1, "powershell"), + ): + assert count_physical_cpus() == 1 + + # Test TimeoutExpired + with mock.patch( + "subprocess.run", + side_effect=subprocess.TimeoutExpired("powershell", 10), + ): + assert count_physical_cpus() == 1 + + # Test ValueError when converting invalid output + with mock.patch( + "subprocess.run", return_value=mock.Mock(returncode=0, stdout="invalid") + ): + assert count_physical_cpus() == 1 + def test_count_physical_cpus_linux(self): with mock.patch("platform.system", return_value="Linux"): lscpu_output = "Socket(s): 2\n"