From faa56e396f71003b608780062b2c8a8cb205d1a3 Mon Sep 17 00:00:00 2001 From: sakno Date: Tue, 19 Mar 2024 20:37:50 +0200 Subject: [PATCH] Release 5.3.0 --- CHANGELOG.md | 22 +++++ README.md | 27 +++--- .../Concurrent/IndexPoolBenchmark.cs | 42 +++++++++ src/DotNext.IO/DotNext.IO.csproj | 2 +- .../DotNext.Metaprogramming.csproj | 2 +- src/DotNext.Tests/IO/StreamSourceTests.cs | 55 ++++++++++++ .../Collections/Concurrent/IndexPool.cs | 22 ++--- .../DotNext.Threading.csproj | 2 +- src/DotNext.Unsafe/DotNext.Unsafe.csproj | 2 +- src/DotNext/DotNext.csproj | 2 +- src/DotNext/IO/ReadOnlyStream.cs | 2 - src/DotNext/IO/SharedReadOnlyMemoryStream.cs | 88 +++++++++++++++++++ src/DotNext/IO/StreamSource.cs | 12 +++ src/DotNext/Numerics/Number.cs | 4 +- .../DotNext.AspNetCore.Cluster.csproj | 2 +- .../DotNext.Net.Cluster.csproj | 2 +- .../Raft/PersistentState.NodeState.cs | 5 +- .../Consensus/Raft/PersistentState.Options.cs | 1 - 18 files changed, 255 insertions(+), 39 deletions(-) create mode 100644 src/DotNext.Benchmarks/Collections/Concurrent/IndexPoolBenchmark.cs create mode 100644 src/DotNext/IO/SharedReadOnlyMemoryStream.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index b56d4a9fe9..1d5d0e9c41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,28 @@ Release Notes ==== +# 03-19-2024 +DotNext 5.3.0 +* Added `StreamSource.AsSharedStream` extension method that allows to obtain read-only stream over memory block which position is local for each consuming async flow or thread. In other words, the stream can be shared between async flows for independent reads. + +DotNext.Metaprogramming 5.3.0 +* Updated dependencies + +DotNext.Unsafe 5.3.0 +* Updated dependencies + +DotNext.Threading 5.3.0 +* Improved performance of `IndexPool.Take` method + +DotNext.IO 5.3.0 +* Updated dependencies + +DotNext.Net.Cluster 5.3.0 +* Smallish performance improvements of WAL + +DotNext.AspNetCore.Cluster 5.3.0 +* Smallish performance improvements of WAL + # 03-08-2024 DotNext 5.2.0 * Added `Number.IsPrime` static method that allows to check whether the specified number is a prime number diff --git a/README.md b/README.md index 6e06f5798b..85ebf97e51 100644 --- a/README.md +++ b/README.md @@ -44,31 +44,28 @@ 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: 03-08-2024 +Release Date: 03-19-2024 -DotNext 5.2.0 -* Added `Number.IsPrime` static method that allows to check whether the specified number is a prime number -* Fixed AOT compatibility issues +DotNext 5.3.0 +* Added `StreamSource.AsSharedStream` extension method that allows to obtain read-only stream over memory block which position is local for each consuming async flow or thread. In other words, the stream can be shared between async flows for independent reads. -DotNext.Metaprogramming 5.2.0 +DotNext.Metaprogramming 5.3.0 * Updated dependencies -DotNext.Unsafe 5.2.0 +DotNext.Unsafe 5.3.0 * Updated dependencies -DotNext.Threading 5.2.0 -* Added specialized `IndexPool` data type that can be useful for implementing fast object pools +DotNext.Threading 5.3.0 +* Improved performance of `IndexPool.Take` method -DotNext.IO 5.2.0 +DotNext.IO 5.3.0 * Updated dependencies -DotNext.Net.Cluster 5.2.0 -* Fixed [226](https://github.com/dotnet/dotNext/issues/226) -* Fixed [221](https://github.com/dotnet/dotNext/issues/221) +DotNext.Net.Cluster 5.3.0 +* Smallish performance improvements of WAL -DotNext.AspNetCore.Cluster 5.2.0 -* Fixed [226](https://github.com/dotnet/dotNext/issues/226) -* Fixed [221](https://github.com/dotnet/dotNext/issues/221) +DotNext.AspNetCore.Cluster 5.3.0 +* Smallish performance improvements of WAL Changelog for previous versions located [here](./CHANGELOG.md). diff --git a/src/DotNext.Benchmarks/Collections/Concurrent/IndexPoolBenchmark.cs b/src/DotNext.Benchmarks/Collections/Concurrent/IndexPoolBenchmark.cs new file mode 100644 index 0000000000..9279a03a3c --- /dev/null +++ b/src/DotNext.Benchmarks/Collections/Concurrent/IndexPoolBenchmark.cs @@ -0,0 +1,42 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Order; +using System.Collections.Concurrent; + +namespace DotNext.Collections.Concurrent; + +[SimpleJob(runStrategy: RunStrategy.Throughput, launchCount: 1)] +[Orderer(SummaryOrderPolicy.Declared)] +public class IndexPoolBenchmark +{ + private readonly ConcurrentBag bag = new(CreateObjects()); + private readonly object[] objects = CreateObjects(); + private IndexPool pool = new(); + + [Benchmark(Description = "IndexPool Take/Return", Baseline = true)] + public int IndexPoolTakeReturn() + { + pool.TryTake(out var index); + var hashCode = objects[index].GetHashCode(); + + pool.Return(index); + return hashCode; + } + + [Benchmark(Description = "ConcurrentBag Take/Return")] + public int ConcurrentBagTakeReturn() + { + bag.TryTake(out var obj); + var hashCode = obj.GetHashCode(); + + bag.Add(obj); + return hashCode; + } + + private static object[] CreateObjects() + { + var result = new object[64]; + Span.Initialize(result); + return result; + } +} \ No newline at end of file diff --git a/src/DotNext.IO/DotNext.IO.csproj b/src/DotNext.IO/DotNext.IO.csproj index 829ed28143..c659aa49a9 100644 --- a/src/DotNext.IO/DotNext.IO.csproj +++ b/src/DotNext.IO/DotNext.IO.csproj @@ -11,7 +11,7 @@ .NET Foundation and Contributors .NEXT Family of Libraries - 5.2.0 + 5.3.0 DotNext.IO MIT diff --git a/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj b/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj index c8b5b134a6..0485bff8cd 100644 --- a/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj +++ b/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj @@ -8,7 +8,7 @@ true false nullablePublicOnly - 5.2.0 + 5.3.0 .NET Foundation .NEXT Family of Libraries diff --git a/src/DotNext.Tests/IO/StreamSourceTests.cs b/src/DotNext.Tests/IO/StreamSourceTests.cs index 488a12f724..dac0619884 100644 --- a/src/DotNext.Tests/IO/StreamSourceTests.cs +++ b/src/DotNext.Tests/IO/StreamSourceTests.cs @@ -1,8 +1,10 @@ using System.Buffers; +using System.Runtime.CompilerServices; namespace DotNext.IO; using Buffers; +using Runtime.CompilerServices; public sealed class StreamSourceTests : Test { @@ -602,4 +604,57 @@ static ValueTask WriteToBuffer(ReadOnlyMemory block, ArrayBufferWriter))] + static async Task ReadStreamAsync(Stream source) + { + source.Position = 0L; + await Task.Yield(); + + var result = new byte[source.Length]; + await source.ReadExactlyAsync(result); + return result; + } + } + + [Fact] + public static void SharedStreamConcurrentRead() + { + byte[] expected = [10, 20, 30, 40, 50, 60]; + + using var stream = StreamSource.AsSharedStream(new(expected)); + + var thread1 = new Thread(ReadStream); + var thread2 = new Thread(ReadStream); + + thread1.Start(); + thread2.Start(); + + thread1.Join(); + thread2.Join(); + + void ReadStream() + { + stream.Position = 0L; + + var actual = new byte[stream.Length]; + stream.ReadExactly(actual); + Equal(expected, actual); + } + } } \ No newline at end of file diff --git a/src/DotNext.Threading/Collections/Concurrent/IndexPool.cs b/src/DotNext.Threading/Collections/Concurrent/IndexPool.cs index b8e3e5bccc..ae36d1d07b 100644 --- a/src/DotNext.Threading/Collections/Concurrent/IndexPool.cs +++ b/src/DotNext.Threading/Collections/Concurrent/IndexPool.cs @@ -73,19 +73,21 @@ public readonly bool TryPeek(out int result) /// public bool TryTake(out int result) { - ulong current, newValue = Volatile.Read(in bitmask); - do - { - result = BitOperations.TrailingZeroCount(current = newValue); + return TryTake(ref bitmask, maxValue, out result); - if (result > maxValue) - return false; + static bool TryTake(ref ulong bitmask, int maxValue, out int result) + { + var current = Volatile.Read(in bitmask); + for (ulong newValue; ; current = newValue) + { + newValue = current & (current - 1UL); // Reset lowest set bit, the same as BLSR instruction + newValue = Interlocked.CompareExchange(ref bitmask, newValue, current); + if (newValue == current) + break; + } - newValue = current ^ (1UL << result); + return (result = BitOperations.TrailingZeroCount(current)) <= maxValue; } - while ((newValue = Interlocked.CompareExchange(ref bitmask, newValue, current)) != current); - - return true; } /// diff --git a/src/DotNext.Threading/DotNext.Threading.csproj b/src/DotNext.Threading/DotNext.Threading.csproj index 45872754cb..22ac829b71 100644 --- a/src/DotNext.Threading/DotNext.Threading.csproj +++ b/src/DotNext.Threading/DotNext.Threading.csproj @@ -7,7 +7,7 @@ true true nullablePublicOnly - 5.2.0 + 5.3.0 .NET Foundation and Contributors .NEXT Family of Libraries diff --git a/src/DotNext.Unsafe/DotNext.Unsafe.csproj b/src/DotNext.Unsafe/DotNext.Unsafe.csproj index abad20ae6b..8078881daa 100644 --- a/src/DotNext.Unsafe/DotNext.Unsafe.csproj +++ b/src/DotNext.Unsafe/DotNext.Unsafe.csproj @@ -7,7 +7,7 @@ enable true true - 5.2.0 + 5.3.0 nullablePublicOnly .NET Foundation and Contributors diff --git a/src/DotNext/DotNext.csproj b/src/DotNext/DotNext.csproj index 7f376f80ab..aa6661cd4d 100644 --- a/src/DotNext/DotNext.csproj +++ b/src/DotNext/DotNext.csproj @@ -11,7 +11,7 @@ .NET Foundation and Contributors .NEXT Family of Libraries - 5.2.0 + 5.3.0 DotNext MIT diff --git a/src/DotNext/IO/ReadOnlyStream.cs b/src/DotNext/IO/ReadOnlyStream.cs index f2516933d6..d0fb462c0a 100644 --- a/src/DotNext/IO/ReadOnlyStream.cs +++ b/src/DotNext/IO/ReadOnlyStream.cs @@ -2,8 +2,6 @@ namespace DotNext.IO; -using Buffers; - internal abstract class ReadOnlyStream : Stream { public sealed override bool CanRead => true; diff --git a/src/DotNext/IO/SharedReadOnlyMemoryStream.cs b/src/DotNext/IO/SharedReadOnlyMemoryStream.cs new file mode 100644 index 0000000000..77f475fe87 --- /dev/null +++ b/src/DotNext/IO/SharedReadOnlyMemoryStream.cs @@ -0,0 +1,88 @@ +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace DotNext.IO; + +using static Buffers.Memory; + +internal sealed class SharedReadOnlyMemoryStream(ReadOnlySequence sequence) : ReadOnlyStream +{ + // don't use BoxedValue due to limitations of AsyncLocal + private readonly AsyncLocal> position = new(); + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private SequencePosition LocalPosition + { + get => position.Value?.Value ?? sequence.Start; + set => (position.Value ??= new()).Value = value; + } + + private ReadOnlySequence GetRemainingSequence(out SequencePosition start) + => sequence.Slice(start = LocalPosition); + + public override bool CanSeek => true; + + public override long Length => sequence.Length; + + public override long Position + { + get => sequence.GetOffset(LocalPosition); + set + { + ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)value, (ulong)sequence.Length, nameof(value)); + + LocalPosition = sequence.GetPosition(value); + } + } + + public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken token) + { + ValidateCopyToArguments(destination, bufferSize); + + foreach (var segment in GetRemainingSequence(out _)) + await destination.WriteAsync(segment, token).ConfigureAwait(false); + + LocalPosition = sequence.End; + } + + public override void CopyTo(Stream destination, int bufferSize) + { + ValidateCopyToArguments(destination, bufferSize); + + foreach (var segment in GetRemainingSequence(out _)) + destination.Write(segment.Span); + + LocalPosition = sequence.End; + } + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override int Read(Span buffer) + { + GetRemainingSequence(out var startPos).CopyTo(buffer, out var writtenCount); + LocalPosition = sequence.GetPosition(writtenCount, startPos); + return writtenCount; + } + + public override long Seek(long offset, SeekOrigin origin) + { + var newPosition = origin switch + { + SeekOrigin.Begin => offset, + SeekOrigin.Current => sequence.GetOffset(LocalPosition) + offset, + SeekOrigin.End => sequence.Length + offset, + _ => throw new ArgumentOutOfRangeException(nameof(origin)) + }; + + if (newPosition < 0L) + throw new IOException(); + + ArgumentOutOfRangeException.ThrowIfGreaterThan(newPosition, sequence.Length, nameof(offset)); + + LocalPosition = sequence.GetPosition(newPosition); + return newPosition; + } + + public override string ToString() => sequence.ToString(); +} \ No newline at end of file diff --git a/src/DotNext/IO/StreamSource.cs b/src/DotNext/IO/StreamSource.cs index e6f9f50bb1..88a72c3325 100644 --- a/src/DotNext/IO/StreamSource.cs +++ b/src/DotNext/IO/StreamSource.cs @@ -43,6 +43,18 @@ public static Stream AsStream(this ReadOnlySequence sequence) public static Stream AsStream(this ReadOnlyMemory memory) => AsStream(new ReadOnlySequence(memory)); + /// + /// Gets read-only stream that can be shared across async flows for independent reads. + /// + /// + /// You need to set a position explicitly before using stream for each parallel async flow. + /// is not supported to avoid different views of the same stream. + /// + /// The sequence of bytes. + /// The stream over sequence of bytes. + public static Stream AsSharedStream(this ReadOnlySequence sequence) + => sequence.IsEmpty ? Stream.Null : new SharedReadOnlyMemoryStream(sequence); + /// /// Returns writable synchronous stream. /// diff --git a/src/DotNext/Numerics/Number.cs b/src/DotNext/Numerics/Number.cs index e17f345b0a..893f3b0524 100644 --- a/src/DotNext/Numerics/Number.cs +++ b/src/DotNext/Numerics/Number.cs @@ -131,7 +131,7 @@ public static bool IsPrime(T value) return value == two; - // https://math.stackexchange.com/questions/2469446/what-is-a-fast-algorithm-for-finding-the-integer-square-root + // https://math.stackexchange.com/questions/2469446/what-is-a-fast-algorithm-for-finding-the-integer-square-root/4674078#4674078 static T Sqrt(T value) { var log2x = T.Log2(value) - T.One; @@ -221,4 +221,4 @@ static bool TryGetFromTable(ReadOnlySpan cachedPrimes, T value, out T result) return success; } } -} \ No newline at end of file +} diff --git a/src/cluster/DotNext.AspNetCore.Cluster/DotNext.AspNetCore.Cluster.csproj b/src/cluster/DotNext.AspNetCore.Cluster/DotNext.AspNetCore.Cluster.csproj index c781a53562..c917e5a68c 100644 --- a/src/cluster/DotNext.AspNetCore.Cluster/DotNext.AspNetCore.Cluster.csproj +++ b/src/cluster/DotNext.AspNetCore.Cluster/DotNext.AspNetCore.Cluster.csproj @@ -8,7 +8,7 @@ true true nullablePublicOnly - 5.2.0 + 5.3.0 .NET Foundation and Contributors .NEXT Family of Libraries diff --git a/src/cluster/DotNext.Net.Cluster/DotNext.Net.Cluster.csproj b/src/cluster/DotNext.Net.Cluster/DotNext.Net.Cluster.csproj index c776fc7cf7..851ebb76a2 100644 --- a/src/cluster/DotNext.Net.Cluster/DotNext.Net.Cluster.csproj +++ b/src/cluster/DotNext.Net.Cluster/DotNext.Net.Cluster.csproj @@ -8,7 +8,7 @@ enable true nullablePublicOnly - 5.2.0 + 5.3.0 .NET Foundation and Contributors .NEXT Family of Libraries diff --git a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/PersistentState.NodeState.cs b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/PersistentState.NodeState.cs index c325fb6698..6e38760e2a 100644 --- a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/PersistentState.NodeState.cs +++ b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/PersistentState.NodeState.cs @@ -86,8 +86,9 @@ private NodeState(string fileName, MemoryAllocator allocator, bool integri // reopen handle in asynchronous mode handle.Dispose(); - const FileOptions dontSkipBuffer = FileOptions.Asynchronous | FileOptions.SequentialScan; - const FileOptions skipBuffer = FileOptions.Asynchronous | FileOptions.WriteThrough | FileOptions.SequentialScan; + // FileOptions.RandomAccess to keep the whole file cached in the page cache + const FileOptions dontSkipBuffer = FileOptions.Asynchronous | FileOptions.RandomAccess; + const FileOptions skipBuffer = FileOptions.Asynchronous | FileOptions.WriteThrough | FileOptions.RandomAccess; handle = File.OpenHandle(fileName, FileMode.Open, FileAccess.ReadWrite, FileShare.Read, writeThrough ? skipBuffer : dontSkipBuffer); // restore state diff --git a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/PersistentState.Options.cs b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/PersistentState.Options.cs index 57962bd9c9..8529105cbf 100644 --- a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/PersistentState.Options.cs +++ b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/PersistentState.Options.cs @@ -5,7 +5,6 @@ namespace DotNext.Net.Cluster.Consensus.Raft; using Buffers; -using IO.Log; public partial class PersistentState {