Skip to content

Commit e0fe29d

Browse files
committed
sparsify snapshot: punch zero pages to reduce disk usage by ~80%
After writing the HLS snapshot, scan for all-zero 4 KiB pages and punch them with fallocate(PUNCH_HOLE). This makes the file sparse: zeros still read back via mmap but consume no disk blocks. On a typical pyhl snapshot (Python + pandas/numpy warm): Before: 2016 MiB on disk After: 648 MiB on disk (same 2016 MiB apparent size) No load-time overhead — mmap of sparse files is identical to dense. Linux-only; other platforms get a no-op fallback. Signed-off-by: danbugs <danilochiarlone@gmail.com>
1 parent f788363 commit e0fe29d

4 files changed

Lines changed: 85 additions & 6 deletions

File tree

host/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

host/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ base64 = "0.22"
3838

3939
[target.'cfg(unix)'.dependencies]
4040
nix = { version = "0.29", features = ["fs"] }
41+
libc = "0.2"
4142

host/src/bin/pyhl.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,8 +373,9 @@ fn cmd_setup(args: SetupArgs) -> Result<()> {
373373
mib(&dst_initrd)
374374
);
375375
eprintln!(
376-
" snapshot: {} ({} MiB)",
376+
" snapshot: {} ({} MiB on disk, {} MiB apparent)",
377377
dst_snapshot.display(),
378+
disk_mib(&dst_snapshot),
378379
mib(&dst_snapshot)
379380
);
380381
Ok(())
@@ -384,6 +385,19 @@ fn mib(p: &Path) -> u64 {
384385
fs::metadata(p).map(|m| m.len() / 1024 / 1024).unwrap_or(0)
385386
}
386387

388+
#[cfg(unix)]
389+
fn disk_mib(p: &Path) -> u64 {
390+
use std::os::unix::fs::MetadataExt;
391+
fs::metadata(p)
392+
.map(|m| m.blocks() * 512 / 1024 / 1024)
393+
.unwrap_or_else(|_| mib(p))
394+
}
395+
396+
#[cfg(not(unix))]
397+
fn disk_mib(p: &Path) -> u64 {
398+
mib(p)
399+
}
400+
387401
/// Lightweight timestamp (seconds since epoch in ISO-8601-ish) so we don't
388402
/// need to pull chrono just for the VERSION stamp.
389403
fn now_iso8601() -> String {

host/src/lib.rs

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1355,17 +1355,19 @@ impl Sandbox {
13551355
Ok(())
13561356
}
13571357

1358-
/// Persist the current post-evolve (or post-`snapshot_now`) snapshot
1359-
/// to disk so a later process can skip evolve + init and go straight
1360-
/// to `call`. Uses hyperlight's `Snapshot::to_file` — the file
1361-
/// format and cross-platform mmap load are documented in
1362-
/// hyperlight/docs/snapshot-file-implementation-plan.md.
1358+
/// Persist the current snapshot to disk as a sparse file.
1359+
///
1360+
/// After writing the raw HLS snapshot, zero-filled 4 KiB pages are
1361+
/// punched out with `fallocate(PUNCH_HOLE)` so they consume no disk
1362+
/// space. The file is still mmap-loadable — holes read back as
1363+
/// zeros with no decompression overhead.
13631364
pub fn save_snapshot<P: AsRef<Path>>(&self, path: P) -> Result<()> {
13641365
let snap = self
13651366
.snapshot
13661367
.as_ref()
13671368
.ok_or_else(|| anyhow!("no snapshot present; build() or snapshot_now() first"))?;
13681369
snap.to_file(path.as_ref())?;
1370+
sparsify_snapshot(path.as_ref())?;
13691371
Ok(())
13701372
}
13711373

@@ -1530,6 +1532,67 @@ pub fn run_vm_capture_output(
15301532
})
15311533
}
15321534

1535+
// ---------------------------------------------------------------------------
1536+
// Snapshot sparsification
1537+
// ---------------------------------------------------------------------------
1538+
1539+
/// Punch holes in zero-filled 4 KiB pages of a snapshot file.
1540+
///
1541+
/// The HLS snapshot format is a 4 KiB header followed by a dense memory
1542+
/// blob where ~80 % of pages are all-zeros (unused heap). Punching them
1543+
/// with `fallocate(PUNCH_HOLE)` turns the file sparse — the zeros still
1544+
/// read back via mmap but consume no disk blocks.
1545+
#[cfg(target_os = "linux")]
1546+
fn sparsify_snapshot(path: &Path) -> Result<()> {
1547+
use std::os::unix::io::AsRawFd;
1548+
1549+
let file = std::fs::OpenOptions::new()
1550+
.read(true)
1551+
.write(true)
1552+
.open(path)?;
1553+
let len = file.metadata()?.len();
1554+
let mmap = unsafe { memmap2::Mmap::map(&file)? };
1555+
1556+
const PAGE: usize = 4096;
1557+
const HEADER: usize = PAGE;
1558+
let zero_page = [0u8; PAGE];
1559+
1560+
let mut punched = 0u64;
1561+
let mut offset = HEADER;
1562+
while offset + PAGE <= len as usize {
1563+
if mmap[offset..offset + PAGE] == zero_page {
1564+
let ret = unsafe {
1565+
libc::fallocate(
1566+
file.as_raw_fd(),
1567+
libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE,
1568+
offset as i64,
1569+
PAGE as i64,
1570+
)
1571+
};
1572+
if ret == 0 {
1573+
punched += 1;
1574+
}
1575+
}
1576+
offset += PAGE;
1577+
}
1578+
drop(mmap);
1579+
1580+
if punched > 0 {
1581+
eprintln!(
1582+
" sparsified: punched {} zero pages ({} MiB saved on disk)",
1583+
punched,
1584+
punched * 4 / 1024
1585+
);
1586+
}
1587+
1588+
Ok(())
1589+
}
1590+
1591+
#[cfg(not(target_os = "linux"))]
1592+
fn sparsify_snapshot(_path: &Path) -> Result<()> {
1593+
Ok(())
1594+
}
1595+
15331596
// ---------------------------------------------------------------------------
15341597
// FsSandbox tests — prove that host-side path resolution rejects escapes.
15351598
//

0 commit comments

Comments
 (0)