Enabled windows ProcessorAffinity support#629
Enabled windows ProcessorAffinity support#629RakeshwarK wants to merge 16 commits intomicrosoft:mainfrom
Conversation
nchapagain001
left a comment
There was a problem hiding this comment.
Let's make changes directly in CreateProcessWithAffinity() to allow windows process affinity. Users shouldn't have to make additional call for the same thing.
There was a problem hiding this comment.
Pull request overview
This PR enables CPU affinity support for Virtual Client processes on Windows by using the Process.ProcessorAffinity API. The implementation follows a different approach than Linux: Windows applies affinity immediately after starting the process (creating a small ~1ms timing window), while Linux uses numactl to wrap the process execution command (zero timing window). The Windows implementation is limited to 64 cores due to processor group constraints.
Changes:
- Added
WindowsProcessAffinityConfigurationclass to handle Windows-specific CPU affinity using bitmask-based ProcessorAffinity API - Created
ApplyAffinityextension method forIProcessProxyto apply Windows affinity after process start - Added comprehensive unit and functional tests for Windows affinity support
- Provided example executor (
ExampleWorkloadWithAffinityExecutor) and profile demonstrating CPU affinity usage - Updated documentation to explain CPU affinity support and platform differences
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/VirtualClient/VirtualClient.Common/ProcessAffinity/WindowsProcessAffinityConfiguration.cs | Core implementation of Windows CPU affinity using ProcessorAffinity bitmask, includes reflection-based approach to access underlying process |
| src/VirtualClient/VirtualClient.Common/ProcessAffinity/ProcessAffinityConfiguration.cs | Updated factory method to support Windows platform in addition to Linux |
| src/VirtualClient/VirtualClient.Core/ProcessExtensions.cs | Added ApplyAffinity extension method for applying Windows affinity to running processes |
| src/VirtualClient/VirtualClient.TestFramework/InMemoryProcess.cs | Added OnApplyAffinity delegate to support testing affinity application |
| src/VirtualClient/VirtualClient.Common.UnitTests/ProcessAffinity/WindowsProcessAffinityConfigurationTests.cs | Unit tests for Windows affinity configuration including bitmask calculation and validation |
| src/VirtualClient/VirtualClient.Core.UnitTests/ProcessExtensionsAffinityTests.cs | Unit tests for process extension methods, verifying Windows throws on pre-start affinity methods |
| src/VirtualClient/VirtualClient.Actions/Examples/ExampleWorkloadWithAffinityExecutor.cs | Reference implementation showing how to use CPU affinity in workload executors (contains critical bug in Linux command construction) |
| src/VirtualClient/VirtualClient.Actions.FunctionalTests/ExampleWorkloadWithAffinityProfileTests.cs | Functional tests for the example affinity workload on both Linux and Windows platforms |
| src/VirtualClient/VirtualClient.Main/profiles/PERF-CPU-EXAMPLE-AFFINITY.json | Example profile demonstrating CPU affinity configuration |
| website/docs/developing/0030-workload-onboarding.md | Documentation for CPU affinity feature including core specification formats and platform differences |
| website/docs/developing/0010-develop-guide.md | Updated developer guide to reference CPU affinity support |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| this.WorkloadExecutablePath, | ||
| commandArguments); |
There was a problem hiding this comment.
The usage of GetCommandWithAffinity is incorrect for this scenario. The method is designed to wrap bash commands (e.g., GetCommandWithAffinity("bash -c", "command args") produces bash -c "numactl -C 0-3 command args"). However, here it's being called with the executable path directly, which would produce an invalid command structure like /path/to/exe "numactl -C 0-3 args". The correct approach would be to pass empty string as the first parameter and combine the executable and arguments in the second parameter, or use the pattern shown in the tests: GetCommandWithAffinity("", $"{this.WorkloadExecutablePath} {commandArguments}").
| this.WorkloadExecutablePath, | |
| commandArguments); | |
| string.Empty, | |
| $"{this.WorkloadExecutablePath} {commandArguments}"); |
| // Use reflection to access the protected UnderlyingProcess property | ||
| System.Reflection.PropertyInfo propertyInfo = typeof(ProcessProxy).GetProperty( | ||
| "UnderlyingProcess", | ||
| System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); | ||
|
|
||
| if (propertyInfo == null) | ||
| { | ||
| throw new NotSupportedException("Unable to access the underlying process for affinity configuration."); | ||
| } | ||
|
|
||
| System.Diagnostics.Process underlyingProcess = propertyInfo.GetValue(processProxy) as System.Diagnostics.Process; | ||
| if (underlyingProcess != null) | ||
| { | ||
| underlyingProcess.ProcessorAffinity = this.AffinityMask; | ||
| } |
There was a problem hiding this comment.
The use of reflection to access the protected UnderlyingProcess property creates a fragile coupling that could break silently if the ProcessProxy class structure changes. Consider adding a public method to ProcessProxy (e.g., SetProcessorAffinity or ApplyAffinityMask) that internally accesses UnderlyingProcess and sets ProcessorAffinity. This would create an explicit API contract, improve maintainability, and make the intention clearer. The current approach bypasses encapsulation boundaries which may have been intentional.
| **CPU Affinity Support** | ||
| Virtual Client supports binding workload processes to specific CPU cores to enable core isolation testing. For detailed guidance on implementing | ||
| CPU affinity in workload executors, see the [Workload Onboarding Process](./0030-workload-onboarding.md#cpu-core-affinity-optional) documentation. |
There was a problem hiding this comment.
The indentation and formatting of the "CPU Affinity Support" subsection is inconsistent with the rest of the document. Other sections use bullet points (asterisks) for subsections, but this one doesn't. Consider reformatting to match the existing pattern, for example by indenting it further or adding it as a bullet point under "Process Management" to maintain consistency with the document structure.
| }; | ||
|
|
||
| // Simulate process completion when WaitForExitAsync is called | ||
| Task originalWait = process.WaitForExitAsync(CancellationToken.None); |
There was a problem hiding this comment.
The variable 'originalWait' is assigned but never used. This line creates a Task that is never awaited or stored meaningfully. Either remove this line if it's not needed, or clarify its purpose if it's meant to serve some test setup function.
| Task originalWait = process.WaitForExitAsync(CancellationToken.None); |
We cannot do that because for windows we can only apply the affinity once the process is started, In the executor there is explicit calls for StartAndWait after process creation so we need to call Start then apply affinity and call WaitAsync. |
This PR enables CPU affinity support for Virtual Client process on windows, by setting affinity via Process.ProcessorAffinity API.
Why Different Approaches for Windows vs. Linux?
-On Linux, we use numactl to wrap the process execution command.
-Windows doesn't have a built-in numactl equivalent, the Process.ProcessorAffinity property is the standard .NET approach which leverages the existing process management infrastructure.
process.Start();
process.ProcessorAffinity = affinityMask;
Limitations for Windows:
Set thread group + CPU mask > Create thread in a group > Query group topology > Multi‑group thread affinity