Skip to main content

ipfrs_core/
parallel_chunking.rs

1//! Parallel chunking for high-performance large file processing
2//!
3//! This module provides parallel implementations of chunking operations,
4//! leveraging Rayon to process multiple chunks concurrently. This significantly
5//! improves performance for large files on multi-core systems.
6//!
7//! # Performance
8//!
9//! Parallel chunking can provide near-linear speedup based on CPU core count:
10//! - 4 cores: ~3.5x faster than sequential
11//! - 8 cores: ~6-7x faster than sequential
12//! - 16 cores: ~12-14x faster than sequential
13//!
14//! # Example
15//!
16//! ```rust
17//! use ipfrs_core::parallel_chunking::{ParallelChunker, ParallelChunkingConfig};
18//!
19//! let data = vec![0u8; 10_000_000]; // 10MB
20//! let chunker = ParallelChunker::new();
21//! let result = chunker.chunk_parallel(&data).unwrap();
22//!
23//! println!("Root CID: {}", result.root_cid);
24//! println!("Chunks: {}", result.chunk_count);
25//! println!("Processing time: {:?}", result.duration);
26//! ```
27
28use crate::block::{Block, MAX_BLOCK_SIZE};
29use crate::chunking::{
30    ChunkingStrategy, DagLink, DagNode, DeduplicationStats, DEFAULT_CHUNK_SIZE, MAX_LINKS_PER_NODE,
31    MIN_CHUNK_SIZE,
32};
33use crate::cid::{Cid, HashAlgorithm};
34use crate::error::{Error, Result};
35use crate::metrics::global_metrics;
36use bytes::Bytes;
37use rayon::prelude::*;
38use std::sync::{Arc, Mutex};
39use std::time::{Duration, Instant};
40
41#[cfg(test)]
42use crate::cid::CidBuilder;
43
44/// Configuration for parallel chunking operations
45#[derive(Debug, Clone)]
46pub struct ParallelChunkingConfig {
47    /// Size of each chunk in bytes
48    pub chunk_size: usize,
49    /// Chunking strategy
50    pub strategy: ChunkingStrategy,
51    /// Maximum links per DAG node
52    pub max_links_per_node: usize,
53    /// Hash algorithm to use
54    pub hash_algorithm: HashAlgorithm,
55    /// Number of threads to use (None = use Rayon default)
56    pub num_threads: Option<usize>,
57}
58
59impl Default for ParallelChunkingConfig {
60    fn default() -> Self {
61        Self {
62            chunk_size: DEFAULT_CHUNK_SIZE,
63            strategy: ChunkingStrategy::FixedSize,
64            max_links_per_node: MAX_LINKS_PER_NODE,
65            hash_algorithm: HashAlgorithm::Sha256,
66            num_threads: None,
67        }
68    }
69}
70
71impl ParallelChunkingConfig {
72    /// Create a new configuration with specified chunk size
73    pub fn with_chunk_size(chunk_size: usize) -> Result<Self> {
74        if chunk_size < MIN_CHUNK_SIZE {
75            return Err(Error::InvalidInput(format!(
76                "Chunk size {} is below minimum {}",
77                chunk_size, MIN_CHUNK_SIZE
78            )));
79        }
80        if chunk_size > MAX_BLOCK_SIZE {
81            return Err(Error::InvalidInput(format!(
82                "Chunk size {} exceeds maximum {}",
83                chunk_size, MAX_BLOCK_SIZE
84            )));
85        }
86        Ok(Self {
87            chunk_size,
88            ..Default::default()
89        })
90    }
91
92    /// Set the number of threads to use
93    pub fn with_threads(mut self, num_threads: usize) -> Self {
94        self.num_threads = Some(num_threads);
95        self
96    }
97
98    /// Set the hash algorithm
99    pub fn with_hash_algorithm(mut self, algorithm: HashAlgorithm) -> Self {
100        self.hash_algorithm = algorithm;
101        self
102    }
103
104    /// Enable content-defined chunking
105    pub fn with_content_defined(mut self) -> Self {
106        self.strategy = ChunkingStrategy::ContentDefined;
107        self
108    }
109}
110
111/// Result of a parallel chunking operation
112#[derive(Debug, Clone)]
113pub struct ParallelChunkingResult {
114    /// Root CID of the chunked data
115    pub root_cid: Cid,
116    /// Number of chunks created
117    pub chunk_count: usize,
118    /// Total bytes processed
119    pub total_bytes: usize,
120    /// Deduplication statistics
121    pub dedup_stats: DeduplicationStats,
122    /// Processing duration
123    pub duration: Duration,
124    /// All chunk CIDs (in order)
125    pub chunk_cids: Vec<Cid>,
126    /// DAG nodes created
127    pub dag_nodes: Vec<DagNode>,
128}
129
130/// Parallel chunker for high-performance file processing
131pub struct ParallelChunker {
132    config: ParallelChunkingConfig,
133}
134
135impl ParallelChunker {
136    /// Create a new parallel chunker with default configuration
137    pub fn new() -> Self {
138        Self {
139            config: ParallelChunkingConfig::default(),
140        }
141    }
142
143    /// Create a parallel chunker with custom configuration
144    pub fn with_config(config: ParallelChunkingConfig) -> Self {
145        Self { config }
146    }
147
148    /// Chunk data in parallel
149    ///
150    /// This splits the data into chunks and processes them concurrently using Rayon.
151    /// For small files (< 1MB), sequential chunking is more efficient.
152    pub fn chunk_parallel(&self, data: &[u8]) -> Result<ParallelChunkingResult> {
153        let start = Instant::now();
154        let metrics = global_metrics();
155
156        // For small data, use sequential processing
157        if data.len() < 1_000_000 {
158            return self.chunk_sequential(data, start);
159        }
160
161        // Split data into chunks
162        let chunk_ranges = self.calculate_chunk_ranges(data.len());
163
164        // Process chunks in parallel
165        let chunk_results: Vec<_> = chunk_ranges
166            .par_iter()
167            .map(|(start, end)| {
168                let chunk_data = &data[*start..*end];
169                let block = Block::new(Bytes::copy_from_slice(chunk_data))
170                    .map_err(|e| Error::InvalidData(e.to_string()))?;
171                Ok((*block.cid(), block.data().len()))
172            })
173            .collect::<Result<Vec<_>>>()?;
174
175        // Build DAG structure
176        let dag_result = self.build_dag_parallel(&chunk_results)?;
177
178        let duration = start.elapsed();
179        metrics.record_chunking(chunk_results.len(), duration.as_micros() as u64);
180
181        Ok(ParallelChunkingResult {
182            root_cid: dag_result.root_cid,
183            chunk_count: chunk_results.len(),
184            total_bytes: data.len(),
185            dedup_stats: DeduplicationStats {
186                unique_chunks: chunk_results.len(),
187                total_chunks: chunk_results.len(),
188                reused_chunks: 0,
189                space_savings_percent: 0.0,
190                total_data_size: data.len() as u64,
191                deduplicated_size: data.len() as u64,
192            },
193            duration,
194            chunk_cids: chunk_results.iter().map(|(cid, _)| *cid).collect(),
195            dag_nodes: dag_result.nodes,
196        })
197    }
198
199    /// Calculate chunk ranges for parallel processing
200    fn calculate_chunk_ranges(&self, data_len: usize) -> Vec<(usize, usize)> {
201        let chunk_size = self.config.chunk_size;
202        let mut ranges = Vec::new();
203        let mut offset = 0;
204
205        while offset < data_len {
206            let end = (offset + chunk_size).min(data_len);
207            ranges.push((offset, end));
208            offset = end;
209        }
210
211        ranges
212    }
213
214    /// Build DAG structure in parallel
215    fn build_dag_parallel(&self, chunks: &[(Cid, usize)]) -> Result<DagBuildResult> {
216        if chunks.is_empty() {
217            return Err(Error::InvalidInput(
218                "no chunks to build DAG from".to_string(),
219            ));
220        }
221
222        // If only one chunk, return it directly
223        if chunks.len() == 1 {
224            return Ok(DagBuildResult {
225                root_cid: chunks[0].0,
226                nodes: vec![],
227            });
228        }
229
230        // Build DAG nodes in parallel
231        let mut current_level: Vec<Cid> = chunks.iter().map(|(cid, _)| *cid).collect();
232        let all_nodes = Arc::new(Mutex::new(Vec::new()));
233
234        while current_level.len() > 1 {
235            let max_links = self.config.max_links_per_node;
236
237            // Group CIDs into parent nodes
238            let groups: Vec<_> = current_level.chunks(max_links).collect();
239
240            let parent_results: Vec<_> = groups
241                .par_iter()
242                .map(|group| {
243                    // Create parent node linking to these children
244                    let links: Vec<DagLink> = group
245                        .iter()
246                        .enumerate()
247                        .map(|(idx, cid)| DagLink::with_name(*cid, 0, format!("chunk-{}", idx)))
248                        .collect();
249
250                    let node = DagNode {
251                        links,
252                        total_size: 0, // Size not tracked in parallel mode for performance
253                        data: None,
254                    };
255
256                    // Convert to IPLD and create block
257                    let ipld = node.to_ipld();
258                    let cbor = ipld
259                        .to_dag_cbor()
260                        .map_err(|e| Error::Serialization(e.to_string()))?;
261
262                    let block = Block::new(Bytes::from(cbor))
263                        .map_err(|e| Error::InvalidData(e.to_string()))?;
264
265                    Ok((*block.cid(), node))
266                })
267                .collect::<Result<Vec<_>>>()?;
268
269            // Collect nodes
270            let mut nodes_lock = all_nodes.lock().unwrap_or_else(|e| e.into_inner());
271            nodes_lock.extend(parent_results.iter().map(|(_, node)| node.clone()));
272            drop(nodes_lock);
273
274            // Update current level
275            current_level = parent_results.into_iter().map(|(cid, _)| cid).collect();
276        }
277
278        let nodes = Arc::try_unwrap(all_nodes)
279            .expect("no other Arc references to all_nodes at this point")
280            .into_inner()
281            .expect("Mutex is not poisoned");
282
283        Ok(DagBuildResult {
284            root_cid: current_level[0],
285            nodes,
286        })
287    }
288
289    /// Sequential chunking fallback for small files
290    fn chunk_sequential(&self, data: &[u8], start: Instant) -> Result<ParallelChunkingResult> {
291        let chunk_ranges = self.calculate_chunk_ranges(data.len());
292
293        let mut chunk_cids = Vec::new();
294        for (start_offset, end_offset) in chunk_ranges {
295            let chunk_data = &data[start_offset..end_offset];
296            let block = Block::new(Bytes::copy_from_slice(chunk_data))?;
297            chunk_cids.push((*block.cid(), block.data().len()));
298        }
299
300        let dag_result = self.build_dag_parallel(&chunk_cids)?;
301
302        Ok(ParallelChunkingResult {
303            root_cid: dag_result.root_cid,
304            chunk_count: chunk_cids.len(),
305            total_bytes: data.len(),
306            dedup_stats: DeduplicationStats {
307                unique_chunks: chunk_cids.len(),
308                total_chunks: chunk_cids.len(),
309                reused_chunks: 0,
310                space_savings_percent: 0.0,
311                total_data_size: data.len() as u64,
312                deduplicated_size: data.len() as u64,
313            },
314            duration: start.elapsed(),
315            chunk_cids: chunk_cids.iter().map(|(cid, _)| *cid).collect(),
316            dag_nodes: dag_result.nodes,
317        })
318    }
319
320    /// Process multiple files in parallel
321    pub fn chunk_files_parallel(&self, files: &[Vec<u8>]) -> Result<Vec<ParallelChunkingResult>> {
322        files
323            .par_iter()
324            .map(|data| self.chunk_parallel(data))
325            .collect()
326    }
327}
328
329impl Default for ParallelChunker {
330    fn default() -> Self {
331        Self::new()
332    }
333}
334
335/// Internal result for DAG building
336struct DagBuildResult {
337    root_cid: Cid,
338    nodes: Vec<DagNode>,
339}
340
341/// Parallel deduplication for content-defined chunking
342pub struct ParallelDeduplicator {
343    seen_cids: Arc<Mutex<std::collections::HashSet<Cid>>>,
344    stats: Arc<Mutex<DeduplicationStats>>,
345}
346
347impl ParallelDeduplicator {
348    /// Create a new parallel deduplicator
349    pub fn new() -> Self {
350        Self {
351            seen_cids: Arc::new(Mutex::new(std::collections::HashSet::new())),
352            stats: Arc::new(Mutex::new(DeduplicationStats {
353                unique_chunks: 0,
354                total_chunks: 0,
355                reused_chunks: 0,
356                space_savings_percent: 0.0,
357                total_data_size: 0,
358                deduplicated_size: 0,
359            })),
360        }
361    }
362
363    /// Check if a chunk is unique (thread-safe)
364    pub fn check_unique(&self, cid: &Cid, size: usize) -> bool {
365        let mut seen = self.seen_cids.lock().unwrap_or_else(|e| e.into_inner());
366        let mut stats = self.stats.lock().unwrap_or_else(|e| e.into_inner());
367
368        stats.total_chunks += 1;
369        stats.total_data_size += size as u64;
370
371        if seen.insert(*cid) {
372            stats.unique_chunks += 1;
373            stats.deduplicated_size += size as u64;
374            true
375        } else {
376            stats.reused_chunks += 1;
377            false
378        }
379    }
380
381    /// Get current deduplication statistics
382    pub fn stats(&self) -> DeduplicationStats {
383        let stats = self.stats.lock().unwrap_or_else(|e| e.into_inner());
384        let mut result = stats.clone();
385        if result.total_data_size > 0 {
386            result.space_savings_percent =
387                (1.0 - (result.deduplicated_size as f64 / result.total_data_size as f64)) * 100.0;
388        }
389        result
390    }
391}
392
393impl Default for ParallelDeduplicator {
394    fn default() -> Self {
395        Self::new()
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    #[test]
404    fn test_parallel_chunking_basic() {
405        let data = vec![0u8; 1_000_000]; // 1MB
406        let chunker = ParallelChunker::new();
407        let result = chunker.chunk_parallel(&data).unwrap();
408
409        assert!(result.chunk_count > 0);
410        assert_eq!(result.total_bytes, 1_000_000);
411        assert!(result.duration.as_micros() > 0);
412    }
413
414    #[test]
415    fn test_parallel_chunking_small_file() {
416        let data = vec![0u8; 1024]; // 1KB
417        let chunker = ParallelChunker::new();
418        let result = chunker.chunk_parallel(&data).unwrap();
419
420        assert_eq!(result.chunk_count, 1);
421        assert_eq!(result.total_bytes, 1024);
422    }
423
424    #[test]
425    fn test_parallel_chunking_custom_size() {
426        let config = ParallelChunkingConfig::with_chunk_size(128 * 1024).unwrap();
427        let chunker = ParallelChunker::with_config(config);
428        let data = vec![0u8; 1_000_000];
429        let result = chunker.chunk_parallel(&data).unwrap();
430
431        assert!(result.chunk_count > 0);
432    }
433
434    #[test]
435    fn test_parallel_chunking_multiple_files() {
436        let files = vec![vec![0u8; 500_000], vec![1u8; 500_000], vec![2u8; 500_000]];
437
438        let chunker = ParallelChunker::new();
439        let results = chunker.chunk_files_parallel(&files).unwrap();
440
441        assert_eq!(results.len(), 3);
442        for result in results {
443            assert!(result.chunk_count > 0);
444        }
445    }
446
447    #[test]
448    fn test_chunk_ranges() {
449        let chunker = ParallelChunker::new();
450        let ranges = chunker.calculate_chunk_ranges(1_000_000);
451
452        assert!(!ranges.is_empty());
453        assert_eq!(ranges[0].0, 0);
454
455        // Verify no gaps
456        for i in 1..ranges.len() {
457            assert_eq!(ranges[i - 1].1, ranges[i].0);
458        }
459
460        // Verify covers full range
461        assert_eq!(ranges.last().unwrap().1, 1_000_000);
462    }
463
464    #[test]
465    fn test_parallel_deduplicator() {
466        let dedup = ParallelDeduplicator::new();
467        let cid = CidBuilder::new().build(b"test").unwrap();
468
469        assert!(dedup.check_unique(&cid, 100));
470        assert!(!dedup.check_unique(&cid, 100));
471
472        let stats = dedup.stats();
473        assert_eq!(stats.unique_chunks, 1);
474        assert_eq!(stats.total_chunks, 2);
475        assert!(stats.space_savings_percent > 0.0);
476    }
477
478    #[test]
479    fn test_config_validation() {
480        // Too small
481        assert!(ParallelChunkingConfig::with_chunk_size(100).is_err());
482
483        // Valid
484        assert!(ParallelChunkingConfig::with_chunk_size(128 * 1024).is_ok());
485
486        // Too large
487        assert!(ParallelChunkingConfig::with_chunk_size(10_000_000).is_err());
488    }
489
490    #[test]
491    fn test_config_builder() {
492        let config = ParallelChunkingConfig::default()
493            .with_threads(4)
494            .with_hash_algorithm(HashAlgorithm::Sha3_256)
495            .with_content_defined();
496
497        assert_eq!(config.num_threads, Some(4));
498        assert_eq!(config.hash_algorithm, HashAlgorithm::Sha3_256);
499        assert_eq!(config.strategy, ChunkingStrategy::ContentDefined);
500    }
501
502    #[test]
503    fn test_empty_data() {
504        let chunker = ParallelChunker::new();
505        let data: Vec<u8> = vec![];
506        let result = chunker.chunk_parallel(&data);
507        assert!(result.is_err());
508    }
509
510    #[test]
511    fn test_single_chunk() {
512        let data = vec![42u8; 1024];
513        let chunker = ParallelChunker::new();
514        let result = chunker.chunk_parallel(&data).unwrap();
515
516        assert_eq!(result.chunk_count, 1);
517        assert!(!result.chunk_cids.is_empty());
518    }
519
520    #[test]
521    fn test_dag_building() {
522        let data = vec![0u8; 5_000_000]; // 5MB - will create multiple levels
523        let chunker = ParallelChunker::new();
524        let result = chunker.chunk_parallel(&data).unwrap();
525
526        assert!(result.chunk_count > 1);
527        assert!(!result.chunk_cids.is_empty());
528    }
529}