chromium/tools/android/io_benchmark/io_benchmark.cc

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Benchmarks the IO system on a device, by writing and then readind a file
// filled with random data.

#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include <atomic>
#include <random>
#include <string>
#include <thread>
#include <vector>

#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/memory/aligned_memory.h"
#include "base/posix/eintr_wrapper.h"
#include "base/strings/stringprintf.h"
#include "base/test/test_file_util.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "build/build_config.h"

namespace {

constexpr int kPageSize = 1 << 12;

std::mt19937 RandomEngine() {
  std::random_device r;
  std::seed_seq seed({r(), r(), r(), r(), r(), r(), r(), r()});
  return std::mt19937(seed);
}

std::vector<uint8_t> RandomData(size_t size, std::mt19937* engine) {
  std::uniform_int_distribution<uint32_t> dist(0, 255);
  std::vector<uint8_t> data(size);
  for (size_t i = 0; i < size; ++i)
    data[i] = static_cast<uint8_t>(dist(*engine));

  return data;
}

std::string DurationLogMessage(const char* prefix,
                               const base::TimeTicks& tick,
                               const base::TimeTicks& tock,
                               int size) {
  const base::TimeDelta delta = tock - tick;
  const double mb_per_second = size * delta.ToHz() / 1'000'000;
  return base::StringPrintf("%s %d = %.0fus (%.02fMB/s)", prefix, size,
                            delta.InMicrosecondsF(), mb_per_second);
}

// Returns {write_us, read_us}.
std::pair<int64_t, int64_t> WriteReadData(int size,
                                          const std::string& filename,
                                          bool drop_cache) {
  int64_t read_us, write_us;

  // Using random data for two reasons:
  // - Some filesystems do transparent compression.
  // - Some flash controllers do transparent compression
  //
  // To defeat it and get the actual IO throughput and latency, make the data
  // incompressible (which is also the case when writing compressed data).
  auto engine = RandomEngine();
  std::vector<uint8_t> data = RandomData(size, &engine);

  auto path = base::FilePath(filename);
  // Write.
  {
    auto f = base::File(
        path, base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
    CHECK(f.IsValid());

    auto tick = base::TimeTicks::Now();
    int written =
        f.WriteAtCurrentPos(reinterpret_cast<const char*>(&data[0]), size);
    CHECK_EQ(size, written);
    auto tock = base::TimeTicks::Now();

    LOG(INFO) << DurationLogMessage("\tWrite", tick, tock, size);
    write_us = (tock - tick).InMicroseconds();

    CHECK(f.Flush());
  }

  if (drop_cache) {
    CHECK(base::EvictFileFromSystemCache(path));
    // Sleeping, as posix_fadvise() is asynchronous. On the other hand, we
    // don't need to sleep for too long, as all the pages are already clean
    // after the fsync() above, so no writeback is required here.
    base::PlatformThread::Sleep(base::Seconds(1));
  }

  // Read.
  {
    auto f = base::File(path, base::File::FLAG_OPEN | base::File::FLAG_READ);
    CHECK(f.IsValid());

    auto tick = base::TimeTicks::Now();
    int read = f.ReadAtCurrentPos(reinterpret_cast<char*>(&data[0]), size);
    CHECK_EQ(size, read);
    auto tock = base::TimeTicks::Now();

    LOG(INFO) << DurationLogMessage("\tRead", tick, tock, size);
    read_us = (tock - tick).InMicroseconds();
  }

  CHECK(base::DeleteFile(path));
  return {write_us, read_us};
}

// Will constantly do 4k random IO to |filename| until |should_stop| is true.
void RandomlyReadWrite(std::atomic<bool>* should_stop,
                       const std::string& filename,
                       int i) {
  constexpr int kPages = 1 << 10;
  constexpr int kSize = kPages * kPageSize;  // 4MiB (2**10 4k Pages).
  auto path = base::FilePath(filename);
  auto engine = RandomEngine();
  std::vector<uint8_t> data = RandomData(kSize, &engine);

  LOG(INFO) << "Noisy neighbor " << i << ": initial file write";
  {
    auto f = base::File(
        path, base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
    CHECK(f.IsValid());
    int written =
        f.WriteAtCurrentPos(reinterpret_cast<const char*>(&data[0]), kSize);
    CHECK_EQ(kSize, written);
  }

  auto dist = std::uniform_int_distribution<int>(0, kPages - 1);

  LOG(INFO) << "Noisy neighbor " << i << ": Go";
  {
    // Opening the file ourselves as base::File doesn't have flags for O_DIRECT.
    //
    // O_DIRECT is used to make sure that reads and writes are not cached,
    // and come straight from the storage device.
    int fd = open(filename.c_str(), O_RDWR | O_DIRECT | O_SYNC);
    CHECK_NE(fd, -1);
    auto f = base::File(fd);
    // O_DIRECT has special requirements on read/write buffers alignment,
    // which are unspecified in "man open(2)". However a page-aligned buffer
    // works with linux filesystems (512 bytes is usually enough).
    std::unique_ptr<char, base::AlignedFreeDeleter> page_buffer(
        static_cast<char*>(base::AlignedAlloc(kPageSize, kPageSize)));

    while (!should_stop->load()) {
      int random = dist(engine);
      int offset = random * kPageSize;
      int size_read = f.Read(offset, page_buffer.get(), kPageSize);
      CHECK_EQ(size_read, kPageSize);

      std::vector<uint8_t> random_page = RandomData(kPageSize, &engine);
      int written =
          f.Write(offset, reinterpret_cast<char*>(&random_page[0]), kPageSize);
      CHECK_EQ(written, kPageSize);
    }
  }

  LOG(INFO) << "Noisy neighbor " << i << ": Finishing";
  base::DeleteFile(path);
}

}  // namespace

int main(int argc, char** argv) {
  if (argc != 4) {
    LOG(ERROR) << "\nUsage: " << argv[0]
               << "FILENAME DROP_CACHES NUM_NOISY_NEIGBORS\n\n"
               << "Where: FILENAME            path to the test file "
               << "(writable).\n"
               << "       DROP_CACHES         1 to drop the filesystem cache, "
               << "0 otherwise.\n"
               << "       NUM_NOISY_NEIGBORS  number of noisy neighbor threads "
               << "to start.";
    return 1;
  }
  char* filename = argv[1];
  int drop_caches = atoi(argv[2]);
  int neighbors = atoi(argv[3]);

  std::atomic<bool> should_stop;
  should_stop.store(false);
  std::atomic<bool>* should_stop_ptr = &should_stop;

  std::vector<std::thread> noisy_neighbors;
  for (int i = 0; i < neighbors; ++i) {
    std::string path = base::StringPrintf("%s-noisy_neighbor-%d", filename, i);
    noisy_neighbors.emplace_back(
        [=]() { RandomlyReadWrite(should_stop_ptr, path, i); });
    base::PlatformThread::Sleep(base::Seconds(2));
  }

  for (int i = 0; i < 12; i++) {  // Max 1 << 11 pages = 8MiB.
    int size = (1 << i) * kPageSize;
    LOG(INFO) << "Size = " << size;

    auto write_read_us =
        WriteReadData(size, std::string(filename), drop_caches != 0);
    std::string csv_log =
        base::StringPrintf("%d,%d,%d,%d,%d", drop_caches, neighbors, size,
                           static_cast<int>(write_read_us.first),
                           static_cast<int>(write_read_us.second));
    LOG(INFO) << "CSV: " << csv_log;
  }

  should_stop.store(true);
  for (int i = 0; i < neighbors; ++i) {
    noisy_neighbors[i].join();
  }

  return 0;
}