tracy_client/
state.rs

1use crate::Client;
2
3/// Client initialization and lifetime management.
4impl Client {
5    /// Start the client.
6    ///
7    /// The client must be started with this function before any instrumentation is invoked
8    /// anywhere in the process. This function can be called multiple times to obtain multiple
9    /// `Client` values.
10    ///
11    /// The underlying client implementation will be started up only if it wasn't already running
12    /// yet.
13    ///
14    /// Note that when the `manual-lifetime` feature is used, it is a responsibility of the user
15    /// to stop `tracy` using the [`sys::___tracy_shutdown_profiler`] function. Keep in mind that
16    /// at the time this function is called there can be no other invocations to the tracy
17    /// profiler, even from other threads (or you may get a crash!)
18    ///
19    /// # Example
20    ///
21    /// ```rust
22    /// // fn main() {
23    ///     let _client = tracy_client::Client::start();
24    ///     // ...
25    /// // }
26    /// ```
27    pub fn start() -> Self {
28        #[cfg(not(feature = "enable"))]
29        return Self(());
30        #[cfg(all(feature = "enable", feature = "manual-lifetime"))]
31        return manual_lifetime::start();
32        #[cfg(all(feature = "enable", not(feature = "manual-lifetime")))]
33        return Self(());
34    }
35
36    /// Obtain a client handle, but only if the client is already running.
37    #[must_use]
38    pub fn running() -> Option<Self> {
39        if Self::is_running() {
40            Some(Self(()))
41        } else {
42            None
43        }
44    }
45
46    /// Is the client already running?
47    pub fn is_running() -> bool {
48        #[cfg(not(feature = "enable"))]
49        return true; // If the client is disabled, produce a "no-op" one so that users don’t need
50                     // to wory about conditional use of the instrumentation in their own code.
51        #[cfg(all(feature = "enable", feature = "manual-lifetime"))]
52        return manual_lifetime::is_running();
53        #[cfg(all(feature = "enable", not(feature = "manual-lifetime")))]
54        return true; // The client is started in life-before-main (or upon first use in case of
55                     // `delayed-init`
56    }
57
58    /// Is the client running and a profiler connected?
59    pub fn is_connected() -> bool {
60        #[cfg(not(feature = "enable"))]
61        return false;
62        #[cfg(feature = "enable")]
63        return Self::is_running() && unsafe { sys::___tracy_connected() != 0 };
64    }
65}
66
67impl Clone for Client {
68    /// A cheaper alternative to [`Client::start`] or [`Client::running`]  when there is already a
69    /// handle handy.
70    fn clone(&self) -> Self {
71        // We already know that the state is `ENABLED`, no need to check.
72        Self(())
73    }
74}
75
76#[cfg(all(feature = "enable", feature = "manual-lifetime"))]
77mod manual_lifetime {
78    use std::sync::atomic::Ordering;
79    /// Enabling `Tracy` when it is already enabled, or Disabling when it is already disabled will
80    /// cause applications to crash. I personally think it would be better if this was a sort-of
81    /// reference counted kind-of thing so you could enable as many times as you wish and disable
82    /// just as many times without any reprecursions. At the very least this could significantly
83    /// help tests.
84    ///
85    /// We can also try to implement something like this ourselves. To do this we'd want to track 4
86    /// states that construct a following finite state machine:
87    ///
88    /// ```text
89    ///     0 = disabled  -> 1 = enabling
90    ///         ^                v
91    ///     3 = disabling <- 2 = enabled
92    /// ```
93    ///
94    /// And also include a reference count somewhere in there. Something we can throw in a static
95    /// would be ideal.
96    ///
97    /// Alas, Tracy's extensive use of thread-local storage presents us with another problem – we must
98    /// start up and shut down the client within the same thread. A most straightforward soution for
99    /// that would be to run a separate thread that would be dedicated entirely to just starting up and
100    /// shutting down the profiler.
101    ///
102    /// All that seems like a major pain to implement, and so we’ll punt on disabling entirely until
103    /// somebody comes with a good use-case warranting that sort of complexity.
104    #[cfg(not(loom))]
105    static CLIENT_STATE: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
106    #[cfg(loom)]
107    loom::lazy_static! {
108        static ref CLIENT_STATE: loom::sync::atomic::AtomicUsize =
109            loom::sync::atomic::AtomicUsize::new(0);
110    }
111
112    const STATE_STEP: usize = 1; // Move forward by 1 step in the FSM
113    const STATE_DISABLED: usize = 0;
114    const STATE_ENABLING: usize = STATE_DISABLED + STATE_STEP;
115    const STATE_ENABLED: usize = STATE_ENABLING + STATE_STEP;
116
117    #[inline(always)]
118    fn spin_loop() {
119        #[cfg(loom)]
120        loom::thread::yield_now();
121        #[cfg(not(loom))]
122        std::hint::spin_loop();
123    }
124
125    pub(super) fn start() -> super::Client {
126        let mut old_state = CLIENT_STATE.load(Ordering::Relaxed);
127        loop {
128            match old_state {
129                STATE_ENABLED => return super::Client(()),
130                STATE_ENABLING => {
131                    while !is_running() {
132                        spin_loop();
133                    }
134                    return super::Client(());
135                }
136                STATE_DISABLED => {
137                    let result = CLIENT_STATE.compare_exchange_weak(
138                        old_state,
139                        STATE_ENABLING,
140                        Ordering::Relaxed,
141                        Ordering::Relaxed,
142                    );
143                    if let Err(next_old_state) = result {
144                        old_state = next_old_state;
145                        continue;
146                    } else {
147                        unsafe {
148                            // SAFE: This function must not be called if the profiler has
149                            // already been enabled. While in practice calling this function
150                            // multiple times will only serve to trigger an assertion, we
151                            // cannot exactly rely on this, since it is an undocumented
152                            // behaviour and the upstream might very well just decide to invoke
153                            // UB instead. In the case there are multiple copies of
154                            // `tracy-client` this invariant is not actually maintained, but
155                            // otherwise this is sound due to the `ENABLE_STATE` that we
156                            // manage.
157                            //
158                            // TODO: we _could_ define `ENABLE_STATE` in the `-sys` crate...
159                            let () = sys::___tracy_startup_profiler();
160                            CLIENT_STATE.store(STATE_ENABLED, Ordering::Release);
161                            return super::Client(());
162                        }
163                    }
164                }
165                _ => unreachable!(),
166            }
167        }
168    }
169
170    pub(super) fn is_running() -> bool {
171        return CLIENT_STATE.load(Ordering::Relaxed) == STATE_ENABLED;
172    }
173
174    #[cfg(test)]
175    mod test {
176        use super::*;
177
178        #[test]
179        fn state_transitions() {
180            assert_eq!(0, STATE_DISABLED);
181            assert_eq!(STATE_DISABLED.wrapping_add(STATE_STEP), STATE_ENABLING);
182            assert_eq!(STATE_ENABLING.wrapping_add(STATE_STEP), STATE_ENABLED);
183        }
184    }
185}