Skip to content

Commit d5eb8f3

Browse files
Brent Baudebaude
authored andcommitted
AppleHV - make gz ops sparse
gz by definition is not able to preserve the sparse nature of files. using some code from the crc project and gluing it together with our decompression code, we can re-create the sparseness of a file. one downside is the operation is a little bit slower, but i think the gains from the sparse file are well worth it in IO alone. there are a number of todo's in this PR that would be ripe for quick hitting fixes. [NO NEW TESTS NEEDED] Signed-off-by: Brent Baude <[email protected]>
1 parent 85d8281 commit d5eb8f3

File tree

5 files changed

+237
-11
lines changed

5 files changed

+237
-11
lines changed

pkg/machine/applehv/machine.go

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ package applehv
55
import (
66
"fmt"
77
"os"
8-
"os/exec"
98
"syscall"
109

1110
"github.com/containers/common/pkg/strongunits"
@@ -101,15 +100,7 @@ func checkProcessRunning(processName string, pid int) error {
101100
// is assumed GiB
102101
func resizeDisk(mc *vmconfigs.MachineConfig, newSize strongunits.GiB) error {
103102
logrus.Debugf("resizing %s to %d bytes", mc.ImagePath.GetPath(), newSize.ToBytes())
104-
// seems like os.truncate() is not very performant with really large files
105-
// so exec'ing out to the command truncate
106-
size := fmt.Sprintf("%dG", newSize)
107-
c := exec.Command("truncate", "-s", size, mc.ImagePath.GetPath())
108-
if logrus.IsLevelEnabled(logrus.DebugLevel) {
109-
c.Stderr = os.Stderr
110-
c.Stdout = os.Stdout
111-
}
112-
return c.Run()
103+
return os.Truncate(mc.ImagePath.GetPath(), int64(newSize.ToBytes()))
113104
}
114105

115106
func generateSystemDFilesForVirtiofsMounts(mounts []machine.VirtIoFs) []ignition.Unit {

pkg/machine/applehv/stubber.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,6 @@ func (a AppleHVStubber) VMType() define.VMType {
295295
return define.AppleHvVirt
296296
}
297297

298-
299298
func waitForGvProxy(gvproxySocket *define.VMFile) error {
300299
backoffWait := gvProxyWaitBackoff
301300
logrus.Debug("checking that gvproxy is running")

pkg/machine/compression/copy.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package compression
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"os"
7+
)
8+
9+
// TODO vendor this in ... pkg/os directory is small and code should be negligible
10+
/*
11+
NOTE: copy.go and copy.test were lifted from github.com/crc-org/crc because
12+
i was having trouble getting go to vendor it properly. all credit to them
13+
*/
14+
15+
func copyFile(src, dst string, sparse bool) error {
16+
in, err := os.Open(src)
17+
if err != nil {
18+
return err
19+
}
20+
21+
defer in.Close()
22+
23+
out, err := os.Create(dst)
24+
if err != nil {
25+
return err
26+
}
27+
28+
defer out.Close()
29+
30+
if sparse {
31+
if _, err = CopySparse(out, in); err != nil {
32+
return err
33+
}
34+
} else {
35+
if _, err = io.Copy(out, in); err != nil {
36+
return err
37+
}
38+
}
39+
40+
fi, err := os.Stat(src)
41+
if err != nil {
42+
return err
43+
}
44+
45+
if err = os.Chmod(dst, fi.Mode()); err != nil {
46+
return err
47+
}
48+
49+
return out.Close()
50+
}
51+
52+
func CopyFile(src, dst string) error {
53+
return copyFile(src, dst, false)
54+
}
55+
56+
func CopyFileSparse(src, dst string) error {
57+
return copyFile(src, dst, true)
58+
}
59+
60+
func CopySparse(dst io.WriteSeeker, src io.Reader) (int64, error) {
61+
copyBuf := make([]byte, copyChunkSize)
62+
sparseWriter := newSparseWriter(dst)
63+
64+
bytesWritten, err := io.CopyBuffer(sparseWriter, src, copyBuf)
65+
if err != nil {
66+
return bytesWritten, err
67+
}
68+
err = sparseWriter.Close()
69+
return bytesWritten, err
70+
}
71+
72+
type sparseWriter struct {
73+
writer io.WriteSeeker
74+
lastChunkSparse bool
75+
}
76+
77+
func newSparseWriter(writer io.WriteSeeker) *sparseWriter {
78+
return &sparseWriter{writer: writer}
79+
}
80+
81+
const copyChunkSize = 4096
82+
83+
var emptyChunk = make([]byte, copyChunkSize)
84+
85+
func isEmptyChunk(p []byte) bool {
86+
// HasPrefix instead of bytes.Equal in order to handle the last chunk
87+
// of the file, which may be shorter than len(emptyChunk), and would
88+
// fail bytes.Equal()
89+
return bytes.HasPrefix(emptyChunk, p)
90+
}
91+
92+
func (w *sparseWriter) Write(p []byte) (n int, err error) {
93+
if isEmptyChunk(p) {
94+
offset, err := w.writer.Seek(int64(len(p)), io.SeekCurrent)
95+
if err != nil {
96+
w.lastChunkSparse = false
97+
return 0, err
98+
}
99+
_ = offset
100+
w.lastChunkSparse = true
101+
return len(p), nil
102+
}
103+
w.lastChunkSparse = false
104+
return w.writer.Write(p)
105+
}
106+
107+
func (w *sparseWriter) Close() error {
108+
if w.lastChunkSparse {
109+
if _, err := w.writer.Seek(-1, io.SeekCurrent); err != nil {
110+
return err
111+
}
112+
if _, err := w.writer.Write([]byte{0}); err != nil {
113+
return err
114+
}
115+
}
116+
return nil
117+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package compression
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
func TestCopyFile(t *testing.T) {
10+
testStr := "test-machine"
11+
12+
srcFile, err := os.CreateTemp("", "machine-test-")
13+
if err != nil {
14+
t.Fatal(err)
15+
}
16+
srcFi, err := srcFile.Stat()
17+
if err != nil {
18+
t.Fatal(err)
19+
}
20+
21+
_, _ = srcFile.Write([]byte(testStr)) //nolint:mirror
22+
srcFile.Close()
23+
24+
srcFilePath := filepath.Join(os.TempDir(), srcFi.Name())
25+
26+
destFile, err := os.CreateTemp("", "machine-copy-test-")
27+
if err != nil {
28+
t.Fatal(err)
29+
}
30+
31+
destFi, err := destFile.Stat()
32+
if err != nil {
33+
t.Fatal(err)
34+
}
35+
36+
destFile.Close()
37+
38+
destFilePath := filepath.Join(os.TempDir(), destFi.Name())
39+
40+
if err := CopyFile(srcFilePath, destFilePath); err != nil {
41+
t.Fatal(err)
42+
}
43+
44+
data, err := os.ReadFile(destFilePath)
45+
if err != nil {
46+
t.Fatal(err)
47+
}
48+
49+
if string(data) != testStr {
50+
t.Fatalf("expected data \"%s\"; received \"%s\"", testStr, string(data))
51+
}
52+
}

pkg/machine/compression/decompress.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package compression
33
import (
44
"archive/zip"
55
"bufio"
6+
"compress/gzip"
67
"errors"
78
"io"
89
"os"
@@ -19,12 +20,20 @@ import (
1920
"github.com/ulikunitz/xz"
2021
)
2122

23+
// Decompress is a generic wrapper for various decompression algos
24+
// TODO this needs some love. in the various decompression functions that are
25+
// called, the same uncompressed path is being opened multiple times.
2226
func Decompress(localPath *define.VMFile, uncompressedPath string) error {
2327
var isZip bool
2428
uncompressedFileWriter, err := os.OpenFile(uncompressedPath, os.O_CREATE|os.O_RDWR, 0600)
2529
if err != nil {
2630
return err
2731
}
32+
defer func() {
33+
if err := uncompressedFileWriter.Close(); err != nil {
34+
logrus.Errorf("unable to to close decompressed file %s: %q", uncompressedPath, err)
35+
}
36+
}()
2837
sourceFile, err := localPath.Read()
2938
if err != nil {
3039
return err
@@ -44,6 +53,11 @@ func Decompress(localPath *define.VMFile, uncompressedPath string) error {
4453
if isZip && runtime.GOOS == "windows" {
4554
return decompressZip(prefix, localPath.GetPath(), uncompressedFileWriter)
4655
}
56+
57+
// Unfortunately GZ is not sparse capable. Lets handle it differently
58+
if compressionType == archive.Gzip && runtime.GOOS == "darwin" {
59+
return decompressGzWithSparse(prefix, localPath, uncompressedPath)
60+
}
4761
return decompressEverythingElse(prefix, localPath.GetPath(), uncompressedFileWriter)
4862
}
4963

@@ -182,3 +196,56 @@ func decompressZip(prefix string, src string, output io.WriteCloser) error {
182196
p.Wait()
183197
return err
184198
}
199+
200+
func decompressGzWithSparse(prefix string, compressedPath *define.VMFile, uncompressedPath string) error {
201+
stat, err := os.Stat(compressedPath.GetPath())
202+
if err != nil {
203+
return err
204+
}
205+
206+
dstFile, err := os.OpenFile(uncompressedPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, stat.Mode())
207+
if err != nil {
208+
return err
209+
}
210+
defer func() {
211+
if err := dstFile.Close(); err != nil {
212+
logrus.Errorf("unable to close uncompressed file %s: %q", uncompressedPath, err)
213+
}
214+
}()
215+
216+
f, err := os.Open(compressedPath.GetPath())
217+
if err != nil {
218+
return err
219+
}
220+
defer func() {
221+
if err := f.Close(); err != nil {
222+
logrus.Errorf("unable to close on compressed file %s: %q", compressedPath.GetPath(), err)
223+
}
224+
}()
225+
226+
gzReader, err := gzip.NewReader(f)
227+
if err != nil {
228+
return err
229+
}
230+
defer func() {
231+
if err := gzReader.Close(); err != nil {
232+
logrus.Errorf("unable to close gzreader: %q", err)
233+
}
234+
}()
235+
236+
// TODO remove the following line when progress bars work
237+
_ = prefix
238+
// p, bar := utils.ProgressBar(prefix, stat.Size(), prefix+": done")
239+
// proxyReader := bar.ProxyReader(f)
240+
// defer func() {
241+
// if err := proxyReader.Close(); err != nil {
242+
// logrus.Error(err)
243+
// }
244+
// }()
245+
246+
logrus.Debugf("decompressing %s", compressedPath.GetPath())
247+
_, err = CopySparse(dstFile, gzReader)
248+
logrus.Debug("decompression complete")
249+
// p.Wait()
250+
return err
251+
}

0 commit comments

Comments
 (0)