Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Follow up on Scatter/Gather API changes #58447

Merged
merged 9 commits into from
Oct 14, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -180,18 +180,17 @@ public async Task WriteAsyncUsingMultipleBuffers(bool asyncOperation, bool async
Assert.Equal(content, File.ReadAllBytes(filePath));
}

[Fact]
public async Task ReadWriteAsyncUsingNonPageSizedMultipleBuffers()
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task ReadWriteAsyncUsingMultipleBuffers(bool memoryPageSized)
{
string filePath = GetTestFilePath();
// The Windows scatter/gather APIs accept segments that are exactly one page long.
// Combined with the FILE_FLAG_NO_BUFFERING's requirements, the segments must also
// be aligned at page size boundaries and have a size of a multiple of the page size.
// Using segments with a length of twice the page size adheres to the second requirement
// but not the first. The RandomAccess implementation will see it and issue sequential
// read/write syscalls per segment, instead of one scatter/gather syscall.
// This test verifies that fallback behavior.
int bufferSize = Environment.SystemPageSize * 2;
// We test with buffers both one and two memory pages long. In the former case,
// the I/O operations will issue one scatter/gather API call, and in the latter
// case they will issue multiple calls; one per buffer. The buffers must still
// be aligned to comply with FILE_FLAG_NO_BUFFERING's requirements.
int bufferSize = Environment.SystemPageSize * (memoryPageSized ? 1 : 2);
int fileSize = bufferSize * 2;
byte[] content = RandomNumberGenerator.GetBytes(fileSize);

Expand All @@ -205,10 +204,11 @@ public async Task ReadWriteAsyncUsingNonPageSizedMultipleBuffers()
await RandomAccess.WriteAsync(handle, new ReadOnlyMemory<byte>[] { firstHalf, secondHalf }, 0);

buffer.GetSpan().Clear();
await RandomAccess.ReadAsync(handle, new Memory<byte>[] { firstHalf, secondHalf }, 0);
}
long nRead = await RandomAccess.ReadAsync(handle, new Memory<byte>[] { firstHalf, secondHalf }, 0);

Assert.Equal(content, await File.ReadAllBytesAsync(filePath));
Assert.Equal(buffer.GetSpan().Length, nRead);
AssertExtensions.SequenceEqual(buffer.GetSpan(), content.AsSpan());
}
}

// when using FileOptions.Asynchronous we are testing Scatter&Gather APIs on Windows (FILE_FLAG_OVERLAPPED requirement)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO.Strategies;
using System.Numerics;
using System.Runtime.InteropServices;
Expand Down Expand Up @@ -437,7 +438,7 @@ private static bool CanUseScatterGatherWindowsAPIs(SafeFileHandle handle)
// The pinned MemoryHandles and the pointer to the segments must be cleaned-up
// with the CleanupScatterGatherBuffers method.
private static unsafe bool TryPrepareScatterGatherBuffers<T, THandler>(IReadOnlyList<T> buffers,
THandler handler, out MemoryHandle[] handlesToDispose, out IntPtr segmentsPtr, out int totalBytes)
THandler handler, [NotNullWhen(true)] out MemoryHandle[]? handlesToDispose, out IntPtr segmentsPtr, out int totalBytes)
where THandler: struct, IMemoryHandler<T>
{
int pageSize = s_cachedPageSize;
Expand All @@ -447,13 +448,11 @@ private static unsafe bool TryPrepareScatterGatherBuffers<T, THandler>(IReadOnly
long alignedAtPageSizeMask = pageSize - 1;

int buffersCount = buffers.Count;
handlesToDispose = new MemoryHandle[buffersCount];
handlesToDispose = null;
segmentsPtr = IntPtr.Zero;
totalBytes = 0;

// "The array must contain enough elements to store nNumberOfBytesToWrite bytes of data, and one element for the terminating NULL. "
long* segments = (long*) NativeMemory.Alloc((nuint)(buffersCount + 1), sizeof(long));
segments[buffersCount] = 0;
long* segments = null;

bool success = false;
try
Expand All @@ -469,18 +468,32 @@ private static unsafe bool TryPrepareScatterGatherBuffers<T, THandler>(IReadOnly
return false;
}

MemoryHandle handle = handlesToDispose[i] = handler.Pin(in buffer);
long ptr = segments[i] = (long)handle.Pointer;
MemoryHandle handle = handler.Pin(in buffer);
long ptr = (long)handle.Pointer;
if ((ptr & alignedAtPageSizeMask) != 0)
{
handle.Dispose();
return false;
}

// We avoid allocations if there are no
// buffers or the first one is unacceptable.
(handlesToDispose ??= new MemoryHandle[buffersCount])[i] = handle;
if (segments == null)
{
// "The array must contain enough elements to store nNumberOfBytesToWrite
// bytes of data, and one element for the terminating NULL."
segments = (long*)NativeMemory.Alloc((nuint)(buffersCount + 1), sizeof(long));
}
segments[i] = ptr;
}

Debug.Assert(segments != null);
teo-tsirpanis marked this conversation as resolved.
Show resolved Hide resolved
segments[buffersCount] = 0;
segmentsPtr = (IntPtr)segments;
totalBytes = (int)totalBytes64;
success = true;
return true;
return handlesToDispose != null;
}
finally
{
Expand All @@ -491,14 +504,20 @@ private static unsafe bool TryPrepareScatterGatherBuffers<T, THandler>(IReadOnly
}
}

private static unsafe void CleanupScatterGatherBuffers(MemoryHandle[] handlesToDispose, IntPtr segmentsPtr)
private static unsafe void CleanupScatterGatherBuffers(MemoryHandle[]? handlesToDispose, IntPtr segmentsPtr)
{
foreach (MemoryHandle handle in handlesToDispose)
if (handlesToDispose != null)
{
handle.Dispose();
foreach (MemoryHandle handle in handlesToDispose)
{
handle.Dispose();
}
}

NativeMemory.Free((void*) segmentsPtr);
if (segmentsPtr != IntPtr.Zero)
{
NativeMemory.Free((void*) segmentsPtr);
}
}

private static ValueTask<long> ReadScatterAtOffsetAsync(SafeFileHandle handle, IReadOnlyList<Memory<byte>> buffers,
Expand All @@ -510,7 +529,7 @@ private static ValueTask<long> ReadScatterAtOffsetAsync(SafeFileHandle handle, I
}

if (CanUseScatterGatherWindowsAPIs(handle)
&& TryPrepareScatterGatherBuffers(buffers, default(MemoryHandler), out MemoryHandle[] handlesToDispose, out IntPtr segmentsPtr, out int totalBytes))
&& TryPrepareScatterGatherBuffers(buffers, default(MemoryHandler), out MemoryHandle[]? handlesToDispose, out IntPtr segmentsPtr, out int totalBytes))
{
return ReadScatterAtOffsetSingleSyscallAsync(handle, handlesToDispose, segmentsPtr, fileOffset, totalBytes, cancellationToken);
}
Expand Down Expand Up @@ -607,7 +626,7 @@ private static ValueTask WriteGatherAtOffsetAsync(SafeFileHandle handle, IReadOn
}

if (CanUseScatterGatherWindowsAPIs(handle)
&& TryPrepareScatterGatherBuffers(buffers, default(ReadOnlyMemoryHandler), out MemoryHandle[] handlesToDispose, out IntPtr segmentsPtr, out int totalBytes))
&& TryPrepareScatterGatherBuffers(buffers, default(ReadOnlyMemoryHandler), out MemoryHandle[]? handlesToDispose, out IntPtr segmentsPtr, out int totalBytes))
{
return WriteGatherAtOffsetSingleSyscallAsync(handle, handlesToDispose, segmentsPtr, fileOffset, totalBytes, cancellationToken);
}
Expand Down Expand Up @@ -671,13 +690,12 @@ private static unsafe ValueTask WriteFileGatherAsync(SafeFileHandle handle, IntP

private static async ValueTask WriteGatherAtOffsetMultipleSyscallsAsync(SafeFileHandle handle, IReadOnlyList<ReadOnlyMemory<byte>> buffers, long fileOffset, CancellationToken cancellationToken)
{
long bytesWritten = 0;
int buffersCount = buffers.Count;
for (int i = 0; i < buffersCount; i++)
{
ReadOnlyMemory<byte> rom = buffers[i];
await WriteAtOffsetAsync(handle, rom, fileOffset + bytesWritten, cancellationToken).ConfigureAwait(false);
bytesWritten += rom.Length;
await WriteAtOffsetAsync(handle, rom, fileOffset, cancellationToken).ConfigureAwait(false);
fileOffset += rom.Length;
}
}

Expand Down