use std::time::{Duration, Instant};
use num_bigint::BigUint;
use tokio::sync::broadcast::{self, error::TryRecvError};
use tycho_simulation::{
protocol::models::Update,
tycho_common::models::Chain,
tycho_ethereum::gas::{BlockGasPrice, GasPrice},
};
use crate::{
derived::{
computation::DerivedComputation,
computations::{ComponentDepthComputation, SpotPriceComputation, TokenGasPriceComputation},
manager::{ChangedComponents, ComputationManagerConfig, SharedDerivedDataRef},
store::DerivedData,
},
feed::{events::MarketEvent, market_data::MarketData, tycho_feed::TychoFeed, TychoFeedConfig},
types::constants::native_token,
};
const DEFAULT_GAS_PRICE_WEI: u64 = 10_000_000_000;
pub struct DerivedBenchSettings {
pub chain: Chain,
pub pricing_max_hops: usize,
pub gas_price_wei: Option<BigUint>,
}
struct TimedComputations {
spot_prices: SpotPriceComputation,
token_prices: TokenGasPriceComputation,
pool_depths: ComponentDepthComputation,
}
struct BlockRun<'a> {
market: &'a MarketData,
store: &'a SharedDerivedDataRef,
changed: &'a ChangedComponents,
block: u64,
}
impl TimedComputations {
fn build(settings: &DerivedBenchSettings) -> Self {
let gas_token =
native_token(&settings.chain).expect("the recording's chain has a native token");
let config = ComputationManagerConfig::new()
.with_gas_token(gas_token)
.with_max_hop(settings.pricing_max_hops)
.with_pricing_pass_budget(Duration::from_secs(24 * 60 * 60));
Self {
spot_prices: SpotPriceComputation::new(),
token_prices: config.build_token_price_computation(),
pool_depths: ComponentDepthComputation::new(config.depth_slippage_threshold())
.expect("the default depth slippage threshold is valid"),
}
}
async fn run_block(&self, run: &BlockRun<'_>) {
time_computation(&self.spot_prices, run, |out| out.len()).await;
time_computation(&self.token_prices, run, |out| out.len()).await;
time_computation(&self.pool_depths, run, |out| out.len()).await;
}
}
async fn time_computation<C: DerivedComputation>(
computation: &C,
run: &BlockRun<'_>,
output_len: impl Fn(&C::Output) -> usize,
) {
let block = run.block;
let start = Instant::now();
let output = computation
.compute(run.market, run.store, run.changed)
.await
.unwrap_or_else(|error| panic!("{} failed on block {block}: {error}", C::ID));
let elapsed = start.elapsed();
println!(
"block {block} {:<17} {:>10.1} ms items={} failed={}",
C::ID,
elapsed.as_secs_f64() * 1000.0,
output_len(&output.data),
output.failed_items.len(),
);
C::persist(&mut *run.store.write().await, output, block, run.changed.is_full_recompute);
}
fn drain_events(events: &mut broadcast::Receiver<MarketEvent>) -> Vec<MarketEvent> {
let mut drained = Vec::new();
loop {
match events.try_recv() {
Ok(event) => drained.push(event),
Err(TryRecvError::Empty) => return drained,
Err(TryRecvError::Lagged(skipped)) => {
panic!("the bench dropped {skipped} market events; drain after every update")
}
Err(TryRecvError::Closed) => panic!("the feed closed its event channel"),
}
}
}
async fn set_gas_price(market: &MarketData, gas_price_wei: Option<BigUint>) {
let gas_price = gas_price_wei.unwrap_or_else(|| BigUint::from(DEFAULT_GAS_PRICE_WEI));
let block_number = read_current_block(market).await;
market
.write()
.await
.update_gas_price(BlockGasPrice {
block_number,
block_hash: Default::default(),
block_timestamp: 0,
pricing: GasPrice::Legacy { gas_price },
});
}
async fn read_current_block(market: &MarketData) -> u64 {
market
.read()
.await
.last_updated()
.map_or(0, |block| block.number())
}
pub async fn time_derived_computations(settings: &DerivedBenchSettings, updates: Vec<Update>) {
let computations = TimedComputations::build(settings);
let market = MarketData::new_shared();
let feed = TychoFeed::new(
TychoFeedConfig::new("ws://replay".to_string(), settings.chain, None, false, vec![], 0.0),
market.clone(),
);
let mut events = feed.subscribe();
let store = DerivedData::new_shared();
let mut updates = updates.into_iter();
let snapshot = updates
.next()
.expect("the recording holds at least one update");
let start = Instant::now();
feed.handle_tycho_message(snapshot)
.await
.expect("the snapshot replays");
set_gas_price(&market, settings.gas_price_wei.clone()).await;
drain_events(&mut events);
let components = market
.read()
.await
.component_topology()
.len();
println!(
"replayed the snapshot in {:.1} s: {components} components",
start.elapsed().as_secs_f64()
);
let full_recompute = ChangedComponents { is_full_recompute: true, ..Default::default() };
let block = read_current_block(&market).await;
computations
.run_block(&BlockRun { market: &market, store: &store, changed: &full_recompute, block })
.await;
for update in updates {
feed.handle_tycho_message(update)
.await
.expect("the update replays");
for event in drain_events(&mut events) {
let MarketEvent::MarketUpdated {
added_components,
removed_components,
updated_components,
} = event;
let changed = ChangedComponents {
added: added_components,
removed: removed_components,
updated: updated_components,
is_full_recompute: false,
};
if changed.added.is_empty() && changed.removed.is_empty() && changed.updated.is_empty()
{
continue;
}
let block = read_current_block(&market).await;
println!(
"block {block}: added={} removed={} updated={}",
changed.added.len(),
changed.removed.len(),
changed.updated.len()
);
computations
.run_block(&BlockRun { market: &market, store: &store, changed: &changed, block })
.await;
}
}
}