技术开发 频道

ASP.NET性能提升之UI加速和存储过程

 【IT168 专稿】

  让UI加载速度更快

  在无需回传的情况下,基于AJAX的网站几乎都是尽可能地加载很多特性到浏览器内。如果你看下Pageflakes站点的起始页,就会发现在不发生回传的情况下,仅仅一个单一的页面就加载整个应用程序的所有特性。

  第一篇:ASP.NET性能提升秘诀之管道与进程优化

  第二篇:ASP.NET性能提升之站点部署与内容传输

  第三篇:ASP.NET性能提升之浏览器缓存的调用

  实现这一功能的一个快速方法就是在页面加载期间,传递尽可能多的HTML片段到隐藏的DIVs中,然后在需要的时候使这些DIVs可见。但是这会使得第一次加载变得非常慢并且会导致浏览器处理大量信息而降低性能。因此,一个较好的方法就是按需加载HTML片段以及一些必须的JavaScript代码。在我的dropthings工程中,我展示了一个如何实现这一功能的示例。



         
  当你点击“帮助”链接时,系统会动态地加载帮助内容。这段HTML并不是从default.aspx 页面的一部分派生而来呈现第一个页面的。因此,在站点加载性能上,大量的HTML和相关的图像对于帮助节没有影响。仅当用户点击“帮助”链接时进行加载。此外,内容会缓存到浏览器上,因此仅仅加载一次即可。当用户再次点击“帮助”链接时,系统会直接从浏览器缓存中提取内容,而不是再次从源服务器上去读取数据。

  其原理就是对调用的*.aspx 页面发出XMLHTTP,再获得响应的HTML,将响应的HTML放置到一个DIV容器中,使得DIV可见。

  AJAX框架有一个 Sys.Net.WebRequest类,你可以使用它进行规则的HTTP调用。你可以定义HTTP方法,URI,调用的头和主体。通过XMLHTTP直接调用适合于一些低级别的功能。一旦你构建了一个Web请求,你就可以使用Sys.Net.XMLHttpExecutor执行。

function showHelp()
{
   var request
= new Sys.Net.WebRequest();
   request.set_httpVerb(
"GET");
   request.set_url(
'help.aspx');
   request.add_completed( function( executor )
   {
      
if (executor.get_responseAvailable())
      {

         var helpDiv
= $get('HelpDiv');
         var helpLink
= $get('HelpLink');
         var helpLinkBounds
= Sys.UI.DomElement.getBounds(helpLink);

         helpDiv.style.top
= (helpLinkBounds.y + helpLinkBounds.height) + "px";
         var content
= executor.get_responseData();
         helpDiv.innerHTML
= content;
         helpDiv.style.display
= "block";                      

      }
   });

   var executor
= new Sys.Net.XMLHttpExecutor();
   request.set_executor(executor);
   executor.executeRequest();
}

  这个例子展示了如何通过点击help.aspx页面并注入自身响应到HelpDiv中来进行加载的方法。该响应可以通过设置页面help.aspx.的输出缓存指令来进行缓存。因此,下次当用户点击该连接时,UI就会立即弹出来。help.aspx文件没有<html>块,仅仅只是将内容设置到DIV中了。

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Help.aspx.cs"
    Inherits
="Help" %>
<%@ OutputCache Location="ServerAndClient" Duration="604800" VaryByParam="none" %>
<div class="helpContent">
<div id="lipsum">
<p>
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Duis lorem
eros, volutpat sit amet, venenatis vitae, condimentum at, dolor. Nunc
porttitor eleifend tellus. Praesent vitae neque ut mi rutrum cursus.

  使用这种方法,你可以把UI分解成更多小的*.aspx 文件。虽然这些*.aspx 文件不能包涵有JavaScript或者stylesheet块,但是它们可以包涵大量你需要显示到UI上的HTML。因此,你可以保持只下载加载所需要的基本信息。当用户在站点上浏览新的特性时,就会立即加载这些区域。

  优化 ASP.NET 2.0 Profile Provider 
 
  你可知道在ASP.NET 2.0 Profile Provider中有两个能进行优化的重要存储过程吗?如果在没有进行任何必要优化的情况下使用过它们,你的服务器将会因为业务量的增长而变得异常繁忙。这里有一个故事:

  在2006年3月份的MIX大会上展示了Pageflakes。当时我们是我们最富有魅力的时候。我们是作为支持Showcase of Atlas Web site的第一家公司。每天访问站点的用户数量接连攀升。有一天我们注意到,数据库服务器不再工作了。然后我们重新启动了服务器,工作恢复了正常,可过了一小时后,服务器再次死掉了。在我们对服务器主体部分进行了检查分析之后,我们发现CPU占用率高达100%并且IO使用率更高。

  硬盘驱动器发热,并进行了自动关闭以保护其不受损坏。这对我们来说感到十分惊奇,因为我们原以为我们一直都很聪明,并且针对每个单独的Web服务功能都使用了profile。因此,我们对上百兆的日志进行了分析希望能找到那个Web服务耗费了这么多时间。我们对其中一个产生了怀凝。它是加载用户页面配置的第一个功能。我们将这个功能分解成了很多小的部分以便我们能快速找到那一部分花费了大部分时间。

private GetPageflake(string source, string pageID, string userUniqueName)
{
  
if( Profile.IsAnonymous )
  {
  
using (new TimedLog(Profile.UserName,"GetPageflake"))
  {

  正如你所看到的情形,整个方法主体就是用于计时。如果你想了解这种计时是如何工作的,我会在一篇新的文章中进行解释。我们也对其中一小部分我们怀凝最耗费资源的功能进行了计时。但是在我们的代码中需要花费大量时间处理的部分很多。我们的代码一直都是经过优化的(毕竟,你知道是谁在查看它,就是我)。

  同时,用户开始了大叫,管理也开始混乱,支持部门的员工也开始抱怨这么多电话。开发人员搞得焦头乱额此时也变得胡言乱语。这并不是什么特殊情况,仅仅就是一个每个月会遇到2次的一个典型解决方案。

  现在你一定在大声叫喊了,“你可以使用SQL Profiler啊,你这个傻瓜!”。问题是我们使用的是SQL Server工作组版本。不支持SQL Profiler这个功能。因此我们不得不采用我们的方法来解决这个问题,无论怎样也得使其在服务器上正常运行。不要问这个到底如何实现。在运行了SQL Profiler以后,孩子,真让我们吃惊!原来才是这个巨大的存储过程dbo.aspnet_Profile_GetProfiles给我们带来了痛苦!

  我们习惯大量使用(并且一直使用)Profile provider这个工具。

  该存储过程如下: 

CREATE PROCEDURE [dbo].[aspnet_Profile_GetProfiles]
  
@ApplicationName nvarchar(256),
    
@ProfileAuthOptions int,
    
@PageIndex  int,
    
@PageSize   int,
    
@UserNameToMatch   nvarchar(256) = NULL,
    
@InactiveSinceDate datetime = NULL
AS
BEGIN
    
DECLARE @ApplicationId uniqueidentifier
    
SELECT @ApplicationId = NULL
    
SELECT @ApplicationId = ApplicationId
                
FROM aspnet_Applications
                    
WHERE LOWER(@ApplicationName)
                            
= LoweredApplicationName
    
  
IF (@ApplicationId IS NULL)
        
RETURN

  
-- Set the page bounds
    DECLARE @PageLowerBound int
    
DECLARE @PageUpperBound int
    
DECLARE @TotalRecords   int
  
SET @PageLowerBound = @PageSize * @PageIndex
  
SET @PageUpperBound = @PageSize - 1 + @PageLowerBound

    
-- Create a temp table TO store the select results
    CREATE TABLE #PageIndexForUsers
    (
      IndexId
int IDENTITY (0, 1) NOT NULL,
      UserId
uniqueidentifier
    )

    
-- Insert into our temp table
   INSERT INTO #PageIndexForUsers (UserId)
      
      
SELECT u.UserId
        
FROM    dbo.aspnet_Users
            u, dbo.aspnet_Profile p
      
WHERE   ApplicationId = @ApplicationId
            
AND u.UserId = p.UserId      
                
AND (@InactiveSinceDate
                
IS NULL OR LastActivityDate
                        
<= @InactiveSinceDate)
                
AND (  
                    (
@ProfileAuthOptions = 2)
                
OR (@ProfileAuthOptions = 0
                        
AND IsAnonymous = 1)
                
OR (@ProfileAuthOptions = 1
                        
AND IsAnonymous = 0)
                    )
                
AND (@UserNameToMatch
                
IS NULL OR LoweredUserName
                    
LIKE LOWER(@UserNameToMatch))
        
ORDER BY UserName

    
SELECT u.UserName, u.IsAnonymous, u.LastActivityDate,
      p.LastUpdatedDate,
DATALENGTH(p.PropertyNames)
      
+ DATALENGTH(p.PropertyValuesString)
      
+ DATALENGTH(p.PropertyValuesBinary)
    
FROM    dbo.aspnet_Users
                    u, dbo.aspnet_Profile p, #PageIndexForUsers i
    
WHERE  
      u.UserId
= p.UserId
      
AND p.UserId = i.UserId
      
AND i.IndexId >= @PageLowerBound
      
AND i.IndexId <= @PageUpperBound

    
DROP TABLE #PageIndexForUsers

    
END
END

 

  首先查找 ApplicationID. 

   DECLARE @ApplicationId  uniqueidentifier

    
SELECT @ApplicationId = NULL

  
SELECT @ApplicationId = ApplicationId FROM aspnet_Applications
  
WHERE LOWER(@ApplicationName) = LoweredApplicationName

  
IF (@ApplicationId IS NULL)
      
RETURN

  然后创建一个临时表(应该使用表数据类型)以便存储用户的profiles信息。

    -- Create a temp table TO store the select results
    CREATE TABLE #PageIndexForUsers
    (
        IndexId
int IDENTITY (0, 1) NOT NULL,
      UserId
uniqueidentifier
    )
  
-- Insert into our temp table
    INSERT INTO #PageIndexForUsers (UserId)

  如果这个调用非常频繁,由于临时表的创建这会变得很高的IO吞吐量。它的运行也会贯穿两个大表――aspnet_Users和aspnet_Profile。采用这种方式编写的存储过程,如果一个用户有多个profiles,那么它会返回用户的所有profiles。但是正常情况下,我们会为每个用户存储一个profile。因此,这里不需要创建临时表。

  此外,也不需要做像LIKE LOWER(@UserNameToMatch)这样的操作。我们也总是使用equal操作符来判断两个直接匹配的用户名。
因此,我们打开这个存储过程并做了一些如下调查: 

IF @UserNameToMatch IS NOT NULL
BEGIN
        
SELECT u.UserName, u.IsAnonymous, u.LastActivityDate, p.LastUpdatedDate,
      
DATALENGTH(p.PropertyNames)
        
+ DATALENGTH(p.PropertyValuesString) + DATALENGTH(p.PropertyValuesBinary)
      
FROM    dbo.aspnet_Users u
        
INNER JOIN dbo.aspnet_Profile p ON u.UserId = p.UserId
      
WHERE u.LoweredUserName = LOWER(@UserNameToMatch)
        
SELECT @@ROWCOUNT
END
ELSE
    
BEGIN -- Do the original bad things

  在本地运行得很好。现在是该放到服务器上去运行的时候了。该存储过程是ASP.NET 2.0 Profile Provider使用的重要存储过程,也是ASP.NET框架的核心。如果我们在这里做了些错误的处理,虽然我们可能不会立即看到这个问题,但是可能一个月后我们会意识到用户的profile发生了混乱并且没有办法返回了。因此,在没有做好充分测试的情况下,直接将其部署到产品服务器上是一项艰难的决定。但是我们并没有足够的时间来做测试。我们已经关闭了服务器。因此,我们聚集在一起,心理默默祈祷并点击了在SQL Server Management Studio 上的“执行”按钮。

  这个存储过程运行得很好。在服务器上我们注意到CPU使用率从100%降低到了30%。IO使用率也下降到了40%。

  我们再次活过来了!

  这里的另一个存储过程在每个页面加载和web服务调用我们的站点时进行调用,因为我们大量使用了Profile provider。

CREATE PROCEDURE [dbo].[aspnet_Profile_GetProperties]
    
@ApplicationName   nvarchar(256),
    
@UserName  nvarchar(256),
    
@CurrentTimeUtc  datetime

AS
BEGIN
    
DECLARE @ApplicationId uniqueidentifier
    
SELECT @ApplicationId = NULL
    
SELECT @ApplicationId = ApplicationId
                
FROM dbo.aspnet_Applications
                        
WHERE LOWER(@ApplicationName) = LoweredApplicationName

    
IF (@ApplicationId IS NULL)
        
RETURN

    
DECLARE @UserId uniqueidentifier
    
SELECT @UserId = NULL

    
SELECT @UserId = UserId
    
FROM   dbo.aspnet_Users
    
WHERE ApplicationId = @ApplicationId
                
AND LoweredUserName =
                        
LOWER(@UserName)
    
IF (@UserId IS NULL)
        
RETURN
    
SELECT TOP 1 PropertyNames, PropertyValuesString, PropertyValuesBinary
    
FROM         dbo.aspnet_Profile
    
WHERE        UserId = @UserId
    
IF (@@ROWCOUNT > 0)
    
BEGIN
        
UPDATE dbo.aspnet_Users
        
SET    LastActivityDate=@CurrentTimeUtc
        
WHERE UserId = @UserId
    
END
END

  当运行这个存储过程时,看下面的统计: 

Table 'aspnet_Applications'. Scan count 1, logical reads 2, physical reads 0,
                        
read-ahead reads 0, lob logical reads 0, lob physical
                            reads
0, lob read-ahead reads 0.
(
1 row(s) affected)
Table 'aspnet_Users'. Scan count 1, logical reads 4, physical reads 0,
                        
read-ahead reads 0, lob logical reads 0, lob physical
                            reads
0, lob read-ahead reads 0.

(
1 row(s) affected)
(
1 row(s) affected)
Table 'aspnet_Profile'. Scan count 0, logical reads 3, physical reads 0,
                        
read-ahead reads 0, lob logical reads 0, lob physical
                            reads
0, lob read-ahead reads 0.
(
1 row(s) affected)
Table 'aspnet_Users'. Scan count 0, logical reads 27, physical reads 0,
                        
read-ahead reads 0, lob logical reads 0, lob physical
                            reads
0, lob read-ahead reads 0.
(
1 row(s) affected)
(
1 row(s) affected)

  该存储过程操作绑定了所有客户属性的Profile 对象。在第一次请求过程中,无论什么时候该对象都会被访问。

  首先,它会通过SELECT 表aspnet_application根据应用程序的名称找出应用程序的ID。你可以方便的在SP中将应用程序ID进行硬编码来取代这个方法,而且这样做也会节约很多时间。正常情况下,我们仅仅会在我们的产品服务器上运行一个应用程序。因此,不需要为每次单独的调用查找应用程序ID。这是一种快速优化方法。然而,从客户端统计来看,你可以看到真实的性能瓶颈在那里:


 
  然后看看最后一个块,其中 aspnet_users表的LastActivityDate字段被更新了。这是耗费资源最昂贵的一个。


 
  这样做是为了确保Profile provider记住什么时候是用户的profile被最后一次访问。我们不需要在每个单独页面加载以及Web服务调用Profile对象时都这样做。我们可以在用户第一次登录和注销时这么做。这样仅仅只是一个页面而已。因此,我们可以方便地删除它以便节约更多的开销来确保在每个单独Web服务调用时需要更新的大表aspnet_users。

0
相关文章