1use std::cell::RefCell;
6
7use super::types::{
8 AllocationTracker, AnnotatedTimeline, CallTreeNode, ComprehensiveProfilingReport, CountingStep,
9 EventFilter, FlameGraph, FlameNode, GcCollectionRecord, GcProfiler, HeatMap, Histogram,
10 PerfCounter, ProfileReport, ProfileSample, Profiler, ProfilerConfig, ProfilingEvent,
11 ProfilingMiddleware, ProfilingSession, RealTimeMonitor, SamplingProfiler, StackSnapshot,
12 TacticProfileLog, TacticProfilingEvent, TimelineAnnotation, TimelineView,
13};
14
15thread_local! {
16 #[doc = " Per-thread profiler instance."] static THREAD_PROFILER : RefCell < Profiler
17 > = RefCell::new(Profiler::new());
18}
19pub fn profiler_enable() {
21 THREAD_PROFILER.with(|p| p.borrow_mut().enable());
22}
23pub fn profiler_disable() {
25 THREAD_PROFILER.with(|p| p.borrow_mut().disable());
26}
27pub fn profiler_report() -> ProfileReport {
29 THREAD_PROFILER.with(|p| p.borrow().generate_report())
30}
31pub fn profiler_enter(name: &str) {
33 THREAD_PROFILER.with(|p| p.borrow_mut().enter_function(name));
34}
35pub fn profiler_exit(name: &str) {
37 THREAD_PROFILER.with(|p| p.borrow_mut().exit_function(name));
38}
39pub fn profiler_alloc(size: usize, tag: &str) {
41 THREAD_PROFILER.with(|p| p.borrow_mut().alloc(size, tag));
42}
43pub fn profiler_dealloc(size: usize, tag: &str) {
45 THREAD_PROFILER.with(|p| p.borrow_mut().dealloc(size, tag));
46}
47#[cfg(test)]
48mod tests {
49 use super::*;
50 #[test]
51 fn test_profiler_basic() {
52 let mut p = Profiler::new();
53 p.enter_function("foo");
54 p.exit_function("foo");
55 assert!(p.events.is_empty());
56 p.enable();
57 p.enter_function("bar");
58 p.exit_function("bar");
59 assert_eq!(p.events.len(), 2);
60 }
61 #[test]
62 fn test_profiler_report() {
63 let mut p = Profiler::new();
64 p.enable();
65 p.enter_function("compute");
66 p.exit_function("compute");
67 p.enter_function("compute");
68 p.exit_function("compute");
69 p.alloc(1024, "heap");
70 p.alloc(512, "stack");
71 let report = p.generate_report();
72 assert_eq!(report.total_calls, 2);
73 assert_eq!(report.total_alloc_bytes, 1536);
74 assert_eq!(report.gc_cycles, 0);
75 assert!(!report.hot_functions.is_empty());
76 assert_eq!(report.hot_functions[0].0, "compute");
77 }
78 #[test]
79 fn test_memory_profile() {
80 let mut p = Profiler::new();
81 p.enable();
82 p.alloc(100, "a");
83 p.alloc(200, "b");
84 p.dealloc(100, "a");
85 let mem = p.memory_profile();
86 assert_eq!(mem.total_allocs, 2);
87 assert_eq!(mem.peak_bytes, 300);
88 assert_eq!(mem.current_bytes, 200);
89 let text = mem.to_text();
90 assert!(text.contains("Memory Profile"));
91 assert!(text.contains("300"));
92 }
93 #[test]
94 fn test_profiler_disabled() {
95 let mut p = Profiler::new();
96 p.alloc(1000, "test");
97 p.enter_function("ignored");
98 p.gc_cycle(10, 50);
99 assert!(p.events.is_empty());
100 assert!(p.call_stack.is_empty());
101 p.enable();
102 p.alloc(42, "enabled");
103 assert_eq!(p.events.len(), 1);
104 }
105 #[test]
106 fn test_profiler_json() {
107 let mut p = Profiler::new();
108 p.enable();
109 p.enter_function("alpha");
110 p.exit_function("alpha");
111 p.alloc(256, "buf");
112 let report = p.generate_report();
113 let json = report.to_json();
114 assert!(json.contains("total_calls"));
115 assert!(json.contains("total_alloc_bytes"));
116 assert!(json.contains("gc_cycles"));
117 assert!(json.contains("hot_functions"));
118 assert!(json.starts_with('{'));
119 assert!(json.ends_with('}'));
120 }
121 #[test]
122 fn test_gc_cycle() {
123 let mut p = Profiler::new();
124 p.enable();
125 p.gc_cycle(50, 200);
126 p.gc_cycle(30, 170);
127 let report = p.generate_report();
128 assert_eq!(report.gc_cycles, 2);
129 let text = report.to_text();
130 assert!(text.contains("Profile Report"));
131 assert!(text.contains("GC cycles"));
132 }
133}
134pub(super) fn profiler_now_ns() -> u64 {
135 use std::time::{SystemTime, UNIX_EPOCH};
136 SystemTime::now()
137 .duration_since(UNIX_EPOCH)
138 .map(|d| d.as_nanos() as u64)
139 .unwrap_or(0)
140}
141#[cfg(test)]
142mod profiler_extended_tests {
143 use super::*;
144 #[test]
145 fn test_sampling_profiler_basic() {
146 let mut p = SamplingProfiler::new(1_000_000);
147 p.enable();
148 p.enter("foo");
149 p.enter("bar");
150 p.take_sample(0);
151 assert_eq!(p.sample_count(), 1);
152 let sample = &p.samples[0];
153 assert_eq!(sample.top_function(), Some("bar"));
154 assert_eq!(sample.depth(), 2);
155 }
156 #[test]
157 fn test_flat_profile() {
158 let mut p = SamplingProfiler::new(1_000_000);
159 p.enable();
160 for _ in 0..3 {
161 p.enter("foo");
162 p.take_sample(0);
163 p.leave("foo");
164 }
165 p.enter("bar");
166 p.take_sample(0);
167 p.leave("bar");
168 let flat = p.flat_profile();
169 assert_eq!(flat[0].0, "foo");
170 assert_eq!(flat[0].1, 3);
171 assert_eq!(flat[1].0, "bar");
172 assert_eq!(flat[1].1, 1);
173 }
174 #[test]
175 fn test_cumulative_profile() {
176 let mut p = SamplingProfiler::new(1_000_000);
177 p.enable();
178 p.current_stack = vec!["inner".to_string(), "outer".to_string()];
179 p.take_sample(0);
180 p.take_sample(0);
181 let cum = p.cumulative_profile();
182 let has_inner = cum.iter().any(|(n, _)| n == "inner");
183 let has_outer = cum.iter().any(|(n, _)| n == "outer");
184 assert!(has_inner);
185 assert!(has_outer);
186 }
187 #[test]
188 fn test_flame_graph_basic() {
189 let mut fg = FlameGraph::new();
190 let stack = vec![
191 "main".to_string(),
192 "compute".to_string(),
193 "inner".to_string(),
194 ];
195 fg.add_stack(&stack);
196 fg.add_stack(&stack);
197 assert_eq!(fg.total_samples, 2);
198 let text = fg.render_text();
199 assert!(text.contains("(all)"));
200 }
201 #[test]
202 fn test_flame_node_get_or_create() {
203 let mut node = FlameNode::new("root");
204 {
205 let child = node.get_or_create_child("child1");
206 child.count += 1;
207 }
208 {
209 let child = node.get_or_create_child("child1");
210 child.count += 1;
211 }
212 {
213 node.get_or_create_child("child2");
214 }
215 assert_eq!(node.children.len(), 2);
216 assert_eq!(node.children[0].count, 2);
217 }
218 #[test]
219 fn test_perf_counter() {
220 let mut pc = PerfCounter::new();
221 pc.simulate_instructions(1000);
222 pc.simulate_cache_miss();
223 pc.simulate_branch_misprediction();
224 assert_eq!(pc.instructions_retired, 1000);
225 assert_eq!(pc.cache_misses, 1);
226 assert_eq!(pc.branch_mispredictions, 1);
227 assert!(pc.ipc() > 0.0);
228 let summary = pc.summary();
229 assert!(summary.contains("PerfCounters"));
230 assert!(summary.contains("IPC"));
231 }
232 #[test]
233 fn test_allocation_tracker() {
234 let mut tracker = AllocationTracker::new();
235 tracker.record_alloc("heap", 1024);
236 tracker.record_alloc("heap", 512);
237 tracker.record_alloc("stack", 256);
238 tracker.record_dealloc("heap", 512);
239 let heap_stats = tracker
240 .stats_for("heap")
241 .expect("test operation should succeed");
242 assert_eq!(heap_stats.alloc_count, 2);
243 assert_eq!(heap_stats.total_bytes, 1536);
244 assert_eq!(heap_stats.live_bytes, 1024);
245 assert_eq!(tracker.total_live_bytes(), 1024 + 256);
246 let top = tracker.top_allocators(1);
247 assert_eq!(top[0].0, "heap");
248 }
249 #[test]
250 fn test_tactic_profile_log() {
251 let mut log = TacticProfileLog::new();
252 log.record(TacticProfilingEvent::new("intro", 500, true, 1, 1));
253 log.record(TacticProfilingEvent::new("apply", 1500, true, 1, 0));
254 log.record(TacticProfilingEvent::new("rw", 300, false, 1, 1));
255 assert_eq!(log.total_duration_ns(), 2300);
256 assert_eq!(log.success_count(), 2);
257 let top = log.top_slow(1);
258 assert_eq!(top[0].tactic, "apply");
259 assert!((log.avg_duration_ns() - 2300.0 / 3.0).abs() < 1.0);
260 }
261 #[test]
262 fn test_tactic_profiling_event_goals_eliminated() {
263 let event = TacticProfilingEvent::new("exact", 100, true, 3, 1);
264 assert_eq!(event.goals_eliminated(), 2);
265 }
266 #[test]
267 fn test_stack_snapshot() {
268 let frames = vec!["main".to_string(), "foo".to_string(), "bar".to_string()];
269 let snap = StackSnapshot::new(12345, frames.clone()).with_label("checkpoint");
270 assert_eq!(snap.depth(), 3);
271 assert_eq!(snap.label.as_deref(), Some("checkpoint"));
272 let formatted = snap.format();
273 assert!(formatted.contains("checkpoint"));
274 assert!(formatted.contains("main"));
275 assert!(formatted.contains("bar"));
276 }
277 #[test]
278 fn test_profiler_config_builder() {
279 let cfg = ProfilerConfig::new().enable_all();
280 assert!(cfg.event_profiling);
281 assert!(cfg.sampling_profiling);
282 let cfg2 = ProfilerConfig::default().disable_all();
283 assert!(!cfg2.event_profiling);
284 assert!(!cfg2.sampling_profiling);
285 }
286 #[test]
287 fn test_call_tree_node() {
288 let mut root = CallTreeNode::new("main");
289 root.inclusive_ns = 10_000;
290 root.exclusive_ns = 1_000;
291 root.call_count = 2;
292 let mut child = CallTreeNode::new("compute");
293 child.inclusive_ns = 9_000;
294 child.exclusive_ns = 9_000;
295 child.call_count = 5;
296 root.children.push(child);
297 assert!((root.avg_exclusive_ns() - 500.0).abs() < 1.0);
298 assert!((root.avg_inclusive_ns() - 5000.0).abs() < 1.0);
299 assert!(root.find_child("compute").is_some());
300 assert!(root.find_child("missing").is_none());
301 }
302 #[test]
303 fn test_sampling_profiler_avg_stack_depth() {
304 let mut p = SamplingProfiler::new(1_000_000);
305 p.enabled = true;
306 p.samples
307 .push(ProfileSample::new(0, vec!["a".into(), "b".into()], 0));
308 p.samples.push(ProfileSample::new(
309 1,
310 vec!["a".into(), "b".into(), "c".into()],
311 0,
312 ));
313 assert!((p.avg_stack_depth() - 2.5).abs() < 1e-9);
314 }
315}
316#[cfg(test)]
317mod profiler_extended_tests_2 {
318 use super::*;
319 #[test]
320 fn test_event_filter_timestamp() {
321 let filter = EventFilter {
322 min_timestamp_ns: 100,
323 max_timestamp_ns: 200,
324 ..EventFilter::new()
325 };
326 let e1 = ProfilingEvent::FunctionCall {
327 name: "f".to_string(),
328 depth: 0,
329 };
330 let e2 = ProfilingEvent::FunctionCall {
331 name: "g".to_string(),
332 depth: 0,
333 };
334 assert!(filter.matches(150, &e1));
335 assert!(!filter.matches(50, &e2));
336 assert!(!filter.matches(250, &e2));
337 }
338 #[test]
339 fn test_event_filter_function_name() {
340 let filter = EventFilter {
341 function_names: vec!["foo".to_string()],
342 ..EventFilter::new()
343 };
344 let e_foo = ProfilingEvent::FunctionCall {
345 name: "foo".to_string(),
346 depth: 0,
347 };
348 let e_bar = ProfilingEvent::FunctionCall {
349 name: "bar".to_string(),
350 depth: 0,
351 };
352 assert!(filter.matches(0, &e_foo));
353 assert!(!filter.matches(0, &e_bar));
354 }
355 #[test]
356 fn test_event_filter_alloc_size() {
357 let filter = EventFilter {
358 min_alloc_bytes: 100,
359 ..EventFilter::new()
360 };
361 let small = ProfilingEvent::Allocation {
362 size: 50,
363 tag: "x".to_string(),
364 };
365 let large = ProfilingEvent::Allocation {
366 size: 200,
367 tag: "y".to_string(),
368 };
369 assert!(!filter.matches(0, &small));
370 assert!(filter.matches(0, &large));
371 }
372 #[test]
373 fn test_timeline_view_build() {
374 let mut p = Profiler::new();
375 p.enable();
376 p.enter_function("foo");
377 p.exit_function("foo");
378 p.alloc(1024, "heap");
379 p.gc_cycle(10, 90);
380 let view = TimelineView::build(&p);
381 assert!(!view.entries.is_empty());
382 let func_entries = view.by_category("function");
383 assert!(!func_entries.is_empty());
384 }
385 #[test]
386 fn test_heat_map() {
387 let mut hm = HeatMap::new(10, 1_000_000_000);
388 let base = 0u64;
389 hm.record(0, base);
390 hm.record(100_000_000, base);
391 hm.record(900_000_000, base);
392 assert_eq!(hm.counts[0], 1);
393 let ascii = hm.render_ascii();
394 assert!(ascii.contains('|'));
395 }
396 #[test]
397 fn test_profiling_session() {
398 let mut session = ProfilingSession::new("test_session");
399 assert!(!session.running);
400 session.start();
401 assert!(session.running);
402 session.enter_function("main");
403 session.alloc(512, "buf");
404 session.dealloc(512, "buf");
405 session.exit_function("main");
406 session.stop();
407 assert!(!session.running);
408 let report = session.combined_report();
409 assert!(report.contains("test_session"));
410 }
411 #[test]
412 fn test_real_time_monitor() {
413 let mut mon = RealTimeMonitor::new("metrics", 100);
414 mon.record("cpu", 0.5);
415 mon.record("cpu", 0.7);
416 mon.record("mem", 1024.0);
417 assert!((mon.latest("cpu").expect("test operation should succeed") - 0.7).abs() < 1e-9);
418 assert!((mon.avg("cpu") - 0.6).abs() < 1e-9);
419 assert_eq!(mon.count("cpu"), 2);
420 assert_eq!(mon.count("mem"), 1);
421 assert!(mon.latest("missing").is_none());
422 }
423 #[test]
424 fn test_real_time_monitor_capacity() {
425 let mut mon = RealTimeMonitor::new("capacity_test", 3);
426 for i in 0..5 {
427 mon.record("x", i as f64);
428 }
429 assert_eq!(mon.snapshots.len(), 3);
430 }
431 #[test]
432 fn test_histogram() {
433 let mut h = Histogram::new(5, 0.0, 100.0);
434 h.record(10.0);
435 h.record(10.0);
436 h.record(50.0);
437 h.record(90.0);
438 assert_eq!(h.total, 4);
439 assert!((h.mean() - 40.0).abs() < 1e-9);
440 let mode = h.mode_bucket().expect("test operation should succeed");
441 assert!(mode.count >= 1);
442 let ascii = h.render_ascii();
443 assert!(ascii.contains('['));
444 }
445 #[test]
446 fn test_histogram_edge_case() {
447 let mut h = Histogram::new(3, 0.0, 10.0);
448 h.record(0.0);
449 h.record(5.0);
450 h.record(9.99);
451 h.record(100.0);
452 assert_eq!(h.total, 4);
453 }
454}
455#[allow(dead_code)]
457pub trait ProfilingStep {
458 fn process(&mut self, events: &[(u64, ProfilingEvent)]);
460 fn name(&self) -> &str;
462}
463#[cfg(test)]
464mod profiler_extended_tests_3 {
465 use super::*;
466 #[test]
467 fn test_gc_collection_record() {
468 let r = GcCollectionRecord::new(1000, 50, 150, 500_000);
469 assert!((r.efficiency() - 50.0 / 200.0).abs() < 1e-9);
470 }
471 #[test]
472 fn test_gc_profiler() {
473 let mut gcp = GcProfiler::new();
474 gcp.record(30, 100, 1_000_000);
475 gcp.record(20, 80, 2_000_000);
476 assert_eq!(gcp.collection_count(), 2);
477 assert_eq!(gcp.total_collected(), 50);
478 assert!((gcp.avg_pause_ns() - 1_500_000.0).abs() < 1.0);
479 assert_eq!(gcp.max_pause_ns(), 2_000_000);
480 let summary = gcp.summary();
481 assert!(summary.contains("2 collections"));
482 }
483 #[test]
484 fn test_counting_step() {
485 let mut step = CountingStep::new("counter");
486 assert_eq!(step.name(), "counter");
487 let events = vec![
488 (
489 0u64,
490 ProfilingEvent::FunctionCall {
491 name: "f".to_string(),
492 depth: 0,
493 },
494 ),
495 (
496 1u64,
497 ProfilingEvent::Allocation {
498 size: 100,
499 tag: "t".to_string(),
500 },
501 ),
502 (
503 2u64,
504 ProfilingEvent::FunctionCall {
505 name: "g".to_string(),
506 depth: 0,
507 },
508 ),
509 ];
510 step.process(&events);
511 assert_eq!(step.counts.get("FunctionCall").copied().unwrap_or(0), 2);
512 assert_eq!(step.counts.get("Allocation").copied().unwrap_or(0), 1);
513 }
514 #[test]
515 fn test_annotated_timeline() {
516 let mut tl = AnnotatedTimeline::new();
517 tl.annotate(TimelineAnnotation::new(100, "start", "checkpoint"));
518 tl.annotate(TimelineAnnotation::new(500, "mid", "checkpoint"));
519 tl.annotate(TimelineAnnotation::new(1000, "end", "checkpoint"));
520 let in_range = tl.annotations_in_range(200, 700);
521 assert_eq!(in_range.len(), 1);
522 assert_eq!(in_range[0].text, "mid");
523 }
524 #[test]
525 fn test_comprehensive_report() {
526 let mut session = ProfilingSession::new("test");
527 session.start();
528 session.enter_function("main");
529 session.alloc(1024, "heap");
530 session.sampler.take_sample(0);
531 session.exit_function("main");
532 session.stop();
533 let report = ComprehensiveProfilingReport::build(&session);
534 let text = report.to_text();
535 assert!(text.contains("Profile Report"));
536 }
537 #[test]
538 fn test_gc_profiler_empty() {
539 let gcp = GcProfiler::new();
540 assert_eq!(gcp.collection_count(), 0);
541 assert_eq!(gcp.total_collected(), 0);
542 assert!((gcp.avg_pause_ns() - 0.0).abs() < 1e-9);
543 assert_eq!(gcp.max_pause_ns(), 0);
544 }
545 #[test]
546 fn test_timeline_annotation_categories() {
547 let mut tl = AnnotatedTimeline::new();
548 tl.annotate(TimelineAnnotation::new(0, "start error", "error"));
549 tl.annotate(TimelineAnnotation::new(0, "start ok", "checkpoint"));
550 let errors: Vec<_> = tl
551 .annotations
552 .iter()
553 .filter(|a| a.category == "error")
554 .collect();
555 assert_eq!(errors.len(), 1);
556 }
557 #[test]
558 fn test_flame_graph_from_profiler() {
559 let mut p = SamplingProfiler::new(1_000_000);
560 p.enabled = true;
561 p.current_stack = vec!["main".into(), "foo".into()];
562 p.take_sample(0);
563 p.take_sample(0);
564 let fg = FlameGraph::from_profiler(&p);
565 assert_eq!(fg.total_samples, 2);
566 let text = fg.render_text();
567 assert!(text.contains("(all)"));
568 }
569}
570#[cfg(test)]
571mod middleware_tests {
572 use super::*;
573 #[test]
574 fn test_middleware_instrument() {
575 let mut mw = ProfilingMiddleware::new();
576 let result = mw.instrument("compute", || 6 * 7);
577 assert_eq!(result, 42);
578 let report = mw.report();
579 assert_eq!(report.total_calls, 1);
580 }
581 #[test]
582 fn test_middleware_inactive() {
583 let mut mw = ProfilingMiddleware::new();
584 mw.active = false;
585 mw.instrument("ignored", || ());
586 let report = mw.report();
587 assert_eq!(report.total_calls, 0);
588 }
589}