Skip to content

Commit d54977b

Browse files
committed
optimize image copy on windows
this commit adds an optimization of the image copy process on Windows. It leverages robocopy, when available, which is a file replication tool built into Windows. By using it we reduce the time to copy of ~40%. Robocopy is launched with flags: - /J: Copies using unbuffered I/O (recommended for large files) - /MT: Creates multi-threaded copies with n threads. Default value of n is 8 - /R:<n> Specifies the number of retries on failed copies. 0 in this patch - /IS Includes Same files, which forces an overwrite even if the destination file appears identical to the source. Robocopy also uses specific exit code numbers which does not follow classic values. E.g. exit code 1 means all copies has been completed successfully. All exit codes are listed at https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy#exit-return-codes If robocopy is not found on the Win system, it fallback to the old copyFile function. Signed-off-by: lstocchi <lstocchi@redhat.com>
1 parent 189dc7b commit d54977b

File tree

1 file changed

+79
-14
lines changed

1 file changed

+79
-14
lines changed

pkg/imagepullers/noop.go

Lines changed: 79 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ package imagepullers
22

33
import (
44
"bufio"
5+
"errors"
56
"fmt"
67
"io"
78
"os"
9+
"os/exec"
810
"path/filepath"
911

12+
"github.com/sirupsen/logrus"
13+
1014
"github.com/containers/podman/v5/pkg/machine/define"
1115

1216
"github.com/containers/podman/v5/pkg/machine/env"
@@ -91,21 +95,17 @@ func doCopyFile(src, dest string, vmType define.VMType) error {
9195
}
9296
defer srcF.Close()
9397

94-
destF, err := os.Create(dest)
95-
if err != nil {
96-
return err
97-
}
98-
defer destF.Close()
99-
10098
switch vmType {
10199
case define.AppleHvVirt, define.LibKrun:
102-
return copyFileMac(srcF, destF)
100+
return copyFileMac(srcF, dest)
101+
case define.WSLVirt, define.HyperVVirt:
102+
return copyFileWin(srcF, dest)
103103
default:
104-
return copyFile(srcF, destF)
104+
return copyFile(srcF, dest)
105105
}
106106
}
107107

108-
func copyFileMac(src, dest *os.File) error {
108+
func copyFileMac(src *os.File, dest string) error {
109109
srcImg, err := qcow2reader.Open(src)
110110
if err != nil {
111111
return err
@@ -124,18 +124,83 @@ func copyFileMac(src, dest *os.File) error {
124124
}
125125
}
126126

127-
func convertToRaw(srcImg image.Image, dest *os.File) error {
127+
func copyFileWin(srcF *os.File, dest string) error {
128+
binary, err := exec.LookPath("robocopy")
129+
if err != nil {
130+
logrus.Debugf("warning: failed to get robocopy binary: %v. Falling back to default file copy between %s and %s\n", err, srcF.Name(), dest)
131+
return copyFile(srcF, dest)
132+
}
133+
134+
srcDir, srcFile := filepath.Split(srcF.Name())
135+
destDir := filepath.Dir(dest)
136+
137+
// Executes the robocopy command with options optimized for a fast, single-file copy.
138+
// /J: Copies using unbuffered I/O (better for large files).
139+
// /MT: Enables multi-threaded copying for improved performance.
140+
// /R:0: Sets retries on failed copies to 0 to avoid long waits.
141+
// /IS: Includes Same files, which forces an overwrite even if the destination
142+
// file appears identical to the source.
143+
cmd := exec.Command(binary, "/J", "/MT", "/R:0", "/IS", srcDir, destDir, srcFile)
144+
if logrus.IsLevelEnabled(logrus.DebugLevel) {
145+
cmd.Stdout = os.Stdout
146+
cmd.Stderr = os.Stderr
147+
} else {
148+
cmd.Stdout = io.Discard
149+
cmd.Stderr = io.Discard
150+
}
151+
152+
err = cmd.Run()
153+
if err != nil {
154+
// robocopy does not use classic exit codes like linux commands, so we need to check for specific errors
155+
// Only exit code 8 is considered an error, all other exit codes are considered successful with exceptions
156+
// see https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy
157+
var exitErr *exec.ExitError
158+
if errors.As(err, &exitErr) {
159+
exitCode := exitErr.ExitCode()
160+
if exitCode >= 8 {
161+
return fmt.Errorf("failed to run robocopy: %w", err)
162+
}
163+
} else {
164+
return fmt.Errorf("failed to run robocopy: %w", err)
165+
}
166+
}
167+
168+
if err := os.Remove(dest); err != nil && !errors.Is(err, os.ErrNotExist) {
169+
return fmt.Errorf("failed to remove existing destination file: %w", err)
170+
}
171+
172+
err = os.Rename(filepath.Join(destDir, srcFile), dest)
173+
if err != nil {
174+
return fmt.Errorf("failed to rename file: %w", err)
175+
}
176+
177+
return nil
178+
}
179+
180+
func convertToRaw(srcImg image.Image, dest string) error {
181+
destF, err := os.Create(dest)
182+
if err != nil {
183+
return err
184+
}
185+
defer destF.Close()
186+
128187
if err := srcImg.Readable(); err != nil {
129188
return fmt.Errorf("source image is not readable: %w", err)
130189
}
131190

132-
return convert.Convert(dest, srcImg, convert.Options{})
191+
return convert.Convert(destF, srcImg, convert.Options{})
133192
}
134193

135-
func copyFile(src, dst *os.File) error {
136-
bufferedWriter := bufio.NewWriter(dst)
194+
func copyFile(src *os.File, dest string) error {
195+
destF, err := os.Create(dest)
196+
if err != nil {
197+
return err
198+
}
199+
defer destF.Close()
200+
201+
bufferedWriter := bufio.NewWriter(destF)
137202
defer bufferedWriter.Flush()
138203

139-
_, err := io.Copy(bufferedWriter, src)
204+
_, err = io.Copy(bufferedWriter, src)
140205
return err
141206
}

0 commit comments

Comments
 (0)