diff --git a/Directory.Packages.props b/Directory.Packages.props index df66545fc2..7efc103789 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,6 +12,8 @@ + + diff --git a/Garnet.sln b/Garnet.sln index cf0c3713c4..e99560537d 100644 --- a/Garnet.sln +++ b/Garnet.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31808.319 MinimumVisualStudioVersion = 10.0.40219.1 @@ -96,6 +96,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommandInfoUpdater", "playg EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleModule", "playground\SampleModule\SampleModule.csproj", "{A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Garnet.gen", "libs\gen\Garnet.gen.csproj", "{E248012E-1D19-4114-924F-EFC9E859C4CD}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GarnetJSON", "playground\GarnetJSON\GarnetJSON.csproj", "{2C8F1F5D-31E5-4D00-A46E-F3B1D9BC098F}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MigrateBench", "playground\MigrateBench\MigrateBench.csproj", "{6B66B394-E410-4B61-9A5A-1595FF6F5E08}" EndProject @@ -289,6 +291,14 @@ Global {A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}.Release|Any CPU.Build.0 = Release|Any CPU {A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}.Release|x64.ActiveCfg = Release|Any CPU {A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}.Release|x64.Build.0 = Release|Any CPU + {E248012E-1D19-4114-924F-EFC9E859C4CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E248012E-1D19-4114-924F-EFC9E859C4CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E248012E-1D19-4114-924F-EFC9E859C4CD}.Debug|x64.ActiveCfg = Debug|Any CPU + {E248012E-1D19-4114-924F-EFC9E859C4CD}.Debug|x64.Build.0 = Debug|Any CPU + {E248012E-1D19-4114-924F-EFC9E859C4CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E248012E-1D19-4114-924F-EFC9E859C4CD}.Release|Any CPU.Build.0 = Release|Any CPU + {E248012E-1D19-4114-924F-EFC9E859C4CD}.Release|x64.ActiveCfg = Release|Any CPU + {E248012E-1D19-4114-924F-EFC9E859C4CD}.Release|x64.Build.0 = Release|Any CPU {2C8F1F5D-31E5-4D00-A46E-F3B1D9BC098F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2C8F1F5D-31E5-4D00-A46E-F3B1D9BC098F}.Debug|Any CPU.Build.0 = Debug|Any CPU {2C8F1F5D-31E5-4D00-A46E-F3B1D9BC098F}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -342,6 +352,7 @@ Global {9F6E4734-6341-4A9C-A7FF-636A39D8BEAD} = {346A5A53-51E4-4A75-B7E6-491D950382CE} {9BE474A2-1547-43AC-B4F2-FB48A01FA995} = {69A71E2C-00E3-42F3-854E-BE157A24834E} {A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F} = {69A71E2C-00E3-42F3-854E-BE157A24834E} + {E248012E-1D19-4114-924F-EFC9E859C4CD} = {147FCE31-EC09-4C90-8E4D-37CA87ED18C3} {2C8F1F5D-31E5-4D00-A46E-F3B1D9BC098F} = {69A71E2C-00E3-42F3-854E-BE157A24834E} {6B66B394-E410-4B61-9A5A-1595FF6F5E08} = {69A71E2C-00E3-42F3-854E-BE157A24834E} {697766CD-2046-46D9-958A-0FD3B46C98D4} = {01823EA4-4446-4D66-B268-DFEE55951964} diff --git a/libs/common/EnumUtils.cs b/libs/common/EnumUtils.cs deleted file mode 100644 index 07adc9f380..0000000000 --- a/libs/common/EnumUtils.cs +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; - -namespace Garnet.common -{ - /// - /// Utilities for enums - /// - public static class EnumUtils - { - private static readonly Dictionary> EnumNameToDescriptionCache = new(); - private static readonly Dictionary>> EnumDescriptionToNameCache = new(); - - /// - /// Gets a mapping between an enum's string value to its description, for each of the enum's values - /// - /// Enum type - /// A dictionary mapping between the enum's string value to its description - public static IDictionary GetEnumNameToDescription() where T : Enum - { - // Check if mapping is already in the cache. If not, add it to the cache. - if (!EnumNameToDescriptionCache.ContainsKey(typeof(T))) - AddTypeToCache(); - - return EnumNameToDescriptionCache[typeof(T)]; - } - - /// - /// If enum does not have the 'Flags' attribute, gets an array of size 1 with the description of the enum's value. - /// If no description exists, returns the ToString() value of the input value. - /// If enum has the 'Flags' attribute, gets an array with all the descriptions of the flags which are turned on in the input value. - /// If no description exists, returns the ToString() value of the flag. - /// - /// Enum type - /// Enum value - /// Array of descriptions - public static string[] GetEnumDescriptions(T value) where T : Enum - { - var nameToDesc = GetEnumNameToDescription(); - return value.ToString().Split(',').Select(f => nameToDesc.ContainsKey(f.Trim()) ? nameToDesc[f.Trim()] : f).ToArray(); - } - - /// - /// Gets an enum's values based on the description attribute - /// - /// Enum type - /// Enum description - /// Enum values - /// True if matched more than one value successfully - public static bool TryParseEnumsFromDescription(string strVal, out IEnumerable vals) where T : struct, Enum - { - vals = new List(); - - if (!EnumDescriptionToNameCache.ContainsKey(typeof(T))) - AddTypeToCache(); - - if (!EnumDescriptionToNameCache[typeof(T)].ContainsKey(strVal)) - return false; - - foreach (var enumName in EnumDescriptionToNameCache[typeof(T)][strVal]) - { - if (Enum.TryParse(enumName, out T enumVal)) - { - ((List)vals).Add(enumVal); - } - } - - return ((List)vals).Count > 0; - } - - /// - /// Gets an enum's value based on its description attribute - /// If more than one values match the same description, returns the first one - /// - /// Enum type - /// Enum description - /// Enum value - /// True if successful - public static bool TryParseEnumFromDescription(string strVal, out T val) where T : struct, Enum - { - var isSuccessful = TryParseEnumsFromDescription(strVal, out IEnumerable vals); - val = isSuccessful ? vals.First() : default; - return isSuccessful; - } - - - private static void AddTypeToCache() - { - var valToDesc = new Dictionary(); - var descToVals = new Dictionary>(); - - foreach (var flagFieldInfo in typeof(T).GetFields()) - { - var descAttr = (DescriptionAttribute)flagFieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault(); - if (descAttr != null) - { - valToDesc.Add(flagFieldInfo.Name, descAttr.Description); - if (!descToVals.ContainsKey(descAttr.Description)) - descToVals.Add(descAttr.Description, new List()); - descToVals[descAttr.Description].Add(flagFieldInfo.Name); - } - } - - EnumNameToDescriptionCache.Add(typeof(T), valToDesc); - EnumDescriptionToNameCache.Add(typeof(T), descToVals); - } - } -} \ No newline at end of file diff --git a/libs/common/GenerateEnumUtilsAttribute.cs b/libs/common/GenerateEnumUtilsAttribute.cs new file mode 100644 index 0000000000..05042925a3 --- /dev/null +++ b/libs/common/GenerateEnumUtilsAttribute.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; + +namespace Garnet.common +{ + /// + /// Specifies that utility methods for generating enum descriptions should be generated for the target enum. + /// + [AttributeUsage(AttributeTargets.Enum)] + public sealed class GenerateEnumDescriptionUtilsAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/libs/gen/EnumsSourceGenerator.cs b/libs/gen/EnumsSourceGenerator.cs new file mode 100644 index 0000000000..9697b7ad5d --- /dev/null +++ b/libs/gen/EnumsSourceGenerator.cs @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.CodeDom.Compiler; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Garnet; + +[Generator] +public class EnumsSourceGenerator : IIncrementalGenerator +{ + const string GeneratedClassName = "EnumUtils"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var enumDetails = context.SyntaxProvider + .ForAttributeWithMetadataName( + "Garnet.common.GenerateEnumDescriptionUtilsAttribute", + predicate: static (_, _) => true, + transform: static (ctx, _) => TransformEnumDetails((EnumDeclarationSyntax)ctx.TargetNode, ctx.SemanticModel) + ); + var enumUtils = enumDetails.Select(static (details, _) => Execute(details)); + + context.RegisterSourceOutput(enumUtils, (ctx, source) => ctx.AddSource($"{GeneratedClassName}.{source.EnumName}.g.cs", source.ClassSource)); + } + + private static EnumDetails TransformEnumDetails(EnumDeclarationSyntax enumDeclaration, SemanticModel semanticModel) + { + var namespaceDeclaration = enumDeclaration.FirstAncestorOrSelf(); + var enumName = enumDeclaration.Identifier.Text; + + var values = enumDeclaration.Members + .Select(m => ( + m.Identifier.Text, + semanticModel.GetOperation(m.EqualsValue!.Value)!.ConstantValue.Value, + Description: m.AttributeLists + .SelectMany(al => al.Attributes).Where(a => a.Name.ToString() == "Description") + .SingleOrDefault()?.ArgumentList?.Arguments.Single().ToString() + )) + .ToArray(); + return new EnumDetails(namespaceDeclaration!.Name.ToString(), enumName, new EquatableArray<(string Name, object? Value, string? Description)>(values)); + } + + private static (string EnumName, string ClassSource) Execute(EnumDetails details) + { + using var classWriter = new IndentedTextWriter(new StringWriter()); + classWriter.WriteLine("// "); + classWriter.WriteLine($"// This code was generated by the {nameof(EnumsSourceGenerator)} source generator."); + classWriter.WriteLine("// "); + + classWriter.WriteLine("using System;"); + classWriter.WriteLine("using System.ComponentModel;"); + classWriter.WriteLine("using System.Numerics;"); + classWriter.WriteLine(); + classWriter.WriteLine($"namespace {details.Namespace};"); + classWriter.WriteLine(); + classWriter.WriteLine("/// "); + classWriter.WriteLine($"/// Utility methods for enums."); + classWriter.WriteLine("/// "); + classWriter.WriteLine($"public static partial class {GeneratedClassName}"); + classWriter.WriteLine("{"); + classWriter.Indent++; + GenerateTryParseEnumFromDescriptionMethod(classWriter, details); + classWriter.WriteLine(); + GenerateGetEnumDescriptionsMethod(classWriter, details); + classWriter.Indent--; + classWriter.WriteLine("}"); + + return (details.EnumName, classWriter.InnerWriter.ToString()!); + } + + private static void GenerateTryParseEnumFromDescriptionMethod(IndentedTextWriter classWriter, EnumDetails details) + { + classWriter.WriteLine("/// "); + classWriter.WriteLine("/// Tries to parse the enum value from the description."); + classWriter.WriteLine("/// "); + classWriter.WriteLine("/// Enum description."); + classWriter.WriteLine("/// Enum value."); + classWriter.WriteLine("/// True if successful."); + classWriter.WriteLine($"public static bool TryParse{details.EnumName}FromDescription(string description, out {details.EnumName} result)"); + classWriter.WriteLine("{"); + classWriter.Indent++; + classWriter.WriteLine("result = default;"); + classWriter.WriteLine("switch (description)"); + classWriter.WriteLine("{"); + classWriter.Indent++; + foreach (var (name, _, description) in details.Values) + { + bool hasDescription = false; + if (description is not null) + { + hasDescription = true; + classWriter.WriteLine($"case {description}:"); + } + + if (!hasDescription) continue; + classWriter.Indent++; + classWriter.WriteLine($"result = {details.EnumName}.{name};"); + classWriter.WriteLine("return true;"); + classWriter.Indent--; + } + classWriter.Indent--; + classWriter.WriteLine("}"); + classWriter.WriteLine(); + classWriter.WriteLine("return false;"); + classWriter.Indent--; + classWriter.WriteLine("}"); + classWriter.WriteLine(); + } + + private static void GenerateGetEnumDescriptionsMethod(IndentedTextWriter classWriter, EnumDetails details) + { + classWriter.WriteLine("/// "); + classWriter.WriteLine("/// Gets the descriptions of the set flags. Assumes the enum is a flags enum."); + classWriter.WriteLine("/// If no description exists, returns the ToString() value of the input value."); + classWriter.WriteLine("/// "); + classWriter.WriteLine("/// Enum value."); + classWriter.WriteLine("/// Array of descriptions."); + classWriter.WriteLine($"public static string[] Get{details.EnumName}Descriptions({details.EnumName} value)"); + classWriter.WriteLine("{"); + classWriter.Indent++; + foreach (var (name, value, description) in details.Values) + { + if (!IsPow2(value)) + { + var toStringValue = description ?? $"\"{name}\""; + classWriter.WriteLine($"if (value is {details.EnumName}.{name}) return [{toStringValue}];"); + } + } + classWriter.WriteLine("var setFlags = BitOperations.PopCount((ulong)value);"); + classWriter.WriteLine("if (setFlags == 1)"); + classWriter.WriteLine("{"); + classWriter.Indent++; + classWriter.WriteLine("return value switch"); + classWriter.WriteLine("{"); + classWriter.Indent++; + foreach (var (name, value, description) in details.Values) + { + if (description is null) continue; + if (!IsPow2(value)) continue; + classWriter.WriteLine($"{details.EnumName}.{name} => [{description}],"); + } + classWriter.WriteLine($"_ => [value.ToString()],"); + classWriter.Indent--; + classWriter.WriteLine("};"); + classWriter.Indent--; + classWriter.WriteLine("}"); + classWriter.WriteLine(); + classWriter.WriteLine("var descriptions = new string[setFlags];"); + classWriter.WriteLine("var index = 0;"); + foreach (var (name, value, description) in details.Values) + { + if (description is null) continue; + if (!IsPow2(value)) continue; + classWriter.WriteLine($"if ((value & {details.EnumName}.{name}) != 0) descriptions[index++] = {description};"); + } + classWriter.WriteLine(); + classWriter.WriteLine("return descriptions;"); + classWriter.Indent--; + classWriter.WriteLine("}"); + } + + static bool IsPow2(object? value) + { + if (value is int x) return x != 0 && (x & (x - 1)) == 0; + return false; + } + + private record struct EnumDetails(string Namespace, string EnumName, EquatableArray<(string Name, object? Value, string? Description)> Values); + + private class EnumDetailsComparer : IEqualityComparer + { + public bool Equals(EnumDetails x, EnumDetails y) => x.EnumName.Equals(y.EnumName) && x.Namespace.Equals(y.Namespace) && x.Values.SequenceEqual(y.Values); + public int GetHashCode(EnumDetails obj) + { + var hash = new HashCode(); + hash.Add(obj.EnumName); + hash.Add(obj.Namespace); + foreach (var value in obj.Values) + { + hash.Add(value); + } + return hash.ToHashCode(); + } + } +} \ No newline at end of file diff --git a/libs/gen/EquatableArray.cs b/libs/gen/EquatableArray.cs new file mode 100644 index 0000000000..4ee9665644 --- /dev/null +++ b/libs/gen/EquatableArray.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Garnet; + +/// +/// An immutable, equatable array. This is equivalent to but with value equality support. +/// +/// The type of values in the array. +internal readonly struct EquatableArray : IEquatable>, IEnumerable + where T : IEquatable +{ + public static readonly EquatableArray Empty = new([]); + + /// + /// The underlying array. + /// + private readonly T[]? _array; + + /// + /// Creates a new instance. + /// + /// The input to wrap. + public EquatableArray(T[] array) + { + _array = array; + } + + /// + public bool Equals(EquatableArray array) + { + return AsSpan().SequenceEqual(array.AsSpan()); + } + + /// + public override bool Equals(object? obj) + { + return obj is EquatableArray array && Equals(this, array); + } + + /// + public override int GetHashCode() + { + if (_array is not T[] array) + { + return 0; + } + + HashCode hashCode = default; + + foreach (T item in array) + { + hashCode.Add(item); + } + + return hashCode.ToHashCode(); + } + + /// + /// Returns a wrapping the current items. + /// + /// A wrapping the current items. + public ReadOnlySpan AsSpan() + { + return _array.AsSpan(); + } + + /// + /// Gets the underlying array if there is one + /// + public T[]? GetArray() => _array; + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)(_array ?? [])).GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)(_array ?? [])).GetEnumerator(); + } + + public int Count => _array?.Length ?? 0; + + /// + /// Checks whether two values are the same. + /// + /// The first value. + /// The second value. + /// Whether and are equal. + public static bool operator ==(EquatableArray left, EquatableArray right) + { + return left.Equals(right); + } + + /// + /// Checks whether two values are not the same. + /// + /// The first value. + /// The second value. + /// Whether and are not equal. + public static bool operator !=(EquatableArray left, EquatableArray right) + { + return !left.Equals(right); + } +} \ No newline at end of file diff --git a/libs/gen/Garnet.gen.csproj b/libs/gen/Garnet.gen.csproj new file mode 100644 index 0000000000..795c1ed291 --- /dev/null +++ b/libs/gen/Garnet.gen.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0 + enable + enable + Latest + true + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/libs/gen/HashCode.cs b/libs/gen/HashCode.cs new file mode 100644 index 0000000000..a015da414b --- /dev/null +++ b/libs/gen/HashCode.cs @@ -0,0 +1,384 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Garnet; + +/// +/// Polyfill for .NET 6 HashCode +/// +internal struct HashCode +{ + private static readonly uint s_seed = GenerateGlobalSeed(); + + private const uint Prime1 = 2654435761U; + private const uint Prime2 = 2246822519U; + private const uint Prime3 = 3266489917U; + private const uint Prime4 = 668265263U; + private const uint Prime5 = 374761393U; + + private uint _v1, _v2, _v3, _v4; + private uint _queue1, _queue2, _queue3; + private uint _length; + + private static uint GenerateGlobalSeed() + { + var buffer = new byte[sizeof(uint)]; + new Random().NextBytes(buffer); + return BitConverter.ToUInt32(buffer, 0); + } + + public static int Combine(T1 value1) + { + // Provide a way of diffusing bits from something with a limited + // input hash space. For example, many enums only have a few + // possible hashes, only using the bottom few bits of the code. Some + // collections are built on the assumption that hashes are spread + // over a larger space, so diffusing the bits may help the + // collection work more efficiently. + + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + + uint hash = MixEmptyState(); + hash += 4; + + hash = QueueRound(hash, hc1); + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + + uint hash = MixEmptyState(); + hash += 8; + + hash = QueueRound(hash, hc1); + hash = QueueRound(hash, hc2); + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + + uint hash = MixEmptyState(); + hash += 12; + + hash = QueueRound(hash, hc1); + hash = QueueRound(hash, hc2); + hash = QueueRound(hash, hc3); + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 16; + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 20; + + hash = QueueRound(hash, hc5); + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + uint hc6 = (uint)(value6?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 24; + + hash = QueueRound(hash, hc5); + hash = QueueRound(hash, hc6); + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + uint hc6 = (uint)(value6?.GetHashCode() ?? 0); + uint hc7 = (uint)(value7?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 28; + + hash = QueueRound(hash, hc5); + hash = QueueRound(hash, hc6); + hash = QueueRound(hash, hc7); + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + uint hc6 = (uint)(value6?.GetHashCode() ?? 0); + uint hc7 = (uint)(value7?.GetHashCode() ?? 0); + uint hc8 = (uint)(value8?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + v1 = Round(v1, hc5); + v2 = Round(v2, hc6); + v3 = Round(v3, hc7); + v4 = Round(v4, hc8); + + uint hash = MixState(v1, v2, v3, v4); + hash += 32; + + hash = MixFinal(hash); + return (int)hash; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4) + { + v1 = s_seed + Prime1 + Prime2; + v2 = s_seed + Prime2; + v3 = s_seed; + v4 = s_seed - Prime1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Round(uint hash, uint input) + { + return RotateLeft(hash + input * Prime2, 13) * Prime1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint QueueRound(uint hash, uint queuedValue) + { + return RotateLeft(hash + queuedValue * Prime3, 17) * Prime4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixState(uint v1, uint v2, uint v3, uint v4) + { + return RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18); + } + + private static uint MixEmptyState() + { + return s_seed + Prime5; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixFinal(uint hash) + { + hash ^= hash >> 15; + hash *= Prime2; + hash ^= hash >> 13; + hash *= Prime3; + hash ^= hash >> 16; + return hash; + } + + public void Add(T value) + { + Add(value?.GetHashCode() ?? 0); + } + + public void Add(T value, IEqualityComparer? comparer) + { + Add(value is null ? 0 : (comparer?.GetHashCode(value) ?? value.GetHashCode())); + } + + private void Add(int value) + { + // The original xxHash works as follows: + // 0. Initialize immediately. We can't do this in a struct (no + // default ctor). + // 1. Accumulate blocks of length 16 (4 uints) into 4 accumulators. + // 2. Accumulate remaining blocks of length 4 (1 uint) into the + // hash. + // 3. Accumulate remaining blocks of length 1 into the hash. + + // There is no need for #3 as this type only accepts ints. _queue1, + // _queue2 and _queue3 are basically a buffer so that when + // ToHashCode is called we can execute #2 correctly. + + // We need to initialize the xxHash32 state (_v1 to _v4) lazily (see + // #0) nd the last place that can be done if you look at the + // original code is just before the first block of 16 bytes is mixed + // in. The xxHash32 state is never used for streams containing fewer + // than 16 bytes. + + // To see what's really going on here, have a look at the Combine + // methods. + + uint val = (uint)value; + + // Storing the value of _length locally shaves of quite a few bytes + // in the resulting machine code. + uint previousLength = _length++; + uint position = previousLength % 4; + + // Switch can't be inlined. + + if (position == 0) + _queue1 = val; + else if (position == 1) + _queue2 = val; + else if (position == 2) + _queue3 = val; + else // position == 3 + { + if (previousLength == 3) + Initialize(out _v1, out _v2, out _v3, out _v4); + + _v1 = Round(_v1, _queue1); + _v2 = Round(_v2, _queue2); + _v3 = Round(_v3, _queue3); + _v4 = Round(_v4, val); + } + } + + public int ToHashCode() + { + // Storing the value of _length locally shaves of quite a few bytes + // in the resulting machine code. + uint length = _length; + + // position refers to the *next* queue position in this method, so + // position == 1 means that _queue1 is populated; _queue2 would have + // been populated on the next call to Add. + uint position = length % 4; + + // If the length is less than 4, _v1 to _v4 don't contain anything + // yet. xxHash32 treats this differently. + + uint hash = length < 4 ? MixEmptyState() : MixState(_v1, _v2, _v3, _v4); + + // _length is incremented once per Add(Int32) and is therefore 4 + // times too small (xxHash length is in bytes, not ints). + + hash += length * 4; + + // Mix what remains in the queue + + // Switch can't be inlined right now, so use as few branches as + // possible by manually excluding impossible scenarios (position > 1 + // is always false if position is not > 0). + if (position > 0) + { + hash = QueueRound(hash, _queue1); + if (position > 1) + { + hash = QueueRound(hash, _queue2); + if (position > 2) + hash = QueueRound(hash, _queue3); + } + } + + hash = MixFinal(hash); + return (int)hash; + } + +#pragma warning disable 0809 + // Obsolete member 'memberA' overrides non-obsolete member 'memberB'. + // Disallowing GetHashCode and Equals is by design + + // * We decided to not override GetHashCode() to produce the hash code + // as this would be weird, both naming-wise as well as from a + // behavioral standpoint (GetHashCode() should return the object's + // hash code, not the one being computed). + + // * Even though ToHashCode() can be called safely multiple times on + // this implementation, it is not part of the contract. If the + // implementation has to change in the future we don't want to worry + // about people who might have incorrectly used this type. + + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => throw new NotSupportedException("Hash code not supported"); + + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object? obj) => throw new NotSupportedException("Equality not supported"); +#pragma warning restore 0809 + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint RotateLeft(uint value, int offset) + => (value << offset) | (value >> (32 - offset)); +} \ No newline at end of file diff --git a/libs/server/Garnet.server.csproj b/libs/server/Garnet.server.csproj index 596927fcd4..dd0620c8bf 100644 --- a/libs/server/Garnet.server.csproj +++ b/libs/server/Garnet.server.csproj @@ -5,6 +5,7 @@ ../../Garnet.snk false true + true @@ -27,4 +28,9 @@ + + + + + \ No newline at end of file diff --git a/libs/server/Resp/RespCommandInfoFlags.cs b/libs/server/Resp/RespCommandInfoFlags.cs index d2cfcd9c30..f9513fbad1 100644 --- a/libs/server/Resp/RespCommandInfoFlags.cs +++ b/libs/server/Resp/RespCommandInfoFlags.cs @@ -4,6 +4,7 @@ using System; using System.ComponentModel; using System.Net.NetworkInformation; +using Garnet.common; namespace Garnet.server { @@ -11,6 +12,7 @@ namespace Garnet.server /// RESP command flags /// [Flags] + [GenerateEnumDescriptionUtils] public enum RespCommandFlags { None = 0, @@ -62,6 +64,7 @@ public enum RespCommandFlags /// RESP ACL categories /// [Flags] + [GenerateEnumDescriptionUtils] public enum RespAclCategories { None = 0, diff --git a/libs/server/Resp/RespCommandInfoParser.cs b/libs/server/Resp/RespCommandInfoParser.cs index b79b259819..2fa34e46f8 100644 --- a/libs/server/Resp/RespCommandInfoParser.cs +++ b/libs/server/Resp/RespCommandInfoParser.cs @@ -46,7 +46,7 @@ public static unsafe bool TryReadFromResp(ref byte* ptr, byte* end, IReadOnlyDic for (var flagIdx = 0; flagIdx < flagCount; flagIdx++) { if (!RespReadUtils.ReadSimpleString(out var strFlag, ref ptr, end) - || !EnumUtils.TryParseEnumFromDescription(strFlag, out var flag)) + || !EnumUtils.TryParseRespCommandFlagsFromDescription(strFlag, out var flag)) return false; flags |= flag; } @@ -69,7 +69,7 @@ public static unsafe bool TryReadFromResp(ref byte* ptr, byte* end, IReadOnlyDic for (var aclCatIdx = 0; aclCatIdx < aclCatCount; aclCatIdx++) { if (!RespReadUtils.ReadSimpleString(out var strAclCat, ref ptr, end) - || !EnumUtils.TryParseEnumFromDescription(strAclCat.TrimStart('@'), out var aclCat)) + || !EnumUtils.TryParseRespAclCategoriesFromDescription(strAclCat.TrimStart('@'), out var aclCat)) return false; aclCategories |= aclCat; } @@ -153,7 +153,7 @@ internal static unsafe bool TryReadFromResp(ref byte* ptr, byte* end, out RespCo for (var flagIdx = 0; flagIdx < flagsCount; flagIdx++) { if (!RespReadUtils.ReadSimpleString(out var strFlag, ref ptr, end) - || !EnumUtils.TryParseEnumFromDescription(strFlag, out var flag)) + || !EnumUtils.TryParseKeySpecificationFlagsFromDescription(strFlag, out var flag)) return false; flags |= flag; } diff --git a/libs/server/Resp/RespCommandKeySpecification.cs b/libs/server/Resp/RespCommandKeySpecification.cs index f11292b8a9..93e14a35c2 100644 --- a/libs/server/Resp/RespCommandKeySpecification.cs +++ b/libs/server/Resp/RespCommandKeySpecification.cs @@ -40,7 +40,7 @@ public KeySpecificationFlags Flags init { this.flags = value; - this.respFormatFlags = EnumUtils.GetEnumDescriptions(this.flags); + this.respFormatFlags = EnumUtils.GetKeySpecificationFlagsDescriptions(this.flags); } } @@ -100,6 +100,7 @@ public string ToRespFormat() /// RESP key specification flags /// [Flags] + [GenerateEnumDescriptionUtils] public enum KeySpecificationFlags : ushort { None = 0, diff --git a/libs/server/Resp/RespCommandsInfo.cs b/libs/server/Resp/RespCommandsInfo.cs index 81f9455841..4ed8e34304 100644 --- a/libs/server/Resp/RespCommandsInfo.cs +++ b/libs/server/Resp/RespCommandsInfo.cs @@ -52,7 +52,7 @@ public RespCommandFlags Flags init { this.flags = value; - this.respFormatFlags = EnumUtils.GetEnumDescriptions(this.flags); + this.respFormatFlags = EnumUtils.GetRespCommandFlagsDescriptions(this.flags); } } @@ -80,7 +80,7 @@ public RespAclCategories AclCategories init { this.aclCategories = value; - this.respFormatAclCategories = EnumUtils.GetEnumDescriptions(this.aclCategories); + this.respFormatAclCategories = EnumUtils.GetRespAclCategoriesDescriptions(this.aclCategories); } }