From 2fc66dc530c407dad94a005d2dc50bbd4e6484a6 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 19 Nov 2025 23:09:56 +0100 Subject: [PATCH 1/3] Cleanup constants and helpers --- .../Text/Json/JsonConstants.cs | 81 ------------------- .../Fusion.Execution/Text/Json/JsonHelpers.cs | 22 ----- .../src/Json/HotChocolate.Text.Json.csproj | 4 - 3 files changed, 107 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonConstants.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonConstants.cs index 7fd0b4379d4..71dafe31735 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonConstants.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonConstants.cs @@ -13,48 +13,22 @@ internal static class JsonConstants public const byte LineFeed = (byte)'\n'; public const byte Tab = (byte)'\t'; public const byte Comma = (byte)','; - public const byte KeyValueSeparator = (byte)':'; public const byte Quote = (byte)'"'; public const byte BackSlash = (byte)'\\'; public const byte Slash = (byte)'/'; public const byte BackSpace = (byte)'\b'; public const byte FormFeed = (byte)'\f'; - public const byte Asterisk = (byte)'*'; public const byte Colon = (byte)':'; - public const byte Period = (byte)'.'; - public const byte Plus = (byte)'+'; - public const byte Hyphen = (byte)'-'; - public const byte UtcOffsetToken = (byte)'Z'; - public const byte TimePrefix = (byte)'T'; - public const byte NewLineLineFeed = (byte)'\n'; - // \u2028 and \u2029 are considered respectively line and paragraph separators - // UTF-8 representation for them is E2, 80, A8/A9 - public const byte StartingByteOfNonStandardSeparator = 0xE2; - public static ReadOnlySpan Data => "data"u8; public static ReadOnlySpan Errors => "errors"u8; public static ReadOnlySpan Extensions => "extensions"u8; - public static ReadOnlySpan Utf8Bom => [0xEF, 0xBB, 0xBF]; public static ReadOnlySpan TrueValue => "true"u8; public static ReadOnlySpan FalseValue => "false"u8; public static ReadOnlySpan NullValue => "null"u8; - public static ReadOnlySpan NaNValue => "NaN"u8; - public static ReadOnlySpan PositiveInfinityValue => "Infinity"u8; - public static ReadOnlySpan NegativeInfinityValue => "-Infinity"u8; - public const int MaximumFloatingPointConstantLength = 9; - - // Used to search for the end of a number - public static ReadOnlySpan Delimiters => ",}] \n\r\t/"u8; - - // Explicitly skipping ReverseSolidus since that is handled separately - public static ReadOnlySpan EscapableChars => "\"nrt/ubf"u8; - - public const int RemoveFlagsBitMask = 0x7FFFFFFF; - // In the worst case, an ASCII character represented as a single utf-8 byte could expand 6x when escaped. // For example: '+' becomes '\u0043' // Escaping surrogate pairs (represented by 3 or 4 utf-8 bytes) would expand to 12 bytes (which is still <= 6x). @@ -66,65 +40,10 @@ internal static class JsonConstants // All other UTF-16 characters can be represented by either 1 or 2 UTF-8 bytes. public const int MaxExpansionFactorWhileTranscoding = 3; - // When transcoding from UTF8 -> UTF16, the byte count threshold where we rent from the array pool before performing a normal alloc. - public const long ArrayPoolMaxSizeBeforeUsingNormalAlloc = -#if NET - 1024 * 1024 * 1024; // ArrayPool limit increased in .NET 6 -#else - 1024 * 1024; -#endif - - // The maximum number of characters allowed when writing raw UTF-16 JSON. This is the maximum length that we can guarantee can - // be safely transcoded to UTF-8 and fit within an integer-length span, given the max expansion factor of a single character (3). - public const int MaxUtf16RawValueLength = int.MaxValue / MaxExpansionFactorWhileTranscoding; - - public const int MaxEscapedTokenSize = 1_000_000_000; // Max size for already escaped value. - public const int MaxUnescapedTokenSize = MaxEscapedTokenSize / MaxExpansionFactorWhileEscaping; // 166_666_666 bytes - public const int MaxCharacterTokenSize = MaxEscapedTokenSize / MaxExpansionFactorWhileEscaping; // 166_666_666 characters - - public const int MaximumFormatBooleanLength = 5; - public const int MaximumFormatInt64Length = 20; // 19 + sign (i.e. -9223372036854775808) - public const int MaximumFormatUInt32Length = 10; // i.e. 4294967295 - public const int MaximumFormatUInt64Length = 20; // i.e. 18446744073709551615 - public const int MaximumFormatDoubleLength = 128; // default (i.e. 'G'), using 128 (rather than say 32) to be future-proof. - public const int MaximumFormatSingleLength = 128; // default (i.e. 'G'), using 128 (rather than say 32) to be future-proof. - public const int MaximumFormatDecimalLength = 31; // default (i.e. 'G') - public const int MaximumFormatGuidLength = 36; // default (i.e. 'D'), 8 + 4 + 4 + 4 + 12 + 4 for the hyphens (e.g. 094ffa0a-0442-494d-b452-04003fa755cc) - public const int MaximumEscapedGuidLength = MaxExpansionFactorWhileEscaping * MaximumFormatGuidLength; - public const int MaximumFormatDateTimeLength = 27; // StandardFormat 'O', e.g. 2017-06-12T05:30:45.7680000 - public const int MaximumFormatDateTimeOffsetLength = 33; // StandardFormat 'O', e.g. 2017-06-12T05:30:45.7680000-07:00 - public const int MaxDateTimeUtcOffsetHours = 14; // The UTC offset portion of a TimeSpan or DateTime can be no more than 14 hours and no less than -14 hours. - public const int DateTimeNumFractionDigits = 7; // TimeSpan and DateTime formats allow exactly up to many digits for specifying the fraction after the seconds. - public const int MaxDateTimeFraction = 9_999_999; // The largest fraction expressible by TimeSpan and DateTime formats - public const int DateTimeParseNumFractionDigits = 16; // The maximum number of fraction digits the Json DateTime parser allows - public const int MaximumDateTimeOffsetParseLength = MaximumFormatDateTimeOffsetLength - + (DateTimeParseNumFractionDigits - DateTimeNumFractionDigits); // Like StandardFormat 'O' for DateTimeOffset, but allowing 9 additional (up to 16) fraction digits. - public const int MinimumDateTimeParseLength = 10; // YYYY-MM-DD - public const int MaximumEscapedDateTimeOffsetParseLength = MaxExpansionFactorWhileEscaping * MaximumDateTimeOffsetParseLength; - - public const int MaximumLiteralLength = 5; // Must be able to fit null, true, & false. - - // Encoding Helpers - public const char HighSurrogateStart = '\ud800'; - public const char HighSurrogateEnd = '\udbff'; - public const char LowSurrogateStart = '\udc00'; - public const char LowSurrogateEnd = '\udfff'; - public const int UnicodePlane01StartValue = 0x10000; public const int HighSurrogateStartValue = 0xD800; public const int HighSurrogateEndValue = 0xDBFF; public const int LowSurrogateStartValue = 0xDC00; public const int LowSurrogateEndValue = 0xDFFF; public const int BitShiftBy10 = 0x400; - - // The maximum number of parameters a constructor can have where it can be considered - // for a path on deserialization where we don't box the constructor arguments. - public const int UnboxedParameterCountThreshold = 4; - - // Two space characters is the default indentation. - public const char DefaultIndentCharacter = ' '; - public const char TabIndentCharacter = '\t'; - public const int DefaultIndentSize = 2; - public const int MinimumIndentSize = 0; - public const int MaximumIndentSize = 127; // If this value is changed, the impact on the options masking used in the JsonWriterOptions struct must be checked carefully. } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonHelpers.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonHelpers.cs index 6de1d7c5347..f8d9b5a8b12 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonHelpers.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonHelpers.cs @@ -4,13 +4,6 @@ namespace HotChocolate.Fusion.Text.Json; internal static class JsonHelpers { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsValidDateTimeOffsetParseLength(int length) - => IsInRangeInclusive( - length, - JsonConstants.MinimumDateTimeParseLength, - JsonConstants.MaximumEscapedDateTimeOffsetParseLength); - /// /// Returns if is between /// and , inclusive. @@ -18,19 +11,4 @@ public static bool IsValidDateTimeOffsetParseLength(int length) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRangeInclusive(uint value, uint lowerBound, uint upperBound) => (value - lowerBound) <= (upperBound - lowerBound); - - /// - /// Returns if is between - /// and , inclusive. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsInRangeInclusive(int value, int lowerBound, int upperBound) - => (uint)(value - lowerBound) <= (uint)(upperBound - lowerBound); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsValidUnescapedDateTimeOffsetParseLength(int length) - => IsInRangeInclusive( - length, - JsonConstants.MinimumDateTimeParseLength, - JsonConstants.MaximumDateTimeOffsetParseLength); } diff --git a/src/HotChocolate/Json/src/Json/HotChocolate.Text.Json.csproj b/src/HotChocolate/Json/src/Json/HotChocolate.Text.Json.csproj index c66c31fec8c..3366abcbd96 100644 --- a/src/HotChocolate/Json/src/Json/HotChocolate.Text.Json.csproj +++ b/src/HotChocolate/Json/src/Json/HotChocolate.Text.Json.csproj @@ -6,8 +6,4 @@ preview - - - - From e133a9b8436dadf123e94858cf8d1c285a25f750 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 20 Nov 2025 08:54:55 +0100 Subject: [PATCH 2/3] Copied metadb to the core --- .../Core/src/Types/HotChocolate.Types.csproj | 38 +++++ .../src/Types/Text/Json/ElementTokenType.cs | 21 +++ .../Types/Text/Json/ResultDocument.DbRow.cs | 157 ++++++++++++++++++ .../Json/CompositeResultDocument.DbRow.cs | 52 ++++-- .../Text/Json/JsonConstants.cs | 4 + .../Fusion.Execution/Text/Json/JsonHelpers.cs | 4 + 6 files changed, 260 insertions(+), 16 deletions(-) create mode 100644 src/HotChocolate/Core/src/Types/Text/Json/ElementTokenType.cs create mode 100644 src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.DbRow.cs diff --git a/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj b/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj index c97a2503d2a..efa4a1d2e5a 100644 --- a/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj +++ b/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj @@ -67,6 +67,40 @@ + + + + Text\Json\JsonConstants.cs + + + + Text\Json\JsonHelpers.cs + + + + Text\Json\JsonConstants.cs + + + + Text\Json\JsonConstants.cs + + + + Text\Json\JsonConstants.cs + + + + Text\Json\JsonConstants.cs + + + + Text\Json\JsonConstants.cs + + + + Text\Json\JsonConstants.cs + + @@ -152,4 +186,8 @@ + + + + diff --git a/src/HotChocolate/Core/src/Types/Text/Json/ElementTokenType.cs b/src/HotChocolate/Core/src/Types/Text/Json/ElementTokenType.cs new file mode 100644 index 00000000000..095f8521b55 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Text/Json/ElementTokenType.cs @@ -0,0 +1,21 @@ +namespace HotChocolate.Text.Json; + +internal enum ElementTokenType : byte +{ + None = 0, + StartObject = 1, + EndObject = 2, + StartArray = 3, + EndArray = 4, + PropertyName = 5, + // Retained for compatibility, we do not actually need this. + Comment = 6, + String = 7, + Number = 8, + True = 9, + False = 10, + Null = 11, + // A reference in case a property or array element point + // to an array or an object + Reference = 12 +} diff --git a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.DbRow.cs b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.DbRow.cs new file mode 100644 index 00000000000..701c7734e40 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.DbRow.cs @@ -0,0 +1,157 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace HotChocolate.Text.Json; + +public sealed partial class ResultDocument +{ + [StructLayout(LayoutKind.Sequential)] + internal readonly struct DbRow + { + public const int Size = 20; + public const int UnknownSize = -1; + + // 27 bits for location + 2 bits OpRefType + 3 reserved bits + private readonly int _locationAndOpRefType; + + // Sign bit for HasComplexChildren + 31 bits for size/length + private readonly int _sizeOrLengthUnion; + + // 4 bits TokenType + 27 bits NumberOfRows + 1 reserved bit + private readonly int _numberOfRowsTypeAndReserved; + + // 15 bits SourceDocumentId + 17 bits (high 17 bits of ParentRow) + private readonly int _sourceAndParentHigh; + + // 15 bits OperationReferenceId + 6 bits Flags + 11 bits (low bits of ParentRow) + private readonly int _selectionSetFlagsAndParentLow; + + public DbRow( + ElementTokenType tokenType, + int location, + int sizeOrLength = 0, + int sourceDocumentId = 0, + int parentRow = 0, + int operationReferenceId = 0, + OperationReferenceType operationReferenceType = OperationReferenceType.None, + int numberOfRows = 0, + ElementFlags flags = ElementFlags.None) + { + Debug.Assert((byte)tokenType < 16); + Debug.Assert(location is >= 0 and <= 0x07FFFFFF); // 27 bits + Debug.Assert(sizeOrLength >= UnknownSize); + Debug.Assert(sourceDocumentId is >= 0 and <= 0x7FFF); // 15 bits + Debug.Assert(parentRow is >= 0 and <= 0x0FFFFFFF); // 28 bits + Debug.Assert(operationReferenceId is >= 0 and <= 0x7FFF); // 15 bits + Debug.Assert(numberOfRows is >= 0 and <= 0x07FFFFFF); // 27 bits + Debug.Assert((byte)flags <= 63); // 6 bits (0x3F) + Debug.Assert((byte)operationReferenceType <= 3); // 2 bits + Debug.Assert(Unsafe.SizeOf() == Size); + + _locationAndOpRefType = location | ((int)operationReferenceType << 27); + _sizeOrLengthUnion = sizeOrLength; + _numberOfRowsTypeAndReserved = ((int)tokenType << 28) | (numberOfRows & 0x07FFFFFF); + _sourceAndParentHigh = sourceDocumentId | ((parentRow >> 11) << 15); + _selectionSetFlagsAndParentLow = operationReferenceId | ((int)flags << 15) | ((parentRow & 0x7FF) << 21); + } + + /// + /// Element token type (includes Reference for composition). + /// + /// + /// 4 bits = possible values + /// + public ElementTokenType TokenType => (ElementTokenType)(unchecked((uint)_numberOfRowsTypeAndReserved) >> 28); + + /// + /// Operation reference type indicating the type of GraphQL operation element. + /// + /// + /// 2 bits = 4 possible values + /// + public OperationReferenceType OperationReferenceType + => (OperationReferenceType)((_locationAndOpRefType >> 27) & 0x03); + + /// + /// Byte offset in source data OR metaDb row index for references. + /// + /// + /// 2 bits = 4 possible values + /// + public int Location => _locationAndOpRefType & 0x07FFFFFF; + + /// + /// Length of data in JSON payload, number of elements if array or number of properties in an object. + /// + /// + /// 27 bits = 134M limit + /// + public int SizeOrLength => _sizeOrLengthUnion & int.MaxValue; + + /// + /// String/PropertyName: Unescaping required. + /// + public bool HasComplexChildren => _sizeOrLengthUnion < 0; + + /// + /// Specifies if a size for the item has ben set. + /// + public bool IsUnknownSize => _sizeOrLengthUnion == UnknownSize; + + /// + /// Number of metadb rows this element spans. + /// + /// + /// 27 bits = 134M rows + /// + public int NumberOfRows => _numberOfRowsTypeAndReserved & 0x07FFFFFF; + + /// + /// Index of parent element in metadb for navigation and null propagation. + /// + /// + /// 28 bits = 268M rows + /// + public int ParentRow + => ((int)((uint)_sourceAndParentHigh >> 15) << 11) | ((_selectionSetFlagsAndParentLow >> 21) & 0x7FF); + + /// + /// Reference to GraphQL selection set or selection metadata. + /// 15 bits = 32K selections + /// + public int OperationReferenceId => _selectionSetFlagsAndParentLow & 0x7FFF; + + /// + /// Element metadata flags. + /// + /// + /// 6 bits = 64 combinations + /// + public ElementFlags Flags => (ElementFlags)((_selectionSetFlagsAndParentLow >> 15) & 0x3F); + + /// + /// True for primitive JSON values (strings, numbers, booleans, null). + /// + public bool IsSimpleValue => TokenType is >= ElementTokenType.PropertyName and <= ElementTokenType.Null; + } + + internal enum OperationReferenceType : byte + { + None = 0, + SelectionSet = 1, + Selection = 2 + } + + [Flags] + internal enum ElementFlags : byte + { + None = 0, + Invalidated = 1, + SourceResult = 2, + IsNullable = 4, + IsRoot = 8, + IsInternal = 16, + IsExcluded = 32 + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/CompositeResultDocument.DbRow.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/CompositeResultDocument.DbRow.cs index 08fdf7eeff7..42785ef9614 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/CompositeResultDocument.DbRow.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/CompositeResultDocument.DbRow.cs @@ -57,67 +57,87 @@ public DbRow( } /// - /// Byte offset in source data OR metaDb row index for references. - /// 27 bits = 134M limit (increased from 26 bits / 67M limit) + /// Element token type (includes Reference for composition). /// - public int Location => _locationAndOpRefType & 0x07FFFFFF; + /// + /// 4 bits = possible values + /// + public ElementTokenType TokenType => (ElementTokenType)(unchecked((uint)_numberOfRowsTypeAndReserved) >> 28); /// /// Operation reference type indicating the type of GraphQL operation element. - /// 2 bits = 4 possible values /// + /// + /// 2 bits = 4 possible values + /// public OperationReferenceType OperationReferenceType => (OperationReferenceType)((_locationAndOpRefType >> 27) & 0x03); + /// + /// Byte offset in source data OR metaDb row index for references + /// + /// + /// 27 bits = 134M limit + /// + public int Location => _locationAndOpRefType & 0x07FFFFFF; + /// /// Length of data in JSON payload, number of elements if array or number of properties in an object. - /// 31 bits = 2GB limit /// + /// + /// 31 bits = 2GB limit + /// public int SizeOrLength => _sizeOrLengthUnion & int.MaxValue; /// /// String/PropertyName: Unescaping required. - /// Array: Contains complex children. /// public bool HasComplexChildren => _sizeOrLengthUnion < 0; - public bool IsUnknownSize => _sizeOrLengthUnion == UnknownSize; - /// - /// Element token type (includes Reference for composition). - /// 4 bits = 16 types + /// Specifies if a size for the item has ben set. /// - public ElementTokenType TokenType => (ElementTokenType)(unchecked((uint)_numberOfRowsTypeAndReserved) >> 28); + public bool IsUnknownSize => _sizeOrLengthUnion == UnknownSize; /// /// Number of metadb rows this element spans. - /// 27 bits = 134M rows /// + /// + /// 27 bits = 134M rows + /// public int NumberOfRows => _numberOfRowsTypeAndReserved & 0x07FFFFFF; /// /// Which source JSON document contains the data. - /// 15 bits = 32K documents /// + /// + /// 15 bits = 32K documents + /// public int SourceDocumentId => _sourceAndParentHigh & 0x7FFF; /// /// Index of parent element in metadb for navigation and null propagation. - /// 28 bits = 268M rows (reconstructed from high+low bits) /// + /// + /// 28 bits = 268M rows + /// public int ParentRow => ((int)((uint)_sourceAndParentHigh >> 15) << 11) | ((_selectionSetFlagsAndParentLow >> 21) & 0x7FF); /// /// Reference to GraphQL selection set or selection metadata. - /// 15 bits = 32K selections /// + /// + /// 15 bits = 32K selections + /// public int OperationReferenceId => _selectionSetFlagsAndParentLow & 0x7FFF; /// /// Element metadata flags. - /// 6 bits = 64 combinations /// + /// + /// 6 bits = 64 combinations + /// public ElementFlags Flags => (ElementFlags)((_selectionSetFlagsAndParentLow >> 15) & 0x3F); /// diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonConstants.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonConstants.cs index 71dafe31735..108aa42d801 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonConstants.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonConstants.cs @@ -1,4 +1,8 @@ +#if Fusion namespace HotChocolate.Fusion.Text.Json; +#else +namespace HotChocolate.Text.Json; +#endif internal static class JsonConstants { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonHelpers.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonHelpers.cs index f8d9b5a8b12..e824b996bab 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonHelpers.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonHelpers.cs @@ -1,6 +1,10 @@ using System.Runtime.CompilerServices; +#if Fusion namespace HotChocolate.Fusion.Text.Json; +#else +namespace HotChocolate.Text.Json; +#endif internal static class JsonHelpers { From baac8d853206507d02d9985e9e3b2e1288ee60e2 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 20 Nov 2025 09:17:13 +0100 Subject: [PATCH 3/3] Added metabd --- .../Core/src/Types/HotChocolate.Types.csproj | 6 +- .../Json/CompositeResultDocument.MetaDb.cs | 383 ++++++++++++++++++ .../Types/Text/Json/ResultDocument.Cursor.cs | 171 ++++++++ .../Text/Json/JsonConstants.cs | 2 +- .../Fusion.Execution/Text/Json/JsonHelpers.cs | 2 +- .../Text/Json/MetaDbEventSource.cs | 6 + 6 files changed, 565 insertions(+), 5 deletions(-) create mode 100644 src/HotChocolate/Core/src/Types/Text/Json/CompositeResultDocument.MetaDb.cs create mode 100644 src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.Cursor.cs diff --git a/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj b/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj index efa4a1d2e5a..ebf81309726 100644 --- a/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj +++ b/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj @@ -67,7 +67,7 @@ - + Text\Json\JsonConstants.cs @@ -77,8 +77,8 @@ Text\Json\JsonHelpers.cs - - Text\Json\JsonConstants.cs + + Text\Json\MetaDbEventSource.cs diff --git a/src/HotChocolate/Core/src/Types/Text/Json/CompositeResultDocument.MetaDb.cs b/src/HotChocolate/Core/src/Types/Text/Json/CompositeResultDocument.MetaDb.cs new file mode 100644 index 00000000000..5f6561ac0ec --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Text/Json/CompositeResultDocument.MetaDb.cs @@ -0,0 +1,383 @@ +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static HotChocolate.Text.Json.MetaDbEventSource; + +namespace HotChocolate.Text.Json; + +public sealed partial class ResultDocument +{ + internal struct MetaDb : IDisposable + { + private const int TokenTypeOffset = 8; + private static readonly ArrayPool s_arrayPool = ArrayPool.Shared; + + private byte[][] _chunks; + private Cursor _next; + private bool _disposed; + + internal static MetaDb CreateForEstimatedRows(int estimatedRows) + { + var chunksNeeded = Math.Max(4, (estimatedRows / Cursor.RowsPerChunk) + 1); + var chunks = s_arrayPool.Rent(chunksNeeded); + var log = Log; + + log.MetaDbCreated(2, estimatedRows, 1); + + // Rent the first chunk now to avoid branching on first append + chunks[0] = MetaDbMemory.Rent(); + log.ChunkAllocated(2, 0); + + for (var i = 1; i < chunks.Length; i++) + { + chunks[i] = []; + } + + return new MetaDb + { + _chunks = chunks, + _next = Cursor.Zero + }; + } + + public Cursor NextCursor => _next; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Cursor Append( + ElementTokenType tokenType, + int location = 0, + int sizeOrLength = 0, + int sourceDocumentId = 0, + int parentRow = 0, + int operationReferenceId = 0, + OperationReferenceType operationReferenceType = OperationReferenceType.None, + int numberOfRows = 0, + ElementFlags flags = ElementFlags.None) + { + var log = Log; + var next = _next; + var chunkIndex = next.Chunk; + var byteOffset = next.ByteOffset; + + var chunks = _chunks.AsSpan(); + var chunksLength = chunks.Length; + + if (byteOffset + DbRow.Size > Cursor.ChunkBytes) + { + chunkIndex++; + byteOffset = 0; + next = Cursor.FromByteOffset(chunkIndex, byteOffset); + } + + // make sure we have enough space for the chunk referenced by the chunkIndex. + if (chunkIndex >= chunksLength) + { + // if we do not have enough space we will double the size we have for + // chunks of memory. + var nextChunksLength = chunksLength * 2; + var newChunks = s_arrayPool.Rent(nextChunksLength); + log.ChunksExpanded(2, chunksLength, nextChunksLength); + + // copy chunks to new buffer + Array.Copy(_chunks, newChunks, chunksLength); + + for (var i = chunksLength; i < nextChunksLength; i++) + { + newChunks[i] = []; + } + + // clear and return old chunks buffer + chunks.Clear(); + s_arrayPool.Return(_chunks); + + // assign new chunks buffer + _chunks = newChunks; + chunks = newChunks.AsSpan(); + } + + var chunk = chunks[chunkIndex]; + + // if the chunk is empty we did not yet rent any memory for it + if (chunk.Length == 0) + { + chunk = chunks[chunkIndex] = MetaDbMemory.Rent(); + log.ChunkAllocated(2, chunkIndex); + } + + var row = new DbRow( + tokenType, + location, + sizeOrLength, + sourceDocumentId, + parentRow, + operationReferenceId, + operationReferenceType, + numberOfRows, + flags); + + ref var dest = ref MemoryMarshal.GetArrayDataReference(chunk); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref dest, byteOffset), row); + + // Advance write head by one row + _next = next + 1; + return next; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void Replace( + Cursor cursor, + ElementTokenType tokenType, + int location = 0, + int sizeOrLength = 0, + int sourceDocumentId = 0, + int parentRow = 0, + int operationReferenceId = 0, + OperationReferenceType operationReferenceType = OperationReferenceType.None, + int numberOfRows = 0, + ElementFlags flags = ElementFlags.None) + { + AssertValidCursor(cursor); + + var row = new DbRow( + tokenType, + location, + sizeOrLength, + sourceDocumentId, + parentRow, + operationReferenceId, + operationReferenceType, + numberOfRows, + flags); + + var span = _chunks[cursor.Chunk].AsSpan(cursor.ByteOffset); + + MemoryMarshal.Write(span, in row); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal DbRow Get(Cursor cursor) + { + AssertValidCursor(cursor); + + var span = _chunks[cursor.Chunk].AsSpan(cursor.ByteOffset); + + return MemoryMarshal.Read(span); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal (Cursor, ElementTokenType) GetStartCursor(Cursor cursor) + { + AssertValidCursor(cursor); + + var chunks = _chunks.AsSpan(); + var span = chunks[cursor.Chunk].AsSpan(cursor.ByteOffset); + var union = MemoryMarshal.Read(span[TokenTypeOffset..]); + var tokenType = (ElementTokenType)(union >> 28); + + if (tokenType is ElementTokenType.Reference) + { + var index = MemoryMarshal.Read(span) & 0x07FFFFFF; + cursor = Cursor.FromIndex(index); + span = chunks[cursor.Chunk].AsSpan(cursor.ByteOffset + TokenTypeOffset); + union = MemoryMarshal.Read(span); + tokenType = (ElementTokenType)(union >> 28); + } + + return (cursor, tokenType); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal int GetLocation(Cursor cursor) + { + AssertValidCursor(cursor); + + var span = _chunks[cursor.Chunk].AsSpan(cursor.ByteOffset); + + var locationAndOpRefType = MemoryMarshal.Read(span); + return locationAndOpRefType & 0x07FFFFFF; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Cursor GetLocationCursor(Cursor cursor) + { + AssertValidCursor(cursor); + + var span = _chunks[cursor.Chunk].AsSpan(cursor.ByteOffset); + + var locationAndOpRefType = MemoryMarshal.Read(span); + return Cursor.FromIndex(locationAndOpRefType & 0x07FFFFFF); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal int GetParent(Cursor cursor) + { + AssertValidCursor(cursor); + + var span = _chunks[cursor.Chunk].AsSpan(cursor.ByteOffset); + + var sourceAndParentHigh = MemoryMarshal.Read(span[12..]); + var selectionSetFlagsAndParentLow = MemoryMarshal.Read(span[16..]); + + return (sourceAndParentHigh >>> 15 << 11) + | ((selectionSetFlagsAndParentLow >> 21) & 0x7FF); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Cursor GetParentCursor(Cursor cursor) + { + AssertValidCursor(cursor); + + var span = _chunks[cursor.Chunk].AsSpan(cursor.ByteOffset); + + var sourceAndParentHigh = MemoryMarshal.Read(span[12..]); + var selectionSetFlagsAndParentLow = MemoryMarshal.Read(span[16..]); + + var index = (sourceAndParentHigh >>> 15 << 11) + | ((selectionSetFlagsAndParentLow >> 21) & 0x7FF); + + return Cursor.FromIndex(index); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal int GetNumberOfRows(Cursor cursor) + { + AssertValidCursor(cursor); + + var span = _chunks[cursor.Chunk].AsSpan(cursor.ByteOffset + TokenTypeOffset); + + var value = MemoryMarshal.Read(span); + return value & 0x0FFFFFFF; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ElementFlags GetFlags(Cursor cursor) + { + AssertValidCursor(cursor); + + var span = _chunks[cursor.Chunk].AsSpan(cursor.ByteOffset + 16); + + var selectionSetFlagsAndParentLow = MemoryMarshal.Read(span); + return (ElementFlags)((selectionSetFlagsAndParentLow >> 15) & 0x3F); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void SetFlags(Cursor cursor, ElementFlags flags) + { + AssertValidCursor(cursor); + Debug.Assert((byte)flags <= 63, "Flags value exceeds 6-bit limit"); + + var fieldSpan = _chunks[cursor.Chunk].AsSpan(cursor.ByteOffset + 16); + var currentValue = MemoryMarshal.Read(fieldSpan); + + var clearedValue = currentValue & 0xFFE07FFF; // ~(0x3F << 15) + var newValue = (int)(clearedValue | (uint)((int)flags << 15)); + + MemoryMarshal.Write(fieldSpan, newValue); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal int GetSizeOrLength(Cursor cursor) + { + AssertValidCursor(cursor); + + var span = _chunks[cursor.Chunk].AsSpan(cursor.ByteOffset + 4); + var value = MemoryMarshal.Read(span); + + return value & int.MaxValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void SetSizeOrLength(Cursor cursor, int sizeOrLength) + { + AssertValidCursor(cursor); + Debug.Assert(sizeOrLength >= 0 && sizeOrLength <= int.MaxValue, "SizeOrLength value exceeds 31-bit limit"); + + var fieldSpan = _chunks[cursor.Chunk].AsSpan(cursor.ByteOffset + 4); + var currentValue = MemoryMarshal.Read(fieldSpan); + + // Keep only the sign bit (HasComplexChildren) + var clearedValue = currentValue & unchecked((int)0x80000000); + var newValue = clearedValue | (sizeOrLength & int.MaxValue); + + MemoryMarshal.Write(fieldSpan, newValue); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void SetNumberOfRows(Cursor cursor, int numberOfRows) + { + AssertValidCursor(cursor); + Debug.Assert(numberOfRows >= 0 && numberOfRows <= 0x0FFFFFFF, "NumberOfRows value exceeds 28-bit limit"); + + var fieldSpan = _chunks[cursor.Chunk].AsSpan(cursor.ByteOffset + TokenTypeOffset); + var currentValue = MemoryMarshal.Read(fieldSpan); + + // Keep only the top 4 bits (token type) + var clearedValue = currentValue & unchecked((int)0xF0000000); + var newValue = clearedValue | (numberOfRows & 0x0FFFFFFF); + + MemoryMarshal.Write(fieldSpan, newValue); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ElementTokenType GetElementTokenType(Cursor cursor, bool resolveReferences = true) + { + AssertValidCursor(cursor); + + var union = MemoryMarshal.Read(_chunks[cursor.Chunk].AsSpan(cursor.ByteOffset + TokenTypeOffset)); + var tokenType = (ElementTokenType)(union >> 28); + + if (resolveReferences && tokenType == ElementTokenType.Reference) + { + var idx = GetLocation(cursor); + var resolved = Cursor.FromIndex(idx); + union = MemoryMarshal.Read(_chunks[resolved.Chunk].AsSpan(resolved.ByteOffset + TokenTypeOffset)); + tokenType = (ElementTokenType)(union >> 28); + } + + return tokenType; + } + + [Conditional("DEBUG")] + private void AssertValidCursor(Cursor cursor) + { + Debug.Assert(cursor.Chunk >= 0, "Negative chunk"); + Debug.Assert(cursor.Chunk < _chunks.Length, "Chunk index out of bounds"); + Debug.Assert(_chunks[cursor.Chunk].Length > 0, "Accessing unallocated chunk"); + + var maxExclusive = _next.Chunk * Cursor.RowsPerChunk + _next.Row; + var absoluteIndex = (cursor.Chunk * Cursor.RowsPerChunk) + cursor.Row; + + Debug.Assert(absoluteIndex >= 0 && absoluteIndex < maxExclusive, + $"Cursor points to row {absoluteIndex}, but only {maxExclusive} rows are valid."); + Debug.Assert(cursor.ByteOffset + DbRow.Size <= MetaDbMemory.BufferSize, "Cursor byte offset out of bounds"); + } + + public void Dispose() + { + if (!_disposed) + { + var cursor = _next; + var chunksLength = cursor.Chunk + 1; + var chunks = _chunks.AsSpan(0, chunksLength); + Log.MetaDbDisposed(2, chunksLength, cursor.Row); + + foreach (var chunk in chunks) + { + if (chunk.Length == 0) + { + break; + } + + MetaDbMemory.Return(chunk); + } + + chunks.Clear(); + s_arrayPool.Return(_chunks); + + _chunks = []; + _disposed = true; + } + } + } +} diff --git a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.Cursor.cs b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.Cursor.cs new file mode 100644 index 00000000000..4e8ee11677b --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.Cursor.cs @@ -0,0 +1,171 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace HotChocolate.Text.Json; + +public sealed partial class ResultDocument +{ + /// + /// Comparable MetaDb cursor (chunk, row) + /// + [StructLayout(LayoutKind.Sequential, Size = 4)] + internal readonly struct Cursor : IEquatable, IComparable + { + public const int ChunkBytes = 1 << 17; + public const int RowsPerChunk = ChunkBytes / DbRow.Size; + public const int MaxChunks = 4096; + + private const int RowBits = 14; + private const int ChunkBits = 12; + private const int ChunkShift = RowBits; + + private const uint RowMask = (1u << RowBits) - 1u; + private const uint ChunkMask = (1u << ChunkBits) - 1u; + + private readonly uint _value; + + public static readonly Cursor Zero = From(0, 0); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Cursor(uint value) => _value = value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Cursor From(int chunkIndex, int rowWithinChunk) + { + Debug.Assert((uint)chunkIndex < MaxChunks); + Debug.Assert((uint)rowWithinChunk < RowsPerChunk); + return new Cursor(((uint)chunkIndex << ChunkShift) | (uint)rowWithinChunk); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Cursor FromIndex(int rowIndex) + { + Debug.Assert(rowIndex >= 0); + var chunk = rowIndex / RowsPerChunk; + var row = rowIndex - (chunk * RowsPerChunk); + return From(chunk, row); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Cursor FromByteOffset(int chunkIndex, int byteOffset) + { + Debug.Assert(byteOffset % DbRow.Size == 0, "byteOffset must be row-aligned."); + return From(chunkIndex, byteOffset / DbRow.Size); + } + + public uint Value => _value; + + public int Chunk + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (int)((_value >> ChunkShift) & ChunkMask); + } + + public int Row + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (int)(_value & RowMask); + } + + public int ByteOffset + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Row * DbRow.Size; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cursor AddRows(int delta) + { + if (delta == 0) + { + return this; + } + + var row = Row + delta; + var chunk = Chunk; + + if (row >= RowsPerChunk) + { + var carry = row / RowsPerChunk; + row -= carry * RowsPerChunk; + chunk += carry; + } + else if (row < 0) + { + var borrow = (-row + RowsPerChunk - 1) / RowsPerChunk; + row += borrow * RowsPerChunk; + chunk -= borrow; + } + + if (chunk < 0) + { + Debug.Fail("Cursor underflow"); + chunk = 0; + row = 0; + } + else if (chunk >= MaxChunks) + { + Debug.Fail("Cursor overflow"); + chunk = MaxChunks - 1; + row = RowsPerChunk - 1; + } + + return From(chunk, row); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cursor WithChunk(int chunk) => From(chunk, Row); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cursor WithRow(int row) => From(Chunk, row); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ToIndex() => (Chunk * RowsPerChunk) + Row; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ToTotalBytes() => (Chunk * ChunkBytes) + ByteOffset; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Cursor other) => _value == other._value; + + public override bool Equals(object? obj) => obj is Cursor c && Equals(c); + + public override int GetHashCode() => (int)_value; + + public override string ToString() => $"chunk={Chunk}, row={Row} (0x{_value:X8})"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int CompareTo(Cursor other) => _value.CompareTo(other._value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Cursor a, Cursor b) => a._value == b._value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Cursor a, Cursor b) => a._value != b._value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator <(Cursor a, Cursor b) => a._value < b._value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator >(Cursor a, Cursor b) => a._value > b._value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator <=(Cursor a, Cursor b) => a._value <= b._value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator >=(Cursor a, Cursor b) => a._value >= b._value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Cursor operator +(Cursor x, int delta) => x.AddRows(delta); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Cursor operator -(Cursor x, int delta) => x.AddRows(-delta); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Cursor operator ++(Cursor x) => x.AddRows(1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Cursor operator --(Cursor x) => x.AddRows(-1); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonConstants.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonConstants.cs index 108aa42d801..9201cc9fc5d 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonConstants.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonConstants.cs @@ -1,4 +1,4 @@ -#if Fusion +#if FUSION namespace HotChocolate.Fusion.Text.Json; #else namespace HotChocolate.Text.Json; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonHelpers.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonHelpers.cs index e824b996bab..4a281fc2966 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonHelpers.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/JsonHelpers.cs @@ -1,6 +1,6 @@ using System.Runtime.CompilerServices; -#if Fusion +#if FUSION namespace HotChocolate.Fusion.Text.Json; #else namespace HotChocolate.Text.Json; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/MetaDbEventSource.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/MetaDbEventSource.cs index 8135fd7c4f7..9394f2d213d 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/MetaDbEventSource.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/MetaDbEventSource.cs @@ -1,8 +1,14 @@ using System.Diagnostics.Tracing; +#if FUSION namespace HotChocolate.Fusion.Text.Json; [EventSource(Name = "HotChocolate-Fusion-MetaDb")] +#else +namespace HotChocolate.Text.Json; + +[EventSource(Name = "HotChocolate-MetaDb")] +#endif internal sealed class MetaDbEventSource : EventSource { public static readonly MetaDbEventSource Log = new();