Skip to content

Commit

Permalink
-DiscriminatedUnionConverter performance improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK committed Mar 7, 2015
1 parent b1184ef commit 95e04ba
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<UseVSHostingProcess>true</UseVSHostingProcess>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
Expand Down
5 changes: 4 additions & 1 deletion Src/Newtonsoft.Json.TestConsole/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
using System.Threading.Tasks;
using System.Web.Script.Serialization;
using Newtonsoft.Json.Tests;
using Newtonsoft.Json.Tests.Converters;

namespace Newtonsoft.Json.TestConsole
{
Expand All @@ -53,8 +54,10 @@ public static void Main(string[] args)
//DeserializeLargeJson();
//WriteLargeJson();
//DeserializeJson();
ReadLargeJson();
//ReadLargeJson();
//ReadLargeJsonJavaScriptSerializer();
DiscriminatedUnionConverterTests t = new DiscriminatedUnionConverterTests();
t.SerializePerformance();

Console.WriteLine();
Console.WriteLine("Finished");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
// OTHER DEALINGS IN THE SOFTWARE.
#endregion

using System.Diagnostics;
using System.Reflection;
using Microsoft.FSharp.Core;
using Microsoft.FSharp.Reflection;
#if !(NET35 || NET20 || NETFX_CORE || ASPNETCORE50)
using System;
using System.Collections.Generic;
Expand Down Expand Up @@ -83,6 +87,58 @@ public void SerializeBasicUnion()
Assert.AreEqual(@"{""Case"":""AUD""}", json);
}

[Test]
public void SerializePerformance()
{
List<Shape> values = new List<Shape>
{
Shape.NewRectangle(10.0, 5.0),
Shape.NewCircle(7.5)
};

string json = JsonConvert.SerializeObject(values, Formatting.Indented);

Stopwatch ts = new Stopwatch();
ts.Start();

for (int i = 0; i < 2000; i++)
{
JsonConvert.SerializeObject(values);
}

ts.Stop();

Console.WriteLine(ts.Elapsed.TotalSeconds);
Console.WriteLine(json);
}

[Test]
public void DeserializePerformance()
{
string json = @"[
{""Case"":""Rectangle"",""Fields"":[10.0,5.0]},
{""Case"":""Rectangle"",""Fields"":[10.0,5.0]},
{""Case"":""Rectangle"",""Fields"":[10.0,5.0]},
{""Case"":""Rectangle"",""Fields"":[10.0,5.0]},
{""Case"":""Rectangle"",""Fields"":[10.0,5.0]}
]";

JsonConvert.DeserializeObject<List<Shape>>(json);

Stopwatch ts = new Stopwatch();
ts.Start();

for (int i = 0; i < 2000; i++)
{
JsonConvert.DeserializeObject<List<Shape>>(json);
}

ts.Stop();

Console.WriteLine(ts.Elapsed.TotalSeconds);
Console.WriteLine(json);
}

[Test]
public void SerializeUnionWithFields()
{
Expand Down Expand Up @@ -116,6 +172,82 @@ public void DeserializeUnionWithFields()
Assert.AreEqual(10.0, r.width);
}

public class Union
{
public List<UnionCase> Cases;
public Converter<object, int> TagReader { get; set; }
}

public class UnionCase
{
public int Tag;
public string Name;
public PropertyInfo[] Fields;
public Converter<object, object[]> FieldReader;

This comment has been minimized.

Copy link
@SimonCropp

SimonCropp Mar 19, 2015

this doesn exist in portable. perhaps a custom delegate instead?

public Converter<object[], object> Constructor;
}

private Union CreateUnion(Type t)
{
Union u = new Union();

u.TagReader = FSharpFunc<object, int>.ToConverter(FSharpValue.PreComputeUnionTagReader(t, null));
u.Cases = new List<UnionCase>();

UnionCaseInfo[] cases = FSharpType.GetUnionCases(t, null);

foreach (UnionCaseInfo unionCaseInfo in cases)
{
UnionCase unionCase = new UnionCase();
unionCase.Tag = unionCaseInfo.Tag;
unionCase.Name = unionCaseInfo.Name;
unionCase.Fields = unionCaseInfo.GetFields();
unionCase.FieldReader = FSharpFunc<object, object[]>.ToConverter(FSharpValue.PreComputeUnionReader(unionCaseInfo, null));
unionCase.Constructor = FSharpFunc<object[], object>.ToConverter(FSharpValue.PreComputeUnionConstructor(unionCaseInfo, null));

u.Cases.Add(unionCase);
}

return u;
}

[Test]
public void Serialize()
{
Shape value = Shape.NewRectangle(10.0, 5.0);

Union union = CreateUnion(value.GetType());

int tag = union.TagReader.Invoke(value);

UnionCase caseInfo = union.Cases.Single(c => c.Tag == tag);

object[] fields = caseInfo.FieldReader.Invoke(value);

foreach (object field in fields)
{
Console.WriteLine(field);
}
}

[Test]
public void Deserialize()
{
Union union = CreateUnion(typeof(Shape.Rectangle));

UnionCase caseInfo = union.Cases.Single(c => c.Name == "Rectangle");

Shape.Rectangle value = (Shape.Rectangle)caseInfo.Constructor.Invoke(new object[]
{
10.0, 5.0
});

Console.WriteLine(value.ToString());

Console.WriteLine(value.width);
Console.WriteLine(value.length);
}

[Test]
public void DeserializeBasicUnion_NoMatch()
{
Expand Down
107 changes: 78 additions & 29 deletions Src/Newtonsoft.Json/Converters/DiscriminatedUnionConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
#if NET20
using Newtonsoft.Json.Utilities.LinqBridge;
#else
Expand All @@ -46,9 +45,67 @@ namespace Newtonsoft.Json.Converters
/// </summary>
public class DiscriminatedUnionConverter : JsonConverter
{
#region UnionDefinition
internal class Union
{
public List<UnionCase> Cases;
public Converter<object, int> TagReader { get; set; }
}

internal class UnionCase
{
public int Tag;
public string Name;
public PropertyInfo[] Fields;
public Converter<object, object[]> FieldReader;
public Converter<object[], object> Constructor;
}
#endregion

private const string CasePropertyName = "Case";
private const string FieldsPropertyName = "Fields";

private static readonly ThreadSafeStore<Type, Union> UnionCache = new ThreadSafeStore<Type, Union>(CreateUnion);
private static readonly ThreadSafeStore<Type, Type> UnionTypeLookupCache = new ThreadSafeStore<Type, Type>(CreateUnionTypeLookup);

private static Type CreateUnionTypeLookup(Type t)
{
// this lookup is because cases with fields are derived from union type
// need to get declaring type to avoid duplicate Unions in cache

// hacky but I can't find an API to get the declaring type without GetUnionCases
object[] cases = (object[])FSharpUtils.GetUnionCases(null, t, null);

object caseInfo = cases.First();

Type unionType = (Type)FSharpUtils.GetUnionCaseInfoDeclaringType(caseInfo);
return unionType;
}

private static Union CreateUnion(Type t)
{
Union u = new Union();

u.TagReader = (Converter<object, int>)FSharpUtils.PreComputeUnionTagReader(null, t, null);
u.Cases = new List<UnionCase>();

object[] cases = (object[])FSharpUtils.GetUnionCases(null, t, null);

foreach (object unionCaseInfo in cases)
{
UnionCase unionCase = new UnionCase();
unionCase.Tag = (int)FSharpUtils.GetUnionCaseInfoTag(unionCaseInfo);
unionCase.Name = (string)FSharpUtils.GetUnionCaseInfoName(unionCaseInfo);
unionCase.Fields = (PropertyInfo[])FSharpUtils.GetUnionCaseInfoFields(unionCaseInfo);
unionCase.FieldReader = (Converter<object, object[]>)FSharpUtils.PreComputeUnionReader(null, unionCaseInfo, null);
unionCase.Constructor = (Converter<object[], object>)FSharpUtils.PreComputeUnionConstructor(null, unionCaseInfo, null);

u.Cases.Add(unionCase);
}

return u;
}

/// <summary>
/// Writes the JSON representation of the object.
/// </summary>
Expand All @@ -59,21 +116,21 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
{
DefaultContractResolver resolver = serializer.ContractResolver as DefaultContractResolver;

Type t = value.GetType();
Type unionType = UnionTypeLookupCache.Get(value.GetType());
Union union = UnionCache.Get(unionType);

object result = FSharpUtils.GetUnionFields(null, value, t, null);
object info = FSharpUtils.GetUnionCaseInfo(result);
object fields = FSharpUtils.GetUnionCaseFields(result);
object caseName = FSharpUtils.GetUnionCaseInfoName(info);
object[] fieldsAsArray = fields as object[];
int tag = union.TagReader(value);
UnionCase caseInfo = union.Cases.Single(c => c.Tag == tag);

writer.WriteStartObject();
writer.WritePropertyName((resolver != null) ? resolver.GetResolvedPropertyName(CasePropertyName) : CasePropertyName);
writer.WriteValue((string)caseName);
if (fieldsAsArray != null && fieldsAsArray.Length > 0)
writer.WriteValue(caseInfo.Name);
if (caseInfo.Fields != null && caseInfo.Fields.Length > 0)
{
object[] fields = caseInfo.FieldReader(value);

writer.WritePropertyName((resolver != null) ? resolver.GetResolvedPropertyName(FieldsPropertyName) : FieldsPropertyName);
serializer.Serialize(writer, fields);
serializer.Serialize(writer, fields);
}
writer.WriteEndObject();
}
Expand All @@ -91,7 +148,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
if (reader.TokenType == JsonToken.Null)
return null;

object matchingCaseInfo = null;
UnionCase caseInfo = null;
string caseName = null;
JArray fields = null;

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

IEnumerable cases = (IEnumerable)FSharpUtils.GetUnionCases(null, objectType, null);
Union union = UnionCache.Get(objectType);

caseName = reader.Value.ToString();

foreach (object c in cases)
{
if ((string)FSharpUtils.GetUnionCaseInfoName(c) == caseName)
{
matchingCaseInfo = c;
break;
}
}
caseInfo = union.Cases.SingleOrDefault(c => c.Name == caseName);

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

if (matchingCaseInfo == null)
if (caseInfo == null)
throw JsonSerializationException.Create(reader, "No '{0}' property with union name found.".FormatWith(CultureInfo.InvariantCulture, CasePropertyName));

PropertyInfo[] fieldProperties = (PropertyInfo[])FSharpUtils.GetUnionCaseInfoFields(matchingCaseInfo);
object[] typedFieldValues = new object[fieldProperties.Length];

if (fieldProperties.Length > 0 && fields == null)
object[] typedFieldValues = new object[caseInfo.Fields.Length];

if (caseInfo.Fields.Length > 0 && fields == null)
throw JsonSerializationException.Create(reader, "No '{0}' property with union fields found.".FormatWith(CultureInfo.InvariantCulture, FieldsPropertyName));

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


for (int i = 0; i < fields.Count; i++)
{
JToken t = fields[i];
PropertyInfo fieldProperty = fieldProperties[i];
PropertyInfo fieldProperty = caseInfo.Fields[i];

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

return FSharpUtils.MakeUnion(null, matchingCaseInfo, typedFieldValues, null);
return caseInfo.Constructor(typedFieldValues);
}

/// <summary>
Expand Down
Loading

0 comments on commit 95e04ba

Please sign in to comment.