Sunday, 3 May 2015

Performance improvements in JSON serialization

Last week I wrote a post about why Newtonsoft.JSON should be the undisputed choices when dealing with JSON serialization operations. However I ran into some interesting scenarios :). These scenarios do not indicate the there is an issue with NewtonSoft.JSON - they indicate that we should be using it with correct configurations.

1. We use PostSharp to instrument the assembly to inject Trace statements during compilation. The AOP implementation serialized all the parameters (JSON format) and logged in database. What was interesting was that the implementation was supplied with parameters of type like FileStream, NetworkStream, MemoryStream etc. for serialization. Now that is problematic because you do not want to read stream content in the method because that may lead to unwanted side effects. Newtonsoft.JSON does provide us a way to handle that though. 

We can use "JsonSerializerSettings" class to indicate the custom Errorhandler in case serialization fails something that is true for streams. However, that would mean the application flow will need to go through exception handling process unnecessarily and will slow down the logging process. Good news is that Newtonsoft.JSON comes with a ContractResolver option where we can filter out properties (based on their type) and not log those. That solves the problem to some extent.

        var JsonSerializerSettings serializationSettings = new JsonSerializerSettings()
        {
            Error = (s, e) => { e.ErrorContext.Handled = true; },
            ContractResolver = new CustomResolver()
        };

        
2. Now, I have 2 options to serialize the argument collection.

a. Simply serialize the collection directly:

JsonConvert.SerializeObject(items, settings);

b. Loop through items in argument collection, serialize them individually:

public static string ToString(IEnumerable<object> items)

{
            StringBuilder builder = new StringBuilder();
var JsonSerializerSettings settings = new JsonSerializerSettings()
        {
            Error = (s, e) => { e.ErrorContext.Handled = true; },
            ContractResolver = new CustomResolver()
        };

foreach (object argument in items)
            {
               builder.AppendLine(Newtonsoft.Json.JsonConvert.SerializeObject(argument, serializationSettings));
            }
    return builder.ToString();
}

Option b worked better for me. YMMV though :)

3. There was one problem though - performance. 

I wrote a simple test that loops through the ToString function 100,000 times and it took ~62 seconds to complete. 0.6 ms for each iteration, not bad!! 

List arguments = new List();
            arguments.Add(new MemoryStream());
            arguments.Add(1);
            arguments.Add("ioeitosdnksdsd");
            TestClass c = new TestClass()
            {
                MyProperty = 1,
                MyProperty2 = new MemoryStream()
            };
            
            Stopwatch w = new Stopwatch();
            w.Start();
            for (int x = 0; x < 100000; x++)
            {
                var serializationSettings = new JsonSerializerSettings()
                {
                    Error = (s, e) => { e.ErrorContext.Handled = true; },
                    ContractResolver = new CustomResolver()
                };

                StringBuilder argumentBuilder = new StringBuilder();
                foreach (object argument1 in arguments)
                {
                   builder.AppendLine(Newtonsoft.Json.JsonConvert.SerializeObject(argument, serializationSettings));
                }
            }
            w.Stop();
            Console.WriteLine(w.ElapsedMilliseconds);
            Console.ReadLine();
            return;

That was not enough actually. This particular method that we wrote was expected to be used heavily as we have extensive logging in the application. During our load testing, we found out that it can lead to queuing of requests on IIS. 

Let us see if something can be done in this regard. Actually, yes. The serializationSettings instance is not needed to be created everytime because it can be reused for each operation. Move it outside the loop.

List arguments = new List();
            arguments.Add(new MemoryStream());
            arguments.Add(1);
            arguments.Add("ioeitosdnksdsd");
            TestClass c = new TestClass()
            {
                MyProperty = 1,
                MyProperty2 = new MemoryStream()
            };
             var serializationSettings = new JsonSerializerSettings()
                {
                    Error = (s, e) => { e.ErrorContext.Handled = true; },
                    ContractResolver = new CustomResolver()
                };
            Stopwatch w = new Stopwatch();
            w.Start();
            for (int x = 0; x < 100000; x++)
            {
               StringBuilder argumentBuilder = new StringBuilder();
                foreach (object argument1 in arguments)
                {
                   builder.AppendLine(Newtonsoft.Json.JsonConvert.SerializeObject(argument, serializationSettings));
                }
            }
            w.Stop();
            Console.WriteLine(w.ElapsedMilliseconds);
            Console.ReadLine();
            return;

Total time is 171 milliseconds now. Whoa!!

No comments:

Post a Comment