bench: Add inodeop_bench benchmark

Add new benchmark capable of generating specific inode operations
workloads. Currently, it supports create, delete, move, hardlink and
symlink.

Test: Manual run on cuttlefish and physical device
Bug: 165903680
Signed-off-by: Stefano Duo <stefanoduo@google.com>
Change-Id: Ia47f259b7ccea5fe1665b272c3cbc9ec1bf2eb56
diff --git a/bench/inodeop_bench/Android.bp b/bench/inodeop_bench/Android.bp
new file mode 100644
index 0000000..a01ddd1
--- /dev/null
+++ b/bench/inodeop_bench/Android.bp
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+cc_binary {
+    name: "inodeop_bench",
+    srcs: ["inodeop_bench.cpp"],
+}
diff --git a/bench/inodeop_bench/OWNERS b/bench/inodeop_bench/OWNERS
new file mode 100644
index 0000000..3ced4a1
--- /dev/null
+++ b/bench/inodeop_bench/OWNERS
@@ -0,0 +1,3 @@
+balsini@google.com
+stefanoduo@google.com
+zezeozue@google.com
diff --git a/bench/inodeop_bench/inodeop_bench.cpp b/bench/inodeop_bench/inodeop_bench.cpp
new file mode 100644
index 0000000..8ff544f
--- /dev/null
+++ b/bench/inodeop_bench/inodeop_bench.cpp
@@ -0,0 +1,403 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <chrono>
+#include <functional>
+#include <iostream>
+#include <ratio>
+#include <sstream>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include <dirent.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+static constexpr char VERSION[] = "0";
+
+// Self-contained class for collecting and reporting benchmark metrics
+// (currently only execution time).
+class Collector {
+    using time_point = std::chrono::time_point<std::chrono::steady_clock>;
+    using time_unit = std::chrono::duration<double, std::milli>;
+
+    struct Metric {
+        std::string workload;
+        time_unit exec_time;
+        Metric(const std::string& workload, const time_unit& exec_time)
+            : workload(workload), exec_time(exec_time) {}
+    };
+
+    static constexpr char TIME_UNIT[] = "ms";
+    static constexpr char VERSION[] = "0";
+    std::vector<Metric> metrics;
+    time_point reset_time;
+
+  public:
+    Collector() { reset(); }
+
+    void reset() { reset_time = std::chrono::steady_clock::now(); }
+
+    void collect_metric(const std::string& workload) {
+        auto elapsed = std::chrono::steady_clock::now() - reset_time;
+        metrics.emplace_back(workload, std::chrono::duration_cast<time_unit>(elapsed));
+    }
+
+    void report_metrics() {
+        for (const Metric& metric : metrics)
+            std::cout << VERSION << ";" << metric.workload << ";" << metric.exec_time.count() << ";"
+                      << TIME_UNIT << std::endl;
+    }
+};
+
+struct Command {
+    static constexpr char CREATE[] = "create";
+    static constexpr char DELETE[] = "delete";
+    static constexpr char MOVE[] = "move";
+    static constexpr char HARDLINK[] = "hardlink";
+    static constexpr char SYMLINK[] = "symlink";
+    static constexpr char READDIR[] = "readdir";
+    std::string workload;
+    std::string from_dir;
+    std::string from_basename;
+    std::string to_dir;
+    std::string to_basename;
+    bool drop_state;
+    int n_file;
+
+    Command() { reset(); }
+
+    std::string to_string() const {
+        std::stringstream string_repr;
+        string_repr << "Command {\n";
+        string_repr << "\t.workload = " << workload << ",\n";
+        string_repr << "\t.from_dir = " << from_dir << ",\n";
+        string_repr << "\t.from_basename = " << from_basename << ",\n";
+        string_repr << "\t.to_dir = " << to_dir << ",\n";
+        string_repr << "\t.to_basename = " << to_basename << ",\n";
+        string_repr << "\t.drop_state = " << drop_state << ",\n";
+        string_repr << "\t.n_file = " << n_file << "\n";
+        string_repr << "}\n";
+        return string_repr.str();
+    }
+
+    void reset() {
+        workload = "";
+        from_dir = to_dir = "./";
+        from_basename = "from_file";
+        to_basename = "to_file";
+        drop_state = true;
+        n_file = 0;
+    }
+};
+
+void print_version() {
+    std::cout << VERSION << std::endl;
+}
+
+void print_commands(const std::vector<Command>& commands) {
+    for (const Command& command : commands) std::cout << command.to_string();
+}
+
+void usage(std::ostream& ostr, const std::string& program_name) {
+    Command command;
+
+    ostr << "Usage: " << program_name << " [global_options] {[workload_options] -w WORKLOAD_T}\n";
+    ostr << "WORKLOAD_T = {" << Command::CREATE << ", " << Command::DELETE << ", " << Command::MOVE
+         << ", " << Command::HARDLINK << ", " << Command::SYMLINK << "}\n";
+    ostr << "Global options\n";
+    ostr << "\t-v: Print version.\n";
+    ostr << "\t-p: Print parsed workloads and exit.\n";
+    ostr << "Workload options\n";
+    ostr << "\t-d DIR\t\t: Work directory for " << Command::CREATE << "/" << Command::DELETE
+         << " (default '" << command.from_dir << "').\n";
+    ostr << "\t-f FROM-DIR\t: Source directory for " << Command::MOVE << "/" << Command::SYMLINK
+         << "/" << Command::HARDLINK << " (default '" << command.from_dir << "').\n";
+    ostr << "\t-t TO-DIR\t: Destination directory for " << Command::MOVE << "/" << Command::SYMLINK
+         << "/" << Command::HARDLINK << " (default '" << command.to_dir << "').\n";
+    ostr << "\t-n N_FILES\t: Number of files to create/delete etc. (default " << command.n_file
+         << ").\n";
+    ostr << "\t-s\t\t: Do not drop state (caches) before running the workload (default "
+         << !command.drop_state << ").\n";
+    ostr << "NOTE: -w WORKLOAD_T defines a new command and must come after its workload_options."
+         << std::endl;
+}
+
+void drop_state() {
+    // Drop inode/dentry/page caches.
+    std::system("sync; echo 3 > /proc/sys/vm/drop_caches");
+}
+
+static constexpr int OPEN_DIR_FLAGS = O_RDONLY | O_DIRECTORY | O_PATH | O_CLOEXEC;
+
+bool create_files(const std::string& dir, int n_file, const std::string& basename) {
+    int dir_fd = open(dir.c_str(), OPEN_DIR_FLAGS);
+    if (dir_fd == -1) {
+        int error = errno;
+        std::cerr << "Failed to open work directory '" << dir << "', error '" << strerror(error)
+                  << "'." << std::endl;
+        return false;
+    }
+
+    for (int i = 0; i < n_file; i++) {
+        std::string filename = basename + std::to_string(i);
+        int fd = openat(dir_fd, filename.c_str(), O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0777);
+        close(fd);
+    }
+
+    close(dir_fd);
+    return true;
+}
+
+bool delete_files(const std::string& dir, int n_file, const std::string& basename) {
+    int dir_fd = open(dir.c_str(), OPEN_DIR_FLAGS);
+    if (dir_fd == -1) {
+        int error = errno;
+        std::cerr << "Failed to open work directory '" << dir << "', error '" << strerror(error)
+                  << "'." << std::endl;
+        return false;
+    }
+
+    for (int i = 0; i < n_file; i++) {
+        std::string filename = basename + std::to_string(i);
+        unlinkat(dir_fd, filename.c_str(), 0);
+    }
+
+    close(dir_fd);
+    return true;
+}
+
+bool move_files(const std::string& from_dir, const std::string& to_dir, int n_file,
+                const std::string& from_basename, const std::string& to_basename) {
+    int from_dir_fd = open(from_dir.c_str(), OPEN_DIR_FLAGS);
+    if (from_dir_fd == -1) {
+        int error = errno;
+        std::cerr << "Failed to open source directory '" << from_dir << "', error '"
+                  << strerror(error) << "'." << std::endl;
+        return false;
+    }
+    int to_dir_fd = open(to_dir.c_str(), OPEN_DIR_FLAGS);
+    if (to_dir_fd == -1) {
+        int error = errno;
+        std::cerr << "Failed to open destination directory '" << to_dir << "', error '"
+                  << strerror(error) << "'." << std::endl;
+        close(from_dir_fd);
+        return false;
+    }
+
+    for (int i = 0; i < n_file; i++) {
+        std::string from_filename = from_basename + std::to_string(i);
+        std::string to_filename = to_basename + std::to_string(i);
+        renameat(from_dir_fd, from_filename.c_str(), to_dir_fd, to_filename.c_str());
+    }
+
+    close(from_dir_fd);
+    close(from_dir_fd);
+    return true;
+}
+
+bool hardlink_files(const std::string& from_dir, const std::string& to_dir, int n_file,
+                    const std::string& from_basename, const std::string& to_basename) {
+    int from_dir_fd = open(from_dir.c_str(), OPEN_DIR_FLAGS);
+    if (from_dir_fd == -1) {
+        int error = errno;
+        std::cerr << "Failed to open source directory '" << from_dir << "', error '"
+                  << strerror(error) << "'." << std::endl;
+        return false;
+    }
+    int to_dir_fd = open(to_dir.c_str(), OPEN_DIR_FLAGS);
+    if (to_dir_fd == -1) {
+        int error = errno;
+        std::cerr << "Failed to open destination directory '" << to_dir << "', error '"
+                  << strerror(error) << "'." << std::endl;
+        close(from_dir_fd);
+        return false;
+    }
+
+    for (int i = 0; i < n_file; i++) {
+        std::string from_filename = from_basename + std::to_string(i);
+        std::string to_filename = to_basename + std::to_string(i);
+        linkat(from_dir_fd, from_filename.c_str(), to_dir_fd, to_filename.c_str(), 0);
+    }
+
+    close(from_dir_fd);
+    close(to_dir_fd);
+    return true;
+}
+
+bool symlink_files(std::string from_dir, const std::string& to_dir, int n_file,
+                   const std::string& from_basename, const std::string& to_basename) {
+    if (from_dir.back() != '/') from_dir.push_back('/');
+    int to_dir_fd = open(to_dir.c_str(), OPEN_DIR_FLAGS);
+    if (to_dir_fd == -1) {
+        int error = errno;
+        std::cerr << "Failed to open destination directory '" << to_dir << "', error '"
+                  << strerror(error) << "'." << std::endl;
+        return false;
+    }
+
+    for (int i = 0; i < n_file; i++) {
+        std::string from_filepath = from_dir + from_basename + std::to_string(i);
+        std::string to_filename = to_basename + std::to_string(i);
+        symlinkat(from_filepath.c_str(), to_dir_fd, to_filename.c_str());
+    }
+
+    close(to_dir_fd);
+    return true;
+}
+
+bool exhaustive_readdir(const std::string& from_dir) {
+    DIR* dir = opendir(from_dir.c_str());
+    if (dir == nullptr) {
+        int error = errno;
+        std::cerr << "Failed to open working directory '" << from_dir << "', error '"
+                  << strerror(error) << "'." << std::endl;
+        return false;
+    }
+
+    while (readdir(dir) != nullptr)
+        ;
+
+    closedir(dir);
+    return true;
+}
+
+void create_workload(Collector* collector, const Command& command) {
+    if (command.drop_state) drop_state();
+    collector->reset();
+    if (create_files(command.from_dir, command.n_file, command.from_basename))
+        collector->collect_metric(command.workload);
+
+    delete_files(command.from_dir, command.n_file, command.from_basename);
+}
+
+void delete_workload(Collector* collector, const Command& command) {
+    if (!create_files(command.from_dir, command.n_file, command.from_basename)) return;
+
+    if (command.drop_state) drop_state();
+    collector->reset();
+    if (delete_files(command.from_dir, command.n_file, command.from_basename))
+        collector->collect_metric(command.workload);
+}
+
+void move_workload(Collector* collector, const Command& command) {
+    if (!create_files(command.from_dir, command.n_file, command.from_basename)) return;
+
+    if (command.drop_state) drop_state();
+    collector->reset();
+    if (move_files(command.from_dir, command.to_dir, command.n_file, command.from_basename,
+                   command.to_basename))
+        collector->collect_metric(command.workload);
+
+    delete_files(command.to_dir, command.n_file, command.to_basename);
+}
+
+void hardlink_workload(Collector* collector, const Command& command) {
+    if (!create_files(command.from_dir, command.n_file, command.from_basename)) return;
+
+    if (command.drop_state) drop_state();
+    collector->reset();
+    if (hardlink_files(command.from_dir, command.to_dir, command.n_file, command.from_basename,
+                       command.to_basename))
+        collector->collect_metric(command.workload);
+
+    delete_files(command.from_dir, command.n_file, command.from_basename);
+    delete_files(command.to_dir, command.n_file, command.to_basename);
+}
+
+void symlink_workload(Collector* collector, const Command& command) {
+    if (!create_files(command.from_dir, command.n_file, command.from_basename)) return;
+
+    if (command.drop_state) drop_state();
+    collector->reset();
+    if (symlink_files(command.from_dir, command.to_dir, command.n_file, command.from_basename,
+                      command.to_basename))
+        collector->collect_metric(command.workload);
+
+    delete_files(command.to_dir, command.n_file, command.to_basename);
+    delete_files(command.from_dir, command.n_file, command.from_basename);
+}
+
+void readdir_workload(Collector* collector, const Command& command) {
+    if (!create_files(command.from_dir, command.n_file, command.from_basename)) return;
+
+    if (command.drop_state) drop_state();
+    collector->reset();
+    if (exhaustive_readdir(command.from_dir)) collector->collect_metric(command.workload);
+
+    delete_files(command.from_dir, command.n_file, command.from_basename);
+}
+
+using workload_executor_t = std::function<void(Collector*, const Command&)>;
+
+std::unordered_map<std::string, workload_executor_t> executors = {
+        {Command::CREATE, create_workload},   {Command::DELETE, delete_workload},
+        {Command::MOVE, move_workload},       {Command::HARDLINK, hardlink_workload},
+        {Command::SYMLINK, symlink_workload}, {Command::READDIR, readdir_workload}};
+
+int main(int argc, char** argv) {
+    std::vector<Command> commands;
+    Command command;
+    int opt;
+
+    while ((opt = getopt(argc, argv, "hvpsw:d:f:t:n:")) != -1) {
+        switch (opt) {
+            case 'h':
+                usage(std::cout, argv[0]);
+                return EXIT_SUCCESS;
+            case 'v':
+                print_version();
+                return EXIT_SUCCESS;
+            case 'p':
+                print_commands(commands);
+                return EXIT_SUCCESS;
+            case 's':
+                command.drop_state = false;
+                break;
+            case 'w':
+                command.workload = optarg;
+                commands.push_back(command);
+                command.reset();
+                break;
+            case 'd':
+            case 'f':
+                command.from_dir = optarg;
+                break;
+            case 't':
+                command.to_dir = optarg;
+                break;
+            case 'n':
+                command.n_file = std::stoi(optarg);
+                break;
+            default:
+                usage(std::cerr, argv[0]);
+                return EXIT_FAILURE;
+        }
+    }
+
+    Collector collector;
+    for (const Command& command : commands) {
+        auto executor = executors.find(command.workload);
+        if (executor == executors.end()) continue;
+        executor->second(&collector, command);
+    }
+    collector.report_metrics();
+
+    return EXIT_SUCCESS;
+}