diff --git a/Lib/test/test_tools/test_ftscalingbench.py b/Lib/test/test_tools/test_ftscalingbench.py new file mode 100644 index 00000000000000..81fe925c8d64fe --- /dev/null +++ b/Lib/test/test_tools/test_ftscalingbench.py @@ -0,0 +1,75 @@ +"""Tests for Tools/ftscalingbench/ftscalingbench.py.""" + +import sys +import unittest +from unittest import mock + +from test.test_tools import skip_if_missing, imports_under_tool + +skip_if_missing('ftscalingbench') + +with imports_under_tool('ftscalingbench'): + import ftscalingbench + + +def lscpu(rows): + """Build `lscpu -p=cpu,node,core,MAXMHZ` output from (cpu, node, core, mhz).""" + lines = ['# cpu,node,core,MAXMHZ'] + lines += [f'{cpu},{node},{core},{mhz}' for cpu, node, core, mhz in rows] + return '\n'.join(lines) + '\n' + + +def smt_rows(count, mhz, first_cpu=0, first_core=0): + """Rows for `count` cores with two hardware threads each.""" + rows = [] + for i in range(count): + cpu = first_cpu + i * 2 + rows.append((cpu, 0, first_core + i, mhz)) + rows.append((cpu + 1, 0, first_core + i, mhz)) + return rows + + +class DetermineAffinityTests(unittest.TestCase): + + def select(self, output): + with (mock.patch('subprocess.check_output', return_value=output), + mock.patch.object(sys, 'platform', 'linux')): + return ftscalingbench.determine_num_threads_and_affinity() + + def test_performance_cores_binned_at_different_clocks(self): + # Two of the eight performance cores clock higher than the rest. + rows = smt_rows(4, '5000.0000') + rows += smt_rows(2, '5200.0000', first_cpu=8, first_core=4) + rows += smt_rows(2, '5000.0000', first_cpu=12, first_core=6) + rows += [(16 + i, 0, 8 + i, '3700.0000') for i in range(8)] + self.assertEqual(self.select(lscpu(rows)), + [0, 2, 4, 6, 8, 10, 12, 14]) + + def test_efficiency_cores_are_skipped(self): + rows = smt_rows(4, '4800.0000') + rows += [(8 + i, 0, 4 + i, '3600.0000') for i in range(4)] + self.assertEqual(self.select(lscpu(rows)), [0, 2, 4, 6]) + + def test_one_thread_per_physical_core(self): + self.assertEqual(self.select(lscpu(smt_rows(8, '3700.0000'))), + [0, 2, 4, 6, 8, 10, 12, 14]) + + def test_missing_max_clock(self): + # MAXMHZ is empty on some kernels and in many virtual machines. + rows = [(i, 0, i, '') for i in range(4)] + self.assertEqual(self.select(lscpu(rows)), [0, 1, 2, 3]) + + def test_second_numa_node_is_ignored(self): + rows = [(i, 0, i, '3000.0000') for i in range(4)] + rows += [(4 + i, 1, 4 + i, '3000.0000') for i in range(4)] + self.assertEqual(self.select(lscpu(rows)), [0, 1, 2, 3]) + + def test_lscpu_missing(self): + with (mock.patch('subprocess.check_output', side_effect=FileNotFoundError), + mock.patch.object(sys, 'platform', 'linux')): + cpus = ftscalingbench.determine_num_threads_and_affinity() + self.assertTrue(all(cpu is None for cpu in cpus)) + + +if __name__ == '__main__': + unittest.main() diff --git a/Misc/NEWS.d/next/Tools-Demos/2026-08-02-11-42-15.gh-issue-155090.Kq7mZt.rst b/Misc/NEWS.d/next/Tools-Demos/2026-08-02-11-42-15.gh-issue-155090.Kq7mZt.rst new file mode 100644 index 00000000000000..0c32dbc1a1b012 --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2026-08-02-11-42-15.gh-issue-155090.Kq7mZt.rst @@ -0,0 +1,4 @@ +``Tools/ftscalingbench`` now uses every performance core on processors that +clock some of them higher than others, such as Intel parts with Turbo Boost +Max 3.0. It previously kept only the fastest cores and ran with fewer threads +than the machine offered. diff --git a/Tools/ftscalingbench/ftscalingbench.py b/Tools/ftscalingbench/ftscalingbench.py index a79242e740371b..f0e9e0574825aa 100644 --- a/Tools/ftscalingbench/ftscalingbench.py +++ b/Tools/ftscalingbench/ftscalingbench.py @@ -403,10 +403,17 @@ def determine_num_threads_and_affinity(): cpus = [] cores = set() max_mhz_all = max(row[3] for row in table) + min_mhz_all = min(row[3] for row in table) + # Performance cores are not always binned to the same clock, so split them + # from the efficiency cores at the midpoint rather than at the maximum. + if max_mhz_all != min_mhz_all: + min_mhz_wanted = (max_mhz_all + min_mhz_all) / 2 + else: + min_mhz_wanted = 0 for cpu, node, core, maxmhz in table: # Choose only CPUs on the same node, unique cores, and try to avoid # "efficiency" cores. - if node == 0 and core not in cores and maxmhz == max_mhz_all: + if node == 0 and core not in cores and maxmhz >= min_mhz_wanted: cpus.append(cpu) cores.add(core) return cpus