From 06ed95ffbd26a10e56b54f7a95743aafaed9f4db Mon Sep 17 00:00:00 2001 From: sakno Date: Fri, 30 Aug 2024 22:44:48 +0300 Subject: [PATCH] Release 5.13.0 --- CHANGELOG.md | 29 + README.md | 32 +- src/Directory.Packages.props | 2 +- src/DotNext.IO/Buffers/SevenBitEncodedInt.cs | 2 +- src/DotNext.IO/DotNext.IO.csproj | 2 +- src/DotNext.IO/IO/SequenceReader.cs | 68 +- .../DotNext.MaintenanceServices.csproj | 2 +- ...ApplicationMaintenanceCommand.GCCommand.cs | 8 + .../AuthenticationMiddleware.cs | 2 +- .../Authorization/AuthorizationMiddleware.cs | 2 +- .../CommandLine/CommandResources.cs | 4 +- .../CommandLine/CommandResources.restext | 3 +- .../DotNext.Metaprogramming.csproj | 2 +- .../Binary/BinaryTransformationsTests.cs | 1 + .../Buffers/BufferWriterTests.cs | 20 +- src/DotNext.Tests/Buffers/MemoryOwnerTests.cs | 8 +- .../Specialized/InvocationListTests.cs | 16 + .../Specialized/SingletonListTests.cs | 3 +- .../Collections/Specialized/TypeMapTests.cs | 105 +- src/DotNext.Tests/DelegateHelpersTests.cs | 45 + .../IO/AsyncBinaryReaderWriterTests.cs | 87 +- .../IO/SequenceBinaryReaderTests.cs | 32 + src/DotNext.Tests/IO/StreamSourceTests.cs | 10 +- src/DotNext.Tests/IO/TextBufferReaderTests.cs | 24 +- ...ommandLineMaintenanceInterfaceHostTests.cs | 1 + .../Net/Cluster/ClusterMemberIdTests.cs | 14 +- .../Net/EndPointFormatterTests.cs | 30 +- src/DotNext.Tests/RandomTests.cs | 16 +- .../Runtime/ValueReferenceTests.cs | 36 +- src/DotNext.Tests/SpanTests.cs | 68 +- .../Threading/AsyncEventHubTests.cs | 151 +-- .../Threading/AsyncExclusiveLockTests.cs | 21 + .../Collections/Concurrent/IndexPool.cs | 16 +- .../DotNext.Threading.csproj | 2 +- .../Threading/AsyncEventHub.DebugSupport.cs | 26 +- .../Threading/AsyncEventHub.cs | 906 +++++++++--------- .../Threading/AsyncExclusiveLock.cs | 3 +- .../Threading/QueuedSynchronizer.cs | 271 +++--- .../Tasks/ManualResetCompletionSource.cs | 25 +- .../Tasks/ValueTaskCompletionSource.T.cs | 46 +- .../Tasks/ValueTaskCompletionSource.cs | 29 +- src/DotNext.Unsafe/DotNext.Unsafe.csproj | 2 +- .../Runtime/InteropServices/Pointer.cs | 12 +- src/DotNext/BasicExtensions.cs | 1 + .../Binary/BinaryTransformations.Bitwise.cs | 10 +- .../Buffers/SparseBufferWriter.Reader.cs | 24 +- src/DotNext/Buffers/Text/Base64Decoder.cs | 2 +- .../Generic/AsyncEnumerable.Proxy.cs | 18 +- .../Generic/Collection.ConsumingEnumerable.cs | 17 +- src/DotNext/Collections/Generic/Collection.cs | 19 + .../Collections/Generic/IEnumerator.cs | 68 ++ .../ConcurrentTypeMap.Enumerator.cs | 68 +- .../Specialized/ConcurrentTypeMap.cs | 10 +- .../Specialized/IReadOnlyTypeMap.cs | 2 +- .../Collections/Specialized/ITypeMap.cs | 2 +- .../Collections/Specialized/InvocationList.cs | 56 +- .../Collections/Specialized/SingletonList.cs | 18 +- .../Specialized/TypeMap.Enumerator.cs | 69 +- .../Collections/Specialized/TypeMap.cs | 33 +- src/DotNext/DelegateHelpers.cs | 108 +++ src/DotNext/DotNext.csproj | 2 +- src/DotNext/Runtime/ValueReference.cs | 109 ++- src/DotNext/Span.cs | 148 +++ .../Tasks/Synchronization.ValueTask.cs | 24 +- .../DotNext.AspNetCore.Cluster.csproj | 2 +- .../DotNext.Net.Cluster.csproj | 2 +- 66 files changed, 1913 insertions(+), 1083 deletions(-) create mode 100644 src/DotNext/Collections/Generic/IEnumerator.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index ab5cac5cde..74e7417190 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,35 @@ Release Notes ==== +# 08-30-2024 +DotNext 5.13.0 +* Improved interoperability of `DotNext.Runtime.ValueReference` and `DotNext.Runtime.ReadOnlyValueReference` with .NEXT ecosystem +* Fixed [249](https://github.com/dotnet/dotNext/issues/249) +* Improved codegen quality for ad-hoc enumerator types + +DotNext.Metaprogramming 5.13.0 +* Updated dependencies + +DotNext.Unsafe 5.13.0 +* Updated dependencies + +DotNext.Threading 5.13.0 +* Redesigned `AsyncEventHub` to improve overall performance and reduce memory allocation +* Improved codegen quality for ad-hoc enumerator types + +DotNext.IO 5.13.0 +* Improved codegen quality for ad-hoc enumerator types + +DotNext.Net.Cluster 5.13.0 +* Updated dependencies + +DotNext.AspNetCore.Cluster 5.13.0 +* Updated dependencies + +DotNext.MaintenanceServices 0.4.0 +* Added [gc refresh-mem-limit](https://learn.microsoft.com/en-us/dotnet/api/system.gc.refreshmemorylimit) maintenance command +* Updated dependencies + # 08-19-2024 DotNext 5.12.1 * Added support of static field references to `DotNext.Runtime.ValueReference` data type diff --git a/README.md b/README.md index 74efc726de..f536b72a5f 100644 --- a/README.md +++ b/README.md @@ -44,13 +44,35 @@ 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: 08-19-2024 +Release Date: 08-30-2024 -DotNext 5.12.1 -* Added support of static field references to `DotNext.Runtime.ValueReference` data type +DotNext 5.13.0 +* Improved interoperability of `DotNext.Runtime.ValueReference` and `DotNext.Runtime.ReadOnlyValueReference` with .NEXT ecosystem +* Fixed [249](https://github.com/dotnet/dotNext/issues/249) +* Improved codegen quality for ad-hoc enumerator types -DotNext.Threading 5.12.1 -* Smallish performance improvements of `RandomAccessCache` class +DotNext.Metaprogramming 5.13.0 +* Updated dependencies + +DotNext.Unsafe 5.13.0 +* Updated dependencies + +DotNext.Threading 5.13.0 +* Redesigned `AsyncEventHub` to improve overall performance and reduce memory allocation +* Improved codegen quality for ad-hoc enumerator types + +DotNext.IO 5.13.0 +* Improved codegen quality for ad-hoc enumerator types + +DotNext.Net.Cluster 5.13.0 +* Updated dependencies + +DotNext.AspNetCore.Cluster 5.13.0 +* Updated dependencies + +DotNext.MaintenanceServices 0.4.0 +* Added [gc refresh-mem-limit](https://learn.microsoft.com/en-us/dotnet/api/system.gc.refreshmemorylimit) maintenance command +* Updated dependencies Changelog for previous versions located [here](./CHANGELOG.md). diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index eeb6ba5a57..f0b9c57e25 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -33,7 +33,7 @@ - + diff --git a/src/DotNext.IO/Buffers/SevenBitEncodedInt.cs b/src/DotNext.IO/Buffers/SevenBitEncodedInt.cs index 25053182fa..14d0aebce6 100644 --- a/src/DotNext.IO/Buffers/SevenBitEncodedInt.cs +++ b/src/DotNext.IO/Buffers/SevenBitEncodedInt.cs @@ -68,7 +68,7 @@ public bool MoveNext() } [StructLayout(LayoutKind.Auto)] - internal struct Reader() : IBufferReader, ISupplier + internal struct Reader : IBufferReader, ISupplier { private SevenBitEncodedInt value; private bool completed; diff --git a/src/DotNext.IO/DotNext.IO.csproj b/src/DotNext.IO/DotNext.IO.csproj index 26f283b247..97d13b0149 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.12.0 + 5.13.0 DotNext.IO MIT diff --git a/src/DotNext.IO/IO/SequenceReader.cs b/src/DotNext.IO/IO/SequenceReader.cs index c04a5e7f22..2ff2be3fdd 100644 --- a/src/DotNext.IO/IO/SequenceReader.cs +++ b/src/DotNext.IO/IO/SequenceReader.cs @@ -1,4 +1,5 @@ using System.Buffers; +using System.Collections; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -12,6 +13,7 @@ namespace DotNext.IO; using Buffers; using Buffers.Binary; +using Collections.Generic; using static Pipelines.PipeExtensions; using DecodingContext = Text.DecodingContext; @@ -89,7 +91,7 @@ public T Read() /// The integer type. /// The integer value. /// The underlying source doesn't contain necessary amount of bytes to decode the value. - public unsafe T ReadLittleEndian() + public T ReadLittleEndian() where T : notnull, IBinaryInteger { var type = typeof(T); @@ -111,7 +113,7 @@ public unsafe T ReadLittleEndian() /// The integer type. /// The integer value. /// The underlying source doesn't contain necessary amount of bytes to decode the value. - public unsafe T ReadBigEndian() + public T ReadBigEndian() where T : notnull, IBinaryInteger { var type = typeof(T); @@ -215,9 +217,12 @@ public bool TryRead(int maxLength, out ReadOnlyMemory chunk) chunk = remaining.First.TrimLength(maxLength); position = remaining.GetPosition(chunk.Length); } + else + { + chunk = default; + } - chunk = default; - return false; + return !chunk.IsEmpty; } /// @@ -379,18 +384,15 @@ private unsafe TResult Parse(TArg arg, delegate*.MaxSize) { - if (length <= Parsing256Reader.MaxSize) - { - var reader = new Parsing256Reader(arg, parser, length); - return Read>(ref reader); - } - else - { - var reader = new ParsingReader(arg, parser, length); - return Read>(ref reader); - } + var reader = new Parsing256Reader(arg, parser, length); + return Read>(ref reader); + } + else + { + var reader = new ParsingReader(arg, parser, length); + return Read>(ref reader); } } @@ -744,8 +746,9 @@ ValueTask> IAsyncBinaryReader.DecodeAsync(DecodingContext cont } /// - IAsyncEnumerable> IAsyncBinaryReader.DecodeAsync(DecodingContext context, LengthFormat lengthFormat, Memory buffer, CancellationToken token) - => Decode(context, lengthFormat, buffer).AsAsyncEnumerable(token); + IAsyncEnumerable> IAsyncBinaryReader.DecodeAsync(DecodingContext context, LengthFormat lengthFormat, Memory buffer, + CancellationToken token) + => Decode(context, lengthFormat, buffer); /// ValueTask IAsyncBinaryReader.CopyToAsync(Stream destination, long? count, CancellationToken token) @@ -839,7 +842,7 @@ readonly bool IAsyncBinaryReader.TryGetRemainingBytesCount(out long count) /// Represents decoding enumerable. /// [StructLayout(LayoutKind.Auto)] - public readonly struct DecodingEnumerable + public readonly struct DecodingEnumerable : IEnumerable>, IAsyncEnumerable> { private readonly ReadOnlySequence bytes; private readonly DecodingContext context; @@ -859,23 +862,24 @@ internal DecodingEnumerable(ReadOnlySequence bytes, in DecodingContext con /// /// The enumerator over decoded chunks of characters. public Enumerator GetEnumerator() => new(in bytes, in context, buffer); + + /// + IEnumerator> IEnumerable>.GetEnumerator() + => GetEnumerator().ToClassicEnumerator>(); -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - internal async IAsyncEnumerable> AsAsyncEnumerable([EnumeratorCancellation] CancellationToken token) -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - { - foreach (var chunk in this) - { - token.ThrowIfCancellationRequested(); - yield return chunk; - } - } + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator>(); + + /// + IAsyncEnumerator> IAsyncEnumerable>.GetAsyncEnumerator(CancellationToken token) + => GetEnumerator().ToAsyncEnumerator>(token); /// /// Represents enumerator over decoded characters. /// [StructLayout(LayoutKind.Auto)] - public struct Enumerator + public struct Enumerator : IEnumerator> { private readonly ReadOnlySequence bytes; private readonly Decoder decoder; @@ -899,7 +903,7 @@ internal Enumerator(in ReadOnlySequence bytes, in DecodingContext context, /// /// Decodes the next chunk of bytes. /// - /// if decoding is successfull; if nothing to decode. + /// if decoding is successful; if nothing to decode. public bool MoveNext() => (charsWritten = GetChars(in bytes, ref position, decoder, buffer.Span)) > 0; } @@ -941,7 +945,7 @@ public ref struct Enumerator private readonly ReadOnlySequence bytes; private readonly Decoder decoder; private readonly Span buffer; - private ref SequencePosition position; + private readonly ref SequencePosition position; private int charsWritten; internal Enumerator(scoped in ReadOnlySequence bytes, ref SequencePosition position, scoped in DecodingContext context, Span buffer) @@ -960,7 +964,7 @@ internal Enumerator(scoped in ReadOnlySequence bytes, ref SequencePosition /// /// Decodes the next chunk of bytes. /// - /// if decoding is successfull; if nothing to decode. + /// if decoding is successful; if nothing to decode. public bool MoveNext() => (charsWritten = GetChars(in bytes, ref position, decoder, buffer)) > 0; } diff --git a/src/DotNext.MaintenanceServices/DotNext.MaintenanceServices.csproj b/src/DotNext.MaintenanceServices/DotNext.MaintenanceServices.csproj index fcd66665a0..00e3fced30 100644 --- a/src/DotNext.MaintenanceServices/DotNext.MaintenanceServices.csproj +++ b/src/DotNext.MaintenanceServices/DotNext.MaintenanceServices.csproj @@ -11,7 +11,7 @@ .NET Foundation and Contributors .NEXT Family of Libraries - 0.3.0 + 0.4.0 DotNext.MaintenanceServices MIT diff --git a/src/DotNext.MaintenanceServices/Maintenance/CommandLine/ApplicationMaintenanceCommand.GCCommand.cs b/src/DotNext.MaintenanceServices/Maintenance/CommandLine/ApplicationMaintenanceCommand.GCCommand.cs index 13f84f743c..7cc61cdf21 100644 --- a/src/DotNext.MaintenanceServices/Maintenance/CommandLine/ApplicationMaintenanceCommand.GCCommand.cs +++ b/src/DotNext.MaintenanceServices/Maintenance/CommandLine/ApplicationMaintenanceCommand.GCCommand.cs @@ -76,6 +76,13 @@ static GCLargeObjectHeapCompactionMode ParseMode(ArgumentResult result) } } + private static Command RefreshMemoryLimitsCommand() + { + var command = new ApplicationMaintenanceCommand("refresh-mem-limit", CommandResources.GCRefreshMemoryLimit); + command.SetHandler(GC.RefreshMemoryLimit); + return command; + } + /// /// Creates a command that allows to force Garbage Collection. /// @@ -85,6 +92,7 @@ public static ApplicationMaintenanceCommand GCCommand() var command = new ApplicationMaintenanceCommand("gc", CommandResources.GCCommandDescription); command.AddCommand(GCCollectCommand()); command.AddCommand(LohCompactionModeCommand()); + command.AddCommand(RefreshMemoryLimitsCommand()); return command; } } \ No newline at end of file diff --git a/src/DotNext.MaintenanceServices/Maintenance/CommandLine/Authentication/AuthenticationMiddleware.cs b/src/DotNext.MaintenanceServices/Maintenance/CommandLine/Authentication/AuthenticationMiddleware.cs index 0fee943e97..898f63d25b 100644 --- a/src/DotNext.MaintenanceServices/Maintenance/CommandLine/Authentication/AuthenticationMiddleware.cs +++ b/src/DotNext.MaintenanceServices/Maintenance/CommandLine/Authentication/AuthenticationMiddleware.cs @@ -44,7 +44,7 @@ internal static Task SetDefaultPrincipal(InvocationContext context, Func (string)Resources.Get(); - internal static string AccessDenined => (string)Resources.Get(); + internal static string AccessDenied => (string)Resources.Get(); internal static string LoginOptionName => (string)Resources.Get(); @@ -42,6 +42,8 @@ internal static string GCCollectCommandInvalidGenerationArg(string? generation) internal static string GCLohModeCommandInvalidModeArg(string? mode) => Resources.Get().Format(mode); + internal static string GCRefreshMemoryLimit => (string)Resources.Get(); + internal static string InteractiveCommandDescription => (string)Resources.Get(); internal static string ExitCommandDescription => (string)Resources.Get(); diff --git a/src/DotNext.MaintenanceServices/Maintenance/CommandLine/CommandResources.restext b/src/DotNext.MaintenanceServices/Maintenance/CommandLine/CommandResources.restext index 9c8c641646..519ecb7f8b 100644 --- a/src/DotNext.MaintenanceServices/Maintenance/CommandLine/CommandResources.restext +++ b/src/DotNext.MaintenanceServices/Maintenance/CommandLine/CommandResources.restext @@ -1,6 +1,6 @@ WelcomeMessage = Welcome to {0} Maintenance Interface! Type -h or --help to get help CommandTimeoutOccurred=Command has timed out. Please try again -AccessDenined=Access denied +AccessDenied=Access denied GCCommandDescription=Provides interaction with GC GCCollectCommandDescription=Forces Garbage Collection GCCollectCommandGenerationArgDescription=The number of the oldest generation to be garbage collected @@ -10,6 +10,7 @@ GCCollectCommandCompactingOptionDescription=Enables compaction of small object h GCLohModeCommandDescription=Overrides Large Object Heap compaction mode GCLohModeCommandModeArgDescription=Large Object Heap compaction mode GCLohModeCommandInvalidModeArg=Incorrect Large Object Heap compaction mode {0} +GCRefreshMemoryLimit=Instructs the Garbage Collector to reconfigure itself by detecting the various memory limits on the system InteractiveCommandDescription=Enters interactive mode ExitCommandDescription=Leaves interactive mode ProbeCommandDescription=Invokes application probe diff --git a/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj b/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj index 30c63b4c8d..52be6e9bc5 100644 --- a/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj +++ b/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj @@ -8,7 +8,7 @@ true false nullablePublicOnly - 5.12.0 + 5.13.0 .NET Foundation .NEXT Family of Libraries diff --git a/src/DotNext.Tests/Buffers/Binary/BinaryTransformationsTests.cs b/src/DotNext.Tests/Buffers/Binary/BinaryTransformationsTests.cs index c3dabc9ea0..630d0d4a41 100644 --- a/src/DotNext.Tests/Buffers/Binary/BinaryTransformationsTests.cs +++ b/src/DotNext.Tests/Buffers/Binary/BinaryTransformationsTests.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.Buffers.Binary; +using System.Collections; using System.Runtime.InteropServices; namespace DotNext.Buffers.Binary; diff --git a/src/DotNext.Tests/Buffers/BufferWriterTests.cs b/src/DotNext.Tests/Buffers/BufferWriterTests.cs index 000a3bb13b..4f4e3d592a 100644 --- a/src/DotNext.Tests/Buffers/BufferWriterTests.cs +++ b/src/DotNext.Tests/Buffers/BufferWriterTests.cs @@ -53,13 +53,13 @@ public static async Task ReadWriteBufferedStringAsync(LengthFormat lengthEnc) await ReadWriteStringUsingEncodingAsync(testString2, Encoding.UTF32, lengthEnc); } - public static IEnumerable CharWriters() + public static TheoryData> CharWriters() => new() { - yield return new object[] { new PoolingBufferWriter(MemoryPool.Shared.ToAllocator()) }; - yield return new object[] { new PoolingArrayBufferWriter() }; - yield return new object[] { new SparseBufferWriter() }; - yield return new object[] { new SparseBufferWriter(32) }; - } + new PoolingBufferWriter(MemoryPool.Shared.ToAllocator()), + new PoolingArrayBufferWriter(), + new SparseBufferWriter(), + new SparseBufferWriter(32), + }; [Theory] [MemberData(nameof(CharWriters))] @@ -183,11 +183,11 @@ public static void FormatValues() Equal("56", writer.ToString()); } - public static IEnumerable ContiguousBuffers() + public static TheoryData> ContiguousBuffers() => new() { - yield return new object[] { new PoolingBufferWriter() }; - yield return new object[] { new PoolingArrayBufferWriter() }; - } + new PoolingBufferWriter(), + new PoolingArrayBufferWriter(), + }; [Theory] [MemberData(nameof(ContiguousBuffers))] diff --git a/src/DotNext.Tests/Buffers/MemoryOwnerTests.cs b/src/DotNext.Tests/Buffers/MemoryOwnerTests.cs index 3d47b83017..7b35edf465 100644 --- a/src/DotNext.Tests/Buffers/MemoryOwnerTests.cs +++ b/src/DotNext.Tests/Buffers/MemoryOwnerTests.cs @@ -66,11 +66,11 @@ public static void WrapArray() Equal(10, array[2]); } - public static IEnumerable GetArrayAllocators() + public static TheoryData> GetArrayAllocators() => new() { - yield return new[] { Memory.GetArrayAllocator() }; - yield return new[] { Memory.GetPinnedArrayAllocator() }; - } + Memory.GetArrayAllocator(), + Memory.GetPinnedArrayAllocator(), + }; [Theory] [MemberData(nameof(GetArrayAllocators))] diff --git a/src/DotNext.Tests/Collections/Specialized/InvocationListTests.cs b/src/DotNext.Tests/Collections/Specialized/InvocationListTests.cs index e64e172845..a49062a4a3 100644 --- a/src/DotNext.Tests/Collections/Specialized/InvocationListTests.cs +++ b/src/DotNext.Tests/Collections/Specialized/InvocationListTests.cs @@ -1,3 +1,5 @@ +using System.Runtime.CompilerServices; + namespace DotNext.Collections.Specialized; public sealed class InvocationListTests : Test @@ -50,4 +52,18 @@ public static void Enumerator() Same(Predicate.Constant(true)), Same>(Predicate.Constant(false))); } + + [Fact] + public static void Combine() + { + var box = new StrongBox(); + var list = new InvocationList() + Inc + Inc; + list.Combine()?.Invoke(); + Equal(2, box.Value); + + list = default; + Null(list.Combine()); + + void Inc() => box.Value += 1; + } } \ No newline at end of file diff --git a/src/DotNext.Tests/Collections/Specialized/SingletonListTests.cs b/src/DotNext.Tests/Collections/Specialized/SingletonListTests.cs index 59642d7c2a..2d00a71d9e 100644 --- a/src/DotNext.Tests/Collections/Specialized/SingletonListTests.cs +++ b/src/DotNext.Tests/Collections/Specialized/SingletonListTests.cs @@ -35,7 +35,6 @@ public static void ListInterop() [Fact] public static void EmptyEnumerator() { - using var enumerator = new SingletonList.Enumerator(); - False(enumerator.MoveNext()); + False(new SingletonList.Enumerator().MoveNext()); } } \ No newline at end of file diff --git a/src/DotNext.Tests/Collections/Specialized/TypeMapTests.cs b/src/DotNext.Tests/Collections/Specialized/TypeMapTests.cs index b681915eff..dce82fdeb1 100644 --- a/src/DotNext.Tests/Collections/Specialized/TypeMapTests.cs +++ b/src/DotNext.Tests/Collections/Specialized/TypeMapTests.cs @@ -2,13 +2,13 @@ namespace DotNext.Collections.Specialized; public sealed class TypeMapTests : Test { - public static IEnumerable GetMaps() + public static TheoryData> GetMaps() => new() { - yield return new object[] { new TypeMap() }; - yield return new object[] { new TypeMap(1) }; - yield return new object[] { new ConcurrentTypeMap() }; - yield return new object[] { new ConcurrentTypeMap(1) }; - } + new TypeMap(), + new TypeMap(1), + new ConcurrentTypeMap(), + new ConcurrentTypeMap(1), + }; [Theory] [MemberData(nameof(GetMaps))] @@ -125,21 +125,15 @@ public static void DefaultConcurrentMapEnumerator() [Fact] public static void EmptyMapEnumerator() { - var count = 0; - foreach (ref var item in new TypeMap()) - count++; - - Equal(0, count); + Empty(new TypeMap()); + Empty(new TypeMap()); } [Fact] public static void EmptyConcurrentMapEnumerator() { - var count = 0; - foreach (var item in new ConcurrentTypeMap()) - count++; - - Equal(0, count); + Empty(new ConcurrentTypeMap()); + Empty(new ConcurrentTypeMap()); } [Fact] @@ -147,18 +141,24 @@ public static void NotEmptyMapEnumerator() { var map = new TypeMap(); map.Set(42); - - var count = 0; - foreach (ref var item in map) - { - Equal(42, item); - item = 52; - count++; - } - - Equal(1, count); - True(map.TryGetValue(out var value)); - Equal(52, value); + Equal(42, Single(map)); + } + + [Fact] + public static void NotEmptyMapEnumerator2() + { + var map = new TypeMap(); + map.Set(42); + Equal(42, Single(map)); + + using var enumerator = map.As>().GetEnumerator(); + True(enumerator.MoveNext()); + Equal(42, enumerator.Current); + False(enumerator.MoveNext()); + + enumerator.Reset(); + True(enumerator.MoveNext()); + Equal(42, enumerator.Current); } [Fact] @@ -166,24 +166,33 @@ public static void NotEmptyConcurrentMapEnumerator() { var map = new ConcurrentTypeMap(); map.Set(42); + Equal(42, Single(map)); + } + + [Fact] + public static void NotEmptyConcurrentMapEnumerator2() + { + var map = new ConcurrentTypeMap(); + map.Set(42); + Equal(42, Single(map)); - var count = 0; - foreach (var item in map) - { - Equal(42, item); - count++; - } - - Equal(1, count); + using var enumerator = map.As>().GetEnumerator(); + True(enumerator.MoveNext()); + Equal(42, enumerator.Current); + False(enumerator.MoveNext()); + + enumerator.Reset(); + True(enumerator.MoveNext()); + Equal(42, enumerator.Current); } - public static IEnumerable GetSets() + public static TheoryData GetSets() => new() { - yield return new object[] { new TypeMap() }; - yield return new object[] { new TypeMap(1) }; - yield return new object[] { new ConcurrentTypeMap() }; - yield return new object[] { new ConcurrentTypeMap(1) }; - } + new TypeMap(), + new TypeMap(1), + new ConcurrentTypeMap(), + new ConcurrentTypeMap(1), + }; [Theory] [MemberData(nameof(GetSets))] @@ -202,17 +211,17 @@ public static void SetInterfaceMethods(ITypeMap set) set.Clear(); False(set.Contains()); - set.Set(50); - True(set.Remove(out result)); + set.Set(50); + True(set.Remove(out result)); Equal(50, result); - False(set.Set(42, out _)); - True(set.TryGetValue(out result)); + False(set.Set(42, out _)); + True(set.TryGetValue(out result)); Equal(42, result); - True(set.Set(50, out var tmp)); + True(set.Set(50, out var tmp)); Equal(42, tmp); - True(set.TryGetValue(out result)); + True(set.TryGetValue(out result)); Equal(50, result); True(set.Remove()); diff --git a/src/DotNext.Tests/DelegateHelpersTests.cs b/src/DotNext.Tests/DelegateHelpersTests.cs index 2115b845d5..8c6ba50df5 100644 --- a/src/DotNext.Tests/DelegateHelpersTests.cs +++ b/src/DotNext.Tests/DelegateHelpersTests.cs @@ -1,6 +1,7 @@ using System.Globalization; using System.Linq.Expressions; using System.Reflection; +using System.Runtime.CompilerServices; namespace DotNext; @@ -498,4 +499,48 @@ public static void Identity() { Equal(42, Func.Identity().Invoke(42)); } + + [Fact] + public static void ToAsync1() + { + var func = new Action(static () => { }).ToAsync(); + True(func.Invoke(new(canceled: false)).IsCompletedSuccessfully); + True(func.Invoke(new(canceled: true)).IsCanceled); + + func = new Action(static () => throw new Exception()).ToAsync(); + True(func.Invoke(new(canceled: false)).IsFaulted); + } + + [Fact] + public static void ToAsync2() + { + var func = new Action(static _ => { }).ToAsync(); + True(func.Invoke(42, new(canceled: false)).IsCompletedSuccessfully); + True(func.Invoke(42, new(canceled: true)).IsCanceled); + + func = new Action(static _ => throw new Exception()).ToAsync(); + True(func.Invoke(42, new(canceled: false)).IsFaulted); + } + + [Fact] + public static void HideReturnValue1() + { + var box = new StrongBox(); + var action = new Func(ChangeValue).HideReturnValue(); + action.Invoke(); + Equal(42, box.Value); + + int ChangeValue() => box.Value = 42; + } + + [Fact] + public static void HideReturnValue2() + { + var box = new StrongBox(); + var action = new Func(ChangeValue).HideReturnValue(); + action.Invoke(42); + Equal(42, box.Value); + + int ChangeValue(int value) => box.Value = value; + } } \ No newline at end of file diff --git a/src/DotNext.Tests/IO/AsyncBinaryReaderWriterTests.cs b/src/DotNext.Tests/IO/AsyncBinaryReaderWriterTests.cs index 232ed5cf1f..f30b0e5e27 100644 --- a/src/DotNext.Tests/IO/AsyncBinaryReaderWriterTests.cs +++ b/src/DotNext.Tests/IO/AsyncBinaryReaderWriterTests.cs @@ -175,24 +175,23 @@ protected override void Dispose(bool disposing) public new ValueTask DisposeAsync() => base.DisposeAsync(); } - public static IEnumerable GetDataForPrimitives() + public static TheoryData GetDataForPrimitives() => new() { - yield return new object[] { new FileSource(128), Encoding.UTF8 }; - yield return new object[] { new FileSource(1024), Encoding.UTF8 }; - yield return new object[] { new StreamSource(), Encoding.UTF8 }; - yield return new object[] { new PipeSource(), Encoding.UTF8 }; - yield return new object[] { new BufferSource(), Encoding.UTF8 }; - yield return new object[] { new ReadOnlySequenceSource(), Encoding.UTF8 }; - yield return new object[] { new DefaultSource(), Encoding.UTF8 }; - - yield return new object[] { new FileSource(128), Encoding.Unicode }; - yield return new object[] { new FileSource(1024), Encoding.Unicode }; - yield return new object[] { new StreamSource(), Encoding.Unicode }; - yield return new object[] { new PipeSource(), Encoding.Unicode }; - yield return new object[] { new BufferSource(), Encoding.Unicode }; - yield return new object[] { new ReadOnlySequenceSource(), Encoding.Unicode }; - yield return new object[] { new DefaultSource(), Encoding.Unicode }; - } + { new FileSource(128), Encoding.UTF8 }, + { new FileSource(1024), Encoding.UTF8 }, + { new StreamSource(), Encoding.UTF8 }, + { new PipeSource(), Encoding.UTF8 }, + { new BufferSource(), Encoding.UTF8 }, + { new ReadOnlySequenceSource(), Encoding.UTF8 }, + { new DefaultSource(), Encoding.UTF8 }, + { new FileSource(128), Encoding.Unicode }, + { new FileSource(1024), Encoding.Unicode }, + { new StreamSource(), Encoding.Unicode }, + { new PipeSource(), Encoding.Unicode }, + { new BufferSource(), Encoding.Unicode }, + { new ReadOnlySequenceSource(), Encoding.Unicode }, + { new DefaultSource(), Encoding.Unicode } + }; [Theory] [MemberData(nameof(GetDataForPrimitives))] @@ -274,28 +273,28 @@ public static async Task WriteReadPrimitivesAsync(IAsyncBinaryReaderWriterSource } } - public static IEnumerable GetDataForStringEncoding() + public static TheoryData GetDataForStringEncoding() => new() { - yield return new object[] { new StreamSource(), Encoding.UTF8, LengthFormat.Compressed }; - yield return new object[] { new StreamSource(), Encoding.UTF8, LengthFormat.LittleEndian }; - yield return new object[] { new StreamSource(), Encoding.UTF8, LengthFormat.BigEndian }; - yield return new object[] { new StreamSource(), Encoding.UTF8, LengthFormat.LittleEndian }; - - yield return new object[] { new BufferSource(), Encoding.UTF8, LengthFormat.Compressed }; - yield return new object[] { new BufferSource(), Encoding.UTF8, LengthFormat.LittleEndian }; - yield return new object[] { new BufferSource(), Encoding.UTF8, LengthFormat.BigEndian }; - yield return new object[] { new BufferSource(), Encoding.UTF8, LengthFormat.LittleEndian }; - - yield return new object[] { new PipeSource(), Encoding.UTF8, LengthFormat.Compressed }; - yield return new object[] { new PipeSource(), Encoding.UTF8, LengthFormat.LittleEndian }; - yield return new object[] { new PipeSource(), Encoding.UTF8, LengthFormat.BigEndian }; - yield return new object[] { new PipeSource(), Encoding.UTF8, LengthFormat.LittleEndian }; - - yield return new object[] { new ReadOnlySequenceSource(), Encoding.UTF8, LengthFormat.Compressed }; - yield return new object[] { new ReadOnlySequenceSource(), Encoding.UTF8, LengthFormat.LittleEndian }; - yield return new object[] { new ReadOnlySequenceSource(), Encoding.UTF8, LengthFormat.BigEndian }; - yield return new object[] { new ReadOnlySequenceSource(), Encoding.UTF8, LengthFormat.LittleEndian }; - } + { new StreamSource(), Encoding.UTF8, LengthFormat.Compressed }, + { new StreamSource(), Encoding.UTF8, LengthFormat.LittleEndian }, + { new StreamSource(), Encoding.UTF8, LengthFormat.BigEndian }, + { new StreamSource(), Encoding.UTF8, LengthFormat.LittleEndian }, + + { new BufferSource(), Encoding.UTF8, LengthFormat.Compressed }, + { new BufferSource(), Encoding.UTF8, LengthFormat.LittleEndian }, + { new BufferSource(), Encoding.UTF8, LengthFormat.BigEndian }, + { new BufferSource(), Encoding.UTF8, LengthFormat.LittleEndian }, + + { new PipeSource(), Encoding.UTF8, LengthFormat.Compressed }, + { new PipeSource(), Encoding.UTF8, LengthFormat.LittleEndian }, + { new PipeSource(), Encoding.UTF8, LengthFormat.BigEndian }, + { new PipeSource(), Encoding.UTF8, LengthFormat.LittleEndian }, + + { new ReadOnlySequenceSource(), Encoding.UTF8, LengthFormat.Compressed }, + { new ReadOnlySequenceSource(), Encoding.UTF8, LengthFormat.LittleEndian }, + { new ReadOnlySequenceSource(), Encoding.UTF8, LengthFormat.BigEndian }, + { new ReadOnlySequenceSource(), Encoding.UTF8, LengthFormat.LittleEndian }, + }; [Theory] [MemberData(nameof(GetDataForStringEncoding))] @@ -340,13 +339,13 @@ public static async Task WriteReadString2Async(IAsyncBinaryReaderWriterSource so } } - public static IEnumerable GetSources() + public static TheoryData GetSources() => new() { - yield return new object[] { new StreamSource() }; - yield return new object[] { new PipeSource() }; - yield return new object[] { new BufferSource() }; - yield return new object[] { new DefaultSource() }; - } + new StreamSource(), + new PipeSource(), + new BufferSource(), + new DefaultSource(), + }; [Theory] [MemberData(nameof(GetSources))] diff --git a/src/DotNext.Tests/IO/SequenceBinaryReaderTests.cs b/src/DotNext.Tests/IO/SequenceBinaryReaderTests.cs index f3afb5648c..3b9df3de58 100644 --- a/src/DotNext.Tests/IO/SequenceBinaryReaderTests.cs +++ b/src/DotNext.Tests/IO/SequenceBinaryReaderTests.cs @@ -142,4 +142,36 @@ public static void EncodeDecodeUsingEnumerator(string encodingName) Equal(value, bufferWriter.WrittenSpan.ToString()); } + + [Fact] + public static void EmptyReader() + { + True(default(SequenceReader).IsEmpty); + } + + [Fact] + public static void TryRead() + { + ReadOnlyMemory expected = new byte[] { 1, 2, 3 }; + var reader = IAsyncBinaryReader.Create(expected); + False(reader.IsEmpty); + + True(reader.TryRead(out var actual)); + Equal(expected.Span, actual.Span); + True(reader.IsEmpty); + False(reader.TryRead(out actual)); + } + + [Fact] + public static void TryRead2() + { + ReadOnlyMemory expected = new byte[] { 1, 2, 3 }; + var reader = IAsyncBinaryReader.Create(expected); + False(reader.IsEmpty); + + True(reader.TryRead(4, out var actual)); + Equal(expected.Span, actual.Span); + True(reader.IsEmpty); + False(reader.TryRead(4, out actual)); + } } \ No newline at end of file diff --git a/src/DotNext.Tests/IO/StreamSourceTests.cs b/src/DotNext.Tests/IO/StreamSourceTests.cs index 2d7bcce833..d818f9466b 100644 --- a/src/DotNext.Tests/IO/StreamSourceTests.cs +++ b/src/DotNext.Tests/IO/StreamSourceTests.cs @@ -10,12 +10,12 @@ public sealed class StreamSourceTests : Test { private static readonly byte[] data = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120]; - public static IEnumerable TestBuffers() + public static TheoryData> TestBuffers() => new() { - yield return new object[] { new ReadOnlySequence(data) }; - yield return new object[] { ToReadOnlySequence(data, 2) }; - yield return new object[] { ToReadOnlySequence(data, 3) }; - } + new ReadOnlySequence(data), + ToReadOnlySequence(data, 2), + ToReadOnlySequence(data, 3), + }; [Theory] [MemberData(nameof(TestBuffers))] diff --git a/src/DotNext.Tests/IO/TextBufferReaderTests.cs b/src/DotNext.Tests/IO/TextBufferReaderTests.cs index 091e6f5830..0ff434a3c8 100644 --- a/src/DotNext.Tests/IO/TextBufferReaderTests.cs +++ b/src/DotNext.Tests/IO/TextBufferReaderTests.cs @@ -25,23 +25,23 @@ static TextBufferReaderTests() LargeData = data.ToArray(); } - public static IEnumerable SmallDataSet() + public static TheoryData> SmallDataSet() => new() { - yield return new object[] { new ReadOnlySequence(SmallData) }; - yield return new object[] { ToReadOnlySequence(SmallData, 3) }; - } + new ReadOnlySequence(SmallData), + ToReadOnlySequence(SmallData, 3), + }; - public static IEnumerable LargeDataSet() + public static TheoryData> LargeDataSet() => new() { - yield return new object[] { new ReadOnlySequence(LargeData) }; - yield return new object[] { ToReadOnlySequence(LargeData, 500) }; - } + new ReadOnlySequence(LargeData), + ToReadOnlySequence(LargeData, 500), + }; - public static IEnumerable CharDataSet() + public static TheoryData> CharDataSet() => new() { - yield return new object[] { new ReadOnlySequence(CharData) }; - yield return new object[] { ToReadOnlySequence(CharData, 10) }; - } + new ReadOnlySequence(CharData), + ToReadOnlySequence(CharData, 10), + }; [Theory] [MemberData(nameof(SmallDataSet))] diff --git a/src/DotNext.Tests/Maintenance/CommandLine/CommandLineMaintenanceInterfaceHostTests.cs b/src/DotNext.Tests/Maintenance/CommandLine/CommandLineMaintenanceInterfaceHostTests.cs index a990d104d6..c5c7ac993c 100644 --- a/src/DotNext.Tests/Maintenance/CommandLine/CommandLineMaintenanceInterfaceHostTests.cs +++ b/src/DotNext.Tests/Maintenance/CommandLine/CommandLineMaintenanceInterfaceHostTests.cs @@ -29,6 +29,7 @@ public sealed class CommandLineMaintenanceInterfaceHostTests : Test [InlineData("[superr] [supout] [prnec] probe startup 00:00:01", "[0]")] [InlineData("[superr] [supout] [prnec] probe liveness 00:00:01", "[0]")] [InlineData("gc collect 0", "")] + [InlineData("gc refresh-mem-limit", "")] [InlineData("gc loh-compaction-mode CompactOnce", "")] public static async Task DefaultCommandsAsync(string request, string response) { diff --git a/src/DotNext.Tests/Net/Cluster/ClusterMemberIdTests.cs b/src/DotNext.Tests/Net/Cluster/ClusterMemberIdTests.cs index 52fe547a25..06706281fe 100644 --- a/src/DotNext.Tests/Net/Cluster/ClusterMemberIdTests.cs +++ b/src/DotNext.Tests/Net/Cluster/ClusterMemberIdTests.cs @@ -45,14 +45,14 @@ public static void Parsing() False(ClusterMemberId.TryParse(invalidHex.AsSpan(), out _)); } - public static IEnumerable GetEndPoints() + public static TheoryData GetEndPoints() => new() { - yield return new object[] { new IPEndPoint(IPAddress.Parse("127.0.0.1"), 3262) }; - yield return new object[] { new DnsEndPoint("localhost", 3262) }; - yield return new object[] { new UnixDomainSocketEndPoint("/path") }; - yield return new object[] { new UriEndPoint(new Uri("https://localhost:3232/path", UriKind.Absolute)) }; - yield return new object[] { new UriEndPoint(new Uri("http://localhost", UriKind.Absolute)) }; - } + new IPEndPoint(IPAddress.Parse("127.0.0.1"), 3262), + new DnsEndPoint("localhost", 3262), + new UnixDomainSocketEndPoint("/path"), + new UriEndPoint(new Uri("https://localhost:3232/path", UriKind.Absolute)), + new UriEndPoint(new Uri("http://localhost", UriKind.Absolute)), + }; [Theory] [MemberData(nameof(GetEndPoints))] diff --git a/src/DotNext.Tests/Net/EndPointFormatterTests.cs b/src/DotNext.Tests/Net/EndPointFormatterTests.cs index 99c4407cd4..8b319f08e7 100644 --- a/src/DotNext.Tests/Net/EndPointFormatterTests.cs +++ b/src/DotNext.Tests/Net/EndPointFormatterTests.cs @@ -10,24 +10,28 @@ namespace DotNext.Net; public sealed class EndPointFormatterTests : Test { - public static IEnumerable GetTestEndPoints() + public static TheoryData> GetTestEndPoints() { - yield return new object[] { new DnsEndPoint("host", 3262), EqualityComparer.Default }; - yield return new object[] { new IPEndPoint(IPAddress.Parse("192.168.0.1"), 3263), EqualityComparer.Default }; - yield return new object[] { new IPEndPoint(IPAddress.Parse("2001:0db8:0000:0000:0000:8a2e:0370:7334"), 3264), EqualityComparer.Default }; - yield return new object[] { new HttpEndPoint("192.168.0.1", 3262, true, AddressFamily.InterNetwork), EqualityComparer.Default }; - yield return new object[] { new HttpEndPoint("192.168.0.1", 3262, false, AddressFamily.InterNetwork), EqualityComparer.Default }; - yield return new object[] { new HttpEndPoint("2001:0db8:0000:0000:0000:8a2e:0370:7334", 3262, true, AddressFamily.InterNetworkV6), EqualityComparer.Default }; - yield return new object[] { new HttpEndPoint("host", 3262, true), EqualityComparer.Default }; - yield return new object[] { new HttpEndPoint("host", 3262, false), EqualityComparer.Default }; - yield return new object[] { new UriEndPoint(new Uri("http://host:3262/")), EndPointFormatter.UriEndPointComparer }; - yield return new object[] { new UriEndPoint(new Uri("http://host/path/to/resource")), EndPointFormatter.UriEndPointComparer }; + 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(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("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); + rows.Add(new UriEndPoint(new Uri("http://host/path/to/resource")), EndPointFormatter.UriEndPointComparer); if (Socket.OSSupportsUnixDomainSockets) { - yield return new object[] { new UnixDomainSocketEndPoint("@abstract"), EqualityComparer.Default }; - yield return new object[] { new UnixDomainSocketEndPoint(Path.GetTempFileName()), EqualityComparer.Default }; + rows.Add(new UnixDomainSocketEndPoint("@abstract"), EqualityComparer.Default); + rows.Add(new UnixDomainSocketEndPoint(Path.GetTempFileName()), EqualityComparer.Default); } + + return rows; } [Theory] diff --git a/src/DotNext.Tests/RandomTests.cs b/src/DotNext.Tests/RandomTests.cs index 2de52ae725..c313f735bb 100644 --- a/src/DotNext.Tests/RandomTests.cs +++ b/src/DotNext.Tests/RandomTests.cs @@ -4,7 +4,7 @@ namespace DotNext; public sealed class RandomTests : Test { - private sealed class DummyRNG : RandomNumberGenerator + public sealed class DummyRNG : RandomNumberGenerator { private readonly byte[] number; @@ -22,14 +22,14 @@ public static void RandomInt() Equal(42 >> 1, rng.Next()); // because Next applies >> 1 for every randomly generated 32-bit unsigned integer } - public static IEnumerable RandomDoubleTestData() + public static TheoryData RandomDoubleTestData() => new() { - yield return new[] { new DummyRNG(0) }; - yield return new[] { new DummyRNG(int.MaxValue) }; - yield return new[] { new DummyRNG(int.MinValue) }; - yield return new[] { new DummyRNG(-1) }; - yield return new[] { new DummyRNG(1) }; - } + new DummyRNG(0), + new DummyRNG(int.MaxValue), + new DummyRNG(int.MinValue), + new DummyRNG(-1), + new DummyRNG(1), + }; [Theory] [MemberData(nameof(RandomDoubleTestData))] diff --git a/src/DotNext.Tests/Runtime/ValueReferenceTests.cs b/src/DotNext.Tests/Runtime/ValueReferenceTests.cs index 3a1732c9a2..c95891fe9b 100644 --- a/src/DotNext.Tests/Runtime/ValueReferenceTests.cs +++ b/src/DotNext.Tests/Runtime/ValueReferenceTests.cs @@ -8,7 +8,7 @@ public sealed class ValueReferenceTests : Test [Fact] public static void MutableFieldRef() { - var obj = new MyClass() { AnotherField = string.Empty }; + var obj = new MyClass { AnotherField = string.Empty }; var reference = new ValueReference(obj, ref obj.Field); obj.Field = 20; @@ -22,7 +22,7 @@ public static void MutableFieldRef() [Fact] public static void ImmutableFieldRef() { - var obj = new MyClass() { AnotherField = string.Empty }; + var obj = new MyClass { AnotherField = string.Empty }; var reference = new ReadOnlyValueReference(obj, in obj.Field); obj.Field = 20; @@ -35,7 +35,7 @@ public static void ImmutableFieldRef() [Fact] public static void MutableToImmutableRef() { - var obj = new MyClass() { AnotherField = string.Empty }; + var obj = new MyClass { AnotherField = string.Empty }; var reference = new ValueReference(obj, ref obj.Field); ReadOnlyValueReference roReference = reference; @@ -49,7 +49,7 @@ public static void MutableToImmutableRef() [Fact] public static void MutableRefEquality() { - var obj = new MyClass() { AnotherField = string.Empty }; + var obj = new MyClass { AnotherField = string.Empty }; var reference1 = new ValueReference(obj, ref obj.Field); var reference2 = new ValueReference(obj, ref obj.Field); @@ -59,7 +59,7 @@ public static void MutableRefEquality() [Fact] public static void ImmutableRefEquality() { - var obj = new MyClass() { AnotherField = string.Empty }; + var obj = new MyClass { AnotherField = string.Empty }; var reference1 = new ReadOnlyValueReference(obj, in obj.Field); var reference2 = new ReadOnlyValueReference(obj, in obj.Field); @@ -82,23 +82,28 @@ public static void ReferenceToArray() [Fact] public static void MutableEmptyRef() { - var reference = default(ValueReference); + var reference = default(ValueReference); True(reference.IsEmpty); Null(reference.ToString()); - Span span = reference; + Span span = reference; True(span.IsEmpty); + + Throws((Func)reference); + Throws(((Action)reference).Bind(string.Empty)); } [Fact] public static void ImmutableEmptyRef() { - var reference = default(ReadOnlyValueReference); + var reference = default(ReadOnlyValueReference); True(reference.IsEmpty); Null(reference.ToString()); - ReadOnlySpan span = reference; + ReadOnlySpan span = reference; True(span.IsEmpty); + + Throws((Func)reference); } [Fact] @@ -107,10 +112,18 @@ public static void AnonymousValue() var reference = new ValueReference(42); Equal(42, reference.Value); + ((Action)reference).Invoke(52); + Equal(52, ToFunc, int>(reference).Invoke()); + ReadOnlyValueReference roRef = reference; - Equal(42, roRef.Value); + Equal(52, roRef.Value); + Equal(52, ToFunc, int>(reference).Invoke()); } + private static Func ToFunc(TSupplier supplier) + where TSupplier : ISupplier + => supplier.ToDelegate(); + [Fact] public static void StaticObjectAccess() { @@ -124,6 +137,7 @@ public static void StaticObjectAccess() True(reference == new ValueReference(ref MyClass.StaticObject)); Same(MyClass.StaticObject, reference.Value); + Same(MyClass.StaticObject, ToFunc, string>(reference).Invoke()); } [Fact] @@ -193,6 +207,8 @@ public static void ReadOnlySpanInterop() True(Unsafe.AreSame(in reference.Value, in span[0])); } + + private record class MyClass : IResettable { internal static string StaticObject; diff --git a/src/DotNext.Tests/SpanTests.cs b/src/DotNext.Tests/SpanTests.cs index 1617f23a48..1a993477a6 100644 --- a/src/DotNext.Tests/SpanTests.cs +++ b/src/DotNext.Tests/SpanTests.cs @@ -86,15 +86,12 @@ public static void TrimByLength2() Equal(2, span[1]); } - private static string ToHexSlow(byte[] data, bool lowercased) - => string.Join(string.Empty, Array.ConvertAll(data, i => i.ToString(lowercased ? "x2" : "X2", null))); - - public static IEnumerable TestAllocators() + public static TheoryData> TestAllocators() => new() { - yield return new object[] { null }; - yield return new object[] { Memory.GetArrayAllocator() }; - yield return new object[] { ArrayPool.Shared.ToAllocator() }; - } + null, + Memory.GetArrayAllocator(), + ArrayPool.Shared.ToAllocator(), + }; [Theory] [MemberData(nameof(TestAllocators))] @@ -550,4 +547,59 @@ public static void AdvanceSpan() Equal(10, array.Advance()); Equal([20, 30], array.Advance(2)); } + + [Fact] + public static void CheckMask() + { + ReadOnlySpan value = [1, 1, 0]; + + True(value.CheckMask([0, 1, 0])); + True(value.CheckMask([1, 1, 0])); + False(value.CheckMask([0, 0, 1])); + } + + [Fact] + public static void CheckLargeMask() + { + var value = RandomBytes(1024); + var mask = value.AsSpan().ToArray(); + mask.AsSpan(0, 512).Clear(); + + True(new ReadOnlySpan(value).CheckMask(mask)); + mask[0] = (byte)~value[0]; + + False(new ReadOnlySpan(value).CheckMask(mask)); + } + + [Fact] + public static void IsBitwiseAndNonZero() + { + ReadOnlySpan value = [1, 1, 0]; + + True(value.IsBitwiseAndNonZero([0, 1, 1])); + True(value.IsBitwiseAndNonZero([1, 1, 1])); + False(value.IsBitwiseAndNonZero([0, 0, 1])); + } + + [Fact] + public static void IsBitwiseAndNonZeroLargeMask() + { + var value = RandomBytes(1024); + var mask = new byte[value.Length]; + + for (var i = 0; i < value.Length; i++) + { + var b = value[i]; + if ((b & 1) is 1) + { + mask[i] = b; + break; + } + } + + True(new ReadOnlySpan(value).IsBitwiseAndNonZero(mask)); + + Array.Clear(mask); + False(new ReadOnlySpan(value).IsBitwiseAndNonZero(mask)); + } } \ No newline at end of file diff --git a/src/DotNext.Tests/Threading/AsyncEventHubTests.cs b/src/DotNext.Tests/Threading/AsyncEventHubTests.cs index 7d4949facb..0103ecc74e 100644 --- a/src/DotNext.Tests/Threading/AsyncEventHubTests.cs +++ b/src/DotNext.Tests/Threading/AsyncEventHubTests.cs @@ -12,7 +12,7 @@ public static void InvalidCount() [Fact] public static void WaitOne() { - var hub = new AsyncEventHub(3); + using var hub = new AsyncEventHub(3); Equal(3, hub.Count); True(hub.Pulse(0)); @@ -23,107 +23,138 @@ public static void WaitOne() [Fact] public static async Task WaitAny() { - var hub = new AsyncEventHub(3); + using var hub = new AsyncEventHub(3); - int[] indexes = { 0 }; - bool[] flags = { false }; - hub.Pulse(indexes, flags); - True(flags[0]); + var flags = hub.Pulse(new AsyncEventHub.EventGroup([0])); + True(flags.Contains(0)); - Equal(0, await hub.WaitAnyAsync(DefaultTimeout)); - Equal(0, await hub.WaitAnyAsync([0, 1])); + var set = new HashSet(); + await hub.WaitAnyAsync(set, DefaultTimeout); + Equal(0, Single(set)); + + set.Clear(); + await hub.WaitAnyAsync(new AsyncEventHub.EventGroup([0, 1]), set); + Equal(0, Single(set)); } [Fact] public static async Task WaitAny2() { - var hub = new AsyncEventHub(3); - - int[] indexes = { 0 }; - bool[] flags = { false }; - hub.ResetAndPulse(indexes, flags); - True(flags[0]); + using var hub = new AsyncEventHub(3); + + var flags = hub.ResetAndPulse(new AsyncEventHub.EventGroup([0])); + True(flags.Contains(0)); - Equal(0, await hub.WaitAnyAsync()); - Equal(0, await hub.WaitAnyAsync([0, 1], DefaultTimeout)); + await hub.WaitAnyAsync(DefaultTimeout); + await hub.WaitAnyAsync(); } [Fact] public static async Task WaitAny3() { - var hub = new AsyncEventHub(3); + using var hub = new AsyncEventHub(3); - int[] indexes = { 0 }; - Equal(1, hub.ResetAndPulse(indexes)); + True(hub.ResetAndPulse(0)); - Equal(0, await hub.WaitAnyAsync()); - Equal(0, await hub.WaitAnyAsync([0, 1], DefaultTimeout)); + await hub.WaitAnyAsync(DefaultTimeout); + + var set = new HashSet(); + await hub.WaitAnyAsync(set); + Equal(0, Single(set)); } [Fact] - public static async Task WaitAny4() + public static async Task WaitAll() { - var hub = new AsyncEventHub(3); + using var hub = new AsyncEventHub(3); - True(hub.ResetAndPulse(0)); + var flags = hub.PulseAll(); + Equal(3, flags.Count); + Contains(0, flags); + Contains(1, flags); + Contains(2, flags); - Equal(0, await hub.WaitAnyAsync()); - Equal(0, await hub.WaitAnyAsync([0, 1], DefaultTimeout)); - - Equal(2, hub.Pulse([0, 1, 2])); - Equal(0, hub.Pulse([0, 1, 2])); + await hub.WaitAllAsync(new([0, 1]), DefaultTimeout); + await hub.WaitAllAsync(); + await hub.WaitAllAsync(DefaultTimeout); } [Fact] - public static async Task WaitAll() + public static void CaptureState() { - var hub = new AsyncEventHub(3); + using var hub = new AsyncEventHub(3); + Span state = stackalloc bool[hub.Count]; + state.Clear(); - Equal(3, hub.PulseAll()); + var flags = hub.CaptureState(); + Empty(flags); - await hub.WaitAllAsync([0, 1], DefaultTimeout); - await hub.WaitAllAsync(); + True(hub.Pulse(1)); + flags = hub.CaptureState(); + Equal(1, Single(flags)); } [Fact] - public static async Task WaitAll2() + public static async Task CancelPendingTasks() { - var hub = new AsyncEventHub(3); - - bool[] flags = { false, false, false }; - hub.PulseAll(flags); - True(flags[0]); - True(flags[1]); - True(flags[2]); + using var hub = new AsyncEventHub(3); + var task1 = hub.WaitOneAsync(0).AsTask(); + var task2 = hub.WaitOneAsync(1).AsTask(); - await hub.WaitAllAsync([0, 1]); - await hub.WaitAllAsync(DefaultTimeout); + hub.CancelSuspendedCallers(new(canceled: true)); + await ThrowsAsync(Func.Constant(task1)); + await ThrowsAsync(Func.Constant(task2)); } [Fact] - public static void CaptureState() + public static void ResetAndPulse() { - var hub = new AsyncEventHub(3); - Span state = stackalloc bool[hub.Count]; - state.Clear(); - - hub.CaptureState(state); - True(state.IndexOf(true) < 0); + using var hub = new AsyncEventHub(3); True(hub.Pulse(1)); - hub.CaptureState(state); - Equal(1, state.IndexOf(true)); + False(hub.ResetAndPulse(1)); + Empty(hub.ResetAndPulse(new AsyncEventHub.EventGroup([1]))); + + var flags = hub.ResetAndPulse(new AsyncEventHub.EventGroup([0, 2])); + Equal(2, flags.Count); + True(flags.Contains(0)); + True(flags.Contains(2)); + False(flags.Contains(1)); } [Fact] - public static async Task CancelPendingTasks() + public static void Pulse() { - var hub = new AsyncEventHub(3); - var task1 = hub.WaitOneAsync(0); - var task2 = hub.WaitOneAsync(1); + using var hub = new AsyncEventHub(3); + Equal(1, Single(hub.Pulse(new AsyncEventHub.EventGroup([1])))); + Empty(hub.Pulse(new AsyncEventHub.EventGroup([1]))); + } - hub.CancelSuspendedCallers(new(canceled: true)); - await ThrowsAsync(Func.Constant(task1)); - await ThrowsAsync(Func.Constant(task2)); + [Fact] + public static async Task IncorrectGroup() + { + using var hub = new AsyncEventHub(3); + var group = new AsyncEventHub.EventGroup([3]); + Throws(() => hub.ResetAndPulse(group)); + Throws(() => hub.Pulse(group)); + await ThrowsAsync(hub.WaitAllAsync(group).AsTask); + await ThrowsAsync(hub.WaitAnyAsync(group).AsTask); + await ThrowsAsync(hub.WaitAllAsync(group, DefaultTimeout).AsTask); + await ThrowsAsync(hub.WaitAnyAsync(group, DefaultTimeout).AsTask); + } + + [Fact] + public static async Task OutOfOrderWaitQueueProcessing() + { + using var hub = new AsyncEventHub(3); + var task = hub.WaitOneAsync(0); + + True(hub.Pulse(1)); + False(task.IsCompleted); + + True(hub.Pulse(0)); + True(task.IsCompleted); + + await task; } } \ No newline at end of file diff --git a/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs b/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs index b9a469e607..2393065e9c 100644 --- a/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs +++ b/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs @@ -122,6 +122,27 @@ public static async Task CaptureCallerInfo() l.Release(); await suspendedTask; } + + [Fact] + public static async Task CaptureCallerInfo2() + { + using var l = new AsyncExclusiveLock(); + Empty(l.GetSuspendedCallers()); + + l.TrackSuspendedCallers(); + await l.AcquireAsync(); + Empty(l.GetSuspendedCallers()); + + const string callerInfo = "MyThread"; + l.SetCallerInformation(callerInfo); + var suspendedTask = l.AcquireAsync(); + False(suspendedTask.IsCompleted); + NotEmpty(l.GetSuspendedCallers()); + Equal(callerInfo, l.GetSuspendedCallers().FirstOrDefault()); + + l.Release(); + await suspendedTask; + } [Fact] public static async Task LockStealing() diff --git a/src/DotNext.Threading/Collections/Concurrent/IndexPool.cs b/src/DotNext.Threading/Collections/Concurrent/IndexPool.cs index 4c492c2727..ca78d5f249 100644 --- a/src/DotNext.Threading/Collections/Concurrent/IndexPool.cs +++ b/src/DotNext.Threading/Collections/Concurrent/IndexPool.cs @@ -7,6 +7,8 @@ namespace DotNext.Collections.Concurrent; +using Generic; + /// /// Represents a pool of integer values. /// @@ -215,16 +217,18 @@ private static bool Contains(ulong bitmask, int index) public readonly Enumerator GetEnumerator() => new(Volatile.Read(in bitmask), maxValue); /// - readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator().AsClassicEnumerator(); + readonly IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator(); /// - readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator().AsClassicEnumerator(); + readonly IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator(); /// /// Represents an enumerator over available indices in the pool. /// [StructLayout(LayoutKind.Auto)] - public struct Enumerator + public struct Enumerator : IEnumerator { private readonly ulong bitmask; private readonly int maxValue; @@ -258,11 +262,5 @@ public bool MoveNext() return false; } - - internal IEnumerator AsClassicEnumerator() - { - while (MoveNext()) - yield return Current; - } } } \ No newline at end of file diff --git a/src/DotNext.Threading/DotNext.Threading.csproj b/src/DotNext.Threading/DotNext.Threading.csproj index d7c7e7476f..43bb9173f0 100644 --- a/src/DotNext.Threading/DotNext.Threading.csproj +++ b/src/DotNext.Threading/DotNext.Threading.csproj @@ -7,7 +7,7 @@ true true nullablePublicOnly - 5.12.1 + 5.13.0 .NET Foundation and Contributors .NEXT Family of Libraries diff --git a/src/DotNext.Threading/Threading/AsyncEventHub.DebugSupport.cs b/src/DotNext.Threading/Threading/AsyncEventHub.DebugSupport.cs index 4d9f4ca043..850109bb5e 100644 --- a/src/DotNext.Threading/Threading/AsyncEventHub.DebugSupport.cs +++ b/src/DotNext.Threading/Threading/AsyncEventHub.DebugSupport.cs @@ -1,8 +1,7 @@ using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using DotNext.Numerics; namespace DotNext.Threading; @@ -17,25 +16,28 @@ private readonly struct DebugView public DebugView(AsyncEventHub hub) { - hub.CaptureState(States = new bool[hub.Count]); + var state = hub.CaptureState(); + Span flags = stackalloc bool[MaxCount]; + state.Mask.GetBits(flags); + States = flags[..hub.Count].ToArray(); } } /// /// Captures the state of the events. /// - /// - /// Each element of the buffer will be modified with the state of the event matching - /// to the index of the element. - /// - /// A buffer of states to fill. + /// A group of signaled events. [EditorBrowsable(EditorBrowsableState.Advanced)] - public void CaptureState(Span states) + public EventGroup CaptureState() { - lock (sources) + ObjectDisposedException.ThrowIf(IsDisposingOrDisposed, this); + + EventGroup result; + lock (SyncRoot) { - for (var i = 0; i < Math.Min(states.Length, Count); i++) - Unsafe.Add(ref MemoryMarshal.GetReference(states), i) = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(sources), i).Task.IsCompleted; + result = new(state); } + + return result; } } \ No newline at end of file diff --git a/src/DotNext.Threading/Threading/AsyncEventHub.cs b/src/DotNext.Threading/Threading/AsyncEventHub.cs index 944866aa85..81152a698a 100644 --- a/src/DotNext.Threading/Threading/AsyncEventHub.cs +++ b/src/DotNext.Threading/Threading/AsyncEventHub.cs @@ -1,19 +1,27 @@ +using System.Collections; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace DotNext.Threading; -using Diagnostics; -using static Tasks.Conversion; +using Collections.Generic; +using Runtime; +using Tasks; +using Tasks.Pooling; /// /// Represents a collection of asynchronous events. /// [DebuggerDisplay($"Count = {{{nameof(Count)}}}")] -public partial class AsyncEventHub : IResettable +public partial class AsyncEventHub : QueuedSynchronizer, IResettable { - private readonly TaskCompletionSource[] sources; + private static readonly int MaxCount = Unsafe.SizeOf() * 8; + + private readonly EventGroup all; + private ValueTaskPool> pool; + private UInt128 state; /// /// Initializes a new collection of asynchronous events. @@ -22,84 +30,29 @@ public partial class AsyncEventHub : IResettable /// is less than or equal to zero. public AsyncEventHub(int count) { - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count); - - sources = new TaskCompletionSource[count]; + ArgumentOutOfRangeException.ThrowIfZero(count); + ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)count, (uint)MaxCount, nameof(count)); - for (var i = 0; i < sources.Length; i++) - sources[i] = new(i, TaskCreationOptions.RunContinuationsAsynchronously); + Count = count; + pool = new(OnCompleted, count); + all = new(Count == MaxCount ? UInt128.MaxValue : GetBitMask(count) - UInt128.One); } - private static int GetIndex(Task task) + private void OnCompleted(WaitNode node) { - Debug.Assert(task.AsyncState is int); - - return (int)task.AsyncState; - } + lock (SyncRoot) + { + if (node.NeedsRemoval) + RemoveNode(node); - private static void ResetIfNeeded(ref TaskCompletionSource source) - { - if (source is { Task: { IsCompleted: true } task }) - source = new(task.AsyncState, TaskCreationOptions.RunContinuationsAsynchronously); + pool.Return(node); + } } /// /// Gets the number of events. /// - public int Count => sources.Length; - - private Task WaitOneCoreAsync(int eventIndex, TimeSpan timeout, CancellationToken token) - { - Debug.Assert((uint)eventIndex < (uint)sources.Length); - - Task result; - - var lockTaken = false; - var start = new Timestamp(); - try - { - lockTaken = Monitor.TryEnter(sources, timeout); - result = lockTaken && (timeout -= start.Elapsed) > TimeSpan.Zero - ? Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(sources), eventIndex).Task - : throw new TimeoutException(); - } - catch (Exception e) - { - result = Task.FromException(e); - } - finally - { - if (lockTaken) - Monitor.Exit(sources); - } - - return result.WaitAsync(timeout, token); - } - - private Task WaitOneCoreAsync(int eventIndex, CancellationToken token) - { - Debug.Assert((uint)eventIndex < (uint)sources.Length); - - Task result; - - var lockTaken = false; - try - { - Monitor.Enter(sources, ref lockTaken); - result = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(sources), eventIndex).Task; - } - catch (Exception e) - { - result = Task.FromException(e); - } - finally - { - if (lockTaken) - Monitor.Exit(sources); - } - - return result.WaitAsync(token); - } + public int Count { get; } /// /// Waits for the event represented by the specified index. @@ -111,12 +64,14 @@ private Task WaitOneCoreAsync(int eventIndex, CancellationToken token) /// The operation has timed out. /// The operation has been canceled. /// is invalid. - public Task WaitOneAsync(int eventIndex, TimeSpan timeout, CancellationToken token = default) + /// The object is disposed. + public ValueTask WaitOneAsync(int eventIndex, TimeSpan timeout, CancellationToken token = default) { - if ((uint)eventIndex >= (uint)sources.Length) - return Task.FromException(new ArgumentOutOfRangeException(nameof(eventIndex))); + if ((uint)eventIndex > (uint)Count) + return ValueTask.FromException(new ArgumentOutOfRangeException(nameof(eventIndex))); - return timeout < TimeSpan.Zero ? WaitOneCoreAsync(eventIndex, token) : WaitOneCoreAsync(eventIndex, timeout, token); + var manager = new WaitAllManager(this, eventIndex); + return AcquireSpecialAsync(ref pool, ref manager, new TimeoutAndCancellationToken(timeout, token)); } /// @@ -127,51 +82,70 @@ public Task WaitOneAsync(int eventIndex, TimeSpan timeout, CancellationToken tok /// The task representing the event. /// The operation has been canceled. /// is invalid. - public Task WaitOneAsync(int eventIndex, CancellationToken token = default) - => (uint)eventIndex >= (uint)sources.Length - ? Task.FromException(new ArgumentOutOfRangeException(nameof(eventIndex))) - : WaitOneCoreAsync(eventIndex, token); + /// The object is disposed. + public ValueTask WaitOneAsync(int eventIndex, CancellationToken token = default) + { + if ((uint)eventIndex > (uint)Count) + return ValueTask.FromException(new ArgumentOutOfRangeException(nameof(eventIndex))); + + var manager = new WaitAllManager(this, eventIndex); + return AcquireSpecialAsync(ref pool, ref manager, new CancellationTokenOnly(token)); + } /// /// Turns all events to non-signaled state. /// + /// The object is disposed. public void Reset() { - lock (sources) + ObjectDisposedException.ThrowIf(IsDisposingOrDisposed, this); + + lock (SyncRoot) { - foreach (ref var source in sources.AsSpan()) - ResetIfNeeded(ref source); + state = default; } } + private LinkedValueTaskCompletionSource? DrainWaitQueue() + { + Debug.Assert(Monitor.IsEntered(SyncRoot)); + + var detachedQueue = new LinkedValueTaskCompletionSource.LinkedList(); + + for (WaitNode? current = Unsafe.As(WaitQueueHead), next; current is not null; current = next) + { + next = Unsafe.As(current.Next); + + if (current.Matches(state) && RemoveAndSignal(current, out var resumable) && resumable) + detachedQueue.Add(current); + } + + return detachedQueue.First; + } + /// /// Turns the specified event into the signaled state and reset all other events. /// /// The index of the event. /// if the event turned into signaled state; if the event is already in signaled state. /// is invalid. + /// The object is disposed. public bool ResetAndPulse(int eventIndex) { - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)eventIndex, (uint)sources.Length, nameof(eventIndex)); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)eventIndex, (uint)Count, nameof(eventIndex)); + ObjectDisposedException.ThrowIf(IsDisposingOrDisposed, this); - var result = false; - lock (sources) + var newState = GetBitMask(eventIndex); + bool result; + LinkedValueTaskCompletionSource? suspendedCallers; + lock (SyncRoot) { - for (var i = 0; i < sources.Length; i++) - { - ref var source = ref sources[i]; - - if (i == eventIndex) - { - result = source.TrySetResult(); - } - else - { - ResetIfNeeded(ref source); - } - } + result = (state & newState) == UInt128.Zero; + state = newState; + suspendedCallers = DrainWaitQueue(); } + suspendedCallers?.Unwind(); return result; } @@ -181,408 +155,260 @@ public bool ResetAndPulse(int eventIndex) /// The index of the event. /// if the event turned into signaled state; if the event is already in signaled state. /// is invalid. + /// The object is disposed. public bool Pulse(int eventIndex) { - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)eventIndex, (uint)sources.Length, nameof(eventIndex)); - - lock (sources) - { - return Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(sources), eventIndex).TrySetResult(); - } - } - - /// - /// Turns the specified events into signaled state and reset all other events. - /// - /// A span of event indexes. - /// The number of triggered events. - public int ResetAndPulse(ReadOnlySpan eventIndexes) - { - var count = 0; + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)eventIndex, (uint)Count, nameof(eventIndex)); + ObjectDisposedException.ThrowIf(IsDisposingOrDisposed, this); - lock (sources) + var mask = GetBitMask(eventIndex); + bool result; + LinkedValueTaskCompletionSource? suspendedCallers; + lock (SyncRoot) { - for (var i = 0; i < sources.Length; i++) - { - ref var source = ref sources[i]; - - if (!eventIndexes.Contains(i)) - { - ResetIfNeeded(ref source); - } - else if (source.TrySetResult()) - { - count += 1; - } - } + result = (state & mask) == UInt128.Zero; + state |= mask; + suspendedCallers = DrainWaitQueue(); } - return count; + suspendedCallers?.Unwind(); + return result; } /// /// Turns the specified events into signaled state and reset all other events. /// - /// A span of event indexes. - /// - /// A set of event states. The value of each element will be overwritten by the method as follows: - /// if the corresponding event has been moved to the signaled state, - /// or if the event is already in signaled state. - /// - /// The length of is not equal to the length of . - public void ResetAndPulse(ReadOnlySpan eventIndexes, Span flags) + /// A group of events to be signaled. + /// A group of events set by the method. + /// + /// contains an event index that is larger than or equal to . + /// + /// The object is disposed. + public EventGroup ResetAndPulse(in EventGroup events) { - ArgumentOutOfRangeException.ThrowIfNotEqual(eventIndexes.Length, flags.Length, nameof(flags)); - - lock (sources) + ArgumentOutOfRangeException.ThrowIfGreaterThan(events.Mask, all.Mask, nameof(events)); + ObjectDisposedException.ThrowIf(IsDisposingOrDisposed, this); + + EventGroup result; + LinkedValueTaskCompletionSource? suspendedCallers; + lock (SyncRoot) { - for (var i = 0; i < sources.Length; i++) - { - ref var source = ref sources[i]; - - var index = eventIndexes.IndexOf(i); - - if (index < 0) - { - ResetIfNeeded(ref source); - } - else - { - Unsafe.Add(ref MemoryMarshal.GetReference(flags), index) = source.TrySetResult(); - } - } + result = new(events.Mask & ~state); + state = events.Mask; + suspendedCallers = DrainWaitQueue(); } + + suspendedCallers?.Unwind(); + return result; } /// /// Turns the specified events into signaled state. /// - /// A span of event indexes. - /// The number of triggered events. - public int Pulse(ReadOnlySpan eventIndexes) + /// A group of events to be signaled. + /// A group of events set by the method. + /// + /// contains an event index that is larger than or equal to . + /// + /// The object is disposed. + public EventGroup Pulse(in EventGroup events) { - var count = 0; - - if (eventIndexes.IsEmpty) - goto exit; + ArgumentOutOfRangeException.ThrowIfGreaterThan(events.Mask, all.Mask, nameof(events)); + ObjectDisposedException.ThrowIf(IsDisposingOrDisposed, this); - lock (sources) + EventGroup result; + LinkedValueTaskCompletionSource? suspendedCallers; + lock (SyncRoot) { - foreach (var index in eventIndexes) - { - if (sources[index].TrySetResult()) - count += 1; - } + result = new(events.Mask & ~state); + state |= events.Mask; + suspendedCallers = DrainWaitQueue(); } - exit: - return count; + suspendedCallers?.Unwind(); + return result; } /// - /// Turns the specified events into signaled state. + /// Turns all events into the signaled state. /// - /// A span of event indexes. - /// - /// A set of event states. The value of each element will be overwritten by the method as follows: - /// if the corresponding event has been moved to the signaled state, - /// or if the event is already in signaled state. - /// - /// The length of is not equal to the length of . - public void Pulse(ReadOnlySpan eventIndexes, Span flags) - { - ArgumentOutOfRangeException.ThrowIfNotEqual(eventIndexes.Length, flags.Length, nameof(flags)); - - if (eventIndexes.IsEmpty) - return; - - lock (sources) - { - foreach (var index in eventIndexes) - { - flags[index] = sources[index].TrySetResult(); - } - } - } + /// The object is disposed. + public EventGroup PulseAll() + => Pulse(all); /// - /// Turns all events into the signaled state. + /// Waits for any of the specified events. /// - /// The number of triggered events. - public int PulseAll() + /// A group of events to be awaited. + /// The time to wait for an event. + /// The token that can be used to cancel the operation. + /// The task representing the event. + /// + /// contains an event index that is larger than or equal to . + /// + /// The operation has timed out. + /// The operation has been canceled. + /// The object is disposed. + public ValueTask WaitAnyAsync(in EventGroup events, TimeSpan timeout, CancellationToken token = default) { - var count = 0; - - lock (sources) - { - foreach (var source in sources) - { - if (source.TrySetResult()) - count += 1; - } - } - - return count; + if (events.Mask > all.Mask) + return ValueTask.FromException(new ArgumentOutOfRangeException(nameof(events))); + + var manager = new WaitAnyManager(this, events.Mask); + return AcquireSpecialAsync(ref pool, ref manager, new TimeoutAndCancellationToken(timeout, token)); } /// - /// Turns all events into the signaled state. + /// Waits for any of the specified events. /// - /// - /// A set of event states. The value of each element will be overwritten by the method as follows: - /// if the corresponding event has been moved to the signaled state, - /// or if the event is already in signaled state. - /// - /// The length of is less than . - public void PulseAll(Span flags) - { - ArgumentOutOfRangeException.ThrowIfLessThan(flags.Length, sources.Length, nameof(flags)); - - lock (sources) - { - ref var state = ref MemoryMarshal.GetReference(flags); - - for (var i = 0; i < sources.Length; i++) - Unsafe.Add(ref state, i) = sources[i].TrySetResult(); - } - } - - private Task[] GetTasks(ReadOnlySpan eventIndexes) + /// A group of events to be awaited. + /// The token that can be used to cancel the operation. + /// The task representing the event. + /// + /// contains an event index that is larger than or equal to . + /// + /// The operation has been canceled. + /// The object is disposed. + public ValueTask WaitAnyAsync(in EventGroup events, CancellationToken token = default) { - var tasks = new Task[eventIndexes.Length]; - - var taskIndex = 0; - foreach (var i in eventIndexes) - tasks[taskIndex++] = sources[i].Task; - - return tasks; + if (events.Mask > all.Mask) + return ValueTask.FromException(new ArgumentOutOfRangeException(nameof(events))); + + var manager = new WaitAnyManager(this, events.Mask); + return AcquireSpecialAsync(ref pool, ref manager, new CancellationTokenOnly(token)); } - - private Task[] GetTasks() => Array.ConvertAll(sources, static src => src.Task); - - private Task WaitAnyCoreAsync(ReadOnlySpan eventIndexes, TimeSpan timeout, CancellationToken token) + + /// + /// Waits for any of the specified events. + /// + /// A group of events to be awaited. + /// A collection of signaled events set by the method when returned successfully. + /// The time to wait for an event. + /// The token that can be used to cancel the operation. + /// The task representing the event. + /// + /// contains an event index that is larger than or equal to . + /// + /// The operation has timed out. + /// The operation has been canceled. + /// The object is disposed. + public ValueTask WaitAnyAsync(in EventGroup events, ICollection output, TimeSpan timeout, CancellationToken token = default) { - Task result; - - var lockTaken = false; - var start = new Timestamp(); - try - { - lockTaken = Monitor.TryEnter(sources, timeout); - result = lockTaken && (timeout -= start.Elapsed) > TimeSpan.Zero - ? Task.WhenAny(GetTasks(eventIndexes)) - : throw new TimeoutException(); - } - catch (Exception e) - { - result = Task.FromException(e); - } - finally - { - if (lockTaken) - Monitor.Exit(sources); - } - - return result.WaitAsync(timeout, token).Convert(GetIndex); + if (events.Mask > all.Mask) + return ValueTask.FromException(new ArgumentOutOfRangeException(nameof(events))); + + var manager = new WaitAnyManager(this, events.Mask) { Events = output }; + return AcquireSpecialAsync(ref pool, ref manager, new TimeoutAndCancellationToken(timeout, token)); } - private Task WaitAnyCoreAsync(ReadOnlySpan eventIndexes, CancellationToken token) + /// + /// Waits for any of the specified events. + /// + /// A group of events to be awaited. + /// A collection of signaled events set by the method when returned successfully. + /// The token that can be used to cancel the operation. + /// The task representing the event. + /// + /// contains an event index that is larger than or equal to . + /// + /// The operation has been canceled. + /// The object is disposed. + public ValueTask WaitAnyAsync(in EventGroup events, ICollection output, CancellationToken token = default) { - Task result; - - var lockTaken = false; - try - { - Monitor.Enter(sources, ref lockTaken); - result = Task.WhenAny(GetTasks(eventIndexes)); - } - catch (Exception e) - { - result = Task.FromException(e); - } - finally - { - if (lockTaken) - Monitor.Exit(sources); - } - - return result.WaitAsync(token).Convert(GetIndex); + if (events.Mask > all.Mask) + return ValueTask.FromException(new ArgumentOutOfRangeException(nameof(events))); + + var manager = new WaitAnyManager(this, events.Mask) { Events = output }; + return AcquireSpecialAsync(ref pool, ref manager, new CancellationTokenOnly(token)); } /// /// Waits for any of the specified events. /// - /// A set of event indexes to wait for. /// The time to wait for an event. /// The token that can be used to cancel the operation. /// The index of the first signaled event. - /// is empty. /// The operation has timed out. /// The operation has been canceled. - public Task WaitAnyAsync(ReadOnlySpan eventIndexes, TimeSpan timeout, CancellationToken token = default) - { - if (eventIndexes.IsEmpty) - return Task.FromException(new ArgumentOutOfRangeException(nameof(eventIndexes))); - - return timeout < TimeSpan.Zero ? WaitAnyCoreAsync(eventIndexes, token) : WaitAnyCoreAsync(eventIndexes, timeout, token); - } + /// The object is disposed. + public ValueTask WaitAnyAsync(TimeSpan timeout, CancellationToken token = default) + => WaitAnyAsync(all, timeout, token); /// /// Waits for any of the specified events. /// - /// A set of event indexes to wait for. /// The token that can be used to cancel the operation. /// The index of the first signaled event. - /// is empty. /// The operation has been canceled. - public Task WaitAnyAsync(ReadOnlySpan eventIndexes, CancellationToken token = default) - => eventIndexes.IsEmpty - ? Task.FromException(new ArgumentOutOfRangeException(nameof(eventIndexes))) - : WaitAnyCoreAsync(eventIndexes, token); - + /// The object is disposed. + public ValueTask WaitAnyAsync(CancellationToken token = default) + => WaitAnyAsync(all, token); + /// /// Waits for any of the specified events. /// + /// A collection of signaled events set by the method when returned successfully. /// The time to wait for an event. /// The token that can be used to cancel the operation. /// The index of the first signaled event. /// The operation has timed out. /// The operation has been canceled. - public Task WaitAnyAsync(TimeSpan timeout, CancellationToken token = default) - { - return timeout < TimeSpan.Zero ? this.WaitAnyAsync(token) : WaitAnyAsync(); - - Task WaitAnyAsync() - { - Task result; - - var lockTaken = false; - var start = new Timestamp(); - try - { - lockTaken = Monitor.TryEnter(sources, timeout); - result = lockTaken && (timeout -= start.Elapsed) > TimeSpan.Zero - ? Task.WhenAny(GetTasks()) - : throw new TimeoutException(); - } - catch (Exception e) - { - result = Task.FromException(e); - } - finally - { - if (lockTaken) - Monitor.Exit(sources); - } - - return result.WaitAsync(timeout, token).Convert(GetIndex); - } - } + /// The object is disposed. + public ValueTask WaitAnyAsync(ICollection output, TimeSpan timeout, CancellationToken token = default) + => WaitAnyAsync(all, output, timeout, token); /// /// Waits for any of the specified events. /// + /// A collection of signaled events set by the method when returned successfully. /// The token that can be used to cancel the operation. /// The index of the first signaled event. /// The operation has been canceled. - public Task WaitAnyAsync(CancellationToken token = default) - { - Task result; - - var lockTaken = false; - try - { - Monitor.Enter(sources, ref lockTaken); - result = Task.WhenAny(GetTasks()); - } - catch (Exception e) - { - result = Task.FromException(e); - } - finally - { - if (lockTaken) - Monitor.Exit(sources); - } - - return result.WaitAsync(token).Convert(GetIndex); - } - - private Task WaitAllCoreAsync(ReadOnlySpan eventIndexes, CancellationToken token) - { - Task result; - - var lockTaken = false; - try - { - Monitor.Enter(sources, ref lockTaken); - result = Task.WhenAll(GetTasks(eventIndexes)); - } - catch (Exception e) - { - result = Task.FromException(e); - } - finally - { - if (lockTaken) - Monitor.Exit(sources); - } - - return result.WaitAsync(token); - } - - private Task WaitAllCoreAsync(ReadOnlySpan eventIndexes, TimeSpan timeout, CancellationToken token) - { - Task result; - - var lockTaken = false; - var start = new Timestamp(); - try - { - lockTaken = Monitor.TryEnter(sources, timeout); - result = lockTaken && (timeout -= start.Elapsed) > TimeSpan.Zero - ? Task.WhenAll(GetTasks(eventIndexes)) - : throw new TimeoutException(); - } - catch (Exception e) - { - result = Task.FromException(e); - } - finally - { - if (lockTaken) - Monitor.Exit(sources); - } - - return result.WaitAsync(timeout, token); - } + /// The object is disposed. + public ValueTask WaitAnyAsync(ICollection output, CancellationToken token = default) + => WaitAnyAsync(all, output, token); /// /// Waits for all events. /// - /// The indexes of the events. + /// A group of events to be awaited. /// The time to wait for the events. /// The token that can be used to cancel the operation. - /// A task that represents the completion of all of the specified events. + /// A task that represents the completion of all the specified events. + /// + /// contains an event index that is larger than or equal to . + /// /// The operation has timed out. /// The operation has been canceled. - public Task WaitAllAsync(ReadOnlySpan eventIndexes, TimeSpan timeout, CancellationToken token = default) + /// The object is disposed. + public ValueTask WaitAllAsync(in EventGroup events, TimeSpan timeout, CancellationToken token = default) { - if (eventIndexes.IsEmpty) - return Task.CompletedTask; - - return timeout < TimeSpan.Zero ? WaitAllCoreAsync(eventIndexes, token) : WaitAllCoreAsync(eventIndexes, timeout, token); + if (events.Mask > all.Mask) + return ValueTask.FromException(new ArgumentOutOfRangeException(nameof(events))); + + var manager = new WaitAllManager(this, events.Mask); + return AcquireSpecialAsync(ref pool, ref manager, new TimeoutAndCancellationToken(timeout, token)); } /// /// Waits for all events. /// - /// The indexes of the events. + /// A group of events to be awaited. /// The token that can be used to cancel the operation. /// A task that represents the completion of all the specified events. + /// + /// contains an event index that is larger than or equal to . + /// /// The operation has been canceled. - public Task WaitAllAsync(ReadOnlySpan eventIndexes, CancellationToken token = default) - => eventIndexes.IsEmpty ? Task.CompletedTask : WaitAllCoreAsync(eventIndexes, token); + /// The object is disposed. + public ValueTask WaitAllAsync(in EventGroup events, CancellationToken token = default) + { + if (events.Mask > all.Mask) + return ValueTask.FromException(new ArgumentOutOfRangeException(nameof(events))); + + var manager = new WaitAllManager(this, events.Mask); + return AcquireSpecialAsync(ref pool, ref manager, new CancellationTokenOnly(token)); + } /// /// Waits for all events. @@ -592,80 +418,244 @@ public Task WaitAllAsync(ReadOnlySpan eventIndexes, CancellationToken token /// A task that represents the completion of all the specified events. /// The operation has timed out. /// The operation has been canceled. - public Task WaitAllAsync(TimeSpan timeout, CancellationToken token = default) - { - return timeout < TimeSpan.Zero ? this.WaitAllAsync(token) : WaitAllAsync(); + /// The object is disposed. + public ValueTask WaitAllAsync(TimeSpan timeout, CancellationToken token = default) + => WaitAllAsync(all, timeout, token); - Task WaitAllAsync() + /// + /// Waits for all events. + /// + /// The token that can be used to cancel the operation. + /// A task that represents the completion of all the specified events. + /// The operation has been canceled. + /// The object is disposed. + public ValueTask WaitAllAsync(CancellationToken token = default) + => WaitAllAsync(all, token); + + private static UInt128 GetBitMask(int index) => UInt128.One << index; + + private static void FillIndices(UInt128 events, ICollection indices) + { + for (var enumerator = new EventGroup.Enumerator(events); enumerator.MoveNext();) { - Task result; + indices.Add(enumerator.Current); + } + } + + /// + /// Represents a group of events. + /// + /// + /// It's better to cache a set of necessary event groups rather than create them on the fly + /// due to performance reasons. + /// + [StructLayout(LayoutKind.Auto)] + public readonly record struct EventGroup : IReadOnlyCollection + { + internal readonly UInt128 Mask; + + internal EventGroup(UInt128 mask) => Mask = mask; - var lockTaken = false; - var start = new Timestamp(); - try + /// + /// Initializes a new group of events. + /// + /// Indices of the events. + /// has at least one negative index. + public EventGroup(ReadOnlySpan indices) + { + foreach (var index in indices) { - lockTaken = Monitor.TryEnter(sources, timeout); - result = lockTaken && (timeout -= start.Elapsed) > TimeSpan.Zero - ? Task.WhenAll(GetTasks()) - : throw new TimeoutException(); + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(indices)); + + Mask |= GetBitMask(index); } - catch (Exception e) + } + + /// + /// Gets a number of events in this group. + /// + public int Count => int.CreateTruncating(UInt128.PopCount(Mask)); + + /// + /// Checks whether the specified event is in this group. + /// + /// The index of the event. + /// if the event with index is in this group; otherwise, . + public bool Contains(int index) + { + ArgumentOutOfRangeException.ThrowIfNegative(index); + + return (Mask & GetBitMask(index)) != UInt128.Zero; + } + + /// + /// Gets an enumerator over indices in this group. + /// + /// An enumerator over indices. + public Enumerator GetEnumerator() => new(Mask); + + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator(); + + /// + /// Represents an enumerator over indices. + /// + [StructLayout(LayoutKind.Auto)] + public struct Enumerator : IEnumerator + { + private UInt128 state; + + internal Enumerator(in UInt128 state) => this.state = state; + + /// + /// Gets the current index. + /// + public int Current { - result = Task.FromException(e); + readonly get; + private set; } - finally + + /// + public bool MoveNext() { - if (lockTaken) - Monitor.Exit(sources); - } + if (state == UInt128.Zero) + return false; - return result.WaitAsync(timeout, token); + var index = Current = int.CreateTruncating(UInt128.TrailingZeroCount(state)); + state ^= GetBitMask(index); + return true; + } } } - /// - /// Waits for all events. - /// - /// The token that can be used to cancel the operation. - /// A task that represents the completion of all the specified events. - /// The operation has been canceled. - public Task WaitAllAsync(CancellationToken token = default) + // TODO: Move to ref struct in .NET 10 + [StructLayout(LayoutKind.Auto)] + private readonly struct WaitAllManager : ILockManager, IConsumer { - Task result; + private readonly ReadOnlyValueReference state; + private readonly UInt128 mask; - var lockTaken = false; - try + internal WaitAllManager(AsyncEventHub stateHolder, int eventIndex) { - Monitor.Enter(sources, ref lockTaken); - result = Task.WhenAll(GetTasks()); + Debug.Assert(stateHolder is not null); + + mask = GetBitMask(eventIndex); + state = new(stateHolder, in stateHolder.state); } - catch (Exception e) + + internal WaitAllManager(AsyncEventHub stateHolder, in UInt128 mask) { - result = Task.FromException(e); + Debug.Assert(stateHolder is not null); + + this.mask = mask; + state = new(stateHolder, in stateHolder.state); } - finally + + void IConsumer.Invoke(WaitNode node) => node.WaitAll(mask); + + bool ILockManager.IsLockAllowed => (state.Value & mask) == mask; + + void ILockManager.AcquireLock() { - if (lockTaken) - Monitor.Exit(sources); + // no need to reset events } - return result.WaitAsync(token); + static bool ILockManager.RequiresEmptyQueue => false; } + + [StructLayout(LayoutKind.Auto)] + private readonly struct WaitAnyManager : ILockManager, IConsumer + { + private readonly ReadOnlyValueReference state; + private readonly UInt128 mask; - /// - /// Cancels all suspended callers. - /// - /// The token in canceled state. - /// is not in canceled state. - public void CancelSuspendedCallers(CancellationToken token) + internal WaitAnyManager(AsyncEventHub stateHolder, in UInt128 mask) + { + Debug.Assert(stateHolder is not null); + + this.mask = mask; + state = new(stateHolder, in stateHolder.state); + } + + [DisallowNull] + internal ICollection? Events + { + get; + init; + } + + void IConsumer.Invoke(WaitNode node) => node.WaitAny(mask, Events); + + bool ILockManager.IsLockAllowed => (state.Value & mask) != UInt128.Zero; + + void ILockManager.AcquireLock() + { + if (Events is { } collection) + FillIndices(state.Value & mask, collection); + } + + static bool ILockManager.RequiresEmptyQueue => false; + } + + private new sealed class WaitNode : QueuedSynchronizer.WaitNode, IPooledManualResetCompletionSource> { - if (!token.IsCancellationRequested) - throw new ArgumentOutOfRangeException(nameof(token)); + private Action? callback; + private UInt128 mask; + private bool waitAll; + private ICollection? events; + + internal void WaitAll(in UInt128 mask) + { + waitAll = true; + this.mask = mask; + } + + internal void WaitAny(in UInt128 mask, ICollection? events) + { + waitAll = false; + this.mask = mask; + this.events = events; + } + + protected override void AfterConsumed() => callback?.Invoke(this); + + protected override void CleanUp() + { + events = null; + base.CleanUp(); + } + + internal bool Matches(UInt128 state) + { + var result = state & mask; + + if (waitAll) + { + if (result == mask) + return true; + } + else if (result != UInt128.Zero) + { + if (events is not null) + FillIndices(result, events); + + return true; + } + + return false; + } - lock (sources) + Action? IPooledManualResetCompletionSource>.OnConsumed { - foreach (var source in sources) - source.TrySetCanceled(token); + get => callback; + set => callback = value; } } } \ No newline at end of file diff --git a/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs b/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs index bd2b8401c7..76effb6af9 100644 --- a/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs +++ b/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace DotNext.Threading; @@ -20,7 +19,7 @@ private struct LockManager : ILockManager internal readonly bool Value => state; - internal readonly bool VolatileRead() => Volatile.Read(ref Unsafe.AsRef(in state)); + internal readonly bool VolatileRead() => Volatile.Read(in state); public readonly bool IsLockAllowed => !state; diff --git a/src/DotNext.Threading/Threading/QueuedSynchronizer.cs b/src/DotNext.Threading/Threading/QueuedSynchronizer.cs index e5c0739b6b..f5c44e185b 100644 --- a/src/DotNext.Threading/Threading/QueuedSynchronizer.cs +++ b/src/DotNext.Threading/Threading/QueuedSynchronizer.cs @@ -63,10 +63,9 @@ public TagList MeasurementTags } private protected bool RemoveAndSignal(LinkedValueTaskCompletionSource node, out bool resumable) - { - RemoveNode(node); - return node.TrySetResult(Sentinel.Instance, completionToken: null, result: true, out resumable); - } + => RemoveNode(node) + ? node.TrySetResult(Sentinel.Instance, completionToken: null, result: true, out resumable) + : resumable = false; /// /// Enables capturing information about suspended callers in DEBUG configuration. @@ -97,6 +96,8 @@ public void SetCallerInformation(object information) callerInfo.Value = information; } + private protected object? CaptureCallerInformation() => callerInfo?.Capture(); + private IReadOnlyList GetSuspendedCallersCore() { List list; @@ -138,46 +139,51 @@ private protected void EnqueueNode(WaitNode node) SuspendedCallersMeter.Add(1, measurementTags); } - private protected TNode EnqueueNode(ref ValueTaskPool> pool, WaitNodeFlags flags) + private TNode EnqueueNode(ref ValueTaskPool> pool, TInitializer initializer) where TNode : WaitNode, IPooledManualResetCompletionSource>, new() - where TLockManager : struct, ILockManager + where TInitializer : struct, INodeInitializer { Debug.Assert(Monitor.IsEntered(SyncRoot)); var node = pool.Get(); - TLockManager.InitializeNode(node); - node.Initialize(this, flags); + initializer.Invoke(node); + node.Initialize(CaptureCallerInformation(), initializer.Flags); EnqueueNode(node); return node; } + private protected TNode EnqueueNode(ref ValueTaskPool> pool, WaitNodeFlags flags) + where TNode : WaitNode, IPooledManualResetCompletionSource>, new() + where TLockManager : struct, ILockManager + => EnqueueNode>(ref pool, new(flags)); + private protected bool TryAcquire(ref TLockManager manager) where TLockManager : struct, ILockManager { Debug.Assert(Monitor.IsEntered(SyncRoot)); - if (waitQueue.First is null && manager.IsLockAllowed) - { - manager.AcquireLock(); - return true; - } + if (TLockManager.RequiresEmptyQueue && WaitQueueHead is not null || !manager.IsLockAllowed) + return false; - return false; + manager.AcquireLock(); + return true; } - - private protected ValueTask AcquireAsync(ref ValueTaskPool> pool, ref TLockManager manager, TOptions options) - where TNode : WaitNode, IPooledManualResetCompletionSource>, new() - where TLockManager : struct, ILockManager + + private T AcquireAsync(ref ValueTaskPool> pool, ref TLockManager manager, TInitializer initializer, TOptions options) + where T : struct, IEquatable + where TNode : WaitNode, IValueTaskFactory, IPooledManualResetCompletionSource>, new() + where TInitializer : struct, INodeInitializer + where TLockManager : struct, ILockManager where TOptions : struct, IAcquisitionOptions { - ValueTask task; + T task; switch (options.Timeout.Ticks) { case Timeout.InfiniteTicks: goto default; case < 0L or > Timeout.MaxTimeoutParameterTicks: - task = ValueTask.FromException(new ArgumentOutOfRangeException("timeout")); + task = TNode.FromException(new ArgumentOutOfRangeException("timeout")); break; case 0L: // attempt to acquire synchronously LinkedValueTaskCompletionSource? interruptedCallers; @@ -185,7 +191,7 @@ private protected ValueTask AcquireAsync(ref Valu { if (IsDisposingOrDisposed) { - task = new(DisposedTask); + task = TNode.FromException(CreateObjectDisposedException()); break; } @@ -194,8 +200,8 @@ private protected ValueTask AcquireAsync(ref Valu : null; task = TryAcquire(ref manager) - ? ValueTask.CompletedTask - : ValueTask.FromException(new TimeoutException()); + ? TNode.SuccessfulTask + : TNode.TimedOutTask; } interruptedCallers?.Unwind(); @@ -203,16 +209,16 @@ private protected ValueTask AcquireAsync(ref Valu default: if (options.Token.IsCancellationRequested) { - task = ValueTask.FromCanceled(options.Token); + task = TNode.FromCanceled(options.Token); break; } - ISupplier factory; + ISupplier factory; lock (SyncRoot) { if (IsDisposingOrDisposed) { - task = new(DisposedTask); + task = TNode.FromException(CreateObjectDisposedException()); break; } @@ -222,11 +228,11 @@ private protected ValueTask AcquireAsync(ref Valu if (TryAcquire(ref manager)) { - task = ValueTask.CompletedTask; + task = TNode.SuccessfulTask; break; } - factory = EnqueueNode(ref pool, WaitNodeFlags.ThrowOnTimeout); + factory = EnqueueNode(ref pool, initializer); } interruptedCallers?.Unwind(); @@ -235,77 +241,43 @@ private protected ValueTask AcquireAsync(ref Valu } return task; - } - private protected ValueTask TryAcquireAsync(ref ValueTaskPool> pool, ref TLockManager manager, TOptions options) + ObjectDisposedException CreateObjectDisposedException() + => new(GetType().Name); + } + + private protected ValueTask AcquireAsync(ref ValueTaskPool> pool, + ref TLockManager manager, TOptions options) where TNode : WaitNode, IPooledManualResetCompletionSource>, new() where TLockManager : struct, ILockManager where TOptions : struct, IAcquisitionOptions - { - ValueTask task; - - switch (options.Timeout.Ticks) - { - case Timeout.InfiniteTicks: - goto default; - case < 0L or > Timeout.MaxTimeoutParameterTicks: - task = ValueTask.FromException(new ArgumentOutOfRangeException("timeout")); - break; - case 0L: // attempt to acquire synchronously - LinkedValueTaskCompletionSource? interruptedCallers; - lock (SyncRoot) - { - if (IsDisposingOrDisposed) - { - task = new(GetDisposedTask()); - break; - } - - interruptedCallers = TOptions.InterruptionRequired - ? Interrupt(options.InterruptionReason) - : null; - - task = new(TryAcquire(ref manager)); - } - - interruptedCallers?.Unwind(); - break; - default: - if (options.Token.IsCancellationRequested) - { - task = ValueTask.FromCanceled(options.Token); - break; - } - - ISupplier> factory; - lock (SyncRoot) - { - if (IsDisposingOrDisposed) - { - task = new(GetDisposedTask()); - break; - } - - interruptedCallers = TOptions.InterruptionRequired - ? Interrupt(options.InterruptionReason) - : null; - - if (TryAcquire(ref manager)) - { - task = new(true); - break; - } - - factory = EnqueueNode(ref pool, WaitNodeFlags.None); - } - - interruptedCallers?.Unwind(); - task = factory.Invoke(options.Timeout, options.Token); - break; - } - - return task; - } + => AcquireAsync, TLockManager, TOptions>( + ref pool, + ref manager, + new(WaitNodeFlags.ThrowOnTimeout), + options); + + private protected ValueTask TryAcquireAsync(ref ValueTaskPool> pool, + ref TLockManager manager, TOptions options) + where TNode : WaitNode, IPooledManualResetCompletionSource>, new() + where TLockManager : struct, ILockManager + where TOptions : struct, IAcquisitionOptions + => AcquireAsync, TNode, StaticInitializer, TLockManager, TOptions>( + ref pool, + ref manager, + new(WaitNodeFlags.None), + options); + + private protected ValueTask AcquireSpecialAsync(ref ValueTaskPool> pool, + ref TLockManager manager, TOptions options) + where TNode : WaitNode, IPooledManualResetCompletionSource>, new() + where TLockManager : struct, ILockManager, IConsumer + where TOptions : struct, IAcquisitionOptions + => AcquireAsync, TLockManager, TOptions>( + ref pool, + ref manager, + new(WaitNodeFlags.ThrowOnTimeout, ref manager), + options); /// /// Cancels all suspended callers. @@ -329,7 +301,7 @@ public void CancelSuspendedCallers(CancellationToken token) suspendedCallers?.Unwind(); } - private protected unsafe LinkedValueTaskCompletionSource? DetachWaitQueue() + private protected LinkedValueTaskCompletionSource? DetachWaitQueue() { Monitor.IsEntered(SyncRoot); @@ -466,23 +438,60 @@ internal enum WaitNodeFlags None = 0, ThrowOnTimeout = 1, } + + private interface INodeInitializer : IConsumer + where TNode : WaitNode + { + WaitNodeFlags Flags { get; } + } - private protected abstract class WaitNode : LinkedValueTaskCompletionSource + [StructLayout(LayoutKind.Auto)] + private readonly struct StaticInitializer(WaitNodeFlags flags) : INodeInitializer + where TNode : WaitNode + where TLockManager : struct, ILockManager + { + WaitNodeFlags INodeInitializer.Flags => flags; + + void IConsumer.Invoke(TNode node) => TLockManager.InitializeNode(node); + } + + // TODO: Replace with allows ref anti-constraint and ref struct + [StructLayout(LayoutKind.Auto)] + private readonly struct NodeInitializer(WaitNodeFlags flags, ref TLockManager manager) : INodeInitializer + where TNode : WaitNode + where TLockManager : struct, ILockManager, IConsumer + { + private readonly unsafe void* managerOnStack = Unsafe.AsPointer(ref manager); + + WaitNodeFlags INodeInitializer.Flags => flags; + + unsafe void IConsumer.Invoke(TNode node) + => Unsafe.AsRef(managerOnStack).Invoke(node); + } + + private interface IValueTaskFactory : ISupplier + where T : struct, IEquatable + { + static abstract T SuccessfulTask { get; } + + static abstract T TimedOutTask { get; } + + static abstract T FromException(Exception e); + + static abstract T FromCanceled(CancellationToken token); + } + + private protected abstract class WaitNode : LinkedValueTaskCompletionSource, IValueTaskFactory, + IValueTaskFactory> { - private readonly WeakReference owner = new(target: null, trackResurrection: false); private Timestamp createdAt; private WaitNodeFlags flags; // stores information about suspended caller for debugging purposes - internal object? CallerInfo - { - get; - private set; - } + internal object? CallerInfo { get; private set; } protected override void CleanUp() { - owner.SetTarget(target: null); CallerInfo = null; base.CleanUp(); } @@ -490,13 +499,10 @@ protected override void CleanUp() [DebuggerBrowsable(DebuggerBrowsableState.Never)] internal bool NeedsRemoval => CompletionData is null; - internal void Initialize(QueuedSynchronizer owner, WaitNodeFlags flags) + internal void Initialize(object? callerInfo, WaitNodeFlags flags) { - Debug.Assert(owner is not null); - this.flags = flags; - this.owner.SetTarget(owner); - CallerInfo = owner.callerInfo?.Capture(); + CallerInfo = callerInfo; createdAt = new(); } @@ -507,18 +513,39 @@ private protected static void AfterConsumed(T node) where T : WaitNode, IPooledManualResetCompletionSource> { // report lock duration - if (node.owner.TryGetTarget(out var owner)) + if (node.OnConsumed is { } callback) { - LockDurationMeter.Record(node.createdAt.ElapsedMilliseconds, owner.measurementTags); - } + if (callback.Target is QueuedSynchronizer synchronizer) + LockDurationMeter.Record(node.createdAt.ElapsedMilliseconds, synchronizer.measurementTags); - node.OnConsumed?.Invoke(node); + callback(node); + } } + + static ValueTask IValueTaskFactory.SuccessfulTask => ValueTask.CompletedTask; + + static ValueTask IValueTaskFactory>.SuccessfulTask => ValueTask.FromResult(true); + + static ValueTask IValueTaskFactory.TimedOutTask => ValueTask.FromException(new TimeoutException()); + + static ValueTask IValueTaskFactory>.TimedOutTask => ValueTask.FromResult(false); + + static ValueTask IValueTaskFactory>.FromException(Exception e) + => ValueTask.FromException(e); + + static ValueTask IValueTaskFactory.FromException(Exception e) + => ValueTask.FromException(e); + + static ValueTask IValueTaskFactory>.FromCanceled(CancellationToken token) + => ValueTask.FromCanceled(token); + + static ValueTask IValueTaskFactory.FromCanceled(CancellationToken token) + => ValueTask.FromCanceled(token); } private protected sealed class DefaultWaitNode : WaitNode, IPooledManualResetCompletionSource> { - protected sealed override void AfterConsumed() => AfterConsumed(this); + protected override void AfterConsumed() => AfterConsumed(this); Action? IPooledManualResetCompletionSource>.OnConsumed { get; set; } } @@ -528,6 +555,8 @@ private protected interface ILockManager bool IsLockAllowed { get; } void AcquireLock(); + + static virtual bool RequiresEmptyQueue => true; } private protected interface ILockManager : ILockManager @@ -805,13 +834,11 @@ private bool TryAcquireCore(TContext context) { Debug.Assert(Monitor.IsEntered(SyncRoot)); - if (WaitQueueHead is null && CanAcquire(context)) - { - AcquireCore(context); - return true; - } - - return false; + if (WaitQueueHead is not null || !CanAcquire(context)) + return false; + + AcquireCore(context); + return true; } private WaitNode EnqueueNode(TContext context, WaitNodeFlags flags) @@ -820,7 +847,7 @@ private WaitNode EnqueueNode(TContext context, WaitNodeFlags flags) var node = pool.Get(); node.Context = context; - node.Initialize(this, flags); + node.Initialize(CaptureCallerInformation(), flags); EnqueueNode(node); return node; } diff --git a/src/DotNext.Threading/Threading/Tasks/ManualResetCompletionSource.cs b/src/DotNext.Threading/Threading/Tasks/ManualResetCompletionSource.cs index 5046dadfaf..42543a4e5f 100644 --- a/src/DotNext.Threading/Threading/Tasks/ManualResetCompletionSource.cs +++ b/src/DotNext.Threading/Threading/Tasks/ManualResetCompletionSource.cs @@ -1,8 +1,9 @@ +using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using ValueTaskSourceOnCompletedFlags = System.Threading.Tasks.Sources.ValueTaskSourceOnCompletedFlags; +using System.Threading.Tasks.Sources; namespace DotNext.Threading.Tasks; @@ -220,8 +221,26 @@ private void OnCompleted(in Continuation continuation, short token) throw new InvalidOperationException(errorMessage); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private protected void OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) + private protected ValueTaskSourceStatus GetStatus(short token, Exception? e) + { + var snapshot = versionAndStatus.VolatileRead(); // barrier to avoid reordering of result read + + if (token != snapshot.Version) + throw new InvalidOperationException(ExceptionMessages.InvalidSourceToken); + + return !snapshot.IsCompleted + ? ValueTaskSourceStatus.Pending + : e switch + { + null => ValueTaskSourceStatus.Succeeded, + OperationCanceledException => ValueTaskSourceStatus.Canceled, + _ => ValueTaskSourceStatus.Faulted, + }; + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public void OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => OnCompleted(new(continuation, state, flags), token); /// diff --git a/src/DotNext.Threading/Threading/Tasks/ValueTaskCompletionSource.T.cs b/src/DotNext.Threading/Threading/Tasks/ValueTaskCompletionSource.T.cs index 4ce084162b..6ca1cf9b1d 100644 --- a/src/DotNext.Threading/Threading/Tasks/ValueTaskCompletionSource.T.cs +++ b/src/DotNext.Threading/Threading/Tasks/ValueTaskCompletionSource.T.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using System.Runtime.CompilerServices; using System.Threading.Tasks.Sources; using Debug = System.Diagnostics.Debug; @@ -8,7 +9,7 @@ namespace DotNext.Threading.Tasks; /// Represents the producer side of . /// /// -/// In constrast to , this +/// In contrast to , this /// source is resettable. /// From performance point of view, the type offers minimal or zero memory allocation /// for the task itself (excluding continuations). See @@ -37,7 +38,7 @@ private static Result FromCanceled(CancellationToken token) => new(new OperationCanceledException(token)); /// - /// Attempts to complete the task sucessfully. + /// Attempts to complete the task successfully. /// /// The value to be returned to the consumer. /// if the result is completed successfully; if the task has been canceled or timed out. @@ -46,7 +47,7 @@ public bool TrySetResult(T value) => TrySetResult(null, value); /// - /// Attempts to complete the task sucessfully. + /// Attempts to complete the task successfully. /// /// The data to be saved in property that can be accessed from within method. /// The value to be returned to the consumer. @@ -55,7 +56,7 @@ public unsafe bool TrySetResult(object? completionData, T value) => SetResult(completionData, completionToken: null, &Result.FromValue, value); /// - /// Attempts to complete the task sucessfully. + /// Attempts to complete the task successfully. /// /// The completion token previously obtained from method. /// The value to be returned to the consumer. @@ -65,7 +66,7 @@ public bool TrySetResult(short completionToken, T value) => TrySetResult(null, completionToken, value); /// - /// Attempts to complete the task sucessfully. + /// Attempts to complete the task successfully. /// /// The data to be saved in property that can be accessed from within method. /// The completion token previously obtained from method. @@ -219,7 +220,7 @@ ValueTask ISupplier>.Invoke(TimeSpa ValueTask ISupplier.Invoke(TimeSpan timeout, CancellationToken token) => Activate(timeout, token) is { } version ? new(this, version) : throw new InvalidOperationException(ExceptionMessages.InvalidSourceState); - private T GetResult(short token) + private protected T GetResult(short token) { // ensure that instance field access before returning to the pool to avoid // concurrency with Reset() @@ -236,35 +237,10 @@ private T GetResult(short token) /// void IValueTaskSource.GetResult(short token) => GetResult(token); - private ValueTaskSourceStatus GetStatus(short token) - { - var error = result.Error; - var snapshot = versionAndStatus.VolatileRead(); // barrier to avoid reordering of result read - - if (token != snapshot.Version) - throw new InvalidOperationException(ExceptionMessages.InvalidSourceToken); - - return !snapshot.IsCompleted ? ValueTaskSourceStatus.Pending : error switch - { - null => ValueTaskSourceStatus.Succeeded, - OperationCanceledException => ValueTaskSourceStatus.Canceled, - _ => ValueTaskSourceStatus.Faulted, - }; - } - - /// - ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => GetStatus(token); - - /// - ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => GetStatus(token); - - /// - void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) - => OnCompleted(continuation, state, token, flags); - - /// - void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) - => OnCompleted(continuation, state, token, flags); + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public ValueTaskSourceStatus GetStatus(short token) + => GetStatus(token, result.Error); /// /// Creates a linked that can be used cooperatively to diff --git a/src/DotNext.Threading/Threading/Tasks/ValueTaskCompletionSource.cs b/src/DotNext.Threading/Threading/Tasks/ValueTaskCompletionSource.cs index 6a849513fe..cddfb33192 100644 --- a/src/DotNext.Threading/Threading/Tasks/ValueTaskCompletionSource.cs +++ b/src/DotNext.Threading/Threading/Tasks/ValueTaskCompletionSource.cs @@ -136,7 +136,7 @@ public bool TrySetException(object? completionData, short completionToken, Excep => SetResult>(completionData, completionToken, e); /// - /// Attempts to complete the task sucessfully. + /// Attempts to complete the task successfully. /// /// if the result is completed successfully; if the task has been canceled or timed out. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -144,7 +144,7 @@ public bool TrySetResult() => TrySetResult(null); /// - /// Attempts to complete the task sucessfully. + /// Attempts to complete the task successfully. /// /// The data to be saved in property that can be accessed from within method. /// if the result is completed successfully; if the task has been canceled or timed out. @@ -152,7 +152,7 @@ public bool TrySetResult(object? completionData) => SetResult(completionData, completionToken: null, ISupplier.NullOrDefault); /// - /// Attempts to complete the task sucessfully. + /// Attempts to complete the task successfully. /// /// The completion token previously obtained from method. /// if the result is completed successfully; if the task has been canceled or timed out. @@ -160,7 +160,7 @@ public bool TrySetResult(short completionToken) => TrySetResult(null, completionToken); /// - /// Attempts to complete the task sucessfully. + /// Attempts to complete the task successfully. /// /// The data to be saved in property that can be accessed from within method. /// The completion token previously obtained from method. @@ -176,7 +176,7 @@ public bool TrySetResult(object? completionData, short completionToken) /// /// The timeout associated with the task. /// The cancellation token that can be used to cancel the task. - /// A fresh incompleted task. + /// A fresh uncompleted task. /// is less than zero but not equals to . /// The source is in invalid state. public ValueTask CreateTask(TimeSpan timeout, CancellationToken token) @@ -200,24 +200,7 @@ void IValueTaskSource.GetResult(short token) /// ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) - { - var resultCopy = result; - var snapshot = versionAndStatus.VolatileRead(); // barrier to avoid reordering of result read - - if (token != snapshot.Version) - throw new InvalidOperationException(ExceptionMessages.InvalidSourceToken); - - return !snapshot.IsCompleted ? ValueTaskSourceStatus.Pending : resultCopy switch - { - null => ValueTaskSourceStatus.Succeeded, - { SourceException: OperationCanceledException } => ValueTaskSourceStatus.Canceled, - _ => ValueTaskSourceStatus.Faulted, - }; - } - - /// - void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) - => OnCompleted(continuation, state, token, flags); + => GetStatus(token, result?.SourceException); /// /// Creates a linked that can be used cooperatively to diff --git a/src/DotNext.Unsafe/DotNext.Unsafe.csproj b/src/DotNext.Unsafe/DotNext.Unsafe.csproj index 4386ae5872..1a7f9db28f 100644 --- a/src/DotNext.Unsafe/DotNext.Unsafe.csproj +++ b/src/DotNext.Unsafe/DotNext.Unsafe.csproj @@ -7,7 +7,7 @@ enable true true - 5.12.0 + 5.13.0 nullablePublicOnly .NET Foundation and Contributors diff --git a/src/DotNext.Unsafe/Runtime/InteropServices/Pointer.cs b/src/DotNext.Unsafe/Runtime/InteropServices/Pointer.cs index b16cf9f1be..9b7fcb17c2 100644 --- a/src/DotNext.Unsafe/Runtime/InteropServices/Pointer.cs +++ b/src/DotNext.Unsafe/Runtime/InteropServices/Pointer.cs @@ -11,6 +11,7 @@ namespace DotNext.Runtime.InteropServices; +using Collections.Generic; using MemorySource = Buffers.UnmanagedMemory; /// @@ -32,7 +33,6 @@ namespace DotNext.Runtime.InteropServices; IPinnable, ISpanFormattable, IComparisonOperators, Pointer, bool>, - IEqualityOperators, Pointer, bool>, IAdditionOperators, int, Pointer>, IAdditionOperators, long, Pointer>, IAdditionOperators, nint, Pointer>, @@ -51,15 +51,12 @@ namespace DotNext.Runtime.InteropServices; /// Represents enumerator over raw memory. /// [StructLayout(LayoutKind.Auto)] - public unsafe struct Enumerator : IEnumerator + public unsafe struct Enumerator : IEnumerator { private readonly T* ptr; private readonly nuint count; private nuint index; - /// - readonly object IEnumerator.Current => Current; - internal Enumerator(T* ptr, nuint count) { this.count = count > 0 ? count : throw new ArgumentOutOfRangeException(nameof(count)); @@ -95,11 +92,6 @@ public readonly T Current /// Sets the enumerator to its initial position. /// public void Reset() => index = nuint.MaxValue; - - /// - /// Releases all resources with this enumerator. - /// - public void Dispose() => this = default; } /// diff --git a/src/DotNext/BasicExtensions.cs b/src/DotNext/BasicExtensions.cs index 8b03363c63..e1ba0106fd 100644 --- a/src/DotNext/BasicExtensions.cs +++ b/src/DotNext/BasicExtensions.cs @@ -80,6 +80,7 @@ public static bool IsOneOf(this T value, ReadOnlySpan candidates) /// The object reference to reinterpret. /// The reinterpreted reference. [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNullIfNotNull(nameof(obj))] public static T As(this T obj) where T : class? => obj; diff --git a/src/DotNext/Buffers/Binary/BinaryTransformations.Bitwise.cs b/src/DotNext/Buffers/Binary/BinaryTransformations.Bitwise.cs index e100d19093..948e470612 100644 --- a/src/DotNext/Buffers/Binary/BinaryTransformations.Bitwise.cs +++ b/src/DotNext/Buffers/Binary/BinaryTransformations.Bitwise.cs @@ -83,9 +83,7 @@ private static void Transform(ref byte x, int length) { for (; length >= Vector.Count; length -= Vector.Count) { - Unsafe.WriteUnaligned( - ref x, - TTransformation.Transform(Unsafe.ReadUnaligned>(ref x))); + TTransformation.Transform(Vector.LoadUnsafe(ref x)).StoreUnsafe(ref x); x = ref Unsafe.Add(ref x, Vector.Count); } @@ -128,9 +126,7 @@ private static void Transform([In] ref byte x, ref byte y, int { for (; length >= Vector.Count; length -= Vector.Count) { - Unsafe.WriteUnaligned( - ref y, - TTransformation.Transform(Unsafe.ReadUnaligned>(ref x), Unsafe.ReadUnaligned>(ref y))); + TTransformation.Transform(Vector.LoadUnsafe(ref x), Vector.LoadUnsafe(ref y)).StoreUnsafe(ref y); x = ref Unsafe.Add(ref x, Vector.Count); y = ref Unsafe.Add(ref y, Vector.Count); @@ -166,7 +162,7 @@ private static unsafe void Transform(ReadOnlySpan x, Span count = Math.Min(maxLength, x.Length); Transform( - ref Unsafe.As(ref Unsafe.AsRef(in MemoryMarshal.GetReference(x))), + ref Unsafe.As(ref MemoryMarshal.GetReference(x)), ref Unsafe.As(ref MemoryMarshal.GetReference(y)), count * sizeof(T)); } diff --git a/src/DotNext/Buffers/SparseBufferWriter.Reader.cs b/src/DotNext/Buffers/SparseBufferWriter.Reader.cs index d56d53c613..c32e89f737 100644 --- a/src/DotNext/Buffers/SparseBufferWriter.Reader.cs +++ b/src/DotNext/Buffers/SparseBufferWriter.Reader.cs @@ -6,13 +6,15 @@ namespace DotNext.Buffers; +using Collections.Generic; + public partial class SparseBufferWriter : IEnumerable> { /// /// Represents enumerator over memory segments. /// [StructLayout(LayoutKind.Auto)] - public struct Enumerator : IEnumerator> + public struct Enumerator : IEnumerator> { private MemoryChunk? current; private bool initialized; @@ -23,14 +25,11 @@ internal Enumerator(MemoryChunk? head) initialized = false; } - /// + /// public readonly ReadOnlyMemory Current - => current is null ? ReadOnlyMemory.Empty : current.WrittenMemory; - - /// - readonly object IEnumerator.Current => Current; + => current?.WrittenMemory ?? ReadOnlyMemory.Empty; - /// + /// public bool MoveNext() { if (initialized) @@ -40,12 +39,6 @@ public bool MoveNext() return current is not null; } - - /// - readonly void IEnumerator.Reset() => throw new NotSupportedException(); - - /// - void IDisposable.Dispose() => this = default; } /// @@ -244,8 +237,9 @@ public Enumerator GetEnumerator() /// IEnumerator> IEnumerable>.GetEnumerator() - => first is null ? Enumerable.Empty>().GetEnumerator() : GetEnumerator(); + => GetEnumerator().ToClassicEnumerator>(); /// - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator>(); } \ No newline at end of file diff --git a/src/DotNext/Buffers/Text/Base64Decoder.cs b/src/DotNext/Buffers/Text/Base64Decoder.cs index dc8d5113ba..87bab6508a 100644 --- a/src/DotNext/Buffers/Text/Base64Decoder.cs +++ b/src/DotNext/Buffers/Text/Base64Decoder.cs @@ -74,7 +74,7 @@ private readonly ReadOnlySpan BufferedBytes { Debug.Assert((uint)reservedBufferSize <= sizeof(ulong)); - return MemoryMarshal.CreateReadOnlySpan(in InToRef(ref Unsafe.AsRef(in reservedBuffer)), reservedBufferSize); + return MemoryMarshal.CreateReadOnlySpan(in ChangeType(in reservedBuffer), reservedBufferSize); } } } \ No newline at end of file diff --git a/src/DotNext/Collections/Generic/AsyncEnumerable.Proxy.cs b/src/DotNext/Collections/Generic/AsyncEnumerable.Proxy.cs index 33c3cc8383..16d191d679 100644 --- a/src/DotNext/Collections/Generic/AsyncEnumerable.Proxy.cs +++ b/src/DotNext/Collections/Generic/AsyncEnumerable.Proxy.cs @@ -2,18 +2,11 @@ namespace DotNext.Collections.Generic; public static partial class AsyncEnumerable { - internal sealed class Proxy : IAsyncEnumerable + internal sealed class Proxy(IEnumerable enumerable) : IAsyncEnumerable { - internal sealed class Enumerator : Disposable, IAsyncEnumerator + internal sealed class Enumerator(IEnumerable enumerable, CancellationToken token) : Disposable, IAsyncEnumerator { - private readonly IEnumerator enumerator; - private readonly CancellationToken token; - - internal Enumerator(IEnumerable enumerable, CancellationToken token) - { - enumerator = enumerable.GetEnumerator(); - this.token = token; - } + private readonly IEnumerator enumerator = enumerable.GetEnumerator(); public T Current => enumerator.Current; @@ -33,11 +26,6 @@ protected override void Dispose(bool disposing) public new ValueTask DisposeAsync() => base.DisposeAsync(); } - private readonly IEnumerable enumerable; - - internal Proxy(IEnumerable enumerable) - => this.enumerable = enumerable; - public IAsyncEnumerator GetAsyncEnumerator(CancellationToken token) => new Enumerator(enumerable, token); } diff --git a/src/DotNext/Collections/Generic/Collection.ConsumingEnumerable.cs b/src/DotNext/Collections/Generic/Collection.ConsumingEnumerable.cs index 39d7e0595b..d482bbbe51 100644 --- a/src/DotNext/Collections/Generic/Collection.ConsumingEnumerable.cs +++ b/src/DotNext/Collections/Generic/Collection.ConsumingEnumerable.cs @@ -18,7 +18,7 @@ public static partial class Collection /// Represents consumer enumerator. /// [StructLayout(LayoutKind.Auto)] - public struct Enumerator : IEnumerator + public struct Enumerator : IEnumerator { private readonly IProducerConsumerCollection? collection; @@ -35,20 +35,11 @@ internal Enumerator(IProducerConsumerCollection? collection) /// public readonly T Current => current!; - /// - readonly object? IEnumerator.Current => Current; - /// /// Consumes the item from the underlying collection. /// /// if the item has been consumed successfully; if underlying collection is empty. public bool MoveNext() => collection?.TryTake(out current) ?? false; - - /// - readonly void IEnumerator.Reset() => throw new NotSupportedException(); - - /// - void IDisposable.Dispose() => this = default; } private readonly IProducerConsumerCollection? collection; @@ -63,10 +54,12 @@ internal ConsumingEnumerable(IProducerConsumerCollection collection) public Enumerator GetEnumerator() => new(collection); /// - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator(); /// - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator(); } /// diff --git a/src/DotNext/Collections/Generic/Collection.cs b/src/DotNext/Collections/Generic/Collection.cs index 8fd65623f3..ae2dd508aa 100644 --- a/src/DotNext/Collections/Generic/Collection.cs +++ b/src/DotNext/Collections/Generic/Collection.cs @@ -360,4 +360,23 @@ public static IEnumerable Append(this IEnumerable collection, params T[ /// is . public static IAsyncEnumerable ToAsyncEnumerable(this IEnumerable enumerable) => new AsyncEnumerable.Proxy(enumerable ?? throw new ArgumentNullException(nameof(enumerable))); + + /// + /// Converts ad-hoc enumerator to a generic enumerator. + /// + /// Ad-hoc enumerator. + /// The enumerator over values of type . + public static IEnumerator ToClassicEnumerator(this TEnumerator enumerator) + where TEnumerator : struct, IEnumerator + => TEnumerator.ToEnumerator(enumerator); + + /// + /// Converts ad-hoc enumerator to a generic enumerator. + /// + /// Ad-hoc enumerator. + /// The token that can be used to cancel the enumeration. + /// The enumerator over values of type . + public static IAsyncEnumerator ToAsyncEnumerator(this TEnumerator enumerator, CancellationToken token) + where TEnumerator : struct, IEnumerator + => TEnumerator.ToEnumerator(enumerator, token); } \ No newline at end of file diff --git a/src/DotNext/Collections/Generic/IEnumerator.cs b/src/DotNext/Collections/Generic/IEnumerator.cs new file mode 100644 index 0000000000..9289766e27 --- /dev/null +++ b/src/DotNext/Collections/Generic/IEnumerator.cs @@ -0,0 +1,68 @@ +using System.Collections; +using System.ComponentModel; + +namespace DotNext.Collections.Generic; + +/// +/// Represents ad-hoc enumerator implemented as value type to avoid enumerator allocation +/// and prevent the compiler to generate call. +/// +/// +/// The enumerator doesn't implement interface implicitly +/// but can be converted to it by using regular type cast. +/// +/// The value type that implements an enumerator. +/// +[EditorBrowsable(EditorBrowsableState.Advanced)] +public interface IEnumerator + where TSelf : struct, IEnumerator +{ + /// + bool MoveNext(); + + /// + T Current { get; } + + /// + void Reset() => throw new NotSupportedException(); + + /// + /// Converts ad-hoc enumerator to a generic enumerator. + /// + /// Ad-hoc enumerator. + /// The enumerator over values of type . + internal static virtual IEnumerator ToEnumerator(TSelf enumerator) + => new BoxedEnumerator(enumerator); + + /// + /// Converts ad-hoc enumerator to a generic enumerator. + /// + /// Ad-hoc enumerator. + /// The token that can be used to cancel the enumeration. + /// The enumerator over values of type . + internal static virtual IAsyncEnumerator ToEnumerator(TSelf enumerator, CancellationToken token) + => new BoxedEnumerator(enumerator, token); +} + +file sealed class BoxedEnumerator(TEnumerator enumerator, CancellationToken token = default) : IEnumerator, IAsyncEnumerator + where TEnumerator : struct, IEnumerator +{ + public T Current => enumerator.Current; + + object? IEnumerator.Current => enumerator.Current; + + bool IEnumerator.MoveNext() => enumerator.MoveNext(); + + ValueTask IAsyncEnumerator.MoveNextAsync() + => token.IsCancellationRequested ? ValueTask.FromCanceled(token) : ValueTask.FromResult(enumerator.MoveNext()); + + void IEnumerator.Reset() => enumerator.Reset(); + + void IDisposable.Dispose() => enumerator = default; + + ValueTask IAsyncDisposable.DisposeAsync() + { + enumerator = default; + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/src/DotNext/Collections/Specialized/ConcurrentTypeMap.Enumerator.cs b/src/DotNext/Collections/Specialized/ConcurrentTypeMap.Enumerator.cs index 9fae17d55a..951582af18 100644 --- a/src/DotNext/Collections/Specialized/ConcurrentTypeMap.Enumerator.cs +++ b/src/DotNext/Collections/Specialized/ConcurrentTypeMap.Enumerator.cs @@ -1,17 +1,19 @@ +using System.Collections; using System.Runtime.InteropServices; using Unsafe = System.Runtime.CompilerServices.Unsafe; namespace DotNext.Collections.Specialized; +using Generic; using static Runtime.Intrinsics; -public partial class ConcurrentTypeMap +public partial class ConcurrentTypeMap : IEnumerable { /// /// Represents an enumerator over the values in the map. /// [StructLayout(LayoutKind.Auto)] - public struct Enumerator + public struct Enumerator : IEnumerator { private readonly Entry[] entries; private nuint index; @@ -37,7 +39,7 @@ public bool MoveNext() { if (entries is not null) { - for (nuint nextIndex; ;) + for (nuint nextIndex;;) { nextIndex = index + 1U; if (nextIndex >= entries.GetLength()) @@ -56,6 +58,9 @@ public bool MoveNext() return false; } + + /// + void IEnumerator.Reset() => index = nuint.MaxValue; } /// @@ -63,4 +68,61 @@ public bool MoveNext() /// /// The enumerator over the values. public Enumerator GetEnumerator() => new(Volatile.Read(ref entries)); + + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator(); +} + +public partial class ConcurrentTypeMap +{ + /// + /// Represents an enumerator over values stored in the map. + /// + [StructLayout(LayoutKind.Auto)] + public struct Enumerator : IEnumerator + { + private readonly Entry[] entries; + private int index; + private object? current; + + internal Enumerator(Entry[] entries) => this.entries = entries; + + /// + public bool MoveNext() + { + while (entries is not null && index < entries.Length) + { + current = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(entries), index++).Value; + if (current is not null) + return true; + } + + return false; + } + + /// + public readonly object Current => current ?? throw new InvalidOperationException(); + + /// + void IEnumerator.Reset() => index = 0; + } + + /// + /// Gets an enumerator over values in this map. + /// + /// The enumerator over values in this map. + public Enumerator GetEnumerator() => new(entries); + + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator(); } \ No newline at end of file diff --git a/src/DotNext/Collections/Specialized/ConcurrentTypeMap.cs b/src/DotNext/Collections/Specialized/ConcurrentTypeMap.cs index c1a41aa42a..9d691dc725 100644 --- a/src/DotNext/Collections/Specialized/ConcurrentTypeMap.cs +++ b/src/DotNext/Collections/Specialized/ConcurrentTypeMap.cs @@ -73,7 +73,7 @@ internal bool TryAcquireLock(int expectedState) private readonly object syncRoot; - // Assuming that the map will not contain hunders or thousands for entries. + // Assuming that the map will not contain hundreds or thousands for entries. // If so, we can keep the lock for each entry instead of buckets as in ConcurrentDictionaryMap. // As a result, we don't need the concurrency level. Also, we can modify different entries concurrently // and perform resizing in parallel with read/write of individual entry @@ -401,9 +401,9 @@ public void Clear() /// /// Represents thread-safe implementation of interface. /// -public class ConcurrentTypeMap : ITypeMap +public partial class ConcurrentTypeMap : ITypeMap { - private sealed class Entry + internal sealed class Entry { internal volatile object? Value; @@ -465,11 +465,11 @@ private void Resize(Entry[] entries) return; // do resize - var firstUnitialized = entries.Length; + var firstUninitialized = entries.Length; Array.Resize(ref entries, ITypeMap.RecommendedCapacity); // initializes the rest of the array - entries.AsSpan(firstUnitialized).Initialize(); + entries.AsSpan(firstUninitialized).Initialize(); // commit resized storage this.entries = entries; // write barrier is provided by monitor lock diff --git a/src/DotNext/Collections/Specialized/IReadOnlyTypeMap.cs b/src/DotNext/Collections/Specialized/IReadOnlyTypeMap.cs index 851b836699..a24bd86128 100644 --- a/src/DotNext/Collections/Specialized/IReadOnlyTypeMap.cs +++ b/src/DotNext/Collections/Specialized/IReadOnlyTypeMap.cs @@ -28,7 +28,7 @@ public interface IReadOnlyTypeMap /// /// Represents read-only view of a set of typed values. /// -public interface IReadOnlyTypeMap +public interface IReadOnlyTypeMap : IEnumerable { /// /// Determines whether the set has the value of type . diff --git a/src/DotNext/Collections/Specialized/ITypeMap.cs b/src/DotNext/Collections/Specialized/ITypeMap.cs index e5f97670dd..e3e94690b4 100644 --- a/src/DotNext/Collections/Specialized/ITypeMap.cs +++ b/src/DotNext/Collections/Specialized/ITypeMap.cs @@ -4,7 +4,7 @@ namespace DotNext.Collections.Specialized; /// -/// Represents specialized dictionary where the each key is represented by generic +/// Represents specialized dictionary where each key is represented by generic /// parameter. /// /// The type of the value. diff --git a/src/DotNext/Collections/Specialized/InvocationList.cs b/src/DotNext/Collections/Specialized/InvocationList.cs index 563fc64d83..8db960620e 100644 --- a/src/DotNext/Collections/Specialized/InvocationList.cs +++ b/src/DotNext/Collections/Specialized/InvocationList.cs @@ -5,6 +5,8 @@ namespace DotNext.Collections.Specialized; +using Generic; + /// /// Represents immutable list of delegates. /// @@ -22,7 +24,7 @@ namespace DotNext.Collections.Specialized; /// Represents enumerator over the list of delegates. /// [StructLayout(LayoutKind.Auto)] - public struct Enumerator + public struct Enumerator : IEnumerator { private object? list; private int index; @@ -37,11 +39,7 @@ internal Enumerator(object? list) /// /// Gets the current delegate. /// - public TDelegate Current - { - get; - private set; - } + public TDelegate Current { get; private set; } /// /// Moves to the next delegate in the list. @@ -49,26 +47,27 @@ public TDelegate Current /// if the enumerator reaches the end of the list; otherwise, . public bool MoveNext() { - if (list is null) - goto fail; - - if (list is TDelegate) + switch (list) { - Current = Unsafe.As(list); - list = null; - goto success; + case null: + goto fail; + case TDelegate: + Current = Unsafe.As(list); + list = null; + goto success; + default: + var array = Unsafe.As(list); + index += 1; + if ((uint)index >= (uint)array.Length) + goto fail; + + Current = array[index]; + break; } - var array = Unsafe.As(list); - index += 1; - if ((uint)index >= (uint)array.Length) - goto fail; - - Current = array[index]; - - success: + success: return true; - fail: + fail: return false; } } @@ -197,18 +196,13 @@ public InvocationList Remove(TDelegate? d) /// The enumerator over delegates. public Enumerator GetEnumerator() => new(list); - private IEnumerator GetEnumeratorCore() => list switch - { - null => Enumerable.Empty().GetEnumerator(), - TDelegate d => new SingletonList.Enumerator(d), - _ => Unsafe.As>(list).GetEnumerator(), - }; - /// - IEnumerator IEnumerable.GetEnumerator() => GetEnumeratorCore(); + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator(); /// - IEnumerator IEnumerable.GetEnumerator() => GetEnumeratorCore(); + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator(); /// /// Gets a span over list of delegates. diff --git a/src/DotNext/Collections/Specialized/SingletonList.cs b/src/DotNext/Collections/Specialized/SingletonList.cs index 23fac3f148..53b9613fa7 100644 --- a/src/DotNext/Collections/Specialized/SingletonList.cs +++ b/src/DotNext/Collections/Specialized/SingletonList.cs @@ -5,6 +5,8 @@ namespace DotNext.Collections.Specialized; +using Generic; + /// /// Represents a list with one element. /// @@ -16,7 +18,7 @@ public struct SingletonList : IReadOnlyList, IList, ITuple, IReadOnlySe /// Represents an enumerator over the collection containing a single element. /// [StructLayout(LayoutKind.Auto)] - public struct Enumerator : IEnumerator + public struct Enumerator : IEnumerator { private const byte NotRequestedState = 1; private const byte RequestedState = 2; @@ -32,13 +34,7 @@ internal Enumerator(T item) /// /// Gets the current element. /// - public readonly T Current { get; } - - /// - readonly object? IEnumerator.Current => Current; - - /// - void IDisposable.Dispose() => this = default; + public T Current { get; } /// /// Advances the position of the enumerator to the next element. @@ -142,10 +138,12 @@ readonly void ICollection.CopyTo(T[] array, int arrayIndex) public readonly Enumerator GetEnumerator() => new(Item); /// - readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + readonly IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator(); /// - readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + readonly IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator(); /// /// Converts a value to the read-only list. diff --git a/src/DotNext/Collections/Specialized/TypeMap.Enumerator.cs b/src/DotNext/Collections/Specialized/TypeMap.Enumerator.cs index 53dc33ca36..bc136ea50e 100644 --- a/src/DotNext/Collections/Specialized/TypeMap.Enumerator.cs +++ b/src/DotNext/Collections/Specialized/TypeMap.Enumerator.cs @@ -1,18 +1,20 @@ +using System.Collections; using System.Diagnostics; using System.Runtime.InteropServices; using Unsafe = System.Runtime.CompilerServices.Unsafe; namespace DotNext.Collections.Specialized; +using Generic; using static Runtime.Intrinsics; -public partial class TypeMap +public partial class TypeMap : IEnumerable { /// /// Gets the enumerator over the values. /// [StructLayout(LayoutKind.Auto)] - public struct Enumerator + public struct Enumerator : IEnumerator { private readonly Entry[] entries; private nuint index; @@ -29,6 +31,9 @@ internal Enumerator(Entry[] entries) [DebuggerBrowsable(DebuggerBrowsableState.Never)] public readonly ref TValue Current => ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(entries), index).Value!; + /// + readonly TValue IEnumerator.Current => Current; + /// /// Advances this enumerator to the next element. /// @@ -51,6 +56,9 @@ public bool MoveNext() return false; } + + /// + void IEnumerator.Reset() => index = nuint.MaxValue; } /// @@ -58,4 +66,61 @@ public bool MoveNext() /// /// The enumerator over the values. public Enumerator GetEnumerator() => new(entries); + + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator(); +} + +public partial class TypeMap +{ + /// + /// Represents an enumerator over values stored in the map. + /// + [StructLayout(LayoutKind.Auto)] + public struct Enumerator : IEnumerator + { + private readonly object?[] entries; + private int index; + private object? current; + + internal Enumerator(object?[] entries) => this.entries = entries; + + /// + public bool MoveNext() + { + while (entries is not null && index < entries.Length) + { + current = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(entries), index++); + if (current is not null) + return true; + } + + return false; + } + + /// + public readonly object Current => current ?? throw new InvalidOperationException(); + + /// + void IEnumerator.Reset() => index = 0; + } + + /// + /// Gets an enumerator over values in this map. + /// + /// The enumerator over values in this map. + public Enumerator GetEnumerator() => new(entries); + + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator().ToClassicEnumerator(); } \ No newline at end of file diff --git a/src/DotNext/Collections/Specialized/TypeMap.cs b/src/DotNext/Collections/Specialized/TypeMap.cs index 6dbb1fba14..d38aeeca09 100644 --- a/src/DotNext/Collections/Specialized/TypeMap.cs +++ b/src/DotNext/Collections/Specialized/TypeMap.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -235,7 +236,7 @@ public bool TryGetValue([MaybeNullWhen(false)] out TValue value) /// Represents fast implementation of /// which is not thread safe. /// -public class TypeMap : ITypeMap +public partial class TypeMap : ITypeMap { private object?[] entries; @@ -257,6 +258,16 @@ public TypeMap(int capacity) public TypeMap() => entries = new object?[ITypeMap.RecommendedCapacity]; + private ref object? this[int index] + { + get + { + Debug.Assert((uint)index < (uint)entries.Length); + + return ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(entries), index); + } + } + /// public void Add([DisallowNull] T value) { @@ -267,7 +278,7 @@ public void Add([DisallowNull] T value) private bool TryAdd(int index, object value) { EnsureCapacity(index); - ref var holder = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(entries), index); + ref var holder = ref this[index]; if (holder is not null) return false; @@ -282,7 +293,7 @@ public void Set([DisallowNull] T value) private void Set(int index, object value) { EnsureCapacity(index); - Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(entries), index) = value; + this[index] = value; } /// @@ -292,7 +303,7 @@ public bool Set([DisallowNull] T newValue, [NotNullWhen(true)] out T? oldValu private bool Set(int index, T newValue, [NotNullWhen(true)] out T? oldValue) { EnsureCapacity(index); - ref var holder = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(entries), index); + ref var holder = ref this[index]; bool result; oldValue = (result = holder is T) @@ -309,10 +320,8 @@ private bool Set(int index, T newValue, [NotNullWhen(true)] out T? oldValue) /// public bool Contains() { - return Contains(entries, ITypeMap.GetIndex()); - - static bool Contains(object?[] entries, int index) - => (uint)index < (uint)entries.Length && Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(entries), index) is T; + var index = ITypeMap.GetIndex(); + return (uint)index < (uint)entries.Length && this[index] is T; } private bool Remove(int index) @@ -345,6 +354,8 @@ private bool Remove(int index, [NotNullWhen(true)] out T? value) if (holder is T) { value = (T)holder; + Debug.Assert(value is not null); + holder = null; return true; } @@ -362,10 +373,12 @@ private bool TryGetValue(int index, [NotNullWhen(true)] out T? value) { if ((uint)index < (uint)entries.Length) { - var holder = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(entries), index); + var holder = this[index]; if (holder is T) { value = (T)holder; + Debug.Assert(value is not null); + return true; } } @@ -399,7 +412,7 @@ private ref T GetValueRefOrAddDefault(int index, out bool exists) where T : struct { EnsureCapacity(index); - ref var holder = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(entries), index); + ref var holder = ref this[index]; if (holder is T) { exists = true; diff --git a/src/DotNext/DelegateHelpers.cs b/src/DotNext/DelegateHelpers.cs index ac1b7d0a10..657137d588 100644 --- a/src/DotNext/DelegateHelpers.cs +++ b/src/DotNext/DelegateHelpers.cs @@ -88,4 +88,112 @@ public static EventHandler Contravariant(this EventHandler h public static TDelegate ChangeType(this Delegate d) where TDelegate : Delegate => d is TDelegate ? Unsafe.As(d) : ChangeType(d, new EmptyTargetRewriter()); + + /// + /// Converts action to async delegate. + /// + /// Synchronous action. + /// The type of the argument to be passed to the action. + /// The asynchronous function that wraps . + /// is . + public static Func ToAsync(this Action action) + { + ArgumentNullException.ThrowIfNull(action); + + return action.Invoke; + } + + private static ValueTask Invoke(this Action action, T arg, CancellationToken token) + { + ValueTask task; + if (token.IsCancellationRequested) + { + task = ValueTask.FromCanceled(token); + } + else + { + task = ValueTask.CompletedTask; + try + { + action.Invoke(arg); + } + catch (Exception e) + { + task = ValueTask.FromException(e); + } + } + + return task; + } + + /// + /// Converts action to async delegate. + /// + /// Synchronous action. + /// The asynchronous function that wraps . + /// is . + public static Func ToAsync(this Action action) + { + ArgumentNullException.ThrowIfNull(action); + + return action.Invoke; + } + + private static ValueTask Invoke(this Action action, CancellationToken token) + { + ValueTask task; + if (token.IsCancellationRequested) + { + task = ValueTask.FromCanceled(token); + } + else + { + task = ValueTask.CompletedTask; + try + { + action.Invoke(); + } + catch (Exception e) + { + task = ValueTask.FromException(e); + } + } + + return task; + } + + /// + /// Creates a delegate that hides the return value. + /// + /// The function. + /// The type of the result. + /// The action that invokes the same method as . + /// is . + public static Action HideReturnValue(this Func func) + { + ArgumentNullException.ThrowIfNull(func); + + return func.InvokeNoReturn; + } + + private static void InvokeNoReturn(this Func func) + => func.Invoke(); + + /// + /// Creates a delegate that hides the return value. + /// + /// The function. + /// The type of the result. + /// The type of the argument to be passed to the action. + /// The action that invokes the same method as . + /// is . + public static Action HideReturnValue(this Func func) + { + ArgumentNullException.ThrowIfNull(func); + + return func.InvokeNoReturn; + } + + private static void InvokeNoReturn(this Func func, T arg) + => func.Invoke(arg); } \ No newline at end of file diff --git a/src/DotNext/DotNext.csproj b/src/DotNext/DotNext.csproj index dd3db75c39..2e4e5b6de6 100644 --- a/src/DotNext/DotNext.csproj +++ b/src/DotNext/DotNext.csproj @@ -11,7 +11,7 @@ .NET Foundation and Contributors .NEXT Family of Libraries - 5.12.1 + 5.13.0 DotNext MIT diff --git a/src/DotNext/Runtime/ValueReference.cs b/src/DotNext/Runtime/ValueReference.cs index 9d85cbbb66..834ecd8884 100644 --- a/src/DotNext/Runtime/ValueReference.cs +++ b/src/DotNext/Runtime/ValueReference.cs @@ -7,6 +7,8 @@ namespace DotNext.Runtime; +using Runtime.CompilerServices; + /// /// Represents a mutable reference to the field. /// @@ -17,7 +19,9 @@ namespace DotNext.Runtime; [EditorBrowsable(EditorBrowsableState.Advanced)] public readonly struct ValueReference(object owner, ref T fieldRef) : IEquatable>, - IEqualityOperators, ValueReference, bool> + IEqualityOperators, ValueReference, bool>, + ISupplier, + IConsumer { private readonly nint offset = RawData.GetOffset(owner, in fieldRef); @@ -76,8 +80,47 @@ public ValueReference(ref T staticFieldRef) /// public ref T Value => ref RawData.GetObjectData(owner, offset); + /// + void IConsumer.Invoke(T value) => Value = value; + + /// + Action IFunctional>.ToDelegate() => ToAction(); + + /// + T ISupplier.Invoke() => Value; + + /// + Func IFunctional>.ToDelegate() => ToFunc(); + private bool SameObject(object? other) => ReferenceEquals(owner, other); + private Func ToFunc() + => Intrinsics.ChangeType, ReadOnlyValueReference>(in this).ToFunc(); + + private Action ToAction() + { + Action result; + + if (IsEmpty) + { + result = ThrowNullReferenceException; + } + else if (ReferenceEquals(owner, Sentinel.Instance)) + { + result = new StaticFieldAccessor(offset).SetValue; + } + else + { + IConsumer consumer = this; + result = consumer.Invoke; + } + + return result; + + [DoesNotReturn] + static void ThrowNullReferenceException(T value) => throw new NullReferenceException(); + } + /// public override string? ToString() => owner is not null ? RawData.GetObjectData(owner, offset)?.ToString() : null; @@ -126,6 +169,22 @@ public static implicit operator ReadOnlyValueReference(ValueReference refe /// The span that contains ; or empty span if is empty. public static implicit operator Span(ValueReference reference) => reference.IsEmpty ? new() : new(ref reference.Value); + + /// + /// Returns a setter for the memory location. + /// + /// A reference to a value. + /// A setter for the memory location. + public static explicit operator Action(ValueReference reference) + => reference.ToAction(); + + /// + /// Returns a getter for the memory location. + /// + /// A reference to a value. + /// A getter for the memory location. + public static explicit operator Func(ValueReference reference) + => reference.ToFunc(); } /// @@ -138,7 +197,8 @@ public static implicit operator Span(ValueReference reference) [EditorBrowsable(EditorBrowsableState.Advanced)] public readonly struct ReadOnlyValueReference(object owner, ref readonly T fieldRef) : IEquatable>, - IEqualityOperators, ReadOnlyValueReference, bool> + IEqualityOperators, ReadOnlyValueReference, bool>, + ISupplier { private readonly nint offset = RawData.GetOffset(owner, in fieldRef); @@ -179,6 +239,36 @@ public ReadOnlyValueReference(ref readonly T staticFieldRef) /// public ref readonly T Value => ref RawData.GetObjectData(owner, offset); + /// + T ISupplier.Invoke() => Value; + + /// + Func IFunctional>.ToDelegate() => ToFunc(); + + internal Func ToFunc() + { + Func result; + if (IsEmpty) + { + result = ThrowNullReferenceException; + } + else if (ReferenceEquals(owner, Sentinel.Instance)) + { + result = new StaticFieldAccessor(offset).GetValue; + } + else + { + ISupplier supplier = this; + result = supplier.Invoke; + } + + return result; + + [DoesNotReturn] + static T ThrowNullReferenceException() + => throw new NullReferenceException(); + } + private bool SameObject(object? other) => ReferenceEquals(owner, other); /// @@ -221,6 +311,14 @@ public bool Equals(ReadOnlyValueReference reference) /// The span that contains ; or empty span if is empty. public static implicit operator ReadOnlySpan(ReadOnlyValueReference reference) => reference.IsEmpty ? new() : new(in reference.Value); + + /// + /// Returns a getter for the memory location. + /// + /// A reference to a value. + /// A getter for the memory location. + public static explicit operator Func(ReadOnlyValueReference reference) + => reference.ToFunc(); } [SuppressMessage("Performance", "CA1812", Justification = "Used for reinterpret cast")] @@ -259,4 +357,11 @@ internal static ref T GetObjectData(object owner, nint offset) ref var rawData = ref Unsafe.As(owner).data; return ref Unsafe.As(ref Unsafe.Add(ref rawData, offset)); } +} + +file sealed class StaticFieldAccessor(nint offset) +{ + public T GetValue() => RawData.GetObjectData(Sentinel.Instance, offset); + + public void SetValue(T value) => RawData.GetObjectData(Sentinel.Instance, offset) = value; } \ No newline at end of file diff --git a/src/DotNext/Span.cs b/src/DotNext/Span.cs index 9644d7193f..df93e44046 100644 --- a/src/DotNext/Span.cs +++ b/src/DotNext/Span.cs @@ -1,6 +1,7 @@ using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -848,4 +849,151 @@ public static ref T Advance(this ref Span source) source = MemoryMarshal.CreateSpan(ref Unsafe.Add(ref ptr, 1), source.Length - 1); return ref ptr; } + + /// + /// Determines whether the specified value satisfies the given mask. + /// + /// The value to check. + /// The mask. + /// The type of the values. + /// if value & mask == mask; otherwise, . + public static unsafe bool CheckMask(this ReadOnlySpan value, ReadOnlySpan mask) + where T : unmanaged + { + ArgumentOutOfRangeException.ThrowIfNotEqual(value.Length, mask.Length, nameof(mask)); + + for (int maxLength = Array.MaxLength / sizeof(T), count; !value.IsEmpty; value = value.Slice(count), mask = mask.Slice(count)) + { + count = Math.Min(maxLength, value.Length); + + if (CheckMask( + ref Unsafe.As(ref MemoryMarshal.GetReference(value)), + ref Unsafe.As(ref MemoryMarshal.GetReference(mask)), + count * sizeof(T)) is false) + { + return false; + } + } + + return true; + } + + private static bool CheckMask([In] ref byte data, [In]ref byte mask, int length) + { + // iterate by Vector + if (Vector.IsHardwareAccelerated) + { + for (; length >= Vector.Count; length -= Vector.Count) + { + var dataVec = Vector.LoadUnsafe(ref data); + var maskVec = Vector.LoadUnsafe(ref mask); + + if ((dataVec & maskVec) != maskVec) + return false; + + data = ref Unsafe.Add(ref data, Vector.Count); + mask = ref Unsafe.Add(ref mask, Vector.Count); + } + } + + // iterate by nuint.Size + for (; length >= UIntPtr.Size; length -= UIntPtr.Size) + { + var dataVal = Unsafe.ReadUnaligned(ref data); + var maskVal = Unsafe.ReadUnaligned(ref mask); + + if ((dataVal & maskVal) != maskVal) + return false; + + data = ref Unsafe.Add(ref data, UIntPtr.Size); + mask = ref Unsafe.Add(ref mask, UIntPtr.Size); + } + + // iterate by byte + for (; length > 0; length -= 1) + { + var dataVal = data; + var maskVal = mask; + + if ((dataVal & maskVal) != maskVal) + return false; + + data = ref Unsafe.Add(ref data, 1); + mask = ref Unsafe.Add(ref mask, 1); + } + + return true; + } + + /// + /// Determines whether the specified value and the given mask produces non-zero bitwise AND. + /// + /// The value to check. + /// The mask. + /// The type of the values. + /// if value & mask != 0; otherwise, . + public static unsafe bool IsBitwiseAndNonZero(this ReadOnlySpan value, ReadOnlySpan mask) + where T : unmanaged + { + ArgumentOutOfRangeException.ThrowIfNotEqual(value.Length, mask.Length, nameof(mask)); + + for (int maxLength = Array.MaxLength / sizeof(T), count; !value.IsEmpty; value = value.Slice(count), mask = mask.Slice(count)) + { + count = Math.Min(maxLength, value.Length); + + if (IsBitwiseAndNonZero( + ref Unsafe.As(ref MemoryMarshal.GetReference(value)), + ref Unsafe.As(ref MemoryMarshal.GetReference(mask)), + count * sizeof(T)) is false) + { + return false; + } + } + + return true; + } + + private static bool IsBitwiseAndNonZero([In] ref byte data, [In]ref byte mask, int length) + { + // iterate by Vector + if (Vector.IsHardwareAccelerated) + { + for (; length >= Vector.Count; length -= Vector.Count) + { + var dataVec = Vector.LoadUnsafe(ref data); + var maskVec = Vector.LoadUnsafe(ref mask); + + if ((dataVec & maskVec) != Vector.Zero) + return true; + + data = ref Unsafe.Add(ref data, Vector.Count); + mask = ref Unsafe.Add(ref mask, Vector.Count); + } + } + + // iterate by nuint.Size + for (; length >= UIntPtr.Size; length -= UIntPtr.Size) + { + var dataVal = Unsafe.ReadUnaligned(ref data); + var maskVal = Unsafe.ReadUnaligned(ref mask); + + if ((dataVal & maskVal) is not 0) + return true; + + data = ref Unsafe.Add(ref data, UIntPtr.Size); + mask = ref Unsafe.Add(ref mask, UIntPtr.Size); + } + + // iterate by byte + for (; length > 0; length -= 1) + { + if ((data & mask) is not 0) + return true; + + data = ref Unsafe.Add(ref data, 1); + mask = ref Unsafe.Add(ref mask, 1); + } + + return false; + } } \ No newline at end of file diff --git a/src/DotNext/Threading/Tasks/Synchronization.ValueTask.cs b/src/DotNext/Threading/Tasks/Synchronization.ValueTask.cs index 1f8d9c4ac6..80247338a4 100644 --- a/src/DotNext/Threading/Tasks/Synchronization.ValueTask.cs +++ b/src/DotNext/Threading/Tasks/Synchronization.ValueTask.cs @@ -30,19 +30,19 @@ static ValueTask GetTask(in T tuple, int index) } /// - /// Creates a task that will complete when all of the passed tasks have completed. + /// Creates a task that will complete when all the passed tasks have completed. /// /// /// This method avoid memory allocation in the managed heap if all tasks are completed (or will be soon) at the time of calling this method. /// /// The first task to await. /// The second task to await. - /// A task that represents the completion of all of the supplied tasks. + /// A task that represents the completion of all the supplied tasks. public static ValueTask WhenAll(ValueTask task1, ValueTask task2) => WhenAll((task1, task2)); /// - /// Creates a task that will complete when all of the passed tasks have completed. + /// Creates a task that will complete when all the passed tasks have completed. /// /// /// This method avoid memory allocation in the managed heap if all tasks are completed (or will be soon) at the time of calling this method. @@ -78,7 +78,7 @@ public static ValueTask WhenAll(ValueTask task1, ValueTask task2) } /// - /// Creates a task that will complete when all of the passed tasks have completed. + /// Creates a task that will complete when all the passed tasks have completed. /// /// /// This method avoid memory allocation in the managed heap if all tasks are completed (or will be soon) at the time of calling this method. @@ -86,12 +86,12 @@ public static ValueTask WhenAll(ValueTask task1, ValueTask task2) /// The first task to await. /// The second task to await. /// The third task to await. - /// A task that represents the completion of all of the supplied tasks. + /// A task that represents the completion of all the supplied tasks. public static ValueTask WhenAll(ValueTask task1, ValueTask task2, ValueTask task3) => WhenAll((task1, task2, task3)); /// - /// Creates a task that will complete when all of the passed tasks have completed. + /// Creates a task that will complete when all the passed tasks have completed. /// /// /// This method avoid memory allocation in the managed heap if all tasks are completed (or will be soon) at the time of calling this method. @@ -138,7 +138,7 @@ public static ValueTask WhenAll(ValueTask task1, ValueTask task2, ValueTask task } /// - /// Creates a task that will complete when all of the passed tasks have completed. + /// Creates a task that will complete when all the passed tasks have completed. /// /// /// This method avoid memory allocation in the managed heap if all tasks are completed (or will be soon) at the time of calling this method. @@ -147,12 +147,12 @@ public static ValueTask WhenAll(ValueTask task1, ValueTask task2, ValueTask task /// The second task to await. /// The third task to await. /// The fourth task to await. - /// A task that represents the completion of all of the supplied tasks. + /// A task that represents the completion of all the supplied tasks. public static ValueTask WhenAll(ValueTask task1, ValueTask task2, ValueTask task3, ValueTask task4) => WhenAll((task1, task2, task3, task4)); /// - /// Creates a task that will complete when all of the passed tasks have completed. + /// Creates a task that will complete when all the passed tasks have completed. /// /// /// This method avoid memory allocation in the managed heap if all tasks are completed (or will be soon) at the time of calling this method. @@ -210,7 +210,7 @@ public static ValueTask WhenAll(ValueTask task1, ValueTask task2, ValueTask task } /// - /// Creates a task that will complete when all of the passed tasks have completed. + /// Creates a task that will complete when all the passed tasks have completed. /// /// /// This method avoid memory allocation in the managed heap if all tasks are completed (or will be soon) at the time of calling this method. @@ -220,12 +220,12 @@ public static ValueTask WhenAll(ValueTask task1, ValueTask task2, ValueTask task /// The third task to await. /// The fourth task to await. /// The fifth task to await. - /// A task that represents the completion of all of the supplied tasks. + /// A task that represents the completion of all the supplied tasks. public static ValueTask WhenAll(ValueTask task1, ValueTask task2, ValueTask task3, ValueTask task4, ValueTask task5) => WhenAll((task1, task2, task3, task4, task5)); /// - /// Creates a task that will complete when all of the passed tasks have completed. + /// Creates a task that will complete when all the passed tasks have completed. /// /// /// This method avoid memory allocation in the managed heap if all tasks are completed (or will be soon) at the time of calling this method. diff --git a/src/cluster/DotNext.AspNetCore.Cluster/DotNext.AspNetCore.Cluster.csproj b/src/cluster/DotNext.AspNetCore.Cluster/DotNext.AspNetCore.Cluster.csproj index 47767e3afa..88b3d71e09 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.12.0 + 5.13.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 86834c4e23..1a546354e2 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.12.0 + 5.13.0 .NET Foundation and Contributors .NEXT Family of Libraries