mnemosyne-prof 0.2.0

Allocation profiling support for Mnemosyne
Documentation
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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
#[cfg(all(not(nightly_tls_active), not(feature = "std_tls"), not(miri)))]
use core::sync::atomic::Ordering;

#[derive(Clone, Copy)]
pub(crate) struct ThreadState {
    pub(crate) bytes_until_sample: isize,
    pub(crate) in_hook: bool,
}

#[inline(always)]
pub(crate) fn sample_debit(size: usize) -> isize {
    match isize::try_from(size) {
        Ok(size) => size,
        Err(_) => isize::MAX,
    }
}

#[cfg(nightly_tls_active)]
#[thread_local]
static mut THREAD_STATE: ThreadState = ThreadState {
    bytes_until_sample: 0,
    in_hook: false,
};

#[cfg(not(nightly_tls_active))]
std::thread_local! {
    static THREAD_STATE: core::cell::UnsafeCell<ThreadState> = const {
        core::cell::UnsafeCell::new(ThreadState {
            bytes_until_sample: 0,
            in_hook: false,
        })
    };
}

#[cfg(all(not(nightly_tls_active), not(feature = "std_tls"), not(miri)))]
static PROFILER_TLS_KEY: core::sync::atomic::AtomicU32 =
    core::sync::atomic::AtomicU32::new(u32::MAX);

#[cfg(all(not(nightly_tls_active), not(feature = "std_tls"), not(miri)))]
#[inline(always)]
fn get_os_tls_key(atomic_key: &core::sync::atomic::AtomicU32) -> Option<u32> {
    // The atomic publishes an immutable OS TLS slot index only. It does not
    // protect any Rust memory dependency, so relaxed ordering is sufficient.
    let mut key = atomic_key.load(Ordering::Relaxed);
    if key == u32::MAX {
        key = init_os_tls_key(atomic_key)?;
    }
    Some(key)
}

#[cfg(all(not(nightly_tls_active), not(feature = "std_tls"), not(miri)))]
#[cold]
#[inline(never)]
fn init_os_tls_key(atomic_key: &core::sync::atomic::AtomicU32) -> Option<u32> {
    // SAFETY: each branch calls the platform TLS-key FFI with valid arguments —
    // `TlsAlloc`/`TlsFree` take no pointers, `pthread_key_create` receives a
    // valid `&mut key` out-param and a `None` destructor, and any key passed to
    // `TlsFree`/`pthread_key_delete` was just allocated by this call. On a lost
    // publication CAS the freshly-allocated key is freed exactly once.
    unsafe {
        #[cfg(windows)]
        {
            unsafe extern "system" {
                fn TlsAlloc() -> u32;
                fn TlsFree(dwTlsIndex: u32) -> i32;
            }
            let key = TlsAlloc();
            if key == u32::MAX {
                return None;
            }
            match atomic_key.compare_exchange(u32::MAX, key, Ordering::Relaxed, Ordering::Relaxed) {
                Ok(_) => Some(key),
                Err(existing) => {
                    TlsFree(key);
                    (existing != u32::MAX).then_some(existing)
                }
            }
        }
        #[cfg(not(windows))]
        {
            unsafe extern "C" {
                fn pthread_key_create(
                    key: *mut u32,
                    destructor: Option<unsafe extern "C" fn(*mut core::ffi::c_void)>,
                ) -> i32;
                fn pthread_key_delete(key: u32) -> i32;
            }
            let mut key = 0u32;
            let res = pthread_key_create(&mut key, None);
            if res != 0 {
                return None;
            }
            match atomic_key.compare_exchange(u32::MAX, key, Ordering::Relaxed, Ordering::Relaxed) {
                Ok(_) => Some(key),
                Err(existing) => {
                    pthread_key_delete(key);
                    (existing != u32::MAX).then_some(existing)
                }
            }
        }
    }
}

#[cfg(all(not(nightly_tls_active), not(feature = "std_tls"), not(miri)))]
#[allow(dead_code)]
#[inline(always)]
fn get_os_tls_value(key: u32) -> *mut core::ffi::c_void {
    // SAFETY: `key` was returned by a successful `get_os_tls_key`, so it is a
    // valid allocated TLS slot index; the platform getter reads this thread's
    // own slot and returns null for an unset slot — it never dereferences `key`.
    unsafe {
        #[cfg(windows)]
        {
            unsafe extern "system" {
                fn TlsGetValue(dwTlsIndex: u32) -> *mut core::ffi::c_void;
            }
            TlsGetValue(key)
        }
        #[cfg(not(windows))]
        {
            unsafe extern "C" {
                fn pthread_getspecific(key: u32) -> *mut core::ffi::c_void;
            }
            pthread_getspecific(key)
        }
    }
}

#[cfg(all(not(nightly_tls_active), not(feature = "std_tls"), not(miri)))]
#[allow(dead_code)]
#[inline(always)]
fn set_os_tls_value(key: u32, value: *mut core::ffi::c_void) {
    // SAFETY: `key` is a valid allocated TLS slot index; the platform setter
    // stores the opaque `value` in this thread's own slot without dereferencing
    // it.
    unsafe {
        #[cfg(windows)]
        {
            unsafe extern "system" {
                fn TlsSetValue(dwTlsIndex: u32, lpTlsValue: *mut core::ffi::c_void) -> i32;
            }
            TlsSetValue(key, value);
        }
        #[cfg(not(windows))]
        {
            unsafe extern "C" {
                fn pthread_setspecific(key: u32, value: *const core::ffi::c_void) -> i32;
            }
            pthread_setspecific(key, value);
        }
    }
}

#[cfg(all(
    not(nightly_tls_active),
    not(feature = "std_tls"),
    all(windows, target_arch = "x86_64"),
    not(miri)
))]
/// Reads the value stored in this thread's TEB TLS slot `index`.
///
/// # Safety
///
/// `index` must be a TLS slot index obtained from `TlsAlloc` (so the slot is
/// reserved for this process). The caller relies on the Windows x86-64 TEB
/// layout documented inline below.
#[inline(always)]
unsafe fn get_teb_tls_slot(index: u32) -> *mut core::ffi::c_void {
    if index < 64 {
        let val: *mut core::ffi::c_void;
        // SAFETY: on Windows x86-64 the `gs` segment base is the current
        // thread's TEB, and `gs:[0x1480 + index*8]` indexes the TEB's fixed
        // `TlsSlots[64]` array (offset 0x1480 on x64). For `index < 64` this is
        // a single aligned load of this thread's own slot — always-mapped
        // thread-local OS storage, no side effects (`nostack`, `readonly`).
        unsafe {
            core::arch::asm!(
                "mov {}, gs:[0x1480 + {} * 8]",
                out(reg) val,
                in(reg) index as usize,
                options(nostack, preserves_flags, readonly)
            );
        }
        val
    } else {
        let teb: *mut u8;
        // SAFETY: `gs:[0x30]` is the TEB self-pointer (`NtCurrentTeb`); a single
        // aligned read of an always-mapped field, no side effects.
        unsafe {
            core::arch::asm!(
                "mov {}, gs:[0x30]",
                out(reg) teb,
                options(nostack, preserves_flags, readonly)
            );
        }
        // SAFETY: `TEB + 0x1780` is the `TlsExpansionSlots` pointer field (fixed
        // x64 offset); reading it yields the (possibly null) base of the
        // expansion-slot array for indices >= 64.
        let expansion_slots = unsafe { *(teb.add(0x1780) as *mut *mut *mut core::ffi::c_void) };
        if expansion_slots.is_null() {
            core::ptr::null_mut()
        } else {
            // SAFETY: the expansion array is non-null (just checked) and was
            // sized to cover every allocated index >= 64, so `index - 64` is in
            // bounds for a slot reserved by `TlsAlloc`.
            unsafe { *expansion_slots.add(index as usize - 64) }
        }
    }
}

#[cfg(all(
    not(nightly_tls_active),
    not(feature = "std_tls"),
    all(windows, target_arch = "x86_64"),
    not(miri)
))]
/// Stores `value` in this thread's TEB TLS slot `index`.
///
/// # Safety
///
/// `index` must be a TLS slot index obtained from `TlsAlloc`. The caller relies
/// on the Windows x86-64 TEB layout documented inline below.
#[inline(always)]
unsafe fn set_teb_tls_slot(index: u32, value: *mut core::ffi::c_void) {
    if index < 64 {
        // SAFETY: `gs:[0x1480 + index*8]` is this thread's own `TlsSlots[index]`
        // entry (TEB `TlsSlots[64]` array, fixed x64 offset 0x1480); a single
        // aligned store to always-mapped thread-local OS storage.
        unsafe {
            core::arch::asm!(
                "mov gs:[0x1480 + {} * 8], {}",
                in(reg) index as usize,
                in(reg) value,
                options(nostack, preserves_flags)
            );
        }
    } else {
        let teb: *mut u8;
        // SAFETY: `gs:[0x30]` is the TEB self-pointer; a single aligned read.
        unsafe {
            core::arch::asm!(
                "mov {}, gs:[0x30]",
                out(reg) teb,
                options(nostack, preserves_flags, readonly)
            );
        }
        // SAFETY: `TEB + 0x1780` is the `TlsExpansionSlots` pointer field; read
        // the (possibly null) expansion-array base.
        let expansion_slots = unsafe { *(teb.add(0x1780) as *mut *mut *mut core::ffi::c_void) };
        if !expansion_slots.is_null() {
            // SAFETY: the array is non-null (just checked) and covers every
            // allocated index >= 64, so `index - 64` is an in-bounds slot.
            unsafe { *expansion_slots.add(index as usize - 64) = value };
        }
    }
}

#[cfg(not(nightly_tls_active))]
#[inline(always)]
pub(crate) fn get_profiler_state() -> *mut ThreadState {
    #[cfg(any(feature = "std_tls", miri))]
    {
        THREAD_STATE.with(|cell| cell.get())
    }
    #[cfg(all(not(feature = "std_tls"), not(miri)))]
    {
        #[cfg(all(windows, target_arch = "x86_64"))]
        {
            let Some(key) = get_os_tls_key(&PROFILER_TLS_KEY) else {
                return THREAD_STATE.with(|cell| cell.get());
            };
            // SAFETY: `key` is the profiler's own `TlsAlloc`-allocated slot index.
            let ptr = unsafe { get_teb_tls_slot(key) } as *mut ThreadState;
            if !ptr.is_null() {
                ptr
            } else {
                THREAD_STATE.with(|cell| {
                    let p = cell.get();
                    // SAFETY: `key` is the profiler's allocated slot; we publish
                    // this thread's own `THREAD_STATE` cell pointer into it so
                    // future reads on this thread reuse the same state.
                    unsafe { set_teb_tls_slot(key, p as *mut core::ffi::c_void) };
                    p
                })
            }
        }
        #[cfg(not(all(windows, target_arch = "x86_64")))]
        {
            let Some(key) = get_os_tls_key(&PROFILER_TLS_KEY) else {
                return THREAD_STATE.with(|cell| cell.get());
            };
            let ptr = get_os_tls_value(key) as *mut ThreadState;
            if !ptr.is_null() {
                ptr
            } else {
                THREAD_STATE.with(|cell| {
                    let p = cell.get();
                    set_os_tls_value(key, p as *mut core::ffi::c_void);
                    p
                })
            }
        }
    }
}

#[inline(always)]
pub(crate) fn should_skip_alloc_fast_path(
    size: usize,
    hook_absent: bool,
    leak_inactive: bool,
) -> bool {
    // SAFETY: `THREAD_STATE` is this thread's own `#[thread_local]` static, so
    // the reentrancy check and `bytes_until_sample` update cannot race another
    // thread; the `in_hook` guard prevents nested mutation within the thread.
    // `&raw mut` sidesteps a direct `static mut` reference (`static_mut_refs`
    // is deny-by-default in edition 2024); the exclusive reborrow is sound
    // because the static is thread-local and no other reference to it is live
    // across this call.
    #[cfg(nightly_tls_active)]
    unsafe {
        should_skip_alloc_fast_path_state(
            &mut *(&raw mut THREAD_STATE),
            size,
            hook_absent,
            leak_inactive,
        )
    }
    // SAFETY: `get_profiler_state()` returns this thread's own thread-local
    // `ThreadState`; the `&mut` is exclusive (thread-local) and the `in_hook`
    // check below rejects re-entry before any nested `&mut` could form.
    #[cfg(not(nightly_tls_active))]
    unsafe {
        should_skip_alloc_fast_path_state(
            &mut *get_profiler_state(),
            size,
            hook_absent,
            leak_inactive,
        )
    }
}

#[inline(always)]
fn should_skip_alloc_fast_path_state(
    state: &mut ThreadState,
    size: usize,
    hook_absent: bool,
    leak_inactive: bool,
) -> bool {
    if state.in_hook {
        return true;
    }

    if hook_absent && leak_inactive {
        let debit = sample_debit(size);
        if state.bytes_until_sample > debit {
            state.bytes_until_sample -= debit;
            return true;
        }
    }

    false
}

#[inline(always)]
pub(crate) fn enter_hook() -> bool {
    // SAFETY: `THREAD_STATE` is a `#[thread_local]` static owned exclusively by
    // the current thread, so the read-modify-write of `in_hook` cannot race
    // another thread; it is the guard that establishes single-entry, so no
    // nested `&mut` to the state is live while this runs.
    #[cfg(nightly_tls_active)]
    unsafe {
        if THREAD_STATE.in_hook {
            true
        } else {
            THREAD_STATE.in_hook = true;
            false
        }
    }
    // SAFETY: `get_profiler_state()` returns this thread's own thread-local
    // `ThreadState`; the pointee is exclusive to the current thread, and this
    // call is the re-entrancy guard itself, so no other `&mut` to it is live.
    #[cfg(not(nightly_tls_active))]
    unsafe {
        let state = &mut *get_profiler_state();
        if state.in_hook {
            true
        } else {
            state.in_hook = true;
            false
        }
    }
}

#[inline(always)]
pub(crate) fn exit_hook() {
    // SAFETY: `THREAD_STATE` is this thread's own `#[thread_local]` static;
    // clearing `in_hook` is an exclusive thread-local write.
    #[cfg(nightly_tls_active)]
    unsafe {
        THREAD_STATE.in_hook = false;
    }
    // SAFETY: `get_profiler_state()` returns this thread's own thread-local
    // state; clearing `in_hook` through it is an exclusive thread-local write
    // paired with the `enter_hook` that set it.
    #[cfg(not(nightly_tls_active))]
    unsafe {
        (*get_profiler_state()).in_hook = false;
    }
}

#[cfg(nightly_tls_active)]
#[inline(always)]
pub(crate) fn get_bytes_until_sample() -> isize {
    // SAFETY: `THREAD_STATE` is this thread's own `#[thread_local]` static; the
    // read of `bytes_until_sample` cannot race another thread.
    unsafe { THREAD_STATE.bytes_until_sample }
}

#[cfg(nightly_tls_active)]
#[inline(always)]
pub(crate) fn set_bytes_until_sample(val: isize) {
    // SAFETY: `THREAD_STATE` is this thread's own `#[thread_local]` static; the
    // write to `bytes_until_sample` is an exclusive thread-local store.
    unsafe {
        THREAD_STATE.bytes_until_sample = val;
    }
}