Skip to main content

perf_event/
hooks.rs

1//! Intercepting perf-event system calls, for testing and logging.
2//!
3//! Note: this module is only available when the `"hooks"` feature is enabled.
4//!
5//! Many performance counters' behavior is inherently
6//! non-deterministic, making it difficult to write tests for code
7//! that uses the `perf_event` crate. There may be no way to reliably
8//! provoke the Linux kernel into exhibiting the behavior you want to
9//! test against. Or you may want to test functionality like
10//! whole-system profiling, which requires elevated privileges that
11//! one would prefer to avoid granting to tests.
12//!
13//! This module lets you interpose your own implementation of all the
14//! system calls and ioctls that `perf_event` uses, granting you
15//! complete control over `perf_event`'s interactions with the outside
16//! world. You can verify that the system calls receive the parameters
17//! you expect, and provide whatever sorts of interesting responses
18//! you need.
19//!
20//! There are three main pieces:
21//!
22//! - The [`Hooks`] trait has a method for every system call and ioctl
23//!   that the `perf_event` crate uses.
24//!
25//! - The [`set_thread_hooks`] function lets you provide a `Box<dyn Hooks>`
26//!   trait object whose methods the calling thread will use for all subsequent
27//!   `perf_event` operations.
28//!
29//! - The [`clear_thread_hooks`] function restores the thread's
30//!   original state, so that subsequent `perf_event` operations use
31//!   the real Linux system calls.
32//!
33//! This functionality is too low-level for direct use in tests, but
34//! it does provide the means with which one can build more ergonomic
35//! test harnesses.
36//!
37//! ## Stability
38//!
39//! Using `set_thread_hooks`, you can observe the exact sequence of
40//! system operations that the `perf_event` crate performs to carry
41//! out requests from the user. Even if the interface remains the
42//! same, the implementation of those requests can change without
43//! notice, possibly causing a [`Hooks`] implementation to see a
44//! different set of calls.
45//!
46//! The `perf_event` crate will not treat such implementation changes
47//! as breaking changes for semver purposes, despite the fact that
48//! they may break code using this module's functionality.
49use libc::pid_t;
50use perf_event_open_sys as real;
51use perf_event_open_sys::bindings;
52use std::cell::RefCell;
53use std::os::raw::{c_char, c_int, c_uint, c_ulong};
54
55std::thread_local! {
56    static HOOKS: RefCell<Box<dyn Hooks + 'static>> = RefCell::new(Box::new(RealHooks));
57}
58
59/// Direct all perf-event system calls on this thread to `hooks`.
60///
61/// All subsequent uses by this crate of the underlying system calls
62/// and ioctls from the `perf_event_open_sys` crate are redirected to
63/// `hooks`' implementations of the correspoding methods from the
64/// [`Hooks`] trait.
65///
66/// This affects only the calling thread. Any previously established
67/// hooks on that thread are dropped.
68///
69/// # Safety
70///
71/// The specified `hooks` trait object intercepts calls provoked by
72/// previously created [`Counter`] and [`Group`] objects, regardless
73/// of which hooks were in effect when they were created. This could
74/// make a hash of things.
75///
76/// [`Counter`]: crate::Counter
77/// [`Group`]: crate::Group
78pub unsafe fn set_thread_hooks(hooks: Box<dyn Hooks + 'static>) {
79    HOOKS.with(|per_thread| {
80        *per_thread.borrow_mut() = hooks;
81    })
82}
83
84/// Direct all perf-event system calls on this thread to the real system calls.
85///
86/// All subsequent uses by this crate of the underlying system calls
87/// and ioctls from the `perf_event_open_sys` crate are directed to
88/// the underlying Linux operations, without interference.
89///
90/// This affects only the calling thread. Any previously established
91/// hooks on that thread are dropped.
92///
93/// # Safety
94///
95/// The specified `hooks` trait object intercepts calls provoked by
96/// previously created [`Counter`] and [`Group`] values, regardless of
97/// which hooks were in effect when they were created. Letting values
98/// created using hooked system calls suddenly see the real kernel
99/// could make a hash of things.
100///
101/// [`Counter`]: crate::Counter
102/// [`Group`]: crate::Group
103pub unsafe fn clear_thread_hooks() {
104    HOOKS.with(|per_thread| {
105        *per_thread.borrow_mut() = Box::new(RealHooks);
106    })
107}
108
109/// List of ioctls we need wrappers for.
110///
111/// We use this macro to generate the [`Hooks`] trait's definition,
112/// the [`RealHooks`] implementation, and the functions in the `sys`
113/// module that are actually used by callers.
114macro_rules! define_ioctls {
115    ( $expand:ident ) => {
116        $expand ! { ENABLE, perf_event_ioctls_ENABLE, c_uint }
117        $expand ! { DISABLE, perf_event_ioctls_DISABLE, c_uint }
118        $expand ! { REFRESH, perf_event_ioctls_REFRESH, c_int }
119        $expand ! { RESET, perf_event_ioctls_RESET, c_uint }
120        $expand ! { PERIOD, perf_event_ioctls_PERIOD, u64 }
121        $expand ! { SET_OUTPUT, perf_event_ioctls_SET_OUTPUT, c_int }
122        $expand ! { SET_FILTER, perf_event_ioctls_SET_FILTER, *mut c_char }
123        $expand ! { ID, perf_event_ioctls_ID, *mut u64 }
124        $expand ! { SET_BPF, perf_event_ioctls_SET_BPF, u32 }
125        $expand ! { PAUSE_OUTPUT, perf_event_ioctls_PAUSE_OUTPUT, u32 }
126        $expand ! { QUERY_BPF, perf_event_ioctls_QUERY_BPF, *mut bindings::perf_event_query_bpf }
127        $expand ! { MODIFY_ATTRIBUTES, perf_event_ioctls_MODIFY_ATTRIBUTES, *mut bindings::perf_event_attr }
128    }
129}
130
131macro_rules! expand_trait_method {
132    ( $name:ident, $ioctl:ident, $arg_type:ty ) => {
133        /// Wrapper for perf_event ioctl
134        #[doc = stringify!($ioctl)]
135        /// .
136        #[allow(non_snake_case)]
137        unsafe fn $name(&mut self, _fd: c_int, _arg: $arg_type) -> c_int {
138            panic!(
139                "unimplemented `perf_event::hooks::Hooks` method: {}",
140                stringify!($name)
141            );
142        }
143    };
144}
145
146/// A trait with a method for every system call and ioctl used by this crate.
147///
148/// The methods of this trait correspond to the public functions of
149/// the [`perf_event_open_sys`][peos] crate used to implement this
150/// crate's functionality. For testing purposes, you can redirect this
151/// crate to a value of your own design that implements this trait by
152/// calling [`set_thread_hooks`].
153///
154/// Each method has a default definition that panics. This means that
155/// you only need to provide definitions for the operations your tests
156/// actually use; if they touch anything else, you'll get a failure.
157///
158/// The [`RealHooks`] type implements this trait in terms of the real
159/// Linux system calls and ioctls.
160///
161/// [peos]: https://docs.rs/perf-event-open-sys/latest/perf_event_open_sys/
162#[allow(dead_code)]
163pub trait Hooks {
164    /// See [`perf_event_open_sys::perf_event_open`][peo].
165    ///
166    /// [peo]: https://docs.rs/perf-event-open-sys/latest/perf_event_open_sys/fn.perf_event_open.html
167    #[allow(clippy::missing_safety_doc)]
168    unsafe fn perf_event_open(
169        &mut self,
170        attrs: *mut bindings::perf_event_attr,
171        pid: pid_t,
172        cpu: c_int,
173        group_fd: c_int,
174        flags: c_ulong,
175    ) -> c_int;
176    define_ioctls!(expand_trait_method);
177}
178
179macro_rules! expand_realhooks_impl {
180    ( $name:ident, $ioctl_:ident, $arg_type:ty ) => {
181        #[allow(clippy::missing_safety_doc)]
182        unsafe fn $name(&mut self, fd: c_int, arg: $arg_type) -> c_int {
183            real::ioctls::$name(fd, arg)
184        }
185    };
186}
187
188/// An implementation of the [`Hooks`] trait in terms of the real Linux system calls.
189///
190/// This type implements each methods of the [`Hooks`] trait by
191/// calling the underlying system call or ioctl. The following call
192/// is equivalent to calling [`clear_thread_hooks`]:
193///
194///     # use perf_event::hooks;
195///     # use perf_event::hooks::*;
196///     unsafe {
197///         set_thread_hooks(Box::new(RealHooks));
198///     }
199///
200/// If what you want is non-intercepted access to the underlying
201/// system calls, it's probably better to just access the
202/// [`perf_event_open_sys`][peos] crate directly, rather than using this type.
203///
204/// [peos]: https://docs.rs/perf-event-open-sys/latest/perf_event_open_sys/
205pub struct RealHooks;
206impl Hooks for RealHooks {
207    unsafe fn perf_event_open(
208        &mut self,
209        attrs: *mut bindings::perf_event_attr,
210        pid: pid_t,
211        cpu: c_int,
212        group_fd: c_int,
213        flags: c_ulong,
214    ) -> c_int {
215        real::perf_event_open(attrs, pid, cpu, group_fd, flags)
216    }
217
218    define_ioctls!(expand_realhooks_impl);
219}
220
221/// Wrapper around the `perf_event_open_sys` crate that supports
222/// intercepting system calls and returning simulated results, for
223/// testing.
224pub mod sys {
225    use super::HOOKS;
226    use libc::pid_t;
227    use std::os::raw::{c_int, c_ulong};
228
229    pub use perf_event_open_sys::bindings;
230
231    /// See [`perf_event_open_sys::perf_event_open`][peo].
232    ///
233    /// [peo]: https://docs.rs/perf-event-open-sys/latest/perf_event_open_sys/fn.perf_event_open.html
234    #[allow(clippy::missing_safety_doc)]
235    pub unsafe fn perf_event_open(
236        attrs: *mut bindings::perf_event_attr,
237        pid: pid_t,
238        cpu: c_int,
239        group_fd: c_int,
240        flags: c_ulong,
241    ) -> c_int {
242        HOOKS.with(|hooks| {
243            hooks
244                .borrow_mut()
245                .perf_event_open(attrs, pid, cpu, group_fd, flags)
246        })
247    }
248
249    #[allow(dead_code, non_snake_case)]
250    /// See the [`perf_event_open_sys::ioctl` module][peosi].
251    ///
252    /// [peosi]: https://docs.rs/perf-event-open-sys/latest/perf_event_open_sys/ioctls/index.html
253    pub mod ioctls {
254        use super::HOOKS;
255        use perf_event_open_sys::bindings;
256        use std::os::raw::{c_char, c_int, c_uint};
257
258        macro_rules! expand_hooked_ioctl {
259            ( $name:ident, $ioctl_:ident, $arg_type:ty ) => {
260                /// See the [`perf_event_open_sys::ioctl` module][peosi].
261                ///
262                /// [peosi]: https://docs.rs/perf-event-open-sys/latest/perf_event_open_sys/ioctls/index.html
263                #[allow(clippy::missing_safety_doc)]
264                pub unsafe fn $name(fd: c_int, arg: $arg_type) -> c_int {
265                    HOOKS.with(|hooks| hooks.borrow_mut().$name(fd, arg))
266                }
267            };
268        }
269
270        define_ioctls!(expand_hooked_ioctl);
271    }
272}