dotnet / dotnet/SqlClient

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
:new: Triage Needed Area\Engineering Performance :chart_with_upwards_trend:
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.