use crate::pgo::{
BlockFrequency, BranchProbability, DetailedProfileSummary, PGOAdvisor, ProfileSummary,
ProfileSummaryBuilder,
};
use crate::profile_data::{FunctionSamples, ProfileAnnotator};
use crate::x86::{
X86CondCode, X86InstrInfo, X86MicroArchKind, X86Opcode, X86RegisterInfo, X86Subtarget,
};
use std::collections::{BTreeMap, BinaryHeap, HashMap, HashSet};
use std::fmt;
use std::io::{self, BufRead, BufReader, Read, Write};
use std::path::{Path, PathBuf};
use std::{cmp, fs, iter};
pub const X86_MAX_SIMULTANEOUS_HW_COUNTERS: usize = 8;
pub const X86_DEFAULT_SAMPLE_FREQ: u32 = 99;
pub const X86_MAX_UNWIND_DEPTH: usize = 128;
pub const X86_MAX_HOTSPOT_FUNCTIONS: usize = 500;
pub const X86_FLAMEGRAPH_DEFAULT_WIDTH: u32 = 1200;
pub const X86_FLAMEGRAPH_FRAME_HEIGHT: u32 = 16;
pub const X86_MIN_SAMPLE_COUNT: u64 = 10;
pub const TOPDOWN_FRONTEND_BOUND: &str = "Frontend Bound";
pub const TOPDOWN_BACKEND_BOUND: &str = "Backend Bound";
pub const TOPDOWN_BAD_SPECULATION: &str = "Bad Speculation";
pub const TOPDOWN_RETIRING: &str = "Retiring";
pub const PERF_COUNT_HW_CPU_CYCLES: u64 = 0;
pub const PERF_COUNT_HW_INSTRUCTIONS: u64 = 1;
pub const PERF_COUNT_HW_CACHE_REFERENCES: u64 = 2;
pub const PERF_COUNT_HW_CACHE_MISSES: u64 = 3;
pub const PERF_COUNT_HW_BRANCH_INSTRUCTIONS: u64 = 4;
pub const PERF_COUNT_HW_BRANCH_MISSES: u64 = 5;
pub const PERF_COUNT_HW_STALLED_CYCLES_FRONTEND: u64 = 7;
pub const PERF_COUNT_HW_STALLED_CYCLES_BACKEND: u64 = 8;
pub const PERF_COUNT_HW_REF_CPU_CYCLES: u64 = 9;
#[cfg(target_arch = "x86_64")]
pub const PERF_EVENT_OPEN_SYSCALL: i64 = 298;
pub const PERF_FLAG_FD_NO_GROUP: u64 = 1;
pub const PERF_FLAG_FD_OUTPUT: u64 = 2;
pub const PERF_FLAG_PID_CGROUP: u64 = 4;
pub const PERF_FLAG_FD_CLOEXEC: u64 = 8;
pub const PERF_SAMPLE_IP: u64 = 1 << 0;
pub const PERF_SAMPLE_TID: u64 = 1 << 1;
pub const PERF_SAMPLE_TIME: u64 = 1 << 2;
pub const PERF_SAMPLE_ADDR: u64 = 1 << 3;
pub const PERF_SAMPLE_READ: u64 = 1 << 4;
pub const PERF_SAMPLE_CALLCHAIN: u64 = 1 << 5;
pub const PERF_SAMPLE_ID: u64 = 1 << 6;
pub const PERF_SAMPLE_CPU: u64 = 1 << 7;
pub const PERF_SAMPLE_PERIOD: u64 = 1 << 8;
pub const PERF_SAMPLE_STREAM_ID: u64 = 1 << 9;
pub const PERF_SAMPLE_RAW: u64 = 1 << 10;
pub const PERF_SAMPLE_BRANCH_STACK: u64 = 1 << 11;
pub const PERF_SAMPLE_REGS_USER: u64 = 1 << 12;
pub const PERF_SAMPLE_STACK_USER: u64 = 1 << 13;
pub const PERF_SAMPLE_WEIGHT: u64 = 1 << 14;
pub const PERF_SAMPLE_DATA_SRC: u64 = 1 << 15;
pub const ITT_NULL_HANDLE: *const std::ffi::c_void = std::ptr::null();
pub const AMD_IBS_DEFAULT_FETCH_PERIOD: u64 = 0x10000;
pub const AMD_IBS_DEFAULT_OP_PERIOD: u64 = 0x100000;
#[derive(Debug)]
pub struct X86ProfilingTools {
pub perf: X86PerfIntegration,
pub vtune: X86IntelVTune,
pub uprof: X86AMDuProf,
pub sampling: X86SamplingProfiler,
pub instrumented: X86InstrumentedProfiler,
pub visualizer: X86ProfileVisualizer,
pub comparator: X86ProfileComparison,
pub initialized: bool,
pub subtarget: X86Subtarget,
pub output_dir: PathBuf,
}
impl X86ProfilingTools {
pub fn new(subtarget: X86Subtarget) -> Self {
let output_dir = PathBuf::from("x86_profiling_output");
Self {
perf: X86PerfIntegration::new(),
vtune: X86IntelVTune::new(),
uprof: X86AMDuProf::new(),
sampling: X86SamplingProfiler::new(),
instrumented: X86InstrumentedProfiler::new(),
visualizer: X86ProfileVisualizer::new(),
comparator: X86ProfileComparison::new(),
initialized: true,
subtarget,
output_dir,
}
}
pub fn set_output_dir<P: AsRef<Path>>(&mut self, path: P) {
self.output_dir = path.as_ref().to_path_buf();
}
pub fn run_full_session(&mut self, binary: &str, duration_secs: u64) -> X86ProfilingSession {
let perf_config = self
.perf
.build_default_counter_config(binary, duration_secs);
let perf_cmd = self.perf.generate_stat_command(&perf_config);
let sampling_result = self.sampling.collect_samples(binary, duration_secs);
let instrumented_result = self.instrumented.get_profile();
if let Some(ref sr) = sampling_result {
self.sampling.analyze_topdown(sr);
}
X86ProfilingSession {
binary: binary.to_string(),
duration_secs,
perf_counter_config: perf_config,
perf_command: perf_cmd,
sampling_result,
instrumented_result,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
}
}
pub fn compare_sessions(
&mut self,
before: &X86ProfilingSession,
after: &X86ProfilingSession,
) -> X86ProfileDiffReport {
self.comparator
.compare(&before.to_profile_data(), &after.to_profile_data())
}
pub fn generate_flamegraph(&self, sampling: &X86SamplingResult, title: &str) -> Option<String> {
self.visualizer
.generate_flamegraph_svg(&sampling.collapsed_stacks, title)
}
pub fn generate_chrome_trace(&self, profile: &X86InstrumentedProfile) -> Option<String> {
self.visualizer.generate_chrome_tracing_json(profile)
}
pub fn export_all(&self) -> io::Result<Vec<PathBuf>> {
fs::create_dir_all(&self.output_dir)?;
let mut written = Vec::new();
let summary = self.output_dir.join("profiling_summary.txt");
let mut f = fs::File::create(&summary)?;
writeln!(f, "X86 Profiling Tools Summary")?;
writeln!(f, "=========================")?;
writeln!(f, "Initialized: {}", self.initialized)?;
writeln!(f, "Output directory: {}", self.output_dir.display())?;
written.push(summary);
Ok(written)
}
}
impl Default for X86ProfilingTools {
fn default() -> Self {
Self::new(X86Subtarget::default())
}
}
#[derive(Debug, Clone)]
pub struct X86ProfilingSession {
pub binary: String,
pub duration_secs: u64,
pub perf_counter_config: X86PerfCounterConfig,
pub perf_command: String,
pub sampling_result: Option<X86SamplingResult>,
pub instrumented_result: Option<X86InstrumentedProfile>,
pub timestamp: u64,
}
impl X86ProfilingSession {
pub fn to_profile_data(&self) -> X86ProfileData {
X86ProfileData {
binary: self.binary.clone(),
timestamp: self.timestamp,
total_samples: self
.sampling_result
.as_ref()
.map(|s| s.total_samples)
.unwrap_or(0),
function_samples: self
.sampling_result
.as_ref()
.map(|s| s.function_samples.clone())
.unwrap_or_default(),
collapsed_stacks: self
.sampling_result
.as_ref()
.map(|s| s.collapsed_stacks.clone())
.unwrap_or_default(),
function_call_counts: self
.instrumented_result
.as_ref()
.map(|i| i.call_counts.clone())
.unwrap_or_default(),
function_durations_ns: self
.instrumented_result
.as_ref()
.map(|i| i.durations_ns.clone())
.unwrap_or_default(),
counter_values: self.perf_counter_config.readings.clone(),
topdown_metrics: self
.sampling_result
.as_ref()
.map(|s| s.topdown_metrics.clone())
.unwrap_or_default(),
}
}
}
#[derive(Debug)]
pub struct X86PerfIntegration {
pub perf_available: bool,
pub perf_version: String,
pub counter_groups: Vec<X86PerfCounterGroup>,
pub parsed_events: Vec<X86PerfEvent>,
pub collapsed_stacks: HashMap<String, u64>,
}
impl X86PerfIntegration {
pub fn new() -> Self {
Self {
perf_available: Self::detect_perf(),
perf_version: Self::detect_perf_version(),
counter_groups: Vec::new(),
parsed_events: Vec::new(),
collapsed_stacks: HashMap::new(),
}
}
fn detect_perf() -> bool {
std::path::Path::new("/proc/sys/kernel/perf_event_paranoid").exists()
}
fn detect_perf_version() -> String {
std::process::Command::new("perf")
.arg("--version")
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|_| "unknown".to_string())
}
pub fn open_hw_counter(
&self,
event_type: u64,
config: u64,
pid: i32,
cpu: i32,
group_fd: i32,
flags: u64,
) -> X86PerfEventFd {
X86PerfEventFd {
fd: -1, event_type,
event_config: config,
pid,
cpu,
group_fd,
flags,
is_open: false,
}
}
pub fn open_cycles_counter(&self, pid: i32, cpu: i32) -> X86PerfEventFd {
self.open_hw_counter(
perf_event_type::PERF_TYPE_HARDWARE,
PERF_COUNT_HW_CPU_CYCLES,
pid,
cpu,
-1,
0,
)
}
pub fn open_instructions_counter(&self, pid: i32, cpu: i32) -> X86PerfEventFd {
self.open_hw_counter(
perf_event_type::PERF_TYPE_HARDWARE,
PERF_COUNT_HW_INSTRUCTIONS,
pid,
cpu,
-1,
0,
)
}
pub fn open_cache_refs_counter(&self, pid: i32, cpu: i32) -> X86PerfEventFd {
self.open_hw_counter(
perf_event_type::PERF_TYPE_HARDWARE,
PERF_COUNT_HW_CACHE_REFERENCES,
pid,
cpu,
-1,
0,
)
}
pub fn open_cache_misses_counter(&self, pid: i32, cpu: i32) -> X86PerfEventFd {
self.open_hw_counter(
perf_event_type::PERF_TYPE_HARDWARE,
PERF_COUNT_HW_CACHE_MISSES,
pid,
cpu,
-1,
0,
)
}
pub fn open_branch_counter(&self, pid: i32, cpu: i32) -> X86PerfEventFd {
self.open_hw_counter(
perf_event_type::PERF_TYPE_HARDWARE,
PERF_COUNT_HW_BRANCH_INSTRUCTIONS,
pid,
cpu,
-1,
0,
)
}
pub fn open_branch_misses_counter(&self, pid: i32, cpu: i32) -> X86PerfEventFd {
self.open_hw_counter(
perf_event_type::PERF_TYPE_HARDWARE,
PERF_COUNT_HW_BRANCH_MISSES,
pid,
cpu,
-1,
0,
)
}
pub fn open_frontend_stalls_counter(&self, pid: i32, cpu: i32) -> X86PerfEventFd {
self.open_hw_counter(
perf_event_type::PERF_TYPE_HARDWARE,
PERF_COUNT_HW_STALLED_CYCLES_FRONTEND,
pid,
cpu,
-1,
0,
)
}
pub fn open_backend_stalls_counter(&self, pid: i32, cpu: i32) -> X86PerfEventFd {
self.open_hw_counter(
perf_event_type::PERF_TYPE_HARDWARE,
PERF_COUNT_HW_STALLED_CYCLES_BACKEND,
pid,
cpu,
-1,
0,
)
}
pub fn open_ref_cycles_counter(&self, pid: i32, cpu: i32) -> X86PerfEventFd {
self.open_hw_counter(
perf_event_type::PERF_TYPE_HARDWARE,
PERF_COUNT_HW_REF_CPU_CYCLES,
pid,
cpu,
-1,
0,
)
}
pub fn build_default_counter_config(
&self,
binary: &str,
duration_secs: u64,
) -> X86PerfCounterConfig {
let mut config = X86PerfCounterConfig::new(binary, duration_secs);
config.add_event(
"cycles",
"PERF_COUNT_HW_CPU_CYCLES",
PERF_COUNT_HW_CPU_CYCLES,
);
config.add_event(
"instructions",
"PERF_COUNT_HW_INSTRUCTIONS",
PERF_COUNT_HW_INSTRUCTIONS,
);
config.add_event(
"cache-references",
"PERF_COUNT_HW_CACHE_REFERENCES",
PERF_COUNT_HW_CACHE_REFERENCES,
);
config.add_event(
"cache-misses",
"PERF_COUNT_HW_CACHE_MISSES",
PERF_COUNT_HW_CACHE_MISSES,
);
config.add_event(
"branches",
"PERF_COUNT_HW_BRANCH_INSTRUCTIONS",
PERF_COUNT_HW_BRANCH_INSTRUCTIONS,
);
config.add_event(
"branch-misses",
"PERF_COUNT_HW_BRANCH_MISSES",
PERF_COUNT_HW_BRANCH_MISSES,
);
config.add_event(
"stalled-cycles-frontend",
"PERF_COUNT_HW_STALLED_CYCLES_FRONTEND",
PERF_COUNT_HW_STALLED_CYCLES_FRONTEND,
);
config.add_event(
"stalled-cycles-backend",
"PERF_COUNT_HW_STALLED_CYCLES_BACKEND",
PERF_COUNT_HW_STALLED_CYCLES_BACKEND,
);
config.add_event(
"ref-cycles",
"PERF_COUNT_HW_REF_CPU_CYCLES",
PERF_COUNT_HW_REF_CPU_CYCLES,
);
config
}
pub fn configure_intel_raw_event(
&self,
event_select: u8,
umask: u8,
cmask: u8,
inv: bool,
edge: bool,
) -> u64 {
let mut config: u64 = 0;
config |= (event_select as u64) & 0xFF;
config |= ((umask as u64) & 0xFF) << 8;
config |= ((cmask as u64) & 0xFF) << 24;
if inv {
config |= 1 << 23;
}
if edge {
config |= 1 << 18;
}
config
}
pub fn configure_amd_raw_event(&self, event_select: u16, unit_mask: u8) -> u64 {
let mut config: u64 = 0;
config |= (event_select as u64) & 0xFF;
config |= (((event_select as u64) >> 8) & 0xF) << 32;
config |= (unit_mask as u64) << 8;
config
}
pub fn generate_record_command(&self, config: &X86PerfRecordConfig) -> String {
let mut cmd = String::from("perf record");
if let Some(ref freq) = config.frequency {
cmd.push_str(&format!(" -F {}", freq));
}
if let Some(ref count) = config.count {
cmd.push_str(&format!(" -c {}", count));
}
if config.call_graph {
cmd.push_str(" -g");
}
if let Some(ref call_graph_mode) = config.call_graph_mode {
cmd.push_str(&format!(" --call-graph {}", call_graph_mode));
}
if config.branch_stack {
cmd.push_str(" -b");
}
for event in &config.events {
cmd.push_str(&format!(" -e {}", event));
}
if let Some(ref output) = config.output_file {
cmd.push_str(&format!(" -o {}", output));
}
if let Some(duration) = config.duration_secs {
cmd.push_str(&format!(" -- sleep {}", duration));
} else if let Some(ref cmdline) = config.command {
cmd.push_str(&format!(" -- {}", cmdline));
}
cmd
}
pub fn generate_stat_command(&self, config: &X86PerfCounterConfig) -> String {
let mut cmd = String::from("perf stat");
if config.repeat > 1 {
cmd.push_str(&format!(" -r {}", config.repeat));
}
if config.per_thread {
cmd.push_str(" --per-thread");
}
if config.per_core {
cmd.push_str(" --per-core");
}
if config.metric_only {
cmd.push_str(" --metric-only");
}
for event in &config.events {
cmd.push_str(&format!(" -e {}", event.name));
}
if let Some(duration) = config.duration_secs {
cmd.push_str(&format!(" -- sleep {}", duration));
} else if let Some(ref cmdline) = config.command {
cmd.push_str(&format!(" -- {}", cmdline));
}
cmd
}
pub fn generate_annotate_command(&self, data_file: &str, symbol: Option<&str>) -> String {
let mut cmd = format!("perf annotate -i {}", data_file);
if let Some(sym) = symbol {
cmd.push_str(&format!(" --symbol=\"{}\"", sym));
}
cmd.push_str(" --stdio");
cmd
}
pub fn generate_report_command(&self, data_file: &str, sort_key: &str) -> String {
format!("perf report -i {} --sort {} --stdio", data_file, sort_key)
}
pub fn generate_script_command(&self, data_file: &str, fields: &[&str]) -> String {
let mut cmd = format!("perf script -i {}", data_file);
if !fields.is_empty() {
cmd.push_str(" -F ");
cmd.push_str(&fields.join(","));
}
cmd
}
pub fn generate_diff_command(&self, baseline_file: &str, target_file: &str) -> String {
format!("perf diff {} {}", baseline_file, target_file)
}
pub fn generate_top_command(&self, events: &[&str]) -> String {
let mut cmd = String::from("perf top");
for event in events {
cmd.push_str(&format!(" -e {}", event));
}
cmd
}
pub fn generate_list_command(&self, filter: Option<&str>) -> String {
let mut cmd = String::from("perf list");
if let Some(f) = filter {
cmd.push_str(&format!(" {}", f));
}
cmd
}
pub fn parse_script_output(&mut self, script_output: &str) -> Vec<X86PerfEvent> {
let mut events = Vec::new();
for line in script_output.lines() {
if line.trim().is_empty() || line.starts_with('#') {
continue;
}
if let Some(event) = self.parse_single_perf_event(line) {
events.push(event);
}
}
self.parsed_events = events.clone();
events
}
fn parse_single_perf_event(&self, line: &str) -> Option<X86PerfEvent> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 4 {
return None;
}
let command = parts[0].to_string();
let pid: u32 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
let timestamp: f64 = parts
.get(2)
.and_then(|s| s.trim_end_matches(':').parse().ok())
.unwrap_or(0.0);
let event_str = parts.get(3).map(|s| s.to_string()).unwrap_or_default();
let addr: u64 = parts
.get(4)
.and_then(|s| u64::from_str_radix(s.trim_start_matches("0x"), 16).ok())
.unwrap_or(0);
let symbol = if parts.len() > 5 {
parts[5..].join(" ")
} else {
"[unknown]".to_string()
};
Some(X86PerfEvent {
command,
pid,
timestamp,
event_type: event_str,
address: addr,
symbol,
})
}
pub fn parse_script_call_stacks(&mut self, script_output: &str) -> HashMap<String, u64> {
let mut stacks: HashMap<String, u64> = HashMap::new();
for line in script_output.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
if let Some(sep_pos) = trimmed.rfind(' ') {
let stack = &trimmed[..sep_pos];
if let Ok(count) = trimmed[sep_pos + 1..].parse::<u64>() {
*stacks.entry(stack.to_string()).or_insert(0) += count;
}
}
}
self.collapsed_stacks = stacks.clone();
stacks
}
pub fn compute_derived_metrics(&self, counters: &[X86PerfCounterReading]) -> X86DerivedMetrics {
let mut metrics = X86DerivedMetrics::default();
let cycles = Self::find_counter_value(counters, "cycles");
let instructions = Self::find_counter_value(counters, "instructions");
let cache_refs = Self::find_counter_value(counters, "cache-references");
let cache_misses = Self::find_counter_value(counters, "cache-misses");
let branches = Self::find_counter_value(counters, "branches");
let branch_misses = Self::find_counter_value(counters, "branch-misses");
let frontend_stalls = Self::find_counter_value(counters, "stalled-cycles-frontend");
let backend_stalls = Self::find_counter_value(counters, "stalled-cycles-backend");
if let (Some(c), Some(i)) = (cycles, instructions) {
if i > 0 {
metrics.cycles_per_instruction = c as f64 / i as f64;
}
if c > 0 {
metrics.instructions_per_cycle = i as f64 / c as f64;
}
metrics.total_cycles = c;
metrics.total_instructions = i;
}
if let (Some(refs), Some(miss)) = (cache_refs, cache_misses) {
if refs > 0 {
metrics.cache_miss_rate = miss as f64 / refs as f64 * 100.0;
}
}
if let (Some(br), Some(bm)) = (branches, branch_misses) {
if br > 0 {
metrics.branch_mispred_rate = bm as f64 / br as f64 * 100.0;
}
}
if let (Some(c), Some(fe)) = (cycles, frontend_stalls) {
if c > 0 {
metrics.frontend_stall_pct = fe as f64 / c as f64 * 100.0;
}
}
if let (Some(c), Some(be)) = (cycles, backend_stalls) {
if c > 0 {
metrics.backend_stall_pct = be as f64 / c as f64 * 100.0;
}
}
metrics
}
fn find_counter_value(counters: &[X86PerfCounterReading], name: &str) -> Option<u64> {
counters
.iter()
.find(|c| c.event_name == name)
.map(|c| c.value)
}
pub fn reset_counters(&mut self) {
self.parsed_events.clear();
self.collapsed_stacks.clear();
for group in &mut self.counter_groups {
group.readings.clear();
}
}
}
impl Default for X86PerfIntegration {
fn default() -> Self {
Self::new()
}
}
pub mod perf_event_type {
pub const PERF_TYPE_HARDWARE: u64 = 0;
pub const PERF_TYPE_SOFTWARE: u64 = 1;
pub const PERF_TYPE_TRACEPOINT: u64 = 2;
pub const PERF_TYPE_HW_CACHE: u64 = 3;
pub const PERF_TYPE_RAW: u64 = 4;
pub const PERF_TYPE_BREAKPOINT: u64 = 5;
pub const PERF_TYPE_AMD_IBS_FETCH: u64 = 0x13;
pub const PERF_TYPE_AMD_IBS_OP: u64 = 0x14;
}
#[derive(Debug, Clone)]
pub struct X86PerfEventFd {
pub fd: i32,
pub event_type: u64,
pub event_config: u64,
pub pid: i32,
pub cpu: i32,
pub group_fd: i32,
pub flags: u64,
pub is_open: bool,
}
impl X86PerfEventFd {
pub fn read_value(&self) -> Option<u64> {
if !self.is_open || self.fd < 0 {
return None;
}
None
}
pub fn enable(&self) -> io::Result<()> {
if !self.is_open || self.fd < 0 {
return Ok(());
}
Ok(())
}
pub fn disable(&self) -> io::Result<()> {
if !self.is_open || self.fd < 0 {
return Ok(());
}
Ok(())
}
pub fn reset(&self) -> io::Result<()> {
if !self.is_open || self.fd < 0 {
return Ok(());
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct X86PerfRecordConfig {
pub events: Vec<String>,
pub frequency: Option<u32>,
pub count: Option<u64>,
pub call_graph: bool,
pub call_graph_mode: Option<String>,
pub branch_stack: bool,
pub output_file: Option<String>,
pub duration_secs: Option<u64>,
pub command: Option<String>,
pub system_wide: bool,
pub inherit: bool,
}
impl Default for X86PerfRecordConfig {
fn default() -> Self {
Self {
events: vec!["cycles:pp".to_string()],
frequency: Some(X86_DEFAULT_SAMPLE_FREQ),
count: None,
call_graph: true,
call_graph_mode: Some("fp".to_string()),
branch_stack: false,
output_file: Some("perf.data".to_string()),
duration_secs: Some(10),
command: None,
system_wide: false,
inherit: true,
}
}
}
#[derive(Debug, Clone)]
pub struct X86PerfCounterConfig {
pub events: Vec<X86PerfCounterSpec>,
pub binary: String,
pub duration_secs: Option<u64>,
pub command: Option<String>,
pub repeat: u32,
pub per_thread: bool,
pub per_core: bool,
pub metric_only: bool,
pub readings: Vec<X86PerfCounterReading>,
}
impl X86PerfCounterConfig {
pub fn new(binary: &str, duration_secs: u64) -> Self {
Self {
events: Vec::new(),
binary: binary.to_string(),
duration_secs: Some(duration_secs),
command: None,
repeat: 1,
per_thread: false,
per_core: false,
metric_only: false,
readings: Vec::new(),
}
}
pub fn add_event(&mut self, name: &str, perf_name: &str, code: u64) {
self.events.push(X86PerfCounterSpec {
name: name.to_string(),
perf_name: perf_name.to_string(),
event_code: code,
});
}
pub fn set_reading(&mut self, event_name: &str, value: u64, unit: &str) {
self.readings.push(X86PerfCounterReading {
event_name: event_name.to_string(),
value,
unit: unit.to_string(),
});
}
pub fn get_reading(&self, event_name: &str) -> Option<u64> {
self.readings
.iter()
.find(|r| r.event_name == event_name)
.map(|r| r.value)
}
pub fn ipc(&self) -> Option<f64> {
let instr = self.get_reading("instructions")?;
let cycles = self.get_reading("cycles")?;
if cycles > 0 {
Some(instr as f64 / cycles as f64)
} else {
None
}
}
pub fn cache_miss_rate_pct(&self) -> Option<f64> {
let refs = self.get_reading("cache-references")?;
let misses = self.get_reading("cache-misses")?;
if refs > 0 {
Some(misses as f64 / refs as f64 * 100.0)
} else {
None
}
}
pub fn branch_mispred_rate_pct(&self) -> Option<f64> {
let branches = self.get_reading("branches")?;
let misses = self.get_reading("branch-misses")?;
if branches > 0 {
Some(misses as f64 / branches as f64 * 100.0)
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct X86PerfCounterSpec {
pub name: String,
pub perf_name: String,
pub event_code: u64,
}
#[derive(Debug, Clone)]
pub struct X86PerfCounterReading {
pub event_name: String,
pub value: u64,
pub unit: String,
}
#[derive(Debug, Clone)]
pub struct X86PerfCounterGroup {
pub name: String,
pub leader_fd: i32,
pub counters: Vec<X86PerfCounterDescriptor>,
pub readings: Vec<X86PerfCounterReading>,
}
impl X86PerfCounterGroup {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
leader_fd: -1,
counters: Vec::new(),
readings: Vec::new(),
}
}
pub fn add_counter(&mut self, name: &str, perf_name: &str, event_type: u64, config: u64) {
self.counters.push(X86PerfCounterDescriptor {
name: name.to_string(),
perf_name: perf_name.to_string(),
event_type,
event_config: config,
fd: -1,
});
}
pub fn len(&self) -> usize {
self.counters.len()
}
pub fn is_empty(&self) -> bool {
self.counters.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct X86PerfCounterDescriptor {
pub name: String,
pub perf_name: String,
pub event_type: u64,
pub event_config: u64,
pub fd: i32,
}
#[derive(Debug, Clone)]
pub struct X86PerfEvent {
pub command: String,
pub pid: u32,
pub timestamp: f64,
pub event_type: String,
pub address: u64,
pub symbol: String,
}
#[derive(Debug, Clone, Default)]
pub struct X86DerivedMetrics {
pub cycles_per_instruction: f64,
pub instructions_per_cycle: f64,
pub total_cycles: u64,
pub total_instructions: u64,
pub cache_miss_rate: f64,
pub branch_mispred_rate: f64,
pub frontend_stall_pct: f64,
pub backend_stall_pct: f64,
}
#[derive(Debug)]
pub struct X86VTuneDomain {
pub name: String,
pub handle: usize,
}
#[derive(Debug)]
pub struct X86VTuneStringHandle {
pub value: String,
pub handle: usize,
}
#[derive(Debug)]
pub struct X86IntelVTune {
pub vtune_available: bool,
pub vtune_path: Option<PathBuf>,
pub domains: Vec<X86VTuneDomain>,
pub string_handles: Vec<X86VTuneStringHandle>,
next_domain_handle: usize,
next_string_handle: usize,
pub hotspot_results: Vec<X86VTuneHotspotResult>,
pub uarch_results: Vec<X86VTuneUArchResult>,
pub memory_results: Vec<X86VTuneMemoryResult>,
}
impl X86IntelVTune {
pub fn new() -> Self {
Self {
vtune_available: Self::detect_vtune(),
vtune_path: Self::find_vtune_path(),
domains: Vec::new(),
string_handles: Vec::new(),
next_domain_handle: 1,
next_string_handle: 1,
hotspot_results: Vec::new(),
uarch_results: Vec::new(),
memory_results: Vec::new(),
}
}
fn detect_vtune() -> bool {
Self::find_vtune_path().is_some()
}
fn find_vtune_path() -> Option<PathBuf> {
for candidate in &[
"vtune",
"amplxe-cl",
"/opt/intel/vtune_profiler/bin64/vtune",
] {
let output = std::process::Command::new(candidate)
.arg("--version")
.output();
if output.is_ok() {
return Some(PathBuf::from(candidate));
}
}
None
}
pub fn itt_domain_create(&mut self, name: &str) -> usize {
for domain in &self.domains {
if domain.name == name {
return domain.handle;
}
}
let handle = self.next_domain_handle;
self.next_domain_handle += 1;
self.domains.push(X86VTuneDomain {
name: name.to_string(),
handle,
});
handle
}
pub fn itt_string_handle_create(&mut self, value: &str) -> usize {
for sh in &self.string_handles {
if sh.value == value {
return sh.handle;
}
}
let handle = self.next_string_handle;
self.next_string_handle += 1;
self.string_handles.push(X86VTuneStringHandle {
value: value.to_string(),
handle,
});
handle
}
pub fn itt_task_begin(
&self,
domain_handle: usize,
task_name_handle: usize,
) -> X86VTuneTaskMarker {
X86VTuneTaskMarker {
domain_handle,
task_name_handle,
start_time: std::time::Instant::now(),
}
}
pub fn itt_task_end(&self, _marker: X86VTuneTaskMarker) {
}
pub fn lookup_domain(&self, handle: usize) -> Option<&X86VTuneDomain> {
self.domains.iter().find(|d| d.handle == handle)
}
pub fn lookup_string(&self, handle: usize) -> Option<&X86VTuneStringHandle> {
self.string_handles.iter().find(|s| s.handle == handle)
}
pub fn domain_name(&self, handle: usize) -> String {
self.lookup_domain(handle)
.map(|d| d.name.clone())
.unwrap_or_else(|| format!("domain_{}", handle))
}
pub fn string_value(&self, handle: usize) -> String {
self.lookup_string(handle)
.map(|s| s.value.clone())
.unwrap_or_else(|| format!("string_{}", handle))
}
pub fn generate_hotspot_collect_command(
&self,
target: &str,
duration_secs: u64,
result_dir: &str,
) -> String {
let vtune = self
.vtune_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "vtune".to_string());
format!(
"{} -collect hotspots -result-dir {} -knob sampling-interval=1 -- {}",
vtune, result_dir, target
)
}
pub fn generate_uarch_collect_command(&self, target: &str, result_dir: &str) -> String {
let vtune = self
.vtune_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "vtune".to_string());
format!(
"{} -collect uarch-exploration -result-dir {} -- {}",
vtune, result_dir, target
)
}
pub fn generate_memory_collect_command(&self, target: &str, result_dir: &str) -> String {
let vtune = self
.vtune_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "vtune".to_string());
format!(
"{} -collect memory-access -result-dir {} -- {}",
vtune, result_dir, target
)
}
pub fn generate_hotspot_report_command(&self, result_dir: &str) -> String {
let vtune = self
.vtune_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "vtune".to_string());
format!(
"{} -report hotspots -result-dir {} -format csv -csv-delimiter comma",
vtune, result_dir
)
}
pub fn generate_topdown_report_command(&self, result_dir: &str) -> String {
let vtune = self
.vtune_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "vtune".to_string());
format!("{} -report top-down -result-dir {}", vtune, result_dir)
}
pub fn generate_callstacks_report_command(&self, result_dir: &str) -> String {
let vtune = self
.vtune_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "vtune".to_string());
format!("{} -report callstacks -result-dir {}", vtune, result_dir)
}
pub fn parse_hotspot_csv(&mut self, csv_text: &str) -> Vec<X86VTuneHotspotResult> {
let mut results = Vec::new();
for line in csv_text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with("Function") {
continue;
}
let fields: Vec<&str> = line.split(',').collect();
if fields.len() >= 4 {
results.push(X86VTuneHotspotResult {
function: fields.get(0).unwrap_or(&"").to_string(),
module: fields.get(1).unwrap_or(&"").to_string(),
cpu_time_pct: fields.get(2).and_then(|s| s.parse().ok()).unwrap_or(0.0),
cpu_time_sec: fields.get(3).and_then(|s| s.parse().ok()).unwrap_or(0.0),
instructions_retired: fields.get(4).and_then(|s| s.parse().ok()).unwrap_or(0),
cpi_rate: fields.get(5).and_then(|s| s.parse().ok()).unwrap_or(0.0),
});
}
}
self.hotspot_results = results.clone();
results
}
pub fn top_hotspots(&self, n: usize) -> Vec<&X86VTuneHotspotResult> {
let mut sorted: Vec<&X86VTuneHotspotResult> = self.hotspot_results.iter().collect();
sorted.sort_by(|a, b| {
b.cpu_time_pct
.partial_cmp(&a.cpu_time_pct)
.unwrap_or(std::cmp::Ordering::Equal)
});
sorted.truncate(n);
sorted
}
pub fn high_cpi_functions(&self, threshold: f64) -> Vec<&X86VTuneHotspotResult> {
self.hotspot_results
.iter()
.filter(|h| h.cpi_rate > threshold && h.cpu_time_pct > 1.0)
.collect()
}
pub fn high_instruction_functions(&self, min_pct: f64) -> Vec<&X86VTuneHotspotResult> {
self.hotspot_results
.iter()
.filter(|h| h.cpu_time_pct > min_pct)
.collect()
}
pub fn record_uarch_finding(&mut self, finding: X86VTuneUArchResult) {
self.uarch_results.push(finding);
}
pub fn uarch_bottlenecks(&self) -> Vec<&X86VTuneUArchResult> {
let mut sorted: Vec<&X86VTuneUArchResult> = self.uarch_results.iter().collect();
sorted.sort_by(|a, b| {
b.severity
.partial_cmp(&a.severity)
.unwrap_or(std::cmp::Ordering::Equal)
});
sorted
}
pub fn frontend_bottlenecks(&self) -> Vec<&X86VTuneUArchResult> {
self.uarch_results
.iter()
.filter(|r| {
r.category.contains("Frontend")
|| r.category.contains("front-end")
|| r.category.contains("ICache")
|| r.category.contains("ITLB")
|| r.category.contains("Decode")
})
.collect()
}
pub fn backend_bottlenecks(&self) -> Vec<&X86VTuneUArchResult> {
self.uarch_results
.iter()
.filter(|r| {
r.category.contains("Backend")
|| r.category.contains("back-end")
|| r.category.contains("Port")
|| r.category.contains("Execution")
|| r.category.contains("Divider")
})
.collect()
}
pub fn memory_bottlenecks(&self) -> Vec<&X86VTuneUArchResult> {
self.uarch_results
.iter()
.filter(|r| {
r.category.contains("Memory")
|| r.category.contains("Cache")
|| r.category.contains("TLB")
|| r.category.contains("DRAM")
|| r.category.contains("Load")
|| r.category.contains("Store")
})
.collect()
}
pub fn speculation_bottlenecks(&self) -> Vec<&X86VTuneUArchResult> {
self.uarch_results
.iter()
.filter(|r| {
r.category.contains("Branch")
|| r.category.contains("Speculation")
|| r.category.contains("Mispredict")
})
.collect()
}
pub fn record_memory_finding(&mut self, finding: X86VTuneMemoryResult) {
self.memory_results.push(finding);
}
pub fn memory_latency_hotspots(&self) -> Vec<&X86VTuneMemoryResult> {
let mut sorted: Vec<&X86VTuneMemoryResult> = self.memory_results.iter().collect();
sorted.sort_by(|a, b| {
b.avg_latency_cycles
.partial_cmp(&a.avg_latency_cycles)
.unwrap_or(std::cmp::Ordering::Equal)
});
sorted
}
pub fn llc_miss_accesses(&self) -> Vec<&X86VTuneMemoryResult> {
self.memory_results
.iter()
.filter(|r| r.llc_miss_count > 0)
.collect()
}
pub fn high_bandwidth_accesses(&self, threshold_gb_s: f64) -> Vec<&X86VTuneMemoryResult> {
self.memory_results
.iter()
.filter(|r| r.bandwidth_gb_s > threshold_gb_s)
.collect()
}
}
impl Default for X86IntelVTune {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct X86VTuneTaskMarker {
pub domain_handle: usize,
pub task_name_handle: usize,
pub start_time: std::time::Instant,
}
#[derive(Debug, Clone)]
pub struct X86VTuneHotspotResult {
pub function: String,
pub module: String,
pub cpu_time_pct: f64,
pub cpu_time_sec: f64,
pub instructions_retired: u64,
pub cpi_rate: f64,
}
#[derive(Debug, Clone)]
pub struct X86VTuneUArchResult {
pub category: String,
pub description: String,
pub function: String,
pub source_location: String,
pub severity: f64,
pub pipeline_slot_pct: f64,
pub recommendation: String,
}
#[derive(Debug, Clone)]
pub struct X86VTuneMemoryResult {
pub function: String,
pub source_location: String,
pub variable: String,
pub allocation_site: String,
pub l1d_miss_count: u64,
pub l2_miss_count: u64,
pub llc_miss_count: u64,
pub avg_latency_cycles: f64,
pub bandwidth_gb_s: f64,
pub is_numa_remote: bool,
pub dram_access_count: u64,
}
#[derive(Debug)]
pub struct X86AMDuProf {
pub uprof_available: bool,
pub uprof_path: Option<PathBuf>,
pub ibs_fetch_config: X86IBSFetchConfig,
pub ibs_op_config: X86IBSOpConfig,
pub l3_analysis: Vec<X86AMDL3CacheMiss>,
pub ibs_samples: Vec<X86AMDIbsSample>,
pub pipeline_metrics: Vec<X86AMDPipelineMetric>,
}
impl X86AMDuProf {
pub fn new() -> Self {
Self {
uprof_available: Self::detect_uprof(),
uprof_path: Self::find_uprof_path(),
ibs_fetch_config: X86IBSFetchConfig::default(),
ibs_op_config: X86IBSOpConfig::default(),
l3_analysis: Vec::new(),
ibs_samples: Vec::new(),
pipeline_metrics: Vec::new(),
}
}
fn detect_uprof() -> bool {
Self::find_uprof_path().is_some()
}
fn find_uprof_path() -> Option<PathBuf> {
for candidate in &[
"AMDuProfCLI",
"/opt/AMDuProf/bin/AMDuProfCLI",
"AMDuProfPcm",
] {
let output = std::process::Command::new(candidate)
.arg("--version")
.output();
if output.is_ok() {
return Some(PathBuf::from(candidate));
}
}
None
}
pub fn generate_collect_command(
&self,
target: &str,
output_dir: &str,
duration_secs: u64,
) -> String {
let uprof = self
.uprof_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "AMDuProfCLI".to_string());
format!(
"{} collect --config tbp --output-dir {} --duration {} -- {}",
uprof, output_dir, duration_secs, target
)
}
pub fn generate_ibs_collect_command(
&self,
target: &str,
output_dir: &str,
duration_secs: u64,
) -> String {
let uprof = self
.uprof_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "AMDuProfCLI".to_string());
format!(
"{} collect --config ibs --output-dir {} --duration {} -- {}",
uprof, output_dir, duration_secs, target
)
}
pub fn generate_report_command(
&self,
input_dir: &str,
output_file: &str,
report_type: &str,
) -> String {
let uprof = self
.uprof_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "AMDuProfCLI".to_string());
format!(
"{} report --input-dir {} --report {} --output-file {} --format csv",
uprof, input_dir, report_type, output_file
)
}
pub fn configure_ibs_fetch(
&mut self,
period: u64,
enable_randomized: bool,
enable_l2_miss_filter: bool,
) {
self.ibs_fetch_config = X86IBSFetchConfig {
enabled: true,
period,
randomized_period: enable_randomized,
l2_miss_filter: enable_l2_miss_filter,
max_count: 0xFFFFFFFF,
};
}
pub fn configure_ibs_op(
&mut self,
period: u64,
enable_randomized: bool,
enable_dcache_miss_filter: bool,
) {
self.ibs_op_config = X86IBSOpConfig {
enabled: true,
period,
randomized_period: enable_randomized,
dcache_miss_filter: enable_dcache_miss_filter,
max_count: 0xFFFFFFFF,
};
}
pub fn enable_ibs_default(&mut self) {
self.configure_ibs_fetch(AMD_IBS_DEFAULT_FETCH_PERIOD, true, false);
self.configure_ibs_op(AMD_IBS_DEFAULT_OP_PERIOD, true, false);
}
pub fn disable_ibs(&mut self) {
self.ibs_fetch_config.enabled = false;
self.ibs_op_config.enabled = false;
}
pub fn ibs_fetch_period_for_freq(&self, target_freq_hz: u64) -> u64 {
const BASE_CLOCK: u64 = 3_000_000_000;
let scaled =
(BASE_CLOCK as f64 / target_freq_hz as f64) * AMD_IBS_DEFAULT_FETCH_PERIOD as f64;
scaled as u64
}
pub fn ibs_op_period_for_freq(&self, target_freq_hz: u64) -> u64 {
const BASE_CLOCK: u64 = 3_000_000_000;
let scaled = (BASE_CLOCK as f64 / target_freq_hz as f64) * AMD_IBS_DEFAULT_OP_PERIOD as f64;
scaled as u64
}
pub fn record_l3_cache_miss(&mut self, miss: X86AMDL3CacheMiss) {
self.l3_analysis.push(miss);
}
pub fn analyze_l3_hotspots(&self) -> Vec<&X86AMDL3CacheMiss> {
let mut sorted: Vec<&X86AMDL3CacheMiss> = self.l3_analysis.iter().collect();
sorted.sort_by(|a, b| b.count.cmp(&a.count));
sorted
}
pub fn l3_misses_by_function(&self) -> BTreeMap<String, u64> {
let mut map: BTreeMap<String, u64> = BTreeMap::new();
for miss in &self.l3_analysis {
*map.entry(miss.function.clone()).or_insert(0) += miss.count;
}
map
}
pub fn l3_misses_by_location(&self) -> BTreeMap<String, u64> {
let mut map: BTreeMap<String, u64> = BTreeMap::new();
for miss in &self.l3_analysis {
let key = format!("{}:{}", miss.source_file, miss.source_line);
*map.entry(key).or_insert(0) += miss.count;
}
map
}
pub fn top_l3_miss_locations(&self, n: usize) -> Vec<(String, u64)> {
let mut entries: Vec<(String, u64)> = self.l3_misses_by_location().into_iter().collect();
entries.sort_by(|a, b| b.1.cmp(&a.1));
entries.truncate(n);
entries
}
pub fn total_l3_misses(&self) -> u64 {
self.l3_analysis.iter().map(|m| m.count).sum()
}
pub fn record_ibs_sample(&mut self, sample: X86AMDIbsSample) {
self.ibs_samples.push(sample);
}
pub fn avg_load_latency(&self) -> Option<f64> {
let loads: Vec<&X86AMDIbsSample> = self.ibs_samples.iter().filter(|s| s.is_load).collect();
if loads.is_empty() {
return None;
}
let total: u64 = loads.iter().map(|s| s.dcache_miss_latency as u64).sum();
Some(total as f64 / loads.len() as f64)
}
pub fn l1d_miss_samples(&self) -> Vec<&X86AMDIbsSample> {
self.ibs_samples
.iter()
.filter(|s| s.dcache_miss_latency > 0)
.collect()
}
pub fn tlb_miss_samples(&self) -> Vec<&X86AMDIbsSample> {
self.ibs_samples.iter().filter(|s| s.dtlb_miss).collect()
}
pub fn record_pipeline_metric(&mut self, metric: X86AMDPipelineMetric) {
self.pipeline_metrics.push(metric);
}
pub fn pipeline_metrics_for_core(&self, core_id: u32) -> Vec<&X86AMDPipelineMetric> {
self.pipeline_metrics
.iter()
.filter(|m| m.core_id == core_id)
.collect()
}
pub fn avg_dispatch_utilization(&self) -> Option<f64> {
if self.pipeline_metrics.is_empty() {
return None;
}
let total: f64 = self
.pipeline_metrics
.iter()
.map(|m| m.dispatch_utilization)
.sum();
Some(total / self.pipeline_metrics.len() as f64)
}
}
impl Default for X86AMDuProf {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct X86IBSFetchConfig {
pub enabled: bool,
pub period: u64,
pub randomized_period: bool,
pub l2_miss_filter: bool,
pub max_count: u64,
}
impl Default for X86IBSFetchConfig {
fn default() -> Self {
Self {
enabled: false,
period: AMD_IBS_DEFAULT_FETCH_PERIOD,
randomized_period: true,
l2_miss_filter: false,
max_count: 0xFFFFF,
}
}
}
#[derive(Debug, Clone)]
pub struct X86IBSOpConfig {
pub enabled: bool,
pub period: u64,
pub randomized_period: bool,
pub dcache_miss_filter: bool,
pub max_count: u64,
}
impl Default for X86IBSOpConfig {
fn default() -> Self {
Self {
enabled: false,
period: AMD_IBS_DEFAULT_OP_PERIOD,
randomized_period: true,
dcache_miss_filter: false,
max_count: 0xFFFFF,
}
}
}
#[derive(Debug, Clone)]
pub struct X86AMDL3CacheMiss {
pub function: String,
pub source_file: String,
pub source_line: u32,
pub count: u64,
pub address: u64,
pub l3_slice: u8,
pub is_prefetch: bool,
pub ccd_index: u8,
}
#[derive(Debug, Clone)]
pub struct X86AMDIbsSample {
pub ip: u64,
pub function: String,
pub is_load: bool,
pub is_store: bool,
pub is_branch: bool,
pub branch_mispredicted: bool,
pub dcache_miss_latency: u16,
pub dtlb_miss: bool,
pub physical_address: u64,
pub linear_address: u64,
pub num_macro_ops: u8,
pub itlb_miss: bool,
pub icache_miss: bool,
}
#[derive(Debug, Clone)]
pub struct X86AMDPipelineMetric {
pub core_id: u32,
pub timestamp_ms: u64,
pub dispatch_utilization: f64,
pub retire_utilization: f64,
pub int_ops_dispatched: u64,
pub fp_ops_dispatched: u64,
pub branch_ops_dispatched: u64,
pub load_ops_dispatched: u64,
pub store_ops_dispatched: u64,
pub frontend_stall_cycles: u64,
pub backend_stall_cycles: u64,
}
#[derive(Debug)]
pub struct X86SamplingProfiler {
pub sample_freq: u32,
pub unwind_method: X86UnwindMethod,
pub collect_lbr: bool,
pub max_unwind_depth: usize,
pub samples: Vec<X86StackSample>,
pub function_samples: HashMap<String, u64>,
pub collapsed_stacks: HashMap<String, u64>,
pub topdown_results: Option<X86TopDownResult>,
pub topdown_per_function: HashMap<String, X86FunctionTopDownMetrics>,
pub is_active: bool,
}
impl X86SamplingProfiler {
pub fn new() -> Self {
Self {
sample_freq: X86_DEFAULT_SAMPLE_FREQ,
unwind_method: X86UnwindMethod::FramePointer,
collect_lbr: false,
max_unwind_depth: X86_MAX_UNWIND_DEPTH,
samples: Vec::new(),
function_samples: HashMap::new(),
collapsed_stacks: HashMap::new(),
topdown_results: None,
topdown_per_function: HashMap::new(),
is_active: false,
}
}
pub fn set_sample_freq(&mut self, freq: u32) {
self.sample_freq = freq;
}
pub fn set_unwind_method(&mut self, method: X86UnwindMethod) {
self.unwind_method = method;
}
pub fn set_collect_lbr(&mut self, enabled: bool) {
self.collect_lbr = enabled;
}
pub fn collect_samples(
&mut self,
binary: &str,
duration_secs: u64,
) -> Option<X86SamplingResult> {
let expected_samples = (self.sample_freq as u64) * duration_secs;
let _ = binary; let _ = expected_samples;
Some(X86SamplingResult {
total_samples: self.samples.len() as u64,
sample_freq: self.sample_freq,
duration_secs,
binary: binary.to_string(),
function_samples: self.function_samples.clone(),
collapsed_stacks: self.collapsed_stacks.clone(),
topdown_metrics: HashMap::new(),
lbr_data: Vec::new(),
source_location_map: HashMap::new(),
})
}
pub fn record_sample(&mut self, ip: u64, stack: Vec<u64>, function: &str) {
let sample = X86StackSample {
ip,
function: function.to_string(),
call_stack_ips: stack.clone(),
call_stack_symbols: vec![function.to_string()],
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0),
tid: 0,
cpu: 0,
weight: 1,
};
*self
.function_samples
.entry(function.to_string())
.or_insert(0) += 1;
let collapsed = std::iter::once(function.to_string())
.chain(stack.iter().map(|_| "[stack]".to_string()))
.collect::<Vec<_>>()
.join(";");
*self.collapsed_stacks.entry(collapsed).or_insert(0) += 1;
self.samples.push(sample);
}
pub fn reset(&mut self) {
self.samples.clear();
self.function_samples.clear();
self.collapsed_stacks.clear();
self.topdown_results = None;
self.topdown_per_function.clear();
self.is_active = false;
}
pub fn top_functions(&self, n: usize) -> Vec<(&String, &u64)> {
let mut entries: Vec<(&String, &u64)> = self.function_samples.iter().collect();
entries.sort_by(|a, b| b.1.cmp(a.1));
entries.truncate(n);
entries
}
pub fn total_samples(&self) -> u64 {
self.samples.len() as u64
}
pub fn samples_for_function(&self, function: &str) -> Vec<&X86StackSample> {
self.samples
.iter()
.filter(|s| s.function == function)
.collect()
}
pub fn sample_error_margin(&self, function: &str) -> Option<f64> {
let count = *self.function_samples.get(function)? as f64;
if count == 0.0 {
return None;
}
Some(1.0 / count.sqrt() * 100.0)
}
pub fn is_significant(&self, function: &str) -> bool {
self.function_samples
.get(function)
.map(|&c| c >= X86_MIN_SAMPLE_COUNT)
.unwrap_or(false)
}
pub fn unwind_frame_pointer(&self, _current_rbp: u64, _stack_memory: &[u8]) -> Vec<u64> {
let mut stack = Vec::new();
stack
}
pub fn unwind_dwarf(&self, _current_ip: u64, _current_sp: u64, _eh_frame: &[u8]) -> Vec<u64> {
let mut stack = Vec::new();
stack
}
pub fn unwind_lbr(&self, _lbr_entries: &[X86LBREntry]) -> Vec<u64> {
let mut stack = Vec::new();
stack
}
pub fn analyze_topdown(&mut self, sampling_result: &X86SamplingResult) -> X86TopDownResult {
let total_samples = sampling_result.total_samples.max(1) as f64;
let retiring_pct = 40.0; let bad_spec_pct = 10.0;
let frontend_bound_pct = 25.0;
let backend_bound_pct = 25.0;
let result = X86TopDownResult {
retiring_pct,
bad_speculation_pct: bad_spec_pct,
frontend_bound_pct,
backend_bound_pct,
total_pipeline_slots: total_samples as u64,
analysis_timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
};
self.topdown_results = Some(result.clone());
result
}
pub fn set_function_topdown(&mut self, function: &str, metrics: X86FunctionTopDownMetrics) {
self.topdown_per_function
.insert(function.to_string(), metrics);
}
pub fn get_function_topdown(&self, function: &str) -> Option<&X86FunctionTopDownMetrics> {
self.topdown_per_function.get(function)
}
pub fn functions_by_metric(&self, metric: X86TopDownMetric) -> Vec<(&String, f64)> {
let mut entries: Vec<(&String, f64)> = self
.topdown_per_function
.iter()
.filter_map(|(name, m)| {
let value = match metric {
X86TopDownMetric::Retiring => m.retiring_pct,
X86TopDownMetric::BadSpeculation => m.bad_speculation_pct,
X86TopDownMetric::FrontendBound => m.frontend_bound_pct,
X86TopDownMetric::BackendBound => m.backend_bound_pct,
X86TopDownMetric::BackendMemoryBound => m.backend_memory_bound_pct,
X86TopDownMetric::BackendCoreBound => m.backend_core_bound_pct,
};
Some((name, value))
})
.collect();
entries.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
entries
}
pub fn frontend_bound_functions(&self, threshold_pct: f64) -> Vec<&String> {
self.topdown_per_function
.iter()
.filter(|(_, m)| m.frontend_bound_pct > threshold_pct)
.map(|(name, _)| name)
.collect()
}
pub fn backend_bound_functions(&self, threshold_pct: f64) -> Vec<&String> {
self.topdown_per_function
.iter()
.filter(|(_, m)| m.backend_bound_pct > threshold_pct)
.map(|(name, _)| name)
.collect()
}
pub fn bad_speculation_functions(&self, threshold_pct: f64) -> Vec<&String> {
self.topdown_per_function
.iter()
.filter(|(_, m)| m.bad_speculation_pct > threshold_pct)
.map(|(name, _)| name)
.collect()
}
}
impl Default for X86SamplingProfiler {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum X86UnwindMethod {
FramePointer,
DWARF,
LBR,
IntelPT,
Hybrid,
}
#[derive(Debug, Clone)]
pub struct X86StackSample {
pub ip: u64,
pub function: String,
pub call_stack_ips: Vec<u64>,
pub call_stack_symbols: Vec<String>,
pub timestamp: u128,
pub tid: u32,
pub cpu: u32,
pub weight: u64,
}
#[derive(Debug, Clone)]
pub struct X86LBREntry {
pub from_ip: u64,
pub to_ip: u64,
pub predicted: bool,
pub mispredicted: bool,
pub is_call: bool,
pub is_return: bool,
pub cycle_count: u16,
}
#[derive(Debug, Clone)]
pub struct X86SamplingResult {
pub total_samples: u64,
pub sample_freq: u32,
pub duration_secs: u64,
pub binary: String,
pub function_samples: HashMap<String, u64>,
pub collapsed_stacks: HashMap<String, u64>,
pub topdown_metrics: HashMap<String, X86FunctionTopDownMetrics>,
pub lbr_data: Vec<X86LBREntry>,
pub source_location_map: HashMap<u64, String>,
}
#[derive(Debug, Clone)]
pub struct X86TopDownResult {
pub retiring_pct: f64,
pub bad_speculation_pct: f64,
pub frontend_bound_pct: f64,
pub backend_bound_pct: f64,
pub total_pipeline_slots: u64,
pub analysis_timestamp: u64,
}
impl X86TopDownResult {
pub fn is_valid(&self) -> bool {
let total = self.retiring_pct
+ self.bad_speculation_pct
+ self.frontend_bound_pct
+ self.backend_bound_pct;
(total - 100.0).abs() < 1.0
}
pub fn dominant_bottleneck(&self) -> &'static str {
let values = [
(self.retiring_pct, TOPDOWN_RETIRING),
(self.bad_speculation_pct, TOPDOWN_BAD_SPECULATION),
(self.frontend_bound_pct, TOPDOWN_FRONTEND_BOUND),
(self.backend_bound_pct, TOPDOWN_BACKEND_BOUND),
];
values
.iter()
.max_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal))
.map(|(_, name)| *name)
.unwrap_or(TOPDOWN_RETIRING)
}
}
#[derive(Debug, Clone)]
pub struct X86FunctionTopDownMetrics {
pub function: String,
pub retiring_pct: f64,
pub bad_speculation_pct: f64,
pub frontend_bound_pct: f64,
pub backend_bound_pct: f64,
pub backend_memory_bound_pct: f64,
pub backend_core_bound_pct: f64,
pub sample_count: u64,
pub cpi: f64,
}
impl Default for X86FunctionTopDownMetrics {
fn default() -> Self {
Self {
function: String::new(),
retiring_pct: 0.0,
bad_speculation_pct: 0.0,
frontend_bound_pct: 0.0,
backend_bound_pct: 0.0,
backend_memory_bound_pct: 0.0,
backend_core_bound_pct: 0.0,
sample_count: 0,
cpi: 0.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum X86TopDownMetric {
Retiring,
BadSpeculation,
FrontendBound,
BackendBound,
BackendMemoryBound,
BackendCoreBound,
}
#[derive(Debug)]
pub struct X86InstrumentedProfiler {
pub active: bool,
pub total_calls: u64,
pub call_counts: HashMap<String, u64>,
pub total_duration_ns: HashMap<String, u64>,
pub call_durations: Vec<X86FunctionCallRecord>,
pub call_graph: HashMap<String, Vec<(String, u64)>>,
call_stack: Vec<X86ActiveCall>,
pub function_profiles: HashMap<String, X86FunctionProfile>,
pub hot_paths: Vec<X86HotPath>,
}
impl X86InstrumentedProfiler {
pub fn new() -> Self {
Self {
active: false,
total_calls: 0,
call_counts: HashMap::new(),
total_duration_ns: HashMap::new(),
call_durations: Vec::new(),
call_graph: HashMap::new(),
call_stack: Vec::new(),
function_profiles: HashMap::new(),
hot_paths: Vec::new(),
}
}
pub fn start(&mut self) {
self.active = true;
}
pub fn stop(&mut self) {
self.active = false;
}
pub fn function_enter(&mut self, func_name: &str, caller_site: u64) {
if !self.active {
return;
}
self.total_calls += 1;
*self.call_counts.entry(func_name.to_string()).or_insert(0) += 1;
if let Some(parent) = self.call_stack.last() {
self.call_graph
.entry(parent.function.clone())
.or_default()
.push((func_name.to_string(), 1));
}
self.call_stack.push(X86ActiveCall {
function: func_name.to_string(),
entry_time: std::time::Instant::now(),
caller_site,
});
}
pub fn function_exit(&mut self, func_name: &str, _caller_site: u64) {
if !self.active {
return;
}
if let Some(pos) = self
.call_stack
.iter()
.rposition(|c| c.function == func_name)
{
let active = self.call_stack.remove(pos);
let duration = active.entry_time.elapsed().as_nanos() as u64;
*self
.total_duration_ns
.entry(func_name.to_string())
.or_insert(0) += duration;
self.call_durations.push(X86FunctionCallRecord {
function: func_name.to_string(),
duration_ns: duration,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0) as u64,
});
}
}
pub fn current_function(&self) -> Option<&str> {
self.call_stack.last().map(|c| c.function.as_str())
}
pub fn call_depth(&self) -> usize {
self.call_stack.len()
}
pub fn call_count(&self, func_name: &str) -> u64 {
self.call_counts.get(func_name).copied().unwrap_or(0)
}
pub fn most_called_functions(&self, n: usize) -> Vec<(&String, &u64)> {
let mut entries: Vec<(&String, &u64)> = self.call_counts.iter().collect();
entries.sort_by(|a, b| b.1.cmp(a.1));
entries.truncate(n);
entries
}
pub fn distinct_functions(&self) -> usize {
self.call_counts.len()
}
pub fn total_duration(&self, func_name: &str) -> u64 {
self.total_duration_ns.get(func_name).copied().unwrap_or(0)
}
pub fn avg_call_duration(&self, func_name: &str) -> Option<f64> {
let count = self.call_count(func_name);
let total = self.total_duration(func_name);
if count > 0 {
Some(total as f64 / count as f64)
} else {
None
}
}
pub fn min_call_duration(&self, func_name: &str) -> Option<u64> {
self.call_durations
.iter()
.filter(|r| r.function == func_name)
.map(|r| r.duration_ns)
.min()
}
pub fn max_call_duration(&self, func_name: &str) -> Option<u64> {
self.call_durations
.iter()
.filter(|r| r.function == func_name)
.map(|r| r.duration_ns)
.max()
}
pub fn call_duration_stddev(&self, func_name: &str) -> Option<f64> {
let durations: Vec<f64> = self
.call_durations
.iter()
.filter(|r| r.function == func_name)
.map(|r| r.duration_ns as f64)
.collect();
if durations.len() < 2 {
return None;
}
let mean = durations.iter().sum::<f64>() / durations.len() as f64;
let variance = durations
.iter()
.map(|d| (d - mean) * (d - mean))
.sum::<f64>()
/ (durations.len() - 1) as f64;
Some(variance.sqrt())
}
pub fn hottest_functions_by_time(&self, n: usize) -> Vec<(&String, &u64)> {
let mut entries: Vec<(&String, &u64)> = self.total_duration_ns.iter().collect();
entries.sort_by(|a, b| b.1.cmp(a.1));
entries.truncate(n);
entries
}
pub fn slowest_functions(&self, n: usize) -> Vec<(String, f64)> {
let mut entries: Vec<(String, f64)> = self
.total_duration_ns
.keys()
.filter_map(|name| self.avg_call_duration(name).map(|avg| (name.clone(), avg)))
.collect();
entries.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
entries.truncate(n);
entries
}
pub fn build_call_graph(&mut self) -> &HashMap<String, Vec<(String, u64)>> {
let mut aggregated: HashMap<String, Vec<(String, u64)>> = HashMap::new();
for (caller, callees) in &self.call_graph {
let mut counts: HashMap<String, u64> = HashMap::new();
for (callee, _) in callees {
*counts.entry(callee.clone()).or_insert(0) += 1;
}
let mut deduped: Vec<(String, u64)> = counts.into_iter().collect();
deduped.sort_by(|a, b| b.1.cmp(&a.1));
aggregated.insert(caller.clone(), deduped);
}
self.call_graph = aggregated;
&self.call_graph
}
pub fn callees_of(&self, caller: &str) -> Vec<&(String, u64)> {
self.call_graph
.get(caller)
.map(|callees| callees.iter().collect())
.unwrap_or_default()
}
pub fn callers_of(&self, callee: &str) -> Vec<(String, u64)> {
let mut callers: Vec<(String, u64)> = Vec::new();
for (caller, callees) in &self.call_graph {
for (c, count) in callees {
if c == callee {
callers.push((caller.clone(), *count));
}
}
}
callers.sort_by(|a, b| b.1.cmp(&a.1));
callers
}
pub fn export_dot(&self) -> String {
let mut dot = String::from("digraph CallGraph {\n");
dot.push_str(" rankdir=LR;\n");
dot.push_str(" node [shape=box, style=filled, fillcolor=lightyellow];\n\n");
for (caller, callees) in &self.call_graph {
let caller_label = format!(
"{} ({})",
caller,
self.call_counts.get(caller).unwrap_or(&0)
);
dot.push_str(&format!(" \"{}\" [label=\"{}\"];\n", caller, caller_label));
for (callee, count) in callees {
dot.push_str(&format!(
" \"{}\" -> \"{}\" [label=\"{}\"];\n",
caller, callee, count
));
}
}
dot.push_str("}\n");
dot
}
pub fn identify_hot_paths(&mut self, n: usize) -> Vec<X86HotPath> {
self.build_call_graph();
let mut paths: Vec<X86HotPath> = Vec::new();
for (func, duration) in &self.total_duration_ns {
paths.push(X86HotPath {
functions: vec![func.clone()],
total_duration_ns: *duration,
call_count: *self.call_counts.get(func).unwrap_or(&0),
percentage: 0.0, is_recursive: false,
});
}
let total_time: u64 = self.total_duration_ns.values().sum();
if total_time > 0 {
for path in &mut paths {
path.percentage = (path.total_duration_ns as f64 / total_time as f64) * 100.0;
}
}
paths.sort_by(|a, b| b.total_duration_ns.cmp(&a.total_duration_ns));
paths.truncate(n);
self.hot_paths = paths.clone();
paths
}
pub fn critical_path(&self) -> Vec<String> {
let mut funcs: Vec<(&String, &u64)> = self.total_duration_ns.iter().collect();
funcs.sort_by(|a, b| b.1.cmp(a.1));
let cutoff = (funcs.len() as f64 * 0.05).max(1.0) as usize;
funcs
.iter()
.take(cutoff)
.map(|(name, _)| (*name).clone())
.collect()
}
pub fn build_function_profiles(&mut self) {
self.function_profiles.clear();
for (name, count) in &self.call_counts {
let total_ns = self.total_duration_ns.get(name).copied().unwrap_or(0);
let avg = if *count > 0 {
Some(total_ns as f64 / *count as f64)
} else {
None
};
self.function_profiles.insert(
name.clone(),
X86FunctionProfile {
name: name.clone(),
call_count: *count,
total_duration_ns: total_ns,
self_duration_ns: self.self_duration(name),
avg_call_duration_ns: avg,
max_call_duration_ns: self.max_call_duration(name),
min_call_duration_ns: self.min_call_duration(name),
stddev_call_duration_ns: self.call_duration_stddev(name),
num_callers: self.callers_of(name).len() as u64,
num_callees: self.callees_of(name).len() as u64,
},
);
}
}
fn self_duration(&self, func_name: &str) -> u64 {
let total = self.total_duration_ns.get(func_name).copied().unwrap_or(0);
let callee_time: u64 = self
.call_graph
.get(func_name)
.map(|callees| {
callees
.iter()
.map(|(c, _)| self.total_duration_ns.get(c).copied().unwrap_or(0))
.sum()
})
.unwrap_or(0);
total.saturating_sub(callee_time)
}
pub fn get_function_profile(&self, func_name: &str) -> Option<&X86FunctionProfile> {
self.function_profiles.get(func_name)
}
pub fn get_profile(&self) -> Option<X86InstrumentedProfile> {
if self.call_counts.is_empty() {
return None;
}
Some(X86InstrumentedProfile {
total_calls: self.total_calls,
distinct_functions: self.distinct_functions(),
call_counts: self.call_counts.clone(),
durations_ns: self.total_duration_ns.clone(),
call_graph: self.call_graph.clone(),
function_profiles: self.function_profiles.clone(),
hot_paths: self.hot_paths.clone(),
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
})
}
pub fn reset(&mut self) {
self.call_counts.clear();
self.total_duration_ns.clear();
self.call_durations.clear();
self.call_graph.clear();
self.call_stack.clear();
self.function_profiles.clear();
self.hot_paths.clear();
self.total_calls = 0;
}
}
impl Default for X86InstrumentedProfiler {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
struct X86ActiveCall {
function: String,
entry_time: std::time::Instant,
caller_site: u64,
}
#[derive(Debug, Clone)]
pub struct X86FunctionCallRecord {
pub function: String,
pub duration_ns: u64,
pub timestamp: u64,
}
#[derive(Debug, Clone)]
pub struct X86FunctionProfile {
pub name: String,
pub call_count: u64,
pub total_duration_ns: u64,
pub self_duration_ns: u64,
pub avg_call_duration_ns: Option<f64>,
pub max_call_duration_ns: Option<u64>,
pub min_call_duration_ns: Option<u64>,
pub stddev_call_duration_ns: Option<f64>,
pub num_callers: u64,
pub num_callees: u64,
}
#[derive(Debug, Clone)]
pub struct X86HotPath {
pub functions: Vec<String>,
pub total_duration_ns: u64,
pub call_count: u64,
pub percentage: f64,
pub is_recursive: bool,
}
#[derive(Debug, Clone)]
pub struct X86InstrumentedProfile {
pub total_calls: u64,
pub distinct_functions: usize,
pub call_counts: HashMap<String, u64>,
pub durations_ns: HashMap<String, u64>,
pub call_graph: HashMap<String, Vec<(String, u64)>>,
pub function_profiles: HashMap<String, X86FunctionProfile>,
pub hot_paths: Vec<X86HotPath>,
pub timestamp: u64,
}
#[derive(Debug)]
pub struct X86ProfileVisualizer {
pub flamegraph_width: u32,
pub flamegraph_frame_height: u32,
pub palette: X86FlamegraphPalette,
pub default_title: String,
pub include_minimap: bool,
pub font_family: String,
}
impl X86ProfileVisualizer {
pub fn new() -> Self {
Self {
flamegraph_width: X86_FLAMEGRAPH_DEFAULT_WIDTH,
flamegraph_frame_height: X86_FLAMEGRAPH_FRAME_HEIGHT,
palette: X86FlamegraphPalette::default(),
default_title: String::from("Flame Graph"),
include_minimap: false,
font_family: String::from("monospace"),
}
}
pub fn generate_flamegraph_svg(
&self,
collapsed_stacks: &HashMap<String, u64>,
title: &str,
) -> Option<String> {
if collapsed_stacks.is_empty() {
return None;
}
let total_samples: u64 = collapsed_stacks.values().sum();
if total_samples == 0 {
return None;
}
let width = self.flamegraph_width as f64;
let frame_height = self.flamegraph_frame_height as f64;
let mut root = FlameNode::new("root");
for (stack_str, &count) in collapsed_stacks {
let frames: Vec<&str> = stack_str.split(';').collect();
let mut current = &mut root;
for frame in frames {
let frame = frame.trim();
if frame.is_empty() {
continue;
}
current = current.get_or_create_child(frame);
}
current.value += count;
}
for child in &mut root.children {
child.compute_width(total_samples, width);
}
let svg_height = frame_height * 20.0 + 20.0; let mut svg = String::new();
svg.push_str(&format!(
r#"<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" width="{}" height="{}" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:{};stop-opacity:1" />
<stop offset="100%" style="stop-color:{};stop-opacity:1" />
</linearGradient>
</defs>
<style>
text {{ font-family: {}; font-size: 12px; fill: rgb(0,0,0); }}
.title {{ font-size: 17px; font-weight: bold; }}
</style>
<rect width="100%" height="100%" fill="white" />
<text x="10" y="18" class="title">{}</text>
"#,
width,
svg_height,
self.palette.hot_gradient_start(),
self.palette.hot_gradient_end(),
self.font_family,
title
));
let mut y_offset = 22.0;
let mut current_level: Vec<&FlameNode> = root.children.iter().collect();
let max_depth = 20;
for _depth in 0..max_depth {
if current_level.is_empty() {
break;
}
let mut x_pos = 0.0;
let mut next_level: Vec<&FlameNode> = Vec::new();
for node in ¤t_level {
let node_width = node.width;
if node_width < 1.0 {
continue;
}
let color = self
.palette
.color_for_function(&node.name, node.value, total_samples);
let label = if node_width > 30.0 {
format!(
"{} ({:.1}%)",
node.name,
(node.value as f64 / total_samples as f64) * 100.0
)
} else if node_width > 15.0 {
node.name.clone()
} else {
String::new()
};
svg.push_str(&format!(
r#"<rect x="{}" y="{}" width="{}" height="{}" fill="{}" stroke="white" stroke-width="0.5" />"#,
x_pos, y_offset, node_width, frame_height, color
));
if !label.is_empty() {
let text_x = x_pos + 3.0;
let text_y = y_offset + frame_height - 4.0;
svg.push_str(&format!(
r#"<text x="{}" y="{}" clip-path="url(#clip)">{}</text>"#,
text_x, text_y, label
));
}
for child in &node.children {
next_level.push(child);
}
x_pos += node_width;
}
y_offset += frame_height;
current_level = next_level;
}
let legend_y = svg_height - 20.0;
svg.push_str(&format!(
r#"<text x="10" y="{}" font-size="10">Total samples: {} | Functions: {}</text>"#,
legend_y,
total_samples,
collapsed_stacks.len()
));
svg.push_str("\n</svg>");
Some(svg)
}
pub fn flamegraph_to_collapsed(&self, _svg: &str) -> Option<HashMap<String, u64>> {
None
}
pub fn generate_call_graph_dot(
&self,
call_graph: &HashMap<String, Vec<(String, u64)>>,
call_counts: &HashMap<String, u64>,
title: &str,
) -> String {
let mut dot = format!("digraph CallGraph {{\n");
dot.push_str(&format!(" label=\"{}\";\n", title));
dot.push_str(" labelloc=t;\n");
dot.push_str(" fontsize=20;\n");
dot.push_str(" rankdir=LR;\n");
dot.push_str(" node [shape=box, style=filled, fontname=\"monospace\"];\n");
dot.push_str(" edge [fontname=\"monospace\", fontsize=10];\n\n");
let max_count: u64 = call_counts.values().copied().max().unwrap_or(1);
let max_count_f = max_count as f64;
let mut max_calls: u64 = 1;
for callees_vec in call_graph.values() {
for pair in callees_vec.iter() {
let c = pair.1;
if c > max_calls {
max_calls = c;
}
}
}
for (func, count_ref) in call_counts {
let count: u64 = *count_ref;
let intensity = if max_count_f > 0.0 {
(count as f64 / max_count_f) * 255.0
} else {
128.0
};
let red = 255.0 - intensity * 0.7;
let green = 255.0 - intensity * 0.3;
let blue = 255.0 - intensity;
let color = format!("#{:02X}{:02X}{:02X}", red as u8, green as u8, blue as u8);
dot.push_str(&format!(
" \"{}\" [label=\"{}\\n({} calls)\", fillcolor=\"{}\"];\n",
func, func, count, color
));
}
dot.push('\n');
for (caller, callees_vec) in call_graph {
for pair in callees_vec.iter() {
let callee: &String = &pair.0;
let count: u64 = pair.1;
let penwidth = 1.0 + (count as f64 / max_calls as f64) * 4.0;
dot.push_str(&format!(
" \"{}\" -> \"{}\" [label=\"{}\" penwidth={:.1}];\n",
caller, callee, count, penwidth
));
}
}
dot.push_str("}\n");
dot
}
pub fn generate_chrome_tracing_json(&self, profile: &X86InstrumentedProfile) -> Option<String> {
if profile.call_counts.is_empty() {
return None;
}
let mut events = Vec::new();
let mut timestamp_us = 0u64;
for (func_name, func_profile) in &profile.function_profiles {
let duration_us = if func_profile.call_count > 0 {
func_profile.total_duration_ns / 1000 / func_profile.call_count
} else {
100 };
events.push(ChromeTraceEvent {
name: func_name.clone(),
cat: "function".to_string(),
ph: "X".to_string(),
ts: timestamp_us,
dur: duration_us,
pid: 0,
tid: 1,
args: {
let mut args = HashMap::new();
args.insert("calls".to_string(), func_profile.call_count.to_string());
args.insert(
"total_ns".to_string(),
func_profile.total_duration_ns.to_string(),
);
args
},
});
timestamp_us += duration_us + 1;
}
let mut json = String::from("[\n");
for (i, event) in events.iter().enumerate() {
if i > 0 {
json.push_str(",\n");
}
json.push_str(" {\n");
json.push_str(&format!(" \"name\": \"{}\",\n", event.name));
json.push_str(&format!(" \"cat\": \"{}\",\n", event.cat));
json.push_str(&format!(" \"ph\": \"{}\",\n", event.ph));
json.push_str(&format!(" \"ts\": {},\n", event.ts));
json.push_str(&format!(" \"dur\": {},\n", event.dur));
json.push_str(&format!(" \"pid\": {},\n", event.pid));
json.push_str(&format!(" \"tid\": {},\n", event.tid));
json.push_str(" \"args\": {\n");
let arg_count = event.args.len();
for (j, (k, v)) in event.args.iter().enumerate() {
let comma = if j < arg_count - 1 { "," } else { "" };
json.push_str(&format!(" \"{}\": \"{}\"{}\n", k, v, comma));
}
json.push_str(" }\n");
json.push_str(" }");
}
json.push_str("\n]\n");
Some(json)
}
pub fn generate_sampling_trace_json(&self, sampling: &X86SamplingResult) -> Option<String> {
if sampling.function_samples.is_empty() {
return None;
}
let mut events = Vec::new();
let mut ts = 0u64;
let total = sampling.total_samples.max(1);
for (func, &count) in &sampling.function_samples {
let dur = (count as f64 / total as f64 * 1_000_000.0) as u64; events.push(ChromeTraceEvent {
name: func.clone(),
cat: "sampling".to_string(),
ph: "X".to_string(),
ts,
dur: dur.max(1),
pid: 0,
tid: 1,
args: {
let mut args = HashMap::new();
args.insert("samples".to_string(), count.to_string());
args.insert(
"pct".to_string(),
format!("{:.2}", count as f64 / total as f64 * 100.0),
);
args
},
});
ts += dur + 1;
}
let mut json = String::from("[\n");
for (i, event) in events.iter().enumerate() {
if i > 0 {
json.push_str(",\n");
}
json.push_str(" {\n");
json.push_str(&format!(" \"name\": \"{}\",\n", event.name));
json.push_str(&format!(" \"cat\": \"{}\",\n", event.cat));
json.push_str(&format!(" \"ph\": \"{}\",\n", event.ph));
json.push_str(&format!(" \"ts\": {},\n", event.ts));
json.push_str(&format!(" \"dur\": {},\n", event.dur));
json.push_str(&format!(" \"pid\": {},\n", event.pid));
json.push_str(&format!(" \"tid\": {},\n", event.tid));
json.push_str(" \"args\": {\n");
let arg_count = event.args.len();
for (j, (k, v)) in event.args.iter().enumerate() {
let comma = if j < arg_count - 1 { "," } else { "" };
json.push_str(&format!(" \"{}\": \"{}\"{}\n", k, v, comma));
}
json.push_str(" }\n");
json.push_str(" }");
}
json.push_str("\n]\n");
Some(json)
}
pub fn generate_hotspot_table(
&self,
function_samples: &HashMap<String, u64>,
title: &str,
) -> String {
let total: u64 = function_samples.values().sum();
let mut entries: Vec<(&String, &u64)> = function_samples.iter().collect();
entries.sort_by(|a, b| b.1.cmp(a.1));
entries.truncate(X86_MAX_HOTSPOT_FUNCTIONS);
let mut html = String::new();
html.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
html.push_str(&format!("<title>{}</title>\n", title));
html.push_str("<style>\n");
html.push_str(
"body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 20px; }\n",
);
html.push_str("table { border-collapse: collapse; width: 100%; }\n");
html.push_str(
"th { background: #4CAF50; color: white; padding: 10px; text-align: left; }\n",
);
html.push_str("td { padding: 8px; border-bottom: 1px solid #ddd; }\n");
html.push_str("tr:hover { background: #f5f5f5; }\n");
html.push_str(".bar { background: #2196F3; height: 20px; border-radius: 3px; }\n");
html.push_str("</style>\n</head>\n<body>\n");
html.push_str(&format!("<h1>{}</h1>\n", title));
html.push_str(&format!(
"<p>Total samples: {} | Functions shown: {}</p>\n",
total,
entries.len()
));
html.push_str("<table>\n");
html.push_str(
"<tr><th>#</th><th>Function</th><th>Samples</th><th>%</th><th>Graph</th></tr>\n",
);
let max_count = entries.first().map(|&(_, &c)| c).unwrap_or(1) as f64;
for (i, &(ref func, &count)) in entries.iter().enumerate() {
let pct = if total > 0 {
(count as f64 / total as f64) * 100.0
} else {
0.0
};
let bar_width = if max_count > 0.0 {
(count as f64 / max_count * 200.0) as u32
} else {
0
};
html.push_str("<tr>");
html.push_str(&format!("<td>{}</td>", i + 1));
html.push_str(&format!("<td><code>{}</code></td>", func));
html.push_str(&format!("<td>{}</td>", count));
html.push_str(&format!("<td>{:.2}%</td>", pct));
html.push_str(&format!(
"<td><div class=\"bar\" style=\"width:{}px;\"></div></td>",
bar_width
));
html.push_str("</tr>\n");
}
html.push_str("</table>\n</body>\n</html>\n");
html
}
pub fn generate_function_bar_chart(
&self,
data: &HashMap<String, u64>,
title: &str,
max_items: usize,
) -> Option<String> {
if data.is_empty() {
return None;
}
let mut entries: Vec<(&String, &u64)> = data.iter().collect();
entries.sort_by(|a, b| b.1.cmp(a.1));
entries.truncate(max_items);
let max_value = entries.first().map(|&(_, &c)| c).unwrap_or(1) as f64;
let chart_width = 800.0;
let bar_height = 25.0;
let label_width = 250.0;
let margin_left = 10.0;
let margin_top = 40.0;
let height = margin_top + entries.len() as f64 * (bar_height + 5.0) + 20.0;
let total: u64 = data.values().sum();
let mut svg = format!(
r##"<?xml version="1.0" encoding="UTF-8"?>
<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="white" />
<text x="10" y="25" font-family="sans-serif" font-size="16" font-weight="bold">{}</text>
<text x="10" y="40" font-family="sans-serif" font-size="11" fill="#666">Total: {}</text>
"##,
chart_width + label_width + margin_left,
height,
title,
total
);
for (i, &(ref name, &count)) in entries.iter().enumerate() {
let y = margin_top + i as f64 * (bar_height + 5.0);
let bar_width = if max_value > 0.0 {
(count as f64 / max_value) * chart_width
} else {
0.0
};
let pct = if total > 0 {
(count as f64 / total as f64) * 100.0
} else {
0.0
};
let hue = 200.0 + (i as f64 * 25.0) % 160.0;
let color = format!("hsl({}, 70%, 55%)", hue);
svg.push_str(&format!(
r#"<text x="{}" y="{}" font-family="monospace" font-size="11" text-anchor="end">{}</text>"#,
label_width + margin_left - 5.0,
y + bar_height * 0.65,
name
));
svg.push_str(&format!(
r#"<rect x="{}" y="{}" width="{}" height="{}" fill="{}" rx="2" />"#,
label_width + margin_left + 5.0,
y,
bar_width,
bar_height,
color
));
svg.push_str(&format!(
r##"<text x="{}" y="{}" font-family="sans-serif" font-size="10" fill="#333">{} ({:.1}%)</text>"##,
label_width + margin_left + bar_width + 10.0,
y + bar_height * 0.65,
count,
pct
));
}
svg.push_str("\n</svg>");
Some(svg)
}
pub fn generate_pie_chart_svg(
&self,
data: &HashMap<String, u64>,
title: &str,
max_slices: usize,
) -> Option<String> {
if data.is_empty() {
return None;
}
let mut entries: Vec<(&String, &u64)> = data.iter().collect();
entries.sort_by(|a, b| b.1.cmp(a.1));
entries.truncate(max_slices);
let total: u64 = entries.iter().map(|&(_, &c)| c).sum();
let cx = 200.0;
let cy = 200.0;
let radius = 180.0;
let svg_size = 450.0;
let mut svg = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="white" />
<text x="10" y="25" font-family="sans-serif" font-size="16" font-weight="bold">{}</text>
"#,
svg_size,
svg_size + 50.0,
title
);
let mut start_angle: f64 = -90.0;
for (i, &(ref name, &count)) in entries.iter().enumerate() {
let slice_angle = if total > 0 {
(count as f64 / total as f64) * 360.0
} else {
0.0
};
let end_angle = start_angle + slice_angle;
let (x1, y1) = Self::polar_to_cartesian(cx, cy, radius, end_angle);
let (x2, y2) = Self::polar_to_cartesian(cx, cy, radius, start_angle);
let large_arc = if slice_angle > 180.0 { 1 } else { 0 };
let path = format!(
"M {},{} L {},{} A {},{} 0 {},1 {},{} Z",
cx, cy, x2, y2, radius, radius, large_arc, x1, y1
);
let hue = (i as f64 * 40.0 + 10.0) % 360.0;
let color = format!("hsl({}, 65%, 55%)", hue);
svg.push_str(&format!(
r#"<path d="{}" fill="{}" stroke="white" stroke-width="1" />"#,
path, color
));
let mid_angle = (start_angle + end_angle) / 2.0;
let (lx, ly) = Self::polar_to_cartesian(cx, cy, radius * 0.65, mid_angle);
let pct = if total > 0 {
(count as f64 / total as f64) * 100.0
} else {
0.0
};
if pct > 3.0 {
svg.push_str(&format!(
r#"<text x="{}" y="{}" font-family="sans-serif" font-size="10" text-anchor="middle" fill="white">{:.1}%</text>"#,
lx, ly, pct
));
}
start_angle = end_angle;
}
let mut legend_y = cy * 2.0 + 30.0;
for (i, &(ref name, &count)) in entries.iter().enumerate() {
let hue = (i as f64 * 40.0 + 10.0) % 360.0;
let color = format!("hsl({}, 65%, 55%)", hue);
let pct = if total > 0 {
(count as f64 / total as f64) * 100.0
} else {
0.0
};
svg.push_str(&format!(
r#"<rect x="15" y="{}" width="12" height="12" fill="{}" rx="2" />"#,
legend_y - 9.0,
color
));
svg.push_str(&format!(
r##"<text x="32" y="{}" font-family="monospace" font-size="9" fill="#333">{} ({:.1}%)</text>"##,
legend_y, name, pct
));
legend_y += 15.0;
if legend_y > svg_size + 200.0 {
break;
}
}
svg.push_str("\n</svg>");
Some(svg)
}
fn polar_to_cartesian(cx: f64, cy: f64, r: f64, angle_deg: f64) -> (f64, f64) {
let angle_rad = angle_deg * std::f64::consts::PI / 180.0;
(cx + r * angle_rad.cos(), cy + r * angle_rad.sin())
}
}
impl Default for X86ProfileVisualizer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct X86FlamegraphPalette {
pub hot: String,
pub warm: String,
pub cold: String,
pub background: String,
}
impl X86FlamegraphPalette {
pub fn hot_palette() -> Self {
Self {
hot: "#FF0000".to_string(),
warm: "#FFA500".to_string(),
cold: "#FFFF00".to_string(),
background: "#FFFFFF".to_string(),
}
}
pub fn cool_palette() -> Self {
Self {
hot: "#00FF00".to_string(),
warm: "#00AAAA".to_string(),
cold: "#0000FF".to_string(),
background: "#FFFFFF".to_string(),
}
}
pub fn mono_palette() -> Self {
Self {
hot: "#333333".to_string(),
warm: "#999999".to_string(),
cold: "#CCCCCC".to_string(),
background: "#FFFFFF".to_string(),
}
}
pub fn hot_gradient_start(&self) -> &str {
&self.hot
}
pub fn hot_gradient_end(&self) -> &str {
&self.cold
}
pub fn color_for_function(&self, name: &str, value: u64, total: u64) -> String {
let ratio = if total > 0 {
value as f64 / total as f64
} else {
0.0
};
let hash = name
.bytes()
.fold(0u64, |h, b| h.wrapping_mul(31).wrapping_add(b as u64));
let hue = (hash % 360) as f64;
let lightness = 60.0 - ratio * 40.0; format!("hsl({}, 70%, {}%)", hue as u32, lightness as u32)
}
}
impl Default for X86FlamegraphPalette {
fn default() -> Self {
Self::hot_palette()
}
}
#[derive(Debug, Clone)]
struct FlameNode {
name: String,
value: u64,
width: f64,
children: Vec<FlameNode>,
}
impl FlameNode {
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
value: 0,
width: 0.0,
children: Vec::new(),
}
}
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().unwrap()
}
}
fn compute_width(&mut self, total: u64, parent_width: f64) {
self.width = if total > 0 {
(self.value as f64 / total as f64) * parent_width
} else {
0.0
};
for child in &mut self.children {
child.compute_width(total, self.width);
}
}
}
#[derive(Debug, Clone)]
struct ChromeTraceEvent {
name: String,
cat: String,
ph: String,
ts: u64,
dur: u64,
pid: u32,
tid: u32,
args: HashMap<String, String>,
}
#[derive(Debug)]
pub struct X86ProfileComparison {
pub baseline: Option<X86ProfileData>,
pub target: Option<X86ProfileData>,
pub comparison: Option<X86ProfileDiffReport>,
pub regression_threshold_pct: f64,
pub improvement_threshold_pct: f64,
pub min_sample_count: u64,
}
impl X86ProfileComparison {
pub fn new() -> Self {
Self {
baseline: None,
target: None,
comparison: None,
regression_threshold_pct: 5.0, improvement_threshold_pct: 5.0, min_sample_count: X86_MIN_SAMPLE_COUNT,
}
}
pub fn set_regression_threshold(&mut self, pct: f64) {
self.regression_threshold_pct = pct;
}
pub fn set_improvement_threshold(&mut self, pct: f64) {
self.improvement_threshold_pct = pct;
}
pub fn compare(
&mut self,
baseline: &X86ProfileData,
target: &X86ProfileData,
) -> X86ProfileDiffReport {
self.baseline = Some(baseline.clone());
self.target = Some(target.clone());
let all_functions: HashSet<&String> = baseline
.function_samples
.keys()
.chain(target.function_samples.keys())
.collect();
let mut function_diffs: Vec<X86FunctionDiff> = Vec::new();
let mut regressions: Vec<X86Regression> = Vec::new();
let mut improvements: Vec<X86Improvement> = Vec::new();
let mut new_functions: Vec<String> = Vec::new();
let mut removed_functions: Vec<String> = Vec::new();
let total_baseline: u64 = baseline.function_samples.values().sum();
let total_target: u64 = target.function_samples.values().sum();
for func in &all_functions {
let baseline_count = baseline.function_samples.get(*func).copied().unwrap_or(0);
let target_count = target.function_samples.get(*func).copied().unwrap_or(0);
if baseline_count == 0 && target_count > 0 {
new_functions.push(func.to_string());
continue;
}
if target_count == 0 && baseline_count > 0 {
removed_functions.push(func.to_string());
continue;
}
let baseline_pct = if total_baseline > 0 {
(baseline_count as f64 / total_baseline as f64) * 100.0
} else {
0.0
};
let target_pct = if total_target > 0 {
(target_count as f64 / total_target as f64) * 100.0
} else {
0.0
};
let abs_diff = target_count as i64 - baseline_count as i64;
let rel_diff_pct = if baseline_count > 0 {
((target_count as f64 - baseline_count as f64) / baseline_count as f64) * 100.0
} else {
f64::INFINITY
};
let baseline_dur = baseline
.function_durations_ns
.get(*func)
.copied()
.unwrap_or(0);
let target_dur = target
.function_durations_ns
.get(*func)
.copied()
.unwrap_or(0);
let duration_diff_pct = if baseline_dur > 0 {
((target_dur as f64 - baseline_dur as f64) / baseline_dur as f64) * 100.0
} else if target_dur > 0 {
f64::INFINITY
} else {
0.0
};
let diff = X86FunctionDiff {
function: func.to_string(),
baseline_samples: baseline_count,
target_samples: target_count,
absolute_diff: abs_diff,
relative_diff_pct: rel_diff_pct,
baseline_pct,
target_pct,
baseline_duration_ns: baseline_dur,
target_duration_ns: target_dur,
duration_diff_pct,
};
if rel_diff_pct > self.regression_threshold_pct
&& baseline_count >= self.min_sample_count
{
regressions.push(X86Regression {
function: func.to_string(),
baseline_samples: baseline_count,
target_samples: target_count,
increase_pct: rel_diff_pct,
severity: self.classify_severity(rel_diff_pct),
});
}
if rel_diff_pct < -self.improvement_threshold_pct
&& baseline_count >= self.min_sample_count
{
improvements.push(X86Improvement {
function: func.to_string(),
baseline_samples: baseline_count,
target_samples: target_count,
decrease_pct: -rel_diff_pct,
significance: if baseline_count >= 100 {
X86Significance::High
} else {
X86Significance::Moderate
},
});
}
function_diffs.push(diff);
}
function_diffs.sort_by(|a, b| b.absolute_diff.abs().cmp(&a.absolute_diff.abs()));
regressions.sort_by(|a, b| b.severity.cmp(&a.severity));
improvements.sort_by(|a, b| {
b.decrease_pct
.partial_cmp(&a.decrease_pct)
.unwrap_or(std::cmp::Ordering::Equal)
});
let overall_diff = X86OverallDiff {
total_samples_baseline: total_baseline,
total_samples_target: total_target,
sample_change_pct: if total_baseline > 0 {
((total_target as f64 - total_baseline as f64) / total_baseline as f64) * 100.0
} else {
0.0
},
functions_baseline: baseline.function_samples.len() as u64,
functions_target: target.function_samples.len() as u64,
new_functions: new_functions.len() as u64,
removed_functions: removed_functions.len() as u64,
num_regressions: regressions.len() as u64,
num_improvements: improvements.len() as u64,
baseline_binary: baseline.binary.clone(),
target_binary: target.binary.clone(),
};
let report = X86ProfileDiffReport {
overall: overall_diff,
function_diffs,
regressions,
improvements,
new_functions,
removed_functions,
collapsed_stack_diff: self.compare_collapsed_stacks(baseline, target),
topdown_diff: self.compare_topdown(baseline, target),
};
self.comparison = Some(report.clone());
report
}
fn compare_collapsed_stacks(
&self,
baseline: &X86ProfileData,
target: &X86ProfileData,
) -> Vec<X86StackDiff> {
let all_stacks: HashSet<&String> = baseline
.collapsed_stacks
.keys()
.chain(target.collapsed_stacks.keys())
.collect();
let mut diffs: Vec<X86StackDiff> = all_stacks
.iter()
.map(|stack| {
let base_count = baseline.collapsed_stacks.get(*stack).copied().unwrap_or(0);
let tgt_count = target.collapsed_stacks.get(*stack).copied().unwrap_or(0);
X86StackDiff {
stack: stack.to_string(),
baseline_count: base_count,
target_count: tgt_count,
diff: tgt_count as i64 - base_count as i64,
}
})
.collect();
diffs.sort_by(|a, b| b.diff.abs().cmp(&a.diff.abs()));
diffs
}
fn compare_topdown(
&self,
baseline: &X86ProfileData,
target: &X86ProfileData,
) -> Option<X86TopDownDiff> {
let b_metrics = &baseline.topdown_metrics;
let t_metrics = &target.topdown_metrics;
if b_metrics.is_empty() && t_metrics.is_empty() {
return None;
}
let all_funcs: HashSet<&String> = b_metrics.keys().chain(t_metrics.keys()).collect();
let mut func_diffs: Vec<X86FunctionTopDownDiff> = Vec::new();
for func in &all_funcs {
let bm = b_metrics.get(*func);
let tm = t_metrics.get(*func);
func_diffs.push(X86FunctionTopDownDiff {
function: func.to_string(),
retiring_diff: tm.map(|m| m.retiring_pct).unwrap_or(0.0)
- bm.map(|m| m.retiring_pct).unwrap_or(0.0),
bad_spec_diff: tm.map(|m| m.bad_speculation_pct).unwrap_or(0.0)
- bm.map(|m| m.bad_speculation_pct).unwrap_or(0.0),
frontend_bound_diff: tm.map(|m| m.frontend_bound_pct).unwrap_or(0.0)
- bm.map(|m| m.frontend_bound_pct).unwrap_or(0.0),
backend_bound_diff: tm.map(|m| m.backend_bound_pct).unwrap_or(0.0)
- bm.map(|m| m.backend_bound_pct).unwrap_or(0.0),
});
}
Some(X86TopDownDiff {
function_diffs: func_diffs,
})
}
fn classify_severity(&self, increase_pct: f64) -> X86RegressionSeverity {
if increase_pct > 50.0 {
X86RegressionSeverity::Critical
} else if increase_pct > 25.0 {
X86RegressionSeverity::Major
} else if increase_pct > 10.0 {
X86RegressionSeverity::Moderate
} else {
X86RegressionSeverity::Minor
}
}
pub fn generate_diff_flamegraph(
&self,
baseline: &HashMap<String, u64>,
target: &HashMap<String, u64>,
visualizer: &X86ProfileVisualizer,
) -> Option<String> {
let mut diff_stacks: HashMap<String, u64> = HashMap::new();
let all_stacks: HashSet<&String> = baseline.keys().chain(target.keys()).collect();
for stack in &all_stacks {
let b = baseline.get(*stack).copied().unwrap_or(0);
let t = target.get(*stack).copied().unwrap_or(0);
let diff = if t > b {
t - b } else if b > t {
b - t } else {
continue;
};
let direction = if t > b { "+" } else { "-" };
let labeled_stack = format!("{}:{}", direction, stack);
diff_stacks.insert(labeled_stack, diff);
}
if diff_stacks.is_empty() {
return None;
}
visualizer.generate_flamegraph_svg(&diff_stacks, "Difference Flame Graph")
}
pub fn severe_regressions(&self) -> Vec<&X86Regression> {
self.comparison
.as_ref()
.map(|c| {
c.regressions
.iter()
.filter(|r| {
r.severity == X86RegressionSeverity::Critical
|| r.severity == X86RegressionSeverity::Major
})
.collect()
})
.unwrap_or_default()
}
pub fn regression_score(&self) -> f64 {
self.comparison
.as_ref()
.map(|c| {
c.regressions
.iter()
.map(|r| match r.severity {
X86RegressionSeverity::Critical => 10.0,
X86RegressionSeverity::Major => 5.0,
X86RegressionSeverity::Moderate => 2.0,
X86RegressionSeverity::Minor => 0.5,
})
.sum()
})
.unwrap_or(0.0)
}
pub fn generate_summary_report(&self) -> String {
let comparison = match &self.comparison {
Some(c) => c,
None => return "No comparison data available.".to_string(),
};
let mut report = String::new();
report.push_str("╔══════════════════════════════════════════════════════╗\n");
report.push_str("║ Profile Comparison Summary Report ║\n");
report.push_str("╚══════════════════════════════════════════════════════╝\n\n");
let ov = &comparison.overall;
report.push_str(&format!(
"Baseline: {} | Target: {}\n\n",
ov.baseline_binary, ov.target_binary
));
report.push_str("── Sample Counts ──\n");
report.push_str(&format!(
" Baseline: {} samples\n",
ov.total_samples_baseline
));
report.push_str(&format!(
" Target: {} samples\n",
ov.total_samples_target
));
report.push_str(&format!(" Change: {:.2}%\n\n", ov.sample_change_pct));
report.push_str("── Functions ──\n");
report.push_str(&format!(
" Baseline: {} | Target: {}\n",
ov.functions_baseline, ov.functions_target
));
report.push_str(&format!(
" New: {} | Removed: {}\n\n",
ov.new_functions, ov.removed_functions
));
report.push_str("── Changes ──\n");
report.push_str(&format!(" Regressions: {}\n", ov.num_regressions));
report.push_str(&format!(" Improvements: {}\n\n", ov.num_improvements));
let severe = self.severe_regressions();
if !severe.is_empty() {
report.push_str("── ⚠ Severe Regressions ──\n");
for reg in severe.iter().take(10) {
report.push_str(&format!(
" {} [{:?}] +{:.1}% ({} → {})\n",
reg.function,
reg.severity,
reg.increase_pct,
reg.baseline_samples,
reg.target_samples
));
}
report.push('\n');
}
if !comparison.improvements.is_empty() {
report.push_str("── ✓ Top Improvements ──\n");
for imp in comparison.improvements.iter().take(5) {
report.push_str(&format!(
" {} -{:.1}% ({} → {})\n",
imp.function, imp.decrease_pct, imp.baseline_samples, imp.target_samples
));
}
report.push('\n');
}
report
}
}
impl Default for X86ProfileComparison {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct X86ProfileData {
pub binary: String,
pub timestamp: u64,
pub total_samples: u64,
pub function_samples: HashMap<String, u64>,
pub collapsed_stacks: HashMap<String, u64>,
pub function_call_counts: HashMap<String, u64>,
pub function_durations_ns: HashMap<String, u64>,
pub counter_values: Vec<X86PerfCounterReading>,
pub topdown_metrics: HashMap<String, X86FunctionTopDownMetrics>,
}
#[derive(Debug, Clone)]
pub struct X86ProfileDiffReport {
pub overall: X86OverallDiff,
pub function_diffs: Vec<X86FunctionDiff>,
pub regressions: Vec<X86Regression>,
pub improvements: Vec<X86Improvement>,
pub new_functions: Vec<String>,
pub removed_functions: Vec<String>,
pub collapsed_stack_diff: Vec<X86StackDiff>,
pub topdown_diff: Option<X86TopDownDiff>,
}
#[derive(Debug, Clone)]
pub struct X86OverallDiff {
pub total_samples_baseline: u64,
pub total_samples_target: u64,
pub sample_change_pct: f64,
pub functions_baseline: u64,
pub functions_target: u64,
pub new_functions: u64,
pub removed_functions: u64,
pub num_regressions: u64,
pub num_improvements: u64,
pub baseline_binary: String,
pub target_binary: String,
}
#[derive(Debug, Clone)]
pub struct X86FunctionDiff {
pub function: String,
pub baseline_samples: u64,
pub target_samples: u64,
pub absolute_diff: i64,
pub relative_diff_pct: f64,
pub baseline_pct: f64,
pub target_pct: f64,
pub baseline_duration_ns: u64,
pub target_duration_ns: u64,
pub duration_diff_pct: f64,
}
#[derive(Debug, Clone)]
pub struct X86Regression {
pub function: String,
pub baseline_samples: u64,
pub target_samples: u64,
pub increase_pct: f64,
pub severity: X86RegressionSeverity,
}
#[derive(Debug, Clone)]
pub struct X86Improvement {
pub function: String,
pub baseline_samples: u64,
pub target_samples: u64,
pub decrease_pct: f64,
pub significance: X86Significance,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum X86RegressionSeverity {
Minor = 0,
Moderate = 1,
Major = 2,
Critical = 3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum X86Significance {
Low,
Moderate,
High,
}
#[derive(Debug, Clone)]
pub struct X86StackDiff {
pub stack: String,
pub baseline_count: u64,
pub target_count: u64,
pub diff: i64,
}
#[derive(Debug, Clone)]
pub struct X86TopDownDiff {
pub function_diffs: Vec<X86FunctionTopDownDiff>,
}
#[derive(Debug, Clone)]
pub struct X86FunctionTopDownDiff {
pub function: String,
pub retiring_diff: f64,
pub bad_spec_diff: f64,
pub frontend_bound_diff: f64,
pub backend_bound_diff: f64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_profiling_tools_create() {
let tools = X86ProfilingTools::default();
assert!(tools.initialized);
assert_eq!(tools.output_dir, PathBuf::from("x86_profiling_output"));
}
#[test]
fn test_profiling_tools_set_output_dir() {
let mut tools = X86ProfilingTools::default();
tools.set_output_dir("/tmp/profiling");
assert_eq!(tools.output_dir, PathBuf::from("/tmp/profiling"));
}
#[test]
fn test_profiling_tools_run_session() {
let mut tools = X86ProfilingTools::default();
let session = tools.run_full_session("test_binary", 5);
assert_eq!(session.binary, "test_binary");
assert_eq!(session.duration_secs, 5);
assert!(!session.perf_command.is_empty());
}
#[test]
fn test_profiling_session_to_profile_data() {
let mut tools = X86ProfilingTools::default();
let session = tools.run_full_session("test_binary", 5);
let data = session.to_profile_data();
assert_eq!(data.binary, "test_binary");
}
#[test]
fn test_perf_integration_create() {
let perf = X86PerfIntegration::new();
assert_eq!(perf.counter_groups.len(), 0);
assert_eq!(perf.parsed_events.len(), 0);
}
#[test]
fn test_perf_default_counter_config() {
let perf = X86PerfIntegration::new();
let config = perf.build_default_counter_config("test_bin", 10);
assert_eq!(config.events.len(), 9);
assert_eq!(config.binary, "test_bin");
assert_eq!(config.duration_secs, Some(10));
}
#[test]
fn test_perf_counter_config_add_event() {
let mut config = X86PerfCounterConfig::new("bin", 5);
config.add_event("cycles", "PERF_COUNT_HW_CPU_CYCLES", 0);
assert_eq!(config.events.len(), 1);
assert_eq!(config.events[0].name, "cycles");
}
#[test]
fn test_perf_counter_config_set_reading() {
let mut config = X86PerfCounterConfig::new("bin", 5);
config.set_reading("cycles", 1000000, "cycles");
assert_eq!(config.get_reading("cycles"), Some(1000000));
assert_eq!(config.get_reading("instructions"), None);
}
#[test]
fn test_perf_counter_config_ipc() {
let mut config = X86PerfCounterConfig::new("bin", 5);
config.set_reading("cycles", 1000, "cycles");
config.set_reading("instructions", 2500, "instructions");
let ipc = config.ipc().unwrap();
assert!((ipc - 2.5).abs() < 0.01);
}
#[test]
fn test_perf_counter_config_ipc_zero_cycles() {
let config = X86PerfCounterConfig::new("bin", 5);
assert_eq!(config.ipc(), None);
}
#[test]
fn test_perf_counter_config_cache_miss_rate() {
let mut config = X86PerfCounterConfig::new("bin", 5);
config.set_reading("cache-references", 10000, "refs");
config.set_reading("cache-misses", 500, "misses");
let rate = config.cache_miss_rate_pct().unwrap();
assert!((rate - 5.0).abs() < 0.01);
}
#[test]
fn test_perf_counter_config_branch_mispred_rate() {
let mut config = X86PerfCounterConfig::new("bin", 5);
config.set_reading("branches", 100000, "branches");
config.set_reading("branch-misses", 3000, "misses");
let rate = config.branch_mispred_rate_pct().unwrap();
assert!((rate - 3.0).abs() < 0.01);
}
#[test]
fn test_perf_generate_stat_command() {
let perf = X86PerfIntegration::new();
let config = perf.build_default_counter_config("test_bin", 10);
let cmd = perf.generate_stat_command(&config);
assert!(cmd.starts_with("perf stat"));
assert!(cmd.contains("cycles"));
assert!(cmd.contains("instructions"));
}
#[test]
fn test_perf_generate_record_command() {
let perf = X86PerfIntegration::new();
let record_config = X86PerfRecordConfig::default();
let cmd = perf.generate_record_command(&record_config);
assert!(cmd.starts_with("perf record"));
assert!(cmd.contains("-g"));
assert!(cmd.contains("-F 99"));
}
#[test]
fn test_perf_generate_annotate_command() {
let perf = X86PerfIntegration::new();
let cmd = perf.generate_annotate_command("perf.data", Some("main"));
assert!(cmd.contains("perf annotate"));
assert!(cmd.contains("main"));
}
#[test]
fn test_perf_generate_report_command() {
let perf = X86PerfIntegration::new();
let cmd = perf.generate_report_command("perf.data", "dso,sym");
assert!(cmd.contains("perf report"));
assert!(cmd.contains("dso,sym"));
}
#[test]
fn test_perf_generate_script_command() {
let perf = X86PerfIntegration::new();
let cmd = perf.generate_script_command("perf.data", &["comm", "pid", "time", "ip"]);
assert!(cmd.contains("perf script"));
assert!(cmd.contains("comm,pid,time,ip"));
}
#[test]
fn test_perf_generate_diff_command() {
let perf = X86PerfIntegration::new();
let cmd = perf.generate_diff_command("perf_before.data", "perf_after.data");
assert!(cmd.contains("perf diff"));
}
#[test]
fn test_perf_generate_top_command() {
let perf = X86PerfIntegration::new();
let cmd = perf.generate_top_command(&["cycles", "instructions"]);
assert!(cmd.contains("perf top"));
assert!(cmd.contains("cycles"));
assert!(cmd.contains("instructions"));
}
#[test]
fn test_perf_generate_list_command() {
let perf = X86PerfIntegration::new();
let cmd = perf.generate_list_command(Some("cache"));
assert!(cmd.contains("perf list cache"));
}
#[test]
fn test_perf_parse_script_output() {
let mut perf = X86PerfIntegration::new();
let input = "test_app 12345 100.5 cycles:ppp: 0x400100 main\n\
test_app 12345 100.6 instructions: 0x400200 foo";
let events = perf.parse_script_output(input);
assert_eq!(events.len(), 2);
assert_eq!(events[0].command, "test_app");
assert_eq!(events[0].pid, 12345);
assert!(events[0].symbol.contains("main"));
}
#[test]
fn test_perf_parse_script_empty() {
let mut perf = X86PerfIntegration::new();
let events = perf.parse_script_output("");
assert!(events.is_empty());
}
#[test]
fn test_perf_parse_script_comments() {
let mut perf = X86PerfIntegration::new();
let input = "# perf script output\n# another comment\n";
let events = perf.parse_script_output(input);
assert!(events.is_empty());
}
#[test]
fn test_perf_parse_call_stacks() {
let mut perf = X86PerfIntegration::new();
let input = "main;foo;bar 100\nmain;foo;baz 50\nmain 200";
let stacks = perf.parse_script_call_stacks(input);
assert_eq!(stacks.len(), 3);
assert_eq!(stacks.get("main;foo;bar"), Some(&100));
assert_eq!(stacks.get("main"), Some(&200));
}
#[test]
fn test_perf_parse_call_stacks_empty() {
let mut perf = X86PerfIntegration::new();
let stacks = perf.parse_script_call_stacks("");
assert!(stacks.is_empty());
}
#[test]
fn test_perf_compute_derived_metrics() {
let perf = X86PerfIntegration::new();
let readings = vec![
X86PerfCounterReading {
event_name: "cycles".to_string(),
value: 10000,
unit: "cycles".to_string(),
},
X86PerfCounterReading {
event_name: "instructions".to_string(),
value: 8000,
unit: "instructions".to_string(),
},
X86PerfCounterReading {
event_name: "cache-references".to_string(),
value: 5000,
unit: "refs".to_string(),
},
X86PerfCounterReading {
event_name: "cache-misses".to_string(),
value: 250,
unit: "misses".to_string(),
},
X86PerfCounterReading {
event_name: "branches".to_string(),
value: 2000,
unit: "branches".to_string(),
},
X86PerfCounterReading {
event_name: "branch-misses".to_string(),
value: 40,
unit: "misses".to_string(),
},
X86PerfCounterReading {
event_name: "stalled-cycles-frontend".to_string(),
value: 1500,
unit: "cycles".to_string(),
},
X86PerfCounterReading {
event_name: "stalled-cycles-backend".to_string(),
value: 2000,
unit: "cycles".to_string(),
},
];
let metrics = perf.compute_derived_metrics(&readings);
assert!((metrics.instructions_per_cycle - 0.8).abs() < 0.01);
assert!((metrics.cache_miss_rate - 5.0).abs() < 0.01);
assert!((metrics.branch_mispred_rate - 2.0).abs() < 0.01);
assert!((metrics.frontend_stall_pct - 15.0).abs() < 0.01);
assert!((metrics.backend_stall_pct - 20.0).abs() < 0.01);
}
#[test]
fn test_perf_derived_metrics_zero_instructions() {
let perf = X86PerfIntegration::new();
let readings = vec![X86PerfCounterReading {
event_name: "cycles".to_string(),
value: 1000,
unit: "cycles".to_string(),
}];
let metrics = perf.compute_derived_metrics(&readings);
assert_eq!(metrics.total_instructions, 0);
}
#[test]
fn test_perf_reset_counters() {
let mut perf = X86PerfIntegration::new();
perf.parsed_events.push(X86PerfEvent {
command: "test".to_string(),
pid: 1,
timestamp: 1.0,
event_type: "cycles".to_string(),
address: 0x1000,
symbol: "main".to_string(),
});
perf.collapsed_stacks.insert("main;foo".to_string(), 10);
perf.reset_counters();
assert!(perf.parsed_events.is_empty());
assert!(perf.collapsed_stacks.is_empty());
}
#[test]
fn test_perf_counter_group_create() {
let mut group = X86PerfCounterGroup::new("test_group");
assert_eq!(group.name, "test_group");
assert!(group.is_empty());
group.add_counter("cycles", "PERF_COUNT_HW_CPU_CYCLES", 0, 0);
assert_eq!(group.len(), 1);
assert!(!group.is_empty());
}
#[test]
fn test_perf_event_fd() {
let fd = X86PerfEventFd {
fd: -1,
event_type: 0,
event_config: 0,
pid: -1,
cpu: -1,
group_fd: -1,
flags: 0,
is_open: false,
};
assert_eq!(fd.read_value(), None);
}
#[test]
fn test_perf_record_config_defaults() {
let config = X86PerfRecordConfig::default();
assert!(config.call_graph);
assert!(config.inherit);
assert!(!config.system_wide);
assert_eq!(config.frequency, Some(99));
}
#[test]
fn test_configure_intel_raw_event() {
let perf = X86PerfIntegration::new();
let config = perf.configure_intel_raw_event(0xC0, 0x00, 0x00, false, false);
assert!(config > 0);
}
#[test]
fn test_configure_intel_raw_event_with_inv_edge() {
let perf = X86PerfIntegration::new();
let config = perf.configure_intel_raw_event(0xC4, 0x01, 0x0F, true, true);
assert!(config & (1 << 23) != 0); assert!(config & (1 << 18) != 0); }
#[test]
fn test_configure_amd_raw_event() {
let perf = X86PerfIntegration::new();
let config = perf.configure_amd_raw_event(0x076, 0x01);
assert!(config > 0);
}
#[test]
fn test_vtune_create() {
let vtune = X86IntelVTune::new();
assert_eq!(vtune.domains.len(), 0);
assert_eq!(vtune.string_handles.len(), 0);
assert_eq!(vtune.next_domain_handle, 1);
assert_eq!(vtune.next_string_handle, 1);
}
#[test]
fn test_itt_domain_create() {
let mut vtune = X86IntelVTune::new();
let handle1 = vtune.itt_domain_create("MyDomain");
let handle2 = vtune.itt_domain_create("MyDomain");
assert_eq!(handle1, handle2); assert_eq!(vtune.domains.len(), 1);
}
#[test]
fn test_itt_domain_create_unique() {
let mut vtune = X86IntelVTune::new();
let h1 = vtune.itt_domain_create("Domain1");
let h2 = vtune.itt_domain_create("Domain2");
assert_ne!(h1, h2);
assert_eq!(vtune.domains.len(), 2);
}
#[test]
fn test_itt_string_handle_create() {
let mut vtune = X86IntelVTune::new();
let h1 = vtune.itt_string_handle_create("TaskA");
let h2 = vtune.itt_string_handle_create("TaskA");
assert_eq!(h1, h2);
assert_eq!(vtune.string_handles.len(), 1);
}
#[test]
fn test_itt_task_begin_end() {
let mut vtune = X86IntelVTune::new();
let domain = vtune.itt_domain_create("AppDomain");
let task = vtune.itt_string_handle_create("ComputeTask");
let marker = vtune.itt_task_begin(domain, task);
assert_eq!(marker.domain_handle, domain);
assert_eq!(marker.task_name_handle, task);
vtune.itt_task_end(marker);
}
#[test]
fn test_vtune_lookup_domain() {
let mut vtune = X86IntelVTune::new();
let handle = vtune.itt_domain_create("TestDomain");
let domain = vtune.lookup_domain(handle);
assert!(domain.is_some());
assert_eq!(domain.unwrap().name, "TestDomain");
}
#[test]
fn test_vtune_lookup_domain_missing() {
let vtune = X86IntelVTune::new();
assert!(vtune.lookup_domain(999).is_none());
}
#[test]
fn test_vtune_lookup_string() {
let mut vtune = X86IntelVTune::new();
let handle = vtune.itt_string_handle_create("MyTask");
let sh = vtune.lookup_string(handle);
assert!(sh.is_some());
assert_eq!(sh.unwrap().value, "MyTask");
}
#[test]
fn test_vtune_domain_name() {
let mut vtune = X86IntelVTune::new();
let handle = vtune.itt_domain_create("GPUCompute");
assert_eq!(vtune.domain_name(handle), "GPUCompute");
assert_eq!(vtune.domain_name(999), "domain_999");
}
#[test]
fn test_vtune_string_value() {
let mut vtune = X86IntelVTune::new();
let handle = vtune.itt_string_handle_create("RenderFrame");
assert_eq!(vtune.string_value(handle), "RenderFrame");
assert_eq!(vtune.string_value(999), "string_999");
}
#[test]
fn test_vtune_generate_hotspot_collect_command() {
let vtune = X86IntelVTune::new();
let cmd = vtune.generate_hotspot_collect_command("./app", 30, "results_dir");
assert!(cmd.contains("-collect hotspots"));
assert!(cmd.contains("results_dir"));
assert!(cmd.contains("./app"));
}
#[test]
fn test_vtune_generate_uarch_collect_command() {
let vtune = X86IntelVTune::new();
let cmd = vtune.generate_uarch_collect_command("./app", "uarch_results");
assert!(cmd.contains("-collect uarch-exploration"));
}
#[test]
fn test_vtune_generate_memory_collect_command() {
let vtune = X86IntelVTune::new();
let cmd = vtune.generate_memory_collect_command("./app", "mem_results");
assert!(cmd.contains("-collect memory-access"));
}
#[test]
fn test_vtune_generate_hotspot_report_command() {
let vtune = X86IntelVTune::new();
let cmd = vtune.generate_hotspot_report_command("results_dir");
assert!(cmd.contains("-report hotspots"));
assert!(cmd.contains("csv"));
}
#[test]
fn test_vtune_generate_topdown_report_command() {
let vtune = X86IntelVTune::new();
let cmd = vtune.generate_topdown_report_command("results");
assert!(cmd.contains("-report top-down"));
}
#[test]
fn test_vtune_generate_callstacks_report_command() {
let vtune = X86IntelVTune::new();
let cmd = vtune.generate_callstacks_report_command("results");
assert!(cmd.contains("-report callstacks"));
}
#[test]
fn test_vtune_parse_hotspot_csv() {
let mut vtune = X86IntelVTune::new();
let csv = "Function,Module,CPU Time %,CPU Time,Instructions Retired,CPI Rate\n\
main,app,50.5,5.05,1000000,0.8\n\
foo,app,30.0,3.00,600000,1.2";
let results = vtune.parse_hotspot_csv(csv);
assert_eq!(results.len(), 2);
assert_eq!(results[0].function, "main");
assert!((results[0].cpu_time_pct - 50.5).abs() < 0.01);
assert_eq!(results[0].instructions_retired, 1000000);
}
#[test]
fn test_vtune_parse_hotspot_csv_empty() {
let mut vtune = X86IntelVTune::new();
let results = vtune.parse_hotspot_csv("");
assert!(results.is_empty());
}
#[test]
fn test_vtune_top_hotspots() {
let mut vtune = X86IntelVTune::new();
vtune.hotspot_results = vec![
X86VTuneHotspotResult {
function: "slow".to_string(),
module: "app".to_string(),
cpu_time_pct: 80.0,
cpu_time_sec: 8.0,
instructions_retired: 50000,
cpi_rate: 2.5,
},
X86VTuneHotspotResult {
function: "fast".to_string(),
module: "app".to_string(),
cpu_time_pct: 20.0,
cpu_time_sec: 2.0,
instructions_retired: 500000,
cpi_rate: 0.5,
},
];
let top = vtune.top_hotspots(1);
assert_eq!(top.len(), 1);
assert_eq!(top[0].function, "slow");
}
#[test]
fn test_vtune_high_cpi_functions() {
let mut vtune = X86IntelVTune::new();
vtune.hotspot_results = vec![
X86VTuneHotspotResult {
function: "mem_bound".to_string(),
module: "app".to_string(),
cpu_time_pct: 30.0,
cpu_time_sec: 3.0,
instructions_retired: 100000,
cpi_rate: 3.0,
},
X86VTuneHotspotResult {
function: "compute".to_string(),
module: "app".to_string(),
cpu_time_pct: 70.0,
cpu_time_sec: 7.0,
instructions_retired: 1000000,
cpi_rate: 0.8,
},
];
let high_cpi = vtune.high_cpi_functions(2.0);
assert_eq!(high_cpi.len(), 1);
assert_eq!(high_cpi[0].function, "mem_bound");
}
#[test]
fn test_vtune_high_instruction_functions() {
let mut vtune = X86IntelVTune::new();
vtune.hotspot_results = vec![
X86VTuneHotspotResult {
function: "hot".to_string(),
module: "app".to_string(),
cpu_time_pct: 60.0,
cpu_time_sec: 6.0,
instructions_retired: 10000000,
cpi_rate: 1.0,
},
X86VTuneHotspotResult {
function: "cold".to_string(),
module: "app".to_string(),
cpu_time_pct: 0.5,
cpu_time_sec: 0.05,
instructions_retired: 1000,
cpi_rate: 1.0,
},
];
let hot = vtune.high_instruction_functions(50.0);
assert_eq!(hot.len(), 1);
assert_eq!(hot[0].function, "hot");
}
#[test]
fn test_vtune_uarch_bottlenecks() {
let mut vtune = X86IntelVTune::new();
vtune.record_uarch_finding(X86VTuneUArchResult {
category: "Frontend".to_string(),
description: "ICache misses".to_string(),
function: "big_loop".to_string(),
source_location: "file.c:100".to_string(),
severity: 8.0,
pipeline_slot_pct: 25.0,
recommendation: "Reduce code size".to_string(),
});
vtune.record_uarch_finding(X86VTuneUArchResult {
category: "Backend".to_string(),
description: "Port 0 contention".to_string(),
function: "compute".to_string(),
source_location: "file.c:200".to_string(),
severity: 3.0,
pipeline_slot_pct: 10.0,
recommendation: "Balance port usage".to_string(),
});
let bottlenecks = vtune.uarch_bottlenecks();
assert_eq!(bottlenecks.len(), 2);
assert_eq!(bottlenecks[0].category, "Frontend"); }
#[test]
fn test_vtune_frontend_bottlenecks() {
let mut vtune = X86IntelVTune::new();
vtune.record_uarch_finding(X86VTuneUArchResult {
category: "Frontend".to_string(),
description: "Decode stall".to_string(),
function: "func".to_string(),
source_location: "".to_string(),
severity: 5.0,
pipeline_slot_pct: 15.0,
recommendation: "".to_string(),
});
vtune.record_uarch_finding(X86VTuneUArchResult {
category: "Memory".to_string(),
description: "LLC miss".to_string(),
function: "func".to_string(),
source_location: "".to_string(),
severity: 7.0,
pipeline_slot_pct: 20.0,
recommendation: "".to_string(),
});
let fe = vtune.frontend_bottlenecks();
assert_eq!(fe.len(), 1);
assert!(fe[0].description.contains("Decode"));
}
#[test]
fn test_vtune_backend_bottlenecks() {
let mut vtune = X86IntelVTune::new();
vtune.record_uarch_finding(X86VTuneUArchResult {
category: "Backend".to_string(),
description: "Divider busy".to_string(),
function: "div_heavy".to_string(),
source_location: "".to_string(),
severity: 6.0,
pipeline_slot_pct: 12.0,
recommendation: "".to_string(),
});
let be = vtune.backend_bottlenecks();
assert_eq!(be.len(), 1);
}
#[test]
fn test_vtune_memory_bottlenecks() {
let mut vtune = X86IntelVTune::new();
vtune.record_uarch_finding(X86VTuneUArchResult {
category: "Memory".to_string(),
description: "DRAM bound".to_string(),
function: "stream".to_string(),
source_location: "".to_string(),
severity: 9.0,
pipeline_slot_pct: 30.0,
recommendation: "".to_string(),
});
let mem = vtune.memory_bottlenecks();
assert_eq!(mem.len(), 1);
}
#[test]
fn test_vtune_speculation_bottlenecks() {
let mut vtune = X86IntelVTune::new();
vtune.record_uarch_finding(X86VTuneUArchResult {
category: "Branch".to_string(),
description: "High mispredict rate".to_string(),
function: "branchy".to_string(),
source_location: "".to_string(),
severity: 7.0,
pipeline_slot_pct: 18.0,
recommendation: "".to_string(),
});
let spec = vtune.speculation_bottlenecks();
assert_eq!(spec.len(), 1);
}
#[test]
fn test_vtune_memory_latency_hotspots() {
let mut vtune = X86IntelVTune::new();
vtune.record_memory_finding(X86VTuneMemoryResult {
function: "load_heavy".to_string(),
source_location: "file.c:50".to_string(),
variable: "big_array".to_string(),
allocation_site: "file.c:40".to_string(),
l1d_miss_count: 10000,
l2_miss_count: 5000,
llc_miss_count: 1000,
avg_latency_cycles: 120.0,
bandwidth_gb_s: 2.5,
is_numa_remote: false,
dram_access_count: 1000,
});
vtune.record_memory_finding(X86VTuneMemoryResult {
function: "stream".to_string(),
source_location: "file.c:100".to_string(),
variable: "buf".to_string(),
allocation_site: "".to_string(),
l1d_miss_count: 50000,
l2_miss_count: 45000,
llc_miss_count: 40000,
avg_latency_cycles: 300.0,
bandwidth_gb_s: 15.0,
is_numa_remote: true,
dram_access_count: 40000,
});
let hotspots = vtune.memory_latency_hotspots();
assert_eq!(hotspots.len(), 2);
assert_eq!(hotspots[0].function, "stream"); }
#[test]
fn test_vtune_llc_miss_accesses() {
let mut vtune = X86IntelVTune::new();
vtune.record_memory_finding(X86VTuneMemoryResult {
function: "cache_hitter".to_string(),
source_location: "".to_string(),
variable: "".to_string(),
allocation_site: "".to_string(),
l1d_miss_count: 100,
l2_miss_count: 50,
llc_miss_count: 0,
avg_latency_cycles: 4.0,
bandwidth_gb_s: 1.0,
is_numa_remote: false,
dram_access_count: 0,
});
vtune.record_memory_finding(X86VTuneMemoryResult {
function: "cache_misser".to_string(),
source_location: "".to_string(),
variable: "".to_string(),
allocation_site: "".to_string(),
l1d_miss_count: 10000,
l2_miss_count: 8000,
llc_miss_count: 5000,
avg_latency_cycles: 200.0,
bandwidth_gb_s: 5.0,
is_numa_remote: false,
dram_access_count: 5000,
});
let llc_miss = vtune.llc_miss_accesses();
assert_eq!(llc_miss.len(), 1);
assert_eq!(llc_miss[0].function, "cache_misser");
}
#[test]
fn test_vtune_high_bandwidth_accesses() {
let mut vtune = X86IntelVTune::new();
vtune.record_memory_finding(X86VTuneMemoryResult {
function: "stream".to_string(),
source_location: "".to_string(),
variable: "".to_string(),
allocation_site: "".to_string(),
l1d_miss_count: 0,
l2_miss_count: 0,
llc_miss_count: 0,
avg_latency_cycles: 50.0,
bandwidth_gb_s: 20.0,
is_numa_remote: false,
dram_access_count: 0,
});
let high_bw = vtune.high_bandwidth_accesses(10.0);
assert_eq!(high_bw.len(), 1);
}
#[test]
fn test_uprof_create() {
let uprof = X86AMDuProf::new();
assert!(!uprof.ibs_fetch_config.enabled);
assert!(!uprof.ibs_op_config.enabled);
assert!(uprof.l3_analysis.is_empty());
}
#[test]
fn test_uprof_configure_ibs_fetch() {
let mut uprof = X86AMDuProf::new();
uprof.configure_ibs_fetch(0x5000, true, false);
assert!(uprof.ibs_fetch_config.enabled);
assert_eq!(uprof.ibs_fetch_config.period, 0x5000);
assert!(uprof.ibs_fetch_config.randomized_period);
assert!(!uprof.ibs_fetch_config.l2_miss_filter);
}
#[test]
fn test_uprof_configure_ibs_op() {
let mut uprof = X86AMDuProf::new();
uprof.configure_ibs_op(0x80000, false, true);
assert!(uprof.ibs_op_config.enabled);
assert_eq!(uprof.ibs_op_config.period, 0x80000);
assert!(!uprof.ibs_op_config.randomized_period);
assert!(uprof.ibs_op_config.dcache_miss_filter);
}
#[test]
fn test_uprof_enable_ibs_default() {
let mut uprof = X86AMDuProf::new();
uprof.enable_ibs_default();
assert!(uprof.ibs_fetch_config.enabled);
assert!(uprof.ibs_op_config.enabled);
assert_eq!(uprof.ibs_fetch_config.period, AMD_IBS_DEFAULT_FETCH_PERIOD);
assert_eq!(uprof.ibs_op_config.period, AMD_IBS_DEFAULT_OP_PERIOD);
}
#[test]
fn test_uprof_disable_ibs() {
let mut uprof = X86AMDuProf::new();
uprof.enable_ibs_default();
uprof.disable_ibs();
assert!(!uprof.ibs_fetch_config.enabled);
assert!(!uprof.ibs_op_config.enabled);
}
#[test]
fn test_uprof_ibs_fetch_period_for_freq() {
let uprof = X86AMDuProf::new();
let period = uprof.ibs_fetch_period_for_freq(1_500_000_000);
assert!(period > 0);
let period2 = uprof.ibs_fetch_period_for_freq(3_000_000_000);
assert!(period > period2);
}
#[test]
fn test_uprof_ibs_op_period_for_freq() {
let uprof = X86AMDuProf::new();
let period = uprof.ibs_op_period_for_freq(1_500_000_000);
assert!(period > 0);
}
#[test]
fn test_uprof_generate_collect_command() {
let uprof = X86AMDuProf::new();
let cmd = uprof.generate_collect_command("./app", "output", 30);
assert!(cmd.contains("collect"));
assert!(cmd.contains("tbp"));
assert!(cmd.contains("./app"));
}
#[test]
fn test_uprof_generate_ibs_collect_command() {
let uprof = X86AMDuProf::new();
let cmd = uprof.generate_ibs_collect_command("./app", "ibs_out", 60);
assert!(cmd.contains("collect"));
assert!(cmd.contains("ibs"));
}
#[test]
fn test_uprof_generate_report_command() {
let uprof = X86AMDuProf::new();
let cmd = uprof.generate_report_command("input_dir", "report.csv", "top-hotspots");
assert!(cmd.contains("report"));
assert!(cmd.contains("top-hotspots"));
assert!(cmd.contains("csv"));
}
#[test]
fn test_uprof_record_l3_cache_miss() {
let mut uprof = X86AMDuProf::new();
uprof.record_l3_cache_miss(X86AMDL3CacheMiss {
function: "matrix_mul".to_string(),
source_file: "matmul.c".to_string(),
source_line: 42,
count: 15000,
address: 0x7f000000,
l3_slice: 2,
is_prefetch: false,
ccd_index: 0,
});
assert_eq!(uprof.l3_analysis.len(), 1);
assert_eq!(uprof.total_l3_misses(), 15000);
}
#[test]
fn test_uprof_analyze_l3_hotspots() {
let mut uprof = X86AMDuProf::new();
uprof.record_l3_cache_miss(X86AMDL3CacheMiss {
function: "func_a".to_string(),
source_file: "a.c".to_string(),
source_line: 1,
count: 100,
address: 0,
l3_slice: 0,
is_prefetch: false,
ccd_index: 0,
});
uprof.record_l3_cache_miss(X86AMDL3CacheMiss {
function: "func_b".to_string(),
source_file: "b.c".to_string(),
source_line: 2,
count: 500,
address: 0,
l3_slice: 0,
is_prefetch: false,
ccd_index: 0,
});
let hotspots = uprof.analyze_l3_hotspots();
assert_eq!(hotspots[0].function, "func_b");
}
#[test]
fn test_uprof_l3_misses_by_function() {
let mut uprof = X86AMDuProf::new();
uprof.record_l3_cache_miss(X86AMDL3CacheMiss {
function: "f".to_string(),
source_file: "f.c".to_string(),
source_line: 1,
count: 50,
address: 0,
l3_slice: 0,
is_prefetch: false,
ccd_index: 0,
});
uprof.record_l3_cache_miss(X86AMDL3CacheMiss {
function: "f".to_string(),
source_file: "f.c".to_string(),
source_line: 2,
count: 75,
address: 0,
l3_slice: 0,
is_prefetch: false,
ccd_index: 0,
});
let by_func = uprof.l3_misses_by_function();
assert_eq!(by_func.get("f"), Some(&125));
}
#[test]
fn test_uprof_l3_misses_by_location() {
let mut uprof = X86AMDuProf::new();
uprof.record_l3_cache_miss(X86AMDL3CacheMiss {
function: "f".to_string(),
source_file: "f.c".to_string(),
source_line: 10,
count: 30,
address: 0,
l3_slice: 0,
is_prefetch: false,
ccd_index: 0,
});
let by_loc = uprof.l3_misses_by_location();
assert_eq!(by_loc.get("f.c:10"), Some(&30));
}
#[test]
fn test_uprof_top_l3_miss_locations() {
let mut uprof = X86AMDuProf::new();
uprof.record_l3_cache_miss(X86AMDL3CacheMiss {
function: "f".to_string(),
source_file: "f.c".to_string(),
source_line: 1,
count: 10,
address: 0,
l3_slice: 0,
is_prefetch: false,
ccd_index: 0,
});
uprof.record_l3_cache_miss(X86AMDL3CacheMiss {
function: "f".to_string(),
source_file: "f.c".to_string(),
source_line: 2,
count: 100,
address: 0,
l3_slice: 0,
is_prefetch: false,
ccd_index: 0,
});
let top = uprof.top_l3_miss_locations(1);
assert_eq!(top.len(), 1);
assert_eq!(top[0].0, "f.c:2");
assert_eq!(top[0].1, 100);
}
#[test]
fn test_uprof_record_ibs_sample() {
let mut uprof = X86AMDuProf::new();
uprof.record_ibs_sample(X86AMDIbsSample {
ip: 0x401000,
function: "memcpy".to_string(),
is_load: true,
is_store: false,
is_branch: false,
branch_mispredicted: false,
dcache_miss_latency: 200,
dtlb_miss: false,
physical_address: 0x100000,
linear_address: 0x7f000,
num_macro_ops: 4,
itlb_miss: false,
icache_miss: false,
});
assert_eq!(uprof.ibs_samples.len(), 1);
}
#[test]
fn test_uprof_avg_load_latency() {
let mut uprof = X86AMDuProf::new();
uprof.record_ibs_sample(X86AMDIbsSample {
ip: 0x1000,
function: "f".to_string(),
is_load: true,
is_store: false,
is_branch: false,
branch_mispredicted: false,
dcache_miss_latency: 100,
dtlb_miss: false,
physical_address: 0,
linear_address: 0,
num_macro_ops: 2,
itlb_miss: false,
icache_miss: false,
});
uprof.record_ibs_sample(X86AMDIbsSample {
ip: 0x2000,
function: "f".to_string(),
is_load: true,
is_store: false,
is_branch: false,
branch_mispredicted: false,
dcache_miss_latency: 300,
dtlb_miss: false,
physical_address: 0,
linear_address: 0,
num_macro_ops: 2,
itlb_miss: false,
icache_miss: false,
});
let avg = uprof.avg_load_latency().unwrap();
assert!((avg - 200.0).abs() < 0.01);
}
#[test]
fn test_uprof_avg_load_latency_no_loads() {
let mut uprof = X86AMDuProf::new();
uprof.record_ibs_sample(X86AMDIbsSample {
ip: 0x1000,
function: "f".to_string(),
is_load: false,
is_store: true,
is_branch: false,
branch_mispredicted: false,
dcache_miss_latency: 0,
dtlb_miss: false,
physical_address: 0,
linear_address: 0,
num_macro_ops: 1,
itlb_miss: false,
icache_miss: false,
});
assert!(uprof.avg_load_latency().is_none());
}
#[test]
fn test_uprof_l1d_miss_samples() {
let mut uprof = X86AMDuProf::new();
uprof.record_ibs_sample(X86AMDIbsSample {
ip: 0x1000,
function: "f".to_string(),
is_load: true,
is_store: false,
is_branch: false,
branch_mispredicted: false,
dcache_miss_latency: 50,
dtlb_miss: false,
physical_address: 0,
linear_address: 0,
num_macro_ops: 2,
itlb_miss: false,
icache_miss: false,
});
uprof.record_ibs_sample(X86AMDIbsSample {
ip: 0x2000,
function: "g".to_string(),
is_load: true,
is_store: false,
is_branch: false,
branch_mispredicted: false,
dcache_miss_latency: 0, dtlb_miss: false,
physical_address: 0,
linear_address: 0,
num_macro_ops: 2,
itlb_miss: false,
icache_miss: false,
});
let misses = uprof.l1d_miss_samples();
assert_eq!(misses.len(), 1);
}
#[test]
fn test_uprof_tlb_miss_samples() {
let mut uprof = X86AMDuProf::new();
uprof.record_ibs_sample(X86AMDIbsSample {
ip: 0x1000,
function: "f".to_string(),
is_load: true,
is_store: false,
is_branch: false,
branch_mispredicted: false,
dcache_miss_latency: 0,
dtlb_miss: true,
physical_address: 0,
linear_address: 0,
num_macro_ops: 2,
itlb_miss: false,
icache_miss: false,
});
let tlb = uprof.tlb_miss_samples();
assert_eq!(tlb.len(), 1);
}
#[test]
fn test_uprof_pipeline_metrics() {
let mut uprof = X86AMDuProf::new();
uprof.record_pipeline_metric(X86AMDPipelineMetric {
core_id: 0,
timestamp_ms: 1000,
dispatch_utilization: 0.85,
retire_utilization: 0.80,
int_ops_dispatched: 100000,
fp_ops_dispatched: 20000,
branch_ops_dispatched: 15000,
load_ops_dispatched: 40000,
store_ops_dispatched: 25000,
frontend_stall_cycles: 5000,
backend_stall_cycles: 3000,
});
uprof.record_pipeline_metric(X86AMDPipelineMetric {
core_id: 1,
timestamp_ms: 1000,
dispatch_utilization: 0.45,
retire_utilization: 0.42,
int_ops_dispatched: 50000,
fp_ops_dispatched: 10000,
branch_ops_dispatched: 8000,
load_ops_dispatched: 60000,
store_ops_dispatched: 10000,
frontend_stall_cycles: 40000,
backend_stall_cycles: 5000,
});
let core0 = uprof.pipeline_metrics_for_core(0);
assert_eq!(core0.len(), 1);
assert!((core0[0].dispatch_utilization - 0.85).abs() < 0.01);
let avg = uprof.avg_dispatch_utilization().unwrap();
assert!((avg - 0.65).abs() < 0.01);
}
#[test]
fn test_sampling_profiler_create() {
let profiler = X86SamplingProfiler::new();
assert_eq!(profiler.sample_freq, X86_DEFAULT_SAMPLE_FREQ);
assert_eq!(profiler.unwind_method, X86UnwindMethod::FramePointer);
assert!(!profiler.collect_lbr);
assert!(!profiler.is_active);
assert_eq!(profiler.max_unwind_depth, X86_MAX_UNWIND_DEPTH);
}
#[test]
fn test_sampling_profiler_set_freq() {
let mut profiler = X86SamplingProfiler::new();
profiler.set_sample_freq(1000);
assert_eq!(profiler.sample_freq, 1000);
}
#[test]
fn test_sampling_profiler_set_unwind() {
let mut profiler = X86SamplingProfiler::new();
profiler.set_unwind_method(X86UnwindMethod::DWARF);
assert_eq!(profiler.unwind_method, X86UnwindMethod::DWARF);
}
#[test]
fn test_sampling_profiler_set_lbr() {
let mut profiler = X86SamplingProfiler::new();
profiler.set_collect_lbr(true);
assert!(profiler.collect_lbr);
}
#[test]
fn test_sampling_record_sample() {
let mut profiler = X86SamplingProfiler::new();
profiler.record_sample(0x401000, vec![0x400000], "main");
assert_eq!(profiler.total_samples(), 1);
assert_eq!(profiler.function_samples.get("main"), Some(&1));
assert!(!profiler.collapsed_stacks.is_empty());
}
#[test]
fn test_sampling_record_multiple_samples() {
let mut profiler = X86SamplingProfiler::new();
for _ in 0..10 {
profiler.record_sample(0x401000, vec![0x400000], "main");
}
for _ in 0..5 {
profiler.record_sample(0x402000, vec![0x400000], "foo");
}
assert_eq!(profiler.total_samples(), 15);
assert_eq!(*profiler.function_samples.get("main").unwrap(), 10);
assert_eq!(*profiler.function_samples.get("foo").unwrap(), 5);
}
#[test]
fn test_sampling_top_functions() {
let mut profiler = X86SamplingProfiler::new();
profiler.record_sample(0x1000, vec![], "hot");
profiler.record_sample(0x1000, vec![], "hot");
profiler.record_sample(0x2000, vec![], "warm");
let top = profiler.top_functions(2);
assert_eq!(top.len(), 2);
assert_eq!(top[0].0, "hot");
}
#[test]
fn test_sampling_samples_for_function() {
let mut profiler = X86SamplingProfiler::new();
profiler.record_sample(0x1000, vec![], "f1");
profiler.record_sample(0x2000, vec![], "f2");
profiler.record_sample(0x3000, vec![], "f1");
let f1_samples = profiler.samples_for_function("f1");
assert_eq!(f1_samples.len(), 2);
let f2_samples = profiler.samples_for_function("f2");
assert_eq!(f2_samples.len(), 1);
let f3_samples = profiler.samples_for_function("f3");
assert!(f3_samples.is_empty());
}
#[test]
fn test_sampling_sample_error_margin() {
let mut profiler = X86SamplingProfiler::new();
for _ in 0..100 {
profiler.record_sample(0x1000, vec![], "main");
}
let err = profiler.sample_error_margin("main").unwrap();
assert!((err - 10.0).abs() < 0.01); }
#[test]
fn test_sampling_is_significant() {
let mut profiler = X86SamplingProfiler::new();
for _ in 0..5 {
profiler.record_sample(0x1000, vec![], "rare");
}
for _ in 0..50 {
profiler.record_sample(0x2000, vec![], "common");
}
assert!(!profiler.is_significant("rare"));
assert!(profiler.is_significant("common"));
}
#[test]
fn test_sampling_reset() {
let mut profiler = X86SamplingProfiler::new();
profiler.record_sample(0x1000, vec![], "main");
profiler.reset();
assert_eq!(profiler.total_samples(), 0);
assert!(profiler.function_samples.is_empty());
}
#[test]
fn test_sampling_collect_samples() {
let mut profiler = X86SamplingProfiler::new();
let result = profiler.collect_samples("test_bin", 5);
assert!(result.is_some());
assert_eq!(result.unwrap().duration_secs, 5);
}
#[test]
fn test_topdown_result_defaults() {
let result = X86TopDownResult {
retiring_pct: 40.0,
bad_speculation_pct: 10.0,
frontend_bound_pct: 25.0,
backend_bound_pct: 25.0,
total_pipeline_slots: 1000,
analysis_timestamp: 0,
};
assert!(result.is_valid());
assert_eq!(result.dominant_bottleneck(), TOPDOWN_RETIRING);
}
#[test]
fn test_topdown_dominant_backend() {
let result = X86TopDownResult {
retiring_pct: 20.0,
bad_speculation_pct: 10.0,
frontend_bound_pct: 15.0,
backend_bound_pct: 55.0,
total_pipeline_slots: 1000,
analysis_timestamp: 0,
};
assert_eq!(result.dominant_bottleneck(), TOPDOWN_BACKEND_BOUND);
}
#[test]
fn test_topdown_is_valid_tolerance() {
let result = X86TopDownResult {
retiring_pct: 40.1,
bad_speculation_pct: 20.2,
frontend_bound_pct: 20.3,
backend_bound_pct: 20.2,
total_pipeline_slots: 1000,
analysis_timestamp: 0,
};
assert!(result.is_valid());
}
#[test]
fn test_sampling_analyze_topdown() {
let mut profiler = X86SamplingProfiler::new();
let sr = X86SamplingResult {
total_samples: 1000,
sample_freq: 99,
duration_secs: 10,
binary: "test".to_string(),
function_samples: HashMap::new(),
collapsed_stacks: HashMap::new(),
topdown_metrics: HashMap::new(),
lbr_data: Vec::new(),
source_location_map: HashMap::new(),
};
let result = profiler.analyze_topdown(&sr);
assert!(result.total_pipeline_slots > 0);
}
#[test]
fn test_sampling_function_topdown_metrics() {
let mut profiler = X86SamplingProfiler::new();
profiler.set_function_topdown(
"fast_func",
X86FunctionTopDownMetrics {
function: "fast_func".to_string(),
retiring_pct: 60.0,
bad_speculation_pct: 5.0,
frontend_bound_pct: 10.0,
backend_bound_pct: 25.0,
backend_memory_bound_pct: 15.0,
backend_core_bound_pct: 10.0,
sample_count: 500,
cpi: 0.5,
},
);
let m = profiler.get_function_topdown("fast_func").unwrap();
assert!((m.retiring_pct - 60.0).abs() < 0.01);
}
#[test]
fn test_sampling_functions_by_metric() {
let mut profiler = X86SamplingProfiler::new();
profiler.set_function_topdown(
"f1",
X86FunctionTopDownMetrics {
function: "f1".to_string(),
frontend_bound_pct: 50.0,
..Default::default()
},
);
profiler.set_function_topdown(
"f2",
X86FunctionTopDownMetrics {
function: "f2".to_string(),
frontend_bound_pct: 20.0,
..Default::default()
},
);
let by_fe = profiler.functions_by_metric(X86TopDownMetric::FrontendBound);
assert_eq!(by_fe[0].0, "f1");
assert!((by_fe[0].1 - 50.0).abs() < 0.01);
}
#[test]
fn test_sampling_frontend_bound_functions() {
let mut profiler = X86SamplingProfiler::new();
profiler.set_function_topdown(
"slow_fe",
X86FunctionTopDownMetrics {
function: "slow_fe".to_string(),
frontend_bound_pct: 60.0,
..Default::default()
},
);
profiler.set_function_topdown(
"fast_fe",
X86FunctionTopDownMetrics {
function: "fast_fe".to_string(),
frontend_bound_pct: 5.0,
..Default::default()
},
);
let bound = profiler.frontend_bound_functions(30.0);
assert_eq!(bound.len(), 1);
assert_eq!(bound[0], "slow_fe");
}
#[test]
fn test_sampling_backend_bound_functions() {
let mut profiler = X86SamplingProfiler::new();
profiler.set_function_topdown(
"mem_heavy",
X86FunctionTopDownMetrics {
function: "mem_heavy".to_string(),
backend_bound_pct: 70.0,
..Default::default()
},
);
let bound = profiler.backend_bound_functions(50.0);
assert_eq!(bound.len(), 1);
}
#[test]
fn test_sampling_bad_speculation_functions() {
let mut profiler = X86SamplingProfiler::new();
profiler.set_function_topdown(
"branchy",
X86FunctionTopDownMetrics {
function: "branchy".to_string(),
bad_speculation_pct: 35.0,
..Default::default()
},
);
let bad = profiler.bad_speculation_functions(20.0);
assert_eq!(bad.len(), 1);
}
#[test]
fn test_instrumented_profiler_create() {
let profiler = X86InstrumentedProfiler::new();
assert!(!profiler.active);
assert_eq!(profiler.total_calls, 0);
}
#[test]
fn test_instrumented_start_stop() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
assert!(profiler.active);
profiler.stop();
assert!(!profiler.active);
}
#[test]
fn test_instrumented_function_enter_exit() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
profiler.function_enter("main", 0x400000);
assert_eq!(profiler.call_count("main"), 1);
assert_eq!(profiler.call_depth(), 1);
profiler.function_exit("main", 0x400000);
assert_eq!(profiler.call_depth(), 0);
assert!(profiler.total_duration("main") > 0);
}
#[test]
fn test_instrumented_inactive_ignores_calls() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.function_enter("main", 0x400000);
assert_eq!(profiler.call_count("main"), 0);
}
#[test]
fn test_instrumented_nested_calls() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
profiler.function_enter("main", 0x400000);
profiler.function_enter("foo", 0x400100);
profiler.function_enter("bar", 0x400200);
assert_eq!(profiler.call_depth(), 3);
assert_eq!(profiler.current_function(), Some("bar"));
profiler.function_exit("bar", 0x400200);
profiler.function_exit("foo", 0x400100);
profiler.function_exit("main", 0x400000);
assert_eq!(profiler.call_depth(), 0);
}
#[test]
fn test_instrumented_current_function_empty() {
let profiler = X86InstrumentedProfiler::new();
assert_eq!(profiler.current_function(), None);
}
#[test]
fn test_instrumented_call_count() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
profiler.function_exit("f", 0x1000);
profiler.function_enter("f", 0x1000);
profiler.function_exit("f", 0x1000);
assert_eq!(profiler.call_count("f"), 2);
assert_eq!(profiler.total_calls, 2);
}
#[test]
fn test_instrumented_most_called_functions() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
for _ in 0..5 {
profiler.function_enter("hot", 0x1000);
profiler.function_exit("hot", 0x1000);
}
for _ in 0..3 {
profiler.function_enter("warm", 0x2000);
profiler.function_exit("warm", 0x2000);
}
let top = profiler.most_called_functions(2);
assert_eq!(top.len(), 2);
assert_eq!(top[0].0, "hot");
}
#[test]
fn test_instrumented_distinct_functions() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
profiler.function_enter("a", 0x1000);
profiler.function_exit("a", 0x1000);
profiler.function_enter("b", 0x2000);
profiler.function_exit("b", 0x2000);
profiler.function_enter("a", 0x1000);
profiler.function_exit("a", 0x1000);
assert_eq!(profiler.distinct_functions(), 2);
}
#[test]
fn test_instrumented_total_duration() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
profiler.function_enter("slow", 0x1000);
std::thread::sleep(std::time::Duration::from_millis(10));
profiler.function_exit("slow", 0x1000);
let duration = profiler.total_duration("slow");
assert!(duration > 5_000_000); }
#[test]
fn test_instrumented_avg_call_duration() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
profiler.function_enter("func", 0x1000);
std::thread::sleep(std::time::Duration::from_millis(1));
profiler.function_exit("func", 0x1000);
let avg = profiler.avg_call_duration("func");
assert!(avg.is_some());
assert!(avg.unwrap() > 500_000.0); }
#[test]
fn test_instrumented_avg_call_duration_no_calls() {
let profiler = X86InstrumentedProfiler::new();
assert_eq!(profiler.avg_call_duration("nonexistent"), None);
}
#[test]
fn test_instrumented_call_duration_stddev() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
for _ in 0..1 {
profiler.function_enter("func", 0x1000);
profiler.function_exit("func", 0x1000);
}
assert!(profiler.call_duration_stddev("func").is_none());
profiler.function_enter("func", 0x1000);
profiler.function_exit("func", 0x1000);
assert!(profiler.call_duration_stddev("func").is_some());
}
#[test]
fn test_instrumented_hottest_by_time() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
profiler.function_enter("quick", 0x1000);
profiler.function_exit("quick", 0x1000);
profiler.function_enter("slow", 0x2000);
std::thread::sleep(std::time::Duration::from_millis(5));
profiler.function_exit("slow", 0x2000);
let hottest = profiler.hottest_functions_by_time(2);
assert!(!hottest.is_empty());
}
#[test]
fn test_instrumented_slowest_functions() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
profiler.function_enter("fast", 0x1000);
profiler.function_exit("fast", 0x1000);
profiler.function_enter("slow", 0x2000);
std::thread::sleep(std::time::Duration::from_millis(3));
profiler.function_exit("slow", 0x2000);
let slowest = profiler.slowest_functions(2);
assert!(!slowest.is_empty());
}
#[test]
fn test_instrumented_call_graph() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
profiler.function_enter("main", 0x400000);
profiler.function_enter("foo", 0x400100);
profiler.function_exit("foo", 0x400100);
profiler.function_enter("bar", 0x400200);
profiler.function_exit("bar", 0x400200);
profiler.function_exit("main", 0x400000);
profiler.build_call_graph();
let callees = profiler.callees_of("main");
assert!(!callees.is_empty());
}
#[test]
fn test_instrumented_callers_of() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
profiler.function_enter("caller", 0x400000);
profiler.function_enter("callee", 0x400100);
profiler.function_exit("callee", 0x400100);
profiler.function_exit("caller", 0x400000);
profiler.build_call_graph();
let callers = profiler.callers_of("callee");
assert!(!callers.is_empty());
assert_eq!(callers[0].0, "caller");
}
#[test]
fn test_instrumented_export_dot() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
profiler.function_enter("main", 0x400000);
profiler.function_enter("helper", 0x400100);
profiler.function_exit("helper", 0x400100);
profiler.function_exit("main", 0x400000);
profiler.build_call_graph();
let dot = profiler.export_dot();
assert!(dot.starts_with("digraph"));
assert!(dot.contains("main"));
assert!(dot.contains("helper"));
}
#[test]
fn test_instrumented_hot_paths() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
profiler.function_enter("hot_path_fn", 0x400000);
std::thread::sleep(std::time::Duration::from_millis(10));
profiler.function_exit("hot_path_fn", 0x400000);
let paths = profiler.identify_hot_paths(5);
assert!(!paths.is_empty());
}
#[test]
fn test_instrumented_critical_path() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
profiler.function_enter("heavy", 0x400000);
std::thread::sleep(std::time::Duration::from_millis(5));
profiler.function_exit("heavy", 0x400000);
let critical = profiler.critical_path();
assert!(!critical.is_empty());
}
#[test]
fn test_instrumented_build_function_profiles() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
profiler.function_enter("test_func", 0x400000);
profiler.function_exit("test_func", 0x400000);
profiler.build_function_profiles();
let profile = profiler.get_function_profile("test_func");
assert!(profile.is_some());
assert_eq!(profile.unwrap().call_count, 1);
}
#[test]
fn test_instrumented_get_profile() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
profiler.function_enter("func_a", 0x400000);
profiler.function_exit("func_a", 0x400000);
let profile = profiler.get_profile();
assert!(profile.is_some());
assert_eq!(profile.unwrap().total_calls, 1);
}
#[test]
fn test_instrumented_get_profile_empty() {
let profiler = X86InstrumentedProfiler::new();
assert!(profiler.get_profile().is_none());
}
#[test]
fn test_instrumented_reset() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
profiler.function_enter("f", 0x1000);
profiler.function_exit("f", 0x1000);
profiler.reset();
assert_eq!(profiler.call_count("f"), 0);
assert_eq!(profiler.total_calls, 0);
assert!(profiler.call_counts.is_empty());
}
#[test]
fn test_instrumented_min_max_duration() {
let mut profiler = X86InstrumentedProfiler::new();
profiler.start();
profiler.function_enter("f", 0x1000);
profiler.function_exit("f", 0x1000);
profiler.function_enter("f", 0x1000);
std::thread::sleep(std::time::Duration::from_millis(2));
profiler.function_exit("f", 0x1000);
let min = profiler.min_call_duration("f").unwrap();
let max = profiler.max_call_duration("f").unwrap();
assert!(max > min);
}
#[test]
fn test_function_profile_all_fields() {
let profile = X86FunctionProfile {
name: "full_test".to_string(),
call_count: 100,
total_duration_ns: 50_000_000,
self_duration_ns: 30_000_000,
avg_call_duration_ns: Some(500_000.0),
max_call_duration_ns: Some(2_000_000),
min_call_duration_ns: Some(100_000),
stddev_call_duration_ns: Some(200_000.0),
num_callers: 3,
num_callees: 5,
};
assert_eq!(profile.name, "full_test");
assert_eq!(profile.call_count, 100);
assert_eq!(profile.num_callers, 3);
}
#[test]
fn test_hot_path_struct() {
let hp = X86HotPath {
functions: vec!["a".to_string(), "b".to_string(), "c".to_string()],
total_duration_ns: 10_000_000,
call_count: 500,
percentage: 25.0,
is_recursive: false,
};
assert_eq!(hp.functions.len(), 3);
assert!((hp.percentage - 25.0).abs() < 0.01);
}
#[test]
fn test_visualizer_create() {
let viz = X86ProfileVisualizer::new();
assert_eq!(viz.flamegraph_width, X86_FLAMEGRAPH_DEFAULT_WIDTH);
assert_eq!(viz.flamegraph_frame_height, X86_FLAMEGRAPH_FRAME_HEIGHT);
}
#[test]
fn test_visualizer_flamegraph_svg_empty() {
let viz = X86ProfileVisualizer::new();
let collapsed: HashMap<String, u64> = HashMap::new();
assert!(viz.generate_flamegraph_svg(&collapsed, "Empty").is_none());
}
#[test]
fn test_visualizer_flamegraph_svg_basic() {
let viz = X86ProfileVisualizer::new();
let mut collapsed = HashMap::new();
collapsed.insert("main;foo;bar".to_string(), 100);
collapsed.insert("main;foo;baz".to_string(), 50);
let svg = viz.generate_flamegraph_svg(&collapsed, "Test Flamegraph");
assert!(svg.is_some());
let svg_str = svg.unwrap();
assert!(svg_str.contains("<svg"));
assert!(svg_str.contains("main"));
assert!(svg_str.contains("Test Flamegraph"));
}
#[test]
fn test_visualizer_flamegraph_to_collapsed() {
let viz = X86ProfileVisualizer::new();
assert!(viz.flamegraph_to_collapsed("<svg>...</svg>").is_none());
}
#[test]
fn test_visualizer_call_graph_dot() {
let viz = X86ProfileVisualizer::new();
let mut call_graph: HashMap<String, Vec<(String, u64)>> = HashMap::new();
call_graph.insert("main".to_string(), vec![("foo".to_string(), 10)]);
let mut call_counts = HashMap::new();
call_counts.insert("main".to_string(), 10u64);
call_counts.insert("foo".to_string(), 5u64);
let dot = viz.generate_call_graph_dot(&call_graph, &call_counts, "Test");
assert!(dot.starts_with("digraph"));
assert!(dot.contains("main"));
assert!(dot.contains("foo"));
}
#[test]
fn test_visualizer_chrome_tracing_json_empty() {
let viz = X86ProfileVisualizer::new();
let profile = X86InstrumentedProfile {
total_calls: 0,
distinct_functions: 0,
call_counts: HashMap::new(),
durations_ns: HashMap::new(),
call_graph: HashMap::new(),
function_profiles: HashMap::new(),
hot_paths: Vec::new(),
timestamp: 0,
};
assert!(viz.generate_chrome_tracing_json(&profile).is_none());
}
#[test]
fn test_visualizer_chrome_tracing_json_basic() {
let viz = X86ProfileVisualizer::new();
let mut profiles = HashMap::new();
profiles.insert(
"main".to_string(),
X86FunctionProfile {
name: "main".to_string(),
call_count: 1,
total_duration_ns: 100_000,
self_duration_ns: 100_000,
avg_call_duration_ns: Some(100_000.0),
max_call_duration_ns: Some(100_000),
min_call_duration_ns: Some(100_000),
stddev_call_duration_ns: None,
num_callers: 0,
num_callees: 0,
},
);
let profile = X86InstrumentedProfile {
total_calls: 1,
distinct_functions: 1,
call_counts: {
let mut m = HashMap::new();
m.insert("main".to_string(), 1);
m
},
durations_ns: {
let mut m = HashMap::new();
m.insert("main".to_string(), 100_000);
m
},
call_graph: HashMap::new(),
function_profiles: profiles,
hot_paths: Vec::new(),
timestamp: 0,
};
let json = viz.generate_chrome_tracing_json(&profile);
assert!(json.is_some());
assert!(json.unwrap().contains("main"));
}
#[test]
fn test_visualizer_sampling_trace_json_empty() {
let viz = X86ProfileVisualizer::new();
let sr = X86SamplingResult {
total_samples: 0,
sample_freq: 99,
duration_secs: 0,
binary: "".to_string(),
function_samples: HashMap::new(),
collapsed_stacks: HashMap::new(),
topdown_metrics: HashMap::new(),
lbr_data: Vec::new(),
source_location_map: HashMap::new(),
};
assert!(viz.generate_sampling_trace_json(&sr).is_none());
}
#[test]
fn test_visualizer_hotspot_table() {
let viz = X86ProfileVisualizer::new();
let mut samples = HashMap::new();
samples.insert("hot_func".to_string(), 500);
samples.insert("cold_func".to_string(), 10);
let html = viz.generate_hotspot_table(&samples, "Hotspots");
assert!(html.contains("<!DOCTYPE html>"));
assert!(html.contains("hot_func"));
assert!(html.contains("500"));
}
#[test]
fn test_visualizer_function_bar_chart() {
let viz = X86ProfileVisualizer::new();
let mut data = HashMap::new();
data.insert("a".to_string(), 100);
data.insert("b".to_string(), 50);
let svg = viz.generate_function_bar_chart(&data, "Bar Chart", 10);
assert!(svg.is_some());
assert!(svg.unwrap().contains("<svg"));
}
#[test]
fn test_visualizer_function_bar_chart_empty() {
let viz = X86ProfileVisualizer::new();
let data = HashMap::new();
assert!(viz
.generate_function_bar_chart(&data, "Empty", 10)
.is_none());
}
#[test]
fn test_visualizer_pie_chart() {
let viz = X86ProfileVisualizer::new();
let mut data = HashMap::new();
data.insert("slice1".to_string(), 100);
data.insert("slice2".to_string(), 50);
let svg = viz.generate_pie_chart_svg(&data, "Pie Chart", 10);
assert!(svg.is_some());
assert!(svg.unwrap().contains("<svg"));
}
#[test]
fn test_visualizer_pie_chart_empty() {
let viz = X86ProfileVisualizer::new();
let data = HashMap::new();
assert!(viz.generate_pie_chart_svg(&data, "Empty", 10).is_none());
}
#[test]
fn test_polar_to_cartesian() {
let (x, y) = X86ProfileVisualizer::polar_to_cartesian(100.0, 100.0, 50.0, 0.0);
assert!((x - 150.0).abs() < 0.01);
assert!((y - 100.0).abs() < 0.01);
let (x2, y2) = X86ProfileVisualizer::polar_to_cartesian(100.0, 100.0, 50.0, 90.0);
assert!((x2 - 100.0).abs() < 0.01);
assert!((y2 - 150.0).abs() < 0.01);
}
#[test]
fn test_flamegraph_palette_hot() {
let palette = X86FlamegraphPalette::hot_palette();
assert_eq!(palette.hot, "#FF0000");
}
#[test]
fn test_flamegraph_palette_cool() {
let palette = X86FlamegraphPalette::cool_palette();
assert_eq!(palette.hot, "#00FF00");
}
#[test]
fn test_flamegraph_palette_mono() {
let palette = X86FlamegraphPalette::mono_palette();
assert_eq!(palette.hot, "#333333");
}
#[test]
fn test_flamegraph_palette_color_for_function() {
let palette = X86FlamegraphPalette::default();
let color = palette.color_for_function("test_func", 50, 100);
assert!(color.starts_with("hsl"));
}
#[test]
fn test_flamegraph_palette_color_zero_total() {
let palette = X86FlamegraphPalette::default();
let color = palette.color_for_function("test_func", 10, 0);
assert!(color.starts_with("hsl"));
}
#[test]
fn test_flamegraph_palette_gradient_colors() {
let palette = X86FlamegraphPalette::hot_palette();
assert_eq!(palette.hot_gradient_start(), "#FF0000");
}
#[test]
fn test_comparison_create() {
let comp = X86ProfileComparison::new();
assert!(comp.baseline.is_none());
assert!(comp.target.is_none());
assert_eq!(comp.regression_threshold_pct, 5.0);
assert_eq!(comp.improvement_threshold_pct, 5.0);
}
#[test]
fn test_comparison_set_thresholds() {
let mut comp = X86ProfileComparison::new();
comp.set_regression_threshold(10.0);
comp.set_improvement_threshold(3.0);
assert_eq!(comp.regression_threshold_pct, 10.0);
assert_eq!(comp.improvement_threshold_pct, 3.0);
}
#[test]
fn test_comparison_compare_empty() {
let mut comp = X86ProfileComparison::new();
let baseline = X86ProfileData {
binary: "base".to_string(),
timestamp: 0,
total_samples: 0,
function_samples: HashMap::new(),
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let target = baseline.clone();
let report = comp.compare(&baseline, &target);
assert_eq!(report.overall.total_samples_baseline, 0);
assert_eq!(report.regressions.len(), 0);
assert_eq!(report.improvements.len(), 0);
}
#[test]
fn test_comparison_detect_regression() {
let mut comp = X86ProfileComparison::new();
let mut base_samples = HashMap::new();
base_samples.insert("func".to_string(), 100);
let mut tgt_samples = HashMap::new();
tgt_samples.insert("func".to_string(), 120);
let baseline = X86ProfileData {
binary: "base".to_string(),
timestamp: 0,
total_samples: 100,
function_samples: base_samples,
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let target = X86ProfileData {
binary: "tgt".to_string(),
timestamp: 1,
total_samples: 120,
function_samples: tgt_samples,
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let report = comp.compare(&baseline, &target);
assert_eq!(report.regressions.len(), 1);
assert_eq!(report.regressions[0].function, "func");
assert!((report.regressions[0].increase_pct - 20.0).abs() < 0.01);
}
#[test]
fn test_comparison_detect_improvement() {
let mut comp = X86ProfileComparison::new();
let mut base_samples = HashMap::new();
base_samples.insert("optimized_func".to_string(), 100);
let mut tgt_samples = HashMap::new();
tgt_samples.insert("optimized_func".to_string(), 70);
let baseline = X86ProfileData {
binary: "base".to_string(),
timestamp: 0,
total_samples: 100,
function_samples: base_samples,
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let target = X86ProfileData {
binary: "tgt".to_string(),
timestamp: 1,
total_samples: 70,
function_samples: tgt_samples,
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let report = comp.compare(&baseline, &target);
assert_eq!(report.improvements.len(), 1);
assert_eq!(report.improvements[0].function, "optimized_func");
assert!((report.improvements[0].decrease_pct - 30.0).abs() < 0.01);
}
#[test]
fn test_comparison_new_function() {
let mut comp = X86ProfileComparison::new();
let baseline = X86ProfileData {
binary: "base".to_string(),
timestamp: 0,
total_samples: 0,
function_samples: HashMap::new(),
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let mut tgt_samples = HashMap::new();
tgt_samples.insert("new_func".to_string(), 10);
let target = X86ProfileData {
binary: "tgt".to_string(),
timestamp: 1,
total_samples: 10,
function_samples: tgt_samples,
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let report = comp.compare(&baseline, &target);
assert_eq!(report.new_functions.len(), 1);
assert_eq!(report.new_functions[0], "new_func");
assert_eq!(report.overall.new_functions, 1);
}
#[test]
fn test_comparison_removed_function() {
let mut comp = X86ProfileComparison::new();
let mut base_samples = HashMap::new();
base_samples.insert("old_func".to_string(), 10);
let baseline = X86ProfileData {
binary: "base".to_string(),
timestamp: 0,
total_samples: 10,
function_samples: base_samples,
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let target = X86ProfileData {
binary: "tgt".to_string(),
timestamp: 1,
total_samples: 0,
function_samples: HashMap::new(),
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let report = comp.compare(&baseline, &target);
assert_eq!(report.removed_functions.len(), 1);
assert_eq!(report.overall.removed_functions, 1);
}
#[test]
fn test_comparison_severity_classification() {
let comp = X86ProfileComparison::new();
assert_eq!(comp.classify_severity(5.0), X86RegressionSeverity::Minor);
assert_eq!(
comp.classify_severity(15.0),
X86RegressionSeverity::Moderate
);
assert_eq!(comp.classify_severity(30.0), X86RegressionSeverity::Major);
assert_eq!(
comp.classify_severity(60.0),
X86RegressionSeverity::Critical
);
}
#[test]
fn test_comparison_severe_regressions() {
let mut comp = X86ProfileComparison::new();
let mut base = HashMap::new();
base.insert("minor_reg".to_string(), 100);
base.insert("major_reg".to_string(), 100);
base.insert("critical_reg".to_string(), 100);
let mut tgt = HashMap::new();
tgt.insert("minor_reg".to_string(), 106); tgt.insert("major_reg".to_string(), 130); tgt.insert("critical_reg".to_string(), 160); let baseline = X86ProfileData {
binary: "base".to_string(),
timestamp: 0,
total_samples: 300,
function_samples: base,
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let target = X86ProfileData {
binary: "tgt".to_string(),
timestamp: 1,
total_samples: 396,
function_samples: tgt,
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let _report = comp.compare(&baseline, &target);
let severe = comp.severe_regressions();
assert_eq!(severe.len(), 2); }
#[test]
fn test_comparison_regression_score() {
let mut comp = X86ProfileComparison::new();
let mut base = HashMap::new();
base.insert("f".to_string(), 100);
let mut tgt = HashMap::new();
tgt.insert("f".to_string(), 160); let baseline = X86ProfileData {
binary: "base".to_string(),
timestamp: 0,
total_samples: 100,
function_samples: base,
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let target = X86ProfileData {
binary: "tgt".to_string(),
timestamp: 1,
total_samples: 160,
function_samples: tgt,
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
comp.compare(&baseline, &target);
let score = comp.regression_score();
assert!((score - 10.0).abs() < 0.01); }
#[test]
fn test_comparison_no_regression_below_threshold() {
let mut comp = X86ProfileComparison::new();
let mut base = HashMap::new();
base.insert("stable".to_string(), 100);
let mut tgt = HashMap::new();
tgt.insert("stable".to_string(), 103); let baseline = X86ProfileData {
binary: "base".to_string(),
timestamp: 0,
total_samples: 100,
function_samples: base,
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let target = X86ProfileData {
binary: "tgt".to_string(),
timestamp: 1,
total_samples: 103,
function_samples: tgt,
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let report = comp.compare(&baseline, &target);
assert_eq!(report.regressions.len(), 0);
}
#[test]
fn test_comparison_insufficient_sample_count() {
let mut comp = X86ProfileComparison::new();
let mut base = HashMap::new();
base.insert("rare".to_string(), 5); let mut tgt = HashMap::new();
tgt.insert("rare".to_string(), 10);
let baseline = X86ProfileData {
binary: "base".to_string(),
timestamp: 0,
total_samples: 5,
function_samples: base,
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let target = X86ProfileData {
binary: "tgt".to_string(),
timestamp: 1,
total_samples: 10,
function_samples: tgt,
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let report = comp.compare(&baseline, &target);
assert_eq!(report.regressions.len(), 0);
}
#[test]
fn test_comparison_generate_summary_report() {
let mut comp = X86ProfileComparison::new();
let mut base = HashMap::new();
base.insert("func".to_string(), 100);
let mut tgt = HashMap::new();
tgt.insert("func".to_string(), 120);
let baseline = X86ProfileData {
binary: "before".to_string(),
timestamp: 0,
total_samples: 100,
function_samples: base,
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let target = X86ProfileData {
binary: "after".to_string(),
timestamp: 1,
total_samples: 120,
function_samples: tgt,
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
comp.compare(&baseline, &target);
let report = comp.generate_summary_report();
assert!(report.contains("before"));
assert!(report.contains("after"));
assert!(report.contains("Regressions"));
}
#[test]
fn test_comparison_generate_summary_report_no_data() {
let comp = X86ProfileComparison::new();
let report = comp.generate_summary_report();
assert!(report.contains("No comparison data"));
}
#[test]
fn test_comparison_collapsed_stack_diff() {
let mut comp = X86ProfileComparison::new();
let mut base_stacks = HashMap::new();
base_stacks.insert("main;foo".to_string(), 100);
let mut tgt_stacks = HashMap::new();
tgt_stacks.insert("main;foo".to_string(), 80);
tgt_stacks.insert("main;bar".to_string(), 30);
let baseline = X86ProfileData {
binary: "base".to_string(),
timestamp: 0,
total_samples: 100,
function_samples: HashMap::new(),
collapsed_stacks: base_stacks,
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let target = X86ProfileData {
binary: "tgt".to_string(),
timestamp: 1,
total_samples: 110,
function_samples: HashMap::new(),
collapsed_stacks: tgt_stacks,
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let report = comp.compare(&baseline, &target);
assert!(!report.collapsed_stack_diff.is_empty());
}
#[test]
fn test_comparison_diff_flamegraph() {
let comp = X86ProfileComparison::new();
let visualizer = X86ProfileVisualizer::new();
let mut base = HashMap::new();
base.insert("main;foo".to_string(), 100);
let mut tgt = HashMap::new();
tgt.insert("main;foo".to_string(), 80);
tgt.insert("main;bar".to_string(), 30);
let svg = comp.generate_diff_flamegraph(&base, &tgt, &visualizer);
assert!(svg.is_some());
let svg_str = svg.unwrap();
assert!(svg_str.contains("Difference Flame Graph"));
}
#[test]
fn test_comparison_diff_flamegraph_no_diff() {
let comp = X86ProfileComparison::new();
let visualizer = X86ProfileVisualizer::new();
let mut base = HashMap::new();
base.insert("main".to_string(), 100);
let tgt = base.clone();
assert!(comp
.generate_diff_flamegraph(&base, &tgt, &visualizer)
.is_none());
}
#[test]
fn test_e2e_profiling_to_visualization() {
let mut tools = X86ProfilingTools::default();
let session = tools.run_full_session("test_bin", 2);
let data = session.to_profile_data();
assert_eq!(data.binary, "test_bin");
let viz = X86ProfileVisualizer::new();
let svg = viz.generate_flamegraph_svg(&data.collapsed_stacks, "E2E Test");
if !data.collapsed_stacks.is_empty() {
assert!(svg.is_some());
}
}
#[test]
fn test_e2e_sampling_with_counter_config() {
let mut tools = X86ProfilingTools::default();
tools.sampling.set_sample_freq(200);
let result = tools.sampling.collect_samples("test_bin", 1);
assert!(result.is_some());
let sr = result.unwrap();
assert_eq!(sr.sample_freq, 200);
assert_eq!(sr.binary, "test_bin");
}
#[test]
fn test_e2e_instrumented_to_call_graph() {
let mut tools = X86ProfilingTools::default();
tools.instrumented.start();
tools.instrumented.function_enter("main", 0x400000);
tools.instrumented.function_enter("helper", 0x400100);
tools.instrumented.function_exit("helper", 0x400100);
tools.instrumented.function_exit("main", 0x400000);
tools.instrumented.build_call_graph();
let dot = tools.instrumented.export_dot();
assert!(dot.contains("main"));
assert!(dot.contains("helper"));
}
#[test]
fn test_e2e_comparison_pipeline() {
let mut tools = X86ProfilingTools::default();
tools.sampling.record_sample(0x1000, vec![], "slow_func");
tools.sampling.record_sample(0x1000, vec![], "slow_func");
let before = tools.run_full_session("app_v1", 1);
tools.sampling.reset();
tools.sampling.record_sample(0x1000, vec![], "slow_func");
tools.sampling.record_sample(0x2000, vec![], "fast_func");
let after = tools.run_full_session("app_v2", 1);
let _report = tools.compare_sessions(&before, &after);
}
#[test]
fn test_e2e_vtune_to_hotspot_table() {
let mut tools = X86ProfilingTools::default();
tools.vtune.hotspot_results = vec![X86VTuneHotspotResult {
function: "compute_kernel".to_string(),
module: "app".to_string(),
cpu_time_pct: 85.0,
cpu_time_sec: 8.5,
instructions_retired: 50_000_000,
cpi_rate: 1.2,
}];
let top = tools.vtune.top_hotspots(1);
assert_eq!(top[0].function, "compute_kernel");
let high_cpi = tools.vtune.high_cpi_functions(1.0);
assert!(!high_cpi.is_empty());
}
#[test]
fn test_e2e_uprof_l3_to_visualization() {
let mut tools = X86ProfilingTools::default();
tools.uprof.record_l3_cache_miss(X86AMDL3CacheMiss {
function: "stream".to_string(),
source_file: "stream.c".to_string(),
source_line: 42,
count: 10000,
address: 0x7f000000,
l3_slice: 1,
is_prefetch: false,
ccd_index: 0,
});
assert_eq!(tools.uprof.total_l3_misses(), 10000);
let hotspots = tools.uprof.analyze_l3_hotspots();
assert_eq!(hotspots[0].function, "stream");
}
#[test]
fn test_x86_profiling_session_clone() {
let mut tools = X86ProfilingTools::default();
let session = tools.run_full_session("test", 1);
let cloned = session.clone();
assert_eq!(cloned.binary, session.binary);
}
#[test]
fn test_x86_profile_data_clone() {
let data = X86ProfileData {
binary: "app".to_string(),
timestamp: 12345,
total_samples: 1000,
function_samples: HashMap::new(),
collapsed_stacks: HashMap::new(),
function_call_counts: HashMap::new(),
function_durations_ns: HashMap::new(),
counter_values: Vec::new(),
topdown_metrics: HashMap::new(),
};
let cloned = data.clone();
assert_eq!(cloned.binary, "app");
assert_eq!(cloned.total_samples, 1000);
}
#[test]
fn test_unwind_method_all_variants() {
assert_ne!(X86UnwindMethod::FramePointer, X86UnwindMethod::DWARF);
assert_ne!(X86UnwindMethod::LBR, X86UnwindMethod::IntelPT);
assert_ne!(X86UnwindMethod::Hybrid, X86UnwindMethod::FramePointer);
}
#[test]
fn test_regression_severity_ordering() {
assert!(X86RegressionSeverity::Critical > X86RegressionSeverity::Major);
assert!(X86RegressionSeverity::Major > X86RegressionSeverity::Moderate);
assert!(X86RegressionSeverity::Moderate > X86RegressionSeverity::Minor);
}
#[test]
fn test_topdown_metric_all_variants() {
let _metrics = [
X86TopDownMetric::Retiring,
X86TopDownMetric::BadSpeculation,
X86TopDownMetric::FrontendBound,
X86TopDownMetric::BackendBound,
X86TopDownMetric::BackendMemoryBound,
X86TopDownMetric::BackendCoreBound,
];
}
#[test]
fn test_derived_metrics_default() {
let m = X86DerivedMetrics::default();
assert_eq!(m.total_cycles, 0);
assert_eq!(m.total_instructions, 0);
assert_eq!(m.cache_miss_rate, 0.0);
}
#[test]
fn test_ibs_config_defaults() {
let fetch = X86IBSFetchConfig::default();
assert!(!fetch.enabled);
assert!(fetch.randomized_period);
let op = X86IBSOpConfig::default();
assert!(!op.enabled);
assert!(op.randomized_period);
}
#[test]
fn test_constants_are_consistent() {
assert!(X86_MAX_SIMULTANEOUS_HW_COUNTERS > 0);
assert!(X86_DEFAULT_SAMPLE_FREQ > 0);
assert!(X86_MAX_UNWIND_DEPTH > 0);
assert!(X86_MIN_SAMPLE_COUNT > 0);
assert!(X86_FLAMEGRAPH_DEFAULT_WIDTH > 0);
assert!(X86_FLAMEGRAPH_FRAME_HEIGHT > 0);
assert!(AMD_IBS_DEFAULT_FETCH_PERIOD > 0);
assert!(AMD_IBS_DEFAULT_OP_PERIOD > 0);
}
#[test]
fn test_perf_sample_type_constants() {
assert_ne!(PERF_SAMPLE_IP, PERF_SAMPLE_TID);
assert_ne!(PERF_SAMPLE_TIME, PERF_SAMPLE_ADDR);
}
}