TL;DR: Made refactoring for performance, website got slower. Ran the Concurrency Visualizer, the graph looks like the lock convoys as described on MSDN.
Context
I’m helping with refactoring an ASP.NET website to switch user controls from performing business logic on datasets to perform presentation logic on business objects and also reduce database calls made from the user controls.
The issue
We have noticed a significant performance drop (hangs/blockings) after introducing changes involving what we thought would be performance improvements in multiple areas.
We’re using Lean Sentry to monitor our websites’ performance. According to the hang diagnostics, the thread pool was running out of threads and (according to the descriptions on the diagnostics page) when GC runs, it stops more threads from being created. The GC Heap and Gen 0 were consuming a lot of memory (~ 9 GB), according to the memory diagnostics.
What I did so far?
I used memory profiler in Visual Studio and identified issues with our excessive
DataAdapter
andDataTable
usage. Memory consumption dropped to 3 GB but that only helped with GC blocking. It is still slower than it had been before we introduced the changes and I still see blocking on high load caused by functions likeCompilationLock.GetLock()
andBuildManager.GetBuildResultFromCacheInternal()
. Googling them didn’t return anything useful.This is a website that uses JIT compilation. I had assumed that the issue with
CompilationLock
might be because of JIT compiling and wanted to run the website precompiled, but one of our globalUtilities
classes caused ambiguity with some otherUtilities
class/namespace that I don’t know of. I’ve found out that there is aMicrosoft.Build.Utilities
namespace, but it’s not referenced in our website and I can’t reproduce the ambiguity in my own environment when I referenceMicrosoft.Build
myself, so I couldn’t get the website running on precompiled mode on the staging server to test this theory.I made additional changes on memory allocation and the amount of database calls, using Visual Studio’s memory allocation and instrumentation profilers as a measure, but I didn’t notice any progress on performance.
I used a concurrency profiler to gather more information on thread utilization. I haven’t used this tool before, so I’m not sure about my interpretations here. There are multiple threads in each handle and in one handle I’m seeing 42% contention. I see that the
DataAdapter.Fill
andSqlHelper.ExecuteReader
methods show up most when it’s set to “Show Just My Code” andWaitForSingleObjectExImplementation
shows up most when it’s set to “Show All Code”.I encountered a SO question about ASP.NET websites’ performance issues and set
EnableSessionState="ReadOnly"
for each page, but I didn’t notice difference with this change, either.Concurrency Visualizer and Common Patterns for Poorly-Behaved Multithreaded Applications helped me identify the issue. My graph doesn’t seem like serial execution, but I see 80–90% synchronization as shown in Lock Convoys graph. I checked out a SO question on lock convoys debugging, too.
Testing approach
I’m using Screaming Frog to crawl the website in order to reproduce the issues and taking numbers of requests per second and response times in both Screaming Frog and Lean Sentry as a performance measure. It might not be the best way but the difference is noticeable, reproducible and it’s pretty much all I have at this point.
Architecture of the website
The website was originally coded in VB.NET for .NET Framework 1.0 about 10 years ago, and upgraded to .NET Framework 4.6.1 by fixing some compatibility issues. There haven’t been any architectural changes so far. There is a shared SqlHelper
class, which is a collection of shared data access functions like ExecuteDataset
or ExecuteDatareader
, that return either a DataSet
, DataReader
or String
value. These functions read the connection string information from the web.config
file and create a new SqlConnection
, SqlDataAdapter
, SqlDataReader
and SqlCommand
object to perform the database operations. The Data Access Layer that consumes this shared class consists of classes for each module like shopping cart, category, product, etc. to be instantiated in each user control and they consist of functions that represent stored procedures in the database.
The refactoring
We have introduced some new objects to be instantiated either inside page load of the related user control, or inside OnItemDataBound
event of repeaters and attached to its child user controls’ public properties, which are refactored to use the object. However, there are still other child user controls that need multiple data tables, so we decided to store one of the data tables in one of the objects and pass it to related user controls by assigning it to their public properties.
I guess that we hurt performance by introducing these objects. Even though database calls and memory consumption seem to be reduced, I’m wondering if the objects are causing threads to be synced all the time.
The graph before any refactoring happened:
The graph after all the refactoring I mentioned applied:
Will you help me identify the issue?