From 6f48311450473b5a523b5f4c58e32a66409425e7 Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 12 Jan 2020 18:19:58 +0100 Subject: [PATCH 01/30] Add MicroBenchmarks - Bump global.json to 3.1 - Move benchmark projects to dedicated directory --- .gitignore | 3 ++ .../Benchmark/Benchmark.cs | 0 .../Benchmark/Benchmark.csproj | 0 .../MicroBenchmarks/MicroBenchmarks.csproj | 20 +++++++++++++ src/Benchmarks/MicroBenchmarks/Program.cs | 30 +++++++++++++++++++ src/NATS.Client/NATS.Client.csproj | 4 ++- src/NATS.sln | 19 ++++++++++-- 7 files changed, 72 insertions(+), 4 deletions(-) rename src/{Samples => Benchmarks}/Benchmark/Benchmark.cs (100%) rename src/{Samples => Benchmarks}/Benchmark/Benchmark.csproj (100%) create mode 100644 src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj create mode 100644 src/Benchmarks/MicroBenchmarks/Program.cs diff --git a/.gitignore b/.gitignore index 6659ca278..ce8e89617 100644 --- a/.gitignore +++ b/.gitignore @@ -251,3 +251,6 @@ paket-files/ # JetBrains Rider .idea/ *.sln.iml + +# Benchmark artifacts +**/BenchmarkDotNet.Artifacts/ diff --git a/src/Samples/Benchmark/Benchmark.cs b/src/Benchmarks/Benchmark/Benchmark.cs similarity index 100% rename from src/Samples/Benchmark/Benchmark.cs rename to src/Benchmarks/Benchmark/Benchmark.cs diff --git a/src/Samples/Benchmark/Benchmark.csproj b/src/Benchmarks/Benchmark/Benchmark.csproj similarity index 100% rename from src/Samples/Benchmark/Benchmark.csproj rename to src/Benchmarks/Benchmark/Benchmark.csproj diff --git a/src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj b/src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj new file mode 100644 index 000000000..198310900 --- /dev/null +++ b/src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj @@ -0,0 +1,20 @@ + + + + Exe + netcoreapp3.1;net462 + AnyCPU + pdbonly + true + Debug;Release;LinuxRelease;LinuxDebug + + + + + + + + + + + diff --git a/src/Benchmarks/MicroBenchmarks/Program.cs b/src/Benchmarks/MicroBenchmarks/Program.cs new file mode 100644 index 000000000..053ddabc4 --- /dev/null +++ b/src/Benchmarks/MicroBenchmarks/Program.cs @@ -0,0 +1,30 @@ +using System; +using System.Security.Cryptography; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Order; +using BenchmarkDotNet.Running; +using NATS.Client; + +namespace MicroBenchmarks +{ + [DisassemblyDiagnoser(printAsm: true, printSource: true)] + [Orderer(SummaryOrderPolicy.FastestToSlowest)] + [MemoryDiagnoser] + [RPlotExporter] + [MarkdownExporterAttribute.GitHub] + [SimpleJob(RuntimeMoniker.Net462)] + [SimpleJob(RuntimeMoniker.NetCoreApp31)] + public class RandomBenchmark + { + + } + + public class Program + { + public static void Main(string[] args) + { + var summary = BenchmarkRunner.Run(); + } + } +} diff --git a/src/NATS.Client/NATS.Client.csproj b/src/NATS.Client/NATS.Client.csproj index 0a4a2e3ee..468b248c8 100644 --- a/src/NATS.Client/NATS.Client.csproj +++ b/src/NATS.Client/NATS.Client.csproj @@ -12,6 +12,8 @@ https://github.com/nats-io/nats.net/releases Apache-2.0 CNCF NATS Messaging Cloud Publish Subscribe PubSub + pdbonly + true @@ -20,7 +22,7 @@ true ..\NATS.Client.snk - + True diff --git a/src/NATS.sln b/src/NATS.sln index 7dd93f3fa..b4b4e6639 100644 --- a/src/NATS.sln +++ b/src/NATS.sln @@ -28,12 +28,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Replier", "Samples\Replier\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueueGroup", "Samples\QueueGroup\QueueGroup.csproj", "{2F052465-A58D-4670-8600-24CCB2F220A5}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmark", "Samples\Benchmark\Benchmark.csproj", "{8BC71128-1FC5-4126-9582-41223E96C2CA}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RxSample", "Samples\RxSample\RxSample.csproj", "{C21CBF59-9884-436C-8A24-0248F8598979}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinFormsSample", "Samples\WinFormsSample\WinFormsSample.csproj", "{10ED0146-1701-4BC9-9E26-824B22D18CB8}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmark", "Benchmarks\Benchmark\Benchmark.csproj", "{8BC71128-1FC5-4126-9582-41223E96C2CA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroBenchmarks", "Benchmarks\MicroBenchmarks\MicroBenchmarks.csproj", "{8D181EED-3FF7-4CC0-9C85-331EF20B7688}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarks", "Benchmarks", "{A4CB6D96-6357-40DC-B198-50568A7E45C7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -84,6 +88,14 @@ Global {10ED0146-1701-4BC9-9E26-824B22D18CB8}.Debug|Any CPU.Build.0 = Debug|Any CPU {10ED0146-1701-4BC9-9E26-824B22D18CB8}.Release|Any CPU.ActiveCfg = Release|Any CPU {10ED0146-1701-4BC9-9E26-824B22D18CB8}.Release|Any CPU.Build.0 = Release|Any CPU + {8D181EED-3FF7-4CC0-9C85-331EF20B7688}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D181EED-3FF7-4CC0-9C85-331EF20B7688}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D181EED-3FF7-4CC0-9C85-331EF20B7688}.LinuxDebug|Any CPU.ActiveCfg = LinuxDebug|Any CPU + {8D181EED-3FF7-4CC0-9C85-331EF20B7688}.LinuxDebug|Any CPU.Build.0 = LinuxDebug|Any CPU + {8D181EED-3FF7-4CC0-9C85-331EF20B7688}.LinuxRelease|Any CPU.ActiveCfg = LinuxRelease|Any CPU + {8D181EED-3FF7-4CC0-9C85-331EF20B7688}.LinuxRelease|Any CPU.Build.0 = LinuxRelease|Any CPU + {8D181EED-3FF7-4CC0-9C85-331EF20B7688}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D181EED-3FF7-4CC0-9C85-331EF20B7688}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -96,9 +108,10 @@ Global {725F4B11-9275-4966-AD46-383021EEC152} = {776C2E80-958B-4C0D-BCC4-67D39DB4570B} {B5C69B45-658C-431D-9D0D-C9D2D098248A} = {776C2E80-958B-4C0D-BCC4-67D39DB4570B} {2F052465-A58D-4670-8600-24CCB2F220A5} = {776C2E80-958B-4C0D-BCC4-67D39DB4570B} - {8BC71128-1FC5-4126-9582-41223E96C2CA} = {776C2E80-958B-4C0D-BCC4-67D39DB4570B} + {8BC71128-1FC5-4126-9582-41223E96C2CA} = {A4CB6D96-6357-40DC-B198-50568A7E45C7} {C21CBF59-9884-436C-8A24-0248F8598979} = {776C2E80-958B-4C0D-BCC4-67D39DB4570B} {10ED0146-1701-4BC9-9E26-824B22D18CB8} = {776C2E80-958B-4C0D-BCC4-67D39DB4570B} + {8D181EED-3FF7-4CC0-9C85-331EF20B7688} = {A4CB6D96-6357-40DC-B198-50568A7E45C7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0972F03C-E4DF-483A-B767-9216F4434F11} From dceef0df8863b9642b0ae6eebe3b2fdb7e1fe8d5 Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 12 Jan 2020 18:43:39 +0100 Subject: [PATCH 02/30] Make internals visible to test and benchmarks --- src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj | 7 ++++++- src/NATS.Client/Properties/AssemblyInfo.cs | 7 +++++++ src/Tests/UnitTests/UnitTests.csproj | 5 +++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 src/NATS.Client/Properties/AssemblyInfo.cs diff --git a/src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj b/src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj index 198310900..0d684f8d6 100644 --- a/src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj +++ b/src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,6 +9,11 @@ Debug;Release;LinuxRelease;LinuxDebug + + true + ..\..\NATS.Client.snk + + diff --git a/src/NATS.Client/Properties/AssemblyInfo.cs b/src/NATS.Client/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..c089b89a3 --- /dev/null +++ b/src/NATS.Client/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Runtime.CompilerServices; + +// Since NATS.Client is signed, friends must be signed too. +// We use the same SNK for simplicity. +// https://docs.microsoft.com/en-us/dotnet/standard/assembly/create-signed-friend +[assembly: InternalsVisibleTo("UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100db7da1f2f89089327b47d26d69666fad20861f24e9acdb13965fb6c64dfee8da589b495df37a75e934ddbacb0752a42c40f3dbc79614eec9bb2a0b6741f9e2ad2876f95e74d54c23eef0063eb4efb1e7d824ee8a695b647c113c92834f04a3a83fb60f435814ddf5c4e5f66a168139c4c1b1a50a3e60c164d180e265b1f000cd")] +[assembly: InternalsVisibleTo("MicroBenchmarks, PublicKey=0024000004800000940000000602000000240000525341310004000001000100db7da1f2f89089327b47d26d69666fad20861f24e9acdb13965fb6c64dfee8da589b495df37a75e934ddbacb0752a42c40f3dbc79614eec9bb2a0b6741f9e2ad2876f95e74d54c23eef0063eb4efb1e7d824ee8a695b647c113c92834f04a3a83fb60f435814ddf5c4e5f66a168139c4c1b1a50a3e60c164d180e265b1f000cd")] diff --git a/src/Tests/UnitTests/UnitTests.csproj b/src/Tests/UnitTests/UnitTests.csproj index bb978b928..fab8b4641 100644 --- a/src/Tests/UnitTests/UnitTests.csproj +++ b/src/Tests/UnitTests/UnitTests.csproj @@ -5,6 +5,11 @@ true + + true + ..\..\NATS.Client.snk + + From a2d754176641708f7469029f9ba63068d8c15f06 Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 12 Jan 2020 18:52:54 +0100 Subject: [PATCH 03/30] Add baseline benchmarks --- src/Benchmarks/MicroBenchmarks/Program.cs | 10 +++++++--- src/NATS.Client/Properties/AssemblyInfo.cs | 6 ++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Benchmarks/MicroBenchmarks/Program.cs b/src/Benchmarks/MicroBenchmarks/Program.cs index 053ddabc4..9799defbd 100644 --- a/src/Benchmarks/MicroBenchmarks/Program.cs +++ b/src/Benchmarks/MicroBenchmarks/Program.cs @@ -1,6 +1,4 @@ -using System; -using System.Security.Cryptography; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Order; using BenchmarkDotNet.Running; @@ -17,7 +15,13 @@ namespace MicroBenchmarks [SimpleJob(RuntimeMoniker.NetCoreApp31)] public class RandomBenchmark { + private static readonly NUID _nuid = NUID.Instance; + [BenchmarkCategory("NextNuid"), Benchmark(Baseline = true)] + public string NextNuid() => _nuid.Next; + + [BenchmarkCategory("RandomizePrefix"), Benchmark(Baseline = true)] + public void RandomizePrefix() => _nuid.RandomizePrefix(); } public class Program diff --git a/src/NATS.Client/Properties/AssemblyInfo.cs b/src/NATS.Client/Properties/AssemblyInfo.cs index c089b89a3..467223a58 100644 --- a/src/NATS.Client/Properties/AssemblyInfo.cs +++ b/src/NATS.Client/Properties/AssemblyInfo.cs @@ -3,5 +3,11 @@ // Since NATS.Client is signed, friends must be signed too. // We use the same SNK for simplicity. // https://docs.microsoft.com/en-us/dotnet/standard/assembly/create-signed-friend +#if DEBUG +[assembly: InternalsVisibleTo("UnitTests")] +[assembly: InternalsVisibleTo("MicroBenchmarks")] + +#else [assembly: InternalsVisibleTo("UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100db7da1f2f89089327b47d26d69666fad20861f24e9acdb13965fb6c64dfee8da589b495df37a75e934ddbacb0752a42c40f3dbc79614eec9bb2a0b6741f9e2ad2876f95e74d54c23eef0063eb4efb1e7d824ee8a695b647c113c92834f04a3a83fb60f435814ddf5c4e5f66a168139c4c1b1a50a3e60c164d180e265b1f000cd")] [assembly: InternalsVisibleTo("MicroBenchmarks, PublicKey=0024000004800000940000000602000000240000525341310004000001000100db7da1f2f89089327b47d26d69666fad20861f24e9acdb13965fb6c64dfee8da589b495df37a75e934ddbacb0752a42c40f3dbc79614eec9bb2a0b6741f9e2ad2876f95e74d54c23eef0063eb4efb1e7d824ee8a695b647c113c92834f04a3a83fb60f435814ddf5c4e5f66a168139c4c1b1a50a3e60c164d180e265b1f000cd")] +#endif \ No newline at end of file From 5155714d43b002e9e6ce811e65567394cc6e630a Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 12 Jan 2020 23:43:51 +0100 Subject: [PATCH 04/30] Implement Nuid --- src/Benchmarks/MicroBenchmarks/Program.cs | 12 +- src/NATS.Client/Internals/Nuid.cs | 151 ++++++++++++++++++++++ src/Tests/UnitTests/Internals/TestNuid.cs | 136 +++++++++++++++++++ 3 files changed, 295 insertions(+), 4 deletions(-) create mode 100644 src/NATS.Client/Internals/Nuid.cs create mode 100644 src/Tests/UnitTests/Internals/TestNuid.cs diff --git a/src/Benchmarks/MicroBenchmarks/Program.cs b/src/Benchmarks/MicroBenchmarks/Program.cs index 9799defbd..dac726b80 100644 --- a/src/Benchmarks/MicroBenchmarks/Program.cs +++ b/src/Benchmarks/MicroBenchmarks/Program.cs @@ -3,6 +3,7 @@ using BenchmarkDotNet.Order; using BenchmarkDotNet.Running; using NATS.Client; +using NATS.Client.Internals; namespace MicroBenchmarks { @@ -16,12 +17,15 @@ namespace MicroBenchmarks public class RandomBenchmark { private static readonly NUID _nuid = NUID.Instance; + private static readonly Nuid _newNuid = new Nuid(); - [BenchmarkCategory("NextNuid"), Benchmark(Baseline = true)] - public string NextNuid() => _nuid.Next; + [BenchmarkCategory("NextNuid")] + [Benchmark(Baseline = true)] + public string NUIDNext() => _nuid.Next; + + [BenchmarkCategory("NextNuid"), Benchmark] + public string NextNuid() => _newNuid.GetNext(); - [BenchmarkCategory("RandomizePrefix"), Benchmark(Baseline = true)] - public void RandomizePrefix() => _nuid.RandomizePrefix(); } public class Program diff --git a/src/NATS.Client/Internals/Nuid.cs b/src/NATS.Client/Internals/Nuid.cs new file mode 100644 index 000000000..d4ffbe31f --- /dev/null +++ b/src/NATS.Client/Internals/Nuid.cs @@ -0,0 +1,151 @@ +using System; +using System.Security.Cryptography; + +namespace NATS.Client.Internals +{ + internal sealed class Nuid + { + private const int PREFIX_LENGTH = 12; + private const int SEQUENTIAL_LENGTH = 10; + private const int NUID_LENGTH = PREFIX_LENGTH + SEQUENTIAL_LENGTH; + private const int MIN_INCREMENT = 33; + private const int MAX_INCREMENT = 333; + private const long MAX_SEQUENTIAL = 1152921504606846976; // 64^10, 60 bits + private const int BASE = 64; + + private static readonly char[] _characters = new char[BASE]{ + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', + 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', + 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', + 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', + 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', + 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', '_', '-' + }; + + private readonly object _nuidLock = new object(); + + private readonly Random _rng = new Random(); + private readonly object _rngLock = new object(); + private readonly RandomNumberGenerator _cryptoRng; + private readonly object _cryptRngLock = new object(); + + private char[] _prefix = new char[PREFIX_LENGTH]; + private int _increment; + private long _sequential; + + /// + /// Initializes a new instance of . + /// + /// + /// This constructor is intended to be used from unit tests and + /// benchmarks only. For production use use instead. + /// + /// A cryptographically strong random number generator. + /// The intitial sequential. + /// The initial increment. + internal Nuid(RandomNumberGenerator rng = null, long? sequential = null, int? increment = null) + { + if (rng is null) + _cryptoRng = RandomNumberGenerator.Create(); + else + _cryptoRng = rng; +#if NET45 + // Instantiating System.Random multiple times in quick succession without a + // proper seed may result in instances that yield identical sequences on .NET FX. + // See https://docs.microsoft.com/en-us/dotnet/api/system.random?view=netframework-4.8#instantiating-the-random-number-generator + // and https://docs.microsoft.com/en-us/dotnet/api/system.random?view=netframework-4.8#avoiding-multiple-instantiations + var seedBytes = new byte[4]; + _cryptoRng.GetBytes(seedBytes); + _rng = new Random(BitConverter.ToInt32(seedBytes, 0)); +#endif + if (sequential is null) + _sequential = GetSequential(); + else + _sequential = sequential.Value; + + if (increment is null) + _increment = GetIncrement(); + else + _increment = increment.Value; + + SetPrefix(); + } + + /// + /// Initializes a new instance of . + /// + internal Nuid() : this(null) {} + + /// + /// Returns a random NUID string. + /// + /// The NUID + internal string GetNext() + { + var sequential = 0L; + var nuidBuffer = new char[NUID_LENGTH]; + lock (_nuidLock) + { + sequential = _sequential += _increment; + if(_sequential > MAX_SEQUENTIAL) + { + SetPrefix(); + sequential = _sequential = GetSequential(); + _increment = GetIncrement(); + } + Array.Copy(_prefix, nuidBuffer, _prefix.Length); + } + + for(var i = PREFIX_LENGTH; i < nuidBuffer.Length; i++) + { + nuidBuffer[i] = _characters[sequential % _characters.Length]; + sequential /= BASE; + } + + return new string(nuidBuffer); + } + + private int GetIncrement() + { + lock (_rngLock) + { + return _rng.Next(MIN_INCREMENT, MAX_INCREMENT); + } + } + + private long GetSequential() + { + var randomBytes = new byte[8]; + ulong seq; + do + { + lock (_rngLock) + { + _rng.NextBytes(randomBytes); + } + seq = BitConverter.ToUInt64(randomBytes, 0); + + } while (seq > ulong.MaxValue - ((ulong.MaxValue % MAX_SEQUENTIAL) + 1) % MAX_SEQUENTIAL); + // Limit seq to MAX_SEQUENTIAL, see https://stackoverflow.com/a/13095144 + return (long)(seq % MAX_SEQUENTIAL); + } + + //TODO: Synchronize access to _prefix + private void SetPrefix() + { + var randomBytes = new byte[PREFIX_LENGTH]; + lock (_cryptRngLock) + { + _cryptoRng.GetBytes(randomBytes); + } + + for(var i = 0; i < randomBytes.Length; i++) + { + _prefix[i] = _characters[randomBytes[i] % _characters.Length]; + } + + } + } +} diff --git a/src/Tests/UnitTests/Internals/TestNuid.cs b/src/Tests/UnitTests/Internals/TestNuid.cs new file mode 100644 index 000000000..849f11a70 --- /dev/null +++ b/src/Tests/UnitTests/Internals/TestNuid.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using NATS.Client.Internals; +using Xunit; + +namespace UnitTests.Internals +{ + public class TestNuid + { + [Fact] + public void GetNextNuid_ReturnsNuidOfLength22() + { + // Arrange + var nuid = new Nuid(); + + //Act + var result = nuid.GetNext(); + + // Assert + Assert.Equal(22, result.Length); + } + + [Fact] + public void GetNextNuid_ReturnsDifferentNuidEachTime() + { + // Arrange + var nuid = new Nuid(); + + // Act + var firstNuid = nuid.GetNext(); + var secondNuid = nuid.GetNext(); + + // Assert + Assert.NotEqual(firstNuid, secondNuid); + } + + [Fact] + public void GetNextNuid_PrefixIsConstant() + { + // Arrange + var nuid = new Nuid(); + + // Act + var firstNuid = nuid.GetNext().Substring(0, 12); + var secondNuid = nuid.GetNext().Substring(0, 12); + + // Assert + Assert.Equal(firstNuid, secondNuid); + } + + [Fact] + public void GetNextNuid_ContainsOnlyValidCharacters() + { + // Arrange + var nuid = new Nuid(); + + // Act + var result = nuid.GetNext(); + + // Assert + Assert.Matches("[A-z0-9_-]{22}", result); + } + + [Fact] + public void GetNextNuid_PrefixRenewed() + { + // Arrange + var increment = 100; + var maxSequential = 1152921504606846976 - increment; + var nuid = new Nuid(RandomNumberGenerator.Create(), maxSequential, increment); + + // Act + var firstNuid = nuid.GetNext().Substring(0, 12); + var secondNuid = nuid.GetNext().Substring(0, 12); + + // Assert + Assert.NotEqual(firstNuid, secondNuid); + } + + [Fact] + public void GetNextNuid_PrefixAsExpected() + { + // Arrange + var rngBytes = new byte[12] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; + var rng = new ControlledRng(new Queue(new byte[][] { rngBytes, rngBytes })); + + var nuid = new Nuid(rng); + + // Act + var prefix = nuid.GetNext().Substring(0, 12); + + // Assert + Assert.Equal("0123456789AB", prefix); + } + + [Fact] + public void NuidInitialization_RngInvokedOnce() + { + // Arrange + var rngBytes = new byte[12] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; + var rng = new ControlledRng(new Queue(new byte[][] { rngBytes, rngBytes })); + + // Act + var nuid = new Nuid(rng); ; + + // Assert +#if NET452 + Assert.Equal(2, rng.GetBytesInvocations); +#else + Assert.Equal(1, rng.GetBytesInvocations); +#endif + } + + private class ControlledRng : RandomNumberGenerator + { + public int GetBytesInvocations = 0; + private Queue _bytes; + + public ControlledRng(Queue bytes) + { + _bytes = bytes; + } + + public override void GetBytes(byte[] data) + { + var nextBytes = _bytes.Dequeue(); + if (nextBytes.Length < data.Length) + throw new InvalidOperationException($"Lenght of {nameof(data)} is {data.Length}, length of {nameof(nextBytes)} is {nextBytes.Length}"); + + Array.Copy(nextBytes, data, data.Length); + GetBytesInvocations++; + } + } + } +} From e949c6bf9de57afc4f1d8511aef473a055e3dd16 Mon Sep 17 00:00:00 2001 From: jasperd Date: Mon, 13 Jan 2020 20:11:37 +0100 Subject: [PATCH 05/30] Fix MicroBenchmarks on Linux --- src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj b/src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj index 0d684f8d6..cb0596608 100644 --- a/src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj +++ b/src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj @@ -2,13 +2,14 @@ Exe - netcoreapp3.1;net462 + netcoreapp3.1 + netcoreapp3.1;net462 AnyCPU pdbonly true Debug;Release;LinuxRelease;LinuxDebug - + true ..\..\NATS.Client.snk From 955254d95fd9366ca47d989d9ab25ab4f92b830b Mon Sep 17 00:00:00 2001 From: jasperd Date: Mon, 13 Jan 2020 20:15:05 +0100 Subject: [PATCH 06/30] Improve performance by ~40% --- src/NATS.Client/Internals/Nuid.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/NATS.Client/Internals/Nuid.cs b/src/NATS.Client/Internals/Nuid.cs index d4ffbe31f..3728cb466 100644 --- a/src/NATS.Client/Internals/Nuid.cs +++ b/src/NATS.Client/Internals/Nuid.cs @@ -13,15 +13,15 @@ internal sealed class Nuid private const long MAX_SEQUENTIAL = 1152921504606846976; // 64^10, 60 bits private const int BASE = 64; - private static readonly char[] _characters = new char[BASE]{ - '0', '1', '2', '3', '4', '5', '6', '7', - '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', - 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', - 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', - 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', - 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', - 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', - 'u', 'v', 'w', 'x', 'y', 'z', '_', '-' + private static readonly byte[] _characters = new byte[BASE]{ + (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7', + (byte)'8', (byte)'9', (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', + (byte)'G', (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', + (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', (byte)'V', + (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', (byte)'a', (byte)'b', (byte)'c', (byte)'d', + (byte)'e', (byte)'f', (byte)'g', (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', + (byte)'m', (byte)'n', (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', + (byte)'u', (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', (byte)'_', (byte)'-' }; private readonly object _nuidLock = new object(); @@ -98,9 +98,9 @@ internal string GetNext() Array.Copy(_prefix, nuidBuffer, _prefix.Length); } - for(var i = PREFIX_LENGTH; i < nuidBuffer.Length; i++) + for(var i = PREFIX_LENGTH; i < NUID_LENGTH; i++) { - nuidBuffer[i] = _characters[sequential % _characters.Length]; + nuidBuffer[i] = (char)_characters[sequential % BASE]; sequential /= BASE; } @@ -143,7 +143,7 @@ private void SetPrefix() for(var i = 0; i < randomBytes.Length; i++) { - _prefix[i] = _characters[randomBytes[i] % _characters.Length]; + _prefix[i] = (char)_characters[randomBytes[i] % BASE]; } } From bcea8d9560b75ffbf6606a109376d720fa75801a Mon Sep 17 00:00:00 2001 From: jasperd Date: Wed, 15 Jan 2020 23:44:54 +0100 Subject: [PATCH 07/30] Use base64 encoding and optimize div and mod --- src/NATS.Client/Internals/Nuid.cs | 91 ++++++++++++++--------- src/Tests/UnitTests/Internals/TestNuid.cs | 40 ++++++++-- 2 files changed, 91 insertions(+), 40 deletions(-) diff --git a/src/NATS.Client/Internals/Nuid.cs b/src/NATS.Client/Internals/Nuid.cs index 3728cb466..9406e4bb6 100644 --- a/src/NATS.Client/Internals/Nuid.cs +++ b/src/NATS.Client/Internals/Nuid.cs @@ -5,25 +5,25 @@ namespace NATS.Client.Internals { internal sealed class Nuid { - private const int PREFIX_LENGTH = 12; - private const int SEQUENTIAL_LENGTH = 10; - private const int NUID_LENGTH = PREFIX_LENGTH + SEQUENTIAL_LENGTH; + private const uint PREFIX_LENGTH = 12; + private const uint SEQUENTIAL_LENGTH = 10; + private const uint NUID_LENGTH = PREFIX_LENGTH + SEQUENTIAL_LENGTH; private const int MIN_INCREMENT = 33; private const int MAX_INCREMENT = 333; - private const long MAX_SEQUENTIAL = 1152921504606846976; // 64^10, 60 bits + private const long MAX_SEQUENTIAL = 0x1000_0000_0000_0000; //64^10 - 1 private const int BASE = 64; - private static readonly byte[] _characters = new byte[BASE]{ - (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7', - (byte)'8', (byte)'9', (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', - (byte)'G', (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', - (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', (byte)'V', - (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', (byte)'a', (byte)'b', (byte)'c', (byte)'d', - (byte)'e', (byte)'f', (byte)'g', (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', - (byte)'m', (byte)'n', (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', - (byte)'u', (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', (byte)'_', (byte)'-' + private static readonly byte[] _digits = new byte[BASE]{ + (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', (byte)'H', + (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', (byte)'O', (byte)'P', + (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', (byte)'V', (byte)'W', (byte)'X', + (byte)'Y', (byte)'Z', (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', + (byte)'g', (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', + (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', (byte)'v', + (byte)'w', (byte)'x', (byte)'y', (byte)'z', (byte)'0', (byte)'1', (byte)'2', (byte)'3', + (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/', }; - + private readonly object _nuidLock = new object(); private readonly Random _rng = new Random(); @@ -31,7 +31,7 @@ internal sealed class Nuid private readonly RandomNumberGenerator _cryptoRng; private readonly object _cryptRngLock = new object(); - private char[] _prefix = new char[PREFIX_LENGTH]; + private byte[] _prefix = new byte[PREFIX_LENGTH]; private int _increment; private long _sequential; @@ -43,7 +43,7 @@ internal sealed class Nuid /// benchmarks only. For production use use instead. /// /// A cryptographically strong random number generator. - /// The intitial sequential. + /// The initial sequential. /// The initial increment. internal Nuid(RandomNumberGenerator rng = null, long? sequential = null, int? increment = null) { @@ -51,6 +51,8 @@ internal Nuid(RandomNumberGenerator rng = null, long? sequential = null, int? in _cryptoRng = RandomNumberGenerator.Create(); else _cryptoRng = rng; + + //TODO: What about netstandard when not running on CoreCLR?! #if NET45 // Instantiating System.Random multiple times in quick succession without a // proper seed may result in instances that yield identical sequences on .NET FX. @@ -79,9 +81,12 @@ internal Nuid(RandomNumberGenerator rng = null, long? sequential = null, int? in internal Nuid() : this(null) {} /// - /// Returns a random NUID string. + /// Returns a random Nuid string. /// - /// The NUID + /// + /// A Nuid is a 132 bit pseudo-random integer encoded as a base64 string + /// + /// The Nuid internal string GetNext() { var sequential = 0L; @@ -89,19 +94,28 @@ internal string GetNext() lock (_nuidLock) { sequential = _sequential += _increment; - if(_sequential > MAX_SEQUENTIAL) + if(_sequential >= MAX_SEQUENTIAL) { SetPrefix(); sequential = _sequential = GetSequential(); _increment = GetIncrement(); } - Array.Copy(_prefix, nuidBuffer, _prefix.Length); + + // For small arrays this is way faster than Array.Copy and still faster than Buffer.BlockCopy + for (var i = 0; i < PREFIX_LENGTH; i++) + { + nuidBuffer[i] = (char)_prefix[i]; + } } for(var i = PREFIX_LENGTH; i < NUID_LENGTH; i++) { - nuidBuffer[i] = (char)_characters[sequential % BASE]; - sequential /= BASE; + // We operate on unsigned integers and BASE is a power of two + // therefore we can optimize sequential % BASE to sequential & (BASE - 1) + nuidBuffer[i] = (char)_digits[(int)sequential & (BASE - 1)]; + // BASE is 64 = 2^6 and sequential >= 0 + // therefore we can optimize sequential / BASE to sequential >> 6 + sequential >>= 6; } return new string(nuidBuffer); @@ -118,21 +132,27 @@ private int GetIncrement() private long GetSequential() { var randomBytes = new byte[8]; - ulong seq; - do + const ulong s = ulong.MaxValue - ((ulong.MaxValue % MAX_SEQUENTIAL) + 1) % MAX_SEQUENTIAL; + + lock (_rngLock) { - lock (_rngLock) - { - _rng.NextBytes(randomBytes); - } - seq = BitConverter.ToUInt64(randomBytes, 0); + _rng.NextBytes(randomBytes); + } - } while (seq > ulong.MaxValue - ((ulong.MaxValue % MAX_SEQUENTIAL) + 1) % MAX_SEQUENTIAL); - // Limit seq to MAX_SEQUENTIAL, see https://stackoverflow.com/a/13095144 - return (long)(seq % MAX_SEQUENTIAL); + var sequential = BitConverter.ToUInt64(randomBytes, 0); + + // NOTE: Originally we used the following algorithm to create a random long: + // https://stackoverflow.com/a/13095144 + // Here, the uRange is const though, because it is always MAX_SEQUENTIAL - 0, + // so the condition can be reduced to: + // sequential > ulong.MaxValue - ((ulong.MaxValue % MAX_SEQUENTIAL) + 1) % MAX_SEQUENTIAL + // The right hand side of the comparision happens to be const too now and can be folded to: + // 18446744073709551615 which happens to be equal to ulong.MaxValue, hence the condition will + // never be true and we can omit the do-while loop entirely. + + return (long)(sequential % MAX_SEQUENTIAL); } - //TODO: Synchronize access to _prefix private void SetPrefix() { var randomBytes = new byte[PREFIX_LENGTH]; @@ -143,9 +163,10 @@ private void SetPrefix() for(var i = 0; i < randomBytes.Length; i++) { - _prefix[i] = (char)_characters[randomBytes[i] % BASE]; + // We operate on unsigned integers and BASE is a power of two + // therefore we can optimize randomBytes[i] % BASE to randomBytes[i] & (BASE - 1) + _prefix[i] = _digits[randomBytes[i] & (BASE - 1)]; } - } } } diff --git a/src/Tests/UnitTests/Internals/TestNuid.cs b/src/Tests/UnitTests/Internals/TestNuid.cs index 849f11a70..5630862f1 100644 --- a/src/Tests/UnitTests/Internals/TestNuid.cs +++ b/src/Tests/UnitTests/Internals/TestNuid.cs @@ -3,11 +3,19 @@ using System.Security.Cryptography; using NATS.Client.Internals; using Xunit; +using Xunit.Abstractions; namespace UnitTests.Internals { public class TestNuid { + private readonly ITestOutputHelper _outputHelper; + + public TestNuid(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + [Fact] public void GetNextNuid_ReturnsNuidOfLength22() { @@ -59,7 +67,7 @@ public void GetNextNuid_ContainsOnlyValidCharacters() var result = nuid.GetNext(); // Assert - Assert.Matches("[A-z0-9_-]{22}", result); + Assert.Matches("[A-z0-9+/]{22}", result); } [Fact] @@ -67,7 +75,7 @@ public void GetNextNuid_PrefixRenewed() { // Arrange var increment = 100; - var maxSequential = 1152921504606846976 - increment; + var maxSequential = 0x1000_0000_0000_0000 - increment; var nuid = new Nuid(RandomNumberGenerator.Create(), maxSequential, increment); // Act @@ -82,7 +90,7 @@ public void GetNextNuid_PrefixRenewed() public void GetNextNuid_PrefixAsExpected() { // Arrange - var rngBytes = new byte[12] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; + var rngBytes = new byte[12] { 0, 1, 2, 3, 4, 5, 6, 7, 11, 253, 254, 255 }; var rng = new ControlledRng(new Queue(new byte[][] { rngBytes, rngBytes })); var nuid = new Nuid(rng); @@ -91,7 +99,7 @@ public void GetNextNuid_PrefixAsExpected() var prefix = nuid.GetNext().Substring(0, 12); // Assert - Assert.Equal("0123456789AB", prefix); + Assert.Equal("ABCDEFGHL9+/", prefix); } [Fact] @@ -99,19 +107,41 @@ public void NuidInitialization_RngInvokedOnce() { // Arrange var rngBytes = new byte[12] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; - var rng = new ControlledRng(new Queue(new byte[][] { rngBytes, rngBytes })); + var rng = new ControlledRng(new Queue(new[] { rngBytes, rngBytes })); // Act var nuid = new Nuid(rng); ; // Assert #if NET452 + // On .NET FX we have an additional invocation for seeding System.Random Assert.Equal(2, rng.GetBytesInvocations); #else Assert.Equal(1, rng.GetBytesInvocations); #endif } + + [Fact] + public void GetNextNuid_NuidsAreUnique() + { + // Arrange + const int count = 1_000_000; + var nuid = new Nuid(); + var m = new HashSet(count); + // Act + for (var i = 0; i < count; i++) + { + var curNuid = nuid.GetNext(); + + //HashSet.Add returns false if the set already contains the item + if (m.Add(curNuid)) + continue; + + _outputHelper.WriteLine($"Duplicate Nuid {curNuid}"); + Assert.True(false, "Duplicate Nuid detected"); + } + } private class ControlledRng : RandomNumberGenerator { public int GetBytesInvocations = 0; From b075eec0b265bd57c0646c6711e034c1656e5a1e Mon Sep 17 00:00:00 2001 From: jasperd Date: Wed, 15 Jan 2020 23:59:34 +0100 Subject: [PATCH 08/30] Fix test arrangement --- src/Tests/UnitTests/Internals/TestNuid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tests/UnitTests/Internals/TestNuid.cs b/src/Tests/UnitTests/Internals/TestNuid.cs index 5630862f1..a96083f0f 100644 --- a/src/Tests/UnitTests/Internals/TestNuid.cs +++ b/src/Tests/UnitTests/Internals/TestNuid.cs @@ -75,7 +75,7 @@ public void GetNextNuid_PrefixRenewed() { // Arrange var increment = 100; - var maxSequential = 0x1000_0000_0000_0000 - increment; + var maxSequential = 0x1000_0000_0000_0000 - increment - 1; var nuid = new Nuid(RandomNumberGenerator.Create(), maxSequential, increment); // Act From 951d2aad3741cb3ea193aaeeec866782bdcf47cf Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 26 Jan 2020 17:33:04 +0100 Subject: [PATCH 09/30] Use 0 as sequential in benchmarks --- src/Benchmarks/MicroBenchmarks/Program.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Benchmarks/MicroBenchmarks/Program.cs b/src/Benchmarks/MicroBenchmarks/Program.cs index dac726b80..29ecf6697 100644 --- a/src/Benchmarks/MicroBenchmarks/Program.cs +++ b/src/Benchmarks/MicroBenchmarks/Program.cs @@ -16,8 +16,13 @@ namespace MicroBenchmarks [SimpleJob(RuntimeMoniker.NetCoreApp31)] public class RandomBenchmark { - private static readonly NUID _nuid = NUID.Instance; - private static readonly Nuid _newNuid = new Nuid(); + private readonly NUID _nuid = NUID.Instance; + private readonly Nuid _newNuid = new Nuid(null, 0, 1); + + public RandomBenchmark() + { + _nuid.Seq = 0; + } [BenchmarkCategory("NextNuid")] [Benchmark(Baseline = true)] From 89fe7b2944c299688b9fe3bce2c7a36f9af71a8e Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 26 Jan 2020 17:33:59 +0100 Subject: [PATCH 10/30] Use Nuid as inbox subject --- src/NATS.Client/Conn.cs | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/src/NATS.Client/Conn.cs b/src/NATS.Client/Conn.cs index 4ef90b79c..207169dd0 100644 --- a/src/NATS.Client/Conn.cs +++ b/src/NATS.Client/Conn.cs @@ -102,7 +102,7 @@ public class Connection : IConnection, IDisposable // .NET 4.0. private readonly object mu = new Object(); - private Random r = null; + private readonly Nuid _nuid = new Nuid(); Options opts = new Options(); @@ -3191,23 +3191,8 @@ public Task RequestAsync(string subject, byte[] data, int offset, int count /// A unique inbox string. public string NewInbox() { - var prefix = opts.CustomInboxPrefix ?? IC.inboxPrefix; - - if (!opts.UseOldRequestStyle) - { - return prefix + Guid.NewGuid().ToString("N"); - } - else - { - if (r == null) - r = new Random(Guid.NewGuid().GetHashCode()); - - byte[] buf = new byte[13]; - - r.NextBytes(buf); - - return prefix + BitConverter.ToString(buf).Replace("-",""); - } + var prefix = opts.customInboxPrefix ?? IC.inboxPrefix; + return prefix + _nuid.GetNext(); } internal void sendSubscriptionMessage(AsyncSubscription s) From 25597a13fc38724ab42dc02623900f28a5f72f80 Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 26 Jan 2020 17:34:15 +0100 Subject: [PATCH 11/30] Mark NUID as obsolete --- src/NATS.Client/NUID.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NATS.Client/NUID.cs b/src/NATS.Client/NUID.cs index e5d04cf40..48f1992f9 100644 --- a/src/NATS.Client/NUID.cs +++ b/src/NATS.Client/NUID.cs @@ -22,6 +22,7 @@ namespace NATS.Client /// that is started at a pseudo random number and increments with a pseudo-random increment. /// Total is 22 bytes of base 36 ascii text. /// + [Obsolete("NATS.Client.NUID is deprecated and will be removed in a future version")] public class NUID { private static char[] digits = From 006f2e5744e0ea2d5f5667bc4547c16dd63fa660 Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 26 Jan 2020 17:47:49 +0100 Subject: [PATCH 12/30] Fix project and solution files after rebasing --- src/NATS.Client/NATS.Client.csproj | 2 +- src/NATS.sln | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/NATS.Client/NATS.Client.csproj b/src/NATS.Client/NATS.Client.csproj index 468b248c8..b79dd396f 100644 --- a/src/NATS.Client/NATS.Client.csproj +++ b/src/NATS.Client/NATS.Client.csproj @@ -12,7 +12,7 @@ https://github.com/nats-io/nats.net/releases Apache-2.0 CNCF NATS Messaging Cloud Publish Subscribe PubSub - pdbonly + pdbonly true diff --git a/src/NATS.sln b/src/NATS.sln index b4b4e6639..0aa19f0af 100644 --- a/src/NATS.sln +++ b/src/NATS.sln @@ -90,10 +90,6 @@ Global {10ED0146-1701-4BC9-9E26-824B22D18CB8}.Release|Any CPU.Build.0 = Release|Any CPU {8D181EED-3FF7-4CC0-9C85-331EF20B7688}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8D181EED-3FF7-4CC0-9C85-331EF20B7688}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8D181EED-3FF7-4CC0-9C85-331EF20B7688}.LinuxDebug|Any CPU.ActiveCfg = LinuxDebug|Any CPU - {8D181EED-3FF7-4CC0-9C85-331EF20B7688}.LinuxDebug|Any CPU.Build.0 = LinuxDebug|Any CPU - {8D181EED-3FF7-4CC0-9C85-331EF20B7688}.LinuxRelease|Any CPU.ActiveCfg = LinuxRelease|Any CPU - {8D181EED-3FF7-4CC0-9C85-331EF20B7688}.LinuxRelease|Any CPU.Build.0 = LinuxRelease|Any CPU {8D181EED-3FF7-4CC0-9C85-331EF20B7688}.Release|Any CPU.ActiveCfg = Release|Any CPU {8D181EED-3FF7-4CC0-9C85-331EF20B7688}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection From e3405d0486bf05153ce4d450d8e1a92f8c6a95b9 Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 26 Jan 2020 17:59:06 +0100 Subject: [PATCH 13/30] Always initialize System.Random w/ seed --- src/NATS.Client/Internals/Nuid.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/NATS.Client/Internals/Nuid.cs b/src/NATS.Client/Internals/Nuid.cs index 9406e4bb6..2079500de 100644 --- a/src/NATS.Client/Internals/Nuid.cs +++ b/src/NATS.Client/Internals/Nuid.cs @@ -26,7 +26,7 @@ internal sealed class Nuid private readonly object _nuidLock = new object(); - private readonly Random _rng = new Random(); + private readonly Random _rng; private readonly object _rngLock = new object(); private readonly RandomNumberGenerator _cryptoRng; private readonly object _cryptRngLock = new object(); @@ -52,8 +52,6 @@ internal Nuid(RandomNumberGenerator rng = null, long? sequential = null, int? in else _cryptoRng = rng; - //TODO: What about netstandard when not running on CoreCLR?! -#if NET45 // Instantiating System.Random multiple times in quick succession without a // proper seed may result in instances that yield identical sequences on .NET FX. // See https://docs.microsoft.com/en-us/dotnet/api/system.random?view=netframework-4.8#instantiating-the-random-number-generator @@ -61,7 +59,7 @@ internal Nuid(RandomNumberGenerator rng = null, long? sequential = null, int? in var seedBytes = new byte[4]; _cryptoRng.GetBytes(seedBytes); _rng = new Random(BitConverter.ToInt32(seedBytes, 0)); -#endif + if (sequential is null) _sequential = GetSequential(); else @@ -132,7 +130,6 @@ private int GetIncrement() private long GetSequential() { var randomBytes = new byte[8]; - const ulong s = ulong.MaxValue - ((ulong.MaxValue % MAX_SEQUENTIAL) + 1) % MAX_SEQUENTIAL; lock (_rngLock) { From 01cc5cbf1b2b5c6e023db962835edcdd59c76f68 Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 26 Jan 2020 18:00:03 +0100 Subject: [PATCH 14/30] Make string comparision method explicit in test --- src/Tests/UnitTests/Internals/TestNuid.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Tests/UnitTests/Internals/TestNuid.cs b/src/Tests/UnitTests/Internals/TestNuid.cs index a96083f0f..952caf88f 100644 --- a/src/Tests/UnitTests/Internals/TestNuid.cs +++ b/src/Tests/UnitTests/Internals/TestNuid.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Security.Cryptography; using NATS.Client.Internals; using Xunit; @@ -127,21 +128,22 @@ public void GetNextNuid_NuidsAreUnique() // Arrange const int count = 1_000_000; var nuid = new Nuid(); - var m = new HashSet(count); + var nuids = new HashSet(StringComparer.Ordinal); // Act for (var i = 0; i < count; i++) { - var curNuid = nuid.GetNext(); + var currentNuid = nuid.GetNext(); //HashSet.Add returns false if the set already contains the item - if (m.Add(curNuid)) + if (nuids.Add(currentNuid)) continue; - _outputHelper.WriteLine($"Duplicate Nuid {curNuid}"); + _outputHelper.WriteLine($"Duplicate Nuid {currentNuid}"); Assert.True(false, "Duplicate Nuid detected"); } } + private class ControlledRng : RandomNumberGenerator { public int GetBytesInvocations = 0; From 3ac15bf47d989e2eaf4060e43d21290e304c063b Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 26 Jan 2020 18:05:07 +0100 Subject: [PATCH 15/30] Fix test after change in e3405d0 --- src/Tests/UnitTests/Internals/TestNuid.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Tests/UnitTests/Internals/TestNuid.cs b/src/Tests/UnitTests/Internals/TestNuid.cs index 952caf88f..3461eee07 100644 --- a/src/Tests/UnitTests/Internals/TestNuid.cs +++ b/src/Tests/UnitTests/Internals/TestNuid.cs @@ -114,12 +114,8 @@ public void NuidInitialization_RngInvokedOnce() var nuid = new Nuid(rng); ; // Assert -#if NET452 - // On .NET FX we have an additional invocation for seeding System.Random Assert.Equal(2, rng.GetBytesInvocations); -#else - Assert.Equal(1, rng.GetBytesInvocations); -#endif + } [Fact] From f17641a16e4b2e9bfdfdc892ce13f80f2f7ce8ca Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 26 Jan 2020 18:35:20 +0100 Subject: [PATCH 16/30] Fix MicroBenchmarks --- src/Benchmarks/MicroBenchmarks/Directory.Build.props | 7 +++++++ src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj | 3 +-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 src/Benchmarks/MicroBenchmarks/Directory.Build.props diff --git a/src/Benchmarks/MicroBenchmarks/Directory.Build.props b/src/Benchmarks/MicroBenchmarks/Directory.Build.props new file mode 100644 index 000000000..2e8fb63d7 --- /dev/null +++ b/src/Benchmarks/MicroBenchmarks/Directory.Build.props @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj b/src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj index cb0596608..7fe690cf8 100644 --- a/src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj +++ b/src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj @@ -7,10 +7,9 @@ AnyCPU pdbonly true - Debug;Release;LinuxRelease;LinuxDebug - + true ..\..\NATS.Client.snk From 8a50ee547ed8b8ed4cd0465ea6d7ccb5b66e2020 Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 26 Jan 2020 18:35:53 +0100 Subject: [PATCH 17/30] Test different synchronization primitives --- src/Benchmarks/MicroBenchmarks/Program.cs | 17 ++++- src/NATS.Client/Internals/Nuid.cs | 75 ++++++++++++++++++++++- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/src/Benchmarks/MicroBenchmarks/Program.cs b/src/Benchmarks/MicroBenchmarks/Program.cs index 29ecf6697..61f7b937f 100644 --- a/src/Benchmarks/MicroBenchmarks/Program.cs +++ b/src/Benchmarks/MicroBenchmarks/Program.cs @@ -1,4 +1,5 @@ -using BenchmarkDotNet.Attributes; +using System; +using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Order; using BenchmarkDotNet.Running; @@ -18,7 +19,7 @@ public class RandomBenchmark { private readonly NUID _nuid = NUID.Instance; private readonly Nuid _newNuid = new Nuid(null, 0, 1); - + private readonly Random _random = new Random(); public RandomBenchmark() { _nuid.Seq = 0; @@ -31,6 +32,18 @@ public RandomBenchmark() [BenchmarkCategory("NextNuid"), Benchmark] public string NextNuid() => _newNuid.GetNext(); + [BenchmarkCategory("NextNuid"), Benchmark] + public string NextNuidMonitor() => _newNuid.GetNextMonitor(); + + [BenchmarkCategory("NextNuid"), Benchmark] + public string NextNuidMutex() => _newNuid.GetNextMutex(); + + [BenchmarkCategory("NextNuid"), Benchmark] + public string OldNewInbox() { + byte[] buf = new byte[13]; + _random.NextBytes(buf); + return BitConverter.ToString(buf).Replace("-", ""); + } } public class Program diff --git a/src/NATS.Client/Internals/Nuid.cs b/src/NATS.Client/Internals/Nuid.cs index 2079500de..a4d1ea417 100644 --- a/src/NATS.Client/Internals/Nuid.cs +++ b/src/NATS.Client/Internals/Nuid.cs @@ -1,9 +1,10 @@ using System; using System.Security.Cryptography; +using System.Threading; namespace NATS.Client.Internals { - internal sealed class Nuid + internal sealed class Nuid : IDisposable { private const uint PREFIX_LENGTH = 12; private const uint SEQUENTIAL_LENGTH = 10; @@ -30,6 +31,7 @@ internal sealed class Nuid private readonly object _rngLock = new object(); private readonly RandomNumberGenerator _cryptoRng; private readonly object _cryptRngLock = new object(); + private readonly Mutex _nuidMutex = new Mutex(); private byte[] _prefix = new byte[PREFIX_LENGTH]; private int _increment; @@ -119,6 +121,72 @@ internal string GetNext() return new string(nuidBuffer); } + internal string GetNextMonitor() + { + var sequential = 0L; + var nuidBuffer = new char[NUID_LENGTH]; + Monitor.Enter(_nuidLock); + sequential = _sequential += _increment; + if (_sequential >= MAX_SEQUENTIAL) + { + SetPrefix(); + sequential = _sequential = GetSequential(); + _increment = GetIncrement(); + } + + // For small arrays this is way faster than Array.Copy and still faster than Buffer.BlockCopy + for (var i = 0; i < PREFIX_LENGTH; i++) + { + nuidBuffer[i] = (char)_prefix[i]; + } + Monitor.Exit(_nuidLock); + + for (var i = PREFIX_LENGTH; i < NUID_LENGTH; i++) + { + // We operate on unsigned integers and BASE is a power of two + // therefore we can optimize sequential % BASE to sequential & (BASE - 1) + nuidBuffer[i] = (char)_digits[(int)sequential & (BASE - 1)]; + // BASE is 64 = 2^6 and sequential >= 0 + // therefore we can optimize sequential / BASE to sequential >> 6 + sequential >>= 6; + } + + return new string(nuidBuffer); + } + + internal string GetNextMutex() + { + var sequential = 0L; + var nuidBuffer = new char[NUID_LENGTH]; + _nuidMutex.WaitOne(); + sequential = _sequential += _increment; + if (_sequential >= MAX_SEQUENTIAL) + { + SetPrefix(); + sequential = _sequential = GetSequential(); + _increment = GetIncrement(); + } + + // For small arrays this is way faster than Array.Copy and still faster than Buffer.BlockCopy + for (var i = 0; i < PREFIX_LENGTH; i++) + { + nuidBuffer[i] = (char)_prefix[i]; + } + _nuidMutex.ReleaseMutex(); + + for (var i = PREFIX_LENGTH; i < NUID_LENGTH; i++) + { + // We operate on unsigned integers and BASE is a power of two + // therefore we can optimize sequential % BASE to sequential & (BASE - 1) + nuidBuffer[i] = (char)_digits[(int)sequential & (BASE - 1)]; + // BASE is 64 = 2^6 and sequential >= 0 + // therefore we can optimize sequential / BASE to sequential >> 6 + sequential >>= 6; + } + + return new string(nuidBuffer); + } + private int GetIncrement() { lock (_rngLock) @@ -165,5 +233,10 @@ private void SetPrefix() _prefix[i] = _digits[randomBytes[i] & (BASE - 1)]; } } + + public void Dispose() + { + _nuidMutex.Dispose(); + } } } From bd2abf39df3b7c7c06d12ec9a0069e066650f504 Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 26 Jan 2020 19:19:49 +0100 Subject: [PATCH 18/30] Remove slower implementations of GetNext --- .../MicroBenchmarks/Directory.Build.props | 4 +- src/Benchmarks/MicroBenchmarks/Program.cs | 7 -- src/NATS.Client/Internals/Nuid.cs | 77 +------------------ 3 files changed, 4 insertions(+), 84 deletions(-) diff --git a/src/Benchmarks/MicroBenchmarks/Directory.Build.props b/src/Benchmarks/MicroBenchmarks/Directory.Build.props index 2e8fb63d7..ab997fa0c 100644 --- a/src/Benchmarks/MicroBenchmarks/Directory.Build.props +++ b/src/Benchmarks/MicroBenchmarks/Directory.Build.props @@ -1,7 +1,7 @@ - + - \ No newline at end of file + diff --git a/src/Benchmarks/MicroBenchmarks/Program.cs b/src/Benchmarks/MicroBenchmarks/Program.cs index 61f7b937f..68854b70e 100644 --- a/src/Benchmarks/MicroBenchmarks/Program.cs +++ b/src/Benchmarks/MicroBenchmarks/Program.cs @@ -11,7 +11,6 @@ namespace MicroBenchmarks [DisassemblyDiagnoser(printAsm: true, printSource: true)] [Orderer(SummaryOrderPolicy.FastestToSlowest)] [MemoryDiagnoser] - [RPlotExporter] [MarkdownExporterAttribute.GitHub] [SimpleJob(RuntimeMoniker.Net462)] [SimpleJob(RuntimeMoniker.NetCoreApp31)] @@ -32,12 +31,6 @@ public RandomBenchmark() [BenchmarkCategory("NextNuid"), Benchmark] public string NextNuid() => _newNuid.GetNext(); - [BenchmarkCategory("NextNuid"), Benchmark] - public string NextNuidMonitor() => _newNuid.GetNextMonitor(); - - [BenchmarkCategory("NextNuid"), Benchmark] - public string NextNuidMutex() => _newNuid.GetNextMutex(); - [BenchmarkCategory("NextNuid"), Benchmark] public string OldNewInbox() { byte[] buf = new byte[13]; diff --git a/src/NATS.Client/Internals/Nuid.cs b/src/NATS.Client/Internals/Nuid.cs index a4d1ea417..e9b6f86d4 100644 --- a/src/NATS.Client/Internals/Nuid.cs +++ b/src/NATS.Client/Internals/Nuid.cs @@ -4,7 +4,7 @@ namespace NATS.Client.Internals { - internal sealed class Nuid : IDisposable + internal sealed class Nuid { private const uint PREFIX_LENGTH = 12; private const uint SEQUENTIAL_LENGTH = 10; @@ -31,7 +31,6 @@ internal sealed class Nuid : IDisposable private readonly object _rngLock = new object(); private readonly RandomNumberGenerator _cryptoRng; private readonly object _cryptRngLock = new object(); - private readonly Mutex _nuidMutex = new Mutex(); private byte[] _prefix = new byte[PREFIX_LENGTH]; private int _increment; @@ -89,44 +88,10 @@ internal Nuid() : this(null) {} /// The Nuid internal string GetNext() { - var sequential = 0L; var nuidBuffer = new char[NUID_LENGTH]; - lock (_nuidLock) - { - sequential = _sequential += _increment; - if(_sequential >= MAX_SEQUENTIAL) - { - SetPrefix(); - sequential = _sequential = GetSequential(); - _increment = GetIncrement(); - } - - // For small arrays this is way faster than Array.Copy and still faster than Buffer.BlockCopy - for (var i = 0; i < PREFIX_LENGTH; i++) - { - nuidBuffer[i] = (char)_prefix[i]; - } - } - - for(var i = PREFIX_LENGTH; i < NUID_LENGTH; i++) - { - // We operate on unsigned integers and BASE is a power of two - // therefore we can optimize sequential % BASE to sequential & (BASE - 1) - nuidBuffer[i] = (char)_digits[(int)sequential & (BASE - 1)]; - // BASE is 64 = 2^6 and sequential >= 0 - // therefore we can optimize sequential / BASE to sequential >> 6 - sequential >>= 6; - } - - return new string(nuidBuffer); - } - internal string GetNextMonitor() - { - var sequential = 0L; - var nuidBuffer = new char[NUID_LENGTH]; Monitor.Enter(_nuidLock); - sequential = _sequential += _increment; + long sequential = _sequential += _increment; if (_sequential >= MAX_SEQUENTIAL) { SetPrefix(); @@ -154,39 +119,6 @@ internal string GetNextMonitor() return new string(nuidBuffer); } - internal string GetNextMutex() - { - var sequential = 0L; - var nuidBuffer = new char[NUID_LENGTH]; - _nuidMutex.WaitOne(); - sequential = _sequential += _increment; - if (_sequential >= MAX_SEQUENTIAL) - { - SetPrefix(); - sequential = _sequential = GetSequential(); - _increment = GetIncrement(); - } - - // For small arrays this is way faster than Array.Copy and still faster than Buffer.BlockCopy - for (var i = 0; i < PREFIX_LENGTH; i++) - { - nuidBuffer[i] = (char)_prefix[i]; - } - _nuidMutex.ReleaseMutex(); - - for (var i = PREFIX_LENGTH; i < NUID_LENGTH; i++) - { - // We operate on unsigned integers and BASE is a power of two - // therefore we can optimize sequential % BASE to sequential & (BASE - 1) - nuidBuffer[i] = (char)_digits[(int)sequential & (BASE - 1)]; - // BASE is 64 = 2^6 and sequential >= 0 - // therefore we can optimize sequential / BASE to sequential >> 6 - sequential >>= 6; - } - - return new string(nuidBuffer); - } - private int GetIncrement() { lock (_rngLock) @@ -233,10 +165,5 @@ private void SetPrefix() _prefix[i] = _digits[randomBytes[i] & (BASE - 1)]; } } - - public void Dispose() - { - _nuidMutex.Dispose(); - } } } From 90aef28aa4c0b0f275da6e328915e01badfb06af Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 26 Jan 2020 19:20:23 +0100 Subject: [PATCH 19/30] Remove unnecessary locks --- src/NATS.Client/Internals/Nuid.cs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/NATS.Client/Internals/Nuid.cs b/src/NATS.Client/Internals/Nuid.cs index e9b6f86d4..8a3616d43 100644 --- a/src/NATS.Client/Internals/Nuid.cs +++ b/src/NATS.Client/Internals/Nuid.cs @@ -28,9 +28,7 @@ internal sealed class Nuid private readonly object _nuidLock = new object(); private readonly Random _rng; - private readonly object _rngLock = new object(); private readonly RandomNumberGenerator _cryptoRng; - private readonly object _cryptRngLock = new object(); private byte[] _prefix = new byte[PREFIX_LENGTH]; private int _increment; @@ -121,20 +119,14 @@ internal string GetNext() private int GetIncrement() { - lock (_rngLock) - { - return _rng.Next(MIN_INCREMENT, MAX_INCREMENT); - } + return _rng.Next(MIN_INCREMENT, MAX_INCREMENT); } private long GetSequential() { var randomBytes = new byte[8]; - lock (_rngLock) - { - _rng.NextBytes(randomBytes); - } + _rng.NextBytes(randomBytes); var sequential = BitConverter.ToUInt64(randomBytes, 0); @@ -153,10 +145,8 @@ private long GetSequential() private void SetPrefix() { var randomBytes = new byte[PREFIX_LENGTH]; - lock (_cryptRngLock) - { - _cryptoRng.GetBytes(randomBytes); - } + + _cryptoRng.GetBytes(randomBytes); for(var i = 0; i < randomBytes.Length; i++) { From e8fe4d26f60f1750b6c811ee08516dac6e7e2b33 Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 26 Jan 2020 20:12:04 +0100 Subject: [PATCH 20/30] Cleanup benchmarks --- .../MicroBenchmarks/NuidBenchmark.cs | 33 +++++++++++++++ src/Benchmarks/MicroBenchmarks/Program.cs | 41 +------------------ 2 files changed, 35 insertions(+), 39 deletions(-) create mode 100644 src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs diff --git a/src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs b/src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs new file mode 100644 index 000000000..d39ed56bf --- /dev/null +++ b/src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs @@ -0,0 +1,33 @@ +using System; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Order; +using NATS.Client; +using NATS.Client.Internals; + +namespace MicroBenchmarks +{ + [DisassemblyDiagnoser(printAsm: true, printSource: true)] + [Orderer(SummaryOrderPolicy.FastestToSlowest)] + [MemoryDiagnoser] + [MarkdownExporterAttribute.GitHub] + [SimpleJob(RuntimeMoniker.Net462)] + [SimpleJob(RuntimeMoniker.NetCoreApp31)] + public class NuidBenchmark + { + private readonly NUID _nuid = NUID.Instance; + private readonly Nuid _newNuid = new Nuid(null, 0, 1); + private readonly Random _random = new Random(); + public NuidBenchmark() + { + _nuid.Seq = 0; + } + + [BenchmarkCategory("NextNuid")] + [Benchmark(Baseline = true)] + public string NUIDNext() => _nuid.Next; + + [BenchmarkCategory("NextNuid"), Benchmark] + public string NextNuid() => _newNuid.GetNext(); + } +} diff --git a/src/Benchmarks/MicroBenchmarks/Program.cs b/src/Benchmarks/MicroBenchmarks/Program.cs index 68854b70e..822a91325 100644 --- a/src/Benchmarks/MicroBenchmarks/Program.cs +++ b/src/Benchmarks/MicroBenchmarks/Program.cs @@ -1,49 +1,12 @@ -using System; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Order; -using BenchmarkDotNet.Running; -using NATS.Client; -using NATS.Client.Internals; +using BenchmarkDotNet.Running; namespace MicroBenchmarks { - [DisassemblyDiagnoser(printAsm: true, printSource: true)] - [Orderer(SummaryOrderPolicy.FastestToSlowest)] - [MemoryDiagnoser] - [MarkdownExporterAttribute.GitHub] - [SimpleJob(RuntimeMoniker.Net462)] - [SimpleJob(RuntimeMoniker.NetCoreApp31)] - public class RandomBenchmark - { - private readonly NUID _nuid = NUID.Instance; - private readonly Nuid _newNuid = new Nuid(null, 0, 1); - private readonly Random _random = new Random(); - public RandomBenchmark() - { - _nuid.Seq = 0; - } - - [BenchmarkCategory("NextNuid")] - [Benchmark(Baseline = true)] - public string NUIDNext() => _nuid.Next; - - [BenchmarkCategory("NextNuid"), Benchmark] - public string NextNuid() => _newNuid.GetNext(); - - [BenchmarkCategory("NextNuid"), Benchmark] - public string OldNewInbox() { - byte[] buf = new byte[13]; - _random.NextBytes(buf); - return BitConverter.ToString(buf).Replace("-", ""); - } - } - public class Program { public static void Main(string[] args) { - var summary = BenchmarkRunner.Run(); + var summary = BenchmarkRunner.Run(); } } } From 66ce7c18c6d92fcd5de3bd65e797c07d1f692a17 Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 26 Jan 2020 20:13:22 +0100 Subject: [PATCH 21/30] Remove unused field from NuidBenchmark --- src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs b/src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs index d39ed56bf..dc180c2b1 100644 --- a/src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs +++ b/src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs @@ -17,7 +17,7 @@ public class NuidBenchmark { private readonly NUID _nuid = NUID.Instance; private readonly Nuid _newNuid = new Nuid(null, 0, 1); - private readonly Random _random = new Random(); + public NuidBenchmark() { _nuid.Seq = 0; From ec7253ace9402a681fa9754e17c455f1839326e8 Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 26 Jan 2020 20:15:37 +0100 Subject: [PATCH 22/30] Suppress obsolete warnings in benchmarks and tests --- src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs | 5 +++-- src/Tests/UnitTests/TestNUID.cs | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs b/src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs index dc180c2b1..08b472624 100644 --- a/src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs +++ b/src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs @@ -1,5 +1,4 @@ -using System; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Order; using NATS.Client; @@ -15,7 +14,9 @@ namespace MicroBenchmarks [SimpleJob(RuntimeMoniker.NetCoreApp31)] public class NuidBenchmark { +#pragma warning disable CS0618 private readonly NUID _nuid = NUID.Instance; +#pragma warning restore CS0618 private readonly Nuid _newNuid = new Nuid(null, 0, 1); public NuidBenchmark() diff --git a/src/Tests/UnitTests/TestNUID.cs b/src/Tests/UnitTests/TestNUID.cs index 49b38776e..87994786b 100644 --- a/src/Tests/UnitTests/TestNUID.cs +++ b/src/Tests/UnitTests/TestNUID.cs @@ -19,6 +19,7 @@ namespace UnitTests { +#pragma warning disable CS0618 public class TestNUID { [Fact] @@ -110,4 +111,5 @@ public void TestNuidBasicUniqueess() } } +#pragma warning restore CS0618 } From e51e42d46839b42ad9d7d0547d9f27cfc4b3584f Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 26 Jan 2020 20:23:40 +0100 Subject: [PATCH 23/30] Add LF at EOF --- src/NATS.Client/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NATS.Client/Properties/AssemblyInfo.cs b/src/NATS.Client/Properties/AssemblyInfo.cs index 467223a58..346e5cb05 100644 --- a/src/NATS.Client/Properties/AssemblyInfo.cs +++ b/src/NATS.Client/Properties/AssemblyInfo.cs @@ -10,4 +10,4 @@ #else [assembly: InternalsVisibleTo("UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100db7da1f2f89089327b47d26d69666fad20861f24e9acdb13965fb6c64dfee8da589b495df37a75e934ddbacb0752a42c40f3dbc79614eec9bb2a0b6741f9e2ad2876f95e74d54c23eef0063eb4efb1e7d824ee8a695b647c113c92834f04a3a83fb60f435814ddf5c4e5f66a168139c4c1b1a50a3e60c164d180e265b1f000cd")] [assembly: InternalsVisibleTo("MicroBenchmarks, PublicKey=0024000004800000940000000602000000240000525341310004000001000100db7da1f2f89089327b47d26d69666fad20861f24e9acdb13965fb6c64dfee8da589b495df37a75e934ddbacb0752a42c40f3dbc79614eec9bb2a0b6741f9e2ad2876f95e74d54c23eef0063eb4efb1e7d824ee8a695b647c113c92834f04a3a83fb60f435814ddf5c4e5f66a168139c4c1b1a50a3e60c164d180e265b1f000cd")] -#endif \ No newline at end of file +#endif From 4f11c79505beed1b99788176c476ab56f6ded109 Mon Sep 17 00:00:00 2001 From: jasperd Date: Sun, 26 Jan 2020 20:54:40 +0100 Subject: [PATCH 24/30] Fix comment --- src/NATS.Client/Internals/Nuid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NATS.Client/Internals/Nuid.cs b/src/NATS.Client/Internals/Nuid.cs index 8a3616d43..0c9d8053f 100644 --- a/src/NATS.Client/Internals/Nuid.cs +++ b/src/NATS.Client/Internals/Nuid.cs @@ -11,7 +11,7 @@ internal sealed class Nuid private const uint NUID_LENGTH = PREFIX_LENGTH + SEQUENTIAL_LENGTH; private const int MIN_INCREMENT = 33; private const int MAX_INCREMENT = 333; - private const long MAX_SEQUENTIAL = 0x1000_0000_0000_0000; //64^10 - 1 + private const long MAX_SEQUENTIAL = 0x1000_0000_0000_0000; //64^10 private const int BASE = 64; private static readonly byte[] _digits = new byte[BASE]{ From 05da0a31fa248d0c83d5cfa092d525af385db5f9 Mon Sep 17 00:00:00 2001 From: jasperd Date: Mon, 27 Jan 2020 23:32:52 +0100 Subject: [PATCH 25/30] Add license headers --- src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs | 15 ++++++++++++++- src/Benchmarks/MicroBenchmarks/Program.cs | 15 ++++++++++++++- src/NATS.Client/Internals/Nuid.cs | 15 ++++++++++++++- src/Tests/UnitTests/Internals/TestNuid.cs | 16 ++++++++++++++-- 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs b/src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs index 08b472624..0a4717910 100644 --- a/src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs +++ b/src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs @@ -1,4 +1,17 @@ -using BenchmarkDotNet.Attributes; +// Copyright 2020 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Order; using NATS.Client; diff --git a/src/Benchmarks/MicroBenchmarks/Program.cs b/src/Benchmarks/MicroBenchmarks/Program.cs index 822a91325..7ff6d2d6c 100644 --- a/src/Benchmarks/MicroBenchmarks/Program.cs +++ b/src/Benchmarks/MicroBenchmarks/Program.cs @@ -1,4 +1,17 @@ -using BenchmarkDotNet.Running; +// Copyright 2020 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using BenchmarkDotNet.Running; namespace MicroBenchmarks { diff --git a/src/NATS.Client/Internals/Nuid.cs b/src/NATS.Client/Internals/Nuid.cs index 0c9d8053f..32427d297 100644 --- a/src/NATS.Client/Internals/Nuid.cs +++ b/src/NATS.Client/Internals/Nuid.cs @@ -1,4 +1,17 @@ -using System; +// Copyright 2020 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; using System.Security.Cryptography; using System.Threading; diff --git a/src/Tests/UnitTests/Internals/TestNuid.cs b/src/Tests/UnitTests/Internals/TestNuid.cs index 3461eee07..b8416ed14 100644 --- a/src/Tests/UnitTests/Internals/TestNuid.cs +++ b/src/Tests/UnitTests/Internals/TestNuid.cs @@ -1,6 +1,18 @@ -using System; +// Copyright 2020 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; using System.Collections.Generic; -using System.Globalization; using System.Security.Cryptography; using NATS.Client.Internals; using Xunit; From a8b925ed4209834df07ebb43a8b1dfc6153f2b9e Mon Sep 17 00:00:00 2001 From: jasperd Date: Tue, 28 Jan 2020 00:33:41 +0100 Subject: [PATCH 26/30] Use URL and Filename safe digits - https://tools.ietf.org/html/rfc4648#section-5 --- src/NATS.Client/Internals/Nuid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NATS.Client/Internals/Nuid.cs b/src/NATS.Client/Internals/Nuid.cs index 32427d297..c0d63ebc3 100644 --- a/src/NATS.Client/Internals/Nuid.cs +++ b/src/NATS.Client/Internals/Nuid.cs @@ -35,7 +35,7 @@ internal sealed class Nuid (byte)'g', (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', (byte)'0', (byte)'1', (byte)'2', (byte)'3', - (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/', + (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'-', (byte)'_', }; private readonly object _nuidLock = new object(); From 42eb8e88674d84243c9084546a8258e81f01fe0a Mon Sep 17 00:00:00 2001 From: jasperd Date: Tue, 28 Jan 2020 00:37:41 +0100 Subject: [PATCH 27/30] Fix tests cf. a8b925e --- src/Tests/UnitTests/Internals/TestNuid.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tests/UnitTests/Internals/TestNuid.cs b/src/Tests/UnitTests/Internals/TestNuid.cs index b8416ed14..6ed18716d 100644 --- a/src/Tests/UnitTests/Internals/TestNuid.cs +++ b/src/Tests/UnitTests/Internals/TestNuid.cs @@ -80,7 +80,7 @@ public void GetNextNuid_ContainsOnlyValidCharacters() var result = nuid.GetNext(); // Assert - Assert.Matches("[A-z0-9+/]{22}", result); + Assert.Matches("[A-z0-9-_]{22}", result); } [Fact] @@ -112,7 +112,7 @@ public void GetNextNuid_PrefixAsExpected() var prefix = nuid.GetNext().Substring(0, 12); // Assert - Assert.Equal("ABCDEFGHL9+/", prefix); + Assert.Equal("ABCDEFGHL9-_", prefix); } [Fact] From 698acc944445a821cc4a6c65e4f5382fd0573bf7 Mon Sep 17 00:00:00 2001 From: jasperd Date: Tue, 28 Jan 2020 00:51:40 +0100 Subject: [PATCH 28/30] Remove non-existent configuration in condition --- src/Tests/UnitTests/UnitTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tests/UnitTests/UnitTests.csproj b/src/Tests/UnitTests/UnitTests.csproj index fab8b4641..4b1bb9f07 100644 --- a/src/Tests/UnitTests/UnitTests.csproj +++ b/src/Tests/UnitTests/UnitTests.csproj @@ -5,7 +5,7 @@ true - + true ..\..\NATS.Client.snk From 55ca475de081e4bf3df582e2f9001292617b7b36 Mon Sep 17 00:00:00 2001 From: jasperd Date: Tue, 28 Jan 2020 01:33:05 +0100 Subject: [PATCH 29/30] Remove cast and use unsigned integers --- src/NATS.Client/Internals/Nuid.cs | 22 +++++++++++----------- src/Tests/UnitTests/Internals/TestNuid.cs | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/NATS.Client/Internals/Nuid.cs b/src/NATS.Client/Internals/Nuid.cs index c0d63ebc3..874b46825 100644 --- a/src/NATS.Client/Internals/Nuid.cs +++ b/src/NATS.Client/Internals/Nuid.cs @@ -24,7 +24,7 @@ internal sealed class Nuid private const uint NUID_LENGTH = PREFIX_LENGTH + SEQUENTIAL_LENGTH; private const int MIN_INCREMENT = 33; private const int MAX_INCREMENT = 333; - private const long MAX_SEQUENTIAL = 0x1000_0000_0000_0000; //64^10 + private const ulong MAX_SEQUENTIAL = 0x1000_0000_0000_0000; //64^10 private const int BASE = 64; private static readonly byte[] _digits = new byte[BASE]{ @@ -44,8 +44,8 @@ internal sealed class Nuid private readonly RandomNumberGenerator _cryptoRng; private byte[] _prefix = new byte[PREFIX_LENGTH]; - private int _increment; - private long _sequential; + private uint _increment; + private ulong _sequential; /// /// Initializes a new instance of . @@ -57,7 +57,7 @@ internal sealed class Nuid /// A cryptographically strong random number generator. /// The initial sequential. /// The initial increment. - internal Nuid(RandomNumberGenerator rng = null, long? sequential = null, int? increment = null) + internal Nuid(RandomNumberGenerator rng = null, ulong? sequential = null, uint? increment = null) { if (rng is null) _cryptoRng = RandomNumberGenerator.Create(); @@ -102,7 +102,7 @@ internal string GetNext() var nuidBuffer = new char[NUID_LENGTH]; Monitor.Enter(_nuidLock); - long sequential = _sequential += _increment; + ulong sequential = _sequential += _increment; if (_sequential >= MAX_SEQUENTIAL) { SetPrefix(); @@ -121,7 +121,7 @@ internal string GetNext() { // We operate on unsigned integers and BASE is a power of two // therefore we can optimize sequential % BASE to sequential & (BASE - 1) - nuidBuffer[i] = (char)_digits[(int)sequential & (BASE - 1)]; + nuidBuffer[i] = (char)_digits[sequential & (BASE - 1)]; // BASE is 64 = 2^6 and sequential >= 0 // therefore we can optimize sequential / BASE to sequential >> 6 sequential >>= 6; @@ -130,12 +130,12 @@ internal string GetNext() return new string(nuidBuffer); } - private int GetIncrement() + private uint GetIncrement() { - return _rng.Next(MIN_INCREMENT, MAX_INCREMENT); + return (uint)_rng.Next(MIN_INCREMENT, MAX_INCREMENT); } - private long GetSequential() + private ulong GetSequential() { var randomBytes = new byte[8]; @@ -151,8 +151,8 @@ private long GetSequential() // The right hand side of the comparision happens to be const too now and can be folded to: // 18446744073709551615 which happens to be equal to ulong.MaxValue, hence the condition will // never be true and we can omit the do-while loop entirely. - - return (long)(sequential % MAX_SEQUENTIAL); + + return sequential % MAX_SEQUENTIAL; } private void SetPrefix() diff --git a/src/Tests/UnitTests/Internals/TestNuid.cs b/src/Tests/UnitTests/Internals/TestNuid.cs index 6ed18716d..7cd8053a4 100644 --- a/src/Tests/UnitTests/Internals/TestNuid.cs +++ b/src/Tests/UnitTests/Internals/TestNuid.cs @@ -87,8 +87,8 @@ public void GetNextNuid_ContainsOnlyValidCharacters() public void GetNextNuid_PrefixRenewed() { // Arrange - var increment = 100; - var maxSequential = 0x1000_0000_0000_0000 - increment - 1; + var increment = 100U; + var maxSequential = 0x1000_0000_0000_0000UL - increment - 1; var nuid = new Nuid(RandomNumberGenerator.Create(), maxSequential, increment); // Act From 27f35bb96b12e00108676befaca89887adc9e346 Mon Sep 17 00:00:00 2001 From: jasperd Date: Tue, 28 Jan 2020 18:18:14 +0100 Subject: [PATCH 30/30] Use lock statement to ensure monitor is exited - https://github.com/nats-io/nats.net/pull/360#discussion_r371415019 --- src/NATS.Client/Internals/Nuid.cs | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/NATS.Client/Internals/Nuid.cs b/src/NATS.Client/Internals/Nuid.cs index 874b46825..8797acc94 100644 --- a/src/NATS.Client/Internals/Nuid.cs +++ b/src/NATS.Client/Internals/Nuid.cs @@ -100,22 +100,24 @@ internal Nuid() : this(null) {} internal string GetNext() { var nuidBuffer = new char[NUID_LENGTH]; + var sequential = 0UL; - Monitor.Enter(_nuidLock); - ulong sequential = _sequential += _increment; - if (_sequential >= MAX_SEQUENTIAL) + lock (_nuidLock) { - SetPrefix(); - sequential = _sequential = GetSequential(); - _increment = GetIncrement(); - } - - // For small arrays this is way faster than Array.Copy and still faster than Buffer.BlockCopy - for (var i = 0; i < PREFIX_LENGTH; i++) - { - nuidBuffer[i] = (char)_prefix[i]; + sequential = _sequential += _increment; + if (_sequential >= MAX_SEQUENTIAL) + { + SetPrefix(); + sequential = _sequential = GetSequential(); + _increment = GetIncrement(); + } + + // For small arrays this is way faster than Array.Copy and still faster than Buffer.BlockCopy + for (var i = 0; i < PREFIX_LENGTH; i++) + { + nuidBuffer[i] = (char)_prefix[i]; + } } - Monitor.Exit(_nuidLock); for (var i = PREFIX_LENGTH; i < NUID_LENGTH; i++) {