ASP.NET Web API Async Controller Actions

Topics: ASP.NET Web API
Apr 29, 2012 at 12:44 PM

I am doing couple of async experiment with ASP.NET Web API. I have two controller action methods: one is sync and the other one is async.

DB call to SQL server takes approx. 1 second but sync method is pretty good as well. They are nearly the same in terms of performance (I used apache benchmarking tool). Actually, the both sync and the async versions are great. I am wondering if the async fundamentals of Web API causes this. Would you mind having a look at it if I can provide a repro?

Apr 29, 2012 at 4:20 PM
One way you could test the actual scale of the server is to do something you know for certain is async vs. something you know for certain is sync; for example, compare these two:

public string Get()
{
Thread.Sleep(5000);
return "Hello, world!";
}

public async Task<string> Get()
{
await Task.Delay(5000);
return "Hello, world!";
}

That will at least help to understand whether you understand the actual benchmark numbers. Then you can start introducing things about whom your async knowledge is less clear, and see whether they are scaling in the way that you expect.
Apr 29, 2012 at 5:34 PM
Brad,
Can we leverage this in any sensible way in WCF 4 without the Async CTP? If so, what would the equivalend Thread.Sleep method look like? Do we use a ContinueWith? I realize .Result or .Wait will simply block the request thread which achieves nothing.
Thanks
Brian

On Sun, Apr 29, 2012 at 4:20 PM, BradWilson <notifications@codeplex.com> wrote:

From: BradWilson

One way you could test the actual scale of the server is to do something you know for certain is async vs. something you know for certain is sync; for example, compare these two:

public string Get()
{
Thread.Sleep(5000);
return "Hello, world!";
}

public async Task<string> Get()
{
await Task.Delay(5000);
return "Hello, world!";
}

That will at least help to understand whether you understand the actual benchmark numbers. Then you can start introducing things about whom your async knowledge is less clear, and see whether they are scaling in the way that you expect.

Read the full discussion online.

To add a post to this discussion, reply to this email (ASPNETWebStack@discussions.codeplex.com)

To start a new discussion for this project, email ASPNETWebStack@discussions.codeplex.com

You are receiving this email because you subscribed to this discussion on CodePlex. You can unsubscribe or change your settings on codePlex.com.

Please note: Images and attachments will be removed from emails. Any posts to this discussion will also be available online at codeplex.com

--
-----------------------------------------
Brian Noyes
Chief Architect, IDesign Inc
Microsoft Regional Director / MVP
http://www.idesign.net
+1 703-447-3712
-----------------------------------------



Apr 29, 2012 at 5:34 PM
Argh. Acronym inversion, meant ASP.NET 4 with MVC 4 of course.

On Sun, Apr 29, 2012 at 5:33 PM, Brian Noyes <brian.noyes@idesign.net> wrote:
Brad,
Can we leverage this in any sensible way in WCF 4 without the Async CTP? If so, what would the equivalend Thread.Sleep method look like? Do we use a ContinueWith? I realize .Result or .Wait will simply block the request thread which achieves nothing.
Thanks
Brian

On Sun, Apr 29, 2012 at 4:20 PM, BradWilson <notifications@codeplex.com> wrote:

From: BradWilson

One way you could test the actual scale of the server is to do something you know for certain is async vs. something you know for certain is sync; for example, compare these two:

public string Get()
{
Thread.Sleep(5000);
return "Hello, world!";
}

public async Task<string> Get()
{
await Task.Delay(5000);
return "Hello, world!";
}

That will at least help to understand whether you understand the actual benchmark numbers. Then you can start introducing things about whom your async knowledge is less clear, and see whether they are scaling in the way that you expect.

Read the full discussion online.

To add a post to this discussion, reply to this email (ASPNETWebStack@discussions.codeplex.com)

To start a new discussion for this project, email ASPNETWebStack@discussions.codeplex.com

You are receiving this email because you subscribed to this discussion on CodePlex. You can unsubscribe or change your settings on codePlex.com.

Please note: Images and attachments will be removed from emails. Any posts to this discussion will also be available online at codeplex.com

--
-----------------------------------------
Brian Noyes
Chief Architect, IDesign Inc
Microsoft Regional Director / MVP
http://www.idesign.net
+1 703-447-3712
-----------------------------------------

--
-----------------------------------------
Brian Noyes
Chief Architect, IDesign Inc
Microsoft Regional Director / MVP
http://www.idesign.net
+1 703-447-3712
-----------------------------------------



Apr 30, 2012 at 8:59 AM

Hi Brad,

I took your advice and tested against your methods. Here is how they look like:

Sync version:

    public class CarsController : ApiController {

        public string[] GetCars() {

            Thread.Sleep(5000);

            return new[] { 
                "FIAT",
                "Citroen",
                "Ford"
            };
        }
    }

Async version:

    public class Cars2Controller : ApiController {

        public async Task<string[]> GetCars() {

            await Task.Delay(5000);

            return new[] { 
                "FIAT",
                "Citroen",
                "Ford"
            };
        }
    }

I set the wait time to 5 seconds and the end result for 100 requests (20 concurrent) as follows:

Sync:

This is ApacheBench, Version 2.3 <$Revision: 655654 $>

Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/

Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient).....done

Server Software:        Microsoft-IIS/7.5

Server Hostname:        localhost

Server Port:            1885

Document Path:          /api/cars

Document Length:        25 bytes

Concurrency Level:      20

Time taken for tests:   25.120 seconds

Complete requests:      100

Failed requests:        0

Write errors:           0

Total transferred:      39100 bytes

HTML transferred:       2500 bytes

Requests per second:    3.98 [#/sec] (mean)

Time per request:       5024.007 [ms] (mean)

Time per request:       251.200 [ms] (mean, across all concurrent requests)

Transfer rate:          1.52 [Kbytes/sec] received

 

Connection Times (ms)

              min  mean[+/-sd] median   max

Connect:        0    0   1.0      0      10

Processing:  5000 5017  19.0   5010    5070

Waiting:     5000 5016  18.8   5010    5070

Total:       5000 5017  19.0   5010    5070

 

Percentage of the requests served within a certain time (ms)

  50%   5010

  66%   5020

  75%   5030

  80%   5030

  90%   5040

  95%   5060

  98%   5070

  99%   5070

 100%   5070 (longest request)

Async:

This is ApacheBench, Version 2.3 <$Revision: 655654 $>

Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/

Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient).....done

Server Software:        Microsoft-IIS/7.5

Server Hostname:        localhost

Server Port:            1885

Document Path:          /api/cars2

Document Length:        25 bytes

Concurrency Level:      20

Time taken for tests:   25.270 seconds

Complete requests:      100

Failed requests:        0

Write errors:           0

Total transferred:      39100 bytes

HTML transferred:       2500 bytes

Requests per second:    3.96 [#/sec] (mean)

Time per request:       5054.007 [ms] (mean)

Time per request:       252.700 [ms] (mean, across all concurrent requests)

Transfer rate:          1.51 [Kbytes/sec] received

 

Connection Times (ms)

              min  mean[+/-sd] median   max

Connect:        0    0   1.0      0      10

Processing:  5000 5048  32.3   5040    5140

Waiting:     5000 5039  25.5   5040    5090

Total:       5000 5048  32.4   5040    5140

 

Percentage of the requests served within a certain time (ms)

  50%   5040

  66%   5070

  75%   5070

  80%   5080

  90%   5090

  95%   5110

  98%   5120

  99%   5140

 100%   5140 (longest request)

The sync version is better than async one and I really wonder what causes this. Apparently, this is not a bad thing but would be great to know the root cause of this behavior. BTW, I try this under .NET 4.5 beta.

Apr 30, 2012 at 4:19 PM
This just means you haven't saturated the thread pool yet. More requests, more concurrency! :) Until you hit the ceiling for worker threads, you aren't going to see the benefits for asynchronous concurrency.

I believe in .NET 4, the number of threads per core in the ASP.NET thread pool is ~ 1000. It may be higher if your web process is 64-bit.
Apr 30, 2012 at 4:21 PM
And note that you shouldn't at all be interested in tiny deltas in response time with this test, because all you're really measuring is the accuracy of the timers in question. The only time you should be concerned is when the time goes far above the expected 5000ms which would indicate that you've hit the scale limit of the server (because you've run out of threads).
Apr 30, 2012 at 4:54 PM

Thanks Brad. It really helped. 

You are right about the tiny details part. If you are to guess roughly, what would you recommend in such a case with ASP.NET Web API? Do you think we should test differently and further? As a result, in case where the sync and async have shown the above perf results (I know the tests are poor but :s), would you go for async implementations?

Apr 30, 2012 at 4:55 PM
I believe in .NET 4, the number of threads per core in the ASP.NET thread pool is ~ 1000. It may be higher if your web process is 64-bit.

BTW, the above results are under .NET 4.5.

May 2, 2012 at 5:09 PM

Hi Brad,

After your suggestions, I did further tests. It turns out that under a lot of loads, sync version is always better in my case. In fact, the async implementation sometimes kills the web server.

I really would like to provide you a repro since I cannot figure out if I implemented it wrong or the framework behaves it differently. Can you give me your e-mail address? Thanks!

May 2, 2012 at 10:20 PM
Edited May 2, 2012 at 10:23 PM

Tugberk,

Can you please provide some info on IIS version and whether you are using integrated pipeline or classic?

This is very interesting. I would not be surprised to see better performance with sync approach but poor scalability in async is surprising.

You mentioned it kills the web server. So what happens, what is the error? Does w3wp.exe die? Does it run out of memory? Do you get 503 errors? What is in the windows application/system error log?

I would expect to see 503 error with the sync approach if there are too many pending requests. If I remember correctly, ASP.NET has up to around 1000 threads in its request queue (and I would like to know for sure since I struggled to find a response) so this would happen with 1001th request.

Editor
May 3, 2012 at 7:14 AM
Edited May 3, 2012 at 7:22 AM

I've got a repro and I'm investigating. Win08/R2/SP1 (therefore IIS 7.5), integrated pipeline.