Preserve file sparsity in FdFile::Copy
Use lseek SEEK_DATA / SEEK_HOLE in FdFile::Copy to avoid writing empty
blocks. This helps to save disk space on file systems that support
sparse files.
Test: art/tools/run-gtests.sh
Change-Id: Idb9202f4b9473c79225a9301bebd495c80bd6558
diff --git a/libartbase/base/unix_file/fd_file.cc b/libartbase/base/unix_file/fd_file.cc
index c5307e2..bb34b75 100644
--- a/libartbase/base/unix_file/fd_file.cc
+++ b/libartbase/base/unix_file/fd_file.cc
@@ -483,16 +483,83 @@
if (size == 0) {
return true;
}
+
+ off_t out_length = lseek(Fd(), 0, SEEK_END);
+ if (out_length != 0) {
+ // Output file is not empty.
+ //
+ // Currently there is no use-case for copying into non-empty files. To support the use-case,
+ // when needed, the implementation should handle the case in which the input file has an empty
+ // block whereas the corresponding location in the output file already contains some data. The
+ // current implementation would preserve that data instead of overwriting it with zeroes.
+ errno = EINVAL;
+ return false;
+ }
#ifdef __linux__
// Use sendfile(), available for files since linux kernel 2.6.33.
+ // Use lseek with SEEK_HOLE/SEEK_DATA to skip empty blocks, available since kernel 3.1.
+ int64_t offset_diff = -off;
off_t end = off + sz;
+ off_t out_offset = 0;
while (off != end) {
- int result = TEMP_FAILURE_RETRY(
- sendfile(Fd(), input_file->Fd(), &off, end - off));
- if (result == -1) {
- return false;
+ off = lseek(input_file->Fd(), off, SEEK_DATA);
+ if (off == -1) {
+ if (errno != ENXIO) {
+ return false;
+ }
+
+ // This is only expected to happen when there is no non-empty block between the current
+ // position and the end of the input file. In this case, no more copying is to be done, and
+ // the output file length going to be adjusted by `SetLength` below.
+ off = end;
}
- // Ignore the number of bytes in `result`, sendfile() already updated `off`.
+ DCHECK_GE(off, 0);
+
+ if (off >= end) {
+ // Next non-empty block is beyond the location pointed by `end` - no more data to be copied.
+ // Update the offsets for both files and set the output file length as expected - as in, to
+ // the values which would have been set if the block at the end of output file would have
+ // been non-empty.
+ off = lseek(input_file->Fd(), end, SEEK_SET);
+ if (off == -1) {
+ return false;
+ }
+ DCHECK_EQ(off, end);
+
+ out_offset = lseek(Fd(), end + offset_diff, SEEK_SET);
+ if (out_offset == -1) {
+ return false;
+ }
+ DCHECK_EQ(out_offset, end + offset_diff);
+
+ if (SetLength(out_offset) != 0) {
+ return false;
+ }
+ } else {
+ // Find the position of the next empty block.
+ // It could be a gap within the input file or the implicit empty block at the end of the file.
+ off_t hole_offset = lseek(input_file->Fd(), off, SEEK_HOLE);
+ if (hole_offset == -1) {
+ return false;
+ }
+ DCHECK_GT(hole_offset, off);
+
+ // Update output position to match the position of the data to be written from the input file.
+ out_offset = lseek(Fd(), off + offset_diff, SEEK_SET);
+ if (out_offset == -1) {
+ return false;
+ }
+ DCHECK_EQ(out_offset, off + offset_diff);
+
+ // Write the data. If end points in the middle of the current data block, only write the data
+ // until that position.
+ int result = TEMP_FAILURE_RETRY(
+ sendfile(Fd(), input_file->Fd(), &off, std::min(end, hole_offset) - off));
+ if (result == -1) {
+ return false;
+ }
+ // Ignore the number of bytes in `result`, sendfile() already updated `off`.
+ }
}
#else
if (lseek(input_file->Fd(), off, SEEK_SET) != off) {
diff --git a/libartbase/base/unix_file/fd_file_test.cc b/libartbase/base/unix_file/fd_file_test.cc
index 92f8308..f89eab9 100644
--- a/libartbase/base/unix_file/fd_file_test.cc
+++ b/libartbase/base/unix_file/fd_file_test.cc
@@ -14,8 +14,11 @@
* limitations under the License.
*/
+#include <string.h>
+
#include "base/common_art_test.h" // For ScratchFile
#include "base/file_utils.h"
+#include "base/stl_util.h"
#include "gtest/gtest.h"
#include "fd_file.h"
#include "random_access_file_test.h"
@@ -170,7 +173,7 @@
ASSERT_EQ(static_cast<int64_t>(sizeof(src_data)), src.GetLength());
art::ScratchFile dest_tmp;
- FdFile dest(src_tmp.GetFilename(), O_RDWR, false);
+ FdFile dest(dest_tmp.GetFilename(), O_RDWR, false);
ASSERT_GE(dest.Fd(), 0);
ASSERT_TRUE(dest.IsOpened());
@@ -186,6 +189,129 @@
ASSERT_EQ(0, src.Close());
}
+#ifdef __linux__
+TEST_F(FdFileTest, CopySparse) {
+ // The test validates that FdFile::Copy preserves sparsity of the file i.e. doesn't write zeroes
+ // to the output file in locations corresponding to empty blocks in the input file.
+ constexpr size_t kChunkSize = 64 * art::KB;
+
+ std::vector<int8_t> data_buffer(/*count=*/kChunkSize, /*value=*/1);
+ std::vector<int8_t> zero_buffer(/*count=*/kChunkSize, /*value=*/0);
+ std::vector<int8_t> check_buffer(/*count=*/kChunkSize);
+
+ auto verify = [&](size_t empty_prefix, size_t empty_suffix, size_t input_offset) {
+ constexpr size_t kEstimateMaxFileMetadataSize = 131072;
+ constexpr size_t kStatBlockSize = 512;
+ constexpr int kChunksNumber = 16;
+ art::ScratchFile src_tmp;
+ FdFile src(src_tmp.GetFilename(), O_RDWR, /*check_usage=*/false);
+ ASSERT_TRUE(src.IsOpened());
+
+ /*
+ * Layout of the source file:
+ * - Skipped part:
+ * [ optional <input_offset> empty block ]
+ *
+ * - Copied part:
+ * [ optional <empty_prefix> empty block ]
+ * [ <kChunkSize> data block ] -\
+ * [ <kChunkSize> empty block ] |
+ * [ <kChunkSize> data block ] |
+ * [ <kChunkSize> empty block ] > (2 * kChunksNumber - 1) kChunkSize blocks
+ * [ <kChunkSize> data block ] |
+ * [ ... ] |
+ * [ <kChunkSize> data block ] -/
+ * [ optional <empty_suffix> empty block ]
+ */
+ int rc = lseek(src.Fd(), input_offset, SEEK_SET);
+ ASSERT_EQ(rc, input_offset);
+
+ rc = lseek(src.Fd(), empty_prefix, SEEK_CUR);
+ ASSERT_EQ(rc, input_offset + empty_prefix);
+ for (size_t i = 0; i < kChunksNumber; i++) {
+ // Write data leaving chunk size of unwritten space in between them
+ ASSERT_TRUE(src.WriteFully(data_buffer.data(), kChunkSize));
+ rc = lseek(src.Fd(), kChunkSize, SEEK_CUR);
+ ASSERT_NE(rc, -1);
+ }
+
+ ASSERT_EQ(0, src.SetLength(src.GetLength() + empty_suffix));
+ ASSERT_EQ(0, src.Flush());
+
+ size_t expected_length = (2 * kChunksNumber - 1) * kChunkSize + empty_prefix + empty_suffix;
+ ASSERT_EQ(static_cast<int64_t>(expected_length), src.GetLength() - input_offset);
+
+ struct stat src_stat;
+ rc = fstat(src.Fd(), &src_stat);
+ ASSERT_EQ(rc, 0);
+ ASSERT_GE(src_stat.st_blocks, kChunksNumber * kChunkSize / kStatBlockSize);
+ ASSERT_LE(src_stat.st_blocks,
+ kEstimateMaxFileMetadataSize + kChunksNumber * kChunkSize / kStatBlockSize);
+
+ art::ScratchFile dest_tmp;
+ FdFile dest(dest_tmp.GetFilename(), O_RDWR, /*check_usage=*/false);
+ ASSERT_TRUE(dest.IsOpened());
+
+ ASSERT_TRUE(dest.Copy(&src, input_offset, src.GetLength() - input_offset));
+ ASSERT_EQ(0, dest.Flush());
+
+ ASSERT_EQ(static_cast<int64_t>(expected_length), dest.GetLength());
+
+ // Both dest and src file offsets are expected to be at the end of file.
+ ASSERT_EQ(lseek(dest.Fd(), 0, SEEK_CUR), dest.GetLength());
+ ASSERT_EQ(lseek(src.Fd(), 0, SEEK_CUR), src.GetLength());
+
+ struct stat dest_stat;
+ rc = fstat(dest.Fd(), &dest_stat);
+ ASSERT_EQ(rc, 0);
+ ASSERT_EQ(dest_stat.st_blocks, src_stat.st_blocks);
+
+ rc = lseek(dest.Fd(), 0, SEEK_SET);
+ ASSERT_EQ(rc, 0);
+
+ ASSERT_TRUE(dest.ReadFully(check_buffer.data(), empty_prefix));
+ ASSERT_EQ(0, memcmp(check_buffer.data(), zero_buffer.data(), empty_prefix));
+
+ for (size_t i = 0; i < 2 * kChunksNumber - 1; i++) {
+ ASSERT_TRUE(dest.ReadFully(check_buffer.data(), kChunkSize));
+
+ if (i % 2 == 0) {
+ ASSERT_EQ(0, memcmp(check_buffer.data(), data_buffer.data(), kChunkSize));
+ } else {
+ ASSERT_EQ(0, memcmp(check_buffer.data(), zero_buffer.data(), kChunkSize));
+ }
+ }
+
+ ASSERT_TRUE(dest.ReadFully(check_buffer.data(), empty_suffix));
+ ASSERT_EQ(0, memcmp(check_buffer.data(), zero_buffer.data(), empty_suffix));
+
+ ASSERT_EQ(0, dest.Close());
+ ASSERT_EQ(0, src.Close());
+ };
+
+ auto subtest = [&](size_t empty_prefix, size_t empty_suffix) {
+ verify(empty_prefix, empty_suffix, 0);
+ verify(empty_prefix, empty_suffix, kChunkSize);
+ };
+
+ // No empty prefix or suffix
+ subtest(0, 0);
+
+ for (size_t skip_size1 = 1; skip_size1 <= kChunkSize; skip_size1 <<= 2) {
+ // Empty prefix only
+ subtest(skip_size1, 0);
+
+ // Empty suffix only
+ subtest(0, skip_size1);
+
+ // Empty prefix and suffix
+ for (size_t skip_size2 = 1; skip_size2 <= kChunkSize; skip_size2 <<= 2) {
+ subtest(skip_size1, skip_size2);
+ }
+ }
+}
+#endif
+
TEST_F(FdFileTest, MoveConstructor) {
// New scratch file, zero-length.
art::ScratchFile tmp;