Skip to content

Commit c3c2cc2

Browse files
Eirik George Tsarpaliscarlossanlop
authored andcommitted
Fix performance issue when deserializing large payloads in JsonObject extension data.
Bug fix to address performance issues when deserializing large payloads in `JsonObject` extension data. Mitigates performance issues by optimizing the deserialization process for large `JsonObject` extension data properties. - Added `LargeJsonObjectExtensionDataSerializationState` class to temporarily store properties in a dictionary for efficient updates. - Modified `JsonObjectConverter` to use the new state class for large objects. - Updated `ObjectDefaultConverter` and `ObjectWithParameterizedConstructorConverter` to complete deserialization using the new state class. - Added tests in `JsonObjectTests` to validate performance improvements with large payloads.
1 parent 98588b0 commit c3c2cc2

File tree

8 files changed

+106
-5
lines changed

8 files changed

+106
-5
lines changed

src/libraries/Microsoft.Extensions.DependencyModel/src/Microsoft.Extensions.DependencyModel.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
<PropertyGroup>
33
<TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
44
<EnableDefaultItems>true</EnableDefaultItems>
5+
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
6+
<ServicingVersion>1</ServicingVersion>
57
<PackageDescription>Abstractions for reading `.deps` files.
68

79
Commonly Used Types:

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
<Nullable>enable</Nullable>
1010
<IncludeInternalObsoleteAttribute>true</IncludeInternalObsoleteAttribute>
1111
<IsPackable>true</IsPackable>
12-
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
13-
<ServicingVersion>9</ServicingVersion>
12+
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
13+
<ServicingVersion>10</ServicingVersion>
1414
<PackageDescription>Provides high-performance and low-allocating types that serialize objects to JavaScript Object Notation (JSON) text and deserialize JSON text to objects, with UTF-8 support built-in. Also provides types to read and write JSON text encoded as UTF-8, and to create an in-memory document object model (DOM), that is read-only, for random access of the JSON elements within a structured view of the data.
1515

1616
Commonly Used Types:
@@ -106,6 +106,7 @@ System.Text.Json.Utf8JsonReader</PackageDescription>
106106
<Compile Include="System\Text\Json\Serialization\Converters\Collection\ImmutableEnumerableOfTConverterWithReflection.cs" />
107107
<Compile Include="System\Text\Json\Serialization\Converters\Collection\StackOrQueueConverterWithReflection.cs" />
108108
<Compile Include="System\Text\Json\Serialization\Converters\JsonMetadataServicesConverter.cs" />
109+
<Compile Include="System\Text\Json\Serialization\Converters\Node\LargeJsonObjectExtensionDataSerializationState.cs" />
109110
<Compile Include="System\Text\Json\Serialization\Converters\Object\ObjectWithParameterizedConstructorConverter.Large.Reflection.cs" />
110111
<Compile Include="System\Text\Json\Serialization\IgnoreReferenceResolver.cs" />
111112
<Compile Include="System\Text\Json\Serialization\IJsonOnDeserialized.cs" />

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonObjectConverter.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,18 @@ internal override void ReadElementAndSetProperty(
2626
Debug.Assert(obj is JsonObject);
2727
JsonObject jObject = (JsonObject)obj;
2828

29-
Debug.Assert(value == null || value is JsonNode);
30-
JsonNode? jNodeValue = (JsonNode?)value;
29+
if (jObject.Count < LargeJsonObjectExtensionDataSerializationState.LargeObjectThreshold)
30+
{
31+
jObject[propertyName] = value;
32+
}
33+
else
34+
{
35+
LargeJsonObjectExtensionDataSerializationState deserializationState =
36+
state.Current.LargeJsonObjectExtensionDataSerializationState ??= new(jObject);
3137

32-
jObject[propertyName] = jNodeValue;
38+
Debug.Assert(ReferenceEquals(deserializationState.Destination, jObject));
39+
deserializationState.AddProperty(propertyName, value);
40+
}
3341
}
3442

3543
public override void Write(Utf8JsonWriter writer, JsonObject value, JsonSerializerOptions options)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.Text.Json.Nodes;
6+
7+
namespace System.Text.Json.Serialization.Converters
8+
{
9+
/// <summary>
10+
/// Implements a mitigation for deserializing large JsonObject extension data properties.
11+
/// Extension data properties use replace semantics when duplicate keys are encountered,
12+
/// which is an O(n) operation for JsonObject resulting in O(n^2) total deserialization time.
13+
/// This class mitigates the performance issue by storing the deserialized properties in a
14+
/// temporary dictionary (which has O(1) updates) and copies them to the destination object
15+
/// at the end of deserialization.
16+
/// </summary>
17+
internal sealed class LargeJsonObjectExtensionDataSerializationState
18+
{
19+
public const int LargeObjectThreshold = 25;
20+
private readonly Dictionary<string, JsonNode?> _tempDictionary;
21+
public JsonObject Destination { get; }
22+
23+
public LargeJsonObjectExtensionDataSerializationState(JsonObject destination)
24+
{
25+
StringComparer comparer = destination.Options?.PropertyNameCaseInsensitive ?? false
26+
? StringComparer.OrdinalIgnoreCase
27+
: StringComparer.Ordinal;
28+
29+
Destination = destination;
30+
_tempDictionary = new(comparer);
31+
}
32+
33+
/// <summary>
34+
/// Stores a deserialized property to the temporary dictionary, using replace semantics.
35+
/// </summary>
36+
public void AddProperty(string key, JsonNode? value)
37+
{
38+
_tempDictionary[key] = value;
39+
}
40+
41+
/// <summary>
42+
/// Copies the properties from the temporary dictionary to the destination JsonObject.
43+
/// </summary>
44+
public void Complete()
45+
{
46+
// Because we're only appending values to _tempDictionary, this should preserve JSON ordering.
47+
foreach (KeyValuePair<string, JsonNode?> kvp in _tempDictionary)
48+
{
49+
Destination[kvp.Key] = kvp.Value;
50+
}
51+
}
52+
}
53+
}

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,9 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
243243
jsonTypeInfo.UpdateSortedPropertyCache(ref state.Current);
244244
}
245245

246+
// Complete any JsonObject extension data deserializations.
247+
state.Current.LargeJsonObjectExtensionDataSerializationState?.Complete();
248+
246249
return true;
247250
}
248251

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo
165165
state.Current.JsonTypeInfo.UpdateSortedParameterCache(ref state.Current);
166166
}
167167

168+
// Complete any JsonObject extension data deserializations.
169+
state.Current.LargeJsonObjectExtensionDataSerializationState?.Complete();
170+
168171
return true;
169172
}
170173

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Collections.Generic;
55
using System.Diagnostics;
66
using System.Text.Json.Serialization;
7+
using System.Text.Json.Serialization.Converters;
78
using System.Text.Json.Serialization.Metadata;
89

910
namespace System.Text.Json
@@ -34,6 +35,7 @@ internal struct ReadStackFrame
3435
public JsonTypeInfo JsonTypeInfo;
3536
public StackFrameObjectState ObjectState; // State tracking the current object.
3637

38+
public LargeJsonObjectExtensionDataSerializationState? LargeJsonObjectExtensionDataSerializationState;
3739
// Validate EndObject token on array with preserve semantics.
3840
public bool ValidateEndTokenOnArray;
3941

src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonObjectTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.IO;
77
using System.Linq;
8+
using System.Text.Json.Serialization;
89
using System.Text.Json.Serialization.Tests;
910
using Xunit;
1011

@@ -920,5 +921,33 @@ public static void ChangeCollectionWhileEnumeratingFails(JsonObject jObject, int
920921
});
921922
Assert.Equal(1, index);
922923
}
924+
925+
[Theory]
926+
[InlineData(10_000)]
927+
[InlineData(50_000)]
928+
[InlineData(100_000)]
929+
public static void JsonObject_ExtensionData_ManyDuplicatePayloads(int size)
930+
{
931+
// Generate the payload
932+
StringBuilder builder = new StringBuilder();
933+
builder.Append("{");
934+
for (int i = 0; i < size; i++)
935+
{
936+
builder.Append($"\"{i}\": 0,");
937+
builder.Append($"\"{i}\": 0,");
938+
}
939+
builder.Length--; // strip trailing comma
940+
builder.Append("}");
941+
942+
string jsonPayload = builder.ToString();
943+
ClassWithObjectExtensionData result = JsonSerializer.Deserialize<ClassWithObjectExtensionData>(jsonPayload);
944+
Assert.Equal(size, result.ExtensionData.Count);
945+
}
946+
947+
class ClassWithObjectExtensionData
948+
{
949+
[JsonExtensionData]
950+
public JsonObject ExtensionData { get; set; }
951+
}
923952
}
924953
}

0 commit comments

Comments
 (0)