Skip to content

Commit 67398e3

Browse files
authored
Merge pull request #12 from hyperlight-dev/sparsify-snapshot-upstream
2 parents f788363 + d612337 commit 67398e3

4 files changed

Lines changed: 199 additions & 6 deletions

File tree

host/Cargo.lock

Lines changed: 2 additions & 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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,8 @@ base64 = "0.22"
3838

3939
[target.'cfg(unix)'.dependencies]
4040
nix = { version = "0.29", features = ["fs"] }
41+
libc = "0.2"
42+
43+
[target.'cfg(windows)'.dependencies]
44+
windows-sys = { version = "0.61", features = ["Win32_System_IO", "Win32_System_Ioctl", "Win32_Storage_FileSystem"] }
4145

host/src/bin/pyhl.rs

Lines changed: 34 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,38 @@ 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(windows)]
397+
fn disk_mib(p: &Path) -> u64 {
398+
use std::os::windows::ffi::OsStrExt;
399+
let wide: Vec<u16> = p
400+
.as_os_str()
401+
.encode_wide()
402+
.chain(std::iter::once(0))
403+
.collect();
404+
let mut high: u32 = 0;
405+
let low = unsafe {
406+
windows_sys::Win32::Storage::FileSystem::GetCompressedFileSizeW(wide.as_ptr(), &mut high)
407+
};
408+
if low == u32::MAX {
409+
return mib(p);
410+
}
411+
let bytes = ((high as u64) << 32) | (low as u64);
412+
bytes / 1024 / 1024
413+
}
414+
415+
#[cfg(not(any(unix, windows)))]
416+
fn disk_mib(p: &Path) -> u64 {
417+
mib(p)
418+
}
419+
387420
/// Lightweight timestamp (seconds since epoch in ISO-8601-ish) so we don't
388421
/// need to pull chrono just for the VERSION stamp.
389422
fn now_iso8601() -> String {

host/src/lib.rs

Lines changed: 159 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,158 @@ 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+
/// Windows equivalent: mark the file sparse with FSCTL_SET_SPARSE, then
1592+
/// punch zero ranges with FSCTL_SET_ZERO_DATA.
1593+
#[cfg(target_os = "windows")]
1594+
fn sparsify_snapshot(path: &Path) -> Result<()> {
1595+
use std::os::windows::io::AsRawHandle;
1596+
use windows_sys::Win32::System::Ioctl::{FSCTL_SET_SPARSE, FSCTL_SET_ZERO_DATA};
1597+
use windows_sys::Win32::System::IO::DeviceIoControl;
1598+
1599+
let file = std::fs::OpenOptions::new()
1600+
.read(true)
1601+
.write(true)
1602+
.open(path)?;
1603+
let len = file.metadata()?.len();
1604+
let handle = file.as_raw_handle();
1605+
1606+
// Mark file as sparse.
1607+
let ok = unsafe {
1608+
DeviceIoControl(
1609+
handle,
1610+
FSCTL_SET_SPARSE,
1611+
std::ptr::null(),
1612+
0,
1613+
std::ptr::null_mut(),
1614+
0,
1615+
std::ptr::null_mut(),
1616+
std::ptr::null_mut(),
1617+
)
1618+
};
1619+
if ok == 0 {
1620+
return Ok(());
1621+
}
1622+
1623+
let mmap = unsafe { memmap2::Mmap::map(&file)? };
1624+
1625+
const PAGE: usize = 4096;
1626+
const HEADER: usize = PAGE;
1627+
let zero_page = [0u8; PAGE];
1628+
1629+
// Coalesce contiguous zero pages into ranges for fewer syscalls.
1630+
let mut punched = 0u64;
1631+
let mut offset = HEADER;
1632+
while offset + PAGE <= len as usize {
1633+
if mmap[offset..offset + PAGE] != zero_page {
1634+
offset += PAGE;
1635+
continue;
1636+
}
1637+
let range_start = offset;
1638+
while offset + PAGE <= len as usize && mmap[offset..offset + PAGE] == zero_page {
1639+
offset += PAGE;
1640+
}
1641+
let range_end = offset;
1642+
1643+
#[repr(C)]
1644+
struct FileZeroDataInformation {
1645+
file_offset: i64,
1646+
beyond_final_zero: i64,
1647+
}
1648+
1649+
let info = FileZeroDataInformation {
1650+
file_offset: range_start as i64,
1651+
beyond_final_zero: range_end as i64,
1652+
};
1653+
let ok = unsafe {
1654+
DeviceIoControl(
1655+
handle,
1656+
FSCTL_SET_ZERO_DATA,
1657+
&info as *const _ as *const _,
1658+
std::mem::size_of::<FileZeroDataInformation>() as u32,
1659+
std::ptr::null_mut(),
1660+
0,
1661+
std::ptr::null_mut(),
1662+
std::ptr::null_mut(),
1663+
)
1664+
};
1665+
if ok != 0 {
1666+
punched += (range_end - range_start) as u64 / PAGE as u64;
1667+
}
1668+
}
1669+
drop(mmap);
1670+
1671+
if punched > 0 {
1672+
eprintln!(
1673+
" sparsified: punched {} zero pages ({} MiB saved on disk)",
1674+
punched,
1675+
punched * 4 / 1024
1676+
);
1677+
}
1678+
1679+
Ok(())
1680+
}
1681+
1682+
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
1683+
fn sparsify_snapshot(_path: &Path) -> Result<()> {
1684+
Ok(())
1685+
}
1686+
15331687
// ---------------------------------------------------------------------------
15341688
// FsSandbox tests — prove that host-side path resolution rejects escapes.
15351689
//

0 commit comments

Comments
 (0)