matrix_sdk_common/
tracing_timer.rs

1// Copyright 2023 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use ruma::time::Instant;
16use tracing::{callsite::DefaultCallsite, Callsite as _};
17
18/// A named RAII that will show on `Drop` how long its covered section took to
19/// execute.
20pub struct TracingTimer {
21    id: String,
22    callsite: &'static DefaultCallsite,
23    start: Instant,
24    level: tracing::Level,
25}
26
27#[cfg(not(tarpaulin_include))]
28impl std::fmt::Debug for TracingTimer {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        f.debug_struct("TracingTimer").field("id", &self.id).field("start", &self.start).finish()
31    }
32}
33
34impl Drop for TracingTimer {
35    fn drop(&mut self) {
36        let message = format!("{} finished in {}ms", self.id, self.start.elapsed().as_millis());
37
38        let enabled = tracing::level_enabled!(self.level) && {
39            let interest = self.callsite.interest();
40            !interest.is_never()
41                && tracing::__macro_support::__is_enabled(self.callsite.metadata(), interest)
42        };
43
44        if !enabled {
45            return;
46        }
47
48        let metadata = self.callsite.metadata();
49        let fields = metadata.fields();
50        let message_field = fields.field("message").unwrap();
51        #[allow(trivial_casts)] // The compiler is lying, it can't infer this cast
52        let values = [(&message_field, Some(&message as &dyn tracing::Value))];
53
54        // This function is hidden from docs, but we have to use it
55        // because there is no other way of obtaining a `ValueSet`.
56        // It's not entirely clear why it is private. See this issue:
57        // https://github.com/tokio-rs/tracing/issues/2363
58        let values = fields.value_set(&values);
59
60        tracing::Event::dispatch(metadata, &values);
61    }
62}
63
64impl TracingTimer {
65    /// Create a new `TracingTimer` at the `debug` log level.
66    pub fn new_debug(
67        callsite: &'static DefaultCallsite,
68        id: String,
69        level: tracing::Level,
70    ) -> Self {
71        Self { id, callsite, start: Instant::now(), level }
72    }
73}
74
75/// Macro to create a RAII timer that will log on `Drop` how long its covered
76/// section took to execute.
77///
78/// The tracing level can be specified as a first argument, but it's optional.
79/// If it's missing, this will use the debug level.
80///
81/// ```rust,no_run
82/// # fn do_long_computation(_x: u32) {}
83/// # fn main() {
84/// use matrix_sdk_common::timer;
85///
86/// // It's possible to specify the tracing level we want to be used for the log message on drop.
87/// {
88///     let _timer = timer!(tracing::Level::TRACE, "do long computation");
89///     // But it's optional; by default it's set to `DEBUG`.
90///     let _debug_timer = timer!("do long computation but time it in DEBUG");
91///     // The macro doesn't support formatting / structured events (yet?), but you can use
92///     // `format!()` for that.
93///     let other_timer = timer!(format!("do long computation for parameter = {}", 123));
94///     do_long_computation(123);
95/// } // The log statements will happen here.
96/// # }
97/// ```
98#[macro_export]
99macro_rules! timer {
100    ($level:expr, $string:expr) => {{
101        static __CALLSITE: tracing::callsite::DefaultCallsite = tracing::callsite2! {
102            name: tracing::__macro_support::concat!(
103                "event ",
104                file!(),
105                ":",
106                line!(),
107            ),
108            kind: tracing::metadata::Kind::EVENT,
109            target: module_path!(),
110            level: $level,
111            fields: []
112        };
113
114        $crate::tracing_timer::TracingTimer::new_debug(&__CALLSITE, $string.into(), $level)
115    }};
116
117    ($string:expr) => {
118        $crate::timer!(tracing::Level::DEBUG, $string)
119    };
120}
121
122#[cfg(test)]
123mod tests {
124    #[cfg(not(target_family = "wasm"))]
125    #[matrix_sdk_test_macros::async_test]
126    async fn test_timer_name() {
127        use tracing::{span, Level};
128
129        tracing::warn!("Starting test...");
130
131        mod time123 {
132            pub async fn run() {
133                let _timer_guard = timer!(tracing::Level::DEBUG, "test");
134                tokio::time::sleep(ruma::time::Duration::from_millis(123)).await;
135                // Displays: 2023-08-25T15:18:31.169498Z DEBUG
136                // matrix_sdk_common::tracing_timer::tests: test finished in
137                // 124ms
138            }
139        }
140
141        time123::run().await;
142
143        let span = span!(Level::DEBUG, "le 256ms span");
144        let _guard = span.enter();
145
146        let _timer_guard = timer!("in span");
147        tokio::time::sleep(ruma::time::Duration::from_millis(256)).await;
148
149        tracing::warn!("Test about to finish.");
150        // Displays: 2023-08-25T15:18:31.427070Z DEBUG le 256ms span:
151        // matrix_sdk_common::tracing_timer::tests: in span finished in 257ms
152    }
153}