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
75 changes: 75 additions & 0 deletions Lib/test/test_tools/test_ftscalingbench.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Tests for Tools/ftscalingbench/ftscalingbench.py."""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally don't think that we have to keep this file just for internal tooling.


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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
``Tools/ftscalingbench`` now uses every performance core on processors that

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need NEWS.d for internal tool.

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.
9 changes: 8 additions & 1 deletion Tools/ftscalingbench/ftscalingbench.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:

@corona10 corona10 Aug 25, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the core group is separated into two groups as you siad, why just check something like this?

Suggested change
if node == 0 and core not in cores and maxmhz >= min_mhz_wanted:
if node == 0 and core not in cores and maxmhz > min_mhz_all:

cpus.append(cpu)
cores.add(core)
return cpus
Expand Down
Loading