193 lines
5.3 KiB
Go
193 lines
5.3 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Modifications by M/S Omukk
|
|
|
|
package snapshot
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const (
|
|
// DefaultBlockSize is 4KB — standard page size for Firecracker.
|
|
DefaultBlockSize int64 = 4096
|
|
)
|
|
|
|
// ProcessMemfile reads a full memory file produced by Firecracker's
|
|
// PUT /snapshot/create, identifies non-zero blocks, and writes only those
|
|
// blocks to a compact diff file. Returns the Header describing the mapping.
|
|
//
|
|
// The output diff file contains non-zero blocks written sequentially.
|
|
// The header maps each block in the full address space to either:
|
|
// - A position in the diff file (for non-zero blocks)
|
|
// - uuid.Nil (for zero/empty blocks, served as zeros without I/O)
|
|
//
|
|
// buildID identifies this snapshot generation in the header chain.
|
|
func ProcessMemfile(memfilePath, diffPath, headerPath string, buildID uuid.UUID) (*Header, error) {
|
|
src, err := os.Open(memfilePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open memfile: %w", err)
|
|
}
|
|
defer src.Close()
|
|
|
|
info, err := src.Stat()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stat memfile: %w", err)
|
|
}
|
|
memSize := info.Size()
|
|
|
|
dst, err := os.Create(diffPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create diff file: %w", err)
|
|
}
|
|
defer dst.Close()
|
|
|
|
totalBlocks := TotalBlocks(memSize, DefaultBlockSize)
|
|
dirty := make([]bool, totalBlocks)
|
|
empty := make([]bool, totalBlocks)
|
|
buf := make([]byte, DefaultBlockSize)
|
|
|
|
for i := int64(0); i < totalBlocks; i++ {
|
|
n, err := io.ReadFull(src, buf)
|
|
if err != nil && err != io.ErrUnexpectedEOF {
|
|
return nil, fmt.Errorf("read block %d: %w", i, err)
|
|
}
|
|
|
|
// Zero-pad the last block if it's short.
|
|
if int64(n) < DefaultBlockSize {
|
|
for j := n; j < int(DefaultBlockSize); j++ {
|
|
buf[j] = 0
|
|
}
|
|
}
|
|
|
|
if isZeroBlock(buf) {
|
|
empty[i] = true
|
|
continue
|
|
}
|
|
|
|
dirty[i] = true
|
|
if _, err := dst.Write(buf); err != nil {
|
|
return nil, fmt.Errorf("write diff block %d: %w", i, err)
|
|
}
|
|
}
|
|
|
|
// Build header.
|
|
dirtyMappings := CreateMapping(buildID, dirty, DefaultBlockSize)
|
|
emptyMappings := CreateMapping(uuid.Nil, empty, DefaultBlockSize)
|
|
merged := MergeMappings(dirtyMappings, emptyMappings)
|
|
normalized := NormalizeMappings(merged)
|
|
|
|
metadata := NewMetadata(buildID, uint64(DefaultBlockSize), uint64(memSize))
|
|
header, err := NewHeader(metadata, normalized)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create header: %w", err)
|
|
}
|
|
|
|
// Write header to disk.
|
|
headerData, err := Serialize(metadata, normalized)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("serialize header: %w", err)
|
|
}
|
|
if err := os.WriteFile(headerPath, headerData, 0644); err != nil {
|
|
return nil, fmt.Errorf("write header: %w", err)
|
|
}
|
|
|
|
return header, nil
|
|
}
|
|
|
|
// ProcessMemfileWithParent processes a memory file as a new generation on top
|
|
// of an existing parent header. The new diff file contains only blocks that
|
|
// differ from what the parent header maps. This is used for re-pause of a
|
|
// sandbox that was restored from a snapshot.
|
|
func ProcessMemfileWithParent(memfilePath, diffPath, headerPath string, parentHeader *Header, buildID uuid.UUID) (*Header, error) {
|
|
src, err := os.Open(memfilePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open memfile: %w", err)
|
|
}
|
|
defer src.Close()
|
|
|
|
info, err := src.Stat()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stat memfile: %w", err)
|
|
}
|
|
memSize := info.Size()
|
|
|
|
dst, err := os.Create(diffPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create diff file: %w", err)
|
|
}
|
|
defer dst.Close()
|
|
|
|
totalBlocks := TotalBlocks(memSize, DefaultBlockSize)
|
|
dirty := make([]bool, totalBlocks)
|
|
empty := make([]bool, totalBlocks)
|
|
buf := make([]byte, DefaultBlockSize)
|
|
|
|
for i := int64(0); i < totalBlocks; i++ {
|
|
n, err := io.ReadFull(src, buf)
|
|
if err != nil && err != io.ErrUnexpectedEOF {
|
|
return nil, fmt.Errorf("read block %d: %w", i, err)
|
|
}
|
|
|
|
if int64(n) < DefaultBlockSize {
|
|
for j := n; j < int(DefaultBlockSize); j++ {
|
|
buf[j] = 0
|
|
}
|
|
}
|
|
|
|
if isZeroBlock(buf) {
|
|
empty[i] = true
|
|
continue
|
|
}
|
|
|
|
dirty[i] = true
|
|
if _, err := dst.Write(buf); err != nil {
|
|
return nil, fmt.Errorf("write diff block %d: %w", i, err)
|
|
}
|
|
}
|
|
|
|
// Build new generation header merged with parent.
|
|
dirtyMappings := CreateMapping(buildID, dirty, DefaultBlockSize)
|
|
emptyMappings := CreateMapping(uuid.Nil, empty, DefaultBlockSize)
|
|
diffMapping := MergeMappings(dirtyMappings, emptyMappings)
|
|
merged := MergeMappings(parentHeader.Mapping, diffMapping)
|
|
normalized := NormalizeMappings(merged)
|
|
|
|
metadata := parentHeader.Metadata.NextGeneration(buildID)
|
|
header, err := NewHeader(metadata, normalized)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create header: %w", err)
|
|
}
|
|
|
|
headerData, err := Serialize(metadata, normalized)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("serialize header: %w", err)
|
|
}
|
|
if err := os.WriteFile(headerPath, headerData, 0644); err != nil {
|
|
return nil, fmt.Errorf("write header: %w", err)
|
|
}
|
|
|
|
return header, nil
|
|
}
|
|
|
|
// isZeroBlock checks if a block is entirely zero bytes.
|
|
func isZeroBlock(block []byte) bool {
|
|
// Fast path: compare 8 bytes at a time.
|
|
for i := 0; i+8 <= len(block); i += 8 {
|
|
if block[i] != 0 || block[i+1] != 0 || block[i+2] != 0 || block[i+3] != 0 ||
|
|
block[i+4] != 0 || block[i+5] != 0 || block[i+6] != 0 || block[i+7] != 0 {
|
|
return false
|
|
}
|
|
}
|
|
// Tail bytes.
|
|
for i := len(block) &^ 7; i < len(block); i++ {
|
|
if block[i] != 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|