Improving ASP Application Performance

From Mynoteswiki.com

J.D. Meier

March 28, 2000


Contents

Introduction

Developers often ask how to get more performance out of their ASP applications. Because I frequently review Active Server Pages (ASP) applications for performance, I figured I'd share the set of questions I ask to identify problems and recommend improvements. Some of these tips may seem like common sense, while others may be less obvious; however, all of the recommendations are backed by real world experience.

ASP Page Performance

  • Are you storing Visual Basic objects in Session or Application scope?

Visual Basic® and other Single-Threaded Apartment (STA) objects should be used only at page scope. Storing STAs in a Session variable locks the object down to the thread that created the object, defeating the purpose of a thread pool. Storing an STA in Application scope serializes access for all users. See the following articles for more information on this issue:

  • Don Box's "ASP and COM Apartments"
  • Q243543 "INFO: Do Not Store STA Objects in Session or Application"
  • Q243548 "INFO: Design Guidelines for Visual Basic Components Under ASP"
  • Q243815 "PRB: Storing STA COM Component in Session Locks to Single Thread"


  • Are you storing the Scripting.Dictionary in Session or Application scope?

The Scripting.Dictionary is Apartment threaded and should be used only at page scope, or your application will suffer serious serialization issues (see Q194803 "PRB: Scripting.Dictionary Object Fails in ASP Application Scope" ). This limitation usually raises the question of what dictionaries can be used at Session or Application scope. Options are somewhat limited, but include the Commerce Dictionary and the LookupTable Object. See "Abridging the Dictionary Object: The ASP Team Creates a Lookup-Table Object."


  • Are your ASP scripts hundreds of lines long?

Script is interpreted line by line, so eliminating redundant script or creating more efficient script can improve performance. If you have hundreds of lines of ASP script in a single page, perhaps you can better partition your user, business, and data services.

ASP script is great for gathering input or formatting output, but when it comes to business and data services, components offer some additional benefits -- such as early binding and protection of intellectual property. In his article "Component vs. Component Part II," Jason Taylor discusses significant performance gains he found porting his ASP script to custom components, using Windows Script Components (WSC) as an intermediate step.

If you aren't using components, you can still partition your services with functions. For example, if your script is rendering several tables, make a generic function to generate your tables. You can then put these functions into includes, or lay the groundwork for a future port to components. The following sample illustrates using functions and includes for improved maintenance.

  <script language="vbscript" runat="server">

     Sub Main()
           WriteHeader
           WriteBody
           WriteFooter
     End Sub
     Sub WriteBody()
           ...
     End Sub
     Main   'call sub Main
  </script>


  • Are your #include files too big?

There are no hard and fast rules for include-file sizes, but knowing how includes work can help you gauge whether you are using them efficiently. When ASP processes includes, it reads the entire file into memory. Because ASP will cache the entire expanded code (your page + the include), not just the functions you call, you may end up with large, inefficient namespaces that ASP must search when calling methods or looking up variables. Note that this process occurs for each page that uses the include. A good guide is to create fine-grained includes, so that you can be more selective about which pages will include them. For a more detailed explanation of how ASP processes includes, see "The Implications of ASP #include."


  • Are you using global variables?

Global variables increase the namespace used by ASP to retrieve values from memory. Variables declared within subroutines or functions are faster. For more information, see "25+ ASP Tips to Improve Performance and Style."


  • Does your script excessively intersperse ASP and HTML?

Keep blocks of ASP server-side script together, rather than switching back and forth between server-side and client-side code. This switching usually happens when concatenating HTML with simple values from ASP, as when you are writing out an HTML table:

  <%    For iCtr = 1 to 10  %>
<TR><TD>Counting ... <%= iCtr %> <% Next %> You can improve the code's by making it a single script block: <% For iCtr = 1 to 10 Response.Write "<tr><td>Counting " & iCtr & "" Next %> This technique has shown measurable and significant performance improvements.
  • Are you buffering output?
Buffering is on by default in Windows 2000, but may be off if you've upgraded from Internet Information Server (IIS) 4.0. When buffering is on, ASP will wait until processing is complete before sending down the response, reducing network roundtrips and server-processing delays. When buffering is off, ASP waits for TCP acknowledgements from clients, which can really hurt performance, particularly over slow connections. Note that while buffering may improve throughput, it may reduce perceived performance. If perceived performance is an issue, you can turn off buffering with Response.Buffer = False or you can call Response.Flush.
  • Are you using Session state?
ASP Sessions are a convenient facility, but they limit scalability. Sessions limit the scalability of a single box because they consume resources for each user. While Session size is largely determined by what you stuff into the Session variable, the real cost is resource contention. Sessions also limit your application's ability to scale out across multiple machines, because each Session is Web-server specific. In many cases, you can avoid the use of Session objects by using hidden form fields or passing data in your QueryStrings (see Q175167 "HOWTO: Persisting Values Without Sessions" ). These approaches will allow you to scale out but may become more difficult to code as the structure of the data grows in complexity. Another possibility is to use a database. This is suitable for more complex structures, such as a shopping cart. You can store your shopping cart in a database and look it up as required. Many large commerce sites take this approach by transmitting a unique ID to the user and looking up information from the database when needed. This approach will allow you to scale out transparently. It will also let your Web servers scale to more users, and it will make the carts persistent between requests. Many developers overlook this approach, because they believe the cost of reconnecting to the database will hurt performance, but that's where pooling comes into play (more on pooling below).
  • Have you disabled Session state if you aren't using it?
If the application doesn't rely on Sessions, disable Session state for the Web or virtual directory through the ISM (Internet Services Manager). Disabling Session state allows ASP to skip an extensive amount of source code, reducing overhead. If you need Session state enabled for you site, you can prevent ASP from unnecessarily checking for Session information on specific pages by using the following page declarative: @EnableSessionState = False You might do this on pages that use frames. ASP serializes concurrent requests from the same session, causing ASP pages in frames to load sequentially. If you turn off Sessions with @EnableSessionState, frames will load concurrently.
  • Are you using third-party components designed for ASP?
Ask your third-party component vendor whether the component was both designed and tested for ASP. "Troubleshooting Components Under ASP Technology" provides some guidelines for ASP compliance.
  • Are you using some form of caching for data?
Caching is one of the most difficult aspects of Web application development, because it threatens the scalability of your application. Determining what to cache is based on data volatility and scope. Data that is static or used application-wide can be a good candidate for caching. Data that changes frequently or is user-specific would not be a good candidate. Determining where in your application you should cache your data is based on your application goals, and you need to know the trade-offs. For example, caching an ActiveX® Data Objects (ADO) Recordset offers flexibility, because you can still grab an array or save the recordset as XML. However, if your application will be rendering the same HTML repeatedly, such as to display a list box of countries, you might cache the HTML string in Application scope, rather than just the data. XML has certainly opened up new doors in terms of caching flexibility. For example, you might store data as XML strings and apply XSL transformation as required. For a more thorough investigation of this topic, see Paul Enfield's article "Using Server-Side XSL for Early Rendering: Generating Frequently Accessed Data-Driven Web Pages in Advance." A good stress test early in your design can help you determine which caching technique to use.
  • Are you using CDO?
If you're using Windows NT® 4.0, use CDO-NTS and NT SP 5 or later (see Q214685 XFOR: CDONTS Not Thread-Safe, Crashes Under Stress). Collaboration Data Objects (CDO) was not tested for server-side use, except in the case of Outlook Web Access. In Windows 2000, use "CDO for Windows 2000, CDOSYS.dll, when possible, which is designed for server-side development. See the following articles:
  • "Collaboration Data Objects Roadmap"
  • Q195683 "INFO: Relationship between 1.x CDO Libraries and CDOSYS.DLL"
  • Q177850 "INFO: What is the Difference Between CDO 1.2 and CDONTS?"
  • Are you redimensioning arrays?
It is generally more expensive to redimension arrays than it is to grab more than you need up front. Redimensioning arrays requires Visual Basic Scripting Edition (VBScript) to allocate space for the new array -- and, if you've used the Preserve modifier to preserve the contents of the array, to copy the data from the old array into the new. This means that not only are you spending extra processor cycles to redimension the array, but the process initially requires twice as much memory for the copy. However, if you allocate more space than you need initially (you need only five elements initially, but allocate space for 128), then adding more data to the array requires VBScript to insert only the new values into the existing array.
  • Are you using multiple languages on a page?
Multiple languages on a given page mean multiple script engines for that page. Script engines use Thread Local Storage (TLS), so multiple threads cannot use an instance of a script engine concurrently. Therefore, five simultaneous requests to the same page will cause ASP to instantiate five script engines. More engines means more overhead, so you may be able to gain some performance by limiting your number of languages used on a given page.
  • Are you checking Response.IsClientConnected before processing long routines?
By testing Response.IsClientConnected your application can avoid wasting CPU cycles by quitting methods if the client is no longer connected. Note that IIS 5.0 overcomes a limitation in IIS 4.0 (the need to send content to the browser before checking the property). For sample code, refer to Q182892 "HOWTO: Use IsClientConnected to Check If Browser is Connected."
  • Are you using Server.MapPath unnecessarily?
When you request Server.MapPath, you are generating an additional request for the server to process. To improve performance, replace Server.MapPath with a fully qualified path when deploying your Web site.
  • Are you parsing strings?
Use regular expressions in validation routines, in formatting functions, and instead of looping through strings. See "String Manipulation and Pattern Testing with Regular Expressions" for practical advise on using regular expressions.
  • Are you using the same object many times?
VBScript 5.0 provides the With statement. The With statement allows you to execute a series of statements on a specific object without requalifying the name of the object. For more information, see the Windows Script Technologies Web site . Does the global.asa contain empty Session_OnStart or Session_OnEnd methods? Stripping out empty Session events reduces the amount of source code that ASP must traverse, and improves performance.

Component Performance

  • Are you storing your objects in Session or Application scope?

Storing references to objects in ASP's Session or Application scope will cause many performance and scalability issues if those objects aren't designed to be shared across threads or activities. Only agile components -- or, in Windows 2000, components marked Neutral -- can be referenced in Session or Application variables with direct access by client threads. Components are considered agile if they are marked Both and aggregate the free-threaded marshaler (FTM). See "Agility in Server Components" for a discussion on how to aggregate the FTM. Components with other threading models impose restrictions when stored in Session or Application scope.

As mentioned earlier, Visual Basic or other STA components should only be used within page scope. Note that solidly written Visual Basic components can perform extremely well if you follow this rule. Single- and Free-threaded components are not recommended, because of security issues and expensive proxy/stubs that get created when marshaling across apartment boundaries.

See the following articles for additional information:

  • "ASP Component Guidelines"
  • Q243544 "INFO: Component Threading Model Summary Under ASP"
  • Q150777 "INFO: Descriptions and Workings of OLE Threading Models"


  • Are you concatenating strings in components?

Use fixed-length strings in Visual Basic for string concatenation. Don't just keep adding to a string, or you'll reallocate it multiple times -- and reallocation is expensive. Bad:

  Public Function BadConcatenation() As String
   Dim intLoop As Integer
   Dim strTemp As String      'this will be expensive
       
   For intLoop = 1 To 1000
       strTemp = strTemp & "<tr><td>Counting "
       strTemp = strTemp & CStr(i)
strTemp = strTemp & "" Next intLoop BadConcatenation = strTemp End Function Good: Public Function GoodConcatenation() As String Dim intCtr As Integer Dim intLoop As Integer Dim strTemp As String * 32000 'this improves performance intCtr = 1 For intLoop = 1 To 1000 Mid(strTemp, intCtr) = "<tr><td>Counting " intCtr = intCtr + 17 Mid(strTemp, intCtr) = CStr(intLoop) intCtr = intCtr + Len(CStr(intLoop)) Mid(strTemp, intCtr) = "" intCtr = intCtr + 10 Next intLoop GoodConcatenation = strTemp End Function Several large-scale sites found major performance gains from this tip alone. If you've got extensive string concatenation in your application, put this technique high on your priority list. Experiment with different string sizes, and test to see which size will work best for your particular routine.
  • Are you using SQL Server for your middle-tier cache?
If you need to cache data that is read frequently and seldom updated in the middle-tier, use SQL Server rather than a roll-your-own solution. SQL Server provides high-performance middle-tier caching. For more information, read "Middle-Tier High-Speed Data Caching Involving COM/MTS and COM+."
  • Are you using transactions when you don't need to?
Transactions provide a service -- and that service can add a performance hit. Evaluate whether methods actually need transactions. For example, if you're grabbing a recordset to hand off to a browser client for reading data, you don't need a transaction. By factoring out operations that read data into separate components from operations that perform updates, you have more flexibility in marking your components for transactions. The following articles provide scalable patterns that you can use as guidelines to help you factor out your own components:
  • "Scalable Design Patterns"
  • "Simplify MTS Apps with a Design Pattern"
  • "FMStocks Application: Start Here"
  • Are you calling SetComplete/SetAbort in each method?
Calling SetComplete and SetAbort in each method of your Microsoft Transaction Server (MTS) components will release resources earlier, and will ensure that a component does not live outside the scope of its transaction. In Windows 2000, COM+ provides the setting 'Automatically deactivate this object when this method returns,' which performs the equivalent code. You can enable this setting on a per-method basis in the Component Services console. This performance gain is more applicable to large-scale sites, but is also good form.
  • Are you creating child MTS components with CreateInstance?
In Windows NT 4.0, use CreateInstance to create child MTS components. Create non-MTS components using CreateObject or New (unless the components are in the same Visual Basic project). Other combinations cause code paths and checks that could otherwise be avoided. For more information, see Ted Pattison's article "Creating Objects Properly in an MTS App." In Windows 2000, this distinction between creating objects with different techniques goes away, and you simply use CreateObject (see Q250865 "INFO: CreateObject and CreateInstance Have the Same Effect in COM+" ).
  • Are your components in Server packages or Library packages?
Library packages run in their caller's process, while components in a Server package run in a new process. As such, Library packages do not have the cross-process performance hit that Server packages have. A typical recommendation is to run the Web application out of process, but have the components in a Library package to provide both process isolation and high performance. In situations where you need to use a Server package, as when you are calling remote MTS/COM+ components, take steps to minimize cross process overhead.
  • Are you crossing processes effectively?
Minimizing marshaling overhead and reducing network calls are keys to improving performance of distributed applications. Use early binding in your components to minimize expensive network round trips by eliminating the extra call to GetIdsOfNames that late binding incurs. You can further reduce network trips by bundling your parameters into arguments for method calls, instead of setting a bunch of properties individually. Rather than pass parameters ByRef, pass ByVal where you can to minimize marshaling overhead.
  • Are you using remote components?
Many developers find themselves in situations where they need to call remote components. All of the rules for crossing processes effectively apply. An additional recommendation is to use an intermediary package (see Q159311 "Instantiating Remote Components in MTS and IIS" ). Using an intermediary package to call the remote component avoids some security complications and allows you to use early binding.
  • Do your client and server machines use the same protocols in DCOM?
A common cause of activation delays of remote components results from the client and server machines having different DCOM protocol lists. Use DCOMCNFG.EXE on each machine to match the protocol sequence. For example, if both machines are using TCP/IP, move TCP/IP to the top of the list on both machines. A few guidelines apply when modifying your DCOM protocol list:
  • Move TCP/IP to the top when you can.
  • Remove protocols that you don't need.
  • Any changes to the protocol list require a reboot.
  • Are you using ASP built-in objects from a remote component?
Don't. Even if you can figure out how to marshal the ASP built-in objects (Request, Response, and so forth) across machines, the performance cost outweighs any benefits.
  • Are you using Visual Basic?
Visual Basic performs extremely well when you have the right version, use the right settings and follow good design guidelines:
  • Use Visual Basic 6.0 Service Pack 3.
  • Set Unattended Execution and Retain in Memory in your projects (See Figure below). See Q186273 "BUG: AV Running VB-Built Component in Multi-Threaded Environment" for additional information.
  • Read Q243548 "INFO: Design Guidelines for VB Components Under ASP" to avoid many common development mistakes.
  • Are you using MFC?
Active Template Library (ATL) components are lighter than Microsoft Foundation Classes (MFC) and are preferred for server components. MFC code is heavy for the server and may bring about unexpected serialization for state management. Also, you cannot create components marked Both that aggregate the FTM. MFC only produces STA objects, which are limited to page scope.
  • Are you using Java?
Make sure you have the latest Virtual Machine from http://www.microsoft.com/java/ and read Q232368 "PRB: Java Threads Blocking when Accessing COM Objects."
  • Are you waiting for stuff that could be done asynchronously?
If your ASP application is making long database calls or calling components that are waiting on other components to raise events, consider making the call asynchronous through a queueing mechanism, such as MSMQ. For information on MSMQ, see the following:
  • Q173339 "HOWTO: Use MSMQ from an ASP Page"
  • Q181839 "Mqasp.exe MSMQ Basic Queue Operations Using IIS/ASP"
  • Q243546 "PRB: ASP Does Not Support Events"
  • Are you looping through large datasets in the middle tier?
Push more of these large or complex data operations to stored procedures where you can.

Data Access Performance

  • Are you using indexes in your database?

Indexes provide immediate impact on your application's performance. Poor indexes will slow your application to a crawl, while good indexes will help optimize your application's performance. For information on tuning indexes, see "Top Ten Tips: Accessing SQL Through ADO and ASP." or SQL Books Online.


  • Are you calling stored procedures rather than dynamic SQL?

Using stored procedures prevents your database from having to recompile your SQL statements repeatedly. Use stored procedures or parameterized SQL strings.


  • Are you returning just the required data?

Check your SELECT statements to ensure that you're returning only the required columns and only the necessary rows. If you have queries that can potentially return a lot of records, consider paging through your recordsets. See the following articles for more information:

  • "Recordset Paging with ADO 2.0"
  • "Ad Hoc Web Reporting with ADO 2.0"
  • "6 Ways to Boost ADO Application Performance"


  • Are you using DAO or RDO?

Remote Data Objects (RDO) and Data Access Objects (DAO) are intended for a single-client application process, and weren't tested for the Web. ADO is designed and tested for Web use. Which version of MDAC are you using?

Updated versions of MDAC provide improved reliability and performance. You should be using MDAC version 2.1 Service Pack 2 or later. MDAC 2.5 is recommended, because it's the most stable and it's been tested extensively. If you don't know which version you have on your box, you can grab the Component Checker tool from http://www.microsoft.com/data/ .


  • Are you following MDAC best practices?

See "Improve MDAC Application Performance."


  • Are you pooling connections?

Pooling allows you to reuse the effort of connecting to a database. ODBC Connection pooling is on by default in MDAC 2.0. In MDAC 2.1 or later, OleDb Session pooling is the default. Remember that in order for pooling to work, the user name, password, and resource in the connection string need to match (it's a byte-by-byte comparison).

See the following articles for additional information on pooling:

  • "Pooling in the Microsoft Data Access Components"
  • Q169470 "INFO: Frequently Asked Questions About ODBC Connection Pooling"
  • Q187874 "CnPool.exe Test Connection Pooling with Tempdb Objects"
  • Q191572 "INFO: Connection Pool Management by ADO Objects Called From ASP"


  • Are you storing ADO connection in Session or Application scope?

This defeats the purpose of connection pooling and creates resource contention. Create connection at page scope or within the functions that need them, and set the connections to nothing to free the connection back to the pool.


Are you explicitly closing Recordset and Connection variables?

Recordsets need to be closed if they are going to be reused (but reusing recordsets is discouraged). Closing Connection variables as soon as you can releases them back to the pool, so that they can be pooled for reuse. It is always good practice to explicitly close your object variables.


  • Are you reusing Recordset and Command variables?

Create new Recordset and Command variables rather than reusing existing ones. This won't necessarily improve your application's performance but it will make your application more reliable and easier to maintain. See Q197449 "PRB: Problems Reusing ADO Command Object on Multiple Recordsets" for more information.

  • Are you disconnecting the recordsets?

Disconnecting recordsets frees the Connection object back to the pool, allowing the Connection to be closed and reused sooner. Are you using the right cursor and lock-type for the job?

Use "Firehose" (forward-only, read-only) cursors when you need to make a single pass through the data. Firehose cursors, the default in ADO, provide the fastest performance and have the least amount of overhead. See ADO documentation for more information on cursor and lock types.

  • Are you using DSN-less connections?

In general, DSN-less connections are faster than System DSNs (data source names), which are faster than File DSNs. Are you using Access?

Microsoft Access is a file-based database, so don't expect it to perform well with concurrent users under IIS. Are you using SQL Server?

Use SQL Server 7.0. SQL Server 7.0 is superior to earlier versions of SQL Server, and provides row-level locking, as well as other performance benefits. You've heard SQL scales -- but for proof, see http://www.tpc.org/ , and read Jim Gray's paper at http://research.microsoft.com/scalable/ .


  • Are you using TCP/IP for your network library?

Use TCP/IP for your network library for performance and scalability.


  • Are you using the OLEDB SQL provider?

The SQLOLEDB, the SQL provider, is recommended over MSDASQL, the OLEDB provider for ODBC for performance and reliability. Are you using Oracle?

Oracle performance really depends on a combination of the right MDAC bits with the correct Oracle client patches. If you're using Oracle, take the time to read the following articles:

  • "Microsoft OLE DB Provider for Oracle: Tips, Tricks, and Traps"
  • "Fitch & Mather Stocks: Data Access Layer for Oracle 8"

If you're using Oracle and MTS, be sure to review the following articles:

  • Q193893 "INFO: Using Oracle Databases with Microsoft Transaction Server"
  • Q191168 "INFO: Failed to Enlist on Calling Object's Transaction"


  • Are you using the same database for reporting and transactions?

Many large Web sites maintain separate databases for read-only and transactional data as an effective way to boost performance. This has the added benefit of allowing you to design your database schema to be optimized for reporting.

IIS Settings

  • Is ASP debugging enabled?

Check the ISM. If ASP debugging is enabled, the application is locked down to a single thread of execution. See Q216580 "PRB: Blocking/Serialization When Using InProc Component (DLL) from ASP."


  • Is ASP configured to have enough threads/script engines?

Read "The Art and Science of Web Server Tuning with Internet Information Services 5.0" and "Navigating the Maze of Settings for Web Server Performance Optimization."


  • Are you using SSL?

Secure Sockets Layer (SSL) is expensive in terms of bandwidth and CPU usage. If you're using SSL, it's because of security needs. Your best bet is to restrict SSL usage to where you need it, and keep the pages simple.

Stress Testing

It's a common misconception that performance equals scalability. Performance for ASP means the rate at which pages can be served. Scalability is measured by how much performance degrades under additional load. To put these terms in perspective, your ASP application may perform well with 10 users, but does not scale to 1000 users, because performance becomes unacceptable. Use stress testing to measure the performance and scalability of your ASP application. For more information on scalability of WinDNA applications, see "A Blueprint for Building Web Sites Using the Microsoft Windows DNA Platform."


  • What are your performance expectations?

Performance can be measured in terms of the number of ASP requests per second that your servers can handle. Using ASP's performance counter, ASP Requests Per Second, you can set benchmarks to measure against.


  • Are you trying to stress test with a browser?

Stress testing with a browser may be throwing off your results. As mentioned earlier, ASP will serialize concurrent requests from the same Session. For example, if Cookies are enabled and you're hitting the server from one machine, then those requests will serialize. Use the Web Application Stress tool (WAS -- formerly known as Homer). For an introduction to WAS, see "I Can't Stress It Enough -- Load Test Your ASP Application."


  • Have you performed end-to-end testing?

While it's important and feasible to stress test your database, components, and ASP layers separately, end to end testing is how you'll find your application's real bottlenecks.

Conclusion

Reviewing ASP applications for performance means taking a look at several things. By breaking the application down into its various layers, you can build a framework for analyzing performance issues. While there are many guidelines and recommendations, nothing replaces stress testing your application and using sound judgment.

J.D. Meier was born and reared on the U.S. East Coast. Since heeding Horace Greeley's advice, he has worked as a Developer Support engineer specializing in server-side components and Windows DNA applications involving MTS and ASP technology.