标记

标记是 Firefox 代码添加到概要文件中的任意数据包,通常用于指示某个时间点或某个时间间隔内发生的某些重要事件。

每个标记都有一个名称、一个类别、一些常见的可选信息(时间、回溯等)以及一个可选的特定类型有效负载(包含与该类型相关的任意数据)。

注意

本指南深入解释了 C++ 标记。要了解如何在 JavaScript、Rust 或 JVM 中添加标记,请分别查看其在JavaScript 代码插桩Rust 代码插桩Android 代码插桩中的文档。

示例

简短示例,详细信息如下。

注意:大多数与标记相关的标识符都在 mozilla 命名空间中,需要时添加。

// Record a simple marker with the category of DOM.
PROFILER_MARKER_UNTYPED("Marker Name", DOM);

// Create a marker with some additional text information. (Be wary of printf!)
PROFILER_MARKER_TEXT("Marker Name", JS, MarkerOptions{}, "Additional text information.");

// Record a custom marker of type `ExampleNumberMarker` (see definition below).
PROFILER_MARKER("Number", OTHER, MarkerOptions{}, ExampleNumberMarker, 42);
// Marker type definition.
struct ExampleNumberMarker : public BaseMarkerType<ExampleNumberMarker> {
  // Unique marker type name.
  static constexpr const char* Name = "number";
  // Marker description.
  static constexpr const char* Description = "This is a number marker.";

  // For convenience.
  using MS = MarkerSchema;
  // Fields of payload for the marker.
  static constexpr MS::PayloadField PayloadFields[] = {
      {"number", MS::InputType::Uint32t, "Number", MS::Format::Integer}};

  // Locations this marker should be displayed.
  static constexpr MS::Location Locations[] = {MS::Location::MarkerChart,
                                               MS::Location::MarkerTable};
  // Location specific label for this marker.
  static constexpr const char* ChartLabel = "Number: {marker.data.number}";

  // Data specific to this marker type, as passed to PROFILER_MARKER/profiler_add_marker.
  static void StreamJSONMarkerData(SpliceableJSONWriter& aWriter, uint32_t a number) {
    // Custom writer for marker fields, or using the default parent
    // implementation if the function arguments match the schema.
    StreamJSONMarkerDataImpl(aWriter, a number);
  }
};

在添加参数与模式不同的标记时,可以使用转换器函数和自定义的 StreamJSONMarkerData 实现。

// Marker type definition.
struct ExampleBooleanMarker : public BaseMarkerType<ExampleBooleanMarker> {
  // Unique marker type name.
  static constexpr const char* Name = "boolean";
  // Marker description.
  static constexpr const char* Description = "This is a boolean marker.";

  // For convenience.
  using MS = MarkerSchema;
  // Fields of payload for the marker.
  static constexpr MS::PayloadField PayloadFields[] = {
      {"boolean", MS::InputType::CString, "Boolean"}};

  // Locations this marker should be displayed.
  static constexpr MS::Location Locations[] = {MS::Location::MarkerChart,
                                               MS::Location::MarkerTable};
  // Location specific label for this marker.
  static constexpr const char* ChartLabel = "Boolean: {marker.data.boolean}";

  // Data specific to this marker type, as passed to PROFILER_MARKER/profiler_add_marker.
  static void StreamJSONMarkerData(SpliceableJSONWriter& aWriter, bool aBoolean) {
    // Note the schema expects a string, we cannot use the default implementation.
    if (aBoolean) {
      aWriter.StringProperty("boolean", "true");
    } else {
      aWriter.StringProperty("boolean", "false");
    }
  }

  // The translation to the schema must also be defined in a translator function.
  // The argument list should match that to PROFILER_MARKER/profiler_add_marker.
  static void TranslateMarkerInputToSchema(void* aContext, bool aBoolean) {
    // This should call ETW::OutputMarkerSchema with an argument list matching the schema.
    if (aIsStart) {
      ETW::OutputMarkerSchema(aContext, ExampleBooleanMarker{}, ProfilerStringView("true"));
    } else {
      ETW::OutputMarkerSchema(aContext, ExampleBooleanMarker{}, ProfilerStringView("false"));
    }
  }
};

下面提供了更详细的描述。

如何记录标记

包含的头文件

如果编译单元仅定义和记录无类型、文本和/或其自身的标记,则包含主要概要文件标记头文件

#include "mozilla/ProfilerMarkers.h"

如果它还记录了在ProfilerMarkerTypes.h中定义的其他常用标记之一,则包含该头文件

#include "mozilla/ProfilerMarkerTypes.h"

如果它使用任何其他概要文件函数(例如,标签),则使用主要 Gecko 概要文件头文件代替

#include "GeckoProfiler.h"

上述方法适用于最终位于 libxul 中的源文件,这对于大多数 Firefox 源代码都是正确的。但是,某些文件位于 libxul 之外,例如 mfbt,在这种情况下,建议相同,但等效的头文件来自基本概要文件

#include "mozilla/BaseProfilerMarkers.h" // Only own/untyped/text markers
#include "mozilla/BaseProfilerMarkerTypes.h" // Only common markers
#include "BaseProfiler.h" // Markers and other profiler functions

无类型标记

无类型标记除了常见标记数据外不携带任何信息:名称、类别、选项。

PROFILER_MARKER_UNTYPED(
    // Name, and category pair.
    "Marker Name", OTHER,
    // Marker options, may be omitted if all defaults are acceptable.
    MarkerOptions(MarkerStack::Capture(), ...));

PROFILER_MARKER_UNTYPED 是一个宏,它通过添加适当的命名空间以及周围的 #ifdef MOZ_GECKO_PROFILER 保护来简化主要 profiler_add_marker 函数的使用。

  1. 标记名称

    第一个参数是此标记的名称。这将在显示标记的大多数地方显示。它可以是文字 C 字符串,也可以是任何动态字符串对象。

  2. 类别对名称

    类别列表中选择一个类别和子类别。例如,这是每行 SUBCATEGORY 的第二个参数 LAYOUT_Reflow。(在内部,这实际上是一个MarkerCategory 对象,如果您需要在其他地方构造它)。

  3. MarkerOptions

    请参阅下面的选项。如果没有任何其他参数,可以省略它,{}MarkerOptions()(无指定选项);仅以下选项类型之一;或 MarkerOptions(...),其中包含一个或多个以下选项类型

    • MarkerThreadId

      很少使用,因为它默认为当前线程。否则,它指定标记应显示的目标“线程 ID”(也称为“轨迹”);当引用发生在另一个线程上的内容时,这可能很有用(使用来自原始线程的 profiler_current_thread_id() 获取其 ID);或者对于某些重要标记,它们可能会发送到“主线程”,可以使用 MarkerThreadId::MainThread() 指定。

    • MarkerTiming

      这指定了时间点或时间间隔。如果未指定,则默认为当前时间点。否则,使用 MarkerTiming::InstantAt(timestamp)MarkerTiming::Interval(ts1, ts2);时间戳通常使用 TimeStamp::Now() 捕获。也可以仅记录时间间隔的开始或结束,成对的开始/结束标记将按其名称匹配。注意:即将推出的“标记集”功能将使此配对更加可靠,并且还允许连接多个标记

    • MarkerStack

      默认情况下,标记不会记录“堆栈”(或“回溯”)。要在此时以最有效的方式记录堆栈,请指定 MarkerStack::Capture()。要记录先前捕获的堆栈,首先使用 profiler_capture_backtrace() 将堆栈存储到 UniquePtr<ProfileChunkedBuffer> 中,然后将其传递给标记,使用 MarkerStack::TakeBacktrace(std::move(stack))

    • MarkerInnerWindowId

      如果您有权访问“内部窗口 ID”,请考虑将其指定为选项,以帮助 profiler.firefox.com 按标签对其进行分类。

“自动”作用域区间标记

为了捕获某些重要操作周围的时间间隔,通常会存储时间戳,执行工作,然后记录标记,例如

void DoTimedWork() {
  TimeStamp start = TimeStamp::Now();
  DoWork();
  PROFILER_MARKER_TEXT("Timed work", OTHER, MarkerTiming::IntervalUntilNowFrom(start), "Details");
}

RAII 对象通过记录对象构造时的时间,然后在对象在其 C++ 作用域结束时被销毁时记录标记来自动执行此操作。如果有多个作用域退出点,这尤其有用。

AUTO_PROFILER_MARKER_TEXT目前唯一实现的方法。

void MaybeDoTimedWork(bool aDoIt) {
  AUTO_PROFILER_MARKER_TEXT("Timed work", OTHER, "Details");
  if (!aDoIt) { /* Marker recorded here... */ return; }
  DoWork();
  /* ... or here. */
}

请注意,这些 RAII 对象仅记录一个标记。在某些情况下,如果在概要文件会话结束时未完成,则可能会错过非常长的操作。在这种情况下,请考虑使用 MarkerTiming::IntervalStart()MarkerTiming::IntervalEnd() 记录两个不同的标记。

文本标记

文本标记非常常见,除了标记名称外,它们还额外携带文本作为第四个参数。使用以下宏

PROFILER_MARKER_TEXT(
    // Name, category pair, options.
    "Marker Name", OTHER, {},
    // Text string.
    "Here are some more details."
);

尽管它很有用,但使用代价高昂的 printf 操作来生成复杂的文本会带来各种问题字符串。它可能会泄漏潜在的敏感信息,例如 URL 可能会在概要文件共享步骤中泄漏。profiler.firefox.com 无法以编程方式访问这些信息。它不会获得内置标记模式的格式化优势。请考虑使用自定义标记类型来分离和更好地呈现数据。

其他类型标记

从 C++ 代码中,某个类型 YourMarker(类型定义的详细信息如下)的标记可以这样记录

PROFILER_MARKER(
    "YourMarker name", OTHER,
    MarkerOptions(MarkerTiming::IntervalUntilNowFrom(someStartTimestamp),
                  MarkerInnerWindowId(innerWindowId))),
    YourMarker, "some string", 12345, "http://example.com", someTimeStamp);

在最初三个常见参数(如 PROFILER_MARKER_UNTYPED 中)之后,有

  1. 标记类型,即定义该类型的 C++ struct 的名称。

  2. 类型特定的变参列表。它们必须与数量匹配,并且必须可转换为模式中定义的类型。如果不是,则它们必须与数量匹配,并且必须可转换为 StreamJSONMarkerDataTranslateMarkerInputToSchema 中的类型。

在哪里定义新的标记类型

第一步是确定标记类型定义的位置

  • 如果此类型仅在一个函数或一个组件中使用,则可以在其使用位置的本地通用位置定义它。

  • 对于可以在多个位置使用的更常见的类型

如何定义新的标记类型

每个标记类型必须且只能定义一次。定义是一个 C++ struct,它继承自 BaseMarkerType,其标识符在 C++ 中记录该类型的标记时使用。按照惯例,建议使用后缀“Marker”以更好地将其与源代码中的非概要文件实体区分开来。

struct YourMarker : BaseMarkerType<YourMarker> {

标记类型名称和描述

标记类型必须具有唯一的名称,用于跟踪概要文件存储中的标记类型,并在 profiler.firefox.com 上唯一地识别它们。(它不需要与 struct 的名称相同。)

此类型名称在特殊的静态数据成员 Name 中定义

// …
  static constexpr const char* Name = "YourMarker";

此外,您必须在特殊的静态数据成员 Description 中添加标记的描述

// …
  static constexpr const char* Description = "This is my marker!";

如果您希望用户为标记的各个实例传递唯一名称,则可能需要添加以下内容以确保在使用 ETW 时存储这些名称

// …
  static constexpr bool StoreName = true;

标记类型数据

任何类型的所有标记都有一些公共数据:名称、类别、时间等选项,如前所述。

此外,某个标记类型可能携带零个或多个任意信息片段,并且对于该类型的所有标记始终相同。

这些在 PayloadField 的特殊静态成员数据数组中定义。每个有效负载字段指定一个键、一个 C++ 类型描述、一个标签、一个格式,以及可选的一些其他选项(请参阅 PayloadField 类型)。最重要的字段是

  • 键:在 StreamJSONMarkerData 中流式传输的元素属性名称。

  • 类型:描述传递给 PROFILER_MARKER/profiler_add_marker 的 C++ 类型的枚举值。

  • 标签:用于标记字段的显示前缀。

  • 格式:如何格式化数据元素值,请参阅MarkerSchema::Format 的详细信息

// …
  // This will be used repeatedly and is done for convenience.
  using MS = MarkerSchema;
  static constexpr MS::PayloadField PayloadFields[] = {
      {"number", MS::InputType::Uint32t, "Number", MS::Format::Integer}};

此外,必须定义一个 StreamJSONMarkerData 函数,该函数将 C++ 参数类型与 PROFILER_MARKER 匹配。

第一个函数参数始终是 SpliceableJSONWriter& aWriter,它将用于将数据作为 JSON 流式传输,以便稍后由 profiler.firefox.com 读取。

// …
  static void StreamJSONMarkerData(SpliceableJSONWriter& aWriter,

以下函数参数是数据如何从调用站点作为 C++ 对象接收。

  • 大多数 C/C++POD(普通旧数据)平凡可复制 类型应该按原样工作,包括 TimeStamp

  • 字符字符串应使用 const ProfilerString8View& 传递(这处理字面量字符串,以及各种 std::stringnsCString 类型,以及带有或不带空终止符的跨度)。对于 16 位字符串(例如 nsString),请使用 const ProfilerString16View&

  • 如果其他类型为 ProfileBufferEntryWriter::SerializerProfileBufferEntryReader::Deserializer 定义了专门化,则可以使用它们。您很少需要定义新的专门化,但如果需要,请查看现有专门化的编写方式,或 联系 perf-tools 团队寻求帮助

建议按值或按 const 引用传递,因为参数以二进制形式序列化(即,没有可优化的 move 操作)。

例如,以下是如何处理字符串、64 位数字、另一个字符串和时间戳

// …
                                   const ProfilerString8View& aString,
                                   const int64_t aBytes,
                                   const ProfilerString8View& aURL,
                                   const TimeStamp& aTime) {

然后函数体将这些参数转换为 JSON 流。

如果这些参数类型与模式中指定的类型在顺序和数量上都匹配。它可以简单地调用默认实现。

// …
  static void StreamJSONMarkerData(SpliceableJSONWriter& aWriter,
                                   const ProfilerString8View& aString,
                                   const int64_t aBytes,
                                   const ProfilerString8View& aURL,
                                   const TimeStamp& aTime) {
    StreamJSONMarkerDataImpl(aWrite, aString, aBytes, aURL, aTime);
  }

如果传递给 PROFILER_MARKER 的参数与模式不匹配,则需要执行一些额外的工作。

当调用此函数时,写入器刚刚开始了一个 JSON 对象,因此写入的所有内容都应是命名对象属性。使用 SpliceableJSONWriter 函数,在大多数情况下使用其父类 JSONWriter 中的 ...Property 函数:NullPropertyBoolPropertyIntPropertyDoublePropertyStringProperty。(分析器不支持其他嵌套的 JSON 类型,如数组或对象。)

作为特殊情况,必须使用 aWriter.TimeProperty(timestamp) 流式传输 TimeStamps

属性名称将用于识别每个数据片段的存储位置以及如何在 profiler.firefox.com 上显示它(请参阅下一节)。

假设我们的标记模式为布尔值定义了一个字符串,以下是其流式传输方式。

// …

  static void StreamJSONMarkerData(SpliceableJSONWriter& aWriter,
                                   bool aBoolean) {
    aWriter.StringProperty("myBoolean", aBoolean ? "true" : "false");
  }

此外,必须添加 TranslateMarkerInputToSchema 函数以确保正确输出到 ETW。

// The translation to the schema must also be defined in a translator function.
// The argument list should match that to PROFILER_MARKER/profiler_add_marker.
static void TranslateMarkerInputToSchema(void* aContext, bool aBoolean) {
  // This should call ETW::OutputMarkerSchema with an argument list matching the schema.
  if (aIsStart) {
    ETW::OutputMarkerSchema(aContext, YourMarker{}, ProfilerStringView("true"));
  } else {
    ETW::OutputMarkerSchema(aContext, YourMarker{}, ProfilerStringView("false"));
  }
}

标记类型显示模式

现在我们已经定义了如何流式传输类型特定的数据(从 Firefox 到 profiler.firefox.com),我们需要描述这些数据将在 profiler.firefox.com 上的何处以及如何显示。

location 数据成员确定此标记将在 profiler.firefox.com UI 中的何处显示。请参阅 MarkerSchema::Location 枚举以获取完整列表

这是最常见的定位集,在“标记图表”和“标记表”面板中显示该类型的标记

// …
  static constexpr MS::Location Locations[] = {MS::Location::MarkerChart,
                                               MS::Location::MarkerTable};

可以可选地指定一些标签,以在不同位置显示某些信息:ChartLabelTooltipLabelTableLabel;或 AllLabels 以相同的方式定义所有标签。

参数是一个字符串,它可能引用大括号内的标记数据

  • {marker.name}:标记名称。

  • {marker.data.X}:类型特定的数据,使用 StreamJSONMarkerData 中属性名称“X”流式传输(例如,aWriter.IntProperty("X", a number);

例如,以下是如何将“标记图表”标签设置为显示标记名称和 myBytes 字节数

// …
    static constexpr const char* ChartLabel = "{marker.name} – {marker.data.myBytes}";

profiler.firefox.com 将以一致的方式应用带数据的标签。例如,使用此标签定义,它可以在 Firefox Profiler 的“标记图表”中显示如下标记信息

  • “标记名称 – 10B”

  • “标记名称 – 25.204KB”

  • “标记名称 – 512.54MB”

有关此处理的实现详细信息,请参阅分析器前端中的 src/profiler-logic/marker-schema.js

任何其他 struct 成员函数都将被忽略。上述强制函数可以使用实用程序函数来使代码更清晰。

这就是标记定义 struct 的结尾。

// …
};

性能注意事项

在分析期间,最好减少执行分析器操作所花费的工作量,因为它们可能会影响要分析的代码的性能。

在可能的情况下,请考虑将简单类型传递给标记函数,以便 StreamJSONMarkerData 将执行最少的工作量,以将标记类型特定的参数序列化为其内部缓冲区表示形式。POD 类型(数字)和字符串是最容易且最便宜的序列化对象。如果您想更好地了解所做的工作,请查看相应的 ProfileBufferEntryWriter::Serializer 专门化。

避免在记录标记时执行昂贵的操作。例如:将不同内容 printf 到字符串中,或复杂的计算;而是将 printf/计算参数直接传递给标记函数,以便 StreamJSONMarkerData 可以在分析会话结束时执行昂贵的工作。

标记架构描述

以上各节应提供添加您自己的标记类型所需的所有信息。但是,如果您希望处理标记架构本身,则本节将描述系统的工作原理。

待办事项
  • 简要描述缓冲区和序列化。

  • 描述用于生成标记类型的模板策略

  • 描述序列化并链接到标记处理的分析器前端文档(如果存在)