APGen Documentation Previous Topic: APGen Examples Next Topic: Web Authoring Examples Parent Topic: APGen Examples    APGen Examples
AdventureWorks E-Commerce Example
See Also:

This example demonstrates how to optimize an ASP online store using Active Page Generator.  For general information on how to optimize an ASP web application, see Web Site Optimization Techniques.

The Examples

The code shown here is from two separate applications: The original ASP-only application (awe_asp); and the finished application, an APGen-optimized ASP application (awe_apg).  This topic shows the process of converting an ASP-only application to an APGen-optimized application using the guidelines discussed in Web Site Optimization Techniques.  The ASP-only application code is found in

Program Files\APGen\Examples\awe_asp\

and the APGen application code is found in

Program Files\APGen\Examples\awe_apg\

The "awe" prefix in the directory names stands for "Adventure Works Equipment".  The ASP-only application was adapted from the Adventure Works example site shipped with ASP 1.0.  The online store pages were extracted from the rest of the site, and a few improvements were made to the ASP code.  The improvements include:

You can view the ASP-only online store at http://localhost/apgen/awe_asp/.  This code simulates an online store, albeit a very simple online store.  The store allows you to browse and purchase items, and even recognizes a return shopper (using a cookie).

You can view the APGen optimized online store at http://localhost/apgen/awe_apg/.  The finished store behaves exactly like the original store, but is dramatically faster and more scalable.

Motivation

This ASP-only online store is a perfect candidate for APGen optimization.  The product information is rendered for each visitor to the site, yet the product information has not changed (or changes infrequently).  For example, say user A views the backpacks page.  Then user B views the backpacks page.  In both cases, the database is queried, and the page is built.  But, the database has not changed since user A visited the page, so building the page again for user B is wasted work.  Multiply this inefficiency by thousands of users; the end result is a waste of web server and database resources.

APGen provides an optimal solution to inefficient web pages:  APGen allows you to run script and render content independent of HTTP requests.  There is no need for cache-checking code or cache invalidation, and there are no cache misses.  Using APGen, the work of rendering product information can be performed once: When the product database changes.  This results in huge performance improvements, as shown in the following graphs.

Note: These graphs show figures for an older system running Windows NT 4.0.  The optimized ASPs and static HTML perform significantly better under Windows 2000.

Graph 1: Capacity


Graph 2: Latency

Graph 1 shows the server capacity improvements (measured in maximum pages served per minute), and Graph 2 shows the latency improvements (average time from client HTTP Request to receiving a HTTP Response).  These results were compiled using Microsoft InetMonitor, using a test scenario that simulated 20 simultaneous users browsing the store.  For the maximum capacity tests, the users were hitting pages as quickly as possible, so the 20 "users" were behaving like 1,000+ real users.

Graph 1 also shows the results that can be obtained if the pages are made 100% static.  This store can not be entirely static, because there are shopping carts and customers to deal with - these pages require ASP code.  The static page results are shown for comparison.  In addition to creating almost no server load, static pages can be cached on the client or across the net, which reduces the bandwidth consumed per user.

The Optimization Process

We began by copying all the ASP files to a new directory (/awe_apg/).  Then, we changed all image links to point to the /awe_asp/ virtual directory.  We did this so you can compare the unoptimized site with the optimized site, without taking up extra disk space by having two identical sets of images.  Changing all the image links required that "./images/" be replaced with "../awe_asp/images/" in all .asp and .inc files.  This path fixup for <img> tags is not normally necessary.

Since both sites share the same database, the connection string (which contains the path of the Access MDB file) was changed in global.asa.

The following conversion steps are from How to Optimize ASPs Using Two-Phase Content Generation:

Step 1: Identify Optimizable Blocks

The most critical part of the optimization process is to identify the optimizable blocks.  See Guidelines for Writing Two-Phase APG Scripts for help with this process.

Optimizing the Adventure Works store follows from 2 key observations: Many of the catalog pages query the product database and use the results to render content to a web page.  And, the product database does not change very often.  So, the catalog pages are performing too much repetitive work, and therefore should be optimized using APGen.

At the same time, there are blocks of script that need to be run by the web server, because they are dependent on the individual user.  For example, the shopping cart, checkout, and customer information forms all need to remain as ASP code.

The pages were optimized as follows:

global.asa Not optimizable - required for shopping cart initialization.  Leave unchanged.
catalog_type.asp Optimizable - Since there are multiple output combinations, page fragment caching is used for this page.  This page is covered later.
check_out.asp Not optimizable - displays and allows editing of shopping cart contents for individual shoppers.
congratulations.asp Optimizable, though optimization does not add significant performance gains, since the page is mostly static to start with.  The only optimizable code is to move the server-side navbar include to an APG include.  This change makes Navbar.inc optimizable.
default.asp Optimizable, though optimization does not add significant performance gains, since the page is mostly static.  The only optimizable code is to move the server-side navbar include to an APG include.  This change makes Navbar.inc optimizable.
equip.asp Optimizable - all the ASP script that renders product information is converted to APG script.  Since there are only three output combinations, two-phase content generation is used instead of page fragment caching.  Shopping cart code needs to remain as ASP script.
GetCustomer.asp Not optimizable - performs customer information data entry for individual shoppers.
shipping.asp Not optimizable - performs payment and shipping data entry for individual shoppers.
Cart.inc Not optimizable - contains constants that are used by the shopping cart ASP code.
Navbar.inc Optimizable - The dynamic border around a graphic (used to show which page you are on) is optimizable.  The check-out graphic, which is only displayed when the user has items in their shopping cart, is not optimizable.
Navbar2.inc Optimizable - queries the product database, uses that information to render a navbar.  All the code should be converted to APG script.

Now that we have identified which pages and which blocks of code are optimizable, it is time to start the conversion.

Step 2: Copy optimizeable files to a /apg directory and rename the files to use the .apg extension.

We copied files congratulations.asp, default.asp, equip.asp, navbar.inc, and navbar2.inc from the awe_apg directory to an awe_apg/apg subdirectory.  Then we renamed all .asp extensions to .apg.  We left the .inc files with a .inc extension, even though they are going to be converted from ASP include files to APG script include files.

Since catalog_type.asp will be optimized using page fragment caching (and not two-phase content generation), it was not copied to the /apg directory.  Catalog_type.asp will be discussed later.

We put the APG scripts in a single subdirectory, named "apg".  The output ASP files will be placed in the script's parent directory ("..").  In a real-world solution, you may not want to have the APG scripts in a subdirectory of the directory containing the ASP files.  Someone could accidentally view your APG scripts, if you fail to remove web server permissions for the apg directory.

Step 3: Convert optimizable ASP blocks to APG blocks, and set the Output.Filename

and

Step 4: Change ASP object references within APG script to their APGen equivalent

In these steps we reviewed the APG scripts, and converted the optimizable ASP blocks to APG script blocks.  We converted the script brackets (append a "#" after "<%", and insert a "#" before "%>") for the optimizable blocks.  Then, modifications were made to preserve all functionality using APG script. 

Congratulations.apg and Default.apg

Since these two files were mostly static to begin with, the only changes were: 

  1. Convert script brackets around "Option Explicit"
  2. Change include file to an APG include
  3. Set the Output.Filename

Congratulations.apg generates congratulations.asp, and default.apg generates default.asp.  So computing the output filename is easily accomplished by adding:

Output.Filename = "Congratulations.asp"

to the beginning of congratulations.apg.  The same technique is used for default.apg.

Equip.apg

Equip.asp displays a series of products that are extracted from the database.  The changes made were:

  1. Convert all script blocks to APG script except the ASP block that decides whether or not to draw the "Checkout" graphic:

    <%If Session("ItemCount") > 0 Then%>
         <A HREF="./check_out.asp">
         <IMG src="img/../awe_asp/images/checkout.gif" WIDTH="85" HEIGHT="45" ALT="Check Out" BORDER=O></A>
    <%End If%>

    This block is dependent on data stored in the ASP Session object.  This block displays a graphic if the user has 1 or more items in their shopping cart, so it needs to remain as an ASP block.
  2. Change the "EquipmentType" parameter from being passed in by Request.QueryString to being passed in using a script argument.
  3. Use g_sConnectionString from global.inc instead of Session("ConnectionString") to obtain the DSN.  This can be done because Session("ConnectionString") is not a user-dependent value - it is the same for all users.

    We added file global.inc to the /apg/ directory:

    <%#
    ' global.inc
    '
    ' File that defines global application variables

    Dim g_sConnectionString
    g_sConnectionString = "DSN=aw_equip"

    #%>

  4. Change Server.CreateObject to Script.CreateObject.
  5. Change the include file to an APG include file.
  6. Change Response.Write to Output.Write.
  7. Set the Output.Filename.  The generated filename uses the sEquipmentType string since there are 3 equipment types (and 3 different output combinations):

    Output.Filename = "Equip_" & sEquipmentType & ".asp"

    In the original ASP application, a URL like

    http://localhost/apgen/awe_asp/equip.asp?EquipmentType=Climbing

    was used to display the catalog page for rock climbing shoes.  In our optimized application, the URL becomes:

    http://localhost/apgen/awe_apg/equip_Climbing.asp

    Note that the links to this page in default.asp need to be fixed accordingly.

The original ASP block, which determines the sEquipmentType value, and creates the ADODB.Connection object:

<%
Option Explicit

Dim sEquipmentType, sEquipmentTypeTitle, sTitleGIF

sEquipmentType = Request.QueryString("EquipmentType")
Select Case sEquipmentType
Case "Camping"
     sEquipmentTypeTitle = "Camping Equipment"
     sTitleGIF = "camping"
Case "Climbing"
     sEquipmentTypeTitle = "Climbing Equipment"
     sTitleGIF = "climbing"
Case "Clothing"
     sEquipmentTypeTitle = "Clothing"
     sTitleGIF = "clothing"
Case Else
     Response.Redirect("./default.asp")
End Select

' The queries to return the best selling items by equipment type
' are encapsulated in stored procedures (or "querydefs" as they are called
' in Microsoft Access).  There is one stored procedure for each equipment
' category.  The stored procedure name to invoke is the EquipmentType
' plus the name "TopSales"
Dim cn, sSQL, rs
Set cn = Server.CreateObject("ADODB.Connection")
sSQL = "{call " & sEquipmentType & "TopSales}"
cn.Open Session("ConnectionString")
Set rs = cn.Execute(sSQL)
%>

was converted to:

<%#
Option Explicit

Dim sEquipmentType, sEquipmentTypeTitle, sTitleGIF

' Equipment type should be stored in the APG Param
sEquipmentType = CStr(Script.Arguments(0))
Select Case sEquipmentType
Case "Camping"
     sEquipmentTypeTitle = "Camping Equipment"
     sTitleGIF = "camping"
Case "Climbing"
     sEquipmentTypeTitle = "Climbing Equipment"
     sTitleGIF = "climbing"
Case "Clothing"
     sEquipmentTypeTitle = "Clothing"
     sTitleGIF = "clothing"
Case Else
     Log.Write "Invalid Equipment Type Param in equip.apg", apgSeverityError
     Script.Abort
End Select

Output.Filename = "Equip_" & sEquipmentType & ".asp"

' The queries to return the best selling items by equipment type
' are encapsulated in stored procedures (or "querydefs" as they are called
' in Microsoft Access).  There is one stored procedure for each equipment
' category.  The stored procedure name to invoke is the EquipmentType
' plus the name "TopSales"
#%><!-- #include apg="global.inc" --><%#
Dim cn, sSQL, rs
Set cn = Script.CreateObject("ADODB.Connection")
sSQL = "{call " & sEquipmentType & "TopSales}"
cn.Open g_sConnectionString
Set rs = cn.Execute(sSQL)
#%>

Step 5: Optimize #include files

Navbar.inc and Navbar2.inc were moved to the /apg directory, and modified as follows:

Navbar.inc

Like catalog_type.asp and equip.apg, navbar.inc contains an ASP block that displays a "checkout" image if the user has more than one item the shopping cart.  This block must remain as ASP script, since the block is dependent on the individual user.

Navbar.inc also contains some script that computes a border around a navbar graphic to indicate that you are already at that page.  The original ASP code is:

<%
     Dim sScriptName
     sScriptName = Request.ServerVariables("SCRIPT_NAME")
     If StrComp(Mid(sScriptName, InStrRev(sScriptName, "/")+1), _
                    "default.asp", vbTextCompare) = 0 Then
          Response.Write("2")
     Else
          Response.Write("0")
     End If
%>

The ASP code uses Request.ServerVariables and some parsing code to isolate the name of the current file.  While APGen does not have an equivalent term to Request.ServerVariables, you can use Output.Filename to return the name of the output file.  The converted APG code is:

<%#
     If StrComp(Output.Filename, "default.asp", vbTextCompare) = 0 Then
          Output.Write("2")
     Else
          Output.Write("0")
     End If
#%>

Navbar2.inc

Navbar2.inc can be completely converted to APG script.  The three changes consist of:

  1. Convert ASP script brackets to APG script brackets.
  2. Convert Server.CreateObject to Script.CreateObject
  3. Convert Session("ConnectionString") to g_sConnectionString.

Since the script that #includes navbar2.inc (equip.apg) already #include global.inc, navbar2.inc does not have to #include global.inc.

Step 6: Write a build script/web page

Once individual web pages are optimized, a way to execute the first phase (execute the APG scripts) needs to be created.  Build scripts, or build web pages, can be written to execute all the necessary APG scripts.  They make building and rebuilding the web site very easy.

The awe_apg example includes both a build script (apg\build.apg) and a build web page (build.asp).  They show two different ways to build the site: The build script can be run by a local administrator or by other software.  The build web page can be run by a remote administrator.  The code for both is essentially the same, but build.asp includes ASP reporting code so the remote administrator can view whether all pages were built and if any errors occurred.

The security settings for build.asp are set to NT Authentication (anonymous user access is disabled).  This ensures that only administrators are able to view/run the build page.

As mentioned earlier, the APG scripts are contained in a subdirectory named "apg".  The output files need to be placed in the parent directory.  This is accomplished with the following code:

APGen.OutputDir = ".."

If you wanted to place all the output files in a different directory, change the line to:

APGen.OutputDir = "C:\Inetpub\wwwroot\awe_apg"

Now that we have set the output directory, it is time to run a few of the scripts.  Default.apg and congratulations.apg do not take any parameters, so here is the code for running these scripts:

'
' build default.apg, only 1 page
'

APGen.Run "default.apg"

...
'
' build congratulations.apg, only 1 page
'

APGen.Run "congratulations.apg"

Equip.apg requires arguments.  If equip.apg is run without arguments, an error will occur.  Since there are three different equipment types, we create an array and loop through the array:

'
' build equip.apg, once for each equipment type (3 types)
'

Dim rgEquipTypes, sEquipType
rgEquipTypes = Array("Camping", "Climbing", "Clothing")

For Each sEquipType In rgEquipTypes
     APGen.RunArgs "equip.apg", sEquipType
Next

Page Fragment Caching in Catalog_type.asp

Catalog_type.asp was optimized using page fragment caching.  The decision to use page fragment caching instead of two-phase content generation was based on the fact that catalog_type.asp has numerous output combinations (12 to be exact).  For pages with numerous output combinations, page fragment caching offers more efficient CPU and memory utilization.

Page fragments are static, but they are included in a dynamic ASP page.  Two page fragments were broken out of the original catalog_type.asp page - a navbar fragment and a body fragment.  Since the navbar and body portions of the page vary for each product type, there are 2 page fragments per product type.  The page fragments are stored in the /producttypes/ directory.  For example, when catalog_type.asp is viewed with ProductType=Boot, fragments /awe_apg/producttypes/navbar2_boot.htm and /awe_apg/producttypes/catalog_type_boot.htm are used.

Let's walk through the conversion according to the steps in How to Implement Page Fragment Caching:

Step 1: Write APG scripts that generate static page fragments

Two fragments were broken out of catalog_type.asp - we chose to write APG scripts /apg/navbar2_frag.apg and /apg/catalog_type_frag.apg.  When each fragment script is run, the producttype string is passed in as the first argument.

Navbar2_frag.apg

The /apg/navbar2_frag.apg fragment generates the same content as the original navbar2.inc.  To convert navbar2.inc to navbar2_frag.apg, we did the following:

  1. Wrote initializer script to initialize variables and create the appropriate output file:

    <%#
    Option Explicit

    #%><!-- #include apg="global.inc" --><%#

    Dim sProductType
    sProductType = CStr(Script.Arguments(0))
    Output.CanCreateDirs = True
    Output.Dir = APGen.OutputDir & "producttypes\"
    Output.FileName = "navbar2_" & sProductType & ".htm"

    #%>

  2. Converted ASP script brackets to APG script brackets.
  3. Converted Server.CreateObject to Script.CreateObject
  4. Converted Session("ConnectionString") to g_sConnectionString from global.inc.

Catalog_type_frag.apg

The /apg/catalog_type_frag.apg fragment was extracted from the body of catalog_type.asp, then the following modifications were made:

  1. Wrote initializer script to initialize variables and create the appropriate output file:

    <%#
    Option Explicit

    #%><!-- #include apg="global.inc" --><%#

    Dim sProductType
    sProductType = CStr(Script.Arguments(0))
    Output.CanCreateDirs = True
    Output.Dir = APGen.OutputDir & "producttypes\"
    Output.FileName = "catalog_type_" & sProductType & ".htm"

  2. Converted ASP script brackets to APG script brackets.
  3. Converted Server.CreateOjbect to Script.CreateObject.
  4. Converted Session("ConnectionString") to g_sConnectionString from global.inc.
  5. Converted Response.Write to Output.Write:

              Response.Write "</TR><TR><TD HEIGHT=10></TD></TR>" 'break the row

    became

              Output.Write "</TR><TR><TD HEIGHT=10></TD></TR>" 'break the row

Step 2: Write code to execute the page fragment scripts

The next step in page fragment caching for catalog_type.asp is to execute the page fragment scripts.  This is easily done in build.apg and build.asp by looping through the producttypes, and executing the page fragment scripts once for each product type:

' build the catalog_type.asp page fragments
'
' These are generated by catalog_type_frag.apg and navbar2_frag.apg.
' Run each of these scripts once for each product type.
'

Dim cn, rsProductTypes, sSQL, oScript, oScriptNav
Set cn = CreateObject("ADODB.Connection")
cn.Open g_sConnectionString
sSQL = "SELECT DISTINCT ProductType FROM Products"
Set rsProductTypes = cn.Execute(sSQL)
Set oScript = APGen.OpenScript(Script.Dir & "catalog_type_frag.apg")
Set oScriptNav = APGen.OpenScript(Script.Dir & "navbar2_frag.apg")
Do While Not rsProductTypes.EOF
     oScript.RunArgs rsProductTypes("ProductType")
     oScriptNav.RunArgs rsProductTypes("ProductType")
     rsProductTypes.MoveNext
Loop

Set rsProductTypes = Nothing

Step 3: Instantiate an ASPCache object in global.asa

To instantiate an Application-wide instance of the ASPCache component, this line was added to global.asa:

<OBJECT RUNAT=Server SCOPE=Application ID=Cache PROGID=ASPCache></OBJECT>

Step 4: Include the page fragments in the host page

The new code for catalog_type.asp uses Cache.WriteFile() to output the page fragments for the navbar and the body:

<%
Option Explicit

Dim sProductType
sProductType = Request.QueryString("ProductType")
If sProductType = "" Then
     Response.Redirect("default.asp")
End If
%>

. . .

<!--Begin Navigational Buttons-->

<TR>
<TD ROWSPAN=5 ALIGN=LEFT VALIGN=TOP>
<%     ' Insert the navbar fragment for this product
If Not Cache.WriteFile( "producttypes\navbar2_" & sProductType & ".htm", _
                               True, True, 0, Response ) Then
     Response.Redirect("default.asp")
End If
%>
<BR>

<!-- Display the "checkout" image if the user has
     any items in their shopping basket -->
<%If Session("ItemCount") > 0 Then%>
     <A HREF="./check_out.asp">
     <IMG SRC="../awe_asp/images/checkout.gif" WIDTH="85" HEIGHT="45" ALT="Check Out" BORDER=O></A>
<%End If%>

. . .

<%     ' Insert the body fragment for this product
If Not Cache.WriteFile( "producttypes\catalog_type_" & sProductType & ".htm", _
                               True, True, 0, Response ) Then
     Response.Redirect("default.asp")
End If
%>

</TABLE>
</BODY>
</HTML>

Taking it Further

You can run performance tests to see how much faster the optimized site is compared to the unoptimized version.  The graphs shown earlier in this article summarize the performance results on an older server running Windows NT 4.0.  Those results show that web site capacity was improved by 768%, and average page execution time was reduced from 244.96 to 14.17 milliseconds.

In this example, you have to manually run build.apg or view build.asp whenever the product information changes (such as new products or price changes).  This may not be acceptable for production applications.  Options for improving the update process include: