Skip to content

Research: Holotree Packfile Format for Reduced Filesystem Churn #66

Description

@joshyorko

Research: Holotree Packfile Format

Executive Summary

Following PR #64 (zstd compression), profiling shows that filesystem operations are now the dominant bottleneck at 25-40% of CPU time, not decompression or hashing. This issue explores a Git-inspired packfile format to reduce syscall overhead.


🔬 RESEARCH FINDINGS (Self-Answered Questions)

Q1: Relocation Handling ✅ ANSWERED

Data from actual catalogs:

Catalog Total Files Relocations %
Large env (cd27b9ae) 25,367 666 2.6%
Micromamba layer (68705df3) 11,443 652 5.7%

Conclusion: 94-97% of files need NO relocation handling!

Strategy:

  • Stream 94%+ directly without seek-back
  • For relocatable files: write first, then seek back for rewrites
  • OR: Hybrid approach - keep relocatable files as individual files

Q2: Windows AV Behavior ✅ ANSWERED

Research findings:

  • Windows Defender scans on WRITE (file creation), not just read
  • Each new file triggers an AV scan event
  • Many small files = many scan events = slow
  • Single large file = fewer scan events = faster

Benchmark (Linux, pattern applies cross-platform):

Operation                    Time        Speedup
──────────────────────────────────────────────────
Create 1000 x 1KB files     2.103s      baseline
Create 1 x 1MB file         0.005s      420x faster!
Read 1000 individual files  0.013s      baseline
Read 1 packed file          0.001s      13x faster

Impact Analysis:

Operation Current Packfile Benefit
READ from hololib 10,563 opens 2 opens 99.98% reduction
WRITE to holotree 10,563 creates 10,563 creates ❌ Unchanged
AV scan events (read) ~10,563 ~2 ✅ Major reduction
AV scan events (write) ~10,563 ~10,563 ❌ Still required

Conclusion: Packfile helps the READ side dramatically (~50% syscall reduction). The WRITE side is unchanged - we still must create individual destination files. However, eliminating temp files (.part files) could help:

Current flow:

1. open(hololib/digest)           # AV scan
2. create(dest.part)              # AV scan  
3. write data
4. rename(dest.part → dest)       # AV scan

Optimized flow (streaming):

1. read from pack (single open)   # 1 AV scan for entire pack
2. create(dest)                   # AV scan
3. stream write directly

This eliminates the .part temp file, reducing AV events by ~33% on the write side too.


Q3: Enterprise / Shared Storage ✅ ANSWERED

Code analysis (common/variables.go, htfs/commands.go):

  • SharedHolotree mode already exists
  • Uses file locking (pathlib.Locker) for coordination
  • RCC_REMOTE_ORIGIN for pulling from shared locations

Packfile is BETTER for shared storage:

Factor Individual Files Packfile
Network round-trips ~10,563 ~2
Lock contention Per-file Per-pack
Atomic updates Complex Simple (write + rename)
Integrity check Per-file hash Single pack hash

Concern: Pack corruption affects all files (vs single file)
Mitigation:

  • Pack-level checksums (already planned)
  • Atomic write (temp + rename)
  • Existing catalog still tracks individual file hashes

Current State Analysis

Profiling Data (Large Environment - pandas, SQLAlchemy, pymongo)

Component gzip (before) zstd (after) % of Time
Decompression 2.99s (43%) 0.19s (3%) ✅ Fixed by PR #64
SHA256 hashing 1.09s (15.7%) 1.27s (20.5%) Acceptable
Syscalls (I/O) 1.32s (19%) 1.60s (25.8%) 🎯 New bottleneck

File Statistics (Real Environment)

Hololib unique files: 10,563
Holotree files: 12,455

Size distribution:
  < 1KB:     5,051 (48%) ← HIGH SYSCALL OVERHEAD
  1-10KB:    4,595 (43%)
  10-100KB:    774 (7%)
  > 100KB:     143 (1%)

Average file size: 16,768 bytes
Total size: 169 MB

Per-File Syscall Overhead

Each file restore currently requires:

1x open()      - source file (hololib)
1x fstat()     - get metadata
1x open()      - destination file (create)
Nx read()/write() - copy data
1x chmod()     - set permissions
2x close()
1x rename()    - atomic move to final location
─────────────────────────────────────────
≈ 8-10 syscalls per file minimum

For 10,563 files: ~85,000-106,000 syscalls just for restore

Syscall Breakdown from Profiling

Operation        Time    % of I/O
─────────────────────────────────
openat()         0.40s   25%
rename()         0.33s   21%
unlinkat()       0.25s   16%
fstatat()        0.21s   13%
write()          0.26s   16%
chmod()          0.09s   6%
lstat()          0.11s   7%

Proposed Solution: Holotree Packfile Format

Inspiration: Git Packfiles

Git packfiles solve a similar problem:

  • Store multiple objects in a single file
  • Single I/O operation to read many objects
  • Index file for fast object lookup
  • Checksum trailer for integrity verification

Proposed Format: hololib.pack

┌────────────────────────────────────────────────┐
│ HEADER (32 bytes)                              │
├────────────────────────────────────────────────┤
│ Magic: "HPAK" (4 bytes)                        │
│ Version: uint32 (4 bytes)                      │
│ Flags: uint32 (4 bytes)                        │
│ Object Count: uint64 (8 bytes)                 │
│ Uncompressed Size: uint64 (8 bytes)            │
│ Reserved: (4 bytes)                            │
├────────────────────────────────────────────────┤
│ OBJECT ENTRIES (variable)                      │
├────────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────┐   │
│ │ Entry Header:                            │   │
│ │   Digest: [32]byte (SHA256)              │   │
│ │   Compressed Size: uint32                │   │
│ │   Uncompressed Size: uint32              │   │
│ │   Mode: uint32                           │   │
│ │   Relocation Count: uint16               │   │
│ │   Relocation Offsets: []int64            │   │
│ ├──────────────────────────────────────────┤   │
│ │ Compressed Data (zstd)                   │   │
│ └──────────────────────────────────────────┘   │
│ ... (repeated for each object)                 │
├────────────────────────────────────────────────┤
│ FOOTER                                         │
├────────────────────────────────────────────────┤
│ Pack Checksum: SHA256 (32 bytes)               │
└────────────────────────────────────────────────┘

Syscall Reduction Estimate

Operation Current Packfile Reduction
Source opens 10,563 2 99.98%
fstat calls 10,563 ~0 100%
Dest creates 10,563 10,563 0%
chmod 10,563 10,563 0%
rename 10,563 0 (direct write) 100%
Total ~85,000 ~32,000 ~62%

Implementation Recommendation: Hybrid Approach

Based on our research, a hybrid approach minimizes risk while capturing most benefits:

File Type Storage % of Files Rationale
Small (<10KB) + no relocation Pack ~85% Maximum syscall reduction
Large (>10KB) Individual ~9% Streaming efficiency
Has relocations Individual ~6% Easy seek-back

Phase A: Hybrid (Low Risk)

  1. Pack small, non-relocatable files together
  2. Keep large and relocatable files separate
  3. Dual-format read (like gzip→zstd migration)

Phase B: Full Packfile (Once Proven)

  1. All files in single pack
  2. Streaming unpack with relocation post-processing
  3. Eliminate .part temp files

Estimated Impact

Platform Current With Packfile Improvement
Linux SSD 1.8s ~1.2s 33%
Windows SSD 3-5s ~1.5-2s 50-60%
macOS SSD 2-3s ~1.5s 40-50%
NFS/SMB mount 10-30s ~5-10s 50-70%

Windows and NFS see the biggest gains due to:

  • Windows: Reduced AV scan events
  • NFS: Reduced network round-trips

Security Considerations

Preserved:

  • Full integrity verification (pack checksum)
  • No hardlinks (files still copied)
  • Relocation security (same as current)
  • SharedHolotree locking still works

⚠️ New considerations:

  • Pack corruption affects all files (vs single file)
  • Index tampering could misdirect reads
  • Mitigation: Sign pack + index together, atomic writes

Conclusion

The packfile approach addresses the I/O bottleneck exposed by zstd compression (PR #64). Key findings:

  1. Relocations are rare (2.6-5.7%) - streaming is viable
  2. Windows AV benefits from fewer file opens - pack helps read side
  3. Enterprise/shared storage benefits most - fewer network ops

Recommendation: Start with hybrid approach (Phase A) to validate with minimal risk.


Research conducted via profiling, code analysis, and benchmarking - December 2024

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions