#![forbid(unsafe_code)]
#![deny(missing_docs)]
mod data;
mod frame_data;
mod global_profiler;
mod merge;
mod profile_view;
mod scope_details;
mod thread_profiler;
mod utils;
use std::num::NonZeroU32;
use std::sync::atomic::{AtomicBool, Ordering};
pub use data::{Error, Reader, Result, Scope, ScopeRecord, Stream, StreamInfo, StreamInfoRef};
pub use frame_data::{FrameData, FrameMeta, UnpackedFrameData};
pub use global_profiler::{FrameSink, GlobalProfiler};
pub use merge::{merge_scopes_for_thread, MergeScope};
pub use profile_view::{select_slowest, FrameView, GlobalFrameView};
pub use scope_details::{ScopeCollection, ScopeDetails, ScopeType};
pub use thread_profiler::{ThreadInfo, ThreadProfiler};
pub use utils::{clean_function_name, short_file_name, shorten_rust_function_name, type_name_of};
static MACROS_ON: AtomicBool = AtomicBool::new(false);
pub fn set_scopes_on(on: bool) {
MACROS_ON.store(on, Ordering::Relaxed);
}
pub fn are_scopes_on() -> bool {
MACROS_ON.load(Ordering::Relaxed)
}
pub type NanoSecond = i64;
pub type FrameIndex = u64;
type NsSource = fn() -> NanoSecond;
static SCOPE_ID_TRACKER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(1);
fn fetch_add_scope_id() -> ScopeId {
let new_id = SCOPE_ID_TRACKER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
ScopeId(
NonZeroU32::new(new_id)
.expect("safe because integer is retrieved from fetch-add atomic operation"),
)
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct FrameSinkId(u64);
#[inline]
#[cfg(any(not(target_arch = "wasm32"), feature = "web"))]
pub fn now_ns() -> NanoSecond {
#[cfg(target_arch = "wasm32")]
fn nanos_since_epoch() -> NanoSecond {
(js_sys::Date::new_0().get_time() * 1e6) as _
}
#[cfg(not(target_arch = "wasm32"))]
fn nanos_since_epoch() -> NanoSecond {
if let Ok(duration_since_epoch) = std::time::UNIX_EPOCH.elapsed() {
duration_since_epoch.as_nanos() as NanoSecond
} else {
0 }
}
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
#[cfg(target_arch = "wasm32")]
use web_time::Instant;
static START_TIME: once_cell::sync::Lazy<(NanoSecond, Instant)> =
once_cell::sync::Lazy::new(|| (nanos_since_epoch(), Instant::now()));
START_TIME.0 + START_TIME.1.elapsed().as_nanos() as NanoSecond
}
#[inline]
#[cfg(all(target_arch = "wasm32", not(feature = "web")))]
pub fn now_ns() -> NanoSecond {
panic!("Wasm without the `web` feature requires passing a custom source of time via `ThreadProfiler::initialize`");
}
pub struct ProfilerScope {
start_stream_offset: usize,
_dont_send_me: std::marker::PhantomData<*const ()>,
}
impl ProfilerScope {
#[inline]
pub fn new(scope_id: ScopeId, data: impl AsRef<str>) -> Self {
Self {
start_stream_offset: ThreadProfiler::call(|tp| tp.begin_scope(scope_id, data.as_ref())),
_dont_send_me: Default::default(),
}
}
}
impl Drop for ProfilerScope {
#[inline]
fn drop(&mut self) {
ThreadProfiler::call(|tp| tp.end_scope(self.start_stream_offset));
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct ScopeId(pub NonZeroU32);
impl ScopeId {
#[cfg(test)]
pub(crate) fn new(id: u32) -> Self {
ScopeId(NonZeroU32::new(id).expect("Scope id was not non-zero u32"))
}
}
#[macro_export]
macro_rules! current_function_name {
() => {{
fn f() {}
$crate::type_name_of(f)
}};
}
#[allow(clippy::doc_markdown)] #[macro_export]
macro_rules! profile_function {
() => {
$crate::profile_function!("");
};
($data:expr) => {
let _profiler_scope = if $crate::are_scopes_on() {
static SCOPE_ID: std::sync::OnceLock<$crate::ScopeId> = std::sync::OnceLock::new();
let scope_id = SCOPE_ID.get_or_init(|| {
$crate::ThreadProfiler::call(|tp| {
let id = tp.register_function_scope(
$crate::clean_function_name($crate::current_function_name!()),
$crate::short_file_name(file!()),
line!(),
);
id
})
});
Some($crate::ProfilerScope::new(*scope_id, $data))
} else {
None
};
};
}
#[allow(clippy::doc_markdown)] #[macro_export]
macro_rules! profile_scope {
($name:expr) => {
$crate::profile_scope!($name, "");
};
($name:expr, $data:expr) => {
let _profiler_scope = if $crate::are_scopes_on() {
static SCOPE_ID: std::sync::OnceLock<$crate::ScopeId> = std::sync::OnceLock::new();
let scope_id = SCOPE_ID.get_or_init(|| {
$crate::ThreadProfiler::call(|tp| {
let id = tp.register_named_scope(
$name,
$crate::clean_function_name($crate::current_function_name!()),
$crate::short_file_name(file!()),
line!(),
);
id
})
});
Some($crate::ProfilerScope::new(*scope_id, $data))
} else {
None
};
};
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use crate::{
clean_function_name, set_scopes_on, short_file_name, utils::USELESS_SCOPE_NAME_SUFFIX,
GlobalFrameView, GlobalProfiler, ScopeId,
};
#[test]
fn test_short_file_name() {
for (before, after) in [
("", ""),
("foo.rs", "foo.rs"),
("foo/bar.rs", "foo/bar.rs"),
("foo/bar/baz.rs", "bar/baz.rs"),
("crates/cratename/src/main.rs", "cratename/src/main.rs"),
("crates/cratename/src/module/lib.rs", "cratename/…/module/lib.rs"),
("workspace/cratename/examples/hello_world.rs", "examples/hello_world.rs"),
("/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs", "core/…/function.rs"),
("/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs", "tokio-1.24.1/…/runtime.rs"),
]
{
assert_eq!(short_file_name(before), after);
}
}
#[test]
fn test_clean_function_name() {
assert_eq!(clean_function_name(""), "");
assert_eq!(
clean_function_name(&format!("foo{}", USELESS_SCOPE_NAME_SUFFIX)),
"foo"
);
assert_eq!(
clean_function_name(&format!("foo::bar{}", USELESS_SCOPE_NAME_SUFFIX)),
"foo::bar"
);
assert_eq!(
clean_function_name(&format!("foo::bar::baz{}", USELESS_SCOPE_NAME_SUFFIX)),
"bar::baz"
);
assert_eq!(
clean_function_name(&format!(
"some::GenericThing<_, _>::function_name{}",
USELESS_SCOPE_NAME_SUFFIX
)),
"GenericThing<_, _>::function_name"
);
assert_eq!(
clean_function_name(&format!(
"<some::ConcreteType as some::bloody::Trait>::function_name{}",
USELESS_SCOPE_NAME_SUFFIX
)),
"<ConcreteType as Trait>::function_name"
);
}
#[test]
fn profile_macros_test() {
set_scopes_on(true);
let frame_view = GlobalFrameView::default();
GlobalProfiler::lock().add_sink(Box::new(|data| {
if data.frame_index() == 0 {
assert_eq!(data.frame_index(), 0);
assert_eq!(data.meta().num_scopes, 2);
assert_eq!(data.meta().num_bytes, 62);
} else if data.frame_index() == 1 {
assert_eq!(data.frame_index(), 1);
assert_eq!(data.meta().num_scopes, 2);
assert_eq!(data.meta().num_bytes, 62);
} else {
panic!("Only two frames in this test");
}
}));
let line_nr_fn = line!() + 3;
let line_nr_scope = line!() + 4;
fn a() {
profile_function!();
{
profile_scope!("my-scope");
}
}
a();
GlobalProfiler::lock().new_frame();
let lock = frame_view.lock();
let scope_details = lock
.scope_collection()
.fetch_by_id(&ScopeId::new(1))
.unwrap();
assert_eq!(scope_details.file_path, "puffin/src/lib.rs");
assert_eq!(scope_details.function_name, "profile_macros_test::a");
assert_eq!(scope_details.line_nr, line_nr_fn);
let scope_details = lock
.scope_collection()
.fetch_by_id(&ScopeId::new(2))
.unwrap();
assert_eq!(scope_details.file_path, "puffin/src/lib.rs");
assert_eq!(scope_details.function_name, "profile_macros_test::a");
assert_eq!(scope_details.scope_name, Some(Cow::Borrowed("my-scope")));
assert_eq!(scope_details.line_nr, line_nr_scope);
let scope_details = lock.scope_collection().fetch_by_name("my-scope").unwrap();
assert_eq!(scope_details, &ScopeId::new(2));
drop(lock);
a();
GlobalProfiler::lock().new_frame();
}
}