Perf harness: SetProcessAffinityMask called without ctypes argtypes, so Windows CPU pinning does not take effect
- Dominant language
- C#
- Stars
- 989
- Forks
- 340
- Avg merge
- 4d 19h
- Merged PRs (30d)
- 72
Description
### 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
```python
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`:
```python
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.
Contributor guide
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.
Assessment
- Tech stack
- python
- Domain
- performance, tooling
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100