use crate::{ProfileEvent, TorshResult};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use torsh_core::TorshError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstrumentsConfig {
pub signpost_enabled: bool,
pub time_profiling: bool,
pub allocations_tracking: bool,
pub leaks_detection: bool,
pub energy_tracking: bool,
pub activity_tracing: bool,
pub system_trace: bool,
pub sampling_interval_us: u64,
pub output_dir: Option<String>,
pub device_udid: Option<String>,
}
impl Default for InstrumentsConfig {
fn default() -> Self {
Self {
signpost_enabled: true,
time_profiling: true,
allocations_tracking: false,
leaks_detection: false,
energy_tracking: false,
activity_tracing: true,
system_trace: false,
sampling_interval_us: 1000, output_dir: None,
device_udid: None,
}
}
}
pub struct InstrumentsProfiler {
config: InstrumentsConfig,
events: Arc<Mutex<Vec<ProfileEvent>>>,
start_time: Instant,
enabled: bool,
session_id: String,
trace_id: u64,
}
impl InstrumentsProfiler {
pub fn new(config: InstrumentsConfig) -> Self {
Self {
config,
events: Arc::new(Mutex::new(Vec::new())),
start_time: Instant::now(),
enabled: false,
session_id: format!("instruments_session_{}", chrono::Utc::now().timestamp()),
trace_id: 0,
}
}
pub fn enable(&mut self) -> TorshResult<()> {
self.enabled = true;
self.start_time = Instant::now();
self.trace_id += 1;
if let Ok(mut events) = self.events.lock() {
events.clear();
}
if self.config.signpost_enabled {
self.init_signpost()?;
}
self.start_instruments_trace()?;
Ok(())
}
pub fn disable(&mut self) -> TorshResult<()> {
self.enabled = false;
self.stop_instruments_trace()?;
if self.config.signpost_enabled {
self.finalize_signpost()?;
}
Ok(())
}
pub fn start_signpost_interval(
&self,
name: &str,
category: &str,
) -> TorshResult<SignpostInterval> {
if !self.enabled || !self.config.signpost_enabled {
return Ok(SignpostInterval::new_disabled());
}
let start_time = Instant::now();
let interval = SignpostInterval::new(name.to_string(), category.to_string(), start_time);
Ok(interval)
}
pub fn emit_signpost_event(
&self,
name: &str,
category: &str,
message: &str,
) -> TorshResult<()> {
if !self.enabled || !self.config.signpost_enabled {
return Ok(());
}
let mut events = self.events.lock().map_err(|_| {
TorshError::InvalidArgument("Failed to acquire lock on events".to_string())
})?;
let start_us = self.start_time.elapsed().as_micros() as u64;
let event_name = format!("{name} [{category}]");
let _metadata = format!(
"{{\"session_id\": \"{}\", \"trace_id\": {}, \"message\": \"{}\"}}",
self.session_id, self.trace_id, message
);
events.push(ProfileEvent {
name: event_name,
category: "instruments_signpost".to_string(),
start_us,
duration_us: 0, thread_id: format!("{:?}", std::thread::current().id())
.parse()
.unwrap_or(0),
operation_count: Some(1),
flops: Some(0),
bytes_transferred: Some(0),
stack_trace: None,
});
Ok(())
}
pub fn record_time_profile(
&self,
function_name: &str,
file: &str,
line: u32,
duration: Duration,
cpu_time: Option<Duration>,
wall_time: Option<Duration>,
) -> TorshResult<()> {
if !self.enabled || !self.config.time_profiling {
return Ok(());
}
let mut events = self.events.lock().map_err(|_| {
TorshError::InvalidArgument("Failed to acquire lock on events".to_string())
})?;
let start_us = self.start_time.elapsed().as_micros() as u64;
let duration_us = duration.as_micros() as u64;
let event_name = format!("{function_name}() [{file}:{line}]");
let mut metadata = format!(
"{{\"session_id\": \"{}\", \"trace_id\": {}",
self.session_id, self.trace_id
);
if let Some(cpu) = cpu_time {
metadata.push_str(&format!(", \"cpu_time_us\": {}", cpu.as_micros()));
}
if let Some(wall) = wall_time {
metadata.push_str(&format!(", \"wall_time_us\": {}", wall.as_micros()));
}
metadata.push('}');
events.push(ProfileEvent {
name: event_name,
category: "instruments_time".to_string(),
start_us,
duration_us,
thread_id: format!("{:?}", std::thread::current().id())
.parse()
.unwrap_or(0),
operation_count: Some(1),
flops: Some(0),
bytes_transferred: Some(0),
stack_trace: None,
});
Ok(())
}
pub fn record_allocation(
&self,
allocation_type: AllocationType,
size: usize,
address: Option<u64>,
stack_trace: Option<&str>,
) -> TorshResult<()> {
if !self.enabled || !self.config.allocations_tracking {
return Ok(());
}
let mut events = self.events.lock().map_err(|_| {
TorshError::InvalidArgument("Failed to acquire lock on events".to_string())
})?;
let start_us = self.start_time.elapsed().as_micros() as u64;
let event_name = format!(
"{:?} [{}{}]",
allocation_type,
if size < 1024 {
format!("{size}B")
} else if size < 1024 * 1024 {
format!("{}KB", size / 1024)
} else {
format!("{}MB", size / (1024 * 1024))
},
address
.map(|addr| format!(", 0x{addr:x}"))
.unwrap_or_default()
);
let mut metadata = format!(
"{{\"session_id\": \"{}\", \"trace_id\": {}, \"size\": {}",
self.session_id, self.trace_id, size
);
if let Some(addr) = address {
metadata.push_str(&format!(", \"address\": \"0x{addr:x}\""));
}
if let Some(trace) = stack_trace {
metadata.push_str(&format!(
", \"stack_trace\": \"{}\"",
trace.replace('"', "\\\"")
));
}
metadata.push('}');
events.push(ProfileEvent {
name: event_name,
category: "instruments_allocation".to_string(),
start_us,
duration_us: 0, thread_id: format!("{:?}", std::thread::current().id())
.parse()
.unwrap_or(0),
operation_count: Some(1),
flops: Some(0),
bytes_transferred: Some(size as u64),
stack_trace: None,
});
Ok(())
}
pub fn record_energy_usage(
&self,
component: EnergyComponent,
power_mw: f64,
energy_mj: f64,
duration: Duration,
) -> TorshResult<()> {
if !self.enabled || !self.config.energy_tracking {
return Ok(());
}
let mut events = self.events.lock().map_err(|_| {
TorshError::InvalidArgument("Failed to acquire lock on events".to_string())
})?;
let start_us = self.start_time.elapsed().as_micros() as u64;
let duration_us = duration.as_micros() as u64;
let event_name = format!("{component:?} [{power_mw}mW, {energy_mj}mJ]");
let _metadata = format!(
"{{\"session_id\": \"{}\", \"trace_id\": {}, \"power_mw\": {}, \"energy_mj\": {}}}",
self.session_id, self.trace_id, power_mw, energy_mj
);
events.push(ProfileEvent {
name: event_name,
category: "instruments_energy".to_string(),
start_us,
duration_us,
thread_id: format!("{:?}", std::thread::current().id())
.parse()
.unwrap_or(0),
operation_count: Some(1),
flops: Some(0),
bytes_transferred: Some(0),
stack_trace: None,
});
Ok(())
}
pub fn export_instruments_data(&self, filename: &str) -> TorshResult<()> {
let events = self.events.lock().map_err(|_| {
TorshError::InvalidArgument("Failed to acquire lock on events".to_string())
})?;
let instruments_data = InstrumentsExportData {
session_id: self.session_id.clone(),
trace_id: self.trace_id,
config: self.config.clone(),
events: events.clone(),
total_events: events.len(),
total_duration_us: events.iter().map(|e| e.duration_us).sum(),
timestamp: chrono::Utc::now(),
};
let json_data = serde_json::to_string_pretty(&instruments_data)
.map_err(|e| TorshError::InvalidArgument(format!("Failed to serialize data: {e}")))?;
std::fs::write(filename, json_data)
.map_err(|e| TorshError::InvalidArgument(format!("Failed to write file: {e}")))?;
Ok(())
}
pub fn get_instruments_stats(&self) -> TorshResult<InstrumentsStats> {
let events = self.events.lock().map_err(|_| {
TorshError::InvalidArgument("Failed to acquire lock on events".to_string())
})?;
let time_events: Vec<_> = events
.iter()
.filter(|e| e.category == "instruments_time")
.collect();
let allocation_events: Vec<_> = events
.iter()
.filter(|e| e.category == "instruments_allocation")
.collect();
let energy_events: Vec<_> = events
.iter()
.filter(|e| e.category == "instruments_energy")
.collect();
let signpost_events: Vec<_> = events
.iter()
.filter(|e| e.category == "instruments_signpost")
.collect();
let total_time_us: u64 = time_events.iter().map(|e| e.duration_us).sum();
let total_allocations: usize = allocation_events.len();
let total_allocated_bytes: usize = allocation_events
.iter()
.map(|e| e.bytes_transferred.unwrap_or(0) as usize)
.sum();
let avg_function_duration_us = if !time_events.is_empty() {
total_time_us as f64 / time_events.len() as f64
} else {
0.0
};
Ok(InstrumentsStats {
total_events: events.len(),
time_events: time_events.len(),
allocation_events: allocation_events.len(),
energy_events: energy_events.len(),
signpost_events: signpost_events.len(),
total_time_us,
total_allocations,
total_allocated_bytes,
avg_function_duration_us,
session_id: self.session_id.clone(),
trace_id: self.trace_id,
})
}
fn init_signpost(&self) -> TorshResult<()> {
Ok(())
}
fn finalize_signpost(&self) -> TorshResult<()> {
Ok(())
}
fn start_instruments_trace(&self) -> TorshResult<()> {
Ok(())
}
fn stop_instruments_trace(&self) -> TorshResult<()> {
Ok(())
}
}
pub struct SignpostInterval {
name: String,
category: String,
start_time: Instant,
enabled: bool,
}
impl SignpostInterval {
fn new(name: String, category: String, start_time: Instant) -> Self {
Self {
name,
category,
start_time,
enabled: true,
}
}
fn new_disabled() -> Self {
Self {
name: String::new(),
category: String::new(),
start_time: Instant::now(),
enabled: false,
}
}
pub fn duration(&self) -> Duration {
self.start_time.elapsed()
}
pub fn name(&self) -> &str {
&self.name
}
pub fn category(&self) -> &str {
&self.category
}
}
impl Drop for SignpostInterval {
fn drop(&mut self) {
if self.enabled {
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum AllocationType {
Malloc,
Calloc,
Realloc,
Free,
New,
Delete,
MmapAnonymous,
MmapFile,
Munmap,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum EnergyComponent {
CPU,
GPU,
ANE, Display,
Network,
Location,
Camera,
Bluetooth,
WiFi,
Cellular,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct InstrumentsExportData {
pub session_id: String,
pub trace_id: u64,
pub config: InstrumentsConfig,
pub events: Vec<ProfileEvent>,
pub total_events: usize,
pub total_duration_us: u64,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstrumentsStats {
pub total_events: usize,
pub time_events: usize,
pub allocation_events: usize,
pub energy_events: usize,
pub signpost_events: usize,
pub total_time_us: u64,
pub total_allocations: usize,
pub total_allocated_bytes: usize,
pub avg_function_duration_us: f64,
pub session_id: String,
pub trace_id: u64,
}
pub fn create_instruments_profiler() -> InstrumentsProfiler {
InstrumentsProfiler::new(InstrumentsConfig::default())
}
pub fn create_instruments_profiler_with_config(config: InstrumentsConfig) -> InstrumentsProfiler {
InstrumentsProfiler::new(config)
}
pub fn export_instruments_json(profiler: &InstrumentsProfiler, filename: &str) -> TorshResult<()> {
profiler.export_instruments_data(filename)
}
pub fn get_instruments_statistics(profiler: &InstrumentsProfiler) -> TorshResult<InstrumentsStats> {
profiler.get_instruments_stats()
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_instruments_profiler_creation() {
let profiler = create_instruments_profiler();
assert!(!profiler.enabled);
}
#[test]
fn test_instruments_profiler_enable_disable() {
let mut profiler = create_instruments_profiler();
assert!(profiler.enable().is_ok());
assert!(profiler.enabled);
assert!(profiler.disable().is_ok());
assert!(!profiler.enabled);
}
#[test]
#[ignore = "Flaky test - passes individually but may fail in full suite"]
fn test_signpost_interval() {
let mut profiler = create_instruments_profiler();
profiler.enable().unwrap();
let interval = profiler
.start_signpost_interval("test_interval", "test_category")
.unwrap();
assert_eq!(interval.name(), "test_interval");
assert_eq!(interval.category(), "test_category");
assert!(interval.duration().as_nanos() > 0);
}
#[test]
fn test_signpost_event() {
let mut profiler = create_instruments_profiler();
profiler.enable().unwrap();
let result = profiler.emit_signpost_event("test_event", "test_category", "test message");
assert!(result.is_ok());
let stats = profiler.get_instruments_stats().unwrap();
assert_eq!(stats.signpost_events, 1);
}
#[test]
fn test_time_profile_recording() {
let mut profiler = create_instruments_profiler();
profiler.enable().unwrap();
let result = profiler.record_time_profile(
"test_function",
"test.rs",
42,
Duration::from_micros(100),
Some(Duration::from_micros(80)),
Some(Duration::from_micros(120)),
);
assert!(result.is_ok());
let stats = profiler.get_instruments_stats().unwrap();
assert_eq!(stats.time_events, 1);
assert_eq!(stats.total_time_us, 100);
}
#[test]
fn test_allocation_recording() {
let mut profiler = create_instruments_profiler();
profiler.config.allocations_tracking = true;
profiler.enable().unwrap();
let result = profiler.record_allocation(
AllocationType::Malloc,
1024,
Some(0x1000),
Some("test_stack_trace"),
);
assert!(result.is_ok());
let stats = profiler.get_instruments_stats().unwrap();
assert_eq!(stats.allocation_events, 1);
assert_eq!(stats.total_allocated_bytes, 1024);
}
#[test]
fn test_energy_recording() {
let mut profiler = create_instruments_profiler();
profiler.config.energy_tracking = true;
profiler.enable().unwrap();
let result = profiler.record_energy_usage(
EnergyComponent::CPU,
1500.0, 100.0, Duration::from_millis(100),
);
assert!(result.is_ok());
let stats = profiler.get_instruments_stats().unwrap();
assert_eq!(stats.energy_events, 1);
}
#[test]
fn test_export_instruments_data() {
let mut profiler = create_instruments_profiler();
profiler.enable().unwrap();
profiler
.record_time_profile(
"test_function",
"test.rs",
42,
Duration::from_micros(100),
None,
None,
)
.unwrap();
let temp_file = std::env::temp_dir().join("test_instruments_export.json");
let temp_str = temp_file.display().to_string();
let result = profiler.export_instruments_data(&temp_str);
assert!(result.is_ok());
let _ = std::fs::remove_file(&temp_file);
}
#[test]
fn test_custom_config() {
let config = InstrumentsConfig {
signpost_enabled: false,
time_profiling: false,
allocations_tracking: true,
leaks_detection: true,
energy_tracking: true,
activity_tracing: false,
system_trace: true,
sampling_interval_us: 500,
output_dir: Some(
std::env::temp_dir()
.join("instruments")
.display()
.to_string(),
),
device_udid: Some("test-device-udid".to_string()),
};
let profiler = create_instruments_profiler_with_config(config.clone());
assert_eq!(profiler.config.sampling_interval_us, 500);
assert!(profiler.config.allocations_tracking);
assert!(!profiler.config.signpost_enabled);
}
}