|
APGen Examples |
|
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 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.
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.
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:
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.
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.
and
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:
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:
<%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%>
<%#
' global.inc
'
' File that defines global application variables
Dim g_sConnectionString
g_sConnectionString = "DSN=aw_equip"
#%>
Output.Filename = "Equip_" & sEquipmentType & ".asp"
In the original ASP application, a URL likehttp://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 indefault.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)
#%>
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:
Since the script that #includes navbar2.inc (equip.apg) already #include global.inc, navbar2.inc does not have to #include global.inc.
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
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:
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:
<%#
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"
#%>
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:
<%#
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"
Response.Write "</TR><TR><TD HEIGHT=10></TD></TR>" 'break the row
becameOutput.Write "</TR><TR><TD HEIGHT=10></TD></TR>" 'break the row
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
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>
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>
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: