标记¶
标记是 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
函数的使用。
- 标记名称
第一个参数是此标记的名称。这将在显示标记的大多数地方显示。它可以是文字 C 字符串,也可以是任何动态字符串对象。
- 类别对名称
从类别列表中选择一个类别和子类别。例如,这是每行
SUBCATEGORY
的第二个参数LAYOUT_Reflow
。(在内部,这实际上是一个MarkerCategory 对象,如果您需要在其他地方构造它)。
- 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
中)之后,有
标记类型,即定义该类型的 C++
struct
的名称。类型特定的变参列表。它们必须与数量匹配,并且必须可转换为模式中定义的类型。如果不是,则它们必须与数量匹配,并且必须可转换为
StreamJSONMarkerData
和TranslateMarkerInputToSchema
中的类型。
在哪里定义新的标记类型¶
第一步是确定标记类型定义的位置
如果此类型仅在一个函数或一个组件中使用,则可以在其使用位置的本地通用位置定义它。
对于可以在多个位置使用的更常见的类型
如果没有对 XUL 的依赖关系,则可以在基本概要文件中定义它,该概要文件可以在代码库的大多数位置使用:mozglue/baseprofiler/public/BaseProfilerMarkerTypes.h
但是,如果存在 XUL 依赖关系,则需要在 Gecko 概要文件中定义它:tools/profiler/public/ProfilerMarkerTypes.h
如何定义新的标记类型¶
每个标记类型必须且只能定义一次。定义是一个 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::string
和nsCString
类型,以及带有或不带空终止符的跨度)。对于 16 位字符串(例如nsString
),请使用const ProfilerString16View&
。如果其他类型为
ProfileBufferEntryWriter::Serializer
和ProfileBufferEntryReader::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
函数:NullProperty
、BoolProperty
、IntProperty
、DoubleProperty
、StringProperty
。(分析器不支持其他嵌套的 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};
可以可选地指定一些标签,以在不同位置显示某些信息:ChartLabel
、TooltipLabel
和 TableLabel
;或 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
可以在分析会话结束时执行昂贵的工作。
标记架构描述¶
以上各节应提供添加您自己的标记类型所需的所有信息。但是,如果您希望处理标记架构本身,则本节将描述系统的工作原理。
- 待办事项
简要描述缓冲区和序列化。
描述用于生成标记类型的模板策略
描述序列化并链接到标记处理的分析器前端文档(如果存在)