diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f56d86612..c36208dc3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,42 @@ Release Notes ==== +# 12-29-2024 +This release is aimed to improve AOT compatibility. All the examples in the repo are now AOT compatible. +DotNext 5.17.0 +* Fixed AOT compatibility in `TaskType` class +* Added [ISpanFormattable](https://learn.microsoft.com/en-us/dotnet/api/system.ispanformattable) and [IParsable<T>](https://learn.microsoft.com/en-us/dotnet/api/system.iparsable-1) interfaces to `HttpEndPoint` +* Introduced `TryEncodeAsUtf8` extension method for `SpanWriter` +* Added more factory methods to `DotNext.Buffers.Memory` class to create [ReadOnlySequence<T>](https://learn.microsoft.com/en-us/dotnet/api/system.buffers.readonlysequence-1) +* `Intrinsics.KeepAlive` is introduced for value types +* Added `Synchronization.Wait()` synchronous methods for blocking wait of [value tasks](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask) without wait handles + +DotNext.Metaprogramming 5.17.0 +* Updated dependencies + +DotNext.Unsafe 5.17.0 +* Improved AOT support +* Fixed finalizer for unmanaged memory manager that allows to release the allocated unmanaged memory automatically by GC to avoid memory leak +* Updated dependencies + +DotNext.Threading 5.17.0 +* Improved AOT support + +DotNext.IO 5.17.0 +* Reduced memory consumption for applications that use `FileReader` and `FileWriter` classes. These classes are now implemented by using lazy buffer pattern. It means that the different instances can reuse the same buffer taken from the pool +* Fixed [255](https://github.com/dotnet/dotNext/issues/255) +* `PoolingBufferedStream` is introduced to replace classic [BufferedStream](https://learn.microsoft.com/en-us/dotnet/api/system.io.bufferedstream). This class supports memory pooling and implements lazy buffer pattern + +DotNext.Net.Cluster 5.17.0 +* Improved AOT support + +DotNext.AspNetCore.Cluster 5.17.0 +* Improved AOT support +* Fixed [254](https://github.com/dotnet/dotNext/issues/254) + +DotNext.MaintenanceServices 0.5.0 +* Improved AOT support + # 12-07-2024 DotNext 5.16.1 * Added [LEB128](https://en.wikipedia.org/wiki/LEB128) encoder and decoder as a public API. See `DotNext.Buffers.Binary.Leb128` type for more information diff --git a/README.md b/README.md index 9cdaa12210..9cf9fed9b1 100644 --- a/README.md +++ b/README.md @@ -44,34 +44,42 @@ All these things are implemented in 100% managed code on top of existing .NET AP * [NuGet Packages](https://www.nuget.org/profiles/rvsakno) # What's new -Release Date: 12-07-2024 - -DotNext 5.16.1 -* Added [LEB128](https://en.wikipedia.org/wiki/LEB128) encoder and decoder as a public API. See `DotNext.Buffers.Binary.Leb128` type for more information -* Added `SlideToEnd` method to `SpanWriter` type -* Added `IsBitSet` and `SetBit` generic methods to `Number` type -* Added `DetachOrCopyBuffer` to `BufferWriterSlim` type - -DotNext.Metaprogramming 5.16.1 +Release Date: 12-29-2024 + +This release is aimed to improve AOT compatibility. All the examples in the repo are now AOT compatible. +DotNext 5.17.0 +* Fixed AOT compatibility in `TaskType` class +* Added [ISpanFormattable](https://learn.microsoft.com/en-us/dotnet/api/system.ispanformattable) and [IParsable<T>](https://learn.microsoft.com/en-us/dotnet/api/system.iparsable-1) interfaces to `HttpEndPoint` +* Introduced `TryEncodeAsUtf8` extension method for `SpanWriter` +* Added more factory methods to `DotNext.Buffers.Memory` class to create [ReadOnlySequence<T>](https://learn.microsoft.com/en-us/dotnet/api/system.buffers.readonlysequence-1) +* `Intrinsics.KeepAlive` is introduced for value types +* Added `Synchronization.Wait()` synchronous methods for blocking wait of [value tasks](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask) without wait handles + +DotNext.Metaprogramming 5.17.0 * Updated dependencies -DotNext.Unsafe 5.16.1 +DotNext.Unsafe 5.17.0 +* Improved AOT support +* Fixed finalizer for unmanaged memory manager that allows to release the allocated unmanaged memory automatically by GC to avoid memory leak * Updated dependencies -DotNext.Threading 5.16.1 -* Async locks with synchronous acquisition methods now throw [LockRecursionException](https://learn.microsoft.com/en-us/dotnet/api/system.threading.lockrecursionexception) if the current thread tries to acquire the lock synchronously and recursively. -* Added support of cancellation token to synchronous acquisition methods of `AsyncExclusiveLock` and `AsyncReaderWriterLock` classes -* Introduced `LinkTo` method overload that supports multiple cancellation tokens +DotNext.Threading 5.17.0 +* Improved AOT support -DotNext.IO 5.16.1 -* Introduced `RandomAccessStream` class that represents [Stream](https://learn.microsoft.com/en-us/dotnet/api/system.io.stream) wrapper over the underlying data storage that supports random access pattern -* Added extension method for `SpanWriter` that provides length-prefixed string encoding +DotNext.IO 5.17.0 +* Reduced memory consumption for applications that use `FileReader` and `FileWriter` classes. These classes are now implemented by using lazy buffer pattern. It means that the different instances can reuse the same buffer taken from the pool +* Fixed [255](https://github.com/dotnet/dotNext/issues/255) +* `PoolingBufferedStream` is introduced to replace classic [BufferedStream](https://learn.microsoft.com/en-us/dotnet/api/system.io.bufferedstream). This class supports memory pooling and implements lazy buffer pattern -DotNext.Net.Cluster 5.16.1 -* Updated dependencies +DotNext.Net.Cluster 5.17.0 +* Improved AOT support -DotNext.AspNetCore.Cluster 5.16.1 -* Updated dependencies +DotNext.AspNetCore.Cluster 5.17.0 +* Improved AOT support +* Fixed [254](https://github.com/dotnet/dotNext/issues/254) + +DotNext.MaintenanceServices 0.5.0 +* Improved AOT support Changelog for previous versions located [here](./CHANGELOG.md). diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 695220793b..ab3f7d3764 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -2,6 +2,8 @@ variables: Solution: src/DotNext.sln TestFolder: src/DotNext.Tests TestProject: $(TestFolder)/DotNext.Tests.csproj + AotTestFolder: src/DotNext.AotTests + AotTestProject: $(AotTestFolder)/DotNext.AotTests.csproj InternetAccess: false DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true isMain: $[eq(variables['Build.SourceBranch'], 'refs/heads/master')] @@ -47,6 +49,19 @@ stages: feedsToUse: 'config' nugetConfigPath: 'NuGet.config' arguments: --configuration Debug + - task: DotNetCoreCLI@2 + displayName: Publish AOT Tests + inputs: + command: publish + publishWebProjects: false + zipAfterPublish: false + projects: $(AotTestProject) + arguments: --configuration Release --output $(AotTestFolder)/bin/ + - task: CmdLine@2 + displayName: Run AOT Tests + inputs: + workingDirectory: $(AotTestFolder)/bin/DotNext.AotTests + script: ./DotNext.AotTests - task: DotNetCoreCLI@2 displayName: Test Debug inputs: diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 8a6086688a..a9b5278073 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -28,6 +28,12 @@ + + + + + + diff --git a/src/DotNext.AotTests/Collections/Generic/ListTests.cs b/src/DotNext.AotTests/Collections/Generic/ListTests.cs new file mode 100644 index 0000000000..2b1eb50da2 --- /dev/null +++ b/src/DotNext.AotTests/Collections/Generic/ListTests.cs @@ -0,0 +1,24 @@ +namespace DotNext.Collections.Generic; + +[TestClass] +public class ListTests +{ + [TestMethod] + public void Indexer() + { + IList array = [5L, 6L, 30L]; + Assert.AreEqual(30L, List.Indexer.Getter(array, 2)); + List.Indexer.Setter(array, 1, 10L); + Assert.AreEqual(10L, array.IndexerGetter().Invoke(1)); + array.IndexerSetter().Invoke(0, 6L); + Assert.AreEqual(6L, array.IndexerGetter().Invoke(0)); + } + + [TestMethod] + public void ReadOnlyIndexer() + { + IReadOnlyList array = [5L, 6L, 20L]; + Assert.AreEqual(20L, List.Indexer.ReadOnly(array, 2)); + Assert.AreEqual(6L, array.IndexerGetter().Invoke(1)); + } +} \ No newline at end of file diff --git a/src/DotNext.AotTests/DotNext.AotTests.csproj b/src/DotNext.AotTests/DotNext.AotTests.csproj new file mode 100644 index 0000000000..7c3cc758f8 --- /dev/null +++ b/src/DotNext.AotTests/DotNext.AotTests.csproj @@ -0,0 +1,35 @@ + + + Exe + net8.0 + DotNext + enable + enable + 5.17.0 + false + .NET Foundation and Contributors + .NEXT Family of Libraries + AOT compatibility tests for .NEXT Familiy of Libraries + Copyright © .NET Foundation and Contributors + https://github.com/dotnet/DotNext/blob/master/LICENSE + https://github.com/dotnet/DotNext + https://github.com/dotnet/DotNext.git + git + true + true + + + + + + + + + + + + + + + + diff --git a/src/DotNext.AotTests/Reflection/TaskTypeTests.cs b/src/DotNext.AotTests/Reflection/TaskTypeTests.cs new file mode 100644 index 0000000000..ed1bfab8d0 --- /dev/null +++ b/src/DotNext.AotTests/Reflection/TaskTypeTests.cs @@ -0,0 +1,23 @@ +namespace DotNext.Reflection; + +[TestClass] +public class TaskTypeTests +{ + [TestMethod] + public void IsCompletedSuccessfullyPropertyGetter() + { + Assert.IsTrue(TaskType.IsCompletedSuccessfullyGetter(Task.CompletedTask)); + } + + [TestMethod] + public void GetResultSynchronously() + { + Assert.AreEqual(42, TaskType.GetResultGetter().Invoke(Task.FromResult(42))); + } + + [TestMethod] + public void IsCompletedPropertyGetter() + { + Assert.IsTrue(Task.CompletedTask.GetIsCompletedGetter().Invoke()); + } +} \ No newline at end of file diff --git a/src/DotNext.Benchmarks/DotNext.Benchmarks.csproj b/src/DotNext.Benchmarks/DotNext.Benchmarks.csproj index 3b35466501..88bbe4db00 100644 --- a/src/DotNext.Benchmarks/DotNext.Benchmarks.csproj +++ b/src/DotNext.Benchmarks/DotNext.Benchmarks.csproj @@ -8,7 +8,7 @@ DotNext DotNext.Program false - 5.16.1 + 5.17.0 .NET Foundation and Contributors .NEXT Family of Libraries Various benchmarks demonstrating performance aspects of .NEXT extensions diff --git a/src/DotNext.IO/Buffers/IBufferedChannel.cs b/src/DotNext.IO/Buffers/IBufferedChannel.cs new file mode 100644 index 0000000000..fc8ea7fe74 --- /dev/null +++ b/src/DotNext.IO/Buffers/IBufferedChannel.cs @@ -0,0 +1,19 @@ +using System.Buffers; + +namespace DotNext.Buffers; + +/// +/// Represents buffered reader or writer. +/// +public interface IBufferedChannel : IResettable, IDisposable +{ + /// + /// Gets buffer allocator. + /// + MemoryAllocator? Allocator { get; init; } + + /// + /// Gets the maximum size of the internal buffer. + /// + int MaxBufferSize { get; init; } +} \ No newline at end of file diff --git a/src/DotNext.IO/Buffers/IBufferedReader.cs b/src/DotNext.IO/Buffers/IBufferedReader.cs new file mode 100644 index 0000000000..5d4849f04f --- /dev/null +++ b/src/DotNext.IO/Buffers/IBufferedReader.cs @@ -0,0 +1,55 @@ +namespace DotNext.Buffers; + +/// +/// Represents buffered reader. +/// +public interface IBufferedReader : IBufferedChannel +{ + /// + /// Gets unconsumed part of the buffer. + /// + ReadOnlyMemory Buffer { get; } + + /// + /// Advances read position. + /// + /// The number of consumed bytes. + /// The reader has been disposed. + /// is larger than the length of . + void Consume(int count); + + /// + /// Fetches the data from the underlying storage to the internal buffer. + /// + /// The token that can be used to cancel the operation. + /// + /// if the data has been copied from the underlying storage to the internal buffer; + /// if no more data to read. + /// + /// The reader has been disposed. + /// Internal buffer has no free space. + /// The operation has been canceled. + ValueTask ReadAsync(CancellationToken token = default); + + /// + /// Reads the block of the memory. + /// + /// The output buffer. + /// The token that can be used to cancel the operation. + /// The number of bytes copied to . + /// The reader has been disposed. + /// The operation has been canceled. + async ValueTask ReadAsync(Memory destination, CancellationToken token = default) + { + var result = 0; + for (int bytesRead; result < destination.Length; result += bytesRead, destination = destination.Slice(bytesRead)) + { + Buffer.Span.CopyTo(destination.Span, out bytesRead); + Consume(bytesRead); + if (!await ReadAsync(token).ConfigureAwait(false)) + break; + } + + return result; + } +} \ No newline at end of file diff --git a/src/DotNext.IO/Buffers/IBufferedWriter.cs b/src/DotNext.IO/Buffers/IBufferedWriter.cs new file mode 100644 index 0000000000..e04d2fa475 --- /dev/null +++ b/src/DotNext.IO/Buffers/IBufferedWriter.cs @@ -0,0 +1,73 @@ +using System.Buffers; + +namespace DotNext.Buffers; + +/// +/// Represents buffered writer. +/// +public interface IBufferedWriter : IBufferedChannel, IBufferWriter +{ + /// + /// Marks the specified number of bytes in the buffer as produced. + /// + /// The number of produced bytes. + /// is larger than the length of . + /// The writer has been disposed. + void Produce(int count); + + /// + /// The remaining part of the internal buffer available for write. + /// + /// + /// The size of returned buffer may be less than or equal to . + /// + Memory Buffer { get; } + + /// + /// Flushes buffered data to the underlying storage. + /// + /// The token that can be used to cancel the operation. + /// The task representing asynchronous result. + /// The writer has been disposed. + /// The operation has been canceled. + ValueTask WriteAsync(CancellationToken token = default); + + /// + /// Writes the data to the underlying storage through the buffer. + /// + /// The input data to write. + /// The token that can be used to cancel the operation. + /// The task representing asynchronous result. + /// The object has been disposed. + /// The operation has been canceled. + async ValueTask WriteAsync(ReadOnlyMemory input, CancellationToken token = default) + { + for (int bytesWritten; !input.IsEmpty; input = input.Slice(bytesWritten)) + { + input.Span.CopyTo(Buffer.Span, out bytesWritten); + Produce(bytesWritten); + await WriteAsync(token).ConfigureAwait(false); + } + } + + /// + void IBufferWriter.Advance(int count) => Produce(count); + + /// + Memory IBufferWriter.GetMemory(int sizeHint) + { + ArgumentOutOfRangeException.ThrowIfNegative(sizeHint); + + var result = Buffer; + return sizeHint <= result.Length ? result : throw new InsufficientMemoryException(); + } + + /// + Span IBufferWriter.GetSpan(int sizeHint) + { + ArgumentOutOfRangeException.ThrowIfNegative(sizeHint); + + var result = Buffer.Span; + return sizeHint <= result.Length ? result : throw new InsufficientMemoryException(); + } +} \ No newline at end of file diff --git a/src/DotNext.IO/DotNext.IO.csproj b/src/DotNext.IO/DotNext.IO.csproj index abca1d4222..4bda5105be 100644 --- a/src/DotNext.IO/DotNext.IO.csproj +++ b/src/DotNext.IO/DotNext.IO.csproj @@ -5,13 +5,13 @@ latest enable true - true + true nullablePublicOnly DotNext .NET Foundation and Contributors .NEXT Family of Libraries - 5.16.1 + 5.17.0 DotNext.IO MIT diff --git a/src/DotNext.IO/ExceptionMessages.cs b/src/DotNext.IO/ExceptionMessages.cs index 6a0aebb7e6..5a0cf11805 100644 --- a/src/DotNext.IO/ExceptionMessages.cs +++ b/src/DotNext.IO/ExceptionMessages.cs @@ -22,7 +22,9 @@ internal static string DirectoryNotFound(string path) internal static string WriterInReadMode => (string)Resources.Get(); - internal static string NoConsumerProvided => (string)Resources.Get(); - internal static string FileHandleClosed => (string)Resources.Get(); + + internal static string ReadBufferNotEmpty => (string)Resources.Get(); + + internal static string WriteBufferNotEmpty => (string)Resources.Get(); } \ No newline at end of file diff --git a/src/DotNext.IO/ExceptionMessages.restext b/src/DotNext.IO/ExceptionMessages.restext index 4dbd90f40f..c941177ef3 100644 --- a/src/DotNext.IO/ExceptionMessages.restext +++ b/src/DotNext.IO/ExceptionMessages.restext @@ -3,5 +3,6 @@ StreamNotWritable=Stream is not writable StreamNotReadable=Stream is not readable DirectoryNotFound=Directory {0} doesn't exist WriterInReadMode=The writer is in read-only mode. Dispose active memory manager obtained from writer -NoConsumerProvided=No actual consumer is provided -FileHandleClosed=The file handle is closed \ No newline at end of file +FileHandleClosed=The file handle is closed +ReadBufferNotEmpty=The internal buffer has unconsumed data to read +WriteBufferNotEmpty=The internal buffer has data to flush \ No newline at end of file diff --git a/src/DotNext.IO/IO/FileReader.Binary.cs b/src/DotNext.IO/IO/FileReader.Binary.cs index 562fae07b9..f920ced45e 100644 --- a/src/DotNext.IO/IO/FileReader.Binary.cs +++ b/src/DotNext.IO/IO/FileReader.Binary.cs @@ -1,4 +1,5 @@ using System.Buffers; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Numerics; using System.Runtime.CompilerServices; @@ -229,24 +230,32 @@ public async ValueTask ParseAsync(LengthFormat lengthFormat, IFormatProvid where T : notnull, IUtf8SpanParsable { var length = await ReadLengthAsync(lengthFormat, token).ConfigureAwait(false); + T result; if (length <= 0) - return T.Parse([], provider); - - // fast path without allocation of temp buffer - if (TryConsume(length, out var block)) { + result = T.Parse([], provider); + } + else if (TryRead(length, out var block)) + { + // fast path without allocation of temp buffer block = TrimLength(block, this.length); this.length -= block.Length; - return block.Length == length + result = block.Length == length ? T.Parse(block.Span, provider) : throw new EndOfStreamException(); + + ResetIfNeeded(); + } + else + { + // slow path with temp buffer + using var buffer = Allocator.AllocateExactly(length); + await ReadAsync(new(buffer.Memory), token).ConfigureAwait(false); + result = T.Parse(buffer.Span, provider); } - // slow path with temp buffer - using var buffer = allocator.AllocateExactly(length); - await ReadAsync(new(buffer.Memory), token).ConfigureAwait(false); - return T.Parse(buffer.Span, provider); + return result; } /// @@ -266,22 +275,29 @@ public async ValueTask ParseAsync(LengthFormat lengthFormat, NumberStyles { var length = await ReadLengthAsync(lengthFormat, token).ConfigureAwait(false); + T result; if (length <= 0) - return T.Parse(ReadOnlySpan.Empty, style, provider); - - // fast path without allocation of temp buffer - if (TryConsume(length, out var block)) { + result = T.Parse(ReadOnlySpan.Empty, style, provider); + } + else if (TryRead(length, out var block)) + { + // fast path without allocation of temp buffer block = TrimLength(block, this.length); this.length -= block.Length; - return block.Length == length + result = block.Length == length ? T.Parse(block.Span, style, provider) : throw new EndOfStreamException(); + ResetIfNeeded(); + } + else + { + using var buffer = Allocator.AllocateExactly(length); + await ReadAsync(new(buffer.Memory), token).ConfigureAwait(false); + result = T.Parse(buffer.Span, style, provider); } - using var buffer = allocator.AllocateExactly(length); - await ReadAsync(new(buffer.Memory), token).ConfigureAwait(false); - return T.Parse(buffer.Span, style, provider); + return result; } /// diff --git a/src/DotNext.IO/IO/FileReader.Segment.cs b/src/DotNext.IO/IO/FileReader.Segment.cs index eeb433953e..a2ecdb8b5c 100644 --- a/src/DotNext.IO/IO/FileReader.Segment.cs +++ b/src/DotNext.IO/IO/FileReader.Segment.cs @@ -24,6 +24,8 @@ public SegmentLength() { } + internal int Truncate(int value) => IsInfinite ? value : (int)Math.Min(value, this.value); + internal bool IsInfinite => value < 0L; public bool Equals(long other) => !IsInfinite && value == other; @@ -98,8 +100,8 @@ public long? ReaderSegmentLength } private static ReadOnlyMemory TrimLength(ReadOnlyMemory buffer, SegmentLength length) - => length.IsInfinite ? buffer : buffer.TrimLength(int.CreateSaturating((long)length)); + => buffer.TrimLength(length.Truncate(buffer.Length)); private static Memory TrimLength(Memory buffer, SegmentLength length) - => length.IsInfinite ? buffer : buffer.TrimLength(int.CreateSaturating((long)length)); + => buffer.TrimLength(length.Truncate(buffer.Length)); } \ No newline at end of file diff --git a/src/DotNext.IO/IO/FileReader.Utils.cs b/src/DotNext.IO/IO/FileReader.Utils.cs index b0dee8a659..6063fc1dbe 100644 --- a/src/DotNext.IO/IO/FileReader.Utils.cs +++ b/src/DotNext.IO/IO/FileReader.Utils.cs @@ -9,10 +9,17 @@ namespace DotNext.IO; public partial class FileReader : IDynamicInterfaceCastable { - private readonly Action readCallback, readDirectCallback; + private Action? readCallback, readDirectCallback, readAndCopyCallback; private ManualResetValueTaskSourceCore source; private ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter; private int extraCount; + private Memory destinationBuffer; + + private Action ReadCallback => readCallback ??= OnRead; + + private Action ReadDirectCallback => readDirectCallback ??= OnReadDirect; + + private Action ReadAndCopyCallback => readAndCopyCallback ??= OnReadAndCopy; private int GetAsyncResult(short token) { @@ -37,6 +44,7 @@ private void OnRead() count = awaiter.GetResult(); bufferEnd += count; + ResetIfNeeded(); } catch (Exception e) { @@ -72,6 +80,33 @@ private void OnReadDirect() source.SetResult(count); } + private void OnReadAndCopy() + { + var awaiter = this.awaiter; + this.awaiter = default; + + var extraCount = this.extraCount; + this.extraCount = 0; + + var destinationBuffer = this.destinationBuffer; + this.destinationBuffer = default; + + int result; + try + { + result = awaiter.GetResult(); + bufferEnd += result; + result = ReadFromBuffer(destinationBuffer.Span) + extraCount; + } + catch (Exception e) + { + source.SetException(e); + return; + } + + source.SetResult(result); + } + private ValueTask SubmitAsInt32(ValueTask task, Action callback) { awaiter = task.ConfigureAwait(false).GetAwaiter(); diff --git a/src/DotNext.IO/IO/FileReader.cs b/src/DotNext.IO/IO/FileReader.cs index af31be02f4..d7b1c3266b 100644 --- a/src/DotNext.IO/IO/FileReader.cs +++ b/src/DotNext.IO/IO/FileReader.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Runtime.CompilerServices; using SafeFileHandle = Microsoft.Win32.SafeHandles.SafeFileHandle; namespace DotNext.IO; @@ -12,13 +13,16 @@ namespace DotNext.IO; /// This class is not thread-safe. However, it's possible to share the same file /// handle across multiple readers and use dedicated reader in each thread. /// -public partial class FileReader : Disposable, IResettable +public partial class FileReader : Disposable, IBufferedReader { + private const int MinBufferSize = 16; + private const int DefaultBufferSize = 4096; + /// /// Represents the file handle. /// protected readonly SafeFileHandle handle; - private readonly MemoryAllocator? allocator; + private readonly int maxBufferSize; private MemoryOwner buffer; private int bufferStart, bufferEnd; private long fileOffset; @@ -27,45 +31,35 @@ public partial class FileReader : Disposable, IResettable /// Initializes a new buffered file reader. /// /// The file handle. - /// The initial offset within the file. - /// The buffer size. - /// The buffer allocator. - /// - /// is less than zero; - /// or is less than 16 bytes. - /// /// is . - /// - /// is less than zero; - /// or too small. - /// - public FileReader(SafeFileHandle handle, long fileOffset = 0L, int bufferSize = 4096, MemoryAllocator? allocator = null) + public FileReader(SafeFileHandle handle) { ArgumentNullException.ThrowIfNull(handle); ArgumentOutOfRangeException.ThrowIfNegative(fileOffset); - ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(bufferSize, 16); - buffer = allocator.AllocateAtLeast(bufferSize); + maxBufferSize = DefaultBufferSize; this.handle = handle; - this.fileOffset = fileOffset; - this.allocator = allocator; - - readCallback = OnRead; - readDirectCallback = OnReadDirect; } /// /// Initializes a new buffered file reader. /// /// Readable file stream. - /// The buffer size. - /// The buffer allocator. /// is not readable. - public FileReader(FileStream source, int bufferSize = 4096, MemoryAllocator? allocator = null) - : this(source.SafeFileHandle, source.Position, bufferSize, allocator) + public FileReader(FileStream source) + : this(source.SafeFileHandle) { if (source.CanRead is false) throw new ArgumentException(ExceptionMessages.StreamNotReadable, nameof(source)); + + FilePosition = source.Position; + } + + /// + public MemoryAllocator? Allocator + { + get; + init; } /// @@ -81,7 +75,7 @@ public long FilePosition ArgumentOutOfRangeException.ThrowIfNegative(value); if (HasBufferedData) - throw new InvalidOperationException(); + throw new InvalidOperationException(ExceptionMessages.ReadBufferNotEmpty); fileOffset = value; } @@ -99,50 +93,52 @@ public long FilePosition [DebuggerBrowsable(DebuggerBrowsableState.Never)] private int BufferLength => bufferEnd - bufferStart; - /// - /// Gets unconsumed part of the buffer. - /// - public ReadOnlyMemory Buffer => buffer.Memory.Slice(bufferStart, BufferLength); + /// + public ReadOnlyMemory Buffer => buffer.Memory[bufferStart..bufferEnd]; [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private ReadOnlySpan BufferSpan => buffer.Span.Slice(bufferStart, BufferLength); + private ReadOnlySpan BufferSpan => buffer.Span[bufferStart..bufferEnd]; + + private ref readonly MemoryOwner EnsureBufferAllocated() + { + ref var result = ref buffer; + if (result.IsEmpty) + result = Allocator.AllocateExactly(maxBufferSize); + + Debug.Assert(!result.IsEmpty); + return ref result; + } /// /// Gets a value indicating that the read buffer is not empty. /// public bool HasBufferedData => bufferStart < bufferEnd; - /// - /// Gets the maximum possible amount of data that can be placed to the buffer. - /// - public int MaxBufferSize => buffer.Length; - - /// - /// Advances read position. - /// - /// The number of consumed bytes. - /// is larger than the length of . - public void Consume(int bytes) + /// + public int MaxBufferSize { - var newPosition = bytes + bufferStart; - ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)newPosition, (uint)bufferEnd, nameof(bytes)); + get => maxBufferSize; + init => maxBufferSize = value >= MinBufferSize ? value : throw new ArgumentOutOfRangeException(nameof(value)); + } - if (newPosition == bufferEnd) - { - Reset(); - } - else - { - bufferStart = newPosition; - } + /// + public void Consume(int count) + { + ObjectDisposedException.ThrowIf(IsDisposed, this); + + var newPosition = count + bufferStart; + ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)newPosition, (uint)bufferEnd, nameof(count)); - fileOffset += bytes; + Consume(count, newPosition); } - private void ConsumeUnsafe(int bytes) - { - var newPosition = bytes + bufferStart; + private void ConsumeUnsafe(int count) => Consume(count, count + bufferStart); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Consume(int count, int newPosition) + { + Debug.Assert(newPosition == count + bufferStart); + if (newPosition == bufferEnd) { Reset(); @@ -152,76 +148,85 @@ private void ConsumeUnsafe(int bytes) bufferStart = newPosition; } - fileOffset += bytes; + fileOffset += count; } - - /// - /// Attempts to consume buffered data. - /// - /// The number of bytes to consume. - /// The slice of internal buffer containing consumed bytes. - /// if the specified number of bytes is consumed successfully; otherwise, . - public bool TryConsume(int bytes, out ReadOnlyMemory buffer) + + private bool TryRead(int count, out ReadOnlyMemory buffer) { - var newPosition = bytes + bufferStart; + var newPosition = count + bufferStart; if ((uint)newPosition > (uint)bufferEnd) { buffer = default; return false; } - buffer = this.buffer.Memory.Slice(bufferStart, bytes); + buffer = this.buffer.Memory.Slice(bufferStart, count); if (newPosition == bufferEnd) { - Reset(); + bufferStart = bufferEnd = 0; } else { bufferStart = newPosition; } - fileOffset += bytes; + fileOffset += count; return true; } + private void ResetIfNeeded() + { + if (bufferStart == bufferEnd) + Reset(); + } + /// /// Clears the read buffer. /// - public void Reset() => bufferStart = bufferEnd = 0; + public void Reset() + { + bufferStart = bufferEnd = 0; + buffer.Dispose(); + } - /// - /// Reads the data from the file to the underlying buffer. - /// - /// The token that can be used to cancel the operation. - /// - /// if the data has been copied from the file to the internal buffer; - /// if no more data to read. - /// - /// The reader has been disposed. - /// Internal buffer has no free space. - /// The operation has been canceled. + /// public ValueTask ReadAsync(CancellationToken token = default) { - if (IsDisposed) - return new(GetDisposedTask()); + return IsDisposed + ? new(GetDisposedTask()) + : SubmitAsBoolean(ReadCoreAsync(token), ReadCallback); + } - var buffer = this.buffer.Memory; + private ValueTask ReadCoreAsync(CancellationToken token) + { + return PrepareReadBuffer(out var readBuffer) + ? RandomAccess.ReadAsync(handle, readBuffer.Slice(bufferEnd), fileOffset + bufferEnd, token) + : ValueTask.FromException(new InternalBufferOverflowException()); + } + private bool PrepareReadBuffer(out Memory readBuffer) + { switch (bufferStart) { - case 0 when bufferEnd == buffer.Length: - return ValueTask.FromException(new InternalBufferOverflowException()); + case 0 when bufferEnd == maxBufferSize: + readBuffer = default; + return false; case > 0: + readBuffer = buffer.Memory; + // compact buffer - buffer.Slice(bufferStart, BufferLength).CopyTo(buffer); + readBuffer[bufferStart..bufferEnd].CopyTo(readBuffer); bufferEnd -= bufferStart; bufferStart = 0; break; + default: + readBuffer = EnsureBufferAllocated().Memory; + break; } - return SubmitAsBoolean(RandomAccess.ReadAsync(handle, buffer.Slice(bufferEnd), fileOffset + bufferEnd, token), readCallback); + return true; } - + /// /// Reads the data from the file to the underlying buffer. /// @@ -235,61 +240,58 @@ public bool Read() { ObjectDisposedException.ThrowIf(IsDisposed, this); - var buffer = this.buffer.Span; + return ReadCore(); + } - switch (bufferStart) - { - case 0 when bufferEnd == buffer.Length: - throw new InternalBufferOverflowException(); - case > 0: - // compact buffer - buffer.Slice(bufferStart, BufferLength).CopyTo(buffer); - bufferEnd -= bufferStart; - bufferStart = 0; - break; - } + private bool ReadCore() + { + if (!PrepareReadBuffer(out var readBuffer)) + throw new InternalBufferOverflowException(); - var count = RandomAccess.Read(handle, buffer.Slice(bufferEnd), fileOffset + bufferEnd); + var count = RandomAccess.Read(handle, readBuffer.Span.Slice(bufferEnd), fileOffset + bufferEnd); bufferEnd += count; + + ResetIfNeeded(); return count > 0; } - /// - /// Reads the block of the memory. - /// - /// The output buffer. - /// The token that can be used to cancel the operation. - /// The number of bytes copied to . - /// The reader has been disposed. - /// The operation has been canceled. + /// public ValueTask ReadAsync(Memory destination, CancellationToken token = default) { + ValueTask task; if (IsDisposed) { - return new(GetDisposedTask()); + task = new(GetDisposedTask()); } - - if (destination.IsEmpty) + else if (destination.IsEmpty) { - return ValueTask.FromResult(0); + task = new(result: 0); } - - if (!HasBufferedData) + else { - return ReadDirectAsync(destination, token); - } + extraCount = ReadFromBuffer(destination.Span); + destination = destination.Slice(extraCount); - BufferSpan.CopyTo(destination.Span, out extraCount); - ConsumeUnsafe(extraCount); - destination = destination.Slice(extraCount); + if (destination.Length > maxBufferSize) + { + task = ReadDirectAsync(destination, token); + } + else if (destination.IsEmpty) + { + task = new(extraCount); + } + else + { + destinationBuffer = destination; + task = SubmitAsInt32(ReadCoreAsync(token), ReadAndCopyCallback); + } + } - return destination.IsEmpty - ? ValueTask.FromResult(extraCount) - : ReadDirectAsync(destination, token); + return task; } private ValueTask ReadDirectAsync(Memory output, CancellationToken token) - => SubmitAsInt32(RandomAccess.ReadAsync(handle, output, fileOffset, token), readDirectCallback); + => SubmitAsInt32(RandomAccess.ReadAsync(handle, output, fileOffset, token), ReadDirectCallback); /// /// Reads the block of the memory. @@ -306,46 +308,50 @@ public int Read(Span destination) { count = 0; } - else if (!HasBufferedData) - { - count = RandomAccess.Read(handle, destination, fileOffset); - fileOffset += count; - } else { - BufferSpan.CopyTo(destination, out count); - ConsumeUnsafe(count); + count = ReadFromBuffer(destination); destination = destination.Slice(count); - - if (!destination.IsEmpty) + if (destination.Length > maxBufferSize) { var directBytes = RandomAccess.Read(handle, destination, fileOffset); fileOffset += directBytes; count += directBytes; } + else if (!destination.IsEmpty && ReadCore()) + { + count += ReadFromBuffer(destination); + } } return count; } + private int ReadFromBuffer(Span destination) + { + BufferSpan.CopyTo(destination, out var bytesCopied); + ConsumeUnsafe(bytesCopied); + return bytesCopied; + } + /// /// Skips the specified number of bytes and advances file read cursor. /// - /// The number of bytes to skip. - /// is less than zero. - public void Skip(long bytes) + /// The number of bytes to skip. + /// is less than zero. + public void Skip(long count) { ObjectDisposedException.ThrowIf(IsDisposed, this); - ArgumentOutOfRangeException.ThrowIfNegative(bytes); + ArgumentOutOfRangeException.ThrowIfNegative(count); - if (bytes < BufferLength) + if (count < BufferLength) { - ConsumeUnsafe((int)bytes); + ConsumeUnsafe((int)count); } else { Reset(); - fileOffset += bytes; + fileOffset += count; } } @@ -355,6 +361,7 @@ protected override void Dispose(bool disposing) if (disposing) { buffer.Dispose(); + readCallback = readDirectCallback = readAndCopyCallback = null; } fileOffset = 0L; diff --git a/src/DotNext.IO/IO/FileWriter.Binary.cs b/src/DotNext.IO/IO/FileWriter.Binary.cs index 49311bbebd..9bbbae8806 100644 --- a/src/DotNext.IO/IO/FileWriter.Binary.cs +++ b/src/DotNext.IO/IO/FileWriter.Binary.cs @@ -32,7 +32,7 @@ private ValueTask WriteAsync(T arg, SpanAction writer, int length, C task = ValueTask.FromException(e); } } - else if (MaxBufferSize >= length) + else if (maxBufferSize >= length) { task = WriteBufferedAsync(arg, writer, length, token); } @@ -56,7 +56,7 @@ private async ValueTask WriteBufferedAsync(T arg, SpanAction writer, private async ValueTask WriteDirectAsync(T arg, SpanAction writer, int length, CancellationToken token) { - using var buffer = allocator.AllocateExactly(length); + using var buffer = Allocator.AllocateExactly(length); writer(buffer.Span, arg); await WriteDirectAsync(buffer.Memory, token).ConfigureAwait(false); } @@ -162,11 +162,12 @@ public async ValueTask WriteAsync(ReadOnlyMemory input, LengthFormat lengt public async ValueTask EncodeAsync(ReadOnlyMemory chars, EncodingContext context, LengthFormat? lengthFormat, CancellationToken token = default) { long result; + if (lengthFormat.HasValue) { if (FreeCapacity < Leb128.MaxSizeInBytes) await FlushAsync(token).ConfigureAwait(false); - + result = WriteLength(context.Encoding.GetByteCount(chars.Span), lengthFormat.GetValueOrDefault()); } else @@ -286,12 +287,13 @@ private async ValueTask FormatSlowAsync(T value, LengthFormat? lengthFor where T : notnull, IUtf8SpanFormattable { await FlushAsync(token).ConfigureAwait(false); + if (!TryFormat(value, lengthFormat, format, provider, out var bytesWritten)) { const int maxBufferSize = int.MaxValue / 2; - for (var bufferSize = MaxBufferSize + Leb128.MaxSizeInBytes; ; bufferSize = bufferSize <= maxBufferSize ? bufferSize << 1 : throw new InsufficientMemoryException()) + for (var bufferSize = maxBufferSize + Leb128.MaxSizeInBytes; ; bufferSize = bufferSize <= maxBufferSize ? bufferSize << 1 : throw new InsufficientMemoryException()) { - using var buffer = allocator.AllocateAtLeast(bufferSize); + using var buffer = Allocator.AllocateAtLeast(bufferSize); if (value.TryFormat(buffer.Span, out bytesWritten, format, provider)) { if (lengthFormat.HasValue) @@ -331,12 +333,14 @@ public async ValueTask CopyFromAsync(Stream input, CancellationToken token = def { await WriteAsync(token).ConfigureAwait(false); - var buffer = this.buffer.Memory; + var buffer = EnsureBufferAllocated().Memory; for (int bytesWritten; (bytesWritten = await input.ReadAsync(buffer, token).ConfigureAwait(false)) > 0; fileOffset += bytesWritten) { await RandomAccess.WriteAsync(handle, buffer.Slice(0, bytesWritten), fileOffset, token).ConfigureAwait(false); } + + Reset(); } /// @@ -355,7 +359,7 @@ public async ValueTask CopyFromAsync(Stream source, long count, CancellationToke await WriteAsync(token).ConfigureAwait(false); - var buffer = this.buffer.Memory; + var buffer = EnsureBufferAllocated().Memory; for (int bytesWritten; count > 0L; fileOffset += bytesWritten, count -= bytesWritten) { @@ -365,6 +369,8 @@ public async ValueTask CopyFromAsync(Stream source, long count, CancellationToke await RandomAccess.WriteAsync(handle, buffer.Slice(0, bytesWritten), fileOffset, token).ConfigureAwait(false); } + + Reset(); } /// diff --git a/src/DotNext.IO/IO/FileWriter.BufferWriter.cs b/src/DotNext.IO/IO/FileWriter.BufferWriter.cs deleted file mode 100644 index b2206e5344..0000000000 --- a/src/DotNext.IO/IO/FileWriter.BufferWriter.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Buffers; - -namespace DotNext.IO; - -public partial class FileWriter : IBufferWriter -{ - /// - void IBufferWriter.Advance(int count) => Produce(count); - - /// - Memory IBufferWriter.GetMemory(int sizeHint) - { - ArgumentOutOfRangeException.ThrowIfNegative(sizeHint); - - var result = Buffer; - return sizeHint <= result.Length ? result : throw new InsufficientMemoryException(); - } - - /// - Span IBufferWriter.GetSpan(int sizeHint) - { - ArgumentOutOfRangeException.ThrowIfNegative(sizeHint); - - var result = BufferSpan; - return sizeHint <= result.Length ? result : throw new InsufficientMemoryException(); - } -} \ No newline at end of file diff --git a/src/DotNext.IO/IO/FileWriter.Utils.cs b/src/DotNext.IO/IO/FileWriter.Utils.cs index 47eece1ce5..e015035e7d 100644 --- a/src/DotNext.IO/IO/FileWriter.Utils.cs +++ b/src/DotNext.IO/IO/FileWriter.Utils.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -10,11 +11,15 @@ namespace DotNext.IO; public partial class FileWriter : IDynamicInterfaceCastable { - private readonly Action writeCallback, writeAndCopyCallback; + private Action? writeCallback, writeAndCopyCallback; private ReadOnlyMemory secondBuffer; private ManualResetValueTaskSourceCore source; private ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter; + private Action WriteCallback => writeCallback ??= OnWrite; + + private Action WriteAndCopyCallback => writeAndCopyCallback ??= OnWriteAndCopy; + private ReadOnlyMemory GetBuffer(int index) => index switch { 0 => WrittenBuffer, @@ -66,16 +71,15 @@ private void OnWrite() try { awaiter.GetResult(); - fileOffset += secondBuffer.Length + bufferOffset; - bufferOffset = 0; + Reset(); } catch (Exception e) { source.SetException(e); return; } - + source.SetResult(0); } @@ -87,6 +91,7 @@ private void OnWriteAndCopy() var secondBuffer = this.secondBuffer; this.secondBuffer = default; + Debug.Assert(buffer.Length > 0); try { awaiter.GetResult(); diff --git a/src/DotNext.IO/IO/FileWriter.cs b/src/DotNext.IO/IO/FileWriter.cs index ef1ee3c54b..11dbd68b12 100644 --- a/src/DotNext.IO/IO/FileWriter.cs +++ b/src/DotNext.IO/IO/FileWriter.cs @@ -12,13 +12,16 @@ namespace DotNext.IO; /// This class is not thread-safe. However, it's possible to share the same file /// handle across multiple writers and use dedicated writer in each thread. /// -public partial class FileWriter : Disposable, IFlushable +public partial class FileWriter : Disposable, IFlushable, IBufferedWriter { + private const int MinBufferSize = 16; + private const int DefaultBufferSize = 4096; + /// /// Represents the file handle. /// protected readonly SafeFileHandle handle; - private readonly MemoryAllocator? allocator; + private readonly int maxBufferSize; private MemoryOwner buffer; private int bufferOffset; private long fileOffset; @@ -27,44 +30,34 @@ public partial class FileWriter : Disposable, IFlushable /// Creates a new writer backed by the file. /// /// The file handle. - /// The initial offset within the file. - /// The buffer size. - /// The buffer allocator. - /// - /// is less than zero; - /// or is less than 16 bytes. - /// /// is . - /// - /// is less than zero; - /// or to small. - /// - public FileWriter(SafeFileHandle handle, long fileOffset = 0L, int bufferSize = 4096, MemoryAllocator? allocator = null) + public FileWriter(SafeFileHandle handle) { ArgumentNullException.ThrowIfNull(handle); - ArgumentOutOfRangeException.ThrowIfNegative(fileOffset); - ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(bufferSize, 16); - buffer = allocator.AllocateAtLeast(bufferSize); + maxBufferSize = DefaultBufferSize; this.handle = handle; - this.fileOffset = fileOffset; - this.allocator = allocator; - writeCallback = OnWrite; - writeAndCopyCallback = OnWriteAndCopy; } /// /// Creates a new writer backed by the file. /// /// Writable file stream. - /// The buffer size. - /// The buffer allocator. /// is not writable. - public FileWriter(FileStream destination, int bufferSize = 4096, MemoryAllocator? allocator = null) - : this(destination.SafeFileHandle, destination.Position, bufferSize, allocator) + public FileWriter(FileStream destination) + : this(destination.SafeFileHandle) { if (!destination.CanWrite) throw new ArgumentException(ExceptionMessages.StreamNotWritable, nameof(destination)); + + FilePosition = destination.Position; + } + + /// + public MemoryAllocator? Allocator + { + get; + init; } /// @@ -72,7 +65,23 @@ public FileWriter(FileStream destination, int bufferSize = 4096, MemoryAllocator /// public ReadOnlyMemory WrittenBuffer => buffer.Memory.Slice(0, bufferOffset); - private int FreeCapacity => buffer.Length - bufferOffset; + private int FreeCapacity => maxBufferSize - bufferOffset; + + private ref readonly MemoryOwner EnsureBufferAllocated() + { + ref var result = ref buffer; + if (result.IsEmpty) + result = Allocator.AllocateExactly(maxBufferSize); + + Debug.Assert(!result.IsEmpty); + return ref result; + } + + [Conditional("DEBUG")] + private void AssertState() + { + Debug.Assert(bufferOffset <= buffer.Length, $"Offset = {bufferOffset}, Buffer Size = {buffer.Length}"); + } /// /// The remaining part of the internal buffer available for write. @@ -80,43 +89,78 @@ public FileWriter(FileStream destination, int bufferSize = 4096, MemoryAllocator /// /// The size of returned buffer may be less than or equal to . /// - public Memory Buffer => buffer.Memory.Slice(bufferOffset); + public Memory Buffer => EnsureBufferAllocated().Memory.Slice(bufferOffset); [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private Span BufferSpan => buffer.Span.Slice(bufferOffset); + private Span BufferSpan => EnsureBufferAllocated().Span.Slice(bufferOffset); - /// - /// Gets the maximum available buffer size. - /// - public int MaxBufferSize => buffer.Length; + /// + public int MaxBufferSize + { + get => maxBufferSize; + init => maxBufferSize = value >= MinBufferSize ? value : throw new ArgumentOutOfRangeException(nameof(value)); + } + + /// + public void Produce(int count) + { + ObjectDisposedException.ThrowIf(IsDisposed, this); + ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)count, (uint)FreeCapacity, nameof(count)); + + if (count > 0 && buffer.IsEmpty) + buffer = Allocator.AllocateExactly(maxBufferSize); + + bufferOffset += count; + } /// - /// Marks the specified number of bytes in the buffer as produced. + /// Tries to write the data to the internal buffer. /// - /// The number of produced bytes. - /// is larger than the length of . - public void Produce(int bytes) + /// The input data to be copied. + /// if the internal buffer has enough space to place the data from ; + /// otherwise, . + /// + public bool TryWrite(ReadOnlySpan input) { - ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)bytes, (uint)FreeCapacity, nameof(bytes)); + ObjectDisposedException.ThrowIf(IsDisposed, this); - bufferOffset += bytes; + bool result; + if (result = input.Length <= FreeCapacity) + { + input.CopyTo(BufferSpan); + bufferOffset += input.Length; + } + + return result; } /// /// Drops all buffered data. /// - public void ClearBuffer() => bufferOffset = 0; + public void Reset() + { + bufferOffset = 0; + buffer.Dispose(); + } /// /// Gets a value indicating that this writer has buffered data. /// - public bool HasBufferedData => bufferOffset > 0; + public bool HasBufferedData + { + get + { + AssertState(); + + return bufferOffset > 0; + } + } /// /// Gets or sets the cursor position within the file. /// /// The value is less than zero. - /// There is buffered data present. Call or before changing the position. + /// There is buffered data present. Call or before changing the position. public long FilePosition { get => fileOffset; @@ -125,7 +169,7 @@ public long FilePosition ArgumentOutOfRangeException.ThrowIfNegative(value); if (HasBufferedData) - throw new InvalidOperationException(); + throw new InvalidOperationException(ExceptionMessages.WriteBufferNotEmpty); fileOffset = value; } @@ -141,22 +185,16 @@ public long FilePosition public long WritePosition => fileOffset + bufferOffset; private ValueTask FlushAsync(CancellationToken token) - => Submit(RandomAccess.WriteAsync(handle, WrittenBuffer, fileOffset, token), writeCallback); + => Submit(RandomAccess.WriteAsync(handle, WrittenBuffer, fileOffset, token), WriteCallback); private void Flush() { RandomAccess.Write(handle, WrittenBuffer.Span, fileOffset); fileOffset += bufferOffset; - bufferOffset = 0; + Reset(); } - /// - /// Flushes buffered data to the file. - /// - /// The token that can be used to cancel the operation. - /// The task representing asynchronous result. - /// The writer has been disposed. - /// The operation has been canceled. + /// public ValueTask WriteAsync(CancellationToken token = default) { if (IsDisposed) @@ -165,6 +203,7 @@ public ValueTask WriteAsync(CancellationToken token = default) if (token.IsCancellationRequested) return ValueTask.FromCanceled(token); + AssertState(); return HasBufferedData ? FlushAsync(token) : ValueTask.CompletedTask; } @@ -176,6 +215,7 @@ public void FlushToDisk() { ObjectDisposedException.ThrowIf(IsDisposed, this); + AssertState(); RandomAccess.FlushToDisk(handle); } @@ -199,57 +239,60 @@ public void Write() private void WriteSlow(ReadOnlySpan input) { - if (input.Length >= buffer.Length) + if (input.Length >= maxBufferSize) { RandomAccess.Write(handle, WrittenBuffer.Span, fileOffset); fileOffset += bufferOffset; RandomAccess.Write(handle, input, fileOffset); fileOffset += input.Length; - bufferOffset = 0; + Reset(); } else { RandomAccess.Write(handle, WrittenBuffer.Span, fileOffset); fileOffset += bufferOffset; - input.CopyTo(buffer.Span); + input.CopyTo(EnsureBufferAllocated().Span); bufferOffset += input.Length; } } - /// - /// Writes the data to the file through the buffer. - /// - /// The input data to write. - /// The token that can be used to cancel the operation. - /// The task representing asynchronous result. - /// The object has been disposed. - /// The operation has been canceled. + /// public ValueTask WriteAsync(ReadOnlyMemory input, CancellationToken token = default) { + ValueTask task; + if (IsDisposed) - return new(DisposedTask); - - if (input.IsEmpty) - goto completed_synchronously; - - var freeCapacity = FreeCapacity; - switch (input.Length.CompareTo(freeCapacity)) { - case < 0: - input.CopyTo(Buffer); - bufferOffset += input.Length; - break; - case 0: - return WriteDirectAsync(input, token); - case > 0 when input.Length < MaxBufferSize: - return WriteAndCopyAsync(input, token); - default: - goto case 0; + task = new(DisposedTask); + } + else if (input.IsEmpty) + { + task = new(); + } + else + { + AssertState(); + var freeCapacity = FreeCapacity; + switch (input.Length.CompareTo(freeCapacity)) + { + case < 0: + input.CopyTo(Buffer); + bufferOffset += input.Length; + task = new(); + break; + case 0: + task = WriteDirectAsync(input, token); + break; + case > 0 when input.Length < maxBufferSize: + task = WriteAndCopyAsync(input, token); + break; + default: + goto case 0; + } } - completed_synchronously: - return ValueTask.CompletedTask; + return task; } private ValueTask WriteDirectAsync(ReadOnlyMemory input, CancellationToken token) @@ -266,7 +309,7 @@ private ValueTask WriteDirectAsync(ReadOnlyMemory input, CancellationToken task = RandomAccess.WriteAsync(handle, input, fileOffset, token); } - return Submit(task, writeCallback); + return Submit(task, WriteCallback); } private ValueTask WriteAndCopyAsync(ReadOnlyMemory input, CancellationToken token) @@ -274,7 +317,7 @@ private ValueTask WriteAndCopyAsync(ReadOnlyMemory input, CancellationToke Debug.Assert(HasBufferedData); secondBuffer = input; - return Submit(RandomAccess.WriteAsync(handle, WrittenBuffer, fileOffset, token), writeAndCopyCallback); + return Submit(RandomAccess.WriteAsync(handle, WrittenBuffer, fileOffset, token), WriteAndCopyCallback); } /// @@ -286,6 +329,7 @@ public void Write(ReadOnlySpan input) { ObjectDisposedException.ThrowIf(IsDisposed, this); + AssertState(); if (input.Length <= FreeCapacity) { input.CopyTo(BufferSpan); @@ -302,6 +346,7 @@ protected override void Dispose(bool disposing) { if (disposing) { + writeCallback = writeAndCopyCallback = null; buffer.Dispose(); } diff --git a/src/DotNext.IO/IO/Pipelines/PipeExtensions.Readers.cs b/src/DotNext.IO/IO/Pipelines/PipeExtensions.Readers.cs index 271c8b2ee2..d46da49e61 100644 --- a/src/DotNext.IO/IO/Pipelines/PipeExtensions.Readers.cs +++ b/src/DotNext.IO/IO/Pipelines/PipeExtensions.Readers.cs @@ -530,12 +530,11 @@ public static ValueTask ReadAsync(this PipeReader reader, Memory outp public static async IAsyncEnumerable> ReadAllAsync(this PipeReader reader, [EnumeratorCancellation] CancellationToken token = default) { ReadResult result; - ReadOnlySequence buffer; do { result = await reader.ReadAsync(token).ConfigureAwait(false); result.ThrowIfCancellationRequested(reader, token); - buffer = result.Buffer; + var buffer = result.Buffer; var consumed = buffer.Start; try @@ -550,6 +549,58 @@ public static async IAsyncEnumerable> ReadAllAsync(this Pip } while (!result.IsCompleted); } + + /// + /// Reads exactly the specified amount of bytes as a sequence of chunks. + /// + /// The pipe reader. + /// The numbers of bytes to read. + /// The token that can be used to cancel the operation. + /// A collection of chunks. + /// Reader doesn't have enough data to skip. + /// The operation has been canceled. + /// is + public static IAsyncEnumerable> ReadExactlyAsync(this PipeReader reader, long length, CancellationToken token = default) + { + return length switch + { + < 0L => AsyncEnumerable.Throw>(new ArgumentOutOfRangeException(nameof(length))), + 0L => AsyncEnumerable.Empty>(), + _ => DoReadExactlyAsync(reader, length, token), + }; + + static async IAsyncEnumerable> DoReadExactlyAsync(PipeReader reader, long length, [EnumeratorCancellation] CancellationToken token) + { + ReadResult result; + do + { + result = await reader.ReadAsync(token).ConfigureAwait(false); + result.ThrowIfCancellationRequested(reader, token); + var buffer = result.Buffer; + var consumed = buffer.Start; + + try + { + for (ReadOnlyMemory block; + length > 0L && buffer.TryGet(ref consumed, out block, advance: false) && !block.IsEmpty; + consumed = buffer.GetPosition(block.Length, consumed), + length -= block.Length) + { + block = block.TrimLength(int.CreateSaturating(length)); + yield return block; + } + } + finally + { + reader.AdvanceTo(consumed, buffer.End); + } + } + while (!result.IsCompleted); + + if (length > 0L) + throw new EndOfStreamException(); + } + } /// /// Decodes null-terminated UTF-8 encoded string. diff --git a/src/DotNext.IO/IO/PoolingBufferedStream.cs b/src/DotNext.IO/IO/PoolingBufferedStream.cs new file mode 100644 index 0000000000..4013f348a1 --- /dev/null +++ b/src/DotNext.IO/IO/PoolingBufferedStream.cs @@ -0,0 +1,835 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace DotNext.IO; + +using Buffers; + +/// +/// Represents alternative implementation of that supports +/// memory pooling. +/// +/// +/// The stream implements lazy buffer pattern. It means that the stream releases the buffer when there is no buffered data. +/// +/// The underlying stream to be buffered. +/// to leave open after the object is disposed; otherwise, . +public sealed class PoolingBufferedStream(Stream stream, bool leaveOpen = false) : Stream, IBufferedWriter, IFlushable, IBufferedReader +{ + private const int MinBufferSize = 16; + private const int DefaultBufferSize = 4096; + + private readonly int maxBufferSize = DefaultBufferSize; + private int readPosition, writePosition, readLength; + private MemoryOwner buffer; + private Stream? stream = stream ?? throw new ArgumentNullException(nameof(stream)); + + /// + /// Gets or sets buffer allocator. + /// + public MemoryAllocator? Allocator + { + get; + init; + } + + /// + /// Gets the base stream. + /// + public Stream BaseStream + { + get + { + ThrowIfDisposed(); + return stream; + } + } + + /// + /// Gets the maximum size of the internal buffer, in bytes. + /// + public int MaxBufferSize + { + get => maxBufferSize; + init => maxBufferSize = value >= MinBufferSize ? value : throw new ArgumentOutOfRangeException(nameof(value)); + } + + /// + public override bool CanRead => stream?.CanRead ?? false; + + /// + public override bool CanWrite => stream?.CanWrite ?? false; + + /// + public override bool CanSeek => stream?.CanSeek ?? false; + + /// + public override bool CanTimeout => stream?.CanTimeout ?? false; + + /// + public override int ReadTimeout + { + get => stream?.ReadTimeout ?? throw new InvalidOperationException(); + set + { + if (stream is null) + throw new InvalidOperationException(); + + stream.ReadTimeout = value; + } + } + + /// + public override int WriteTimeout + { + get => stream?.WriteTimeout ?? throw new InvalidOperationException(); + set + { + if (stream is null) + throw new InvalidOperationException(); + + stream.WriteTimeout = value; + } + } + + /// + public override long Length + { + get + { + ThrowIfDisposed(); + + WriteCore(); + return stream.Length; + } + } + + /// + public override void SetLength(long value) + { + ArgumentOutOfRangeException.ThrowIfNegative(value); + ThrowIfDisposed(); + + WriteCore(); + stream.SetLength(value); + } + + /// + public override long Position + { + get + { + ThrowIfDisposed(); + + return stream.Position + (readPosition - readLength + writePosition); + } + + set + { + ArgumentOutOfRangeException.ThrowIfNegative(value); + + Seek(value, SeekOrigin.Begin); + } + } + + [MemberNotNull(nameof(stream))] + private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(stream is null, this); + + /// + /// Resets the internal buffer. + /// + public void Reset() + { + readPosition = writePosition = readLength = 0; + buffer.Dispose(); + } + + private void EnsureReadBufferIsEmpty() + { + if (readPosition != readLength) + throw new InvalidOperationException(ExceptionMessages.ReadBufferNotEmpty); + } + + /// + Memory IBufferedWriter.Buffer + { + get + { + ThrowIfDisposed(); + EnsureReadBufferIsEmpty(); + + return EnsureBufferAllocated().Memory.Slice(writePosition); + } + } + + /// + void IBufferedWriter.Produce(int count) + { + ThrowIfDisposed(); + EnsureReadBufferIsEmpty(); + + var freeCapacity = maxBufferSize - writePosition; + ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)count, (uint)freeCapacity, nameof(count)); + + if (count > 0 && buffer.IsEmpty) + buffer = Allocator.AllocateExactly(maxBufferSize); + + writePosition += count; + } + + /// + /// Gets a value indicating that the stream has buffered data in write buffer. + /// + public bool HasBufferedDataToWrite => writePosition > 0; + + private ReadOnlyMemory WrittenMemory => buffer.Memory.Slice(0, writePosition); + + /// + /// Writes the buffered data to the underlying stream. + /// + public void Write() + { + AssertState(); + ThrowIfDisposed(); + + if (!stream.CanWrite) + throw new NotSupportedException(); + + WriteCore(); + } + + /// + /// Writes the buffered data to the underlying stream. + /// + /// The token that can be used to cancel the operation. + /// The task representing asynchronous execution of the operation. + /// The operation has been canceled. + /// The stream is disposed. + public ValueTask WriteAsync(CancellationToken token = default) + { + ValueTask task; + if (stream is null) + { + task = new(DisposedTask); + } + else if (stream.CanWrite) + { + task = WriteCoreAsync(token); + } + else + { + task = ValueTask.FromException(new NotSupportedException()); + } + + return task; + } + + private void WriteCore() + { + Debug.Assert(stream is not null); + + if (WrittenMemory.Span is { IsEmpty: false } writeBuf) + { + stream.Write(writeBuf); + writePosition = 0; + } + } + + private ValueTask WriteCoreAsync(CancellationToken token) + => WrittenMemory is { IsEmpty: false } writeBuf + ? WriteAndResetAsync(writeBuf, token) + : ValueTask.CompletedTask; + + private ValueTask WriteAndResetAsync(ReadOnlyMemory data, CancellationToken token) + { + Debug.Assert(stream is not null); + + writePosition = 0; + return stream.WriteAsync(data, token); + } + + private void ClearReadBufferBeforeWrite() + { + var relativePos = readPosition - readLength; + if (relativePos is not 0) + { + Debug.Assert(stream is not null); + stream.Seek(relativePos, SeekOrigin.Current); + } + + readLength = readPosition = 0; + } + + private ref readonly MemoryOwner EnsureBufferAllocated() + { + ref var result = ref buffer; + if (result.IsEmpty) + result = Allocator.AllocateExactly(maxBufferSize); + + Debug.Assert(!result.IsEmpty); + return ref result; + } + + private void ResetIfNeeded() + { + if (writePosition is 0 && readLength == readPosition) + Reset(); + } + + /// + public override void Write(ReadOnlySpan data) + { + AssertState(); + ThrowIfDisposed(); + + if (!stream.CanWrite) + throw new NotSupportedException(); + + if (!data.IsEmpty) + WriteCore(data); + } + + private void WriteCore(ReadOnlySpan data) + { + Debug.Assert(stream is not null); + + if (writePosition is 0) + ClearReadBufferBeforeWrite(); + + var freeBuf = EnsureBufferAllocated().Span.Slice(writePosition); + + // drain buffered data if needed + if (freeBuf.Length < data.Length) + WriteCore(); + + // if internal buffer has not enough space then just write through + if (data.Length > freeBuf.Length) + { + stream.Write(data); + ResetIfNeeded(); + } + else + { + data.CopyTo(freeBuf); + writePosition += data.Length; + } + } + + /// + public override void Write(byte[] data, int offset, int count) + { + ValidateBufferArguments(data, offset, count); + + Write(new ReadOnlySpan(data, offset, count)); + } + + /// + public override void WriteByte(byte value) + => Write(new ReadOnlySpan(in value)); + + /// + public override ValueTask WriteAsync(ReadOnlyMemory data, CancellationToken token = default) + { + AssertState(); + + ValueTask task; + if (stream is null) + { + task = new(DisposedTask); + } + else if (!stream.CanWrite) + { + task = ValueTask.FromException(new NotSupportedException()); + } + else if (data.IsEmpty) + { + task = new(); + } + else + { + task = WriteCoreAsync(data, token); + } + + return task; + } + + private async ValueTask WriteCoreAsync(ReadOnlyMemory data, CancellationToken token) + { + Debug.Assert(stream is not null); + + if (writePosition is 0) + ClearReadBufferBeforeWrite(); + + var freeBuf = EnsureBufferAllocated().Memory.Slice(writePosition); + + // drain buffered data if needed + if (freeBuf.Length < data.Length) + { + await WriteCoreAsync(token).ConfigureAwait(false); + freeBuf = buffer.Memory.Slice(writePosition); + } + + // if internal buffer has not enough space then just write through + if (data.Length > freeBuf.Length) + { + await stream.WriteAsync(data, token).ConfigureAwait(false); + ResetIfNeeded(); + } + else + { + data.CopyTo(freeBuf); + writePosition += data.Length; + } + } + + /// + public override Task WriteAsync(byte[] data, int offset, int count, CancellationToken token) + => WriteAsync(new ReadOnlyMemory(data, offset, count), token).AsTask(); + + /// + public override IAsyncResult BeginWrite(byte[] data, int offset, int count, AsyncCallback? callback, object? state) + => TaskToAsyncResult.Begin(WriteAsync(data, offset, count), callback, state); + + /// + public override void EndWrite(IAsyncResult asyncResult) => TaskToAsyncResult.End(asyncResult); + + private ReadOnlyMemory MemoryToRead => buffer.Memory[readPosition..readLength]; + + private int ReadFromBuffer(Span destination) + { + int count; + if (MemoryToRead.Span is { IsEmpty: false } readBuf) + { + readBuf.CopyTo(destination, out count); + readPosition += count; + ResetIfNeeded(); + } + else + { + count = 0; + } + + return count; + } + + /// + /// Gets a value indicating that the stream has data in read buffer. + /// + public bool HasBufferedDataToRead => readPosition != readLength; + + /// + public override int Read(Span data) + { + AssertState(); + ThrowIfDisposed(); + + if (!stream.CanRead) + throw new InvalidOperationException(); + + return data.IsEmpty ? 0 : ReadCore(data); + } + + private int ReadCore(Span data) + { + Debug.Assert(stream is not null); + + var bytesRead = ReadFromBuffer(data); + data = data.Slice(bytesRead); + WriteCore(); + + if (data.IsEmpty) + { + // nothing to do + } + else if (data.Length > MaxBufferSize) + { + bytesRead += stream.Read(data); + } + else + { + readLength = stream.Read(EnsureBufferAllocated().Span); + bytesRead += ReadFromBuffer(data); + } + + return bytesRead; + } + + /// + public override int Read(byte[] data, int offset, int count) + { + ValidateBufferArguments(data, offset, count); + + return Read(data.AsSpan(offset, count)); + } + + /// + public override ValueTask ReadAsync(Memory data, CancellationToken token = default) + { + AssertState(); + ValueTask task; + + if (stream is null) + { + task = GetDisposedTask(); + } + else if (!stream.CanRead) + { + task = ValueTask.FromException(new NotSupportedException()); + } + else if (buffer.IsEmpty) + { + task = new(result: 0); + } + else + { + task = ReadCoreAsync(data, token); + } + + return task; + } + + /// + public override Task ReadAsync(byte[] data, int offset, int count, CancellationToken token) + => ReadAsync(data.AsMemory(offset, count), token).AsTask(); + + private async ValueTask ReadCoreAsync(Memory data, CancellationToken token) + { + Debug.Assert(stream is not null); + + var bytesRead = ReadFromBuffer(data.Span); + data = data.Slice(bytesRead); + + await WriteCoreAsync(token).ConfigureAwait(false); + if (data.IsEmpty) + { + // nothing to do + } + else if (data.Length > MaxBufferSize) + { + bytesRead += await stream.ReadAsync(data, token).ConfigureAwait(false); + } + else + { + readLength = await stream.ReadAsync(EnsureBufferAllocated().Memory, token).ConfigureAwait(false); + bytesRead += ReadFromBuffer(data.Span); + } + + return bytesRead; + } + + /// + /// Fetches the internal buffer from the underlying stream. + /// + /// The token that can be used to cancel the operation. + /// + /// if the data has been copied from the file to the internal buffer; + /// if no more data to read. + /// + /// The operation has been canceled. + /// The stream is disposed. + /// The internal buffer is full. + public async ValueTask ReadAsync(CancellationToken token = default) + { + AssertState(); + ThrowIfDisposed(); + + if (!stream.CanRead) + throw new NotSupportedException(); + + await WriteCoreAsync(token).ConfigureAwait(false); + + var count = PrepareReadBuffer(out var readBuf) + ? await stream.ReadAsync(readBuf, token).ConfigureAwait(false) + : throw new InternalBufferOverflowException(); + readLength += count; + ResetIfNeeded(); + + return count > 0; + } + + /// + /// Populates the internal buffer from the underlying stream. + /// + /// if + /// The stream is disposed. + /// The internal buffer is full. + public bool Read() + { + AssertState(); + ThrowIfDisposed(); + + if (!stream.CanRead) + throw new NotSupportedException(); + + WriteCore(); + + var count = PrepareReadBuffer(out var readBuf) + ? stream.Read(readBuf.Span) + : throw new InternalBufferOverflowException(); + readLength += count; + ResetIfNeeded(); + + return count > 0; + } + + private bool PrepareReadBuffer(out Memory readBuffer) + { + Debug.Assert(writePosition is 0); + + switch (readPosition) + { + case 0 when readLength == maxBufferSize: + readBuffer = default; + return false; + case > 0: + readBuffer = buffer.Memory; + + // compact buffer + readBuffer[readPosition..readLength].CopyTo(readBuffer); + readLength -= readPosition; + readPosition = 0; + break; + default: + readBuffer = EnsureBufferAllocated().Memory; + break; + } + + return true; + } + + /// + public override IAsyncResult BeginRead(byte[] data, int offset, int count, AsyncCallback? callback, object? state) + => TaskToAsyncResult.Begin(ReadAsync(data, offset, count), callback, state); + + /// + public override int EndRead(IAsyncResult asyncResult) => TaskToAsyncResult.End(asyncResult); + + /// + public override Task FlushAsync(CancellationToken token) + { + Task task; + if (writePosition > 0) + { + task = WriteAndFlushAsync(token); + } + else if (stream is null) + { + task = DisposedTask; + } + else + { + Reset(); + task = stream.FlushAsync(token); + } + + return task; + } + + private Task DisposedTask => Task.FromException(new ObjectDisposedException(GetType().Name)); + + private ValueTask GetDisposedTask() => ValueTask.FromException(new ObjectDisposedException(GetType().Name)); + + private async Task WriteAndFlushAsync(CancellationToken token) + { + Debug.Assert(writePosition > 0); + Debug.Assert(buffer.Length > 0); + + ThrowIfDisposed(); + + await stream.WriteAsync(WrittenMemory, token).ConfigureAwait(false); + await stream.FlushAsync(token).ConfigureAwait(false); + + Reset(); + } + + /// + public override void Flush() + { + AssertState(); + ThrowIfDisposed(); + + WriteCore(); + stream.Flush(); + Reset(); + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + AssertState(); + ThrowIfDisposed(); + + long result; + if (WrittenMemory.Span is { IsEmpty: false } writeBuf) + { + stream.Write(writeBuf); + result = stream.Seek(offset, origin); + ResetIfNeeded(); + } + else + { + result = SeekNoWriteBuffer(offset, origin); + } + + return result; + } + + private long SeekNoWriteBuffer(long offset, SeekOrigin origin) + { + Debug.Assert(stream is not null); + + var readBytes = readLength - readPosition; + if (origin is SeekOrigin.Current && readBytes > 0) + offset -= readBytes; + + var oldPos = stream.Position - readBytes; + var newPos = stream.Seek(offset, origin); + + var readPos = newPos - oldPos + readPosition; + if (readPos >= 0L && readPos < readLength) + { + readPosition = (int)readPos; + stream.Seek(readLength - readPosition, SeekOrigin.Current); + } + else + { + Reset(); + } + + return newPos; + } + + /// + public override int ReadByte() + { + Unsafe.SkipInit(out byte value); + return Read(new Span(ref value)) > 0 ? value : -1; + } + + private void EnsureWriteBufferIsEmpty() + { + if (writePosition is not 0) + throw new InvalidOperationException(ExceptionMessages.WriteBufferNotEmpty); + } + + /// + ReadOnlyMemory IBufferedReader.Buffer + { + get + { + AssertState(); + ThrowIfDisposed(); + EnsureWriteBufferIsEmpty(); + + return buffer.Memory[readPosition..readLength]; + } + } + + /// + void IBufferedReader.Consume(int count) + { + AssertState(); + ThrowIfDisposed(); + EnsureWriteBufferIsEmpty(); + + var newPosition = count + readPosition; + ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)newPosition, (uint)readLength, nameof(count)); + + if (newPosition == readLength) + { + Reset(); + } + else + { + readPosition = newPosition; + } + } + + [Conditional("DEBUG")] + private void AssertState() + { + // if reader or writer state differs from the default one, the buffer must be allocated + Debug.Assert((readPosition == readLength && writePosition is 0) || buffer.Length > 0); + } + + /// + public override void CopyTo(Stream destination, int bufferSize) + { + AssertState(); + ValidateCopyToArguments(destination, bufferSize); + ThrowIfDisposed(); + + if (MemoryToRead.Span is { IsEmpty: false } readBuf) + { + destination.Write(readBuf); + readLength = readPosition = 0; + } + else if (WrittenMemory.Span is { IsEmpty: false } writeBuf) + { + stream.Write(writeBuf); + writePosition = 0; + } + + stream.CopyTo(destination, bufferSize); + ResetIfNeeded(); + } + + /// + public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken token) + { + AssertState(); + ValidateCopyToArguments(destination, bufferSize); + ThrowIfDisposed(); + + if (MemoryToRead is { IsEmpty: false } readBuf) + { + await destination.WriteAsync(readBuf, token).ConfigureAwait(false); + readLength = readPosition = 0; + } + else if (WrittenMemory is { IsEmpty: false } writeBuf) + { + await stream.WriteAsync(writeBuf, token).ConfigureAwait(false); + writePosition = 0; + } + + await stream.CopyToAsync(destination, bufferSize, token).ConfigureAwait(false); + ResetIfNeeded(); + } + + /// + public override ValueTask DisposeAsync() + { + ValueTask task; + if (stream is null) + { + task = new(); + } + else + { + Reset(); + task = leaveOpen ? new() : stream.DisposeAsync(); + stream = null; + } + + return task; + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (!leaveOpen && stream is not null) + stream.Dispose(); + + stream = null; + } + + Reset(); + base.Dispose(disposing); + } + + /// + ~PoolingBufferedStream() => Dispose(false); +} \ No newline at end of file diff --git a/src/DotNext.IO/IO/RandomAccessStream.cs b/src/DotNext.IO/IO/RandomAccessStream.cs index eb6b256f63..169849c6b7 100644 --- a/src/DotNext.IO/IO/RandomAccessStream.cs +++ b/src/DotNext.IO/IO/RandomAccessStream.cs @@ -141,7 +141,10 @@ public override long Seek(long offset, SeekOrigin origin) ? newPosition : throw new IOException(); } - + + /// + public override bool CanSeek => true; + /// protected override void Dispose(bool disposing) { diff --git a/src/DotNext.IO/IO/SequenceReader.cs b/src/DotNext.IO/IO/SequenceReader.cs index 8904eb8e76..8cd7d54914 100644 --- a/src/DotNext.IO/IO/SequenceReader.cs +++ b/src/DotNext.IO/IO/SequenceReader.cs @@ -252,7 +252,7 @@ public void Skip(long length) /// The memory allocator used to place the decoded block of bytes. /// The decoded block of bytes. /// Unexpected end of sequence. - public MemoryOwner ReadBlock(LengthFormat lengthFormat, MemoryAllocator? allocator = null) + public MemoryOwner ReadBlock(LengthFormat lengthFormat, MemoryAllocator? allocator) { var length = ReadLength(lengthFormat); MemoryOwner result; @@ -269,6 +269,15 @@ public MemoryOwner ReadBlock(LengthFormat lengthFormat, MemoryAllocator + /// Reads length-prefixed block of bytes. + /// + /// The format of the block length encoded in the underlying stream. + /// The decoded block of bytes. + /// Unexpected end of sequence. + public ReadOnlySequence ReadBlock(LengthFormat lengthFormat) + => Read(ReadLength(lengthFormat)); + private int Read7BitEncodedInt32() { var parser = new SevenBitEncodedIntReader(); diff --git a/src/DotNext.IO/IO/SparseStream.cs b/src/DotNext.IO/IO/SparseStream.cs index eaa21bcbc2..bee687bdb0 100644 --- a/src/DotNext.IO/IO/SparseStream.cs +++ b/src/DotNext.IO/IO/SparseStream.cs @@ -1,39 +1,41 @@ +using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace DotNext.IO; +using Buffers; + /// /// Represents multiple streams as a single stream. /// /// /// The stream is available for read-only operations. /// -internal sealed class SparseStream : Stream, IFlushable +internal abstract class SparseStream(bool leaveOpen) : Stream, IFlushable { - private readonly IEnumerator enumerator; - private bool streamAvailable; - - /// - /// Initializes a new sparse stream. - /// - /// A collection of readable streams. - public SparseStream(IEnumerable streams) + private int runningIndex; + + protected abstract ReadOnlySpan Streams { get; } + + private Stream? Current { - enumerator = streams.GetEnumerator(); - streamAvailable = enumerator.MoveNext(); - } + get + { + var streams = Streams; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void MoveToNextStream() => streamAvailable = enumerator.MoveNext(); + return (uint)runningIndex < (uint)streams.Length ? streams[runningIndex] : null; + } + } /// - public override int ReadByte() + public sealed override int ReadByte() { var result = -1; - for (; streamAvailable; MoveToNextStream()) + for (; Current is { } current; runningIndex++) { - result = enumerator.Current.ReadByte(); + result = current.ReadByte(); if (result >= 0) break; @@ -43,12 +45,12 @@ public override int ReadByte() } /// - public override int Read(Span buffer) + public sealed override int Read(Span buffer) { int count; - for (count = 0; streamAvailable; MoveToNextStream()) + for (count = 0; Current is { } current; runningIndex++) { - count = enumerator.Current.Read(buffer); + count = current.Read(buffer); if (count > 0) break; @@ -58,7 +60,7 @@ public override int Read(Span buffer) } /// - public override int Read(byte[] buffer, int offset, int count) + public sealed override int Read(byte[] buffer, int offset, int count) { ValidateBufferArguments(buffer, offset, count); @@ -67,12 +69,12 @@ public override int Read(byte[] buffer, int offset, int count) /// [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] - public override async ValueTask ReadAsync(Memory buffer, CancellationToken token = default) + public sealed override async ValueTask ReadAsync(Memory buffer, CancellationToken token = default) { int count; - for (count = 0; streamAvailable; MoveToNextStream()) + for (count = 0; Current is { } current; runningIndex++) { - count = await enumerator.Current.ReadAsync(buffer, token).ConfigureAwait(false); + count = await current.ReadAsync(buffer, token).ConfigureAwait(false); if (count > 0) break; @@ -82,95 +84,168 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation } /// - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken token) + public sealed override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken token) => ReadAsync(buffer.AsMemory(offset, count), token).AsTask(); + public sealed override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + => TaskToAsyncResult.Begin(ReadAsync(buffer, offset, count), callback, state); + + public sealed override int EndRead(IAsyncResult asyncResult) + => TaskToAsyncResult.End(asyncResult); + /// - public override void CopyTo(Stream destination, int bufferSize) + public sealed override void CopyTo(Stream destination, int bufferSize) { ValidateCopyToArguments(destination, bufferSize); - for (; streamAvailable; MoveToNextStream()) - enumerator.Current.CopyTo(destination, bufferSize); + for (; Current is { } current; runningIndex++) + current.CopyTo(destination, bufferSize); } /// - public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken token) + public sealed override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken token) { ValidateCopyToArguments(destination, bufferSize); - for (; streamAvailable; MoveToNextStream()) - await enumerator.Current.CopyToAsync(destination, bufferSize, token).ConfigureAwait(false); + for (; Current is { } current; runningIndex++) + await current.CopyToAsync(destination, bufferSize, token).ConfigureAwait(false); } /// - public override bool CanRead => true; + public sealed override bool CanRead => true; /// - public override bool CanWrite => false; + public sealed override bool CanWrite => false; /// - public override bool CanSeek => false; + public sealed override bool CanSeek => false; /// - public override long Position + public sealed override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } /// - public override void Flush() - { - if (streamAvailable) - enumerator.Current.Flush(); - } + public sealed override void Flush() => Current?.Flush(); /// - public override Task FlushAsync(CancellationToken token) - => streamAvailable ? enumerator.Current.FlushAsync(token) : Task.CompletedTask; + public sealed override Task FlushAsync(CancellationToken token) + => Current?.FlushAsync(token) ?? Task.CompletedTask; /// - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public sealed override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); /// - public override long Length => throw new NotSupportedException(); + public sealed override long Length + { + get + { + var length = 0L; + + foreach (var stream in Streams) + { + length += stream.Length; + } + + return length; + } + } /// - public override void SetLength(long value) => throw new NotSupportedException(); + public sealed override void SetLength(long value) => throw new NotSupportedException(); /// - public override void WriteByte(byte value) => throw new NotSupportedException(); + public sealed override void WriteByte(byte value) => throw new NotSupportedException(); /// - public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public sealed override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); /// - public override void Write(ReadOnlySpan buffer) => throw new NotSupportedException(); + public sealed override void Write(ReadOnlySpan buffer) => throw new NotSupportedException(); /// - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken token) => Task.FromException(new NotSupportedException()); + public sealed override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken token) => Task.FromException(new NotSupportedException()); /// - public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + public sealed override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => ValueTask.FromException(new NotSupportedException()); /// - public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + public sealed override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => throw new NotSupportedException(); /// - public override void EndWrite(IAsyncResult asyncResult) => throw new InvalidOperationException(); + public sealed override void EndWrite(IAsyncResult asyncResult) => throw new InvalidOperationException(); + + protected override void Dispose(bool disposing) + { + if (disposing && !leaveOpen) + { + Disposable.Dispose(Streams); + } + } + + public override async ValueTask DisposeAsync() + { + if (!leaveOpen) + { + for (var i = 0; i < Streams.Length; i++) + { + await Streams[i].DisposeAsync().ConfigureAwait(false); + } + } + + GC.SuppressFinalize(this); + } +} + +internal sealed class SparseStream(T streams, bool leaveOpen) : SparseStream(leaveOpen) + where T : struct, ITuple +{ + protected override ReadOnlySpan Streams + => MemoryMarshal.CreateReadOnlySpan(in Unsafe.As(ref Unsafe.AsRef(in streams)), streams.Length); +} + +internal sealed class UnboundedSparseStream : SparseStream +{ + private MemoryOwner streams; + + internal UnboundedSparseStream(Stream stream, ReadOnlySpan streams, bool leaveOpen) + : base(leaveOpen) + { + Debug.Assert(streams.Length < int.MaxValue); + + this.streams = Memory.AllocateExactly(streams.Length + 1); + var output = this.streams.Span; + output[0] = stream; + streams.CopyTo(output.Slice(1)); + } + + protected override ReadOnlySpan Streams => streams.Span; - /// protected override void Dispose(bool disposing) { - if (disposing) + try { - enumerator.Dispose(); + base.Dispose(disposing); } + finally + { + streams.Dispose(); + } + } - streamAvailable = false; - base.Dispose(disposing); + public override async ValueTask DisposeAsync() + { + try + { + await base.DisposeAsync().ConfigureAwait(false); + } + finally + { + streams.Dispose(); + } } } \ No newline at end of file diff --git a/src/DotNext.IO/IO/StreamExtensions.Readers.cs b/src/DotNext.IO/IO/StreamExtensions.Readers.cs index 23e9333f79..e540b16189 100644 --- a/src/DotNext.IO/IO/StreamExtensions.Readers.cs +++ b/src/DotNext.IO/IO/StreamExtensions.Readers.cs @@ -461,6 +461,7 @@ public static async ValueTask CopyToAsync(this Stream source, IBufferWriter or is negative. /// doesn't support reading. /// The operation has been canceled. + /// The underlying source doesn't contain necessary amount of bytes. public static async ValueTask CopyToAsync(this Stream source, IBufferWriter destination, long count, int bufferSize = 0, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(destination); @@ -482,21 +483,55 @@ public static async ValueTask CopyToAsync(this Stream source, IBufferWriter /// The returned memory block should not be used between iterations. /// - /// Readable stream. + /// Readable stream. /// The buffer size. /// The allocator of the buffer. /// The token that can be used to cancel the enumeration. /// A collection of memory blocks that can be obtained sequentially to read a whole stream. /// is less than 1. - public static async IAsyncEnumerable> ReadAllAsync(this Stream stream, int bufferSize, MemoryAllocator? allocator = null, [EnumeratorCancellation] CancellationToken token = default) + /// The operation has been canceled. + /// doesn't support reading. + public static async IAsyncEnumerable> ReadAllAsync(this Stream source, int bufferSize, MemoryAllocator? allocator = null, [EnumeratorCancellation] CancellationToken token = default) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize); + + using var buffer = allocator.AllocateAtLeast(bufferSize); + + for (int bytesRead; (bytesRead = await source.ReadAsync(buffer.Memory, token).ConfigureAwait(false)) > 0;) + yield return buffer.Memory.Slice(0, bytesRead); + } + + /// + /// Reads exactly the specified amount of bytes as a sequence of chunks. + /// + /// + /// The returned memory block should not be used between iterations. + /// + /// The stream to read from. + /// The numbers of bytes to read. + /// The maximum size of chunks to be returned. + /// The buffer allocator. + /// The token that can be used to cancel the enumeration. + /// A collection of memory blocks that can be obtained sequentially to read the requested amount of bytes. + /// The underlying source doesn't contain necessary amount of bytes. + /// is less than 1; or is less than 0. + /// The operation has been canceled. + public static async IAsyncEnumerable> ReadExactlyAsync(this Stream stream, long length, int bufferSize, + MemoryAllocator? allocator = null, [EnumeratorCancellation] CancellationToken token = default) { + ArgumentOutOfRangeException.ThrowIfNegative(length); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize); - using var bufferOwner = allocator.AllocateAtLeast(bufferSize); - var buffer = bufferOwner.Memory; + using var buffer = allocator.AllocateAtLeast(bufferSize); - for (int count; (count = await stream.ReadAsync(buffer, token).ConfigureAwait(false)) > 0;) - yield return buffer.Slice(0, count); + for (int bytesRead; length > 0L; length -= bytesRead) + { + bytesRead = await stream.ReadAsync(buffer.Memory.TrimLength(int.CreateSaturating(length)), token).ConfigureAwait(false); + if (bytesRead <= 0) + throw new EndOfStreamException(); + + yield return buffer.Memory.Slice(0, bytesRead); + } } /// diff --git a/src/DotNext.IO/IO/StreamExtensions.cs b/src/DotNext.IO/IO/StreamExtensions.cs index edb7b62137..47f929e432 100644 --- a/src/DotNext.IO/IO/StreamExtensions.cs +++ b/src/DotNext.IO/IO/StreamExtensions.cs @@ -3,6 +3,9 @@ namespace DotNext.IO; +using Buffers; +using static Runtime.Intrinsics; + /// /// Represents high-level read/write methods for the stream. /// @@ -18,14 +21,70 @@ internal static void ThrowIfEmpty(in Memory buffer, [CallerArgumentExpress throw new ArgumentException(ExceptionMessages.BufferTooSmall, expression); } + private static Stream Combine(Stream stream, ReadOnlySpan others, bool leaveOpen) + => others switch + { + [] => stream, + [var s] => new SparseStream<(Stream, Stream)>((stream, s), leaveOpen), + [var s1, var s2] => new SparseStream<(Stream, Stream, Stream)>((stream, s1, s2), leaveOpen), + [var s1, var s2, var s3] => new SparseStream<(Stream, Stream, Stream, Stream)>((stream, s1, s2, s3), leaveOpen), + [var s1, var s2, var s3, var s4] => new SparseStream<(Stream, Stream, Stream, Stream, Stream)>((stream, s1, s2, s3, s4), leaveOpen), + [var s1, var s2, var s3, var s4, var s5] => new SparseStream<(Stream, Stream, Stream, Stream, Stream, Stream)>((stream, s1, s2, s3, s4, + s5), leaveOpen), + [var s1, var s2, var s3, var s4, var s5, var s6] => new SparseStream<(Stream, Stream, Stream, Stream, Stream, Stream, Stream)>((stream, s1, s2, s3, s4, + s5, s6), leaveOpen), + { Length: int.MaxValue } => throw new InsufficientMemoryException(), + _ => new UnboundedSparseStream(stream, others, leaveOpen), + }; + /// /// Combines multiple readable streams. /// /// The stream to combine. /// A collection of streams. /// An object that represents multiple streams as one logical stream. - public static Stream Combine(this Stream stream, ReadOnlySpan others) - => others is { Length: > 0 } ? new SparseStream([stream, .. others]) : stream; + public static Stream Combine(this Stream stream, ReadOnlySpan others) // TODO: Use params in future + => Combine(stream, others, leaveOpen: true); + + /// + /// Combines multiple readable streams. + /// + /// A collection of streams. + /// to keep the wrapped streams alive when combined stream disposed; otherwise, . + /// An object that represents multiple streams as one logical stream. + /// is empty. + public static Stream Combine(this ReadOnlySpan streams, bool leaveOpen = true) + => streams is [var first, .. var rest] + ? Combine(first, rest, leaveOpen) + : throw new ArgumentException(ExceptionMessages.BufferTooSmall, nameof(streams)); + + /// + /// Combines multiple readable streams. + /// + /// A collection of streams. + /// to keep the wrapped streams alive when combined stream disposed; otherwise, . + /// An object that represents multiple streams as one logical stream. + /// is empty. + public static Stream Combine(this IEnumerable streams, bool leaveOpen = true) + { + // Use buffer to allocate streams on the stack + var buffer = new StreamBuffer(); + var writer = new BufferWriterSlim(buffer); + + Stream result; + try + { + writer.AddAll(streams); + result = Combine(writer.WrittenSpan, leaveOpen); + } + finally + { + writer.Dispose(); + KeepAlive(in buffer); + } + + return result; + } /// /// Creates a stream for the specified file handle. @@ -46,4 +105,10 @@ public static Stream AsUnbufferedStream(this SafeFileHandle handle, FileAccess a ? new UnbufferedFileStream(handle, access) : throw new ArgumentException(ExceptionMessages.FileHandleClosed, nameof(handle)); } + + [InlineArray(32)] + private struct StreamBuffer + { + private Stream element0; + } } \ No newline at end of file diff --git a/src/DotNext.IO/IO/TextStreamExtensions.cs b/src/DotNext.IO/IO/TextStreamExtensions.cs index 2e24be6a35..4690dd343d 100644 --- a/src/DotNext.IO/IO/TextStreamExtensions.cs +++ b/src/DotNext.IO/IO/TextStreamExtensions.cs @@ -199,16 +199,15 @@ public static void WriteLine(this TextWriter writer, MemoryAllocator? allo /// The buffer size. /// The allocator of the buffer. /// The token that can be used to cancel the enumeration. - /// A collection of memort blocks that can be obtained sequentially to read a whole stream. + /// A collection of memory blocks that can be obtained sequentially to read a whole stream. /// is less than 1. public static async IAsyncEnumerable> ReadAllAsync(this TextReader reader, int bufferSize, MemoryAllocator? allocator = null, [EnumeratorCancellation] CancellationToken token = default) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize); - using var bufferOwner = allocator.AllocateAtLeast(bufferSize); - var buffer = bufferOwner.Memory; + using var buffer = allocator.AllocateAtLeast(bufferSize); - for (int count; (count = await reader.ReadAsync(buffer, token).ConfigureAwait(false)) > 0;) - yield return buffer.Slice(0, count); + for (int count; (count = await reader.ReadAsync(buffer.Memory, token).ConfigureAwait(false)) > 0;) + yield return buffer.Memory.Slice(0, count); } } \ No newline at end of file diff --git a/src/DotNext.MaintenanceServices/DotNext.MaintenanceServices.csproj b/src/DotNext.MaintenanceServices/DotNext.MaintenanceServices.csproj index 00e3fced30..c7fa4198ae 100644 --- a/src/DotNext.MaintenanceServices/DotNext.MaintenanceServices.csproj +++ b/src/DotNext.MaintenanceServices/DotNext.MaintenanceServices.csproj @@ -5,13 +5,13 @@ latest enable true - true + true nullablePublicOnly DotNext .NET Foundation and Contributors .NEXT Family of Libraries - 0.4.0 + 0.5.0 DotNext.MaintenanceServices MIT diff --git a/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj b/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj index 144ae42529..0dcdba38ad 100644 --- a/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj +++ b/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj @@ -6,9 +6,9 @@ latest enable true - false + false nullablePublicOnly - 5.16.1 + 5.17.0 .NET Foundation .NEXT Family of Libraries diff --git a/src/DotNext.Tests/Buffers/Binary/Leb128Tests.cs b/src/DotNext.Tests/Buffers/Binary/Leb128Tests.cs index 627edd60ed..7e7e812e6b 100644 --- a/src/DotNext.Tests/Buffers/Binary/Leb128Tests.cs +++ b/src/DotNext.Tests/Buffers/Binary/Leb128Tests.cs @@ -83,4 +83,12 @@ public static void DifferenceBetweenSignedAndUnsignedEncoding() True(Leb128.TryGetBytes(0x7F, buffer, out bytesWritten)); Equal(2, bytesWritten); } + + [Fact] + public static void MaxSizeInBytes() + { + Equal(sizeof(uint) + 1, Leb128.MaxSizeInBytes); + Equal(sizeof(ulong) + 2, Leb128.MaxSizeInBytes); + Equal(16 + 3, Leb128.MaxSizeInBytes); + } } \ No newline at end of file diff --git a/src/DotNext.Tests/Buffers/BufferWriterSlimTests.cs b/src/DotNext.Tests/Buffers/BufferWriterSlimTests.cs index ce8975f6ba..11e71be962 100644 --- a/src/DotNext.Tests/Buffers/BufferWriterSlimTests.cs +++ b/src/DotNext.Tests/Buffers/BufferWriterSlimTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.Numerics; using System.Text; using DotNext.Buffers.Binary; @@ -19,7 +20,7 @@ public static void GrowableBuffer() Equal(2, builder.Capacity); Equal(2, builder.FreeCapacity); - builder.Write(stackalloc int[] { 10, 20 }); + builder.Write([10, 20]); Equal(2, builder.WrittenCount); Equal(2, builder.Capacity); Equal(0, builder.FreeCapacity); @@ -27,7 +28,7 @@ public static void GrowableBuffer() Equal(10, builder[0]); Equal(20, builder[1]); - builder.Write(stackalloc int[] { 30, 40 }); + builder.Write([30, 40]); Equal(4, builder.WrittenCount); True(builder.Capacity >= 2); Equal(30, builder[2]); @@ -39,7 +40,7 @@ public static void GrowableBuffer() builder.Clear(true); Equal(0, builder.WrittenCount); - builder.Write(stackalloc int[] { 50, 60, 70, 80 }); + builder.Write([50, 60, 70, 80]); Equal(4, builder.WrittenCount); True(builder.Capacity >= 2); Equal(50, builder[0]); @@ -47,9 +48,9 @@ public static void GrowableBuffer() Equal(70, builder[2]); Equal(80, builder[3]); - builder.Clear(false); + builder.Clear(); Equal(0, builder.WrittenCount); - builder.Write(stackalloc int[] { 10, 20, 30, 40 }); + builder.Write([10, 20, 30, 40]); Equal(4, builder.WrittenCount); True(builder.Capacity >= 2); Equal(10, builder[0]); @@ -404,7 +405,7 @@ public static void WriteLengthPrefixedBytes(LengthFormat format) using var buffer = writer.DetachOrCopyBuffer(); var reader = IAsyncBinaryReader.Create(buffer.Memory); - using var actual = reader.ReadBlock(format); + using var actual = reader.ReadBlock(format, allocator: null); Equal(expected, actual.Span); } @@ -455,4 +456,66 @@ public static void EncodeDecodeString(string encodingName, LengthFormat? format) Equal(expected, actual.Span); } } + + [Fact] + public static void AddList() + { + var writer = new BufferWriterSlim(); + try + { + writer.AddAll(new List { 1, 2 }); + Equal([1, 2], writer.WrittenSpan); + } + finally + { + writer.Dispose(); + } + } + + [Fact] + public static void AddArray() + { + var writer = new BufferWriterSlim(); + try + { + writer.AddAll([1, 2]); + Equal([1, 2], writer.WrittenSpan); + } + finally + { + writer.Dispose(); + } + } + + [Fact] + public static void AddString() + { + var writer = new BufferWriterSlim(); + try + { + const string expected = "ab"; + writer.AddAll(expected); + + Equal(expected, writer.WrittenSpan); + } + finally + { + writer.Dispose(); + } + } + + [Fact] + public static void AddCountableCollection() + { + var writer = new BufferWriterSlim(); + try + { + writer.AddAll(ImmutableList.Create(1, 2)); + Equal([1, 2], writer.WrittenSpan); + } + finally + { + writer.Dispose(); + } + } } \ No newline at end of file diff --git a/src/DotNext.Tests/Buffers/ChunkSequenceTests.cs b/src/DotNext.Tests/Buffers/ChunkSequenceTests.cs index 0df4a8037a..dce6ca3771 100644 --- a/src/DotNext.Tests/Buffers/ChunkSequenceTests.cs +++ b/src/DotNext.Tests/Buffers/ChunkSequenceTests.cs @@ -46,7 +46,19 @@ static IEnumerable> ToEnumerable(ReadOnlyMemory block } [Fact] - public static void StringConcatenation() + public static void Concatenation3() + { + Equal([], Memory.ToReadOnlySequence([]).ToArray()); + + ReadOnlyMemory block1 = new byte[] { 1, 2 }; + Equal(block1.Span, Memory.ToReadOnlySequence([block1]).ToArray()); + + ReadOnlyMemory block2 = new byte[] { 3, 4 }; + Equal([1, 2, 3, 4], Memory.ToReadOnlySequence([block1, block2]).ToArray()); + } + + [Fact] + public static void StringConcatenation1() { string block1 = string.Empty, block2 = null; Equal(string.Empty, new[] { block1, block2 }.ToReadOnlySequence().ToString()); @@ -68,6 +80,18 @@ static IEnumerable ToEnumerable(string block1, string block2) yield return block2; } } + + [Fact] + public static void StringConcatenation2() + { + Equal([], Memory.ToReadOnlySequence(ReadOnlySpan.Empty).ToArray()); + + const string block1 = "Hello"; + Equal(block1, Memory.ToReadOnlySequence([block1]).ToString()); + + const string block2 = ", world!"; + Equal(block1 + block2, Memory.ToReadOnlySequence([block1, block2]).ToString()); + } [Fact] public static void CopyFromSequence() diff --git a/src/DotNext.Tests/Buffers/MemoryOwnerTests.cs b/src/DotNext.Tests/Buffers/MemoryOwnerTests.cs index 7b35edf465..b1670a487b 100644 --- a/src/DotNext.Tests/Buffers/MemoryOwnerTests.cs +++ b/src/DotNext.Tests/Buffers/MemoryOwnerTests.cs @@ -127,4 +127,20 @@ public static void ResizeBuffer() True(buffer.TryResize(10)); } + + [Fact] + public static void FromFactoryWithSize() + { + const int size = 512; + using var buffer = new MemoryOwner(MemoryPool.Shared.Rent, size); + True(buffer.Length >= size); + } + + [Fact] + public static void FromFactoryWithoutSize() + { + const int size = 512; + using var buffer = new MemoryOwner(static () => MemoryPool.Shared.Rent(size)); + True(buffer.Length >= size); + } } \ No newline at end of file diff --git a/src/DotNext.Tests/Buffers/SpanReaderWriterTests.cs b/src/DotNext.Tests/Buffers/SpanReaderWriterTests.cs index d353a09186..e3661097b5 100644 --- a/src/DotNext.Tests/Buffers/SpanReaderWriterTests.cs +++ b/src/DotNext.Tests/Buffers/SpanReaderWriterTests.cs @@ -449,8 +449,7 @@ public static void WriteLengthPrefixedBytes(LengthFormat format) True(writer.Write(expected, format) > 0); var reader = IAsyncBinaryReader.Create(buffer.AsMemory(0, writer.WrittenCount)); - using var actual = reader.ReadBlock(format); - Equal(expected, actual.Span); + Equal(expected, reader.ReadBlock(format).FirstSpan); } private static void EncodeDecodeLeb128(ReadOnlySpan values) diff --git a/src/DotNext.Tests/Buffers/UnmanagedMemoryPoolTests.cs b/src/DotNext.Tests/Buffers/UnmanagedMemoryPoolTests.cs index 6dff5a604d..eb8bb8c684 100644 --- a/src/DotNext.Tests/Buffers/UnmanagedMemoryPoolTests.cs +++ b/src/DotNext.Tests/Buffers/UnmanagedMemoryPoolTests.cs @@ -2,6 +2,8 @@ namespace DotNext.Buffers; +using static Runtime.Intrinsics; + public sealed class UnmanagedMemoryPoolTests : Test { [Fact] @@ -234,5 +236,7 @@ public static unsafe void MarshalAsMemory() var memory = UnmanagedMemory.AsMemory(ptr, 3); False(memory.IsEmpty); Equal([10, 20, 30], memory.Span); + + KeepAlive(in memory); } } \ No newline at end of file diff --git a/src/DotNext.Tests/Collections/Generic/ListTests.cs b/src/DotNext.Tests/Collections/Generic/ListTests.cs index 8b75914fc3..e12e2f0243 100644 --- a/src/DotNext.Tests/Collections/Generic/ListTests.cs +++ b/src/DotNext.Tests/Collections/Generic/ListTests.cs @@ -34,7 +34,7 @@ public static void OrderedInsertion() [Fact] public static void ReadOnlyView() { - var view = new ReadOnlyListView(new[] { "1", "2", "3" }, new Converter(int.Parse)); + var view = new ReadOnlyListView(["1", "2", "3"], new Converter(int.Parse)); Equal(3, view.Count); Equal(1, view[0]); Equal(2, view[1]); @@ -46,7 +46,7 @@ public static void ReadOnlyView() [Fact] public static void ReadOnlyIndexer() { - IReadOnlyList array = new[] { 5L, 6L, 20L }; + IReadOnlyList array = [5L, 6L, 20L]; Equal(20L, List.Indexer.ReadOnly(array, 2)); Equal(6L, array.IndexerGetter().Invoke(1)); } @@ -54,7 +54,7 @@ public static void ReadOnlyIndexer() [Fact] public static void Indexer() { - IList array = new[] { 5L, 6L, 30L }; + IList array = [5L, 6L, 30L]; Equal(30L, List.Indexer.Getter(array, 2)); List.Indexer.Setter(array, 1, 10L); Equal(10L, array.IndexerGetter().Invoke(1)); @@ -99,8 +99,8 @@ private static void SliceTest(IList list) public static void SliceList() { SliceTest(new List { 10L, 20L, 30L, 40L }); - SliceTest(new[] { 10L, 20L, 30L, 40L }); - SliceTest(new ArraySegment(new[] { 10L, 20L, 30L, 40L }, 0, 4)); + SliceTest([10L, 20L, 30L, 40L]); + SliceTest(new ArraySegment([10L, 20L, 30L, 40L], 0, 4)); } [Fact] @@ -117,7 +117,7 @@ public static void InsertRemove() [Fact] public static void ArraySlice() { - var segment = List.Slice(new int[] { 10, 20, 30 }, 0..2); + var segment = List.Slice([10, 20, 30], 0..2); True(segment.TryGetSpan(out var span)); Equal(2, span.Length); Equal(10, span[0]); diff --git a/src/DotNext.Tests/DotNext.Tests.csproj b/src/DotNext.Tests/DotNext.Tests.csproj index 8bee2de2b3..23e778e5a4 100644 --- a/src/DotNext.Tests/DotNext.Tests.csproj +++ b/src/DotNext.Tests/DotNext.Tests.csproj @@ -6,7 +6,7 @@ latest true false - 5.16.1 + 5.17.0 false .NET Foundation and Contributors .NEXT Family of Libraries @@ -52,6 +52,7 @@ + diff --git a/src/DotNext.Tests/GlobalUsings.cs b/src/DotNext.Tests/GlobalUsings.cs deleted file mode 100644 index 8c927eb747..0000000000 --- a/src/DotNext.Tests/GlobalUsings.cs +++ /dev/null @@ -1 +0,0 @@ -global using Xunit; \ No newline at end of file diff --git a/src/DotNext.Tests/IO/AsyncBinaryReaderWriterTests.cs b/src/DotNext.Tests/IO/AsyncBinaryReaderWriterTests.cs index f30b0e5e27..3777f99f30 100644 --- a/src/DotNext.Tests/IO/AsyncBinaryReaderWriterTests.cs +++ b/src/DotNext.Tests/IO/AsyncBinaryReaderWriterTests.cs @@ -143,9 +143,9 @@ private sealed class FileSource : Disposable, IAsyncBinaryReaderWriterSource, IF public FileSource(int bufferSize) { var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read, FileOptions.Asynchronous); - writer = new(handle, bufferSize: bufferSize); - reader = new(handle, bufferSize: bufferSize); + handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read, FileOptions.Asynchronous | FileOptions.DeleteOnClose); + writer = new(handle) { MaxBufferSize = bufferSize }; + reader = new(handle) { MaxBufferSize = bufferSize }; } public IAsyncBinaryWriter CreateWriter() => writer; @@ -156,8 +156,10 @@ public FileSource(int bufferSize) void IFlushable.Flush() { - using (var task = FlushAsync(CancellationToken.None)) - task.Wait(DefaultTimeout); + using var cts = new CancellationTokenSource(); + using var task = FlushAsync(cts.Token); + cts.CancelAfter(DefaultTimeout); + task.Wait(); } protected override void Dispose(bool disposing) diff --git a/src/DotNext.Tests/IO/DictionarySerializer.cs b/src/DotNext.Tests/IO/DictionarySerializer.cs index 843ef46d7f..a7ad7d136d 100644 --- a/src/DotNext.Tests/IO/DictionarySerializer.cs +++ b/src/DotNext.Tests/IO/DictionarySerializer.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text; using DotNext.Buffers; +using DotNext.Text; namespace DotNext.IO; @@ -12,11 +13,13 @@ internal static async Task SerializeAsync(IReadOnlyDictionary di // write count await output.WriteLittleEndianAsync(dictionary.Count, buffer); + var context = new EncodingContext(Encoding.UTF8, reuseEncoder: true); + // write pairs foreach (var (key, value) in dictionary) { - await output.EncodeAsync(key.AsMemory(), Encoding.UTF8, LengthFormat.LittleEndian, buffer); - await output.EncodeAsync(value.AsMemory(), Encoding.UTF8, LengthFormat.LittleEndian, buffer); + await output.EncodeAsync(key.AsMemory(), context, LengthFormat.LittleEndian, buffer); + await output.EncodeAsync(value.AsMemory(), context, LengthFormat.LittleEndian, buffer); } } @@ -24,11 +27,12 @@ internal static async Task> DeserializeAsync { var count = await input.ReadLittleEndianAsync(buffer); var result = new Dictionary(count); + var context = new DecodingContext(Encoding.UTF8, reuseDecoder: true); while (--count >= 0) { - using var key = await input.DecodeAsync(Encoding.UTF8, LengthFormat.LittleEndian, buffer); - using var value = await input.DecodeAsync(Encoding.UTF8, LengthFormat.LittleEndian, buffer); + using var key = await input.DecodeAsync(context, LengthFormat.LittleEndian, buffer); + using var value = await input.DecodeAsync(context, LengthFormat.LittleEndian, buffer); result.Add(key.ToString(), value.ToString()); } diff --git a/src/DotNext.Tests/IO/FileReaderTests.cs b/src/DotNext.Tests/IO/FileReaderTests.cs index 582fa06d6c..15b71f5664 100644 --- a/src/DotNext.Tests/IO/FileReaderTests.cs +++ b/src/DotNext.Tests/IO/FileReaderTests.cs @@ -6,7 +6,8 @@ public sealed class FileReaderTests : Test public static async Task SimpleReadAsync() { var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous); + using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, + FileOptions.Asynchronous | FileOptions.DeleteOnClose); using var reader = new FileReader(handle); False(reader.HasBufferedData); True(reader.Buffer.IsEmpty); @@ -26,26 +27,27 @@ public static async Task SimpleReadAsync() True(reader.As().TryGetRemainingBytesCount(out remainingCount)); Equal(expected.Length, remainingCount); - Equal(expected, reader.Buffer.ToArray()); + Equal(expected, reader.Buffer); } [Fact] public static async Task ReadBufferTwiceAsync() { var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous); - using var reader = new FileReader(handle, bufferSize: 32); + using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, + FileOptions.Asynchronous | FileOptions.DeleteOnClose); + using var reader = new FileReader(handle) { MaxBufferSize = 32 }; var expected = RandomBytes(reader.MaxBufferSize * 2); await RandomAccess.WriteAsync(handle, expected, 0L); True(await reader.ReadAsync()); - Equal(expected.AsMemory(0, reader.Buffer.Length).ToArray(), reader.Buffer.ToArray()); + Equal(expected.AsMemory(0, reader.Buffer.Length), reader.Buffer); reader.Consume(16); True(await reader.ReadAsync()); - Equal(expected.AsMemory(16, reader.Buffer.Length).ToArray(), reader.Buffer.ToArray()); + Equal(expected.AsMemory(16, reader.Buffer.Length), reader.Buffer); reader.Consume(16); True(await reader.ReadAsync()); @@ -60,20 +62,19 @@ public static async Task ReadBufferTwiceAsync() public static async Task ReadLargeDataAsync() { var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous); - using var reader = new FileReader(handle, bufferSize: 32); + using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, + FileOptions.Asynchronous | FileOptions.DeleteOnClose); + using var reader = new FileReader(handle) { MaxBufferSize = 32 }; var expected = RandomBytes(reader.MaxBufferSize * 2); await RandomAccess.WriteAsync(handle, expected, 0L); True(await reader.ReadAsync()); - Equal(expected.AsMemory(0, reader.Buffer.Length).ToArray(), reader.Buffer.ToArray()); + Equal(expected.AsMemory(0, reader.Buffer.Length), reader.Buffer); var actual = new byte[expected.Length]; Equal(actual.Length, await reader.ReadAsync(actual)); - Equal(expected, actual); - False(await reader.ReadAsync()); } @@ -81,7 +82,8 @@ public static async Task ReadLargeDataAsync() public static void SimpleRead() { var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous); + using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, + FileOptions.Asynchronous | FileOptions.DeleteOnClose); using var reader = new FileReader(handle); False(reader.HasBufferedData); True(reader.Buffer.IsEmpty); @@ -93,26 +95,27 @@ public static void SimpleRead() True(reader.HasBufferedData); False(reader.Buffer.IsEmpty); - Equal(expected, reader.Buffer.ToArray()); + Equal(expected, reader.Buffer); } [Fact] public static void ReadBufferTwice() { var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous); - using var reader = new FileReader(handle, bufferSize: 32); + using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, + FileOptions.Asynchronous | FileOptions.DeleteOnClose); + using var reader = new FileReader(handle) { MaxBufferSize = 32 }; var expected = RandomBytes(reader.MaxBufferSize * 2); RandomAccess.Write(handle, expected, 0L); True(reader.Read()); - Equal(expected.AsMemory(0, reader.Buffer.Length).ToArray(), reader.Buffer.ToArray()); + Equal(expected.AsMemory(0, reader.Buffer.Length), reader.Buffer); reader.Consume(16); True(reader.Read()); - Equal(expected.AsMemory(16, reader.Buffer.Length).ToArray(), reader.Buffer.ToArray()); + Equal(expected.AsMemory(16, reader.Buffer.Length), reader.Buffer); reader.Consume(16); True(reader.Read()); @@ -127,14 +130,15 @@ public static void ReadBufferTwice() public static void ReadLargeData() { var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous); - using var reader = new FileReader(handle, bufferSize: 32); + using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, + FileOptions.Asynchronous | FileOptions.DeleteOnClose); + using var reader = new FileReader(handle) { MaxBufferSize = 32 }; var expected = RandomBytes(reader.MaxBufferSize * 2); RandomAccess.Write(handle, expected, 0L); True(reader.Read()); - Equal(expected.AsMemory(0, reader.Buffer.Length).ToArray(), reader.Buffer.ToArray()); + Equal(expected.AsMemory(0, reader.Buffer.Length), reader.Buffer); var actual = new byte[expected.Length]; Equal(actual.Length, reader.Read(actual)); @@ -148,8 +152,9 @@ public static void ReadLargeData() public static async Task ReadSequentially() { var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - await using var fs = new FileStream(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.Asynchronous); - using var reader = new FileReader(fs, bufferSize: 32); + await using var fs = new FileStream(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 4096, + FileOptions.Asynchronous | FileOptions.DeleteOnClose); + using var reader = new FileReader(fs) { MaxBufferSize = 32 }; var bytes = RandomBytes(1024); await fs.WriteAsync(bytes); diff --git a/src/DotNext.Tests/IO/FileWriterTests.cs b/src/DotNext.Tests/IO/FileWriterTests.cs index 3d00484e7b..4cced67e31 100644 --- a/src/DotNext.Tests/IO/FileWriterTests.cs +++ b/src/DotNext.Tests/IO/FileWriterTests.cs @@ -11,8 +11,9 @@ public sealed class FileWriterTests : Test public static async Task WriteWithoutOverflowAsync() { var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous); - using var writer = new FileWriter(handle, bufferSize: 64); + using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, + FileOptions.Asynchronous | FileOptions.DeleteOnClose); + using var writer = new FileWriter(handle) { MaxBufferSize = 64 }; False(writer.HasBufferedData); Equal(0L, writer.FilePosition); @@ -34,8 +35,9 @@ public static async Task WriteWithoutOverflowAsync() public static async Task WriteWithOverflowAsync() { var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous); - using var writer = new FileWriter(handle, bufferSize: 64); + using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, + FileOptions.Asynchronous | FileOptions.DeleteOnClose); + using var writer = new FileWriter(handle) { MaxBufferSize = 64 }; var expected = RandomBytes(writer.Buffer.Length + 10); await writer.WriteAsync(expected); @@ -52,8 +54,9 @@ public static async Task WriteWithOverflowAsync() public static async Task WritDirectAsync() { var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous); - using var writer = new FileWriter(handle, bufferSize: 64); + using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, + FileOptions.Asynchronous | FileOptions.DeleteOnClose); + using var writer = new FileWriter(handle) { MaxBufferSize = 64 }; var expected = RandomBytes(writer.Buffer.Length << 2); await writer.WriteAsync(expected.AsMemory(0, 63)); @@ -71,8 +74,8 @@ public static async Task WritDirectAsync() public static void WriteWithoutOverflow() { var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.None); - using var writer = new FileWriter(handle, bufferSize: 64); + using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.DeleteOnClose); + using var writer = new FileWriter(handle) { MaxBufferSize = 64 }; False(writer.HasBufferedData); Equal(0L, writer.FilePosition); @@ -94,8 +97,8 @@ public static void WriteWithoutOverflow() public static void WriteWithOverflow() { var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.None); - using var writer = new FileWriter(handle, bufferSize: 64); + using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.DeleteOnClose); + using var writer = new FileWriter(handle) { MaxBufferSize = 64 }; var expected = RandomBytes(writer.Buffer.Length + 10); writer.Write(expected); @@ -112,8 +115,8 @@ public static void WriteWithOverflow() public static void WriteUsingBufferWriter() { var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - using var fs = new FileStream(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.None); - using var writer = new FileWriter(fs, bufferSize: 64); + using var fs = new FileStream(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.DeleteOnClose); + using var writer = new FileWriter(fs) { MaxBufferSize = 64 }; False(writer.HasBufferedData); Equal(0L, writer.FilePosition); @@ -135,8 +138,8 @@ public static void WriteUsingBufferWriter() public static void WriteUsingBufferWriter2() { var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.None); - using var writer = new FileWriter(handle, bufferSize: 64); + using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.DeleteOnClose); + using var writer = new FileWriter(handle) { MaxBufferSize = 64 }; False(writer.HasBufferedData); Equal(0L, writer.FilePosition); @@ -159,8 +162,9 @@ public static void WriteUsingBufferWriter2() public static async Task FlushWithOffsetAsync() { var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous); - using var writer = new FileWriter(handle, fileOffset: 100L, bufferSize: 64); + using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, + FileOptions.Asynchronous | FileOptions.DeleteOnClose); + using var writer = new FileWriter(handle) { FilePosition = 100L, MaxBufferSize = 64 }; writer.Buffer.Span[0] = 1; writer.Buffer.Span[1] = 2; writer.Produce(2); @@ -176,8 +180,9 @@ public static async Task FlushWithOffsetAsync() public static async Task WriteDirect() { var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous); - using var writer = new FileWriter(handle, fileOffset: 0L, bufferSize: 64); + using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, + FileOptions.Asynchronous | FileOptions.DeleteOnClose); + using var writer = new FileWriter(handle) { MaxBufferSize = 64 }; await writer.WriteAsync(new Blittable { Value = default }); False(writer.HasBufferedData); Equal(writer.FilePosition, Unsafe.SizeOf()); @@ -187,14 +192,27 @@ public static async Task WriteDirect() public static async Task BufferOverflow() { var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous); - using var writer = new FileWriter(handle, fileOffset: 0L, bufferSize: 64); + using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, + FileOptions.Asynchronous | FileOptions.DeleteOnClose); + using var writer = new FileWriter(handle) { MaxBufferSize = 64 }; await writer.WriteAsync(new byte[2]); await writer.WriteAsync(new Blittable { Value = default }); False(writer.HasBufferedData); Equal(writer.FilePosition, Unsafe.SizeOf() + 2); } + [Fact] + public static void TryWrite() + { + var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.DeleteOnClose); + using var writer = new FileWriter(handle) { MaxBufferSize = 64 }; + + True(writer.TryWrite(new byte[2])); + True(writer.HasBufferedData); + Equal("\0\0"u8, writer.WrittenBuffer.Span); + } + [InlineArray(512)] private struct Buffer512 { diff --git a/src/DotNext.Tests/IO/Pipelines/PipeExtensionsTests.cs b/src/DotNext.Tests/IO/Pipelines/PipeExtensionsTests.cs index efdea66410..872c4f9206 100644 --- a/src/DotNext.Tests/IO/Pipelines/PipeExtensionsTests.cs +++ b/src/DotNext.Tests/IO/Pipelines/PipeExtensionsTests.cs @@ -302,6 +302,39 @@ public static async Task ReadPortionAsync() } } + [Fact] + public static async Task ReadBlockExactlyAsync() + { + var bytes = RandomBytes(128); + + var reader = PipeReader.Create(new(bytes)); + + using var destination = new MemoryStream(); + await foreach (var chunk in reader.ReadExactlyAsync(64L)) + { + await destination.WriteAsync(chunk); + } + + Equal(new ReadOnlySpan(bytes, 0, 64), destination.ToArray()); + } + + [Fact] + public static void ReadEmptyBlockAsync() + { + var reader = PipeReader.Create(ReadOnlySequence.Empty); + + Empty(reader.ReadExactlyAsync(0L)); + } + + [Fact] + public static async Task ReadInvalidSizedBlockAsync() + { + var reader = PipeReader.Create(ReadOnlySequence.Empty); + + await using var enumerator = reader.ReadExactlyAsync(-1L).GetAsyncEnumerator(); + await ThrowsAsync(enumerator.MoveNextAsync().AsTask); + } + [Fact] public static async Task DecodeNullTerminatedStringAsync() { diff --git a/src/DotNext.Tests/IO/PoolingBufferedStreamTests.cs b/src/DotNext.Tests/IO/PoolingBufferedStreamTests.cs new file mode 100644 index 0000000000..6747a9b96c --- /dev/null +++ b/src/DotNext.Tests/IO/PoolingBufferedStreamTests.cs @@ -0,0 +1,344 @@ +using DotNext.Buffers; + +namespace DotNext.IO; + +public sealed class PoolingBufferedStreamTests : Test +{ + [Fact] + public static void SimpleRead() + { + const int bufferSize = 4096; + var expected = RandomBytes(bufferSize); + using var stream = new MemoryStream(expected.Length); + stream.Write(expected); + stream.Seek(0L, SeekOrigin.Begin); + + using var bufferedStream = new PoolingBufferedStream(stream, leaveOpen: true) { MaxBufferSize = bufferSize }; + Same(stream, bufferedStream.BaseStream); + False(bufferedStream.HasBufferedDataToRead); + False(bufferedStream.HasBufferedDataToWrite); + Equal(stream.Position, bufferedStream.Position); + + Equal(expected[0], bufferedStream.ReadByte()); + True(bufferedStream.HasBufferedDataToRead); + False(bufferedStream.HasBufferedDataToWrite); + Equal(bufferSize, stream.Position); + Equal(1L, bufferedStream.Position); + + var actual = new byte[bufferSize - 1]; + Equal(actual.Length, bufferedStream.Read(actual)); + Equal(stream.Position, bufferedStream.Position); + Equal(expected.AsSpan(1), actual.AsSpan()); + + Equal(0, bufferedStream.Read(actual)); + Equal(-1, bufferedStream.ReadByte()); + False(bufferedStream.HasBufferedDataToRead); + False(bufferedStream.HasBufferedDataToWrite); + } + + [Fact] + public static async Task SimpleReadAsync() + { + const int bufferSize = 4096; + var expected = RandomBytes(bufferSize); + await using var stream = new MemoryStream(expected.Length); + stream.Write(expected); + stream.Seek(0L, SeekOrigin.Begin); + + await using var bufferedStream = new PoolingBufferedStream(stream, leaveOpen: true) { MaxBufferSize = bufferSize }; + False(bufferedStream.HasBufferedDataToRead); + False(bufferedStream.HasBufferedDataToWrite); + Equal(stream.Position, bufferedStream.Position); + + Equal(expected[0], bufferedStream.ReadByte()); + True(bufferedStream.HasBufferedDataToRead); + False(bufferedStream.HasBufferedDataToWrite); + Equal(bufferSize, stream.Position); + Equal(1L, bufferedStream.Position); + + var actual = new byte[bufferSize - 1]; + Equal(actual.Length, await bufferedStream.ReadAsync(actual)); + Equal(stream.Position, bufferedStream.Position); + Equal(expected.AsSpan(1), actual.AsSpan()); + + Equal(0, await bufferedStream.ReadAsync(actual)); + Equal(-1, bufferedStream.ReadByte()); + False(bufferedStream.HasBufferedDataToRead); + False(bufferedStream.HasBufferedDataToWrite); + } + + [Fact] + public static void BufferizeAndAdvancePosition() + { + const int bufferSize = 4096; + var expected = RandomBytes(bufferSize); + using var stream = new MemoryStream(expected.Length); + stream.Write(expected); + stream.Seek(0L, SeekOrigin.Begin); + + using var bufferedStream = new PoolingBufferedStream(stream, leaveOpen: true) { MaxBufferSize = bufferSize }; + True(bufferedStream.Read()); + + bufferedStream.Position = bufferSize / 2; + True(bufferedStream.HasBufferedDataToRead); + + var actual = new byte[bufferSize / 2]; + Equal(actual.Length, bufferedStream.Read(actual, 0, actual.Length)); + Equal(stream.Position, bufferedStream.Position); + + Equal(expected.AsSpan(bufferSize / 2), actual.AsSpan()); + } + + [Fact] + public static async Task BufferizeAndAdvancePositionAsync() + { + const int bufferSize = 4096; + var expected = RandomBytes(bufferSize); + using var stream = new MemoryStream(expected.Length); + stream.Write(expected); + stream.Seek(0L, SeekOrigin.Begin); + + await using var bufferedStream = new PoolingBufferedStream(stream, leaveOpen: true) { MaxBufferSize = bufferSize }; + True(await bufferedStream.ReadAsync()); + + bufferedStream.Position = bufferSize / 2; + Equal(bufferSize, stream.Position); + True(bufferedStream.HasBufferedDataToRead); + + var actual = new byte[bufferSize / 2]; + Equal(actual.Length, await bufferedStream.ReadAsync(actual, 0, actual.Length)); + Equal(stream.Position, bufferedStream.Position); + + Equal(expected.AsSpan(bufferSize / 2), actual.AsSpan()); + } + + [Fact] + public static void DrainBuffer() + { + const int bufferSize = 4096; + using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize }; + + var expected = RandomBytes(bufferSize); + bufferedStream.Write(expected); + True(bufferedStream.HasBufferedDataToWrite); + False(bufferedStream.HasBufferedDataToRead); + + Equal(0L, bufferedStream.BaseStream.Position); + Equal(bufferSize, bufferedStream.Length); + + bufferedStream.Write(); + Equal(bufferSize, bufferedStream.BaseStream.Position); + Equal(bufferedStream.BaseStream.Length, bufferedStream.Length); + } + + [Fact] + public static async Task DrainBufferAsync() + { + const int bufferSize = 4096; + await using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize }; + + var expected = RandomBytes(bufferSize); + await bufferedStream.WriteAsync(expected); + True(bufferedStream.HasBufferedDataToWrite); + False(bufferedStream.HasBufferedDataToRead); + + Equal(0L, bufferedStream.BaseStream.Position); + Equal(bufferSize, bufferedStream.Length); + + await bufferedStream.WriteAsync(); + Equal(bufferSize, bufferedStream.BaseStream.Position); + Equal(bufferedStream.BaseStream.Length, bufferedStream.Length); + } + + [Fact] + public static void CheckProperties() + { + using var stream = new MemoryStream(); + using var bufferedStream = new PoolingBufferedStream(stream, leaveOpen: true); + + Equal(stream.CanRead, bufferedStream.CanRead); + Equal(stream.CanWrite, bufferedStream.CanWrite); + Equal(stream.CanSeek, bufferedStream.CanSeek); + Equal(stream.CanTimeout, bufferedStream.CanTimeout); + + Throws(() => stream.ReadTimeout); + Throws(() => stream.ReadTimeout = 10); + + Throws(() => stream.WriteTimeout); + Throws(() => stream.WriteTimeout = 10); + } + + [Fact] + public static async Task BufferedWriter() + { + const int bufferSize = 4096; + await using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize }; + + var expected = RandomBytes(bufferSize); + IBufferedWriter writer = bufferedStream; + expected.CopyTo(writer.Buffer); + writer.Produce(expected.Length); + True(bufferedStream.HasBufferedDataToWrite); + False(bufferedStream.HasBufferedDataToRead); + await writer.WriteAsync(); + + False(bufferedStream.HasBufferedDataToWrite); + False(bufferedStream.HasBufferedDataToRead); + } + + [Fact] + public static async Task BufferedReader() + { + const int bufferSize = 4096; + await using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize }; + + var expected = RandomBytes(bufferSize); + await bufferedStream.WriteAsync(expected); + await bufferedStream.WriteAsync(); + bufferedStream.Position = 0L; + + False(bufferedStream.HasBufferedDataToRead); + IBufferedReader reader = bufferedStream; + await reader.ReadAsync(); + True(bufferedStream.HasBufferedDataToRead); + Equal(expected, reader.Buffer); + } + + [Fact] + public static void InvalidReaderWriterStates() + { + const int bufferSize = 4096; + using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize }; + bufferedStream.WriteByte(42); + bufferedStream.WriteByte(43); + Throws(() => bufferedStream.As().Buffer); + Throws(() => bufferedStream.As().Consume(1)); + + bufferedStream.Flush(); + bufferedStream.Position = 0; + bufferedStream.Read(); + bufferedStream.As().Consume(1); + Equal([43], bufferedStream.As().Buffer.Span); + + Throws(() => bufferedStream.As().Buffer); + Throws(() => bufferedStream.As().Produce(1)); + } + + [Fact] + public static void StreamLength() + { + const int bufferSize = 4096; + using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize }; + Equal(0L, bufferedStream.Length); + + bufferedStream.WriteByte(42); + bufferedStream.WriteByte(43); + Equal(2L, bufferedStream.Length); + } + + [Fact] + public static void CopyStream() + { + const int bufferSize = 4096; + using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize }; + + var expected = RandomBytes(bufferSize); + bufferedStream.Write(expected); + bufferedStream.Flush(); + bufferedStream.Position = 0L; + + using var destination = new MemoryStream(bufferSize); + bufferedStream.CopyTo(destination); + + Equal(expected, destination.GetBuffer()); + } + + [Fact] + public static void CopyStream2() + { + const int bufferSize = 4096; + using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize }; + + var expected = RandomBytes(bufferSize); + bufferedStream.Write(expected); + bufferedStream.Flush(); + bufferedStream.Position = 0L; + True(bufferedStream.Read()); + + using var destination = new MemoryStream(bufferSize); + bufferedStream.CopyTo(destination); + + Equal(expected, destination.GetBuffer()); + } + + [Fact] + public static async Task CopyStreamAsync() + { + const int bufferSize = 4096; + await using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize }; + + var expected = RandomBytes(bufferSize); + await bufferedStream.WriteAsync(expected); + await bufferedStream.FlushAsync(); + bufferedStream.Position = 0L; + + await using var destination = new MemoryStream(bufferSize); + await bufferedStream.CopyToAsync(destination); + + Equal(expected, destination.GetBuffer()); + } + + [Fact] + public static async Task CopyStream2Async() + { + const int bufferSize = 4096; + await using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize }; + + var expected = RandomBytes(bufferSize); + await bufferedStream.WriteAsync(expected); + await bufferedStream.FlushAsync(); + bufferedStream.Position = 0L; + True(await bufferedStream.ReadAsync()); + + await using var destination = new MemoryStream(bufferSize); + await bufferedStream.CopyToAsync(destination); + + Equal(expected, destination.GetBuffer()); + } + + [Fact] + public static void SetLength() + { + const int bufferSize = 4096; + using var stream = new MemoryStream(bufferSize); + using var bufferedStream = new PoolingBufferedStream(stream, leaveOpen: false) + { + MaxBufferSize = bufferSize, + Allocator = Memory.GetArrayAllocator(), + }; + + var expected = RandomBytes(bufferSize); + bufferedStream.Write(expected); + + bufferedStream.SetLength(bufferSize); + Equal(expected, stream.GetBuffer()); + } + + [Fact] + public static void ResetBuffer() + { + const int bufferSize = 4096; + using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize), leaveOpen: false) + { + MaxBufferSize = bufferSize, + Allocator = Memory.GetArrayAllocator(), + }; + + var expected = RandomBytes(bufferSize); + bufferedStream.Write(expected); + True(bufferedStream.HasBufferedDataToWrite); + + bufferedStream.Reset(); + False(bufferedStream.HasBufferedDataToWrite); + } +} \ No newline at end of file diff --git a/src/DotNext.Tests/IO/StreamExtensionsTests.cs b/src/DotNext.Tests/IO/StreamExtensionsTests.cs index 6b362f6526..245fd5e4a9 100644 --- a/src/DotNext.Tests/IO/StreamExtensionsTests.cs +++ b/src/DotNext.Tests/IO/StreamExtensionsTests.cs @@ -1,14 +1,10 @@ using System.Buffers; -using System.Globalization; using System.Text; -using static System.Globalization.CultureInfo; -using DateTimeStyles = System.Globalization.DateTimeStyles; namespace DotNext.IO; -using DotNext.Buffers.Binary; -using Net.Cluster; -using Text; +using Buffers.Binary; +using Collections.Generic; public sealed class StreamExtensionsTests : Test { @@ -156,7 +152,7 @@ public static async Task CombineStreamsAsync() { await using var ms1 = new MemoryStream([1, 2, 3]); await using var ms2 = new MemoryStream([4, 5, 6]); - await using var combined = ms1.Combine([ms2]); + await using var combined = StreamExtensions.Combine([ms1, ms2]); var buffer = new byte[6]; await combined.ReadExactlyAsync(buffer); @@ -169,7 +165,7 @@ public static void CopyCombinedStreams() { using var ms1 = new MemoryStream([1, 2, 3]); using var ms2 = new MemoryStream([4, 5, 6]); - using var combined = ms1.Combine([ms2]); + using var combined = List.Singleton(ms1).Append(ms2).Combine(); using var result = new MemoryStream(); combined.CopyTo(result, 128); @@ -204,6 +200,37 @@ public static void ReadBytesFromCombinedStream() Equal(-1, combined.ReadByte()); } + [Theory] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(7)] + [InlineData(8)] + [InlineData(9)] + [InlineData(10)] + public static void CombineManyStreams(byte streamCount) + { + using var stream = GetStreams(streamCount).Combine(leaveOpen: false); + Equal(streamCount, stream.Length); + var actual = new byte[streamCount]; + stream.ReadExactly(actual); + var expected = Set.Range, DisclosedEndpoint>(0, streamCount); + Equal(expected, actual); + + static IEnumerable GetStreams(byte streamCount) + { + for (var i = 0; i < streamCount; i++) + { + var ms = new MemoryStream(); + ms.WriteByte((byte)i); + ms.Seek(0L, SeekOrigin.Begin); + yield return ms; + } + } + } + [Fact] public static async Task UnsupportedMethodsOfSparseStream() { @@ -351,4 +378,19 @@ public static void DecodeNullTerminatedEmptyString() ms.ReadUtf8(stackalloc byte[8], writer); Equal(string.Empty, writer.WrittenSpan.ToString()); } + + [Fact] + public static async Task ReadBlockAsSequenceAsync() + { + var bytes = RandomBytes(1024); + using var source = new MemoryStream(bytes, false); + using var destination = new MemoryStream(1024); + + await foreach (var chunk in source.ReadExactlyAsync(512L, 64)) + { + await destination.WriteAsync(chunk); + } + + Equal(new ReadOnlySpan(bytes, 0, 512), destination.ToArray()); + } } \ No newline at end of file diff --git a/src/DotNext.Tests/Net/EndPointFormatterTests.cs b/src/DotNext.Tests/Net/EndPointFormatterTests.cs index 8b319f08e7..351bfcc459 100644 --- a/src/DotNext.Tests/Net/EndPointFormatterTests.cs +++ b/src/DotNext.Tests/Net/EndPointFormatterTests.cs @@ -12,14 +12,17 @@ public sealed class EndPointFormatterTests : Test { public static TheoryData> GetTestEndPoints() { + var ip = IPAddress.Parse("192.168.0.1"); var rows = new TheoryData>(); rows.Add(new DnsEndPoint("host", 3262), EqualityComparer.Default); - rows.Add(new IPEndPoint(IPAddress.Parse("192.168.0.1"), 3263), EqualityComparer.Default); + rows.Add(new IPEndPoint(ip, 3263), EqualityComparer.Default); rows.Add(new IPEndPoint(IPAddress.Parse("2001:0db8:0000:0000:0000:8a2e:0370:7334"), 3264), EqualityComparer.Default); rows.Add(new HttpEndPoint("192.168.0.1", 3262, true, AddressFamily.InterNetwork), EqualityComparer.Default); rows.Add(new HttpEndPoint("192.168.0.1", 3262, false, AddressFamily.InterNetwork), EqualityComparer.Default); rows.Add(new HttpEndPoint("2001:0db8:0000:0000:0000:8a2e:0370:7334", 3262, true, AddressFamily.InterNetworkV6), EqualityComparer.Default); + rows.Add(new HttpEndPoint(ip, 8080, false), EqualityComparer.Default); + rows.Add(new HttpEndPoint(new IPEndPoint(ip, 8080), false), EqualityComparer.Default); rows.Add(new HttpEndPoint("host", 3262, true), EqualityComparer.Default); rows.Add(new HttpEndPoint("host", 3262, false), EqualityComparer.Default); rows.Add(new UriEndPoint(new Uri("http://host:3262/")), EndPointFormatter.UriEndPointComparer); diff --git a/src/DotNext.Tests/Net/Http/HttpEndPointTests.cs b/src/DotNext.Tests/Net/Http/HttpEndPointTests.cs index 789b9c1746..ce142f8d01 100644 --- a/src/DotNext.Tests/Net/Http/HttpEndPointTests.cs +++ b/src/DotNext.Tests/Net/Http/HttpEndPointTests.cs @@ -1,4 +1,6 @@ using System.Net.Sockets; +using System.Text; +using Microsoft.AspNetCore.Components.Forms; namespace DotNext.Net.Http; @@ -64,4 +66,46 @@ public static void ToUri() Equal(Uri.UriSchemeHttps, uri.Scheme, ignoreCase: true); Equal(3262, ep.Port); } + + [Fact] + public static void Format() + { + const string expected = "http://localhost:3262/"; + Span buffer = stackalloc char[64]; + ISpanFormattable formattable = new HttpEndPoint(new Uri(expected)); + True(formattable.TryFormat(buffer, out var charsWritten, ReadOnlySpan.Empty, provider: null)); + + Equal(expected, buffer.Slice(0, charsWritten)); + Equal(expected, formattable.ToString(format: null, formatProvider: null)); + } + + [Fact] + public static void FormatAsUtf8() + { + const string expected = "http://localhost:3262/"; + var expectedBytes = Encoding.UTF8.GetBytes(expected); + + Span buffer = stackalloc byte[64]; + IUtf8SpanFormattable formattable = new HttpEndPoint(new Uri(expected)); + True(formattable.TryFormat(buffer, out var charsWritten, ReadOnlySpan.Empty, provider: null)); + + Equal(expectedBytes, buffer.Slice(0, charsWritten)); + } + + [Fact] + public static void Parse() + { + const string expected = "http://localhost:3262/"; + Equal(expected, Parse(expected).ToString()); + True(TryParse(expected, out var ep)); + Equal(expected, ep.ToString()); + + static T Parse(string input) + where T : IParsable + => T.Parse(input, provider: null); + + static bool TryParse(string input, out T result) + where T : IParsable + => T.TryParse(input, provider: null, out result); + } } \ No newline at end of file diff --git a/src/DotNext.Tests/Reflection/TaskTypeTests.cs b/src/DotNext.Tests/Reflection/TaskTypeTests.cs index 4d9c62e8a1..657665f272 100644 --- a/src/DotNext.Tests/Reflection/TaskTypeTests.cs +++ b/src/DotNext.Tests/Reflection/TaskTypeTests.cs @@ -39,6 +39,6 @@ public static void GetResultSynchronously() [Fact] public static void IsCompletedPropertyGetter() { - True(TaskType.GetIsCompletedGetter(Task.CompletedTask).Invoke()); + True(Task.CompletedTask.GetIsCompletedGetter().Invoke()); } } \ No newline at end of file diff --git a/src/DotNext.Tests/SpanTests.cs b/src/DotNext.Tests/SpanTests.cs index 1a993477a6..b90a5e5a3d 100644 --- a/src/DotNext.Tests/SpanTests.cs +++ b/src/DotNext.Tests/SpanTests.cs @@ -562,6 +562,9 @@ public static void CheckMask() public static void CheckLargeMask() { var value = RandomBytes(1024); + if (value[0] is byte.MaxValue) + value[0] = byte.MaxValue - 1; + var mask = value.AsSpan().ToArray(); mask.AsSpan(0, 512).Clear(); diff --git a/src/DotNext.Tests/Threading/Tasks/SynchronizationTests.cs b/src/DotNext.Tests/Threading/Tasks/SynchronizationTests.cs index d756a21a6f..d60009f798 100644 --- a/src/DotNext.Tests/Threading/Tasks/SynchronizationTests.cs +++ b/src/DotNext.Tests/Threading/Tasks/SynchronizationTests.cs @@ -179,4 +179,39 @@ public static async Task WhenAllWithResult5() Equal(40, result4.Value); Equal(50, result5.Value); } + + [Fact] + public static void SynchronousWait() + { + var cts = new TaskCompletionSource(); + var task = new ValueTask(cts.Task); + ThreadPool.UnsafeQueueUserWorkItem(static cts => + { + Thread.Sleep(100); + cts.SetResult(); + }, cts, preferLocal: false); + task.Wait(); + + // ensure that the current thread in not interrupted + Thread.Sleep(0); + } + + [Fact] + public static void SynchronousWaitWithResult() + { + var cts = new TaskCompletionSource(); + var task = new ValueTask(cts.Task); + + const int expected = 42; + ThreadPool.UnsafeQueueUserWorkItem(static cts => + { + Thread.Sleep(100); + cts.SetResult(expected); + }, cts, preferLocal: false); + + Equal(expected, task.Wait()); + + // ensure that the current thread in not interrupted + Thread.Sleep(0); + } } \ No newline at end of file diff --git a/src/DotNext.Threading/DotNext.Threading.csproj b/src/DotNext.Threading/DotNext.Threading.csproj index 33f32226f5..e6407ffa25 100644 --- a/src/DotNext.Threading/DotNext.Threading.csproj +++ b/src/DotNext.Threading/DotNext.Threading.csproj @@ -5,9 +5,9 @@ latest enable true - true + true nullablePublicOnly - 5.16.1 + 5.17.0 .NET Foundation and Contributors .NEXT Family of Libraries diff --git a/src/DotNext.Threading/Threading/AsyncReaderWriterLock.cs b/src/DotNext.Threading/Threading/AsyncReaderWriterLock.cs index 583f2d5e6b..c6d9ed16f1 100644 --- a/src/DotNext.Threading/Threading/AsyncReaderWriterLock.cs +++ b/src/DotNext.Threading/Threading/AsyncReaderWriterLock.cs @@ -36,6 +36,7 @@ private enum LockType : byte // describes internal state of reader/writer lock [StructLayout(LayoutKind.Auto)] + [SuppressMessage("Usage", "CA1001", Justification = "The disposable field is disposed in the Dispose() method")] internal struct State : IDisposable { private ulong version; // version of write lock diff --git a/src/DotNext.Unsafe/Buffers/UnmanagedMemory.cs b/src/DotNext.Unsafe/Buffers/UnmanagedMemory.cs index 0eb708d296..f0aa19f6c1 100644 --- a/src/DotNext.Unsafe/Buffers/UnmanagedMemory.cs +++ b/src/DotNext.Unsafe/Buffers/UnmanagedMemory.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -79,7 +80,7 @@ public static unsafe Memory AsMemory(T* pointer, int length) if (length > 0) { - MemoryManager manager = new UnmanagedMemory((nint)pointer, length); + var manager = new UnmanagedMemory((nint)pointer, length); // GC perf: manager doesn't own the memory represented by the pointer, no need to call Dispose from finalizer GC.SuppressFinalize(manager); @@ -146,6 +147,8 @@ internal void Reallocate(int length) public sealed override Span GetSpan() => address is not null ? new(address, Length) : []; + public sealed override Memory Memory => address is not null ? CreateMemory(Length) : Memory.Empty; + public sealed override MemoryHandle Pin(int elementIndex = 0) { ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)elementIndex, (uint)Length, nameof(elementIndex)); @@ -167,6 +170,9 @@ protected override void Dispose(bool disposing) address = null; Length = 0; } + + [SuppressMessage("Reliability", "CA2015", Justification = "The caller must hold the reference to the memory object.")] + ~UnmanagedMemory() => Dispose(disposing: false); } internal class UnmanagedMemoryOwner : UnmanagedMemory, IUnmanagedMemory diff --git a/src/DotNext.Unsafe/DotNext.Unsafe.csproj b/src/DotNext.Unsafe/DotNext.Unsafe.csproj index 5d086a8ea8..05e006b259 100644 --- a/src/DotNext.Unsafe/DotNext.Unsafe.csproj +++ b/src/DotNext.Unsafe/DotNext.Unsafe.csproj @@ -6,8 +6,8 @@ latest enable true - true - 5.16.1 + true + 5.17.0 nullablePublicOnly .NET Foundation and Contributors diff --git a/src/DotNext.sln b/src/DotNext.sln index dce0ee1c5a..f8f215fad7 100644 --- a/src/DotNext.sln +++ b/src/DotNext.sln @@ -42,6 +42,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLineAMI", "examples\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RandomAccessCacheBenchmark", "examples\RandomAccessCacheBenchmark\RandomAccessCacheBenchmark.csproj", "{73185946-8EFD-4153-8AC4-05AFD8BAC2E4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNext.AotTests", "DotNext.AotTests\DotNext.AotTests.csproj", "{155A8FEF-5FDD-42D3-AC31-335542FC588C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Bench|Any CPU = Bench|Any CPU @@ -133,6 +135,12 @@ Global {73185946-8EFD-4153-8AC4-05AFD8BAC2E4}.Debug|Any CPU.Build.0 = Debug|Any CPU {73185946-8EFD-4153-8AC4-05AFD8BAC2E4}.Release|Any CPU.ActiveCfg = Release|Any CPU {73185946-8EFD-4153-8AC4-05AFD8BAC2E4}.Release|Any CPU.Build.0 = Release|Any CPU + {155A8FEF-5FDD-42D3-AC31-335542FC588C}.Bench|Any CPU.ActiveCfg = Debug|Any CPU + {155A8FEF-5FDD-42D3-AC31-335542FC588C}.Bench|Any CPU.Build.0 = Debug|Any CPU + {155A8FEF-5FDD-42D3-AC31-335542FC588C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {155A8FEF-5FDD-42D3-AC31-335542FC588C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {155A8FEF-5FDD-42D3-AC31-335542FC588C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {155A8FEF-5FDD-42D3-AC31-335542FC588C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -146,6 +154,7 @@ Global {1A2FAA85-95B5-4377-B430-622DC5000C89} = {F8DCA620-40E7-411E-8970-85DC808B6BAF} {39583DDE-E579-44AD-B7AF-5BB77D979E55} = {F8DCA620-40E7-411E-8970-85DC808B6BAF} {73185946-8EFD-4153-8AC4-05AFD8BAC2E4} = {F8DCA620-40E7-411E-8970-85DC808B6BAF} + {155A8FEF-5FDD-42D3-AC31-335542FC588C} = {4956E982-79AA-462C-B592-E904D24EFFAE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1CE24157-AFE0-4CDF-B063-2876343F0A66} diff --git a/src/DotNext/Buffers/BufferWriterSlim.cs b/src/DotNext/Buffers/BufferWriterSlim.cs index 30c8950f24..8017ed4d97 100644 --- a/src/DotNext/Buffers/BufferWriterSlim.cs +++ b/src/DotNext/Buffers/BufferWriterSlim.cs @@ -353,6 +353,58 @@ public void Clear(bool reuseBuffer = false) position = 0; } + /// + /// Writes a collection of elements. + /// + /// A collection of elements. + /// is . + public void AddAll(IEnumerable collection) + { + ArgumentNullException.ThrowIfNull(collection); + + ReadOnlySpan input; + switch (collection) + { + case List list: + input = CollectionsMarshal.AsSpan(list); + break; + case T[] array: + input = array; + break; + case string str: + input = Unsafe.BitCast, ReadOnlyMemory>(str.AsMemory()).Span; + break; + case ArraySegment segment: + input = segment; + break; + default: + WriteSlow(collection); + return; + } + + Write(input); + } + + private void WriteSlow(IEnumerable collection) + { + using var enumerator = collection.GetEnumerator(); + if (collection.TryGetNonEnumeratedCount(out var count)) + { + var buffer = InternalGetSpan(count); + for (count = 0; count < buffer.Length && enumerator.MoveNext(); count++) + { + buffer[count] = enumerator.Current; + } + + position += count; + } + + while (enumerator.MoveNext()) + { + Add(enumerator.Current); + } + } + /// /// Releases internal buffer used by this builder. /// diff --git a/src/DotNext/Buffers/ByteBuffer.cs b/src/DotNext/Buffers/ByteBuffer.cs index 672755fcfc..256fcb9a6d 100644 --- a/src/DotNext/Buffers/ByteBuffer.cs +++ b/src/DotNext/Buffers/ByteBuffer.cs @@ -1,6 +1,7 @@ using System.Buffers; using System.Diagnostics.CodeAnalysis; using System.Numerics; +using System.Text.Unicode; namespace DotNext.Buffers; @@ -324,6 +325,21 @@ public static bool TryFormat(this ref SpanWriter writer, T value, ReadO return result; } + /// + /// Writes the specified sequence of characters as UTF-8 encoded octets. + /// + /// The buffer writer. + /// The input characters to be encoded. + /// if is encoded successfully; otherwise, . + public static bool TryEncodeAsUtf8(this ref SpanWriter writer, ReadOnlySpan input) + { + bool result; + if (result = Utf8.FromUtf16(input, writer.RemainingSpan, out _, out var bytesWritten, replaceInvalidSequences: false) is OperationStatus.Done) + writer.Advance(bytesWritten); + + return result; + } + /// /// Writes 32-bit integer in a compressed format. /// diff --git a/src/DotNext/Buffers/Chunk.cs b/src/DotNext/Buffers/Chunk.cs index dfc9af07ab..43387156fe 100644 --- a/src/DotNext/Buffers/Chunk.cs +++ b/src/DotNext/Buffers/Chunk.cs @@ -26,6 +26,10 @@ internal static void AddChunk(in ReadOnlyMemory segment, [AllowNull] ref Chun : last.Next(segment); } - internal static ReadOnlySequence CreateSequence(Chunk head, Chunk tail) - => new(head, 0, tail, tail.Memory.Length); + internal static ReadOnlySequence CreateSequence(Chunk? head, Chunk? tail) + => head is null || tail is null + ? new() + : ReferenceEquals(head, tail) + ? new(head.Memory) + : new(head, 0, tail, tail.Memory.Length); } \ No newline at end of file diff --git a/src/DotNext/Buffers/Memory.ReadOnlySequence.cs b/src/DotNext/Buffers/Memory.ReadOnlySequence.cs index 1d6ba732cd..068bd79c0d 100644 --- a/src/DotNext/Buffers/Memory.ReadOnlySequence.cs +++ b/src/DotNext/Buffers/Memory.ReadOnlySequence.cs @@ -1,4 +1,5 @@ using System.Buffers; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -13,17 +14,19 @@ public static partial class Memory /// The sequence of memory blocks. /// The type of elements in the memory blocks. /// The constructed instance containing memory blocks. - public static ReadOnlySequence ToReadOnlySequence(this IEnumerable> chunks) + public static ReadOnlySequence ToReadOnlySequence(this IEnumerable>? chunks) { Chunk? head = null, tail = null; switch (chunks) { + case null: + break; case ReadOnlyMemory[] array: - FromSpan(array.AsSpan(), ref head, ref tail); + CreateChunks(array.AsSpan(), ref head, ref tail); break; case List> list: - FromSpan(CollectionsMarshal.AsSpan(list), ref head, ref tail); + CreateChunks(CollectionsMarshal.AsSpan(list), ref head, ref tail); break; case LinkedList> list: FromLinkedList(list, ref head, ref tail); @@ -33,12 +36,6 @@ public static ReadOnlySequence ToReadOnlySequence(this IEnumerable.Empty; - - if (ReferenceEquals(head, tail)) - return new(head.Memory); - return Chunk.CreateSequence(head, tail); static void ToReadOnlySequenceSlow(IEnumerable> chunks, ref Chunk? head, ref Chunk? tail) @@ -50,15 +47,6 @@ static void ToReadOnlySequenceSlow(IEnumerable> chunks, ref Ch } } - static void FromSpan(ReadOnlySpan> chunks, ref Chunk? head, ref Chunk? tail) - { - foreach (ref readonly var segment in chunks) - { - if (!segment.IsEmpty) - Chunk.AddChunk(segment, ref head, ref tail); - } - } - static void FromLinkedList(LinkedList> chunks, ref Chunk? head, ref Chunk? tail) { for (var current = chunks.First; current is not null; current = current.Next) @@ -70,36 +58,63 @@ static void FromLinkedList(LinkedList> chunks, ref Chunk? h } } + /// + /// Converts the sequence of memory blocks to data type. + /// + /// The sequence of memory blocks. + /// The type of elements in the memory blocks. + /// The constructed instance containing memory blocks. + public static ReadOnlySequence ToReadOnlySequence(ReadOnlySpan> chunks) // TODO: use params + { + switch (chunks) + { + case []: + return ReadOnlySequence.Empty; + case [var chunk]: + return new(chunk); + default: + Chunk? head = null, tail = null; + CreateChunks(chunks, ref head, ref tail); + return Chunk.CreateSequence(head, tail); + } + } + + private static void CreateChunks(ReadOnlySpan> chunks, ref Chunk? head, ref Chunk? tail) + { + foreach (ref readonly var segment in chunks) + { + if (!segment.IsEmpty) + Chunk.AddChunk(segment, ref head, ref tail); + } + } + /// /// Constructs a sequence of characters from a collection of strings. /// /// A collection of strings. /// A sequence of characters representing concatenated strings. - public static ReadOnlySequence ToReadOnlySequence(this IEnumerable strings) + public static ReadOnlySequence ToReadOnlySequence(this IEnumerable? strings) { Chunk? head = null, tail = null; switch (strings) { + case null: + break; case List list: - ToReadOnlySequence(CollectionsMarshal.AsSpan(list), ref head, ref tail); + CreateChunks(CollectionsMarshal.AsSpan(list), ref head, ref tail); break; case string?[] array: - ToReadOnlySequence(array.AsSpan(), ref head, ref tail); + CreateChunks(array.AsSpan(), ref head, ref tail); break; default: ToReadOnlySequenceSlow(strings, ref head, ref tail); break; } - if (head is null || tail is null) - return ReadOnlySequence.Empty; - - return ReferenceEquals(head, tail) - ? new(head.Memory) - : Chunk.CreateSequence(head, tail); + return Chunk.CreateSequence(head, tail); - static void ToReadOnlySequence(ReadOnlySpan strings, ref Chunk? head, ref Chunk? tail) + static void ToReadOnlySequenceSlow(IEnumerable strings, ref Chunk? head, ref Chunk? tail) { foreach (var str in strings) { @@ -107,14 +122,34 @@ static void ToReadOnlySequence(ReadOnlySpan strings, ref Chunk? h Chunk.AddChunk(str.AsMemory(), ref head, ref tail); } } + } - static void ToReadOnlySequenceSlow(IEnumerable strings, ref Chunk? head, ref Chunk? tail) + /// + /// Constructs a sequence of characters from a collection of strings. + /// + /// A collection of strings. + /// A sequence of characters representing concatenated strings. + public static ReadOnlySequence ToReadOnlySequence(ReadOnlySpan strings) // TODO: Use params + { + switch (strings) { - foreach (var str in strings) - { - if (str is { Length: > 0 }) - Chunk.AddChunk(str.AsMemory(), ref head, ref tail); - } + case []: + return ReadOnlySequence.Empty; + case [var str]: + return new(str.AsMemory()); + default: + Chunk? head = null, tail = null; + CreateChunks(strings, ref head, ref tail); + return Chunk.CreateSequence(head, tail); + } + } + + private static void CreateChunks(ReadOnlySpan strings, ref Chunk? head, ref Chunk? tail) + { + foreach (var str in strings) + { + if (str is { Length: > 0 }) + Chunk.AddChunk(str.AsMemory(), ref head, ref tail); } } diff --git a/src/DotNext/Buffers/SparseBufferWriter.Reader.cs b/src/DotNext/Buffers/SparseBufferWriter.Reader.cs index c32e89f737..1108f9b349 100644 --- a/src/DotNext/Buffers/SparseBufferWriter.Reader.cs +++ b/src/DotNext/Buffers/SparseBufferWriter.Reader.cs @@ -145,12 +145,6 @@ public ReadOnlySequence Read(ref SequencePosition position, long count) if (count > 0L) throw new InvalidOperationException(ExceptionMessages.EndOfBuffer(count)); - if (head is null || tail is null) - return ReadOnlySequence.Empty; - - if (ReferenceEquals(head, tail)) - return new(head.Memory); - return Chunk.CreateSequence(head, tail); } diff --git a/src/DotNext/Disposable.cs b/src/DotNext/Disposable.cs index 2e0176a0f6..f85c15c8c8 100644 --- a/src/DotNext/Disposable.cs +++ b/src/DotNext/Disposable.cs @@ -156,7 +156,8 @@ public static async ValueTask DisposeAsync(IEnumerable object /// Disposes many objects in safe manner. /// /// An array of objects to dispose. - public static void Dispose(ReadOnlySpan objects) + public static void Dispose(ReadOnlySpan objects) + where T : IDisposable? { foreach (var obj in objects) obj?.Dispose(); diff --git a/src/DotNext/DotNext.csproj b/src/DotNext/DotNext.csproj index 43af4e637e..1bd85e3d14 100644 --- a/src/DotNext/DotNext.csproj +++ b/src/DotNext/DotNext.csproj @@ -5,13 +5,13 @@ latest enable true - true + true nullablePublicOnly DotNext .NET Foundation and Contributors .NEXT Family of Libraries - 5.16.1 + 5.17.0 DotNext MIT diff --git a/src/DotNext/Dynamic/TaskResultBinder.cs b/src/DotNext/Dynamic/TaskResultBinder.cs index 23b08fc006..4cfdace94f 100644 --- a/src/DotNext/Dynamic/TaskResultBinder.cs +++ b/src/DotNext/Dynamic/TaskResultBinder.cs @@ -7,8 +7,8 @@ namespace DotNext.Dynamic; -[RequiresUnreferencedCode("Runtime binding may be incompatible with IL trimming")] [RequiresDynamicCode("DLR is required to resolve underlying task type at runtime")] +[RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] internal sealed class TaskResultBinder : CallSiteBinder { private static Expression BindProperty(PropertyInfo resultProperty, Expression target, out Expression restrictions) diff --git a/src/DotNext/EqualityComparerBuilder.cs b/src/DotNext/EqualityComparerBuilder.cs index c4d9693cd8..7173c830aa 100644 --- a/src/DotNext/EqualityComparerBuilder.cs +++ b/src/DotNext/EqualityComparerBuilder.cs @@ -64,6 +64,7 @@ internal ConstructedEqualityComparer(Func equality, Func h } [RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] + [RequiresDynamicCode("Dynamic code generation may be incompatible with AOT")] private static PropertyInfo? GetDefaultEqualityComparer(Type target) => typeof(EqualityComparer<>) .MakeGenericType(target) @@ -71,6 +72,7 @@ internal ConstructedEqualityComparer(Func equality, Func h private static FieldInfo? HashSaltField => typeof(RandomExtensions).GetField(nameof(RandomExtensions.BitwiseHashSalt), BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly); + [RequiresDynamicCode("Runtime binding requires dynamic code compilation")] [RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] private static MethodCallExpression EqualsMethodForValueType(MemberExpression first, MemberExpression second) { @@ -97,6 +99,7 @@ private static MethodCallExpression EqualsMethodForValueType(MemberExpression fi return Expression.Call(Expression.Property(null, defaultProperty), method, first, second); } + [RequiresDynamicCode("Runtime binding requires dynamic code compilation")] [RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] private static Expression HashCodeMethodForValueType(Expression expr, ConstantExpression salted) { @@ -130,6 +133,7 @@ private static Expression HashCodeMethodForValueType(Expression expr, ConstantEx return expr; } + [RequiresDynamicCode("Runtime binding requires dynamic code compilation")] [RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] private static MethodInfo EqualsMethodForArrayElementType(Type itemType) { @@ -151,6 +155,7 @@ private static MethodInfo EqualsMethodForArrayElementType(Type itemType) return new Func?, IEnumerable?, bool>(Collection.SequenceEqual).Method; } + [RequiresDynamicCode("Runtime binding requires dynamic code compilation")] [RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] private static MethodCallExpression EqualsMethodForArrayElementType(MemberExpression fieldX, MemberExpression fieldY) { @@ -159,6 +164,7 @@ private static MethodCallExpression EqualsMethodForArrayElementType(MemberExpres return Expression.Call(method, fieldX, fieldY); } + [RequiresDynamicCode("Runtime binding requires dynamic code compilation")] [RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] private static MethodInfo HashCodeMethodForArrayElementType(Type itemType) { @@ -174,6 +180,7 @@ private static MethodInfo HashCodeMethodForArrayElementType(Type itemType) .MakeGenericMethod(itemType); } + [RequiresDynamicCode("Runtime binding requires dynamic code compilation")] [RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] private static MethodCallExpression HashCodeMethodForArrayElementType(Expression expr, ConstantExpression salted) { @@ -192,11 +199,10 @@ private static IEnumerable GetAllFields(Type type) } } + [RequiresDynamicCode("Runtime binding requires dynamic code compilation")] [RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] private Func BuildEquals() { - if (!RuntimeFeature.IsDynamicCodeSupported) - throw new PlatformNotSupportedException(); var x = Expression.Parameter(typeof(T)); if (x.Type.IsPrimitive) return EqualityComparer.Default.Equals; @@ -238,11 +244,10 @@ private static IEnumerable GetAllFields(Type type) return Expression.Lambda>(expr, false, x, y).Compile(); } + [RequiresDynamicCode("Runtime binding requires dynamic code compilation")] [RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] private Func BuildGetHashCode() { - if (!RuntimeFeature.IsDynamicCodeSupported) - throw new PlatformNotSupportedException(); Expression expr; var inputParam = Expression.Parameter(typeof(T)); if (inputParam.Type.IsPrimitive) @@ -291,6 +296,7 @@ private Func BuildGetHashCode() /// The implementation of equality check. /// The implementation of hash code. /// Dynamic code generation is not supported by underlying CLR implementation. + [RequiresDynamicCode("Runtime binding requires dynamic code compilation")] [RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] public void Build(out Func equals, out Func hashCode) { @@ -303,6 +309,7 @@ public void Build(out Func equals, out Func hashCode) /// /// The generated equality comparer. /// Dynamic code generation is not supported by underlying CLR implementation. + [RequiresDynamicCode("Runtime binding requires dynamic code compilation")] [RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] public IEqualityComparer Build() { diff --git a/src/DotNext/Net/Http/HttpEndPoint.cs b/src/DotNext/Net/Http/HttpEndPoint.cs index b87cc97af8..96411a65fb 100644 --- a/src/DotNext/Net/Http/HttpEndPoint.cs +++ b/src/DotNext/Net/Http/HttpEndPoint.cs @@ -5,10 +5,12 @@ namespace DotNext.Net.Http; +using Buffers; + /// /// Represents HTTP endpoint. /// -public sealed class HttpEndPoint : DnsEndPoint, ISupplier, IEquatable, IEqualityOperators +public sealed class HttpEndPoint : DnsEndPoint, ISupplier, IEquatable, IEqualityOperators, ISpanFormattable, IParsable, IUtf8SpanFormattable { private const StringComparison HostNameComparison = StringComparison.OrdinalIgnoreCase; @@ -145,7 +147,46 @@ public override int GetHashCode() /// Converts endpoint to its string representation. /// /// The string representation of this end point. - public override string ToString() => $"{Scheme}://{Host}:{Port}/"; + public override string ToString() => ToString(format: null, formatProvider: null); + + /// + public string ToString(string? format, IFormatProvider? formatProvider) + => $"{Scheme}://{Host}:{Port.ToString(format, formatProvider)}/"; + + /// + bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) + { + var writer = new SpanWriter(destination); + + bool success; + charsWritten = (success = writer.TryWrite(Scheme) + && writer.TryWrite("://") + && writer.TryWrite(Host) + && writer.TryAdd(':') + && writer.TryFormat(Port, format, provider) + && writer.TryAdd('/')) + ? writer.WrittenCount + : default; + + return success; + } + + /// + bool IUtf8SpanFormattable.TryFormat(Span destination, out int bytesWritten, ReadOnlySpan format, IFormatProvider? provider) + { + var writer = new SpanWriter(destination); + bool success; + bytesWritten = (success = writer.TryEncodeAsUtf8(Scheme) + && writer.TryWrite("://"u8) + && writer.TryEncodeAsUtf8(Host) + && writer.TryAdd((byte)':') + && writer.TryFormat(Port, format, provider) + && writer.TryAdd((byte)'/')) + ? writer.WrittenCount + : default; + + return success; + } /// /// Attempts to parse HTTP endpoint. @@ -164,4 +205,12 @@ public static bool TryParse(string? str, [NotNullWhen(true)] out HttpEndPoint? r result = null; return false; } + + /// + static HttpEndPoint IParsable.Parse(string s, IFormatProvider? provider) + => new(new Uri(s)); + + /// + static bool IParsable.TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out HttpEndPoint result) + => TryParse(s, out result); } \ No newline at end of file diff --git a/src/DotNext/Reflection/TaskType.cs b/src/DotNext/Reflection/TaskType.cs index 62adba158d..14c1996ca9 100644 --- a/src/DotNext/Reflection/TaskType.cs +++ b/src/DotNext/Reflection/TaskType.cs @@ -1,4 +1,6 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Reflection; using static InlineIL.IL; using static InlineIL.IL.Emit; using static InlineIL.MethodRef; @@ -20,11 +22,16 @@ private static class Cache static Cache() { - Ldnull(); - Ldftn(PropertyGet(Type>(), nameof(Task.Result))); - Newobj(Constructor(Type, T>>(), Type(), Type())); - Pop(out Func, T> propertyGetter); - ResultGetter = propertyGetter; + Ldtoken(PropertyGet(Type>(), nameof(Task.Result))); + Pop(out RuntimeMethodHandle getterHandle); + + Ldtoken>(); + Pop(out RuntimeTypeHandle taskHandle); + + var getterInfo = MethodBase.GetMethodFromHandle(getterHandle, taskHandle) as MethodInfo; + Debug.Assert(getterInfo is not null); + + ResultGetter = getterInfo.CreateDelegate, T>>(); } } @@ -34,11 +41,16 @@ static TaskType() { CompletedTaskType = Task.CompletedTask.GetType(); - Ldnull(); - Ldftn(PropertyGet(Type(), nameof(Task.IsCompletedSuccessfully))); - Newobj(Constructor(Type>(), Type(), Type())); - Pop(out Func propertyGetter); - IsCompletedSuccessfullyGetter = propertyGetter; + Ldtoken(PropertyGet(Type(), nameof(Task.IsCompletedSuccessfully))); + Pop(out RuntimeMethodHandle getterHandle); + + Ldtoken(); + Pop(out RuntimeTypeHandle taskHandle); + + var getterInfo = MethodBase.GetMethodFromHandle(getterHandle, taskHandle) as MethodInfo; + Debug.Assert(getterInfo is not null); + + IsCompletedSuccessfullyGetter = getterInfo.CreateDelegate>(); } /// @@ -51,7 +63,8 @@ static TaskType() /// /// /// - [RequiresUnreferencedCode("Runtime generic instantiation may be incompatible with IL trimming")] + [RequiresDynamicCode("Runtime binding requires dynamic code compilation")] + [RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] public static Type MakeTaskType(this Type taskResult, bool valueTask = false) { if (taskResult == typeof(void)) diff --git a/src/DotNext/Runtime/Intrinsics.cs b/src/DotNext/Runtime/Intrinsics.cs index ef33cec8ca..05e738e7d7 100644 --- a/src/DotNext/Runtime/Intrinsics.cs +++ b/src/DotNext/Runtime/Intrinsics.cs @@ -387,4 +387,28 @@ public static int AlignOf() [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool AreCompatible() => Unsafe.SizeOf() == Unsafe.SizeOf() && AlignOf() == AlignOf(); + + /// + /// Keeps the reference to the value type alive. + /// + /// A location of the object. + /// The value type. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void KeepAlive(ref readonly T location) + where T : struct + { + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + KeepAlive(in InToRef(in location)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + [StackTraceHidden] + private static void KeepAlive(ref readonly byte location) + { + // We cannot inline this check to avoid compiler optimization to eliminate null check. + // This check can be eliminated because typically the location points to the field in the class + // and that field is already statically checked for null + if (Unsafe.IsNullRef(in location)) + throw new ArgumentNullException(nameof(location)); + } } \ No newline at end of file diff --git a/src/DotNext/Text/Json/OptionalConverterFactory.cs b/src/DotNext/Text/Json/OptionalConverterFactory.cs index 4009632a9f..0c3cb121da 100644 --- a/src/DotNext/Text/Json/OptionalConverterFactory.cs +++ b/src/DotNext/Text/Json/OptionalConverterFactory.cs @@ -12,6 +12,7 @@ namespace DotNext.Text.Json; /// converter explicitly as an argument for . /// [RequiresUnreferencedCode("This type instantiates OptionalConverter dynamically. Use OptionalConverter instead.")] +[RequiresDynamicCode("Runtime binding requires dynamic code compilation")] public sealed class OptionalConverterFactory : JsonConverterFactory { /// diff --git a/src/DotNext/Threading/Tasks/DynamicTaskAwaitable.cs b/src/DotNext/Threading/Tasks/DynamicTaskAwaitable.cs index 9315f6e385..87d8bdd877 100644 --- a/src/DotNext/Threading/Tasks/DynamicTaskAwaitable.cs +++ b/src/DotNext/Threading/Tasks/DynamicTaskAwaitable.cs @@ -47,7 +47,8 @@ internal Awaiter(Task task, ConfigureAwaitOptions options) public void UnsafeOnCompleted(Action continuation) => awaiter.UnsafeOnCompleted(continuation); - [RequiresUnreferencedCode("Runtime binding may be incompatible with IL trimming")] + [RequiresDynamicCode("Runtime binding requires dynamic code compilation")] + [RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] internal object? GetRawResult() { awaiter.GetResult(); @@ -55,7 +56,8 @@ public void UnsafeOnCompleted(Action continuation) return IsTaskWithResult(task.GetType()) ? GetDynamicResult(task) : Missing.Value; - [RequiresUnreferencedCode("Runtime binding may be incompatible with IL trimming")] + [RequiresDynamicCode("Runtime binding requires dynamic code compilation")] + [RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] static object? GetDynamicResult(Task task) { var callSite = getResultCallSite ??= CallSite>.Create(new TaskResultBinder()); @@ -70,7 +72,8 @@ static bool IsTaskWithResult(Type type) /// Gets dynamically typed task result. /// /// The result of the completed task; or if underlying task is not of type . - [RequiresUnreferencedCode("Runtime binding may be incompatible with IL trimming")] + [RequiresDynamicCode("Runtime binding requires dynamic code compilation")] + [RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] public dynamic? GetResult() => GetRawResult(); } diff --git a/src/DotNext/Threading/Tasks/Synchronization.ValueTask.cs b/src/DotNext/Threading/Tasks/Synchronization.ValueTask.cs index 80247338a4..00f6719eec 100644 --- a/src/DotNext/Threading/Tasks/Synchronization.ValueTask.cs +++ b/src/DotNext/Threading/Tasks/Synchronization.ValueTask.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using static System.Threading.Timeout; namespace DotNext.Threading.Tasks; @@ -15,7 +16,7 @@ private static async ValueTask WhenAll(T tasks) { try { - await GetTask(tasks, i).ConfigureAwait(false); + await GetTask(in tasks, i).ConfigureAwait(false); } catch (Exception e) { @@ -25,8 +26,8 @@ private static async ValueTask WhenAll(T tasks) aggregator.ThrowIfNeeded(); - static ValueTask GetTask(in T tuple, int index) - => Unsafe.Add(ref Unsafe.As(ref Unsafe.AsRef(in tuple)), index); + static ref readonly ValueTask GetTask(ref readonly T tuple, int index) + => ref Unsafe.Add(ref Unsafe.As(ref Unsafe.AsRef(in tuple)), index); } /// @@ -292,4 +293,82 @@ public static ValueTask WhenAll(ValueTask task1, ValueTask task2, ValueTask task return result; } + + /// + /// Waits for the task synchronously. + /// + /// + /// In contrast to this method doesn't use wait handles. + /// + /// The task to wait. + public static void Wait(this in ValueTask task) + { + var awaiter = task.ConfigureAwait(false).GetAwaiter(); + + unsafe + { + Wait(ref awaiter, &IsCompleted); + } + + awaiter.GetResult(); + + static bool IsCompleted(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) + => awaiter.IsCompleted; + } + + /// + /// Waits for the task synchronously. + /// + /// + /// In contrast to this method doesn't use wait handles. + /// + /// The type of the task result. + /// The task to wait. + public static T Wait(this in ValueTask task) + { + var awaiter = task.ConfigureAwait(false).GetAwaiter(); + + unsafe + { + Wait(ref awaiter, &IsCompleted); + } + + return awaiter.GetResult(); + + static bool IsCompleted(ref ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter) + => awaiter.IsCompleted; + } + + private static unsafe void Wait(ref TAwaiter awaiter, delegate* isCompleted) + where TAwaiter : struct, ICriticalNotifyCompletion + { + if (!SpinWait(ref awaiter, isCompleted)) + BlockingWait(ref awaiter, isCompleted); + + static bool SpinWait(ref TAwaiter awaiter, delegate* isCompleted) + { + bool result; + for (var spinner = new SpinWait();; spinner.SpinOnce()) + { + if ((result = isCompleted(ref awaiter)) || spinner.NextSpinWillYield) + break; + } + + return result; + } + + static void BlockingWait(ref TAwaiter awaiter, delegate* isCompleted) + { + awaiter.UnsafeOnCompleted(Thread.CurrentThread.Interrupt); + try + { + // park thread + Thread.Sleep(Infinite); + } + catch (ThreadInterruptedException) when (isCompleted(ref awaiter)) + { + // suppress exception + } + } + } } \ No newline at end of file diff --git a/src/DotNext/Threading/Tasks/Synchronization.cs b/src/DotNext/Threading/Tasks/Synchronization.cs index be2e0cc2bf..f44144f28b 100644 --- a/src/DotNext/Threading/Tasks/Synchronization.cs +++ b/src/DotNext/Threading/Tasks/Synchronization.cs @@ -76,7 +76,8 @@ public static Result GetResult(this Task task, Cancel /// The task to synchronize. /// Cancellation token. /// Task result; or returned from if is not of type . - [RequiresUnreferencedCode("Runtime binding may be incompatible with IL trimming")] + [RequiresDynamicCode("Runtime binding requires dynamic code compilation")] + [RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] public static Result GetResult(this Task task, CancellationToken token) { Result result; @@ -105,7 +106,8 @@ public static Result GetResult(this Task task, Cancel /// Synchronization timeout. /// Task result; or returned from if is not of type . /// Task is not completed. - [RequiresUnreferencedCode("Runtime binding may be incompatible with IL trimming")] + [RequiresDynamicCode("Runtime binding requires dynamic code compilation")] + [RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] public static Result GetResult(this Task task, TimeSpan timeout) { Result result; diff --git a/src/cluster/DotNext.AspNetCore.Cluster/DotNext.AspNetCore.Cluster.csproj b/src/cluster/DotNext.AspNetCore.Cluster/DotNext.AspNetCore.Cluster.csproj index 977e984dc7..eab80f861e 100644 --- a/src/cluster/DotNext.AspNetCore.Cluster/DotNext.AspNetCore.Cluster.csproj +++ b/src/cluster/DotNext.AspNetCore.Cluster/DotNext.AspNetCore.Cluster.csproj @@ -1,14 +1,14 @@  - net8.0 + net8.0 DotNext enable latest true - true + true nullablePublicOnly - 5.16.1 + 5.17.0 .NET Foundation and Contributors .NEXT Family of Libraries @@ -26,6 +26,7 @@ true + true diff --git a/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Consensus/Raft/Http/ConfigurationExtensions.cs b/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Consensus/Raft/Http/ConfigurationExtensions.cs index ced9afac3f..a8ae792a42 100644 --- a/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Consensus/Raft/Http/ConfigurationExtensions.cs +++ b/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Consensus/Raft/Http/ConfigurationExtensions.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -48,8 +47,6 @@ private static IServiceCollection AddClusterAsSingleton(this IServiceCollection /// The collection of services. /// The configuration of local cluster node. /// The modified collection of services. - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(HttpClusterMemberConfiguration))] - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "All public members preserved")] public static IServiceCollection ConfigureLocalNode(this IServiceCollection services, IConfiguration memberConfig) { Func> configCast = ServiceProviderServiceExtensions.GetRequiredService>; @@ -71,7 +68,7 @@ public static IServiceCollection ConfigureLocalNode(this IServiceCollection serv public static IServiceCollection ConfigureLocalNode(this IServiceCollection services, Action memberConfig) { Func> configCast = ServiceProviderServiceExtensions.GetRequiredService>; - return services.Configure(memberConfig).AddSingleton(configCast).AddClusterAsSingleton(); + return services.Configure(memberConfig).AddSingleton(configCast).AddClusterAsSingleton(); } private static void JoinCluster(HostBuilderContext context, IServiceCollection services) diff --git a/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/ConfigurationExtensions.cs b/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/ConfigurationExtensions.cs index 9ff22e80af..e56424a434 100644 --- a/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/ConfigurationExtensions.cs +++ b/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/ConfigurationExtensions.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -14,7 +13,6 @@ namespace DotNext.Net.Cluster.Discovery.HyParView.Http; /// Represents configuration methods that allows to embed HyParView membership /// protocol into ASP.NET Core application. /// -[CLSCompliant(false)] public static class ConfigurationExtensions { private static IServiceCollection AddPeerController(this IServiceCollection services) @@ -33,8 +31,6 @@ private static IServiceCollection AddPeerController(this IServiceCollection serv /// The collection of services. /// The configuration of local peer. /// The modified collection of services. - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "All public members preserved")] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(HttpPeerConfiguration))] public static IServiceCollection ConfigureLocalPeer(this IServiceCollection services, IConfiguration configuration) { Func> configCast = ServiceProviderServiceExtensions.GetRequiredService>; @@ -51,9 +47,9 @@ public static IServiceCollection ConfigureLocalPeer(this IServiceCollection serv public static IServiceCollection ConfigureLocalPeer(this IServiceCollection services, Action configuration) { Func> configCast = ServiceProviderServiceExtensions.GetRequiredService>; - return services.Configure(configuration).AddSingleton(configCast).AddPeerController(); + return services.Configure(configuration).AddSingleton(configCast).AddPeerController(); } - + private static void JoinMesh(HostBuilderContext context, IServiceCollection services) => services.ConfigureLocalPeer(context.Configuration); @@ -133,6 +129,7 @@ public static IHostBuilder JoinMesh(this IHostBuilder builder, ActionThe application builder. /// The delegate that can be used to provide local peer configuration. /// + [CLSCompliant(false)] public static void JoinMesh(this WebApplicationBuilder builder, Action peerConfig) => builder.Host.JoinMesh(peerConfig); @@ -155,6 +152,7 @@ public static IHostBuilder JoinMesh(this IHostBuilder builder, string configSect /// /// The application builder. /// The name of configuration section containing configuration of the local peer. + [CLSCompliant(false)] public static void JoinMesh(this WebApplicationBuilder builder, string configSection) => builder.Host.JoinMesh(configSection); @@ -166,6 +164,7 @@ private static void ConfigureHyParViewProtocolHandler(this HttpPeerController co /// /// The application builder. /// The modified application builder. + [CLSCompliant(false)] public static IApplicationBuilder UseHyParViewProtocolHandler(this IApplicationBuilder builder) { var controller = builder.ApplicationServices.GetRequiredService(); diff --git a/src/cluster/DotNext.Net.Cluster/DotNext.Net.Cluster.csproj b/src/cluster/DotNext.Net.Cluster/DotNext.Net.Cluster.csproj index 7937404b0c..8599083aba 100644 --- a/src/cluster/DotNext.Net.Cluster/DotNext.Net.Cluster.csproj +++ b/src/cluster/DotNext.Net.Cluster/DotNext.Net.Cluster.csproj @@ -1,14 +1,14 @@  - net8.0 + net8.0 DotNext latest true enable - true + true nullablePublicOnly - 5.16.1 + 5.17.0 .NET Foundation and Contributors .NEXT Family of Libraries diff --git a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/Commands/CommandInterpreter.cs b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/Commands/CommandInterpreter.cs index e2d5945f9f..b0a3eebd71 100644 --- a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/Commands/CommandInterpreter.cs +++ b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/Commands/CommandInterpreter.cs @@ -49,9 +49,8 @@ internal UnknownCommandException(int id) /// Initializes a new interpreter and discovers methods marked /// with attribute. /// - [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(CommandHandler<>))] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Func<,>))] [RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] + [RequiresDynamicCode("Runtime binding requires dynamic code compilation")] protected CommandInterpreter() { // explore command types diff --git a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/PersistentState.Internal.cs b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/PersistentState.Internal.cs index cc82927fea..e6efa8bd72 100644 --- a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/PersistentState.Internal.cs +++ b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/PersistentState.Internal.cs @@ -209,7 +209,7 @@ public void Format(Span output) } } - private sealed class VersionedFileReader(SafeFileHandle handle, long fileOffset, int bufferSize, MemoryAllocator allocator, ulong version) : FileReader(handle, fileOffset, bufferSize, allocator) + private sealed class VersionedFileReader(SafeFileHandle handle, ulong version) : FileReader(handle) { internal void VerifyVersion(ulong expected) { @@ -262,13 +262,13 @@ private protected ConcurrentStorageAccess(string fileName, int fileOffset, int b } this.fileOffset = fileOffset; - writer = new(Handle, fileOffset, bufferSize, allocator); + writer = new(Handle) { FilePosition = fileOffset, MaxBufferSize = bufferSize, Allocator = allocator }; readers = new VersionedFileReader[readersCount]; this.allocator = allocator; FileName = fileName; if (readers.Length is 1) - readers[0] = new(Handle, fileOffset, bufferSize, allocator, version); + readers[0] = new(Handle, version) { FilePosition = this.fileOffset, MaxBufferSize = bufferSize, Allocator = allocator }; autoFlush = writeMode is WriteMode.AutoFlush; } @@ -322,7 +322,12 @@ private protected FileReader GetSessionReader(int sessionId) var version = Volatile.Read(in this.version); if (result is null) { - result = new(Handle, fileOffset, writer.MaxBufferSize, allocator, version); + result = new(Handle, version) + { + FilePosition = fileOffset, + MaxBufferSize = writer.MaxBufferSize, + Allocator = allocator, + }; } else { diff --git a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/PersistentState.Partition.cs b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/PersistentState.Partition.cs index 9a6e61b55d..d6cac99d7c 100644 --- a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/PersistentState.Partition.cs +++ b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/PersistentState.Partition.cs @@ -603,7 +603,7 @@ private async ValueTask FlushAndSealAsync(CancellationToken token) { bufferTuple = (writer.WrittenBuffer, footer.Memory); await RandomAccess.WriteAsync(Handle, this, writer.FilePosition, token).ConfigureAwait(false); - writer.ClearBuffer(); + writer.Reset(); writer.FilePosition += bufferTuple.Item1.Length; bufferTuple = default; } diff --git a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Messaging/MessageHandler.cs b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Messaging/MessageHandler.cs index 579ed7df72..eefb5748c4 100644 --- a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Messaging/MessageHandler.cs +++ b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Messaging/MessageHandler.cs @@ -34,6 +34,7 @@ public partial class MessageHandler : IInputChannel /// Initializes a new typed message handler and discover all methods suitable for handling messages. /// [RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")] + [RequiresDynamicCode("Runtime binding requires dynamic code compilation")] protected MessageHandler() { // inspect message types diff --git a/src/examples/CommandLineAMI/CommandLineAMI.csproj b/src/examples/CommandLineAMI/CommandLineAMI.csproj index 46a965af1a..c88ba8b0aa 100644 --- a/src/examples/CommandLineAMI/CommandLineAMI.csproj +++ b/src/examples/CommandLineAMI/CommandLineAMI.csproj @@ -6,7 +6,9 @@ latest enable true - 5.0.0 + 5.17.0 + true + true diff --git a/src/examples/HyParViewPeer/HyParViewClientHandlerFactory.cs b/src/examples/HyParViewPeer/HyParViewClientHandlerFactory.cs index 055d9152e0..0a52ac3d5b 100644 --- a/src/examples/HyParViewPeer/HyParViewClientHandlerFactory.cs +++ b/src/examples/HyParViewPeer/HyParViewClientHandlerFactory.cs @@ -5,11 +5,11 @@ namespace HyParViewPeer; internal sealed class HyParViewClientHandlerFactory : IHttpMessageHandlerFactory { - internal static bool AllowCertificate(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) => true; + private static bool AllowCertificate(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) => true; public HttpMessageHandler CreateHandler(string name) { - var handler = new SocketsHttpHandler { ConnectTimeout = TimeSpan.FromMilliseconds(100) }; + var handler = new SocketsHttpHandler { ConnectTimeout = TimeSpan.FromMilliseconds(1000) }; handler.SslOptions.RemoteCertificateValidationCallback = AllowCertificate; return handler; } diff --git a/src/examples/HyParViewPeer/HyParViewPeer.csproj b/src/examples/HyParViewPeer/HyParViewPeer.csproj index 1a92f59471..b4e0b0e972 100644 --- a/src/examples/HyParViewPeer/HyParViewPeer.csproj +++ b/src/examples/HyParViewPeer/HyParViewPeer.csproj @@ -6,7 +6,9 @@ latest enable true - 5.11.0 + 5.17.0 + true + true diff --git a/src/examples/HyParViewPeer/Program.cs b/src/examples/HyParViewPeer/Program.cs index 1918fb68e9..ee214c4bd4 100644 --- a/src/examples/HyParViewPeer/Program.cs +++ b/src/examples/HyParViewPeer/Program.cs @@ -1,9 +1,17 @@ using System.Net; +using System.Net.Http.Headers; using System.Reflection; using System.Security.Cryptography.X509Certificates; +using System.Text; +using DotNext; +using DotNext.Net; +using DotNext.Net.Cluster.Discovery.HyParView; using DotNext.Net.Cluster.Discovery.HyParView.Http; -using Microsoft.Extensions.Logging.Console; -using SslOptions = DotNext.Net.Security.SslOptions; +using DotNext.Net.Cluster.Messaging.Gossip; +using DotNext.Net.Http; +using HyParViewPeer; +using Microsoft.AspNetCore.Connections; +using Microsoft.Extensions.Options; int port; int? contactNodePort = null; @@ -37,19 +45,36 @@ if (contactNodePort.HasValue) configuration.Add("contactNode", $"https://localhost:{contactNodePort.GetValueOrDefault()}"); -await new HostBuilder().ConfigureWebHost(webHost => +var builder = WebApplication.CreateSlimBuilder(); +builder.Configuration.AddInMemoryCollection(configuration); + +// web server +builder.WebHost.ConfigureKestrel(options => { - webHost.UseKestrel(options => - { - options.ListenLocalhost(port, static listener => listener.UseHttps(LoadCertificate())); - }) - .UseStartup(); -}) -.ConfigureLogging(static builder => builder.AddConsole().SetMinimumLevel(LogLevel.Error)) -.ConfigureAppConfiguration(builder => builder.AddInMemoryCollection(configuration)) -.JoinMesh() -.Build() -.RunAsync(); + options.ListenLocalhost(port, static listener => listener.UseHttps(LoadCertificate())); +}); + +// services +builder.Services + .AddSingleton(static sp => new RumorSpreadingManager(EndPointFormatter.UriEndPointComparer)) + .AddSingleton() + .AddSingleton(); + +// misc +builder.Logging.AddConsole().SetMinimumLevel(LogLevel.Debug); +builder.JoinMesh(); + +await using var app = builder.Build(); + +// endpoints +app.UseHyParViewProtocolHandler().UseRouting().UseEndpoints(static endpoints => +{ + endpoints.MapGet(RumorSender.RumorResource, SendRumourAsync); + endpoints.MapGet(RumorSender.NeighborsResource, PrintNeighborsAsync); + endpoints.MapPost(RumorSender.BroadcastResource, BroadcastAsync); +}); + +await app.RunAsync(); static X509Certificate2 LoadCertificate() { @@ -58,4 +83,101 @@ static X509Certificate2 LoadCertificate() rawCertificate?.CopyTo(ms); ms.Seek(0, SeekOrigin.Begin); return new X509Certificate2(ms.ToArray(), "1234"); +} + +static (Uri, RumorTimestamp) PrepareMessageId(IServiceProvider sp) +{ + var config = sp.GetRequiredService>().Value; + var manager = sp.GetRequiredService(); + return (config.LocalNode!, manager.Tick()); +} + +static Task BroadcastAsync(HttpContext context) +{ + var senderAddress = RumorSender.ParseSenderAddress(context.Request); + var senderId = RumorSender.ParseRumorId(context.Request); + + var spreadingManager = context.RequestServices.GetRequiredService(); + if (!spreadingManager.CheckOrder(new UriEndPoint(senderAddress), senderId)) + return Task.CompletedTask; + + Console.WriteLine($"Spreading rumor from {senderAddress} with sequence number = {senderId}"); + + return context.RequestServices + .GetRequiredService() + .EnqueueBroadcastAsync(controller => new RumorSender((IPeerMesh)controller, senderAddress, senderId)) + .AsTask(); +} + +static Task SendRumourAsync(HttpContext context) +{ + var (sender, id) = PrepareMessageId(context.RequestServices); + + return context.RequestServices + .GetRequiredService() + .EnqueueBroadcastAsync(controller => new RumorSender((IPeerMesh)controller, sender, id)) + .AsTask(); +} + +static Task PrintNeighborsAsync(HttpContext context) +{ + var mesh = context.RequestServices.GetRequiredService>(); + var sb = new StringBuilder(); + + foreach (var peer in mesh.Peers) + sb.AppendLine(peer.ToString()); + + return context.Response.WriteAsync(sb.ToString(), context.RequestAborted); +} + +file sealed class RumorSender : Disposable, IRumorSender +{ + internal const string SenderAddressHeader = "X-Sender-Address"; + internal const string SenderIdHeader = "X-Rumor-ID"; + + internal const string RumorResource = "/rumor"; + internal const string BroadcastResource = "/broadcast"; + internal const string NeighborsResource = "/neighbors"; + + private readonly IPeerMesh mesh; + private readonly Uri senderAddress; + private readonly RumorTimestamp senderId; + + internal RumorSender(IPeerMesh mesh, Uri sender, RumorTimestamp id) + { + this.mesh = mesh; + senderAddress = sender; + senderId = id; + } + + private async Task SendAsync(HttpPeerClient client, CancellationToken token) + { + using var request = new HttpRequestMessage(HttpMethod.Post, BroadcastResource); + AddSenderAddress(request.Headers, senderAddress); + AddRumorId(request.Headers, senderId); + using var response = await client.SendAsync(request, token); + response.EnsureSuccessStatusCode(); + } + + Task IRumorSender.SendAsync(EndPoint peer, CancellationToken token) + { + var client = mesh.TryGetPeer(peer); + return client is not null && !EndPointFormatter.UriEndPointComparer.Equals(new UriEndPoint(senderAddress), peer) + ? SendAsync(client, token) + : Task.CompletedTask; + } + + public new ValueTask DisposeAsync() => base.DisposeAsync(); + + private static void AddSenderAddress(HttpRequestHeaders headers, Uri address) + => headers.Add(SenderAddressHeader, address.ToString()); + + internal static Uri ParseSenderAddress(HttpRequest request) + => new(request.Headers[SenderAddressHeader]!, UriKind.Absolute); + + private static void AddRumorId(HttpRequestHeaders headers, in RumorTimestamp id) + => headers.Add(SenderIdHeader, id.ToString()); + + internal static RumorTimestamp ParseRumorId(HttpRequest request) + => RumorTimestamp.TryParse(request.Headers[SenderIdHeader], out var result) ? result : throw new FormatException("Invalid rumor ID"); } \ No newline at end of file diff --git a/src/examples/HyParViewPeer/Startup.cs b/src/examples/HyParViewPeer/Startup.cs deleted file mode 100644 index d50f7d7c3b..0000000000 --- a/src/examples/HyParViewPeer/Startup.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System.Net; -using System.Net.Http.Headers; -using System.Text; -using DotNext; -using DotNext.Net; -using DotNext.Net.Http; -using DotNext.Net.Cluster.Discovery.HyParView; -using DotNext.Net.Cluster.Discovery.HyParView.Http; -using DotNext.Net.Cluster.Messaging.Gossip; -using Microsoft.AspNetCore.Connections; -using Microsoft.Extensions.Options; - -namespace HyParViewPeer; - -internal sealed class Startup -{ - private const string SenderAddressHeader = "X-Sender-Address"; - private const string SenderIdHeader = "X-Rumor-ID"; - - private const string RumorResource = "/rumor"; - private const string BroadcastResource = "/broadcast"; - private const string NeighborsResource = "/neighbors"; - - private sealed class RumorSender : Disposable, IRumorSender - { - private readonly IPeerMesh mesh; - private readonly Uri senderAddress; - private readonly RumorTimestamp senderId; - - internal RumorSender(IPeerMesh mesh, Uri sender, RumorTimestamp id) - { - this.mesh = mesh; - this.senderAddress = sender; - this.senderId = id; - } - - private async Task SendAsync(HttpPeerClient client, CancellationToken token) - { - using var request = new HttpRequestMessage(HttpMethod.Post, BroadcastResource); - AddSenderAddress(request.Headers, senderAddress); - AddRumorId(request.Headers, senderId); - using var response = await client.SendAsync(request, token); - response.EnsureSuccessStatusCode(); - } - - Task IRumorSender.SendAsync(EndPoint peer, CancellationToken token) - { - var client = mesh.TryGetPeer(peer); - return client is not null && !EndPointFormatter.UriEndPointComparer.Equals(new UriEndPoint(senderAddress), peer) - ? SendAsync(client, token) - : Task.CompletedTask; - } - - public new ValueTask DisposeAsync() => base.DisposeAsync(); - - private static void AddSenderAddress(HttpRequestHeaders headers, Uri address) - => headers.Add(SenderAddressHeader, address.ToString()); - - internal static Uri ParseSenderAddress(HttpRequest request) - => new(request.Headers[SenderAddressHeader]!, UriKind.Absolute); - - private static void AddRumorId(HttpRequestHeaders headers, in RumorTimestamp id) - => headers.Add(SenderIdHeader, id.ToString()); - - internal static RumorTimestamp ParseRumorId(HttpRequest request) - => RumorTimestamp.TryParse(request.Headers[SenderIdHeader], out var result) ? result : throw new FormatException("Invalid rumor ID"); - } - - public void Configure(IApplicationBuilder app) - { - app.UseHyParViewProtocolHandler().UseRouting().UseEndpoints(static endpoints => - { - endpoints.MapGet(RumorResource, SendRumourAsync); - endpoints.MapGet(NeighborsResource, PrintNeighborsAsync); - endpoints.MapPost(BroadcastResource, BroadcastAsync); - }); - } - - private static (Uri, RumorTimestamp) PrepareMessageId(IServiceProvider sp) - { - var config = sp.GetRequiredService>().Value; - var manager = sp.GetRequiredService(); - return (config.LocalNode!, manager.Tick()); - } - - private static Task BroadcastAsync(HttpContext context) - { - var senderAddress = RumorSender.ParseSenderAddress(context.Request); - var senderId = RumorSender.ParseRumorId(context.Request); - - var spreadingManager = context.RequestServices.GetRequiredService(); - if (!spreadingManager.CheckOrder(new UriEndPoint(senderAddress), senderId)) - return Task.CompletedTask; - - Console.WriteLine($"Spreading rumor from {senderAddress} with sequence number = {senderId}"); - - return context.RequestServices - .GetRequiredService() - .EnqueueBroadcastAsync(controller => new RumorSender((IPeerMesh)controller, senderAddress, senderId)) - .AsTask(); - } - - private static Task SendRumourAsync(HttpContext context) - { - var (sender, id) = PrepareMessageId(context.RequestServices); - - return context.RequestServices - .GetRequiredService() - .EnqueueBroadcastAsync(controller => new RumorSender((IPeerMesh)controller, sender, id)) - .AsTask(); - } - - private static Task PrintNeighborsAsync(HttpContext context) - { - var mesh = context.RequestServices.GetRequiredService>(); - var sb = new StringBuilder(); - - foreach (var peer in mesh.Peers) - sb.AppendLine(peer.ToString()); - - return context.Response.WriteAsync(sb.ToString(), context.RequestAborted); - } - - public void ConfigureServices(IServiceCollection services) - { - services.AddSingleton(static sp => new RumorSpreadingManager(EndPointFormatter.UriEndPointComparer)) - .AddSingleton() - .AddSingleton() - .AddOptions() - .AddRouting(); - } -} \ No newline at end of file diff --git a/src/examples/RaftNode/Program.cs b/src/examples/RaftNode/Program.cs index c9088971fc..279cba158a 100644 --- a/src/examples/RaftNode/Program.cs +++ b/src/examples/RaftNode/Program.cs @@ -1,10 +1,13 @@ using System.Net; using System.Reflection; using System.Security.Cryptography.X509Certificates; +using DotNext; using DotNext.Net.Cluster.Consensus.Raft; using DotNext.Net.Cluster.Consensus.Raft.Http; using DotNext.Net.Cluster.Consensus.Raft.Membership; +using Microsoft.AspNetCore.Connections; using RaftNode; +using static System.Globalization.CultureInfo; using SslOptions = DotNext.Net.Security.SslOptions; switch (args.LongLength) @@ -21,34 +24,80 @@ break; } -static Task UseAspNetCoreHost(int port, string? persistentStorage = null) +static async Task UseAspNetCoreHost(int port, string? persistentStorage = null) { var configuration = new Dictionary - { - {"partitioning", "false"}, - {"lowerElectionTimeout", "150" }, - {"upperElectionTimeout", "300" }, - {"requestTimeout", "00:10:00"}, - {"publicEndPoint", $"https://localhost:{port}"}, - {"coldStart", "false"}, - {"requestJournal:memoryLimit", "5" }, - {"requestJournal:expiration", "00:01:00" } - }; - if (!string.IsNullOrEmpty(persistentStorage)) - configuration[SimplePersistentState.LogLocation] = persistentStorage; - return new HostBuilder().ConfigureWebHost(webHost => { - webHost.UseKestrel(options => + { "partitioning", "false" }, + { "lowerElectionTimeout", "150" }, + { "upperElectionTimeout", "300" }, + { "requestTimeout", "00:10:00" }, + { "publicEndPoint", $"https://localhost:{port}" }, + { "coldStart", "false" }, + { "requestJournal:memoryLimit", "5" }, + { "requestJournal:expiration", "00:01:00" }, + { SimplePersistentState.LogLocation, persistentStorage }, + }; + + var builder = WebApplication.CreateSlimBuilder(); + builder.Configuration.AddInMemoryCollection(configuration); + builder.WebHost.ConfigureKestrel(options => + { + options.ListenLocalhost(port, static listener => listener.UseHttps(LoadCertificate())); + }); + + builder.Services + .UseInMemoryConfigurationStorage(AddClusterMembers) + .ConfigureCluster() + .AddSingleton() + .AddOptions() + .AddRouting(); + + if (!string.IsNullOrWhiteSpace(persistentStorage)) + { + builder.Services.UsePersistenceEngine, SimplePersistentState>() + .AddSingleton(); + } + + ConfigureLogging(builder.Logging); + builder.JoinCluster(); + + await using var app = builder.Build(); + + const string leaderResource = "/leader"; + const string valueResource = "/value"; + app.UseConsensusProtocolHandler() + .RedirectToLeader(leaderResource) + .UseRouting() + .UseEndpoints(static endpoints => { - options.ListenLocalhost(port, static listener => listener.UseHttps(LoadCertificate())); - }) - .UseStartup(); - }) - .ConfigureLogging(ConfigureLogging) - .ConfigureAppConfiguration(builder => builder.AddInMemoryCollection(configuration)) - .JoinCluster() - .Build() - .RunAsync(); + endpoints.MapGet(leaderResource, RedirectToLeaderAsync); + endpoints.MapGet(valueResource, GetValueAsync); + }); + await app.RunAsync(); + + static Task RedirectToLeaderAsync(HttpContext context) + { + var cluster = context.RequestServices.GetRequiredService(); + return context.Response.WriteAsync($"Leader address is {cluster.Leader?.EndPoint}. Current address is {context.Connection.LocalIpAddress}:{context.Connection.LocalPort}", context.RequestAborted); + } + + static async Task GetValueAsync(HttpContext context) + { + var cluster = context.RequestServices.GetRequiredService(); + var provider = context.RequestServices.GetRequiredService>(); + + await cluster.ApplyReadBarrierAsync(context.RequestAborted); + await context.Response.WriteAsync(provider.Invoke().ToString(InvariantCulture), context.RequestAborted); + } + + // NOTE: this way of adding members to the cluster is not recommended in production code + static void AddClusterMembers(ICollection members) + { + members.Add(new UriEndPoint(new("https://localhost:3262", UriKind.Absolute))); + members.Add(new UriEndPoint(new("https://localhost:3263", UriKind.Absolute))); + members.Add(new UriEndPoint(new("https://localhost:3264", UriKind.Absolute))); + } } static async Task UseConfiguration(RaftCluster.NodeConfiguration config, string? persistentStorage) diff --git a/src/examples/RaftNode/RaftNode.csproj b/src/examples/RaftNode/RaftNode.csproj index c2d636332a..cb008c885b 100644 --- a/src/examples/RaftNode/RaftNode.csproj +++ b/src/examples/RaftNode/RaftNode.csproj @@ -6,7 +6,9 @@ latest enable true - 5.11.0 + 5.17.0 + true + true diff --git a/src/examples/RaftNode/Startup.cs b/src/examples/RaftNode/Startup.cs deleted file mode 100644 index 6406301a7c..0000000000 --- a/src/examples/RaftNode/Startup.cs +++ /dev/null @@ -1,64 +0,0 @@ -using DotNext; -using DotNext.Net.Cluster.Consensus.Raft; -using DotNext.Net.Cluster.Consensus.Raft.Http; -using Microsoft.AspNetCore.Connections; -using static System.Globalization.CultureInfo; - -namespace RaftNode; - -internal sealed class Startup(IConfiguration configuration) -{ - private static Task RedirectToLeaderAsync(HttpContext context) - { - var cluster = context.RequestServices.GetRequiredService(); - return context.Response.WriteAsync($"Leader address is {cluster.Leader?.EndPoint}. Current address is {context.Connection.LocalIpAddress}:{context.Connection.LocalPort}", context.RequestAborted); - } - - private static async Task GetValueAsync(HttpContext context) - { - var cluster = context.RequestServices.GetRequiredService(); - var provider = context.RequestServices.GetRequiredService>(); - - await cluster.ApplyReadBarrierAsync(context.RequestAborted); - await context.Response.WriteAsync(provider.Invoke().ToString(InvariantCulture), context.RequestAborted); - } - - public void Configure(IApplicationBuilder app) - { - const string LeaderResource = "/leader"; - const string ValueResource = "/value"; - - app.UseConsensusProtocolHandler() - .RedirectToLeader(LeaderResource) - .UseRouting() - .UseEndpoints(static endpoints => - { - endpoints.MapGet(LeaderResource, RedirectToLeaderAsync); - endpoints.MapGet(ValueResource, GetValueAsync); - }); - } - - public void ConfigureServices(IServiceCollection services) - { - services.UseInMemoryConfigurationStorage(AddClusterMembers) - .ConfigureCluster() - .AddSingleton() - .AddOptions() - .AddRouting(); - - var path = configuration[SimplePersistentState.LogLocation]; - if (!string.IsNullOrWhiteSpace(path)) - { - services.UsePersistenceEngine, SimplePersistentState>() - .AddSingleton(); - } - } - - // NOTE: this way of adding members to the cluster is not recommended in production code - private static void AddClusterMembers(ICollection members) - { - members.Add(new UriEndPoint(new("https://localhost:3262", UriKind.Absolute))); - members.Add(new UriEndPoint(new("https://localhost:3263", UriKind.Absolute))); - members.Add(new UriEndPoint(new("https://localhost:3264", UriKind.Absolute))); - } -} \ No newline at end of file