#![deny(unsafe_code)]
#![warn(
clippy::all,
clippy::await_holding_lock,
clippy::char_lit_as_u8,
clippy::checked_conversions,
clippy::dbg_macro,
clippy::debug_assert_with_mut_call,
clippy::disallowed_methods,
clippy::disallowed_types,
clippy::doc_markdown,
clippy::empty_enum,
clippy::enum_glob_use,
clippy::exit,
clippy::expl_impl_clone_on_copy,
clippy::explicit_deref_methods,
clippy::explicit_into_iter_loop,
clippy::fallible_impl_from,
clippy::filter_map_next,
clippy::flat_map_option,
clippy::float_cmp_const,
clippy::fn_params_excessive_bools,
clippy::from_iter_instead_of_collect,
clippy::if_let_mutex,
clippy::implicit_clone,
clippy::imprecise_flops,
clippy::inefficient_to_string,
clippy::invalid_upcast_comparisons,
clippy::large_digit_groups,
clippy::large_stack_arrays,
clippy::large_types_passed_by_value,
clippy::let_unit_value,
clippy::linkedlist,
clippy::lossy_float_literal,
clippy::macro_use_imports,
clippy::manual_ok_or,
clippy::map_err_ignore,
clippy::map_flatten,
clippy::map_unwrap_or,
clippy::match_on_vec_items,
clippy::match_same_arms,
clippy::match_wild_err_arm,
clippy::match_wildcard_for_single_variants,
clippy::mem_forget,
clippy::mismatched_target_os,
clippy::missing_enforced_import_renames,
clippy::mut_mut,
clippy::mutex_integer,
clippy::needless_borrow,
clippy::needless_continue,
clippy::needless_for_each,
clippy::option_option,
clippy::path_buf_push_overwrite,
clippy::ptr_as_ptr,
clippy::rc_mutex,
clippy::ref_option_ref,
clippy::rest_pat_in_fully_bound_structs,
clippy::same_functions_in_if_condition,
clippy::semicolon_if_nothing_returned,
clippy::single_match_else,
clippy::string_add_assign,
clippy::string_add,
clippy::string_lit_as_bytes,
clippy::string_to_string,
clippy::todo,
clippy::trait_duplication_in_bounds,
clippy::unimplemented,
clippy::unnested_or_patterns,
clippy::unused_self,
clippy::useless_transmute,
clippy::verbose_file_reads,
clippy::zero_sized_map_values,
future_incompatible,
nonstandard_style,
rust_2018_idioms
)]
mod data;
mod frame_data;
mod merge;
mod profile_view;
pub use data::*;
pub use frame_data::{FrameData, FrameMeta, UnpackedFrameData};
pub use merge::*;
pub use profile_view::{select_slowest, FrameView, GlobalFrameView};
use std::collections::BTreeMap;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
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;
#[derive(Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Stream(Vec<u8>);
impl Stream {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn bytes(&self) -> &[u8] {
&self.0
}
pub fn clear(&mut self) {
self.0.clear();
}
fn extend(&mut self, bytes: &[u8]) {
self.0.extend(bytes);
}
}
impl From<Vec<u8>> for Stream {
fn from(v: Vec<u8>) -> Self {
Self(v)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Record<'s> {
pub start_ns: NanoSecond,
pub duration_ns: NanoSecond,
pub id: &'s str,
pub location: &'s str,
pub data: &'s str,
}
impl<'s> Record<'s> {
#[inline]
pub fn stop_ns(&self) -> NanoSecond {
self.start_ns + self.duration_ns
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Scope<'s> {
pub record: Record<'s>,
pub child_begin_position: u64,
pub child_end_position: u64,
pub next_sibling_position: u64,
}
#[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ThreadInfo {
pub start_time_ns: Option<NanoSecond>,
pub name: String,
}
pub type FrameIndex = u64;
#[derive(Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct StreamInfo {
pub stream: Stream,
pub num_scopes: usize,
pub depth: usize,
pub range_ns: (NanoSecond, NanoSecond),
}
impl Default for StreamInfo {
fn default() -> Self {
Self {
stream: Default::default(),
num_scopes: 0,
depth: 0,
range_ns: (NanoSecond::MAX, NanoSecond::MIN),
}
}
}
impl StreamInfo {
pub fn parse(stream: Stream) -> Result<StreamInfo> {
let top_scopes = Reader::from_start(&stream).read_top_scopes()?;
if top_scopes.is_empty() {
Ok(StreamInfo {
stream,
num_scopes: 0,
depth: 0,
range_ns: (NanoSecond::MAX, NanoSecond::MIN),
})
} else {
let (num_scopes, depth) = Reader::count_scope_and_depth(&stream)?;
let min_ns = top_scopes.first().unwrap().record.start_ns;
let max_ns = top_scopes.last().unwrap().record.stop_ns();
Ok(StreamInfo {
stream,
num_scopes,
depth,
range_ns: (min_ns, max_ns),
})
}
}
pub fn extend(&mut self, other: &StreamInfoRef<'_>) {
self.stream.extend(other.stream);
self.num_scopes += other.num_scopes;
self.depth = self.depth.max(other.depth);
self.range_ns.0 = self.range_ns.0.min(other.range_ns.0);
self.range_ns.1 = self.range_ns.1.max(other.range_ns.1);
}
pub fn clear(&mut self) {
let Self {
stream,
num_scopes,
depth,
range_ns,
} = self;
stream.clear();
*num_scopes = 0;
*depth = 0;
*range_ns = (NanoSecond::MAX, NanoSecond::MIN);
}
pub fn as_stream_into_ref(&self) -> StreamInfoRef<'_> {
StreamInfoRef {
stream: self.stream.bytes(),
num_scopes: self.num_scopes,
depth: self.depth,
range_ns: self.range_ns,
}
}
}
#[derive(Clone, Copy)]
pub struct StreamInfoRef<'a> {
pub stream: &'a [u8],
pub num_scopes: usize,
pub depth: usize,
pub range_ns: (NanoSecond, NanoSecond),
}
type NsSource = fn() -> NanoSecond;
type ThreadReporter = fn(ThreadInfo, &StreamInfoRef<'_>);
pub fn global_reporter(info: ThreadInfo, stream_info: &StreamInfoRef<'_>) {
GlobalProfiler::lock().report(info, stream_info);
}
pub struct ThreadProfiler {
stream_info: StreamInfo,
depth: usize,
now_ns: NsSource,
reporter: ThreadReporter,
start_time_ns: Option<NanoSecond>,
}
impl Default for ThreadProfiler {
fn default() -> Self {
Self {
stream_info: Default::default(),
depth: 0,
now_ns: crate::now_ns,
reporter: global_reporter,
start_time_ns: None,
}
}
}
impl ThreadProfiler {
pub fn initialize(now_ns: NsSource, reporter: ThreadReporter) {
ThreadProfiler::call(|tp| {
tp.now_ns = now_ns;
tp.reporter = reporter;
});
}
#[must_use]
pub fn begin_scope(&mut self, id: &str, location: &str, data: &str) -> usize {
let now_ns = (self.now_ns)();
self.start_time_ns = Some(self.start_time_ns.unwrap_or(now_ns));
self.depth += 1;
self.stream_info.range_ns.0 = self.stream_info.range_ns.0.min(now_ns);
self.stream_info
.stream
.begin_scope(now_ns, id, location, data)
}
pub fn end_scope(&mut self, start_offset: usize) {
let now_ns = (self.now_ns)();
self.stream_info.depth = self.stream_info.depth.max(self.depth);
self.stream_info.num_scopes += 1;
self.stream_info.range_ns.1 = self.stream_info.range_ns.1.max(now_ns);
if self.depth > 0 {
self.depth -= 1;
} else {
eprintln!("puffin ERROR: Mismatched scope begin/end calls");
}
self.stream_info.stream.end_scope(start_offset, now_ns);
if self.depth == 0 {
let info = ThreadInfo {
start_time_ns: self.start_time_ns,
name: std::thread::current().name().unwrap_or_default().to_owned(),
};
(self.reporter)(info, &self.stream_info.as_stream_into_ref());
self.stream_info.clear();
}
}
#[inline]
pub fn call<R>(f: impl Fn(&mut Self) -> R) -> R {
thread_local! {
pub static THREAD_PROFILER: std::cell::RefCell<ThreadProfiler> = Default::default();
}
THREAD_PROFILER.with(|p| f(&mut p.borrow_mut()))
}
}
pub type FrameSink = Box<dyn Fn(Arc<FrameData>) + Send>;
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct FrameSinkId(u64);
pub struct GlobalProfiler {
current_frame_index: FrameIndex,
current_frame: BTreeMap<ThreadInfo, StreamInfo>,
next_sink_id: FrameSinkId,
sinks: std::collections::HashMap<FrameSinkId, FrameSink>,
}
impl Default for GlobalProfiler {
fn default() -> Self {
Self {
current_frame_index: 0,
current_frame: Default::default(),
next_sink_id: FrameSinkId(1),
sinks: Default::default(),
}
}
}
impl GlobalProfiler {
#[cfg(feature = "parking_lot")]
pub fn lock() -> parking_lot::MutexGuard<'static, Self> {
use once_cell::sync::Lazy;
static GLOBAL_PROFILER: Lazy<parking_lot::Mutex<GlobalProfiler>> =
Lazy::new(Default::default);
GLOBAL_PROFILER.lock()
}
#[cfg(not(feature = "parking_lot"))]
pub fn lock() -> std::sync::MutexGuard<'static, Self> {
use once_cell::sync::Lazy;
static GLOBAL_PROFILER: Lazy<std::sync::Mutex<GlobalProfiler>> =
Lazy::new(Default::default);
GLOBAL_PROFILER.lock().expect("poisoned mutex")
}
pub fn new_frame(&mut self) {
let current_frame_index = self.current_frame_index;
self.current_frame_index += 1;
let new_frame =
match FrameData::new(current_frame_index, std::mem::take(&mut self.current_frame)) {
Ok(new_frame) => Arc::new(new_frame),
Err(Error::Empty) => {
return; }
Err(err) => {
eprintln!("puffin ERROR: Bad frame: {err:?}");
return;
}
};
self.add_frame(new_frame);
}
pub fn add_frame(&mut self, new_frame: Arc<FrameData>) {
for sink in self.sinks.values() {
sink(new_frame.clone());
}
}
pub fn report(&mut self, info: ThreadInfo, stream_info: &StreamInfoRef<'_>) {
self.current_frame
.entry(info)
.or_default()
.extend(stream_info);
}
pub fn add_sink(&mut self, sink: FrameSink) -> FrameSinkId {
let id = self.next_sink_id;
self.next_sink_id.0 += 1;
self.sinks.insert(id, sink);
id
}
pub fn remove_sink(&mut self, id: FrameSinkId) -> Option<FrameSink> {
self.sinks.remove(&id)
}
}
#[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 }
}
use instant::Instant;
use once_cell::sync::Lazy;
static START_TIME: Lazy<(NanoSecond, Instant)> =
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(id: &'static str, location: &str, data: impl AsRef<str>) -> Self {
Self {
start_stream_offset: ThreadProfiler::call(|tp| {
tp.begin_scope(id, location, 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));
}
}
#[doc(hidden)]
#[inline(always)]
pub fn type_name_of<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}
#[macro_export]
macro_rules! current_function_name {
() => {{
fn f() {}
let name = $crate::type_name_of(f);
let name = &name.get(..name.len() - 3).unwrap();
$crate::clean_function_name(name)
}};
}
#[doc(hidden)]
#[inline]
pub fn clean_function_name(name: &str) -> &str {
if let Some(colon) = name.rfind("::") {
if let Some(colon) = name[..colon].rfind("::") {
&name[colon + 2..]
} else {
name
}
} else {
name
}
}
#[test]
fn test_clean_function_name() {
assert_eq!(clean_function_name(""), "");
assert_eq!(clean_function_name("foo"), "foo");
assert_eq!(clean_function_name("foo::bar"), "foo::bar");
assert_eq!(clean_function_name("foo::bar::baz"), "bar::baz");
}
#[macro_export]
macro_rules! current_file_name {
() => {
$crate::short_file_name(file!())
};
}
#[doc(hidden)]
#[inline]
pub fn short_file_name(name: &str) -> &str {
if let Some(separator) = name.rfind(&['/', '\\'][..]) {
&name[separator + 1..]
} else {
name
}
}
#[test]
fn test_short_file_name() {
assert_eq!(short_file_name(""), "");
assert_eq!(short_file_name("foo.rs"), "foo.rs");
assert_eq!(short_file_name("foo/bar.rs"), "bar.rs");
assert_eq!(short_file_name("foo/bar/baz.rs"), "baz.rs");
assert_eq!(short_file_name(r"C:\\windows\is\weird\src.rs"), "src.rs");
}
#[allow(clippy::doc_markdown)] #[macro_export]
macro_rules! profile_function {
() => {
$crate::profile_function!("");
};
($data:expr) => {
let _profiler_scope = if $crate::are_scopes_on() {
Some($crate::ProfilerScope::new(
$crate::current_function_name!(),
$crate::current_file_name!(),
$data,
))
} else {
None
};
};
}
#[allow(clippy::doc_markdown)] #[macro_export]
macro_rules! profile_scope {
($id:expr) => {
$crate::profile_scope!($id, "");
};
($id:expr, $data:expr) => {
let _profiler_scope = if $crate::are_scopes_on() {
Some($crate::ProfilerScope::new(
$id,
$crate::current_file_name!(),
$data,
))
} else {
None
};
};
}