Click here to Skip to main content
15,891,828 members
Articles / General Programming / Performance

Tweaking WCF to build highly scalable async REST API

Rate me:
Please Sign up or sign in to vote.
4.92/5 (35 votes)
31 Jul 2011CPOL25 min read 118.7K   1.2K   85  
You can build async REST API using WCF but due to some bug in WCF implementation it does not scale as you would want it to. Here's my journey with Microsoft's WCF team to explore the problem and find the right fix.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Net;
using System.Web;
using System.IO;
using System.Configuration;

namespace LoadTestProxy
{
    class Program
    {
        readonly static int MAX_THREAD_COUNT = int.Parse(ConfigurationManager.AppSettings["MAX_THREAD_COUNT"]);
        readonly static int ATTEMPTS = int.Parse(ConfigurationManager.AppSettings["ATTEMPTS"]);
        readonly static string WcfServiceHost = ConfigurationManager.AppSettings["WcfServiceHost"]; // "132.146.124.132/WcfAsyncRestApi"; // "localhost:8080";  // 
        readonly static string SomeServiceHost = ConfigurationManager.AppSettings["SomeServiceHost"]; // "localhost:8000";//  

        static readonly string EchoUrl = HttpUtility.UrlEncode(string.Format(ConfigurationManager.AppSettings["EchoUrl"], SomeServiceHost));
        static readonly string AsyncServiceUrl = string.Format(
            ConfigurationManager.AppSettings["AsyncServiceUrl"],
            EchoUrl, 0, WcfServiceHost);
        static readonly string RegularServiceUrl = string.Format(
            ConfigurationManager.AppSettings["SyncServiceUrl"],
            EchoUrl, 0, WcfServiceHost);
        static readonly string ResponsivenessTestUrl = string.Format(ConfigurationManager.AppSettings["ResponsivenessUrl"], WcfServiceHost);

        static void Main(string[] args)
        {
            var serviceResponseTimesForAsync = new List<TimeSpan>();
            var aspnetResponseTimesForAsync = new List<TimeSpan>();

            var serviceResponseTimesForRegular = new List<TimeSpan>();
            var aspnetResponseTimesForRegular = new List<TimeSpan>();

            int asyncSlowASPNETResponseCount = 0;
            int regularSlowASPNETResponseCount = 0;

            Console.WriteLine("Warming up Services");
            Console.WriteLine("================================");
            HitService(AsyncServiceUrl, ResponsivenessTestUrl, 2, new TimeSpan[MAX_THREAD_COUNT/2], new TimeSpan[MAX_THREAD_COUNT/2], out asyncSlowASPNETResponseCount, "[ASYNC]");
            HitService(RegularServiceUrl, ResponsivenessTestUrl, 2, new TimeSpan[MAX_THREAD_COUNT / 2], new TimeSpan[MAX_THREAD_COUNT / 2], out regularSlowASPNETResponseCount, "[SYNC]");
            Thread.Sleep(10000);

            asyncSlowASPNETResponseCount = 0;
            asyncSlowASPNETResponseCount = 0;

            TimeSpan[] asyncServiceAvgResponseTimes = new TimeSpan[ATTEMPTS];
            TimeSpan[] regularServiceAvgResponseTimes = new TimeSpan[ATTEMPTS];            
            for (int i = 0; i < ATTEMPTS; i++)
            {
                HitAsyncService(serviceResponseTimesForAsync, aspnetResponseTimesForAsync, asyncServiceAvgResponseTimes, i, out asyncSlowASPNETResponseCount);
                Thread.Sleep(10000);
                HitRegularService(serviceResponseTimesForRegular, aspnetResponseTimesForRegular, regularServiceAvgResponseTimes, i, out regularSlowASPNETResponseCount);
                Thread.Sleep(10000);
            }

            // Compare slow responses
            Console.WriteLine("Regular service slow responses: {0}", regularSlowASPNETResponseCount);
            Console.WriteLine("Async service slow responses: {0}", asyncSlowASPNETResponseCount);
            
            // Calculate average service response time
            CalculateAndCompareServiceResponseTimes(asyncServiceAvgResponseTimes, regularServiceAvgResponseTimes);

            // Calculate average ASP.NET response time
            CalculateAndCompareASPNETResponseTimes(aspnetResponseTimesForAsync, aspnetResponseTimesForRegular);

            ComparePercentilePerformance(95, serviceResponseTimesForAsync, aspnetResponseTimesForAsync, serviceResponseTimesForRegular, aspnetResponseTimesForRegular);

            File.WriteAllLines(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "AsyncServiceResponseTimes.txt"),
                serviceResponseTimesForAsync.ConvertAll<string>(t => t.TotalSeconds.ToString()).ToArray());
            File.WriteAllLines(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "RegularServiceResponseTimes.txt"),
                serviceResponseTimesForRegular.ConvertAll<string>(t => t.TotalSeconds.ToString()).ToArray());
            File.WriteAllLines(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ASPNETAsyncResponseTimes.txt"),
                aspnetResponseTimesForAsync.ConvertAll<string>(t => t.TotalSeconds.ToString()).ToArray());
            File.WriteAllLines(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ASPNETRegularResponseTimes.txt"),
                aspnetResponseTimesForRegular.ConvertAll<string>(t => t.TotalSeconds.ToString()).ToArray());

            Console.ReadLine();
        }

        private static void ComparePercentilePerformance(double percentile, List<TimeSpan> serviceResponseTimesForAsync, List<TimeSpan> aspnetResponseTimesForAsync, List<TimeSpan> serviceResponseTimesForRegular, List<TimeSpan> aspnetResponseTimesForRegular)
        {
            // Calculate 90%ile for async service and ASP.NET response during async service calls
            var percentileServiceResponseTimeForAsync = CalculatePercentile(serviceResponseTimesForAsync.ConvertAll<double>(t => t.TotalSeconds), percentile);
            var percentileAspNetResponseTimeForAsync = CalculatePercentile(aspnetResponseTimesForAsync.ConvertAll<double>(t => t.TotalSeconds), percentile);
            Console.WriteLine("Async {1}%ile Service Response Time: {0}", percentileServiceResponseTimeForAsync, percentile);
            Console.WriteLine("Async {1}%ile ASP.NET Response Time: {0}", percentileAspNetResponseTimeForAsync, percentile);

            // Calculate 95%ile for regular service and ASP.NET response during regular service calls
            var percentileServiceResponseTimeForRegular = CalculatePercentile(serviceResponseTimesForRegular.ConvertAll<double>(t => t.TotalSeconds), percentile);
            var percentileAspNetResponseTimeForRegular = CalculatePercentile(aspnetResponseTimesForRegular.ConvertAll<double>(t => t.TotalSeconds), percentile);
            Console.WriteLine("Regular {1}%ile Service Response Time: {0}", percentileServiceResponseTimeForRegular, percentile);
            Console.WriteLine("Regular {1}%ile ASP.NET Response Time: {0}", percentileAspNetResponseTimeForRegular, percentile);

            // Compare ASP.NET response time during async vs regular service 
            if (percentileAspNetResponseTimeForAsync < percentileAspNetResponseTimeForRegular)
                Console.WriteLine("{1}%ile ASP.NET Response time is better for Async by {0}%",
                    100 - (percentileAspNetResponseTimeForAsync / percentileAspNetResponseTimeForRegular * 100), percentile);
            else
                Console.WriteLine("{1}%ile ASP.NET Response time is worse for Async by {0}%",
                    100 - (percentileAspNetResponseTimeForRegular / percentileAspNetResponseTimeForAsync * 100), percentile);

            // Compare service response time during async vs regular service call
            if (percentileServiceResponseTimeForAsync < percentileServiceResponseTimeForRegular)
                Console.WriteLine("{1}%ile Service Response time is better for Async by {0}%",
                    100 - (percentileServiceResponseTimeForAsync / percentileServiceResponseTimeForRegular * 100), percentile);
            else
                Console.WriteLine("{1}%ile Service Response time is worse for Async by {0}%",
                    100 - (percentileServiceResponseTimeForRegular / percentileServiceResponseTimeForAsync * 100), percentile);
        }

        private static void CalculateAndCompareASPNETResponseTimes(List<TimeSpan> aspnetResponseTimesForAsync, List<TimeSpan> aspnetResponseTimesForRegular)
        {
            var averageAspnetResponseTimesForRegular = aspnetResponseTimesForRegular.Average(t => t.TotalSeconds);
            var averageAspnetResponseTimeForAsync = aspnetResponseTimesForAsync.Average(t => t.TotalSeconds);

            Console.WriteLine("Regular ASP.NET average response time: {0}", averageAspnetResponseTimesForRegular);
            Console.WriteLine("Async ASP.NET average response time: {0}", averageAspnetResponseTimeForAsync);

            if (averageAspnetResponseTimesForRegular < averageAspnetResponseTimeForAsync)
                Console.WriteLine("Async ASP.NET is {0}% slower.",
                    100 - (averageAspnetResponseTimesForRegular / averageAspnetResponseTimeForAsync * 100));
            else
                Console.WriteLine("Async ASP.NET is {0}% faster.",
                    100 - (averageAspnetResponseTimeForAsync / averageAspnetResponseTimesForRegular * 100));
        }

        private static void CalculateAndCompareServiceResponseTimes(TimeSpan[] asyncServiceAvgResponseTimes, TimeSpan[] regularServiceAvgResponseTimes)
        {
            var regularServiceAvgResponseTime = regularServiceAvgResponseTimes.Average(t => t.TotalSeconds);
            var asyncServiceAvgResponseTime = asyncServiceAvgResponseTimes.Average(t => t.TotalSeconds);

            Console.WriteLine("Regular service average response time: {0}", regularServiceAvgResponseTime);
            Console.WriteLine("Async service average response time: {0}", asyncServiceAvgResponseTime);

            if (regularServiceAvgResponseTime < asyncServiceAvgResponseTime)
                Console.WriteLine("Async service is {0}% slower.",
                    100 - (regularServiceAvgResponseTime / asyncServiceAvgResponseTime * 100));
            else
                Console.WriteLine("Async service is {0}% faster.",
                    100 - (asyncServiceAvgResponseTime / regularServiceAvgResponseTime * 100));
        }

        private static void HitRegularService(List<TimeSpan> serviceResponseTimesForRegular, List<TimeSpan> aspnetResponseTimesForRegular, TimeSpan[] regularServiceAvgResponseTimes, int i, out int regularSlowASPNETResponseCount)
        {
            Console.WriteLine("Testing Regular Service ");
            Console.WriteLine("================================");
            var serviceResponseTimesForRegularAttempt = new TimeSpan[MAX_THREAD_COUNT / 2];
            var aspnetResponseTimesForRegularAttmpt = new TimeSpan[MAX_THREAD_COUNT / 2];
            regularServiceAvgResponseTimes[i] = HitService(RegularServiceUrl, ResponsivenessTestUrl, MAX_THREAD_COUNT, serviceResponseTimesForRegularAttempt, aspnetResponseTimesForRegularAttmpt, out regularSlowASPNETResponseCount, "[SYNC]");
            serviceResponseTimesForRegular.AddRange(serviceResponseTimesForRegularAttempt);
            aspnetResponseTimesForRegular.AddRange(aspnetResponseTimesForRegularAttmpt);
        }

        private static void HitAsyncService(List<TimeSpan> serviceResponseTimesForAsync, List<TimeSpan> aspnetResponseTimesForAsync, TimeSpan[] asyncServiceAvgResponseTimes, int i, out int asyncSlowASPNETResponseCount)
        {
            Console.WriteLine("Testing Async Service");
            Console.WriteLine("================================");
            var serviceResponseTimesForAsyncAttemp = new TimeSpan[MAX_THREAD_COUNT / 2];
            var aspnetResponseTimesForAsyncAttemp = new TimeSpan[MAX_THREAD_COUNT / 2];
            asyncServiceAvgResponseTimes[i] = HitService(AsyncServiceUrl, ResponsivenessTestUrl, MAX_THREAD_COUNT, serviceResponseTimesForAsyncAttemp, aspnetResponseTimesForAsyncAttemp, out asyncSlowASPNETResponseCount, "[ASYNC]");
            serviceResponseTimesForAsync.AddRange(serviceResponseTimesForAsyncAttemp);
            aspnetResponseTimesForAsync.AddRange(aspnetResponseTimesForAsyncAttemp);
        }

        //static double CalculateAverage(string url, string responsivenessUrl, int attempts, List<TimeSpan> serviceResponseTimes, List<TimeSpan> aspnetResponseTimes)
        //{
        //    TimeSpan[] durations = new TimeSpan[attempts];
        //    for (int i = 0; i < durations.Length; i++)
        //    {
        //        Console.WriteLine("Test {0}", i);
        //        Console.WriteLine("--------------");
        //        durations[i] = HitService(url, responsivenessUrl, serviceResponseTimes, aspnetResponseTimes);
        //    }

        //    Console.Write("Average duration: ");
        //    var average = durations.Sum(t => t.TotalSeconds) / durations.Length;
        //    Console.WriteLine(average);
        //    return average;
        //}
        static TimeSpan HitService(string url, 
            string responsivenessUrl, 
            int threadCount, 
            TimeSpan[] serviceResponseTimes, 
            TimeSpan[] aspnetResponseTimes, 
            out int slowASPNETResponseCount,
            string logPrefix)
        {            
            Thread[] threads = new Thread[threadCount];
            var serviceResponseTimesCount = 0;
            var aspnetResponseTimesCount = 0;
            var slowCount = 0;
            var startTime = DateTime.Now;
            var serviceThreadOrderNo = 0;
            var aspnetThreadOrderNo = 0;
            for (int i = 0; i < threadCount/2; i++)
            {
                Thread aThread = new Thread(new ThreadStart(() =>
                    {
                        using (WebClient client = new WebClient())
                        {
                            try
                            {
                                var start = DateTime.Now;
                                Console.WriteLine("{0}\t{1}\t{2} Service call Start", logPrefix, serviceThreadOrderNo,
                                    Thread.CurrentThread.ManagedThreadId);
                                var content = client.DownloadString(url);
                                var duration = DateTime.Now - start;
                                lock (serviceResponseTimes)
                                {
                                    serviceResponseTimes[serviceResponseTimesCount++] = duration;
                                    Console.WriteLine("{0}\t{1}\t{2} End Service call. Duration: {2}", 
                                        logPrefix, serviceThreadOrderNo,
                                        Thread.CurrentThread.ManagedThreadId, duration.TotalSeconds);

                                    serviceThreadOrderNo++;
                                }
                                    
                            }
                            catch (Exception x)
                            {
                                Console.WriteLine(x);
                            }
                        }
                    }));
                aThread.Start();
                threads[i] = aThread;
            }

            // Give chance to start the calls
            Thread.Sleep(500);

            for (int i = threadCount / 2; i < threadCount; i ++)
            {
                Thread aThread = new Thread(new ThreadStart(() =>
                    {
                        using (WebClient client = new WebClient())
                        {

                            try
                            {
                                var start = DateTime.Now;
                                Console.WriteLine("{0}\t{1}\t{2} ASP.NET Page Start", logPrefix, aspnetThreadOrderNo,
                                    Thread.CurrentThread.ManagedThreadId);
                                var content = client.DownloadString(responsivenessUrl);
                                var duration = DateTime.Now - start;

                                lock (aspnetResponseTimes)
                                {
                                    aspnetResponseTimes[aspnetResponseTimesCount++] = duration;
                                    Console.WriteLine("{0}\t{1}\t{2} End of ASP.NET Call. Duration: {3}", 
                                        logPrefix, aspnetThreadOrderNo,
                                        Thread.CurrentThread.ManagedThreadId, duration.TotalSeconds);
                                    aspnetThreadOrderNo++;
                                }
                                if (serviceResponseTimesCount > 0)
                                {
                                    Console.WriteLine("{0} WARNING! ASP.NET requests running slower than service.", logPrefix);
                                    slowCount++;
                                }
                            }
                            catch (Exception x)
                            {
                                Console.WriteLine(x);
                            }
                        }
                    }));
                aThread.Start();
                threads[i] = aThread;
            }
            
            // wait for all threads to finish execution
            foreach (Thread thread in threads)
                thread.Join();
            
            var endTime = DateTime.Now;
            var totalDuration = endTime - startTime;
            Console.WriteLine(totalDuration.TotalSeconds);
            slowASPNETResponseCount = slowCount;
            return totalDuration;
        }

        /// <summary>
        /// Calculate percentile of a sorted data set
        /// </summary>
        /// <param name="sortedData">array of double values</param>
        /// <param name="p">percentile, value 0-100</param>
        /// <returns></returns>
        internal static double CalculatePercentile(List<double> list, double p)
        {
            double[] sortedData = list.ToArray();
            Array.Sort(sortedData);

            // algo derived from Aczel pg 15 bottom
            if (p >= 100.0d) return sortedData[sortedData.Length - 1];

            double position = (double)(sortedData.Length + 1) * p / 100.0;
            double leftNumber = 0.0d, rightNumber = 0.0d;

            double n = p / 100.0d * (sortedData.Length - 1) + 1.0d;

            if (position >= 1)
            {
                leftNumber = sortedData[(int)System.Math.Floor(n) - 1];
                rightNumber = sortedData[(int)System.Math.Floor(n)];
            }
            else
            {
                leftNumber = sortedData[0]; // first data
                rightNumber = sortedData[1]; // first data
            }

            if (leftNumber == rightNumber)
                return leftNumber;
            else
            {
                double part = n - System.Math.Floor(n);
                return leftNumber + part * (rightNumber - leftNumber);
            }
        } // end of internal function percentile
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect BT, UK (ex British Telecom)
United Kingdom United Kingdom

Comments and Discussions