| // 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 <errno.h> |
| #include <fcntl.h> |
| #include <fuchsia/hardware/block/c/fidl.h> |
| #include <getopt.h> |
| #include <lib/fdio/cpp/caller.h> |
| #include <lib/fzl/time.h> |
| #include <lib/zx/time.h> |
| #include <string.h> |
| #include <zircon/assert.h> |
| |
| #include <fbl/string_printf.h> |
| #include <fbl/unique_fd.h> |
| #include <fs-test-utils/perftest.h> |
| |
| namespace fs_test_utils { |
| namespace { |
| |
| constexpr char kUsage[] = R"( |
| Usage: |
| |
| %s [mode] [fixture options] [test options] |
| Runs a set of benchmarks and write results. |
| |
| Note: Argument order matters, latest overrides earliest. |
| |
| [Mode] |
| -h,--help Print usage description. This message. |
| |
| -p Performance test mode. Default mode is |
| Unit test. |
| |
| [Fixture Options] |
| --block_device PATH The block device exposed in PATH will be |
| used as block device. |
| |
| --use_ramdisk A ramdisk will be used as block device. |
| |
| --ramdisk_block_size SIZE Size in bytes of the ramdisk's block. |
| |
| --ramdisk_block_count COUNT Number of blocks in the ramdisk. |
| |
| --use_fvm A FVM will be created on the block |
| device. |
| |
| --fvm_slice_size SIZE Size in bytes of the FVM's slices. |
| |
| --fs FS_NAME Will use FS_NAME filesystem to format |
| the block device. |
| (Options: blobfs, minfs) |
| |
| --seed SEED An unsigned integer to initialize |
| pseudo-ramdom number generator. |
| |
| [Test Options] |
| --out PATH In performance test mode, collected |
| results will be written to PATH. |
| |
| --summary_path PATH In performance test mode, result summary |
| statistics will be written to PATH. |
| |
| --print_statistics In performance test mode, result summary |
| statistics will be written to STDOUT. |
| |
| --runs COUNT In performance test mode, limits the |
| number of times to execute each test to |
| COUNT. |
| |
| )"; |
| |
| // Gather some info about the executed tests, and use it to display |
| // gTest lookalike summary. |
| struct TestStats { |
| uint32_t passed = 0; |
| uint32_t failed = 0; |
| uint32_t skipped = 0; |
| uint32_t total = 0; |
| }; |
| |
| uint64_t GetDelta(zx::ticks start) { return (fzl::TicksToNs(zx::ticks::now() - start).to_msecs()); } |
| |
| void PrintTestStart(const fbl::String& name, FILE* out) { |
| if (out) { |
| fprintf(out, "[ RUN ] %s\n", name.c_str()); |
| } |
| } |
| |
| void PrintTestSkipped(const fbl::String& name, zx::ticks start, FILE* out) { |
| if (out) { |
| fprintf(out, "[ SKIPPED ] %s(%lu ms total)\n", name.c_str(), GetDelta(start)); |
| } |
| } |
| |
| void PrintTestFailed(const fbl::String& name, zx::ticks start, FILE* out) { |
| if (out) { |
| fprintf(out, "[ FAILED ] %s(%lu ms total)\n", name.c_str(), GetDelta(start)); |
| } |
| } |
| |
| void PrintTestPassed(const fbl::String& name, zx::ticks start, FILE* out) { |
| if (out) { |
| fprintf(out, "[ PASSED ] %s(%lu ms total)\n", name.c_str(), GetDelta(start)); |
| } |
| } |
| |
| void PrintTestCaseStart(const fbl::String& name, size_t test_count, FILE* out) { |
| if (out) { |
| fprintf(out, "[----------] %lu tests from %s\n", test_count, name.c_str()); |
| } |
| } |
| |
| void PrintTestCaseEnd(const fbl::String& name, size_t test_count, zx::ticks start, FILE* out) { |
| if (out) { |
| fprintf(out, "[----------] %lu tests from %s(%lu ms total)\n\n", test_count, name.c_str(), |
| GetDelta(start)); |
| } |
| } |
| |
| void PrintTestsCasesSummary(size_t test_case_count, const TestStats& stats, zx::ticks start, |
| FILE* out) { |
| if (out) { |
| fprintf(out, "[==========] %d tests from %lu test cases ran. (%lu ms total)\n", stats.total, |
| test_case_count, fzl::TicksToNs(zx::ticks::now() - start).to_msecs()); |
| fprintf(out, "[ PASSED ] %d tests.\n", stats.passed); |
| fprintf(out, "[ FAILED ] %d tests.\n", stats.failed); |
| fprintf(out, "[ SKIPPED ] %d tests.\n", stats.skipped); |
| } |
| } |
| |
| void PrintUsage(char* arg0, FILE* out) { fprintf(out, kUsage, arg0); } |
| |
| bool HasEnoughSpace(const fbl::String& block_device_path, size_t required_space) { |
| if (required_space == 0) { |
| return true; |
| } |
| |
| fbl::unique_fd fd(open(block_device_path.c_str(), O_RDONLY)); |
| fdio_cpp::FdioCaller disk_caller(std::move(fd)); |
| fuchsia_hardware_block_BlockInfo block_info; |
| zx_status_t io_status, status; |
| io_status = |
| fuchsia_hardware_block_BlockGetInfo(disk_caller.borrow_channel(), &status, &block_info); |
| if (io_status != ZX_OK || status != ZX_OK) { |
| LOG_ERROR(status, "Failed to verify block_device size.\n %s\n", block_device_path.c_str()); |
| return false; |
| } |
| |
| if (required_space > block_info.block_count * block_info.block_size) { |
| return false; |
| } |
| return true; |
| } |
| |
| void RunTest(const TestInfo& test, uint32_t sample_count, bool skip, Fixture* fixture, |
| perftest::ResultsSet* result_set, TestStats* stats, FILE* out) { |
| zx::ticks test_start = zx::ticks::now(); |
| PrintTestStart(test.name, out); |
| stats->total++; |
| fbl::String error; |
| if (skip) { |
| stats->skipped++; |
| PrintTestSkipped(test.name, test_start, out); |
| return; |
| } |
| auto test_wrapper = [&test, fixture](perftest::RepeatState* state) { |
| // Reset the seed before running the test. |
| *fixture->mutable_seed() = fixture->options().seed; |
| bool result = test.test_fn(state, fixture); |
| return result; |
| }; |
| |
| constexpr char kTestSuite[] = "fuchsia.zircon"; |
| bool failed = !perftest::RunTest(kTestSuite, test.name.c_str(), test_wrapper, sample_count, |
| result_set, &error); |
| if (failed) { |
| // Log if the error is from the perftest lib. |
| if (!error.empty()) { |
| LOG_ERROR(ZX_ERR_INTERNAL, "%s\n", error.c_str()); |
| } |
| stats->failed++; |
| PrintTestFailed(test.name, test_start, out); |
| return; |
| } |
| PrintTestPassed(test.name, test_start, out); |
| stats->passed++; |
| } |
| |
| // Runs all tests in the given testcase. |
| void RunTestCase(const FixtureOptions& fixture_options, |
| const PerformanceTestOptions& performance_test_options, |
| const TestCaseInfo& test_case, perftest::ResultsSet* result_set, |
| TestStats* global_stats, FILE* out) { |
| Fixture fixture(fixture_options); |
| |
| zx::ticks start = zx::ticks::now(); |
| PrintTestCaseStart(test_case.name, test_case.tests.size(), out); |
| bool skip_tests = (fixture.SetUpTestCase() != ZX_OK); |
| bool setUp = true; |
| for (auto& test : test_case.tests) { |
| // Verify that the disk has enough space to run the test. This is set up by the user |
| // since the actual may change depending on the test input. |
| skip_tests = !HasEnoughSpace(fixture.GetFsBlockDevice(), test.required_disk_space); |
| if (skip_tests) { |
| LOG_ERROR(ZX_ERR_NO_SPACE, "Not enough space on disk to run test.\n"); |
| } else if (setUp) { |
| skip_tests = (fixture.SetUp() != ZX_OK); |
| setUp = false; |
| } |
| uint32_t actual_sample_count = test_case.sample_count; |
| if (actual_sample_count == 0 || performance_test_options.is_unittest) { |
| actual_sample_count = performance_test_options.sample_count; |
| } |
| RunTest(test, actual_sample_count, skip_tests, &fixture, result_set, global_stats, out); |
| if (test_case.teardown) { |
| fixture.TearDown(); |
| setUp = true; |
| } |
| } |
| if (!test_case.teardown) { |
| fixture.TearDown(); |
| } |
| fixture.TearDownTestCase(); |
| PrintTestCaseEnd(test_case.name, test_case.tests.size(), start, out); |
| } |
| |
| } // namespace |
| |
| bool PerformanceTestOptions::IsValid(fbl::String* error) const { |
| if (is_unittest) { |
| return true; |
| } |
| |
| // Something must be printed. |
| if (result_path.empty() && summary_path.empty() && !print_statistics) { |
| *error = |
| "Performance test must produce output." |
| " result_path(out), summary_path or prints_statistics must be set.\n"; |
| return false; |
| } |
| |
| if (result_path == summary_path && !result_path.empty()) { |
| *error = "result_path(out) and summary_path cannot point to the same file.\n"; |
| return false; |
| } |
| |
| if (sample_count == 0) { |
| *error = "sample_count must be a positive integer.\n"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool RunTestCases(const FixtureOptions& fixture_options, |
| const PerformanceTestOptions& performance_test_options, |
| const fbl::Vector<TestCaseInfo>& test_cases, FILE* out) { |
| TestStats stats; |
| perftest::ResultsSet result_set; |
| bool error = false; |
| zx::ticks start = zx::ticks::now(); |
| for (auto& test_case : test_cases) { |
| RunTestCase(fixture_options, performance_test_options, test_case, &result_set, &stats, out); |
| } |
| PrintTestsCasesSummary(test_cases.size(), stats, start, out); |
| if (performance_test_options.print_statistics) { |
| fprintf(out, "\n"); |
| result_set.PrintSummaryStatistics(out); |
| fprintf(out, "\n"); |
| } |
| |
| if (!performance_test_options.summary_path.empty()) { |
| FILE* fp = fopen(performance_test_options.summary_path.c_str(), "w"); |
| if (fp) { |
| result_set.PrintSummaryStatistics(fp); |
| fclose(fp); |
| } else { |
| LOG_ERROR(ZX_ERR_IO, "%s\n", strerror(errno)); |
| error = true; |
| } |
| } |
| |
| if (!performance_test_options.result_path.empty()) { |
| FILE* fp = fopen(performance_test_options.result_path.c_str(), "w"); |
| if (fp) { |
| result_set.WriteJSON(fp); |
| fclose(fp); |
| } else { |
| LOG_ERROR(ZX_ERR_IO, "%s\n", strerror(errno)); |
| } |
| } |
| return stats.failed == 0 && !error; |
| } |
| |
| bool ParseCommandLineArgs(int argc, const char* const* argv, FixtureOptions* fixture_options, |
| PerformanceTestOptions* performance_test_options, FILE* out) { |
| static const struct option opts[] = { |
| {"help", no_argument, nullptr, 'h'}, |
| {"block_device", required_argument, nullptr, 0}, |
| {"use_ramdisk", no_argument, nullptr, 0}, |
| {"ramdisk_block_size", required_argument, nullptr, 0}, |
| {"ramdisk_block_count", required_argument, nullptr, 0}, |
| {"use_fvm", no_argument, nullptr, 0}, |
| {"fvm_slice_size", required_argument, nullptr, 0}, |
| {"fs", required_argument, nullptr, 0}, |
| {"out", required_argument, nullptr, 0}, |
| {"summary_path", required_argument, nullptr, 0}, |
| {"print_statistics", no_argument, nullptr, 0}, |
| {"runs", required_argument, nullptr, 0}, |
| {"seed", required_argument, nullptr, 0}, |
| {"compression", required_argument, nullptr, 'c'}, |
| {0, 0, 0, 0}, |
| }; |
| // Resets the internal state of getopt*, making this function idempotent. |
| optind = 0; |
| bool ramdisk_set = false; |
| bool block_device_set = false; |
| |
| *performance_test_options = PerformanceTestOptions::UnitTest(); |
| // get_opt expects non const pointers. |
| char** argvs = const_cast<char**>(argv); |
| |
| int c = -1; |
| int option_index = -1; |
| while ((c = getopt_long(argc, argvs, "phc:", opts, &option_index)) >= 0) { |
| switch (c) { |
| case 0: |
| switch (option_index) { |
| case 0: |
| PrintUsage(argvs[0], out); |
| return false; |
| case 1: |
| fixture_options->block_device_path = optarg; |
| block_device_set = true; |
| break; |
| case 2: |
| fixture_options->use_ramdisk = true; |
| ramdisk_set = true; |
| break; |
| case 3: |
| fixture_options->ramdisk_block_size = atoi(optarg); |
| break; |
| case 4: |
| fixture_options->ramdisk_block_count = atoi(optarg); |
| break; |
| case 5: |
| fixture_options->use_fvm = true; |
| break; |
| case 6: |
| fixture_options->fvm_slice_size = atoi(optarg); |
| break; |
| case 7: |
| if (strcmp(optarg, "minfs") == 0) { |
| fixture_options->fs_type = DISK_FORMAT_MINFS; |
| } else if (strcmp(optarg, "blobfs") == 0) { |
| fixture_options->fs_type = DISK_FORMAT_BLOBFS; |
| } else { |
| LOG_ERROR(ZX_ERR_INVALID_ARGS, |
| "Unknown disk_format %s. Support values are minfs and blobfs.\n", optarg); |
| return false; |
| } |
| break; |
| case 8: |
| performance_test_options->result_path = optarg; |
| break; |
| case 9: |
| performance_test_options->summary_path = optarg; |
| break; |
| case 10: |
| performance_test_options->print_statistics = true; |
| break; |
| case 11: |
| performance_test_options->sample_count = atoi(optarg); |
| break; |
| case 12: |
| fixture_options->seed = static_cast<unsigned int>(strtoul(optarg, NULL, 0)); |
| break; |
| default: |
| break; |
| } |
| break; |
| case 'p': |
| *performance_test_options = PerformanceTestOptions::PerformanceTest(); |
| break; |
| case 'c': |
| fixture_options->write_compression_algorithm = optarg; |
| break; |
| case 'h': |
| default: |
| PrintUsage(argvs[0], out); |
| return false; |
| } |
| } |
| |
| // Unset ramdisk when not requested and block device is passed. |
| if (block_device_set && !ramdisk_set) { |
| fixture_options->use_ramdisk = false; |
| } |
| |
| bool ok = true; |
| fbl::String error; |
| if (!fixture_options->IsValid(&error)) { |
| LOG_ERROR(ZX_ERR_INVALID_ARGS, "%s\n", error.c_str()); |
| ok = false; |
| } |
| |
| if (!performance_test_options->IsValid(&error)) { |
| LOG_ERROR(ZX_ERR_INVALID_ARGS, "%s\n", error.c_str()); |
| ok = false; |
| } |
| |
| if (!ok) { |
| PrintUsage(argvs[0], out); |
| } |
| |
| return ok; |
| } |
| |
| } // namespace fs_test_utils |