From 563c979f911d08a36ffe8cf471a9c89b20b7d7b6 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 26 Sep 2026 03:39:33 +0000 Subject: [PATCH] Add perfetto for rust Make perfetto available to rust --- .bazelrc | 5 + AGENTS.md | 1 + build/deps/deps.jsonc | 3 +- build/deps/gen/deps.MODULE.bazel | 1 + build/wd_rust_crate.bzl | 4 +- deps/rust/Cargo.lock | 8 + deps/rust/Cargo.toml | 5 + docs/v8-updates.md | 5 + ...r-client-and-expose-Rust-SDK-sources.patch | 86 +++ src/rust/AGENTS.md | 17 + src/rust/perfetto/BUILD.bazel | 28 + src/rust/perfetto/disabled.rs | 79 +++ src/rust/perfetto/enabled.rs | 141 ++++ src/rust/perfetto/ffi.rs | 13 + src/rust/perfetto/lib-test.rs | 41 ++ src/rust/perfetto/lib.rs | 624 ++++++++++++++++++ src/rust/perfetto/sdk/BUILD.bazel | 55 ++ src/rust/perfetto/test/BUILD.bazel | 31 + src/rust/perfetto/test/lib-test.rs | 299 +++++++++ src/rust/perfetto/test/lib.rs | 19 + src/rust/perfetto/test/test-helper.c++ | 95 +++ src/rust/perfetto/test/test-helper.h | 23 + src/workerd/util/AGENTS.md | 1 + src/workerd/util/BUILD.bazel | 35 + src/workerd/util/perfetto-tracing.c++ | 15 + 25 files changed, 1631 insertions(+), 3 deletions(-) create mode 100644 patches/perfetto/0004-Add-C-ABI-library-over-client-and-expose-Rust-SDK-sources.patch create mode 100644 src/rust/perfetto/BUILD.bazel create mode 100644 src/rust/perfetto/disabled.rs create mode 100644 src/rust/perfetto/enabled.rs create mode 100644 src/rust/perfetto/ffi.rs create mode 100644 src/rust/perfetto/lib-test.rs create mode 100644 src/rust/perfetto/lib.rs create mode 100644 src/rust/perfetto/sdk/BUILD.bazel create mode 100644 src/rust/perfetto/test/BUILD.bazel create mode 100644 src/rust/perfetto/test/lib-test.rs create mode 100644 src/rust/perfetto/test/lib.rs create mode 100644 src/rust/perfetto/test/test-helper.c++ create mode 100644 src/rust/perfetto/test/test-helper.h diff --git a/.bazelrc b/.bazelrc index 2bc39d91751..edbf652473d 100644 --- a/.bazelrc +++ b/.bazelrc @@ -89,8 +89,13 @@ build:macos --copt=-UDEBUG # Keep workerd and V8's Perfetto build configurations in sync. build:perfetto --//src/workerd/util:use_perfetto=True build:perfetto --@v8//:v8_use_perfetto=True +# Perfetto trace points in Rust (//src/rust/perfetto). The flag defaults to off so that embedders +# that don't read this file (and may not provide the Perfetto C ABI and Rust SDK) get the no-op +# facade; see the comment on the flag in src/workerd/util/BUILD.bazel. +build:perfetto --//src/workerd/util:use_rust_perfetto=True build:no-perfetto --//src/workerd/util:use_perfetto=False build:no-perfetto --@v8//:v8_use_perfetto=False +build:no-perfetto --//src/workerd/util:use_rust_perfetto=False build:unix --config=perfetto # Increasing the optimization level of V8 significantly speeds up tests using V8 a lot, especially diff --git a/AGENTS.md b/AGENTS.md index c1603a214ff..45ffde976fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -201,6 +201,7 @@ Be aware that workerd uses tcmalloc for memory allocation in the typical case. W | JS benchmarks | `src/workerd/api/bench.{h,c++}`, `build/wd_bench.bzl` | `bench` handler; runner `BenchController`; report schema `io/bench.capnp`; driver `server/server/bench.rs` | | TypeScript types | `types/` | Extracted from C++ RTTI + hand-written `defines/*.d.ts`; see `types/AGENTS.md` for detailed typings guidance | | V8 patches | `patches/v8/` | 33 patches; see `docs/v8-updates.md` | +| Perfetto trace points | `src/workerd/util/perfetto-tracing.h` (C++), `src/rust/perfetto/` (Rust) | Rust backend behind `--//src/workerd/util:use_rust_perfetto`; see `src/rust/AGENTS.md` PERFETTO TRACING | ## Coding Conventions diff --git a/build/deps/deps.jsonc b/build/deps/deps.jsonc index 432ddee9032..ac39a5a5f7e 100644 --- a/build/deps/deps.jsonc +++ b/build/deps/deps.jsonc @@ -116,7 +116,8 @@ "patches": [ "//:patches/perfetto/0001-Don-t-attempt-to-use-rules_android.patch", "//:patches/perfetto/0002-disable-info-level-logging-re2.patch", - "//:patches/perfetto/0003-Load-rules-removed-from-Bazel-9-builtins.patch" + "//:patches/perfetto/0003-Load-rules-removed-from-Bazel-9-builtins.patch", + "//:patches/perfetto/0004-Add-C-ABI-library-over-client-and-expose-Rust-SDK-sources.patch" ] }, { diff --git a/build/deps/gen/deps.MODULE.bazel b/build/deps/gen/deps.MODULE.bazel index 203d54e448b..f59eb19361f 100644 --- a/build/deps/gen/deps.MODULE.bazel +++ b/build/deps/gen/deps.MODULE.bazel @@ -123,6 +123,7 @@ archive_override( "//:patches/perfetto/0001-Don-t-attempt-to-use-rules_android.patch", "//:patches/perfetto/0002-disable-info-level-logging-re2.patch", "//:patches/perfetto/0003-Load-rules-removed-from-Bazel-9-builtins.patch", + "//:patches/perfetto/0004-Add-C-ABI-library-over-client-and-expose-Rust-SDK-sources.patch", ], sha256 = "4fcc98fe4754269a70e297f6caba429420039c89a1c6cb561dffb4057de63abb", strip_prefix = "google-perfetto-9e9bdaf", diff --git a/build/wd_rust_crate.bzl b/build/wd_rust_crate.bzl index 6878644f528..cb2422eb1fa 100644 --- a/build/wd_rust_crate.bzl +++ b/build/wd_rust_crate.bzl @@ -80,6 +80,7 @@ def wd_rust_crate( cxx_bridge_features = [], cxx_bridge_visibility = [], testonly = False, + crate_features = [], visibility = None): """Define rust crate. @@ -112,6 +113,7 @@ def wd_rust_crate( another package that includes a bridge's header (it must also link the crate). testonly: True for a crate that only tests depend on (a test harness, the Rust half of a C++ test). Like other test code, it is not held to //build/rust:lints. + crate_features: cargo-style features to enable for the crate and its test; may be a select(). """ if srcs == None: srcs = native.glob(["**/*.rs"]) @@ -165,8 +167,6 @@ def wd_rust_crate( for bridge_src in cxx_bridge_srcs: link_deps = link_deps + [bridge_src + "@cxx"] - crate_features = [] - library_kwargs = {} if crate_root != None: library_kwargs["crate_root"] = crate_root diff --git a/deps/rust/Cargo.lock b/deps/rust/Cargo.lock index 4faf1e395b7..8f39ef450ba 100644 --- a/deps/rust/Cargo.lock +++ b/deps/rust/Cargo.lock @@ -561,6 +561,7 @@ dependencies = [ "anyhow", "async-trait", "base64 0.23.1", + "bitflags", "bytes", "capnp", "capnp-rpc", @@ -589,6 +590,7 @@ dependencies = [ "lol_html_c_api", "nix", "notify", + "pastey", "percent-encoding", "pico-args", "proc-macro2", @@ -1540,6 +1542,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "percent-encoding" version = "2.3.2" diff --git a/deps/rust/Cargo.toml b/deps/rust/Cargo.toml index 4d8b2deaae7..c20a05f368f 100644 --- a/deps/rust/Cargo.toml +++ b/deps/rust/Cargo.toml @@ -34,6 +34,10 @@ async-trait = { version = "0", default-features = false } # kj-hyper's WebSocket handshake: Sec-WebSocket-Key / -Accept (RFC 6455 section 4). base64 = { version = "0", default-features = false, features = ["std"] } sha1 = { version = "0", default-features = false } +# bitflags and pastey are dependencies of the Perfetto Rust SDK, which is built from the +# @perfetto sources by //src/rust/perfetto/sdk. pastey is a maintained drop-in replacement for the +# archived `paste` crate that the SDK imports; it's aliased to `paste` there. +bitflags = "2" bytes = { version = "1", default-features = false } capnp = "0.27" capnpc = "0.27" @@ -70,6 +74,7 @@ hashlink = "0" # kj-rs-io: `fs` for fcntl (KJ's fd flags), `signal` for ignoring SIGPIPE once per process like # kj::UnixEventPort does. nix = { version = "0", features = ["fs", "signal"] } +pastey = "0" pico-args = "0" # kj-hyper: WebSocket keys and frame masks. Every Cargo.toml merged into the crate_universe # workspace (downstream repos include this one alongside their own) must request the same diff --git a/docs/v8-updates.md b/docs/v8-updates.md index f7fd3848b67..622804a2fab 100644 --- a/docs/v8-updates.md +++ b/docs/v8-updates.md @@ -84,6 +84,11 @@ from the V8 directory. figure out what version of the github code V8 depends on. Instead, it should be safe to just bump these dependencies to the latest version on github. + When bumping `perfetto`, rebase `patches/perfetto/0004-*.patch`. It adds + `:libperfetto_c_over_client`, whose `srcs` and `deps` follow upstream's `:libperfetto_c`, + and exposes the Rust SDK sources. Then run `bazel test //src/rust/perfetto/...` to check + the Rust SDK still builds against the new C ABI and interoperates with the C++ SDK. + 11. Check workerd's tests pass with the updated V8. ```sh diff --git a/patches/perfetto/0004-Add-C-ABI-library-over-client-and-expose-Rust-SDK-sources.patch b/patches/perfetto/0004-Add-C-ABI-library-over-client-and-expose-Rust-SDK-sources.patch new file mode 100644 index 00000000000..f1e59392b0e --- /dev/null +++ b/patches/perfetto/0004-Add-C-ABI-library-over-client-and-expose-Rust-SDK-sources.patch @@ -0,0 +1,86 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: James M Snell +Date: Sat, 26 Sep 2026 00:00:00 +0000 +Subject: Add C ABI library over the client library and expose Rust SDK sources + + +diff --git a/BUILD b/BUILD +index e531db5..9c57994 100644 +--- a/BUILD ++++ b/BUILD +@@ -12275,5 +12275,48 @@ perfetto_py_binary( + + exports_files( + ["ui/src/assets/favicon.png"], + visibility = PERFETTO_CONFIG.public_visibility, + ) ++ ++# workerd: the Perfetto C ABI (src/shared_lib) layered on top of ++# :libperfetto_client_experimental instead of beside it. :libperfetto_c compiles its own copy of ++# the whole client/service stack, so linking it next to the C++ client would duplicate symbols and ++# produce a second perfetto::Tracing singleton. This target only adds the C ABI implementation, so ++# C ABI users (e.g. the Rust SDK in contrib/rust-sdk) and C++ SDK users share one Tracing instance ++# and one tracing session. Linked statically, so PERFETTO_SDK_SHLIB_IMPLEMENTATION is not defined. ++perfetto_cc_library( ++ name = "libperfetto_c_over_client", ++ srcs = [ ++ "src/tracing/internal/tracing_muxer_impl.h", ++ ":src_shared_lib_for_testing", ++ ":src_shared_lib_shared_lib", ++ ":src_shared_lib_track_event_intern_map", ++ ":src_shared_lib_track_event_track_event", ++ ], ++ hdrs = [ ++ ":include_perfetto_public_abi_base", ++ ":include_perfetto_public_abi_public", ++ ":include_perfetto_public_base", ++ ":include_perfetto_public_protos_protos", ++ ":include_perfetto_public_protozero", ++ ":include_perfetto_public_public", ++ ], ++ visibility = PERFETTO_CONFIG.public_visibility, ++ deps = [ ++ ":libperfetto_client_experimental", ++ ":protos_perfetto_common_cpp", ++ ":protos_perfetto_common_zero", ++ ":protos_perfetto_config_cpp", ++ ":protos_perfetto_config_track_event_cpp", ++ ":protos_perfetto_config_track_event_zero", ++ ":protos_perfetto_config_zero", ++ ":protos_perfetto_trace_interned_data_zero", ++ ":protos_perfetto_trace_minimal_zero", ++ ":protos_perfetto_trace_non_minimal_zero", ++ ":protos_perfetto_trace_track_event_cpp", ++ ":protos_perfetto_trace_track_event_zero", ++ ":protozero", ++ ":src_base_base", ++ ], ++ linkstatic = True, ++) +diff --git a/contrib/rust-sdk/BUILD.bazel b/contrib/rust-sdk/BUILD.bazel +new file mode 100644 +index 0000000..301b71c +--- /dev/null ++++ b/contrib/rust-sdk/BUILD.bazel +@@ -0,0 +1,21 @@ ++# workerd: exposes the Rust SDK sources so embedders can define their own rust_library targets ++# (this repository does not depend on rules_rust). perfetto-sys is used with its checked-in ++# bindings.rs and linked against //:libperfetto_c_over_client; its build.rs is not used. ++ ++package(default_visibility = ["//visibility:public"]) ++ ++exports_files([ ++ "perfetto-sys/src/lib.rs", ++ "perfetto/src/lib.rs", ++ "perfetto/README.md", ++]) ++ ++filegroup( ++ name = "perfetto_sys_srcs", ++ srcs = glob(["perfetto-sys/src/**/*.rs"]), ++) ++ ++filegroup( ++ name = "perfetto_srcs", ++ srcs = glob(["perfetto/src/**/*.rs"]), ++) diff --git a/src/rust/AGENTS.md b/src/rust/AGENTS.md index ca4f2686b39..aa6a243af7c 100644 --- a/src/rust/AGENTS.md +++ b/src/rust/AGENTS.md @@ -28,6 +28,9 @@ _Snapshot — the set drifts as crates come and go; `bazel query //src/rust/...` | `transpiler/` | TS type stripping via SWC (`ts_strip()`, `StripOnly` mode) | | `python-parser/` | Python import extraction via `ruff_python_parser`; **namespace: `edgeworker::rust::`** | | `gen-compile-cache/` | Binary crate — V8 bytecode cache generator; calls C++ `compile()` via CXX | +| `perfetto/` | Crate `perfetto`: Perfetto trace point macros (`trace_event!` etc.) writing into the C++-owned session; no-op unless built with Rust Perfetto (see PERFETTO TRACING) | +| `perfetto/sdk/` | `rust_library` targets for the Perfetto Rust SDK built from the `@perfetto` archive; only `perfetto/` may depend on them | +| `perfetto/test/` | Non-production crate; its test checks Rust and C++ events share one session with matching flow/track/counter IDs, in every build configuration | ## CONVENTIONS @@ -73,6 +76,20 @@ mod tests; - `wd_rust_crate`, `wd_rust_binary` and `wd_rust_proc_macro` take the `*-test.rs` files out of the crate's `srcs` and compile them only into its `_test` target (`split_rust_test_srcs` in `build/wd_rust_test.bzl`). A crate defined with `rust_library` directly does the same by hand: `exclude = ["*-test.rs"]` in the library's `srcs` glob, and `compile_data = glob(["*-test.rs"])` on its `wd_rust_test` (a `rust_test` built from a `crate` rejects `srcs`). An unconditional `mod` declaration of a test file therefore fails the production build. - Crates that are test code throughout are exempt: a test-support crate in a `tests/` directory (e.g. `cxx/kj-rs/tests/`), and a `wd_rust_test` built from its own `srcs` (e.g. `tsan/`). +## PERFETTO TRACING + +Rust trace points use the `perfetto` crate (`//src/rust/perfetto`); the crate docs in `lib.rs` cover the macro API and its C++ equivalents. + +- **Backend**: the Perfetto Rust SDK, built from `@perfetto//contrib/rust-sdk` by `perfetto/sdk/` and linked against `@perfetto//:libperfetto_c_over_client` (added by `patches/perfetto/0004-*.patch`). That library layers Perfetto's C ABI over the C++ client library, so Rust, C++ and V8 events share one `perfetto::Tracing` instance and one session. Don't depend on upstream `:libperfetto_c`: it bundles a second copy of the client. +- **Build flag**: the backend is compiled in (crate feature `perfetto`) only under `//src/workerd/util:really_use_rust_perfetto` = `really_use_perfetto` && `--//src/workerd/util:use_rust_perfetto`. The flag defaults to off and workerd's `.bazelrc` turns it on in `build:perfetto`, because embedders don't read that file and may have no C ABI or SDK targets in their `@perfetto`. Keep every reference to those targets inside a `select()` on `really_use_rust_perfetto`; the `perfetto/sdk/` targets are incompatible otherwise. +- **Both configurations must build**: callers compile against either `enabled.rs` or `disabled.rs`. The public types (`Flow`, `Track`, `EventContext`, `ScopedEvent`, …) are shared, and static assertions pin their auto traits. `disabled.rs` type-checks closures behind a constant `false` and never runs them. When changing the crate, lint and test both ways: `just clippy perfetto`, then the same with `--//src/workerd/util:use_rust_perfetto=False`. For tests, also try `--config=no-perfetto`. +- **Categories**: listed in `define_categories!` in `lib.rs`, which generates both `CATEGORIES` and the SDK's category table in the same order, so indices match. Macros resolve category names at compile time, so a typo is a compile error. A name shared with a C++ category (currently `workerd`) is enabled for both languages by the same trace config. +- **Registration**: C++ owns Perfetto initialization. `PerfettoSession::registerWorkerdTracks()` (`src/workerd/util/perfetto-tracing.c++`) calls `workerd::rust::perfetto::register_track_events()` under `WORKERD_USE_RUST_PERFETTO`; that runs `TrackEvent::init()` (the C ABI's `PerfettoTeInit`) and registers the categories. Until then every Rust category reads as disabled. +- **IDs**: `Flow::from_ref`/`from_ptr`, `Track::from_ref`/`from_ptr` and `trace_counter!` compute IDs the same way as C++ `PERFETTO_FLOW_FROM_POINTER`, `PERFETTO_TRACK_FROM_POINTER` and `perfetto::CounterTrack(name)`, all relative to the process track UUID. +- **Cost**: a disabled trace point costs two atomic loads. An enabled one takes a process-wide mutex in the SDK's generated `emit()` (it guards a `static mut` category table because `TrackEventCategory::emit` takes `&mut self`) and allocates the event's extras. Fixing that means patching the SDK. +- **Crate dependencies**: the SDK needs `bitflags`, `thiserror` and `paste`. `paste` is archived upstream, so `perfetto/sdk/` aliases the drop-in `pastey` to `paste`. Embedders that splice `deps/rust/Cargo.toml` into their own crate repository must repin when it changes. +- **Async**: `trace_event!` is scope-based on the thread track; don't hold it across `.await`. Use `trace_event_begin!`/`trace_event_end!` on a `Track` instead. + ## CXX BRIDGE: ASYNC AND ERROR HANDLING ### C++ calling async Rust (`extern "Rust"`) diff --git a/src/rust/perfetto/BUILD.bazel b/src/rust/perfetto/BUILD.bazel new file mode 100644 index 00000000000..740710959f3 --- /dev/null +++ b/src/rust/perfetto/BUILD.bazel @@ -0,0 +1,28 @@ +load("//:build/wd_rust_crate.bzl", "wd_rust_crate") + +# Perfetto trace points for Rust. The SDK backend is only compiled in (feature "perfetto") under +# //src/workerd/util:really_use_rust_perfetto; otherwise every macro compiles to a no-op and this +# crate has no dependency on Perfetto. Keep all references to Perfetto targets inside these +# selects so embedders that don't provide them can still build this crate. +wd_rust_crate( + name = "perfetto", + srcs = [ + "disabled.rs", + "enabled.rs", + "ffi.rs", + "lib.rs", + "lib-test.rs", + ], + crate_features = select({ + "//src/workerd/util:really_use_rust_perfetto": ["perfetto"], + "//conditions:default": [], + }), + cxx_bridge_src = "ffi.rs", + visibility = ["//visibility:public"], + deps = [ + "@crates_vendor//:static_assertions", + ] + select({ + "//src/workerd/util:really_use_rust_perfetto": ["//src/rust/perfetto/sdk:perfetto_sdk"], + "//conditions:default": [], + }), +) diff --git a/src/rust/perfetto/disabled.rs b/src/rust/perfetto/disabled.rs new file mode 100644 index 00000000000..870a09db520 --- /dev/null +++ b/src/rust/perfetto/disabled.rs @@ -0,0 +1,79 @@ +//! Backend used when Perfetto is not compiled in. Every function has the same signature as in +//! `enabled.rs`. Closures are still type-checked (behind `if false`), so code that builds in one +//! configuration builds in the other, but they're never called and no event is emitted. + +use std::ffi::CStr; +use std::marker::PhantomData; + +use crate::CounterValue; +use crate::DebugArg; +use crate::Flow; +use crate::Track; + +pub const fn register_track_events() {} + +pub const fn process_track_uuid() -> u64 { + 0 +} + +#[inline] +pub const fn is_enabled(_category: usize) -> bool { + false +} + +pub struct EventContext<'a> { + _marker: PhantomData<&'a mut ()>, +} + +impl EventContext<'_> { + #[expect(clippy::unused_self, reason = "mirrors enabled.rs")] + pub const fn add_arg(&self, _name: &str, _value: DebugArg<'_>) {} + + #[expect(clippy::unused_self, reason = "mirrors enabled.rs")] + pub const fn set_track(&self, _track: Track) {} + + #[expect(clippy::unused_self, reason = "mirrors enabled.rs")] + pub const fn set_flow(&self, _flow: Flow) {} + + #[expect(clippy::unused_self, reason = "mirrors enabled.rs")] + pub const fn set_terminating_flow(&self, _flow: Flow) {} +} + +// Guards code that must type-check but never run. +const TYPE_CHECK_ONLY: bool = false; + +#[inline] +fn type_check_only(f: impl FnOnce(EventContext<'_>)) { + if TYPE_CHECK_ONLY { + f(EventContext { + _marker: PhantomData, + }); + } +} + +#[inline] +pub fn emit_instant(_category: usize, _name: &'static CStr, f: impl FnOnce(EventContext<'_>)) { + type_check_only(f); +} + +#[inline] +pub fn emit_begin( + _category: usize, + _name: &'static CStr, + f: impl FnOnce(EventContext<'_>), +) -> bool { + type_check_only(f); + false +} + +#[inline] +pub fn emit_end(_category: usize, f: impl FnOnce(EventContext<'_>)) { + type_check_only(f); +} + +#[inline] +pub fn emit_counter(_category: usize, _name: &'static str, value: impl FnOnce() -> CounterValue) { + if TYPE_CHECK_ONLY { + let _ = value(); + } +} diff --git a/src/rust/perfetto/enabled.rs b/src/rust/perfetto/enabled.rs new file mode 100644 index 00000000000..9a097e07511 --- /dev/null +++ b/src/rust/perfetto/enabled.rs @@ -0,0 +1,141 @@ +//! Backend used when Perfetto is compiled in: forwards to the Perfetto Rust SDK, which writes +//! through the Perfetto C ABI into the process's (C++-owned) Perfetto instance. + +use std::ffi::CStr; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; + +use perfetto_sdk::protos::trace::track_event::track_descriptor::TrackDescriptorFieldNumber; +use perfetto_sdk::track_event::EventContext as SdkContext; +use perfetto_sdk::track_event::TrackEvent; +use perfetto_sdk::track_event::TrackEventCounter; +use perfetto_sdk::track_event::TrackEventDebugArg; +use perfetto_sdk::track_event::TrackEventFlow; +use perfetto_sdk::track_event::TrackEventProtoField; +use perfetto_sdk::track_event::TrackEventProtoTrack; +use perfetto_sdk::track_event::TrackEventTrack; +use perfetto_sdk::track_event::TrackEventType; + +use crate::CounterValue; +use crate::DebugArg; +use crate::Flow; +use crate::Track; +use crate::sdk::categories as sdk_categories; +use crate::truncate_at_nul; + +// Set once the SDK's category table is populated. `is_category_enabled()` reads each category's +// `enabled` pointer without the lock `register()` writes it under, so it must not run before +// registration finishes. Once registered, the pointers don't change. +static CATEGORIES_READY: AtomicBool = AtomicBool::new(false); + +pub fn register_track_events() { + // Registers the C ABI's track_event data source. A no-op when already done. + TrackEvent::init(); + // The only error is "already registered", which is fine. + let _ = sdk_categories::register(); + CATEGORIES_READY.store(true, Ordering::Release); +} + +pub fn process_track_uuid() -> u64 { + TrackEventTrack::process_track_uuid() +} + +#[inline] +pub fn is_enabled(category: usize) -> bool { + CATEGORIES_READY.load(Ordering::Acquire) && sdk_categories::is_category_enabled(category) +} + +pub struct EventContext<'a> { + ctx: &'a mut SdkContext, +} + +impl EventContext<'_> { + pub fn add_arg(&mut self, name: &str, value: DebugArg<'_>) { + let value = match value { + DebugArg::Bool(v) => TrackEventDebugArg::Bool(v), + DebugArg::Int(v) => TrackEventDebugArg::Int64(v), + DebugArg::Uint(v) => TrackEventDebugArg::Uint64(v), + DebugArg::Double(v) => TrackEventDebugArg::Double(v), + // The SDK copies strings into a CString, which must not contain NUL. + DebugArg::Str(v) => TrackEventDebugArg::String(truncate_at_nul(v)), + DebugArg::Pointer(v) => TrackEventDebugArg::Pointer(v), + }; + self.ctx.add_debug_arg(name, value); + } + + pub fn set_track(&mut self, track: Track) { + // An anonymous child of the process track, like C++ `perfetto::Track(id)`. + let parent = process_track_uuid(); + self.ctx.set_proto_track(&TrackEventProtoTrack { + uuid: track.uuid(), + fields: &[TrackEventProtoField::VarInt( + TrackDescriptorFieldNumber::ParentUuid as u32, + parent, + )], + }); + } + + pub fn set_flow(&mut self, flow: Flow) { + self.ctx.set_flow(&TrackEventFlow::global_flow(flow.id())); + } + + pub fn set_terminating_flow(&mut self, flow: Flow) { + self.ctx + .set_terminating_flow(&TrackEventFlow::global_flow(flow.id())); + } +} + +fn emit(category: usize, variant: TrackEventType, f: impl FnOnce(EventContext<'_>)) { + let mut ctx = SdkContext::default(); + f(EventContext { ctx: &mut ctx }); + sdk_categories::emit(category, variant, &mut ctx); +} + +#[inline] +pub fn emit_instant(category: usize, name: &'static CStr, f: impl FnOnce(EventContext<'_>)) { + if is_enabled(category) { + emit(category, TrackEventType::Instant(name.as_ptr()), f); + } +} + +/// Returns whether the event was emitted. +#[inline] +pub fn emit_begin(category: usize, name: &'static CStr, f: impl FnOnce(EventContext<'_>)) -> bool { + if is_enabled(category) { + emit(category, TrackEventType::SliceBegin(name.as_ptr()), f); + true + } else { + false + } +} + +#[inline] +pub fn emit_end(category: usize, f: impl FnOnce(EventContext<'_>)) { + if is_enabled(category) { + emit(category, TrackEventType::SliceEnd, f); + } +} + +#[inline] +pub fn emit_counter(category: usize, name: &'static str, value: impl FnOnce() -> CounterValue) { + if !is_enabled(category) { + return; + } + let name = truncate_at_nul(name); + // Same UUID as C++ `perfetto::CounterTrack(name)`, so both languages share the track. + let parent = process_track_uuid(); + let mut ctx = SdkContext::default(); + ctx.set_proto_track(&TrackEventProtoTrack { + uuid: TrackEventTrack::counter_track_uuid(name, parent), + fields: &[ + TrackEventProtoField::VarInt(TrackDescriptorFieldNumber::ParentUuid as u32, parent), + TrackEventProtoField::Cstr(TrackDescriptorFieldNumber::Name as u32, name), + TrackEventProtoField::Bytes(TrackDescriptorFieldNumber::Counter as u32, &[]), + ], + }); + ctx.set_counter(match value() { + CounterValue::Int(v) => TrackEventCounter::Int64(v), + CounterValue::Double(v) => TrackEventCounter::Double(v), + }); + sdk_categories::emit(category, TrackEventType::Counter, &mut ctx); +} diff --git a/src/rust/perfetto/ffi.rs b/src/rust/perfetto/ffi.rs new file mode 100644 index 00000000000..6fa39d3128b --- /dev/null +++ b/src/rust/perfetto/ffi.rs @@ -0,0 +1,13 @@ +#![allow(unsafe_code, reason = "the cxx bridge expands to unsafe code")] + +#[cxx::bridge(namespace = "workerd::rust::perfetto")] +mod bridge { + extern "Rust" { + /// Registers the Rust track event categories with Perfetto. Called by + /// `workerd::PerfettoSession::registerWorkerdTracks()` after `perfetto::Tracing` has been + /// initialized. + fn register_track_events(); + } +} + +use crate::register_track_events; diff --git a/src/rust/perfetto/lib-test.rs b/src/rust/perfetto/lib-test.rs new file mode 100644 index 00000000000..187a0603e39 --- /dev/null +++ b/src/rust/perfetto/lib-test.rs @@ -0,0 +1,41 @@ +use super::*; + +#[test] +fn category_index_is_const() { + const WORKERD: usize = __private::category_index("workerd"); + assert_eq!(CATEGORIES[WORKERD], "workerd"); +} + +#[test] +fn truncates_at_nul() { + assert_eq!(truncate_at_nul("abc"), "abc"); + assert_eq!(truncate_at_nul("a\0bc"), "a"); + assert_eq!(truncate_at_nul(""), ""); +} + +// Trace points must compile and be callable in both build configurations, whether or not +// Perfetto has been initialized. +#[test] +fn macros_compile_without_session() { + let value = 42i64; + let name = String::from("name"); + trace_event!("workerd", "scoped"); + trace_event!("workerd", "scoped with args", |ctx| { + ctx.add_arg("value", value) + .add_arg("name", &name) + .add_arg("flag", true) + .add_arg("ratio", 0.5) + .set_flow(Flow::from_ref(&value)); + }); + trace_event_begin!("workerd", "begin", |ctx| { + ctx.set_track(Track::from_ref(&value)); + }); + trace_event_end!("workerd", |ctx| { + ctx.set_track(Track::from_ref(&value)) + .set_terminating_flow(Flow::global(1)); + }); + trace_event_instant!("workerd", "instant"); + trace_counter!("workerd", "counter", value); + trace_counter!("workerd", "counter", 1.5); + assert!(!trace_event_category_enabled!("workerd")); +} diff --git a/src/rust/perfetto/lib.rs b/src/rust/perfetto/lib.rs new file mode 100644 index 00000000000..874d381f147 --- /dev/null +++ b/src/rust/perfetto/lib.rs @@ -0,0 +1,624 @@ +//! Perfetto trace points for Rust code in workerd. +//! +//! Rust trace points are written into the same Perfetto session as workerd's C++ `TRACE_EVENT`s +//! and V8's events, so they appear on the same timeline in one trace file. The macros mirror the +//! C++ ones from `src/workerd/util/perfetto-tracing.h`: +//! +//! | C++ | Rust | +//! | ------------------------------------ | ---------------------------------------------- | +//! | `TRACE_EVENT` | [`trace_event!`] | +//! | `TRACE_EVENT_BEGIN` | [`trace_event_begin!`] | +//! | `TRACE_EVENT_END` | [`trace_event_end!`] | +//! | `TRACE_EVENT_INSTANT` | [`trace_event_instant!`] | +//! | `TRACE_COUNTER` | [`trace_counter!`] | +//! | `TRACE_EVENT_CATEGORY_ENABLED` | [`trace_event_category_enabled!`] | +//! | `PERFETTO_FLOW_FROM_POINTER` | [`Flow::from_ref`], [`Flow::from_ptr`] | +//! | `PERFETTO_TERMINATING_FLOW_FROM_POINTER` | [`EventContext::set_terminating_flow`] | +//! | `PERFETTO_TRACK_FROM_POINTER` | [`Track::from_ref`], [`Track::from_ptr`] | +//! +//! Arguments go in a closure instead of trailing macro arguments: +//! `trace_event!("workerd", "Foo::bar()", |ctx| { ctx.add_arg("id", id); })`. +//! +//! ```ignore +//! use perfetto::{trace_event, Flow}; +//! +//! fn handle(&self, url: &str) { +//! trace_event!("workerd", "Handler::handle()", |ctx| { +//! ctx.add_arg("url", url).set_flow(Flow::from_ref(self)); +//! }); +//! // ... +//! } +//! ``` +//! +//! Categories are string literals checked at compile time against [`CATEGORIES`]. Event names are +//! string literals too. The closure that adds arguments only runs when the category is enabled in +//! the active session, so it must not have side effects beyond filling in the [`EventContext`]. +//! +//! Flow and track IDs are computed the same way as in the C++ SDK, so a flow started in C++ with +//! `PERFETTO_FLOW_FROM_POINTER(p)` connects to `Flow::from_ref(p)` in Rust, and begin/end events on +//! `PERFETTO_TRACK_FROM_POINTER(p)` and `Track::from_ref(p)` land on the same track. +//! +//! # Build configurations +//! +//! The Perfetto backend is compiled in only when the `perfetto` feature is enabled, which Bazel +//! does under `//src/workerd/util:really_use_rust_perfetto`. Without it every macro still +//! type-checks its arguments but compiles to nothing, and this crate has no Perfetto dependency. +//! Code using these macros must build in both configurations; the public types are identical in +//! both, including their auto traits. +//! +//! # Cost +//! +//! A trace point whose category is disabled costs two atomic loads. An enabled one is more +//! expensive than its C++ counterpart: the Perfetto Rust SDK takes a process-wide mutex for every +//! emitted event (its generated category table is a `static mut` that it guards with a lock) and +//! allocates for the event's extras. Keep that in mind before adding trace points to hot paths. +//! +//! # Async code +//! +//! A [`trace_event!`] slice is tied to the enclosing scope on the current thread track. Don't hold +//! one across an `.await`: other tasks run on the same thread in the meantime and the slices no +//! longer nest, and the task may resume on another thread. [`ScopedEvent`] isn't `Send`, so a +//! `Send` future that holds one across an `.await` doesn't compile. For work that awaits, emit [`trace_event_begin!`] / [`trace_event_end!`] on a +//! [`Track`] derived from an object that lives for the duration of the work. +//! +//! # Initialization +//! +//! The C++ side owns Perfetto initialization. `workerd::PerfettoSession::registerWorkerdTracks()` +//! calls into this crate to register the Rust categories after `perfetto::Tracing::Initialize()`. +//! Until then all Rust categories read as disabled. + +// Production code must not panic; test code is exempt via clippy.toml allow-*-in-tests. +#![deny(clippy::expect_used, clippy::panic, clippy::unreachable)] +#![deny(clippy::todo, clippy::unimplemented)] + +use std::ffi::CStr; +use std::marker::PhantomData; + +#[cfg(not(feature = "perfetto"))] +mod disabled; +#[cfg(feature = "perfetto")] +mod enabled; +mod ffi; + +#[cfg(not(feature = "perfetto"))] +use disabled as backend; +#[cfg(feature = "perfetto")] +use enabled as backend; + +/// Defines the Rust track event categories. Each entry is `(name, description)`. +/// +/// The Perfetto SDK's category table is generated from the same list, so the index of a category +/// in [`CATEGORIES`] is also its index in the SDK. +macro_rules! define_categories { + ($(($name:literal, $description:literal)),+ $(,)?) => { + /// Names of the track event categories that Rust code can emit into. + pub const CATEGORIES: &[&str] = &[$($name),+]; + + #[cfg(feature = "perfetto")] + #[expect( + clippy::significant_drop_tightening, + reason = "in the Perfetto SDK's generated code" + )] + mod sdk { + perfetto_sdk::track_event_categories! { + pub(crate) mod categories { + $(($name, $description, [])),+ + } + } + } + }; +} + +define_categories! { + // Shares its name with the C++ `workerd` category, so enabling `workerd` in a trace config + // enables trace points in both languages. This keeps a module's trace points under the same + // category when it's ported from C++ to Rust. + ("workerd", "workerd runtime events"), +} + +/// Whether this build includes the Perfetto backend. When `false`, all trace points are no-ops. +pub const ENABLED_IN_BUILD: bool = cfg!(feature = "perfetto"); + +/// Registers the Rust track event categories with Perfetto. +/// +/// Called by C++ (`PerfettoSession::registerWorkerdTracks()`) after Perfetto has been initialized. +/// Calling it more than once is harmless. +pub fn register_track_events() { + backend::register_track_events(); +} + +/// A debug annotation value attached to an event with [`EventContext::add_arg`]. +#[derive(Debug, Clone, Copy)] +pub enum DebugArg<'a> { + Bool(bool), + Int(i64), + Uint(u64), + Double(f64), + /// Strings are truncated at the first NUL byte. + Str(&'a str), + Pointer(usize), +} + +macro_rules! debug_arg_from { + ($variant:ident($target:ty): $($source:ty),+) => { + $( + impl From<$source> for DebugArg<'_> { + fn from(value: $source) -> Self { + Self::$variant(<$target>::from(value)) + } + } + )+ + }; +} + +debug_arg_from!(Bool(bool): bool); +debug_arg_from!(Int(i64): i8, i16, i32, i64); +debug_arg_from!(Uint(u64): u8, u16, u32, u64); +debug_arg_from!(Double(f64): f32, f64); + +impl From for DebugArg<'_> { + fn from(value: usize) -> Self { + // usize is at most 64 bits on every supported target. + Self::Uint(value as u64) + } +} + +impl From for DebugArg<'_> { + fn from(value: isize) -> Self { + // isize is at most 64 bits on every supported target. + Self::Int(value as i64) + } +} + +impl<'a> From<&'a str> for DebugArg<'a> { + fn from(value: &'a str) -> Self { + Self::Str(value) + } +} + +impl<'a> From<&'a String> for DebugArg<'a> { + fn from(value: &'a String) -> Self { + Self::Str(value.as_str()) + } +} + +/// A value recorded with [`trace_counter!`]. +#[derive(Debug, Clone, Copy)] +pub enum CounterValue { + Int(i64), + Double(f64), +} + +macro_rules! counter_value_from { + ($variant:ident($target:ty): $($source:ty),+) => { + $( + impl From<$source> for CounterValue { + fn from(value: $source) -> Self { + Self::$variant(<$target>::from(value)) + } + } + )+ + }; +} + +counter_value_from!(Int(i64): i8, i16, i32, i64, u8, u16, u32); +counter_value_from!(Double(f64): f32, f64); + +/// Connects events across threads and across the C++/Rust boundary. +/// +/// Attach it to the events to connect with [`EventContext::set_flow`], and to the last one with +/// [`EventContext::set_terminating_flow`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Flow { + id: u64, + process_scoped: bool, +} + +impl Flow { + /// A flow identified by an object's address. Equivalent to C++ + /// `PERFETTO_FLOW_FROM_POINTER(&object)`. + /// + /// The address can be reused once the object is freed, so terminate the flow before that. + #[must_use] + pub fn from_ref(object: &T) -> Self { + Self::from_ptr(std::ptr::from_ref(object)) + } + + /// A flow identified by an address. Equivalent to C++ `PERFETTO_FLOW_FROM_POINTER(ptr)`. + #[must_use] + pub fn from_ptr(ptr: *const T) -> Self { + Self::process_scoped(address(ptr)) + } + + /// A flow identified by an ID that is unique within this process. Equivalent to C++ + /// `perfetto::Flow::ProcessScoped(id)`. + #[must_use] + pub const fn process_scoped(id: u64) -> Self { + Self { + id, + process_scoped: true, + } + } + + /// A flow identified by a globally unique ID. Equivalent to C++ `perfetto::Flow::Global(id)`. + #[must_use] + pub const fn global(id: u64) -> Self { + Self { + id, + process_scoped: false, + } + } + + /// The flow ID as written to the trace. Process-scoped IDs incorporate the process track + /// UUID, which is only known once Perfetto is initialized; it's 0 in builds without Perfetto. + #[must_use] + pub fn id(&self) -> u64 { + if self.process_scoped { + self.id ^ backend::process_track_uuid() + } else { + self.id + } + } +} + +/// An anonymous track, child of the process track, for begin/end events that don't belong on the +/// current thread's track (typically async work). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Track { + id: u64, +} + +impl Track { + /// A track identified by an object's address. Equivalent to C++ + /// `PERFETTO_TRACK_FROM_POINTER(&object)`. + #[must_use] + pub fn from_ref(object: &T) -> Self { + Self::from_ptr(std::ptr::from_ref(object)) + } + + /// A track identified by an address. Equivalent to C++ `PERFETTO_TRACK_FROM_POINTER(ptr)`. + #[must_use] + pub fn from_ptr(ptr: *const T) -> Self { + Self::new(address(ptr)) + } + + /// A track identified by an ID that is unique within this process. Equivalent to C++ + /// `perfetto::Track(id)`. + #[must_use] + pub const fn new(id: u64) -> Self { + Self { id } + } + + /// The track UUID as written to the trace. It incorporates the process track UUID, which is + /// only known once Perfetto is initialized; it's 0 in builds without Perfetto. + #[must_use] + pub fn uuid(&self) -> u64 { + self.id ^ backend::process_track_uuid() + } +} + +/// Extra data for an event. Passed to the closure given to the trace macros, which only runs when +/// the event's category is enabled. +pub struct EventContext<'a> { + inner: backend::EventContext<'a>, + // The track set with `set_track`, which a `trace_event!` slice also ends on. + track: Option, + // Not Send or Sync in either build configuration. + _not_send: PhantomData<*mut ()>, +} + +impl EventContext<'_> { + /// Adds a debug annotation. Names are truncated at the first NUL byte. + pub fn add_arg<'v>(&mut self, name: &str, value: impl Into>) -> &mut Self { + self.inner.add_arg(truncate_at_nul(name), value.into()); + self + } + + /// Puts the event on `track` instead of the current thread's track. Begin and end events of a + /// slice must use the same track; a [`trace_event!`] slice ends on the track set here. + pub fn set_track(&mut self, track: Track) -> &mut Self { + self.inner.set_track(track); + self.track = Some(track); + self + } + + /// Connects the event to other events with the same flow. + pub fn set_flow(&mut self, flow: Flow) -> &mut Self { + self.inner.set_flow(flow); + self + } + + /// Connects the event to other events with the same flow and ends the flow. + pub fn set_terminating_flow(&mut self, flow: Flow) -> &mut Self { + self.inner.set_terminating_flow(flow); + self + } +} + +/// Ends the slice started by [`trace_event!`] when dropped, on the track where it began. +/// +/// Not `Send`: unless the slice was put on a [`Track`] with [`EventContext::set_track`], it +/// ends on the track of the thread that drops it, which must be the thread where it began. +#[must_use] +pub struct ScopedEvent { + category: usize, + // Whether the begin event was emitted; the end event is only emitted if so. + active: bool, + // The track set on the begin event, if any. + track: Option, + // A scoped event must be dropped on the thread where its slice began. + _not_send: PhantomData<*mut ()>, +} + +impl Drop for ScopedEvent { + fn drop(&mut self) { + if self.active { + let track = self.track; + __private::end(self.category, |ctx| { + if let Some(track) = track { + ctx.set_track(track); + } + }); + } + } +} + +// Code using the facade must compile in both build configurations, so the public types must have +// the same auto traits in both. These assertions are checked in both configurations. +static_assertions::assert_impl_all!(Flow: Send, Sync, Unpin); +static_assertions::assert_impl_all!(Track: Send, Sync, Unpin); +static_assertions::assert_impl_all!(ScopedEvent: Unpin); +static_assertions::assert_not_impl_any!(ScopedEvent: Send, Sync); +static_assertions::assert_impl_all!(DebugArg<'static>: Send, Sync, Unpin); +static_assertions::assert_impl_all!(CounterValue: Send, Sync, Unpin); +static_assertions::assert_not_impl_any!(EventContext<'static>: Send, Sync); + +fn address(ptr: *const T) -> u64 { + // Addresses are at most 64 bits on every supported target. + ptr.cast::<()>().addr() as u64 +} + +fn truncate_at_nul(s: &str) -> &str { + s.find('\0').map_or(s, |end| &s[..end]) +} + +/// Implementation details of the macros. Not part of the public API. +#[doc(hidden)] +pub mod __private { + use super::CATEGORIES; + use super::CStr; + use super::CounterValue; + use super::EventContext; + use super::PhantomData; + use super::ScopedEvent; + use super::backend; + + /// Returns the index of `category` in [`CATEGORIES`]. Evaluated at compile time by the macros, + /// so an unknown category is a compile error. + #[expect( + clippy::panic, + reason = "only evaluated in const contexts, where a panic is a compile error" + )] + #[must_use] + pub const fn category_index(category: &str) -> usize { + let mut i = 0; + while i < CATEGORIES.len() { + if str_eq(CATEGORIES[i], category) { + return i; + } + i += 1; + } + panic!("unknown Perfetto category; add it to perfetto::CATEGORIES") + } + + /// Converts a NUL-terminated string literal into a `CStr`. Evaluated at compile time by the + /// macros, so an event name with an interior NUL is a compile error. + #[expect( + clippy::panic, + reason = "only evaluated in const contexts, where a panic is a compile error" + )] + #[must_use] + pub const fn event_name(with_nul: &'static str) -> &'static CStr { + match CStr::from_bytes_with_nul(with_nul.as_bytes()) { + Ok(name) => name, + Err(_) => panic!("Perfetto event names must not contain NUL"), + } + } + + const fn str_eq(a: &str, b: &str) -> bool { + let a = a.as_bytes(); + let b = b.as_bytes(); + if a.len() != b.len() { + return false; + } + let mut i = 0; + while i < a.len() { + if a[i] != b[i] { + return false; + } + i += 1; + } + true + } + + fn wrap(inner: backend::EventContext<'_>) -> EventContext<'_> { + EventContext { + inner, + track: None, + _not_send: PhantomData, + } + } + + // Each of these takes the closure through a generic `FnOnce(&mut EventContext)` bound so that + // closure parameter types are inferred at the call site. + + #[inline] + #[must_use] + pub fn is_enabled(category: usize) -> bool { + backend::is_enabled(category) + } + + #[inline] + pub fn instant(category: usize, name: &'static CStr, f: impl FnOnce(&mut EventContext<'_>)) { + backend::emit_instant(category, name, |ctx| f(&mut wrap(ctx))); + } + + #[inline] + pub fn begin(category: usize, name: &'static CStr, f: impl FnOnce(&mut EventContext<'_>)) { + backend::emit_begin(category, name, |ctx| f(&mut wrap(ctx))); + } + + #[inline] + pub fn end(category: usize, f: impl FnOnce(&mut EventContext<'_>)) { + backend::emit_end(category, |ctx| f(&mut wrap(ctx))); + } + + #[inline] + pub fn scoped( + category: usize, + name: &'static CStr, + f: impl FnOnce(&mut EventContext<'_>), + ) -> ScopedEvent { + let mut track = None; + let active = backend::emit_begin(category, name, |ctx| { + let mut ctx = wrap(ctx); + f(&mut ctx); + track = ctx.track; + }); + ScopedEvent { + category, + active, + track, + _not_send: PhantomData, + } + } + + #[inline] + pub fn counter(category: usize, name: &'static str, value: impl FnOnce() -> CounterValue) { + backend::emit_counter(category, name, value); + } +} + +/// Emits a slice covering the rest of the enclosing scope. Equivalent to C++ `TRACE_EVENT`. +/// +/// ```ignore +/// trace_event!("workerd", "Foo::bar()"); +/// trace_event!("workerd", "Foo::bar()", |ctx| { ctx.add_arg("id", id); }); +/// ``` +#[macro_export] +macro_rules! trace_event { + ($category:literal, $name:literal $(,)?) => { + $crate::trace_event!($category, $name, |_| {}) + }; + ($category:literal, $name:literal, $f:expr $(,)?) => { + let _workerd_perfetto_scoped_event = $crate::__private::scoped( + { + const CATEGORY: usize = $crate::__private::category_index($category); + CATEGORY + }, + { + const NAME: &::core::ffi::CStr = + $crate::__private::event_name(::core::concat!($name, "\0")); + NAME + }, + $f, + ); + }; +} + +/// Begins a slice. End it with [`trace_event_end!`] on the same track. Equivalent to C++ +/// `TRACE_EVENT_BEGIN`. +#[macro_export] +macro_rules! trace_event_begin { + ($category:literal, $name:literal $(,)?) => { + $crate::trace_event_begin!($category, $name, |_| {}) + }; + ($category:literal, $name:literal, $f:expr $(,)?) => { + $crate::__private::begin( + { + const CATEGORY: usize = $crate::__private::category_index($category); + CATEGORY + }, + { + const NAME: &::core::ffi::CStr = + $crate::__private::event_name(::core::concat!($name, "\0")); + NAME + }, + $f, + ) + }; +} + +/// Ends the most recent slice begun with [`trace_event_begin!`] on the same track. Equivalent to +/// C++ `TRACE_EVENT_END`. +#[macro_export] +macro_rules! trace_event_end { + ($category:literal $(,)?) => { + $crate::trace_event_end!($category, |_| {}) + }; + ($category:literal, $f:expr $(,)?) => { + $crate::__private::end( + { + const CATEGORY: usize = $crate::__private::category_index($category); + CATEGORY + }, + $f, + ) + }; +} + +/// Emits an instant event. Equivalent to C++ `TRACE_EVENT_INSTANT`. +#[macro_export] +macro_rules! trace_event_instant { + ($category:literal, $name:literal $(,)?) => { + $crate::trace_event_instant!($category, $name, |_| {}) + }; + ($category:literal, $name:literal, $f:expr $(,)?) => { + $crate::__private::instant( + { + const CATEGORY: usize = $crate::__private::category_index($category); + CATEGORY + }, + { + const NAME: &::core::ffi::CStr = + $crate::__private::event_name(::core::concat!($name, "\0")); + NAME + }, + $f, + ) + }; +} + +/// Records a counter value on a process-wide counter track named `name`. +/// +/// Equivalent to C++ `TRACE_COUNTER(category, name, value)`, and uses the same track as a C++ +/// counter with the same name. `value` is only evaluated when the category is enabled. +#[macro_export] +macro_rules! trace_counter { + ($category:literal, $name:literal, $value:expr $(,)?) => { + $crate::__private::counter( + { + const CATEGORY: usize = $crate::__private::category_index($category); + CATEGORY + }, + $name, + || $crate::CounterValue::from($value), + ) + }; +} + +/// Whether `category` is enabled in an active tracing session. Equivalent to C++ +/// `TRACE_EVENT_CATEGORY_ENABLED`. Always `false` in builds without Perfetto. +#[macro_export] +macro_rules! trace_event_category_enabled { + ($category:literal $(,)?) => { + $crate::__private::is_enabled({ + const CATEGORY: usize = $crate::__private::category_index($category); + CATEGORY + }) + }; +} + +#[cfg(test)] +#[path = "lib-test.rs"] +mod tests; diff --git a/src/rust/perfetto/sdk/BUILD.bazel b/src/rust/perfetto/sdk/BUILD.bazel new file mode 100644 index 00000000000..128615498ed --- /dev/null +++ b/src/rust/perfetto/sdk/BUILD.bazel @@ -0,0 +1,55 @@ +load("@rules_rust//rust:defs.bzl", "rust_library") + +# The Perfetto Rust SDK, built from the sources in the @perfetto archive (exposed by +# patches/perfetto/0004-*.patch) so that the checked-in bindings, the C headers and the C ABI +# implementation always come from the same Perfetto commit. perfetto-sys's build.rs, which would +# compile a vendored amalgamation, is not used: it links @perfetto//:libperfetto_c_over_client, +# which shares the C++ client library's perfetto::Tracing instance. +# +# Only //src/rust/perfetto should depend on these, and only under +# //src/workerd/util:really_use_rust_perfetto; see the comment on that flag. + +_COMPATIBLE = select({ + "//src/workerd/util:really_use_rust_perfetto": [], + "//conditions:default": ["@platforms//:incompatible"], +}) + +# Third-party code: don't apply workerd's lints to it. +_THIRD_PARTY_RUSTC_FLAGS = ["--cap-lints=allow"] + +_THIRD_PARTY_TAGS = [ + "noclippy", + "norustfmt", +] + +rust_library( + name = "perfetto_sdk_sys", + srcs = ["@perfetto//contrib/rust-sdk:perfetto_sys_srcs"], + crate_name = "perfetto_sdk_sys", + crate_root = "@perfetto//contrib/rust-sdk:perfetto-sys/src/lib.rs", + edition = "2024", + link_deps = ["@perfetto//:libperfetto_c_over_client"], + rustc_flags = _THIRD_PARTY_RUSTC_FLAGS, + tags = _THIRD_PARTY_TAGS, + target_compatible_with = _COMPATIBLE, +) + +rust_library( + name = "perfetto_sdk", + srcs = ["@perfetto//contrib/rust-sdk:perfetto_srcs"], + aliases = {"@crates_vendor//:pastey": "paste"}, + compile_data = ["@perfetto//contrib/rust-sdk:perfetto/README.md"], + crate_name = "perfetto_sdk", + crate_root = "@perfetto//contrib/rust-sdk:perfetto/src/lib.rs", + edition = "2024", + proc_macro_deps = ["@crates_vendor//:pastey"], + rustc_flags = _THIRD_PARTY_RUSTC_FLAGS, + tags = _THIRD_PARTY_TAGS, + target_compatible_with = _COMPATIBLE, + visibility = ["//src/rust/perfetto:__subpackages__"], + deps = [ + ":perfetto_sdk_sys", + "@crates_vendor//:bitflags", + "@crates_vendor//:thiserror", + ], +) diff --git a/src/rust/perfetto/test/BUILD.bazel b/src/rust/perfetto/test/BUILD.bazel new file mode 100644 index 00000000000..4fafd9c9319 --- /dev/null +++ b/src/rust/perfetto/test/BUILD.bazel @@ -0,0 +1,31 @@ +load("//:build/linking.bzl", "CC_LIBRARY_LINKSTATIC") +load("//:build/wd_cc_library.bzl", "wd_cc_library") +load("//:build/wd_rust_crate.bzl", "wd_rust_crate") + +# Non-production crate whose test checks that Rust and C++ trace points land in the same Perfetto +# session with matching flow and track IDs. It runs in every build configuration and checks what +# that configuration should produce: both languages' events, only C++ events (Perfetto without +# Rust Perfetto, as in embedders that don't enable it), or nothing (no Perfetto). +wd_rust_crate( + name = "perfetto-test", + testonly = True, + crate_features = select({ + "//src/workerd/util:really_use_rust_perfetto": ["rust_perfetto"], + "//conditions:default": [], + }), + cxx_bridge_src = "lib.rs", + link_deps = [":test-helper"], + deps = ["//src/rust/perfetto"], +) + +wd_cc_library( + name = "test-helper", + srcs = ["test-helper.c++"], + hdrs = ["test-helper.h"], + linkstatic = CC_LIBRARY_LINKSTATIC, + deps = [ + "//src/rust/cxx:core", + "//src/workerd/util:perfetto", + "@capnp-cpp//src/kj", + ], +) diff --git a/src/rust/perfetto/test/lib-test.rs b/src/rust/perfetto/test/lib-test.rs new file mode 100644 index 00000000000..888ab7d0515 --- /dev/null +++ b/src/rust/perfetto/test/lib-test.rs @@ -0,0 +1,299 @@ +use std::cell::Cell; +use std::collections::HashMap; + +use perfetto::Flow; +use perfetto::Track; +use perfetto::trace_counter; +use perfetto::trace_event; +use perfetto::trace_event_begin; +use perfetto::trace_event_category_enabled; +use perfetto::trace_event_end; +use perfetto::trace_event_instant; + +use super::ffi; + +// TrackEvent.Type +const TYPE_SLICE_BEGIN: u64 = 1; +const TYPE_SLICE_END: u64 = 2; +const TYPE_COUNTER: u64 = 4; + +enum Field<'a> { + Varint(u64), + Fixed64(u64), + Bytes(&'a [u8]), + Fixed32, +} + +fn read_varint(data: &mut &[u8]) -> u64 { + let mut result = 0u64; + let mut shift = 0; + loop { + let (&byte, rest) = data.split_first().unwrap(); + *data = rest; + result |= u64::from(byte & 0x7f) << shift; + if byte & 0x80 == 0 { + return result; + } + shift += 7; + } +} + +// Minimal protobuf decoder: yields (field number, value) for each field of a message. +fn fields(mut data: &[u8]) -> Vec<(u64, Field<'_>)> { + let mut result = Vec::new(); + while !data.is_empty() { + let key = read_varint(&mut data); + let field = match key & 7 { + 0 => Field::Varint(read_varint(&mut data)), + 1 => { + let (bytes, rest) = data.split_at(8); + data = rest; + Field::Fixed64(u64::from_le_bytes(bytes.try_into().unwrap())) + } + 2 => { + let len = usize::try_from(read_varint(&mut data)).unwrap(); + let (bytes, rest) = data.split_at(len); + data = rest; + Field::Bytes(bytes) + } + 5 => { + data = &data[4..]; + Field::Fixed32 + } + wire_type => panic!("unsupported wire type {wire_type}"), + }; + result.push((key >> 3, field)); + } + result +} + +#[derive(Debug, Default)] +struct Event { + name: Option, + kind: u64, + track_uuid: Option, + flow_ids: Vec, + terminating_flow_ids: Vec, + counter_value: Option, + debug_annotations: usize, +} + +// Decodes the track events of a serialized perfetto.protos.Trace, resolving interned names. +fn parse_trace(trace: &[u8]) -> Vec { + let mut interned_names = HashMap::new(); + let mut events = Vec::new(); + for (id, packet) in fields(trace) { + let Field::Bytes(packet) = packet else { + continue; + }; + if id != 1 { + continue; + } + let packet = fields(packet); + let sequence = packet + .iter() + .find_map(|(id, f)| match (id, f) { + // trusted_packet_sequence_id + (10, Field::Varint(v)) => Some(*v), + _ => None, + }) + .unwrap_or(0); + // Interned data comes before the events that use it within a packet sequence. + for (id, field) in &packet { + if let (12, Field::Bytes(interned)) = (id, field) { + for (id, entry) in fields(interned) { + // InternedData.event_names + let (2, Field::Bytes(entry)) = (id, entry) else { + continue; + }; + let mut iid = None; + let mut name = None; + for (id, f) in fields(entry) { + match (id, f) { + (1, Field::Varint(v)) => iid = Some(v), + (2, Field::Bytes(b)) => { + name = Some(String::from_utf8(b.to_vec()).unwrap()); + } + _ => {} + } + } + interned_names.insert((sequence, iid.unwrap()), name.unwrap()); + } + } + } + for (id, field) in &packet { + let (11, Field::Bytes(track_event)) = (id, field) else { + continue; + }; + let mut event = Event::default(); + for (id, f) in fields(track_event) { + match (id, f) { + (9, Field::Varint(v)) => event.kind = v, + (10, Field::Varint(iid)) => { + event.name = interned_names.get(&(sequence, iid)).cloned(); + } + (23, Field::Bytes(b)) => { + event.name = Some(String::from_utf8(b.to_vec()).unwrap()); + } + (11, Field::Varint(v)) => event.track_uuid = Some(v), + (4, Field::Bytes(_)) => event.debug_annotations += 1, + (47 | 36, Field::Fixed64(v) | Field::Varint(v)) => event.flow_ids.push(v), + (48, Field::Fixed64(v) | Field::Varint(v)) => { + event.terminating_flow_ids.push(v); + } + #[expect(clippy::cast_precision_loss, reason = "small test values")] + (30, Field::Varint(v)) => { + event.counter_value = Some(v.cast_signed() as f64); + } + (44, Field::Fixed64(v)) => event.counter_value = Some(f64::from_bits(v)), + _ => {} + } + } + events.push(event); + } + } + events +} + +fn named<'a>(events: &'a [Event], name: &str) -> Option<&'a Event> { + events.iter().find(|e| e.name.as_deref() == Some(name)) +} + +fn fnv1a(s: &str) -> u64 { + let mut hash = 14_695_981_039_346_656_037u64; + for b in s.bytes() { + hash ^= u64::from(b); + hash = hash.wrapping_mul(1_099_511_628_211); + } + hash +} + +// Takes a pointer because the events are identified by the address, not the value. +fn emit_rust_events(token: *const u64) { + trace_event!("workerd", "rust-scoped", |ctx| { + ctx.add_arg("answer", 42i64) + .add_arg("text", "hello") + .set_terminating_flow(Flow::from_ptr(token)); + }); + trace_event_instant!("workerd", "rust-instant"); + trace_event_begin!("workerd", "rust-begin", |ctx| { + ctx.set_track(Track::from_ptr(token)); + }); + trace_event_end!("workerd", |ctx| { + ctx.set_track(Track::from_ptr(token)); + }); + { + trace_event!("workerd", "rust-scoped-on-track", |ctx| { + ctx.set_track(Track::from_ptr(token)); + }); + } + trace_counter!("workerd", "shared-counter", 7); +} + +#[test] +fn cpp_and_rust_events_share_a_session() { + ffi::start_trace("workerd").unwrap(); + + let token = Box::new(0u64); + // The C++ events start a flow and use a track derived from `token`'s address; the Rust + // events end the flow and use the same track. + ffi::emit_cpp_events(std::ptr::from_ref::(&token).addr()); + let rust_enabled = trace_event_category_enabled!("workerd"); + emit_rust_events(std::ptr::from_ref::(&token)); + + // Computed after start_trace(), which initializes Perfetto and the process track UUID. + let expected_flow = Flow::from_ref::(&token).id(); + let expected_track = Track::from_ref::(&token).uuid(); + let process_uuid = Track::new(0).uuid(); + + let events = parse_trace(&ffi::stop_trace().unwrap()); + + if !ffi::perfetto_in_build() { + assert!(events.is_empty()); + assert!(!rust_enabled); + return; + } + + let cpp_scoped = named(&events, "cpp-scoped").expect("C++ event missing"); + let cpp_begin = named(&events, "cpp-begin").expect("C++ event missing"); + + if !cfg!(feature = "rust_perfetto") { + // Perfetto without Rust Perfetto: C++ events only. + assert!(!rust_enabled); + assert!(events.iter().all(|e| { + e.name + .as_deref() + .is_none_or(|name| !name.starts_with("rust-")) + })); + return; + } + assert!(rust_enabled); + + assert!(cpp_scoped.flow_ids.contains(&expected_flow), "{events:#?}"); + let rust_scoped = named(&events, "rust-scoped").expect("Rust event missing"); + assert_eq!(rust_scoped.kind, TYPE_SLICE_BEGIN); + assert!( + rust_scoped.terminating_flow_ids.contains(&expected_flow), + "{events:#?}" + ); + assert_eq!(rust_scoped.debug_annotations, 2); + assert!(named(&events, "rust-instant").is_some()); + + assert_eq!(cpp_begin.track_uuid, Some(expected_track)); + let rust_begin = named(&events, "rust-begin").expect("Rust event missing"); + assert_eq!(rust_begin.track_uuid, Some(expected_track)); + let rust_on_track = named(&events, "rust-scoped-on-track").expect("Rust event missing"); + assert_eq!(rust_on_track.track_uuid, Some(expected_track)); + // The ends of cpp-begin, rust-begin and rust-scoped-on-track. + assert!( + events + .iter() + .filter(|e| e.kind == TYPE_SLICE_END) + .filter(|e| e.track_uuid == Some(expected_track)) + .count() + == 3, + "{events:#?}" + ); + + // C++ `perfetto::CounterTrack(name)` UUID. + let counter_track = 0xb1a4_a67d_7970_839e ^ fnv1a("shared-counter") ^ process_uuid; + let counters: Vec<_> = events + .iter() + .filter(|e| e.kind == TYPE_COUNTER) + .map(|e| (e.track_uuid, e.counter_value)) + .collect(); + assert!( + counters.contains(&(Some(counter_track), Some(42.0))), + "{counters:?}" + ); + assert!( + counters.contains(&(Some(counter_track), Some(7.0))), + "{counters:?}" + ); +} + +#[test] +fn disabled_category_emits_nothing() { + // A session that doesn't enable `workerd`. + ffi::start_trace("v8").unwrap(); + let token = 0u64; + assert!(!trace_event_category_enabled!("workerd")); + emit_rust_events(&raw const token); + ffi::emit_cpp_events(std::ptr::from_ref(&token).addr()); + + // Closures and counter values are only evaluated when the category is enabled. + let evaluated = Cell::new(0); + let evaluate = || evaluated.set(evaluated.get() + 1); + trace_event!("workerd", "unevaluated-scoped", |_| evaluate()); + trace_event_instant!("workerd", "unevaluated-instant", |_| evaluate()); + trace_event_begin!("workerd", "unevaluated-begin", |_| evaluate()); + trace_event_end!("workerd", |_| evaluate()); + trace_counter!("workerd", "unevaluated-counter", { + evaluate(); + 1 + }); + assert_eq!(evaluated.get(), 0); + + let events = parse_trace(&ffi::stop_trace().unwrap()); + assert!(events.iter().all(|e| e.name.is_none()), "{events:#?}"); +} diff --git a/src/rust/perfetto/test/lib.rs b/src/rust/perfetto/test/lib.rs new file mode 100644 index 00000000000..b0365248b9a --- /dev/null +++ b/src/rust/perfetto/test/lib.rs @@ -0,0 +1,19 @@ +//! Checks that Rust trace points (`//src/rust/perfetto`) and C++ trace points are recorded in the +//! same Perfetto session, with matching flow, track and counter track IDs. + +#[cxx::bridge(namespace = "workerd::rust::perfetto_test")] +#[cfg_attr(not(test), expect(dead_code, reason = "only used by the tests"))] +mod ffi { + unsafe extern "C++" { + include!("workerd/rust/perfetto/test/test-helper.h"); + + fn perfetto_in_build() -> bool; + fn start_trace(categories: &str) -> Result<()>; + fn emit_cpp_events(address: usize); + fn stop_trace() -> Result>; + } +} + +#[cfg(test)] +#[path = "lib-test.rs"] +mod tests; diff --git a/src/rust/perfetto/test/test-helper.c++ b/src/rust/perfetto/test/test-helper.c++ new file mode 100644 index 00000000000..68c953fad57 --- /dev/null +++ b/src/rust/perfetto/test/test-helper.c++ @@ -0,0 +1,95 @@ +#include "test-helper.h" + +#include + +#include +#include +#include + +#ifdef WORKERD_USE_PERFETTO +#include +#include + +#include +#endif + +namespace workerd::rust::perfetto_test { + +#ifdef WORKERD_USE_PERFETTO + +namespace { + +struct ActiveTrace { + kj::Maybe session; + // A second descriptor for the session's (unlinked) trace file, to read it back. + kj::OwnFd readFd; +}; + +kj::Maybe activeTrace; + +} // namespace + +bool perfetto_in_build() { + return true; +} + +void start_trace(::rust::Str categories) { + KJ_REQUIRE(activeTrace == kj::none, "a trace is already active"); + FILE* file = tmpfile(); + KJ_REQUIRE(file != nullptr, "tmpfile() failed"); + KJ_DEFER(fclose(file)); + int sessionFd; + KJ_SYSCALL(sessionFd = dup(fileno(file))); + int readFd; + KJ_SYSCALL(readFd = dup(sessionFd)); + activeTrace = ActiveTrace{ + .session = PerfettoSession(sessionFd, kj::heapString(categories.data(), categories.size())), + .readFd = kj::OwnFd(readFd), + }; +} + +void emit_cpp_events(size_t address) { + auto ptr = reinterpret_cast(address); + { TRACE_EVENT("workerd", "cpp-scoped", PERFETTO_FLOW_FROM_POINTER(ptr)); } + TRACE_EVENT_BEGIN("workerd", "cpp-begin", PERFETTO_TRACK_FROM_POINTER(ptr)); + TRACE_EVENT_END("workerd", PERFETTO_TRACK_FROM_POINTER(ptr)); + TRACE_COUNTER("workerd", "shared-counter", 42); +} + +::rust::Vec stop_trace() { + auto& trace = KJ_REQUIRE_NONNULL(activeTrace, "no active trace"); + // Destroying the session flushes and stops it. + trace.session = kj::none; + + ::rust::Vec result; + KJ_SYSCALL(lseek(trace.readFd.get(), 0, SEEK_SET)); + kj::byte buffer[4096]; + for (;;) { + ssize_t n; + KJ_SYSCALL(n = read(trace.readFd.get(), buffer, sizeof(buffer))); + if (n == 0) break; + for (auto b: kj::arrayPtr(buffer, n)) { + result.push_back(b); + } + } + activeTrace = kj::none; + return result; +} + +#else // defined(WORKERD_USE_PERFETTO) + +bool perfetto_in_build() { + return false; +} + +void start_trace(::rust::Str) {} + +void emit_cpp_events(size_t) {} + +::rust::Vec stop_trace() { + return {}; +} + +#endif // defined(WORKERD_USE_PERFETTO) + +} // namespace workerd::rust::perfetto_test diff --git a/src/rust/perfetto/test/test-helper.h b/src/rust/perfetto/test/test-helper.h new file mode 100644 index 00000000000..5dcc6a28508 --- /dev/null +++ b/src/rust/perfetto/test/test-helper.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include +#include + +namespace workerd::rust::perfetto_test { + +// Whether this build has Perfetto (WORKERD_USE_PERFETTO). +bool perfetto_in_build(); + +// Starts a PerfettoSession recording `categories` into a temporary file. No-op without Perfetto. +void start_trace(::rust::Str categories); + +// Emits C++ trace points that use a flow and a track derived from `address`, and a counter. +void emit_cpp_events(size_t address); + +// Stops the session started by start_trace() and returns the serialized trace. Returns an empty +// vector without Perfetto. +::rust::Vec stop_trace(); + +} // namespace workerd::rust::perfetto_test diff --git a/src/workerd/util/AGENTS.md b/src/workerd/util/AGENTS.md index c346fe3777a..b6422ecc903 100644 --- a/src/workerd/util/AGENTS.md +++ b/src/workerd/util/AGENTS.md @@ -24,6 +24,7 @@ Shared utility library: data structures, SQLite wrapper, feature gating, logging | `stream-utils.h` | `NeuterableInputStream`, `newNullInputStream` | Disconnectable I/O; null/identity stream factories | | `mimetype.h` | MIME type parser/serializer | `MimeType::extract()` from content-type header | | `wait-list.h` | Cross-request event subscription | Shared fulfiller list for signaling waiters | +| `perfetto-tracing.h/c++` | `PerfettoSession`, `TRACE_EVENT` categories, `PERFETTO_*_FROM_POINTER` | `WORKERD_USE_PERFETTO` gates everything; `registerWorkerdTracks()` also registers Rust categories under `WORKERD_USE_RUST_PERFETTO` (flag `:use_rust_perfetto`; see `src/rust/AGENTS.md` PERFETTO TRACING) | ## Guidelines diff --git a/src/workerd/util/BUILD.bazel b/src/workerd/util/BUILD.bazel index 1d56cbe0eca..d02ca7f3f5d 100644 --- a/src/workerd/util/BUILD.bazel +++ b/src/workerd/util/BUILD.bazel @@ -22,6 +22,32 @@ selects.config_setting_group( visibility = ["//visibility:public"], ) +# Enables Perfetto trace points in Rust (//src/rust/perfetto), which needs the Perfetto C ABI +# (@perfetto//:libperfetto_c_over_client) and the Rust SDK sources (@perfetto//contrib/rust-sdk). +# This defaults to off and is turned on by workerd's .bazelrc (`build:perfetto`), because embedders +# that build workerd from their own workspace don't read that file and may provide a @perfetto +# repository without those targets. With the flag off, the Rust facade compiles to no-ops and +# nothing references the new @perfetto targets. Keep every such reference inside a select() on +# :really_use_rust_perfetto. +bool_flag( + name = "use_rust_perfetto", + build_setting_default = False, +) + +config_setting( + name = "set_use_rust_perfetto", + flag_values = {"use_rust_perfetto": "True"}, +) + +selects.config_setting_group( + name = "really_use_rust_perfetto", + match_all = [ + ":really_use_perfetto", + ":set_use_rust_perfetto", + ], + visibility = ["//visibility:public"], +) + # The routing layer owning the unprefixed zlib symbol names; @zlib//:zlib depends on this, so # every zlib consumer links it. Deliberately kj/capnp-free (see zlib-router.c++). wd_cc_library( @@ -63,12 +89,21 @@ wd_cc_library( ":really_use_perfetto": ["WORKERD_USE_PERFETTO"], "//conditions:default": [], }), + local_defines = select({ + ":really_use_rust_perfetto": ["WORKERD_USE_RUST_PERFETTO"], + "//conditions:default": [], + }), visibility = ["//visibility:public"], deps = [ "@capnp-cpp//src/kj", ] + select({ ":really_use_perfetto": ["@perfetto//:libperfetto_client_experimental"], "//conditions:default": [], + }) + select({ + ":really_use_rust_perfetto": [ + "//src/rust/perfetto", + ], + "//conditions:default": [], }), ) # TODO(later): Currently perfetto support is not enabled on Windows simply because the diff --git a/src/workerd/util/perfetto-tracing.c++ b/src/workerd/util/perfetto-tracing.c++ index 9a4cc27cc89..b70439bae93 100644 --- a/src/workerd/util/perfetto-tracing.c++ +++ b/src/workerd/util/perfetto-tracing.c++ @@ -14,6 +14,16 @@ #include #include +#ifdef WORKERD_USE_RUST_PERFETTO +#include + +#include + +// The Perfetto C ABI shares category state as C11 `atomic_bool`, which the Rust SDK reads as a +// plain bool-sized atomic. +static_assert(sizeof(std::atomic) == sizeof(bool)); +#endif + PERFETTO_TRACK_EVENT_STATIC_STORAGE_IN_NAMESPACE(workerd::traces); namespace workerd { @@ -60,6 +70,11 @@ void PerfettoSession::registerWorkerdTracks() { KJ_ASSERT(once, "workerd perfetto tracks are already registered"); if (perfetto::Tracing::IsInitialized()) { workerd::traces::TrackEvent::Register(); +#ifdef WORKERD_USE_RUST_PERFETTO + // Registers the Perfetto C ABI's track_event data source and the Rust categories + // (//src/rust/perfetto) with the Perfetto instance initialized above. + workerd::rust::perfetto::register_track_events(); +#endif once = false; } }