内容


应用程序性能调优,第 2 部分

Comments

在这个由两部分组成的关于应用程序性能调优的系列的第 1 部分中,我们谈到了会影响应用程序性能的数据库、视图和表单属性。现在,您也许已经启用或禁用了一些属性,并且获得了某种程度的性能提升。但是您还可以做更多的事情来提高应用程序的性能。在这个系列的第 2 部分中,我们将关注能提高性能的编程实践。我们还将考察一些常见的 LotusScript 方法,考察在不同条件下哪些方法可以带来最佳性能。本文假设您是一名有经验的 Notes/Domino 应用程序开发人员,并且熟悉 LotusScript。

前端和后端的编程

说到编程,首先让我们简单地区分一下我们所说的前端代码和后端代码。前端代码是指从应用程序的用户界面(UI)执行的代码,例如手动地从 Actions 菜单运行一个代理。与这类代码有关的问题比较容易诊断,因为可以通过手动测试代码何时运行得好,何时运行得不好,立即识别出问题所在。与前端代码相关的性能问题常常和字段、按钮、代理等某种可以改变的 UI 元素有关。第一篇文章中曾提到,一个好的测试环境可以帮助您找出问题。

后端代码是指在后台执行的代码。这包括按时调度的代理,这些代理有时候会导致与系统性能相关的问题。例如,您可能有一个按时调度的代理,负责消除来自一个数据库的保存/复制冲突,您可能发现,这个代理执行任务所花费的时间不正常,或者无节制地消耗系统资源。为测试这个后端代码,可以查找日志中的偏差,例如超时完成任务的失误。 如果所测试的后端代码是一个代理,那么检查代理日志,以发现代理执行任务的情况。

至此,我们已经区分了前端代码和后端代码,现在让我们看看一些同时影响这两种代码的常见编程错误。

临时变量

在编程过程中,开发人员最常犯的一个错误是,对于检索起来代价较高的数据,没有使用临时变量来作为这种数据的占位符(placeholder)。最明显的例子是依赖于 @DbLookup 公式结果的代码。如果代码要经常引用您所查找的数据,那么应该将该数据设为一个临时变量。这样就可以随时引用它,包括:

  • 检查错误条件时
  • 将数据解析为更小的单位(例如一个多值的列表)时
  • 对数据排序时

另一个常见的例子是,使用一个包含大量数据的文档,并且用户可以通过单击按钮以不同方式对数据排序。这种功能的用意可能是模仿即时(on-the-fly)排序的视图功能,但事实上却是在一个文档中的一个大型表内排序。 这种情况下无疑应该将成组的信息设为临时变量,然后对这些临时变量排序。两种做法在性能上的不同令人吃惊。作者过去曾经有一次很尴尬的经历,当时的代码执行起来要花一分多钟,后来通过使用临时变量,代码的性能一下子提升到次秒级。

最后,第三个例子是,让代码在一个数据库中搜索具有特定名称的一个视图。您可能会不由自主地去循环使用 db.views 属性,但是通过设置一个临时变量,例如 viewLIST = db.views,可以节省时间。设置了临时变量之后,就可以迭代这个临时变量,从而可以获得很好的性能。

Computed 字段

限制文档中的计算次数可以提高性能。具体来讲,在一个文档中执行的计算越多,性能就越慢。无论何时以读模式打开一个文档,都会发生某些计算。您应当清楚以读方式打开文档与以编辑模式打开文档相比在时间上所占的百分比,从而知道减少什么字段/计算。下面是各种情况的一些例子:

  • 如果通常只是读文档,不对其进行编辑
    在这种情况下,@DbLookup 和 @DbColumn 公式将被触发,所以应该避免在读模式下执行包含这些公式的代码字段。例如,您可以将该公式包含在一个检查 @IsDocBeingEdited 的 if 语句中。对于这种代码,关键词字段是首选,因为这些字段通常包含 @DbLookup 或 @DbColumn 字段。例如,一个名为 kList 的关键词字段可能有以下公式:

    @If(@IsDocBeingEdited; @DbColumn("Notes"; ""; ViewName; 1); kList)

    注意,在 Else 条件中,我们保留了该字段在读模式下的内容,只显示所有被选中的值,但是没有进一步的计算。或者,您也可以把该代码添加到一个按钮,通过用户单击按钮来执行代码。注意,在读模式下,Computed for Display 字段会发生计算,因此要确保那些字段不包含代价高的公式。
  • 如果通常首先读文档,然后切换到编辑模式
    在这种情况下,应确保前面不让执行的代码(如前所述)在切换到编辑模式时执行。例如,当用户以读模式打开文档时,首先被压制的是包含 @DbLookup 或 @DbColumn 公式的关键词字段。但是如果用户将文档切换到编辑模式,则使用一个 PostModeChange 事件强制刷新文档(例如,if source.editmode then call source.refresh)。此外,选择关键词字段选项 “Refresh choices on document refresh”。选择该选项后,当用户从读模式切换到编辑模式时,文档会自动刷新一次,并强制重新计算关键词字段。

    图 1. Refresh fields on keyword change 选项
    Refresh fields on keyword change option
    Refresh fields on keyword change option
  • 如果通常以编辑模式打开文档
    在这种情况下,您可能希望将那些昂贵的(指在性能方面)代码移入到按钮中,以便不致于使经常性的编辑工作陷入泥沼。例如,它可能假设,即使在编辑文档的时候,大多数用户也不需要更改所有的关键词字段。

您可能会想,上面的建议需要更多的计算,而不是更少的计算。在某种意义上说这没错。这些步骤应该只用于避免代价昂贵的计算,例如 @Db 公式,而不应该将简单的 @ProperCase 和 @If(@IsDocBeingEdited) 包含在一起。这样做是不值得的。

刷新字段值

您可以通过选择 Form Properties 对话框上 Form Info 标签页中的 Automatically refresh field 选项,使字段值自动更新。

图 2. Form Properties 对话框
Form Properties box
Form Properties box

这样做对性能会有负面影响,因为每当有用户将鼠标滑过这些表单字段时,所有之前的字段又要重新计算。这样大量的计算主要目的本来是检查 Input Translation 和 Input Validation 公式,但是实际上所有代码都要执行。

如果当用户选择一个特定的值时需要刷新 Computed 字段中的关键词列表,那么选择我们前面提到的 Field Properties 对话框 Control 标签页上的 "Refresh fields on keyword change" 选项。例如,假设在一个表单上有多个关键词字段,并且第 2、3 和 4 个关键词字段的值取决于用户在第 1 个关键词字段中所选择的值。在这种情况下,应当使用 "Refresh fields on keyword change" 选项。这就像是按下 F9 键,但是每当这个关键词字段中的值发生改变时,都会自动进行。这样得到的性能要好于 Form Properties 对话框上的 "Automatically refresh fields" 选项,因为只有在这一个字段中的值发生变化时才会刷新文档,而不是每当任何值发生变化时都去刷新文档。注意,对于其他关键词字段,必须设置字段选项 "Refresh choices on document refresh"。任何不需要参与到这种更动态的关系中的关键词字段都不需要做那样的选项设置,所以当第一个关键词字段的值发生变化时,这些关键词字段都不会刷新。

图 3. Refresh choices on document refresh 选项
Refresh choices on document refresh option
Refresh choices on document refresh option

使用 Computed when composed 字段类型

当用户创建一个文档时,Computed when composed 字段类型计算一个字段的值。您可以使用一个 Computed when composed 字段来继承值,或者,如果一个字段将要由其他代码来设置,但您又想使之保持不变,那么也可以使用这种字段。例如,在一个响应文档中有一个名为 OriginalSubject 的字段,该字段包含公式 Subject。当用户创建一个响应文档时,该字段将从选择的主文档继承值,而不再进行计算。另一个例子是一个名为 DateClosed 的字段,该字段由一个动作栏按钮背后的代码设置。由于我们不希望这个字段改变它的值,所以我们将其设置为 Computed when composed,并使用公式 DateClosed。这样它就成为一个占位符公式。只有在第一次被创建时它才尝试计算(在这种情况下,假设不存在继承),之后就一直保持给它的值。注意,只要用户保存文档,Computed when composed 字段会将适当的数据类型应用到给它们的值。

您可能会问,在这两个例子中,使用 Computed when composed 字段和使用 Computed 字段有什么不同。两者之间的主要区别是,每当文档被编辑、刷新和保存时,Computed 字段都要计算,即使没有实际的工作要执行。如果只有少量上面所说的简单公式,那么这种字段还不会对应用程序的性能产生多大影响。但是,如果在表单中有很多那样的字段,那么对性能肯定会有影响。由于在这个例子中没必要计算字段,因此用户也可以得到更好的性能。这真是免费的午餐!

缓存和非缓存参数

缓存(cache)和非缓存(nocache)参数适用于所有 @Db 公式。当指定缓存参数时,公式的值存储在一个缓存中,以便于获取。当指定非缓存参数时,公式的值不存储在缓存中,所以每次都要查找数据库。开发人员常常过多地使用非缓存参数,从而花费更多的资源从数据库获取数据,而不是从缓存得到数据。不要犯这样的错误:根据您认为的数据的重要性而决定使用非缓存参数。

相反,应该考虑数据更改的频率:数据更改得越频繁,就越需要使用非缓存数据。对于更改较少的数据,则应使用缓存参数。例如,假设有一个讨论数据库,用户可以在其中指定用于创建新类别的关键词。(也就是说,每当用户创建一个新的主题,他或她都可以为那个主题指定任何类别)。在这种情况下,数据相对来说就不是很重要,但是仍然需要使用非缓存参数,让在一行中输入两个或三个主题的用户可以在下一个主题的关键词字段中看到即时更新的新类别。为了防止出现不良性能,应该和用于 ODBC Access 的 "Generate unique keys in index" 选项一起使用非缓存参数,这在本系列的第 1 部分中有过讨论。记住,"Generate unique keys in index" 选项只需列出惟一的类别,因此所需的视图索引会更小一些。

图 4. Generate unique keys in index 选项
Generate unique keys in index option
Generate unique keys in index option

作为反例,假设您需要查询薪水信息。这是极其重要的信息,但是在最佳情况下,薪水在几个月内至多变化一次,因此这种数据很适于进行缓存。

LotusScript 方法

Lotus 进行了多次测试,以判断哪些常用的 LotusScript 方法在获得一组文档(在任何 LotusScript 代码中这都是最常见的任务)时具有最佳性能。在这一节中,我们将比较下面这些常用的 LotusScript 方法:

  • db.FTSearch
  • db.Search
  • view.GetAllDocumentsByKey
  • view.GetDocumentByKey

在这些测试中使用了不同大小的数据库(分别包含 10,000、100,000 和 1,000,000 个文档),以观察每种方法的执行情况。

db.FTSearch 方法

db.FTSearch 根据对数据库的全文本搜索返回一组文档。它执行得很好,但是需要一个当前全文本索引,为了掌握语法,可能还需要更陡峭的学习曲线。此外,取决于服务器 Notes.ini 的设置,对于返回的文档集可能存在大小上的限制。当然,如果搜索是基于富文本字段的内容,那么这是惟一行得通的选项!

db.Search 方法

db.Search 根据对数据库的搜索返回一组文档,这种搜索实际上使用了一个视图选择公式。对于大型数据库中的小型文档集,这种方法的性能相对来说比较差。例如,如果数据库中有 100,000 个文档,而您只需要找到 5 到 10 个文档,那么应该避免使用 db.Search。另一方面,由于它不需要全文本索引,也无需预先建立视图,因此是非常方便的搜索方法。例如,如果您正在搜索一个数据库,对于该数据库您没有多少控制权,那么这个方法也许是惟一适合的选择。

view.GetAllDocumentsByKey 方法

从 Release 5 开始,这个方法是获取一组文档的最快方式。惟一的不足是,它需要预先建立相关视图。然而,只要优化视图设计,并避免使用代价昂贵的时间/日期敏感的公式(在本系列的前一篇文章中有描述),那么这些视图对性能和磁盘空间的影响就很小,而利用视图 view.GetAllDocumentsByKey 从这些视图中获取一组组文档的代码也会非常快。

通常,当对用上述任何方法检索到的文档集进行迭代时,代码应该使用

set doc = DocumentCollection.GetNextDocument ( doc )

,而不是

set doc = DocumentCollection.GetNthDocument ( i )

其中 i 从 1 增加到 DocumentCollection.count。对于小型文档集,以及独立运行的代码,例如按时调度的代理,性能损失很小,但是对于大型文档集,或者由多个用户同时运行的代码,就会有较高的性能成本,使得 GetNth 成为不明智的选择。在某些情况下,您希望从文档集中选取文档,而不只是迭代整个文档集,而 GetNth 方法正是这些情况下专用的。

view.GetDocumentByKey 方法

这是惟一不从内存中获得文档集的方法。相反,view.GetDocumentByKey 使用一个已经建好的视图索引作为它的文档集,每次从视图中获取一个文档。当结合 view.AutoUpdate = False 一起使用时,该方法非常快,不需要在内存中存放可能很大的文档集。

注:如果之前的文档已经从视图中删除,在获取到视图中下一个文档的句柄时可能产生错误消息,view.AutoUpdate = False 主要用于避免这种错误消息,但实际上它也会提高代理的运行性能。当更改文档中的数据时,如果设置了 view.AutoUpdate = False,那么视图中会有显著的改善。

事件、共享元素及其他

下面这些是另外需要记住的编程提示:

  • 注意表单中事件的数量,不要“过度编程(overcode)”。
    当删除代码时,记得要完全删除。不要只是将代码注释掉,或者部分地消除代码。您可以通过判断圆括号/大括号中是否有内容来判断一个事件是否认为它有代码。
  • 共享元素在性能上略差一些。但是,由于它可以在多个地方使用,从而弥补了其在性能上的缺陷。
    应当谨慎考虑何时使用共享元素来减少一些工作,何时重复使用一个元素以提高性能。
  • 如果要实现错误检查,那么应确保在碰到错误时检查能够停止。
    如果能够小心地编程,那么可以保证代码不会有在逻辑上应该结束的情况下仍然继续执行这样的“漏洞”。
  • 大型子表单的性能比较差。
    大型子表单会影响应用程序的性能。如果在应用程序中不需要多次使用一个大型子表单,那么应当考虑在每个表单中重复使用子表单上的字段,而不是使用子表单。
  • 使用更少的字段。
    在文档中使用更少的字段不光关系到文档的大小,更关系到性能。使用更少的字段,让每个字段有更多数据,例如多值字段,而不是使用更多的字段,每个字段有更少的数据,这样可以提高应用程序的性能。对于不熟悉 Notes/Domino 应用程序开发的传统程序员,这也许不符合直觉,但通过测试可以很清楚地表明这种做法的合理性。
  • 使用 view.Autoupdate=False 来防止刷新视图。
    如前所述,结合该属性使用 view.GetDocumentByKey 方法可以带来很好的性能。
  • 使用 StampAll 方法一次性修改一大组文档。
    如果需要给一大组文档贴上一个静态值,例如当前时间/日期,或者将一个标记设为一个值,那么这种方法很有效。
  • ForAll 语句是迭代一个循环的最快方式。
  • 固定数组的性能比动态数组好。
    动态数组比固定数组的性能要稍微差一些,但是动态数组可以作适应性的调整,所以在决定使用固定数组还是动态数组的时候要权衡这两个方面。

结束语

我们希望这个系列中给出的提示对您有所帮助,并希望您在进行实践后能看到应用程序的性能有所提升。我们渴望从您那里听到应用程序性能调优的最佳实践,所以如果您想和更大的 Notes/Domino 应用程序开发人员社区共享您自己的技巧,那么请向我们提交您的技巧


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Lotus
ArticleID=161707
ArticleTitle=应用程序性能调优,第 2 部分
publish-date=05012003