4

During performance testing of a very high throughput application we found a problem with JSON.NET's ContractResolver. Unfortunately, it appears that when you specify a ContractResolver the performance becomes unbearable, INCLUDING the DefaultContractResolver

Looking for advice from other experts out there to any suggestions around how to get performance to not lock down the CPU and eat up an unreasonable amount of time. Right now we are seeing an 87% reduction in performance due to this issue (80 requests per second with any ContractResolver defined and 600 requests per second with no ContractResolver defined.

The output of the test runs was:

Default Resolver: Time elapsed 3736 milliseconds

NoOp Resolver: Time elapsed 4150 milliseconds

No Resolver: Time elapsed 8 milliseconds

SnakeCase: Time elapsed 4753 milliseconds

Third Party (SnakeCase.JsonNet): Time elapsed 3881 milliseconds

The test to highlight this is as follows:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using SnakeCase.JsonNet;

namespace Anonymous.Public.Namespace
{
    public class Person
    {
        public string Name { get; set; }
        public DateTime DateOfBirth { get; set; }
        public bool EatsMeat { get; set; }
        public decimal YearlyHouseholdIncome { get; set; }

        public List<Car> CarList { get; set; }
    }

    public class Car
    {
        public string Make { get; set; }
        public string Model { get; set; }
        public int Year { get; set; }
    }

    public class NoOpNamingStrategy : NamingStrategy
    {
        public NoOpNamingStrategy(bool processDictionaryKeys, bool overrideSpecifiedNames)
        {
            base.ProcessDictionaryKeys = processDictionaryKeys;
            base.OverrideSpecifiedNames = overrideSpecifiedNames;
        }

        public NoOpNamingStrategy(bool processDictionaryKeys, bool overrideSpecifiedNames, bool processExtensionDataNames) : this(processDictionaryKeys, overrideSpecifiedNames)
        {
            base.ProcessExtensionDataNames = processExtensionDataNames;
        }

        public NoOpNamingStrategy()
        {
        }

        protected override string ResolvePropertyName(string name)
        {
            return name;
        }
    }

    [TestClass]
    public class SerializationTest
    {
        public static Person p { get; set; } = new Person
        {
            Name = "Foo Bar",
            DateOfBirth = new DateTime(1970, 01, 01),
            EatsMeat = true,
            YearlyHouseholdIncome = 47333M,
            CarList = new List<Car>
            {
                new Car
                {
                    Make = "Honda",
                    Model = "Civic",
                    Year = 2019
                }
            }
        };

        public const int ITERATIONS = 1000;

        [TestMethod]
        public void TestSnakeCase()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < ITERATIONS; i++)
            {
                var str = JsonConvert.SerializeObject(p, new JsonSerializerSettings
                {
                    ContractResolver = new DefaultContractResolver
                    {
                        NamingStrategy = new SnakeCaseNamingStrategy()
                    }
                });
            }

            sw.Stop();
            var elapsed = sw.ElapsedMilliseconds;
            Debug.WriteLine($"Time elapsed {elapsed} milliseconds");
        }

        [TestMethod]
        public void TestNoResolver()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < ITERATIONS; i++)
            {
                var str = JsonConvert.SerializeObject(p);
            }

            sw.Stop();
            var elapsed = sw.ElapsedMilliseconds;
            Debug.WriteLine($"Time elapsed {elapsed} milliseconds");
        }

        [TestMethod]
        public void TestDefaultResolver()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < ITERATIONS; i++)
            {
                var str = JsonConvert.SerializeObject(p, new JsonSerializerSettings
                {
                    ContractResolver = new DefaultContractResolver()
                });
            }

            sw.Stop();
            var elapsed = sw.ElapsedMilliseconds;
            Debug.WriteLine($"Time elapsed {elapsed} milliseconds");
        }

        [TestMethod]
        public void TestThirdPartySnakeResolver()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < ITERATIONS; i++)
            {
                var str = JsonConvert.SerializeObject(p, new JsonSerializerSettings
                {
                    ContractResolver = new SnakeCaseContractResolver()
                });
            }

            sw.Stop();
            var elapsed = sw.ElapsedMilliseconds;
            Debug.WriteLine($"Time elapsed {elapsed} milliseconds");
        }

        [TestMethod]
        public void TestNoOpResolver()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < ITERATIONS; i++)
            {
                var str = JsonConvert.SerializeObject(p, new JsonSerializerSettings
                {
                    ContractResolver = new DefaultContractResolver
                    {
                        NamingStrategy = new NoOpNamingStrategy()
                    }
                });
            }

            sw.Stop();
            var elapsed = sw.ElapsedMilliseconds;
            Debug.WriteLine($"Time elapsed {elapsed} milliseconds");
        }
    }
}
VulgarBinary
  • 3,520
  • 4
  • 20
  • 54
  • 3
    Why do you create new serializer settings for each iteration? Surely that could have an impact – phuzi Jul 30 '18 at 22:29
  • 1
    Possible duplicate of [Does Json.NET cache types' serialization information?](https://stackoverflow.com/questions/33557737/does-json-net-cache-types-serialization-information) – mjwills Jul 30 '18 at 23:13
  • No... that is asking about does it... this is important. I've seen 30 people asking the same question always unanswered. Most end up using a different serialization engine due to this. This question needs left alone. – VulgarBinary Jul 31 '18 at 22:06

1 Answers1

6

It appears as if the ContractResolver requires reflection and will cache the object types if held onto. Storing the ContractResolver at a global scope drastically changes the times to:

Default Resolver: Time elapsed 10 milliseconds

NoOp Resolver: Time elapsed 7 milliseconds

No Resolver: Time elapsed 7 milliseconds

SnakeCase: Time elapsed 178 milliseconds

Third Party (SnakeCase.JsonNet): Time elapsed 10 milliseconds

Updated test code:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using SnakeCase.JsonNet;

namespace Anonymous.Public.Namespace
{
    public class Person
    {
        public string Name { get; set; }
        public DateTime DateOfBirth { get; set; }
        public bool EatsMeat { get; set; }
        public decimal YearlyHouseholdIncome { get; set; }

        public List<Car> CarList { get; set; }
    }

    public class Car
    {
        public string Make { get; set; }
        public string Model { get; set; }
        public int Year { get; set; }
    }
    public class NoOpNamingStrategy : NamingStrategy
    {
        public NoOpNamingStrategy(bool processDictionaryKeys, bool overrideSpecifiedNames)
        {
            base.ProcessDictionaryKeys = processDictionaryKeys;
            base.OverrideSpecifiedNames = overrideSpecifiedNames;
        }

        public NoOpNamingStrategy(bool processDictionaryKeys, bool overrideSpecifiedNames, bool processExtensionDataNames) : this(processDictionaryKeys, overrideSpecifiedNames)
        {
            base.ProcessExtensionDataNames = processExtensionDataNames;
        }

        public NoOpNamingStrategy()
        {
        }

        protected override string ResolvePropertyName(string name)
        {
            return name;
        }
    }

    [TestClass]
    public class SerializationTest
    {
        public static Person p { get; set; } = new Person
        {
            Name = "Foo Bar",
            DateOfBirth = new DateTime(1970, 01, 01),
            EatsMeat = true,
            YearlyHouseholdIncome = 47333M,
            CarList = new List<Car>
            {
                new Car
                {
                    Make = "Honda",
                    Model = "Civic",
                    Year = 2019
                }
            }
        };

        public static IContractResolver Default { get; set; } = new DefaultContractResolver();

        public static IContractResolver NoOp { get; set; } = new DefaultContractResolver
        {
            NamingStrategy = new NoOpNamingStrategy()
        };

        public static IContractResolver SnakeCase { get; set; } = new DefaultContractResolver
        {
            NamingStrategy = new SnakeCaseNamingStrategy()
        };

        public static IContractResolver ThirdParty { get; set; } = new SnakeCaseContractResolver();

        public const int ITERATIONS = 1000;

        [TestMethod]
        public void TestSnakeCase()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < ITERATIONS; i++)
            {
                var str = JsonConvert.SerializeObject(p, new JsonSerializerSettings
                {
                    ContractResolver = SnakeCase
                });
            }

            sw.Stop();
            var elapsed = sw.ElapsedMilliseconds;
            Debug.WriteLine($"Time elapsed {elapsed} milliseconds");
        }

        [TestMethod]
        public void TestNoResolver()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < ITERATIONS; i++)
            {
                var str = JsonConvert.SerializeObject(p);
            }

            sw.Stop();
            var elapsed = sw.ElapsedMilliseconds;
            Debug.WriteLine($"Time elapsed {elapsed} milliseconds");
        }

        [TestMethod]
        public void TestDefaultResolver()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < ITERATIONS; i++)
            {
                var str = JsonConvert.SerializeObject(p, new JsonSerializerSettings
                {
                    ContractResolver = Default
                });
            }

            sw.Stop();
            var elapsed = sw.ElapsedMilliseconds;
            Debug.WriteLine($"Time elapsed {elapsed} milliseconds");
        }

        [TestMethod]
        public void TestThirdPartySnakeResolver()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < ITERATIONS; i++)
            {
                var str = JsonConvert.SerializeObject(p, new JsonSerializerSettings
                {
                    ContractResolver = ThirdParty
                });
            }

            sw.Stop();
            var elapsed = sw.ElapsedMilliseconds;
            Debug.WriteLine($"Time elapsed {elapsed} milliseconds");
        }

        [TestMethod]
        public void TestNoOpResolver()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < ITERATIONS; i++)
            {
                var str = JsonConvert.SerializeObject(p, new JsonSerializerSettings
                {
                    ContractResolver = NoOp
                });
            }

            sw.Stop();
            var elapsed = sw.ElapsedMilliseconds;
            Debug.WriteLine($"Time elapsed {elapsed} milliseconds");
        }
    }
}
VulgarBinary
  • 3,520
  • 4
  • 20
  • 54
  • Was correct, but barring verification it's not really an answer. Originally we had held onto the naming strategy which is how Json.NET's own documentation suggests... This imo is an issue that JSON.NET needs to have in big bold print in their examples. Grabbing their code and using reflector it's obvious why performance sucked, but didn't go to that stage originally. Regardless, solved. – VulgarBinary Jul 30 '18 at 22:42