Rust 代码插桩

有多种方法可以使用分析器与 Rust 进行交互。原生栈采样已经包含了 Rust 栈帧,无需特殊处理。存在“原生栈”分析器功能(通过 about:profiling),它可以启用原生代码的栈回溯。对于每个分析器预设,这很可能已经启用。

除此之外,还有一个分析器 Rust API 用于对 Rust 代码进行插桩并向配置文件数据添加更多信息。主要有三个功能可以使用

  1. 将 Rust 线程注册到分析器,以便分析器可以记录这些线程。

  2. 添加栈帧标签以注释和分类栈的一部分。

  3. 添加标记以专门标记时间点或持续时间。这有助于理解代码的特定部分,或记录通常不会出现在样本中的事件。

作为依赖项包含的板条箱

分析器 Rust API 位于 gecko-profiler 板条箱中。在使用以下功能之前,需要将其包含在项目依赖项中。

为了能够包含它,需要在项目的 Cargo.toml 文件中添加一个新的依赖项条目,如下所示

[dependencies]
gecko-profiler = { path = "../../tools/profiler/rust-api" }

请注意,根据项目在 mozilla-central 中的位置,需要更新相对路径。

更新 Cargo.toml 后,需要执行 cargo update -p gkrust-shared 来更新 Cargo.lock 文件。

注册线程

为了能够在配置文件数据中看到线程,需要将它们注册到分析器中。此外,当它们退出时需要取消注册。为线程提供唯一名称非常重要,以便可以轻松地对其进行过滤。

注册和取消注册线程非常简单

// Register it with a given name.
gecko_profiler::register_thread("Thread Name");
// After doing some work, and right before exiting the thread, unregister it.
gecko_profiler::unregister_thread();

例如,以下是如何注册和取消注册一个简单的线程

let thread_name = "New Thread";
std::thread::Builder::new()
    .name(thread_name.into())
    .spawn(move || {
        gecko_profiler::register_thread(thread_name);
        // DO SOME WORK
        gecko_profiler::unregister_thread();
    })
    .unwrap();

或使用线程池

let worker = rayon::ThreadPoolBuilder::new()
    .thread_name(move |idx| format!("Worker#{}", idx))
    .start_handler(move |idx| {
        gecko_profiler::register_thread(&format!("Worker#{}", idx));
    })
    .exit_handler(|_idx| {
        gecko_profiler::unregister_thread();
    })
    .build();

注意

仅注册线程不会使其出现在配置文件数据中。此外,需要将其添加到 about:profiling 中的“线程”过滤器中。此过滤器输入是一个逗号分隔的列表。它匹配部分名称并支持通配符 *

添加栈帧标签

栈帧标签用于使用类别注释调用栈的一部分。该类别将显示在 Firefox 分析器分析页面的各个位置,例如时间线、调用树选项卡、火焰图选项卡等。

gecko_profiler_label! 宏用于添加一个新的标签帧。添加的标签帧将存在于此宏的调用与当前作用域结束之间。

添加栈帧标签

// Marking the stack as "Layout" category, no subcategory provided.
gecko_profiler_label!(Layout);
// Marking the stack as "JavaScript" category and "Parsing" subcategory.
gecko_profiler_label!(JavaScript, Parsing);

// Or the entire function scope can be marked with a procedural macro. This is
// essentially a syntactical sugar and it expands into a function with a
// gecko_profiler_label! call at the very start:
#[gecko_profiler_fn_label(DOM)]
fn foo(bar: u32) -> u32 {
    bar
}

请参阅 profiling_categories.yaml 文件中所有分析类别的列表。

添加标记

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

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

注意

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

示例

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

// Record a simple marker with the category of Graphics, DisplayListBuilding.
gecko_profiler::add_untyped_marker(
    // Name of the marker as a string.
    "Marker Name",
    // Category with an optional sub-category.
    gecko_profiler_category!(Graphics, DisplayListBuilding),
    // MarkerOptions that keeps options like marker timing and marker stack.
    // It will be a point in type by default.
    Default::default(),
);
// Create a marker with some additional text information.
let info = "info about this marker";
gecko_profiler::add_text_marker(
    // Name of the marker as a string.
    "Marker Name",
    // Category with an optional sub-category.
    gecko_profiler_category!(DOM),
    // MarkerOptions that keeps options like marker timing and marker stack.
    MarkerOptions {
        timing: MarkerTiming::instant_now(),
        ..Default::default()
    },
    // Additional information as a string.
    info,
);
// Record a custom marker of type `ExampleNumberMarker` (see definition below).
gecko_profiler::add_marker(
    // Name of the marker as a string.
    "Marker Name",
    // Category with an optional sub-category.
    gecko_profiler_category!(Graphics, DisplayListBuilding),
    // MarkerOptions that keeps options like marker timing and marker stack.
    Default::default(),
    // Marker payload.
    ExampleNumberMarker { number: 5 },
);

....

// Marker type definition. It needs to derive Serialize, Deserialize.
#[derive(Serialize, Deserialize, Debug)]
pub struct ExampleNumberMarker {
    number: i32,
}

// Marker payload needs to implement the ProfilerMarker trait.
impl gecko_profiler::ProfilerMarker for ExampleNumberMarker {
    // Unique marker type name.
    fn marker_type_name() -> &'static str {
        "example number"
    }
    // Data specific to this marker type, serialized to JSON for profiler.firefox.com.
    fn stream_json_marker_data(&self, json_writer: &mut gecko_profiler::JSONWriter) {
        json_writer.int_property("number", self.number.into());
    }
    // Where and how to display the marker and its data.
    fn marker_type_display() -> gecko_profiler::MarkerSchema {
        use gecko_profiler::marker::schema::*;
        let mut schema = MarkerSchema::new(&[Location::MarkerChart]);
        schema.set_chart_label("Name: {marker.name}");
        schema.add_key_label_format("number", "Number", Format::Integer);
        schema
    }
}

无类型标记

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

gecko_profiler::add_untyped_marker(
    // Name of the marker as a string.
    "Marker Name",
    // Category with an optional sub-category.
    gecko_profiler_category!(Graphics, DisplayListBuilding),
    // MarkerOptions that keeps options like marker timing and marker stack.
    MarkerOptions {
        timing: MarkerTiming::instant_now(),
        ..Default::default()
    },
);
  1. 标记名称

    第一个参数是此标记的名称。这将显示在标记显示的大多数地方。它可以是字面量字符串或任何动态字符串。

  2. 分析类别对

    来自 类别列表 的类别 + 子类别对。应使用 gecko_profiler_category! 宏创建分析类别对,因为它更易于使用,例如 gecko_profiler_category!(JavaScript, Parsing)。可以省略第二个参数以直接使用默认子类别。gecko_profiler_category! 宏建议使用,但如果需要,也可以使用 ProfilingCategoryPair 枚举。

  3. MarkerOptions

    请参阅下面的选项。如果没有任何参数,则可以使用 Default::default() 省略它。一些选项也可以省略,MarkerOptions {<options>, ..Default::default()},并使用以下一个或多个选项类型

    • MarkerTiming

      这指定了一个时间点或时间间隔。如果未指定,则默认为当前时间点。否则使用 MarkerTiming::instant_at(ProfilerTime)MarkerTiming::interval(pt1, pt2);时间戳通常使用 ProfilerTime::Now() 捕获。也可以仅记录间隔的开始或结束,成对的开始/结束标记将根据其名称进行匹配。

    • MarkerStack

      默认情况下,标记不会记录“栈”(或“回溯”)。为了以最有效的方式在此处记录栈,请指定 MarkerStack::Full。为了捕获没有原生帧的栈以减少开销,请指定 MarkerStack::NonNative

    注意:目前,并非所有 C++ 标记选项都存在于 Rust 端。它们将在将来添加。

文本标记

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

let info = "info about this marker";
gecko_profiler::add_text_marker(
    // Name of the marker as a string.
    "Marker Name",
    // Category with an optional sub-category.
    gecko_profiler_category!(DOM),
    // MarkerOptions that keeps options like marker timing and marker stack.
    MarkerOptions {
        stack: MarkerStack::Full,
        ..Default::default()
    },
    // Additional information as a string.
    info,
);

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

其他类型标记

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

gecko_profiler::add_marker(
    // Name of the marker as a string.
    "Marker Name",
    // Category with an optional sub-category.
    gecko_profiler_category!(JavaScript),
    // MarkerOptions that keeps options like marker timing and marker stack.
    Default::default(),
    // Marker payload.
    YourMarker { number: 5, text: "some string".to_string() },
);

在前面三个常见参数(如 gecko_profiler::add_untyped_marker 中)之后,有一个标记有效负载结构,需要对其进行定义。让我们看看如何定义它。

如何定义新的标记类型

每个标记类型必须且只能定义一次。定义是一个 Rust struct,它是在 Rust 中记录该类型的标记时构造的。每个标记结构都包含使其在 profiler.firefox.com 上显示所需的数据。按照惯例,建议使用后缀“Marker”来更好地区分源代码中非分析器实体。

每个标记有效负载必须派生 serde::Serializeserde::Deserialize。如果项目没有它,它们也会从 gecko-profiler 板条箱中导出。每个标记有效负载应将其数据作为其字段包含,如下所示

#[derive(Serialize, Deserialize, Debug)]
pub struct YourMarker {
    number: i32,
    text: String,
}

每个标记结构还必须实现 ProfilerMarker 特性。

ProfilerMarker 特性

所有标记类型都必须实现 ProfilerMarker 特性。其方法类似于 C++ 对应方法,请参考 C++ 标记指南以了解更多信息。它包含三个需要实现的方法

  1. marker_type_name() -> &'static str:

    标记类型必须具有唯一的名称,它用于跟踪分析器存储中标记的类型,并在 profiler.firefox.com 上唯一标识它们。(它不需要与结构的名称相同。)

    例如:

    fn marker_type_name() -> &'static str {
        "your marker type"
    }
    
  2. stream_json_marker_data(&self, json_writer: &mut JSONWriter)

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

    此外,某个标记类型可能携带零个或多个任意信息,并且对于该类型的全部标记来说,这些信息始终相同。

    这些信息定义在特殊的静态成员函数 stream_json_marker_data 中。

    它是一个成员方法,并接受 &mut JSONWriter 作为参数,它将用于以 JSON 格式流式传输数据,以便稍后由 profiler.firefox.com 读取。请参阅 JSONWriter 对象及其方法

    例如:

    fn stream_json_marker_data(&self, json_writer: &mut JSONWriter) {
        json_writer.int_property("number", self.number.into());
        json_writer.string_property("text", &self.text);
    }
    
  3. marker_type_display() -> schema::MarkerSchema

    现在,已经定义了如何流式传输类型特定数据(从 Firefox 到 profiler.firefox.com),需要描述此数据将在 profiler.firefox.com 上显示的位置和方式。

    静态成员函数 marker_type_display 返回一个不透明的 MarkerSchema 对象,该对象将转发到 profiler.firefox.com。

    请参阅 MarkerSchema::Location 枚举以获取完整列表。另请参阅 MarkerSchema 结构以获取其可能的方法

    例如:

    fn marker_type_display() -> schema::MarkerSchema {
        // Import MarkerSchema related types for easier use.
        use crate::marker::schema::*;
        // Create a MarkerSchema struct with a list of locations provided.
        // One or more constructor arguments determine where this marker will be displayed in
        // the profiler.firefox.com UI.
        let mut schema = MarkerSchema::new(&[Location::MarkerChart]);
    
        // Some labels can optionally be specified, to display certain information in different
        // locations: set_chart_label, set_tooltip_label, and set_table_label``; or
        // set_all_labels to define all of them the same way.
        schema.set_all_labels("{marker.name} - {marker.data.number});
    
        // Next, define the main display of marker data, which will appear in the Marker Chart
        // tooltips and the Marker Table sidebar.
        schema.add_key_label_format("number", "Number", Format::Number);
        schema.add_key_label_format("text", "Text", Format::String);
        schema.add_static_label_value("Help", "This is my own marker type");
    
        // Lastly, return the created schema.
        schema
    }
    

    请注意,set_all_labels 中的字符串可能引用花括号内的标记数据

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

    • {marker.data.X}:从 stream_json_marker_data 以属性名“X”流式传输的特定类型数据。

    有关详细信息,请参阅 C++ 标记指南。.