性能分析

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 报告通用结构的详细信息,请参阅 导入数据

第一步是生成适合通过 JSON 发送的 LNT 格式的分析数据。要导入分析,请使用 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 发送,我们必须对其进行 Base64 编码

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)

将概要文件序列化到给定的文件名(基础)。如果 fname 为 None,则作为字节实例返回。

static upgrade(old)

获取“old”中的先前概要文件实现,并返回此版本的新 ProfileImpl。必须支持的唯一旧版本是紧接其前的版本(例如,版本 3 只需处理来自版本 2 的升级)。

您的子类可以实现所有指定的功能,或者像 perf.py 一样,只实现 checkFile()deserialize() 静态函数。在此模型中,在 deserialize() 内部,您将把概要文件数据解析成一个简单的字典结构,并从中创建一个 ProfileV1Impl 对象。这是一个非常简单的概要文件实现,只需从字典表示中工作即可。

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

ProfileV1 文件在任何方面都不聪明。它们是简单的 Python 对象,其中概要文件数据以最明显的方式(用于生产/使用)布局,然后被腌制并压缩。

预计它们将通过简单地存储到 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)

将概要文件序列化到给定的文件名(基础)。如果 fname 为 None,则作为字节实例返回。

static upgrade(old)

获取“old”中的先前概要文件实现,并返回此版本的新 ProfileImpl。必须支持的唯一旧版本是紧接其前的版本(例如,版本 3 只需处理来自版本 2 的升级)。

查看概要文件

将概要文件提交到 LNT 后,可以通过手动 URL 或“运行”页面访问它们。

在运行结果页面上,如果提供了概要文件数据,则将鼠标悬停在表格行上时,会显示“查看概要文件”链接。

注意

已知此悬停效果不适合触摸屏,并且可能不直观。此页面应尽快修改,以使概要文件数据链接更明显。

或者,可以通过手动构建 URL 来查看概要文件

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

其中

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

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

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

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