--- title: What would VCS look like if SSD wear was the primary constraint? date: 2026-04-20 layout: post --- Git: 1819 inodes, 59 MB to track a 36.59 MB repo pre-GC. GC collected 1514 inodes. Packing reclaimed 6 MB. Can we do better? PoC implements status, add, commit, log, show, diff. Supports symlinks, binary files. Optimized for SSD longevity — sequential reads/writes, reduced TBW/WA. Architecture: sorted index tracks files. Staging copies object to staging area, records path, mtime, size, SHA-1: ``` my $p = $wrk_entry->{path}; my $current_hash = hash_file_content($p); my $stg_path = File::Spec->catfile(TMP_DIR, $p); make_path(dirname($stg_path)); (-l $p) ? symlink(readlink($p), $stg_path) : copy($p, $stg_path); printf $out "%-40s\t%-40s\t%-40s\t%-12d\t%-10d\t%s\n", $current_hash, $idx_entry->{c_hash}, $idx_entry->{b_hash}, $wrk_entry->{mtime}, $wrk_entry->{size}, $p; ``` No CAS, no delta chains. One base file per tracked object. Revisions are patches against the base. When the patch outgrows the file, snapshot and rebase. Commits record directory structure and patchset. Patches stored in a tarball—one inode per commit. Tarballs >512 bytes gzipped. Trees deduplicated by content hash. Diff/show: look up revision, find tree and patchset, apply patch. O(1) checkout. Single corrupt patch can't poison history. External tools: sort, diff, patch, tar, gzip—all base system. Pipes and streams throughout. MEM_LIMIT falls back to disk for large repos: ``` use constant MEM_LIMIT => 64 * 1024 * 1024; use constant CHUNK_LEN => 8192; use constant IO_LAYER => ":raw:perlio(layer=" . CHUNK_LEN . ")"; if (!$use_disk) { @buf = sort @buf; return sub { my $line = shift @buf; return unless $line; chomp $line; my ($p, $m, $s) = split(/\t/, $line); return { path => $p, mtime => $m, size => $s }; }; } else { $flush->() if @buf; close $tmp_fh; open(my $sort_fh, "-|", "sort", "-t", "\t", "-k1,1", $tmp_path) or die $!; return sub { my $line = <$sort_fh>; unless ($line) { close $sort_fh; return; } chomp $line; my ($p, $s, $m) = split(/\t/, $line); return { path => $p, mtime => $m, size => $s }; }; } ``` Index and tree processing: O(N) two-finger walk. Streamed line-by-line, calibrated buffers. No mmap. ## Benchmarks Performed on T490 (i7-10510U, OpenBSD 7.8) against git v2.51.0: ### Impact of repository size Small repo: ``` ============================================================= BENCHMARK: 200 files @ 20 depth ============================================================= ACTION: Status ------------------------------------------------------------- METRIC | URN | GIT ----------------+----------------------+--------------------- Time | 0.10s | 0.00s Max RSS | 0.02 MB | 0.00 MB Page faults | Maj:0 / Min:0 | Maj:0 / Min:0 Inodes | 6 | 27 Repo size | 20 KB | 116 KB ------------------------------------------------------------- ACTION: Add ------------------------------------------------------------- METRIC | URN | GIT ----------------+----------------------+--------------------- Time | 0.16s | 0.17s Max RSS | 0.02 MB | 0.00 MB Page faults | Maj:0 / Min:0 | Maj:0 / Min:0 Inodes | 225 | 360 Repo size | 3700 KB | 3348 KB ------------------------------------------------------------- ACTION: Commit ------------------------------------------------------------- METRIC | URN | GIT ----------------+----------------------+--------------------- Time | 0.17s | 0.04s Max RSS | 0.02 MB | 0.01 MB Page faults | Maj:0 / Min:0 | Maj:0 / Min:0 Inodes | 347 | 397 Repo size | 4212 KB | 3496 KB ------------------------------------------------------------- ACTION: Status(Clean) ------------------------------------------------------------- METRIC | URN | GIT ----------------+----------------------+--------------------- Time | 0.10s | 0.01s Max RSS | 0.02 MB | 0.00 MB Page faults | Maj:0 / Min:0 | Maj:0 / Min:0 Inodes | 347 | 397 Repo size | 4212 KB | 3496 KB ------------------------------------------------------------- ``` Larger repo: ``` ============================================================= BENCHMARK: 5000 files @ 50 depth ============================================================= ACTION: Status ------------------------------------------------------------- METRIC | URN | GIT ----------------+----------------------+--------------------- Time | 0.26s | 0.00s Max RSS | 0.02 MB | 0.00 MB Page faults | Maj:0 / Min:0 | Maj:0 / Min:0 Inodes | 6 | 27 Repo size | 20 KB | 116 KB ------------------------------------------------------------- ACTION: Add ------------------------------------------------------------- METRIC | URN | GIT ----------------+----------------------+--------------------- Time | 2.82s | 4.62s Max RSS | 0.02 MB | 0.01 MB Page faults | Maj:0 / Min:0 | Maj:0 / Min:0 Inodes | 5055 | 5284 Repo size | 89444 KB | 70360 KB ------------------------------------------------------------- ACTION: Commit ------------------------------------------------------------- METRIC | URN | GIT ----------------+----------------------+--------------------- Time | 1.18s | 0.93s Max RSS | 0.03 MB | 0.01 MB Page faults | Maj:0 / Min:0 | Maj:0 / Min:0 Inodes | 5264 | 5342 Repo size | 91620 KB | 70592 KB ------------------------------------------------------------- ACTION: Status (Clean) ------------------------------------------------------------- METRIC | URN | GIT ----------------+----------------------+--------------------- Time | 0.34s | 0.10s Max RSS | 0.02 MB | 0.01 MB Page faults | Maj:0 / Min:0 | Maj:0 / Min:0 Inodes | 5264 | 5342 Repo size | 91620 KB | 70592 KB ------------------------------------------------------------- ``` ### Impact of commits over time Each commit modifies 2% of files — a few lines added, a few deleted. ``` ============================================================= HISTORY BENCHMARK: 1000 files (100 commits) ============================================================= SNAPSHOT: Commit #20 ------------------------------------------------------------- METRIC | URN | GIT ----------------+----------------------+--------------------- Time | 0.34s | 0.18s Max RSS | 0.02 MB | 0.01 MB Page Faults | Maj:0/Min:0 | Maj:0/Min:0 Inodes | 1302 | 2122 Repo Size | 19220 KB | 21944 KB ------------------------------------------------------------- SNAPSHOT: Commit #40 ------------------------------------------------------------- METRIC | URN | GIT ----------------+----------------------+--------------------- Time | 0.41s | 0.11s Max RSS | 0.02 MB | 0.01 MB Page Faults | Maj:0/Min:0 | Maj:0/Min:0 Inodes | 1342 | 2924 Repo Size | 19380 KB | 28848 KB ------------------------------------------------------------- SNAPSHOT: Commit #60 ------------------------------------------------------------- METRIC | URN | GIT ----------------+----------------------+--------------------- Time | 0.41s | 0.09s Max RSS | 0.02 MB | 0.01 MB Page Faults | Maj:0/Min:0 | Maj:0/Min:0 Inodes | 1383 | 3719 Repo Size | 19544 KB | 35796 KB ------------------------------------------------------------- SNAPSHOT: Commit #80 ------------------------------------------------------------- METRIC | URN | GIT ----------------+----------------------+--------------------- Time | 0.42s | 0.11s Max RSS | 0.02 MB | 0.01 MB Page Faults | Maj:0/Min:0 | Maj:0/Min:0 Inodes | 1424 | 4532 Repo Size | 19708 KB | 42868 KB ------------------------------------------------------------- SNAPSHOT: Commit #100 ------------------------------------------------------------- METRIC | URN | GIT ----------------+----------------------+--------------------- Time | 0.40s | 0.10s Max RSS | 0.02 MB | 0.01 MB Page Faults | Maj:0/Min:0 | Maj:0/Min:0 Inodes | 1464 | 5341 Repo Size | 19868 KB | 49840 KB ------------------------------------------------------------- ``` Git wins on speed and memory. Cold start is the exception — urn's add + commit beats git there. Git's zlib compression wins on initial disk usage. Over time the picture flips. 80 commits: git wrote 27 MB, urn wrote 0.6 MB. Git's inode count went from 2,122 to 5,341. Urn's went from 1,302 to 1,464. The thing it was built to do, it does. Commit: 57eb41d