C++ 生成见解 SDK
C++ Build Insights SDK 与 Visual Studio 2017 及更高版本兼容。 若要查看这些版本对应的文档,请将本文的 Visual Studio“版本”选择器控件设置为 Visual Studio 2017 或更高版本。 它位于此页面上目录表的顶部。
C++ Build Insights SDK 是一个 API 集合,可便于在 C++ Build Insights 平台基础之上创建个性化工具。 本页提供了有助于你入门的简要概述。
获取 SDK
可以将 C++ Build Insights SDK 下载为 NuGet 包,具体步骤如下:
- 在 Visual Studio 2017 及更高版本中,新建 C++ 项目。
- 在“解决方案资源管理器”窗格中,右键单击你的项目。
- 选择关联菜单中的“管理 NuGet 包”。
- 选择右上角的“nuget.org”包源。
- 搜索最新版 Microsoft.Cpp.BuildInsights 包。
- 选择“安装”。
- 接受许可证。
继续阅读有关此 SDK 的一般概念的信息。 你还可以访问官方的 C++ Build Insights 示例 GitHub 存储库,以查看使用此 SDK 的实际 C++ 应用程序示例。
收集跟踪
若要使用 C++ Build Insights SDK 分析来自 MSVC 工具链的事件,需要先收集跟踪。 此 SDK 使用 Windows 事件跟踪 (ETW) 作为基础跟踪技术。 可以通过两种方式来收集跟踪:
方法 1:在 Visual Studio 2019 及更高版本中使用 vcperf
打开适用于 VS 2019 的提升的 x64 本机工具命令提示。
运行以下命令:
vcperf /start MySessionName
生成项目。
运行以下命令:
vcperf /stopnoanalyze MySessionName outputTraceFile.etl
重要
使用 vcperf 停止跟踪时,请运行
/stopnoanalyze
命令。 不能使用 C++ Build Insights SDK 来分析由常规/stop
命令停止的跟踪。
方法 2:以编程方式
使用下面这些 C++ Build Insights SDK 跟踪收集函数来以编程方式启动和停止跟踪。 执行这些函数调用的程序必须有管理权限。 只有启动和停止跟踪函数需要管理权限。 C++ Build Insights SDK 中的其他所有函数都可以在没有管理权限的情况下执行。
与跟踪收集相关的 SDK 函数
接下来的各部分介绍了如何配置分析或重新记录会话。 这是合并的功能函数(如 StopAndAnalyzeTracingSession)所必需的。
使用跟踪
有了 ETW 跟踪之后,使用 C++ Build Insights SDK 来解包它。 此 SDK 以一种可便于快速开发工具的格式提供事件。 不建议在不使用此 SDK 的情况下使用原始 ETW 跟踪。 MSVC 使用的事件格式是没有文档记录的,经过优化可以扩展为大型生成,并且很难理解。 此外,C++ Build Insights SDK API 是稳定的,而原始 ETW 跟踪格式可能会在不另行通知的情况下发生变更。
与跟踪使用相关的 SDK 类型和函数
功能 | C++ API | C API | 说明 |
---|---|---|---|
创建事件回叫 | IAnalyzer IRelogger |
ANALYSIS_CALLBACKS RELOG_CALLBACKS |
C++ Build Insights SDK 通过回叫函数提供事件。 在 C++ 中,通过创建继承自 IAnalyzer 或 IRelogger 接口的 analyzer 或 relogger 类来实现回叫函数。 在 C 中,在全局函数中实现回叫,并在 ANALYSIS_CALLBACKS 或 RELOG_CALLBACKS 结构中提供指向它们的指针。 |
生成组 | MakeStaticAnalyzerGroup MakeStaticReloggerGroup MakeDynamicAnalyzerGroup MakeDynamicReloggerGroup |
C++ API 提供了帮助程序函数和类型,以将多个 analyzer 和 relogger 对象组合在一起。 分组是一种将复杂分析划分为更简单步骤的简洁方法。 vcperf 就是按照这种方法进行组织的。 | |
分析或重新记录 | 分析 Relog |
AnalyzeA AnalyzeW RelogA RelogW |
分析和重新记录
跟踪使用是通过分析会话或重新记录会话来完成的。
使用常规分析适用于大多数情况。 使用这种方法,可以灵活地选择输出格式:printf
文本、xml、JSON、数据库、REST 调用等。
重新记录用于需要生成 ETW 输出文件的特殊用途分析。 使用重新记录,可以将 C++ Build Insights 事件转换为采用你自己的 ETW 事件格式。 重新记录的一种合适用法是,将 C++ Build Insights 数据挂钩到现有的 ETW 工具和基础结构。 例如,vcperf 使用了重新记录接口。 这是因为它必须生成作为 ETW 工具的 Windows Performance Analyzer 可以理解的数据。 如果你计划使用重新记录接口,则需要预先了解 ETW 的工作原理。
创建 analyzer 组
请务必了解如何创建组。 这是一个示例,展示了如何创建一个打印 Hello, world! 的分析器组。对于它收到的每个活动开始事件。
using namespace Microsoft::Cpp::BuildInsights;
class Hello : public IAnalyzer
{
public:
AnalysisControl OnStartActivity(
const EventStack& eventStack) override
{
std::cout << "Hello, " << std::endl;
return AnalysisControl::CONTINUE;
}
};
class World : public IAnalyzer
{
public:
AnalysisControl OnStartActivity(
const EventStack& eventStack) override
{
std::cout << "world!" << std::endl;
return AnalysisControl::CONTINUE;
}
};
int main()
{
Hello hello;
World world;
// Let's make Hello the first analyzer in the group
// so that it receives events and prints "Hello, "
// first.
auto group = MakeStaticAnalyzerGroup(&hello, &world);
unsigned numberOfAnalysisPasses = 1;
// Calling this function initiates the analysis and
// forwards all events from "inputTrace.etl" to my analyzer
// group.
Analyze("inputTrace.etl", numberOfAnalysisPasses, group);
return 0;
}
使用事件
与事件相关的 SDK 类型和函数
活动和简单事件
事件分为“活动”和“简单事件”这两类。 活动是指正在进行的进程,有开始时间,也有结束时间。 简单事件是准时发生的,没有持续时间。 当使用 C++ Build Insights SDK 分析 MSVC 跟踪时,你会在活动开始和停止时收到单独的事件。 当一个简单事件发生时,你将只收到一个事件。
父子关系
活动和简单事件通过父子关系相互关联。 活动或简单事件的父级是包含它们的活动。 例如,在编译源文件时,编译器必须先分析文件,再生成代码。 分析文件和代码生成这两个活动都是编译器活动的子级。
由于简单事件没有持续时间,因此其内部不会发生其他任何事件。 所以,简单事件从来没有任何子级。
每个活动和简单事件的父子关系在事件表中指明。 使用 C++ Build Insights 事件时,请务必了解这些关系。 你常常不得不依赖他们来理解事件的完整上下文。
属性
所有事件都具有以下属性:
properties | 说明 |
---|---|
类型标识符 | 唯一标识事件类型的编号。 |
实例标识符 | 在跟踪内唯一标识事件的编号。 如果在跟踪中发生了相同类型的两个事件,那么两个事件都将获得唯一的实例标识符。 |
开始时间 | 活动启动的时间或简单事件发生的时间。 |
进程标识符 | 标识事件发生进程的编号。 |
线程标识符 | 标识事件发生线程的编号。 |
处理器索引 | 指明事件由哪个逻辑处理器发出的从零开始编制的索引。 |
事件名称 | 描述事件类型的字符串。 |
除简单事件之外的所有活动也都具有以下属性:
properties | 说明 |
---|---|
停止时间 | 活动停止时间。 |
独占持续时间 | 花费在某项活动上的时间,不包括花费在其子活动上的时间。 |
CPU 时间 | CPU 在附加到活动的线程中执行代码所花费的时间。 它不包括附加到活动的线程的休眠时间。 |
独占 CPU 时间 | 与 CPU 时间相同,但不包括子活动所花费的 CPU 时间。 |
时钟时间责任 | 活动对总时钟时间的贡献。 时钟时间责任将活动之间的并行度考虑在内。 例如,假设两个不相关的活动并行运行。 两个活动的持续时间都是 10 秒,且开始时间和停止时间完全相同。 在这种情况下,Build Insights 分配的时钟时间责任都是 5 秒。 相反,如果这些活动一个接一个地运行且没有重叠,则为它们分配的时钟时间责任都是 10 秒。 |
独占时钟时间责任 | 与时钟时间责任相同,但不包括子活动的时钟时间责任。 |
有些事件除了上面提到的属性外,还有它们自己的属性。 在这种情况下,这些附加属性列在事件表中。
使用 C++ Build Insights SDK 提供的事件
事件堆栈
每当 C++ Build Insights SDK 提供事件时,它都以堆栈的形式出现。 堆栈中的最后一个条目是当前事件,在它之前的条目是它的父层次结构。 例如,LTCG 启动和停止事件发生在链接器的第 1 阶段。 在这种情况下,你收到的堆栈包含: [LINKER, PASS1, LTCG]。 父层次结构很方便,因为可以将事件追溯到它的根。 如果上面提到的 LTCG 活动比较慢,则可以立即了解涉及到哪个链接器调用。
匹配事件和事件堆栈
C++ Build Insights SDK 为你提供了跟踪中的所有事件,但大多数情况下你只关注其中一部分。 在某些情况下,你可能只关注事件堆栈的一部分。 SDK 提供了一些工具,可帮助你快速提取所需的事件或事件堆栈,并拒绝不需要的事件或事件堆栈。 这是通过下面这些匹配函数来完成的:
函数 | 说明 |
---|---|
MatchEvent | 如果事件与指定的类型之一匹配,则予以保留。 将匹配的事件转发给 lambda 或其他可调用类型。 此函数不考虑事件的父层次结构。 |
MatchEventInMemberFunction | 如果事件与成员函数的参数中指定的类型匹配,则予以保留。 将匹配的事件转发给成员函数。 此函数不考虑事件的父层次结构。 |
MatchEventStack | 如果事件及其父层次结构都与指定的类型匹配,则予以保留。 将事件和匹配的父层次结构事件转发给 lambda 或其他可调用类型。 |
MatchEventStackInMemberFunction | 如果事件及其父层次结构都与成员函数的参数列表中指定的类型匹配,则予以保留。 将事件和匹配的父层次结构事件转发给成员函数。 |
事件堆栈匹配函数(如 MatchEventStack
)在描述要匹配的父层次结构时允许存在间隙。 例如,你可能会表示自己关注 [LINKER, LTCG] 堆栈。 它还会匹配 [LINKER, PASS1, LTCG] 堆栈。 指定的最后一个类型必须是要匹配的事件类型,并且不属于父层次结构。
Capture 类
使用 Match*
函数需要指定要匹配的类型。 这些类型是从捕获类列表中选择的。 捕获类分为几个类别,如下所述。
类别 | 说明 |
---|---|
Exact | 此类捕获类用于匹配特定的事件类型,而不是其他类型。 例如,Compiler 类匹配 COMPILER 事件。 |
通配符 | 此类捕获类可用于匹配它们所支持的事件列表中的任何事件。 例如,Activity 通配符匹配任何活动事件。 再例如,CompilerPass 通配符可以匹配 FRONT_END_PASS 或 BACK_END_PASS 事件。 |
组 | 组捕获类的名称以 Group 结尾。 它们用于匹配连续多个类型相同的事件,同时忽略间隙。 它们只有在匹配递归事件时才有意义,因为你不知道事件堆栈中有多少事件。 例如,每当编译器分析文件时,FRONT_END_FILE 活动都会发生。 此活动是递归的,因为编译器在分析文件时可能会发现 include 指令。 FrontEndFile 类只匹配堆栈中的一个 FRONT_END_FILE 事件。 使用 FrontEndFileGroup 类可以匹配整个 include 层次结构。 |
通配符组 | 通配符组将通配符和组的属性合并在一起。 此类别的唯一类是 InvocationGroup,它匹配并捕获一个事件堆栈中的所有 LINKER 和 COMPILER 事件。 |
请参阅事件表,了解可以使用哪些捕获类来匹配每个事件。
匹配后:使用捕获的事件
在成功完成匹配后,Match*
函数便会构造捕获类对象,并将它们转发给指定的函数。 使用这些捕获类对象可以访问事件的属性。
示例
AnalysisControl MyAnalyzer::OnStartActivity(const EventStack& eventStack)
{
// The event types to match are specified in the PrintIncludes function
// signature.
MatchEventStackInMemberFunction(eventStack, this, &MyAnalyzer::PrintIncludes);
}
// We want to capture event stacks where:
// 1. The current event is a FrontEndFile activity.
// 2. The current FrontEndFile activity has at least one parent FrontEndFile activity
// and possibly many.
void PrintIncludes(FrontEndFileGroup parentIncludes, FrontEndFile currentFile)
{
// Once we reach this point, the event stack we are interested in has been matched.
// The current FrontEndFile activity has been captured into 'currentFile', and
// its entire inclusion hierarchy has been captured in 'parentIncludes'.
cout << "The current file being parsed is: " << currentFile.Path() << endl;
cout << "This file was reached through the following inclusions:" << endl;
for (auto& f : parentIncludes)
{
cout << f.Path() << endl;
}
}