1use crate::{ProfileEvent, TorshResult};
8use serde::{Deserialize, Serialize};
9use std::sync::{Arc, Mutex};
10use std::time::{Duration, Instant};
11use torsh_core::TorshError;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct InstrumentsConfig {
16 pub signpost_enabled: bool,
18 pub time_profiling: bool,
20 pub allocations_tracking: bool,
22 pub leaks_detection: bool,
24 pub energy_tracking: bool,
26 pub activity_tracing: bool,
28 pub system_trace: bool,
30 pub sampling_interval_us: u64,
32 pub output_dir: Option<String>,
34 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, output_dir: None,
50 device_udid: None,
51 }
52 }
53}
54
55pub 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 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 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 if self.config.signpost_enabled {
90 self.init_signpost()?;
91 }
92
93 self.start_instruments_trace()?;
95
96 Ok(())
97 }
98
99 pub fn disable(&mut self) -> TorshResult<()> {
101 self.enabled = false;
102
103 self.stop_instruments_trace()?;
105
106 if self.config.signpost_enabled {
108 self.finalize_signpost()?;
109 }
110
111 Ok(())
112 }
113
114 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 let interval = SignpostInterval::new(name.to_string(), category.to_string(), start_time);
128
129 Ok(interval)
130 }
131
132 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, 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 Ok(())
173 }
174
175 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 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, 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 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 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 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 fn init_signpost(&self) -> TorshResult<()> {
424 Ok(())
427 }
428
429 fn finalize_signpost(&self) -> TorshResult<()> {
430 Ok(())
432 }
433
434 fn start_instruments_trace(&self) -> TorshResult<()> {
435 Ok(())
438 }
439
440 fn stop_instruments_trace(&self) -> TorshResult<()> {
441 Ok(())
444 }
445}
446
447pub 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 pub fn duration(&self) -> Duration {
476 self.start_time.elapsed()
477 }
478
479 pub fn name(&self) -> &str {
481 &self.name
482 }
483
484 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 }
495 }
496}
497
498#[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#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
514pub enum EnergyComponent {
515 CPU,
516 GPU,
517 ANE, Display,
519 Network,
520 Location,
521 Camera,
522 Bluetooth,
523 WiFi,
524 Cellular,
525}
526
527#[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#[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
555pub fn create_instruments_profiler() -> InstrumentsProfiler {
557 InstrumentsProfiler::new(InstrumentsConfig::default())
558}
559
560pub fn create_instruments_profiler_with_config(config: InstrumentsConfig) -> InstrumentsProfiler {
562 InstrumentsProfiler::new(config)
563}
564
565pub fn export_instruments_json(profiler: &InstrumentsProfiler, filename: &str) -> TorshResult<()> {
567 profiler.export_instruments_data(filename)
568}
569
570pub 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, 100.0, 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 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}