Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 57 additions & 4 deletions cmd/ateom-microvm/internal/tarutil/tarutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ package tarutil

import (
"archive/tar"
"bufio"
"context"
"errors"
"fmt"
Expand All @@ -45,10 +46,43 @@ import (
"path/filepath"
"sort"
"strings"
"sync"

"golang.org/x/sys/unix"
)

// streamBufSize batches tar headers and padding into fewer file operations.
// It must be smaller than copyBufSize so file contents bypass the buffer.
const streamBufSize = 64 << 10

// copyBufSize is the scratch buffer io.CopyBuffer streams file contents
// through, in place of the fresh buffer io.Copy allocates per call.
const copyBufSize = 128 << 10

// File contents must bypass the stream buffer.
const _ = uint(copyBufSize - streamBufSize - 1)

// Stream buffers must be Reset(nil) before pooling to avoid retaining files.
var (
tarWriterPool = sync.Pool{New: func() any { return bufio.NewWriterSize(nil, streamBufSize) }}
tarReaderPool = sync.Pool{New: func() any { return bufio.NewReaderSize(nil, streamBufSize) }}
)

var copyBufPool = sync.Pool{New: func() any {
b := make([]byte, copyBufSize)
return &b
}}

// copyPooled masks the fast-path interfaces so io.CopyBuffer uses the pooled buffer.
func copyPooled(dst io.Writer, src io.Reader) (int64, error) {
bp := copyBufPool.Get().(*[]byte)
defer copyBufPool.Put(bp)
return io.CopyBuffer(writerOnly{dst}, readerOnly{src}, *bp)
}

type writerOnly struct{ io.Writer }
type readerOnly struct{ io.Reader }

// Create writes a tar archive of srcDir's contents to tarPath. Entry names are
// relative to srcDir, so extracting into another directory reproduces the tree.
// srcDir itself is not an entry.
Expand All @@ -75,13 +109,24 @@ func CreateFiltered(ctx context.Context, tarPath, srcDir string, skip SkipFunc)
}
defer f.Close()

tw := tar.NewWriter(f)
bw := tarWriterPool.Get().(*bufio.Writer)
bw.Reset(f)
defer func() {
bw.Reset(nil)
tarWriterPool.Put(bw)
}()
tw := tar.NewWriter(bw)
if err := writeTree(ctx, tw, srcDir, skip); err != nil {
return err
}
if err := tw.Close(); err != nil {
return fmt.Errorf("closing tar %q: %w", tarPath, err)
}
// The buffer has to reach the file before the sync below, or the sync
// durably persists a truncated archive.
if err := bw.Flush(); err != nil {
return fmt.Errorf("flushing tar %q: %w", tarPath, err)
}
// Durable-dir tars are handed to atelet for upload as soon as we return, so
// flush to disk rather than trusting the page cache to outlive us.
if err := f.Sync(); err != nil {
Expand Down Expand Up @@ -211,7 +256,7 @@ func copyFileInto(tw *tar.Writer, path string) error {
return fmt.Errorf("opening %q: %w", path, err)
}
defer in.Close()
if _, err := io.Copy(tw, in); err != nil {
if _, err := copyPooled(tw, in); err != nil {
return fmt.Errorf("archiving contents of %q: %w", path, err)
}
return nil
Expand Down Expand Up @@ -243,7 +288,15 @@ func Extract(tarPath, dstDir string) error {
// are applied after every child exists (see restoreDirMeta).
dirs := map[string]*tar.Header{}

tr := tar.NewReader(f)
// Buffered like the writer: most of an archive is 512-byte headers, each
// of which would otherwise be a read(2) of its own.
br := tarReaderPool.Get().(*bufio.Reader)
br.Reset(f)
defer func() {
br.Reset(nil)
tarReaderPool.Put(br)
}()
tr := tar.NewReader(br)
for {
hdr, err := tr.Next()
if errors.Is(err, io.EOF) {
Expand Down Expand Up @@ -286,7 +339,7 @@ func extractEntry(root *os.Root, tr *tar.Reader, hdr *tar.Header, name string, d
if err != nil {
return fmt.Errorf("creating file %q: %w", name, err)
}
_, copyErr := io.Copy(out, tr)
_, copyErr := copyPooled(out, tr)
closeErr := out.Close()
if copyErr != nil {
return fmt.Errorf("writing contents of %q: %w", name, copyErr)
Expand Down
Loading