Skip to content

Commit be46d16

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 in `System.Text.Json.Serialization.Converters` to handle large payloads efficiently. - Updated `JsonObjectConverter` to use the new state class for large objects. - Modified `ObjectDefaultConverter`, `ObjectWithParameterizedConstructorConverter`, and `ReadStackFrame` to complete deserialization using the new state class. - Added tests in `JsonObjectTests` to validate the performance improvements with large payloads.
1 parent b236305 commit be46d16

File tree

8 files changed

+111
-7
lines changed

8 files changed

+111
-7
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
<TargetFrameworks>$(NetCoreAppCurrent);$(NetCoreAppPrevious);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum)</TargetFrameworks>
44
<EnableDefaultItems>true</EnableDefaultItems>
55
<IsPackable>true</IsPackable>
6-
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
7-
<ServicingVersion>1</ServicingVersion>
6+
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
7+
<ServicingVersion>2</ServicingVersion>
88
<PackageDescription>Provides abstractions for reading `.deps` files. When a .NET application is compiled, the SDK generates a JSON manifest file (`&lt;ApplicationName&gt;.deps.json`) that contains information about application dependencies. You can use `Microsoft.Extensions.DependencyModel` to read information from this manifest at run time. This is useful when you want to dynamically compile code (for example, using Roslyn Emit API) referencing the same dependencies as your main application.
99

1010
By default, the dependency manifest contains information about the application's target framework and runtime dependencies. Set the PreserveCompilationContext project property to `true` to additionally include information about reference assemblies used during compilation.</PackageDescription>

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
<NoWarn>CS8969</NoWarn>
99
<IncludeInternalObsoleteAttribute>true</IncludeInternalObsoleteAttribute>
1010
<IsPackable>true</IsPackable>
11-
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
12-
<ServicingVersion>4</ServicingVersion>
11+
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
12+
<ServicingVersion>5</ServicingVersion>
1313
<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.
1414

1515
The System.Text.Json library is built-in as part of the shared framework in .NET Runtime. The package can be installed when you need to use it in other target frameworks.</PackageDescription>
@@ -122,6 +122,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
122122
<Compile Include="System\Text\Json\Serialization\Converters\Collection\MemoryConverterFactory.cs" />
123123
<Compile Include="System\Text\Json\Serialization\Converters\Collection\StackOrQueueConverterWithReflection.cs" />
124124
<Compile Include="System\Text\Json\Serialization\Converters\JsonMetadataServicesConverter.cs" />
125+
<Compile Include="System\Text\Json\Serialization\Converters\Node\LargeJsonObjectExtensionDataSerializationState.cs" />
125126
<Compile Include="System\Text\Json\Serialization\Converters\Object\ObjectWithParameterizedConstructorConverter.Large.Reflection.cs" />
126127
<Compile Include="System\Text\Json\Serialization\Converters\Collection\MemoryConverter.cs" />
127128
<Compile Include="System\Text\Json\Serialization\Converters\Value\ReadOnlyMemoryByteConverter.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
@@ -27,10 +27,18 @@ internal override void ReadElementAndSetProperty(
2727
Debug.Assert(obj is JsonObject);
2828
JsonObject jObject = (JsonObject)obj;
2929

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

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

3644
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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,9 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
254254
jsonTypeInfo.UpdateSortedPropertyCache(ref state.Current);
255255
}
256256

257+
// Complete any JsonObject extension data deserializations.
258+
state.Current.LargeJsonObjectExtensionDataSerializationState?.Complete();
259+
257260
return true;
258261
}
259262

@@ -299,6 +302,9 @@ internal static void PopulatePropertiesFastPath(object obj, JsonTypeInfo jsonTyp
299302
{
300303
jsonTypeInfo.UpdateSortedPropertyCache(ref state.Current);
301304
}
305+
306+
// Complete any JsonObject extension data deserializations.
307+
state.Current.LargeJsonObjectExtensionDataSerializationState?.Complete();
302308
}
303309

304310
internal sealed override bool OnTryWrite(

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
@@ -267,6 +267,9 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo
267267
state.Current.JsonTypeInfo.UpdateSortedParameterCache(ref state.Current);
268268
}
269269

270+
// Complete any JsonObject extension data deserializations.
271+
state.Current.LargeJsonObjectExtensionDataSerializationState?.Complete();
272+
270273
return true;
271274
}
272275

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Runtime.CompilerServices;
88
using System.Runtime.InteropServices;
99
using System.Text.Json.Serialization;
10+
using System.Text.Json.Serialization.Converters;
1011
using System.Text.Json.Serialization.Metadata;
1112

1213
namespace System.Text.Json
@@ -40,6 +41,8 @@ internal struct ReadStackFrame
4041
public JsonTypeInfo JsonTypeInfo;
4142
public StackFrameObjectState ObjectState; // State tracking the current object.
4243

44+
public LargeJsonObjectExtensionDataSerializationState? LargeJsonObjectExtensionDataSerializationState;
45+
4346
// Current object can contain metadata
4447
public bool CanContainMetadata;
4548
public MetadataPropertyName LatestMetadataPropertyName;

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
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;
10+
using System.Text.Json.Tests;
911
using System.Threading.Tasks;
1012
using Xunit;
1113

@@ -1189,5 +1191,33 @@ public static void ReplaceWith()
11891191
Assert.Null(jValue.Parent);
11901192
Assert.Equal("{\"value\":5}", jObject.ToJsonString());
11911193
}
1194+
1195+
[Theory]
1196+
[InlineData(10_000)]
1197+
[InlineData(50_000)]
1198+
[InlineData(100_000)]
1199+
public static void JsonObject_ExtensionData_ManyDuplicatePayloads(int size)
1200+
{
1201+
// Generate the payload
1202+
StringBuilder builder = new StringBuilder();
1203+
builder.Append("{");
1204+
for (int i = 0; i < size; i++)
1205+
{
1206+
builder.Append($"\"{i}\": 0,");
1207+
builder.Append($"\"{i}\": 0,");
1208+
}
1209+
builder.Length--; // strip trailing comma
1210+
builder.Append("}");
1211+
1212+
string jsonPayload = builder.ToString();
1213+
ClassWithObjectExtensionData result = JsonSerializer.Deserialize<ClassWithObjectExtensionData>(jsonPayload);
1214+
Assert.Equal(size, result.ExtensionData.Count);
1215+
}
1216+
1217+
class ClassWithObjectExtensionData
1218+
{
1219+
[JsonExtensionData]
1220+
public JsonObject ExtensionData { get; set; }
1221+
}
11921222
}
11931223
}

0 commit comments

Comments
 (0)