From f566fa177a4c1f3821570cb310b7279076593be3 Mon Sep 17 00:00:00 2001 From: Kould Date: Fri, 27 Sep 2024 18:46:53 +0800 Subject: [PATCH] benchmark: add read & write benchmark and tokio fs unit tests --- .github/dependabot.yml | 30 +++++++++ .github/workflows/ci.yml | 108 +++++++++++++++++++++++++++++++ Cargo.toml | 9 ++- fusio/Cargo.toml | 10 +++ fusio/benches/tokio.rs | 133 +++++++++++++++++++++++++++++++++++++++ fusio/src/buf.rs | 2 + fusio/src/fs/options.rs | 16 ++--- fusio/src/lib.rs | 114 +++++++++++++++++++++++++++++++++ fusio/src/path/mod.rs | 2 +- 9 files changed, 414 insertions(+), 10 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 fusio/benches/tokio.rs diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d2ef7c4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,30 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + + # Maintain dependencies for rust + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ec9b39e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,108 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + CARGO_REGISTRIES_MY_REGISTRY_INDEX: https://github.com/rust-lang/crates.io-index + +jobs: + # 1 + tokio_check: + name: Rust project check on tokio + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install latest + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + components: rustfmt, clippy + + # `cargo check` command here will use installed `nightly` + # as it is set as an "override" for current directory + + - name: Run cargo clippy on tokio + uses: actions-rs/cargo@v1 + with: + command: check + args: --package fusio --features "tokio, futures" + + - name: Run cargo build on tokio + uses: actions-rs/cargo@v1 + with: + command: build + args: --package fusio --features "tokio, futures" + + - name: Run cargo test on tokio + uses: actions-rs/cargo@v1 + with: + command: test + args: --package fusio --features "tokio, futures" + + monoio_check: + name: Rust project check on monoio + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install latest + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + components: rustfmt, clippy + + # `cargo check` command here will use installed `nightly` + # as it is set as an "override" for current directory + + - name: Run cargo clippy on monoio + uses: actions-rs/cargo@v1 + with: + command: check + args: --package fusio --features "monoio, futures" + + - name: Run cargo build on monoio + uses: actions-rs/cargo@v1 + with: + command: build + args: --package fusio --features "monoio, futures" + + - name: Run cargo test on monoio + uses: actions-rs/cargo@v1 + with: + command: test + args: --package fusio --features "monoio, futures" + + # 2 + fmt: + name: Rust fmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install latest nightly + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + components: rustfmt, clippy + + # `cargo check` command here will use installed `nightly` + # as it is set as an "override" for current directory + + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: -- --check \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 5d471ff..6027088 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,13 @@ repository = "https://github.com/tonbo-io/fusio" [workspace.dependencies] bytes = { version = "1.7" } -[profile.bench] +[target.'cfg(unix)'.dev-dependencies] +pprof = { version = "0.13", features = ["criterion", "flamegraph"] } + +[profile.release] codegen-units = 1 lto = "thin" + +[profile.bench] +debug = true +inherits = "release" diff --git a/fusio/Cargo.toml b/fusio/Cargo.toml index 49d5671..ef03f7d 100644 --- a/fusio/Cargo.toml +++ b/fusio/Cargo.toml @@ -20,6 +20,7 @@ bytes = ["dep:bytes"] completion-based = [] default = ["dyn", "fs"] dyn = [] +futures = ["dep:futures-util"] fs = ["tokio?/rt"] h2 = ["dep:h2"] http = [ @@ -38,6 +39,12 @@ tokio = ["async-stream", "dep:tokio"] tokio-http = ["dep:reqwest", "http"] tokio-uring = ["async-stream", "completion-based", "dep:tokio-uring", "no-send"] +[[bench]] +harness = false +name = "tokio" +path = "benches/tokio.rs" +required-features = ["tokio"] + [dependencies] async-stream = { version = "0.3", optional = true } bytes = { workspace = true, optional = true } @@ -79,9 +86,12 @@ url = { version = "2" } tokio-uring = { version = "0.5", default-features = false, optional = true } [dev-dependencies] +criterion = { version = "0.5", features = ["async_tokio", "html_reports"] } +futures-executor = "0.3" hyper = { version = "1", features = ["full"] } hyper-util = { version = "0.1", features = ["full"] } monoio = { version = "0.2" } +rand = "0.8" tempfile = "3" tokio = { version = "1", features = ["full"] } diff --git a/fusio/benches/tokio.rs b/fusio/benches/tokio.rs new file mode 100644 index 0000000..589d485 --- /dev/null +++ b/fusio/benches/tokio.rs @@ -0,0 +1,133 @@ +use std::{cell::RefCell, io::SeekFrom, rc::Rc, sync::Arc}; + +use criterion::{criterion_group, criterion_main, Criterion}; +use fusio::{ + fs::{Fs, OpenOptions}, + local::TokioFs, + path::Path, + IoBuf, IoBufMut, Write, +}; +use rand::Rng; +use tempfile::NamedTempFile; + +fn write(c: &mut Criterion) { + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(8) + .enable_all() + .build() + .unwrap(); + + let mut group = c.benchmark_group("write"); + + let mut write_bytes = [0u8; 4096]; + rand::thread_rng().fill(&mut write_bytes); + let write_bytes = Arc::new(write_bytes); + + let temp_file = NamedTempFile::new().unwrap(); + let path = Path::from_filesystem_path(temp_file.path()).unwrap(); + + let fs = TokioFs; + let file = Rc::new(RefCell::new(runtime.block_on(async { + fs.open_options(&path, OpenOptions::default().write(true).append(true)) + .await + .unwrap() + }))); + + group.bench_function("fusio write 4K", |b| { + b.to_async(&runtime).iter(|| { + let bytes = write_bytes.clone(); + let file = file.clone(); + + async move { + let (result, _) = + fusio::dynamic::DynWrite::write_all(&mut *(*file).borrow_mut(), unsafe { + (&bytes.as_ref()[..]).to_buf_nocopy() + }) + .await; + result.unwrap(); + } + }) + }); + group.bench_function("tokio write 4K", |b| { + b.to_async(&runtime).iter(|| { + let bytes = write_bytes.clone(); + let file = file.clone(); + + async move { + tokio::io::AsyncWriteExt::write_all( + &mut *(*file).borrow_mut(), + &bytes.as_ref()[..], + ) + .await + .unwrap(); + } + }) + }); + + group.finish(); +} + +fn read(c: &mut Criterion) { + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(8) + .enable_all() + .build() + .unwrap(); + + let mut group = c.benchmark_group("read"); + + let mut write_bytes = [0u8; 4096]; + rand::thread_rng().fill(&mut write_bytes); + + let temp_file = NamedTempFile::new().unwrap(); + let path = Path::from_filesystem_path(temp_file.path()).unwrap(); + + let fs = TokioFs; + let file = Rc::new(RefCell::new(runtime.block_on(async { + let mut file = fs + .open_options(&path, OpenOptions::default().write(true).append(true)) + .await + .unwrap(); + let (result, _) = file.write_all(&write_bytes[..]).await; + result.unwrap(); + file + }))); + + group.bench_function("fusio read 4K", |b| { + b.to_async(&runtime).iter(|| { + let file = file.clone(); + + async move { + let _ = fusio::dynamic::DynSeek::seek(&mut *(*file).borrow_mut(), 0) + .await + .unwrap(); + let _ = fusio::dynamic::DynRead::read_exact(&mut *(*file).borrow_mut(), unsafe { + vec![0u8; 4096].to_buf_mut_nocopy() + }) + .await + .unwrap(); + } + }) + }); + + group.bench_function("tokio read 4K", |b| { + b.to_async(&runtime).iter(|| { + let file = file.clone(); + let mut bytes = [0u8; 4096]; + + async move { + let _ = + tokio::io::AsyncSeekExt::seek(&mut *(*file).borrow_mut(), SeekFrom::Start(0)) + .await + .unwrap(); + let _ = + tokio::io::AsyncReadExt::read_exact(&mut *(*file).borrow_mut(), &mut bytes[..]) + .await + .unwrap(); + } + }) + }); +} + +criterion_group!(benches, write, read); +criterion_main!(benches); diff --git a/fusio/src/buf.rs b/fusio/src/buf.rs index 62269be..8298500 100644 --- a/fusio/src/buf.rs +++ b/fusio/src/buf.rs @@ -47,8 +47,10 @@ pub trait IoBufMut: IoBuf { unsafe { std::slice::from_raw_parts_mut(self.as_mut_ptr(), self.bytes_init()) } } + #[warn(clippy::missing_safety_doc)] unsafe fn to_buf_mut_nocopy(self) -> BufMut; + #[warn(clippy::missing_safety_doc)] unsafe fn recover_from_buf_mut(buf: BufMut) -> Self; } diff --git a/fusio/src/fs/options.rs b/fusio/src/fs/options.rs index 72c97e5..9c8e502 100644 --- a/fusio/src/fs/options.rs +++ b/fusio/src/fs/options.rs @@ -21,23 +21,23 @@ impl Default for OpenOptions { } impl OpenOptions { - pub fn read(mut self) -> Self { - self.read = true; + pub fn read(mut self, read: bool) -> Self { + self.read = read; self } - pub fn write(mut self) -> Self { - self.write = Some(WriteMode::Overwrite); + pub fn write(mut self, write: bool) -> Self { + self.write = write.then_some(WriteMode::Overwrite); self } - pub fn create(mut self) -> Self { - self.create = true; + pub fn create(mut self, create: bool) -> Self { + self.create = create; self } - pub fn append(mut self) -> Self { - self.write = Some(WriteMode::Append); + pub fn append(mut self, append: bool) -> Self { + self.write = append.then_some(WriteMode::Append); self } } diff --git a/fusio/src/lib.rs b/fusio/src/lib.rs index 3ed863f..9c9bbc3 100644 --- a/fusio/src/lib.rs +++ b/fusio/src/lib.rs @@ -268,6 +268,112 @@ mod tests { assert_eq!(buf.as_slice(), &[2, 0, 2, 4]); } + #[cfg(feature = "futures")] + async fn test_local_fs(fs: S) -> Result<(), Error> + where + S: crate::fs::Fs, + { + use std::collections::HashSet; + + use futures_util::StreamExt; + use tempfile::TempDir; + + use crate::{fs::OpenOptions, path::Path, DynFs}; + + let tmp_dir = TempDir::new()?; + let work_dir_path = tmp_dir.path().join("work"); + let work_file_path = work_dir_path.join("test.file"); + + fs.create_dir_all(&Path::from_absolute_path(&work_dir_path)?) + .await?; + + assert!(work_dir_path.exists()); + assert!(fs + .open_options( + &Path::from_absolute_path(&work_file_path)?, + OpenOptions::default() + ) + .await + .is_err()); + { + let _ = fs + .open_options( + &Path::from_absolute_path(&work_file_path)?, + OpenOptions::default().create(true).write(true), + ) + .await?; + assert!(work_file_path.exists()); + } + { + let mut file = fs + .open_options( + &Path::from_absolute_path(&work_file_path)?, + OpenOptions::default().write(true), + ) + .await?; + file.write_all("Hello! fusio".as_bytes()).await.0?; + let mut file = fs + .open_options( + &Path::from_absolute_path(&work_file_path)?, + OpenOptions::default().write(true), + ) + .await?; + file.write_all("Hello! world".as_bytes()).await.0?; + + assert!(file.read_exact(vec![0u8; 24]).await.is_err()); + } + { + let mut file = fs + .open_options( + &Path::from_absolute_path(&work_file_path)?, + OpenOptions::default().append(true), + ) + .await?; + file.write_all("Hello! fusio".as_bytes()).await.0?; + + assert!(file.read_exact(vec![0u8; 24]).await.is_err()); + } + { + let mut file = fs + .open_options( + &Path::from_absolute_path(&work_file_path)?, + OpenOptions::default(), + ) + .await?; + + assert_eq!( + "Hello! worldHello! fusio".as_bytes(), + &file.read_to_end(Vec::new()).await? + ) + } + fs.remove(&Path::from_filesystem_path(&work_file_path)?) + .await?; + assert!(!work_file_path.exists()); + + let mut file_set = HashSet::new(); + for i in 0..10 { + let _ = fs + .open_options( + &Path::from_absolute_path(work_dir_path.join(i.to_string()))?, + OpenOptions::default().create(true).write(true), + ) + .await?; + file_set.insert(i.to_string()); + } + + let path = Path::from_filesystem_path(&work_dir_path)?; + let mut file_stream = Box::pin(fs.list(&path).await?); + + while let Some(file_meta) = file_stream.next().await { + if let Some(file_name) = file_meta?.path.filename() { + assert!(file_set.remove(file_name)); + } + } + assert!(file_set.is_empty()); + + Ok(()) + } + #[cfg(feature = "tokio")] #[tokio::test] async fn test_tokio() { @@ -280,6 +386,14 @@ mod tests { write_and_read(File::from_std(write), File::from_std(read)).await; } + #[cfg(all(feature = "tokio", feature = "futures"))] + #[tokio::test] + async fn test_tokio_fs() { + use crate::local::TokioFs; + + test_local_fs(TokioFs).await.unwrap(); + } + #[cfg(feature = "monoio")] #[monoio::test] async fn test_monoio() { diff --git a/fusio/src/path/mod.rs b/fusio/src/path/mod.rs index b073f96..bd60280 100644 --- a/fusio/src/path/mod.rs +++ b/fusio/src/path/mod.rs @@ -513,6 +513,6 @@ mod tests { let this_path = Path::from_filesystem_path(temp_file.path()).unwrap(); let std_path = path_to_local(&this_path).unwrap(); - assert_eq!(std_path, std::fs::canonicalize(temp_file.path()).unwrap()); + assert_eq!(std_path, temp_file.path()); } }