1use 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#[derive(Debug, Clone)]
46pub struct ParallelChunkingConfig {
47 pub chunk_size: usize,
49 pub strategy: ChunkingStrategy,
51 pub max_links_per_node: usize,
53 pub hash_algorithm: HashAlgorithm,
55 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 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 pub fn with_threads(mut self, num_threads: usize) -> Self {
94 self.num_threads = Some(num_threads);
95 self
96 }
97
98 pub fn with_hash_algorithm(mut self, algorithm: HashAlgorithm) -> Self {
100 self.hash_algorithm = algorithm;
101 self
102 }
103
104 pub fn with_content_defined(mut self) -> Self {
106 self.strategy = ChunkingStrategy::ContentDefined;
107 self
108 }
109}
110
111#[derive(Debug, Clone)]
113pub struct ParallelChunkingResult {
114 pub root_cid: Cid,
116 pub chunk_count: usize,
118 pub total_bytes: usize,
120 pub dedup_stats: DeduplicationStats,
122 pub duration: Duration,
124 pub chunk_cids: Vec<Cid>,
126 pub dag_nodes: Vec<DagNode>,
128}
129
130pub struct ParallelChunker {
132 config: ParallelChunkingConfig,
133}
134
135impl ParallelChunker {
136 pub fn new() -> Self {
138 Self {
139 config: ParallelChunkingConfig::default(),
140 }
141 }
142
143 pub fn with_config(config: ParallelChunkingConfig) -> Self {
145 Self { config }
146 }
147
148 pub fn chunk_parallel(&self, data: &[u8]) -> Result<ParallelChunkingResult> {
153 let start = Instant::now();
154 let metrics = global_metrics();
155
156 if data.len() < 1_000_000 {
158 return self.chunk_sequential(data, start);
159 }
160
161 let chunk_ranges = self.calculate_chunk_ranges(data.len());
163
164 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 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 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 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 chunks.len() == 1 {
224 return Ok(DagBuildResult {
225 root_cid: chunks[0].0,
226 nodes: vec![],
227 });
228 }
229
230 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 let groups: Vec<_> = current_level.chunks(max_links).collect();
239
240 let parent_results: Vec<_> = groups
241 .par_iter()
242 .map(|group| {
243 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, data: None,
254 };
255
256 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 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 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 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 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
335struct DagBuildResult {
337 root_cid: Cid,
338 nodes: Vec<DagNode>,
339}
340
341pub struct ParallelDeduplicator {
343 seen_cids: Arc<Mutex<std::collections::HashSet<Cid>>>,
344 stats: Arc<Mutex<DeduplicationStats>>,
345}
346
347impl ParallelDeduplicator {
348 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 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 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]; 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]; 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 for i in 1..ranges.len() {
457 assert_eq!(ranges[i - 1].1, ranges[i].0);
458 }
459
460 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 assert!(ParallelChunkingConfig::with_chunk_size(100).is_err());
482
483 assert!(ParallelChunkingConfig::with_chunk_size(128 * 1024).is_ok());
485
486 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]; 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}