mod harness;
use std::time::Instant;
use harness::TestRepo;
const SMALL_FILES: usize = 2000;
const SMALL_SIZE: usize = 4096;
const CIPHER_BYTES: usize = 8 * 1024 * 1024;
const RUNS: usize = 5;
fn incompressible(size: usize, seed: u64) -> Vec<u8> {
let mut state = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
(0..size)
.map(|_| {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
(state >> 33) as u8
})
.collect()
}
fn timed_add(declaration: Option<&str>, files: usize, size: usize) -> u128 {
let repo = TestRepo::init();
repo.git_ok(["config", "core.autocrlf", "false"]);
if let Some(patterns) = declaration {
repo.init_xcrypt();
repo.write_xcrypt_config(&format!("{patterns}\n"));
repo.xcrypt_ok(["sync"]);
}
for index in 0..files {
repo.write_file(
&format!("src/f{index:04}.bin"),
&incompressible(size, index as u64),
);
}
let started = Instant::now();
repo.git_ok(["add", "-A"]);
started.elapsed().as_millis()
}
fn best(declaration: Option<&str>, files: usize, size: usize) -> u128 {
(0..RUNS)
.map(|_| timed_add(declaration, files, size))
.min()
.expect("RUNS is not zero")
}
#[test]
#[ignore = "timing; run deliberately with --release, see the module comment"]
fn a_file_that_is_only_passed_through_stays_almost_free() {
let bare = best(None, SMALL_FILES, SMALL_SIZE);
let filtered = best(Some("nothing/"), SMALL_FILES, SMALL_SIZE);
let overhead = filtered.saturating_sub(bare);
let per_file = overhead as f64 * 1000.0 / SMALL_FILES as f64;
println!(
"pass-through: {bare} ms bare, {filtered} ms filtered, \
{overhead} ms over {SMALL_FILES} files = {per_file:.1} µs/file (budget 25)"
);
assert!(
per_file <= 25.0,
"passing a file through costs {per_file:.1} µs, over the 25 µs budget \
in PRD §Non-Functional Requirements. This is charged to every file in \
every repository, not only to encrypted ones."
);
}
#[test]
#[ignore = "timing; run deliberately with --release, see the module comment"]
fn encrypting_a_small_file_stays_within_its_per_file_budget() {
let bare = best(None, SMALL_FILES, SMALL_SIZE);
let encrypted = best(Some("src/"), SMALL_FILES, SMALL_SIZE);
let overhead = encrypted.saturating_sub(bare);
let per_file = overhead as f64 * 1000.0 / SMALL_FILES as f64;
println!(
"encrypt per file: {bare} ms bare, {encrypted} ms encrypted, \
{overhead} ms over {SMALL_FILES} files = {per_file:.1} µs/file (budget 30)"
);
assert!(
per_file <= 30.0,
"encrypting costs {per_file:.1} µs per file, over the 30 µs budget in \
PRD §Non-Functional Requirements"
);
}
#[test]
#[ignore = "timing; run deliberately with --release, see the module comment"]
fn the_cipher_stays_within_its_per_byte_budget() {
let repo = TestRepo::init();
repo.init_xcrypt();
repo.write_xcrypt_config("src/\n");
repo.xcrypt_ok(["sync"]);
repo.write_file("src/big.bin", &incompressible(CIPHER_BYTES, 1));
repo.commit_all("one large secret");
let ciphertext = repo.blob_bytes("src/big.bin");
assert_eq!(
ciphertext.len(),
CIPHER_BYTES + 38,
"the blob is not ciphertext, so this would time nothing"
);
repo.write_file("carrier.bin", &ciphertext);
let elapsed = (0..RUNS)
.map(|_| {
let started = Instant::now();
let out = repo.xcrypt_ok(["diff", "carrier.bin"]);
let taken = started.elapsed();
assert_eq!(out.stdout.len(), CIPHER_BYTES, "diff did not decrypt it");
taken
})
.min()
.expect("RUNS is not zero");
let per_byte = elapsed.as_secs_f64() * 1e9 / CIPHER_BYTES as f64;
println!(
"cipher: {:.1} ms for {:.0} MB = {per_byte:.2} ns/B (budget 2.00, {:.0} MB/s)",
elapsed.as_secs_f64() * 1000.0,
CIPHER_BYTES as f64 / 1048576.0,
1000.0 / per_byte
);
assert!(
per_byte <= 2.0,
"the cipher runs at {per_byte:.2} ns/byte ({:.0} MB/s), over the 2 ns \
budget in PRD §Non-Functional Requirements. Check that `aes` picked a \
hardware backend on this target — `aes::hardware_accelerated()` \
answers it — and that the budget was calibrated for this machine.",
1000.0 / per_byte
);
}
#[test]
#[ignore = "timing; run deliberately with --release, see the module comment"]
fn the_attribute_stack_pays_for_ancestors_not_for_the_tree() {
use git_xcrypt::git::attributes::AttributeResolver;
let repo = TestRepo::init();
repo.write_file(".gitattributes", b"* filter=git-xcrypt\n");
repo.write_file("secrets/db.env", b"api_key = value\n");
for directory in 0..3000 {
let path = repo.path().join(format!("bulk/d{directory:04}"));
std::fs::create_dir_all(&path).expect("bulk directories");
for file in 0..10 {
std::fs::write(path.join(format!("f{file}.o")), b"x").expect("bulk files");
}
}
let elapsed = (0..RUNS)
.map(|_| {
let started = Instant::now();
let mut resolver = AttributeResolver::new(
repo.path(),
&repo.path().join(".git"),
None,
false,
Vec::new(),
);
let resolution = resolver.resolve(b"secrets/db.env");
let taken = started.elapsed();
assert!(
resolution.filter.is_ours(),
"the stack no longer resolves the catch-all, so this would time \
a resolver that reads nothing"
);
taken
})
.min()
.expect("RUNS is not zero");
let ms = elapsed.as_secs_f64() * 1000.0;
println!("attribute stack: {ms:.2} ms to build and answer once (budget 10.00)");
assert!(
ms <= 10.0,
"building the attribute stack took {ms:.2} ms on a tree whose bulk is \
not on the resolved path's ancestor chain — over the 10 ms budget in \
PRD §Non-Functional Requirements. The walk of the whole working tree \
is probably back in `AttributeResolver`."
);
}