blob: 1abaa6b1359d7503d23931c1e04355110b65921b [file] [log] [blame]
// Copyright 2022 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.
use crate::{Benchmark, CacheClearableFilesystem, Filesystem, OperationDuration, OperationTimer};
use async_trait::async_trait;
use std::collections::VecDeque;
use std::ffi::{CStr, CString};
use std::mem::MaybeUninit;
use std::ops::Range;
use std::os::fd::RawFd;
use std::os::unix::ffi::OsStringExt;
use std::path::{Path, PathBuf};
/// Describes the structure of a directory tree to be benchmarked.
#[derive(Copy, Clone)]
pub struct DirectoryTreeStructure {
/// The number of files that will be created within each directory.
pub files_per_directory: u64,
/// The number of subdirectories that will be created within each directory. No subdirectories
/// are created once |max_depth| is reached.
pub directories_per_directory: u64,
/// Specifies how many levels of directories to create.
pub max_depth: u32,
}
impl DirectoryTreeStructure {
/// Returns the maximum size of queue required to do a breadth first traversal of a directory
/// tree with this structure.
fn max_pending(&self) -> u64 {
self.directories_per_directory.pow(self.max_depth)
}
/// Creates a directory tree as described by this object starting from |root|.
fn create_directory_tree(&self, root: PathBuf) {
struct DirInfo {
path: PathBuf,
depth: u32,
}
let mut pending = VecDeque::new();
pending.push_back(DirInfo { path: root, depth: 0 });
while let Some(dir) = pending.pop_front() {
for i in 0..self.files_per_directory {
let path = dir.path.join(file_name(i));
std::fs::File::create(path).unwrap();
}
if self.max_depth == dir.depth {
continue;
}
for i in 0..self.directories_per_directory {
let path = dir.path.join(dir_name(i));
std::fs::create_dir(&path).unwrap();
pending.push_back(DirInfo { path, depth: dir.depth + 1 });
}
}
}
/// Generates a list of paths that would be created by `create_directory_tree`.
fn enumerate_paths(&self) -> Vec<PathBuf> {
let mut paths = Vec::new();
self.enumerate_paths_impl(Path::new(""), &mut paths);
paths
}
fn enumerate_paths_impl(&self, base: &Path, paths: &mut Vec<PathBuf>) {
for i in 0..self.files_per_directory {
paths.push(base.join(file_name(i)));
}
if self.max_depth == 0 {
return;
}
for i in 0..self.directories_per_directory {
let path = base.join(dir_name(i));
paths.push(path.clone());
Self { max_depth: self.max_depth - 1, ..*self }.enumerate_paths_impl(&path, paths);
}
}
}
/// A benchmark that measures how long it takes to walk a directory that should not already be
/// cached in memory.
#[derive(Clone)]
pub struct WalkDirectoryTreeCold {
dts: DirectoryTreeStructure,
iterations: u64,
}
impl WalkDirectoryTreeCold {
pub fn new(dts: DirectoryTreeStructure, iterations: u64) -> Self {
Self { dts, iterations }
}
}
#[async_trait]
impl<T: CacheClearableFilesystem> Benchmark<T> for WalkDirectoryTreeCold {
async fn run(&self, fs: &mut T) -> Vec<OperationDuration> {
storage_trace::duration!(
c"benchmark",
c"WalkDirectoryTreeCold",
"files_per_directory" => self.dts.files_per_directory,
"directories_per_directory" => self.dts.directories_per_directory,
"max_depth" => self.dts.max_depth
);
let root = fs.benchmark_dir().to_path_buf();
self.dts.create_directory_tree(root.clone());
let max_pending = self.dts.max_pending() as usize;
let mut durations = Vec::new();
for i in 0..self.iterations {
fs.clear_cache().await;
storage_trace::duration!(c"benchmark", c"WalkDirectoryTree", "iteration" => i);
let timer = OperationTimer::start();
walk_directory_tree(root.clone(), max_pending);
durations.push(timer.stop());
}
durations
}
fn name(&self) -> String {
format!(
"WalkDirectoryTreeCold/Files{}/Dirs{}/Depth{}",
self.dts.files_per_directory, self.dts.directories_per_directory, self.dts.max_depth
)
}
}
/// A benchmark that measures how long it takes to walk a directory that was recently accessed and
/// may be cached in memory.
#[derive(Clone)]
pub struct WalkDirectoryTreeWarm {
dts: DirectoryTreeStructure,
iterations: u64,
}
impl WalkDirectoryTreeWarm {
pub fn new(dts: DirectoryTreeStructure, iterations: u64) -> Self {
Self { dts, iterations }
}
}
#[async_trait]
impl<T: Filesystem> Benchmark<T> for WalkDirectoryTreeWarm {
async fn run(&self, fs: &mut T) -> Vec<OperationDuration> {
storage_trace::duration!(
c"benchmark",
c"WalkDirectoryTreeWarm",
"files_per_directory" => self.dts.files_per_directory,
"directories_per_directory" => self.dts.directories_per_directory,
"max_depth" => self.dts.max_depth
);
let root = fs.benchmark_dir().to_path_buf();
self.dts.create_directory_tree(root.clone());
let max_pending = self.dts.max_pending() as usize;
let mut durations = Vec::new();
for i in 0..self.iterations {
storage_trace::duration!(c"benchmark", c"WalkDirectoryTree", "iteration" => i);
let timer = OperationTimer::start();
walk_directory_tree(root.clone(), max_pending);
durations.push(timer.stop());
}
durations
}
fn name(&self) -> String {
format!(
"WalkDirectoryTreeWarm/Files{}/Dirs{}/Depth{}",
self.dts.files_per_directory, self.dts.directories_per_directory, self.dts.max_depth
)
}
}
/// Performs a breadth first traversal of the directory tree starting at |root|. As an optimization,
/// |max_pending| can be supplied to pre-allocate the queue needed for the breadth first traversal.
fn walk_directory_tree(root: PathBuf, max_pending: usize) {
let mut pending = VecDeque::new();
pending.reserve(max_pending);
pending.push_back(root);
while let Some(dir) = pending.pop_front() {
storage_trace::duration!(c"benchmark", c"read_dir");
for entry in std::fs::read_dir(&dir).unwrap() {
let entry = entry.unwrap();
if entry.file_type().unwrap().is_dir() {
pending.push_back(entry.path());
}
}
}
}
/// A benchmark that measures how long it takes to call stat on a path to a file. A distinct file is
/// used for each iteration.
#[derive(Clone)]
pub struct StatPath {
file_count: u64,
}
impl StatPath {
pub fn new() -> Self {
Self { file_count: 100 }
}
}
#[async_trait]
impl<T: Filesystem> Benchmark<T> for StatPath {
async fn run(&self, fs: &mut T) -> Vec<OperationDuration> {
storage_trace::duration!(c"benchmark", c"StatPath");
let root = fs.benchmark_dir().to_path_buf();
for i in 0..self.file_count {
std::fs::File::create(root.join(file_name(i))).unwrap();
}
let root_fd =
open_path(&path_buf_to_c_string(root), libc::O_DIRECTORY | libc::O_RDONLY).unwrap();
let mut durations = Vec::with_capacity(self.file_count as usize);
for i in 0..self.file_count {
let path = path_buf_to_c_string(file_name(i));
storage_trace::duration!(c"benchmark", c"stat", "file" => i);
let timer = OperationTimer::start();
stat_path_at(&root_fd, &path).unwrap();
durations.push(timer.stop());
}
durations
}
fn name(&self) -> String {
"StatPath".to_string()
}
}
/// A benchmark that measures how long it takes to open a file. A distinct file is used for each
/// iteration.
#[derive(Clone)]
pub struct OpenFile {
file_count: u64,
}
impl OpenFile {
pub fn new() -> Self {
Self { file_count: 100 }
}
}
#[async_trait]
impl<T: Filesystem> Benchmark<T> for OpenFile {
async fn run(&self, fs: &mut T) -> Vec<OperationDuration> {
storage_trace::duration!(c"benchmark", c"OpenFile");
let root = fs.benchmark_dir().to_path_buf();
for i in 0..self.file_count {
std::fs::File::create(root.join(file_name(i))).unwrap();
}
let root_fd =
open_path(&path_buf_to_c_string(root), libc::O_DIRECTORY | libc::O_RDONLY).unwrap();
let mut durations = Vec::with_capacity(self.file_count as usize);
for i in 0..self.file_count {
let path = path_buf_to_c_string(file_name(i));
// Pull the file outside of the trace so it doesn't capture the close call.
let _file = {
storage_trace::duration!(c"benchmark", c"open", "file" => i);
let timer = OperationTimer::start();
let file = open_path_at(&root_fd, &path, libc::O_RDWR);
durations.push(timer.stop());
file
};
}
durations
}
fn name(&self) -> String {
"OpenFile".to_string()
}
}
/// A benchmark that measures how long it takes to open a file from a path that contains multiple
/// directories. A distinct path and file is used for each iteration.
#[derive(Clone)]
pub struct OpenDeeplyNestedFile {
file_count: u64,
depth: u64,
}
impl OpenDeeplyNestedFile {
pub fn new() -> Self {
Self { file_count: 100, depth: 5 }
}
fn dir_path(&self, n: u64) -> PathBuf {
let mut path = PathBuf::new();
for i in 0..self.depth {
path = path.join(format!("dir-{:03}-{:03}", n, i));
}
path
}
}
#[async_trait]
impl<T: Filesystem> Benchmark<T> for OpenDeeplyNestedFile {
async fn run(&self, fs: &mut T) -> Vec<OperationDuration> {
storage_trace::duration!(c"benchmark", c"OpenDeeplyNestedFile");
let root = fs.benchmark_dir().to_path_buf();
for i in 0..self.file_count {
let dir_path = root.join(self.dir_path(i));
std::fs::create_dir_all(&dir_path).unwrap();
std::fs::File::create(dir_path.join(file_name(i))).unwrap();
}
let root_fd =
open_path(&path_buf_to_c_string(root), libc::O_DIRECTORY | libc::O_RDONLY).unwrap();
let mut durations = Vec::with_capacity(self.file_count as usize);
for i in 0..self.file_count {
let path = path_buf_to_c_string(self.dir_path(i).join(file_name(i)));
// Pull the file outside of the trace so it doesn't capture the close call.
let _file = {
storage_trace::duration!(c"benchmark", c"open", "file" => i);
let timer = OperationTimer::start();
let file = open_path_at(&root_fd, &path, libc::O_RDWR);
durations.push(timer.stop());
file
};
}
durations
}
fn name(&self) -> String {
"OpenDeeplyNestedFile".to_string()
}
}
/// A benchmark that mimics the filesystem usage pattern of `git status`.
#[derive(Clone)]
pub struct GitStatus {
dts: DirectoryTreeStructure,
iterations: u64,
/// The number of threads to use when stat'ing all of the files.
stat_threads: usize,
}
impl GitStatus {
pub fn new() -> Self {
// This will create 254 directories and 1020 files. A depth of 13 would create 16382
// directories and 65532 files which is close to the size of fuchsia.git.
let dts = DirectoryTreeStructure {
files_per_directory: 4,
directories_per_directory: 2,
max_depth: 7,
};
// Git uses many threads when stat'ing large repositories but using multiple threads in the
// benchmarks significantly increases the performance variation between runs.
Self { dts, iterations: 5, stat_threads: 1 }
}
/// Stat all of the paths in `paths`. This mimics git checking to see if any of the files in its
/// index have been modified.
fn stat_paths(&self, root: &OpenFd, paths: &Vec<CString>) {
storage_trace::duration!(c"benchmark", c"GitStatus::stat_paths");
std::thread::scope(|scope| {
for thread in 0..self.stat_threads {
scope.spawn(move || {
let paths = &paths;
for path in batch_range(paths.len(), self.stat_threads, thread) {
storage_trace::duration!(c"benchmark", c"stat");
stat_path_at(root, &paths[path]).unwrap();
}
});
}
});
}
/// Performs a recursive depth first traversal of the directory tree.
fn walk_repo(&self, dir: &Path) {
storage_trace::duration!(c"benchmark", c"GitStatus::walk_repo");
for entry in std::fs::read_dir(&dir).unwrap() {
let entry = entry.unwrap();
if entry.file_type().unwrap().is_dir() {
self.walk_repo(&entry.path());
}
}
}
}
#[async_trait]
impl<T: Filesystem> Benchmark<T> for GitStatus {
async fn run(&self, fs: &mut T) -> Vec<OperationDuration> {
let root = &fs.benchmark_dir().to_path_buf();
self.dts.create_directory_tree(root.clone());
let paths = self.dts.enumerate_paths().into_iter().map(path_buf_to_c_string).collect();
let root_fd =
open_path(&path_buf_to_c_string(root.clone()), libc::O_DIRECTORY | libc::O_RDONLY)
.unwrap();
let mut durations = Vec::new();
for i in 0..self.iterations {
storage_trace::duration!(c"benchmark", c"GitStatus", "iteration" => i);
let timer = OperationTimer::start();
self.stat_paths(&root_fd, &paths);
self.walk_repo(&root);
durations.push(timer.stop());
}
durations
}
fn name(&self) -> String {
"GitStatus".to_owned()
}
}
pub fn file_name(n: u64) -> PathBuf {
format!("file-{:03}.txt", n).into()
}
pub fn dir_name(n: u64) -> PathBuf {
format!("dir-{:03}", n).into()
}
pub fn path_buf_to_c_string(path: PathBuf) -> CString {
CString::new(path.into_os_string().into_vec()).unwrap()
}
pub fn stat_path_at(dir: &OpenFd, path: &CStr) -> Result<libc::stat, std::io::Error> {
unsafe {
let mut stat = MaybeUninit::uninit();
// `libc::fstatat` is directly used instead of `std::fs::metadata` because
// `std::fs::metadata` uses `statx` on Linux.
let result =
libc::fstatat(dir.0, path.as_ptr(), stat.as_mut_ptr(), libc::AT_SYMLINK_NOFOLLOW);
if result == 0 {
Ok(stat.assume_init())
} else {
Err(std::io::Error::last_os_error())
}
}
}
/// Splits a collection of items into batches and returns the range of items that `batch_num`
/// contains.
fn batch_range(item_count: usize, batch_count: usize, batch_num: usize) -> Range<usize> {
let items_per_batch = (item_count + (batch_count - 1)) / batch_count;
let start = items_per_batch * batch_num;
let end = std::cmp::min(item_count, start + items_per_batch);
start..end
}
pub struct OpenFd(RawFd);
impl Drop for OpenFd {
fn drop(&mut self) {
unsafe { libc::close(self.0) };
}
}
pub fn open_path(path: &CStr, flags: libc::c_int) -> Result<OpenFd, std::io::Error> {
let result = unsafe { libc::open(path.as_ptr(), flags) };
if result >= 0 {
Ok(OpenFd(result))
} else {
Err(std::io::Error::last_os_error())
}
}
pub fn open_path_at(
dir: &OpenFd,
path: &CStr,
flags: libc::c_int,
) -> Result<OpenFd, std::io::Error> {
let result = unsafe { libc::openat(dir.0, path.as_ptr(), flags) };
if result >= 0 {
Ok(OpenFd(result))
} else {
Err(std::io::Error::last_os_error())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::TestFilesystem;
const ITERATION_COUNT: u64 = 3;
#[derive(PartialEq, Debug)]
struct DirectoryTree {
files: u64,
directories: Vec<DirectoryTree>,
}
fn read_in_directory_tree(root: PathBuf) -> DirectoryTree {
let mut tree = DirectoryTree { files: 0, directories: vec![] };
for entry in std::fs::read_dir(root).unwrap() {
let entry = entry.unwrap();
if entry.file_type().unwrap().is_dir() {
tree.directories.push(read_in_directory_tree(entry.path()));
} else {
tree.files += 1;
}
}
tree
}
#[fuchsia::test]
async fn create_directory_tree_with_depth_of_zero_test() {
let test_fs = Box::new(TestFilesystem::new());
let dts = DirectoryTreeStructure {
files_per_directory: 3,
directories_per_directory: 2,
max_depth: 0,
};
let root = test_fs.benchmark_dir().to_owned();
dts.create_directory_tree(root.clone());
let directory_tree = read_in_directory_tree(root);
assert_eq!(directory_tree, DirectoryTree { files: 3, directories: vec![] });
test_fs.shutdown().await;
}
#[fuchsia::test]
async fn create_directory_tree_with_depth_of_two_test() {
let test_fs = Box::new(TestFilesystem::new());
let dts = DirectoryTreeStructure {
files_per_directory: 4,
directories_per_directory: 2,
max_depth: 2,
};
let root = test_fs.benchmark_dir().to_owned();
dts.create_directory_tree(root.clone());
let directory_tree = read_in_directory_tree(root);
assert_eq!(
directory_tree,
DirectoryTree {
files: 4,
directories: vec![
DirectoryTree {
files: 4,
directories: vec![
DirectoryTree { files: 4, directories: vec![] },
DirectoryTree { files: 4, directories: vec![] }
]
},
DirectoryTree {
files: 4,
directories: vec![
DirectoryTree { files: 4, directories: vec![] },
DirectoryTree { files: 4, directories: vec![] }
]
}
]
}
);
test_fs.shutdown().await;
}
#[fuchsia::test]
fn enumerate_paths_test() {
let dts = DirectoryTreeStructure {
files_per_directory: 2,
directories_per_directory: 2,
max_depth: 0,
};
let paths = dts.enumerate_paths();
assert_eq!(paths, vec![PathBuf::from("file-000.txt"), PathBuf::from("file-001.txt")],);
let dts = DirectoryTreeStructure {
files_per_directory: 2,
directories_per_directory: 2,
max_depth: 1,
};
let paths = dts.enumerate_paths();
assert_eq!(
paths,
vec![
PathBuf::from("file-000.txt"),
PathBuf::from("file-001.txt"),
PathBuf::from("dir-000"),
PathBuf::from("dir-000/file-000.txt"),
PathBuf::from("dir-000/file-001.txt"),
PathBuf::from("dir-001"),
PathBuf::from("dir-001/file-000.txt"),
PathBuf::from("dir-001/file-001.txt"),
],
);
}
#[fuchsia::test]
fn batch_range_test() {
assert_eq!(batch_range(10, 1, 0), 0..10);
assert_eq!(batch_range(10, 3, 0), 0..4);
assert_eq!(batch_range(10, 3, 1), 4..8);
assert_eq!(batch_range(10, 3, 2), 8..10);
assert_eq!(batch_range(10, 4, 3), 9..10);
assert_eq!(batch_range(12, 4, 3), 9..12);
}
#[fuchsia::test]
async fn walk_directory_tree_cold_test() {
let mut test_fs = Box::new(TestFilesystem::new());
let dts = DirectoryTreeStructure {
files_per_directory: 2,
directories_per_directory: 2,
max_depth: 2,
};
let results = WalkDirectoryTreeCold::new(dts, ITERATION_COUNT).run(test_fs.as_mut()).await;
assert_eq!(results.len(), ITERATION_COUNT as usize);
assert_eq!(test_fs.clear_cache_count().await, ITERATION_COUNT);
test_fs.shutdown().await;
}
#[fuchsia::test]
async fn walk_directory_tree_warm_test() {
let mut test_fs = Box::new(TestFilesystem::new());
let dts = DirectoryTreeStructure {
files_per_directory: 2,
directories_per_directory: 2,
max_depth: 2,
};
let results = WalkDirectoryTreeWarm::new(dts, ITERATION_COUNT).run(test_fs.as_mut()).await;
assert_eq!(results.len(), ITERATION_COUNT as usize);
assert_eq!(test_fs.clear_cache_count().await, 0);
test_fs.shutdown().await;
}
#[fuchsia::test]
async fn stat_path_test() {
let mut test_fs = Box::new(TestFilesystem::new());
let benchmark = StatPath { file_count: 5 };
let results = benchmark.run(test_fs.as_mut()).await;
assert_eq!(results.len(), 5);
assert_eq!(test_fs.clear_cache_count().await, 0);
test_fs.shutdown().await;
}
#[fuchsia::test]
async fn open_file_test() {
let mut test_fs = Box::new(TestFilesystem::new());
let benchmark = OpenFile { file_count: 5 };
let results = benchmark.run(test_fs.as_mut()).await;
assert_eq!(results.len(), 5);
assert_eq!(test_fs.clear_cache_count().await, 0);
test_fs.shutdown().await;
}
#[fuchsia::test]
async fn open_deeply_nested_file_test() {
let mut test_fs = Box::new(TestFilesystem::new());
let benchmark = OpenDeeplyNestedFile { file_count: 5, depth: 3 };
let results = benchmark.run(test_fs.as_mut()).await;
assert_eq!(results.len(), 5);
assert_eq!(test_fs.clear_cache_count().await, 0);
test_fs.shutdown().await;
}
#[fuchsia::test]
async fn git_status_test() {
let mut test_fs = Box::new(TestFilesystem::new());
let dts = DirectoryTreeStructure {
files_per_directory: 2,
directories_per_directory: 2,
max_depth: 2,
};
let benchmark = GitStatus { dts, iterations: ITERATION_COUNT, stat_threads: 1 };
let results = benchmark.run(test_fs.as_mut()).await;
assert_eq!(results.len(), ITERATION_COUNT as usize);
assert_eq!(test_fs.clear_cache_count().await, 0);
test_fs.shutdown().await;
}
}