libsnapshot: Group CowOperations into clusters
This introduces clusters of CowOperations to the Cow Format, improving
reader performance by allowing many operations to be read at once.
When the header's cluster_ops is a nonzero value, operations will be
clumped into packs of that number, where the last op is a special
CowClusterOp, which points to the start of the next cluster. Each
cluster is immediately followed by it's data, with the footer following
at the end of the file.
cluster_ops must not be 1, as we must include a CowClusterOp in each
group.
Change-Id: I2855288958d56569d4078f377ef0b8c01ca5c6e7
Bug: 172026020
Test: cow_api_test
diff --git a/fs_mgr/libsnapshot/cow_api_test.cpp b/fs_mgr/libsnapshot/cow_api_test.cpp
index 4db6584..35020f4 100644
--- a/fs_mgr/libsnapshot/cow_api_test.cpp
+++ b/fs_mgr/libsnapshot/cow_api_test.cpp
@@ -525,6 +525,165 @@
ASSERT_TRUE(iter->Done());
}
+TEST_F(CowTest, ClusterTest) {
+ CowOptions options;
+ options.cluster_ops = 4;
+ auto writer = std::make_unique<CowWriter>(options);
+ ASSERT_TRUE(writer->Initialize(cow_->fd));
+
+ std::string data = "This is some data, believe it";
+ data.resize(options.block_size, '\0');
+ ASSERT_TRUE(writer->AddRawBlocks(50, data.data(), data.size()));
+
+ ASSERT_TRUE(writer->AddLabel(4));
+
+ ASSERT_TRUE(writer->AddZeroBlocks(50, 2)); // Cluster split in middle
+
+ ASSERT_TRUE(writer->AddLabel(5));
+
+ ASSERT_TRUE(writer->AddCopy(5, 6));
+
+ // Cluster split
+
+ ASSERT_TRUE(writer->AddLabel(6));
+
+ ASSERT_TRUE(writer->Finalize()); // No data for cluster, so no cluster split needed
+
+ ASSERT_EQ(lseek(cow_->fd, 0, SEEK_SET), 0);
+
+ // Read back all ops
+ CowReader reader;
+ ASSERT_TRUE(reader.Parse(cow_->fd));
+
+ StringSink sink;
+
+ auto iter = reader.GetOpIter();
+ ASSERT_NE(iter, nullptr);
+
+ ASSERT_FALSE(iter->Done());
+ auto op = &iter->Get();
+ ASSERT_EQ(op->type, kCowReplaceOp);
+ ASSERT_TRUE(reader.ReadData(*op, &sink));
+ ASSERT_EQ(sink.stream(), data.substr(0, options.block_size));
+
+ iter->Next();
+ sink.Reset();
+
+ ASSERT_FALSE(iter->Done());
+ op = &iter->Get();
+ ASSERT_EQ(op->type, kCowLabelOp);
+ ASSERT_EQ(op->source, 4);
+
+ iter->Next();
+
+ ASSERT_FALSE(iter->Done());
+ op = &iter->Get();
+ ASSERT_EQ(op->type, kCowZeroOp);
+
+ iter->Next();
+
+ ASSERT_FALSE(iter->Done());
+ op = &iter->Get();
+ ASSERT_EQ(op->type, kCowClusterOp);
+
+ iter->Next();
+
+ ASSERT_FALSE(iter->Done());
+ op = &iter->Get();
+ ASSERT_EQ(op->type, kCowZeroOp);
+
+ iter->Next();
+
+ ASSERT_FALSE(iter->Done());
+ op = &iter->Get();
+ ASSERT_EQ(op->type, kCowLabelOp);
+ ASSERT_EQ(op->source, 5);
+
+ iter->Next();
+
+ ASSERT_FALSE(iter->Done());
+ op = &iter->Get();
+ ASSERT_EQ(op->type, kCowCopyOp);
+
+ iter->Next();
+
+ ASSERT_FALSE(iter->Done());
+ op = &iter->Get();
+ ASSERT_EQ(op->type, kCowClusterOp);
+
+ iter->Next();
+
+ ASSERT_FALSE(iter->Done());
+ op = &iter->Get();
+ ASSERT_EQ(op->type, kCowLabelOp);
+ ASSERT_EQ(op->source, 6);
+
+ iter->Next();
+
+ ASSERT_TRUE(iter->Done());
+}
+
+TEST_F(CowTest, ClusterAppendTest) {
+ CowOptions options;
+ options.cluster_ops = 3;
+ auto writer = std::make_unique<CowWriter>(options);
+ ASSERT_TRUE(writer->Initialize(cow_->fd));
+
+ ASSERT_TRUE(writer->AddLabel(50));
+ ASSERT_TRUE(writer->Finalize()); // Adds a cluster op, should be dropped on append
+
+ ASSERT_EQ(lseek(cow_->fd, 0, SEEK_SET), 0);
+
+ writer = std::make_unique<CowWriter>(options);
+ ASSERT_TRUE(writer->InitializeAppend(cow_->fd, 50));
+
+ std::string data2 = "More data!";
+ data2.resize(options.block_size, '\0');
+ ASSERT_TRUE(writer->AddRawBlocks(51, data2.data(), data2.size()));
+ ASSERT_TRUE(writer->Finalize()); // Adds a cluster op
+
+ ASSERT_EQ(lseek(cow_->fd, 0, SEEK_SET), 0);
+
+ struct stat buf;
+ ASSERT_EQ(fstat(cow_->fd, &buf), 0);
+ ASSERT_EQ(buf.st_size, writer->GetCowSize());
+
+ // Read back both operations, plus cluster op at end
+ CowReader reader;
+ uint64_t label;
+ ASSERT_TRUE(reader.Parse(cow_->fd));
+ ASSERT_TRUE(reader.GetLastLabel(&label));
+ ASSERT_EQ(label, 50);
+
+ StringSink sink;
+
+ auto iter = reader.GetOpIter();
+ ASSERT_NE(iter, nullptr);
+
+ ASSERT_FALSE(iter->Done());
+ auto op = &iter->Get();
+ ASSERT_EQ(op->type, kCowLabelOp);
+ ASSERT_EQ(op->source, 50);
+
+ iter->Next();
+
+ ASSERT_FALSE(iter->Done());
+ op = &iter->Get();
+ ASSERT_EQ(op->type, kCowReplaceOp);
+ ASSERT_TRUE(reader.ReadData(*op, &sink));
+ ASSERT_EQ(sink.stream(), data2);
+
+ iter->Next();
+
+ ASSERT_FALSE(iter->Done());
+ op = &iter->Get();
+ ASSERT_EQ(op->type, kCowClusterOp);
+
+ iter->Next();
+
+ ASSERT_TRUE(iter->Done());
+}
+
} // namespace snapshot
} // namespace android
diff --git a/fs_mgr/libsnapshot/cow_format.cpp b/fs_mgr/libsnapshot/cow_format.cpp
index 49ba11f..0753c49 100644
--- a/fs_mgr/libsnapshot/cow_format.cpp
+++ b/fs_mgr/libsnapshot/cow_format.cpp
@@ -35,6 +35,10 @@
os << "kCowFooterOp, ";
else if (op.type == kCowLabelOp)
os << "kCowLabelOp, ";
+ else if (op.type == kCowClusterOp)
+ os << "kCowClusterOp ";
+ else if (op.type == kCowFooterOp)
+ os << "kCowFooterOp ";
else
os << (int)op.type << "?,";
os << "compression:";
@@ -52,11 +56,35 @@
return os;
}
-int64_t GetNextOpOffset(const CowOperation& op) {
- if (op.type == kCowReplaceOp)
+int64_t GetNextOpOffset(const CowOperation& op, uint32_t cluster_ops) {
+ if (op.type == kCowClusterOp) {
+ return op.source;
+ } else if (op.type == kCowReplaceOp && cluster_ops == 0) {
return op.data_length;
- else
+ } else {
return 0;
+ }
+}
+
+int64_t GetNextDataOffset(const CowOperation& op, uint32_t cluster_ops) {
+ if (op.type == kCowClusterOp) {
+ return cluster_ops * sizeof(CowOperation);
+ } else if (cluster_ops == 0) {
+ return sizeof(CowOperation);
+ } else {
+ return 0;
+ }
+}
+
+bool IsMetadataOp(const CowOperation& op) {
+ switch (op.type) {
+ case kCowLabelOp:
+ case kCowClusterOp:
+ case kCowFooterOp:
+ return true;
+ default:
+ return false;
+ }
}
} // namespace snapshot
diff --git a/fs_mgr/libsnapshot/cow_reader.cpp b/fs_mgr/libsnapshot/cow_reader.cpp
index 0907032..3fbc5f3 100644
--- a/fs_mgr/libsnapshot/cow_reader.cpp
+++ b/fs_mgr/libsnapshot/cow_reader.cpp
@@ -81,6 +81,24 @@
<< sizeof(CowFooter);
return false;
}
+ if (header_.op_size != sizeof(CowOperation)) {
+ LOG(ERROR) << "Operation size unknown, read " << header_.op_size << ", expected "
+ << sizeof(CowOperation);
+ return false;
+ }
+ if (header_.cluster_ops == 1) {
+ LOG(ERROR) << "Clusters must contain at least two operations to function.";
+ return false;
+ }
+ if (header_.op_size != sizeof(CowOperation)) {
+ LOG(ERROR) << "Operation size unknown, read " << header_.op_size << ", expected "
+ << sizeof(CowOperation);
+ return false;
+ }
+ if (header_.cluster_ops == 1) {
+ LOG(ERROR) << "Clusters must contain at least two operations to function.";
+ return false;
+ }
if ((header_.major_version != kCowVersionMajor) ||
(header_.minor_version != kCowVersionMinor)) {
@@ -103,45 +121,64 @@
}
auto ops_buffer = std::make_shared<std::vector<CowOperation>>();
+ uint64_t current_op_num = 0;
+ uint64_t cluster_ops = header_.cluster_ops ?: 1;
+ bool done = false;
- // Alternating op and data
- while (true) {
- ops_buffer->emplace_back();
- if (!android::base::ReadFully(fd_, &ops_buffer->back(), sizeof(CowOperation))) {
+ // Alternating op clusters and data
+ while (!done) {
+ uint64_t to_add = std::min(cluster_ops, (fd_size_ - pos) / sizeof(CowOperation));
+ if (to_add == 0) break;
+ ops_buffer->resize(current_op_num + to_add);
+ if (!android::base::ReadFully(fd_, &ops_buffer->data()[current_op_num],
+ to_add * sizeof(CowOperation))) {
PLOG(ERROR) << "read op failed";
return false;
}
+ // Parse current cluster to find start of next cluster
+ while (current_op_num < ops_buffer->size()) {
+ auto& current_op = ops_buffer->data()[current_op_num];
+ current_op_num++;
+ pos += sizeof(CowOperation) + GetNextOpOffset(current_op, header_.cluster_ops);
- auto& current_op = ops_buffer->back();
- off_t offs = lseek(fd_.get(), GetNextOpOffset(current_op), SEEK_CUR);
- if (offs < 0) {
+ if (current_op.type == kCowClusterOp) {
+ break;
+ } else if (current_op.type == kCowLabelOp) {
+ last_label_ = {current_op.source};
+
+ // If we reach the requested label, stop reading.
+ if (label && label.value() == current_op.source) {
+ done = true;
+ break;
+ }
+ } else if (current_op.type == kCowFooterOp) {
+ footer_.emplace();
+ CowFooter* footer = &footer_.value();
+ memcpy(&footer_->op, ¤t_op, sizeof(footer->op));
+ off_t offs = lseek(fd_.get(), pos, SEEK_SET);
+ if (offs < 0 || pos != static_cast<uint64_t>(offs)) {
+ PLOG(ERROR) << "lseek next op failed";
+ return false;
+ }
+ if (!android::base::ReadFully(fd_, &footer->data, sizeof(footer->data))) {
+ LOG(ERROR) << "Could not read COW footer";
+ return false;
+ }
+
+ // Drop the footer from the op stream.
+ current_op_num--;
+ done = true;
+ break;
+ }
+ }
+
+ // Position for next cluster read
+ off_t offs = lseek(fd_.get(), pos, SEEK_SET);
+ if (offs < 0 || pos != static_cast<uint64_t>(offs)) {
PLOG(ERROR) << "lseek next op failed";
return false;
}
- pos = static_cast<uint64_t>(offs);
-
- if (current_op.type == kCowLabelOp) {
- last_label_ = {current_op.source};
-
- // If we reach the requested label, stop reading.
- if (label && label.value() == current_op.source) {
- break;
- }
- } else if (current_op.type == kCowFooterOp) {
- footer_.emplace();
-
- CowFooter* footer = &footer_.value();
- memcpy(&footer_->op, ¤t_op, sizeof(footer->op));
-
- if (!android::base::ReadFully(fd_, &footer->data, sizeof(footer->data))) {
- LOG(ERROR) << "Could not read COW footer";
- return false;
- }
-
- // Drop the footer from the op stream.
- ops_buffer->pop_back();
- break;
- }
+ ops_buffer->resize(current_op_num);
}
// To successfully parse a COW file, we need either:
@@ -198,9 +235,7 @@
// Remove all the metadata operations
ops_->erase(std::remove_if(ops_.get()->begin(), ops_.get()->end(),
- [](CowOperation& op) {
- return (op.type == kCowFooterOp || op.type == kCowLabelOp);
- }),
+ [](CowOperation& op) { return IsMetadataOp(op); }),
ops_.get()->end());
// We will re-arrange the vector in such a way that
diff --git a/fs_mgr/libsnapshot/cow_writer.cpp b/fs_mgr/libsnapshot/cow_writer.cpp
index f7cc6ff..8535252 100644
--- a/fs_mgr/libsnapshot/cow_writer.cpp
+++ b/fs_mgr/libsnapshot/cow_writer.cpp
@@ -90,8 +90,10 @@
header_.minor_version = kCowVersionMinor;
header_.header_size = sizeof(CowHeader);
header_.footer_size = sizeof(CowFooter);
+ header_.op_size = sizeof(CowOperation);
header_.block_size = options_.block_size;
header_.num_merge_ops = 0;
+ header_.cluster_ops = options_.cluster_ops;
footer_ = {};
footer_.op.data_length = 64;
footer_.op.type = kCowFooterOp;
@@ -108,6 +110,10 @@
LOG(ERROR) << "unrecognized compression: " << options_.compression;
return false;
}
+ if (options_.cluster_ops == 1) {
+ LOG(ERROR) << "Clusters must contain at least two operations to function.";
+ return false;
+ }
return true;
}
@@ -165,6 +171,19 @@
return OpenForAppend(label);
}
+void CowWriter::InitPos() {
+ next_op_pos_ = sizeof(header_);
+ cluster_size_ = header_.cluster_ops * sizeof(CowOperation);
+ if (header_.cluster_ops) {
+ next_data_pos_ = next_op_pos_ + cluster_size_;
+ } else {
+ next_data_pos_ = next_op_pos_ + sizeof(CowOperation);
+ }
+ ops_.clear();
+ current_cluster_size_ = 0;
+ current_data_size_ = 0;
+}
+
bool CowWriter::OpenForWrite() {
// This limitation is tied to the data field size in CowOperation.
if (header_.block_size > std::numeric_limits<uint16_t>::max()) {
@@ -184,7 +203,7 @@
return false;
}
- next_op_pos_ = sizeof(header_);
+ InitPos();
return true;
}
@@ -197,13 +216,14 @@
}
options_.block_size = header_.block_size;
+ options_.cluster_ops = header_.cluster_ops;
// Reset this, since we're going to reimport all operations.
footer_.op.num_ops = 0;
- next_op_pos_ = sizeof(header_);
- ops_.resize(0);
+ InitPos();
auto iter = reader->GetOpIter();
+
while (!iter->Done()) {
AddOperation(iter->Get());
iter->Next();
@@ -234,14 +254,12 @@
bool CowWriter::EmitRawBlocks(uint64_t new_block_start, const void* data, size_t size) {
const uint8_t* iter = reinterpret_cast<const uint8_t*>(data);
- uint64_t pos;
CHECK(!merge_in_progress_);
for (size_t i = 0; i < size / header_.block_size; i++) {
CowOperation op = {};
op.type = kCowReplaceOp;
op.new_block = new_block_start + i;
- GetDataPos(&pos);
- op.source = pos + sizeof(op);
+ op.source = next_data_pos_;
if (compression_) {
auto data = Compress(iter, header_.block_size);
@@ -293,6 +311,14 @@
return WriteOperation(op) && Sync();
}
+bool CowWriter::EmitCluster() {
+ CowOperation op = {};
+ op.type = kCowClusterOp;
+ // Next cluster starts after remainder of current cluster and the next data block.
+ op.source = current_data_size_ + cluster_size_ - current_cluster_size_ - sizeof(CowOperation);
+ return WriteOperation(op);
+}
+
std::basic_string<uint8_t> CowWriter::Compress(const void* data, size_t length) {
switch (compression_) {
case kCowCompressGz: {
@@ -345,11 +371,23 @@
}
bool CowWriter::Finalize() {
- footer_.op.ops_size = ops_.size();
- uint64_t pos;
+ auto continue_cluster_size = current_cluster_size_;
+ auto continue_data_size = current_data_size_;
+ auto continue_data_pos = next_data_pos_;
+ auto continue_op_pos = next_op_pos_;
+ auto continue_size = ops_.size();
+ bool extra_cluster = false;
- if (!GetDataPos(&pos)) {
- PLOG(ERROR) << "failed to get file position";
+ // Footer should be at the end of a file, so if there is data after the current block, end it
+ // and start a new cluster.
+ if (cluster_size_ && current_data_size_ > 0) {
+ EmitCluster();
+ extra_cluster = true;
+ }
+
+ footer_.op.ops_size = ops_.size();
+ if (lseek(fd_.get(), next_op_pos_, SEEK_SET) < 0) {
+ PLOG(ERROR) << "Failed to seek to footer position.";
return false;
}
memset(&footer_.data.ops_checksum, 0, sizeof(uint8_t) * 32);
@@ -364,16 +402,24 @@
return false;
}
- // Re-position for any subsequent writes.
- if (lseek(fd_.get(), pos, SEEK_SET) < 0) {
- PLOG(ERROR) << "lseek ops failed";
- return false;
+ // Reposition for additional Writing
+ if (extra_cluster) {
+ current_cluster_size_ = continue_cluster_size;
+ current_data_size_ = continue_data_size;
+ next_data_pos_ = continue_data_pos;
+ next_op_pos_ = continue_op_pos;
+ ops_.resize(continue_size);
}
+
return Sync();
}
uint64_t CowWriter::GetCowSize() {
- return next_op_pos_ + sizeof(footer_);
+ if (current_data_size_ > 0) {
+ return next_data_pos_ + sizeof(footer_);
+ } else {
+ return next_op_pos_ + sizeof(footer_);
+ }
}
bool CowWriter::GetDataPos(uint64_t* pos) {
@@ -387,6 +433,15 @@
}
bool CowWriter::WriteOperation(const CowOperation& op, const void* data, size_t size) {
+ // If there isn't room for this op and the cluster end op, end the current cluster
+ if (cluster_size_ && op.type != kCowClusterOp &&
+ cluster_size_ < current_cluster_size_ + 2 * sizeof(op)) {
+ if (!EmitCluster()) return false;
+ }
+ if (lseek(fd_.get(), next_op_pos_, SEEK_SET) < 0) {
+ PLOG(ERROR) << "lseek failed for writing operation.";
+ return false;
+ }
if (!android::base::WriteFully(fd_, reinterpret_cast<const uint8_t*>(&op), sizeof(op))) {
return false;
}
@@ -399,11 +454,26 @@
void CowWriter::AddOperation(const CowOperation& op) {
footer_.op.num_ops++;
- next_op_pos_ += sizeof(CowOperation) + GetNextOpOffset(op);
+
+ if (op.type == kCowClusterOp) {
+ current_cluster_size_ = 0;
+ current_data_size_ = 0;
+ } else if (header_.cluster_ops) {
+ current_cluster_size_ += sizeof(op);
+ current_data_size_ += op.data_length;
+ }
+
+ next_data_pos_ += op.data_length + GetNextDataOffset(op, header_.cluster_ops);
+ next_op_pos_ += sizeof(CowOperation) + GetNextOpOffset(op, header_.cluster_ops);
ops_.insert(ops_.size(), reinterpret_cast<const uint8_t*>(&op), sizeof(op));
}
bool CowWriter::WriteRawData(const void* data, size_t size) {
+ if (lseek(fd_.get(), next_data_pos_, SEEK_SET) < 0) {
+ PLOG(ERROR) << "lseek failed for writing data.";
+ return false;
+ }
+
if (!android::base::WriteFully(fd_, data, size)) {
return false;
}
diff --git a/fs_mgr/libsnapshot/include/libsnapshot/cow_format.h b/fs_mgr/libsnapshot/include/libsnapshot/cow_format.h
index 80766ff..797b8ef 100644
--- a/fs_mgr/libsnapshot/include/libsnapshot/cow_format.h
+++ b/fs_mgr/libsnapshot/include/libsnapshot/cow_format.h
@@ -57,9 +57,15 @@
// Size of footer struct
uint16_t footer_size;
+ // Size of op struct
+ uint16_t op_size;
+
// The size of block operations, in bytes.
uint32_t block_size;
+ // The number of ops to cluster together. 0 For no clustering. Cannot be 1.
+ uint32_t cluster_ops;
+
// Tracks merge operations completed
uint64_t num_merge_ops;
} __attribute__((packed));
@@ -113,13 +119,15 @@
// For copy operations, this is a block location in the source image.
//
// For replace operations, this is a byte offset within the COW's data
- // section (eg, not landing within the header or metadata). It is an
+ // sections (eg, not landing within the header or metadata). It is an
// absolute position within the image.
//
// For zero operations (replace with all zeroes), this is unused and must
// be zero.
//
// For Label operations, this is the value of the applied label.
+ //
+ // For Cluster operations, this is the length of the following data region
uint64_t source;
} __attribute__((packed));
@@ -129,6 +137,7 @@
static constexpr uint8_t kCowReplaceOp = 2;
static constexpr uint8_t kCowZeroOp = 3;
static constexpr uint8_t kCowLabelOp = 4;
+static constexpr uint8_t kCowClusterOp = 5;
static constexpr uint8_t kCowFooterOp = -1;
static constexpr uint8_t kCowCompressNone = 0;
@@ -142,7 +151,10 @@
std::ostream& operator<<(std::ostream& os, CowOperation const& arg);
-int64_t GetNextOpOffset(const CowOperation& op);
+int64_t GetNextOpOffset(const CowOperation& op, uint32_t cluster_size);
+int64_t GetNextDataOffset(const CowOperation& op, uint32_t cluster_size);
+
+bool IsMetadataOp(const CowOperation& op);
} // namespace snapshot
} // namespace android
diff --git a/fs_mgr/libsnapshot/include/libsnapshot/cow_writer.h b/fs_mgr/libsnapshot/include/libsnapshot/cow_writer.h
index 36cd238..fd43cce 100644
--- a/fs_mgr/libsnapshot/include/libsnapshot/cow_writer.h
+++ b/fs_mgr/libsnapshot/include/libsnapshot/cow_writer.h
@@ -33,6 +33,9 @@
// Maximum number of blocks that can be written.
std::optional<uint64_t> max_blocks;
+
+ // Number of CowOperations in a cluster. 0 for no clustering. Cannot be 1.
+ uint32_t cluster_ops = 0;
};
// Interface for writing to a snapuserd COW. All operations are ordered; merges
@@ -111,6 +114,7 @@
virtual bool EmitLabel(uint64_t label) override;
private:
+ bool EmitCluster();
void SetupHeaders();
bool ParseOptions();
bool OpenForWrite();
@@ -120,6 +124,7 @@
bool WriteOperation(const CowOperation& op, const void* data = nullptr, size_t size = 0);
void AddOperation(const CowOperation& op);
std::basic_string<uint8_t> Compress(const void* data, size_t length);
+ void InitPos();
bool SetFd(android::base::borrowed_fd fd);
bool Sync();
@@ -132,6 +137,10 @@
CowFooter footer_{};
int compression_ = 0;
uint64_t next_op_pos_ = 0;
+ uint64_t next_data_pos_ = 0;
+ uint32_t cluster_size_ = 0;
+ uint32_t current_cluster_size_ = 0;
+ uint64_t current_data_size_ = 0;
bool is_dev_null_ = false;
bool merge_in_progress_ = false;
bool is_block_device_ = false;
diff --git a/fs_mgr/libsnapshot/snapshot_reader.cpp b/fs_mgr/libsnapshot/snapshot_reader.cpp
index b56d879..5ee8e25 100644
--- a/fs_mgr/libsnapshot/snapshot_reader.cpp
+++ b/fs_mgr/libsnapshot/snapshot_reader.cpp
@@ -90,7 +90,7 @@
op_iter_ = cow_->GetOpIter();
while (!op_iter_->Done()) {
const CowOperation* op = &op_iter_->Get();
- if (op->type == kCowLabelOp || op->type == kCowFooterOp) {
+ if (IsMetadataOp(*op)) {
op_iter_->Next();
continue;
}
diff --git a/fs_mgr/libsnapshot/snapuserd.cpp b/fs_mgr/libsnapshot/snapuserd.cpp
index d1d2521..34254a3 100644
--- a/fs_mgr/libsnapshot/snapuserd.cpp
+++ b/fs_mgr/libsnapshot/snapuserd.cpp
@@ -545,7 +545,7 @@
struct disk_exception* de =
reinterpret_cast<struct disk_exception*>((char*)de_ptr.get() + offset);
- if (cow_op->type == kCowFooterOp || cow_op->type == kCowLabelOp) {
+ if (IsMetadataOp(*cow_op)) {
cowop_riter_->Next();
continue;
}