Skip to content

Commit 4951e38

Browse files
More WriteGather fixes (#109826)
* don't run these tests in parallel, as each test cases uses more than 4 GB ram and disk! * fix the test: handle incomplete reads that should happen when we hit the max buffer limit * incomplete write fix: - pin the buffers only once - when re-trying, do that only for the actual reminder * Use native memory to get OOM a soon as we run out of memory (hoping to avoid the process getting killed on Linux when OOM happens) * For macOS preadv and pwritev can fail with EINVAL when the total length of all vectors overflows a 32-bit integer. * add an assert that is going to warn us if vector.Count is ever more than Int32.MaxValue --------- Co-authored-by: Michał Petryka <35800402+MichalPetryka@users.noreply.github.com>
1 parent 7f2f2b9 commit 4951e38

File tree

3 files changed

+140
-68
lines changed

3 files changed

+140
-68
lines changed

src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -168,38 +168,30 @@ internal static unsafe void WriteGatherAtOffset(SafeFileHandle handle, IReadOnly
168168

169169
var handles = new MemoryHandle[buffersCount];
170170
Span<Interop.Sys.IOVector> vectors = buffersCount <= IovStackThreshold ?
171-
stackalloc Interop.Sys.IOVector[IovStackThreshold] :
171+
stackalloc Interop.Sys.IOVector[IovStackThreshold].Slice(0, buffersCount) :
172172
new Interop.Sys.IOVector[buffersCount];
173173

174174
try
175175
{
176-
int buffersOffset = 0, firstBufferOffset = 0;
177-
while (true)
176+
long totalBytesToWrite = 0;
177+
for (int i = 0; i < buffersCount; i++)
178178
{
179-
long totalBytesToWrite = 0;
180-
181-
for (int i = buffersOffset; i < buffersCount; i++)
182-
{
183-
ReadOnlyMemory<byte> buffer = buffers[i];
184-
totalBytesToWrite += buffer.Length;
185-
186-
MemoryHandle memoryHandle = buffer.Pin();
187-
vectors[i] = new Interop.Sys.IOVector { Base = firstBufferOffset + (byte*)memoryHandle.Pointer, Count = (UIntPtr)(buffer.Length - firstBufferOffset) };
188-
handles[i] = memoryHandle;
189-
190-
firstBufferOffset = 0;
191-
}
179+
ReadOnlyMemory<byte> buffer = buffers[i];
180+
totalBytesToWrite += buffer.Length;
192181

193-
if (totalBytesToWrite == 0)
194-
{
195-
break;
196-
}
182+
MemoryHandle memoryHandle = buffer.Pin();
183+
vectors[i] = new Interop.Sys.IOVector { Base = (byte*)memoryHandle.Pointer, Count = (UIntPtr)buffer.Length };
184+
handles[i] = memoryHandle;
185+
}
197186

187+
int buffersOffset = 0;
188+
while (totalBytesToWrite > 0)
189+
{
198190
long bytesWritten;
199191
Span<Interop.Sys.IOVector> left = vectors.Slice(buffersOffset);
200192
fixed (Interop.Sys.IOVector* pinnedVectors = &MemoryMarshal.GetReference(left))
201193
{
202-
bytesWritten = Interop.Sys.PWriteV(handle, pinnedVectors, buffersCount - buffersOffset, fileOffset);
194+
bytesWritten = Interop.Sys.PWriteV(handle, pinnedVectors, left.Length, fileOffset);
203195
}
204196

205197
FileStreamHelpers.CheckFileCall(bytesWritten, handle.Path);
@@ -211,22 +203,29 @@ internal static unsafe void WriteGatherAtOffset(SafeFileHandle handle, IReadOnly
211203
// The write completed successfully but for fewer bytes than requested.
212204
// We need to perform next write where the previous one has finished.
213205
fileOffset += bytesWritten;
206+
totalBytesToWrite -= bytesWritten;
214207
// We need to try again for the remainder.
215-
for (int i = 0; i < buffersCount; i++)
208+
while (buffersOffset < buffersCount && bytesWritten > 0)
216209
{
217-
int n = buffers[i].Length;
210+
int n = (int)vectors[buffersOffset].Count;
218211
if (n <= bytesWritten)
219212
{
220-
buffersOffset++;
221213
bytesWritten -= n;
222-
if (bytesWritten == 0)
223-
{
224-
break;
225-
}
214+
buffersOffset++;
226215
}
227216
else
228217
{
229-
firstBufferOffset = (int)(bytesWritten - n);
218+
// A partial read: the vector needs to point to the new offset.
219+
// But that offset needs to be relative to the previous attempt.
220+
// Example: we have a single buffer with 30 bytes and the first read returned 10.
221+
// The next read should try to read the remaining 20 bytes, but in case it also reads just 10,
222+
// the third attempt should read last 10 bytes (not 20 again).
223+
Interop.Sys.IOVector current = vectors[buffersOffset];
224+
vectors[buffersOffset] = new Interop.Sys.IOVector
225+
{
226+
Base = current.Base + (int)(bytesWritten),
227+
Count = current.Count - (UIntPtr)(bytesWritten)
228+
};
230229
break;
231230
}
232231
}

src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/WriteGatherAsync.cs

Lines changed: 74 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Buffers;
45
using System.Collections.Generic;
56
using System.Linq;
67
using System.Security.Cryptography;
@@ -13,6 +14,7 @@
1314
namespace System.IO.Tests
1415
{
1516
[SkipOnPlatform(TestPlatforms.Browser, "async file IO is not supported on browser")]
17+
[Collection(nameof(DisableParallelization))] // don't run in parallel, as some of these tests use a LOT of resources
1618
public class RandomAccess_WriteGatherAsync : RandomAccess_Base<ValueTask>
1719
{
1820
protected override ValueTask MethodUnderTest(SafeFileHandle handle, byte[] bytes, long fileOffset)
@@ -151,21 +153,6 @@ public async Task NoInt32OverflowForLargeInputs(bool asyncFile, bool asyncMethod
151153
const int BufferSize = int.MaxValue / 1000;
152154
const long FileSize = (long)BufferCount * BufferSize;
153155
string filePath = GetTestFilePath();
154-
ReadOnlyMemory<byte> writeBuffer = RandomNumberGenerator.GetBytes(BufferSize);
155-
List<ReadOnlyMemory<byte>> writeBuffers = Enumerable.Repeat(writeBuffer, BufferCount).ToList();
156-
List<Memory<byte>> readBuffers = new List<Memory<byte>>(BufferCount);
157-
158-
try
159-
{
160-
for (int i = 0; i < BufferCount; i++)
161-
{
162-
readBuffers.Add(new byte[BufferSize]);
163-
}
164-
}
165-
catch (OutOfMemoryException)
166-
{
167-
throw new SkipTestException("Not enough memory.");
168-
}
169156

170157
FileOptions options = asyncFile ? FileOptions.Asynchronous : FileOptions.None; // we need to test both code paths
171158
options |= FileOptions.DeleteOnClose;
@@ -180,29 +167,86 @@ public async Task NoInt32OverflowForLargeInputs(bool asyncFile, bool asyncMethod
180167
throw new SkipTestException("Not enough disk space.");
181168
}
182169

183-
long fileOffset = 0, bytesRead = 0;
184-
try
170+
using (sfh)
185171
{
186-
if (asyncMethod)
172+
ReadOnlyMemory<byte> writeBuffer = RandomNumberGenerator.GetBytes(BufferSize);
173+
List<ReadOnlyMemory<byte>> writeBuffers = Enumerable.Repeat(writeBuffer, BufferCount).ToList();
174+
175+
List<NativeMemoryManager> memoryManagers = new List<NativeMemoryManager>(BufferCount);
176+
List<Memory<byte>> readBuffers = new List<Memory<byte>>(BufferCount);
177+
178+
try
187179
{
188-
await RandomAccess.WriteAsync(sfh, writeBuffers, fileOffset);
189-
bytesRead = await RandomAccess.ReadAsync(sfh, readBuffers, fileOffset);
180+
try
181+
{
182+
for (int i = 0; i < BufferCount; i++)
183+
{
184+
// We are using native memory here to get OOM as soon as possible.
185+
NativeMemoryManager nativeMemoryManager = new(BufferSize);
186+
memoryManagers.Add(nativeMemoryManager);
187+
readBuffers.Add(nativeMemoryManager.Memory);
188+
}
189+
}
190+
catch (OutOfMemoryException)
191+
{
192+
throw new SkipTestException("Not enough memory.");
193+
}
194+
195+
await Verify(asyncMethod, FileSize, sfh, writeBuffer, writeBuffers, readBuffers);
190196
}
191-
else
197+
finally
192198
{
193-
RandomAccess.Write(sfh, writeBuffers, fileOffset);
194-
bytesRead = RandomAccess.Read(sfh, readBuffers, fileOffset);
199+
foreach (IDisposable memoryManager in memoryManagers)
200+
{
201+
memoryManager.Dispose();
202+
}
195203
}
196204
}
197-
finally
198-
{
199-
sfh.Dispose(); // delete the file ASAP to avoid running out of resources in CI
200-
}
201205

202-
Assert.Equal(FileSize, bytesRead);
203-
for (int i = 0; i < BufferCount; i++)
206+
static async Task Verify(bool asyncMethod, long FileSize, SafeFileHandle sfh, ReadOnlyMemory<byte> writeBuffer, List<ReadOnlyMemory<byte>> writeBuffers, List<Memory<byte>> readBuffers)
204207
{
205-
Assert.Equal(writeBuffer, readBuffers[i]);
208+
if (asyncMethod)
209+
{
210+
await RandomAccess.WriteAsync(sfh, writeBuffers, 0);
211+
}
212+
else
213+
{
214+
RandomAccess.Write(sfh, writeBuffers, 0);
215+
}
216+
217+
Assert.Equal(FileSize, RandomAccess.GetLength(sfh));
218+
219+
long fileOffset = 0;
220+
while (fileOffset < FileSize)
221+
{
222+
long bytesRead = asyncMethod
223+
? await RandomAccess.ReadAsync(sfh, readBuffers, fileOffset)
224+
: RandomAccess.Read(sfh, readBuffers, fileOffset);
225+
226+
Assert.InRange(bytesRead, 0, FileSize);
227+
228+
while (bytesRead > 0)
229+
{
230+
Memory<byte> readBuffer = readBuffers[0];
231+
if (bytesRead >= readBuffer.Length)
232+
{
233+
AssertExtensions.SequenceEqual(writeBuffer.Span, readBuffer.Span);
234+
235+
bytesRead -= readBuffer.Length;
236+
fileOffset += readBuffer.Length;
237+
238+
readBuffers.RemoveAt(0);
239+
}
240+
else
241+
{
242+
// A read has finished somewhere in the middle of one of the read buffers.
243+
// Example: buffer had 30 bytes and only 10 were read.
244+
// We don't read the missing part, but try to read the whole buffer again.
245+
// It's not optimal from performance perspective, but it keeps the test logic simple.
246+
break;
247+
}
248+
}
249+
}
206250
}
207251
}
208252

src/native/libs/System.Native/pal_io.c

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1950,19 +1950,48 @@ int32_t SystemNative_PWrite(intptr_t fd, void* buffer, int32_t bufferSize, int64
19501950
}
19511951

19521952
#if (HAVE_PREADV || HAVE_PWRITEV) && !defined(TARGET_WASM)
1953-
static int GetAllowedVectorCount(int32_t vectorCount)
1953+
static int GetAllowedVectorCount(IOVector* vectors, int32_t vectorCount)
19541954
{
1955+
#if defined(IOV_MAX)
1956+
const int IovMax = IOV_MAX;
1957+
#else
1958+
// In theory all the platforms that we support define IOV_MAX,
1959+
// but we want to be extra safe and provde a fallback
1960+
// in case it turns out to not be true.
1961+
// 16 is low, but supported on every platform.
1962+
const int IovMax = 16;
1963+
#endif
1964+
19551965
int allowedCount = (int)vectorCount;
19561966

1957-
#if defined(IOV_MAX)
1958-
if (IOV_MAX < allowedCount)
1967+
// We need to respect the limit of items that can be passed in iov.
1968+
// In case of writes, the managed code is responsible for handling incomplete writes.
1969+
// In case of reads, we simply returns the number of bytes read and it's up to the users.
1970+
if (IovMax < allowedCount)
19591971
{
1960-
// We need to respect the limit of items that can be passed in iov.
1961-
// In case of writes, the managed code is reponsible for handling incomplete writes.
1962-
// In case of reads, we simply returns the number of bytes read and it's up to the users.
1963-
allowedCount = IOV_MAX;
1972+
allowedCount = IovMax;
19641973
}
1974+
1975+
#if defined(TARGET_APPLE)
1976+
// For macOS preadv and pwritev can fail with EINVAL when the total length
1977+
// of all vectors overflows a 32-bit integer.
1978+
size_t totalLength = 0;
1979+
for (int i = 0; i < allowedCount; i++)
1980+
{
1981+
assert(INT_MAX >= vectors[i].Count);
1982+
1983+
totalLength += vectors[i].Count;
1984+
1985+
if (totalLength > INT_MAX)
1986+
{
1987+
allowedCount = i;
1988+
break;
1989+
}
1990+
}
1991+
#else
1992+
(void)vectors;
19651993
#endif
1994+
19661995
return allowedCount;
19671996
}
19681997
#endif // (HAVE_PREADV || HAVE_PWRITEV) && !defined(TARGET_WASM)
@@ -1975,7 +2004,7 @@ int64_t SystemNative_PReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount,
19752004
int64_t count = 0;
19762005
int fileDescriptor = ToFileDescriptor(fd);
19772006
#if HAVE_PREADV && !defined(TARGET_WASM) // preadv is buggy on WASM
1978-
int allowedVectorCount = GetAllowedVectorCount(vectorCount);
2007+
int allowedVectorCount = GetAllowedVectorCount(vectors, vectorCount);
19792008
while ((count = preadv(fileDescriptor, (struct iovec*)vectors, allowedVectorCount, (off_t)fileOffset)) < 0 && errno == EINTR);
19802009
#else
19812010
int64_t current;
@@ -2016,7 +2045,7 @@ int64_t SystemNative_PWriteV(intptr_t fd, IOVector* vectors, int32_t vectorCount
20162045
int64_t count = 0;
20172046
int fileDescriptor = ToFileDescriptor(fd);
20182047
#if HAVE_PWRITEV && !defined(TARGET_WASM) // pwritev is buggy on WASM
2019-
int allowedVectorCount = GetAllowedVectorCount(vectorCount);
2048+
int allowedVectorCount = GetAllowedVectorCount(vectors, vectorCount);
20202049
while ((count = pwritev(fileDescriptor, (struct iovec*)vectors, allowedVectorCount, (off_t)fileOffset)) < 0 && errno == EINTR);
20212050
#else
20222051
int64_t current;

0 commit comments

Comments
 (0)