Skip to main content

vyre_libs/parsing/
parallel_parse.rs

1//! ROADMAP L3  -  parallel parse across file corpus.
2//!
3//! Fan out `ParsedSourceLru::get_or_parse` across all available cores via
4//! `rayon::par_iter`.  Corpus-wide deduplication still happens because each
5//! unique `(content, extra)` pair is submitted to the cache exactly once;
6//! duplicate entries in the input slice map back to the same `Arc<T>`.
7//!
8//! ## Design notes
9//!
10//! - Ordering is preserved: the final sequential pass maps each input index
11//!   back to its parsed `Arc<T>`.
12//! - The cache is `Send + Sync` (backed by `Mutex<…>`), so sharing a
13//!   `&ParsedSourceLru<T>` across rayon workers is safe.
14//! - `parse` must be `Fn(&[u8]) -> T + Sync`; the closure is invoked from
15//!   multiple threads but never mutates shared state.
16//! - To avoid paying the parse cost multiple times for the same key under
17//!   concurrent cache misses (the L2 cache does not dedup in-flight parses),
18//!   the implementation first identifies unique keys, then calls
19//!   `get_or_parse` once per unique key.
20
21use rayon::prelude::*;
22// SourceHash is a 32-byte digest  -  FxHash on byte arrays is materially
23// faster than std SipHash, and these tables are pure-internal scratch
24// (no adversarial-input concern; the hash is already a content digest).
25use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
26use std::sync::Arc;
27
28use super::source_cache::{ParsedSourceLru, SourceHash};
29
30/// Parse every `(content, extra_key)` pair in `sources` in parallel,
31/// memoising through `cache`.  Returns `Arc<T>` values in input order.
32///
33/// # Type parameters
34///
35/// * `T`  -  parsed artifact; must be `Send + Sync` so it can cross thread
36///   boundaries inside `Arc<T>`.
37/// * `F`  -  parse function; must be `Sync` because it is called from
38///   multiple rayon workers concurrently.
39///
40/// # Example
41///
42/// ```
43/// use vyre_libs::parsing::source_cache::ParsedSourceLru;
44/// use vyre_libs::parsing::parallel_parse::parse_corpus_parallel;
45/// use std::sync::Arc;
46///
47/// let cache = ParsedSourceLru::with_capacity(64);
48/// let sources: Vec<(Vec<u8>, Vec<u8>)> = vec![
49///     (b"int main() {}".to_vec(), b"".to_vec()),
50///     (b"float x;".to_vec(), b"-O2".to_vec()),
51/// ];
52/// let results: Vec<Arc<usize>> = parse_corpus_parallel(&sources, &cache, |src| src.len());
53/// assert_eq!(*results[0], 13);
54/// assert_eq!(*results[1], 8);
55/// ```
56pub fn parse_corpus_parallel<T, F>(
57    sources: &[(Vec<u8>, Vec<u8>)],
58    cache: &ParsedSourceLru<T>,
59    parse: F,
60) -> Vec<Arc<T>>
61where
62    T: Send + Sync,
63    F: Fn(&[u8]) -> T + Sync,
64{
65    // Phase 1  -  compute all content hashes in parallel (no locking).
66    let keys: Vec<SourceHash> = sources
67        .par_iter()
68        .map(|(content, extra)| SourceHash::of(content, extra))
69        .collect();
70
71    // Phase 2  -  sequentially identify the first index of each unique key.
72    // This is O(N) and allocation-light; it guarantees that even on a
73    // cold cache the expensive `parse` closure runs once per unique source.
74    let mut unique_indices = Vec::with_capacity(keys.len());
75    let mut seen = HashSet::with_capacity_and_hasher(keys.len(), Default::default());
76    for (idx, key) in keys.iter().enumerate() {
77        if seen.insert(*key) {
78            unique_indices.push(idx);
79        }
80    }
81
82    // Phase 3  -  parse each unique source in parallel via get_or_parse.
83    let unique_parsed: Vec<(SourceHash, Arc<T>)> = unique_indices
84        .into_par_iter()
85        .map(|idx| {
86            let (content, extra) = &sources[idx];
87            let arc = cache.get_or_parse(content, extra, |s| parse(s));
88            (keys[idx], arc)
89        })
90        .collect();
91
92    // Phase 4  -  build lookup and map back to input order.
93    let lookup: HashMap<SourceHash, Arc<T>> = unique_parsed.into_iter().collect();
94    keys.into_iter()
95        .enumerate()
96        .map(|(idx, key)| {
97            if let Some(parsed) = lookup.get(&key) {
98                parsed.clone()
99            } else {
100                let (content, extra) = &sources[idx];
101                cache.get_or_parse(content, extra, |s| parse(s))
102            }
103        })
104        .collect()
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use std::sync::atomic::{AtomicUsize, Ordering};
111
112    /// Parse 10 distinct sources and assert every result matches the
113    /// expected value.
114    #[test]
115    fn distinct_corpus_parses_correctly() {
116        let cache: ParsedSourceLru<usize> = ParsedSourceLru::with_capacity(16);
117        let sources: Vec<(Vec<u8>, Vec<u8>)> = (0..10)
118            .map(|i| {
119                let content = format!("source {}", i).into_bytes();
120                (content, vec![])
121            })
122            .collect();
123
124        let results = parse_corpus_parallel(&sources, &cache, |src| src.len());
125
126        assert_eq!(results.len(), 10);
127        for (i, arc) in results.iter().enumerate() {
128            assert_eq!(**arc, format!("source {}", i).len());
129        }
130    }
131
132    /// Many entries share the same content; the parse closure must run
133    /// once per unique (content, extra) pair.
134    #[test]
135    fn shared_content_dedups_parse_calls() {
136        let cache: ParsedSourceLru<usize> = ParsedSourceLru::with_capacity(16);
137        let calls = AtomicUsize::new(0);
138
139        // 20 entries, only 3 unique (content, extra) pairs.
140        let sources: Vec<(Vec<u8>, Vec<u8>)> = vec![
141            (b"alpha".to_vec(), b"".to_vec()),
142            (b"beta".to_vec(), b"".to_vec()),
143            (b"alpha".to_vec(), b"".to_vec()),
144            (b"gamma".to_vec(), b"x".to_vec()),
145            (b"beta".to_vec(), b"".to_vec()),
146            (b"alpha".to_vec(), b"".to_vec()),
147            (b"gamma".to_vec(), b"x".to_vec()),
148            (b"beta".to_vec(), b"".to_vec()),
149            (b"alpha".to_vec(), b"".to_vec()),
150            (b"gamma".to_vec(), b"x".to_vec()),
151            (b"beta".to_vec(), b"".to_vec()),
152            (b"alpha".to_vec(), b"".to_vec()),
153            (b"gamma".to_vec(), b"x".to_vec()),
154            (b"beta".to_vec(), b"".to_vec()),
155            (b"alpha".to_vec(), b"".to_vec()),
156            (b"gamma".to_vec(), b"x".to_vec()),
157            (b"beta".to_vec(), b"".to_vec()),
158            (b"alpha".to_vec(), b"".to_vec()),
159            (b"gamma".to_vec(), b"x".to_vec()),
160            (b"beta".to_vec(), b"".to_vec()),
161        ];
162
163        let _results = parse_corpus_parallel(&sources, &cache, |src| {
164            calls.fetch_add(1, Ordering::SeqCst);
165            src.len()
166        });
167
168        // 3 unique keys => 3 parse calls.
169        assert_eq!(calls.load(Ordering::SeqCst), 3);
170
171        // Cache should hold exactly 3 entries.
172        assert_eq!(cache.len(), 3);
173    }
174
175    /// Empty corpus returns an empty vector without panicking.
176    #[test]
177    fn empty_corpus_returns_empty() {
178        let cache: ParsedSourceLru<usize> = ParsedSourceLru::with_capacity(4);
179        let sources: Vec<(Vec<u8>, Vec<u8>)> = vec![];
180        let results = parse_corpus_parallel(&sources, &cache, |src| src.len());
181        assert!(results.is_empty());
182    }
183
184    /// With many distinct sources, parsing must overlap across rayon
185    /// workers instead of degenerating into serial execution.
186    ///
187    /// Overlap is DETECTED, not hoped for. This used to spin 50,000 trivial
188    /// iterations and assert that two workers happened to be inside the
189    /// closure at the same instant. In a release build that loop optimizes to
190    /// almost nothing (the `black_box` is outside it), so on a busy machine
191    /// the closures finished before a second worker ever entered and the test
192    /// failed for reasons that had nothing to do with the code under test.
193    ///
194    /// Instead each closure now announces itself and waits, up to a generous
195    /// deadline, for a second worker to arrive. If the pool really is
196    /// parallel, the first two closures rendezvous and the assertion holds
197    /// every time. If rayon runs them serially the wait simply expires, which
198    /// is a bounded delay rather than a deadlock, and the assertion then
199    /// reports the real serialization.
200    #[test]
201    fn parallel_parse_overlaps_workers() {
202        let cache: ParsedSourceLru<usize> = ParsedSourceLru::with_capacity(32);
203        let n = rayon::current_num_threads().max(2) * 4;
204        let sources: Vec<(Vec<u8>, Vec<u8>)> = (0..n)
205            .map(|i| (format!("slow{}", i).into_bytes(), vec![]))
206            .collect();
207
208        let active = AtomicUsize::new(0);
209        let max_active = AtomicUsize::new(0);
210        let results = parse_corpus_parallel(&sources, &cache, |src| {
211            let now_active = active.fetch_add(1, Ordering::SeqCst) + 1;
212            let mut observed = max_active.load(Ordering::SeqCst);
213            while now_active > observed {
214                match max_active.compare_exchange(
215                    observed,
216                    now_active,
217                    Ordering::SeqCst,
218                    Ordering::SeqCst,
219                ) {
220                    Ok(_) => break,
221                    Err(next) => observed = next,
222                }
223            }
224            // Hold the closure open until a second worker joins, or the
225            // deadline expires. The deadline is what keeps a single-threaded
226            // pool from hanging here.
227            let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
228            while active.load(Ordering::SeqCst) < 2 && std::time::Instant::now() < deadline {
229                std::thread::yield_now();
230            }
231            // Re-read after the rendezvous: the peak may have been reached by
232            // the other worker while this one was waiting.
233            let peak = active.load(Ordering::SeqCst);
234            let mut observed = max_active.load(Ordering::SeqCst);
235            while peak > observed {
236                match max_active.compare_exchange(
237                    observed,
238                    peak,
239                    Ordering::SeqCst,
240                    Ordering::SeqCst,
241                ) {
242                    Ok(_) => break,
243                    Err(next) => observed = next,
244                }
245            }
246            active.fetch_sub(1, Ordering::SeqCst);
247            src.len()
248        });
249
250        assert_eq!(results.len(), n);
251        for (i, arc) in results.iter().enumerate() {
252            assert_eq!(
253                **arc,
254                format!("slow{}", i).len(),
255                "result value mismatch at index {}",
256                i
257            );
258        }
259
260        if rayon::current_num_threads() > 1 {
261            assert!(
262                max_active.load(Ordering::SeqCst) > 1,
263                "parse closures did not overlap across rayon workers"
264            );
265        }
266    }
267
268    /// Adversarial: zero-capacity cache with duplicate content.  Every
269    /// entry must still parse successfully even though the cache discards
270    /// everything.
271    #[test]
272    fn zero_capacity_cache_still_returns_all() {
273        let cache: ParsedSourceLru<usize> = ParsedSourceLru::with_capacity(0);
274        let sources: Vec<(Vec<u8>, Vec<u8>)> = vec![(b"dup".to_vec(), b"".to_vec()); 5];
275
276        let results = parse_corpus_parallel(&sources, &cache, |src| src.len());
277
278        assert_eq!(results.len(), 5);
279        for arc in &results {
280            assert_eq!(**arc, 3);
281        }
282    }
283
284    /// Adversarial: extremely large corpus (10 000 entries) where every
285    /// entry is identical.  Must not deadlock, must return correct length,
286    /// and parse closure must run exactly once because dedup happens
287    /// before any cache interaction.
288    #[test]
289    fn massive_identical_corpus_no_deadlock() {
290        let cache: ParsedSourceLru<usize> = ParsedSourceLru::with_capacity(1);
291        let n = 10_000;
292        let sources: Vec<(Vec<u8>, Vec<u8>)> = vec![(b"identical".to_vec(), b"".to_vec()); n];
293
294        let calls = AtomicUsize::new(0);
295        let results = parse_corpus_parallel(&sources, &cache, |src| {
296            calls.fetch_add(1, Ordering::SeqCst);
297            src.len()
298        });
299
300        assert_eq!(results.len(), n);
301        for arc in &results {
302            assert_eq!(**arc, 9);
303        }
304        // Phase 2 dedup guarantees exactly one parse call for one unique key.
305        assert_eq!(calls.load(Ordering::SeqCst), 1);
306    }
307}