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 包,具体步骤如下:

  1. 在 Visual Studio 2017 及更高版本中,新建 C++ 项目。
  2. 在“解决方案资源管理器”窗格中,右键单击你的项目。
  3. 选择关联菜单中的“管理 NuGet 包”
  4. 选择右上角的“nuget.org”包源。
  5. 搜索最新版 Microsoft.Cpp.BuildInsights 包。
  6. 选择“安装”
  7. 接受许可证。

继续阅读有关此 SDK 的一般概念的信息。 你还可以访问官方的 C++ Build Insights 示例 GitHub 存储库,以查看使用此 SDK 的实际 C++ 应用程序示例。

收集跟踪

若要使用 C++ Build Insights SDK 分析来自 MSVC 工具链的事件,需要先收集跟踪。 此 SDK 使用 Windows 事件跟踪 (ETW) 作为基础跟踪技术。 可以通过两种方式来收集跟踪:

方法 1:在 Visual Studio 2019 及更高版本中使用 vcperf

  1. 打开适用于 VS 2019 的提升的 x64 本机工具命令提示。

  2. 运行以下命令:vcperf /start MySessionName

  3. 生成项目。

  4. 运行以下命令:vcperf /stopnoanalyze MySessionName outputTraceFile.etl

    重要

    使用 vcperf 停止跟踪时,请运行 /stopnoanalyze 命令。 不能使用 C++ Build Insights SDK 来分析由常规 /stop 命令停止的跟踪。

方法 2:以编程方式

使用下面这些 C++ Build Insights SDK 跟踪收集函数来以编程方式启动和停止跟踪。 执行这些函数调用的程序必须有管理权限。 只有启动和停止跟踪函数需要管理权限。 C++ Build Insights SDK 中的其他所有函数都可以在没有管理权限的情况下执行。

功能 C++ API C API
启动跟踪 StartTracingSession StartTracingSessionA
StartTracingSessionW
停止跟踪 StopTracingSession StopTracingSessionA
StopTracingSessionW
停止跟踪并
立即分析结果
StopAndAnalyzeTracingSession StopAndAnalyzeTracingSessionA
StopAndAnalyzeTracingSession
停止跟踪并
立即重新记录结果
StopAndRelogTracingSession StopAndRelogTracingSessionA
StopAndRelogTracingSessionW

接下来的各部分介绍了如何配置分析或重新记录会话。 这是合并的功能函数(如 StopAndAnalyzeTracingSession)所必需的。

使用跟踪

有了 ETW 跟踪之后,使用 C++ Build Insights SDK 来解包它。 此 SDK 以一种可便于快速开发工具的格式提供事件。 不建议在不使用此 SDK 的情况下使用原始 ETW 跟踪。 MSVC 使用的事件格式是没有文档记录的,经过优化可以扩展为大型生成,并且很难理解。 此外,C++ Build Insights SDK API 是稳定的,而原始 ETW 跟踪格式可能会在不另行通知的情况下发生变更。

功能 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;
}

使用事件

功能 C++ API C API 说明
匹配和筛选事件 MatchEventStackInMemberFunction
MatchEventStack
MatchEventInMemberFunction
MatchEvent
此 C++ API 提供了一些函数,可便于你从跟踪中轻松地提取你关注的事件。 使用此 C API 时,必须手动完成这种筛选。
事件数据类型 活动
BackEndPass
BottomUp
C1DLL
C2DLL
CodeGeneration
CommandLine
Compiler
CompilerPass
EnvironmentVariable
事件
EventGroup
EventStack
ExecutableImageOutput
ExpOutput
FileInput
FileOutput
ForceInlinee
FrontEndFile
FrontEndFileGroup
FrontEndPass
Function
HeaderUnit
ImpLibOutput
调用
InvocationGroup
LibOutput
链接器
LinkerGroup
LinkerPass
LTCG
模块
ObjOutput
OptICF
OptLBR
OptRef
Pass1
Pass2
PrecompiledHeader
PreLTCGOptRef
SimpleEvent
SymbolName
TemplateInstantiation
TemplateInstantiationGroup
线程
TopDown
TraceInfo
TranslationUnitType
WholeProgramAnalysis
CL_PASS_DATA
EVENT_COLLECTION_DATA
EVENT_DATA
EVENT_ID
FILE_DATA
FILE_TYPE_CODE
FRONT_END_FILE_DATA
FUNCTION_DATA
FUNCTION_FORCE_INLINEE_DATA
INVOCATION_DATA
INVOCATION_VERSION_DATA
MSVC_TOOL_CODE
NAME_VALUE_PAIR_DATA
SYMBOL_NAME_DATA
TEMPLATE_INSTANTIATION_DATA
TEMPLATE_INSTANTIATION_KIND_CODE
TRACE_INFO_DATA
TRANSLATION_UNIT_PASS_CODE
TRANSLATION_UNIT_TYPE
TRANSLATION_UNIT_TYPE_DATA

活动和简单事件

事件分为“活动”和“简单事件”这两类。 活动是指正在进行的进程,有开始时间,也有结束时间。 简单事件是准时发生的,没有持续时间。 当使用 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_PASSBACK_END_PASS 事件。
组捕获类的名称以 Group 结尾。 它们用于匹配连续多个类型相同的事件,同时忽略间隙。 它们只有在匹配递归事件时才有意义,因为你不知道事件堆栈中有多少事件。 例如,每当编译器分析文件时,FRONT_END_FILE 活动都会发生。 此活动是递归的,因为编译器在分析文件时可能会发现 include 指令。 FrontEndFile 类只匹配堆栈中的一个 FRONT_END_FILE 事件。 使用 FrontEndFileGroup 类可以匹配整个 include 层次结构。
通配符组 通配符组将通配符和组的属性合并在一起。 此类别的唯一类是 InvocationGroup,它匹配并捕获一个事件堆栈中的所有 LINKERCOMPILER 事件。

请参阅事件表,了解可以使用哪些捕获类来匹配每个事件。

匹配后:使用捕获的事件

在成功完成匹配后,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;
    }
}