Skip to main content

runmat_runtime/
coverage.rs

1use 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/// Scope-owned backend-neutral coverage counters for one exact executable
13/// invocation. VM, native, and browser-capable executors report the same stable
14/// counter keys through this runtime boundary.
15#[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}