-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Rendering large Razor views is much slower in MVC 6 than MVC 5 #3516
Comments
Some allocation data for ~430 requests The bulk of this is just coming from TagHelpers and related infrastructure. These issues are known, and are being worked on 👍 There's a significant contribution from Removing TagHelper support leads you to a repro of #3196 |
With 2eb6cd6 - I can easily max out my 100mbit network with this scenario (no taghelpers) using about 4% CPU. We should rerun this on the benchmarking hardware w/without taghelpers. |
Fixes here: I'm able to also max out my 100mbit network easily with the taghelpers version of this benchmark. We should remeasure. |
We measured this again today with these fixes:
Without UrlResolutionTagHelper we max out the network card at 2701 rps. I suspect that we're doing something a little less efficient with chunking which could explain the delta between MVC5 and MVC6, but that change won't come from MVC. We're going to take further steps to mitigate the effects of UrlResolutionTagHelper since that's part of the default experience. See aspnet/Razor#684 |
Remeasuring this
Best throughput was at 32 connections in wrk. We were able to push CPU and network near max. Suspect pushing further will require taking a look at chunking in Kestrel |
The only issue we're seeing now is large memory usage in the TagHelpers variant
When a lot of requests hit a page that hasn't been compiled yet they will queue waiting for the compilation task to complete. Once it's completed, they all begin executing the page and will check out buffers to hold their output. If the number of requests is high, or the page takes a while to compile (like on startup where we have load roslyn) then the number of concurrent requests can result in the |
@rynowak could you verify that by adding a call to |
I'll hack this up and give it a try. |
Yeah, I don't think it's the I/O that's the issue here, it's compilation/startup. What I noticed trying this again is that if I visit the page in a browser before hitting it with the load then the memory usage is normal. If I hit the site with the load generator right away then memory spikes as soon as the page is compiled because there are lots of threads executing it. You would hope that FlushAsync would mitigate this, and it does when you have TagHelpers disabled. However, it doesn't really help you in the default case because ...... because TagHelpers. Each TagHelper needs to buffer all output that happens while rendering it's body, and because of the "after form" taghelper and the "option/select" taghelper there's a lot of these. |
Hi, |
We didn't close the issue, we just haven't identified any additional work for RC2. Take a look at the data here: #3516 (comment) The main change that we had to make was to make TagHelpers smarter in some cases about when they run and not. We've also put a lot of work into the backing buffer that Razor writes to which has helped as well. |
Hmmmm 👍 |
We've done some more analysis here - the memory growth issue is an intentional behavior of the buffer pool. When a bucket is exhausted, the pool will try the next bucket, and the next, and the next, until they've all been tried. For a scenario like this where we're blasting the server with load while the page is being compiled, the number of concurrent/queued requests results in us 'trying' each bucket and thus populating it up to the theoretical max of ~800mb. So Razor asks for a 32-element array and might get a 2048-element array. We're looking at making a tweak to this policy to have the memory pool fall back to allocation mode faster and try only a few buckets. /cc @stephentoub |
I'll amend my previous statement, this was working as intended when there's no contention 😆 |
@dougbu let's re-analyze this scenario by comparing:
Current test app is here: https://github.com/aspnet/Performance/tree/dev/testapp/LargeStaticView Buffer is here: Mvc/src/Microsoft.AspNetCore.Mvc.Core/Internal/MemoryPoolHttpResponseStreamWriterFactory.cs Line 20 in 760c8f3
|
Took a quick look at |
Should instead have mentioned |
@rynowak would have to answer regarding allocations vs. RPS. As far as where to run ASP.NET Core, run it on .NET Core only. Not super interested in .NET Framework numbers for that. |
@dougbu - we want to understand if we are better or worse than MVC5 in this regard, by how much, and why (if we're worse). Better includes both throughput and latency. Allocations helps us understand why but isn't on it's own an outcome when we're looking at this as an end to end. With regard to your specifics, the world is your oyster. We would consider changing anything that makes the future brighter. |
…e all of allocated arrays - aspnet/Mvc#3516 - when first array (`byte[]` or `char[]`) is larger than requested, use it all - also allocate second array (`char[]` or `byte[]`) to match larger size nit: add bounds check of `bufferSize` in `HttpResponseStreamWriter` constructor
For the scenarios mentioned above, throughput on Azure is normally network-limited. But the CPU numbers tell a positive story for the latest ASP.NET Core stack.
On larger Azure hardware, throughput improves but hits another limit. CPU utilization looks reasonable though worse for .NET Core MVC than ASP.NET 5.2:
(LargeStaticFile uses We may have more room for improvement with the latest tag helper infrastructure. But, they do involve more work. |
- #3516 - fix tests that relied on otherwise-unused `HttpResponseStreamWriter.DefaultBufferSize`
- #3516 - fix tests that relied on otherwise-unused `HttpResponseStreamWriter.DefaultBufferSize`
Have a few potential follow-ups on the BigViews and LargeJsonApi scenarios. Will talk a bit internally then file the appropriate issues. |
…e all of allocated arrays - aspnet/Mvc#3516 - when first array (`byte[]` or `char[]`) is larger than requested, use it all - also allocate second array (`char[]` or `byte[]`) to match larger size nit: add bounds check of `bufferSize` in `HttpResponseStreamWriter` constructor
Did some experiments comparing throughput of largish views (i.e. ~325 KB, browse http://www.msn.com and save "view source" as HTML/CSHTML file) in the SmurfLab.
The results show that serving large views via Razor in MVC 5 is not a huge drop from raw IIS static file serving, but MVC 6 is much slower than MVC 5 (6x). A quick look at the .NET memory counters shows a very high number of Gen 1 GCs taking place when serving from MVC 6 and a time in GC at ~15%.
The text was updated successfully, but these errors were encountered: