1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
use std::time::SystemTime;

#[cfg(feature = "enabled")]
pub use serde_json::{json, to_writer as json_to_writer, Value as JsonValue};

pub fn time_to_micros(time: SystemTime) -> u64 {
    time.duration_since(SystemTime::UNIX_EPOCH)
        .unwrap()
        .as_micros() as u64
}

pub fn now_micros() -> u64 {
    time_to_micros(SystemTime::now())
}

pub enum TimeInput {
    Microseconds(u64),
    SystemTime(SystemTime),
}

impl TimeInput {
    pub fn micros(self) -> u64 {
        match self {
            Self::Microseconds(v) => v,
            Self::SystemTime(v) => time_to_micros(v),
        }
    }
}

impl From<u64> for TimeInput {
    fn from(value: u64) -> Self {
        Self::Microseconds(value)
    }
}

impl From<SystemTime> for TimeInput {
    fn from(value: SystemTime) -> Self {
        Self::SystemTime(value)
    }
}

/// Declare traces group.
///
/// Creates a module with the passed `$name`.
///
/// **Note:**Traces are stored locally for each thread (`thread_local`).
///
/// Module exposes two main methods:
/// - `fn start_tracing()` - Simply calls `take_traces()` and discards
///   the result, in order to clean up old traces.
/// - `fn take_traces()` - Take accumulated traces.
#[cfg(feature = "enabled")]
#[macro_export]
macro_rules! decl_traces {
    ($name:ident; $($checkpoint:ident),+) => {

        pub mod $name {
            use std::rc::Rc;
            use std::cell::RefCell;

            #[derive(serde::Serialize, Debug, Default, Clone)]
            pub struct Traces {
                $(
                pub $checkpoint: (u64, $crate::JsonValue),
                )*
            }

            impl std::fmt::Display for Traces {
                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                    let mut arr = [
                        $(
                        (stringify!($checkpoint), self.$checkpoint.0, &self.$checkpoint.1),
                        )+
                    ];
                    arr.sort_by_key(|v| v.1);
                    let mut buf = Vec::new();
                    for (name, time, meta) in arr.into_iter().filter(|v| v.1 != 0) {
                        $crate::json_to_writer(&mut buf, &(name, (time as f64) / 1_000_000.0)).unwrap();
                        buf.push(b'\n');
                        if !meta.is_null() {
                            $crate::json_to_writer(&mut buf, meta).unwrap();
                            buf.push(b'\n');
                        }
                    }
                    write!(f, "{}", &String::from_utf8(buf).unwrap())
                }
            }

            impl From<Traces> for String {
                fn from(t: Traces) -> Self {
                    t.to_string()
                }
            }

            thread_local! {
                pub static TRACES: Rc<RefCell<Traces>> = Default::default();
            }

            /// Clean up old traces and start fresh.
            pub fn start_tracing() {
                take_traces();
            }

            /// Take captured traces.
            pub fn take_traces() -> Traces {
                TRACES.with(|v| v.take())
            }

            #[cfg(feature = "ocaml_types")]
            pub mod caml {
                use super::*;

                #[derive(Debug, ocaml::IntoValue, ocaml::FromValue, ocaml_gen::Struct)]
                pub struct CamlTraces(String);

                impl From<Traces> for CamlTraces {
                    fn from(t: Traces) -> Self {
                        Self(t.to_string())
                    }
                }
            }
        }
    };
}
/// Noop. Internal tracing not enabled!
#[cfg(not(feature = "enabled"))]
#[macro_export]
macro_rules! decl_traces {
    ($($_ignored:tt)+) => {};
}

/// Capture the trace/checkpoint.
#[cfg(feature = "enabled")]
#[macro_export]
macro_rules! checkpoint {
    ($name:ident; $checkpoint:ident) => {
        $crate::checkpoint!(|$name; $checkpoint, $crate::now_micros(), $crate::json!(null));
    };
    ($name:ident; $checkpoint:ident, {$($metadata:tt)+}) => {
        $crate::checkpoint!(|$name; $checkpoint, $crate::now_micros(), $crate::json!({$($metadata)+}));
    };
    ($name:ident; $checkpoint:ident, $time:expr) => {
        $crate::checkpoint!(|$name; $checkpoint, $crate::TimeInput::from($time).micros(), $crate::json!(null));
    };
    ($name:ident; $checkpoint:ident, $time:expr, {$($metadata:tt)+}) => {
        $crate::checkpoint!(|$name; $checkpoint, $crate::TimeInput::from($time).micros(), $crate::json!({$($metadata)+}));
    };
    (|$name:ident; $checkpoint:ident, $time:expr, $metadata:expr) => {
        $name::TRACES.with(|traces| traces.borrow_mut().$checkpoint = ($time, $metadata));
    };
}
/// Noop. Internal tracing not enabled!
#[cfg(not(feature = "enabled"))]
#[macro_export]
macro_rules! checkpoint {
    ($($_ignored:tt)+) => {};
}

#[cfg(feature = "enabled")]
#[cfg(test)]
mod tests {
    use super::*;

    decl_traces!(test_fn; c1, c2, c3, c4);

    #[test]
    fn test_fn() {
        test_fn::start_tracing();

        checkpoint!(test_fn; c1);
        checkpoint!(test_fn; c2, 2);
        checkpoint!(test_fn; c3, { "arg": 1 });
        checkpoint!(test_fn; c4, 3, { "arg": 2 });

        let traces = test_fn::take_traces();

        assert_ne!(traces.c1.0, 0);
        assert_eq!(traces.c1.1, serde_json::Value::Null);

        assert_eq!(traces.c2.0, 2);
        assert_eq!(traces.c2.1, serde_json::Value::Null);

        assert_ne!(traces.c3.0, 0);
        assert_eq!(traces.c3.1, serde_json::json!({ "arg": 1 }));

        assert_eq!(traces.c4.0, 3);
        assert_eq!(traces.c4.1, serde_json::json!({ "arg": 2 }));
    }
}