| // Copyright 2017 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 <errno.h> |
| #include <fcntl.h> |
| #include <stdint.h> |
| #include <sys/stat.h> |
| |
| #include <memory> |
| #include <utility> |
| |
| #include <blobfs/format.h> |
| #include <digest/digest.h> |
| #include <digest/merkle-tree.h> |
| #include <fbl/algorithm.h> |
| #include <fbl/function.h> |
| #include <fbl/string.h> |
| #include <fbl/string_buffer.h> |
| #include <fbl/string_printf.h> |
| #include <fbl/unique_fd.h> |
| #include <fs-management/mount.h> |
| #include <fs-test-utils/fixture.h> |
| #include <fs-test-utils/perftest.h> |
| #include <perftest/perftest.h> |
| #include <unittest/unittest.h> |
| |
| namespace { |
| |
| using digest::Digest; |
| using digest::MerkleTreeCreator; |
| using digest::MerkleTreeVerifier; |
| using fs_test_utils::Fixture; |
| using fs_test_utils::FixtureOptions; |
| using fs_test_utils::PerformanceTestOptions; |
| using fs_test_utils::TestCaseInfo; |
| using fs_test_utils::TestInfo; |
| |
| // Supported read orders for this benchmark. |
| enum class ReadOrder { |
| // Blobs are read in the order they were written |
| kSequentialForward, |
| // Blobs are read in the inverse order they were written |
| kSequentialReverse, |
| // Blobs are read in a random order |
| kRandom, |
| }; |
| |
| // An in-memory representation of a blob. |
| struct BlobInfo { |
| // Path to the generated blob. |
| fbl::StringBuffer<fs_test_utils::kPathSize> path; |
| |
| std::unique_ptr<char[]> merkle; |
| size_t size_merkle; |
| |
| std::unique_ptr<char[]> data; |
| size_t size_data; |
| }; |
| |
| // Describes the parameters of the test case. |
| struct BlobfsInfo { |
| // Total number of blobs in blobfs. |
| ssize_t blob_count; |
| |
| // Size in bytes of each blob in BlobFs. |
| size_t blob_size; |
| |
| // Path to every blob in Blobfs |
| fbl::Vector<fbl::StringBuffer<fs_test_utils::kPathSize>> paths; |
| |
| // Order in which to read the blobs from blobfs. |
| fbl::Vector<uint64_t> path_index; |
| }; |
| |
| // Helper for streaming operations (such as read, write) which may need to be |
| // repeated multiple times. |
| template <typename T, typename U> |
| inline int StreamAll(T func, int fd, U* buf, size_t max) { |
| size_t n = 0; |
| while (n != max) { |
| ssize_t d = func(fd, &buf[n], max - n); |
| if (d < 0) { |
| return -1; |
| } |
| n += d; |
| } |
| return 0; |
| } |
| |
| // Get a readable name for a given number of bytes. |
| fbl::String GetNameForSize(size_t size_in_bytes) { |
| static const char* const kUnits[] = {"bytes", "Kbytes", "Mbytes", "Gbytes"}; |
| size_t current_unit = 0; |
| size_t current_size = size_in_bytes; |
| size_t size; |
| while (current_unit < fbl::count_of(kUnits) && |
| current_size >= (1u << (10 * (current_unit + 1)))) { |
| current_size = current_size / (1 << 10 * current_unit); |
| ++current_unit; |
| } |
| |
| size = (size_in_bytes >> (10 * current_unit)); |
| return fbl::StringPrintf("%lu%s", size, kUnits[current_unit]); |
| } |
| |
| fbl::String GetNameForOrder(ReadOrder order) { |
| switch (order) { |
| case ReadOrder::kSequentialForward: |
| return "Sequential"; |
| case ReadOrder::kSequentialReverse: |
| return "Reverse"; |
| case ReadOrder::kRandom: |
| return "Random"; |
| } |
| |
| return ""; |
| } |
| |
| // Creates a an in memory blob. |
| bool MakeBlob(fbl::String fs_path, size_t blob_size, unsigned int* seed, |
| std::unique_ptr<BlobInfo>* out) { |
| BEGIN_HELPER; |
| // Generate a Blob of random data |
| fbl::AllocChecker ac; |
| std::unique_ptr<BlobInfo> info(new (&ac) BlobInfo); |
| EXPECT_EQ(ac.check(), true); |
| info->data.reset(new (&ac) char[blob_size]); |
| EXPECT_EQ(ac.check(), true); |
| // rand_r produces a cyclic sequence, in order to avoid hitting that cap |
| // and generating identical blobs, we avoid consuming an element of the |
| // sequence for each byte. We did hit this issue, which translates into |
| // test failures. |
| unsigned int initial_seed = rand_r(seed); |
| for (size_t i = 0; i < blob_size; i++) { |
| info->data[i] = static_cast<char>(rand_r(&initial_seed)); |
| } |
| info->size_data = blob_size; |
| |
| // Generate the Merkle Tree |
| Digest digest; |
| std::unique_ptr<uint8_t[]> tree; |
| ASSERT_EQ(MerkleTreeCreator::Create(info->data.get(), info->size_data, &tree, &info->size_merkle, |
| &digest), |
| ZX_OK, "Couldn't create Merkle Tree"); |
| info->merkle.reset(reinterpret_cast<char*>(tree.release())); |
| info->path.AppendPrintf("%s/%s", fs_path.c_str(), digest.ToString().c_str()); |
| |
| // Sanity-check the merkle tree |
| ASSERT_EQ(MerkleTreeVerifier::Verify(info->data.get(), info->size_data, 0, info->size_data, |
| info->merkle.get(), info->size_merkle, digest), |
| ZX_OK, "Failed to validate Merkle Tree"); |
| |
| *out = std::move(info); |
| END_HELPER; |
| } |
| |
| // Returns a path within the fs such that it is a valid blobpath. |
| // The generated path is 'root_path/0....0'. |
| fbl::String GetNegativeLookupPath(const fbl::String& fs_path) { |
| fbl::String negative_path = |
| fbl::StringPrintf("%s/%*d", fs_path.c_str(), static_cast<int>(2 * digest::kSha256Length), 0); |
| return negative_path; |
| } |
| |
| class BlobfsTest { |
| public: |
| BlobfsTest(BlobfsInfo&& info) : info_(std::move(info)) {} |
| |
| // Measure how much time each of the operations in the Fs takes, for a known size. |
| // First we add as many blobs as we need to, and then, we proceed to execute each operation. |
| bool ApiTest(perftest::RepeatState* state, Fixture* fixture) { |
| BEGIN_HELPER; |
| |
| // How many blobs do we need to add. |
| std::unique_ptr<BlobInfo> new_blob; |
| |
| for (int64_t curr = 0; curr < info_.blob_count; ++curr) { |
| MakeBlob(fixture->fs_path(), info_.blob_size, fixture->mutable_seed(), &new_blob); |
| fbl::unique_fd fd(open(new_blob->path.c_str(), O_CREAT | O_RDWR)); |
| ASSERT_TRUE(fd, strerror(errno)); |
| ASSERT_EQ(ftruncate(fd.get(), info_.blob_size), 0, strerror(errno)); |
| ASSERT_EQ(StreamAll(write, fd.get(), new_blob->data.get(), new_blob->size_data), 0, |
| strerror(errno)); |
| info_.paths.push_back(new_blob->path); |
| info_.path_index.push_back(curr); |
| new_blob.reset(); |
| } |
| |
| fbl::AllocChecker ac; |
| std::unique_ptr<char[]> buffer(new (&ac) char[info_.blob_size]); |
| ASSERT_TRUE(ac.check()); |
| |
| state->DeclareStep("generate_blob"); |
| state->DeclareStep("create"); |
| state->DeclareStep("truncate"); |
| state->DeclareStep("write"); |
| state->DeclareStep("close_write_fd"); |
| state->DeclareStep("open"); |
| state->DeclareStep("read"); |
| state->DeclareStep("unlink"); |
| state->DeclareStep("close_read_fd"); |
| |
| // At this specific state, measure how much time in average it takes to perform each of the |
| // operations declared. |
| while (state->KeepRunning()) { |
| MakeBlob(fixture->fs_path(), info_.blob_size, fixture->mutable_seed(), &new_blob); |
| state->NextStep(); |
| |
| fbl::unique_fd fd(open(new_blob->path.c_str(), O_CREAT | O_RDWR)); |
| ASSERT_TRUE(fd); |
| state->NextStep(); |
| |
| ASSERT_EQ(ftruncate(fd.get(), info_.blob_size), 0); |
| state->NextStep(); |
| |
| ASSERT_EQ(StreamAll(write, fd.get(), new_blob->data.get(), info_.blob_size), 0, |
| "Failed to write Data"); |
| // Force pending writes to be sent to the underlying device. |
| ASSERT_EQ(fsync(fd.get()), 0); |
| state->NextStep(); |
| |
| ASSERT_EQ(close(fd.release()), 0); |
| state->NextStep(); |
| |
| fd.reset(open(new_blob->path.c_str(), O_RDONLY)); |
| ASSERT_TRUE(fd); |
| state->NextStep(); |
| |
| ASSERT_EQ(StreamAll(read, fd.get(), &buffer[0], info_.blob_size), 0); |
| ASSERT_EQ(memcmp(buffer.get(), new_blob->data.get(), new_blob->size_data), 0); |
| state->NextStep(); |
| |
| unlink(new_blob->path.c_str()); |
| ASSERT_EQ(fsync(fd.get()), 0); |
| |
| state->NextStep(); |
| ASSERT_EQ(close(fd.release()), 0); |
| } |
| END_HELPER; |
| } |
| |
| // After doing the API test, we use the written blobs to measure, lookup, negative-lookup |
| // read |
| bool ReadTest(ReadOrder order, perftest::RepeatState* state, Fixture* fixture) { |
| BEGIN_HELPER; |
| state->DeclareStep("lookup"); |
| state->DeclareStep("read"); |
| state->DeclareStep("negative_lookup"); |
| ASSERT_EQ(info_.path_index.size(), info_.paths.size()); |
| ASSERT_GT(info_.path_index.size(), 0); |
| SortPathsByOrder(order, fixture->mutable_seed()); |
| |
| fbl::AllocChecker ac; |
| std::unique_ptr<char[]> buffer(new (&ac) char[info_.blob_size]); |
| ASSERT_TRUE(ac.check()); |
| |
| uint64_t current = 0; |
| fbl::String negative_path = GetNegativeLookupPath(fixture->fs_path()); |
| |
| while (state->KeepRunning()) { |
| size_t path_index = info_.path_index[current % info_.paths.size()]; |
| fbl::unique_fd fd(open(info_.paths[path_index].c_str(), O_RDONLY)); |
| ASSERT_TRUE(fd); |
| state->NextStep(); |
| ASSERT_EQ(StreamAll(read, fd.get(), &buffer[0], info_.blob_size), 0); |
| state->NextStep(); |
| fbl::unique_fd no_fd(open(negative_path.c_str(), O_RDONLY)); |
| ASSERT_FALSE(no_fd); |
| ++current; |
| } |
| END_HELPER; |
| } |
| |
| private: |
| void SortPathsByOrder(ReadOrder order, unsigned int* seed) { |
| switch (order) { |
| case ReadOrder::kSequentialForward: |
| for (uint64_t curr = 0; curr < info_.paths.size(); ++curr) { |
| info_.path_index[curr] = curr; |
| } |
| break; |
| |
| case ReadOrder::kSequentialReverse: |
| for (uint64_t curr = 0; curr < info_.paths.size(); ++curr) { |
| info_.path_index[curr] = info_.paths.size() - curr - 1; |
| } |
| break; |
| |
| case ReadOrder::kRandom: |
| int64_t swaps = info_.paths.size(); |
| while (swaps > 0) { |
| size_t src = rand_r(seed) % info_.paths.size(); |
| size_t target = rand_r(seed) % info_.paths.size(); |
| info_.path_index[src] = info_.path_index[target]; |
| info_.path_index[target] = info_.path_index[src]; |
| --swaps; |
| } |
| break; |
| } |
| } |
| |
| BlobfsInfo info_; |
| }; |
| |
| bool RunBenchmark(int argc, char** argv) { |
| FixtureOptions f_opts = FixtureOptions::Default(DISK_FORMAT_BLOBFS); |
| PerformanceTestOptions p_opts; |
| // 30 Samples for each operation at each stage. |
| constexpr uint32_t kSampleCount = 100; |
| const size_t blob_sizes[] = { |
| 128, // 128 b |
| 128 * 1024, // 128 Kb |
| 1024 * 1024, // 1 MB |
| }; |
| const size_t blob_counts[] = { |
| 10, |
| 100, |
| 1000, |
| 10000, |
| }; |
| const ReadOrder orders[] = { |
| ReadOrder::kSequentialForward, |
| ReadOrder::kSequentialReverse, |
| ReadOrder::kRandom, |
| }; |
| |
| if (!fs_test_utils::ParseCommandLineArgs(argc, argv, &f_opts, &p_opts)) { |
| return false; |
| } |
| |
| fbl::Vector<TestCaseInfo> testcases; |
| fbl::Vector<BlobfsTest> blobfs_tests; |
| size_t test_index = 0; |
| for (auto blob_size : blob_sizes) { |
| for (auto blob_count : blob_counts) { |
| // Skip the largest blob size/count combination because it |
| // increases the overall running time too much. |
| if (blob_size >= 1024 * 1024 && blob_count >= 10000) { |
| continue; |
| } |
| BlobfsInfo fs_info; |
| fs_info.blob_count = (p_opts.is_unittest) ? 1 : blob_count; |
| fs_info.blob_size = blob_size; |
| blobfs_tests.push_back(std::move(fs_info)); |
| TestCaseInfo testcase; |
| testcase.teardown = false; |
| testcase.sample_count = kSampleCount; |
| |
| fbl::String size = GetNameForSize(blob_size); |
| |
| TestInfo api_test; |
| api_test.name = fbl::StringPrintf("%s/%s/%luBlobs/Api", disk_format_string_[f_opts.fs_type], |
| size.c_str(), blob_count); |
| // There should be enough space for each blob, the merkle tree nodes, and the inodes. |
| api_test.required_disk_space = |
| blob_count * (blob_size + 2 * digest::kDefaultNodeSize + blobfs::kBlobfsInodeSize); |
| api_test.test_fn = [test_index, &blobfs_tests](perftest::RepeatState* state, |
| fs_test_utils::Fixture* fixture) { |
| return blobfs_tests[test_index].ApiTest(state, fixture); |
| }; |
| testcase.tests.push_back(std::move(api_test)); |
| |
| if (blob_count > 0) { |
| for (auto order : orders) { |
| TestInfo read_test; |
| read_test.name = |
| fbl::StringPrintf("%s/%s/%luBlobs/Read%s", disk_format_string_[f_opts.fs_type], |
| size.c_str(), blob_count, GetNameForOrder(order).c_str()); |
| read_test.test_fn = [test_index, order, &blobfs_tests](perftest::RepeatState* state, |
| fs_test_utils::Fixture* fixture) { |
| return blobfs_tests[test_index].ReadTest(order, state, fixture); |
| }; |
| read_test.required_disk_space = |
| blob_count * (blob_size + 2 * digest::kDefaultNodeSize + blobfs::kBlobfsInodeSize); |
| testcase.tests.push_back(std::move(read_test)); |
| } |
| } |
| testcases.push_back(std::move(testcase)); |
| ++test_index; |
| } |
| } |
| |
| return fs_test_utils::RunTestCases(f_opts, p_opts, testcases); |
| } |
| |
| } // namespace |
| |
| int main(int argc, char** argv) { |
| return fs_test_utils::RunWithMemFs([argc, argv]() { return RunBenchmark(argc, argv) ? 0 : -1; }); |
| } |