// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { using digest::Digest; using digest::MerkleTree; 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 path; fbl::unique_ptr merkle; size_t size_merkle; fbl::unique_ptr 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> paths; // Order in which to read the blobs from blobfs. fbl::Vector path_index; }; // Helper for streaming operations (such as read, write) which may need to be // repeated multiple times. template 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, fbl::unique_ptr* out) { BEGIN_HELPER; // Generate a Blob of random data fbl::AllocChecker ac; fbl::unique_ptr 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(rand_r(&initial_seed)); } info->size_data = blob_size; // Generate the Merkle Tree info->size_merkle = MerkleTree::GetTreeLength(blob_size); if (info->size_merkle == 0) { info->merkle = nullptr; } else { info->merkle.reset(new (&ac) char[info->size_merkle]); ASSERT_EQ(ac.check(), true); } Digest digest; ASSERT_EQ(MerkleTree::Create(&info->data[0], info->size_data, &info->merkle[0], info->size_merkle, &digest), ZX_OK, "Couldn't create Merkle Tree"); fbl::StringBufferpath)> path; path.AppendPrintf("%s/", fs_path.c_str()); digest.ToString(path.data() + path.size(), path.capacity() - path.size()); strcpy(info->path.data(), path.c_str()); // Sanity-check the merkle tree ASSERT_EQ(MerkleTree::Verify(&info->data[0], info->size_data, &info->merkle[0], info->size_merkle, 0, info->size_data, digest), ZX_OK, "Failed to validate Merkle Tree"); *out = fbl::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(2 * Digest::kLength), 0); return negative_path; } class BlobfsTest { public: BlobfsTest(BlobfsInfo&& info) : info_(fbl::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. fbl::unique_ptr 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; fbl::unique_ptr 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"); state->DeclareStep("open"); state->DeclareStep("read"); state->DeclareStep("unlink"); state->DeclareStep("close"); // 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; fbl::unique_ptr 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 testcases; fbl::Vector blobfs_tests; size_t test_index = 0; for (auto blob_size : blob_sizes) { for (auto blob_count : blob_counts) { BlobfsInfo fs_info; fs_info.blob_count = (p_opts.is_unittest) ? 1 : blob_count; fs_info.blob_size = blob_size; blobfs_tests.push_back(fbl::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 * MerkleTree::kNodeSize + 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(fbl::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 * MerkleTree::kNodeSize + blobfs::kBlobfsInodeSize); testcase.tests.push_back(fbl::move(read_test)); } } testcases.push_back(fbl::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; }); }