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}