From f29c6049fcaed1146a4de882f03ff863abd3058a Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Sun, 19 Aug 2018 11:38:26 +0100 Subject: [PATCH] local: preallocate files on Windows to reduce fragmentation #2469 Before this change on Windows, files copied locally could become heavily fragmented (300+ fragments for maybe 100 MB), no matter how much contiguous free space there was (even if it's over 1TiB). This can needlessly yet severely adversely affect performance on hard disks. This changes uses NtSetInformationFile to pre-allocate the space to avoid this. It does nothing on other OSes other than Windows. --- backend/local/local.go | 6 +++ backend/local/preallocate_other.go | 10 ++++ backend/local/preallocate_windows.go | 79 ++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 backend/local/preallocate_other.go create mode 100644 backend/local/preallocate_windows.go diff --git a/backend/local/local.go b/backend/local/local.go index 71b52b723..8c6f30d6a 100644 --- a/backend/local/local.go +++ b/backend/local/local.go @@ -817,6 +817,12 @@ func (o *Object) Update(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOptio return err } + // Pre-allocate the file for performance reasons + err = preAllocate(src.Size(), out) + if err != nil { + fs.Debugf(o, "Failed to pre-allocate: %v", err) + } + // Calculate the hash of the object we are reading as we go along hash, err := hash.NewMultiHasherTypes(hashes) if err != nil { diff --git a/backend/local/preallocate_other.go b/backend/local/preallocate_other.go new file mode 100644 index 000000000..7afff5e29 --- /dev/null +++ b/backend/local/preallocate_other.go @@ -0,0 +1,10 @@ +//+build !windows + +package local + +import "os" + +// preAllocate the file for performance reasons +func preAllocate(size int64, out *os.File) error { + return nil +} diff --git a/backend/local/preallocate_windows.go b/backend/local/preallocate_windows.go new file mode 100644 index 000000000..dfd88369d --- /dev/null +++ b/backend/local/preallocate_windows.go @@ -0,0 +1,79 @@ +//+build windows + +package local + +import ( + "os" + "syscall" + "unsafe" + + "github.com/pkg/errors" + "golang.org/x/sys/windows" +) + +var ( + ntdll = windows.NewLazySystemDLL("ntdll.dll") + ntQueryVolumeInformationFile = ntdll.NewProc("NtQueryVolumeInformationFile") + ntSetInformationFile = ntdll.NewProc("NtSetInformationFile") +) + +type fileAllocationInformation struct { + AllocationSize uint64 +} + +type fileFsSizeInformation struct { + TotalAllocationUnits uint64 + AvailableAllocationUnits uint64 + SectorsPerAllocationUnit uint32 + BytesPerSector uint32 +} + +type ioStatusBlock struct { + Status, Information uintptr +} + +// preAllocate the file for performance reasons +func preAllocate(size int64, out *os.File) error { + if size <= 0 { + return nil + } + + var ( + iosb ioStatusBlock + fsSizeInfo fileFsSizeInformation + allocInfo fileAllocationInformation + ) + + // Query info about the block sizes on the file system + _, _, e1 := ntQueryVolumeInformationFile.Call( + uintptr(out.Fd()), + uintptr(unsafe.Pointer(&iosb)), + uintptr(unsafe.Pointer(&fsSizeInfo)), + uintptr(unsafe.Sizeof(fsSizeInfo)), + uintptr(3), // FileFsSizeInformation + ) + if e1 != nil && e1 != syscall.Errno(0) { + return errors.Wrap(e1, "preAllocate NtQueryVolumeInformationFile failed") + } + + // Calculate the allocation size + clusterSize := uint64(fsSizeInfo.BytesPerSector) * uint64(fsSizeInfo.SectorsPerAllocationUnit) + if clusterSize <= 0 { + return errors.Errorf("preAllocate clusterSize %d <= 0", clusterSize) + } + allocInfo.AllocationSize = (1 + uint64(size-1)/clusterSize) * clusterSize + + // Ask for the allocation + _, _, e1 = ntSetInformationFile.Call( + uintptr(out.Fd()), + uintptr(unsafe.Pointer(&iosb)), + uintptr(unsafe.Pointer(&allocInfo)), + uintptr(unsafe.Sizeof(allocInfo)), + uintptr(19), // FileAllocationInformation + ) + if e1 != nil && e1 != syscall.Errno(0) { + return errors.Wrap(e1, "preAllocate NtSetInformationFile failed") + } + + return nil +}