Skip to content

Commit 6ea319d

Browse files
committed
wip
1 parent 925da06 commit 6ea319d

20 files changed

Lines changed: 432 additions & 230 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@ sequencer.db
33
sequencer.db-shm
44
sequencer.db-wal
55
benchmarks/results/
6+
/benchmarks/.deps/
7+
/examples/canonical-app/out/
68
/out/
9+
/.DS_Store

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,18 @@ Success response:
155155
## Local Test Prerequisites
156156

157157
- Some `sequencer` tests spin up `Anvil`; install Foundry locally if you want the full test suite:
158+
- Self-contained benchmarks also spawn `Anvil` from a preloaded rollups state dump.
158159

159160
```bash
160161
foundryup
161162
```
162163

164+
- Prepare local benchmark + guest build dependencies:
165+
166+
```bash
167+
just setup
168+
```
169+
163170
- Enable the Anvil-backed reader tests explicitly:
164171

165172
```bash

benchmarks/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ instance they are targeting.
2222
From repository root:
2323

2424
```bash
25+
just setup
2526
just --justfile benchmarks/justfile bench-unit
2627
just --justfile benchmarks/justfile bench-ack-self
2728
just --justfile benchmarks/justfile bench-e2e-self
@@ -63,10 +64,12 @@ cargo run -p benchmarks --bin compare_latest --release -- --results-dir benchmar
6364

6465
## Notes
6566

67+
- Self-contained variants launch `anvil --load-state` from the preloaded rollups dump under `benchmarks/.deps/`; run `just setup` first.
68+
- Self-contained variants therefore require Foundry's `anvil` binary to be installed locally.
6669
- Networked benches fail by default if any tx is rejected. Pass `--allow-rejections` to inspect mixed traffic.
6770
- `e2e_latency` drains existing WS backlog before timing so stale history does not pollute the measurement window.
6871
- `bench-sweep mode=e2e` carries `from_offset` forward across rounds to avoid re-reading old WS history.
6972
- `--stop-on-first-non-200` now does exactly what it says: it stops on the first HTTP non-`200`, not on client-side transport failures.
7073
- If sweep hits `Too many open files`, increase the shell limit (`ulimit -n 4096`) or use a smaller concurrency list.
71-
- Self-contained variants automatically build a temp DB, spawn a sequencer, and persist logs/results under `benchmarks/results`.
74+
- Self-contained variants automatically build a temp DB, spawn `anvil`, start the sequencer, and persist logs/results under `benchmarks/results`.
7275
- For non-self-contained runs, start a sequencer instance first and make sure the benchmark domain matches the sequencer domain.

benchmarks/justfile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
set shell := ["bash", "-euo", "pipefail", "-c"]
22
set working-directory := ".."
33

4+
anvil_dump_name := "rollups-contracts-2.2.0-anvil-v1.4.3"
5+
anvil_dump_dir := "benchmarks/.deps/" + anvil_dump_name
6+
anvil_dump_tar := "benchmarks/.deps/" + anvil_dump_name + ".tar.gz"
7+
anvil_dump_url := "https://github.com/cartesi/rollups-contracts/releases/download/v2.2.0/rollups-contracts-2.2.0-anvil-v1.4.3.tar.gz"
8+
root_anvil_dump_tar := "rollups-contracts-2.2.0-anvil-v1.4.3.tar.gz"
9+
410
default:
511
@just --justfile benchmarks/justfile --list
612

13+
setup:
14+
mkdir -p benchmarks/.deps
15+
if [[ ! -f {{anvil_dump_tar}} ]]; then if [[ -f {{root_anvil_dump_tar}} ]]; then cp {{root_anvil_dump_tar}} {{anvil_dump_tar}}; else wget {{anvil_dump_url}} -O {{anvil_dump_tar}}; fi; fi
16+
if [[ ! -f {{anvil_dump_dir}}/state.json ]]; then rm -rf {{anvil_dump_dir}}; mkdir -p {{anvil_dump_dir}}; tar -xzf {{anvil_dump_tar}} -C {{anvil_dump_dir}}; fi
17+
18+
clean:
19+
rm -rf benchmarks/.deps
20+
721
bench-unit count="10000" max_fee="0":
822
cargo run -p benchmarks --bin unit_hot_path --release -- --count {{count}} --max-fee {{max_fee}}
923

benchmarks/src/runtime.rs

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// (c) Cartesi and individual authors (see AUTHORS)
22
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)
33

4+
use alloy_primitives::Address;
45
use serde::{Deserialize, Serialize};
56
use std::fs::{self, OpenOptions};
67
use std::path::{Path, PathBuf};
@@ -18,10 +19,13 @@ use crate::{BenchResult, self_contained_domain};
1819
pub const DEFAULT_SEQUENCER_BIN: &str = "target/release/sequencer";
1920
pub const DEFAULT_MEMORY_SAMPLE_INTERVAL_MS: u64 = 500;
2021
pub const DEFAULT_SEQUENCER_LOGS_DIR: &str = "benchmarks/results";
22+
pub const DEFAULT_ANVIL_STATE_DIR: &str = "benchmarks/.deps/rollups-contracts-2.2.0-anvil-v1.4.3";
2123

2224
const DEFAULT_SEQUENCER_START_TIMEOUT: Duration = Duration::from_secs(10);
2325
const DEFAULT_SEQUENCER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(3);
2426
const DEFAULT_SEQUENCER_RUST_LOG: &str = "info";
27+
const DEFAULT_ANVIL_START_TIMEOUT: Duration = Duration::from_secs(10);
28+
const DEFAULT_ANVIL_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(3);
2529

2630
#[derive(Debug, Clone)]
2731
pub struct ManagedSequencerConfig {
@@ -30,6 +34,7 @@ pub struct ManagedSequencerConfig {
3034
}
3135

3236
pub struct ManagedSequencer {
37+
anvil: ManagedAnvil,
3338
child: Child,
3439
shutdown_timeout: Duration,
3540
temp_dir: Option<TempDir>,
@@ -41,6 +46,7 @@ impl ManagedSequencer {
4146
pub async fn spawn(config: ManagedSequencerConfig) -> BenchResult<Self> {
4247
let (endpoint, http_addr) = build_local_endpoint()?;
4348
let domain = self_contained_domain();
49+
let anvil = ManagedAnvil::spawn(config.log_prefix).await?;
4450

4551
let dir = tempfile::tempdir()?;
4652
let db_path = dir_path_join(dir.path(), "sequencer.db");
@@ -62,6 +68,10 @@ impl ManagedSequencer {
6268
.arg(http_addr)
6369
.arg("--db-path")
6470
.arg(path_as_str(db_path.as_path())?)
71+
.arg("--eth-rpc-url")
72+
.arg(anvil.endpoint.as_str())
73+
.arg("--input-box-address")
74+
.arg(anvil.input_box_address.to_string())
6575
.arg("--domain-chain-id")
6676
.arg(domain.chain_id.to_string())
6777
.arg("--domain-verifying-contract")
@@ -85,6 +95,7 @@ impl ManagedSequencer {
8595
.await?;
8696

8797
Ok(Self {
98+
anvil,
8899
child,
89100
shutdown_timeout: DEFAULT_SEQUENCER_SHUTDOWN_TIMEOUT,
90101
temp_dir,
@@ -103,6 +114,86 @@ impl ManagedSequencer {
103114

104115
pub async fn shutdown(mut self) -> BenchResult<()> {
105116
let _ = self.temp_dir.take();
117+
send_graceful_terminate(&mut self.child).await;
118+
let sequencer_result: BenchResult<()> =
119+
match tokio::time::timeout(self.shutdown_timeout, self.child.wait()).await {
120+
Ok(wait_result) => {
121+
let _ = wait_result?;
122+
Ok(())
123+
}
124+
Err(_) => {
125+
self.child.start_kill()?;
126+
let _ = self.child.wait().await;
127+
Ok(())
128+
}
129+
};
130+
let anvil_result = self.anvil.shutdown().await;
131+
sequencer_result?;
132+
anvil_result
133+
}
134+
}
135+
136+
struct ManagedAnvil {
137+
child: Child,
138+
shutdown_timeout: Duration,
139+
endpoint: String,
140+
input_box_address: Address,
141+
}
142+
143+
impl ManagedAnvil {
144+
async fn spawn(log_prefix: &str) -> BenchResult<Self> {
145+
let state_dir = PathBuf::from(DEFAULT_ANVIL_STATE_DIR);
146+
let state_path = dir_path_join(state_dir.as_path(), "state.json");
147+
let deployment_path = dir_path_join(state_dir.as_path(), "deployments/31337/InputBox.json");
148+
149+
ensure_exists(
150+
state_path.as_path(),
151+
format!("missing {}; run `just setup` first", state_path.display()),
152+
)?;
153+
ensure_exists(
154+
deployment_path.as_path(),
155+
format!(
156+
"missing {}; run `just setup` first",
157+
deployment_path.display()
158+
),
159+
)?;
160+
161+
let input_box_address = read_input_box_address(deployment_path.as_path())?;
162+
let (endpoint, http_addr) = build_local_endpoint()?;
163+
let log_path = default_anvil_log_path(log_prefix);
164+
if let Some(parent) = log_path.parent() {
165+
fs::create_dir_all(parent)?;
166+
}
167+
let stdout_log = OpenOptions::new()
168+
.create(true)
169+
.truncate(true)
170+
.write(true)
171+
.open(log_path.as_path())?;
172+
let stderr_log = stdout_log.try_clone()?;
173+
174+
let mut child = Command::new("anvil")
175+
.arg("--host")
176+
.arg("127.0.0.1")
177+
.arg("--port")
178+
.arg(http_addr.rsplit(':').next().expect("port"))
179+
.arg("--load-state")
180+
.arg(path_as_str(state_path.as_path())?)
181+
.stdout(Stdio::from(stdout_log))
182+
.stderr(Stdio::from(stderr_log))
183+
.spawn()
184+
.map_err(|err| io_other(format!("failed to spawn anvil: {err}")))?;
185+
186+
wait_for_rpc_readiness(endpoint.as_str(), &mut child, DEFAULT_ANVIL_START_TIMEOUT).await?;
187+
188+
Ok(Self {
189+
child,
190+
shutdown_timeout: DEFAULT_ANVIL_SHUTDOWN_TIMEOUT,
191+
endpoint,
192+
input_box_address,
193+
})
194+
}
195+
196+
async fn shutdown(mut self) -> BenchResult<()> {
106197
send_graceful_terminate(&mut self.child).await;
107198
match tokio::time::timeout(self.shutdown_timeout, self.child.wait()).await {
108199
Ok(wait_result) => {
@@ -237,6 +328,16 @@ pub fn default_sequencer_log_path(prefix: &str) -> PathBuf {
237328
PathBuf::from(format!("{DEFAULT_SEQUENCER_LOGS_DIR}/{prefix}-{ts}.log"))
238329
}
239330

331+
fn default_anvil_log_path(prefix: &str) -> PathBuf {
332+
let ts = std::time::SystemTime::now()
333+
.duration_since(std::time::UNIX_EPOCH)
334+
.map(|value| value.as_millis())
335+
.unwrap_or(0);
336+
PathBuf::from(format!(
337+
"{DEFAULT_SEQUENCER_LOGS_DIR}/{prefix}-anvil-{ts}.log"
338+
))
339+
}
340+
240341
async fn wait_for_readiness(
241342
endpoint: &str,
242343
child: &mut Child,
@@ -263,6 +364,29 @@ async fn wait_for_readiness(
263364
}
264365
}
265366

367+
async fn wait_for_rpc_readiness(
368+
endpoint: &str,
369+
child: &mut Child,
370+
timeout: Duration,
371+
) -> BenchResult<()> {
372+
let deadline = tokio::time::Instant::now() + timeout;
373+
loop {
374+
if let Some(status) = child.try_wait()? {
375+
return Err(io_other(format!("anvil exited before readiness: status={status}")).into());
376+
}
377+
if rpc_endpoint_is_ready(endpoint).await {
378+
return Ok(());
379+
}
380+
if tokio::time::Instant::now() >= deadline {
381+
return Err(io_other(format!(
382+
"timed out waiting for anvil readiness at {endpoint}"
383+
))
384+
.into());
385+
}
386+
tokio::time::sleep(Duration::from_millis(100)).await;
387+
}
388+
}
389+
266390
async fn http_endpoint_is_ready(endpoint: &str) -> bool {
267391
let Some(host_port) = endpoint.strip_prefix("http://") else {
268392
return false;
@@ -288,6 +412,35 @@ async fn http_endpoint_is_ready(endpoint: &str) -> bool {
288412
}
289413
}
290414

415+
async fn rpc_endpoint_is_ready(endpoint: &str) -> bool {
416+
let Some(host_port) = endpoint.strip_prefix("http://") else {
417+
return false;
418+
};
419+
let mut stream =
420+
match tokio::time::timeout(Duration::from_millis(300), TcpStream::connect(host_port)).await
421+
{
422+
Ok(Ok(value)) => value,
423+
_ => return false,
424+
};
425+
426+
let body = r#"{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}"#;
427+
let request = format!(
428+
"POST / HTTP/1.1\r\nHost: {host_port}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
429+
body.len()
430+
);
431+
if stream.write_all(request.as_bytes()).await.is_err() {
432+
return false;
433+
}
434+
let mut head = [0_u8; 128];
435+
match tokio::time::timeout(Duration::from_millis(300), stream.read(&mut head)).await {
436+
Ok(Ok(read)) if read > 0 => std::str::from_utf8(&head[..read])
437+
.ok()
438+
.map(|text| text.contains("200 OK"))
439+
.unwrap_or(false),
440+
_ => false,
441+
}
442+
}
443+
291444
async fn send_graceful_terminate(child: &mut Child) {
292445
let Some(pid) = child.id() else {
293446
return;
@@ -322,23 +475,55 @@ fn dir_path_join(base: &Path, file: &str) -> PathBuf {
322475
path
323476
}
324477

478+
fn ensure_exists(path: &Path, missing_message: String) -> BenchResult<()> {
479+
if path.exists() {
480+
Ok(())
481+
} else {
482+
Err(io_other(missing_message).into())
483+
}
484+
}
485+
325486
fn path_as_str(path: &Path) -> BenchResult<&str> {
326487
path.to_str()
327488
.ok_or_else(|| io_other(format!("path is not valid UTF-8: {}", path.display())).into())
328489
}
329490

491+
fn read_input_box_address(path: &Path) -> BenchResult<Address> {
492+
#[derive(Deserialize)]
493+
struct DeploymentInfo {
494+
address: String,
495+
}
496+
497+
let deployment: DeploymentInfo = serde_json::from_str(&fs::read_to_string(path)?)
498+
.map_err(|err| io_other(format!("failed to parse {}: {err}", path.display())))?;
499+
deployment.address.parse().map_err(|err| {
500+
io_other(format!(
501+
"invalid InputBox address in {}: {err}",
502+
path.display()
503+
))
504+
.into()
505+
})
506+
}
507+
330508
fn io_other(message: impl Into<String>) -> std::io::Error {
331509
std::io::Error::other(message.into())
332510
}
333511

334512
#[cfg(test)]
335513
mod tests {
336-
use super::default_sequencer_log_path;
514+
use super::{default_anvil_log_path, default_sequencer_log_path};
337515

338516
#[test]
339517
fn default_log_path_uses_results_dir() {
340518
let value = default_sequencer_log_path("ack-latency");
341519
assert!(value.to_string_lossy().contains("benchmarks/results/"));
342520
assert!(value.to_string_lossy().contains("ack-latency"));
343521
}
522+
523+
#[test]
524+
fn default_anvil_log_path_uses_results_dir() {
525+
let value = default_anvil_log_path("ack-latency");
526+
assert!(value.to_string_lossy().contains("benchmarks/results/"));
527+
assert!(value.to_string_lossy().contains("ack-latency-anvil"));
528+
}
344529
}

0 commit comments

Comments
 (0)