1use std::result::Result;
2
3use more_asserts::{debug_assert_le, debug_assert_lt};
4use xet_core_structures::MerkleHashMap;
5use xet_core_structures::merklehash::{ChunkHashList, MerkleHash, file_hash};
6use xet_core_structures::metadata_shard::file_structs::{
7 FileDataSequenceEntry, FileDataSequenceHeader, FileMetadataExt, FileVerificationEntry, MDBFileInfo,
8};
9use xet_core_structures::metadata_shard::hash_is_global_dedup_eligible;
10use xet_runtime::core::XetContext;
11
12use super::constants::{MAX_XORB_BYTES, MAX_XORB_CHUNKS};
13use super::data_aggregator::DataAggregator;
14use super::dedup_metrics::DeduplicationMetrics;
15use super::defrag_prevention::DefragPrevention;
16use super::interface::DeduplicationDataInterface;
17use super::{Chunk, RawXorbData};
18use crate::progress_tracking::upload_tracking::FileXorbDependency;
19
20pub struct FileDeduper<DataInterfaceType: DeduplicationDataInterface> {
21 #[cfg_attr(not(feature = "simulation"), allow(dead_code))]
22 ctx: XetContext,
23
24 data_mng: DataInterfaceType,
25
26 file_id: u64,
28
29 new_data: Vec<Chunk>,
31
32 new_data_size: usize,
34
35 new_data_hash_lookup: MerkleHashMap<usize>,
37
38 chunk_hashes: ChunkHashList,
40
41 file_info: Vec<FileDataSequenceEntry>,
43
44 internally_referencing_entries: Vec<usize>,
46
47 defrag_tracker: DefragPrevention,
49
50 min_spacing_between_global_dedup_queries: usize,
53
54 next_chunk_index_eligible_for_global_dedup_query: usize,
56
57 deduplication_metrics: DeduplicationMetrics,
59}
60
61impl<DataInterfaceType: DeduplicationDataInterface> FileDeduper<DataInterfaceType> {
62 pub fn new(data_manager: DataInterfaceType, file_id: u64, ctx: XetContext) -> Self {
63 Self {
64 ctx: ctx.clone(),
65 data_mng: data_manager,
66 file_id,
67 new_data: Vec::new(),
68 new_data_size: 0,
69 new_data_hash_lookup: MerkleHashMap::new(),
70 chunk_hashes: Vec::new(),
71 file_info: Vec::new(),
72 internally_referencing_entries: Vec::new(),
73 defrag_tracker: DefragPrevention::new(&ctx),
74 min_spacing_between_global_dedup_queries: 0,
75 next_chunk_index_eligible_for_global_dedup_query: 0,
76 deduplication_metrics: DeduplicationMetrics::default(),
77 }
78 }
79
80 pub async fn process_chunks(
81 &mut self,
82 chunks: &[Chunk],
83 ) -> Result<DeduplicationMetrics, DataInterfaceType::ErrorType> {
84 let mut dedup_metrics = DeduplicationMetrics::default();
86
87 let mut xorb_dependencies = Vec::new();
89
90 let global_chunk_index_start = self.chunk_hashes.len();
92
93 #[cfg(feature = "simulation")]
94 let xorb_cut_bytes = self
95 .ctx
96 .config
97 .xorb
98 .simulation_max_bytes
99 .map(|bs| bs.as_u64().min(*MAX_XORB_BYTES as u64) as usize)
100 .unwrap_or(*MAX_XORB_BYTES);
101 #[cfg(not(feature = "simulation"))]
102 let xorb_cut_bytes = *MAX_XORB_BYTES;
103 #[cfg(feature = "simulation")]
104 let xorb_cut_chunks = self
105 .ctx
106 .config
107 .xorb
108 .simulation_max_chunks
109 .unwrap_or(*MAX_XORB_CHUNKS)
110 .min(*MAX_XORB_CHUNKS);
111 #[cfg(not(feature = "simulation"))]
112 let xorb_cut_chunks = *MAX_XORB_CHUNKS;
113
114 let chunk_hashes = Vec::from_iter(chunks.iter().map(|c| c.hash));
115
116 let mut deduped_blocks = vec![None; chunks.len()];
123
124 for first_pass in [true, false] {
128 let mut local_chunk_index = 0;
130 while local_chunk_index < chunks.len() {
131 let global_chunk_index = global_chunk_index_start + local_chunk_index;
132
133 if let Some((n_deduped, _, _)) = &deduped_blocks[local_chunk_index] {
135 local_chunk_index += n_deduped;
136 } else if let Some((n_deduped, fse, is_uploaded_shard)) =
137 self.data_mng.chunk_hash_dedup_query(&chunk_hashes[local_chunk_index..]).await?
138 {
139 if !first_pass {
140 dedup_metrics.deduped_chunks_by_global_dedup += n_deduped as u64;
143 dedup_metrics.deduped_bytes_by_global_dedup += fse.unpacked_segment_bytes as u64;
144 }
145
146 deduped_blocks[local_chunk_index] = Some((n_deduped, fse, is_uploaded_shard));
147 local_chunk_index += n_deduped;
148
149 } else {
157 if
159 first_pass
161 && (global_chunk_index == 0
163 || hash_is_global_dedup_eligible(&chunk_hashes[local_chunk_index]))
164 && global_chunk_index >= self.next_chunk_index_eligible_for_global_dedup_query
166 {
167 self.data_mng
168 .register_global_dedup_query(chunk_hashes[local_chunk_index])
169 .await?;
170
171 self.next_chunk_index_eligible_for_global_dedup_query =
172 global_chunk_index + self.min_spacing_between_global_dedup_queries;
173 }
174
175 local_chunk_index += 1;
176 }
177 }
178
179 let new_shards_added = self.data_mng.complete_global_dedup_queries().await?;
181
182 if !new_shards_added {
183 break;
184 }
185 }
186
187 let mut cur_idx = 0;
189
190 while cur_idx < chunks.len() {
191 let mut dedupe_query = deduped_blocks[cur_idx].take();
192
193 if dedupe_query.is_none() {
194 dedupe_query = self.dedup_query_against_local_data(&chunk_hashes[cur_idx..]);
197 }
198
199 if let Some((n_deduped, fse, is_external)) = dedupe_query {
200 dedup_metrics.deduped_chunks += n_deduped as u64;
201 dedup_metrics.deduped_bytes += fse.unpacked_segment_bytes as u64;
202 dedup_metrics.total_chunks += n_deduped as u64;
203 dedup_metrics.total_bytes += fse.unpacked_segment_bytes as u64;
204
205 if self.file_data_sequence_continues_current(&fse)
208 || self.defrag_tracker.allow_dedup_on_next_range(n_deduped)
209 {
210 if fse.xorb_hash != MerkleHash::marker() {
214 xorb_dependencies.push(FileXorbDependency {
215 file_id: self.file_id,
216 xorb_hash: fse.xorb_hash,
217 n_bytes: fse.unpacked_segment_bytes as u64,
218 is_external,
219 });
220 }
221
222 self.add_file_data_sequence_entry(fse, n_deduped);
224
225 cur_idx += n_deduped;
226 continue;
227 } else {
228 dedup_metrics.defrag_prevented_dedup_chunks += n_deduped as u64;
229 dedup_metrics.defrag_prevented_dedup_bytes += fse.unpacked_segment_bytes as u64;
230 }
231 }
232
233 let n_bytes = chunks[cur_idx].data.len();
235
236 dedup_metrics.total_chunks += 1;
237 dedup_metrics.total_bytes += n_bytes as u64;
238 dedup_metrics.new_bytes += n_bytes as u64;
239 dedup_metrics.new_chunks += 1;
240
241 if self.new_data_size + n_bytes > xorb_cut_bytes || self.new_data.len() + 1 > xorb_cut_chunks {
243 let new_xorb = self.cut_new_xorb();
244 xorb_dependencies.push(FileXorbDependency {
245 file_id: self.file_id,
246 xorb_hash: new_xorb.hash(),
247 n_bytes: new_xorb.num_bytes() as u64,
248 is_external: false,
249 });
250 self.data_mng.register_new_xorb(new_xorb).await?;
251 }
252
253 if !self.file_info.is_empty()
254 && self.file_info.last().unwrap().xorb_hash == MerkleHash::marker()
255 && self.file_info.last().unwrap().chunk_index_end as usize == self.new_data.len()
256 {
257 let last_entry = self.file_info.last_mut().unwrap();
260 last_entry.unpacked_segment_bytes += n_bytes as u32;
261 last_entry.chunk_index_end += 1;
262 self.defrag_tracker.increment_last_range_in_fragmentation_estimate(1);
263 } else {
264 let file_info_len = self.file_info.len();
268 self.internally_referencing_entries.push(file_info_len);
269 let chunk_idx = self.new_data.len();
270
271 self.file_info.push(FileDataSequenceEntry::new(
272 MerkleHash::marker(),
273 n_bytes,
274 chunk_idx,
275 chunk_idx + 1,
276 ));
277 self.defrag_tracker.add_range_to_fragmentation_estimate(1);
278 }
279
280 let chunk = chunks[cur_idx].clone();
281 self.new_data_size += chunk.data.len();
282 self.new_data_hash_lookup.insert(chunk.hash, self.new_data.len());
283 self.new_data.push(chunk);
284
285 cur_idx += 1;
287 }
288
289 self.deduplication_metrics.merge_in(&dedup_metrics);
290 self.chunk_hashes.extend(chunks.iter().map(|c| (c.hash, c.data.len() as u64)));
291
292 if !xorb_dependencies.is_empty() {
294 self.data_mng.register_xorb_dependencies(&xorb_dependencies).await;
295 }
296
297 Ok(dedup_metrics)
298 }
299
300 fn file_data_sequence_continues_current(&self, fse: &FileDataSequenceEntry) -> bool {
301 !self.file_info.is_empty()
302 && self.file_info.last().unwrap().xorb_hash == fse.xorb_hash
303 && self.file_info.last().unwrap().chunk_index_end == fse.chunk_index_start
304 }
305
306 fn add_file_data_sequence_entry(&mut self, fse: FileDataSequenceEntry, n_deduped: usize) {
309 if self.file_data_sequence_continues_current(&fse) {
312 let last_entry = self.file_info.last_mut().unwrap();
314 last_entry.unpacked_segment_bytes += fse.unpacked_segment_bytes;
315 last_entry.chunk_index_end = fse.chunk_index_end;
316
317 self.defrag_tracker.increment_last_range_in_fragmentation_estimate(n_deduped);
319 } else {
320 if fse.xorb_hash == MerkleHash::marker() {
322 self.internally_referencing_entries.push(self.file_info.len());
323 }
324 self.file_info.push(fse);
326 self.defrag_tracker.add_range_to_fragmentation_estimate(n_deduped);
327 }
328 }
329
330 fn cut_new_xorb(&mut self) -> RawXorbData {
332 let new_xorb = RawXorbData::from_chunks(&self.new_data[..], vec![0]);
334
335 let xorb_hash = new_xorb.hash();
336
337 for &idx in self.internally_referencing_entries.iter() {
340 let fse = &mut self.file_info[idx];
341 debug_assert_eq!(fse.xorb_hash, MerkleHash::marker());
342 debug_assert_lt!(fse.chunk_index_start as usize, self.new_data.len());
343 debug_assert_le!(fse.chunk_index_end as usize, self.new_data.len());
344
345 fse.xorb_hash = xorb_hash;
346 }
347
348 #[cfg(debug_assertions)]
349 {
350 for fse in self.file_info.iter() {
352 debug_assert_ne!(fse.xorb_hash, MerkleHash::marker());
353 }
354 }
355
356 self.new_data.clear();
358 self.new_data_hash_lookup.clear();
359 self.new_data_size = 0;
360 self.internally_referencing_entries.clear();
361
362 new_xorb
363 }
364
365 fn dedup_query_against_local_data(
368 &mut self,
369 chunks: &[MerkleHash],
370 ) -> Option<(usize, FileDataSequenceEntry, bool)> {
371 if let Some(&base_idx) = self.new_data_hash_lookup.get(&chunks[0]) {
374 let mut n_bytes = self.new_data[base_idx].data.len();
375
376 let mut end_idx = base_idx + 1;
377 for (i, chunk) in chunks.iter().enumerate().skip(1) {
378 if let Some(&idx) = self.new_data_hash_lookup.get(chunk)
379 && idx == base_idx + i
380 {
381 end_idx = idx + 1;
382 n_bytes += self.new_data[idx].data.len();
383 continue;
384 }
385 break;
386 }
387
388 Some((
389 end_idx - base_idx,
390 FileDataSequenceEntry::new(MerkleHash::marker(), n_bytes, base_idx, end_idx),
391 false,
392 ))
393 } else {
394 None
395 }
396 }
397
398 pub fn finalize(
404 self,
405 metadata_ext: Option<FileMetadataExt>,
406 ) -> (MerkleHash, ChunkHashList, DataAggregator, DeduplicationMetrics) {
407 let file_hash = file_hash(&self.chunk_hashes);
408
409 let metadata = FileDataSequenceHeader::new(file_hash, self.file_info.len(), true, metadata_ext.is_some());
410
411 let mut chunk_idx = 0;
412
413 let verification = self
415 .file_info
416 .iter()
417 .map(|entry| {
418 let n_chunks = (entry.chunk_index_end - entry.chunk_index_start) as usize;
419 let chunk_hashes: Vec<_> = self.chunk_hashes[chunk_idx..chunk_idx + n_chunks]
420 .iter()
421 .map(|(hash, _)| *hash)
422 .collect();
423 let range_hash =
424 xet_core_structures::metadata_shard::chunk_verification::range_hash_from_chunks(&chunk_hashes);
425 chunk_idx += n_chunks;
426
427 FileVerificationEntry::new(range_hash)
428 })
429 .collect();
430
431 let fi = MDBFileInfo {
432 metadata,
433 segments: self.file_info,
434 verification,
435 metadata_ext,
436 };
437
438 let remaining_data = DataAggregator::new(self.new_data, fi, self.internally_referencing_entries, self.file_id);
439
440 (file_hash, self.chunk_hashes, remaining_data, self.deduplication_metrics)
441 }
442}