性能分析

LNT 支持存储和显示性能分析。这些分析的目的是为了揭示测试样本之间代码生成的差异,并允许轻松识别代码的热点部分。

LNT 中分析的原理

LNT 中的分析以自定义格式表示。用户界面完全基于对此自定义格式的查询进行操作。编写适配器以将其他格式转换为 LNT 的分析格式。分析数据作为正常 JSON 报告的一部分上传到 LNT 服务器。

生成分析数据

分析生成可以直接通过 python API 调用(lnt profile 是其包装器)或使用 lnt runtests 工具驱动。

通过 lnt runtests test-suite 生成分析数据

注意

通过 LNT 收集分析目前仅在 Linux 系统上受支持,因为目前编写的唯一适配器使用了 Linux 的 perf 基础设施。当编写更多适配器后,LNT 可以扩展对它们的支持。

如果您的测试系统已经使用 lnt runtests 来构建和运行测试,那么生成分析的最简单方法是简单地添加一个参数

--use-perf=all

--use-perf 选项指定了 Linux Perf 的用途。选项包括

  • none:不为任何事情使用 perf

  • time:使用 perf 来测量编译和执行时间。这可以比 time 更准确。

  • profile:仅使用 perf 进行分析。

  • all:使用 perf 进行分析和计时。

生成的分析与每个测试可执行文件一起存在,命名为 $TEST.perf_data。这些分析在测试执行结束时被处理并转换为 LNT 的分析格式,并插入到生成的 report.json 中。

不通过 lnt runtests test-suite 生成分析数据

LNT 的一个受支持的用例是使用 LNT 服务器进行性能跟踪,但使用与 lnt runtests 不同的测试驱动程序来实际构建、运行和收集测试的统计信息。

分析数据位于提交给 LNT 的 JSON 报告中。本节将描述如何向已存在的 JSON 报告添加分析数据;有关 JSON 报告的总体结构的详细信息,请参阅 导入数据

第一步是以 LNT 格式生成分析数据本身,以便通过 JSON 发送。要导入分析,请使用 lnt profile upgrade 命令

lnt profile upgrade my_profile.perf_data /tmp/my_profile.lntprof

my_profile.perf_data 在这里被假定为 Linux Perf 格式,但可以是任何已注册适配器的格式(目前只有 Linux Perf,但预计随着时间的推移会添加更多格式)。

/tmp/my_profile.lntprof 现在是以节省空间的二进制形式存在的 LNT 分析。为了准备好通过 JSON 发送它,我们必须对其进行 base-64 编码

base64 -i /tmp/my_profile.lntprof > /tmp/my_profile.txt

现在我们只需要将其添加到报告中。分析看起来类似于哈希,因为它们是带有字符串数据的样本

{
  "format_version": "2",
  "machine": {
     ...
  },
  "run": {
     ...
  },
  "tests": [
     {
         "name": "nts.suite1/program1",
         "execution_time": [ 0.1056, 0.1055 ],
         "profile": "eJxNj8EOgjAMhu99Cm9wULMOEHgBE888QdkASWCQFWJ8e1v04JIt+9f//7qmfkVoEj8yMXdzO70v/RJn2hJYrRQiveSWATdJvwe3jUtgecgh9Wsh9T6gyJvKUjm0kegK0mmt9UCjJUSgB5q8KsobUJOQ96dozr8tAbRApPbssOeCcm83ddoLC7ijMcA/RGUUwXt7iviPEDLJN92yh62LR7I8aBUMysgLnaKNFNzzMo8y7uGplQ4sa/j6rfn60WYaGdRhtT9fP5+JUW4="
     }
  ]
 }

支持的格式

Linux Perf

Perf 分析直接从二进制 perf.data 文件读取,而无需使用 perf 包装器工具或任何 Linux/GPL 标头。这使其可以在非 Linux 平台上运行,尽管这实际上仅对调试有用,因为被分析的二进制文件/库应该是可读的。

perf 导入代码使用了一个名为 cPerf 的 C++ 扩展,它是为 LNT 项目编写的。它的功能不如 perf annotateperf report,但以机器可读的形式生成的数据大致相同,速度快约 6 倍。它用 C++ 编写是因为很难编写可读的 Python,以便在二进制数据上高效地执行。一旦事件流被聚合,就会创建一个 python 字典对象,并将处理返回给 Python。速度在这个阶段很重要,因为分析导入可能在较旧或性能较低的硬件上运行,并且 LLVM 的测试套件包含数百个必须导入的测试!

注意

在最近版本的 Perf 中,存在一个新的子命令:perf data。这以 CTF 格式 输出事件跟踪,然后可以使用 babeltrace 及其 Python 绑定进行查询。只要它的性能相似,这将允许删除 LNT 中的大量自定义代码。

为新的分析格式添加支持

要创建新的分析适配器,必须在 lnt.testing.profile 包中创建一个新的 Python 类,该类继承 ProfileImpl

class lnt.testing.profile.profile.ProfileImpl
static checkFile(fname)

如果 ‘fname’ 是此分析实现的序列化版本,则返回 True。

static deserialize(fobj)

从 ‘fobj’ 读取分析,返回一个新的分析对象。这可以是惰性的。

getCodeForFunction(fname)

返回一个生成器,它将为每次调用返回一个三元组

(counters, address, text)

其中 counters 是一个字典:(例如){'cycles': 50.0},text 的格式与 getDisassemblyFormat() 返回的格式相同,address 是一个整数。

计数器值必须是百分比(函数总计的百分比),而不是绝对数字。

getDisassemblyFormat()

返回 getCodeForFunction() 返回的反汇编字符串的格式。可能的值是

  • raw - 没有可用的解释;

    纯字符串。

  • marked-up-disassembly - LLVM 标记的反汇编格式。

getFunctions()

返回一个字典,其中包含函数名称以及有关该函数的信息。

信息字典包含

  • counters - 函数的计数器值。

  • length - 调用 getCodeForFunction 以获取所有指令的次数。

字典应包含反汇编/函数内容。计数器值必须是百分比,而不是绝对数字。

例如。

{'main': {'counters': {'cycles': 50.0, 'branch-misses': 0},
          'length': 200},
 'dotest': {'counters': {'cycles': 50.0, 'branch-misses': 0},
            'length': 4}
}
getTopLevelCounters()

返回一个字典,其中包含整个分析的计数器。这些将是绝对数字:例如 {'cycles': 5000.0}

getVersion()

返回分析版本。

serialize(fname=None)

将分析序列化为给定的文件名(base)。如果 fname 为 None,则作为字节实例返回。

static upgrade(old)

接受 ‘old’ 中的先前分析实现,并为此版本返回一个新的 ProfileImpl。唯一必须支持的旧版本是紧邻的先前版本(例如,版本 3 只需要处理从版本 2 的升级)。

您的子类可以实现所有指定的函数,或者执行 perf.py 所做的事情,即仅实现 checkFile()deserialize() 静态函数。在这种模型中,在 deserialize() 内部,您将把您的分析数据解析为简单的字典结构,并从中创建一个 ProfileV1Impl 对象。这是一个非常简单的分析实现,它仅从字典表示形式工作

class lnt.testing.profile.profilev1impl.ProfileV1(data)

ProfileV1 文件在任何方面都不聪明。它们是简单的 Python 对象,其中分析数据以最明显的生产/消费方式布局,然后进行 pickle 序列化和压缩。

它们应该通过简单地存储到 self.data 成员中来创建。

self.data 成员具有以下格式

{
 counters: {'cycles': 12345.0, 'branch-misses': 200.0}, # absolute values.
 disassembly-format: 'raw',
 functions: {
   name: {
     counters: {'cycles': 45.0, ...}, # Note counters are now percentages.
     data: [
       [463464, {'cycles': 23.0, ...}, '      add r0, r0, r1'}],
       ...
     ]
   }
  }
}
static checkFile(fn)

如果 ‘fname’ 是此分析实现的序列化版本,则返回 True。

static deserialize(fobj)

从 ‘fobj’ 读取分析,返回一个新的分析对象。这可以是惰性的。

getCodeForFunction(fname)

返回一个生成器,它将为每次调用返回一个三元组

(counters, address, text)

其中 counters 是一个字典:(例如){'cycles': 50.0},text 的格式与 getDisassemblyFormat() 返回的格式相同,address 是一个整数。

计数器值必须是百分比(函数总计的百分比),而不是绝对数字。

getDisassemblyFormat()

返回 getCodeForFunction() 返回的反汇编字符串的格式。可能的值是

  • raw - 没有可用的解释;

    纯字符串。

  • marked-up-disassembly - LLVM 标记的反汇编格式。

getFunctions()

返回一个字典,其中包含函数名称以及有关该函数的信息。

信息字典包含

  • counters - 函数的计数器值。

  • length - 调用 getCodeForFunction 以获取所有指令的次数。

字典应包含反汇编/函数内容。计数器值必须是百分比,而不是绝对数字。

例如。

{'main': {'counters': {'cycles': 50.0, 'branch-misses': 0},
          'length': 200},
 'dotest': {'counters': {'cycles': 50.0, 'branch-misses': 0},
            'length': 4}
}
getTopLevelCounters()

返回一个字典,其中包含整个分析的计数器。这些将是绝对数字:例如 {'cycles': 5000.0}

getVersion()

返回分析版本。

serialize(fname=None)

将分析序列化为给定的文件名(base)。如果 fname 为 None,则作为字节实例返回。

static upgrade(old)

接受 ‘old’ 中的先前分析实现,并为此版本返回一个新的 ProfileImpl。唯一必须支持的旧版本是紧邻的先前版本(例如,版本 3 只需要处理从版本 2 的升级)。

查看分析

一旦分析被提交到 LNT,它们就可以通过手动 URL 或 “runs” 页面获得。

在运行结果页面上,当鼠标悬停在表格行上时,如果分析数据可用,则应显示“查看分析”链接。

注意

已知这种悬停效果对触摸屏不友好,并且可能不够直观。此页面应尽快修改,以使分析数据链接更加明显。

或者,可以通过手动构建 URL 来查看分析

db_default/v4/nts/profile/<test-id>/<run1-id>/<run2-id>

其中

  • test-id 是要显示的测试的数据库 TestID

  • run1-id 是要显示在显示屏左侧的运行的数据库 RunID

  • run2-id 是要显示在显示屏右侧的运行的数据库 RunID

显然,此 URL 有点难以构建,因此建议使用上面运行页面中的链接。