blob: f932d1ff00363c055086de24552ed34b57388e66 [file] [log] [blame]
// Copyright 2020 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 "flash_stress.h"
#include <fuchsia/hardware/block/cpp/fidl.h>
#include <fuchsia/hardware/block/volume/cpp/fidl.h>
#include <inttypes.h>
#include <lib/fdio/directory.h>
#include <lib/zx/clock.h>
#include <lib/zx/fifo.h>
#include <lib/zx/time.h>
#include <lib/zx/vmar.h>
#include <lib/zx/vmo.h>
#include <zircon/assert.h>
#include <zircon/status.h>
#include <queue>
#include <string>
#include <utility>
#include <fbl/unique_fd.h>
#include <fs-management/fvm.h>
#include <src/lib/uuid/uuid.h>
#include "status.h"
#include "util.h"
namespace hwstress {
namespace {
constexpr uint32_t kMaxInFlightRequests = 8;
constexpr uint32_t kDefaultTransferSize = 1024 * 1024;
constexpr uint32_t kMinFvmFreeSpace = 16 * 1024 * 1024;
constexpr uint32_t kMinPartitionFreeSpace = 2 * 1024 * 1024;
void WriteBlockData(zx_vaddr_t start, uint32_t block_size, uint64_t value) {
uint64_t num_words = block_size / sizeof(value);
uint64_t* data = reinterpret_cast<uint64_t*>(start);
for (uint64_t i = 0; i < num_words; i++) {
data[i] = value;
}
}
void VerifyBlockData(zx_vaddr_t start, uint32_t block_size, uint64_t value) {
uint64_t num_words = block_size / sizeof(value);
uint64_t* data = reinterpret_cast<uint64_t*>(start);
for (uint64_t i = 0; i < num_words; i++) {
if (unlikely(data[i] != value)) {
ZX_PANIC("Found error: expected 0x%016" PRIX64 ", got 0x%016" PRIX64 " at offset %" PRIu64
"\n",
value, data[i], value * block_size + i * sizeof(value));
}
}
}
zx_status_t OpenBlockDevice(const std::string& path,
fuchsia::hardware::block::BlockSyncPtr* device) {
// Create a channel, and connect to block device.
zx::channel client, server;
zx_status_t status = zx::channel::create(0, &client, &server);
if (status != ZX_OK) {
return status;
}
status = fdio_service_connect(path.c_str(), server.release());
if (status != ZX_OK) {
return status;
}
device->Bind(std::move(client));
return ZX_OK;
}
zx_status_t SendFifoRequest(const zx::fifo& fifo, const block_fifo_request_t& request) {
zx_status_t r = fifo.write(sizeof(request), &request, 1, nullptr);
if (r == ZX_OK || r == ZX_ERR_SHOULD_WAIT) {
return r;
}
fprintf(stderr, "error: failed writing fifo: %s\n", zx_status_get_string(r));
return r;
}
zx_status_t ReceiveFifoResponse(const zx::fifo& fifo, block_fifo_response_t* resp) {
zx_status_t r = fifo.read(sizeof(*resp), resp, 1, nullptr);
if (r == ZX_ERR_SHOULD_WAIT) {
// Nothing ready yet.
return r;
}
if (r != ZX_OK) {
// Transport error.
fprintf(stderr, "error: failed reading fifo: %s\n", zx_status_get_string(r));
return r;
}
if (resp->status != ZX_OK) {
// Block device error.
fprintf(stderr, "error: io txn failed: %s\n", zx_status_get_string(resp->status));
return resp->status;
}
return ZX_OK;
}
} // namespace
zx_status_t FlashIo(const BlockDevice& device, size_t bytes_to_test, size_t transfer_size,
bool is_write_test) {
ZX_ASSERT(bytes_to_test % device.info.block_size == 0);
size_t bytes_to_send = bytes_to_test;
size_t bytes_to_receive = bytes_to_test;
size_t blksize = device.info.block_size;
size_t vmo_byte_offset = 0;
size_t dev_off = 0;
uint32_t opcode = is_write_test ? BLOCKIO_WRITE : BLOCKIO_READ;
std::queue<reqid_t> ready_to_send;
block_fifo_request_t reqs[kMaxInFlightRequests];
for (reqid_t next_reqid = 0; next_reqid < kMaxInFlightRequests; next_reqid++) {
reqs[next_reqid] = {.opcode = opcode,
.reqid = next_reqid,
.vmoid = device.vmoid.id,
// |length|, |vmo_offset|, and |dev_offset| are measured in blocks.
.length = static_cast<uint32_t>(transfer_size / blksize),
.vmo_offset = vmo_byte_offset / blksize};
ready_to_send.push(next_reqid);
vmo_byte_offset += transfer_size;
}
while (bytes_to_receive > 0) {
// Ensure we are ready to either write to or read from the FIFO.
zx_signals_t flags = ZX_FIFO_PEER_CLOSED;
if (!ready_to_send.empty() && bytes_to_send > 0) {
flags |= ZX_FIFO_WRITABLE;
}
if (ready_to_send.size() < kMaxInFlightRequests) {
flags |= ZX_FIFO_READABLE;
}
zx_signals_t pending_signals;
device.fifo.wait_one(flags, zx::time(ZX_TIME_INFINITE), &pending_signals);
// If we lost our connection to the block device, abort the test.
if ((pending_signals & ZX_FIFO_PEER_CLOSED) != 0) {
fprintf(stderr, "Error: connection to block device lost\n");
return ZX_ERR_PEER_CLOSED;
}
// If the FIFO is writable send a request unless we have kMaxInFlightRequests in flight,
// or have finished reading/writing.
if ((pending_signals & ZX_FIFO_WRITABLE) != 0 && !ready_to_send.empty() && bytes_to_send > 0) {
reqid_t reqid = ready_to_send.front();
reqs[reqid].dev_offset = dev_off / blksize;
reqs[reqid].length = std::min(transfer_size, bytes_to_send) / blksize;
if (is_write_test) {
vmo_byte_offset = reqs[reqid].vmo_offset * blksize;
for (size_t i = 0; i < reqs[reqid].length; i++) {
uint64_t value = reqs[reqid].dev_offset + i;
WriteBlockData(device.vmo_addr + vmo_byte_offset + blksize * i, blksize, value);
}
}
zx_status_t r = SendFifoRequest(device.fifo, reqs[reqid]);
if (r != ZX_OK) {
return r;
}
dev_off += transfer_size;
ready_to_send.pop();
bytes_to_send -= reqs[reqid].length * blksize;
continue;
}
// Process response from the block device if the FIFO is readable.
if ((pending_signals & ZX_FIFO_READABLE) != 0) {
ZX_ASSERT(ready_to_send.size() < kMaxInFlightRequests);
block_fifo_response_t resp;
zx_status_t r = ReceiveFifoResponse(device.fifo, &resp);
if (r != ZX_OK) {
return r;
}
reqid_t reqid = resp.reqid;
bytes_to_receive -= reqs[reqid].length * blksize;
if (!is_write_test) {
vmo_byte_offset = reqs[reqid].vmo_offset * blksize;
for (size_t i = 0; i < reqs[reqid].length; i++) {
uint64_t value = reqs[reqid].dev_offset + i;
VerifyBlockData(device.vmo_addr + vmo_byte_offset + blksize * i, blksize, value);
}
}
if (bytes_to_send > 0) {
ready_to_send.push(reqid);
}
continue;
}
}
return ZX_OK;
}
zx_status_t SetupBlockFifo(const std::string& path, BlockDevice* device) {
zx_status_t status;
// Fetch a FIFO for communicating with the block device over.
zx::fifo fifo;
zx_status_t io_status = device->device->GetFifo(&status, &fifo);
if (io_status != ZX_OK || status != ZX_OK) {
fprintf(stderr, "Error: cannot get FIFO for '%s'\n", path.c_str());
return ZX_ERR_INTERNAL;
}
// Setup a shared VMO with the block device.
zx::vmo vmo;
status = zx::vmo::create(device->vmo_size, /*options=*/0, &vmo);
if (status != ZX_OK) {
fprintf(stderr, "Error: could not allocate memory: %s\n", zx_status_get_string(status));
return status;
}
zx::vmo shared_vmo;
status = vmo.duplicate(ZX_RIGHT_SAME_RIGHTS, &shared_vmo);
if (status != ZX_OK) {
fprintf(stderr, "Error: cannot duplicate handle: %s\n", zx_status_get_string(status));
return status;
}
std::unique_ptr<fuchsia::hardware::block::VmoId> vmo_id;
io_status = device->device->AttachVmo(std::move(shared_vmo), &status, &vmo_id);
if (io_status != ZX_OK || status != ZX_OK || vmo_id == nullptr) {
fprintf(stderr, "Error: cannot attach VMO for '%s'\n", path.c_str());
return ZX_ERR_INTERNAL;
}
device->vmoid = *vmo_id;
// Map the VMO into memory.
status = zx::vmar::root_self()->map(
/*options=*/(ZX_VM_PERM_READ | ZX_VM_PERM_WRITE | ZX_VM_MAP_RANGE),
/*vmar_offset=*/0, vmo, /*vmo_offset=*/0, /*len=*/device->vmo_size,
&device->vmo_addr);
if (status != ZX_OK) {
fprintf(stderr, "Error: VMO could not be mapped into memory: %s", zx_status_get_string(status));
return status;
}
device->fifo = std::move(fifo);
device->vmo = std::move(vmo);
return ZX_OK;
}
std::unique_ptr<TemporaryFvmPartition> TemporaryFvmPartition::Create(int fvm_fd,
uint64_t slices_requested) {
uuid::Uuid unique_guid = uuid::Uuid::Generate();
alloc_req_t request{.slice_count = slices_requested,
.name = "flash-test-fs",
.flags = fuchsia::hardware::block::volume::ALLOCATE_PARTITION_FLAG_INACTIVE};
memcpy(request.guid, unique_guid.bytes(), sizeof(request.guid));
memcpy(request.type, kTestPartGUID.bytes(), sizeof(request.type));
// Create a new partition.
fbl::unique_fd fd(fvm_allocate_partition(fvm_fd, &request));
if (!fd) {
fprintf(stderr, "Error: Could not allocate and open FVM partition\n");
return nullptr;
}
char partition_path[PATH_MAX];
fd.reset(open_partition(unique_guid.bytes(), kTestPartGUID.bytes(), 0, partition_path));
if (!fd) {
destroy_partition(unique_guid.bytes(), kTestPartGUID.bytes());
fprintf(stderr, "Could not locate FVM partition\n");
return nullptr;
}
return std::unique_ptr<TemporaryFvmPartition>(
new TemporaryFvmPartition(partition_path, unique_guid));
}
TemporaryFvmPartition::TemporaryFvmPartition(std::string partition_path, uuid::Uuid unique_guid)
: partition_path_(std::move(partition_path)), unique_guid_(unique_guid) {}
TemporaryFvmPartition::~TemporaryFvmPartition() {
ZX_ASSERT(destroy_partition(unique_guid_.bytes(), kTestPartGUID.bytes()) == ZX_OK);
}
std::string TemporaryFvmPartition::GetPartitionPath() { return partition_path_; }
// Start a stress test.
bool StressFlash(StatusLine* status, const CommandLineArgs& args, zx::duration duration) {
// Access the FVM.
fbl::unique_fd fvm_fd(open(args.fvm_path.c_str(), O_RDWR));
if (!fvm_fd) {
status->Log("Error: Could not open FVM\n");
return false;
}
// Calculate available space and number of slices needed.
fuchsia_hardware_block_volume_VolumeInfo fvm_info;
if (fvm_query(fvm_fd.get(), &fvm_info) != ZX_OK) {
status->Log("Error: Could not get FVM info\n");
return false;
}
// Default to using all available disk space.
uint64_t slices_available = fvm_info.pslice_total_count - fvm_info.pslice_allocated_count;
uint64_t bytes_to_test = slices_available * fvm_info.slice_size -
RoundUp(kMinFvmFreeSpace, fvm_info.slice_size) - kMinPartitionFreeSpace;
// If a value was specified and does not exceed the free disk space, use that.
if (args.mem_to_test_megabytes.has_value()) {
uint64_t bytes_requested = args.mem_to_test_megabytes.value() * 1024 * 1024;
if (bytes_requested <= bytes_to_test) {
bytes_to_test = bytes_requested;
} else {
status->Log("Specified disk size (%ld bytes) exceeds available disk size (%ld bytes).\n",
bytes_requested, bytes_to_test);
return false;
}
}
uint64_t slices_requested = RoundUp(bytes_to_test, fvm_info.slice_size) / fvm_info.slice_size;
std::unique_ptr<TemporaryFvmPartition> fvm_partition =
TemporaryFvmPartition::Create(fvm_fd.get(), slices_requested);
if (fvm_partition == nullptr) {
status->Log("Failed to create FVM partition");
return false;
}
std::string partition_path = fvm_partition->GetPartitionPath();
BlockDevice device;
if (OpenBlockDevice(partition_path, &device.device) != ZX_OK) {
status->Log("Error: Block device could not be opened");
return false;
}
// Fetch information about the underlying block device, such as block size.
zx_status_t info_status;
std::unique_ptr<fuchsia::hardware::block::BlockInfo> block_info;
zx_status_t io_status = device.device->GetInfo(&info_status, &block_info);
if (io_status != ZX_OK || info_status != ZX_OK) {
status->Log("Error: cannot get block device info for '%s'\n", partition_path.c_str());
return ZX_ERR_INTERNAL;
}
device.info = *block_info;
size_t actual_transfer_size = RoundDown(
std::min(kDefaultTransferSize, device.info.max_transfer_size), device.info.block_size);
device.vmo_size = actual_transfer_size * kMaxInFlightRequests;
if (SetupBlockFifo(partition_path, &device) != ZX_OK) {
status->Log("Error: Block device could not be set up");
return false;
}
bool iterations_set = false;
if (args.iterations > 0) {
iterations_set = true;
}
zx::time end_time = zx::deadline_after(duration);
uint64_t num_tests = 1;
do {
zx::time test_start = zx::clock::get_monotonic();
if (FlashIo(device, bytes_to_test, actual_transfer_size, /*is_write_test=*/true) != ZX_OK) {
status->Log("Error writing to vmo.");
return false;
}
zx::duration test_duration = zx::clock::get_monotonic() - test_start;
status->Log("Test %4ld: Write: %0.3fs, throughput: %0.2f MiB/s", num_tests,
DurationToSecs(test_duration),
bytes_to_test / (DurationToSecs(test_duration) * 1024 * 1024));
test_start = zx::clock::get_monotonic();
if (FlashIo(device, bytes_to_test, actual_transfer_size, /*is_write_test=*/false) != ZX_OK) {
status->Log("Error reading from vmo.");
return false;
}
test_duration = zx::clock::get_monotonic() - test_start;
status->Log("Test %4ld: Read: %0.3fs, throughput: %0.2f MiB/s", num_tests,
DurationToSecs(test_duration),
bytes_to_test / (DurationToSecs(test_duration) * 1024 * 1024));
num_tests++;
// If 'iterations' is set the duration will be infinite
} while (zx::clock::get_monotonic() < end_time &&
(!iterations_set || num_tests <= args.iterations));
return true;
}
void DestroyFlashTestPartitions(StatusLine* status) {
uint32_t count = 0;
// Remove any partitions from previous tests
while (destroy_partition(nullptr, kTestPartGUID.bytes()) == ZX_OK) {
count++;
}
status->Log("Deleted %u partitions", count);
}
} // namespace hwstress