diff --git a/src/HotChocolate/Language/benchmarks/Language.Benchmarks/ParserBenchmarks.cs b/src/HotChocolate/Language/benchmarks/Language.Benchmarks/ParserBenchmarks.cs new file mode 100644 index 00000000000..38b163ce073 --- /dev/null +++ b/src/HotChocolate/Language/benchmarks/Language.Benchmarks/ParserBenchmarks.cs @@ -0,0 +1,149 @@ +using System.Text; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using HotChocolate.Language; + +namespace HotChocolate.Language.Benchmarks; + +[MemoryDiagnoser] +[ShortRunJob(RuntimeMoniker.Net10_0)] +public class ParserBenchmarks +{ + private byte[] _kitchenSinkQuery = null!; + private byte[] _schemaKitchenSink = null!; + private byte[] _simpleQuery = null!; + + [GlobalSetup] + public void Setup() + { + _kitchenSinkQuery = Encoding.UTF8.GetBytes(""" + "Query description" + query queryName("$foo description" $foo: ComplexType, "$site description" $site: Site = MOBILE) { + whoever123is: node(id: [123, 456]) { + id , + ... on User @defer { + field2 { + id , + alias: field1(first:10, after:$foo,) @include(if: $foo) { + id, + ...frag + } + } + } + ... @skip(unless: $foo) { + id + } + ... { + id + } + } + } + + "Mutation description" + mutation likeStory { + like(story: 123) @defer { + story { + id + } + } + } + + "Subscription description" + subscription StoryLikeSubscription("$input description" $input: StoryLikeSubscribeInput) { + storyLikeSubscribe(input: $input) { + story { + likers { + count + } + likeSentence { + text + } + } + } + } + + "Fragment description" + fragment frag on Friend { + foo(size: $size, bar: $b, obj: {key: "value"}) + } + + { + unnamed(truthy: true, falsey: false, nullish: null), + query + } + """); + + _schemaKitchenSink = Encoding.UTF8.GetBytes(""" + schema { + query: QueryType + mutation: MutationType + } + + type Foo implements Bar & Baz { + one: Type + two(argument: InputType!): Type + three(argument: InputType, other: String): Int + four(argument: String = "string"): String + five(argument: [String] = ["string", "string"]): String + six(argument: InputType = {key: "value"}): Type + seven(argument: Int = null): Type + } + + type AnnotatedObject @onObject(arg: "value") { + annotatedField(arg: Type = "default" @onArg): Type @onField + } + + interface Bar { + one: Type + four(argument: String = "string"): String + } + + union Feed = Story | Article | Advert + + scalar CustomScalar + + enum Site { + DESKTOP + MOBILE + } + + input InputType { + key: String! + answer: Int = 42 + } + + directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + + extend type Foo { + seven(argument: [String]): Type + } + + extend interface Bar { + two(argument: InputType!): Type + } + """); + + _simpleQuery = Encoding.UTF8.GetBytes(""" + { + hero { + name + friends { + name + } + } + } + """); + } + + [Benchmark] + public DocumentNode ParseKitchenSinkQuery() + => Utf8GraphQLParser.Parse(_kitchenSinkQuery); + + [Benchmark] + public DocumentNode ParseSchemaKitchenSink() + => Utf8GraphQLParser.Parse(_schemaKitchenSink); + + [Benchmark] + public DocumentNode ParseSimpleQuery() + => Utf8GraphQLParser.Parse(_simpleQuery); +} diff --git a/src/HotChocolate/Language/benchmarks/Language.Benchmarks/UnescapeBenchmarks.cs b/src/HotChocolate/Language/benchmarks/Language.Benchmarks/UnescapeBenchmarks.cs new file mode 100644 index 00000000000..cf75971a52d --- /dev/null +++ b/src/HotChocolate/Language/benchmarks/Language.Benchmarks/UnescapeBenchmarks.cs @@ -0,0 +1,72 @@ +using System.Text; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using HotChocolate.Language; + +namespace HotChocolate.Language.Benchmarks; + +[MemoryDiagnoser] +[ShortRunJob(RuntimeMoniker.Net10_0)] +public class UnescapeBenchmarks +{ + private byte[] _noEscapes = null!; + private byte[] _singleEscape = null!; + private byte[] _multipleEscapes = null!; + private byte[] _unicodeEscapes = null!; + private byte[] _blockString = null!; + + [GlobalSetup] + public void Setup() + { + _noEscapes = Encoding.UTF8.GetBytes("This is a simple string with no escape characters at all and it is moderately long"); + _singleEscape = Encoding.UTF8.GetBytes("This is a string with a single\\nnewline escape in it somewhere"); + _multipleEscapes = Encoding.UTF8.GetBytes("Line1\\nLine2\\tTabbed\\rReturn\\\\Backslash\\\"Quote\\/Slash"); + _unicodeEscapes = Encoding.UTF8.GetBytes("Unicode: \\u0041\\u0042\\u0043 and \\u00e9\\u00e8\\u00ea more text"); + _blockString = Encoding.UTF8.GetBytes("This is a block string\n with indentation\n and multiple lines\n but no escapes needed"); + } + + [Benchmark] + public int UnescapeNoEscapes() + { + ReadOnlySpan input = _noEscapes; + Span output = stackalloc byte[_noEscapes.Length]; + Utf8Helper.Unescape(in input, ref output, isBlockString: false); + return output.Length; + } + + [Benchmark] + public int UnescapeSingleEscape() + { + ReadOnlySpan input = _singleEscape; + Span output = stackalloc byte[_singleEscape.Length]; + Utf8Helper.Unescape(in input, ref output, isBlockString: false); + return output.Length; + } + + [Benchmark] + public int UnescapeMultipleEscapes() + { + ReadOnlySpan input = _multipleEscapes; + Span output = stackalloc byte[_multipleEscapes.Length]; + Utf8Helper.Unescape(in input, ref output, isBlockString: false); + return output.Length; + } + + [Benchmark] + public int UnescapeUnicodeEscapes() + { + ReadOnlySpan input = _unicodeEscapes; + Span output = stackalloc byte[_unicodeEscapes.Length]; + Utf8Helper.Unescape(in input, ref output, isBlockString: false); + return output.Length; + } + + [Benchmark] + public int UnescapeBlockString() + { + ReadOnlySpan input = _blockString; + Span output = stackalloc byte[_blockString.Length]; + Utf8Helper.Unescape(in input, ref output, isBlockString: true); + return output.Length; + } +} diff --git a/src/HotChocolate/Language/src/Language.Utf8/HotChocolate.Language.Utf8.csproj b/src/HotChocolate/Language/src/Language.Utf8/HotChocolate.Language.Utf8.csproj index 4446b00f3f6..fa37aeb9247 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/HotChocolate.Language.Utf8.csproj +++ b/src/HotChocolate/Language/src/Language.Utf8/HotChocolate.Language.Utf8.csproj @@ -20,6 +20,7 @@ + diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Extensions.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Extensions.cs index 95c5d081d5b..c839021b4ac 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Extensions.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Extensions.cs @@ -13,41 +13,52 @@ private ITypeSystemExtensionNode ParseTypeExtension() MoveNext(); - if (_reader.Kind == TokenKind.Name) + if (_reader.Kind == TokenKind.Name && _reader.Value.Length > 0) { - if (_reader.Value.SequenceEqual(GraphQLKeywords.Schema)) + switch (_reader.Value[0]) { - return ParseSchemaExtension(in start); - } - - if (_reader.Value.SequenceEqual(GraphQLKeywords.Scalar)) - { - return ParseScalarTypeExtension(in start); - } - - if (_reader.Value.SequenceEqual(GraphQLKeywords.Type)) - { - return ParseObjectTypeExtension(in start); - } - - if (_reader.Value.SequenceEqual(GraphQLKeywords.Interface)) - { - return ParseInterfaceTypeExtension(in start); - } - - if (_reader.Value.SequenceEqual(GraphQLKeywords.Union)) - { - return ParseUnionTypeExtension(in start); - } - - if (_reader.Value.SequenceEqual(GraphQLKeywords.Enum)) - { - return ParseEnumTypeExtension(in start); - } - - if (_reader.Value.SequenceEqual(GraphQLKeywords.Input)) - { - return ParseInputObjectTypeExtension(in start); + case (byte)'s': + if (_reader.Value.SequenceEqual(GraphQLKeywords.Schema)) + { + return ParseSchemaExtension(in start); + } + if (_reader.Value.SequenceEqual(GraphQLKeywords.Scalar)) + { + return ParseScalarTypeExtension(in start); + } + break; + + case (byte)'t': + if (_reader.Value.SequenceEqual(GraphQLKeywords.Type)) + { + return ParseObjectTypeExtension(in start); + } + break; + + case (byte)'i': + if (_reader.Value.SequenceEqual(GraphQLKeywords.Interface)) + { + return ParseInterfaceTypeExtension(in start); + } + if (_reader.Value.SequenceEqual(GraphQLKeywords.Input)) + { + return ParseInputObjectTypeExtension(in start); + } + break; + + case (byte)'u': + if (_reader.Value.SequenceEqual(GraphQLKeywords.Union)) + { + return ParseUnionTypeExtension(in start); + } + break; + + case (byte)'e': + if (_reader.Value.SequenceEqual(GraphQLKeywords.Enum)) + { + return ParseEnumTypeExtension(in start); + } + break; } } diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Operations.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Operations.cs index 1b69bc094cc..4c0e166b0f1 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Operations.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Operations.cs @@ -35,6 +35,33 @@ private OperationDefinitionNode ParseOperationDefinition() selectionSet); } + /// + /// Parses an operation definition with a pre-matched operation type, + /// avoiding redundant keyword comparison. + /// + private OperationDefinitionNode ParseOperationDefinition(OperationType operation) + { + var start = Start(); + + // skip the operation type keyword (already matched by caller) + MoveNext(); + + var name = _reader.Kind == TokenKind.Name ? ParseName() : null; + var variableDefinitions = ParseVariableDefinitions(); + var directives = ParseDirectives(false); + var selectionSet = ParseSelectionSet(); + var location = CreateLocation(in start); + + return new OperationDefinitionNode( + location, + name, + TakeDescription(), + operation, + variableDefinitions, + directives, + selectionSet); + } + /// /// Parses a shorthand form operation definition. /// : @@ -179,7 +206,7 @@ private SelectionSetNode ParseSelectionSet() TokenPrinter.Print(ref _reader))); } - var selections = new List(); + var selections = new List(8); // skip opening token MoveNext(); @@ -269,7 +296,7 @@ private List ParseArguments(bool isConstant) { if (_reader.Kind == TokenKind.LeftParenthesis) { - var list = new List(); + var list = new List(4); // skip opening token MoveNext(); diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.TypeDefinition.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.TypeDefinition.cs index 3b8261741ea..e1003fb7136 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.TypeDefinition.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.TypeDefinition.cs @@ -8,6 +8,7 @@ public ref partial struct Utf8GraphQLParser private static readonly List s_emptyEnumValues = []; private static readonly List s_emptyInputValues = []; private static readonly List s_emptyFieldDefinitions = []; + private static readonly List s_emptyNamedTypes = []; /// /// Parses a description. @@ -158,10 +159,10 @@ private ObjectTypeDefinitionNode ParseObjectTypeDefinition() /// private List ParseImplementsInterfaces() { - var list = new List(); - if (SkipImplementsKeyword()) { + var list = new List(); + // skip optional leading ampersand. SkipAmpersand(); @@ -170,9 +171,11 @@ private List ParseImplementsInterfaces() list.Add(ParseNamedType()); } while (SkipAmpersand()); + + return list; } - return list; + return s_emptyNamedTypes; } /// @@ -357,10 +360,10 @@ private UnionTypeDefinitionNode ParseUnionTypeDefinition() /// private List ParseUnionMemberTypes() { - var list = new List(); - if (SkipEqual()) { + var list = new List(); + // skip optional leading pipe (might not exist!) SkipPipe(); @@ -369,9 +372,11 @@ private List ParseUnionMemberTypes() list.Add(ParseNamedType()); } while (SkipPipe()); + + return list; } - return list; + return s_emptyNamedTypes; } /// diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Values.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Values.cs index 25b3ca0bd34..c3d3b1a6963 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Values.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Values.cs @@ -95,7 +95,7 @@ private ListValueNode ParseList(bool isConstant) Print(ref _reader)); } - var items = new List(); + var items = new List(4); // skip opening token MoveNext(); @@ -140,7 +140,7 @@ private ObjectValueNode ParseObject(bool isConstant) Print(ref _reader)); } - var fields = new List(); + var fields = new List(4); // skip opening token MoveNext(); diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs index f46f902377c..868ccff1e11 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs @@ -87,7 +87,7 @@ public DocumentNode Parse() try { _parsedNodes = 0; - var definitions = new List(); + var definitions = new List(16); var start = Start(); @@ -125,61 +125,89 @@ private IDefinitionNode ParseDefinition() if (_reader.Kind == TokenKind.Name) { - if (_reader.Value.SequenceEqual(GraphQLKeywords.Query) - || _reader.Value.SequenceEqual(GraphQLKeywords.Mutation) - || _reader.Value.SequenceEqual(GraphQLKeywords.Subscription)) + if (_reader.Value.Length > 0) { - return ParseOperationDefinition(); - } - - if (_reader.Value.SequenceEqual(GraphQLKeywords.Fragment)) - { - return ParseFragmentDefinition(); - } - - if (_reader.Value.SequenceEqual(GraphQLKeywords.Schema)) - { - return ParseSchemaDefinition(); - } - - if (_reader.Value.SequenceEqual(GraphQLKeywords.Scalar)) - { - return ParseScalarTypeDefinition(); - } - - if (_reader.Value.SequenceEqual(GraphQLKeywords.Type)) - { - return ParseObjectTypeDefinition(); - } - - if (_reader.Value.SequenceEqual(GraphQLKeywords.Interface)) - { - return ParseInterfaceTypeDefinition(); - } - - if (_reader.Value.SequenceEqual(GraphQLKeywords.Union)) - { - return ParseUnionTypeDefinition(); - } - - if (_reader.Value.SequenceEqual(GraphQLKeywords.Enum)) - { - return ParseEnumTypeDefinition(); - } - - if (_reader.Value.SequenceEqual(GraphQLKeywords.Input)) - { - return ParseInputObjectTypeDefinition(); - } - - if (_reader.Value.SequenceEqual(GraphQLKeywords.Extend)) - { - return ParseTypeExtension(); - } - - if (_reader.Value.SequenceEqual(GraphQLKeywords.Directive)) - { - return ParseDirectiveDefinition(); + switch (_reader.Value[0]) + { + case (byte)'q': + if (_reader.Value.SequenceEqual(GraphQLKeywords.Query)) + { + return ParseOperationDefinition(OperationType.Query); + } + break; + + case (byte)'m': + if (_reader.Value.SequenceEqual(GraphQLKeywords.Mutation)) + { + return ParseOperationDefinition(OperationType.Mutation); + } + break; + + case (byte)'s': + if (_reader.Value.SequenceEqual(GraphQLKeywords.Subscription)) + { + return ParseOperationDefinition(OperationType.Subscription); + } + if (_reader.Value.SequenceEqual(GraphQLKeywords.Schema)) + { + return ParseSchemaDefinition(); + } + if (_reader.Value.SequenceEqual(GraphQLKeywords.Scalar)) + { + return ParseScalarTypeDefinition(); + } + break; + + case (byte)'f': + if (_reader.Value.SequenceEqual(GraphQLKeywords.Fragment)) + { + return ParseFragmentDefinition(); + } + break; + + case (byte)'t': + if (_reader.Value.SequenceEqual(GraphQLKeywords.Type)) + { + return ParseObjectTypeDefinition(); + } + break; + + case (byte)'i': + if (_reader.Value.SequenceEqual(GraphQLKeywords.Interface)) + { + return ParseInterfaceTypeDefinition(); + } + if (_reader.Value.SequenceEqual(GraphQLKeywords.Input)) + { + return ParseInputObjectTypeDefinition(); + } + break; + + case (byte)'u': + if (_reader.Value.SequenceEqual(GraphQLKeywords.Union)) + { + return ParseUnionTypeDefinition(); + } + break; + + case (byte)'e': + if (_reader.Value.SequenceEqual(GraphQLKeywords.Enum)) + { + return ParseEnumTypeDefinition(); + } + if (_reader.Value.SequenceEqual(GraphQLKeywords.Extend)) + { + return ParseTypeExtension(); + } + break; + + case (byte)'d': + if (_reader.Value.SequenceEqual(GraphQLKeywords.Directive)) + { + return ParseDirectiveDefinition(); + } + break; + } } } else if (_reader.Kind == TokenKind.LeftBrace) diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLReader.Utilities.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLReader.Utilities.cs index bf3ad8bcaf0..743c27e216a 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLReader.Utilities.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLReader.Utilities.cs @@ -153,7 +153,10 @@ public string GetComment() return GetString(_value); } - public readonly string GetName() => GetString(_value); + public readonly string GetName() + => WellKnownNames.TryGetWellKnownName(_value, out var name) + ? name + : GetString(_value); public readonly string GetScalarValue() => GetString(_value); diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8Helper.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8Helper.cs index 664525a0667..7aff0db9d7f 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8Helper.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8Helper.cs @@ -52,10 +52,8 @@ public static void Unescape( ref highSurrogate, isBlockString); #if NET8_0_OR_GREATER - var remaining = escapedString.Length - readPos; - - // Vector256 path (32 bytes at a time) if we have enough bytes remain - if (Vector256.IsHardwareAccelerated && remaining >= Vector256.Count) + // Vector256 path (32 bytes at a time) + if (Vector256.IsHardwareAccelerated && (escapedString.Length - readPos) >= Vector256.Count) { ref var srcStart = ref MemoryMarshal.GetReference(escapedString); ref var dstStart = ref MemoryMarshal.GetReference(unescapedString); @@ -93,8 +91,9 @@ public static void Unescape( } } } - // Vector128 fallback (16 bytes at a time), if we have enough bytes remaining - else if (Vector128.IsHardwareAccelerated && remaining >= Vector128.Count) + + // Vector128 path (16 bytes at a time) — processes remainder after V256 or runs standalone + if (Vector128.IsHardwareAccelerated && (escapedString.Length - readPos) >= Vector128.Count) { ref var srcStart = ref MemoryMarshal.GetReference(escapedString); ref var dstStart = ref MemoryMarshal.GetReference(unescapedString); @@ -134,23 +133,34 @@ public static void Unescape( } #endif - // Scalar tail for remaining bytes + // Scalar tail for remaining bytes — bulk-copy up to next backslash while (readPos < escapedString.Length) { - var code = escapedString[readPos]; + var tail = escapedString.Slice(readPos); + var nextBackslash = tail.IndexOf(GraphQLCharacters.Backslash); - if (code == GraphQLCharacters.Backslash) + if (nextBackslash == -1) + { + // No more escapes — copy all remaining and we're done + tail.CopyTo(unescapedString.Slice(writePos)); + writePos += tail.Length; + readPos += tail.Length; + } + else { + // Copy bytes before the backslash + if (nextBackslash > 0) + { + tail.Slice(0, nextBackslash).CopyTo(unescapedString.Slice(writePos)); + writePos += nextBackslash; + readPos += nextBackslash; + } + ProcessEscapeSequence( escapedString, unescapedString, ref readPos, ref writePos, ref highSurrogate, isBlockString); } - else - { - unescapedString[writePos++] = code; - readPos++; - } } if (unescapedString.Length > writePos) @@ -178,6 +188,27 @@ private static void ProcessEscapeSequence( readPos++; var code = escaped[readPos++]; + // Hot path: simple escape characters (most common) + if (code != GraphQLCharacters.U && !isBlockString && code.IsValidEscapeCharacter()) + { + unescaped[writePos++] = code.EscapeCharacter(); + return; + } + + // Cold path: unicode, block strings, and error handling + ProcessEscapeSequenceCold(escaped, unescaped, ref readPos, ref writePos, ref highSurrogate, isBlockString, code); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ProcessEscapeSequenceCold( + in ReadOnlySpan escaped, + Span unescaped, + ref int readPos, + ref int writePos, + ref int highSurrogate, + bool isBlockString, + byte code) + { if (isBlockString && code == GraphQLCharacters.Quote) { if (readPos + 1 < escaped.Length @@ -194,59 +225,57 @@ private static void ProcessEscapeSequence( throw new Utf8EncodingException(Utf8Helper_InvalidQuoteEscapeCount); } } - else if (code.IsValidEscapeCharacter()) + else if (code == GraphQLCharacters.U) { - if (code == GraphQLCharacters.U) + if (readPos + 3 >= escaped.Length) { - if (readPos + 3 >= escaped.Length) - { - throw new Utf8EncodingException( - string.Format(Utf8Helper_InvalidEscapeChar, 'u')); - } + throw new Utf8EncodingException( + string.Format(Utf8Helper_InvalidEscapeChar, 'u')); + } - var unicodeDecimal = UnescapeUtf8Hex( - escaped[readPos], - escaped[readPos + 1], - escaped[readPos + 2], - escaped[readPos + 3]); - readPos += 4; + var unicodeDecimal = UnescapeUtf8Hex( + escaped[readPos], + escaped[readPos + 1], + escaped[readPos + 2], + escaped[readPos + 3]); + readPos += 4; - if (unicodeDecimal >= 0xD800 && unicodeDecimal <= 0xDBFF) - { - // High surrogate - if (highSurrogate >= 0) - { - throw new Utf8EncodingException("Unexpected high surrogate."); - } - highSurrogate = unicodeDecimal; - } - else if (unicodeDecimal >= 0xDC00 && unicodeDecimal <= 0xDFFF) + if (unicodeDecimal >= 0xD800 && unicodeDecimal <= 0xDBFF) + { + // High surrogate + if (highSurrogate >= 0) { - // Low surrogate - if (highSurrogate < 0) - { - throw new Utf8EncodingException("Unexpected low surrogate."); - } - var fullUnicode = ((highSurrogate - 0xD800) << 10) - + (unicodeDecimal - 0xDC00) - + 0x10000; - UnescapeUtf8Hex(fullUnicode, ref writePos, unescaped); - highSurrogate = -1; + throw new Utf8EncodingException("Unexpected high surrogate."); } - else + highSurrogate = unicodeDecimal; + } + else if (unicodeDecimal >= 0xDC00 && unicodeDecimal <= 0xDFFF) + { + // Low surrogate + if (highSurrogate < 0) { - if (highSurrogate >= 0) - { - throw new Utf8EncodingException("High surrogate not followed by low surrogate."); - } - UnescapeUtf8Hex(unicodeDecimal, ref writePos, unescaped); + throw new Utf8EncodingException("Unexpected low surrogate."); } + var fullUnicode = ((highSurrogate - 0xD800) << 10) + + (unicodeDecimal - 0xDC00) + + 0x10000; + UnescapeUtf8Hex(fullUnicode, ref writePos, unescaped); + highSurrogate = -1; } else { - unescaped[writePos++] = code.EscapeCharacter(); + if (highSurrogate >= 0) + { + throw new Utf8EncodingException("High surrogate not followed by low surrogate."); + } + UnescapeUtf8Hex(unicodeDecimal, ref writePos, unescaped); } } + else if (code.IsValidEscapeCharacter()) + { + // Block string with non-quote, non-unicode escape + unescaped[writePos++] = code.EscapeCharacter(); + } else { throw new Utf8EncodingException( @@ -290,15 +319,36 @@ public static void UnescapeUtf8Hex( } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int HexToDecimal(int a) + private static readonly byte[] s_hexLookup = CreateHexLookup(); + + private static byte[] CreateHexLookup() { - return a switch + var table = new byte[256]; + // Initialize all entries to 0xFF (invalid) + table.AsSpan().Fill(0xFF); + + // '0'-'9' => 0-9 + for (var c = '0'; c <= '9'; c++) + { + table[c] = (byte)(c - '0'); + } + + // 'A'-'F' => 10-15 + for (var c = 'A'; c <= 'F'; c++) + { + table[c] = (byte)(c - 'A' + 10); + } + + // 'a'-'f' => 10-15 + for (var c = 'a'; c <= 'f'; c++) { - >= 48 and <= 57 => a - 48, - >= 65 and <= 70 => a - 55, - >= 97 and <= 102 => a - 87, - _ => -1 - }; + table[c] = (byte)(c - 'a' + 10); + } + + return table; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int HexToDecimal(int a) + => s_hexLookup[(byte)a]; } diff --git a/src/HotChocolate/Language/src/Language.Utf8/WellKnownNames.cs b/src/HotChocolate/Language/src/Language.Utf8/WellKnownNames.cs new file mode 100644 index 00000000000..0ca26ede0a0 --- /dev/null +++ b/src/HotChocolate/Language/src/Language.Utf8/WellKnownNames.cs @@ -0,0 +1,368 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace HotChocolate.Language; + +/// +/// Provides string interning for well-known GraphQL names to avoid +/// repeated UTF-8 to string conversions for common identifiers. +/// +internal static class WellKnownNames +{ + // Built-in scalar type names + private const string String = "String"; + private const string Int = "Int"; + private const string Float = "Float"; + private const string Boolean = "Boolean"; + private const string ID = "ID"; + + // Operation type names + private const string Query = "Query"; + private const string Mutation = "Mutation"; + private const string Subscription = "Subscription"; + + // Introspection names +#pragma warning disable IDE1006 // Naming Styles + private const string __typename = "__typename"; + private const string __schema = "__schema"; + private const string __type = "__type"; + private const string __Type = "__Type"; + private const string __Field = "__Field"; + private const string __InputValue = "__InputValue"; + private const string __EnumValue = "__EnumValue"; + private const string __Directive = "__Directive"; + private const string __Schema = "__Schema"; +#pragma warning restore IDE1006 // Naming Styles + + // Common field/argument names + private const string Id = "id"; + private const string Name = "name"; + private const string Description = "description"; + private const string Type = "type"; + private const string Kind = "kind"; + private const string Fields = "fields"; + private const string Args = "args"; + private const string Node = "node"; + private const string Nodes = "nodes"; + private const string Edges = "edges"; + private const string Cursor = "cursor"; + private const string First = "first"; + private const string Last = "last"; + private const string After = "after"; + private const string Before = "before"; + private const string Value = "value"; + private const string Key = "key"; + private const string Input = "input"; + private const string Where = "where"; + private const string Order = "order"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryGetWellKnownName( + ReadOnlySpan utf8Value, +#if NETSTANDARD2_0 + out string? result) +#else + [NotNullWhen(true)] out string? result) +#endif + { + if (utf8Value.Length == 0) + { + result = null; + return false; + } + + switch (utf8Value[0]) + { + case (byte)'S': + if (utf8Value.SequenceEqual("String"u8)) + { + result = String; + return true; + } + + if (utf8Value.SequenceEqual("Subscription"u8)) + { + result = Subscription; + return true; + } + + break; + + case (byte)'I': + if (utf8Value.SequenceEqual("Int"u8)) + { + result = Int; + return true; + } + + if (utf8Value.SequenceEqual("ID"u8)) + { + result = ID; + return true; + } + + break; + + case (byte)'F': + if (utf8Value.SequenceEqual("Float"u8)) + { + result = Float; + return true; + } + + break; + + case (byte)'B': + if (utf8Value.SequenceEqual("Boolean"u8)) + { + result = Boolean; + return true; + } + + break; + + case (byte)'Q': + if (utf8Value.SequenceEqual("Query"u8)) + { + result = Query; + return true; + } + + break; + + case (byte)'M': + if (utf8Value.SequenceEqual("Mutation"u8)) + { + result = Mutation; + return true; + } + + break; + + case (byte)'_': + if (utf8Value.Length > 1 && utf8Value[1] == (byte)'_') + { + if (utf8Value.SequenceEqual("__typename"u8)) + { + result = __typename; + return true; + } + + if (utf8Value.SequenceEqual("__schema"u8)) + { + result = __schema; + return true; + } + + if (utf8Value.SequenceEqual("__type"u8)) + { + result = __type; + return true; + } + + if (utf8Value.SequenceEqual("__Type"u8)) + { + result = __Type; + return true; + } + + if (utf8Value.SequenceEqual("__Field"u8)) + { + result = __Field; + return true; + } + + if (utf8Value.SequenceEqual("__InputValue"u8)) + { + result = __InputValue; + return true; + } + + if (utf8Value.SequenceEqual("__EnumValue"u8)) + { + result = __EnumValue; + return true; + } + + if (utf8Value.SequenceEqual("__Directive"u8)) + { + result = __Directive; + return true; + } + + if (utf8Value.SequenceEqual("__Schema"u8)) + { + result = __Schema; + return true; + } + } + + break; + + case (byte)'i': + if (utf8Value.SequenceEqual("id"u8)) + { + result = Id; + return true; + } + + if (utf8Value.SequenceEqual("input"u8)) + { + result = Input; + return true; + } + + break; + + case (byte)'n': + if (utf8Value.SequenceEqual("name"u8)) + { + result = Name; + return true; + } + + if (utf8Value.SequenceEqual("node"u8)) + { + result = Node; + return true; + } + + if (utf8Value.SequenceEqual("nodes"u8)) + { + result = Nodes; + return true; + } + + break; + + case (byte)'d': + if (utf8Value.SequenceEqual("description"u8)) + { + result = Description; + return true; + } + + break; + + case (byte)'t': + if (utf8Value.SequenceEqual("type"u8)) + { + result = Type; + return true; + } + + break; + + case (byte)'k': + if (utf8Value.SequenceEqual("kind"u8)) + { + result = Kind; + return true; + } + + if (utf8Value.SequenceEqual("key"u8)) + { + result = Key; + return true; + } + + break; + + case (byte)'f': + if (utf8Value.SequenceEqual("fields"u8)) + { + result = Fields; + return true; + } + + if (utf8Value.SequenceEqual("first"u8)) + { + result = First; + return true; + } + + break; + + case (byte)'a': + if (utf8Value.SequenceEqual("args"u8)) + { + result = Args; + return true; + } + + if (utf8Value.SequenceEqual("after"u8)) + { + result = After; + return true; + } + + break; + + case (byte)'e': + if (utf8Value.SequenceEqual("edges"u8)) + { + result = Edges; + return true; + } + + break; + + case (byte)'c': + if (utf8Value.SequenceEqual("cursor"u8)) + { + result = Cursor; + return true; + } + + break; + + case (byte)'l': + if (utf8Value.SequenceEqual("last"u8)) + { + result = Last; + return true; + } + + break; + + case (byte)'b': + if (utf8Value.SequenceEqual("before"u8)) + { + result = Before; + return true; + } + + break; + + case (byte)'v': + if (utf8Value.SequenceEqual("value"u8)) + { + result = Value; + return true; + } + + break; + + case (byte)'w': + if (utf8Value.SequenceEqual("where"u8)) + { + result = Where; + return true; + } + + break; + + case (byte)'o': + if (utf8Value.SequenceEqual("order"u8)) + { + result = Order; + return true; + } + + break; + } + + result = null; + return false; + } +}