use std::collections::HashMap;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EventKind {
FunctionEnter,
FunctionExit,
Allocation {
size_bytes: usize,
},
GcPause {
duration_ns: u64,
},
TailCall,
ThunkForced,
ClosureCreated,
ClosureApplied,
RuntimeError,
}
impl EventKind {
pub fn variant_name(&self) -> &'static str {
match self {
EventKind::FunctionEnter => "FunctionEnter",
EventKind::FunctionExit => "FunctionExit",
EventKind::Allocation { .. } => "Allocation",
EventKind::GcPause { .. } => "GcPause",
EventKind::TailCall => "TailCall",
EventKind::ThunkForced => "ThunkForced",
EventKind::ClosureCreated => "ClosureCreated",
EventKind::ClosureApplied => "ClosureApplied",
EventKind::RuntimeError => "RuntimeError",
}
}
}
impl std::fmt::Display for EventKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.variant_name())
}
}
#[derive(Clone, Debug)]
pub struct ProfilerEvent {
pub timestamp_ns: u64,
pub kind: EventKind,
pub name: String,
pub data: Option<u64>,
}
impl ProfilerEvent {
pub fn new(kind: EventKind, name: impl Into<String>, data: Option<u64>) -> Self {
Self {
timestamp_ns: wall_clock_ns(),
kind,
name: name.into(),
data,
}
}
pub fn with_timestamp(
timestamp_ns: u64,
kind: EventKind,
name: impl Into<String>,
data: Option<u64>,
) -> Self {
Self {
timestamp_ns,
kind,
name: name.into(),
data,
}
}
}
#[derive(Clone, Debug)]
pub struct CallRecord {
pub name: String,
pub calls: u64,
pub total_ns: u64,
pub self_ns: u64,
pub max_ns: u64,
pub min_ns: u64,
}
impl CallRecord {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
calls: 0,
total_ns: 0,
self_ns: 0,
max_ns: 0,
min_ns: u64::MAX,
}
}
pub fn mean_ns(&self) -> f64 {
if self.calls == 0 {
0.0
} else {
self.total_ns as f64 / self.calls as f64
}
}
}
#[derive(Clone, Debug)]
pub struct AllocationRecord {
pub type_name: String,
pub count: u64,
pub total_bytes: u64,
}
impl AllocationRecord {
pub fn new(type_name: impl Into<String>) -> Self {
Self {
type_name: type_name.into(),
count: 0,
total_bytes: 0,
}
}
pub fn avg_bytes(&self) -> f64 {
if self.count == 0 {
0.0
} else {
self.total_bytes as f64 / self.count as f64
}
}
}
#[derive(Clone, Debug)]
pub struct ProfileReport2 {
pub call_records: Vec<CallRecord>,
pub alloc_records: Vec<AllocationRecord>,
pub total_events: u64,
pub wall_time_ns: u64,
pub gc_pauses: Vec<u64>,
}
impl ProfileReport2 {
pub fn empty() -> Self {
Self {
call_records: Vec::new(),
alloc_records: Vec::new(),
total_events: 0,
wall_time_ns: 0,
gc_pauses: Vec::new(),
}
}
pub fn total_gc_pause_ns(&self) -> u64 {
self.gc_pauses.iter().sum()
}
pub fn gc_pause_count(&self) -> usize {
self.gc_pauses.len()
}
pub fn mean_gc_pause_ns(&self) -> f64 {
if self.gc_pauses.is_empty() {
0.0
} else {
self.total_gc_pause_ns() as f64 / self.gc_pauses.len() as f64
}
}
}
#[derive(Clone, Debug)]
pub struct ProfilerConfig2 {
pub max_events: usize,
pub sample_rate: u32,
pub track_allocations: bool,
}
impl ProfilerConfig2 {
pub fn new() -> Self {
Self {
max_events: 1_000_000,
sample_rate: 1,
track_allocations: true,
}
}
pub fn with_max_events(mut self, max_events: usize) -> Self {
self.max_events = max_events;
self
}
pub fn with_sample_rate(mut self, sample_rate: u32) -> Self {
self.sample_rate = sample_rate.max(1);
self
}
pub fn with_track_allocations(mut self, track_allocations: bool) -> Self {
self.track_allocations = track_allocations;
self
}
}
impl Default for ProfilerConfig2 {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
pub(super) struct InFlightFrame {
pub(super) name: String,
pub(super) enter_instant: Instant,
pub(super) callee_ns: u64,
}
impl InFlightFrame {
pub(super) fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
enter_instant: Instant::now(),
callee_ns: 0,
}
}
}
pub struct Profiler2 {
pub enabled: bool,
pub(super) events: Vec<ProfilerEvent>,
pub(super) call_stack: Vec<InFlightFrame>,
pub(super) call_records: HashMap<String, CallRecord>,
pub(super) alloc_records: HashMap<String, AllocationRecord>,
pub(super) gc_pauses: Vec<u64>,
pub(super) start_instant: Option<Instant>,
pub(super) config: ProfilerConfig2,
pub(super) event_counter: u64,
}
impl Profiler2 {
pub fn new(config: ProfilerConfig2) -> Self {
Self {
enabled: false,
events: Vec::new(),
call_stack: Vec::new(),
call_records: HashMap::new(),
alloc_records: HashMap::new(),
gc_pauses: Vec::new(),
start_instant: None,
config,
event_counter: 0,
}
}
pub fn new_enabled(config: ProfilerConfig2) -> Self {
let mut p = Self::new(config);
p.enable();
p
}
pub fn enable(&mut self) {
if !self.enabled {
self.enabled = true;
self.start_instant = Some(Instant::now());
}
}
pub fn disable(&mut self) {
self.enabled = false;
}
pub(super) fn should_sample(&mut self) -> bool {
self.event_counter = self.event_counter.wrapping_add(1);
let rate = self.config.sample_rate as u64;
(self.event_counter % rate) == 0
}
pub(super) fn push_event(&mut self, event: ProfilerEvent) {
if self.events.len() >= self.config.max_events {
self.events.remove(0);
}
self.events.push(event);
}
}
#[derive(Clone, Debug)]
pub struct FlameGraphNode {
pub name: String,
pub self_time: u64,
pub total_time: u64,
pub children: Vec<FlameGraphNode>,
}
impl FlameGraphNode {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
self_time: 0,
total_time: 0,
children: Vec::new(),
}
}
pub fn get_or_create_child(&mut self, name: &str) -> &mut FlameGraphNode {
if let Some(pos) = self.children.iter().position(|c| c.name == name) {
&mut self.children[pos]
} else {
self.children.push(FlameGraphNode::new(name));
self.children
.last_mut()
.expect("just pushed; cannot be empty")
}
}
pub fn node_count(&self) -> usize {
1 + self.children.iter().map(|c| c.node_count()).sum::<usize>()
}
}
pub(super) fn wall_clock_ns() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.as_ref()
.map(Duration::as_nanos)
.unwrap_or(0) as u64
}