diff --git a/src/backends/mod.rs b/src/backends/mod.rs index a0a1df6d..fca51677 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -9,10 +9,13 @@ use crate::Result; use std::fmt::Debug; /// Backend State -#[derive(Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum State { + /// Backend is uninitialized. Uninitialized, + /// Backend is ready to be used. Ready, + /// Backend is running. Running, } @@ -24,10 +27,15 @@ impl Default for State { /// Backend Trait pub trait Backend: Send + Debug { + /// Get the backend state. fn get_state(&self) -> State; + /// Initialize the backend. fn initialize(&mut self, sample_rate: i32) -> Result<()>; + /// Start the backend. fn start(&mut self) -> Result<()>; + /// Stop the backend. fn stop(&mut self) -> Result<()>; + /// Generate profiling report fn report(&mut self) -> Result>; } diff --git a/src/error.rs b/src/error.rs index 474f29d6..5ee48322 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,7 +11,7 @@ use thiserror::Error; pub type Result = std::result::Result; /// Error type of Pyroscope -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub struct PyroscopeError { pub msg: String, } diff --git a/src/pyroscope.rs b/src/pyroscope.rs index 06b6bb9d..95af6c54 100644 --- a/src/pyroscope.rs +++ b/src/pyroscope.rs @@ -19,12 +19,19 @@ use crate::session::SessionManager; use crate::session::SessionSignal; use crate::timer::Timer; -/// Represent PyroscopeAgent Configuration +/// Pyroscope Agent Configuration. This is the configuration that is passed to the agent. +/// # Example +/// ``` +/// use pyroscope::pyroscope::PyroscopeConfig; +/// let config = PyroscopeConfig::new("http://localhost:8080", "my-app"); +/// ``` #[derive(Clone, Debug)] pub struct PyroscopeConfig { + /// Pyroscope Server Address pub url: String, /// Application Name pub application_name: String, + /// Tags pub tags: HashMap, /// Sample rate used in Hz pub sample_rate: i32, @@ -37,7 +44,11 @@ pub struct PyroscopeConfig { impl PyroscopeConfig { /// Create a new PyroscopeConfig object. url and application_name are required. - /// tags and sample_rate are optional. + /// tags and sample_rate are optional. If sample_rate is not specified, it will default to 100. + /// # Example + /// ```ignore + /// let config = PyroscopeConfig::new("http://localhost:8080", "my-app"); + /// ``` pub fn new>(url: S, application_name: S) -> Self { Self { url: url.as_ref().to_owned(), @@ -48,6 +59,12 @@ impl PyroscopeConfig { } /// Set the Sample rate + /// # Example + /// ```ignore + /// let mut config = PyroscopeConfig::new("http://localhost:8080", "my-app"); + /// config.set_sample_rate(10) + /// .unwrap(); + /// ``` pub fn sample_rate(self, sample_rate: i32) -> Self { Self { sample_rate, @@ -55,7 +72,14 @@ impl PyroscopeConfig { } } - /// Set Tags + /// Set the tags + /// # Example + /// ```ignore + /// use pyroscope::pyroscope::PyroscopeConfig; + /// let config = PyroscopeConfig::new("http://localhost:8080", "my-app") + /// .tags(vec![("env", "dev")]) + /// .unwrap(); + /// ``` pub fn tags(self, tags: &[(&str, &str)]) -> Self { // Convert &[(&str, &str)] to HashMap(String, String) let tags_hashmap: HashMap = tags @@ -76,6 +100,13 @@ impl PyroscopeConfig { /// /// Alternatively, you can use PyroscopeAgent::build() which is a short-hand /// for calling PyroscopeAgentBuilder::new() +/// +/// # Example +/// ```ignore +/// use pyroscope::pyroscope::PyroscopeAgentBuilder; +/// let builder = PyroscopeAgentBuilder::new("http://localhost:8080", "my-app"); +/// let agent = builder.build().unwrap(); +/// ``` pub struct PyroscopeAgentBuilder { /// Profiler backend backend: Arc>, @@ -84,8 +115,13 @@ pub struct PyroscopeAgentBuilder { } impl PyroscopeAgentBuilder { - /// Create a new PyroscopeConfig object. url and application_name are required. + /// Create a new PyroscopeAgentBuilder object. url and application_name are required. /// tags and sample_rate are optional. + /// + /// # Example + /// ```ignore + /// let builder = PyroscopeAgentBuilder::new("http://localhost:8080", "my-app"); + /// ``` pub fn new>(url: S, application_name: S) -> Self { Self { backend: Arc::new(Mutex::new(Pprof::default())), // Default Backend @@ -94,6 +130,13 @@ impl PyroscopeAgentBuilder { } /// Set the agent backend. Default is pprof. + /// # Example + /// ```ignore + /// let builder = PyroscopeAgentBuilder::new("http://localhost:8080", "my-app") + /// .backend(Pprof::default()) + /// .build() + /// .unwrap(); + /// ``` pub fn backend(self, backend: T) -> Self where T: Backend, @@ -105,6 +148,13 @@ impl PyroscopeAgentBuilder { } /// Set the Sample rate. Default value is 100. + /// # Example + /// ```ignore + /// let builder = PyroscopeAgentBuilder::new("http://localhost:8080", "my-app") + /// .sample_rate(99) + /// .build() + /// .unwrap(); + /// ``` pub fn sample_rate(self, sample_rate: i32) -> Self { Self { config: self.config.sample_rate(sample_rate), @@ -113,6 +163,13 @@ impl PyroscopeAgentBuilder { } /// Set tags. Default is empty. + /// # Example + /// ```ignore + /// let builder = PyroscopeAgentBuilder::new("http://localhost:8080", "my-app") + /// .tags(vec![("env", "dev")]) + /// .build() + /// .unwrap(); + /// ``` pub fn tags(self, tags: &[(&str, &str)]) -> Self { Self { config: self.config.tags(tags), @@ -148,17 +205,18 @@ impl PyroscopeAgentBuilder { } } -/// PyroscopeAgent +/// PyroscopeAgent is the main object of the library. It is used to start and stop the profiler, schedule the timer, and send the profiler data to the server. #[derive(Debug)] pub struct PyroscopeAgent { - pub backend: Arc>, timer: Timer, session_manager: SessionManager, tx: Option>, handle: Option>>, running: Arc<(Mutex, Condvar)>, - // Session Data + /// Profiler backend + pub backend: Arc>, + /// Configuration Object pub config: PyroscopeConfig, } @@ -192,13 +250,22 @@ impl Drop for PyroscopeAgent { } impl PyroscopeAgent { - /// Short-hand for PyroscopeAgentBuilder::build() + /// Short-hand for PyroscopeAgentBuilder::build(). This is a convenience method. + /// # Example + /// ```ignore + /// let agent = PyroscopeAgent::builder("http://localhost:8080", "my-app").build().unwrap(); + /// ``` pub fn builder>(url: S, application_name: S) -> PyroscopeAgentBuilder { // Build PyroscopeAgent PyroscopeAgentBuilder::new(url, application_name) } - /// Start profiling and sending data. The agent will keep running until stopped. + /// Start profiling and sending data. The agent will keep running until stopped. The agent will send data to the server every 10s secondy. + /// # Example + /// ```ignore + /// let agent = PyroscopeAgent::builder("http://localhost:8080", "my-app").build().unwrap(); + /// agent.start().unwrap(); + /// ``` pub fn start(&mut self) -> Result<()> { log::debug!("PyroscopeAgent - Starting"); @@ -258,7 +325,14 @@ impl PyroscopeAgent { Ok(()) } - /// Stop the agent. + /// Stop the agent. The agent will stop profiling and send a last report to the server. + /// # Example + /// ```ignore + /// let agent = PyroscopeAgent::builder("http://localhost:8080", "my-app").build().unwrap(); + /// agent.start().unwrap(); + /// // Expensive operation + /// agent.stop().unwrap(); + /// ``` pub fn stop(&mut self) -> Result<()> { log::debug!("PyroscopeAgent - Stopping"); // get tx and send termination signal @@ -278,8 +352,22 @@ impl PyroscopeAgent { } /// Add tags. This will restart the agent. + /// # Example + /// ```ignore + /// let agent = PyroscopeAgent::builder("http://localhost:8080", "my-app").build().unwrap(); + /// agent.start().unwrap(); + /// // Expensive operation + /// agent.add_tags(vec!["tag", "value"]).unwrap(); + /// // Tagged operation + /// agent.stop().unwrap(); + /// ``` pub fn add_tags(&mut self, tags: &[(&str, &str)]) -> Result<()> { log::debug!("PyroscopeAgent - Adding tags"); + // Check that tags are not empty + if tags.is_empty() { + return Ok(()); + } + // Stop Agent self.stop()?; @@ -300,8 +388,24 @@ impl PyroscopeAgent { } /// Remove tags. This will restart the agent. + /// # Example + /// ```ignore + /// let agent = PyroscopeAgent::builder("http://localhost:8080", "my-app") + /// .tags(vec![("tag", "value")]) + /// .build().unwrap(); + /// agent.start().unwrap(); + /// // Expensive operation + /// agent.remove_tags(vec!["tag"]).unwrap(); + /// // Un-Tagged operation + /// agent.stop().unwrap(); pub fn remove_tags(&mut self, tags: &[&str]) -> Result<()> { log::debug!("PyroscopeAgent - Removing tags"); + + // Check that tags are not empty + if tags.is_empty() { + return Ok(()); + } + // Stop Agent self.stop()?; diff --git a/src/session.rs b/src/session.rs index a4f8caa2..21b7280f 100644 --- a/src/session.rs +++ b/src/session.rs @@ -15,16 +15,22 @@ use crate::utils::merge_tags_with_app_name; use crate::Result; /// Session Signal +/// +/// This enum is used to send data to the session thread. It can also kill the session thread. #[derive(Debug)] pub enum SessionSignal { + /// Send session data to the session thread. Session(Session), + /// Kill the session thread. Kill, } -/// SessionManager +/// Manage sessions and send data to the server. #[derive(Debug)] pub struct SessionManager { + /// The SessionManager thread. pub handle: Option>>, + /// Channel to send data to the SessionManager thread. pub tx: SyncSender, } @@ -71,6 +77,8 @@ impl SessionManager { } /// Pyroscope Session +/// +/// Used to contain the session data, and send it to the server. #[derive(Clone, Debug)] pub struct Session { pub config: PyroscopeConfig, @@ -80,6 +88,14 @@ pub struct Session { } impl Session { + /// Create a new Session + /// # Example + /// ```ignore + /// let config = PyroscopeConfig::new("https://localhost:8080", "my-app"); + /// let report = vec![1, 2, 3]; + /// let until = 154065120; + /// let session = Session::new(until, config, report).unwrap(); + /// ``` pub fn new(mut until: u64, config: PyroscopeConfig, report: Vec) -> Result { log::info!("Session - Creating Session"); // Session interrupted (0 signal), determine the time @@ -103,6 +119,15 @@ impl Session { }) } + /// Send the session to the server and consumes the session object. + /// # Example + /// ```ignore + /// let config = PyroscopeConfig::new("https://localhost:8080", "my-app"); + /// let report = vec![1, 2, 3]; + /// let until = 154065120; + /// let session = Session::new(until, config, report).unwrap(); + /// session.send().unwrap(); + /// ``` pub fn send(self) -> Result<()> { log::info!("Session - Sending Session"); diff --git a/src/timer/epoll.rs b/src/timer/epoll.rs index 0c931c9d..666c9e8c 100644 --- a/src/timer/epoll.rs +++ b/src/timer/epoll.rs @@ -13,6 +13,14 @@ use std::sync::{ }; use std::{thread, thread::JoinHandle}; +/// A thread that sends a notification every 10th second +/// +/// Timer will send an event to attached listeners (mpsc::Sender) every 10th +/// second (...10, ...20, ...) +/// +/// The Timer thread will run continously until all Senders are dropped. +/// The Timer thread will be joined when all Senders are dropped. + #[derive(Debug, Default)] pub struct Timer { /// A vector to store listeners (mpsc::Sender) @@ -23,6 +31,7 @@ pub struct Timer { } impl Timer { + /// Initialize Timer and run a thread to send events to attached listeners pub fn initialize(self) -> Result { let txs = Arc::clone(&self.txs); @@ -136,6 +145,7 @@ impl Timer { Ok(epoll_fd) } + /// Wait for an event on the epoll file descriptor fn epoll_wait(timer_fd: libc::c_int, epoll_fd: libc::c_int) -> Result<()> { // vector to store events let mut events = Vec::with_capacity(1); diff --git a/src/timer/kqueue.rs b/src/timer/kqueue.rs index 410e0793..ed0b5652 100644 --- a/src/timer/kqueue.rs +++ b/src/timer/kqueue.rs @@ -13,6 +13,14 @@ use std::sync::{ }; use std::{thread, thread::JoinHandle}; +/// A thread that sends a notification every 10th second +/// +/// Timer will send an event to attached listeners (mpsc::Sender) every 10th +/// second (...10, ...20, ...) +/// +/// The Timer thread will run continously until all Senders are dropped. +/// The Timer thread will be joined when all Senders are dropped. + #[derive(Debug, Default)] pub struct Timer { /// A vector to store listeners (mpsc::Sender) @@ -23,6 +31,7 @@ pub struct Timer { } impl Timer { + /// Initialize Timer and run a thread to send events to attached listeners pub fn initialize(self) -> Result { let txs = Arc::clone(&self.txs); @@ -90,10 +99,13 @@ impl Timer { Ok(()) } + /// Wait for the timer event fn wait_event(kqueue: i32, events: *mut libc::kevent) -> Result<()> { kevent(kqueue, [].as_mut_ptr(), 0, events, 1, std::ptr::null())?; Ok(()) } + + /// Register an initial expiration event fn register_initial_expiration(kqueue: i32) -> Result { // Get the next event time let now = std::time::SystemTime::now() @@ -123,6 +135,8 @@ impl Timer { Ok(initial_event) } + + /// Register a loop expiration event fn register_loop_expiration(kqueue: i32) -> Result { let loop_event = libc::kevent { ident: 1, diff --git a/src/timer/sleep.rs b/src/timer/sleep.rs index 86227e9b..5ca99f62 100644 --- a/src/timer/sleep.rs +++ b/src/timer/sleep.rs @@ -6,7 +6,10 @@ use crate::Result; -use std::sync::{mpsc::Sender, Arc, Mutex}; +use std::sync::{ + mpsc::{channel, Receiver, Sender}, + Arc, Mutex, +}; use std::time::Duration; use std::{thread, thread::JoinHandle}; @@ -16,6 +19,7 @@ use std::{thread, thread::JoinHandle}; /// second (...10, ...20, ...) /// /// The Timer thread will run continously until all Senders are dropped. +/// The Timer thread will be joined when all Senders are dropped. #[derive(Debug, Default)] pub struct Timer { diff --git a/src/utils.rs b/src/utils.rs index 5a182f4b..5ac38132 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -30,13 +30,12 @@ pub fn merge_tags_with_app_name( } #[cfg(test)] -mod tests { +mod merge_tags_with_app_name_tests { use std::collections::HashMap; use crate::utils::merge_tags_with_app_name; #[test] - fn merge_tags_with_app_name_with_tags() { let mut tags = HashMap::new(); tags.insert("env".to_string(), "staging".to_string()); @@ -63,3 +62,18 @@ pub fn check_err(num: T) -> Result { } Ok(num) } + +#[cfg(test)] +mod check_err_tests { + use crate::utils::check_err; + + #[test] + fn check_err_success() { + assert_eq!(check_err(1).unwrap(), 1) + } + + #[test] + fn check_err_error() { + assert!(check_err(-1).is_err()) + } +} diff --git a/tests/agent.rs b/tests/agent.rs new file mode 100644 index 00000000..8e87e19e --- /dev/null +++ b/tests/agent.rs @@ -0,0 +1,38 @@ +use pyroscope::pyroscope::PyroscopeConfig; + +#[test] +fn test_PyroscopeConfig_new() { + let config = PyroscopeConfig::new("http://localhost:8080", "myapp"); + assert_eq!(config.url, "http://localhost:8080"); + assert_eq!(config.application_name, "myapp"); + assert_eq!(config.sample_rate, 100i32); + assert_eq!(config.tags.len(), 0); +} + +#[test] +fn test_PyroscopeConfig_sample_rate() { + let config = PyroscopeConfig::new("http://localhost:8080", "myapp").sample_rate(10); + assert_eq!(config.sample_rate, 10i32); +} + +#[test] +fn test_PyroscopeConfig_tags_empty() { + let config = PyroscopeConfig::new("http://localhost:8080", "myapp"); + assert_eq!(config.tags.len(), 0); +} + +#[test] +fn test_PyroscopeConfig_tags() { + let config = PyroscopeConfig::new("http://localhost:8080", "myapp").tags(&[("tag", "value")]); + assert_eq!(config.tags.len(), 1); + assert_eq!(config.tags.get("tag"), Some(&"value".to_owned())); +} + +#[test] +fn test_PyroscopeConfig_tags_multiple() { + let config = PyroscopeConfig::new("http://localhost:8080", "myapp") + .tags(&[("tag1", "value1"), ("tag2", "value2")]); + assert_eq!(config.tags.len(), 2); + assert_eq!(config.tags.get("tag1"), Some(&"value1".to_owned())); + assert_eq!(config.tags.get("tag2"), Some(&"value2".to_owned())); +} diff --git a/tests/backends.rs b/tests/backends.rs new file mode 100644 index 00000000..5e5f33e8 --- /dev/null +++ b/tests/backends.rs @@ -0,0 +1,6 @@ +use pyroscope::backends::{State}; + +#[test] +fn test_state_default() { + assert_eq!(State::default(), State::Uninitialized); +} diff --git a/tests/session.rs b/tests/session.rs new file mode 100644 index 00000000..6a44dd69 --- /dev/null +++ b/tests/session.rs @@ -0,0 +1,57 @@ +use pyroscope::{ + pyroscope::PyroscopeConfig, + session::{Session, SessionManager, SessionSignal}, + PyroscopeError, +}; +use std::{ + collections::HashMap, + sync::mpsc::{sync_channel, Receiver, SyncSender}, + thread, + thread::JoinHandle, +}; + +#[test] +fn test_session_manager_new() { + let session_manager = SessionManager::new().unwrap(); + assert!(session_manager.handle.is_some()); +} + +#[test] +fn test_session_manager_push_kill() { + let session_manager = SessionManager::new().unwrap(); + session_manager.push(SessionSignal::Kill).unwrap(); + assert_eq!(session_manager.handle.unwrap().join().unwrap().unwrap(), ()); +} + +#[test] +fn test_session_new() { + let config = PyroscopeConfig { + url: "http://localhost:8080".to_string(), + application_name: "test".to_string(), + tags: HashMap::new(), + sample_rate: 100, + }; + + let report = vec![1, 2, 3]; + + let session = Session::new(1950, config, report).unwrap(); + + assert_eq!(session.from, 1940); + assert_eq!(session.until, 1950); +} + +#[test] +fn test_session_send_error() { + let config = PyroscopeConfig { + url: "http://invalid_url".to_string(), + application_name: "test".to_string(), + tags: HashMap::new(), + sample_rate: 100, + }; + + let report = vec![1, 2, 3]; + + let session = Session::new(1950, config, report).unwrap(); + + // TODO: to figure this out +} diff --git a/tests/timer-epoll.rs b/tests/timer-epoll.rs new file mode 100644 index 00000000..fa65d0a3 --- /dev/null +++ b/tests/timer-epoll.rs @@ -0,0 +1,85 @@ +#[cfg(target_os = "linux")] +mod tests { + use pyroscope::timer::epoll::{ + epoll_create1, epoll_ctl, epoll_wait, timerfd_create, timerfd_settime, + }; + + #[test] + fn test_timerfd_create() { + let timer_fd = timerfd_create(libc::CLOCK_REALTIME, libc::TFD_NONBLOCK).unwrap(); + assert!(timer_fd > 0); + } + + #[test] + fn test_timerfd_settime() { + let mut new_value = libc::itimerspec { + it_interval: libc::timespec { + tv_sec: 10, + tv_nsec: 0, + }, + it_value: libc::timespec { + tv_sec: 0, + tv_nsec: 0, + }, + }; + + let mut old_value = libc::itimerspec { + it_interval: libc::timespec { + tv_sec: 0, + tv_nsec: 0, + }, + it_value: libc::timespec { + tv_sec: 0, + tv_nsec: 0, + }, + }; + + let timer_fd = timerfd_create(libc::CLOCK_REALTIME, libc::TFD_NONBLOCK).unwrap(); + let void = timerfd_settime( + timer_fd, + libc::TFD_TIMER_ABSTIME, + &mut new_value, + &mut old_value, + ) + .unwrap(); + assert!(void == ()); + } + + #[test] + fn test_epoll_create1() { + let epoll_fd = epoll_create1(0).unwrap(); + assert!(epoll_fd > 0); + } + + #[test] + fn test_epoll_ctl() { + let mut event = libc::epoll_event { + events: libc::EPOLLIN as u32, + u64: 1, + }; + + let epoll_fd = epoll_create1(0).unwrap(); + let timer_fd = timerfd_create(libc::CLOCK_REALTIME, libc::TFD_NONBLOCK).unwrap(); + let void = epoll_ctl(epoll_fd, libc::EPOLL_CTL_ADD, timer_fd, &mut event).unwrap(); + assert!(void == ()); + } + + #[test] + fn test_epoll_wait() { + let mut event = libc::epoll_event { + events: libc::EPOLLIN as u32, + u64: 1, + }; + + let epoll_fd = epoll_create1(0).unwrap(); + let timer_fd = timerfd_create(libc::CLOCK_REALTIME, libc::TFD_NONBLOCK).unwrap(); + epoll_ctl(epoll_fd, libc::EPOLL_CTL_ADD, timer_fd, &mut event).unwrap(); + + let mut events = vec![libc::epoll_event { events: 0, u64: 0 }]; + + // Expire in 1ms + let void = epoll_wait(epoll_fd, events.as_mut_ptr(), 1, 1).unwrap(); + + assert!(void == ()); + } +} diff --git a/tests/timer.rs b/tests/timer.rs new file mode 100644 index 00000000..0923f48d --- /dev/null +++ b/tests/timer.rs @@ -0,0 +1,26 @@ +use pyroscope::timer::Timer; + +#[test] +fn test_timer() { + // Initialize Timer + let mut timer = Timer::default().initialize().unwrap(); + + // Attach a listener + let (tx, rx) = std::sync::mpsc::channel(); + timer.attach_listener(tx).unwrap(); + + // Wait for event (should arrive in 10s) + let recv: u64 = rx.recv().unwrap(); + + // Get current time + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + // Check that recv and now are within 10s of each other + assert!(recv - now < 10); + + // Check that recv is divisible by 10 + assert!(recv % 10 == 0); +}