Skip to content

Commit d9b9676

Browse files
Performance improvements in JsonValue. (#103733)
* 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.
1 parent b0c4728 commit d9b9676

23 files changed

+833
-466
lines changed

src/libraries/System.Text.Json/src/System.Text.Json.csproj

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
7575
<Compile Include="System\Text\Json\JsonTokenType.cs" />
7676
<Compile Include="System\Text\Json\Nodes\JsonArray.cs" />
7777
<Compile Include="System\Text\Json\Nodes\JsonArray.IList.cs" />
78+
<Compile Include="System\Text\Json\Nodes\JsonValueOfElement.cs" />
7879
<Compile Include="System\Text\Json\Nodes\JsonNode.cs" />
7980
<Compile Include="System\Text\Json\Nodes\JsonNode.Operators.cs" />
8081
<Compile Include="System\Text\Json\Nodes\JsonNode.Parse.cs" />
@@ -380,12 +381,6 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
380381
<Compile Include="System\Text\Json\Reader\JsonReaderHelper.netstandard.cs" />
381382
</ItemGroup>
382383

383-
<ItemGroup>
384-
<None Include="System\Text\Json\OrderedDictionary.cs" />
385-
<None Include="System\Text\Json\OrderedDictionary.KeyCollection.cs" />
386-
<None Include="System\Text\Json\OrderedDictionary.ValueCollection.cs" />
387-
</ItemGroup>
388-
389384
<!-- Application tfms (.NETCoreApp, .NETFramework) need to use the same or higher version of .NETStandard's dependencies. -->
390385
<ItemGroup>
391386
<ProjectReference Include="$(LibrariesProjectRoot)System.Text.Encodings.Web\src\System.Text.Encodings.Web.csproj" />

src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,17 @@ internal JsonTokenType GetJsonTokenType(int index)
118118
return _parsedData.GetJsonTokenType(index);
119119
}
120120

121+
internal bool ValueIsEscaped(int index, bool isPropertyName)
122+
{
123+
CheckNotDisposed();
124+
125+
int matchIndex = isPropertyName ? index - DbRow.Size : index;
126+
DbRow row = _parsedData.Get(matchIndex);
127+
Debug.Assert(!isPropertyName || row.TokenType is JsonTokenType.PropertyName);
128+
129+
return row.HasComplexChildren;
130+
}
131+
121132
internal int GetArrayLength(int index)
122133
{
123134
CheckNotDisposed();
@@ -363,6 +374,16 @@ internal string GetNameOfPropertyValue(int index)
363374
return GetString(index - DbRow.Size, JsonTokenType.PropertyName)!;
364375
}
365376

377+
internal ReadOnlySpan<byte> GetPropertyNameRaw(int index)
378+
{
379+
CheckNotDisposed();
380+
381+
DbRow row = _parsedData.Get(index - DbRow.Size);
382+
Debug.Assert(row.TokenType is JsonTokenType.PropertyName);
383+
384+
return _utf8Json.Span.Slice(row.Location, row.SizeOrLength);
385+
}
386+
366387
internal bool TryGetValue(int index, [NotNullWhen(true)] out byte[]? value)
367388
{
368389
CheckNotDisposed();

src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,13 @@ internal string GetPropertyName()
11641164
return _parent.GetNameOfPropertyValue(_idx);
11651165
}
11661166

1167+
internal ReadOnlySpan<byte> GetPropertyNameRaw()
1168+
{
1169+
CheckValidInstance();
1170+
1171+
return _parent.GetPropertyNameRaw(_idx);
1172+
}
1173+
11671174
/// <summary>
11681175
/// Gets the original input data backing this value, returning it as a <see cref="string"/>.
11691176
/// </summary>
@@ -1194,6 +1201,154 @@ internal string GetPropertyRawText()
11941201
return _parent.GetPropertyRawValueAsString(_idx);
11951202
}
11961203

1204+
internal bool ValueIsEscaped
1205+
{
1206+
// TODO make public https://github.com/dotnet/runtime/issues/77666
1207+
get
1208+
{
1209+
CheckValidInstance();
1210+
1211+
return _parent.ValueIsEscaped(_idx, isPropertyName: false);
1212+
}
1213+
}
1214+
1215+
internal ReadOnlySpan<byte> ValueSpan
1216+
{
1217+
// TODO make public https://github.com/dotnet/runtime/issues/77666
1218+
get
1219+
{
1220+
CheckValidInstance();
1221+
1222+
return _parent.GetRawValue(_idx, includeQuotes: false).Span;
1223+
}
1224+
}
1225+
1226+
// TODO make public https://github.com/dotnet/runtime/issues/33388
1227+
internal static bool DeepEquals(JsonElement left, JsonElement right)
1228+
{
1229+
Debug.Assert(left._parent != null);
1230+
Debug.Assert(right._parent != null);
1231+
1232+
JsonValueKind kind = left.ValueKind;
1233+
if (kind != right.ValueKind)
1234+
{
1235+
return false;
1236+
}
1237+
1238+
switch (kind)
1239+
{
1240+
case JsonValueKind.Null or JsonValueKind.False or JsonValueKind.True:
1241+
return true;
1242+
1243+
case JsonValueKind.Number:
1244+
// JSON numbers are equal if their raw representations are equal.
1245+
return left.GetRawValue().Span.SequenceEqual(right.GetRawValue().Span);
1246+
1247+
case JsonValueKind.String:
1248+
if (right.ValueIsEscaped)
1249+
{
1250+
if (left.ValueIsEscaped)
1251+
{
1252+
// Both values are escaped, force an allocation to unescape the RHS.
1253+
return left.ValueEquals(right.GetString());
1254+
}
1255+
1256+
// Swap values so that unescaping is handled by the LHS.
1257+
(left, right) = (right, left);
1258+
}
1259+
1260+
return left.ValueEquals(right.ValueSpan);
1261+
1262+
case JsonValueKind.Array:
1263+
ArrayEnumerator rightArrayEnumerator = right.EnumerateArray();
1264+
foreach (JsonElement leftElement in left.EnumerateArray())
1265+
{
1266+
if (!rightArrayEnumerator.MoveNext() || !DeepEquals(leftElement, rightArrayEnumerator.Current))
1267+
{
1268+
return false;
1269+
}
1270+
}
1271+
1272+
return !rightArrayEnumerator.MoveNext();
1273+
1274+
default:
1275+
Debug.Assert(kind is JsonValueKind.Object);
1276+
ObjectEnumerator leftObjectEnumerator = left.EnumerateObject();
1277+
ObjectEnumerator rightObjectEnumerator = right.EnumerateObject();
1278+
1279+
// Two JSON objects are considered equal if they define the same set of properties.
1280+
// Start optimistically with sequential comparison, but fall back to unordered
1281+
// comparison as soon as a mismatch is encountered.
1282+
1283+
while (leftObjectEnumerator.MoveNext())
1284+
{
1285+
if (!rightObjectEnumerator.MoveNext())
1286+
{
1287+
return false;
1288+
}
1289+
1290+
JsonProperty leftProp = leftObjectEnumerator.Current;
1291+
JsonProperty rightProp = rightObjectEnumerator.Current;
1292+
1293+
if (!NameEquals(leftProp, rightProp))
1294+
{
1295+
// We have our first mismatch, fall back to unordered comparison.
1296+
return UnorderedObjectDeepEquals(leftObjectEnumerator, rightObjectEnumerator);
1297+
}
1298+
1299+
if (!DeepEquals(leftProp.Value, rightProp.Value))
1300+
{
1301+
return false;
1302+
}
1303+
}
1304+
1305+
return !rightObjectEnumerator.MoveNext();
1306+
1307+
static bool UnorderedObjectDeepEquals(ObjectEnumerator left, ObjectEnumerator right)
1308+
{
1309+
Dictionary<string, JsonElement> rightElements = new(StringComparer.Ordinal);
1310+
do
1311+
{
1312+
JsonProperty prop = right.Current;
1313+
rightElements.TryAdd(prop.Name, prop.Value);
1314+
}
1315+
while (right.MoveNext());
1316+
1317+
int leftCount = 0;
1318+
do
1319+
{
1320+
JsonProperty prop = left.Current;
1321+
if (!rightElements.TryGetValue(prop.Name, out JsonElement rightElement) || !DeepEquals(prop.Value, rightElement))
1322+
{
1323+
return false;
1324+
}
1325+
1326+
leftCount++;
1327+
}
1328+
while (left.MoveNext());
1329+
1330+
return leftCount == rightElements.Count;
1331+
}
1332+
1333+
static bool NameEquals(JsonProperty left, JsonProperty right)
1334+
{
1335+
if (right.NameIsEscaped)
1336+
{
1337+
if (left.NameIsEscaped)
1338+
{
1339+
// Both values are escaped, force an allocation to unescape the RHS.
1340+
return left.NameEquals(right.Name);
1341+
}
1342+
1343+
// Swap values so that unescaping is handled by the LHS
1344+
(left, right) = (right, left);
1345+
}
1346+
1347+
return left.NameEquals(right.NameSpan);
1348+
}
1349+
}
1350+
}
1351+
11971352
/// <summary>
11981353
/// Compares <paramref name="text" /> to the string value of this element.
11991354
/// </summary>
@@ -1292,6 +1447,13 @@ internal bool TextEqualsHelper(ReadOnlySpan<char> text, bool isPropertyName)
12921447
return _parent.TextEquals(_idx, text, isPropertyName);
12931448
}
12941449

1450+
internal bool ValueIsEscapedHelper(bool isPropertyName)
1451+
{
1452+
CheckValidInstance();
1453+
1454+
return _parent.ValueIsEscaped(_idx, isPropertyName);
1455+
}
1456+
12951457
/// <summary>
12961458
/// Write the element into the provided writer as a JSON value.
12971459
/// </summary>

src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@ public readonly struct JsonProperty
1818
public JsonElement Value { get; }
1919
private string? _name { get; }
2020

21-
internal JsonProperty(JsonElement value, string? name = null)
21+
internal JsonProperty(JsonElement value)
2222
{
2323
Value = value;
24-
_name = name;
2524
}
2625

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

96+
// TODO make public https://github.com/dotnet/runtime/issues/77666
97+
internal bool NameIsEscaped => Value.ValueIsEscapedHelper(isPropertyName: true);
98+
internal ReadOnlySpan<byte> NameSpan => Value.GetPropertyNameRaw();
99+
97100
/// <summary>
98101
/// Write the property into the provided writer as a named JSON object property.
99102
/// </summary>

src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonArray.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public sealed partial class JsonArray : JsonNode
2323
private JsonElement? _jsonElement;
2424
private List<JsonNode?>? _list;
2525

26+
internal override JsonElement? UnderlyingElement => _jsonElement;
27+
2628
/// <summary>
2729
/// Initializes a new instance of the <see cref="JsonArray"/> class that is empty.
2830
/// </summary>
@@ -93,11 +95,11 @@ internal override JsonNode DeepCloneCore()
9395
return jsonArray;
9496
}
9597

96-
internal override bool DeepEqualsCore(JsonNode? node)
98+
internal override bool DeepEqualsCore(JsonNode node)
9799
{
98100
switch (node)
99101
{
100-
case null or JsonObject:
102+
case JsonObject:
101103
return false;
102104
case JsonValue value:
103105
// JsonValue instances have special comparison semantics, dispatch to their implementation.

src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.To.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,12 @@ public override string ToString()
4444
// Special case for string; don't quote it.
4545
if (this is JsonValue)
4646
{
47-
if (this is JsonValue<string> jsonString)
47+
if (this is JsonValuePrimitive<string> jsonString)
4848
{
4949
return jsonString.Value;
5050
}
5151

52-
if (this is JsonValue<JsonElement> jsonElement &&
53-
jsonElement.Value.ValueKind == JsonValueKind.String)
52+
if (this is JsonValueOfElement { Value.ValueKind: JsonValueKind.String } jsonElement)
5453
{
5554
return jsonElement.Value.GetString()!;
5655
}

src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,17 @@ namespace System.Text.Json.Nodes
1414
/// declared as an <see cref="object"/> should be deserialized as a <see cref="JsonNode"/>.
1515
public abstract partial class JsonNode
1616
{
17+
// Default options instance used when calling built-in JsonNode converters.
18+
private protected static readonly JsonSerializerOptions s_defaultOptions = new();
19+
1720
private JsonNode? _parent;
1821
private JsonNodeOptions? _options;
1922

23+
/// <summary>
24+
/// The underlying JsonElement if the node is backed by a JsonElement.
25+
/// </summary>
26+
internal virtual JsonElement? UnderlyingElement => null;
27+
2028
/// <summary>
2129
/// Options to control the behavior.
2230
/// </summary>
@@ -300,11 +308,15 @@ public static bool DeepEquals(JsonNode? node1, JsonNode? node2)
300308
{
301309
return node2 is null;
302310
}
311+
else if (node2 is null)
312+
{
313+
return false;
314+
}
303315

304316
return node1.DeepEqualsCore(node2);
305317
}
306318

307-
internal abstract bool DeepEqualsCore(JsonNode? node);
319+
internal abstract bool DeepEqualsCore(JsonNode node);
308320

309321
/// <summary>
310322
/// Replaces this node with a new value.
@@ -375,7 +387,7 @@ internal void AssignParent(JsonNode parent)
375387
}
376388

377389
var jsonTypeInfo = (JsonTypeInfo<T>)JsonSerializerOptions.Default.GetTypeInfo(typeof(T));
378-
return new JsonValueCustomized<T>(value, jsonTypeInfo, options);
390+
return JsonValue.CreateFromTypeInfo(value, jsonTypeInfo, options);
379391
}
380392
}
381393
}

src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.IDictionary.cs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ public partial class JsonObject : IDictionary<string, JsonNode?>
2525
/// </exception>
2626
public void Add(string propertyName, JsonNode? value)
2727
{
28+
if (propertyName is null)
29+
{
30+
ThrowHelper.ThrowArgumentNullException(nameof(propertyName));
31+
}
32+
2833
Dictionary.Add(propertyName, value);
2934
value?.AssignParent(this);
3035
}
@@ -74,7 +79,15 @@ public void Clear()
7479
/// <exception cref="ArgumentNullException">
7580
/// <paramref name="propertyName"/> is <see langword="null"/>.
7681
/// </exception>
77-
public bool ContainsKey(string propertyName) => Dictionary.ContainsKey(propertyName);
82+
public bool ContainsKey(string propertyName)
83+
{
84+
if (propertyName is null)
85+
{
86+
ThrowHelper.ThrowArgumentNullException(nameof(propertyName));
87+
}
88+
89+
return Dictionary.ContainsKey(propertyName);
90+
}
7891

7992
/// <summary>
8093
/// Gets the number of elements contained in <see cref="JsonObject"/>.
@@ -180,7 +193,15 @@ public bool Remove(string propertyName)
180193
/// <exception cref="ArgumentNullException">
181194
/// <paramref name="propertyName"/> is <see langword="null"/>.
182195
/// </exception>
183-
bool IDictionary<string, JsonNode?>.TryGetValue(string propertyName, out JsonNode? jsonNode) => Dictionary.TryGetValue(propertyName, out jsonNode);
196+
bool IDictionary<string, JsonNode?>.TryGetValue(string propertyName, out JsonNode? jsonNode)
197+
{
198+
if (propertyName is null)
199+
{
200+
ThrowHelper.ThrowArgumentNullException(nameof(propertyName));
201+
}
202+
203+
return Dictionary.TryGetValue(propertyName, out jsonNode);
204+
}
184205

185206
/// <summary>
186207
/// Returns <see langword="false"/>.

0 commit comments

Comments
 (0)