runmat_runtime/
coverage.rs1use std::cell::RefCell;
2use std::collections::{BTreeMap, HashMap};
3use std::rc::Rc;
4
5type SharedCounters = Rc<RefCell<BTreeMap<u64, u64>>>;
6
7runmat_thread_local::runmat_thread_local! {
8 static ACTIVE: RefCell<HashMap<runmat_execution::ExecutionScopeId, Vec<SharedCounters>>> =
9 RefCell::new(HashMap::new());
10}
11
12#[derive(Debug)]
16pub struct CoverageSession {
17 scope_id: runmat_execution::ExecutionScopeId,
18 counters: SharedCounters,
19}
20
21impl CoverageSession {
22 pub fn start(runtime: &crate::context::RuntimeContext) -> Self {
23 let scope_id = runtime.execution().scope_id();
24 let counters = Rc::new(RefCell::new(BTreeMap::new()));
25 ACTIVE.with(|active| {
26 active
27 .borrow_mut()
28 .entry(scope_id)
29 .or_default()
30 .push(counters.clone());
31 });
32 Self { scope_id, counters }
33 }
34
35 pub fn counts(&self) -> BTreeMap<u64, u64> {
36 self.counters.borrow().clone()
37 }
38}
39
40impl Drop for CoverageSession {
41 fn drop(&mut self) {
42 ACTIVE.with(|active| {
43 let mut active = active.borrow_mut();
44 let remove_scope = if let Some(stack) = active.get_mut(&self.scope_id) {
45 let popped = stack.pop();
46 debug_assert!(popped
47 .as_ref()
48 .is_some_and(|counters| Rc::ptr_eq(counters, &self.counters)));
49 stack.is_empty()
50 } else {
51 debug_assert!(false, "coverage session scope missing during cleanup");
52 false
53 };
54 if remove_scope {
55 active.remove(&self.scope_id);
56 }
57 });
58 }
59}
60
61#[inline]
62pub fn hit_sites(sites: &[u64]) {
63 let Some(runtime) = crate::context::legacy::active() else {
64 return;
65 };
66 hit_sites_in(&runtime, sites);
67}
68
69#[inline]
70pub fn hit_sites_in(runtime: &crate::context::RuntimeContext, sites: &[u64]) {
71 if sites.is_empty() {
72 return;
73 }
74 let scope_id = runtime.execution().scope_id();
75 ACTIVE.with(|active| {
76 let counters = active
77 .borrow()
78 .get(&scope_id)
79 .and_then(|stack| stack.last().cloned());
80 let Some(counters) = counters else { return };
81 let mut counters = counters.borrow_mut();
82 for site in sites {
83 let counter = counters.entry(*site).or_default();
84 *counter = counter.saturating_add(1);
85 }
86 });
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 fn runtime() -> crate::context::RuntimeContext {
94 crate::context::RuntimeContext::new(Rc::new(
95 crate::execution::RuntimeExecutionService::new(),
96 ))
97 }
98
99 #[test]
100 fn nested_sessions_restore_the_outer_collector() {
101 let runtime = runtime();
102 futures::executor::block_on(runtime.scope(async {
103 let outer = CoverageSession::start(&runtime);
104 hit_sites(&[0]);
105 {
106 let inner = CoverageSession::start(&runtime);
107 hit_sites(&[1]);
108 assert_eq!(inner.counts(), BTreeMap::from([(1, 1)]));
109 }
110 hit_sites(&[0]);
111 assert_eq!(outer.counts(), BTreeMap::from([(0, 2)]));
112 }));
113 }
114
115 #[test]
116 fn independently_scoped_collectors_do_not_share_hits() {
117 let first = runtime();
118 let second = runtime();
119 let first_coverage = CoverageSession::start(&first);
120 let second_coverage = CoverageSession::start(&second);
121 hit_sites_in(&first, &[1]);
122 hit_sites_in(&second, &[2]);
123 assert_eq!(first_coverage.counts(), BTreeMap::from([(1, 1)]));
124 assert_eq!(second_coverage.counts(), BTreeMap::from([(2, 1)]));
125 }
126}