Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 43 additions & 4 deletions crates/core/src/archiver/file_archiver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::{
error::{ErrorKind, RusticError, RusticResult},
index::{ReadGlobalIndex, indexer::SharedIndexer},
progress::Progress,
repofile::configfile::ConfigFile,
repofile::configfile::{Chunker, ConfigFile},
};

/// The `FileArchiver` is responsible for archiving files.
Expand All @@ -35,6 +35,8 @@ pub(crate) struct FileArchiver<'a, BE: DecryptWriteBackend, I: ReadGlobalIndex>
index: &'a I,
data_packer: Packer<BE>,
config: ConfigFile,
/// Length and id of the chunks the chunker cuts from a run of zeros.
zero_chunk: (usize, DataId),
}

impl<'a, BE: DecryptWriteBackend, I: ReadGlobalIndex> FileArchiver<'a, BE, I> {
Expand Down Expand Up @@ -66,10 +68,18 @@ impl<'a, BE: DecryptWriteBackend, I: ReadGlobalIndex> FileArchiver<'a, BE, I> {
PackSizer::from_config(config, BlobType::Data, index.total_size(BlobType::Data));
let data_packer = Packer::new(be, BlobType::Data, indexer, pack_sizer)?;

// The Rabin hash of a window of zeros is 0, so runs of zeros are cut at the minimum size.
let zero_len = match config.chunker() {
Chunker::Rabin => config.chunk_min_size(),
Chunker::FixedSize => config.chunk_size(),
};
Comment on lines +71 to +75

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this in file_archiver as it is a property of the chunker. Can you move this as a method there?

let zero_chunk = (zero_len, DataId::from(hash(&vec![0; zero_len])));

Ok(Self {
index,
data_packer,
config: config.clone(),
zero_chunk,
})
}

Expand Down Expand Up @@ -148,14 +158,20 @@ impl<'a, BE: DecryptWriteBackend, I: ReadGlobalIndex> FileArchiver<'a, BE, I> {
)?
.map(|chunk| {
let chunk = chunk?;
let id = hash(&chunk);
// Checking for zeros is much faster than hashing, which speeds up sparse files and
// disk images.
let id = if chunk.len() == self.zero_chunk.0 && is_zero(&chunk) {
self.zero_chunk.1
} else {
DataId::from(hash(&chunk))
};
let size = chunk.len() as u64;

if !self.index.has_data(&DataId::from(id)) {
if !self.index.has_data(&id) {
self.data_packer.add(chunk.into(), BlobId::from(id))?;
}
p.inc(size);
Ok((DataId::from(id), size))
Ok((id, size))
})
.collect::<RusticResult<_>>()?;

Expand All @@ -180,3 +196,26 @@ impl<'a, BE: DecryptWriteBackend, I: ReadGlobalIndex> FileArchiver<'a, BE, I> {
self.data_packer.finalize()
}
}

/// Checks if `data` contains only zeros. Unlike `iter().all()`, the inner loop is vectorized.
fn is_zero(data: &[u8]) -> bool {
data.chunks(4096)
.all(|block| block.iter().fold(0, |acc, &b| acc | b) == 0)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn is_zero_finds_any_nonzero_byte() {
let mut data = vec![0; 3 * 4096 + 7];
assert!(is_zero(&data));
assert!(is_zero(&[]));
for i in [0, 4095, 4096, data.len() - 1] {
data[i] = 1;
assert!(!is_zero(&data), "byte {i}");
data[i] = 0;
}
}
}
46 changes: 44 additions & 2 deletions crates/core/tests/integration/chunker.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use std::{path::PathBuf, str::FromStr};
use std::{collections::HashSet, fs, path::PathBuf, str::FromStr};

use anyhow::Result;
use bytesize::ByteSize;
use insta::{Settings, assert_ron_snapshot};
use rstest::rstest;
use tempfile::tempdir;

use rustic_core::{
BackupOptions, ConfigOptions,
BackupOptions, CheckOptions, ConfigOptions, PathList,
repofile::{Chunker, SnapshotFile},
};

Expand Down Expand Up @@ -46,3 +47,44 @@ fn test_chunker_params(

Ok(())
}

#[rstest]
#[case(Chunker::Rabin)]
#[case(Chunker::FixedSize)]
fn test_backup_zeros(set_up_repo: Result<RepoOpen>, #[case] chunker: Chunker) -> Result<()> {
let mut repo = set_up_repo?.to_indexed_ids()?;
let config_opts = ConfigOptions::default().set_chunker(chunker);
_ = repo.apply_config(&config_opts)?;

// Runs of zeros between data, so that zero chunks follow and precede other chunks.
let mut data = vec![0; 8 << 20];
let mut x: u32 = 1;
for range in [0..100_000, 3_000_123..3_100_000, (8 << 20) - 5..8 << 20] {
for b in &mut data[range] {
x = x.wrapping_mul(1_103_515_245).wrapping_add(12345);
*b = (x >> 24) as u8 | 1;
}
}
let dir = tempdir()?;
let file = dir.path().join("zeros");
fs::write(&file, &data)?;

let opts = BackupOptions::default().as_path(PathBuf::from_str("test")?);
_ = repo.backup(
&opts,
&PathList::from_iter(Some(dir.path().to_path_buf())),
SnapshotFile::default(),
)?;

// The zero chunks are stored once, under the hash of their content.
let repo = repo.to_indexed()?;
repo.check(CheckOptions::default().read_data(true))?
.is_ok()?;
let node = repo.node_from_snapshot_path("latest:test/zeros", |_| true)?;
let content = node.content.as_ref().unwrap();
assert!(content.iter().collect::<HashSet<_>>().len() < content.len());
let mut restored = Vec::new();
repo.dump(&node, &mut restored)?;
assert_eq!(restored, data);
Ok(())
}
Loading