Skip to content

Commit 36c3791

Browse files
[CherryPick] Improve Execution Performance (#1392) (#2068)
(note: descriptions that are quoted were intially authored by GitHub Copilot and modified for brevity, correctness, and conciseness. @seantleonard) ## Why this change? Take advantage from Hot Chocolates execution pipeline to improve execution of pre-acquired data. Without this change, there exists inefficiencies in `JsonDocument` object allocations and disposal, inefficiencies in GraphQL result processing in the `ResolverMiddleware` class ## What is this change? + Introduction of `ResolverTypeInterceptor.cs` class + Introduction of `ExecutionHelper.cs` class + Introduction of `ArrayPoolWriter.cs` class > The `ArrayPoolWriter.cs` file provides a way to manage and interact with pooled arrays, mainly byte arrays, in a more efficient and controlled manner. Here are some key benefits: > **Memory Efficiency**: It uses `ArrayPool<byte>`, a shared pool of byte arrays. This is an efficient way to handle memory because it reuses arrays, thereby reducing the frequency of garbage collection and the memory footprint of the application. > > **Buffer Management**: The class ArrayPoolWriter implements the `IBufferWriter<byte>` interface, providing a standard way to write to the buffer. It also implements the `IDisposable` interface, ensuring proper cleanup of resources. > > **Exception Handling**: The class includes robust error handling, throwing exceptions when methods are used incorrectly, such as attempting to advance the buffer past its capacity or trying to use the writer after it has been disposed. > > **Dynamic Buffer Expansion**: If the required capacity exceeds the current buffer's capacity, it automatically rents a larger buffer from the pool, copies existing data, and returns the original buffer, ensuring that the buffer size can grow as needed. > Access Flexibility: It provides methods to access written data as both `ReadOnlyMemory<byte>` and `ReadOnlySpan<byte>`, offering flexible ways to interact with the data. > > This class can be beneficial in scenarios where you anticipate writing to a byte buffer repeatedly or where the amount of data to write may grow over time. + JsonObjectExtensions.cs > **Easy Conversion**: Provides a straightforward way to convert `JsonObject` instances to `JsonElement` or `JsonDocument`. > > **Memory Optimization**: The conversion process leverages the ArrayPoolWriter for writing the JsonObject to a pooled buffer, which avoids the need for serializing to a full JSON string. This can potentially save memory and improve performance. + SqlQueryEngine updates > The changes in the `SqlQueryEngine.cs` file can be summarized as follows: > > The `ResolveInnerObject` method is now renamed to `ResolveObject`. The new method handles the case where the JsonElement is of JsonValueKind.String type and requires parsing into a JSON object. It also handles the case where the JsonElement is of JsonValueKind.Object type, directly returning the element. > > The `ResolveListType` method is now renamed to `ResolveList`. It has been refactored to handle two types of `JsonElement`: `JsonValueKind.Array` and `JsonValueKind.String`. It deserializes both types into a list of JsonElements. The method now also checks and handles the case where metadata is not null. > > The `public JsonDocument? ResolveInnerObject(JsonElement element, IObjectField fieldSchema, ref IMetadata metadata)` method signature has been changed to `public JsonElement ResolveObject(JsonElement element, IObjectField fieldSchema, ref IMetadata metadata)` to account for no longer passing back and forth `JsonDocument` objects. > > The `public object? ResolveListType(JsonElement element, IObjectField fieldSchema, ref IMetadata metadata)` method signature has been changed to `public object ResolveList(JsonElement array, IObjectField fieldSchema, ref IMetadata? metadata)` to account for no longer passing back and forth `JsonDocument` objects. > > The `ResolveObject` and `ResolveList` methods now have detailed summary comments explaining their functionality. An extra check for `parentMetadata.Subqueries.TryGetValue(QueryBuilder.PAGINATION_FIELD_NAME, out PaginationMetadata? paginationObjectMetadata)` has been added in the `ResolveObject` method. If true, `parentMetadata` is updated with `paginationObjectMetadata`. > > Overall, these changes focus on handling the cases where `JsonElement` is in the form of a string and needs to be parsed into a JSON object or array, as well as improving the metadata handling. ExecutionHelper.cs > The `ExecutionHelper` class contains methods that are primarily used for interacting with the query engine and mutation engine to execute queries and mutations, respectively. > `ExecuteQueryAsync(...)`: Represents the root query resolver and fetches initial data from the query engine. It accepts a context parameter and uses the query engine to execute the query. If the selection type is a list, it will execute a list query, register a cleanup action to dispose the documents after the query, and set the result. If not, it will execute a single item query and set the result. > > `ExecuteMutateAsync(...)`: Represents the root mutation resolver and invokes the mutation on the query engine. Similar to the ExecuteQueryAsync method, it handles both list and single item mutations. > > The `ExecuteLeafField`, `ExecuteObjectField`, `ExecuteListField` methods are HotChocolate coined "pure resolvers" used to resolve the results of specific types of fields in the GraphQL request. > > The `GetMetadata`, `GetMetadataObjectField`, `SetNewMetadata`, `SetNewMetadataChildren` methods handling storing and retreiving SqlPaginationMetadata objects utilized by each level of depth in the path of a GraphQL result. - Added fix for DW JSON result differing from MSSQL json result. DW has extraneous escape characters in the json which results in JsonDocument/Element not resolving the expected JsonValueKind, and requires additional JsonDocument.Parse operation to remediate. ## Testing - Unit testing added for the ArrayPoolWriter functionality. This was adopted from HotChocolate code. - GraphQL integration tests that already existed were used to ensure the refactor maintained the ability to handle request scenarios we test for. There were tests that initially failed that were used to help validate SqlPaginationMetadata usage, storage, and creation within the PureResolver context. The following test classes are directly applicable for testing query execution through the PureResolver code paths established in `ExecutionHelper` - `Azure.DataApiBuilder.Service.Tests.SqlTests.GraphQLFilterTests` - `Azure.DataApiBuilder.Service.Tests.SqlTests.GraphQLMutationTests` - `Azure.DataApiBuilder.Service.Tests.SqlTests.GraphQLPaginationTests` - `Azure.DataApiBuilder.Service.Tests.SqlTests.GraphQLQueryTests` - `Azure.DataApiBuilder.Service.Tests.SqlTests.GraphQLSupportedTypesTests` Co-authored-by: Michael Staib <[email protected]>
1 parent c9a2125 commit 36c3791

20 files changed

+1394
-467
lines changed

src/Core/Models/GraphQLFilterParsers.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Azure.DataApiBuilder.Service.Exceptions;
1212
using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives;
1313
using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries;
14+
using Azure.DataApiBuilder.Service.Services;
1415
using HotChocolate.Language;
1516
using HotChocolate.Resolvers;
1617
using Microsoft.AspNetCore.Http;
@@ -65,12 +66,12 @@ public Predicate Parse(
6566
string dataSourceName = _configProvider.GetConfig().GetDataSourceNameFromEntityName(entityName);
6667
ISqlMetadataProvider metadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName);
6768

68-
InputObjectType filterArgumentObject = ResolverMiddleware.InputObjectTypeFromIInputField(filterArgumentSchema);
69+
InputObjectType filterArgumentObject = ExecutionHelper.InputObjectTypeFromIInputField(filterArgumentSchema);
6970

7071
List<PredicateOperand> predicates = new();
7172
foreach (ObjectFieldNode field in fields)
7273
{
73-
object? fieldValue = ResolverMiddleware.ExtractValueFromIValueNode(
74+
object? fieldValue = ExecutionHelper.ExtractValueFromIValueNode(
7475
value: field.Value,
7576
argumentSchema: filterArgumentObject.Fields[field.Name.Value],
7677
variables: ctx.Variables);
@@ -85,7 +86,7 @@ public Predicate Parse(
8586
bool fieldIsAnd = string.Equals(name, $"{PredicateOperation.AND}", StringComparison.OrdinalIgnoreCase);
8687
bool fieldIsOr = string.Equals(name, $"{PredicateOperation.OR}", StringComparison.OrdinalIgnoreCase);
8788

88-
InputObjectType filterInputObjectType = ResolverMiddleware.InputObjectTypeFromIInputField(filterArgumentObject.Fields[name]);
89+
InputObjectType filterInputObjectType = ExecutionHelper.InputObjectTypeFromIInputField(filterArgumentObject.Fields[name]);
8990
if (fieldIsAnd || fieldIsOr)
9091
{
9192
PredicateOperation op = fieldIsAnd ? PredicateOperation.AND : PredicateOperation.OR;
@@ -509,7 +510,7 @@ private Predicate ParseAndOr(
509510
List<PredicateOperand> operands = new();
510511
foreach (IValueNode field in fields)
511512
{
512-
object? fieldValue = ResolverMiddleware.ExtractValueFromIValueNode(
513+
object? fieldValue = ExecutionHelper.ExtractValueFromIValueNode(
513514
value: field,
514515
argumentSchema: argumentSchema,
515516
ctx.Variables);
@@ -598,11 +599,11 @@ public static Predicate Parse(
598599
{
599600
List<PredicateOperand> predicates = new();
600601

601-
InputObjectType argumentObject = ResolverMiddleware.InputObjectTypeFromIInputField(argumentSchema);
602+
InputObjectType argumentObject = ExecutionHelper.InputObjectTypeFromIInputField(argumentSchema);
602603
foreach (ObjectFieldNode field in fields)
603604
{
604605
string name = field.Name.ToString();
605-
object? value = ResolverMiddleware.ExtractValueFromIValueNode(
606+
object? value = ExecutionHelper.ExtractValueFromIValueNode(
606607
value: field.Value,
607608
argumentSchema: argumentObject.Fields[field.Name.Value],
608609
variables: ctx.Variables);
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Buffers;
5+
6+
/// <summary>
7+
/// A helper to write to pooled arrays.
8+
/// </summary>
9+
internal sealed class ArrayPoolWriter : IBufferWriter<byte>, IDisposable
10+
{
11+
private const int INITIAL_BUFFER_SIZE = 512;
12+
private byte[] _buffer;
13+
private int _capacity;
14+
private int _start;
15+
private bool _disposed;
16+
17+
/// <summary>
18+
/// Initializes a new instance of the <see cref="ArrayPoolWriter"/> class.
19+
/// </summary>
20+
public ArrayPoolWriter()
21+
{
22+
_buffer = ArrayPool<byte>.Shared.Rent(INITIAL_BUFFER_SIZE);
23+
_capacity = _buffer.Length;
24+
_start = 0;
25+
}
26+
27+
/// <summary>
28+
/// Gets the part of the buffer that has been written to.
29+
/// </summary>
30+
/// <returns>
31+
/// A <see cref="ReadOnlyMemory{T}"/> of the written portion of the buffer.
32+
/// </returns>
33+
public ReadOnlyMemory<byte> GetWrittenMemory()
34+
=> _buffer.AsMemory()[.._start];
35+
36+
/// <summary>
37+
/// Gets the part of the buffer that has been written to.
38+
/// </summary>
39+
/// <returns>
40+
/// A <see cref="ReadOnlySpan{T}"/> of the written portion of the buffer.
41+
/// </returns>
42+
public ReadOnlySpan<byte> GetWrittenSpan()
43+
=> _buffer.AsSpan()[.._start];
44+
45+
/// <summary>
46+
/// Advances the writer by the specified number of bytes.
47+
/// </summary>
48+
/// <param name="count">
49+
/// The number of bytes to advance the writer by.
50+
/// </param>
51+
/// <exception cref="ArgumentOutOfRangeException">
52+
/// Thrown if <paramref name="count"/> is negative or
53+
/// if <paramref name="count"/> is greater than the
54+
/// available capacity on the internal buffer.
55+
/// </exception>
56+
public void Advance(int count)
57+
{
58+
if (_disposed)
59+
{
60+
throw new ObjectDisposedException(nameof(ArrayPoolWriter));
61+
}
62+
63+
if (count < 0)
64+
{
65+
throw new ArgumentOutOfRangeException(nameof(count));
66+
}
67+
68+
if (count > _capacity)
69+
{
70+
throw new ArgumentOutOfRangeException(nameof(count), count, "Cannot advance past the end of the buffer.");
71+
}
72+
73+
_start += count;
74+
_capacity -= count;
75+
}
76+
77+
/// <summary>
78+
/// Gets a <see cref="Memory{T}"/> to write to.
79+
/// </summary>
80+
/// <param name="sizeHint">
81+
/// The minimum size of the returned <see cref="Memory{T}"/>.
82+
/// </param>
83+
/// <returns>
84+
/// A <see cref="Memory{T}"/> to write to.
85+
/// </returns>
86+
/// <exception cref="ArgumentOutOfRangeException">
87+
/// Thrown if <paramref name="sizeHint"/> is negative.
88+
/// </exception>
89+
public Memory<byte> GetMemory(int sizeHint = 0)
90+
{
91+
if (_disposed)
92+
{
93+
throw new ObjectDisposedException(nameof(ArrayPoolWriter));
94+
}
95+
96+
if (sizeHint < 0)
97+
{
98+
throw new ArgumentOutOfRangeException(nameof(sizeHint));
99+
}
100+
101+
int size = sizeHint < 1 ? INITIAL_BUFFER_SIZE : sizeHint;
102+
EnsureBufferCapacity(size);
103+
return _buffer.AsMemory().Slice(_start, size);
104+
}
105+
106+
/// <summary>
107+
/// Gets a <see cref="Span{T}"/> to write to.
108+
/// </summary>
109+
/// <param name="sizeHint">
110+
/// The minimum size of the returned <see cref="Span{T}"/>.
111+
/// </param>
112+
/// <returns>
113+
/// A <see cref="Span{T}"/> to write to.
114+
/// </returns>
115+
/// <exception cref="ArgumentOutOfRangeException">
116+
/// Thrown if <paramref name="sizeHint"/> is negative.
117+
/// </exception>
118+
public Span<byte> GetSpan(int sizeHint = 0)
119+
{
120+
if (_disposed)
121+
{
122+
throw new ObjectDisposedException(nameof(ArrayPoolWriter));
123+
}
124+
125+
if (sizeHint < 0)
126+
{
127+
throw new ArgumentOutOfRangeException(nameof(sizeHint));
128+
}
129+
130+
int size = sizeHint < 1 ? INITIAL_BUFFER_SIZE : sizeHint;
131+
EnsureBufferCapacity(size);
132+
return _buffer.AsSpan().Slice(_start, size);
133+
}
134+
135+
/// <summary>
136+
/// Ensures that the internal buffer has the needed capacity.
137+
/// </summary>
138+
/// <param name="neededCapacity">
139+
/// The needed capacity on the internal buffer.
140+
/// </param>
141+
private void EnsureBufferCapacity(int neededCapacity)
142+
{
143+
// check if we have enough capacity available on the buffer.
144+
if (_capacity < neededCapacity)
145+
{
146+
// if we need to expand the buffer we first capture the original buffer.
147+
byte[] buffer = _buffer;
148+
149+
// next we determine the new size of the buffer, we at least double the size to avoid
150+
// expanding the buffer too often.
151+
int newSize = buffer.Length * 2;
152+
153+
// if that new buffer size is not enough to satisfy the needed capacity
154+
// we add the needed capacity to the doubled buffer capacity.
155+
if (neededCapacity > newSize - _start)
156+
{
157+
newSize += neededCapacity;
158+
}
159+
160+
// next we will rent a new array from the array pool that supports
161+
// the new capacity requirements.
162+
_buffer = ArrayPool<byte>.Shared.Rent(newSize);
163+
164+
// the rented array might have a larger size than the needed capacity,
165+
// so we will take the buffer length and calculate from that the free capacity.
166+
_capacity += _buffer.Length - buffer.Length;
167+
168+
// finally we copy the data from the original buffer to the new buffer.
169+
buffer.AsSpan().CopyTo(_buffer);
170+
171+
// last but not least we return the original buffer to the array pool.
172+
ArrayPool<byte>.Shared.Return(buffer);
173+
}
174+
}
175+
176+
/// <inheritdoc/>
177+
public void Dispose()
178+
{
179+
if (!_disposed)
180+
{
181+
ArrayPool<byte>.Shared.Return(_buffer);
182+
_buffer = Array.Empty<byte>();
183+
_capacity = 0;
184+
_start = 0;
185+
_disposed = true;
186+
}
187+
}
188+
}

src/Core/Resolvers/CosmosQueryEngine.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -189,14 +189,14 @@ public Task<IActionResult> ExecuteAsync(StoredProcedureRequestContext context, s
189189
}
190190

191191
/// <inheritdoc />
192-
public JsonDocument ResolveInnerObject(JsonElement element, IObjectField fieldSchema, ref IMetadata metadata)
192+
public JsonElement ResolveObject(JsonElement element, IObjectField fieldSchema, ref IMetadata metadata)
193193
{
194-
//TODO: Try to avoid additional deserialization/serialization here.
195-
return JsonDocument.Parse(element.ToString());
194+
return element;
196195
}
197196

198197
/// <inheritdoc />
199-
public object ResolveListType(JsonElement element, IObjectField fieldSchema, ref IMetadata metadata)
198+
/// metadata is not used in this method, but it is required by the interface.
199+
public object ResolveList(JsonElement array, IObjectField fieldSchema, ref IMetadata metadata)
200200
{
201201
IType listType = fieldSchema.Type;
202202
// Is the List type nullable? [...]! vs [...]
@@ -217,10 +217,10 @@ public object ResolveListType(JsonElement element, IObjectField fieldSchema, ref
217217

218218
if (listType.IsObjectType())
219219
{
220-
return JsonSerializer.Deserialize<List<JsonElement>>(element);
220+
return JsonSerializer.Deserialize<List<JsonElement>>(array);
221221
}
222222

223-
return JsonSerializer.Deserialize(element, fieldSchema.RuntimeType);
223+
return JsonSerializer.Deserialize(array, fieldSchema.RuntimeType);
224224
}
225225

226226
/// <summary>

src/Core/Resolvers/IQueryEngine.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ public interface IQueryEngine
4444
/// <summary>
4545
/// Resolves a jsonElement representing an inner object based on the field's schema and metadata
4646
/// </summary>
47-
public JsonDocument? ResolveInnerObject(JsonElement element, IObjectField fieldSchema, ref IMetadata metadata);
47+
public JsonElement ResolveObject(JsonElement element, IObjectField fieldSchema, ref IMetadata metadata);
4848

4949
/// <summary>
5050
/// Resolves a jsonElement representing a list type based on the field's schema and metadata
5151
/// </summary>
52-
public object? ResolveListType(JsonElement element, IObjectField fieldSchema, ref IMetadata metadata);
52+
public object ResolveList(JsonElement array, IObjectField fieldSchema, ref IMetadata? metadata);
5353
}
5454
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Buffers;
5+
using System.Text.Json;
6+
using System.Text.Json.Nodes;
7+
8+
/// <summary>
9+
/// This extension class provides helpers to convert a mutable JSON object
10+
/// to a JSON element or JSON document.
11+
/// </summary>
12+
internal static class JsonObjectExtensions
13+
{
14+
/// <summary>
15+
/// Converts a mutable JSON object to an immutable JSON element.
16+
/// </summary>
17+
/// <param name="obj">
18+
/// The mutable JSON object to convert.
19+
/// </param>
20+
/// <returns>
21+
/// An immutable JSON element.
22+
/// </returns>
23+
/// <exception cref="ArgumentNullException">
24+
/// Thrown if <paramref name="obj"/> is <see langword="null"/>.
25+
/// </exception>
26+
public static JsonElement ToJsonElement(this JsonObject obj)
27+
{
28+
if (obj == null)
29+
{
30+
throw new ArgumentNullException(nameof(obj));
31+
}
32+
33+
// we first write the mutable JsonObject to the pooled buffer and avoid serializing
34+
// to a full JSON string.
35+
using ArrayPoolWriter buffer = new();
36+
obj.WriteTo(buffer);
37+
38+
// next we take the reader here and parse the JSON element from the buffer.
39+
Utf8JsonReader reader = new(buffer.GetWrittenSpan());
40+
41+
// the underlying JsonDocument will not use pooled arrays to store metadata on it ...
42+
// this JSON element can be safely returned.
43+
return JsonElement.ParseValue(ref reader);
44+
}
45+
46+
/// <summary>
47+
/// Converts a mutable JSON object to an immutable JSON document.
48+
/// </summary>
49+
/// <param name="obj">
50+
/// The mutable JSON object to convert.
51+
/// </param>
52+
/// <returns>
53+
/// An immutable JSON document.
54+
/// </returns>
55+
/// <exception cref="ArgumentNullException">
56+
/// Thrown if <paramref name="obj"/> is <see langword="null"/>.
57+
/// </exception>
58+
public static JsonDocument ToJsonDocument(this JsonObject obj)
59+
{
60+
if (obj == null)
61+
{
62+
throw new ArgumentNullException(nameof(obj));
63+
}
64+
65+
// we first write the mutable JsonObject to the pooled buffer and avoid serializing
66+
// to a full JSON string.
67+
using ArrayPoolWriter buffer = new();
68+
obj.WriteTo(buffer);
69+
70+
// next we parse the JSON document from the buffer.
71+
// this JSON document will be disposed by the GraphQL execution engine.
72+
return JsonDocument.Parse(buffer.GetWrittenMemory());
73+
}
74+
75+
private static void WriteTo(this JsonObject obj, IBufferWriter<byte> bufferWriter)
76+
{
77+
if (obj == null)
78+
{
79+
throw new ArgumentNullException(nameof(obj));
80+
}
81+
82+
if (bufferWriter == null)
83+
{
84+
throw new ArgumentNullException(nameof(bufferWriter));
85+
}
86+
87+
using Utf8JsonWriter writer = new(bufferWriter);
88+
obj.WriteTo(writer);
89+
writer.Flush();
90+
}
91+
}

0 commit comments

Comments
 (0)