Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple Response.writeAsync Calls

I have been researching Asp.Net Security and I found some surprising code:

Strange Code?

context.Response.ContentType = "text/html";
await context.Response.WriteAsync("<html><body>");
await context.Response.WriteAsync("An remote error has occured: " + context.Request.Query["ErrorMessage"] + "<br>");
await context.Response.WriteAsync("<a href=\"/\">Home</a>");
await context.Response.WriteAsync("</body></html>");

What surprised me is the multiple calls to WriteAsync with short strings.

What I would have done

I would have used a template with String.Format or a StringBuilder to concatenate the strings and then write that in a single call:

var template = @"
<html><body>
An remote error has occured:{0}<br>
<a href=\"/\">Home</a>
</body></html>
";

var html = string.format(template, context.Request.Query["ErrorMessage"]);
await context.Response.WriteAsync(html);

The differences I observe

  • My code is much easier to modify.
  • I've got some extra white-space.
  • My code uses a larger hard-coded string instead of a bunch of small hard-coded strings.
  • I use String.Format which may have a performance hit compared to concatenation.
  • If string concatenation should be avoided, this part should be broken up:

    "An remote error has occured: " + context.Request.Query["ErrorMessage"] + "<br>"
    

Questions

For the purposes of discussion. Let's assume that this is in the context of a web server with an average of ~10,000 simultaneous active users: So performance is important.

  • Why is this done like this?
  • How does it affect performance?
  • When should await Response.WriteAsync be called instead of Response.Write?
  • How often should Response.WriteAsync be called?
    • As often as possible with tiny amounts of data
    • Only when a large amount of text is ready
like image 669
Rick Love Avatar asked Jan 07 '23 03:01

Rick Love


1 Answers

I created an Azure website (running on Basic 1 - 1 Small Instance) to benchmark this. Then I used the free service at https://loader.io to run each test at 100 users/second over 1 minute.

I ran each test 3 times in different orders. The times for each test run were within 200ms of each other.

Results:

The results were clear: StringBuilder won significantly. The cost of each async call far out weighs the cost of any form of string concatenation (even String.Format performed better than the multiple async calls).

  • 1992ms - StringBuilder.Append
  • 3071ms - StringBuilder.AppendFormat
  • 4257ms - WriteAsync with String.Format
  • 9265ms - WriteAsync

Here is the code for each test:

    // Do not write this code - It is ugly and performs terribly
    private async Task TestWriteAsync(HttpContext context)
    {
        var r = context.Response;

        var id = "id";
        var size = "12";
        var text = "text";

        await r.WriteAsync("<div style='display:none'>");

        for (int i = 0; i < 10000; i++)
        {
            await r.WriteAsync("<li id='");
            await r.WriteAsync(id);
            await r.WriteAsync("' style='font-size:");
            await r.WriteAsync(size);
            await r.WriteAsync("'>");
            await r.WriteAsync(text);
            await r.WriteAsync("</li>");
        }

        await r.WriteAsync("</div>");
    }

    // This is much better, but still not great
    private async Task TestWriteAsyncFormat(HttpContext context)
    {
        var r = context.Response;

        var id = "id";
        var size = "12";
        var text = "text";
        var template = "<li id='{0}' style='font-size:{1}'>{2}</li>";

        await r.WriteAsync("<div style='display:none'>");

        for (int i = 0; i < 10000; i++)
        {
            await r.WriteAsync(string.Format(template, id, size, text));
        }

        await r.WriteAsync("</div>");
    }

    // The Performance Winner, but ugly code
    private async Task TestStringBuilder(HttpContext context)
    {
        var sb = new StringBuilder();

        var id = "id";
        var size = "12";
        var text = "text";

        sb.Append("<div style='display:none'>");

        for (int i = 0; i < 10000; i++)
        {
            sb.Append("<li id='");
            sb.Append(id);
            sb.Append("' style='font-size:");
            sb.Append(size);
            sb.Append("'>");
            sb.Append(text);
            sb.Append("</li>");
        }

        sb.Append("</div>");

        await context.Response.WriteAsync(sb.ToString());
    }

    // Decent performance and Clean Code
    private async Task TestStringBuilderFormat(HttpContext context)
    {
        var sb = new StringBuilder();

        var id = "id";
        var size = "12";
        var text = "text";
        var template = "<li id='{0}' style='font-size:{1}'>{2}</li>";

        sb.Append("<div style='display:none'>");

        for (int i = 0; i < 10000; i++)
        {
            sb.AppendFormat(template, id, size, text);
        }

        sb.Append("</div>");

        await context.Response.WriteAsync(sb.ToString());
    }

So although the old "Response.Write" is faster than StringBuilder with synchronous requests, "await Response.WriteAsync" is much slower (because of the async overhead).

Test Screenshots:

enter image description here

enter image description here

enter image description here

enter image description here

like image 138
Rick Love Avatar answered Jan 15 '23 09:01

Rick Love