1use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
11pub struct ProfileEntry {
12 pub op_name: String,
14 pub duration_ns: u64,
16 pub input_elements: u64,
18 pub output_elements: u64,
20 pub tick: u64,
22}
23
24#[derive(Debug, Clone)]
26pub struct OpProfile {
27 pub op_name: String,
29 pub call_count: u64,
31 pub total_ns: u64,
33 pub min_ns: u64,
35 pub max_ns: u64,
37 pub total_input_elements: u64,
39 pub total_output_elements: u64,
41}
42
43#[derive(Debug, Clone)]
45pub struct ProfilerStats {
46 pub total_entries: usize,
48 pub unique_ops: usize,
50 pub total_ns: u64,
52 pub enabled: bool,
54}
55
56pub struct TensorProfiler {
63 entries: Vec<ProfileEntry>,
64 op_profiles: HashMap<String, OpProfile>,
65 enabled: bool,
66 current_tick: u64,
67 max_entries: usize,
68}
69
70impl TensorProfiler {
71 pub fn new(max_entries: usize) -> Self {
75 Self {
76 entries: Vec::new(),
77 op_profiles: HashMap::new(),
78 enabled: true,
79 current_tick: 0,
80 max_entries,
81 }
82 }
83
84 pub fn record(
89 &mut self,
90 op_name: &str,
91 duration_ns: u64,
92 input_elements: u64,
93 output_elements: u64,
94 ) {
95 if !self.enabled {
96 return;
97 }
98
99 if self.max_entries > 0 {
101 if self.entries.len() >= self.max_entries {
103 self.entries.remove(0);
104 }
105
106 let entry = ProfileEntry {
107 op_name: op_name.to_string(),
108 duration_ns,
109 input_elements,
110 output_elements,
111 tick: self.current_tick,
112 };
113 self.entries.push(entry);
114 }
115
116 let profile = self
118 .op_profiles
119 .entry(op_name.to_string())
120 .or_insert_with(|| OpProfile {
121 op_name: op_name.to_string(),
122 call_count: 0,
123 total_ns: 0,
124 min_ns: u64::MAX,
125 max_ns: 0,
126 total_input_elements: 0,
127 total_output_elements: 0,
128 });
129
130 profile.call_count += 1;
131 profile.total_ns += duration_ns;
132 if duration_ns < profile.min_ns {
133 profile.min_ns = duration_ns;
134 }
135 if duration_ns > profile.max_ns {
136 profile.max_ns = duration_ns;
137 }
138 profile.total_input_elements += input_elements;
139 profile.total_output_elements += output_elements;
140 }
141
142 pub fn enable(&mut self) {
144 self.enabled = true;
145 }
146
147 pub fn disable(&mut self) {
149 self.enabled = false;
150 }
151
152 pub fn is_enabled(&self) -> bool {
154 self.enabled
155 }
156
157 pub fn get_profile(&self, op_name: &str) -> Option<&OpProfile> {
159 self.op_profiles.get(op_name)
160 }
161
162 pub fn avg_ns(&self, op_name: &str) -> Option<f64> {
164 self.op_profiles.get(op_name).map(|p| {
165 if p.call_count == 0 {
166 0.0
167 } else {
168 p.total_ns as f64 / p.call_count as f64
169 }
170 })
171 }
172
173 pub fn throughput(&self, op_name: &str) -> Option<f64> {
177 self.op_profiles.get(op_name).map(|p| {
178 if p.total_ns == 0 {
179 0.0
180 } else {
181 p.total_output_elements as f64 / (p.total_ns as f64 / 1e9)
182 }
183 })
184 }
185
186 pub fn hottest_ops(&self, n: usize) -> Vec<&OpProfile> {
188 let mut profiles: Vec<&OpProfile> = self.op_profiles.values().collect();
189 profiles.sort_by_key(|p| std::cmp::Reverse(p.total_ns));
190 profiles.truncate(n);
191 profiles
192 }
193
194 pub fn tick(&mut self) {
196 self.current_tick += 1;
197 }
198
199 pub fn reset(&mut self) {
201 self.entries.clear();
202 self.op_profiles.clear();
203 self.current_tick = 0;
204 }
205
206 pub fn entry_count(&self) -> usize {
208 self.entries.len()
209 }
210
211 pub fn stats(&self) -> ProfilerStats {
213 let total_ns: u64 = self.op_profiles.values().map(|p| p.total_ns).sum();
214 ProfilerStats {
215 total_entries: self.entries.len(),
216 unique_ops: self.op_profiles.len(),
217 total_ns,
218 enabled: self.enabled,
219 }
220 }
221}
222
223#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_new_profiler_defaults() {
233 let p = TensorProfiler::new(100);
234 assert!(p.is_enabled());
235 assert_eq!(p.entry_count(), 0);
236 let s = p.stats();
237 assert_eq!(s.total_entries, 0);
238 assert_eq!(s.unique_ops, 0);
239 assert_eq!(s.total_ns, 0);
240 assert!(s.enabled);
241 }
242
243 #[test]
244 fn test_record_updates_profile() {
245 let mut p = TensorProfiler::new(100);
246 p.record("matmul", 1000, 64, 32);
247 let prof = p.get_profile("matmul").expect("profile should exist");
248 assert_eq!(prof.call_count, 1);
249 assert_eq!(prof.total_ns, 1000);
250 assert_eq!(prof.min_ns, 1000);
251 assert_eq!(prof.max_ns, 1000);
252 assert_eq!(prof.total_input_elements, 64);
253 assert_eq!(prof.total_output_elements, 32);
254 assert_eq!(p.entry_count(), 1);
255 }
256
257 #[test]
258 fn test_disabled_skips_recording() {
259 let mut p = TensorProfiler::new(100);
260 p.disable();
261 p.record("matmul", 500, 10, 10);
262 assert_eq!(p.entry_count(), 0);
263 assert!(p.get_profile("matmul").is_none());
264 }
265
266 #[test]
267 fn test_avg_ns_calculation() {
268 let mut p = TensorProfiler::new(100);
269 p.record("add", 100, 10, 10);
270 p.record("add", 200, 10, 10);
271 p.record("add", 300, 10, 10);
272 let avg = p.avg_ns("add").expect("avg should exist");
273 assert!((avg - 200.0).abs() < f64::EPSILON);
274 }
275
276 #[test]
277 fn test_avg_ns_missing_op() {
278 let p = TensorProfiler::new(100);
279 assert!(p.avg_ns("nope").is_none());
280 }
281
282 #[test]
283 fn test_throughput_calculation() {
284 let mut p = TensorProfiler::new(100);
285 p.record("conv", 1_000_000_000, 1000, 500);
287 let tp = p.throughput("conv").expect("throughput should exist");
288 assert!((tp - 500.0).abs() < 1e-6);
289 }
290
291 #[test]
292 fn test_throughput_zero_time() {
293 let mut p = TensorProfiler::new(100);
294 p.record("noop", 0, 0, 0);
295 let tp = p.throughput("noop").expect("throughput should exist");
296 assert!((tp - 0.0).abs() < f64::EPSILON);
297 }
298
299 #[test]
300 fn test_throughput_missing_op() {
301 let p = TensorProfiler::new(100);
302 assert!(p.throughput("nope").is_none());
303 }
304
305 #[test]
306 fn test_hottest_ops_ordering() {
307 let mut p = TensorProfiler::new(1000);
308 p.record("fast", 100, 1, 1);
309 p.record("slow", 5000, 1, 1);
310 p.record("mid", 2000, 1, 1);
311
312 let hot = p.hottest_ops(3);
313 assert_eq!(hot.len(), 3);
314 assert_eq!(hot[0].op_name, "slow");
315 assert_eq!(hot[1].op_name, "mid");
316 assert_eq!(hot[2].op_name, "fast");
317 }
318
319 #[test]
320 fn test_hottest_ops_fewer_than_n() {
321 let mut p = TensorProfiler::new(100);
322 p.record("only", 100, 1, 1);
323 let hot = p.hottest_ops(10);
324 assert_eq!(hot.len(), 1);
325 }
326
327 #[test]
328 fn test_max_entries_eviction() {
329 let mut p = TensorProfiler::new(3);
330 p.record("a", 10, 1, 1);
331 p.record("b", 20, 1, 1);
332 p.record("c", 30, 1, 1);
333 assert_eq!(p.entry_count(), 3);
334
335 p.record("d", 40, 1, 1);
337 assert_eq!(p.entry_count(), 3);
338
339 assert!(p.get_profile("a").is_some());
341 assert!(p.get_profile("d").is_some());
342 }
343
344 #[test]
345 fn test_reset_clears_all() {
346 let mut p = TensorProfiler::new(100);
347 p.record("x", 100, 10, 10);
348 p.tick();
349 p.reset();
350 assert_eq!(p.entry_count(), 0);
351 assert!(p.get_profile("x").is_none());
352 let s = p.stats();
353 assert_eq!(s.total_entries, 0);
354 assert_eq!(s.unique_ops, 0);
355 assert_eq!(s.total_ns, 0);
356 }
357
358 #[test]
359 fn test_multiple_ops_tracked_independently() {
360 let mut p = TensorProfiler::new(100);
361 p.record("matmul", 1000, 64, 32);
362 p.record("relu", 200, 32, 32);
363 p.record("matmul", 1500, 64, 32);
364
365 let mm = p.get_profile("matmul").expect("matmul profile");
366 assert_eq!(mm.call_count, 2);
367 assert_eq!(mm.total_ns, 2500);
368
369 let relu = p.get_profile("relu").expect("relu profile");
370 assert_eq!(relu.call_count, 1);
371 assert_eq!(relu.total_ns, 200);
372 }
373
374 #[test]
375 fn test_min_max_tracking() {
376 let mut p = TensorProfiler::new(100);
377 p.record("op", 300, 1, 1);
378 p.record("op", 100, 1, 1);
379 p.record("op", 500, 1, 1);
380 let prof = p.get_profile("op").expect("profile");
381 assert_eq!(prof.min_ns, 100);
382 assert_eq!(prof.max_ns, 500);
383 }
384
385 #[test]
386 fn test_enable_disable_toggle() {
387 let mut p = TensorProfiler::new(100);
388 assert!(p.is_enabled());
389 p.disable();
390 assert!(!p.is_enabled());
391 p.enable();
392 assert!(p.is_enabled());
393 }
394
395 #[test]
396 fn test_stats_accuracy() {
397 let mut p = TensorProfiler::new(100);
398 p.record("a", 100, 10, 5);
399 p.record("b", 200, 20, 10);
400 p.record("a", 300, 30, 15);
401
402 let s = p.stats();
403 assert_eq!(s.total_entries, 3);
404 assert_eq!(s.unique_ops, 2);
405 assert_eq!(s.total_ns, 600); assert!(s.enabled);
407 }
408
409 #[test]
410 fn test_empty_profiler() {
411 let p = TensorProfiler::new(100);
412 assert_eq!(p.entry_count(), 0);
413 assert!(p.get_profile("any").is_none());
414 assert!(p.avg_ns("any").is_none());
415 assert!(p.throughput("any").is_none());
416 let hot = p.hottest_ops(5);
417 assert!(hot.is_empty());
418 }
419
420 #[test]
421 fn test_tick_increments() {
422 let mut p = TensorProfiler::new(100);
423 p.record("a", 10, 1, 1);
424 p.tick();
425 p.record("b", 20, 1, 1);
426
427 assert_eq!(p.entries[0].tick, 0);
428 assert_eq!(p.entries[1].tick, 1);
429 }
430
431 #[test]
432 fn test_record_after_enable() {
433 let mut p = TensorProfiler::new(100);
434 p.disable();
435 p.record("x", 100, 1, 1);
436 assert_eq!(p.entry_count(), 0);
437
438 p.enable();
439 p.record("x", 200, 1, 1);
440 assert_eq!(p.entry_count(), 1);
441 let prof = p.get_profile("x").expect("profile");
442 assert_eq!(prof.total_ns, 200);
443 }
444
445 #[test]
446 fn test_input_elements_accumulation() {
447 let mut p = TensorProfiler::new(100);
448 p.record("op", 10, 100, 50);
449 p.record("op", 20, 200, 100);
450 let prof = p.get_profile("op").expect("profile");
451 assert_eq!(prof.total_input_elements, 300);
452 assert_eq!(prof.total_output_elements, 150);
453 }
454
455 #[test]
456 fn test_hottest_ops_empty() {
457 let p = TensorProfiler::new(100);
458 assert!(p.hottest_ops(5).is_empty());
459 }
460
461 #[test]
462 fn test_max_entries_zero() {
463 let mut p = TensorProfiler::new(0);
465 p.record("a", 10, 1, 1);
466 assert_eq!(p.entry_count(), 0);
468 let prof = p.get_profile("a").expect("profile");
469 assert_eq!(prof.call_count, 1);
470 }
471
472 #[test]
473 fn test_stats_disabled() {
474 let mut p = TensorProfiler::new(100);
475 p.disable();
476 let s = p.stats();
477 assert!(!s.enabled);
478 }
479
480 #[test]
481 fn test_large_duration_values() {
482 let mut p = TensorProfiler::new(100);
483 let big = u64::MAX / 2;
484 p.record("big", big, 1, 1);
485 let prof = p.get_profile("big").expect("profile");
486 assert_eq!(prof.total_ns, big);
487 assert_eq!(prof.min_ns, big);
488 assert_eq!(prof.max_ns, big);
489 }
490
491 #[test]
492 fn test_eviction_preserves_order() {
493 let mut p = TensorProfiler::new(2);
494 p.record("a", 10, 1, 1);
495 p.record("b", 20, 1, 1);
496 p.record("c", 30, 1, 1); assert_eq!(p.entry_count(), 2);
499 assert_eq!(p.entries[0].op_name, "b");
500 assert_eq!(p.entries[1].op_name, "c");
501 }
502
503 #[test]
504 fn test_single_entry_avg_ns() {
505 let mut p = TensorProfiler::new(100);
506 p.record("solo", 42, 1, 1);
507 let avg = p.avg_ns("solo").expect("avg");
508 assert!((avg - 42.0).abs() < f64::EPSILON);
509 }
510
511 #[test]
512 fn test_throughput_multiple_records() {
513 let mut p = TensorProfiler::new(100);
514 p.record("op", 1_000_000_000, 100, 100);
517 p.record("op", 1_000_000_000, 100, 100);
518 let tp = p.throughput("op").expect("throughput");
519 assert!((tp - 100.0).abs() < 1e-6);
520 }
521
522 #[test]
523 fn test_reset_then_record() {
524 let mut p = TensorProfiler::new(100);
525 p.record("a", 100, 1, 1);
526 p.reset();
527 p.record("b", 200, 2, 2);
528 assert_eq!(p.entry_count(), 1);
529 assert!(p.get_profile("a").is_none());
530 let prof = p.get_profile("b").expect("profile");
531 assert_eq!(prof.call_count, 1);
532 }
533
534 #[test]
535 fn test_op_name_preserves_string() {
536 let mut p = TensorProfiler::new(100);
537 let name = "my_custom_op_v2.1";
538 p.record(name, 10, 1, 1);
539 let prof = p.get_profile(name).expect("profile");
540 assert_eq!(prof.op_name, name);
541 }
542}