From 3a50f431de22364b0d2b0f38ba6add04b9423693 Mon Sep 17 00:00:00 2001 From: Noel Georgi Date: Thu, 18 Dec 2025 17:38:45 +0530 Subject: [PATCH] fix: buffered writes when using `io.Copy` When copying files to a fat filesystem and if using `io.Copy` on the `filesystem.OpenFile` it was slow which the entries were flushed for each write which is in-efficient. Only flush once on `Close()`. Signed-off-by: Noel Georgi --- filesystem/fat32/fat32.go | 138 +++++++++++- filesystem/fat32/file.go | 275 +++++++++++++++++++++-- filesystem/fat32/file_test.go | 143 +++++++++++- filesystem/fat32/write_benchmark_test.go | 117 ++++++++++ go.mod | 7 +- go.sum | 1 + 6 files changed, 647 insertions(+), 34 deletions(-) create mode 100644 filesystem/fat32/write_benchmark_test.go diff --git a/filesystem/fat32/fat32.go b/filesystem/fat32/fat32.go index 06a59636..bf31948e 100644 --- a/filesystem/fat32/fat32.go +++ b/filesystem/fat32/fat32.go @@ -60,6 +60,7 @@ type FileSystem struct { size int64 start int64 backend backend.Storage + fatDirty bool // tracks if FAT needs to be flushed } // Equal compare if two filesystems are equal @@ -351,7 +352,6 @@ func Read(b backend.Storage, size, start, blocksize int64) (*FileSystem, error) return nil, fmt.Errorf("only could read %d bytes from file", n) } bs, err := msDosBootSectorFromBytes(bsb) - if err != nil { return nil, fmt.Errorf("error reading MS-DOS Boot Sector: %w", err) } @@ -994,7 +994,6 @@ func (fs *FileSystem) mkLabel(parent *Directory, name string) (*directoryEntry, // if it does not exist, it may or may not make it func (fs *FileSystem) readDirWithMkdir(p string, doMake bool) (*Directory, []*directoryEntry, error) { paths, err := splitPath(p) - if err != nil { return nil, nil, err } @@ -1214,19 +1213,140 @@ func (fs *FileSystem) allocateSpace(size uint64, previous uint32) ([]uint32, err // update the FSIS fs.fsis.lastAllocatedCluster = lastAllocatedCluster - if err := fs.writeFsis(); err != nil { - return nil, fmt.Errorf("failed to write the file system information sector: %w", err) - } - // write the FAT tables - if err := fs.writeFat(); err != nil { - return nil, fmt.Errorf("failed to write the file allocation table: %w", err) - } + // Mark FAT as dirty so it will be flushed when the file is closed + fs.fatDirty = true // return all of the clusters return append(clusters, allocated...), nil } +// allocateSpaceWithCache is an optimized version of allocateSpace that reuses +// a cached cluster list to avoid repeated getClusterList calls +func (fs *FileSystem) allocateSpaceWithCache(size uint64, previous uint32, cachedClusters []uint32) ([]uint32, error) { + if previous > fs.table.maxCluster { + return nil, fmt.Errorf("invalid cluster chain at %d", previous) + } + + var lastAllocatedCluster uint32 + + // what is the total count of clusters needed? + count := int(size / uint64(fs.bytesPerCluster)) + if size%uint64(fs.bytesPerCluster) > 0 { + count++ + } + + // Use the cached cluster list + clusters := cachedClusters + originalClusterCount := len(clusters) + extraClusterCount := count - originalClusterCount + + // what if we do not need to change anything? + if extraClusterCount == 0 { + return clusters, nil + } + + // get the last cluster for chaining + if previous >= 2 && len(clusters) > 0 { + previous = clusters[len(clusters)-1] + } + + // get a list of allocated clusters, so we can know which ones are unallocated + maxCluster := fs.table.maxCluster + + if extraClusterCount > 0 { + // Pre-allocate space for new clusters + allocated := make([]uint32, 0, extraClusterCount) + + for i := uint32(2); i < maxCluster && len(allocated) < extraClusterCount; i++ { + if fs.table.clusters[i] == 0 { + allocated = append(allocated, i) + } + } + + // did we allocate them all? + if len(allocated) < extraClusterCount { + return nil, errors.New("no space left on device") + } + + // mark last allocated one as EOC + lastAlloc := len(allocated) - 1 + + // extend the chain and fill them in + if previous > 0 { + fs.table.clusters[previous] = allocated[0] + } + for i := 0; i < lastAlloc; i++ { + fs.table.clusters[allocated[i]] = allocated[i+1] + } + fs.table.clusters[allocated[lastAlloc]] = fs.table.eocMarker + + // update the FSIS + lastAllocatedCluster = allocated[len(allocated)-1] + + // Extend the cached cluster list in-place if there's capacity + if cap(clusters)-len(clusters) >= len(allocated) { + // We have capacity, extend the slice + clusters = append(clusters, allocated...) + } else { + // Need to reallocate - use exponential growth strategy + // Double capacity or add what we need, whichever is larger + newLen := len(clusters) + len(allocated) + newCap := max(cap(clusters)*2, newLen) + // Add extra capacity for future growth (25% more or at least 1000 clusters) + extraGrowth := max(newCap/4, 1000) + newCap += extraGrowth + + newClusters := make([]uint32, len(clusters), newCap) + copy(newClusters, clusters) + newClusters = append(newClusters, allocated...) + clusters = newClusters + } + } else { + var ( + lastAlloc int + deallocated []uint32 + ) + toRemove := abs(extraClusterCount) + lastAlloc = len(clusters) - toRemove - 1 + if lastAlloc < 0 { + lastAlloc = 0 + } + deallocated = clusters[lastAlloc+1:] + + if uint32(lastAlloc) > fs.table.maxCluster || clusters[lastAlloc] > fs.table.maxCluster { + return nil, fmt.Errorf("invalid cluster chain at %d", lastAlloc) + } + + // mark last allocated one as EOC + fs.table.clusters[clusters[lastAlloc]] = fs.table.eocMarker + + // unmark all of the unused ones + lastAllocatedCluster = fs.fsis.lastAllocatedCluster + for _, cl := range deallocated { + if cl > fs.table.maxCluster { + return nil, fmt.Errorf("invalid cluster chain at %d", cl) + } + + fs.table.clusters[cl] = fs.table.unusedMarker + if cl == lastAllocatedCluster { + lastAllocatedCluster-- + } + } + + // Shrink the cached cluster list + clusters = clusters[:lastAlloc+1] + } + + // update the FSIS + fs.fsis.lastAllocatedCluster = lastAllocatedCluster + + // Mark FAT as dirty so it will be flushed when the file is closed + fs.fatDirty = true + + return clusters, nil +} + func abs(x int) int { if x < 0 { return -x diff --git a/filesystem/fat32/file.go b/filesystem/fat32/file.go index 4d274ef7..8c04e6be 100644 --- a/filesystem/fat32/file.go +++ b/filesystem/fat32/file.go @@ -1,21 +1,32 @@ package fat32 import ( + "errors" "fmt" "io" "os" + "github.com/diskfs/go-diskfs/backend" "github.com/diskfs/go-diskfs/filesystem" ) +// ErrFileTooLarge is returned when attempting to write beyond FAT32's 4GB file size limit +var ErrFileTooLarge = errors.New("file size exceeds FAT32 limit of 4GB") + +const maxFAT32FileSize = (1 << 32) - 1 // 4,294,967,295 bytes (4GB - 1) + // File represents a single file in a FAT32 filesystem type File struct { *directoryEntry - isReadWrite bool - isAppend bool - offset int64 - parent *Directory - filesystem *FileSystem + isReadWrite bool + isAppend bool + offset int64 + parent *Directory + filesystem *FileSystem + needsFlush bool // tracks if directory entries need to be written on Close() + cachedClusters []uint32 // cached cluster chain to avoid repeated lookups + allocatedSize uint64 // size for which clusters have been allocated + writableFile backend.WritableFile // cached writable file handle } // Get the full cluster chain of the File. @@ -155,6 +166,174 @@ func (fl *File) Read(b []byte) (int, error) { return totalRead, retErr } +// ReadFrom reads data from r until EOF and writes it to the file. +// This is an optimized implementation that io.Copy will prefer over repeated Write calls. +// It allocates disk space in larger chunks to minimize FAT table scanning overhead. +func (fl *File) ReadFrom(r io.Reader) (int64, error) { + if fl == nil || fl.filesystem == nil { + return 0, os.ErrClosed + } + + if !fl.isReadWrite { + return 0, filesystem.ErrReadonlyFilesystem + } + + // Cache the writable file handle + if fl.writableFile == nil { + wf, err := fl.filesystem.backend.Writable() + if err != nil { + return 0, err + } + + fl.writableFile = wf + } + + writableFile := fl.writableFile + fs := fl.filesystem + bytesPerCluster := fs.bytesPerCluster + + start := int64(fs.dataStart) + + // Try to determine source size for optimal allocation using Seeker interface + var knownSize int64 = -1 + + if seeker, ok := r.(io.Seeker); ok { + currentPos, err := seeker.Seek(0, io.SeekCurrent) + if err == nil { + if endPos, err := seeker.Seek(0, io.SeekEnd); err == nil { + knownSize = endPos - currentPos + // Reset to original position + if _, err = seeker.Seek(currentPos, io.SeekStart); err != nil { + return 0, fmt.Errorf("unable to reset reader position after size determination: %v", err) + } + } + } + } + + // Allocation chunk size: allocate in 16MB chunks to reduce FAT scanning overhead + // (only used when source size is unknown) + const allocationChunkSize = 16 * 1024 * 1024 + chunkClusters := max(allocationChunkSize/bytesPerCluster, 1) + + // Pre-allocate all space if we know the size upfront + if knownSize > 0 && fl.allocatedSize == 0 { + targetSize := fl.offset + knownSize + // Only pre-allocate if it won't exceed FAT32 limit + if targetSize > 0 && targetSize <= maxFAT32FileSize { + clusters, err := fs.allocateSpace(uint64(targetSize), fl.clusterLocation) + if err == nil { + fl.cachedClusters = clusters + fl.allocatedSize = uint64(targetSize) + if len(clusters) > 0 && fl.clusterLocation == 0 { + fl.clusterLocation = clusters[0] + } + } + } + } + + totalWritten := int64(0) + buffer := make([]byte, 32*1024) // 32KB read buffer (matches io.Copy default) + + for { + // Read data from source + n, readErr := r.Read(buffer) + if n > 0 { + // Calculate new size after this write + oldSize := int64(fl.fileSize) + newSize := max(fl.offset+int64(n), oldSize) + + // Check FAT32 file size limit + if newSize > maxFAT32FileSize { + return totalWritten, ErrFileTooLarge + } + + // Allocate space in chunks if needed + var clusters []uint32 + var err error + + if fl.cachedClusters != nil && uint64(newSize) <= fl.allocatedSize { + // Use cached cluster list - no need to reallocate + clusters = fl.cachedClusters + } else { + // Need more space - allocate ahead in chunks to minimize FAT scanning + // Calculate how much extra to allocate beyond what we need right now + allocateSize := newSize + extraNeeded := allocateSize - int64(fl.allocatedSize) + if extraNeeded > 0 { + // Round up to next allocation chunk boundary + extraClusters := (extraNeeded + int64(bytesPerCluster) - 1) / int64(bytesPerCluster) + chunks := (extraClusters + int64(chunkClusters) - 1) / int64(chunkClusters) + allocateSize = int64(fl.allocatedSize) + chunks*int64(chunkClusters)*int64(bytesPerCluster) + } + + if fl.cachedClusters != nil { + clusters, err = fs.allocateSpaceWithCache(uint64(allocateSize), fl.clusterLocation, fl.cachedClusters) + } else { + clusters, err = fs.allocateSpace(uint64(allocateSize), fl.clusterLocation) + } + if err != nil { + return totalWritten, fmt.Errorf("unable to allocate clusters for file: %v", err) + } + + fl.cachedClusters = clusters + fl.allocatedSize = uint64(allocateSize) + + // Update cluster location if this is the first allocation + if len(clusters) > 0 && fl.clusterLocation == 0 { + fl.clusterLocation = clusters[0] + } + } + + // Update file size + if oldSize != newSize { + fl.fileSize = uint32(newSize) + } + + // Write the data to clusters + totalWritten += int64(n) + clusterIndex := int(fl.offset) / bytesPerCluster + remainder := fl.offset % int64(bytesPerCluster) + writePos := 0 + + // Handle partial first cluster if offset isn't cluster-aligned + if remainder != 0 { + lastCluster := clusters[clusterIndex] + offset := start + int64(lastCluster-2)*int64(bytesPerCluster) + remainder + toWrite := min(int64(bytesPerCluster)-remainder, int64(n)) + _, err := writableFile.WriteAt(buffer[writePos:writePos+int(toWrite)], offset+fs.start) + if err != nil { + return totalWritten, fmt.Errorf("unable to write to file: %v", err) + } + writePos += int(toWrite) + clusterIndex++ + } + + // Write remaining full/partial clusters + for writePos < n && clusterIndex < len(clusters) { + left := n - writePos + toWrite := min(bytesPerCluster, left) + offset := start + int64(clusters[clusterIndex]-2)*int64(bytesPerCluster) + _, err := writableFile.WriteAt(buffer[writePos:writePos+toWrite], offset+fs.start) + if err != nil { + return totalWritten, fmt.Errorf("unable to write to file: %v", err) + } + writePos += toWrite + clusterIndex++ + } + + fl.offset += int64(n) + fl.needsFlush = true + } + + if readErr != nil { + if readErr == io.EOF { + return totalWritten, nil + } + return totalWritten, readErr + } + } +} + // Write writes len(b) bytes to the File. // It returns the number of bytes written and an error, if any. // returns a non-nil error when n != len(b) @@ -167,10 +346,16 @@ func (fl *File) Write(p []byte) (int, error) { } totalWritten := 0 - writableFile, err := fl.filesystem.backend.Writable() - if err != nil { - return totalWritten, err + + // Cache the writable file handle + if fl.writableFile == nil { + wf, err := fl.filesystem.backend.Writable() + if err != nil { + return totalWritten, err + } + fl.writableFile = wf } + writableFile := fl.writableFile fs := fl.filesystem // if the file was not opened RDWR, nothing we can do @@ -184,10 +369,34 @@ func (fl *File) Write(p []byte) (int, error) { if newSize < oldSize { newSize = oldSize } - // 1- ensure we have space and clusters - clusters, err := fs.allocateSpace(uint64(newSize), fl.clusterLocation) - if err != nil { - return 0x00, fmt.Errorf("unable to allocate clusters for file: %v", err) + + // Check FAT32 file size limit + if newSize > maxFAT32FileSize { + return 0, ErrFileTooLarge + } + + // Get or allocate clusters - use cached cluster list if available and sufficient + var ( + clusters []uint32 + err error + ) + if fl.cachedClusters != nil && uint64(newSize) <= fl.allocatedSize { + // Use cached cluster list - no need to reallocate or query FAT + clusters = fl.cachedClusters + } else { + // Need to allocate more space + // Pass the cached cluster list to avoid re-reading from FAT + if fl.cachedClusters != nil { + clusters, err = fs.allocateSpaceWithCache(uint64(newSize), fl.clusterLocation, fl.cachedClusters) + } else { + clusters, err = fs.allocateSpace(uint64(newSize), fl.clusterLocation) + } + if err != nil { + return 0x00, fmt.Errorf("unable to allocate clusters for file: %v", err) + } + // Cache the cluster list and allocated size + fl.cachedClusters = clusters + fl.allocatedSize = uint64(newSize) } // update the directory entry size for the file @@ -237,11 +446,8 @@ func (fl *File) Write(p []byte) (int, error) { fl.offset += int64(totalWritten) - // update the parent that we have changed the file size - err = fs.writeDirectoryEntries(fl.parent) - if err != nil { - return 0, fmt.Errorf("error writing directory entries to disk: %v", err) - } + // mark that directory entries need to be written on Close() + fl.needsFlush = true return totalWritten, nil } @@ -269,14 +475,37 @@ func (fl *File) Seek(offset int64, whence int) (int64, error) { // Close close the file func (fl *File) Close() error { + if fl == nil || fl.filesystem == nil { + return nil + } + + // if FAT was modified, flush it first + if fl.filesystem.fatDirty { + if err := fl.filesystem.writeFsis(); err != nil { + return fmt.Errorf("error writing file system information sector: %v", err) + } + if err := fl.filesystem.writeFat(); err != nil { + return fmt.Errorf("error writing file allocation table: %v", err) + } + fl.filesystem.fatDirty = false + } + + // if file was modified, flush directory entries + if fl.needsFlush { + if err := fl.filesystem.writeDirectoryEntries(fl.parent); err != nil { + return fmt.Errorf("error writing directory entries to disk: %v", err) + } + fl.needsFlush = false + } + fl.filesystem = nil return nil } func (fl *File) SetSystem(on bool) error { - fs := fl.filesystem fl.isSystem = on - return fs.writeDirectoryEntries(fl.parent) + fl.needsFlush = true + return nil } func (fl *File) IsSystem() bool { @@ -284,9 +513,9 @@ func (fl *File) IsSystem() bool { } func (fl *File) SetHidden(on bool) error { - fs := fl.filesystem fl.isHidden = on - return fs.writeDirectoryEntries(fl.parent) + fl.needsFlush = true + return nil } func (fl *File) IsHidden() bool { @@ -294,9 +523,9 @@ func (fl *File) IsHidden() bool { } func (fl *File) SetReadOnly(on bool) error { - fs := fl.filesystem fl.isReadOnly = on - return fs.writeDirectoryEntries(fl.parent) + fl.needsFlush = true + return nil } func (fl *File) IsReadOnly() bool { diff --git a/filesystem/fat32/file_test.go b/filesystem/fat32/file_test.go index 37ac0fb4..8907b75d 100644 --- a/filesystem/fat32/file_test.go +++ b/filesystem/fat32/file_test.go @@ -1,6 +1,18 @@ package fat32_test -import "testing" +import ( + "errors" + "io" + "os" + "path/filepath" + "testing" + + "github.com/diskfs/go-diskfs" + "github.com/diskfs/go-diskfs/backend/file" + "github.com/diskfs/go-diskfs/disk" + "github.com/diskfs/go-diskfs/filesystem" + "github.com/diskfs/go-diskfs/filesystem/fat32" +) //nolint:unused,revive // keep for future when we implement it and will need t func TestFileRead(t *testing.T) { @@ -11,3 +23,132 @@ func TestFileRead(t *testing.T) { func TestFileWrite(t *testing.T) { } + +func TestFileSizeLimitWrite(t *testing.T) { + tmpDir := t.TempDir() + diskPath := filepath.Join(tmpDir, "test.img") + + // Create a 5GB disk + diskSize := int64(5 * 1024 * 1024 * 1024) + + bk, err := file.CreateFromPath(diskPath, diskSize) + if err != nil { + t.Fatalf("creating backend failed: %v", err) + } + + d, err := diskfs.OpenBackend(bk) + if err != nil { + t.Fatalf("opening disk failed: %v", err) + } + defer d.Close() + + spec := disk.FilesystemSpec{ + Partition: 0, + FSType: filesystem.TypeFat32, + } + + fs, err := d.CreateFilesystem(spec) + if err != nil { + t.Fatalf("creating filesystem failed: %v", err) + } + defer fs.Close() + + // Open a file for writing + f, err := fs.OpenFile("/testfile.dat", os.O_CREATE|os.O_RDWR) + if err != nil { + t.Fatalf("opening file failed: %v", err) + } + defer f.Close() + + // Try to write just under the 4GB limit - should succeed + const maxSize = (1 << 32) - 1 + smallData := []byte("test") + _, err = f.Write(smallData) + if err != nil { + t.Fatalf("writing small data failed: %v", err) + } + + // Seek to just before the 4GB boundary + _, err = f.Seek(maxSize-100, io.SeekStart) + if err != nil { + t.Fatalf("seeking failed: %v", err) + } + + // Write data that would fit within the limit + _, err = f.Write([]byte("within limit")) + if err != nil { + t.Fatalf("writing within limit failed: %v", err) + } + + // Try to write data that exceeds the 4GB limit + _, err = f.Seek(maxSize-10, io.SeekStart) + if err != nil { + t.Fatalf("seeking failed: %v", err) + } + + bigData := make([]byte, 100) + _, err = f.Write(bigData) + if !errors.Is(err, fat32.ErrFileTooLarge) { + t.Errorf("expected ErrFileTooLarge, got: %v", err) + } +} + +func TestFileSizeLimitReadFrom(t *testing.T) { + tmpDir := t.TempDir() + diskPath := filepath.Join(tmpDir, "test.img") + + // Create a 5GB disk + diskSize := int64(5 * 1024 * 1024 * 1024) + + bk, err := file.CreateFromPath(diskPath, diskSize) + if err != nil { + t.Fatalf("creating backend failed: %v", err) + } + + d, err := diskfs.OpenBackend(bk) + if err != nil { + t.Fatalf("opening disk failed: %v", err) + } + defer d.Close() + + spec := disk.FilesystemSpec{ + Partition: 0, + FSType: filesystem.TypeFat32, + } + + fs, err := d.CreateFilesystem(spec) + if err != nil { + t.Fatalf("creating filesystem failed: %v", err) + } + defer fs.Close() + + // Open a file for writing + f, err := fs.OpenFile("/testfile.dat", os.O_CREATE|os.O_RDWR) + if err != nil { + t.Fatalf("opening file failed: %v", err) + } + defer f.Close() + + // Try to copy data that would exceed 4GB using io.Copy (which uses ReadFrom) + const maxSize = (1 << 32) - 1 + const dataSize = maxSize + 1000 // Slightly over 4GB + + // Create an infinite zero reader limited to dataSize bytes + reader := io.LimitReader(zeroReader{}, dataSize) + + // This should fail with ErrFileTooLarge + _, err = io.Copy(f, reader) + if !errors.Is(err, fat32.ErrFileTooLarge) { + t.Errorf("expected ErrFileTooLarge, got: %v", err) + } +} + +// zeroReader is a reader that always returns zeros +type zeroReader struct{} + +func (zeroReader) Read(p []byte) (n int, err error) { + for i := range p { + p[i] = 0 + } + return len(p), nil +} diff --git a/filesystem/fat32/write_benchmark_test.go b/filesystem/fat32/write_benchmark_test.go new file mode 100644 index 00000000..df9fff2b --- /dev/null +++ b/filesystem/fat32/write_benchmark_test.go @@ -0,0 +1,117 @@ +package fat32_test + +import ( + "io" + "os" + "path/filepath" + "testing" + + "github.com/diskfs/go-diskfs" + "github.com/diskfs/go-diskfs/backend/file" + "github.com/diskfs/go-diskfs/disk" + "github.com/diskfs/go-diskfs/filesystem" + "github.com/stretchr/testify/require" +) + +const ( + benchmarkFileSize = (4 * 1024 * 1024 * 1024) - 1 // 4GB +) + +// createTestFile creates a temporary file with specified size using random data +func createTestFile(b *testing.B, size int64) string { + b.Helper() + + tmpDir := b.TempDir() + path := filepath.Join(tmpDir, "source.dat") + + f, err := os.Create(path) + require.NoError(b, err, "creating test file failed") + + defer f.Close() + + require.NoError(b, f.Truncate(size), "truncating test file failed") + + return path +} + +// setupFAT32Disk creates a temporary FAT32 filesystem +func setupFAT32Disk(b *testing.B) (fs filesystem.FileSystem, cleanup func()) { + b.Helper() + + tmpDir := b.TempDir() + diskPath := filepath.Join(tmpDir, "test.img") + + // Create a 5GB disk to have plenty of space for 4GB file + diskSize := int64(5 * 1024 * 1024 * 1024) + + bk, err := file.CreateFromPath(diskPath, diskSize) + require.NoError(b, err, "creating backend failed") + + d, err := diskfs.OpenBackend(bk) + require.NoError(b, err, "opening disk failed") + + spec := disk.FilesystemSpec{ + Partition: 0, + FSType: filesystem.TypeFat32, + } + + fs, err = d.CreateFilesystem(spec) + require.NoError(b, err, "creating filesystem failed") + + return fs, func() { + require.NoError(b, fs.Close()) + require.NoError(b, d.Close()) + } +} + +// BenchmarkFAT32WriteWithReadFile benchmarks writing using os.ReadFile + Write +func BenchmarkFAT32WriteWithReadFile(b *testing.B) { + // Create source file once for all iterations + sourceFile := createTestFile(b, benchmarkFileSize) + + for b.Loop() { + // Create new filesystem for each iteration + fs, cleanupFunc := setupFAT32Disk(b) + + // Read entire file into memory + data, err := os.ReadFile(sourceFile) + require.NoError(b, err, "reading source file failed") + + // Open destination file + destFile, err := fs.OpenFile("/testfile.dat", os.O_CREATE|os.O_RDWR) + require.NoError(b, err, "opening destination file failed") + + // Write all at once + _, err = destFile.Write(data) + require.NoError(b, err, "writing to destination file failed") + require.NoError(b, destFile.Close()) + + cleanupFunc() + } +} + +// BenchmarkFAT32WriteWithIOCopy benchmarks writing using io.Copy +func BenchmarkFAT32WriteWithIOCopy(b *testing.B) { + // Create source file once for all iterations + sourceFile := createTestFile(b, benchmarkFileSize) + + for b.Loop() { + // Create new filesystem for each iteration + fs, cleanupFunc := setupFAT32Disk(b) + + // Open source file + srcFile, err := os.Open(sourceFile) + require.NoError(b, err, "opening source file failed") + + // Open destination file + destFile, err := fs.OpenFile("/testfile.dat", os.O_CREATE|os.O_RDWR) + require.NoError(b, err, "opening destination file failed") + + _, err = io.Copy(destFile, srcFile) + require.NoError(b, err, "copying to destination file failed") + require.NoError(b, srcFile.Close()) + require.NoError(b, destFile.Close()) + + cleanupFunc() + } +} diff --git a/go.mod b/go.mod index 240e08b1..04e5d571 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,13 @@ require ( github.com/pierrec/lz4/v4 v4.1.17 github.com/pkg/xattr v0.4.9 github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af + github.com/stretchr/testify v1.7.1 github.com/ulikunitz/xz v0.5.15 golang.org/x/sys v0.19.0 ) -require github.com/stretchr/testify v1.7.1 // indirect +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/go.sum b/go.sum index 075445b0..becbc4d8 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,7 @@ golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=