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
59impl Clone for Client {
60    /// A cheaper alternative to [`Client::start`] or [`Client::running`]  when there is already a
61    /// handle handy.
62    fn clone(&self) -> Self {
63        // We already know that the state is `ENABLED`, no need to check.
64        Self(())
65    }
66}
67
68#[cfg(all(feature = "enable", feature = "manual-lifetime"))]
69mod manual_lifetime {
70    use std::sync::atomic::Ordering;
71    /// Enabling `Tracy` when it is already enabled, or Disabling when it is already disabled will
72    /// cause applications to crash. I personally think it would be better if this was a sort-of
73    /// reference counted kind-of thing so you could enable as many times as you wish and disable
74    /// just as many times without any reprecursions. At the very least this could significantly
75    /// help tests.
76    ///
77    /// We can also try to implement something like this ourselves. To do this we'd want to track 4
78    /// states that construct a following finite state machine:
79    ///
80    /// ```text
81    ///     0 = disabled  -> 1 = enabling
82    ///         ^                v
83    ///     3 = disabling <- 2 = enabled
84    /// ```
85    ///
86    /// And also include a reference count somewhere in there. Something we can throw in a static
87    /// would be ideal.
88    ///
89    /// Alas, Tracy's extensive use of thread-local storage presents us with another problem – we must
90    /// start up and shut down the client within the same thread. A most straightforward soution for
91    /// that would be to run a separate thread that would be dedicated entirely to just starting up and
92    /// shutting down the profiler.
93    ///
94    /// All that seems like a major pain to implement, and so we’ll punt on disabling entirely until
95    /// somebody comes with a good use-case warranting that sort of complexity.
96    #[cfg(not(loom))]
97    static CLIENT_STATE: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
98    #[cfg(loom)]
99    loom::lazy_static! {
100        static ref CLIENT_STATE: loom::sync::atomic::AtomicUsize =
101            loom::sync::atomic::AtomicUsize::new(0);
102    }
103
104    const STATE_STEP: usize = 1; // Move forward by 1 step in the FSM
105    const STATE_DISABLED: usize = 0;
106    const STATE_ENABLING: usize = STATE_DISABLED + STATE_STEP;
107    const STATE_ENABLED: usize = STATE_ENABLING + STATE_STEP;
108
109    #[inline(always)]
110    fn spin_loop() {
111        #[cfg(loom)]
112        loom::thread::yield_now();
113        #[cfg(not(loom))]
114        std::hint::spin_loop();
115    }
116
117    pub(super) fn start() -> super::Client {
118        let mut old_state = CLIENT_STATE.load(Ordering::Relaxed);
119        loop {
120            match old_state {
121                STATE_ENABLED => return super::Client(()),
122                STATE_ENABLING => {
123                    while !is_running() {
124                        spin_loop();
125                    }
126                    return super::Client(());
127                }
128                STATE_DISABLED => {
129                    let result = CLIENT_STATE.compare_exchange_weak(
130                        old_state,
131                        STATE_ENABLING,
132                        Ordering::Relaxed,
133                        Ordering::Relaxed,
134                    );
135                    if let Err(next_old_state) = result {
136                        old_state = next_old_state;
137                        continue;
138                    } else {
139                        unsafe {
140                            // SAFE: This function must not be called if the profiler has
141                            // already been enabled. While in practice calling this function
142                            // multiple times will only serve to trigger an assertion, we
143                            // cannot exactly rely on this, since it is an undocumented
144                            // behaviour and the upstream might very well just decide to invoke
145                            // UB instead. In the case there are multiple copies of
146                            // `tracy-client` this invariant is not actually maintained, but
147                            // otherwise this is sound due to the `ENABLE_STATE` that we
148                            // manage.
149                            //
150                            // TODO: we _could_ define `ENABLE_STATE` in the `-sys` crate...
151                            let () = sys::___tracy_startup_profiler();
152                            CLIENT_STATE.store(STATE_ENABLED, Ordering::Release);
153                            return super::Client(());
154                        }
155                    }
156                }
157                _ => unreachable!(),
158            }
159        }
160    }
161
162    pub(super) fn is_running() -> bool {
163        return CLIENT_STATE.load(Ordering::Relaxed) == STATE_ENABLED;
164    }
165
166    #[cfg(test)]
167    mod test {
168        use super::*;
169
170        #[test]
171        fn state_transitions() {
172            assert_eq!(0, STATE_DISABLED);
173            assert_eq!(STATE_DISABLED.wrapping_add(STATE_STEP), STATE_ENABLING);
174            assert_eq!(STATE_ENABLING.wrapping_add(STATE_STEP), STATE_ENABLED);
175        }
176    }
177}