use std::collections::HashMap;
use super::functions::profiler_now_ns;
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct TimelineEntry {
pub timestamp_ns: u64,
pub label: String,
pub duration_ns: u64,
pub category: String,
}
impl TimelineEntry {
#[allow(dead_code)]
pub fn new(timestamp_ns: u64, label: &str, duration_ns: u64, category: &str) -> Self {
Self {
timestamp_ns,
label: label.to_string(),
duration_ns,
category: category.to_string(),
}
}
}
#[allow(dead_code)]
pub struct ComprehensiveProfilingReport {
pub event_report: ProfileReport,
pub memory_profile: MemoryProfile,
pub flat_profile: Vec<(String, usize)>,
pub cumulative_profile: Vec<(String, usize)>,
pub gc_summary: String,
}
impl ComprehensiveProfilingReport {
#[allow(dead_code)]
pub fn build(session: &ProfilingSession) -> Self {
let event_report = session.profiler.generate_report();
let memory_profile = session.profiler.memory_profile();
let flat_profile = session.sampler.flat_profile();
let cumulative_profile = session.sampler.cumulative_profile();
let gc_summary = format!(
"GC cycles: {}, total alloc: {} bytes",
event_report.gc_cycles, memory_profile.total_allocs,
);
Self {
event_report,
memory_profile,
flat_profile,
cumulative_profile,
gc_summary,
}
}
#[allow(dead_code)]
pub fn to_text(&self) -> String {
let mut out = self.event_report.to_text();
out.push('\n');
out.push_str(&self.memory_profile.to_text());
out.push('\n');
out.push_str(&self.gc_summary);
out.push('\n');
if !self.flat_profile.is_empty() {
out.push_str("\nFlat profile:\n");
for (name, count) in &self.flat_profile {
out.push_str(&format!(" {:40} {}\n", name, count));
}
}
out
}
}
#[allow(dead_code)]
pub struct GcProfiler {
records: Vec<GcCollectionRecord>,
}
impl GcProfiler {
#[allow(dead_code)]
pub fn new() -> Self {
Self {
records: Vec::new(),
}
}
#[allow(dead_code)]
pub fn record(&mut self, collected: usize, live: usize, pause_ns: u64) {
let ts = profiler_now_ns();
self.records
.push(GcCollectionRecord::new(ts, collected, live, pause_ns));
}
#[allow(dead_code)]
pub fn collection_count(&self) -> usize {
self.records.len()
}
#[allow(dead_code)]
pub fn total_collected(&self) -> usize {
self.records.iter().map(|r| r.collected).sum()
}
#[allow(dead_code)]
pub fn avg_pause_ns(&self) -> f64 {
if self.records.is_empty() {
0.0
} else {
let total: u64 = self.records.iter().map(|r| r.pause_ns).sum();
total as f64 / self.records.len() as f64
}
}
#[allow(dead_code)]
pub fn max_pause_ns(&self) -> u64 {
self.records.iter().map(|r| r.pause_ns).max().unwrap_or(0)
}
#[allow(dead_code)]
pub fn summary(&self) -> String {
format!(
"GC: {} collections, {} total collected, avg_pause={:.0}ns, max_pause={}ns",
self.collection_count(),
self.total_collected(),
self.avg_pause_ns(),
self.max_pause_ns(),
)
}
}
#[allow(dead_code)]
pub struct TacticProfileLog {
events: Vec<TacticProfilingEvent>,
}
impl TacticProfileLog {
#[allow(dead_code)]
pub fn new() -> Self {
Self { events: Vec::new() }
}
#[allow(dead_code)]
pub fn record(&mut self, event: TacticProfilingEvent) {
self.events.push(event);
}
#[allow(dead_code)]
pub fn total_duration_ns(&self) -> u64 {
self.events.iter().map(|e| e.duration_ns).sum()
}
#[allow(dead_code)]
pub fn success_count(&self) -> usize {
self.events.iter().filter(|e| e.success).count()
}
#[allow(dead_code)]
pub fn top_slow(&self, n: usize) -> Vec<&TacticProfilingEvent> {
let mut sorted: Vec<&TacticProfilingEvent> = self.events.iter().collect();
sorted.sort_by_key(|b| std::cmp::Reverse(b.duration_ns));
sorted.truncate(n);
sorted
}
#[allow(dead_code)]
pub fn avg_duration_ns(&self) -> f64 {
if self.events.is_empty() {
0.0
} else {
self.total_duration_ns() as f64 / self.events.len() as f64
}
}
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct GcCollectionRecord {
pub timestamp_ns: u64,
pub collected: usize,
pub live: usize,
pub pause_ns: u64,
}
impl GcCollectionRecord {
#[allow(dead_code)]
pub fn new(timestamp_ns: u64, collected: usize, live: usize, pause_ns: u64) -> Self {
Self {
timestamp_ns,
collected,
live,
pause_ns,
}
}
#[allow(dead_code)]
pub fn efficiency(&self) -> f64 {
let total = self.collected + self.live;
if total == 0 {
0.0
} else {
self.collected as f64 / total as f64
}
}
}
#[derive(Clone, Debug)]
pub enum ProfilingEvent {
FunctionCall {
name: String,
depth: u32,
},
FunctionReturn {
name: String,
duration_ns: u64,
},
Allocation {
size: usize,
tag: String,
},
Deallocation {
size: usize,
tag: String,
},
GcCycle {
collected: usize,
live: usize,
},
TacticStep {
tactic_name: String,
goal_count: u32,
},
}
#[derive(Clone, Debug)]
pub struct ProfileReport {
pub total_calls: usize,
pub total_alloc_bytes: usize,
pub hot_functions: Vec<(String, u64)>,
pub gc_cycles: usize,
}
impl ProfileReport {
pub fn to_text(&self) -> String {
let mut out = String::new();
out.push_str("=== Profile Report ===\n");
out.push_str(&format!("Total function calls : {}\n", self.total_calls));
out.push_str(&format!(
"Total allocations : {} bytes\n",
self.total_alloc_bytes
));
out.push_str(&format!("GC cycles : {}\n", self.gc_cycles));
if !self.hot_functions.is_empty() {
out.push_str("\nHot functions (top 10):\n");
for (i, (name, ns)) in self.hot_functions.iter().enumerate() {
out.push_str(&format!(" {:2}. {:40} {:>12} ns\n", i + 1, name, ns));
}
}
out
}
pub fn to_json(&self) -> String {
let hot_json: Vec<String> = self
.hot_functions
.iter()
.map(|(name, ns)| format!("{{\"name\":\"{}\",\"duration_ns\":{}}}", name, ns))
.collect();
format!(
"{{\"total_calls\":{},\"total_alloc_bytes\":{},\"gc_cycles\":{},\"hot_functions\":[{}]}}",
self.total_calls, self.total_alloc_bytes, self.gc_cycles, hot_json.join(",")
)
}
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct TacticProfilingEvent {
pub tactic: String,
pub duration_ns: u64,
pub success: bool,
pub goals_before: u32,
pub goals_after: u32,
}
impl TacticProfilingEvent {
#[allow(dead_code)]
pub fn new(
tactic: &str,
duration_ns: u64,
success: bool,
goals_before: u32,
goals_after: u32,
) -> Self {
Self {
tactic: tactic.to_string(),
duration_ns,
success,
goals_before,
goals_after,
}
}
#[allow(dead_code)]
pub fn goals_eliminated(&self) -> i32 {
self.goals_before as i32 - self.goals_after as i32
}
}
#[allow(dead_code)]
pub struct HeatMap {
pub buckets: usize,
pub span_ns: u64,
pub counts: Vec<u64>,
}
impl HeatMap {
#[allow(dead_code)]
pub fn new(buckets: usize, span_ns: u64) -> Self {
Self {
buckets,
span_ns,
counts: vec![0; buckets],
}
}
#[allow(dead_code)]
pub fn record(&mut self, timestamp_ns: u64, start_ns: u64) {
if self.span_ns == 0 || self.buckets == 0 {
return;
}
let offset = timestamp_ns.saturating_sub(start_ns);
let bucket = ((offset as u128 * self.buckets as u128) / self.span_ns as u128) as usize;
let bucket = bucket.min(self.buckets - 1);
self.counts[bucket] += 1;
}
#[allow(dead_code)]
pub fn peak_bucket(&self) -> usize {
self.counts
.iter()
.enumerate()
.max_by_key(|(_, &v)| v)
.map(|(i, _)| i)
.unwrap_or(0)
}
#[allow(dead_code)]
pub fn render_ascii(&self) -> String {
let max_count = *self.counts.iter().max().unwrap_or(&1).max(&1);
let height = 8usize;
let mut rows: Vec<String> = Vec::new();
for row in (0..height).rev() {
let threshold = (row as f64 / height as f64 * max_count as f64) as u64;
let line: String = self
.counts
.iter()
.map(|&c| if c > threshold { '#' } else { ' ' })
.collect();
rows.push(format!("|{}", line));
}
rows.push(format!("+{}", "-".repeat(self.buckets)));
rows.join("\n")
}
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct TimelineAnnotation {
pub timestamp_ns: u64,
pub text: String,
pub category: String,
}
impl TimelineAnnotation {
#[allow(dead_code)]
pub fn new(timestamp_ns: u64, text: &str, category: &str) -> Self {
Self {
timestamp_ns,
text: text.to_string(),
category: category.to_string(),
}
}
}
#[allow(dead_code)]
#[derive(Clone, Debug, Default)]
pub struct PerfCounter {
pub instructions_retired: u64,
pub cache_misses: u64,
pub branch_mispredictions: u64,
pub context_switches: u64,
pub cycles: u64,
}
impl PerfCounter {
#[allow(dead_code)]
pub fn new() -> Self {
Self::default()
}
#[allow(dead_code)]
pub fn simulate_instructions(&mut self, n: u64) {
self.instructions_retired += n;
self.cycles += n;
}
#[allow(dead_code)]
pub fn simulate_cache_miss(&mut self) {
self.cache_misses += 1;
self.cycles += 200;
}
#[allow(dead_code)]
pub fn simulate_branch_misprediction(&mut self) {
self.branch_mispredictions += 1;
self.cycles += 15;
}
#[allow(dead_code)]
pub fn ipc(&self) -> f64 {
if self.cycles == 0 {
0.0
} else {
self.instructions_retired as f64 / self.cycles as f64
}
}
#[allow(dead_code)]
pub fn cache_miss_rate_per_1k(&self) -> f64 {
if self.instructions_retired == 0 {
0.0
} else {
(self.cache_misses as f64 / self.instructions_retired as f64) * 1000.0
}
}
#[allow(dead_code)]
pub fn summary(&self) -> String {
format!(
"PerfCounters: instr={}, cycles={}, IPC={:.2}, cache_misses={}, branch_mispredict={}",
self.instructions_retired,
self.cycles,
self.ipc(),
self.cache_misses,
self.branch_mispredictions
)
}
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct ProfilerConfig {
pub event_profiling: bool,
pub sampling_profiling: bool,
pub sampling_interval_ns: u64,
pub max_events: usize,
pub track_gc: bool,
pub track_allocs: bool,
}
impl ProfilerConfig {
#[allow(dead_code)]
pub fn new() -> Self {
Self::default()
}
#[allow(dead_code)]
pub fn enable_all(mut self) -> Self {
self.event_profiling = true;
self.sampling_profiling = true;
self
}
#[allow(dead_code)]
pub fn disable_all(mut self) -> Self {
self.event_profiling = false;
self.sampling_profiling = false;
self
}
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct ProfileSample {
pub timestamp_ns: u64,
pub call_stack: Vec<String>,
pub thread_id: u64,
}
impl ProfileSample {
#[allow(dead_code)]
pub fn new(timestamp_ns: u64, call_stack: Vec<String>, thread_id: u64) -> Self {
Self {
timestamp_ns,
call_stack,
thread_id,
}
}
#[allow(dead_code)]
pub fn top_function(&self) -> Option<&str> {
self.call_stack.first().map(|s| s.as_str())
}
#[allow(dead_code)]
pub fn depth(&self) -> usize {
self.call_stack.len()
}
}
#[allow(dead_code)]
pub struct CountingStep {
pub step_name: String,
pub counts: HashMap<String, u64>,
}
impl CountingStep {
#[allow(dead_code)]
pub fn new(name: &str) -> Self {
Self {
step_name: name.to_string(),
counts: HashMap::new(),
}
}
pub(super) fn variant_name(event: &ProfilingEvent) -> &'static str {
match event {
ProfilingEvent::FunctionCall { .. } => "FunctionCall",
ProfilingEvent::FunctionReturn { .. } => "FunctionReturn",
ProfilingEvent::Allocation { .. } => "Allocation",
ProfilingEvent::Deallocation { .. } => "Deallocation",
ProfilingEvent::GcCycle { .. } => "GcCycle",
ProfilingEvent::TacticStep { .. } => "TacticStep",
}
}
}
#[allow(dead_code)]
pub struct RealTimeMonitor {
pub name: String,
pub snapshots: Vec<(u64, String, f64)>,
pub capacity: usize,
}
impl RealTimeMonitor {
#[allow(dead_code)]
pub fn new(name: &str, capacity: usize) -> Self {
Self {
name: name.to_string(),
snapshots: Vec::new(),
capacity,
}
}
#[allow(dead_code)]
pub fn record(&mut self, metric: &str, value: f64) {
let ts = profiler_now_ns();
if self.snapshots.len() >= self.capacity {
self.snapshots.remove(0);
}
self.snapshots.push((ts, metric.to_string(), value));
}
#[allow(dead_code)]
pub fn latest(&self, metric: &str) -> Option<f64> {
self.snapshots
.iter()
.rev()
.find(|(_, m, _)| m == metric)
.map(|(_, _, v)| *v)
}
#[allow(dead_code)]
pub fn avg(&self, metric: &str) -> f64 {
let values: Vec<f64> = self
.snapshots
.iter()
.filter(|(_, m, _)| m == metric)
.map(|(_, _, v)| *v)
.collect();
if values.is_empty() {
0.0
} else {
values.iter().sum::<f64>() / values.len() as f64
}
}
#[allow(dead_code)]
pub fn count(&self, metric: &str) -> usize {
self.snapshots
.iter()
.filter(|(_, m, _)| m == metric)
.count()
}
}
#[allow(dead_code)]
pub struct Histogram {
buckets: Vec<HistogramBucket>,
pub total: u64,
pub sum: f64,
}
impl Histogram {
#[allow(dead_code)]
pub fn new(n: usize, min_val: f64, max_val: f64) -> Self {
let width = (max_val - min_val) / n as f64;
let buckets = (0..n)
.map(|i| HistogramBucket {
lower: min_val + i as f64 * width,
upper: min_val + (i + 1) as f64 * width,
count: 0,
})
.collect();
Self {
buckets,
total: 0,
sum: 0.0,
}
}
#[allow(dead_code)]
pub fn record(&mut self, value: f64) {
self.total += 1;
self.sum += value;
if let Some(bucket) = self
.buckets
.iter_mut()
.find(|b| value >= b.lower && value < b.upper)
{
bucket.count += 1;
} else if let Some(last) = self.buckets.last_mut() {
if value >= last.lower {
last.count += 1;
}
}
}
#[allow(dead_code)]
pub fn mean(&self) -> f64 {
if self.total == 0 {
0.0
} else {
self.sum / self.total as f64
}
}
#[allow(dead_code)]
pub fn mode_bucket(&self) -> Option<&HistogramBucket> {
self.buckets.iter().max_by_key(|b| b.count)
}
#[allow(dead_code)]
pub fn render_ascii(&self) -> String {
let max_count = self
.buckets
.iter()
.map(|b| b.count)
.max()
.unwrap_or(1)
.max(1);
let bar_width = 40usize;
let mut out = String::new();
for bucket in &self.buckets {
let bar_len = (bucket.count as usize * bar_width) / max_count as usize;
let bar = "#".repeat(bar_len);
out.push_str(&format!(
"[{:.2}, {:.2}): {:6} | {}\n",
bucket.lower, bucket.upper, bucket.count, bar
));
}
out
}
}
#[allow(dead_code)]
pub struct AllocationTracker {
stats: HashMap<String, AllocationStat>,
}
impl AllocationTracker {
#[allow(dead_code)]
pub fn new() -> Self {
Self {
stats: HashMap::new(),
}
}
#[allow(dead_code)]
pub fn record_alloc(&mut self, tag: &str, bytes: u64) {
let s = self.stats.entry(tag.to_string()).or_default();
s.total_bytes += bytes;
s.alloc_count += 1;
s.live_bytes += bytes;
}
#[allow(dead_code)]
pub fn record_dealloc(&mut self, tag: &str, bytes: u64) {
let s = self.stats.entry(tag.to_string()).or_default();
s.dealloc_count += 1;
s.live_bytes = s.live_bytes.saturating_sub(bytes);
}
#[allow(dead_code)]
pub fn stats_for(&self, tag: &str) -> Option<&AllocationStat> {
self.stats.get(tag)
}
#[allow(dead_code)]
pub fn total_live_bytes(&self) -> u64 {
self.stats.values().map(|s| s.live_bytes).sum()
}
#[allow(dead_code)]
pub fn total_allocated_bytes(&self) -> u64 {
self.stats.values().map(|s| s.total_bytes).sum()
}
#[allow(dead_code)]
pub fn top_allocators(&self, n: usize) -> Vec<(&str, u64)> {
let mut v: Vec<(&str, u64)> = self
.stats
.iter()
.map(|(k, v)| (k.as_str(), v.total_bytes))
.collect();
v.sort_by_key(|b| std::cmp::Reverse(b.1));
v.truncate(n);
v
}
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct FlameNode {
pub name: String,
pub count: u64,
pub children: Vec<FlameNode>,
}
impl FlameNode {
#[allow(dead_code)]
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
count: 0,
children: Vec::new(),
}
}
#[allow(dead_code)]
pub fn get_or_create_child(&mut self, name: &str) -> &mut FlameNode {
if let Some(pos) = self.children.iter().position(|c| c.name == name) {
&mut self.children[pos]
} else {
self.children.push(FlameNode::new(name));
self.children
.last_mut()
.expect("just pushed a child so last_mut must return Some")
}
}
#[allow(dead_code)]
pub fn total(&self) -> u64 {
self.count + self.children.iter().map(|c| c.total()).sum::<u64>()
}
#[allow(dead_code)]
pub fn format(&self, depth: usize) -> String {
let indent = " ".repeat(depth);
let mut out = format!("{}{} ({})\n", indent, self.name, self.count);
for child in &self.children {
out.push_str(&child.format(depth + 1));
}
out
}
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct CallTreeNode {
pub name: String,
pub inclusive_ns: u64,
pub exclusive_ns: u64,
pub call_count: u64,
pub children: Vec<CallTreeNode>,
}
impl CallTreeNode {
#[allow(dead_code)]
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
inclusive_ns: 0,
exclusive_ns: 0,
call_count: 0,
children: Vec::new(),
}
}
#[allow(dead_code)]
pub fn avg_exclusive_ns(&self) -> f64 {
if self.call_count == 0 {
0.0
} else {
self.exclusive_ns as f64 / self.call_count as f64
}
}
#[allow(dead_code)]
pub fn avg_inclusive_ns(&self) -> f64 {
if self.call_count == 0 {
0.0
} else {
self.inclusive_ns as f64 / self.call_count as f64
}
}
#[allow(dead_code)]
pub fn find_child(&self, name: &str) -> Option<&CallTreeNode> {
self.children.iter().find(|c| c.name == name)
}
}
pub struct Profiler {
pub enabled: bool,
pub events: Vec<(u64, ProfilingEvent)>,
pub call_stack: Vec<(String, u64)>,
}
impl Profiler {
pub fn new() -> Self {
Self {
enabled: false,
events: Vec::new(),
call_stack: Vec::new(),
}
}
pub fn enable(&mut self) {
self.enabled = true;
}
pub fn disable(&mut self) {
self.enabled = false;
}
pub fn record(&mut self, event: ProfilingEvent) {
if self.enabled {
let ts = Self::now_ns();
self.events.push((ts, event));
}
}
pub fn enter_function(&mut self, name: &str) {
if self.enabled {
let ts = Self::now_ns();
let depth = self.call_stack.len() as u32;
self.call_stack.push((name.to_string(), ts));
self.events.push((
ts,
ProfilingEvent::FunctionCall {
name: name.to_string(),
depth,
},
));
}
}
pub fn exit_function(&mut self, name: &str) {
if self.enabled {
let ts = Self::now_ns();
let duration_ns =
if let Some(idx) = self.call_stack.iter().rposition(|(n, _)| n == name) {
let entry_ts = self.call_stack[idx].1;
self.call_stack.remove(idx);
ts.saturating_sub(entry_ts)
} else {
0
};
self.events.push((
ts,
ProfilingEvent::FunctionReturn {
name: name.to_string(),
duration_ns,
},
));
}
}
pub fn alloc(&mut self, size: usize, tag: &str) {
if self.enabled {
let ts = Self::now_ns();
self.events.push((
ts,
ProfilingEvent::Allocation {
size,
tag: tag.to_string(),
},
));
}
}
pub fn dealloc(&mut self, size: usize, tag: &str) {
if self.enabled {
let ts = Self::now_ns();
self.events.push((
ts,
ProfilingEvent::Deallocation {
size,
tag: tag.to_string(),
},
));
}
}
pub fn gc_cycle(&mut self, collected: usize, live: usize) {
if self.enabled {
let ts = Self::now_ns();
self.events
.push((ts, ProfilingEvent::GcCycle { collected, live }));
}
}
pub fn generate_report(&self) -> ProfileReport {
let mut total_calls: usize = 0;
let mut total_alloc_bytes: usize = 0;
let mut gc_cycles: usize = 0;
let mut fn_durations: HashMap<String, u64> = HashMap::new();
for (_, event) in &self.events {
match event {
ProfilingEvent::FunctionCall { .. } => {
total_calls += 1;
}
ProfilingEvent::FunctionReturn { name, duration_ns } => {
*fn_durations.entry(name.clone()).or_insert(0) += duration_ns;
}
ProfilingEvent::Allocation { size, .. } => {
total_alloc_bytes += size;
}
ProfilingEvent::GcCycle { .. } => {
gc_cycles += 1;
}
_ => {}
}
}
let mut hot_functions: Vec<(String, u64)> = fn_durations.into_iter().collect();
hot_functions.sort_by_key(|b| std::cmp::Reverse(b.1));
hot_functions.truncate(10);
ProfileReport {
total_calls,
total_alloc_bytes,
hot_functions,
gc_cycles,
}
}
pub fn memory_profile(&self) -> MemoryProfile {
let mut current_bytes: usize = 0;
let mut peak_bytes: usize = 0;
let mut total_allocs: usize = 0;
for (_, event) in &self.events {
match event {
ProfilingEvent::Allocation { size, .. } => {
current_bytes += size;
total_allocs += 1;
if current_bytes > peak_bytes {
peak_bytes = current_bytes;
}
}
ProfilingEvent::Deallocation { size, .. } => {
current_bytes = current_bytes.saturating_sub(*size);
}
_ => {}
}
}
MemoryProfile {
peak_bytes,
current_bytes,
total_allocs,
}
}
fn now_ns() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0)
}
}
#[allow(dead_code)]
pub struct ProfilingMiddleware {
pub profiler: Profiler,
pub active: bool,
}
impl ProfilingMiddleware {
#[allow(dead_code)]
pub fn new() -> Self {
let mut profiler = Profiler::new();
profiler.enable();
Self {
profiler,
active: true,
}
}
#[allow(dead_code)]
pub fn instrument<F, T>(&mut self, name: &str, f: F) -> T
where
F: FnOnce() -> T,
{
if self.active {
self.profiler.enter_function(name);
}
let result = f();
if self.active {
self.profiler.exit_function(name);
}
result
}
#[allow(dead_code)]
pub fn report(&self) -> ProfileReport {
self.profiler.generate_report()
}
}
#[allow(dead_code)]
pub struct TimelineView {
pub entries: Vec<TimelineEntry>,
}
impl TimelineView {
#[allow(dead_code)]
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
#[allow(dead_code)]
pub fn build(profiler: &Profiler) -> Self {
let mut view = TimelineView::new();
for (ts, event) in &profiler.events {
let entry = match event {
ProfilingEvent::FunctionCall { name, depth } => {
TimelineEntry::new(*ts, &format!("CALL {}[d={}]", name, depth), 0, "function")
}
ProfilingEvent::FunctionReturn { name, duration_ns } => TimelineEntry::new(
*ts,
&format!("RET {} ({}ns)", name, duration_ns),
*duration_ns,
"function",
),
ProfilingEvent::Allocation { size, tag } => {
TimelineEntry::new(*ts, &format!("ALLOC {} ({} bytes)", tag, size), 0, "memory")
}
ProfilingEvent::Deallocation { size, tag } => {
TimelineEntry::new(*ts, &format!("FREE {} ({} bytes)", tag, size), 0, "memory")
}
ProfilingEvent::GcCycle { collected, live } => TimelineEntry::new(
*ts,
&format!("GC: collected={} live={}", collected, live),
0,
"gc",
),
ProfilingEvent::TacticStep {
tactic_name,
goal_count,
} => TimelineEntry::new(
*ts,
&format!("TACTIC {} goals={}", tactic_name, goal_count),
0,
"tactic",
),
};
view.entries.push(entry);
}
view
}
#[allow(dead_code)]
pub fn by_category(&self, category: &str) -> Vec<&TimelineEntry> {
self.entries
.iter()
.filter(|e| e.category == category)
.collect()
}
#[allow(dead_code)]
pub fn span_ns(&self) -> u64 {
let min = self
.entries
.iter()
.map(|e| e.timestamp_ns)
.min()
.unwrap_or(0);
let max = self
.entries
.iter()
.map(|e| e.timestamp_ns + e.duration_ns)
.max()
.unwrap_or(0);
max.saturating_sub(min)
}
}
#[allow(dead_code)]
pub struct AnnotatedTimeline {
pub entries: Vec<TimelineEntry>,
pub annotations: Vec<TimelineAnnotation>,
}
impl AnnotatedTimeline {
#[allow(dead_code)]
pub fn new() -> Self {
Self {
entries: Vec::new(),
annotations: Vec::new(),
}
}
#[allow(dead_code)]
pub fn annotate(&mut self, annotation: TimelineAnnotation) {
self.annotations.push(annotation);
}
#[allow(dead_code)]
pub fn annotations_in_range(&self, start_ns: u64, end_ns: u64) -> Vec<&TimelineAnnotation> {
self.annotations
.iter()
.filter(|a| a.timestamp_ns >= start_ns && a.timestamp_ns <= end_ns)
.collect()
}
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct EventFilter {
pub function_names: Vec<String>,
pub min_timestamp_ns: u64,
pub max_timestamp_ns: u64,
pub min_alloc_bytes: usize,
}
impl EventFilter {
#[allow(dead_code)]
pub fn new() -> Self {
Self {
function_names: Vec::new(),
min_timestamp_ns: 0,
max_timestamp_ns: u64::MAX,
min_alloc_bytes: 0,
}
}
#[allow(dead_code)]
pub fn matches(&self, ts: u64, event: &ProfilingEvent) -> bool {
if ts < self.min_timestamp_ns || ts > self.max_timestamp_ns {
return false;
}
if !self.function_names.is_empty() {
let name = match event {
ProfilingEvent::FunctionCall { name, .. } => Some(name.as_str()),
ProfilingEvent::FunctionReturn { name, .. } => Some(name.as_str()),
_ => None,
};
if let Some(n) = name {
if !self.function_names.iter().any(|f| f == n) {
return false;
}
}
}
if let ProfilingEvent::Allocation { size, .. } = event {
if *size < self.min_alloc_bytes {
return false;
}
}
true
}
#[allow(dead_code)]
pub fn apply<'a>(&self, events: &'a [(u64, ProfilingEvent)]) -> Vec<&'a (u64, ProfilingEvent)> {
events
.iter()
.filter(|(ts, ev)| self.matches(*ts, ev))
.collect()
}
}
#[derive(Clone, Debug)]
pub struct MemoryProfile {
pub peak_bytes: usize,
pub current_bytes: usize,
pub total_allocs: usize,
}
impl MemoryProfile {
pub fn to_text(&self) -> String {
format!(
"=== Memory Profile ===\nPeak usage : {} bytes\nCurrent usage : {} bytes\nTotal allocs : {}\n",
self.peak_bytes, self.current_bytes, self.total_allocs
)
}
}
#[allow(dead_code)]
pub struct FlameGraph {
pub root: FlameNode,
pub total_samples: u64,
}
impl FlameGraph {
#[allow(dead_code)]
pub fn new() -> Self {
Self {
root: FlameNode::new("(all)"),
total_samples: 0,
}
}
#[allow(dead_code)]
pub fn add_stack(&mut self, stack: &[String]) {
self.total_samples += 1;
self.root.count += 1;
let mut node = &mut self.root;
for frame in stack.iter().rev() {
node = node.get_or_create_child(frame);
node.count += 1;
}
}
#[allow(dead_code)]
pub fn from_profiler(profiler: &SamplingProfiler) -> Self {
let mut fg = FlameGraph::new();
for sample in &profiler.samples {
fg.add_stack(&sample.call_stack);
}
fg
}
#[allow(dead_code)]
pub fn render_text(&self) -> String {
self.root.format(0)
}
}
#[allow(dead_code)]
#[derive(Clone, Debug, Default)]
pub struct HistogramBucket {
pub lower: f64,
pub upper: f64,
pub count: u64,
}
#[allow(dead_code)]
pub struct ProfilingSession {
pub profiler: Profiler,
pub sampler: SamplingProfiler,
pub alloc_tracker: AllocationTracker,
pub tactic_log: TacticProfileLog,
pub name: String,
pub running: bool,
}
impl ProfilingSession {
#[allow(dead_code)]
pub fn new(name: &str) -> Self {
Self {
profiler: Profiler::new(),
sampler: SamplingProfiler::new(1_000_000),
alloc_tracker: AllocationTracker::new(),
tactic_log: TacticProfileLog::new(),
name: name.to_string(),
running: false,
}
}
#[allow(dead_code)]
pub fn start(&mut self) {
self.profiler.enable();
self.sampler.enable();
self.running = true;
}
#[allow(dead_code)]
pub fn stop(&mut self) {
self.profiler.disable();
self.sampler.disable();
self.running = false;
}
#[allow(dead_code)]
pub fn enter_function(&mut self, name: &str) {
self.profiler.enter_function(name);
self.sampler.enter(name);
}
#[allow(dead_code)]
pub fn exit_function(&mut self, name: &str) {
self.profiler.exit_function(name);
self.sampler.leave(name);
}
#[allow(dead_code)]
pub fn alloc(&mut self, bytes: usize, tag: &str) {
self.profiler.alloc(bytes, tag);
self.alloc_tracker.record_alloc(tag, bytes as u64);
}
#[allow(dead_code)]
pub fn dealloc(&mut self, bytes: usize, tag: &str) {
self.profiler.dealloc(bytes, tag);
self.alloc_tracker.record_dealloc(tag, bytes as u64);
}
#[allow(dead_code)]
pub fn combined_report(&self) -> String {
let profile_report = self.profiler.generate_report();
let mem_profile = self.profiler.memory_profile();
format!(
"=== ProfilingSession: {} ===\n{}\n{}\nTactic steps: {}\nSamples: {}\nLive bytes: {}",
self.name,
profile_report.to_text(),
mem_profile.to_text(),
self.tactic_log.success_count(),
self.sampler.sample_count(),
self.alloc_tracker.total_live_bytes(),
)
}
}
#[allow(dead_code)]
pub struct SamplingProfiler {
pub samples: Vec<ProfileSample>,
pub enabled: bool,
pub interval_ns: u64,
pub current_stack: Vec<String>,
}
impl SamplingProfiler {
#[allow(dead_code)]
pub fn new(interval_ns: u64) -> Self {
Self {
samples: Vec::new(),
enabled: false,
interval_ns,
current_stack: Vec::new(),
}
}
#[allow(dead_code)]
pub fn enable(&mut self) {
self.enabled = true;
}
#[allow(dead_code)]
pub fn disable(&mut self) {
self.enabled = false;
}
#[allow(dead_code)]
pub fn enter(&mut self, function: &str) {
if self.enabled {
self.current_stack.insert(0, function.to_string());
}
}
#[allow(dead_code)]
pub fn leave(&mut self, function: &str) {
if self.enabled {
if let Some(pos) = self.current_stack.iter().position(|s| s == function) {
self.current_stack.remove(pos);
}
}
}
#[allow(dead_code)]
pub fn take_sample(&mut self, thread_id: u64) {
if self.enabled {
let ts = profiler_now_ns();
self.samples.push(ProfileSample::new(
ts,
self.current_stack.clone(),
thread_id,
));
}
}
#[allow(dead_code)]
pub fn flat_profile(&self) -> Vec<(String, usize)> {
let mut counts: HashMap<String, usize> = HashMap::new();
for sample in &self.samples {
if let Some(top) = sample.top_function() {
*counts.entry(top.to_string()).or_insert(0) += 1;
}
}
let mut result: Vec<(String, usize)> = counts.into_iter().collect();
result.sort_by_key(|b| std::cmp::Reverse(b.1));
result
}
#[allow(dead_code)]
pub fn cumulative_profile(&self) -> Vec<(String, usize)> {
let mut counts: HashMap<String, usize> = HashMap::new();
for sample in &self.samples {
for func in &sample.call_stack {
*counts.entry(func.clone()).or_insert(0) += 1;
}
}
let mut result: Vec<(String, usize)> = counts.into_iter().collect();
result.sort_by_key(|b| std::cmp::Reverse(b.1));
result
}
#[allow(dead_code)]
pub fn sample_count(&self) -> usize {
self.samples.len()
}
#[allow(dead_code)]
pub fn avg_stack_depth(&self) -> f64 {
if self.samples.is_empty() {
return 0.0;
}
let total: usize = self.samples.iter().map(|s| s.depth()).sum();
total as f64 / self.samples.len() as f64
}
}
#[allow(dead_code)]
#[derive(Clone, Debug, Default)]
pub struct AllocationStat {
pub total_bytes: u64,
pub alloc_count: u64,
pub dealloc_count: u64,
pub live_bytes: u64,
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct StackSnapshot {
pub timestamp_ns: u64,
pub frames: Vec<String>,
pub label: Option<String>,
}
impl StackSnapshot {
#[allow(dead_code)]
pub fn new(timestamp_ns: u64, frames: Vec<String>) -> Self {
Self {
timestamp_ns,
frames,
label: None,
}
}
#[allow(dead_code)]
pub fn with_label(mut self, label: &str) -> Self {
self.label = Some(label.to_string());
self
}
#[allow(dead_code)]
pub fn depth(&self) -> usize {
self.frames.len()
}
#[allow(dead_code)]
pub fn format(&self) -> String {
let label_str = self.label.as_deref().unwrap_or("(no label)");
let mut out = format!("Stack at {} ns [{}]:\n", self.timestamp_ns, label_str);
for (i, frame) in self.frames.iter().enumerate() {
out.push_str(&format!(" {:3}: {}\n", i, frame));
}
out
}
}