Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance improvements in JsonValue. #103733

Merged
merged 4 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
<Compile Include="System\Text\Json\JsonTokenType.cs" />
<Compile Include="System\Text\Json\Nodes\JsonArray.cs" />
<Compile Include="System\Text\Json\Nodes\JsonArray.IList.cs" />
<Compile Include="System\Text\Json\Nodes\JsonValueOfElement.cs" />
<Compile Include="System\Text\Json\Nodes\JsonNode.cs" />
<Compile Include="System\Text\Json\Nodes\JsonNode.Operators.cs" />
<Compile Include="System\Text\Json\Nodes\JsonNode.Parse.cs" />
Expand Down Expand Up @@ -380,12 +381,6 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
<Compile Include="System\Text\Json\Reader\JsonReaderHelper.netstandard.cs" />
</ItemGroup>

<ItemGroup>
<None Include="System\Text\Json\OrderedDictionary.cs" />
<None Include="System\Text\Json\OrderedDictionary.KeyCollection.cs" />
<None Include="System\Text\Json\OrderedDictionary.ValueCollection.cs" />
</ItemGroup>

<!-- Application tfms (.NETCoreApp, .NETFramework) need to use the same or higher version of .NETStandard's dependencies. -->
<ItemGroup>
<ProjectReference Include="$(LibrariesProjectRoot)System.Text.Encodings.Web\src\System.Text.Encodings.Web.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,17 @@ internal JsonTokenType GetJsonTokenType(int index)
return _parsedData.GetJsonTokenType(index);
}

internal bool ValueIsEscaped(int index, bool isPropertyName)
{
CheckNotDisposed();

int matchIndex = isPropertyName ? index - DbRow.Size : index;
DbRow row = _parsedData.Get(matchIndex);
Debug.Assert(!isPropertyName || row.TokenType is JsonTokenType.PropertyName);

return row.HasComplexChildren;
}

internal int GetArrayLength(int index)
{
CheckNotDisposed();
Expand Down Expand Up @@ -363,6 +374,16 @@ internal string GetNameOfPropertyValue(int index)
return GetString(index - DbRow.Size, JsonTokenType.PropertyName)!;
}

internal ReadOnlySpan<byte> GetPropertyNameRaw(int index)
{
CheckNotDisposed();

DbRow row = _parsedData.Get(index - DbRow.Size);
Debug.Assert(row.TokenType is JsonTokenType.PropertyName);

return _utf8Json.Span.Slice(row.Location, row.SizeOrLength);
}

internal bool TryGetValue(int index, [NotNullWhen(true)] out byte[]? value)
{
CheckNotDisposed();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,13 @@ internal string GetPropertyName()
return _parent.GetNameOfPropertyValue(_idx);
}

internal ReadOnlySpan<byte> GetPropertyNameRaw()
{
CheckValidInstance();

return _parent.GetPropertyNameRaw(_idx);
}

/// <summary>
/// Gets the original input data backing this value, returning it as a <see cref="string"/>.
/// </summary>
Expand Down Expand Up @@ -1194,6 +1201,154 @@ internal string GetPropertyRawText()
return _parent.GetPropertyRawValueAsString(_idx);
}

internal bool ValueIsEscaped
{
// TODO make public https://github.com/dotnet/runtime/issues/77666
get
{
CheckValidInstance();

return _parent.ValueIsEscaped(_idx, isPropertyName: false);
}
}

internal ReadOnlySpan<byte> ValueSpan
{
// TODO make public https://github.com/dotnet/runtime/issues/77666
get
{
CheckValidInstance();

return _parent.GetRawValue(_idx, includeQuotes: false).Span;
}
}

// TODO make public https://github.com/dotnet/runtime/issues/33388
internal static bool DeepEquals(JsonElement left, JsonElement right)
{
Debug.Assert(left._parent != null);
Debug.Assert(right._parent != null);

JsonValueKind kind = left.ValueKind;
if (kind != right.ValueKind)
{
return false;
}

switch (kind)
{
case JsonValueKind.Null or JsonValueKind.False or JsonValueKind.True:
return true;

case JsonValueKind.Number:
// JSON numbers are equal if their raw representations are equal.
return left.GetRawValue().Span.SequenceEqual(right.GetRawValue().Span);

case JsonValueKind.String:
if (right.ValueIsEscaped)
{
if (left.ValueIsEscaped)
{
// Both values are escaped, force an allocation to unescape the RHS.
return left.ValueEquals(right.GetString());
}

// Swap values so that unescaping is handled by the LHS.
(left, right) = (right, left);
}

return left.ValueEquals(right.ValueSpan);

case JsonValueKind.Array:
ArrayEnumerator rightArrayEnumerator = right.EnumerateArray();
foreach (JsonElement leftElement in left.EnumerateArray())
{
if (!rightArrayEnumerator.MoveNext() || !DeepEquals(leftElement, rightArrayEnumerator.Current))
{
return false;
}
}

return !rightArrayEnumerator.MoveNext();

default:
Debug.Assert(kind is JsonValueKind.Object);
ObjectEnumerator leftObjectEnumerator = left.EnumerateObject();
ObjectEnumerator rightObjectEnumerator = right.EnumerateObject();

// Two JSON objects are considered equal if they define the same set of properties.
// Start optimistically with sequential comparison, but fall back to unordered
// comparison as soon as a mismatch is encountered.

while (leftObjectEnumerator.MoveNext())
{
if (!rightObjectEnumerator.MoveNext())
{
return false;
}

JsonProperty leftProp = leftObjectEnumerator.Current;
JsonProperty rightProp = rightObjectEnumerator.Current;

if (!NameEquals(leftProp, rightProp))
{
// We have our first mismatch, fall back to unordered comparison.
return UnorderedObjectDeepEquals(leftObjectEnumerator, rightObjectEnumerator);
}

if (!DeepEquals(leftProp.Value, rightProp.Value))
{
return false;
}
}

return !rightObjectEnumerator.MoveNext();

static bool UnorderedObjectDeepEquals(ObjectEnumerator left, ObjectEnumerator right)
{
Dictionary<string, JsonElement> rightElements = new(StringComparer.Ordinal);
do
{
JsonProperty prop = right.Current;
rightElements.TryAdd(prop.Name, prop.Value);
}
while (right.MoveNext());

int leftCount = 0;
do
{
JsonProperty prop = left.Current;
if (!rightElements.TryGetValue(prop.Name, out JsonElement rightElement) || !DeepEquals(prop.Value, rightElement))
{
return false;
}

leftCount++;
}
while (left.MoveNext());

return leftCount == rightElements.Count;
}

static bool NameEquals(JsonProperty left, JsonProperty right)
{
if (right.NameIsEscaped)
{
if (left.NameIsEscaped)
{
// Both values are escaped, force an allocation to unescape the RHS.
return left.NameEquals(right.Name);
}

// Swap values so that unescaping is handled by the LHS
(left, right) = (right, left);
}

return left.NameEquals(right.NameSpan);
}
}
}

/// <summary>
/// Compares <paramref name="text" /> to the string value of this element.
/// </summary>
Expand Down Expand Up @@ -1292,6 +1447,13 @@ internal bool TextEqualsHelper(ReadOnlySpan<char> text, bool isPropertyName)
return _parent.TextEquals(_idx, text, isPropertyName);
}

internal bool ValueIsEscapedHelper(bool isPropertyName)
{
CheckValidInstance();

return _parent.ValueIsEscaped(_idx, isPropertyName);
}

/// <summary>
/// Write the element into the provided writer as a JSON value.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ public readonly struct JsonProperty
public JsonElement Value { get; }
private string? _name { get; }

internal JsonProperty(JsonElement value, string? name = null)
internal JsonProperty(JsonElement value)
{
Value = value;
_name = name;
}

/// <summary>
Expand Down Expand Up @@ -94,6 +93,10 @@ internal bool EscapedNameEquals(ReadOnlySpan<byte> utf8Text)
return Value.TextEqualsHelper(utf8Text, isPropertyName: true, shouldUnescape: false);
}

// TODO make public https://github.com/dotnet/runtime/issues/77666
internal bool NameIsEscaped => Value.ValueIsEscapedHelper(isPropertyName: true);
internal ReadOnlySpan<byte> NameSpan => Value.GetPropertyNameRaw();

/// <summary>
/// Write the property into the provided writer as a named JSON object property.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public sealed partial class JsonArray : JsonNode
private JsonElement? _jsonElement;
private List<JsonNode?>? _list;

internal override JsonElement? UnderlyingElement => _jsonElement;

/// <summary>
/// Initializes a new instance of the <see cref="JsonArray"/> class that is empty.
/// </summary>
Expand Down Expand Up @@ -93,11 +95,11 @@ internal override JsonNode DeepCloneCore()
return jsonArray;
}

internal override bool DeepEqualsCore(JsonNode? node)
internal override bool DeepEqualsCore(JsonNode node)
{
switch (node)
{
case null or JsonObject:
case JsonObject:
return false;
case JsonValue value:
// JsonValue instances have special comparison semantics, dispatch to their implementation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,12 @@ public override string ToString()
// Special case for string; don't quote it.
if (this is JsonValue)
{
if (this is JsonValue<string> jsonString)
if (this is JsonValuePrimitive<string> jsonString)
{
return jsonString.Value;
}

if (this is JsonValue<JsonElement> jsonElement &&
jsonElement.Value.ValueKind == JsonValueKind.String)
if (this is JsonValueOfElement { Value.ValueKind: JsonValueKind.String } jsonElement)
{
return jsonElement.Value.GetString()!;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@ namespace System.Text.Json.Nodes
/// declared as an <see cref="object"/> should be deserialized as a <see cref="JsonNode"/>.
public abstract partial class JsonNode
{
// Default options instance used when calling built-in JsonNode converters.
private protected static readonly JsonSerializerOptions s_defaultOptions = new();

private JsonNode? _parent;
private JsonNodeOptions? _options;

/// <summary>
/// The underlying JsonElement if the node is backed by a JsonElement.
/// </summary>
internal virtual JsonElement? UnderlyingElement => null;

/// <summary>
/// Options to control the behavior.
/// </summary>
Expand Down Expand Up @@ -300,11 +308,15 @@ public static bool DeepEquals(JsonNode? node1, JsonNode? node2)
{
return node2 is null;
}
else if (node2 is null)
{
return false;
}

return node1.DeepEqualsCore(node2);
}

internal abstract bool DeepEqualsCore(JsonNode? node);
internal abstract bool DeepEqualsCore(JsonNode node);

/// <summary>
/// Replaces this node with a new value.
Expand Down Expand Up @@ -375,7 +387,7 @@ internal void AssignParent(JsonNode parent)
}

var jsonTypeInfo = (JsonTypeInfo<T>)JsonSerializerOptions.Default.GetTypeInfo(typeof(T));
return new JsonValueCustomized<T>(value, jsonTypeInfo, options);
return JsonValue.CreateFromTypeInfo(value, jsonTypeInfo, options);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ public partial class JsonObject : IDictionary<string, JsonNode?>
/// </exception>
public void Add(string propertyName, JsonNode? value)
{
if (propertyName is null)
{
ThrowHelper.ThrowArgumentNullException(nameof(propertyName));
}

Dictionary.Add(propertyName, value);
value?.AssignParent(this);
}
Expand Down Expand Up @@ -74,7 +79,15 @@ public void Clear()
/// <exception cref="ArgumentNullException">
/// <paramref name="propertyName"/> is <see langword="null"/>.
/// </exception>
public bool ContainsKey(string propertyName) => Dictionary.ContainsKey(propertyName);
public bool ContainsKey(string propertyName)
{
if (propertyName is null)
{
ThrowHelper.ThrowArgumentNullException(nameof(propertyName));
}

return Dictionary.ContainsKey(propertyName);
}

/// <summary>
/// Gets the number of elements contained in <see cref="JsonObject"/>.
Expand Down Expand Up @@ -180,7 +193,15 @@ public bool Remove(string propertyName)
/// <exception cref="ArgumentNullException">
/// <paramref name="propertyName"/> is <see langword="null"/>.
/// </exception>
bool IDictionary<string, JsonNode?>.TryGetValue(string propertyName, out JsonNode? jsonNode) => Dictionary.TryGetValue(propertyName, out jsonNode);
bool IDictionary<string, JsonNode?>.TryGetValue(string propertyName, out JsonNode? jsonNode)
{
if (propertyName is null)
{
ThrowHelper.ThrowArgumentNullException(nameof(propertyName));
}

return Dictionary.TryGetValue(propertyName, out jsonNode);
}

/// <summary>
/// Returns <see langword="false"/>.
Expand Down
Loading
Loading