Skip to content

Commit 37620ff

Browse files
committed
Implement self-describing binary trace format
A compact, self-describing binary trace format for tokio runtime telemetry. Schemas are embedded in the stream so readers don't need out-of-band type definitions. - Encoder/decoder with ~48M events/s encode, ~30M decode throughput - Derive macro (#[derive(TraceEvent)]) with zero-copy Ref decoding - u24 delta-encoded timestamps in event headers - String interning, symbol tables, stack frame support - Field types: u8/u16/u32 (fixed LE), u64 (LEB128), i64, f64, bool, String, Bytes, StackFrames, StringMap, PooledString - JavaScript decoder (js/decode.js) - Format comparison example, fuzz targets, insta snapshot tests - SPEC.md wire format specification
1 parent cb945af commit 37620ff

34 files changed

Lines changed: 6809 additions & 1 deletion

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
*.bin
33
# except the demo trace
44
!dial9-tokio-telemetry/trace_viewer/demo-trace.bin
5-
/target
5+
target/

Cargo.lock

Lines changed: 68 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22
resolver = "3"
33
members = [
44
"dial9-tokio-telemetry",
5+
"dial9-trace-format",
6+
"dial9-trace-format-derive",
57
"perf-self-profile",
68
"examples/metrics-service",
79
]
10+
exclude = [
11+
"dial9-trace-format/fuzz",
12+
]
813

914
[workspace.package]
1015
version = "0.1.1"

dial9-tokio-telemetry/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ serde = { version = "1", features = ["derive"] }
2121
serde_json = "1"
2222
smallvec = "1"
2323
dial9-perf-self-profile = { workspace = true, optional = true }
24+
dial9-trace-format = { path = "../dial9-trace-format" }
2425
tracing = "0.1.44"
2526

2627
[features]

dial9-tokio-telemetry/src/telemetry/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod collector;
55
pub mod cpu_profile;
66
pub mod events;
77
pub mod format;
8+
pub mod new_format;
89
pub mod recorder;
910
pub mod task_metadata;
1011
pub mod writer;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "dial9-trace-format-derive"
3+
version.workspace = true
4+
edition = "2024"
5+
description = "Derive macro for dial9-trace-format TraceEvent trait"
6+
license = "Apache-2.0"
7+
publish = false
8+
9+
[lib]
10+
proc-macro = true
11+
12+
[dependencies]
13+
syn = { version = "2", features = ["full"] }
14+
quote = "1"
15+
proc-macro2 = "1"
16+
17+
[dev-dependencies]
18+
insta = "1"
19+
prettyplease = "0.2"
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
use proc_macro::TokenStream;
2+
use quote::{format_ident, quote};
3+
use syn::{Data, DeriveInput, Fields, parse_macro_input};
4+
5+
fn derive_trace_event_impl(input: DeriveInput) -> proc_macro2::TokenStream {
6+
let name = &input.ident;
7+
let vis = &input.vis;
8+
let name_str = name.to_string();
9+
let ref_name = format_ident!("{}Ref", name);
10+
11+
let fields = match &input.data {
12+
Data::Struct(data) => match &data.fields {
13+
Fields::Named(f) => &f.named,
14+
_ => panic!("TraceEvent only supports named fields"),
15+
},
16+
_ => panic!("TraceEvent can only be derived for structs"),
17+
};
18+
19+
// Find the field marked with #[traceevent(timestamp)]
20+
let mut timestamp_field_name = None;
21+
for field in fields.iter() {
22+
for attr in &field.attrs {
23+
if attr.path().is_ident("traceevent") {
24+
let _ = attr.parse_nested_meta(|meta| {
25+
if meta.path.is_ident("timestamp") {
26+
timestamp_field_name = Some(field.ident.as_ref().unwrap().clone());
27+
}
28+
Ok(())
29+
});
30+
}
31+
}
32+
}
33+
34+
let mut field_def_tokens = Vec::new();
35+
let mut encode_tokens = Vec::new();
36+
let mut ref_fields = Vec::new();
37+
let mut decode_tokens = Vec::new();
38+
let mut decode_idx = 0usize;
39+
40+
for field in fields.iter() {
41+
let field_name = field.ident.as_ref().unwrap();
42+
let ty = &field.ty;
43+
44+
// Skip the timestamp field in schema/encode/decode — it's in the event header
45+
if timestamp_field_name.as_ref() == Some(field_name) {
46+
continue;
47+
}
48+
49+
let field_name_str = field_name.to_string();
50+
field_def_tokens.push(quote! {
51+
::dial9_trace_format::schema::FieldDef {
52+
name: #field_name_str.to_string(),
53+
field_type: <#ty as ::dial9_trace_format::TraceField>::field_type(),
54+
}
55+
});
56+
encode_tokens.push(quote! {
57+
<#ty as ::dial9_trace_format::TraceField>::encode(&self.#field_name, enc)?;
58+
});
59+
60+
ref_fields.push(quote! {
61+
pub #field_name: <#ty as ::dial9_trace_format::TraceField>::Ref<'a>
62+
});
63+
let idx = decode_idx;
64+
decode_tokens.push(quote! {
65+
#field_name: <#ty as ::dial9_trace_format::TraceField>::decode_ref(fields.get(#idx)?)?
66+
});
67+
decode_idx += 1;
68+
}
69+
70+
let timestamp_impl = if let Some(ref ts_field) = timestamp_field_name {
71+
quote! {
72+
fn timestamp(&self) -> Option<u64> { Some(self.#ts_field) }
73+
}
74+
} else {
75+
quote! {}
76+
};
77+
78+
let has_timestamp_impl = if timestamp_field_name.is_some() {
79+
quote! { fn has_timestamp() -> bool { true } }
80+
} else {
81+
quote! {}
82+
};
83+
84+
// For the Ref struct, timestamp field is NOT included — it comes from the event header
85+
let phantom_field =
86+
if fields.is_empty() || (fields.len() == 1 && timestamp_field_name.is_some()) {
87+
quote! { _marker: ::std::marker::PhantomData<&'a ()>, }
88+
} else {
89+
quote! {}
90+
};
91+
let phantom_init = if fields.is_empty() || (fields.len() == 1 && timestamp_field_name.is_some())
92+
{
93+
quote! { _marker: ::std::marker::PhantomData, }
94+
} else {
95+
quote! {}
96+
};
97+
98+
quote! {
99+
#[derive(Debug, Clone)]
100+
#vis struct #ref_name<'a> {
101+
#(#ref_fields,)*
102+
#phantom_field
103+
}
104+
105+
impl ::dial9_trace_format::TraceEvent for #name {
106+
type Ref<'a> = #ref_name<'a>;
107+
108+
fn event_name() -> &'static str { #name_str }
109+
fn field_defs() -> Vec<::dial9_trace_format::schema::FieldDef> {
110+
vec![#(#field_def_tokens),*]
111+
}
112+
#timestamp_impl
113+
#has_timestamp_impl
114+
fn encode_fields<W: ::std::io::Write>(&self, enc: &mut ::dial9_trace_format::EventEncoder<'_, W>) -> ::std::io::Result<()> {
115+
#(#encode_tokens)*
116+
Ok(())
117+
}
118+
fn decode<'a>(fields: &[::dial9_trace_format::types::FieldValueRef<'a>]) -> Option<Self::Ref<'a>> {
119+
Some(#ref_name {
120+
#(#decode_tokens,)*
121+
#phantom_init
122+
})
123+
}
124+
}
125+
}
126+
}
127+
128+
#[proc_macro_derive(TraceEvent, attributes(traceevent))]
129+
pub fn derive_trace_event(input: TokenStream) -> TokenStream {
130+
let input = parse_macro_input!(input as DeriveInput);
131+
TokenStream::from(derive_trace_event_impl(input))
132+
}
133+
134+
#[cfg(test)]
135+
mod tests {
136+
use super::*;
137+
use insta::assert_snapshot;
138+
139+
fn expand_to_string(input: proc_macro2::TokenStream) -> String {
140+
let input: DeriveInput = syn::parse2(input).unwrap();
141+
let output = derive_trace_event_impl(input);
142+
match syn::parse2::<syn::File>(output.clone()) {
143+
Ok(file) => prettyplease::unparse(&file),
144+
Err(_) => output.to_string(),
145+
}
146+
}
147+
148+
#[test]
149+
fn simple_event() {
150+
assert_snapshot!(expand_to_string(quote! {
151+
struct SimpleEvent {
152+
timestamp_ns: u64,
153+
value: u32,
154+
}
155+
}));
156+
}
157+
158+
#[test]
159+
fn empty_event() {
160+
assert_snapshot!(expand_to_string(quote! {
161+
struct EmptyEvent {}
162+
}));
163+
}
164+
165+
#[test]
166+
fn all_field_types() {
167+
assert_snapshot!(expand_to_string(quote! {
168+
struct AllFieldTypes {
169+
a_u8: u8,
170+
b_u16: u16,
171+
c_u32: u32,
172+
d_u64: u64,
173+
e_i64: i64,
174+
f_f64: f64,
175+
g_bool: bool,
176+
h_string: String,
177+
i_bytes: Vec<u8>,
178+
j_interned: InternedString,
179+
k_frames: StackFrames,
180+
l_map: Vec<(String, String)>,
181+
}
182+
}));
183+
}
184+
185+
#[test]
186+
fn timestamp_attribute() {
187+
assert_snapshot!(expand_to_string(quote! {
188+
struct PollStart {
189+
#[traceevent(timestamp)]
190+
timestamp_ns: u64,
191+
worker_id: u64,
192+
task_id: u64,
193+
}
194+
}));
195+
}
196+
}

0 commit comments

Comments
 (0)