Skip to main content

torsh_profiler/
instruments.rs

1//! Apple Instruments profiling integration
2//!
3//! This module provides integration with Apple Instruments for comprehensive
4//! performance analysis on macOS and iOS platforms, including time profiling,
5//! allocations, leaks, and energy usage.
6
7use crate::{ProfileEvent, TorshResult};
8use serde::{Deserialize, Serialize};
9use std::sync::{Arc, Mutex};
10use std::time::{Duration, Instant};
11use torsh_core::TorshError;
12
13/// Apple Instruments profiling configuration
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct InstrumentsConfig {
16    /// Whether to enable os_signpost API
17    pub signpost_enabled: bool,
18    /// Whether to enable time profiling
19    pub time_profiling: bool,
20    /// Whether to enable allocations tracking
21    pub allocations_tracking: bool,
22    /// Whether to enable leaks detection
23    pub leaks_detection: bool,
24    /// Whether to enable energy usage tracking
25    pub energy_tracking: bool,
26    /// Whether to enable activity tracing
27    pub activity_tracing: bool,
28    /// Whether to enable system trace
29    pub system_trace: bool,
30    /// Sampling interval in microseconds
31    pub sampling_interval_us: u64,
32    /// Output directory for Instruments traces
33    pub output_dir: Option<String>,
34    /// Target device UDID (for iOS)
35    pub device_udid: Option<String>,
36}
37
38impl Default for InstrumentsConfig {
39    fn default() -> Self {
40        Self {
41            signpost_enabled: true,
42            time_profiling: true,
43            allocations_tracking: false,
44            leaks_detection: false,
45            energy_tracking: false,
46            activity_tracing: true,
47            system_trace: false,
48            sampling_interval_us: 1000, // 1ms
49            output_dir: None,
50            device_udid: None,
51        }
52    }
53}
54
55/// Apple Instruments profiler
56pub struct InstrumentsProfiler {
57    config: InstrumentsConfig,
58    events: Arc<Mutex<Vec<ProfileEvent>>>,
59    start_time: Instant,
60    enabled: bool,
61    session_id: String,
62    trace_id: u64,
63}
64
65impl InstrumentsProfiler {
66    /// Create a new Instruments profiler
67    pub fn new(config: InstrumentsConfig) -> Self {
68        Self {
69            config,
70            events: Arc::new(Mutex::new(Vec::new())),
71            start_time: Instant::now(),
72            enabled: false,
73            session_id: format!("instruments_session_{}", chrono::Utc::now().timestamp()),
74            trace_id: 0,
75        }
76    }
77
78    /// Enable Instruments profiling
79    pub fn enable(&mut self) -> TorshResult<()> {
80        self.enabled = true;
81        self.start_time = Instant::now();
82        self.trace_id += 1;
83
84        if let Ok(mut events) = self.events.lock() {
85            events.clear();
86        }
87
88        // Initialize os_signpost if enabled
89        if self.config.signpost_enabled {
90            self.init_signpost()?;
91        }
92
93        // Start Instruments trace
94        self.start_instruments_trace()?;
95
96        Ok(())
97    }
98
99    /// Disable Instruments profiling
100    pub fn disable(&mut self) -> TorshResult<()> {
101        self.enabled = false;
102
103        // Stop Instruments trace
104        self.stop_instruments_trace()?;
105
106        // Finalize signpost if enabled
107        if self.config.signpost_enabled {
108            self.finalize_signpost()?;
109        }
110
111        Ok(())
112    }
113
114    /// Start an os_signpost interval
115    pub fn start_signpost_interval(
116        &self,
117        name: &str,
118        category: &str,
119    ) -> TorshResult<SignpostInterval> {
120        if !self.enabled || !self.config.signpost_enabled {
121            return Ok(SignpostInterval::new_disabled());
122        }
123
124        let start_time = Instant::now();
125
126        // In a real implementation, we would call os_signpost_interval_begin()
127        let interval = SignpostInterval::new(name.to_string(), category.to_string(), start_time);
128
129        Ok(interval)
130    }
131
132    /// Emit an os_signpost event
133    pub fn emit_signpost_event(
134        &self,
135        name: &str,
136        category: &str,
137        message: &str,
138    ) -> TorshResult<()> {
139        if !self.enabled || !self.config.signpost_enabled {
140            return Ok(());
141        }
142
143        let mut events = self.events.lock().map_err(|_| {
144            TorshError::InvalidArgument("Failed to acquire lock on events".to_string())
145        })?;
146
147        let start_us = self.start_time.elapsed().as_micros() as u64;
148
149        let event_name = format!("{name} [{category}]");
150
151        let _metadata = format!(
152            "{{\"session_id\": \"{}\", \"trace_id\": {}, \"message\": \"{}\"}}",
153            self.session_id, self.trace_id, message
154        );
155
156        events.push(ProfileEvent {
157            name: event_name,
158            category: "instruments_signpost".to_string(),
159            start_us,
160            duration_us: 0, // Point event
161            thread_id: format!("{:?}", std::thread::current().id())
162                .parse()
163                .unwrap_or(0),
164            operation_count: Some(1),
165            flops: Some(0),
166            bytes_transferred: Some(0),
167            stack_trace: None,
168        });
169
170        // In a real implementation, we would call os_signpost_event_emit()
171
172        Ok(())
173    }
174
175    /// Record a time profile sample
176    pub fn record_time_profile(
177        &self,
178        function_name: &str,
179        file: &str,
180        line: u32,
181        duration: Duration,
182        cpu_time: Option<Duration>,
183        wall_time: Option<Duration>,
184    ) -> TorshResult<()> {
185        if !self.enabled || !self.config.time_profiling {
186            return Ok(());
187        }
188
189        let mut events = self.events.lock().map_err(|_| {
190            TorshError::InvalidArgument("Failed to acquire lock on events".to_string())
191        })?;
192
193        let start_us = self.start_time.elapsed().as_micros() as u64;
194        let duration_us = duration.as_micros() as u64;
195
196        let event_name = format!("{function_name}() [{file}:{line}]");
197
198        let mut metadata = format!(
199            "{{\"session_id\": \"{}\", \"trace_id\": {}",
200            self.session_id, self.trace_id
201        );
202
203        if let Some(cpu) = cpu_time {
204            metadata.push_str(&format!(", \"cpu_time_us\": {}", cpu.as_micros()));
205        }
206
207        if let Some(wall) = wall_time {
208            metadata.push_str(&format!(", \"wall_time_us\": {}", wall.as_micros()));
209        }
210
211        metadata.push('}');
212
213        events.push(ProfileEvent {
214            name: event_name,
215            category: "instruments_time".to_string(),
216            start_us,
217            duration_us,
218            thread_id: format!("{:?}", std::thread::current().id())
219                .parse()
220                .unwrap_or(0),
221            operation_count: Some(1),
222            flops: Some(0),
223            bytes_transferred: Some(0),
224            stack_trace: None,
225        });
226
227        Ok(())
228    }
229
230    /// Record an allocation event
231    pub fn record_allocation(
232        &self,
233        allocation_type: AllocationType,
234        size: usize,
235        address: Option<u64>,
236        stack_trace: Option<&str>,
237    ) -> TorshResult<()> {
238        if !self.enabled || !self.config.allocations_tracking {
239            return Ok(());
240        }
241
242        let mut events = self.events.lock().map_err(|_| {
243            TorshError::InvalidArgument("Failed to acquire lock on events".to_string())
244        })?;
245
246        let start_us = self.start_time.elapsed().as_micros() as u64;
247
248        let event_name = format!(
249            "{:?} [{}{}]",
250            allocation_type,
251            if size < 1024 {
252                format!("{size}B")
253            } else if size < 1024 * 1024 {
254                format!("{}KB", size / 1024)
255            } else {
256                format!("{}MB", size / (1024 * 1024))
257            },
258            address
259                .map(|addr| format!(", 0x{addr:x}"))
260                .unwrap_or_default()
261        );
262
263        let mut metadata = format!(
264            "{{\"session_id\": \"{}\", \"trace_id\": {}, \"size\": {}",
265            self.session_id, self.trace_id, size
266        );
267
268        if let Some(addr) = address {
269            metadata.push_str(&format!(", \"address\": \"0x{addr:x}\""));
270        }
271
272        if let Some(trace) = stack_trace {
273            metadata.push_str(&format!(
274                ", \"stack_trace\": \"{}\"",
275                trace.replace('"', "\\\"")
276            ));
277        }
278
279        metadata.push('}');
280
281        events.push(ProfileEvent {
282            name: event_name,
283            category: "instruments_allocation".to_string(),
284            start_us,
285            duration_us: 0, // Point event
286            thread_id: format!("{:?}", std::thread::current().id())
287                .parse()
288                .unwrap_or(0),
289            operation_count: Some(1),
290            flops: Some(0),
291            bytes_transferred: Some(size as u64),
292            stack_trace: None,
293        });
294
295        Ok(())
296    }
297
298    /// Record an energy usage event
299    pub fn record_energy_usage(
300        &self,
301        component: EnergyComponent,
302        power_mw: f64,
303        energy_mj: f64,
304        duration: Duration,
305    ) -> TorshResult<()> {
306        if !self.enabled || !self.config.energy_tracking {
307            return Ok(());
308        }
309
310        let mut events = self.events.lock().map_err(|_| {
311            TorshError::InvalidArgument("Failed to acquire lock on events".to_string())
312        })?;
313
314        let start_us = self.start_time.elapsed().as_micros() as u64;
315        let duration_us = duration.as_micros() as u64;
316
317        let event_name = format!("{component:?} [{power_mw}mW, {energy_mj}mJ]");
318
319        let _metadata = format!(
320            "{{\"session_id\": \"{}\", \"trace_id\": {}, \"power_mw\": {}, \"energy_mj\": {}}}",
321            self.session_id, self.trace_id, power_mw, energy_mj
322        );
323
324        events.push(ProfileEvent {
325            name: event_name,
326            category: "instruments_energy".to_string(),
327            start_us,
328            duration_us,
329            thread_id: format!("{:?}", std::thread::current().id())
330                .parse()
331                .unwrap_or(0),
332            operation_count: Some(1),
333            flops: Some(0),
334            bytes_transferred: Some(0),
335            stack_trace: None,
336        });
337
338        Ok(())
339    }
340
341    /// Export Instruments profiling data
342    pub fn export_instruments_data(&self, filename: &str) -> TorshResult<()> {
343        let events = self.events.lock().map_err(|_| {
344            TorshError::InvalidArgument("Failed to acquire lock on events".to_string())
345        })?;
346
347        let instruments_data = InstrumentsExportData {
348            session_id: self.session_id.clone(),
349            trace_id: self.trace_id,
350            config: self.config.clone(),
351            events: events.clone(),
352            total_events: events.len(),
353            total_duration_us: events.iter().map(|e| e.duration_us).sum(),
354            timestamp: chrono::Utc::now(),
355        };
356
357        let json_data = serde_json::to_string_pretty(&instruments_data)
358            .map_err(|e| TorshError::InvalidArgument(format!("Failed to serialize data: {e}")))?;
359
360        std::fs::write(filename, json_data)
361            .map_err(|e| TorshError::InvalidArgument(format!("Failed to write file: {e}")))?;
362
363        Ok(())
364    }
365
366    /// Get Instruments profiling statistics
367    pub fn get_instruments_stats(&self) -> TorshResult<InstrumentsStats> {
368        let events = self.events.lock().map_err(|_| {
369            TorshError::InvalidArgument("Failed to acquire lock on events".to_string())
370        })?;
371
372        let time_events: Vec<_> = events
373            .iter()
374            .filter(|e| e.category == "instruments_time")
375            .collect();
376
377        let allocation_events: Vec<_> = events
378            .iter()
379            .filter(|e| e.category == "instruments_allocation")
380            .collect();
381
382        let energy_events: Vec<_> = events
383            .iter()
384            .filter(|e| e.category == "instruments_energy")
385            .collect();
386
387        let signpost_events: Vec<_> = events
388            .iter()
389            .filter(|e| e.category == "instruments_signpost")
390            .collect();
391
392        let total_time_us: u64 = time_events.iter().map(|e| e.duration_us).sum();
393
394        let total_allocations: usize = allocation_events.len();
395        let total_allocated_bytes: usize = allocation_events
396            .iter()
397            .map(|e| e.bytes_transferred.unwrap_or(0) as usize)
398            .sum();
399
400        let avg_function_duration_us = if !time_events.is_empty() {
401            total_time_us as f64 / time_events.len() as f64
402        } else {
403            0.0
404        };
405
406        Ok(InstrumentsStats {
407            total_events: events.len(),
408            time_events: time_events.len(),
409            allocation_events: allocation_events.len(),
410            energy_events: energy_events.len(),
411            signpost_events: signpost_events.len(),
412            total_time_us,
413            total_allocations,
414            total_allocated_bytes,
415            avg_function_duration_us,
416            session_id: self.session_id.clone(),
417            trace_id: self.trace_id,
418        })
419    }
420
421    // Private helper methods
422
423    fn init_signpost(&self) -> TorshResult<()> {
424        // In a real implementation, we would initialize os_signpost
425        // os_log_create, os_signpost_id_generate, etc.
426        Ok(())
427    }
428
429    fn finalize_signpost(&self) -> TorshResult<()> {
430        // In a real implementation, we would finalize os_signpost
431        Ok(())
432    }
433
434    fn start_instruments_trace(&self) -> TorshResult<()> {
435        // In a real implementation, we would start Instruments tracing
436        // via command line or Instruments API
437        Ok(())
438    }
439
440    fn stop_instruments_trace(&self) -> TorshResult<()> {
441        // In a real implementation, we would stop Instruments tracing
442        // and save the trace file
443        Ok(())
444    }
445}
446
447/// os_signpost interval for profiling
448pub struct SignpostInterval {
449    name: String,
450    category: String,
451    start_time: Instant,
452    enabled: bool,
453}
454
455impl SignpostInterval {
456    fn new(name: String, category: String, start_time: Instant) -> Self {
457        Self {
458            name,
459            category,
460            start_time,
461            enabled: true,
462        }
463    }
464
465    fn new_disabled() -> Self {
466        Self {
467            name: String::new(),
468            category: String::new(),
469            start_time: Instant::now(),
470            enabled: false,
471        }
472    }
473
474    /// Get the duration of this interval
475    pub fn duration(&self) -> Duration {
476        self.start_time.elapsed()
477    }
478
479    /// Get the name of this interval
480    pub fn name(&self) -> &str {
481        &self.name
482    }
483
484    /// Get the category of this interval
485    pub fn category(&self) -> &str {
486        &self.category
487    }
488}
489
490impl Drop for SignpostInterval {
491    fn drop(&mut self) {
492        if self.enabled {
493            // In a real implementation, we would call os_signpost_interval_end()
494        }
495    }
496}
497
498/// Allocation types for Instruments analysis
499#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
500pub enum AllocationType {
501    Malloc,
502    Calloc,
503    Realloc,
504    Free,
505    New,
506    Delete,
507    MmapAnonymous,
508    MmapFile,
509    Munmap,
510}
511
512/// Energy components for Instruments analysis
513#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
514pub enum EnergyComponent {
515    CPU,
516    GPU,
517    ANE, // Apple Neural Engine
518    Display,
519    Network,
520    Location,
521    Camera,
522    Bluetooth,
523    WiFi,
524    Cellular,
525}
526
527/// Instruments export data structure
528#[derive(Debug, Serialize, Deserialize)]
529pub struct InstrumentsExportData {
530    pub session_id: String,
531    pub trace_id: u64,
532    pub config: InstrumentsConfig,
533    pub events: Vec<ProfileEvent>,
534    pub total_events: usize,
535    pub total_duration_us: u64,
536    pub timestamp: chrono::DateTime<chrono::Utc>,
537}
538
539/// Instruments profiling statistics
540#[derive(Debug, Clone, Serialize, Deserialize)]
541pub struct InstrumentsStats {
542    pub total_events: usize,
543    pub time_events: usize,
544    pub allocation_events: usize,
545    pub energy_events: usize,
546    pub signpost_events: usize,
547    pub total_time_us: u64,
548    pub total_allocations: usize,
549    pub total_allocated_bytes: usize,
550    pub avg_function_duration_us: f64,
551    pub session_id: String,
552    pub trace_id: u64,
553}
554
555/// Create a new Instruments profiler with default configuration
556pub fn create_instruments_profiler() -> InstrumentsProfiler {
557    InstrumentsProfiler::new(InstrumentsConfig::default())
558}
559
560/// Create a new Instruments profiler with custom configuration
561pub fn create_instruments_profiler_with_config(config: InstrumentsConfig) -> InstrumentsProfiler {
562    InstrumentsProfiler::new(config)
563}
564
565/// Export Instruments profiling data to JSON format
566pub fn export_instruments_json(profiler: &InstrumentsProfiler, filename: &str) -> TorshResult<()> {
567    profiler.export_instruments_data(filename)
568}
569
570/// Get Instruments profiling statistics
571pub fn get_instruments_statistics(profiler: &InstrumentsProfiler) -> TorshResult<InstrumentsStats> {
572    profiler.get_instruments_stats()
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578    use std::time::Duration;
579
580    #[test]
581    fn test_instruments_profiler_creation() {
582        let profiler = create_instruments_profiler();
583        assert!(!profiler.enabled);
584    }
585
586    #[test]
587    fn test_instruments_profiler_enable_disable() {
588        let mut profiler = create_instruments_profiler();
589        assert!(profiler.enable().is_ok());
590        assert!(profiler.enabled);
591        assert!(profiler.disable().is_ok());
592        assert!(!profiler.enabled);
593    }
594
595    #[test]
596    #[ignore = "Flaky test - passes individually but may fail in full suite"]
597    fn test_signpost_interval() {
598        let mut profiler = create_instruments_profiler();
599        profiler.enable().unwrap();
600        let interval = profiler
601            .start_signpost_interval("test_interval", "test_category")
602            .unwrap();
603        assert_eq!(interval.name(), "test_interval");
604        assert_eq!(interval.category(), "test_category");
605        assert!(interval.duration().as_nanos() > 0);
606    }
607
608    #[test]
609    fn test_signpost_event() {
610        let mut profiler = create_instruments_profiler();
611        profiler.enable().unwrap();
612
613        let result = profiler.emit_signpost_event("test_event", "test_category", "test message");
614
615        assert!(result.is_ok());
616
617        let stats = profiler.get_instruments_stats().unwrap();
618        assert_eq!(stats.signpost_events, 1);
619    }
620
621    #[test]
622    fn test_time_profile_recording() {
623        let mut profiler = create_instruments_profiler();
624        profiler.enable().unwrap();
625
626        let result = profiler.record_time_profile(
627            "test_function",
628            "test.rs",
629            42,
630            Duration::from_micros(100),
631            Some(Duration::from_micros(80)),
632            Some(Duration::from_micros(120)),
633        );
634
635        assert!(result.is_ok());
636
637        let stats = profiler.get_instruments_stats().unwrap();
638        assert_eq!(stats.time_events, 1);
639        assert_eq!(stats.total_time_us, 100);
640    }
641
642    #[test]
643    fn test_allocation_recording() {
644        let mut profiler = create_instruments_profiler();
645        profiler.config.allocations_tracking = true;
646        profiler.enable().unwrap();
647
648        let result = profiler.record_allocation(
649            AllocationType::Malloc,
650            1024,
651            Some(0x1000),
652            Some("test_stack_trace"),
653        );
654
655        assert!(result.is_ok());
656
657        let stats = profiler.get_instruments_stats().unwrap();
658        assert_eq!(stats.allocation_events, 1);
659        assert_eq!(stats.total_allocated_bytes, 1024);
660    }
661
662    #[test]
663    fn test_energy_recording() {
664        let mut profiler = create_instruments_profiler();
665        profiler.config.energy_tracking = true;
666        profiler.enable().unwrap();
667
668        let result = profiler.record_energy_usage(
669            EnergyComponent::CPU,
670            1500.0, // 1.5W
671            100.0,  // 100mJ
672            Duration::from_millis(100),
673        );
674
675        assert!(result.is_ok());
676
677        let stats = profiler.get_instruments_stats().unwrap();
678        assert_eq!(stats.energy_events, 1);
679    }
680
681    #[test]
682    fn test_export_instruments_data() {
683        let mut profiler = create_instruments_profiler();
684        profiler.enable().unwrap();
685
686        profiler
687            .record_time_profile(
688                "test_function",
689                "test.rs",
690                42,
691                Duration::from_micros(100),
692                None,
693                None,
694            )
695            .unwrap();
696
697        let temp_file = std::env::temp_dir().join("test_instruments_export.json");
698        let temp_str = temp_file.display().to_string();
699        let result = profiler.export_instruments_data(&temp_str);
700        assert!(result.is_ok());
701
702        // Clean up
703        let _ = std::fs::remove_file(&temp_file);
704    }
705
706    #[test]
707    fn test_custom_config() {
708        let config = InstrumentsConfig {
709            signpost_enabled: false,
710            time_profiling: false,
711            allocations_tracking: true,
712            leaks_detection: true,
713            energy_tracking: true,
714            activity_tracing: false,
715            system_trace: true,
716            sampling_interval_us: 500,
717            output_dir: Some(
718                std::env::temp_dir()
719                    .join("instruments")
720                    .display()
721                    .to_string(),
722            ),
723            device_udid: Some("test-device-udid".to_string()),
724        };
725
726        let profiler = create_instruments_profiler_with_config(config.clone());
727        assert_eq!(profiler.config.sampling_interval_us, 500);
728        assert!(profiler.config.allocations_tracking);
729        assert!(!profiler.config.signpost_enabled);
730    }
731}