Skip to content

Commit

Permalink
Performance improvements in JsonValue. (#103733)
Browse files Browse the repository at this point in the history
* Performance improvements in JsonValue.

* Fix #103715.

* Add more test cases and fix a number of bugs related to DeepEquals and escaping.

* Fix number handling corner case.
  • Loading branch information
eiriktsarpalis committed Jun 20, 2024
1 parent b0c4728 commit d9b9676
Show file tree
Hide file tree
Showing 23 changed files with 833 additions and 466 deletions.
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

0 comments on commit d9b9676

Please sign in to comment.