blob: 9c0de0c20256a452bd13fba9c25c7db637ef1f5d [file] [log] [blame] [edit]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "block_device.h"
#include <lib/fake_ddk/fake_ddk.h>
#include <lib/fit/function.h>
#include <lib/ftl/volume.h>
#include <lib/fzl/owned-vmo-mapper.h>
#include <lib/inspect/cpp/reader.h>
#include <lib/inspect/cpp/vmo/types.h>
#include <atomic>
#include <memory>
#include <utility>
#include <ddk/protocol/block.h>
#include <ddktl/protocol/nand.h>
#include <fbl/array.h>
#include <zxtest/zxtest.h>
#include "metrics.h"
namespace {
constexpr uint32_t kPageSize = 1024;
constexpr uint32_t kNumPages = 20;
constexpr char kMagic = 'f';
constexpr uint8_t kGuid[ZBI_PARTITION_GUID_LEN] = {'g', 'u', 'i', 'd'};
constexpr uint32_t kWearCount = 1337;
bool CheckPattern(const void* buffer, size_t size, char pattern = kMagic) {
const char* data = reinterpret_cast<const char*>(buffer);
for (; size; size--) {
if (*data++ != pattern) {
return false;
}
}
return true;
}
class FakeNand : public ddk::NandProtocol<FakeNand> {
public:
FakeNand() : proto_({&nand_protocol_ops_, this}) {}
nand_protocol_t* proto() { return &proto_; }
// Nand protocol:
void NandQuery(fuchsia_hardware_nand_Info* out_info, size_t* out_nand_op_size) {
*out_info = {};
out_info->oob_size = 8;
memcpy(out_info->partition_guid, kGuid, sizeof(kGuid));
*out_nand_op_size = 0;
}
void NandQueue(nand_operation_t* operation, nand_queue_callback callback, void* cookie) {}
zx_status_t NandGetFactoryBadBlockList(uint32_t* out_bad_blocks_list, size_t bad_blocks_count,
size_t* out_bad_blocks_actual) {
return ZX_ERR_BAD_STATE;
}
private:
nand_protocol_t proto_;
};
class FakeVolume final : public ftl::Volume {
public:
explicit FakeVolume(ftl::BlockDevice* device) : device_(device) {}
~FakeVolume() final {}
bool written() const { return written_; }
bool flushed() const { return flushed_; }
bool formatted() const { return formatted_; }
bool leveled() const { return leveled_; }
bool trimmed() const { return trimmed_; }
uint32_t first_page() const { return first_page_; }
int num_pages() const { return num_pages_; }
// Volume interface.
const char* Init(std::unique_ptr<ftl::NdmDriver> driver) final {
device_->OnVolumeAdded(kPageSize, kNumPages);
return nullptr;
}
const char* ReAttach() final { return nullptr; }
zx_status_t Read(uint32_t first_page, int num_pages, void* buffer) final {
OnOperation();
first_page_ = first_page;
num_pages_ = num_pages;
memset(buffer, kMagic, num_pages * kPageSize);
return ZX_OK;
}
zx_status_t Write(uint32_t first_page, int num_pages, const void* buffer) final {
OnOperation();
first_page_ = first_page;
num_pages_ = num_pages;
written_ = true;
if (!CheckPattern(buffer, kPageSize * num_pages)) {
return ZX_ERR_IO_DATA_INTEGRITY;
}
return ZX_OK;
}
zx_status_t Format() final {
formatted_ = true;
return ZX_OK;
}
zx_status_t FormatAndLevel() final {
leveled_ = true;
return ZX_OK;
}
zx_status_t Mount() final { return ZX_OK; }
zx_status_t Unmount() final { return ZX_OK; }
zx_status_t Flush() final {
OnOperation();
flushed_ = true;
return ZX_OK;
}
zx_status_t Trim(uint32_t first_page, uint32_t num_pages) final {
OnOperation();
trimmed_ = true;
first_page_ = first_page;
num_pages_ = num_pages;
return ZX_OK;
}
zx_status_t GarbageCollect() final { return ZX_OK; }
zx_status_t GetStats(Stats* stats) final {
*stats = {};
stats->wear_count = wear_count_;
return ZX_OK;
}
zx_status_t GetCounters(Counters* counters) final {
counters->wear_count = wear_count_;
return ZX_OK;
}
void UpdateWearCount(uint32_t wear_count) { wear_count_ = wear_count; }
void SetOnOperation(fit::function<void()> callback) { on_operation_ = std::move(callback); }
private:
void OnOperation() {
if (on_operation_) {
on_operation_();
}
}
ftl::BlockDevice* device_;
uint32_t first_page_ = 0;
int num_pages_ = 0;
uint32_t wear_count_ = kWearCount;
fit::function<void()> on_operation_;
bool written_ = false;
bool flushed_ = false;
bool formatted_ = false;
bool leveled_ = false;
bool trimmed_ = false;
};
TEST(BlockDeviceTest, TrivialLifetime) {
FakeNand nand;
ftl::BlockDevice device;
device.SetVolumeForTest(std::make_unique<FakeVolume>(&device));
device.SetNandParentForTest(*nand.proto());
ASSERT_OK(device.Init());
}
TEST(BlockDeviceTest, DdkLifetime) {
ftl::BlockDevice* device(new ftl::BlockDevice(fake_ddk::kFakeParent));
device->SetVolumeForTest(std::make_unique<FakeVolume>(device));
FakeNand nand;
fake_ddk::Bind ddk;
fbl::Array<fake_ddk::ProtocolEntry> protocols(new fake_ddk::ProtocolEntry[1], 1);
protocols[0] = {ZX_PROTOCOL_NAND, {nand.proto()->ops, nand.proto()->ctx}};
ddk.SetProtocols(std::move(protocols));
ASSERT_OK(device->Bind());
device->DdkAsyncRemove();
EXPECT_TRUE(ddk.Ok());
// This should delete the object, which means this test should not leak.
device->DdkRelease();
}
TEST(BlockDeviceTest, GetSize) {
FakeNand nand;
ftl::BlockDevice device;
device.SetVolumeForTest(std::make_unique<FakeVolume>(&device));
device.SetNandParentForTest(*nand.proto());
ASSERT_OK(device.Init());
EXPECT_EQ(kPageSize * kNumPages, device.DdkGetSize());
}
TEST(BlockDeviceTest, GetName) {
FakeNand nand;
ftl::BlockDevice device;
device.SetVolumeForTest(std::make_unique<FakeVolume>(&device));
device.SetNandParentForTest(*nand.proto());
ASSERT_OK(device.Init());
char name[20];
ASSERT_OK(device.BlockPartitionGetName(name, sizeof(name)));
EXPECT_GT(strlen(name), 0);
}
TEST(BlockDeviceTest, GetType) {
FakeNand nand;
ftl::BlockDevice device;
device.SetVolumeForTest(std::make_unique<FakeVolume>(&device));
device.SetNandParentForTest(*nand.proto());
ASSERT_OK(device.Init());
guid_t guid;
ASSERT_OK(device.BlockPartitionGetGuid(GUIDTYPE_TYPE, &guid));
EXPECT_EQ(0, memcmp(&guid, kGuid, sizeof(guid)));
}
TEST(BlockDeviceTest, Query) {
FakeNand nand;
ftl::BlockDevice device;
device.SetVolumeForTest(std::make_unique<FakeVolume>(&device));
device.SetNandParentForTest(*nand.proto());
ASSERT_OK(device.Init());
block_info_t info;
size_t operation_size;
device.BlockImplQuery(&info, &operation_size);
constexpr block_info_t kInfo = {kNumPages, kPageSize, BLOCK_MAX_TRANSFER_UNBOUNDED,
BLOCK_FLAG_TRIM_SUPPORT, 0};
ASSERT_BYTES_EQ(&info, &kInfo, sizeof(info));
ASSERT_GT(operation_size, sizeof(block_op_t));
}
class BlockDeviceTest;
// Wrapper for a block_op_t.
class Operation {
public:
explicit Operation(size_t op_size, BlockDeviceTest* test) : op_size_(op_size), test_(test) {}
~Operation() {}
// Accessors for the memory represented by the operation's vmo.
size_t buffer_size() const { return buffer_size_; }
void* buffer() const { return mapper_.start(); }
// Creates a vmo and sets the handle on the block_op_t.
bool SetVmo();
block_op_t* GetOperation();
void OnCompletion(zx_status_t status) {
status_ = status;
completed_ = true;
}
bool completed() const { return completed_; }
zx_status_t status() const { return status_; }
BlockDeviceTest* test() const { return test_; }
DISALLOW_COPY_ASSIGN_AND_MOVE(Operation);
private:
zx_handle_t GetVmo();
fzl::OwnedVmoMapper mapper_;
size_t op_size_;
BlockDeviceTest* test_;
zx_status_t status_ = ZX_ERR_ACCESS_DENIED;
bool completed_ = false;
static constexpr size_t buffer_size_ = kPageSize * kNumPages;
std::unique_ptr<char[]> raw_buffer_;
};
bool Operation::SetVmo() {
block_op_t* operation = GetOperation();
if (!operation) {
return false;
}
operation->rw.vmo = GetVmo();
return operation->rw.vmo != ZX_HANDLE_INVALID;
}
block_op_t* Operation::GetOperation() {
if (!raw_buffer_) {
raw_buffer_.reset(new char[op_size_]);
memset(raw_buffer_.get(), 0, op_size_);
}
return reinterpret_cast<block_op_t*>(raw_buffer_.get());
}
zx_handle_t Operation::GetVmo() {
if (mapper_.start()) {
return mapper_.vmo().get();
}
if (mapper_.CreateAndMap(buffer_size_, "") != ZX_OK) {
return ZX_HANDLE_INVALID;
}
return mapper_.vmo().get();
}
// Provides control primitives for tests that issue IO requests to the device.
class BlockDeviceTest : public zxtest::Test {
public:
BlockDeviceTest();
~BlockDeviceTest() {}
ftl::BlockDevice* GetDevice() { return device_.get(); }
size_t op_size() const { return op_size_; }
FakeVolume* GetVolume() { return volume_; }
static void CompletionCb(void* cookie, zx_status_t status, block_op_t* op) {
Operation* operation = reinterpret_cast<Operation*>(cookie);
operation->OnCompletion(status);
operation->test()->num_completed_++;
sync_completion_signal(&operation->test()->event_);
}
bool Wait() {
zx_status_t status = sync_completion_wait(&event_, ZX_SEC(5));
sync_completion_reset(&event_);
return status == ZX_OK;
}
bool WaitFor(int desired) {
while (num_completed_ < desired) {
if (!Wait()) {
return false;
}
}
return true;
}
void Read() {
Operation operation(op_size(), this);
ASSERT_TRUE(operation.SetVmo());
auto* op = operation.GetOperation();
op->rw.command = BLOCK_OP_READ;
op->rw.length = 1;
op->rw.offset_dev = 0;
device_->BlockImplQueue(op, &BlockDeviceTest::CompletionCb, &operation);
ASSERT_TRUE(Wait());
ASSERT_OK(operation.status());
}
void Write() {
Operation operation(op_size(), this);
ASSERT_TRUE(operation.SetVmo());
auto* op = operation.GetOperation();
op->rw.command = BLOCK_OP_WRITE;
op->rw.length = 1;
op->rw.offset_dev = 0;
memset(operation.buffer(), kMagic, kPageSize);
device_->BlockImplQueue(op, &BlockDeviceTest::CompletionCb, &operation);
ASSERT_TRUE(Wait());
ASSERT_OK(operation.status());
}
void Flush() {
Operation operation(op_size(), this);
ASSERT_TRUE(operation.SetVmo());
auto* op = operation.GetOperation();
op->rw.command = BLOCK_OP_FLUSH;
device_->BlockImplQueue(op, &BlockDeviceTest::CompletionCb, &operation);
ASSERT_TRUE(Wait());
ASSERT_OK(operation.status());
}
void Trim() {
Operation operation(op_size(), this);
ASSERT_TRUE(operation.SetVmo());
auto* op = operation.GetOperation();
op->trim.command = BLOCK_OP_TRIM;
op->trim.length = 1;
op->trim.offset_dev = kNumPages - 1;
device_->BlockImplQueue(op, &BlockDeviceTest::CompletionCb, &operation);
ASSERT_TRUE(Wait());
ASSERT_OK(operation.status());
}
DISALLOW_COPY_ASSIGN_AND_MOVE(BlockDeviceTest);
private:
sync_completion_t event_;
std::atomic<int> num_completed_ = 0;
std::unique_ptr<ftl::BlockDevice> device_;
size_t op_size_;
FakeNand nand_;
FakeVolume* volume_ = nullptr; // Object owned by device_.
};
BlockDeviceTest::BlockDeviceTest() : device_(new ftl::BlockDevice()) {
volume_ = new FakeVolume(device_.get());
device_->SetVolumeForTest(std::unique_ptr<FakeVolume>(volume_));
device_->SetNandParentForTest(*nand_.proto());
block_info_t info;
device_->BlockImplQuery(&info, &op_size_);
if (device_->Init() != ZX_OK) {
device_.reset();
}
}
// Tests trivial attempts to queue one operation.
TEST_F(BlockDeviceTest, QueueOne) {
ftl::BlockDevice* device = GetDevice();
ASSERT_TRUE(device);
Operation operation(op_size(), this);
block_op_t* op = operation.GetOperation();
ASSERT_TRUE(op);
op->rw.command = BLOCK_OP_READ;
device->BlockImplQueue(op, &BlockDeviceTest::CompletionCb, &operation);
ASSERT_TRUE(Wait());
ASSERT_EQ(ZX_ERR_OUT_OF_RANGE, operation.status());
op->rw.length = 1;
device->BlockImplQueue(op, &BlockDeviceTest::CompletionCb, &operation);
ASSERT_TRUE(Wait());
ASSERT_EQ(ZX_ERR_INVALID_ARGS, operation.status());
op->rw.offset_dev = kNumPages;
device->BlockImplQueue(op, &BlockDeviceTest::CompletionCb, &operation);
ASSERT_TRUE(Wait());
ASSERT_EQ(ZX_ERR_OUT_OF_RANGE, operation.status());
ASSERT_TRUE(operation.SetVmo());
op->rw.offset_dev = kNumPages - 1;
device->BlockImplQueue(op, &BlockDeviceTest::CompletionCb, &operation);
ASSERT_TRUE(Wait());
ASSERT_OK(operation.status());
}
TEST_F(BlockDeviceTest, ReadWrite) {
ftl::BlockDevice* device = GetDevice();
ASSERT_TRUE(device);
Operation operation(op_size(), this);
ASSERT_TRUE(operation.SetVmo());
block_op_t* op = operation.GetOperation();
ASSERT_TRUE(op);
op->rw.command = BLOCK_OP_READ;
op->rw.length = 2;
op->rw.offset_dev = 3;
ASSERT_TRUE(operation.SetVmo());
device->BlockImplQueue(op, &BlockDeviceTest::CompletionCb, &operation);
ASSERT_TRUE(Wait());
ASSERT_OK(operation.status());
FakeVolume* volume = GetVolume();
EXPECT_FALSE(volume->written());
EXPECT_EQ(2, volume->num_pages());
EXPECT_EQ(3, volume->first_page());
EXPECT_TRUE(CheckPattern(operation.buffer(), kPageSize * 2));
op->rw.command = BLOCK_OP_WRITE;
op->rw.length = 4;
op->rw.offset_dev = 5;
memset(operation.buffer(), kMagic, kPageSize * 5);
device->BlockImplQueue(op, &BlockDeviceTest::CompletionCb, &operation);
ASSERT_TRUE(Wait());
ASSERT_OK(operation.status());
EXPECT_TRUE(volume->written());
EXPECT_EQ(4, volume->num_pages());
EXPECT_EQ(5, volume->first_page());
}
TEST_F(BlockDeviceTest, Trim) {
ftl::BlockDevice* device = GetDevice();
ASSERT_TRUE(device);
Operation operation(op_size(), this);
block_op_t* op = operation.GetOperation();
ASSERT_TRUE(op);
op->trim.command = BLOCK_OP_TRIM;
device->BlockImplQueue(op, &BlockDeviceTest::CompletionCb, &operation);
ASSERT_TRUE(Wait());
ASSERT_EQ(ZX_ERR_OUT_OF_RANGE, operation.status());
op->trim.length = 2;
op->trim.offset_dev = kNumPages - 1;
device->BlockImplQueue(op, &BlockDeviceTest::CompletionCb, &operation);
ASSERT_TRUE(Wait());
ASSERT_EQ(ZX_ERR_OUT_OF_RANGE, operation.status());
op->trim.offset_dev = 3;
device->BlockImplQueue(op, &BlockDeviceTest::CompletionCb, &operation);
ASSERT_TRUE(Wait());
ASSERT_OK(operation.status());
EXPECT_TRUE(GetVolume()->trimmed());
EXPECT_EQ(2, GetVolume()->num_pages());
EXPECT_EQ(3, GetVolume()->first_page());
}
TEST_F(BlockDeviceTest, Flush) {
ftl::BlockDevice* device = GetDevice();
ASSERT_TRUE(device);
Operation operation(op_size(), this);
block_op_t* op = operation.GetOperation();
ASSERT_TRUE(op);
op->rw.command = BLOCK_OP_FLUSH;
device->BlockImplQueue(op, &BlockDeviceTest::CompletionCb, &operation);
ASSERT_TRUE(Wait());
ASSERT_OK(operation.status());
EXPECT_TRUE(GetVolume()->flushed());
}
// Tests serialization of multiple operations.
TEST_F(BlockDeviceTest, QueueMultiple) {
ftl::BlockDevice* device = GetDevice();
ASSERT_TRUE(device);
std::unique_ptr<Operation> operations[10];
for (int i = 0; i < 10; i++) {
operations[i].reset(new Operation(op_size(), this));
Operation& operation = *(operations[i].get());
block_op_t* op = operation.GetOperation();
ASSERT_TRUE(op);
op->rw.command = BLOCK_OP_READ;
op->rw.length = 1;
op->rw.offset_dev = i;
ASSERT_TRUE(operation.SetVmo());
device->BlockImplQueue(op, &BlockDeviceTest::CompletionCb, &operation);
}
ASSERT_TRUE(WaitFor(10));
for (const auto& operation : operations) {
ASSERT_OK(operation->status());
ASSERT_TRUE(operation->completed());
}
}
TEST_F(BlockDeviceTest, Format) {
ftl::BlockDevice* device = GetDevice();
ASSERT_TRUE(device);
EXPECT_OK(device->Format());
EXPECT_TRUE(GetVolume()->formatted());
EXPECT_FALSE(GetVolume()->leveled());
}
TEST_F(BlockDeviceTest, GetInspectVmoContainsCountersAndWearCount) {
ftl::BlockDevice* device = GetDevice();
ASSERT_TRUE(device);
zx::vmo vmo = device->DuplicateInspectVmo();
auto base_hierarchy = inspect::ReadFromVmo(vmo).take_value();
auto* hierarchy = base_hierarchy.GetByPath({"ftl"});
ASSERT_NOT_NULL(hierarchy);
for (const auto& property_name : ftl::Metrics::GetPropertyNames<inspect::UintProperty>()) {
auto* property = hierarchy->node().get_property<inspect::UintPropertyValue>(property_name);
EXPECT_NOT_NULL(property, "Missing Inspect Property: %s", property_name.c_str());
}
for (const auto& property_name : ftl::Metrics::GetPropertyNames<inspect::DoubleProperty>()) {
auto* property = hierarchy->node().get_property<inspect::DoublePropertyValue>(property_name);
EXPECT_NOT_NULL(property, "Missing Inspect Property: %s", property_name.c_str());
}
}
void ReadProperties(ftl::BlockDevice* device, std::map<std::string, uint64_t>& counters,
std::map<std::string, double>& rates) {
zx::vmo vmo = device->DuplicateInspectVmo();
auto base_hierarchy = inspect::ReadFromVmo(vmo).take_value();
auto* hierarchy = base_hierarchy.GetByPath({"ftl"});
// counters are still 0.
for (const auto& property_name : ftl::Metrics::GetPropertyNames<inspect::UintProperty>()) {
auto* property = hierarchy->node().get_property<inspect::UintPropertyValue>(property_name);
ASSERT_NOT_NULL(property, "Missing Inspect Property: %s", property_name.c_str());
counters[property_name] = property->value();
}
for (const auto& property_name : ftl::Metrics::GetPropertyNames<inspect::DoubleProperty>()) {
auto* property = hierarchy->node().get_property<inspect::DoublePropertyValue>(property_name);
ASSERT_NOT_NULL(property, "Missing Inspect Property: %s", property_name.c_str());
rates[property_name] = property->value();
}
}
void VerifyInspectMetrics(BlockDeviceTest* fixture, const std::string& block_metric_prefix,
fit::function<std::string()> clear_op,
fit::function<void()> trigger_metric_update_op) {
ftl::BlockDevice* device = fixture->GetDevice();
ASSERT_TRUE(device);
auto* volume = fixture->GetVolume();
std::map<std::string, uint64_t> counters;
std::map<std::string, double> rates;
std::map<std::string, uint64_t> expected_counters;
std::map<std::string, double> expected_rates;
volume->UpdateWearCount(0);
// Random operation to trigger a metric update.
expected_counters[clear_op()]++;
ReadProperties(device, counters, rates);
for (const auto& counter : counters) {
EXPECT_EQ(counter.second, expected_counters[counter.first],
"Property %s had initial non zero counter.", counter.first.c_str());
}
// The counters are cleared before any operation.
volume->SetOnOperation([&]() {
auto& counters = device->nand_counters();
counters.page_read = 1;
counters.page_write = 2;
counters.block_erase = 3;
});
volume->UpdateWearCount(24);
trigger_metric_update_op();
volume->SetOnOperation([&]() {
auto& counters = device->nand_counters();
counters.page_read = 2;
counters.page_write = 4;
counters.block_erase = 5;
});
volume->UpdateWearCount(12345678);
trigger_metric_update_op();
expected_counters[ftl::Metrics::GetMaxWearPropertyName()] = 12345678;
expected_counters["nand.erase_block.max_wear"] = 12345678;
// Counters
expected_counters[block_metric_prefix + ".count"] = 2;
expected_counters[block_metric_prefix + ".issued_nand_operation.count"] = 17;
expected_counters[block_metric_prefix + ".issued_page_read.count"] = 3;
expected_counters[block_metric_prefix + ".issued_page_write.count"] = 6;
expected_counters[block_metric_prefix + ".issued_block_erase.count"] = 8;
// Rates
expected_rates[block_metric_prefix + ".issued_nand_operation.average_rate"] = 8.5;
expected_rates[block_metric_prefix + ".issued_page_read.average_rate"] = 1.5;
expected_rates[block_metric_prefix + ".issued_page_write.average_rate"] = 3;
expected_rates[block_metric_prefix + ".issued_block_erase.average_rate"] = 4;
ReadProperties(device, counters, rates);
for (const auto& counter : counters) {
EXPECT_EQ(counter.second, expected_counters[counter.first], "Property %s mismatch.",
counter.first.c_str());
}
for (const auto& rate : rates) {
EXPECT_EQ(rate.second, expected_rates[rate.first], "Property %s mismatch.", rate.first.c_str());
}
}
TEST_F(BlockDeviceTest, InspectReadMetricsUpdatedCorrectly) {
VerifyInspectMetrics(
this, "block.read",
[&]() {
Flush();
return "block.flush.count";
},
[&]() { Read(); });
}
TEST_F(BlockDeviceTest, InspectWriteMetricsUpdatedCorrectly) {
VerifyInspectMetrics(
this, "block.write",
[&]() {
Flush();
return "block.flush.count";
},
[&]() { Write(); });
}
TEST_F(BlockDeviceTest, InspectTrimMetricsUpdatedCorrectly) {
VerifyInspectMetrics(
this, "block.trim",
[&]() {
Flush();
return "block.flush.count";
},
[&]() { Trim(); });
}
TEST_F(BlockDeviceTest, InspectFlushMetricsUpdatedCorrectly) {
VerifyInspectMetrics(
this, "block.flush",
[&]() {
Trim();
return "block.trim.count";
},
[&]() { Flush(); });
}
TEST_F(BlockDeviceTest, Suspend) {
ftl::BlockDevice* device = GetDevice();
ASSERT_TRUE(device);
device->Suspend();
EXPECT_TRUE(GetVolume()->flushed());
}
} // namespace