Perf harness: SetProcessAffinityMask called without ctypes argtypes, so Windows CPU pinning does not take effect

Open Beginner friendly
#4,617 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
74/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python

Research direction

Start in eng/pipelines/perf/scripts/interleave_perf.py at apply_affinity and review the Windows SetProcessAffinityMask call and its exception handling. Run interleave_perf.py on 64-bit Windows with --cpus, then verify the spawned processes' affinity using Process Explorer or GetProcessAffinityMask; done means the requested CPUs are applied and genuine failures are reported accurately.

Written by the indexing model from the issue text.

Description

:new: Triage Needed Area\Engineering Performance :chart_with_upwards_trend:
Describe the bug

apply_affinity in eng/pipelines/perf/scripts/interleave_perf.py calls SetProcessAffinityMask through ctypes.windll without declaring argtypes or restype:

https://github.com/dotnet/SqlClient/blob/main/eng/pipelines/perf/scripts/interleave_perf.py#L93-L95

handle = int(proc._handle)  # noqa: SLF001  (Popen exposes the OS handle here)
if ctypes.windll.kernel32.SetProcessAffinityMask(handle, ctypes.c_size_t(mask)) == 0:
    print(f"WARNING: SetProcessAffinityMask failed for pid {proc.pid}.", file=sys.stderr)

Without argtypes, ctypes marshals a plain Python int as a C int (32-bit). handle comes from proc._handle, which is a 64-bit HANDLE on 64-bit Windows, so it is truncated before the call. The result is that the process is not pinned.

Two things make this worse than a loud failure:

  1. The whole body is wrapped in try/except Exception with the docstring "Never raises — pinning is an optimisation, not a gate", so the run continues regardless.
  2. The only signal is a WARNING line on stderr, which is easy to miss in pipeline logs. If a truncated handle happens to collide with another valid handle in the process, the call can succeed against the wrong target instead of failing.

Either way the interleaved A/B runs execute without the CPU isolation the harness is designed to provide.

Note this affects Windows only. The Linux path takes the earlier os.sched_setaffinity branch and is unaffected.

To reproduce

N/A — this is an engineering/infrastructure bug in the perf harness rather than a driver bug, so there is no C# repro.

To observe it, run interleave_perf.py on 64-bit Windows with a --cpus spec and check whether the spawned benchmark processes are actually pinned (for example via Process Explorer, or by calling GetProcessAffinityMask back and comparing). The mask will not match what was requested.

Expected behavior

The spawned benchmark processes are pinned to the requested CPUs on Windows, and a genuine failure to pin is reported accurately.

Suggested fix

Declare the signatures explicitly and pass a real HANDLE:

from ctypes import wintypes

kernel32 = ctypes.windll.kernel32
kernel32.SetProcessAffinityMask.argtypes = [wintypes.HANDLE, ctypes.c_size_t]
kernel32.SetProcessAffinityMask.restype = wintypes.BOOL

handle = wintypes.HANDLE(int(proc._handle))
if not kernel32.SetProcessAffinityMask(handle, ctypes.c_size_t(mask)):
    err = ctypes.get_last_error()
    ...

Worth pairing with a GetProcessAffinityMask read-back so the script can report the mask that actually took effect rather than assuming the set succeeded. GetProcessAffinityMask needs its own argtypes for the same reason.

I have a working patch for this locally and can open a PR.

Further technical details
  • Component: perf test harness (eng/pipelines/perf/scripts/interleave_perf.py)
  • Microsoft.Data.SqlClient version: N/A (does not affect shipped product code)
  • .NET target: N/A
  • SQL Server version: N/A
  • Operating system: 64-bit Windows only

Additional context

Found while working on the perf switch-experiment pipeline in #4543. It is pre-existing on main and unrelated to that PR's changes, so filing separately rather than folding it in.

The practical impact is on perf measurement quality: any Windows perf run that relies on pinning is not actually pinned, which adds scheduler noise and undermines the interleaved A/B comparison the harness exists to produce.

Dominant language
C#
Stars
990
Forks
340
Avg merge
3d 1h
Merged PRs (30d)
62

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/SqlClient

All issues in dotnet/SqlClient

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.