Skip to main content

wow_mpq/
single_archive_parallel.rs

1//! Single archive parallel processing support
2//!
3//! This module provides utilities for reading multiple files from a single MPQ archive
4//! in parallel. This is achieved by cloning file handles for each thread, allowing
5//! concurrent reads without seek conflicts.
6
7use crate::{Archive, Error, Result};
8use rayon::prelude::*;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12/// A thread-safe wrapper around an MPQ archive for parallel operations
13///
14/// `ParallelArchive` enables concurrent reads from a single MPQ archive by
15/// giving each thread its own file handle. This avoids seek conflicts that
16/// would occur with a shared file handle.
17///
18/// # Examples
19///
20/// ```no_run
21/// use wow_mpq::single_archive_parallel::ParallelArchive;
22///
23/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
24/// let archive = ParallelArchive::open("data.mpq")?;
25///
26/// // Extract multiple files in parallel
27/// let files = vec!["file1.txt", "file2.txt", "file3.txt"];
28/// let results = archive.extract_files_parallel(&files)?;
29///
30/// for (filename, data) in results {
31///     println!("{}: {} bytes", filename, data.len());
32/// }
33/// # Ok(())
34/// # }
35/// ```
36#[derive(Debug)]
37pub struct ParallelArchive {
38    /// Path to the archive file
39    path: PathBuf,
40    /// Cached file list for quick lookups
41    file_list: Arc<Vec<String>>,
42}
43
44impl ParallelArchive {
45    /// Open an MPQ archive for parallel processing
46    ///
47    /// This caches the file list upfront to enable efficient parallel operations.
48    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
49        let path = path.as_ref().to_path_buf();
50
51        // Open archive and get file list
52        let mut archive = Archive::open(&path)?;
53        let entries = archive.list()?;
54        let file_list = Arc::new(entries.into_iter().map(|e| e.name).collect());
55
56        Ok(Self { path, file_list })
57    }
58
59    /// Extract multiple files from the archive in parallel
60    ///
61    /// Each file is extracted in a separate thread with its own file handle.
62    /// This allows true parallel I/O without seek conflicts.
63    ///
64    /// # Returns
65    /// A vector of (filename, data) tuples in the same order as the input
66    pub fn extract_files_parallel(&self, filenames: &[&str]) -> Result<Vec<(String, Vec<u8>)>> {
67        filenames
68            .par_iter()
69            .map(|&filename| {
70                // Each thread opens its own file handle
71                let data = self.read_file_with_new_handle(filename)?;
72                Ok((filename.to_string(), data))
73            })
74            .collect()
75    }
76
77    /// Extract files matching a predicate in parallel
78    ///
79    /// This method lists all files in the archive and extracts those that
80    /// match the given predicate function.
81    pub fn extract_matching_parallel<F>(&self, predicate: F) -> Result<Vec<(String, Vec<u8>)>>
82    where
83        F: Fn(&str) -> bool + Sync,
84    {
85        // Get the cached file list
86        let files = self.list_files();
87
88        // Filter and extract in parallel
89        files
90            .par_iter()
91            .filter(|name| predicate(name))
92            .map(|filename| {
93                let data = self.read_file_with_new_handle(filename)?;
94                Ok((filename.clone(), data))
95            })
96            .collect()
97    }
98
99    /// Process files in parallel with a custom function
100    ///
101    /// This is the most flexible method, allowing custom processing of each file.
102    pub fn process_files_parallel<F, T>(&self, filenames: &[&str], processor: F) -> Result<Vec<T>>
103    where
104        F: Fn(&str, Vec<u8>) -> Result<T> + Sync,
105        T: Send,
106    {
107        filenames
108            .par_iter()
109            .map(|&filename| {
110                let data = self.read_file_with_new_handle(filename)?;
111                processor(filename, data)
112            })
113            .collect()
114    }
115
116    /// Read a file using a new file handle
117    ///
118    /// This is the core method that enables parallel reads. Each call opens
119    /// a new file handle, avoiding conflicts with other threads.
120    pub fn read_file_with_new_handle(&self, filename: &str) -> Result<Vec<u8>> {
121        // Open a new file handle for this thread
122        let mut archive = Archive::open(&self.path)?;
123
124        // Read the file
125        archive.read_file(filename)
126    }
127
128    /// Get the cached file list
129    pub fn list_files(&self) -> &[String] {
130        &self.file_list
131    }
132
133    /// Get the number of worker threads that will be used
134    pub fn thread_count(&self) -> usize {
135        rayon::current_num_threads()
136    }
137
138    /// Extract files in batches for better performance with many small files
139    ///
140    /// When extracting many small files, the overhead of opening file handles
141    /// can dominate. This method processes files in batches per thread.
142    pub fn extract_files_batched(
143        &self,
144        filenames: &[&str],
145        batch_size: usize,
146    ) -> Result<Vec<(String, Vec<u8>)>> {
147        // Divide files into chunks
148        let chunks: Vec<_> = filenames.chunks(batch_size).collect();
149
150        // Process each chunk in parallel
151        let results: Result<Vec<_>> = chunks
152            .par_iter()
153            .map(|chunk| {
154                // Open one archive handle per batch
155                let mut archive = Archive::open(&self.path)?;
156
157                // Extract all files in this batch
158                let mut batch_results = Vec::new();
159                for &filename in chunk.iter() {
160                    let data = archive.read_file(filename)?;
161                    batch_results.push((filename.to_string(), data));
162                }
163                Ok(batch_results)
164            })
165            .collect();
166
167        // Flatten the results
168        Ok(results?.into_iter().flatten().collect())
169    }
170}
171
172/// Configuration for parallel extraction operations
173#[derive(Debug, Clone)]
174pub struct ParallelConfig {
175    /// Number of worker threads (None = use rayon default)
176    pub num_threads: Option<usize>,
177    /// Batch size for small file extraction
178    pub batch_size: usize,
179    /// Whether to skip files that fail to extract
180    pub skip_errors: bool,
181}
182
183impl Default for ParallelConfig {
184    fn default() -> Self {
185        Self {
186            num_threads: None,
187            batch_size: 10,
188            skip_errors: false,
189        }
190    }
191}
192
193impl ParallelConfig {
194    /// Create a new configuration with default values
195    pub fn new() -> Self {
196        Self::default()
197    }
198
199    /// Set the number of worker threads
200    pub fn threads(mut self, num: usize) -> Self {
201        self.num_threads = Some(num);
202        self
203    }
204
205    /// Set the batch size for small files
206    pub fn batch_size(mut self, size: usize) -> Self {
207        self.batch_size = size;
208        self
209    }
210
211    /// Set whether to skip extraction errors
212    pub fn skip_errors(mut self, skip: bool) -> Self {
213        self.skip_errors = skip;
214        self
215    }
216}
217
218/// Extract files with custom configuration
219///
220/// This function efficiently handles large numbers of files by using batched extraction
221/// to reduce resource pressure and prevent system overload.
222pub fn extract_with_config<P: AsRef<Path>>(
223    archive_path: P,
224    filenames: &[&str],
225    config: ParallelConfig,
226) -> Result<Vec<(String, Result<Vec<u8>>)>> {
227    // For large file counts, force batched extraction to prevent resource exhaustion
228    let use_batched = filenames.len() > 1000;
229
230    if use_batched {
231        extract_with_config_batched(archive_path, filenames, config)
232    } else {
233        extract_with_config_unbatched(archive_path, filenames, config)
234    }
235}
236
237/// Extract files using batched approach for better resource management
238fn extract_with_config_batched<P: AsRef<Path>>(
239    archive_path: P,
240    filenames: &[&str],
241    config: ParallelConfig,
242) -> Result<Vec<(String, Result<Vec<u8>>)>> {
243    let archive = ParallelArchive::open(archive_path)?;
244
245    // Calculate appropriate batch size based on file count and available threads
246    let num_threads = config
247        .num_threads
248        .unwrap_or_else(rayon::current_num_threads);
249    let effective_batch_size = if filenames.len() > 5000 {
250        // For very large extractions, use larger batches to reduce overhead
251        std::cmp::max(config.batch_size, filenames.len() / (num_threads * 2))
252    } else {
253        config.batch_size
254    };
255
256    // Configure thread pool if specified
257    let pool = if let Some(threads) = config.num_threads {
258        rayon::ThreadPoolBuilder::new()
259            .num_threads(threads)
260            .build()
261            .map_err(|e| {
262                Error::Io(std::io::Error::other(format!(
263                    "Failed to create thread pool: {e}"
264                )))
265            })?
266    } else {
267        rayon::ThreadPoolBuilder::new().build().unwrap()
268    };
269
270    // Execute batched extraction in the configured thread pool
271    pool.install(|| {
272        // Split files into chunks for batched processing
273        let chunks: Vec<_> = filenames.chunks(effective_batch_size).collect();
274
275        // Process chunks in parallel, each chunk using one Archive handle
276        let batch_results: Result<Vec<_>> = chunks
277            .par_iter()
278            .map(|chunk| {
279                // Open one archive handle per batch to limit resource usage
280                let mut archive_handle = Archive::open(archive.path.as_path())?;
281
282                // Process all files in this batch with the same handle
283                let mut batch_results = Vec::with_capacity(chunk.len());
284                for &filename in chunk.iter() {
285                    let result = if config.skip_errors {
286                        archive_handle.read_file(filename)
287                    } else {
288                        let data = archive_handle.read_file(filename)?;
289                        Ok(data)
290                    };
291                    batch_results.push((filename.to_string(), result));
292                }
293                Ok(batch_results)
294            })
295            .collect();
296
297        // Flatten results while preserving order
298        match batch_results {
299            Ok(batches) => Ok(batches.into_iter().flatten().collect()),
300            Err(e) => Err(e),
301        }
302    })
303}
304
305/// Extract files using individual file approach for smaller file sets
306fn extract_with_config_unbatched<P: AsRef<Path>>(
307    archive_path: P,
308    filenames: &[&str],
309    config: ParallelConfig,
310) -> Result<Vec<(String, Result<Vec<u8>>)>> {
311    let archive = ParallelArchive::open(archive_path)?;
312
313    // Configure thread pool if specified
314    let pool = if let Some(threads) = config.num_threads {
315        rayon::ThreadPoolBuilder::new()
316            .num_threads(threads)
317            .build()
318            .map_err(|e| {
319                Error::Io(std::io::Error::other(format!(
320                    "Failed to create thread pool: {e}"
321                )))
322            })?
323    } else {
324        rayon::ThreadPoolBuilder::new().build().unwrap()
325    };
326
327    // Execute in the configured thread pool
328    pool.install(|| {
329        if config.skip_errors {
330            // Return results with individual errors
331            Ok(filenames
332                .par_iter()
333                .map(|&filename| {
334                    let result = archive.read_file_with_new_handle(filename);
335                    (filename.to_string(), result)
336                })
337                .collect())
338        } else {
339            // Fail on first error
340            let results: Result<Vec<_>> = filenames
341                .par_iter()
342                .map(|&filename| {
343                    let data = archive.read_file_with_new_handle(filename)?;
344                    Ok((filename.to_string(), Ok(data)))
345                })
346                .collect();
347            results
348        }
349    })
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355    use crate::ArchiveBuilder;
356    use tempfile::TempDir;
357
358    fn create_test_archive() -> (TempDir, PathBuf) {
359        let temp = TempDir::new().unwrap();
360        let path = temp.path().join("test.mpq");
361
362        let mut builder = ArchiveBuilder::new();
363
364        // Add multiple files for parallel testing
365        for i in 0..20 {
366            let content = format!("File {i} content with some data to make it larger").repeat(100);
367            builder = builder.add_file_data(content.into_bytes(), &format!("file_{i:02}.txt"));
368        }
369
370        builder.build(&path).unwrap();
371        (temp, path)
372    }
373
374    #[test]
375    fn test_parallel_extraction() {
376        let (_temp, archive_path) = create_test_archive();
377        let archive = ParallelArchive::open(&archive_path).unwrap();
378
379        let files = vec!["file_00.txt", "file_05.txt", "file_10.txt", "file_15.txt"];
380        let results = archive.extract_files_parallel(&files).unwrap();
381
382        assert_eq!(results.len(), 4);
383        for (filename, data) in results {
384            assert!(!data.is_empty());
385            assert!(files.contains(&filename.as_str()));
386        }
387    }
388
389    #[test]
390    fn test_extract_matching() {
391        let (_temp, archive_path) = create_test_archive();
392        let archive = ParallelArchive::open(&archive_path).unwrap();
393
394        // Extract files ending with 5
395        let results = archive
396            .extract_matching_parallel(|name| name.ends_with("5.txt"))
397            .unwrap();
398
399        assert_eq!(results.len(), 2); // file_05.txt and file_15.txt
400    }
401
402    #[test]
403    fn test_batched_extraction() {
404        let (_temp, archive_path) = create_test_archive();
405        let archive = ParallelArchive::open(&archive_path).unwrap();
406
407        let files: Vec<&str> = (0..10)
408            .map(|i| Box::leak(format!("file_{i:02}.txt").into_boxed_str()) as &str)
409            .collect();
410
411        let results = archive.extract_files_batched(&files, 3).unwrap();
412        assert_eq!(results.len(), 10);
413    }
414
415    #[test]
416    fn test_custom_processing() {
417        let (_temp, archive_path) = create_test_archive();
418        let archive = ParallelArchive::open(&archive_path).unwrap();
419
420        let files = vec!["file_00.txt", "file_01.txt"];
421
422        // Custom processor that returns file size
423        let sizes = archive
424            .process_files_parallel(&files, |_name, data| Ok(data.len()))
425            .unwrap();
426
427        assert_eq!(sizes.len(), 2);
428        for size in sizes {
429            assert!(size > 0);
430        }
431    }
432
433    #[test]
434    fn test_with_config() {
435        let (_temp, archive_path) = create_test_archive();
436
437        let config = ParallelConfig::new()
438            .threads(2)
439            .batch_size(5)
440            .skip_errors(true);
441
442        let files = vec!["file_00.txt", "nonexistent.txt", "file_01.txt"];
443        let results = extract_with_config(&archive_path, &files, config).unwrap();
444
445        assert_eq!(results.len(), 3);
446        assert!(results[0].1.is_ok());
447        assert!(results[1].1.is_err());
448        assert!(results[2].1.is_ok());
449    }
450}