7

在工作中,我们使用 XML 日志文件。每条日志消息都是一个<message>带有<date><time>节点的块,有<submessage>块、<table>结构等,并且可以稍后使用一些 Delphi 处理和 XSLT 将日志文件转换为本地化的 HTML。

对于中等大小的日志文件(大约 2 MB),我们遇到了性能问题(加载 XML 和进行一些基本操作最多需要一分钟),我可以将它们简化为这样的测试项目(编辑:更新代码并添加测量):

procedure TForm1.PrepareTest(MessageCount : integer);
var
  XML : IXMLDocument;
  i : integer;
begin
  XML := NewXMLDocument;
  XML.DocumentElement := XML.CreateNode('root');
  for i := 1 to MessageCount do
  begin
    XML.DocumentElement.AddChild('message').Text := 'Test Text';
  end;
  XML.SaveToFile(XML_NAME);
end;

procedure TForm1.XMLTest;
var
  StartTime : Cardinal;
  XML : IXMLDocument;
begin
  StartTime := GetTickCount();
  XML := NewXMLDocument;
  XML.LoadFromFile(XML_NAME);
  Memo1.Lines.Add('Node count: ' + IntToStr(XML.DocumentElement.ChildNodes.Count));
  Memo1.Lines.Add('Time: ' + FloatToStr((GetTickCount() - StartTime) / 1000) + ' seconds');
end;

这会产生以下时间测量结果(节点计数每列增加 25%,所有时间都以毫秒为单位):

Node count      8000    10000   12500   15625   19531   24413   30516   38145   47681
Base test time  484     781     1140    1875    2890    4421    6734    10672   16812
Variation 1                             32      47      62      78      78      141
Variation 2     2656    3157    3906    5015    6532    8922    12140   17391   24985
  (delta Base)   2172    2376    2766    3140    3642    4501    5406    6719    8173

请注意这两种变体,第一种是LoadFromFile唯一的,第二种是另外在 XML 的开头(!)添加 10000 个节点,就像PrepareTest做的那样,这是最坏的情况,但是查看基本测试的增量,即使这样也没有显示二次效应。另请注意,计算节点可以替换为任何其他操作,因此看起来涉及的 XML 文件的初始化/验证存在一些延迟,这会导致问题,并且之后的任何操作都会显示预期的行为。

内存使用不高,最后一个测试用例(47681 个节点)的内存使用峰值为 39 MB,其 XML 文件大小为 1.3 MB。

加载 XML 后做的第一件事(例如读取或写入一些节点或访问节点计数)很慢,并且它显示二次运行时行为,因此任何超过 10 MB 的日志文件都无法使用。

我们已经通过解析 100 条消息的小块解决了性能问题以及其他一些问题,而且我知道 Delphi XML 例程不适合/过度杀伤这个用例 - 使用不同的 XML 库很可能会停止性能问题。所以我不是要求解决这个问题(尽管如果不使用不同的 XML 库就可以解决这个问题会很有趣)。

我的问题是:Delphi XML 例程和 MSXML 的二次运行时行为的原因是什么?我无法想象在 XML 加载/解析/验证中会导致这种情况的事情,除了真正“愚蠢”的事情,比如管理链表中的节点而不是树,但我可能忽略了一些东西,可能与 DOM 相关。

4

4 回答 4

7

我同意 mj2008 的观点,即 XML 不适合记录日志。也就是说,这个问题和一般的大型 XML 文件,可以通过使用SAX更快地处理,它在解析传入的 XML 数据流时抛出事件,这使您可以在从磁盘读取项目时对其进行处理,实际上减轻了指数增长在将其交给 XSLT 之前将其全部加载到内存中。

我很遗憾我还没有在 Delphi 中完成 SAX,但我怀疑最难的部分是实现所需的 SAX 接口(例如ISAXContentHandler),但 Delphi 有 TInterfacedObject 和 TAutoObject 等。

于 2013-01-16T19:58:32.303 回答
4

简而言之,您的问题:为什么二进制库 MSXML 这么慢?谁知道。谁在乎。你要拆机吗?闯入微软抢其源代码?这里不是 Delphi,这是微软代码。

虽然 XML 对于日志记录来说是一个糟糕的选择,但 OmniXML 可能是比 MSXML 更好的选择。

但是,更好的选择称为“打开文本文件以进行追加、写入行、关闭文本文件”。请注意固有的可伸缩性,并且不需要解析。

于 2013-01-16T22:06:27.527 回答
3

与其他人的评论相反,我认为 XML 是一种出色的日志记录格式。用于 XML 的 Delphi VCL 包装器对核心内存非常贪婪,因此这可能解释了纯 TXMLDocument 大规模处理性能不佳的原因。

相反,我建议使用简单的 XSLT 转换将其发布到您的 XML 日志中。我尚未大规模测量此解决方案的性能,但我相信这将比您当前报告的内容有很大的改进。

样式表。

例如,假设我们的日志看起来像这样......

<log>
  <message>This is the first message<message/> 
</log>  

这个简单的 XSLT 1.0 样式表,带有参数addend-message...

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes" omit-xml-declaration="yes" />
<xsl:strip-space elements="*" />  
<xsl:param name="addend-message" select="''" />

<xsl:template match="@*|node()">
  <xsl:copy>
    <xsl:apply-templates select="@*|node()" />
  </xsl:copy>
</xsl:template>  

<xsl:template match="log">
  <xsl:copy>
    <xsl:apply-templates select="@*|node()" />
    <message><xsl:value-of select="$addend-message" /></message>
  </xsl:copy>
</xsl:template>  

... 将在日志中附加一条消息。

语言绑定

要在 Delphi 中实现这一点,请使用以下声明...

ITransform = interface
  procedure AddParameter( const sParamBaseName, sParamValue, sNamespaceURI: string);
  procedure Transform;
  property  InputDocumentFileName : string;
  property  OutputDocumentFileName: string;
end;

function MS_Transform( const sStylesheet: string): ITransform;

创建一个 ITransform,将样式表作为字符串传递。将两个文件名属性都设置为日志的文件名。每次您需要向日志中添加消息时,调用AddParameter()然后Transform().

解决方案实施细节

上述语言绑定的一种可能实现可能是......

uses XMLIntf, msxml, msxmldom, sysutils;

type
  ITransform = interface
    ['{1004AE9A-D4AE-40E1-956D-AD98801AF7C1}']
      procedure SetInputDocumentFileName ( const sValue: string);
      procedure SetOutputDocumentFileName( const sValue: string);
      procedure AddParameter( const sParamBaseName, sParamValue, sNamespaceURI: string);
      procedure Transform;

      property InputDocumentFileName : string    write SetInputDocumentFileName;
      property OutputDocumentFileName: string    write SetInputDocumentFileName;
    end;

    TMS_XSLT = class( TInterfacedObject, ITransform)
    private
      FStylesheet: IXSLTemplate;
      FStylesheetAsDoc: IXMLDOMDocument2;
      FInputFN, FOutputFN: string;
      FProcessor: IXSLProcessor;;

      procedure SetInputDocumentFileName ( const sValue: string);
      procedure SetOutputDocumentFileName( const sValue: string);
      procedure MakeProcessor;

    publc
      constructor Create( const sStylesheet: string);
      procedure AddParameter( const sParamBaseName, sParamValue, sNamespaceURI: string);
      procedure Transform;

      property InputDocumentFileName : string    write SetInputDocumentFileName;
      property OutputDocumentFileName: string    write SetInputDocumentFileName;
    end;

  function MS_Transform( const sStylesheet: string): ITransform


function MS_Transform( const sStylesheet: string): ITransform
begin
result := TMS_XSLT.Create( sStylesheet)
end;

constructor TMS_XSLT.Create( const sStylesheet: string);
begin
  FStyleSheet := msxml.CoXSLTemplate60.Create;
  FStylesheetAsDoc := msxml.CoFreeThreadedDOMDocument60.Create;
  FStylesheetAsDoc.loadXML( sStyleSheetContent);
  FStylesheet.stylesheet := FStylesheetAsDoc  
end;

procedure TMS_XSLT.MakeProcessor;
begin
if not assigned( FProcessor) then
  FProcessor := FStylesheet.createProcessor
end;

procedure TMS_XSLT.SetInputDocumentFileName( const sValue: string);
begin
FInputDoc := sValue
end;

procedure TMS_XSLT.SetOutputDocumentFileName( const sValue: string);
begin
FOutputDoc := sValue
end;

procedure TMS_XSLT.AddParameter( const sParamBaseName, sParamValue, sNamespaceURI: string);
begin
MakeProcessor;
FProcessor.addParameter( sParamBaseName, sParamValue, sNamespaceURI)
end;

procedure TMS_XSLT.Transform;
var
  Doc: TXMLDocument;
  DocIntf: IXMLDocument;
  oXMLDOMNode: IXMLDOMNodeRef;
  sOutput: string;
begin
MakeProcessor;
try
  Doc  := TXMLDocument.Create( nil);
  Doc.Options := [doNodeAutoCreate, doNodeAutoIndent, doAttrNull, doAutoPrefix, doNamespaceDecl];
  Doc.DOMVendor := GetDOMVendor( 'MSXML');
  DocIntf := Doc;
  DocIntf.LoadFromFile( FInputFN);
  DocIntf.Active := True;
  if Supports( DocIntf.Node.DOMNode, IXMLDOMNodeRef, XMLDOMNode) then
    FProcessor.input := XMLDOMNode.GetXMLDOMNode;
  FProcessor.transform;
  while oProcessor.readyState <> 4 do sleep(1);
  sOutput := FProcessor.output;
  if sOutput = '' then exit;
  WriteToFile( sFOutputFN, sOutput);
  // Alternate way..
  //  Doc  := TXMLDocument.Create( nil);
  //  Doc.Options := [doNodeAutoCreate, doNodeAutoIndent, doAttrNull, doAutoPrefix, doNamespaceDecl];
  //  Doc.DOMVendor := GetDOMVendor( 'MSXML');
 //   DocIntf := Doc;
  //  DocIntf.LoadFromXML( sOutput);
  //  DocIntf.Active := True;
  //  DocIntf.SaveToFile( FOutputFN)
finally
  FProcessor := nil
  end
end;

这绑定到Microsoft 的 MS XML 库和 XSLT 引擎。遗憾的是,我不知道将Saxon 的 XSLT 处理器绑定到 Delphi 代码的任何便捷方法。

替代实施

我在此处的回答给出了利用 MS 的 XSLT 引擎的替代实现。这种方法的缺点是参数化不是原生的。要参数化样式表,您必须自己滚动,通过在转换之前对样式表进行字符串替换。

性能注意事项

如果您正在快速进行大量日志记录,那么将要记录的消息缓存到内存中可能是一个不错的策略,然后定期但不太频繁地使用单个 XSLT 转换来清除缓存以写入所有消息。

于 2013-01-17T07:35:15.487 回答
1

您是在数学意义上使用术语“指数”,还是仅在通俗意义上使用?例如,知道它是否真的是二次的,或者它是否是某种性能相当线性的函数,直到你达到某个阈值(内存大小),此时它突然降级,这会很有趣。

如果处理 2Mb 需要一分钟,那么确实是非常严重的错误。我不太了解您的环境,无法开始猜测,但这最多需要一秒钟。您需要深入了解时间的去向。首先确定它是花时间解析 XML,还是在解析完成后处理 XML。

于 2013-01-16T23:48:09.487 回答