diff --git a/.github/actions/install-wasm-utils/action.yml b/.github/actions/install-wasm-utils/action.yml index 91c44f66a..fdb8e8892 100644 --- a/.github/actions/install-wasm-utils/action.yml +++ b/.github/actions/install-wasm-utils/action.yml @@ -4,7 +4,7 @@ inputs: binaryen_version: description: "Binaryen Version" required: false - default: "111" + default: "123" runs: using: composite diff --git a/.github/workflows/rs-bench.yml b/.github/workflows/rs-bench.yml new file mode 100644 index 000000000..51e10e145 --- /dev/null +++ b/.github/workflows/rs-bench.yml @@ -0,0 +1,107 @@ +name: '[rs] Benchmarks' + +on: + pull_request: + types: [opened, synchronize, reopened, labeled] + paths: + - 'benchmarks/**' + - 'rs/**' + - 'Cargo.lock' + - 'Cargo.toml' + - '.github/workflows/rs-bench.yml' + - 'examples/**' + push: + branches: [master] + paths: + - 'benchmarks/**' + - 'rs/**' + - 'Cargo.lock' + - 'Cargo.toml' + - '.github/workflows/rs-bench.yml' + - 'examples/**' + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + benchmark: + if: (github.event_name == 'push') || + (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-benchmarks')) + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + + - name: Free Disk Space + uses: ./.github/actions/free-disk-space + + - name: Install wasm-opt + uses: ./.github/actions/install-wasm-utils + + - name: Build bench-analyzer + run: | + make build-bench-analyzer + + - name: Copy current benchmarks for a diff test + run: | + cp benchmarks/bench_data.json benchmarks/bench_data_before_bench_run.json + + - name: Run benchmarks + run: | + make bench + + # The check is done with a threshold test + - name: Check current branch has actual benchmark data + if: github.event_name == 'pull_request' + run: | + ./target/debug/bench-analyzer --current=benchmarks/bench_data.json --other=benchmarks/bench_data_before_bench_run.json --threshold=1 + + # If diff check passes, compare with master baseline + # First copy baseline JSON from master branch + - name: Copy baseline benchmarks from master branch + if: github.event_name == 'pull_request' + run: | + git fetch origin master:refs/remotes/origin/master + git show origin/master:benchmarks/bench_data.json > benchmarks/baseline.json + + # Now compare benchmarks and generate markdown table + - name: Compare Benchmarks vs Master + if: github.event_name == 'pull_request' + run: | + ./target/debug/bench-analyzer --current=benchmarks/bench_data.json --other=benchmarks/baseline.json --output=benchmarks/comparison.md + + # Read the comparison markdown for the comment + - name: Read Comparison Result + if: github.event_name == 'pull_request' + id: comparison + run: | + echo 'COMPARISON_TABLE<> $GITHUB_OUTPUT + cat benchmarks/comparison.md >> $GITHUB_OUTPUT + echo 'EOF' >> $GITHUB_OUTPUT + + # Comment the comparison table on the PR + - name: Comment PR with Benchmark Comparison + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const comparisonTable = `${{ steps.comparison.outputs.COMPARISON_TABLE }}`; + + const commentBody = `${comparisonTable} + + --- + šŸ¤– This comment was automatically generated by the benchmark comparison workflow at ${{ github.sha }}.`; + + // Always create a new comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: commentBody + }); diff --git a/Cargo.lock b/Cargo.lock index d981c18e8..e67ff9780 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -830,6 +830,7 @@ version = "0.9.0" dependencies = [ "alloc-stress", "anyhow", + "clap", "compute-stress", "convert_case 0.7.1", "counter-bench", diff --git a/Makefile b/Makefile index 98f32a86c..d5844f9cf 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,9 @@ clippy: bench: @__GEAR_WASM_BUILDER_NO_FEATURES_TRACKING=1 cargo test --release --manifest-path=benchmarks/Cargo.toml +build-bench-analyzer: + @__GEAR_WASM_BUILDER_NO_FEATURES_TRACKING=1 cargo build --bin bench-analyzer + build-parser: @echo "Building idlparser" @cargo build -p sails-idl-parser --target=wasm32-unknown-unknown --release diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml index bf8a60a51..f5c3f320d 100644 --- a/benchmarks/Cargo.toml +++ b/benchmarks/Cargo.toml @@ -6,12 +6,18 @@ edition.workspace = true license.workspace = true repository.workspace = true +[[bin]] +name = "bench-analyzer" +path = "src/bin/bench_analyzer.rs" + [dependencies] anyhow.workspace = true +clap = { version = "4.0", features = ["derive"] } serde = { workspace = true, features = ["derive"] } serde-json.workspace = true fs2.workspace = true sails-rs.workspace = true +itertools.workspace = true [build-dependencies] sails-rs = { workspace = true, features = ["build"] } @@ -33,4 +39,3 @@ redirect-proxy = { path = "../examples/redirect/proxy" } redirect-proxy-client = { path = "../examples/redirect/proxy-client" } tokio = { workspace = true, features = ["rt", "macros"] } tempfile.workspace = true -itertools.workspace = true diff --git a/benchmarks/src/benchmarks.rs b/benchmarks/src/benchmarks.rs index dcdfb6542..03028e712 100644 --- a/benchmarks/src/benchmarks.rs +++ b/benchmarks/src/benchmarks.rs @@ -169,7 +169,7 @@ async fn alloc_stress_bench() { for (len, gas_benches) in benches { crate::store_bench_data(|bench_data| { - bench_data.alloc.insert(len, median(gas_benches)); + bench_data.update_alloc_bench(len, median(gas_benches)); }) .unwrap(); } @@ -195,7 +195,7 @@ async fn compute_stress_bench() { gas_benches.sort_unstable(); crate::store_bench_data(|bench_data| { - bench_data.compute = median(gas_benches); + bench_data.update_compute_bench(median(gas_benches)); }) .unwrap(); } @@ -240,8 +240,8 @@ async fn counter_bench() { gas_benches_async.sort_unstable(); crate::store_bench_data(|bench_data| { - bench_data.counter.sync_call = median(gas_benches_sync); - bench_data.counter.async_call = median(gas_benches_async); + bench_data.update_counter_bench(false, median(gas_benches_sync)); + bench_data.update_counter_bench(true, median(gas_benches_async)); }) .unwrap(); } @@ -269,7 +269,7 @@ async fn cross_program_bench() { gas_benches.sort_unstable(); crate::store_bench_data(|bench_data| { - bench_data.cross_program = median(gas_benches); + bench_data.update_cross_program_bench(median(gas_benches)); }) .unwrap(); } @@ -321,7 +321,7 @@ async fn redirect_bench() { .collect::>(); crate::store_bench_data(|bench_data| { - bench_data.redirect = median(gas_benches); + bench_data.update_redirect_bench(median(gas_benches)); }) .unwrap(); } diff --git a/benchmarks/src/bin/bench_analyzer.rs b/benchmarks/src/bin/bench_analyzer.rs new file mode 100644 index 000000000..504e80c1a --- /dev/null +++ b/benchmarks/src/bin/bench_analyzer.rs @@ -0,0 +1,158 @@ +use anyhow::{Context, Result, anyhow}; +use benchmarks::{ + BenchCategoryComparison, BenchCategoryComparisonReport, BenchData, BenchDataFile, +}; +use clap::Parser; +use std::{fs, path::PathBuf}; + +#[derive(Parser)] +#[command(version, about, long_about = None)] +#[command(name = "bench-analyzer")] +#[command( + about = "A tool for analyzing benchmark data by comparing current and previous benchmark results." +)] +struct Cli { + /// Current benchmark data file + #[arg(long)] + current: PathBuf, + + /// Other benchmark data file + #[arg(long)] + other: PathBuf, + + /// Threshold percentage for failure + #[arg(long)] + threshold: Option, + + /// Report markdown file + #[arg(long)] + output: Option, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + + analyze_benches(cli.current, cli.other, cli.output, cli.threshold) +} + +fn analyze_benches( + current: PathBuf, + other: PathBuf, + report_output: Option, + threshold: Option, +) -> Result<()> { + // Get benches data from the provided files. + let (current_data, other_data) = get_bench_data(current, other)?; + + // Flag to track if any benchmarks fail the threshold check. + let mut threshold_failed = threshold.map(|_| false); + + // Create unfinished report. + let mut report = current_data + .into_iter() + .zip(other_data) + .map( + // Create a comparison entity for each benchmark category. + |((current_category, current_value), (other_category, other_value))| { + assert_eq!(current_category, other_category, "Categories do not match"); + + let comparison = BenchCategoryComparison::new( + current_category, + current_value, + other_value, + threshold, + ); + + if matches!(threshold_failed, Some(false)) && comparison.has_failed_threshold() { + let _ = threshold_failed.insert(true); + } + + comparison + }, + ) + .fold(initialize_report(), |mut report, comparison| { + // Add each comparison to the report. + add_comparison_to_report(&mut report, comparison); + report + }); + + // Finish the report. + add_report_conclusion(&mut report, threshold, threshold_failed); + + // Printing the finalized report. + println!("{report}"); + + // If any benchmarks failed the threshold check, return an error. + if matches!(threshold_failed, Some(true)) { + return Err(anyhow!("Benchmark contains tests failing the threshold.")); + } + + // If an output path is provided, write the report to that file. + if let Some(report_output) = report_output { + fs::write(&report_output, &report).context("Failed to write report output")?; + + println!( + "\nComparison table written to '{}'", + report_output.display() + ); + } + + Ok(()) +} + +fn get_bench_data(current: PathBuf, previous: PathBuf) -> Result<(BenchData, BenchData)> { + let mut current_file = + BenchDataFile::open(current).context("Failed to open current benchmark data file")?; + let mut previous_file = + BenchDataFile::open(previous).context("Failed to open previous benchmark data file")?; + + let current_data = current_file.read_bench_data()?; + let previous_data = previous_file.read_bench_data()?; + + Ok((current_data, previous_data)) +} + +fn initialize_report() -> String { + let mut report = String::new(); + report.push_str("## šŸ”¬ Benchmark Comparison\n\n"); + report.push_str("| Benchmark | Current | Baseline | Change | Change % | Status |\n"); + report.push_str("|-----------|---------|----------|---------|----------|--------|\n"); + + report +} + +fn add_comparison_to_report(report: &mut String, comparison: BenchCategoryComparison) { + let BenchCategoryComparisonReport { + category, + current, + other, + diff_sign, + diff, + diff_percent_sign, + diff_percent, + status, + } = comparison.into(); + report.push_str(&format!( + "| {category} | {current} | {other} | {diff_sign}{diff} | {diff_percent_sign}{diff_percent:.2}% | {status} |\n", + )); +} + +fn add_report_conclusion( + report: &mut String, + threshold: Option, + threshold_failed: Option, +) { + match threshold_failed { + Some(true) => { + let threshold = threshold.expect("threshold is required when threshold_failed is true"); + let err_str = format!("\nāŒ Benchmark threshold {threshold:.1}% check failed!\n"); + report.push_str(&err_str); + } + Some(false) => { + report.push_str("\nāœ… All benchmark differences are within acceptable thresholds."); + } + None => { + report.push_str("\n### Legend\n- šŸš€ Significant improvement (>5% reduction)\n- šŸ‘ Minor improvement (<5% reduction)\n- āœ… No significant change\n- āš ļø Minor regression (<5% increase)\n- āŒ Significant regression (>5% increase)\n"); + } + } +} diff --git a/benchmarks/src/entities.rs b/benchmarks/src/entities.rs new file mode 100644 index 000000000..110a5d814 --- /dev/null +++ b/benchmarks/src/entities.rs @@ -0,0 +1,288 @@ +use anyhow::{Context, Result}; +use itertools::Either; +use serde::{Deserialize, Serialize}; +use std::{ + collections::{BTreeMap, btree_map::IntoIter as BTreeMapIntoIter}, + fmt::Display, +}; + +/// A collection holding benchmark data categorized by [`BenchCategory`]. +pub struct BenchData(BTreeMap); + +impl BenchData { + /// Creates a new `BenchData` instance from a JSON string. + pub fn from_json_str(str: &str) -> Result { + let data: BenchDataSerde = serde_json::from_str(str) + .context("Failed to deserialize `BenchData` from JSON string")?; + + let mut map = BTreeMap::new(); + map.insert(BenchCategory::Compute, data.compute); + for (key, value) in data.alloc { + map.insert(BenchCategory::Alloc(key), value); + } + map.insert(BenchCategory::CounterSync, data.counter.sync_call); + map.insert(BenchCategory::CounterAsync, data.counter.async_call); + map.insert(BenchCategory::CrossProgram, data.cross_program); + map.insert(BenchCategory::Redirect, data.redirect); + + Ok(Self(map)) + } + + /// Update compute benchmark category value. + pub fn update_compute_bench(&mut self, value: u64) { + self.0.insert(BenchCategory::Compute, value); + } + + /// Update allocation benchmark category value. + pub fn update_alloc_bench(&mut self, size: usize, value: u64) { + self.0.insert(BenchCategory::Alloc(size), value); + } + + /// Update counter benchmark category value. + pub fn update_counter_bench(&mut self, is_async: bool, value: u64) { + if is_async { + self.0.insert(BenchCategory::CounterAsync, value); + } else { + self.0.insert(BenchCategory::CounterSync, value); + } + } + + /// Update cross-program benchmark category value. + pub fn update_cross_program_bench(&mut self, value: u64) { + self.0.insert(BenchCategory::CrossProgram, value); + } + + /// Update redirect benchmark category value. + pub fn update_redirect_bench(&mut self, value: u64) { + self.0.insert(BenchCategory::Redirect, value); + } + + /// Convert the benchmark data into a JSON string. + pub fn into_json_string(self) -> Result { + let mut bench_data = BenchDataSerde::default(); + for (key, value) in self.0 { + // match statement is crucial for not missing any new added category + match key { + BenchCategory::Compute => bench_data.compute = value, + BenchCategory::Alloc(size) => { + bench_data.alloc.insert(size, value); + } + BenchCategory::CounterSync => bench_data.counter.sync_call = value, + BenchCategory::CounterAsync => bench_data.counter.async_call = value, + BenchCategory::CrossProgram => bench_data.cross_program = value, + BenchCategory::Redirect => bench_data.redirect = value, + } + } + + serde_json::to_string_pretty(&bench_data) + .context("Failed to serialize `BenchData` to JSON string") + } +} + +impl IntoIterator for BenchData { + type Item = (BenchCategory, u64); + type IntoIter = BTreeMapIntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +/// Benchmark data stored in the benchmarks file. +/// +/// This struct is used to serialize and deserialize benchmark data +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct BenchDataSerde { + pub compute: u64, + pub alloc: BTreeMap, + pub counter: CounterBenchDataSerde, + pub cross_program: u64, + pub redirect: u64, +} + +/// Counter test benchmark data stored in the benchmarks file. +/// +/// This struct is used to serialize and deserialize benchmark data +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct CounterBenchDataSerde { + pub async_call: u64, + pub sync_call: u64, +} + +/// Benchmark category that can be read (written) from (to) the benchmarks file. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum BenchCategory { + Compute, + Alloc(usize), + CounterSync, + CounterAsync, + CrossProgram, + Redirect, +} + +impl Display for BenchCategory { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BenchCategory::Compute => write!(f, "compute"), + BenchCategory::Alloc(size) => write!(f, "alloc-{size}"), + BenchCategory::CounterSync => write!(f, "counter_sync"), + BenchCategory::CounterAsync => write!(f, "counter_async"), + BenchCategory::CrossProgram => write!(f, "cross_program"), + BenchCategory::Redirect => write!(f, "redirect"), + } + } +} + +/// Comparison entity for benchmark categories. +#[derive(Debug)] +pub struct BenchCategoryComparison { + category: BenchCategory, + current: u64, + other: u64, + diff: i64, + diff_percent: f64, + status: Either, +} + +impl BenchCategoryComparison { + pub fn new( + category: BenchCategory, + current: u64, + other: u64, + maybe_threshold: Option, + ) -> Self { + let diff = current as i64 - other as i64; + let diff_percent = (diff as f64 / other as f64) * 100.0; + let status = match maybe_threshold { + Some(threshold) => { + let exceeds = diff_percent.abs() > threshold; + if exceeds { + Either::Left(ThresholdPassStatus::Fail) + } else { + Either::Left(ThresholdPassStatus::Pass) + } + } + None => { + if diff_percent.abs() < 1.0 { + // [0,..1.0) + Either::Right(PerformanceStatus::NoChange) + } else if diff_percent < -5.0 { + // [-inf, -5.0) + Either::Right(PerformanceStatus::SignificantImprovement) + } else if diff_percent < 0.0 { + // [-5.0, 0.0) + Either::Right(PerformanceStatus::MinorImprovement) + } else if diff_percent < 5.0 { + // [0.0, 5.0) + Either::Right(PerformanceStatus::MinorRegression) + } else { + // [5.0, inf) + Either::Right(PerformanceStatus::SignificantRegression) + } + } + }; + + Self { + category, + current, + other, + diff, + diff_percent, + status, + } + } + + pub fn has_failed_threshold(&self) -> bool { + self.status + .as_ref() + .left() + .map(|status| matches!(status, ThresholdPassStatus::Fail)) + .unwrap_or(false) + } +} + +#[derive(Debug, Clone, Copy)] +enum ThresholdPassStatus { + Pass, + Fail, +} + +#[derive(Debug, Clone, Copy)] +enum PerformanceStatus { + SignificantImprovement, + MinorImprovement, + NoChange, + SignificantRegression, + MinorRegression, +} + +/// Report structure for benchmark category comparison. +/// +/// This struct is a placeholder to formatted benchmark comparison data. +/// The formatted data is later decided on a client side how to be displayed. +pub struct BenchCategoryComparisonReport { + pub category: String, + pub current: String, + pub other: String, + pub diff_sign: &'static str, + pub diff: String, + pub diff_percent_sign: &'static str, + pub diff_percent: f64, + pub status: &'static str, +} + +impl From for BenchCategoryComparisonReport { + fn from(comparison: BenchCategoryComparison) -> Self { + let category = comparison.category.to_string(); + let current = Self::format_number(comparison.current); + let other = Self::format_number(comparison.other); + let diff_sign = if comparison.diff >= 0 { "+" } else { "-" }; + let diff = Self::format_number(comparison.diff.unsigned_abs()); + let diff_percent_sign = if comparison.diff_percent >= 0.0 { + "+" + } else { + "" + }; + let diff_percent = comparison.diff_percent; + let status = Self::status_to_str(&comparison.status); + + BenchCategoryComparisonReport { + category, + current, + other, + diff_sign, + diff, + diff_percent_sign, + diff_percent, + status, + } + } +} + +impl BenchCategoryComparisonReport { + fn format_number(num: u64) -> String { + let num_str = num.to_string(); + let mut result = String::new(); + + for (i, ch) in num_str.chars().rev().enumerate() { + if i > 0 && i % 3 == 0 { + result.push('_'); + } + result.push(ch); + } + + result.chars().rev().collect() + } + + fn status_to_str(status: &Either) -> &'static str { + match status { + Either::Left(ThresholdPassStatus::Pass) => "āœ… PASS", + Either::Left(ThresholdPassStatus::Fail) => "āŒ FAIL", + Either::Right(PerformanceStatus::SignificantImprovement) => "šŸš€", + Either::Right(PerformanceStatus::MinorImprovement) => "šŸ‘", + Either::Right(PerformanceStatus::NoChange) => "āœ…", + Either::Right(PerformanceStatus::SignificantRegression) => "āŒ", + Either::Right(PerformanceStatus::MinorRegression) => "āš ļø", + } + } +} diff --git a/benchmarks/src/file.rs b/benchmarks/src/file.rs new file mode 100644 index 000000000..fcbd24748 --- /dev/null +++ b/benchmarks/src/file.rs @@ -0,0 +1,72 @@ +use crate::BenchData; +use anyhow::{Context, Result}; +use fs2::FileExt; +use std::{ + fs::{File, OpenOptions}, + io::{Read, Seek, SeekFrom, Write}, + path::Path, +}; + +/// A file that holds benchmark data. +pub struct BenchDataFile(File); + +impl BenchDataFile { + /// Opens a benchmark data file. + /// + /// If the file does not exist, the function fails. + pub fn open(path: impl AsRef) -> Result { + let file = OpenOptions::new() + .read(true) + .write(true) + .open(path) + .context("Failed to open or create bench data file")?; + + Ok(Self(file)) + } + + /// Locks the file for exclusive access. + pub fn lock_exclusive(&mut self) -> Result<()> { + self.0 + .lock_exclusive() + .context("Failed to lock bench data file for writing") + } + + /// Unlocks the file after exclusive access. + pub fn unlock(&self) -> Result<()> { + ::unlock(&self.0).context("Failed to unlock bench data file") + } + + /// Reads the benchmark data from the file. + pub fn read_bench_data(&mut self) -> Result { + let mut content = String::new(); + self.0 + .read_to_string(&mut content) + .context("Failed reading bench data bytes to string")?; + let bench_data = + BenchData::from_json_str(&content).context("Failed to deserialize bench data")?; + + Ok(bench_data) + } + + /// Converts the benchmark data into a JSON string and writes it to the file. + pub fn write_bench_data(&mut self, data: BenchData) -> Result<()> { + // Serialize back + let bench_data_string = data + .into_json_string() + .context("Failed to serialize updated bench data")?; + + // Write updated bench data + self.0.set_len(0).context("Failed to erase file content")?; + self.0 + .seek(SeekFrom::Start(0)) + .context("Failed to seek to the start of the file")?; + self.0 + .write_all(bench_data_string.as_bytes()) + .context("Failed to write serialized bench data to file")?; + self.0 + .flush() + .context("Failed to flush bench data to file")?; + + Ok(()) + } +} diff --git a/benchmarks/src/lib.rs b/benchmarks/src/lib.rs index 4fbb7745b..973d8cbce 100644 --- a/benchmarks/src/lib.rs +++ b/benchmarks/src/lib.rs @@ -10,32 +10,19 @@ mod benchmarks; #[cfg(all(test, not(debug_assertions)))] mod clients; +mod entities; +mod file; + use anyhow::{Context, Result}; -use fs2::FileExt; -use serde::{Deserialize, Serialize}; +pub use entities::{ + BenchCategory, BenchCategoryComparison, BenchCategoryComparisonReport, BenchData, +}; +pub use file::BenchDataFile; use std::{ - collections::BTreeMap, env, - fs::{File, OpenOptions}, - io::{Read, Seek, SeekFrom, Write}, path::{Path, PathBuf}, }; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct BenchData { - pub compute: u64, - pub alloc: BTreeMap, - pub counter: CounterBenchData, - pub cross_program: u64, - pub redirect: u64, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct CounterBenchData { - pub async_call: u64, - pub sync_call: u64, -} - pub fn store_bench_data(f: impl FnOnce(&mut BenchData)) -> Result<()> { let path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("bench_data.json"); @@ -43,56 +30,45 @@ pub fn store_bench_data(f: impl FnOnce(&mut BenchData)) -> Result<()> { } fn store_bench_data_to_file(path: impl AsRef, f: impl FnOnce(&mut BenchData)) -> Result<()> { - // Open file - let mut file = OpenOptions::new() - .read(true) - .write(true) - .open(path) - .context("Failed to open or create bench data file")?; - - // Lock file + let mut file = BenchDataFile::open(path).context("Failed to create `BenchDataFile`")?; + file.lock_exclusive().unwrap_or_else(|e| { panic!("Failed to lock bench data file for writing: {e}"); }); - // Read bench data - let mut content = String::new(); - file.read_to_string(&mut content) - .context("Failed reading bench data bytes to string")?; - let mut bench_data = - serde_json::from_str(&content).context("Failed to deserialize bench data")?; + let mut bench_data = file + .read_bench_data() + .context("Failed to read existing bench data")?; // Handle bench data f(&mut bench_data); - // Serialize back - let bench_data_string = serde_json::to_string_pretty(&bench_data)?; - - // Write updated bench data - file.set_len(0).context("Failed to erase file content")?; - file.seek(SeekFrom::Start(0)) - .context("Failed to seek to the start of the file")?; - file.write_all(bench_data_string.as_bytes()) - .context("Failed to write serialized bench data to file")?; - file.flush().context("Failed to flush bench data to file")?; + // Write updated bench data. + file.write_bench_data(bench_data) + .context("Failed to update bench data")?; - // Unlock file - ::unlock(&file).context("Failed to unlock bench data file") + // Unlock the file + file.unlock() + .context("Failed to unlock bench data file after writing") } #[cfg(test)] mod tests { use super::*; - use std::thread; + use crate::entities::{BenchDataSerde, CounterBenchDataSerde}; + use std::{ + io::{Read, Seek, SeekFrom, Write}, + thread, + }; use tempfile::NamedTempFile; #[test] fn test_data_not_overwritten() { // Create initial bench data. - let initial_bench_data = BenchData { + let initial_bench_data = BenchDataSerde { compute: 123, - alloc: BTreeMap::new(), - counter: CounterBenchData { + alloc: Default::default(), + counter: CounterBenchDataSerde { async_call: 53, sync_call: 35, }, @@ -119,17 +95,17 @@ mod tests { // Spawn two threads to modify the bench data concurrently. let h1 = thread::spawn(move || { store_bench_data_to_file(path_h1, |bench_data| { - bench_data.compute = 42; - bench_data.cross_program = 0; + bench_data.update_compute_bench(42); + bench_data.update_cross_program_bench(0); }) .unwrap(); }); let h2 = thread::spawn(move || { store_bench_data_to_file(path_h2, |bench_data| { - bench_data.counter.async_call = 84; - bench_data.counter.sync_call = 126; - bench_data.redirect = 4343; + bench_data.update_counter_bench(true, 84); + bench_data.update_counter_bench(false, 126); + bench_data.update_redirect_bench(4343); }) .unwrap(); }); @@ -144,16 +120,16 @@ mod tests { .as_file_mut() .read_to_string(&mut content) .expect("Failed reading bench data bytes to string"); - let bench_data: BenchData = + let bench_data: BenchDataSerde = serde_json::from_str(&content).expect("Failed to deserialize bench data"); // Check that the bench data was modified correctly. assert_eq!( bench_data, - BenchData { + BenchDataSerde { compute: 42, - alloc: BTreeMap::new(), - counter: CounterBenchData { + alloc: Default::default(), + counter: CounterBenchDataSerde { async_call: 84, sync_call: 126, }, diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 4fa2844cf..11572f7b3 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "stable" +channel = "1.88" targets = ["wasm32-unknown-unknown", "wasm32v1-none"] -components = [ "clippy", "rustfmt", "llvm-tools" ] +components = ["clippy", "rustfmt", "llvm-tools"]