红俊's profile蝈蝈俊的共享空间PhotosBlogListsMore Tools Help

Blog


    August 26

    企业服务性能优化之使用 COM 可封送的参数

    企业服务优化原则中有下面一条原则,最近我优化一个企业服务时,对此有了特别深刻的体验,特整理本篇博客:

    使用 COM 可封送的参数
    如果企业服务组件的方法接受调用方传递数据时所用的参数,强烈建议您尽量使用 COM 和 .NET 之间易于封送的类型,例如:

    • Boolean
    • Byte、SByte
    • Char
    • DateTime
    • Decimal
    • Single、Double
    • Guid
    • Int16、UInt16、Int32、UInt32、Int64、UInt64
    • IntPtr、UIntPtr
    • String

    如果仅使用这些类型,且要避免传递其他复杂的类型(例如 structures 或 arrays),.NET 序列化程序可以优化调用处理堆栈,并将调用直接序列化到线缆上(对于 RPC)或到虚拟线缆上(对于 LRPC)。这样,调用的执行速度更快。不过,如果您的方法要求复杂的类型,代码将使用通常的 DCOM 调用堆栈调用,这将产生额外的处理。

    以上文字来自: .NET 企业服务性能 

    我最近优化的这个企业服务是CSDN新积分制论坛帖子列表企业服务缓存。

    这个帖子列表缓存工作机制如下:

    1、启动一个 ActivationOption.Server 的企业服务(ServicedComponent),这个企业服务是工作在一个独立的服务程序进程。

    2、这个企业服务中,通过单件模式启用了一个缓存,缓存了CSDN每个大小论坛的帖子列表。

    3、当WEB应用启动时候,把每个论坛,每种帖子列表的帖子都从数据库中获得,然后记录到缓存中。方便以后使用。

    4、在优化前,这个版本的新积分制论坛并不是所有CSDN的论坛,有100左右的大小论坛,每个大小论坛又有6种帖子列表,每个帖子列表最多显示满足条件的500条记录。这样,初始化数据大致就需要 100*6*500 大致20万左右的数据需要初始化到帖子列表企业服务缓存。

    下面是我做测试的三种情况:

    1、优化前我代码逻辑:每一个论坛做一次初始化操作,也就是100个论坛调用企业服务的一个方法100次。

    这样的代码逻辑,本地测试环境,完成初始化大致需要130秒左右。

    2、我把这个初始化的代码,即从数据库获得并写到缓存的代码搬到企业服务内部执行,外部只需要传递一个简单的论坛编号即可。这样优化后,这个初始化过程大致需要40秒。

    3、另外一种方法,我把100个论坛所有帖子列表的数据库都在企业服务外部计算好,然后一次性提交给企业服务,即企业服务这个初始化方法只调用一次,完成这个初始化大致需要190秒左右。

    对比我做的上面三个测试情况,我们可以看到,业务逻辑没有发生任何变化,变化的只是企业服务接口参数发生变化了,把一些工作从企业服务外搬到了企业服务内部执行。三种情况对企业服务来说,差别就在于企业服务的参数发生变化了。

    上面情况2的企业服务参数最简单,用的是Guid 类型的参数。

    情况1和情况3用的是自定义的可序列化的类。

    情况1企业服务调用了100次,情况3企业服务只调用了1次。

    结论:

    1、企业服务的参数要尽量使用 COM 和 .NET 之间易于封送的类型,而不是自己定义的实体和传递其他复杂的类型。

    2、企业服务使用自己封装的实体或者复杂的类型,带来的性能损耗,比多次调用企业带来的损耗更厉害(对比情况1和情况3)

    参考资料:

    .NET 企业服务性能

    August 24

    通过编程来让企业服务以服务程序方式运行

    我们要在这篇文章中将实现的功能:

    企业服务配置作为NT Service 来启动

    如上图荧光笔画的地方所示:

    我们需要通过编写服务程序的安装程序,把企业服务上面 Run application as NT Service 选择框的进行选中操作。

     

    阅读本文基础:

    如果你对企业服务不是很了解,可以阅读我之前写的一系列跟企业服务有关的博客。我书写的跟企业服务有关的系列文章可以访问以下链接。

    http://blog.joycode.com/ghj/category/1320.aspx

    如果你对编程控制企业服务根本不了解,需要首先阅读我之前写的这篇博客:

    编程控制企业服务的行为

    本文是在上述文章基础上的加深。

     

    正文

    编程控制企业服务的行为 这篇博客中我讲到,我们可以通过遍历 COMAdminCatalogCollection 来寻找到我们需要操作企业服务的某个属性,然后更新这个属性。这种做法适用于大多数编程操作企业服务属性。但是我们上面这个需求就无法用这种方式来作了。

    我们遍历 COMAdminCatalogCollection 可以更新的属性时候,我们在微软提供的可操作属性列表(如下链接可以看到)中并没有找到可以设置 Run application as NT Service 的选项。

    http://msdn2.microsoft.com/en-us/library/ms686107.aspx

    只能看到一个可能跟这个需求有关的属性:

    ServiceName

    The service name corresponding to the application configured to run as an NT service. If this value is NULL, the application is not configured to run as an NT service. Otherwise, the configuration information for the service can be found by using the service name.

    Access:    ReadOnly
    Type:        String
    Default:    ""
    Platform Requirements:   Windows XP, Windows Server 2003

    这个属性是只读的,我们又没法设置。

     

    如何解决这个问题呢?

    很简单,这个需求不是通过属性来指定的,而是通过 COMAdminCatalog 类的 CreateServiceForApplication 方法。

    简单来说,我们就是要安装代码中有以下代码:

    COMAdmin.COMAdminCatalog ca = new COMAdmin.COMAdminCatalogClass();

    ca.CreateServiceForApplication(ApplicationName, NTServerName, "SERVICE_DEMAND_START", "SERVICE_ERROR_NORMAL", "", null, null, false);

     

    CreateServiceForApplication 函数的定义如下:

    HRESULT CreateServiceForApplication(
      BSTR bstrApplicationIDOrName,
      BSTR bstrServiceName,
      BSTR bstrStartType,
      BSTR bstrErrorControl,
      BSTR bstrDependencies,
      BSTR bstrRunAs,
      BSTR bstrPassword,
      VARIANT_BOOL bDesktopOk);

    每个参数介绍如下:

    bstrApplicationIDOrName 

    应用ID或者应用名字,我们安装这个企业服务后,在服务程序列表中,这个值就是我们看到的 NT Service 的Name 就是这个值。

    bstrServiceName

    服务名字,我们安装这个企业服务后,在服务程序列表中,这个值就是我们看到的 NT Service 的 Description 就是这个值。

    bstrStartType

    服务开始的几种情况,这里可以是下面几个值

    SERVICE_BOOT_START, SERVICE_SYSTEM_START, SERVICE_AUTO_START, SERVICE_DEMAND_START, and SERVICE_DISABLED.

    bstrErrorControl

    服务错误发生时的情况,可以是以下几个值

    SERVICE_ERROR_IGNORE, SERVICE_ERROR_NORMAL, SERVICE_ERROR_SEVERE, and SERVICE_ERROR_CRITICAL.

    其他几个参数一般都比较固定,我直接Copy MSDN的说明。

    bstrDependencies
    [in] A list of dependencies for the service. There are two possible formats for the string: a standard null-delimited, double-null-terminated string (exactly as documented for CreateService); or a script-friendly list of service names separated by "\" (an invalid character to have in a service name). The rpcss service is implicit in this parameter and does not need to be specified.
    bstrRunAs
    [in] The user name to run this service as. This may be NULL to indicate that it should run as Local Service.
    bstrPassword
    [in] The password for the system user account. This must be NULL if the service is configured to run as Local Service.
    bDesktopOk
    [in] Indicates whether or not the service should be allowed to interact with the desktop. This parameter is valid only when the service is marked as Local Service and must be FALSE otherwise.

     

    参考资料:

    Register your Enterprise Service App as NT Service

    ICOMAdminCatalog2::CreateServiceForApplication

    August 17

    使用Grid来对WPF页面进行布局排版

           对于以前用 Windows Form 来开发客户端程序的程序员,在使用 WPF 开发客户端的程序时,在窗体布局上将是他必须面对的一个坎。

           布局产生困惑的一个典型场景如下:

           我们在开发WPF窗体时候,我们会发现,当我们把菜单控件(Menu)、工具条(ToolBar、ToolBarPanel)、状态条(StatusBar)这些最常见的页面元素拖动到WPF窗体的时候。我们会发现 WPF 窗体中,这些页面元素可以放置在任何位置,而不是之前 Windows Form 那样:主菜单在最上面,状态条在最下面。

           WPF中每一个元素如何布局变得更加灵活了,这样可以让美工更好的设计出更漂亮的页面,但是也会让一些缺乏艺术细胞的技术人员页面布局变得巨难看无比。比如我最近在写个简单的调查系统客户端维护工具,使用WPF程序来开发,这个页面布局的事情,就让我非常头大。

           WPF 跟布局有关的控件很多,System.Windows.Controls.Panel 是这些所有布局有关的类的基类。需要注意的是,我们在页面布局上一般都是使用这个类的扩展类来处理布局,而不是使用这个类。这些扩展类包括:

    System.Windows.Controls.Canvas  (画布)
    System.Windows.Controls.DockPanel  (停靠布局)
    System.Windows.Controls.Grid    (表格)
    System.Windows.Controls.StackPanel   (堆栈布局)
    System.Windows.Controls.VirtualizingPanel  (虚堆栈布局)
    System.Windows.Controls.WrapPanel  (覆盖布局)

           我自己在使用中觉得:对于我们从Window Form 习惯来的技术人员,使用 Grid    (表格) 布局就可以满足我们绝大多数的布局需求,而且简单。下面我们就来介绍如何使用 Grid 布局控件来进行窗体布局设计。

           Grid 布局控件很类似 HTML 标签中的 Table 标签。我们事先把一个区域划分成不同的表格,然后决定,某些控件放在那个表格中,那些控件是要跨多少个表格来放置。比如下面窗体效果,是由后面的XAML文件来实现这个效果的。

    WPF窗体的一个效果图

    这个窗口布局上,我使用了Grid控件

    我把这个窗口首先划分成三行三列的一个Grid。如上图我对这个窗体的拆分。

    最上面的显示多少条目,以及下拉列表框,以及刷新按钮,被放在第一行,跨三列

    未发布的调查表列表框被放在第二行,第一列,

    支持来回拖动的GridSplitter被我放在了第二列,第二行

    ListView 则被我放在了第二行,第三列

    最下面的确定,取消按钮被我放在了第三行,跨了三个列

    上述界面效果的XAML文件如下:

     

    <Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Class="HongjunGuo.SurveySystem.Client.QuestionnaireList"
        Title="调查列表" Height="300" Width="500">
        <Grid >
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="150"/>
                <ColumnDefinition Width="5" />
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="35"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="35"/>
            </Grid.RowDefinitions>

            <DockPanel Height="30" Name="dockPanel2" Grid.ColumnSpan="3" Grid.Row="0">
                <Label>显示多少条目:</Label>
                <ComboBox Height="21" Name="comboBox1" Width="120" >
                    <ComboBoxItem IsSelected="True" >5</ComboBoxItem>
                    <ComboBoxItem>10</ComboBoxItem>
                    <ComboBoxItem>20</ComboBoxItem>
                    <ComboBoxItem>50</ComboBoxItem>
                </ComboBox>
                <Button Height="23" Name="button1" Width="75">刷新</Button>
            </DockPanel>

            <ListBox IsSynchronizedWithCurrentItem="True" Name="lb_Type"  Width="Auto" Grid.Column="0" Grid.Row="1"  >
                <ListBoxItem Name="lbi_Draft" ToolTip="最近使用的,并且没有被发布的调查表" Content="本地未发布的调查表草稿" Height="50" IsSelected="True"/>
                <ListBoxItem Name="lbi_Release" Content="本地最近发布的调查表" Height="50"/>
                <ListBoxItem Name="lbi_Seatch" Content="服务器上的调查列表" Height="50"/>
            </ListBox>
            <GridSplitter Grid.Column="1" Grid.Row="1" HorizontalAlignment="Left" Name="gridSplitter1" Width="10" />
            <ListView Name="lv_Data" IsSynchronizedWithCurrentItem="True" Grid.Column="3" Grid.Row="1">
                <ListView.View>
                    <GridView>
                        <GridViewColumn Header="编号">
                        </GridViewColumn>
                        <GridViewColumn Header="标题">
                        </GridViewColumn>
                        <GridViewColumn Header="创建时间">
                        </GridViewColumn>
                    </GridView>
                </ListView.View>
            </ListView>

            <DockPanel Height="30" Name="dockPanel4"  Grid.ColumnSpan="3" Grid.Row="2">
                <Button Height="23" Name="btn_OK" Width="75" IsDefault="True" Click="btn_OK_Click">确 定</Button>

                <Button Height="23" Name="btn_Cancel" Width="75" IsCancel="True">取 消</Button>
            </DockPanel>
        </Grid>
    </Window>

    使用Grid布局控件的时候,一些知识点如下:

    我们可以通过定义Grid的ColumnDifinitions和RowDifinitions来实现对于表格的定义,然后根据Grid.Column和Grid.Row的对象来制定位置的方式实现布局。

    比如上面XAML文件中

        <Grid >
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="150"/>
                <ColumnDefinition Width="5" />
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="35"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="35"/>
            </Grid.RowDefinitions>

    就定义了一个三行三列的表格。

    ColumnDefinition 和 RowDefinition 分别只需要定义 Width 和 Height

    如果我们希望列的宽度或者行的高度是根据内部元素来决定的,我们可以定义为 Auto, 如果我们希望某列或者某行的宽度或者告诉是整体的其他部分,则可以定义成 *,如果我们希望其中一项的长度是另外一项的5倍,则可以一个定义成*,一个定义成5* 。

    我们看 ColumnDefinition  或者 RowDefinition  的 Width 或者 Height 属性的时候,我们可以看到这个属性不是 int或者 double 类型,而是 GridLength 类型。

    下面一个简单的Grid定义来演示上面定义长度的几种写法

    <Window x:Class="WpfApplication1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="300" Width="300">
        <Grid >
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="30"/>
                <ColumnDefinition Width="auto" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="2*" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>

            <Label Grid.Column="0" Grid.Row="0" Background="RosyBrown"  ></Label>
            <Label Grid.Column="1" Grid.Row="0" Background="SkyBlue"  >1234567890</Label>
            <Label Grid.Column="2" Grid.Row="0" Background="Red"  ></Label>

            <Label Grid.Column="0" Grid.Row="1" Background="SpringGreen"  ></Label>
            <Label Grid.Column="1" Grid.Row="1" Background="RoyalBlue"  >abc</Label>
            <Label Grid.Column="2" Grid.Row="1" Background="Violet"  ></Label>

        </Grid>
    </Window>

    这个XAML文件的效果图如下:

    使用Grid进行布局

    此外,我们还可以使用 Grid.ColumnSpan Grid.RowSpan 来实现一块布局跨多个表格项的情况。

     

    小结:

    我个人觉得,把一个窗体或者页面用表格拆分,然后我们在每个表格项中增加我们规划好的控件,这种布局方案在没有比较好的美术细胞下,比较容易做出一个至少不难看的页面布局。

    基于以上的考虑,我觉得我们技术人员开发一些WPF窗体或者页面的时候,Grid控件应该是我们最常用到的。也应该是最应该掌握的控件。

     

    参考资料:

    WPF基本版面布局(精简版)

    .Net Framework3.0 实践纪实(1)

    August 02

    UBB解析优化的心得:Regex构造函数的性能

    昨天和今天,我都在对我之前写的UBB解析代码进行性能优化。优化的结果是:1个具有600多个UBB标签的文本,包含多层UBB嵌套,优化前,解析出这个文本需要2分钟,优化后解析出这个文本需要1秒钟。而这次优化,核心优化的技术只有一点:正则表达式Regex 的构造位置发生变化。下面我就来慢慢来说这次优化。

    UBB解析组件的简单介绍

    需求:

    1、把支持的14个UBB标签解析成不同的Html文本。这14个标签包含:代码高亮标签、禁用UBB标签以及一些通用的UBB标签。

    2、一部分UBB 标签支持嵌套的解析,比如对以下文本的解析: [b]1[i]2[/i]3[/b] ,要求2这个文本,需要解析成加粗同时是斜体;

    3、一部分UBB标签不支持嵌套的解析,比如:代码高亮的UBB标签括的范围内,任何UBB标签都不起作用。

    当然,还有很多其他需求限制,这里只罗列影响我UBB解析算法的一些重要需求。我写的这个UBB代码解析的规范,可以参看以下链接:http://forum.csdn.net/help/ubb.html

     

    我的设计:

    先把一段包含UBB标签的文本解析成一个树,树的每一个末梢节点都是不能再继续拆分下去的一段文本,即:其下没有起作用的嵌套UBB标签。然后把这个树的每个节点解析内容合并成一段新的文本。

    这个算法的瓶颈在把文本解析成树,解析成树后的计算,系统消耗很少,可以忽略不计。

    解析成树的算法,我的设计如下:

    先在这个文本中,使用正则表达式从头开始找起,找到第一个系统支持的UBB标签,比如我们找到了一个[b] 文本。然后从找到位置开始,向后,找 [/b] 文本,这两个寻找都是使用的正则来寻找,根据这两个寻找的三种结果,分别进行处理.

    然后再用递归算法,不停的循环上述处理逻辑,从而把文本解析成树。

     

    我的代码优化

    优化前性能不高的代码:

    // 在一段文本中,从指定位置开始,找到系统支持的UBB标签文本,比如之前的例子,找 [b]  [i] 这些文本

    private bool MatchBeginTag(int beginPos, out UBBCodeFragmentType ubbType, out string ubbParameterValue, out int tagPrePos, out int tagEndPos)
     {
        ......
        Regex rx_MatchBeginTag = new Regex(@"\[(?<tagName>[a-zA-Z]+)(=(?<value>[^\f\n\r\t\v\]]*))?\]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
        ......
    }

    // 从指定位置开始,向后 找指定标签的结束标签

    private bool MatchEndTag(int beginPos, string tagName, out int tagPrePos, out int tagEndPos)
     {
        ......
        Regex rx_MatchEndTag = new Regex(@"\[/" + tagName + @"\]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
        ......
    }

    上述两个函数分别实现之前说的两个功能,这两个函数会被频繁的递归调用,比如我之前说的场景,600多个UBB标签的文本,这两个函数会被600次的调用到。

     

    我的优化方法

    我通过使用 JetBrains dotTrace 3.0 工具,看到 Regex 的构造函数被频繁的调用,累计调用花费的时间非常巨大,我在这里对它进行代码调整.

    对于 MatchBeginTag 函数, 由于它用的 Regex rx_MatchBeginTag 是固定的,很简单,我把这个对象放在函数体之外,把它定义成静态成员,这样它只需要构造一次,改造成如下代码方式:

    private static Regex rx_MatchBeginTag = new Regex(@"\[(?<tagName>[a-zA-Z]+)(=(?<value>[^\f\n\r\t\v\]]*))?\]", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    这一个的改造工作,让我在600多个UBB文本的解析时间从2分钟下降到12秒钟.

     

    对于 MatchEndTag 函数体内的 Regex ,这个是动态构造的,显然不能用前面的这个方法。使用一个静态Regex 对象来记录。

    我的做法是,建立一个 Dictionary<UBBCodeFragmentType, Regex> ht_EndTagRegexArray,这个结构中,存储了系统支持的14个UBB标签对应的正则表达式构建的静态Regex 对象。在这个类被第一使用的时候,上述14个Regex 对象被构造,之后不用再构造,直接使用。

    这样的改造工作后,让我在600多个UBB文本解析的时间,从上一个优化结果12秒变成了1秒钟。

     

    当然我还作了其他优化的工作,但是这些其他的优化工作的结果并不明显。可以一笔带过。

     

    分析:

    我们优化前代码是在递归中使用 new Regex 。

    这样,我们创建的每一个 Regex 对象都没有过生命周期,更不可能被GC释放了,同时并存600个Regex 。就是不考虑构造的花费,这个并存的花费都是非常惊人的。更不用说构造的花费了。

    结论:

    一定要避免频繁的 new Regex 对象,这个过程很耗资源。

     

    参考资料:

     

    正则表达式编译

    Regex Class Caching Changes between .NET Framework 1.1 and .NET Framework 2.0 [Josh Free]