-
Notifications
You must be signed in to change notification settings - Fork 42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(infra): concurrent materializer tests #1243
base: main
Are you sure you want to change the base?
Changes from 9 commits
86437d7
9c4d665
c06cc01
3cbde9d
2de3e9a
82ed89b
8d9041d
4bdfad7
2cae29d
886a0e2
d841316
034c799
d395d1b
14f948c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// Copyright 2022-2024 Protocol Labs | ||
// SPDX-License-Identifier: Apache-2.0, MIT | ||
|
||
use std::collections::HashMap; | ||
use std::time::{Duration, Instant}; | ||
|
||
#[derive(Debug, Clone, Default)] | ||
pub struct Bencher { | ||
pub start_time: Option<Instant>, | ||
pub records: HashMap<String, Duration>, | ||
} | ||
|
||
impl Bencher { | ||
pub fn new() -> Self { | ||
Self { | ||
start_time: None, | ||
records: HashMap::new(), | ||
} | ||
} | ||
|
||
pub async fn start(&mut self) { | ||
self.start_time = Some(Instant::now()); | ||
} | ||
|
||
pub async fn record(&mut self, label: String) { | ||
let duration = self.start_time.unwrap().elapsed(); | ||
self.records.insert(label, duration); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// Copyright 2022-2024 Protocol Labs | ||
// SPDX-License-Identifier: Apache-2.0, MIT | ||
|
||
use std::time::Duration; | ||
|
||
#[derive(Debug, Clone, Default)] | ||
pub struct Execution { | ||
pub steps: Vec<ExecutionStep>, | ||
} | ||
|
||
impl Execution { | ||
pub fn new() -> Self { | ||
Self { steps: Vec::new() } | ||
} | ||
|
||
pub fn add_step(mut self, max_concurrency: usize, secs: u64) -> Self { | ||
self.steps.push(ExecutionStep { | ||
max_concurrency, | ||
duration: Duration::from_secs(secs), | ||
}); | ||
self | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone)] | ||
pub struct ExecutionStep { | ||
pub max_concurrency: usize, | ||
pub duration: Duration, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
// Copyright 2022-2024 Protocol Labs | ||
// SPDX-License-Identifier: Apache-2.0, MIT | ||
|
||
pub mod config; | ||
pub mod nonce_manager; | ||
pub mod reporting; | ||
|
||
pub use reporting::*; | ||
|
||
use crate::bencher::Bencher; | ||
use futures::FutureExt; | ||
use std::future::Future; | ||
use std::pin::Pin; | ||
use std::sync::Arc; | ||
use std::time::Instant; | ||
use tokio::sync::Semaphore; | ||
|
||
pub async fn execute<F>(cfg: config::Execution, test_factory: F) -> Vec<Vec<TestResult>> | ||
where | ||
F: Fn(usize, Bencher) -> Pin<Box<dyn Future<Output = anyhow::Result<Bencher>> + Send>>, | ||
{ | ||
let mut test_id = 0; | ||
let mut results = Vec::new(); | ||
for (step_id, step) in cfg.steps.iter().enumerate() { | ||
let semaphore = Arc::new(Semaphore::new(step.max_concurrency)); | ||
let mut handles = Vec::new(); | ||
let step_results = Arc::new(tokio::sync::Mutex::new(Vec::new())); | ||
let execution_start = Instant::now(); | ||
loop { | ||
if execution_start.elapsed() > step.duration { | ||
break; | ||
} | ||
let permit = semaphore.clone().acquire_owned().await.unwrap(); | ||
let bencher = Bencher::new(); | ||
let task = test_factory(test_id, bencher).boxed(); | ||
let step_results = step_results.clone(); | ||
let handle = tokio::spawn(async move { | ||
let res = task.await; | ||
let (bencher, err) = match res { | ||
Ok(bencher) => (Some(bencher), None), | ||
Err(err) => (None, Some(err)), | ||
}; | ||
step_results.lock().await.push(TestResult { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if this triggers a lot of threads, then everyone is waiting on this as well, also as step_results gets big, so allocation might take some time. Just curious, if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is unlikely to be a bottleneck, it can only impose a small delay in the after-test lifecycle of the future, which isn't recorded nor is time sensitive. But I'll double check that once I'll get to high max concurrency figures. |
||
test_id, | ||
step_id, | ||
bencher, | ||
err, | ||
}); | ||
drop(permit); | ||
}); | ||
handles.push(handle); | ||
test_id += 1; | ||
} | ||
|
||
// Exhaust unfinished handles. | ||
for handle in handles { | ||
handle.await.unwrap(); | ||
} | ||
|
||
let step_results = Arc::try_unwrap(step_results).unwrap().into_inner(); | ||
results.push(step_results) | ||
} | ||
results | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
// Copyright 2022-2024 Protocol Labs | ||
// SPDX-License-Identifier: Apache-2.0, MIT | ||
|
||
use ethers::prelude::H160; | ||
use ethers::types::U256; | ||
use std::collections::HashMap; | ||
use std::sync::Arc; | ||
use tokio::sync::Mutex; | ||
|
||
#[derive(Default)] | ||
pub struct NonceManager { | ||
nonces: Arc<Mutex<HashMap<H160, U256>>>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is a bottom neck as well, every address is waiting on the same lock. Maybe this might help: https://github.com/xacrimon/dashmap There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this is just a temporary solution, I was hoping to remove it entirely. If not, I'll optimize it. |
||
} | ||
|
||
impl NonceManager { | ||
pub fn new() -> Self { | ||
NonceManager { | ||
nonces: Arc::new(Mutex::new(HashMap::new())), | ||
} | ||
} | ||
|
||
pub async fn set(&self, addr: H160, amount: U256) { | ||
let mut nonces = self.nonces.lock().await; | ||
nonces.insert(addr, amount); | ||
} | ||
|
||
pub async fn get_and_increment(&self, addr: H160) -> U256 { | ||
let mut nonces = self.nonces.lock().await; | ||
let next_nonce = nonces.entry(addr).or_insert_with(U256::zero); | ||
let current_nonce = *next_nonce; | ||
*next_nonce += U256::one(); | ||
current_nonce | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
// Copyright 2022-2024 Protocol Labs | ||
// SPDX-License-Identifier: Apache-2.0, MIT | ||
|
||
use crate::bencher::Bencher; | ||
use crate::concurrency::config; | ||
use crate::concurrency::config::ExecutionStep; | ||
use anyhow::anyhow; | ||
use std::collections::{HashMap, HashSet}; | ||
use std::io; | ||
use std::time::Duration; | ||
|
||
#[derive(Debug)] | ||
pub struct TestResult { | ||
pub test_id: usize, | ||
pub step_id: usize, | ||
pub bencher: Option<Bencher>, | ||
pub err: Option<anyhow::Error>, | ||
} | ||
|
||
#[derive(Debug)] | ||
pub struct StepSummary { | ||
pub cfg: ExecutionStep, | ||
pub avg_latencies: HashMap<String, Duration>, | ||
pub errs: Vec<anyhow::Error>, | ||
} | ||
|
||
impl StepSummary { | ||
fn new(cfg: ExecutionStep, results: Vec<TestResult>) -> Self { | ||
let mut total_durations: HashMap<String, Duration> = HashMap::new(); | ||
let mut counts: HashMap<String, usize> = HashMap::new(); | ||
let mut errs = Vec::new(); | ||
for res in results { | ||
let Some(bencher) = res.bencher else { continue }; | ||
for (key, duration) in bencher.records.clone() { | ||
*total_durations.entry(key.clone()).or_insert(Duration::ZERO) += duration; | ||
*counts.entry(key).or_insert(0) += 1; | ||
} | ||
if let Some(err) = res.err { | ||
errs.push(err); | ||
} | ||
} | ||
|
||
let avg_latencies = total_durations | ||
.into_iter() | ||
.map(|(key, total)| { | ||
let count = counts[&key]; | ||
(key, total / count as u32) | ||
}) | ||
.collect(); | ||
|
||
Self { | ||
cfg, | ||
avg_latencies, | ||
errs, | ||
} | ||
} | ||
} | ||
|
||
#[derive(Debug)] | ||
pub struct ExecutionSummary { | ||
pub summaries: Vec<StepSummary>, | ||
} | ||
|
||
impl ExecutionSummary { | ||
pub fn new(cfg: config::Execution, results: Vec<Vec<TestResult>>) -> Self { | ||
let mut summaries = Vec::new(); | ||
for (i, step_results) in results.into_iter().enumerate() { | ||
let cfg = cfg.steps[i].clone(); | ||
summaries.push(StepSummary::new(cfg, step_results)); | ||
} | ||
|
||
Self { summaries } | ||
} | ||
|
||
pub fn to_result(&self) -> anyhow::Result<()> { | ||
let errs = self.errs(); | ||
if errs.is_empty() { | ||
Ok(()) | ||
} else { | ||
Err(anyhow!(errs.join("\n"))) | ||
} | ||
} | ||
|
||
pub fn errs(&self) -> Vec<String> { | ||
let mut errs = Vec::new(); | ||
for summary in self.summaries.iter() { | ||
let cloned_errs: Vec<String> = | ||
summary.errs.iter().map(|e| format!("{:?}", e)).collect(); | ||
errs.extend(cloned_errs); | ||
} | ||
errs | ||
} | ||
|
||
pub fn print(&self) { | ||
let mut data = vec![]; | ||
|
||
let latencies: HashSet<String> = self | ||
.summaries | ||
.iter() | ||
.flat_map(|summary| summary.avg_latencies.keys().cloned()) | ||
.collect(); | ||
|
||
let mut header = vec!["max_concurrency".to_string(), "duration".to_string()]; | ||
header.extend(latencies.iter().map(|key| format!("{} latency (ms)", key))); | ||
data.push(header); | ||
|
||
for summary in self.summaries.iter() { | ||
let mut row = vec![]; | ||
row.push(summary.cfg.max_concurrency.to_string()); | ||
row.push(summary.cfg.duration.as_secs().to_string()); | ||
|
||
for key in &latencies { | ||
let latency = summary | ||
.avg_latencies | ||
.get(key) | ||
.map_or(String::from("-"), |duration| { | ||
duration.as_millis().to_string() | ||
}); | ||
row.push(latency); | ||
} | ||
|
||
data.push(row); | ||
} | ||
|
||
text_tables::render(&mut io::stdout(), data).unwrap(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel expect here if better if you assume caller know calling "start" should happen first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll revise this API once the reporting summary is more solid.