Load Testing Web Apps With Visual Studio 2012 Ultimate – Part 3

Introduction

Part 1 outlined the load testing process and setup the database and application to load test. Part 2 described how to create Web performance tests. Part 3 will explain how to create the load tests. The load tests simply run the Web performance tests created in Part 2.

Create the Load Tests

There will be one load test for each Web performance test for a total of five load tests that are added to the test project. As you will see, there are a lot of knobs you can potentially turn in a load test. However, to keep things simple, we will accept defaults for most of the choices. To create a load test, begin by right-clicking the test project and selecting Add Load Test. This launches a wizard. Click Next on the Welcome screen. Enter a name for the load test scenario as shown in Figure 1 and click Next .

Figure 1

Figure 1

In the next screen, you configure the load test pattern. The load test pattern represents the number of users to simulate and whether a constant load or step load is desired. In this case, we will choose a gradually increasing step load as shown in Figure 2.
This configuration makes the load test start with 20 simulated users. Every 60 seconds, 20 more users will be added until 100 users are reached.

Figure 2

Figure 2

Click Next twice and you’re brought to the Add tests screen. This is where you add the Web performance tests created in Part 2. Click Add and select one web performance test and click OK. Remember we are adding only one Web performance test per load test. Once the Web performance test has been added, click Next three times to get to the performance monitor screen. Click Add Computer, enter the name of the machine to monitor and hit Enter. Performance counter categories will be populated under the machine name. For this example, only select the ASP.NET and IIS categories as shown in Figure 3. While the load test runs, performance counter samples will be taken and written to a local SQL Server database.

Figure 3

Figure 3

Click Next and you are on the load test run settings screen. This is where you set the load test duration as well as the performance counter sampling frequency. Configure the load test run duration to six minutes as shown in Figure 4.

Figure 4

Figure 4

Click Finish and the load test is added to the test project. Repeat these steps for each of the five Web performance tests. The end result is that you should have a Web performance test and load test for each of the five scenarios we’re testing as shown in Figure 5. The load test files were renamed after creation.

Figure 5

Figure 5

Run the Load Tests

To run a load test, open a load test by double clicking it in Solution Explorer. Then right-click the test name and select Run load test. At this point you will see four graphs similar to Figure 6. At this point you can add and remove counters. You can change the number of grids displayed by clicking the grid icon in the toolbar.

Figure 6

Figure 6

When the load test finishes, a summary report opens displaying useful data such as average page time and pages per second.

Run the remaining load tests. Once all the load tests are run, we can analyze and compare the results.

Load Testing Web Apps With Visual Studio 2012 Ultimate – Part 4

Load Testing Web Apps With Visual Studio 2012 Ultimate – Part 2

Introduction

Part 1 outlined the load testing process and setup the database and application to load test. Part 2 will describe how to create Web performance tests. A Web performance test simply records the browsing of a website for replay later. Once the Web Performance tests are created, they can be run by a Visual Studio load lest.

Create the Web Performance Tests

There will be one Web performance test for each of the five scenarios listed in Part 1. Each test simply browses to a single web page and displays the customer sales data in a GridView. The details of the request are saved by the web performance test for subsequent replay by the load test. Before creating a Web performance test, make sure the Microsoft Web Test Recorder 10.0 & Microsoft Web Test Recorder Helper add-ons are enabled in Internet Explorer. Once this is done, right-click an ASPX page in the Web project and select View in Browser. Copy the URL. Now right-click the test project and select Add > Web Performance Test. This opens up Internet Explorer with the Web Test Recorder console. Paste the URL you copied earlier and hit enter. Once the GridView is rendered, click Stop in the Web Test Recorder console to stop the recording. This brings you back to Visual Studio and the Web performance test is opened – the document with the .webtest extension. At this point you can close the document and notice it has been added to the test project. Repeat this step for each of the five scenarios, that is, enter the five URL’s in five different Web Test Recorder sessions. The end result is that you should have five web tests as shown in Figure 1. Don’t worry about the files with the .loadtest extension. Those will be created later. Also, the web test files were renamed after creation.

Figure1

Figure 1

Load Testing Web Apps With Visual Studio 2012 Ultimate – Part 3

Load Testing Web Apps With Visual Studio 2012 Ultimate – Part 1

Introduction

Visual Studio 2012 Ultimate lets you load test you web application (or any code for that matter) within Visual Studio, see the results as the test runs, and export to Excel for further reporting and analysis. Since it is so easy, there shouldn’t be any excuse for load testing your web apps. As we shall see, the load tests are highly configurable. One can configure a load test to use a mix of browsers, network types, among other things. However, we won’t be doing anything that fancy in this series. To get an introduction to load testing, I will demonstrate the responsiveness of an ASP.NET web application that access data using the following methods:

  1. Scenario 1: Synchronous page using synchronous ADO.NET methods
  2. Scenario 2: Synchronous page using the thread pool that uses asynchronous ADO.NET methods
  3. Scenario 3: Synchronous page using the thread pool that uses synchronous ADO.NET methods
  4. Scenario 4: Synchronous page using asynchronous ADO.NET methods
  5. Scenario 5: Asynchronous page using asynchronous ADO.NET methods

You probably already know that the last method is the one that scales best but it is neat to see it for yourself with a load test. It’s just as eye-opening to see how bad the first three methods are.

The Test Environment

The test environment consists of the following applications:

  1. Windows 8 Enterprise 64-bit
  2. Visual Studio 2012 Ultimate
  3. ASP.NET 4.5
  4. SQL Server 2012 Service Pack 1 64-bit

The above software is installed on my laptop which is also where the load tests will be run. The machine specs are as follows:

  1. Processor: Intel(R) Core(TM) i7-3740QM CPU, 2.70GHz, 4 Core(s), 8 Logical Processors
  2. Memory: 16GB
  3. Hard disk: 256GB SanDisk Extreme solid state drive

I’d like to stress that Visual Studio, the ASP.NET website, and SQL Server are all running on my laptop.

The Process

Creating a load test that allows us to test the five scenarios entails the following process:

  1. Create the database.
  2. Create the data access component which will be a C# Class Library project.
  3. Create the ASP.NET web application. There will be a page for each scenario listed above. Each page contains a single GridView control that is bound to the data returned by the data access component.
  4. Create a unit test project.
  5. Create a Web performance test. Since I will have a separate web page for each scenario, I will eventually create five Web performance tests.
  6. Create a load test.
  7. Configure the load test. Since I have five scenarios to test, I will create five load tests with each load test running one of the web performance tests. This is not a requirement because you can add as many web performance tests as you want. However, to keep things simple, I will add only one web performance test per load test.
  8. Run the load test

You can watch a two-minute video demonstrating the load testing feature of Visual Studio 2012 Ultimate. I will be demonstrating the same thing in this series with respect to the four data access scenarios shown above. Additionally, I will delve into the analysis of the load tests which the video does not cover.

The Database

I used the DBGen utility which you can download from The Transaction Processing Performance Council (TPC). This utility writes test data to text files which can be imported into SQL Server using bcp or SSIS. The T-SQL script to create the database and an SSIS package that can be used to import the file data can be found here. Alternatively you can download a SQL Server 2012 backup of the database used in this series here.

The Data Access Component

Listing 1a shows the code for the simple data access component used in this series. Add this class to a C# Library project.

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Threading;

namespace LoadTestDemo
{
	public sealed class SimpleDataAccessComponent
	{
		private readonly string connectionString;
		
		private readonly string customerSalesQuery = @"SELECT TOP 25 c.CUSTKEY AS CustomerKey,
													   c.NAME AS CustomerName,
													   SUM(o.TOTALPRICE) AS TotalSales
												FROM dbo.CUSTOMER AS c
												INNER JOIN dbo.ORDERS AS o
												ON c.CUSTKEY = o.CUSTKEY
												INNER JOIN dbo.NATION AS n
												ON c.NATIONKEY = n.NATIONKEY
												WHERE n.NAME = N'UNITED STATES'
												GROUP BY c.CUSTKEY, c.NAME
												ORDER BY SUM(o.TOTALPRICE) DESC OPTION (MAXDOP 1);";

		private List<CustomerSales> customerSalesList = new List<CustomerSales>();
		private readonly Action<SqlDataReader> dataReaderAction;

		public SimpleDataAccessComponent(
			string connectionString
			)
		{
			if (string.IsNullOrEmpty(connectionString) != false)
			{
				throw new ArgumentNullException(
					"connectionString",
					"Parameter cannot be null or empty."
					);
			}

			this.connectionString = connectionString;

			this.dataReaderAction = new Action<SqlDataReader>(
			_reader =>
				{
					while (_reader.Read() != false)
					{
						CustomerSales customerSales = new CustomerSales(
							(int)_reader["CustomerKey"],
							(string)_reader["CustomerName"],
							(decimal)_reader["TotalSales"]
							);

						this.customerSalesList.Add(
							customerSales
							);
					}
				}
			);

		}

		public IList<CustomerSales> GetCustomerSalesSync()
		{
			this.ExecuteReaderSync(
				this.customerSalesQuery,
				this.dataReaderAction
				);

			return customerSalesList;
		}

		public IList<CustomerSales> GetCustomerSalesAsync()
		{
			this.ExecuteReaderAsync(
				this.customerSalesQuery,
				this.dataReaderAction
				);

			return customerSalesList;
		}


		public IList<CustomerSales> GetCustomerSalesThreadPool(
			bool asyncDataRetrieval
			)
		{
			if (asyncDataRetrieval == false)
			{
				this.ExecuteReaderThreadPool(
					this.customerSalesQuery,
					this.dataReaderAction
					);
			}
			else
			{
				this.ExecuteReaderThreadPool(
					this.customerSalesQuery,
					this.dataReaderAction,
					true
					);
			}

			return customerSalesList;
		}

		#region Private helper data access methods

		private void ExecuteReaderSync(
			string query,
			Action<SqlDataReader> action
			)
		{
			Debug.Assert(
				string.IsNullOrEmpty(query) == false
				);

			Debug.Assert(
				action != null
				);

			SqlConnection connection = null;
			SqlCommand command = null;

			try
			{
				connection = new SqlConnection(
					this.connectionString
					);

				command = new SqlCommand(
					query,
					connection
					);

				connection.Open();
				SqlDataReader reader = command.ExecuteReader();

				action(
					reader
					);
			}
			finally
			{
				if (command != null)
				{
					command.Dispose();
				}

				if (connection != null)
				{
					connection.Close();
				}
			}
		}

		private void ExecuteReaderAsync(
			string query,
			Action<SqlDataReader> action
			)
		{
			Debug.Assert(
				string.IsNullOrEmpty(query) == false
				);

			Debug.Assert(
				action != null
				);

			SqlConnection connection = null;
			SqlCommand command = null;

			try
			{
				connection = new SqlConnection(
					this.connectionString
					);

				command = new SqlCommand(
					query,
					connection
					);

				connection.Open();

				//This event is signaled when the dataReaderAction delegate returns.
				ManualResetEvent queryProcessedEvent = null;

				try
				{
					queryProcessedEvent = new ManualResetEvent(
						false
						);

					command.BeginExecuteReader(
							_ar =>
							{
								SqlDataReader _reader = command.EndExecuteReader(_ar);

								action(
									_reader
									);

								//Signal the event indicating the dataReaderAction delegate has completed.
								queryProcessedEvent.Set();

							},
						command,
						CommandBehavior.SequentialAccess
						);

					//Wait for the dataReaderAction delegate to complete.
					queryProcessedEvent.WaitOne();
				}
				finally
				{
					if (queryProcessedEvent != null)
					{
						queryProcessedEvent.Close();
					}
				}
			}
			finally
			{
				if (command != null)
				{
					command.Dispose();
				}

				if (connection != null)
				{
					connection.Close();
				}
			}
		}

	   private void ExecuteReaderThreadPool(
			string query,
			Action<SqlDataReader> action
		   )
		{
			Debug.Assert(
				 string.IsNullOrEmpty(query) == false
				 );

			Debug.Assert(
				action != null
				);

			this.ExecuteReaderThreadPool(
				query,
				action,
				false
				);
		}

		private void ExecuteReaderThreadPool(
			string query,
			Action<SqlDataReader> action,
			bool asyncDataOperation
			)
		{
			Debug.Assert(
				string.IsNullOrEmpty(query) == false
				);

			Debug.Assert(
				action != null
				);

			SqlConnection connection = null;
			SqlCommand command = null;

			try
			{
				connection = new SqlConnection(
					this.connectionString
					);

				command = new SqlCommand(
					query,
					connection
					);

				connection.Open();

				//This event is signaled when the dataReaderAction delegate returns.
				ManualResetEvent queryProcessedEvent = null;

				try
				{
					queryProcessedEvent = new ManualResetEvent(
						false
						);

					WaitCallback waitCallback = new WaitCallback(
						_object =>
						{
							Action<SqlDataReader> _action = (Action<SqlDataReader>)_object;

							if (asyncDataOperation != false)
							{
								this.ExecuteReaderAsync(
									query,
									_action
									);
							}
							else
							{
								this.ExecuteReaderSync(
									query,
									_action
									);
							}

							//Signal the event indicating the dataReaderAction delegate has completed.
							queryProcessedEvent.Set();
						}
					  );

					ThreadPool.QueueUserWorkItem(
						waitCallback,
						action
						);

					//Wait for the dataReaderAction delegate to complete.
					queryProcessedEvent.WaitOne();
				}
				finally
				{
					if (queryProcessedEvent != null)
					{
						queryProcessedEvent.Close();
					}
				}
			}
			finally
			{
				if (command != null)
				{
					command.Dispose();
				}

				if (connection != null)
				{
					connection.Close();
				}
			}
		}
		#endregion
	}
}

Listing 1a

Listing 1b shows the code for the business object used in this series. Collections of the CutomerSales type are returned by the data access components.


using System;

namespace LoadTestDemo
{
    [Serializable]
    public sealed class CustomerSales
    {
        private int customerKey;
        private string customerName;
        private decimal totalSales;

        public int CustomerKey
        {
            get
            {
                return this.customerKey;
            }
        }

        public string CustomerName
        {
            get
            {
                return this.customerName;
            }
        }

        public decimal TotalSales
        {
            get
            {
                return this.totalSales;
            }
        }

        public CustomerSales(
            int customerKey,
            string customerName,
            decimal totalSales
            )
        {
            if (customerKey < 1)
            {
                throw new ArgumentOutOfRangeException(
                    "customerKey",
                    customerKey,
                    "Value must be a positive integer."
                    );
            }

            if (string.IsNullOrEmpty(customerName) != false)
            {
                throw new ArgumentNullException(
                    "customerName",
                    "Parameter cannot be null or empty."
                    );
            }

            if (totalSales < 0)
            {
                throw new ArgumentOutOfRangeException(
                    "totalSales",
                    totalSales,
                    "Value must be zero or greater."
                    );
            }

            this.customerKey = customerKey;
            this.customerName = customerName;
            this.totalSales = totalSales;
        }
    }
}

Listing 1b

The ASP.NET Web Application

A single ASP.NET Web application will house the five ASP.NET pages corresponding to each data access scenario. Add five web forms to an ASP.NET Empty Web project. Name them Scenario1.aspx, Scenario2.aspx, and so forth. Simply add a single GridView control to each Web form as shown in Listing 2.

<asp:GridView ID="gridResults" runat="server" />

Listing 2

Also add a two connection strings to the web.config file as shown in Listing 3. One connection string is used for asynchronous data access and the other for synchronous data access. Do not change the names of the connection strings as they are referenced in code using these names.

<configuration>
    <system.web>
      <compilation debug="true" targetFramework="4.5" />
      <httpRuntime targetFramework="4.5" />
    </system.web>
  <connectionStrings>
    <add name="CompanyDbAsync" connectionString="Data Source=.;Initial Catalog=Company;Integrated Security=True;Asynchronous Processing=true"/>
    <add name="CompanyDbSync" connectionString="Data Source=.;Initial Catalog=Company;Integrated Security=True"/>
  </connectionStrings>
</configuration>

Listing 3

The Code Behind

The code for each form is simple – query the database and bind the data to the GridView control. All the code is placed in the Page’s Load method. Listings 4-8 show the code behind for the forms corresponding to scenarios 1-5 respectively.

Code for Scenario 1: Synchronous page using synchronous ADO.NET methods

Listing 4 shows the code that synchronously executes the query and binds the result to the GridView.

using System;
using System.Web.Configuration;

namespace LoadTestDemo
{
	public partial class SyncPageSyncDataAccess : System.Web.UI.Page
	{
		protected void Page_Load(object sender, EventArgs e)
		{
			string connectionString = WebConfigurationManager.ConnectionStrings["CompanyDbSync"].ConnectionString;

			SimpleDataAccessComponent c = new SimpleDataAccessComponent(
				connectionString
				);

			this.gridResults.DataSource = c.GetCustomerSalesSync();
			this.gridResults.DataBind();
		}
	}
}

Listing 4

Scenario 2: Synchronous Page Using the Thread Pool That Uses Asynchronous ADO.NET Methods

Listing 5 shows the code that uses a thread pool thread to execute an asynchronous ADO.NET method and binds the result to the GridView.

using System;
using System.Web.Configuration;

namespace LoadTestDemo
{
    public partial class ThreadPoolAsyncDataAccess : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            string connectionString = WebConfigurationManager.ConnectionStrings["CompanyDbAsync"].ConnectionString;

            SimpleDataAccessComponent c = new SimpleDataAccessComponent(
                connectionString
                );

            this.gridResults.DataSource = c.GetCustomerSalesThreadPool(
                true
                );

            this.gridResults.DataBind();
        }
    }
}

Listing 5

Scenario 3: Synchronous Page Using the Thread Pool That Uses Synchronous ADO.NET methods

Listing 6 shows the code that uses a thread pool thread to execute a synchronous ADO.NET method and binds the result to the GridView.

using System;
using System.Web.Configuration;

namespace LoadTestDemo
{
	public partial class ThreadPoolSyncDataAccess : System.Web.UI.Page
	{
		protected void Page_Load(object sender, EventArgs e)
		{
			string connectionString = WebConfigurationManager.ConnectionStrings["CompanyDbSync"].ConnectionString;

			SimpleDataAccessComponent c = new SimpleDataAccessComponent(
				connectionString
				);

			this.gridResults.DataSource = c.GetCustomerSalesThreadPool(
				false
				);

			this.gridResults.DataBind();
		}
	}
}

Listing 6

Scenario 4: Synchronous Page Using Asynchronous ADO.NET Methods

Listing 7 shows the code that uses an asynchronous ADO.NET method and binds the result to the GridView.

using System;
using System.Web.Configuration;

namespace LoadTestDemo
{
	public partial class SyncPageAsyncDataAccess : System.Web.UI.Page
	{
		protected void Page_Load(
			object sender, 
			EventArgs e
			)
		{
			string connectionString = WebConfigurationManager.ConnectionStrings["CompanyDbAsync"].ConnectionString;

			SimpleDataAccessComponent c = new SimpleDataAccessComponent(
				connectionString
				);

			this.gridResults.DataSource = c.GetCustomerSalesAsync();
			this.gridResults.DataBind();
		}
	}
}

Listing 7

Scenario 5: Asynchronous Page Using Asynchronous ADO.NET Methods

Listing 8 shows the code that uses an asynchronous ADO.NET method and binds the result to the GridView.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Web;
using System.Web.Configuration;

namespace LoadTestDemo
{
	public partial class AsyncPage : System.Web.UI.Page
	{
		List<CustomerSales> customerSalesList;

		protected void Page_Load(object sender, EventArgs e)
		{
			SimpleDataAccessComponent c = new SimpleDataAccessComponent(
				WebConfigurationManager.ConnectionStrings["CompanyDbAsync"].ConnectionString
				);

			BeginEventHandler beginHandler = new BeginEventHandler(
				 (_sender, _eventArgs, _callback, _extraData) =>
				 {
					 Task<IList<CustomerSales>> task = null;

					 try
					 {
						 task = new Task<IList<CustomerSales>>(
										() =>
										{
											return c.GetCustomerSalesAsync();
										}
										);

						 task.Start();
						 _callback(task as IAsyncResult);
						 return task as IAsyncResult;
					 }
					 finally
					 {
						 if (task != null)
						 {
							 if (task.Status == TaskStatus.RanToCompletion ||
									 task.Status == TaskStatus.Faulted ||
									 task.Status == TaskStatus.Canceled
								 )
							 {
								 task.Dispose();
							 }
						 }
					 }
				 }
				);

			EndEventHandler endHandler = new EndEventHandler(
				(_asyncResult) =>
					{
						Task<IList<CustomerSales>> task = null;

						try
						{
							task = (Task<IList<CustomerSales>>)_asyncResult;
							task.Wait();
							this.customerSalesList = task.Result as List<CustomerSales>;
						}
						finally
						{
							if (task != null)
							{
								if (task.Status == TaskStatus.RanToCompletion || 
										task.Status == TaskStatus.Faulted || 
										task.Status == TaskStatus.Canceled
									)
								{
									task.Dispose();
								}
							}
						}

					}
				);

		   this.Page.AddOnPreRenderCompleteAsync(
			   beginHandler,
			   endHandler
			   );
		}

		protected override void OnPreRenderComplete(
			EventArgs e
			)
		{

			this.gridResults.DataSource = this.customerSalesList;
			this.gridResults.DataBind();
			base.OnPreRenderComplete(e);
		}
	}
}

Listing 8

The Test Project

Add a Test project which will contain the web performance and load tests.

Load Testing Web Apps With Visual Studio 2012 Ultimate – Part 2

Load Testing Web Apps With Visual Studio 2012 Ultimate – Part 4

Introduction

Part 1 outlined the load testing process and setup the database and application to load test. Part 2 described how to create Web performance tests. Part 3 explained how to create the load tests. In Part 4, we will analyze the five load test reports to determine which data access method is best.

To open a load test’s result, first double-click the load test file in Solution Explorer. Then click the Open and Manage Results icon in the toolbar (Figure 1). Select the test result and click Open (Figure 2).

Figure 1

Figure 1

Figure 2

Figure 2

We will analyze and compare the following counters for each load test,

  1. User Load
  2. Pages/sec
  3. Avg. Page Time
  4. % Processor Time

For easier viewing, switch to one graph by clicking the Four panels grid icon in the toolbar and selecting One panel (Figure 3). Then select Key Indicators in the drop down list above the graph (Figure 4). Finally, add the above counters if not already added.

Figure 3

Figure 3

Figure 4

Figure 4

Scenario 1: Synchronous page using synchronous ADO.NET methods

Figure 5 shows the load test graph for the first scenario. The scenario under test was browsing to a synchronous page that accesses data using synchronous ADO.NET methods.

Figure 5

Figure 5

Notice the trend in pages/sec matches the user load. This makes sense because the I/O and CPU load are more or less growing in proportion to the number of users since the same query is being executed and a single cached plan is being reused. The pages per second served up are very consistent between 10.8 and 12 pages per second. The CPU’s remain pegged at or close to 100% for the duration of the test. The query coupled with the load has exposed a CPU bottleneck on my laptop.

Scenario 2: Synchronous page using the thread pool that uses asynchronous ADO.NET methods

Figure 6 shows the load test graph for the second scenario. The scenario under test was browsing to a synchronous page that accesses data using a thread pool thread that runs asynchronous ADO.NET methods.

Figure 6

Figure 6

As in the first scenario, average page time increases with user load. However, the times are much more erratic. The number of pages serves are higher than the first scenario – an average of about 10 pages per second with a max of 18.8 pages per second. There is more variance in this counter as well. Processor utilization is lower than the first scenario.

Scenario 3: Synchronous page using the thread pool that uses synchronous ADO.NET methods

Figure 7 shows the load test graph for the third scenario. The scenario under test was browsing to a synchronous page that accesses data using a thread pool thread that runs synchronous ADO.NET methods.

Figure 7

Figure 7

The first thing to notice here is that the average page time increases much more per additional user as compared to the first scenario. The pages per second served are lower as compared to the first scenario and more variable. This makes sense because these threads are competing with IIS threads for CPU time. This is in sync with the guidance that the thread pool should not be used from within ASP.NET pages. Also notice the processor utilization is a lot lower than the first scenario.

Scenario 4: Synchronous page using asynchronous ADO.NET methods

Figure 8 shows the load test graph for the fourth scenario. The scenario under test was browsing to a synchronous page that accesses data using asynchronous ADO.NET methods.

Figure 8

Figure 8

Like the first scenario, the page time increases in almost lock step with the user load. The number of pages served up per second is more variable than the first scenario but the range is higher – an average of 11.4 pages per second with a maximum of 19.2 pages per second. The processor are fully utilized for the duration of the test.

Scenario 5: Asynchronous page using asynchronous ADO.NET methods

Figure 9 shows the load test graph for the fifth scenario. The scenario under test was browsing to a asynchronous page that accesses data using asynchronous ADO.NET methods.

Figure 9

Figure 9

Like the first scenario, the page time increases in almost lock step with the user load although there is more variance when there are 100 users. The number of pages served up per second is more variable than the first scenario but the range is higher – an average of 11.2 pages per second with a maximum of 19.6 pages per second. This range is similar to the range in the fourth scenario. In fact the performance as a whole is similar to the fourth scenario.

Box Plots

A box plot can help us see more clearly the performance of each of the scenarios. Figures 10 and 11 show box plots for the pages per second and average page time counters with 100 users. Notice the variance in the asynchronous scenarios (excluding the one using the thread pool) in the first, second, and last boxes. However notice that the upper limit for the async scenarios is also higher. There is little variance in the synchronous scenarios (third and fourth box) however the upper limit is a lot less. The thread pool using synchronous ADO.NET is definitely the worst when it comes to pages per second.

Figure 10

Figure 10

Figure 11

Figure 11

Regarding page time, the asynchronous methods have the lowest values, but they are also highly variable. There is very little variance with the synchronous method that doesn’t use the thread pool but the minimum time is noticeably higher than the asynchronous methods. The thread pool using synchronous ADO.NET is definitely the worst when it comes to page time.

Conclusion

Visual Studio 2012 Ultimate gives us an easy way to load test web sites. The testing done in this series provide empirical evidence for choosing asynchronous data access methods over synchronous data access methods. There is also support for avoiding the explicit use of the thread pool. The use of asynchronous pages over synchronous pages is also supported but the performance difference is a lot closer than one might think, at least this is what my testing showed. As always with performance testing, your mileage may vary :).

Sorting With the ObjectDataSource Control

Introduction

When sorting with the ObjectDataSource control and the method used to retrieve the collection (specified in the ObjectDataSource’s SelectMethod propety) does not return a DataSet or DataTable, the method must implement a string parameter representing the sort expression. The parameter name is specified in the ObjectDataSource’s SortParameterName property.

Listing 1 shows an ObjectDataSource that gets a collection of Employee objects by calling a GetEmployees method. The method has a single parameter – sortExpression specifying how the collection should be sorted. The method returns an IEnumerable<Employee>.

<asp:ObjectDataSource ID="dataSourceEmployeeInformation" 
			runat="server" 
			TypeName="ObjectDataSourceExample.EmployeeDb" 
			SelectMethod="GetEmployees" 
			SortParameterName="sortExpression"/>

Listing 1

An easy way to sort this collection is to convert the collection into a DataView instance so that the type’s built-in sorting capabilities can be leveraged. Then convert the DataView back into an IEnumerable<Employee> which is returned to ASP.NET via the ObjectDataSource ready to be bound to controls on the page.

Since these conversions would likely be done by different classes in an application, they should be generalized. Listing 2 shows the signature of a method that converts an IEnumerable<T> into a DataView. Listing 3 shows the signature of a method that converts an DataView into a IEnumerable<T>. Notice that these two methods are implemented as extension methods.

public static IEnumerable<T> ToEnumerable<T>(
            this DataView view,
            Func<DataRowView, T> createObjectFunction
            )

Listing 2

public static IEnumerable<T> ToEnumerable<T>(
            this DataView view,
            Func<DataRowView, T> createObjectFunction
            )

Listing 3

The DataView.FromEnumerable Extension Method

Let us go through the DataView.FromEnumerable method introduced in Listing 2. The first parameter is the DataView instance the method operates on. The second parameter is the IEnumerable<T> to be converted. The third parameter is an IDictionary<string, Type> instance that will contain the column definitions for the DataView. The dictionary’s keys are the columns’ names and the values are the columns’ data types. The fourth parameter is a delegate representing a method accepting a DataRow and a collection item of type T that returns a DataRowView. Essentially the caller is responsible for the code that assigns the row values based on the values of the collection item instance. The method returns a DataView. Listing 4 shows the complete code for the method.

public static DataView FromEnumerable<T>(
            this DataView dv,
            IEnumerable<T> collection,
            IDictionary<string, Type> columnDefinitions,
            Func<DataRow, T, DataRow> addRowFunction
            )
        {
            if (collection == null)
            {
                throw new ArgumentNullException("collection", "Parameter cannot be null.");
            }

            if (collection.Any() == false)
            {
                return null;
            }

            if (columnDefinitions == null)
            {
                throw new ArgumentNullException("columnDefinitions", "Parameter cannot be null.");
            }

            if (columnDefinitions.Any() == false)
            {
                throw new InvalidOperationException("No column definitions were specified.");
            }

            if (addRowFunction == null)
            {
                throw new ArgumentNullException("addRowFunction", "Parameter cannot be null.");
            }

            DataTable dt = new DataTable();

            foreach (KeyValuePair<string, Type> keyValuePair in columnDefinitions)
            {
                dt.Columns.Add(keyValuePair.Key, keyValuePair.Value);
            }

            foreach (T item in collection)
            {
                DataRow row = dt.NewRow();
                row = addRowFunction(row, item);
                dt.Rows.Add(row);
            }

            return dt.DefaultView;

Listing 4

Lines 33-38 constructs a DataTable instance that is the basis for the DataView using the information provided in the dictionary. Lines 40-47 do the following for each item in the collection:

  1. Create a new DataRow instance.
  2. Calls the delegate, passing in the DataRow and collection item.
  3. The modified row is returned and added to the DataTable.
  4. The DataView represented by the DataTable’s DefaultView property is returned to the caller.

The DataView.ToEnumerable Extension Method

Let us go through the DataView.ToEnumerable method introduced in Listing 3. The first parameter is the DataView instance the method operates on. The second parameter specifies a delegate that takes a DataRowView and returns an item of type T. Essentially the caller is responsible for assigning the collection item’s properties based on the row values. The method returns an IEnumerable<T>. Listing 5 shows the complete code for the method.

public static IEnumerable<T> ToEnumerable<T>(
            this DataView view,
            Func<DataRowView, T> createObjectFunction
            )
        {
            if (view == null)
            {
                throw new ArgumentNullException("view", "Parameter cannot be null.");
            }

            if (createObjectFunction == null)
            {
                throw new ArgumentNullException("createObjectFunction", "Parameter cannot be null.");
            }

            IList<T> collection = new List<T>();

            foreach (DataRowView row in view)
            {
                T item = createObjectFunction(row);
                collection.Add(item);
            }

            return collection;
        }

Listing 5

Line 16 creates a new collection holding items of type T. Lines 18-22 do the following for each row in the view:

  1. Passes the row to the delegate, getting an item of type T in return.
  2. The row is added to the collection.

The collection is returned to the caller on Line 24.

Implementing the GetEmployees Method

Now a GetEmployees method that can be used for sorting by the ObjectDataSource can be written. Listing 6 shows the simple Employee class used in the method.

public class Employee
{
	public int EmployeeId { get; set; }
	public string FirstName { get; set; }
	public string LastName { get; set; }
	
	public Employee(
		int employeeId,
		string firstName,
		string lastName
		)
	{
		this.Id = employeeId;
		this.FirstName = firstName;
		this.LastName = lastName;
	}

	public Employee() : this(int.MinValue, null, null) { }
}

Listing 6

Personally I like to avoid classes with publicly settable properties. However, the ObjectDataSource requires this as well as a parameterless constructor. Listing 7 shows the complete GetEmployees implementation. The method retrieves a collection of Employees from the database and sorts them according to the value of the sortExpression parameter passed to the method.


public IEnumerable<Employee> GetEmployees(
	string sortExpression
	)
{
	//Simulate retrieving a collection from the database.

	IEnumerable<Employee> employees = new List<Employee>()
	{
		new Employee(1, "Farooq", "Mahmud"),
		new Employee(2, "Anny", "Mahmud"),
		new Employee(3, "Noor", "Mahmud"),
		new Employee(4, "Yasin", "Mahmud"),
	};

	Dictionary<string, Type> columnTypeMapping = new Dictionary<string, Type>()
	{
		{ "Id", typeof(int)},
		{"FirstName", typeof(string)},
		{"LastName", typeof(string)}
	};

	DataView dv = new DataView();

	dv = dv.FromEnumerable<Employee>(
		employees,
		columnTypeMapping,
		(row, emp) =>
		{
			row["Id"] = emp.Id;
			row["FirstName"] = emp.FirstName;
			row["LastName"] = emp.LastName;
			return row;
		}
	);

	dv.Sort = sortExpression;

	IEnumerable<Employee> sortedEmployees = dv.ToEnumerable<Employee>(
		row =>
		{
			return new Employee(
				(int)row["Id"],
				(string)row["FirstName"],
				(string)row["LastName"]
				);
		}
	);

	return sortedEmployees;
}

Listing 7

Lines 7-12 simulate retrieving a collection of employees from a database. Lines 15-20 specify the view’s structure. Lines 24-33 demonstrate calling DataView.FromEnumerable. Notice the lambda expression setting the view’s row values based on the Employee instance’s property values. The completed DataView is returned to the caller. On line 36, the view is sorted based on the value of the sortExpression parameter. In this case, assume the parameter is set to “LastName”. Now that the DataView has been sorted, a new collection must be created and the employees inserted in sorted order. Lines 38-47 accomplish that. Observer the lambda expression creating an Employee instance based on the DataView’s row values. Finally, on line 49, the sorted collection is returned to the caller.

Alternative Solutions

The IEnumerable<T> type includes the OrderBy extension method that can sort our employees. However, because the ObjectDataSource forces us to specify a string sort expression, there would be some parsing involved in order to create a method to pass to the OrderBy method.

Don’t Forget the DataKeyNames Attribute On Your GridView Control

Note: This post applies to ASP.NET 4.5.

The ASP.NET GridView control allows you to bind data returned by a custom object via the ObjectDataSource control. Consider the markup shown in Listing 1

Listing 1: ObjectDataSource control declaration
  1. <asp:ObjectDataSourceID=”sourceDepartments”
  2. runat=”server”
  3. TypeName=”ObjectDataSourceExample.DepartmentDb”
  4. DataObjectTypeName=”ObjectDataSourceExample.Department”
  5. SelectMethod=”GetDepartments”
  6. InsertMethod=”InsertDepartment”
  7. UpdateMethod=”UpdateDepartment”
  8. DeleteMethod=”DeleteDepartment />”

The SelectMethod, InsertMethod, UpdateMethod, and DeleteMethod attributes specify the methods in a custom data access layer object that selects, inserts, updates, and deletes data respectively. The TypeName attribute specifies the custom data access layer class name.

InsertDepartment, UpdateDepartment, and DeleteDepartment take an instance of the Department “business-object” type. This is noted in the control markup with the DataObjectTypeName attribute.

The markup in Listing 2 connects the ObjectDataSource to a GridView control.

Listing 1: GridView control declaration
  1. <asp:GridViewID=”gridView”DataSourceID=”sourceDepartments”AutoGenerateEditButton=”true”AutoGenerateDeleteButton=”true”runat=”server” />

At this point, the Department objects can be retrieved, inserted, and updated from the database. However, deletes are a problem because ASP.NET won’t set the Department instance’s property values before passing it to the DeleteDepartment method. Therefore, the method won’t know what to delete.

To make the delete operation work, add the DataKeyNames attribute to the GridView control and set its value to the key of the Department object. In this case, the key is represented by the Id property. The correct GridView markup is shown in Listing 3.

Listing 3: Corrected GridView control declaration
  1. <asp:GridViewID=”gridView”DataSourceID=”sourceDepartments”AutoGenerateEditButton=”true”AutoGenerateDeleteButton=”true”runat=”server”DataKeyNames=”Id”/>