Don't use callbacks when performing sync IO with async handles#126845
Don't use callbacks when performing sync IO with async handles#126845
Conversation
…vent for sync IO with async handles Instead of using ThreadPoolBoundHandle callbacks and reference counting when performing synchronous IO with async file handles, allocate the NativeOverlapped directly via NativeMemory and set the low-order bit of hEvent to suppress I/O completion port notifications. This eliminates the CallbackResetEvent class, the s_callback field, and the AllocateCallback/GetNativeOverlappedForAsyncHandle methods, reducing allocations and complexity. Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d7a61400-c63f-401a-9503-0313459af6f7 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
|
Tagging subscribers to this area: @dotnet/area-system-io |
adamsitnik
left a comment
There was a problem hiding this comment.
@copilot please address my feedback (nits)
src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs
Outdated
Show resolved
Hide resolved
…appedWithEventHandle Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/bcdd8838-718e-4561-abf4-90c9c2deb905 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
🤖 Copilot Code Review — PR #126845Note This review was AI/Copilot-generated via the code-review skill, using multi-model analysis (Gemini 2.5 Pro + GPT-5.3-Codex + my own assessment). Holistic AssessmentMotivation: Justified. The old code used Approach: Sound. The Summary: Detailed Findings✅ Core Approach —
|
@copilot please address this |
… pending assertion Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/0470d835-6cc6-4d14-a53b-8b2f99bc8427 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Removed the |
|
@copilot please invoke the code-review skill and post your analysis/results as a comment on this PR |
🤖 Copilot Code Review — PR #126845Note This review was AI/Copilot-generated via the code-review skill, using multi-model analysis (Gemini 2.5 Pro + GPT-5.3-Codex + my own assessment). Holistic AssessmentMotivation: Justified. The old code used Approach: Sound. The Summary: ✅ LGTM. All three review models (Gemini 2.5 Pro, GPT-5.3-Codex, and my own analysis) agree the change is correct. No blocking or advisory issues remain after the latest commit (99c34d4) removed the unnecessary Detailed Findings✅ Core Approach —
|
adamsitnik
left a comment
There was a problem hiding this comment.
LGTM, to be honest I have not expected such nice performance improvements (EgorBot/Benchmarks#122).
This is definitely going to improve synchronous std out and err reads as well.
|
@EgorBot -windows_x64 -windows_arm64 --filter "*Perf_FileStream.Read(Asynchronous" "*Perf_FileStream.Write(Asynchronous" "Perf_Process.ReadOutputToEnd" |
adamsitnik
left a comment
There was a problem hiding this comment.
@copilot please give my suggestion a try
| { | ||
| if (overlapped != null) | ||
| { | ||
| resetEvent.ReleaseRefCount(overlapped); | ||
| NativeMemory.Free(overlapped); | ||
| } |
There was a problem hiding this comment.
@copilot let's try to implement following approach:
- don't allocate
ManualResetEvent, instead perform the windows sys-call with minimal access right required to performReadFile
CreateEventExW(
NULL,
NULL,
CREATE_EVENT_MANUAL_RESET,
SYNCHRONIZE | EVENT_MODIFY_STATE
);- pass the event to
AllocNativeOverlappedWithEventHandleinstead ofManualResetEvent - instead of waiting on the event handle, pass
bWait = truetoGetOverlappedResult - close the event handle your own by calling
CloseHandle
This will be an observable behavior change if the user code uses SynchronizationContext and overrides waiting.... |
…erlappedResult(bWait:true) Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/9810010f-46e0-4586-a817-6490d5fccd03 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
🤖 Copilot Code Review — PR #126845Note This review was AI/Copilot-generated via the code-review skill, using multi-model analysis (Gemini 2.5 Pro + GPT-5.3-Codex + my own assessment). Holistic AssessmentMotivation: Justified. The old code used Approach: Sound. Using native Summary: ✅ LGTM. All three review models (Gemini 2.5 Pro, GPT-5.3-Codex, and my own analysis) agree the change is correct. The latest commit (d0fc732f) addresses the use-after-free concern by removing all managed wait paths. Detailed Findings✅ Core Approach — Native event +
|
|
@EgorBot -windows_x64 -windows_arm64 -commits d0fc732,d0fc732~1 --filter "*Perf_FileStream.Read(Asynchronous" "*Perf_FileStream.Write(Asynchronous" "Perf_Process.ReadOutputToEnd" |
|
@EgorBot -windows_x64 -windows_arm64 --filter "*Perf_FileStream.Read(Asynchronous" "*Perf_FileStream.Write(Asynchronous" "Perf_Process.ReadOutputToEnd" |
Description
When performing synchronous IO on async file handles (
FILE_FLAG_OVERLAPPED),RandomAccess.Windows.cspreviously usedThreadPoolBoundHandle.UnsafeAllocateNativeOverlappedwith anIOCompletionCallbackand a customCallbackResetEventclass that tracked a reference count to handle the race between the IOCP callback and the caller'sGetOverlappedResult.This is unnecessary. Setting the low-order bit of
hEventin theOVERLAPPEDstructure prevents the I/O completion from being queued to the completion port, eliminating the callback entirely.Changes:
NativeOverlappedviaNativeMemory.AllocZeroedinstead ofThreadPoolBoundHandleEventHandle = handle | 1to suppress IOCP notificationCreateEventExWwith minimal access rights (SYNCHRONIZE | EVENT_MODIFY_STATE) instead of managedManualResetEventGetOverlappedResult(bWait: true)instead of managedWaitOne()— this avoids use-after-free ifWaitOnethrows (e.g., viaSynchronizationContextcallback) sinceGetOverlappedResultis a native blocking call that cannot throw managed exceptionsNativeMemory.Freeand close event handle withCloseHandleinfinally— no ref counting neededs_callback,AllocateCallback(),GetNativeOverlappedForAsyncHandle(),CallbackResetEventclassEnsureThreadPoolBindingInitialized()from sync-over-async paths (no longer usesThreadPoolBoundHandle)overlapped->InternalLow = IntPtr.Zeroin EOF path (only needed forThreadPoolBoundHandle.FreeNativeOverlappedassertion, notNativeMemory.Free)CreateEventExWP/Invoke overload returning rawIntPtrfor use withoutSafeWaitHandleReduces allocations and complexity with no behavioral change, and fixes a potential use-after-free when
WaitOnethrows arbitrary exceptions viaSynchronizationContext.