Skip to main content

perfetto_recorder/
lib.rs

1//! A low-overhead way to capture timings of lots of spans within your application, then at a later
2//! point, gather the traces from each thread and write them as a Perfetto trace file for viewing in
3//! the Perfetto UI.
4
5use crate::schema::DebugAnnotation;
6use crate::schema::ThreadDescriptor;
7use crate::schema::TracePacket;
8use crate::schema::TrackDescriptor;
9use prost::Message;
10use rand::RngCore;
11use rand::rngs::ThreadRng;
12use std::cell::RefCell;
13use std::collections::HashMap;
14use std::path::Path;
15use std::sync::atomic::AtomicBool;
16use std::sync::atomic::Ordering;
17
18#[cfg(all(feature = "enable", target_os = "macos"))]
19#[path = "os_macos.rs"]
20mod os;
21
22#[cfg(all(feature = "enable", unix, not(target_os = "macos")))]
23#[path = "os_unix.rs"]
24mod os;
25
26#[cfg(all(feature = "enable", windows))]
27#[path = "os_windows.rs"]
28mod os;
29
30#[cfg(not(feature = "enable"))]
31#[path = "os_disabled.rs"]
32mod os;
33
34#[cfg(feature = "fastant")]
35type Instant = fastant::Instant;
36
37#[cfg(not(feature = "fastant"))]
38type Instant = std::time::SystemTime;
39
40mod schema;
41
42/// Begins a time span that ends when the current scope ends.
43///
44/// Example usage:
45///
46/// ```
47/// use perfetto_recorder::scope;
48///
49/// scope!("Parsing");
50/// ```
51///
52/// If you need the span to outlive the current scope or you'd like to drop it before the end of the
53/// current scope, then use [start_span] instead.
54#[macro_export]
55macro_rules! scope {
56    ($($args:tt)*) => {
57        let _guard = $crate::start_span!($($args)*);
58    };
59}
60
61/// Begins a timing span, returning a guard, that when dropped will end the span.
62///
63/// Example usage:
64///
65/// ```
66/// use perfetto_recorder::start_span;
67///
68/// let span_guard = start_span!("Parsing");
69/// // Do some work.
70/// drop(span_guard);
71/// ```
72///
73/// If you don't need the span to outlive the scope in which it's created.
74#[macro_export]
75macro_rules! start_span {
76    ($name:expr $(, $($arg_name:ident $( = $arg_value:expr)?),*)?) => {{
77        const SOURCE_INFO: $crate::SourceInfo = $crate::SourceInfo {
78            name: $name,
79            file: file!(),
80            line: line!(),
81            arg_names: &[$($(stringify!($arg_name)),*)?],
82        };
83        if $crate::is_enabled() {
84            $crate::record_event($crate::Event::StartSpan(&SOURCE_INFO));
85                $crate::record_event($crate::Event::Timestamp($crate::time()));
86            $($($crate::RecordArg::record_arg(
87                $crate::start_span!(@arg_value $arg_name $($arg_value)?)
88            );)*)?
89        }
90
91        $crate::SpanGuard::new(&SOURCE_INFO)
92    }};
93
94    (@arg_value $name:ident) => {
95        $name
96    };
97
98    (@arg_value $name:ident $value:expr) => {
99        $value
100    };
101}
102
103#[derive(PartialEq, Eq, Hash, Clone, Copy)]
104struct Pid(i32);
105
106/// A guard that when dropped will end a span.
107///
108/// Created by the [start_span] macro.
109pub struct SpanGuard {
110    #[cfg(feature = "enable")]
111    pub source: &'static SourceInfo,
112}
113
114/// Trace events that occurred on a single thread.
115pub struct ThreadTraceData {
116    events: Vec<Event>,
117    pid: Pid,
118    tid: Pid,
119    thread_name: Option<String>,
120}
121
122impl ThreadTraceData {
123    pub fn take_current_thread() -> Self {
124        let thread = std::thread::current();
125        Self {
126            events: EVENTS.take(),
127            pid: os::getpid(),
128            tid: os::gettid(),
129            thread_name: thread.name().map(str::to_owned),
130        }
131    }
132}
133
134/// The number of events consumed by each span.
135pub const EVENTS_PER_SPAN: usize = 4;
136
137/// The number of events consumed by each argument.
138pub const EVENTS_PER_ARG: usize = 1;
139
140/// The number of events consumed by each counter value.
141pub const EVENTS_PER_COUNTER: usize = 2;
142
143/// Reserve capacity on the current thread for additional spans and their arguments.
144///
145/// See constants [EVENTS_PER_SPAN], [EVENTS_PER_ARG], and [EVENTS_PER_COUNTER] to aid in working
146/// out what a reasonable value might be. Note that string slices will consume additional capacity
147/// for each multiple of 15 in size. Calling this is entirely optional, but might make recording
148/// spans and counters more consistent by reducing the need to reallocate the recording for the
149/// current thread.
150pub fn current_thread_reserve(additional: usize) {
151    EVENTS.with_borrow_mut(|events| events.reserve(additional))
152}
153
154/// Types that implement this trait can be used as arguments to the [span] macro.
155pub trait RecordArg {
156    fn record_arg(self);
157}
158
159impl RecordArg for bool {
160    fn record_arg(self) {
161        record_event(Event::Bool(self));
162    }
163}
164
165impl RecordArg for f64 {
166    fn record_arg(self) {
167        record_event(Event::F64(self));
168    }
169}
170
171impl RecordArg for u64 {
172    fn record_arg(self) {
173        record_event(Event::U64(self));
174    }
175}
176
177impl RecordArg for u32 {
178    fn record_arg(self) {
179        record_event(Event::U64(self.into()));
180    }
181}
182
183impl RecordArg for u16 {
184    fn record_arg(self) {
185        record_event(Event::U64(self.into()));
186    }
187}
188
189impl RecordArg for u8 {
190    fn record_arg(self) {
191        record_event(Event::U64(self.into()));
192    }
193}
194
195impl RecordArg for usize {
196    fn record_arg(self) {
197        record_event(Event::U64(self as u64));
198    }
199}
200
201impl RecordArg for i64 {
202    fn record_arg(self) {
203        record_event(Event::I64(self));
204    }
205}
206
207impl RecordArg for i32 {
208    fn record_arg(self) {
209        record_event(Event::I64(self.into()));
210    }
211}
212
213impl RecordArg for i16 {
214    fn record_arg(self) {
215        record_event(Event::I64(self.into()));
216    }
217}
218
219impl RecordArg for i8 {
220    fn record_arg(self) {
221        record_event(Event::I64(self.into()));
222    }
223}
224
225impl RecordArg for String {
226    fn record_arg(self) {
227        record_event(Event::String(self));
228    }
229}
230
231impl RecordArg for isize {
232    fn record_arg(self) {
233        record_event(Event::I64(self as i64));
234    }
235}
236
237impl RecordArg for &str {
238    fn record_arg(self) {
239        let mut pending: &[u8] = &[];
240        for chunk in self.as_bytes().chunks(STR_PART_LEN) {
241            if let Some(part_bytes) = pending.first_chunk::<STR_PART_LEN>() {
242                record_event(Event::StrPart(*part_bytes));
243            }
244            pending = chunk;
245        }
246        let mut padded_bytes = [0; STR_PART_LEN];
247        padded_bytes[..pending.len()].copy_from_slice(pending);
248        record_event(Event::StrEnd {
249            len: pending.len() as u8,
250            bytes: padded_bytes,
251        });
252    }
253}
254
255#[doc(hidden)]
256#[derive(Debug)]
257pub enum Event {
258    /// The start of a span. Must be followed by a timestamp.
259    StartSpan(&'static SourceInfo),
260
261    /// The end of a span. Must be followed by a timestamp.
262    EndSpan(&'static SourceInfo),
263
264    /// The time at which the preceding start/end span occurred.
265    Timestamp(Instant),
266
267    Bool(bool),
268    U64(u64),
269    I64(i64),
270    F64(f64),
271    String(String),
272
273    /// Part of a str slice. Must be followed by either another [Event::StrPart] or a
274    /// [Event::StrEnd].
275    StrPart([u8; STR_PART_LEN]),
276
277    /// The end of a str slice.
278    StrEnd {
279        len: u8,
280        bytes: [u8; STR_PART_LEN],
281    },
282
283    /// An integer counter value. Must be followed by a timestamp.
284    CounterI64 {
285        uuid: u64,
286        value: i64,
287    },
288
289    /// A floating-point counter value. Must be followed by a timestamp.
290    CounterF64 {
291        uuid: u64,
292        value: f64,
293    },
294}
295
296/// The maximum number of bytes we can fit in an [Event::StrPart].
297const STR_PART_LEN: usize = 15;
298
299#[doc(hidden)]
300#[derive(Debug)]
301pub struct SourceInfo {
302    pub name: &'static str,
303    pub file: &'static str,
304    pub line: u32,
305    pub arg_names: &'static [&'static str],
306}
307
308#[doc(hidden)]
309#[inline(always)]
310pub fn record_event(event: Event) {
311    EVENTS.with_borrow_mut(|events| events.push(event));
312}
313
314thread_local! {
315    static EVENTS: RefCell<Vec<Event>> = const { RefCell::new(Vec::new()) };
316}
317
318thread_local! {
319    static RNG: RefCell<ThreadRng> = RefCell::new(ThreadRng::default());
320}
321
322#[doc(hidden)]
323#[inline(always)]
324pub fn time() -> Instant {
325    Instant::now()
326}
327
328impl Drop for SpanGuard {
329    fn drop(&mut self) {
330        #[cfg(feature = "enable")]
331        if is_enabled() {
332            record_event(Event::EndSpan(self.source));
333            record_event(Event::Timestamp(time()));
334        }
335    }
336}
337
338impl SpanGuard {
339    #[doc(hidden)]
340    #[allow(unused_variables)]
341    pub fn new(source: &'static SourceInfo) -> Self {
342        #[cfg(feature = "enable")]
343        {
344            Self { source }
345        }
346        #[cfg(not(feature = "enable"))]
347        {
348            Self {}
349        }
350    }
351}
352
353const CLOCK_ID: u32 = 6;
354
355static RUNTIME_ENABLED: AtomicBool = AtomicBool::new(false);
356
357/// Enable recording. Can be called multiple times. Any spans emitted prior to the first call will
358/// be discarded.
359pub fn start() -> Result<(), TracingDisabledAtBuildTime> {
360    if !cfg!(feature = "enable") {
361        return Err(TracingDisabledAtBuildTime);
362    }
363
364    RUNTIME_ENABLED.store(true, Ordering::Relaxed);
365    Ok(())
366}
367
368/// Returns whether recording is enabled.
369pub fn is_enabled() -> bool {
370    cfg!(feature = "enable") && RUNTIME_ENABLED.load(Ordering::Relaxed)
371}
372
373/// An error that is produced if [enable] is called when the "enable" feature of this crate is not
374/// active.
375#[derive(Debug)]
376pub struct TracingDisabledAtBuildTime;
377
378/// An error that is produced if [enable] has not been called, but we're trying to build a trace.
379#[derive(Debug)]
380pub struct TracingDisabled;
381
382/// Used to build a trace file.
383///
384/// Example usage:
385/// ```
386/// # use perfetto_recorder::*;
387///
388/// # if perfetto_recorder::is_enabled() {
389///
390/// TraceBuilder::new()?
391///     .process_thread_data(&ThreadTraceData::take_current_thread())
392///     .write_to_file("a.pftrace")?;
393///
394/// # }
395///
396/// # Ok::<(), Box<dyn std::error::Error>>(())
397/// ```
398pub struct TraceBuilder {
399    trace: schema::Trace,
400    pending_interned: Option<schema::InternedData>,
401    name_ids: HashMap<&'static str, u64>,
402    debug_annotation_name_ids: HashMap<&'static str, u64>,
403    source_location_ids: HashMap<(&'static str, u32), u64>,
404    thread_uuids: HashMap<Pid, Uuid>,
405    sequence_id: u32,
406    #[cfg(feature = "fastant")]
407    time_anchor: fastant::Anchor,
408}
409
410impl TraceBuilder {
411    pub fn new() -> Result<TraceBuilder, TracingDisabled> {
412        if !is_enabled() {
413            return Err(TracingDisabled);
414        }
415
416        let sequence_id = RNG.with_borrow_mut(|rng| rng.next_u32());
417
418        let mut builder = TraceBuilder {
419            sequence_id,
420            trace: Default::default(),
421            pending_interned: Default::default(),
422            name_ids: Default::default(),
423            source_location_ids: Default::default(),
424            debug_annotation_name_ids: Default::default(),
425            thread_uuids: Default::default(),
426            #[cfg(feature = "fastant")]
427            time_anchor: fastant::Anchor::new(),
428        };
429
430        builder.add_packet(TracePacket {
431            sequence_flags: Some(
432                schema::trace_packet::SequenceFlags::SeqIncrementalStateCleared as u32,
433            ),
434            ..Default::default()
435        });
436
437        Ok(builder)
438    }
439
440    /// Merges trace data captured from a thread into the trace.
441    pub fn process_thread_data(&mut self, thread: &ThreadTraceData) -> &mut Self {
442        let thread_uuid = self.thread_uuid(thread);
443
444        let mut events = thread.events.iter();
445
446        while let Some(event) = events.next() {
447            match event {
448                Event::StartSpan(source_info) => {
449                    self.emit_track_event(
450                        source_info,
451                        schema::track_event::Type::SliceBegin,
452                        &mut events,
453                        thread_uuid,
454                    );
455                }
456                Event::EndSpan(source_info) => {
457                    self.emit_track_event(
458                        source_info,
459                        schema::track_event::Type::SliceEnd,
460                        &mut events,
461                        thread_uuid,
462                    );
463                }
464                Event::CounterI64 { uuid, value } => {
465                    self.emit_counter_event(
466                        *uuid,
467                        &mut events,
468                        schema::track_event::CounterValueField::CounterValue(*value),
469                    );
470                }
471                Event::CounterF64 { uuid, value } => {
472                    self.emit_counter_event(
473                        *uuid,
474                        &mut events,
475                        schema::track_event::CounterValueField::DoubleCounterValue(*value),
476                    );
477                }
478                other => panic!("Internal error: Unexpected event {other:?}"),
479            }
480        }
481
482        self
483    }
484
485    // Encode the Perfetto trace as bytes.
486    pub fn encode_to_vec(&self) -> Vec<u8> {
487        self.trace.encode_to_vec()
488    }
489
490    pub fn write_to_file(&self, path: impl AsRef<Path>) -> Result<(), std::io::Error> {
491        std::fs::write(path, self.encode_to_vec())
492    }
493
494    fn name_id(&mut self, name: &'static str) -> u64 {
495        let next_id = self.name_ids.len() as u64 + 1;
496        *self.name_ids.entry(name).or_insert_with(|| {
497            self.pending_interned
498                .get_or_insert_default()
499                .event_names
500                .push(schema::EventName {
501                    iid: Some(next_id),
502                    name: Some(name.to_owned()),
503                });
504            next_id
505        })
506    }
507
508    fn debug_annotation_name_id(&mut self, name: &'static str) -> u64 {
509        let next_id = self.debug_annotation_name_ids.len() as u64 + 1;
510        *self
511            .debug_annotation_name_ids
512            .entry(name)
513            .or_insert_with(|| {
514                self.pending_interned
515                    .get_or_insert_default()
516                    .debug_annotation_names
517                    .push(schema::DebugAnnotationName {
518                        iid: Some(next_id),
519                        name: Some(name.to_owned()),
520                    });
521                next_id
522            })
523    }
524
525    fn source_location_id(&mut self, source_location: &'static SourceInfo) -> u64 {
526        let next_id = self.source_location_ids.len() as u64 + 1;
527        *self
528            .source_location_ids
529            .entry((source_location.file, source_location.line))
530            .or_insert_with(|| {
531                self.pending_interned
532                    .get_or_insert_default()
533                    .source_locations
534                    .push(schema::SourceLocation {
535                        iid: Some(next_id),
536                        file_name: Some(source_location.file.to_owned()),
537                        function_name: None,
538                        line_number: Some(source_location.line),
539                    });
540                next_id
541            })
542    }
543
544    fn emit_track_event(
545        &mut self,
546        source_info: &'static SourceInfo,
547        kind: schema::track_event::Type,
548        events: &mut std::slice::Iter<Event>,
549        thread_uuid: Uuid,
550    ) {
551        let Some(Event::Timestamp(timestamp)) = events.next() else {
552            panic!("Internal error: Timestamp must follow top-level events");
553        };
554
555        let name_id = self.name_id(source_info.name);
556        let source_location_id = self.source_location_id(source_info);
557        let mut track_event = schema::TrackEvent::default();
558        track_event.set_type(kind);
559        track_event.name_field = Some(schema::track_event::NameField::NameIid(name_id));
560        track_event.source_location_field = Some(
561            schema::track_event::SourceLocationField::SourceLocationIid(source_location_id),
562        );
563        track_event.track_uuid = Some(thread_uuid.0);
564
565        if kind == schema::track_event::Type::SliceBegin && !source_info.arg_names.is_empty() {
566            track_event.debug_annotations = source_info
567                .arg_names
568                .iter()
569                .map(|arg_name| {
570                    let value = convert_next_arg(events);
571                    DebugAnnotation {
572                        name_field: Some(schema::debug_annotation::NameField::NameIid(
573                            self.debug_annotation_name_id(arg_name),
574                        )),
575                        value: Some(value),
576                    }
577                })
578                .collect();
579        }
580
581        let packet = TracePacket {
582            timestamp: Some(self.get_unix_nanos(*timestamp)),
583            timestamp_clock_id: Some(CLOCK_ID),
584            data: Some(schema::trace_packet::Data::TrackEvent(track_event)),
585            interned_data: self.pending_interned.take(),
586            ..Default::default()
587        };
588
589        self.add_packet(packet);
590    }
591
592    fn emit_counter_event(
593        &mut self,
594        uuid: u64,
595        events: &mut std::slice::Iter<Event>,
596        counter_value_field: schema::track_event::CounterValueField,
597    ) {
598        let Some(Event::Timestamp(timestamp)) = events.next() else {
599            panic!("Internal error: Counter event must be followed by Timestamp");
600        };
601
602        let packet = TracePacket {
603            timestamp: Some(self.get_unix_nanos(*timestamp)),
604            timestamp_clock_id: Some(CLOCK_ID),
605            data: Some(schema::trace_packet::Data::TrackEvent(schema::TrackEvent {
606                track_uuid: Some(uuid),
607                r#type: Some(schema::track_event::Type::Counter as i32),
608                counter_value_field: Some(counter_value_field),
609                ..Default::default()
610            })),
611            ..Default::default()
612        };
613
614        self.add_packet(packet);
615    }
616
617    fn thread_uuid(&mut self, thread: &ThreadTraceData) -> Uuid {
618        if let Some(uuid) = self.thread_uuids.get(&thread.tid) {
619            return *uuid;
620        }
621
622        let uuid = Uuid::new();
623
624        self.add_packet(TracePacket {
625            data: Some(schema::trace_packet::Data::TrackDescriptor(
626                TrackDescriptor {
627                    uuid: Some(uuid.0),
628                    thread: Some(ThreadDescriptor {
629                        pid: Some(thread.pid.0),
630                        tid: Some(thread.tid.0),
631                        thread_name: thread.thread_name.clone(),
632                    }),
633                    ..Default::default()
634                },
635            )),
636            ..Default::default()
637        });
638
639        self.thread_uuids.insert(thread.tid, uuid);
640
641        uuid
642    }
643
644    fn add_packet(&mut self, mut packet: TracePacket) {
645        packet.optional_trusted_packet_sequence_id = Some(
646            schema::trace_packet::OptionalTrustedPacketSequenceId::TrustedPacketSequenceId(
647                self.sequence_id,
648            ),
649        );
650        self.trace.packet.push(packet);
651    }
652
653    #[cfg(feature = "fastant")]
654    fn get_unix_nanos(&self, timestamp: Instant) -> u64 {
655        timestamp.as_unix_nanos(&self.time_anchor)
656    }
657
658    #[cfg(not(feature = "fastant"))]
659    fn get_unix_nanos(&self, timestamp: Instant) -> u64 {
660        timestamp
661            .duration_since(std::time::UNIX_EPOCH)
662            .unwrap()
663            .as_nanos() as u64
664    }
665}
666
667/// Reads the next argument from `events`.
668fn convert_next_arg(events: &mut std::slice::Iter<'_, Event>) -> schema::debug_annotation::Value {
669    let event = events.next().expect("Internal error: missing arg value");
670
671    use schema::debug_annotation::Value;
672    match event {
673        Event::StartSpan(_) => panic!("Internal error: Unexpected StartSpan"),
674        Event::EndSpan(_) => panic!("Internal error: Unexpected EndSpan"),
675        Event::Timestamp(_) => panic!("Internal error: Unexpected Timestamp"),
676        Event::CounterI64 { .. } => panic!("Internal error: Unexpected CounterI64"),
677        Event::CounterF64 { .. } => panic!("Internal error: Unexpected CounterF64"),
678        Event::Bool(value) => Value::BoolValue(*value),
679        Event::U64(value) => Value::UintValue(*value),
680        Event::I64(value) => Value::IntValue(*value),
681        Event::F64(value) => Value::DoubleValue(*value),
682        Event::String(value) => Value::StringValue(value.clone()),
683        Event::StrPart(bytes) => {
684            let mut merged_bytes = Vec::new();
685            merged_bytes.extend_from_slice(bytes);
686            loop {
687                match events.next() {
688                    Some(Event::StrPart(bytes)) => {
689                        merged_bytes.extend_from_slice(bytes);
690                    }
691                    Some(Event::StrEnd { len, bytes }) => {
692                        merged_bytes.extend_from_slice(&bytes[..*len as usize]);
693                        // The string started out as valid UTF-8 &str, so it should still be valid.
694                        break Value::StringValue(String::from_utf8(merged_bytes).unwrap());
695                    }
696                    other => panic!(
697                        "Internal error: Unexpected event {other:?} while looking for StrEnd"
698                    ),
699                }
700            }
701        }
702        Event::StrEnd { len, bytes } => {
703            Value::StringValue(str::from_utf8(&bytes[..*len as usize]).unwrap().to_owned())
704        }
705    }
706}
707
708impl Uuid {
709    fn new() -> Uuid {
710        Uuid(RNG.with_borrow_mut(|rng| rng.next_u64()))
711    }
712}
713
714impl std::error::Error for TracingDisabledAtBuildTime {}
715
716impl std::fmt::Display for TracingDisabledAtBuildTime {
717    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
718        write!(
719            f,
720            "The \"enable\" feature of perfetto-recorder is not active"
721        )
722    }
723}
724
725impl std::error::Error for TracingDisabled {}
726
727impl std::fmt::Display for TracingDisabled {
728    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
729        write!(f, "The `perfetto_recorder::start()` was not called")
730    }
731}
732
733#[derive(Clone, Copy, Debug)]
734struct Uuid(u64);
735
736/// Units for counter tracks.
737#[derive(Debug, Clone)]
738pub enum CounterUnit {
739    /// Unspecified unit.
740    Unspecified,
741    /// Time in nanoseconds.
742    TimeNs,
743    /// Generic count.
744    Count,
745    /// Size in bytes.
746    SizeBytes,
747    /// Custom unit with a name (e.g., "%", "fps", etc.).
748    Custom(String),
749}
750
751impl CounterUnit {
752    fn to_proto_unit(&self) -> Option<i32> {
753        Some(match self {
754            CounterUnit::Unspecified => schema::counter_descriptor::Unit::Unspecified,
755            CounterUnit::TimeNs => schema::counter_descriptor::Unit::TimeNs,
756            CounterUnit::Count => schema::counter_descriptor::Unit::Count,
757            CounterUnit::SizeBytes => schema::counter_descriptor::Unit::SizeBytes,
758            CounterUnit::Custom(_) => schema::counter_descriptor::Unit::Unspecified,
759        } as i32)
760    }
761
762    fn to_proto_unit_name(&self) -> Option<String> {
763        if let Self::Custom(name) = self {
764            Some(name.clone())
765        } else {
766            None
767        }
768    }
769}
770
771/// A handle to a counter track that can be used to record counter values.
772#[derive(Debug, Clone, Copy)]
773pub struct CounterTrack {
774    uuid: u64,
775}
776
777impl CounterTrack {
778    /// Records an integer counter value at a specific timestamp.
779    ///
780    /// This function stores the counter value in thread-local storage. The value will be
781    /// converted to the trace format when `TraceBuilder::process_thread_data()` is called.
782    ///
783    /// # Arguments
784    ///
785    /// * `timestamp` - The timestamp for this value
786    /// * `value` - The counter value
787    ///
788    /// # Example
789    ///
790    /// ```
791    /// # use perfetto_recorder::*;
792    /// # if perfetto_recorder::is_enabled() {
793    /// start()?;
794    /// let mut trace = TraceBuilder::new()?;
795    /// let mut counter = trace.create_counter_track("Memory", CounterUnit::SizeBytes, 1, false);
796    /// counter.record_i64(perfetto_recorder::time(), 1024);
797    /// # }
798    /// # Ok::<(), Box<dyn std::error::Error>>(())
799    /// ```
800    #[inline(always)]
801    pub fn record_i64(&mut self, timestamp: Instant, value: i64) {
802        if !RUNTIME_ENABLED.load(Ordering::Relaxed) {
803            return;
804        }
805        record_event(Event::CounterI64 {
806            uuid: self.uuid,
807            value,
808        });
809        record_event(Event::Timestamp(timestamp));
810    }
811
812    /// Records a floating-point counter value at a specific timestamp.
813    ///
814    /// This function stores the counter value in thread-local storage. The value will be
815    /// converted to the trace format when `TraceBuilder::process_thread_data()` is called.
816    ///
817    /// # Arguments
818    ///
819    /// * `timestamp` - The timestamp for this value
820    /// * `value` - The counter value
821    ///
822    /// # Example
823    ///
824    /// ```
825    /// # use perfetto_recorder::*;
826    /// # if perfetto_recorder::is_enabled() {
827    /// start()?;
828    /// let mut trace = TraceBuilder::new()?;
829    /// let mut counter = trace.create_counter_track("CPU %", CounterUnit::Custom("%".to_string()), 1, false);
830    /// counter.record_f64(perfetto_recorder::time(), 42.5);
831    /// # }
832    /// # Ok::<(), Box<dyn std::error::Error>>(())
833    /// ```
834    #[inline(always)]
835    pub fn record_f64(&mut self, timestamp: Instant, value: f64) {
836        if !RUNTIME_ENABLED.load(Ordering::Relaxed) {
837            return;
838        }
839        record_event(Event::CounterF64 {
840            uuid: self.uuid,
841            value,
842        });
843        record_event(Event::Timestamp(timestamp));
844    }
845}
846
847impl TraceBuilder {
848    /// Creates a new counter track.
849    ///
850    /// Counter tracks display time-series data like CPU usage, memory usage, etc.
851    ///
852    /// # Arguments
853    ///
854    /// * `name` - The name of the counter track
855    /// * `unit` - The unit for the counter values
856    /// * `unit_multiplier` - Multiplier for the values (e.g., 1024*1024 to convert bytes to MB)
857    /// * `is_incremental` - Whether values are incremental (delta) or absolute
858    ///
859    /// # Example
860    ///
861    /// ```
862    /// # use perfetto_recorder::*;
863    /// # if perfetto_recorder::is_enabled() {
864    /// let mut trace = TraceBuilder::new()?;
865    /// let cpu_counter = trace.create_counter_track(
866    ///     "CPU Usage",
867    ///     CounterUnit::Custom("%".to_string()),
868    ///     1,
869    ///     false,
870    /// );
871    /// # }
872    /// # Ok::<(), Box<dyn std::error::Error>>(())
873    /// ```
874    pub fn create_counter_track(
875        &mut self,
876        name: impl Into<String>,
877        unit: CounterUnit,
878        unit_multiplier: i64,
879        is_incremental: bool,
880    ) -> CounterTrack {
881        let uuid = Uuid::new();
882
883        self.add_packet(TracePacket {
884            data: Some(schema::trace_packet::Data::TrackDescriptor(
885                TrackDescriptor {
886                    uuid: Some(uuid.0),
887                    parent_uuid: None,
888                    process: None,
889                    thread: None,
890                    counter: Some(schema::CounterDescriptor {
891                        unit: unit.to_proto_unit(),
892                        unit_name: unit.to_proto_unit_name(),
893                        unit_multiplier: Some(unit_multiplier),
894                        is_incremental: Some(is_incremental),
895                    }),
896                    static_or_dynamic_name: Some(
897                        schema::track_descriptor::StaticOrDynamicName::Name(name.into()),
898                    ),
899                },
900            )),
901            ..Default::default()
902        });
903
904        CounterTrack { uuid: uuid.0 }
905    }
906}
907
908#[cfg(test)]
909mod tests {
910    use super::*;
911
912    #[cfg_attr(not(feature = "enable"), ignore)]
913    #[test]
914    fn test_basic_usage() {
915        start().unwrap();
916        {
917            scope!(
918                "foo",
919                value = 1_u64,
920                foo = 2_i64,
921                baz = "baz",
922                baz_owned = "baz".to_owned()
923            );
924            scope!("bar");
925        }
926
927        let num_events = EVENTS.with_borrow(|events| events.len());
928        assert_eq!(num_events, 12);
929
930        TraceBuilder::new()
931            .unwrap()
932            .process_thread_data(&ThreadTraceData::take_current_thread())
933            .encode_to_vec();
934    }
935
936    #[cfg_attr(feature = "enable", ignore)]
937    #[test]
938    fn test_no_execution_when_disabled() {
939        fn do_not_run() -> u32 {
940            panic!("This should not be called");
941        }
942
943        scope!("foo", value = do_not_run());
944    }
945
946    /// Try different lengths of string slices to make sure we're able to split them into parts and
947    /// join them back together again.
948    #[test]
949    fn str_encoding() {
950        for l in 0..100 {
951            let string: String = (0..l)
952                .map(|i| char::from_u32('A' as u32 + i).unwrap())
953                .collect();
954            let str_slice = string.as_str();
955            RecordArg::record_arg(str_slice);
956            let events = EVENTS.take();
957            let mut events = events.iter();
958            match convert_next_arg(&mut events) {
959                schema::debug_annotation::Value::StringValue(actual) => {
960                    assert_eq!(actual, string);
961                }
962                other => panic!("Unexpected event: {other:?}"),
963            }
964            assert!(events.next().is_none());
965        }
966    }
967
968    #[cfg_attr(not(feature = "enable"), ignore)]
969    #[test]
970    fn test_counter_tracks() {
971        start().unwrap();
972
973        let mut trace = TraceBuilder::new().unwrap();
974
975        // Create different types of counter tracks
976        let mut cpu_counter =
977            trace.create_counter_track("CPU Usage", CounterUnit::Custom("%".to_string()), 1, false);
978
979        let mut memory_counter =
980            trace.create_counter_track("Memory", CounterUnit::SizeBytes, 1024 * 1024, false);
981
982        let mut count_counter = trace.create_counter_track(
983            "Events",
984            CounterUnit::Count,
985            1,
986            true, // incremental
987        );
988
989        // Record some values
990        let t1 = time();
991        cpu_counter.record_f64(t1, 42.5);
992        memory_counter.record_i64(t1, 1024);
993        count_counter.record_i64(t1, 100);
994
995        let t2 = time();
996        cpu_counter.record_f64(t2, 75.0);
997        memory_counter.record_i64(t2, 2048);
998        count_counter.record_i64(t2, 50);
999
1000        // Process the thread data to convert events to trace packets
1001        let thread_data = ThreadTraceData::take_current_thread();
1002        trace.process_thread_data(&thread_data);
1003
1004        // Verify we can encode without errors
1005        let bytes = trace.encode_to_vec();
1006        assert!(!bytes.is_empty());
1007    }
1008}