Skip to content

Commit 95e04ba

Browse files
committed
-DiscriminatedUnionConverter performance improvements
1 parent b1184ef commit 95e04ba

5 files changed

Lines changed: 243 additions & 42 deletions

File tree

Src/Newtonsoft.Json.TestConsole/Newtonsoft.Json.TestConsole.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<DefineConstants>DEBUG;TRACE</DefineConstants>
2222
<ErrorReport>prompt</ErrorReport>
2323
<WarningLevel>4</WarningLevel>
24+
<UseVSHostingProcess>true</UseVSHostingProcess>
2425
</PropertyGroup>
2526
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
2627
<PlatformTarget>AnyCPU</PlatformTarget>

Src/Newtonsoft.Json.TestConsole/Program.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
using System.Threading.Tasks;
3333
using System.Web.Script.Serialization;
3434
using Newtonsoft.Json.Tests;
35+
using Newtonsoft.Json.Tests.Converters;
3536

3637
namespace Newtonsoft.Json.TestConsole
3738
{
@@ -53,8 +54,10 @@ public static void Main(string[] args)
5354
//DeserializeLargeJson();
5455
//WriteLargeJson();
5556
//DeserializeJson();
56-
ReadLargeJson();
57+
//ReadLargeJson();
5758
//ReadLargeJsonJavaScriptSerializer();
59+
DiscriminatedUnionConverterTests t = new DiscriminatedUnionConverterTests();
60+
t.SerializePerformance();
5861

5962
Console.WriteLine();
6063
Console.WriteLine("Finished");

Src/Newtonsoft.Json.Tests/Converters/DiscriminatedUnionConverterTests.cs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
// OTHER DEALINGS IN THE SOFTWARE.
2424
#endregion
2525

26+
using System.Diagnostics;
27+
using System.Reflection;
28+
using Microsoft.FSharp.Core;
29+
using Microsoft.FSharp.Reflection;
2630
#if !(NET35 || NET20 || NETFX_CORE || ASPNETCORE50)
2731
using System;
2832
using System.Collections.Generic;
@@ -83,6 +87,58 @@ public void SerializeBasicUnion()
8387
Assert.AreEqual(@"{""Case"":""AUD""}", json);
8488
}
8589

90+
[Test]
91+
public void SerializePerformance()
92+
{
93+
List<Shape> values = new List<Shape>
94+
{
95+
Shape.NewRectangle(10.0, 5.0),
96+
Shape.NewCircle(7.5)
97+
};
98+
99+
string json = JsonConvert.SerializeObject(values, Formatting.Indented);
100+
101+
Stopwatch ts = new Stopwatch();
102+
ts.Start();
103+
104+
for (int i = 0; i < 2000; i++)
105+
{
106+
JsonConvert.SerializeObject(values);
107+
}
108+
109+
ts.Stop();
110+
111+
Console.WriteLine(ts.Elapsed.TotalSeconds);
112+
Console.WriteLine(json);
113+
}
114+
115+
[Test]
116+
public void DeserializePerformance()
117+
{
118+
string json = @"[
119+
{""Case"":""Rectangle"",""Fields"":[10.0,5.0]},
120+
{""Case"":""Rectangle"",""Fields"":[10.0,5.0]},
121+
{""Case"":""Rectangle"",""Fields"":[10.0,5.0]},
122+
{""Case"":""Rectangle"",""Fields"":[10.0,5.0]},
123+
{""Case"":""Rectangle"",""Fields"":[10.0,5.0]}
124+
]";
125+
126+
JsonConvert.DeserializeObject<List<Shape>>(json);
127+
128+
Stopwatch ts = new Stopwatch();
129+
ts.Start();
130+
131+
for (int i = 0; i < 2000; i++)
132+
{
133+
JsonConvert.DeserializeObject<List<Shape>>(json);
134+
}
135+
136+
ts.Stop();
137+
138+
Console.WriteLine(ts.Elapsed.TotalSeconds);
139+
Console.WriteLine(json);
140+
}
141+
86142
[Test]
87143
public void SerializeUnionWithFields()
88144
{
@@ -116,6 +172,82 @@ public void DeserializeUnionWithFields()
116172
Assert.AreEqual(10.0, r.width);
117173
}
118174

175+
public class Union
176+
{
177+
public List<UnionCase> Cases;
178+
public Converter<object, int> TagReader { get; set; }
179+
}
180+
181+
public class UnionCase
182+
{
183+
public int Tag;
184+
public string Name;
185+
public PropertyInfo[] Fields;
186+
public Converter<object, object[]> FieldReader;
187+
public Converter<object[], object> Constructor;
188+
}
189+
190+
private Union CreateUnion(Type t)
191+
{
192+
Union u = new Union();
193+
194+
u.TagReader = FSharpFunc<object, int>.ToConverter(FSharpValue.PreComputeUnionTagReader(t, null));
195+
u.Cases = new List<UnionCase>();
196+
197+
UnionCaseInfo[] cases = FSharpType.GetUnionCases(t, null);
198+
199+
foreach (UnionCaseInfo unionCaseInfo in cases)
200+
{
201+
UnionCase unionCase = new UnionCase();
202+
unionCase.Tag = unionCaseInfo.Tag;
203+
unionCase.Name = unionCaseInfo.Name;
204+
unionCase.Fields = unionCaseInfo.GetFields();
205+
unionCase.FieldReader = FSharpFunc<object, object[]>.ToConverter(FSharpValue.PreComputeUnionReader(unionCaseInfo, null));
206+
unionCase.Constructor = FSharpFunc<object[], object>.ToConverter(FSharpValue.PreComputeUnionConstructor(unionCaseInfo, null));
207+
208+
u.Cases.Add(unionCase);
209+
}
210+
211+
return u;
212+
}
213+
214+
[Test]
215+
public void Serialize()
216+
{
217+
Shape value = Shape.NewRectangle(10.0, 5.0);
218+
219+
Union union = CreateUnion(value.GetType());
220+
221+
int tag = union.TagReader.Invoke(value);
222+
223+
UnionCase caseInfo = union.Cases.Single(c => c.Tag == tag);
224+
225+
object[] fields = caseInfo.FieldReader.Invoke(value);
226+
227+
foreach (object field in fields)
228+
{
229+
Console.WriteLine(field);
230+
}
231+
}
232+
233+
[Test]
234+
public void Deserialize()
235+
{
236+
Union union = CreateUnion(typeof(Shape.Rectangle));
237+
238+
UnionCase caseInfo = union.Cases.Single(c => c.Name == "Rectangle");
239+
240+
Shape.Rectangle value = (Shape.Rectangle)caseInfo.Constructor.Invoke(new object[]
241+
{
242+
10.0, 5.0
243+
});
244+
245+
Console.WriteLine(value.ToString());
246+
247+
Console.WriteLine(value.width);
248+
Console.WriteLine(value.length);
249+
}
250+
119251
[Test]
120252
public void DeserializeBasicUnion_NoMatch()
121253
{

Src/Newtonsoft.Json/Converters/DiscriminatedUnionConverter.cs

Lines changed: 78 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
using System;
2929
using System.Collections;
3030
using System.Collections.Generic;
31-
using System.ComponentModel;
3231
#if NET20
3332
using Newtonsoft.Json.Utilities.LinqBridge;
3433
#else
@@ -46,9 +45,67 @@ namespace Newtonsoft.Json.Converters
4645
/// </summary>
4746
public class DiscriminatedUnionConverter : JsonConverter
4847
{
48+
#region UnionDefinition
49+
internal class Union
50+
{
51+
public List<UnionCase> Cases;
52+
public Converter<object, int> TagReader { get; set; }
53+
}
54+
55+
internal class UnionCase
56+
{
57+
public int Tag;
58+
public string Name;
59+
public PropertyInfo[] Fields;
60+
public Converter<object, object[]> FieldReader;
61+
public Converter<object[], object> Constructor;
62+
}
63+
#endregion
64+
4965
private const string CasePropertyName = "Case";
5066
private const string FieldsPropertyName = "Fields";
5167

68+
private static readonly ThreadSafeStore<Type, Union> UnionCache = new ThreadSafeStore<Type, Union>(CreateUnion);
69+
private static readonly ThreadSafeStore<Type, Type> UnionTypeLookupCache = new ThreadSafeStore<Type, Type>(CreateUnionTypeLookup);
70+
71+
private static Type CreateUnionTypeLookup(Type t)
72+
{
73+
// this lookup is because cases with fields are derived from union type
74+
// need to get declaring type to avoid duplicate Unions in cache
75+
76+
// hacky but I can't find an API to get the declaring type without GetUnionCases
77+
object[] cases = (object[])FSharpUtils.GetUnionCases(null, t, null);
78+
79+
object caseInfo = cases.First();
80+
81+
Type unionType = (Type)FSharpUtils.GetUnionCaseInfoDeclaringType(caseInfo);
82+
return unionType;
83+
}
84+
85+
private static Union CreateUnion(Type t)
86+
{
87+
Union u = new Union();
88+
89+
u.TagReader = (Converter<object, int>)FSharpUtils.PreComputeUnionTagReader(null, t, null);
90+
u.Cases = new List<UnionCase>();
91+
92+
object[] cases = (object[])FSharpUtils.GetUnionCases(null, t, null);
93+
94+
foreach (object unionCaseInfo in cases)
95+
{
96+
UnionCase unionCase = new UnionCase();
97+
unionCase.Tag = (int)FSharpUtils.GetUnionCaseInfoTag(unionCaseInfo);
98+
unionCase.Name = (string)FSharpUtils.GetUnionCaseInfoName(unionCaseInfo);
99+
unionCase.Fields = (PropertyInfo[])FSharpUtils.GetUnionCaseInfoFields(unionCaseInfo);
100+
unionCase.FieldReader = (Converter<object, object[]>)FSharpUtils.PreComputeUnionReader(null, unionCaseInfo, null);
101+
unionCase.Constructor = (Converter<object[], object>)FSharpUtils.PreComputeUnionConstructor(null, unionCaseInfo, null);
102+
103+
u.Cases.Add(unionCase);
104+
}
105+
106+
return u;
107+
}
108+
52109
/// <summary>
53110
/// Writes the JSON representation of the object.
54111
/// </summary>
@@ -59,21 +116,21 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
59116
{
60117
DefaultContractResolver resolver = serializer.ContractResolver as DefaultContractResolver;
61118

62-
Type t = value.GetType();
119+
Type unionType = UnionTypeLookupCache.Get(value.GetType());
120+
Union union = UnionCache.Get(unionType);
63121

64-
object result = FSharpUtils.GetUnionFields(null, value, t, null);
65-
object info = FSharpUtils.GetUnionCaseInfo(result);
66-
object fields = FSharpUtils.GetUnionCaseFields(result);
67-
object caseName = FSharpUtils.GetUnionCaseInfoName(info);
68-
object[] fieldsAsArray = fields as object[];
122+
int tag = union.TagReader(value);
123+
UnionCase caseInfo = union.Cases.Single(c => c.Tag == tag);
69124

70125
writer.WriteStartObject();
71126
writer.WritePropertyName((resolver != null) ? resolver.GetResolvedPropertyName(CasePropertyName) : CasePropertyName);
72-
writer.WriteValue((string)caseName);
73-
if (fieldsAsArray != null && fieldsAsArray.Length > 0)
127+
writer.WriteValue(caseInfo.Name);
128+
if (caseInfo.Fields != null && caseInfo.Fields.Length > 0)
74129
{
130+
object[] fields = caseInfo.FieldReader(value);
131+
75132
writer.WritePropertyName((resolver != null) ? resolver.GetResolvedPropertyName(FieldsPropertyName) : FieldsPropertyName);
76-
serializer.Serialize(writer, fields);
133+
serializer.Serialize(writer, fields);
77134
}
78135
writer.WriteEndObject();
79136
}
@@ -91,7 +148,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
91148
if (reader.TokenType == JsonToken.Null)
92149
return null;
93150

94-
object matchingCaseInfo = null;
151+
UnionCase caseInfo = null;
95152
string caseName = null;
96153
JArray fields = null;
97154

@@ -105,20 +162,13 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
105162
{
106163
ReadAndAssert(reader);
107164

108-
IEnumerable cases = (IEnumerable)FSharpUtils.GetUnionCases(null, objectType, null);
165+
Union union = UnionCache.Get(objectType);
109166

110167
caseName = reader.Value.ToString();
111168

112-
foreach (object c in cases)
113-
{
114-
if ((string)FSharpUtils.GetUnionCaseInfoName(c) == caseName)
115-
{
116-
matchingCaseInfo = c;
117-
break;
118-
}
119-
}
169+
caseInfo = union.Cases.SingleOrDefault(c => c.Name == caseName);
120170

121-
if (matchingCaseInfo == null)
171+
if (caseInfo == null)
122172
throw JsonSerializationException.Create(reader, "No union type found with the name '{0}'.".FormatWith(CultureInfo.InvariantCulture, caseName));
123173
}
124174
else if (string.Equals(propertyName, FieldsPropertyName, StringComparison.OrdinalIgnoreCase))
@@ -137,31 +187,30 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
137187
ReadAndAssert(reader);
138188
}
139189

140-
if (matchingCaseInfo == null)
190+
if (caseInfo == null)
141191
throw JsonSerializationException.Create(reader, "No '{0}' property with union name found.".FormatWith(CultureInfo.InvariantCulture, CasePropertyName));
142-
143-
PropertyInfo[] fieldProperties = (PropertyInfo[])FSharpUtils.GetUnionCaseInfoFields(matchingCaseInfo);
144-
object[] typedFieldValues = new object[fieldProperties.Length];
145192

146-
if (fieldProperties.Length > 0 && fields == null)
193+
object[] typedFieldValues = new object[caseInfo.Fields.Length];
194+
195+
if (caseInfo.Fields.Length > 0 && fields == null)
147196
throw JsonSerializationException.Create(reader, "No '{0}' property with union fields found.".FormatWith(CultureInfo.InvariantCulture, FieldsPropertyName));
148197

149198
if (fields != null)
150199
{
151-
if (fieldProperties.Length != fields.Count)
200+
if (caseInfo.Fields.Length != fields.Count)
152201
throw JsonSerializationException.Create(reader, "The number of field values does not match the number of properties definied by union '{0}'.".FormatWith(CultureInfo.InvariantCulture, caseName));
153202

154203

155204
for (int i = 0; i < fields.Count; i++)
156205
{
157206
JToken t = fields[i];
158-
PropertyInfo fieldProperty = fieldProperties[i];
207+
PropertyInfo fieldProperty = caseInfo.Fields[i];
159208

160209
typedFieldValues[i] = t.ToObject(fieldProperty.PropertyType, serializer);
161210
}
162211
}
163212

164-
return FSharpUtils.MakeUnion(null, matchingCaseInfo, typedFieldValues, null);
213+
return caseInfo.Constructor(typedFieldValues);
165214
}
166215

167216
/// <summary>

0 commit comments

Comments
 (0)