Skip to content

smite-scenarios: warm up and freeze Eclair's JIT before the snapshot - #161

Open
erickcestari wants to merge 1 commit into
morehouse:masterfrom
erickcestari:optimize-eclair-exec
Open

smite-scenarios: warm up and freeze Eclair's JIT before the snapshot#161
erickcestari wants to merge 1 commit into
morehouse:masterfrom
erickcestari:optimize-eclair-exec

Conversation

@erickcestari

@erickcestari erickcestari commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Snapshot-restore fuzzing of Eclair started every iteration on a cold JVM, so the channel path ran interpreted. Add an Eclair-specific SnapshotSetup that, before the snapshot is taken:

  • drives thousands of open_channel exchanges on throwaway connections so HotSpot JIT-compiles the channel path (batched under Eclair's pending- channel rate limit),
  • waits for the compile queue to drain (polling jcmd Compiler.queue),
  • freezes the JIT with a catch-all Exclude directive so no compiler threads run during fuzzing.

Keeps C1 only (-XX:TieredStopAtLevel=1): C2 measured ~28% slower here as its speculation deopts on varied fuzzing inputs. The runtime image switches to the full JDK for jcmd.

Warming the JVM before taking the snapshot increased steady-state fuzzing throughput from 2.3 to 3.9 execs/sec for or the following input, a 1.70× (69.6%) improvement. (I've used #160 to benchmark it).

We can see the JVM logs optimizing the code with:

docker run --rm \
           -v $PWD/input.bin:/input.bin \
           -e SMITE_INPUT=/input.bin \
           -e RUST_LOG=info \
           -e SMITE_ECLAIR_LOG=1 \
           -e SMITE_ECLAIR_JAVA_OPTS="-XX:TieredStopAtLevel=1 -XX:+PrintCompilation" \
           smite-eclair-ir /eclair-scenario > jvmlogs/raw.log 2>&1
Logs:
5430 9896       1       fr.acinq.eclair.wire.protocol.RecommendedFeeratesTlv$CommitmentFeerateRange::equals (113 bytes)
5442 9897       1       scala.runtime.RichLong::self (8 bytes)
5457 9898       1       scala.collection.immutable.Map$Map2$$anon$3::nextResult (2 bytes)
5458 9899       1       scala.math.Ordering::gt$ (7 bytes)
5458 9900       1       scala.math.Ordering::gt (16 bytes)
5476 9901       1       scala.collection.immutable.NumericRange::isEmpty (21 bytes)
5476 9902       1       scala.collection.immutable.NumericRange::isInclusive (5 bytes)
5486 9903       1       sun.nio.ch.SocketChannelImpl::isNetSocket (26 bytes)
INFO  [smite_scenarios::scenarios::setup] Warmup complete
5836 9904       1       fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor::context (5 bytes)
5985 9905       1       fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor::subscriber (5 bytes)
6055 9906       1       scala.Symbol$::keyFromValue (12 bytes)
6115 9907       1       fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor::ec (5 bytes)
INFO  [smite_scenarios::targets::eclair] JIT compile queue drained
6185 9908       1       scala.UniquenessCache::unapply (6 bytes)
INFO  [smite_scenarios::targets::eclair] Froze JIT via jcmd: 56:
1 compiler directives added
### Excluding generation of native wrapper: static sun.nio.ch.IOUtil::fdVal
made not compilable on level 1  sun.nio.ch.IOUtil::fdVal   excluded by CompileCommand
### Excluding compile: fr.acinq.eclair.Features$ChannelRangeQueries$::hashCode
made not compilable on level 1  fr.acinq.eclair.Features$ChannelRangeQueries$::hashCode (3 bytes)   excluded by CompileCommand
INFO  [smite::scenarios] Scenario initialized! Executing input...
INFO  [smite::runners] Reading input from "/input.bin"
### Excluding compile: zmq.socket.pubsub.XSub::xrecv
Input
v0 = LoadPrivateKey(0x1111111111111111111111111111111111111111111111111111111111111111)
v1 = DerivePoint(v0)
v2 = LoadPrivateKey(0x1212121212121212121212121212121212121212121212121212121212121212)
v3 = DerivePoint(v2)
v4 = LoadPrivateKey(0x1313131313131313131313131313131313131313131313131313131313131313)
v5 = DerivePoint(v4)
v6 = LoadPrivateKey(0x1414141414141414141414141414141414141414141414141414141414141414)
v7 = DerivePoint(v6)
v8 = LoadPrivateKey(0x1515151515151515151515151515151515151515151515151515151515151515)
v9 = DerivePoint(v8)
v10 = LoadPrivateKey(0x1616161616161616161616161616161616161616161616161616161616161616)
v11 = DerivePoint(v10)
v12 = LoadChainHashFromContext()
v13 = LoadChannelId(0x2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a)
v14 = LoadAmount(100000)
v15 = LoadAmount(0)
v16 = LoadAmount(546)
v17 = LoadAmount(100000000)
v18 = LoadAmount(1000)
v19 = LoadAmount(1)
v20 = LoadFeeratePerKw(2500)
v21 = LoadU16(144)
v22 = LoadU16(483)
v23 = LoadU8(0)
v24 = LoadShutdownScript(Empty)
v25 = LoadChannelType(Anchors)
v26 = BuildOpenChannel(v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v1, v3, v5, v7, v9, v11, v23, v24, v25)
v27 = SendOpenChannel(v26)
v28 = RecvAcceptChannel(v27)
v29 = ExtractFundingPubkey(v28)
v30 = CreateFundingTransaction(v1, v29, v14, v20)
v31 = SendFundingCreated(v30, v0, v13)
v32 = RecvFundingSigned(v31)
BroadcastTransaction(v30)
MineBlocks(8)
RecvChannelReady()
v36 = LoadPrivateKey(0x1717171717171717171717171717171717171717171717171717171717171717)
v37 = DerivePoint(v36)
v38 = LoadShortChannelId(0x0x0)
SendChannelReady{include_alias=false}(v32, v37, v38)
Before
Smite Nyx benchmark
  target:      eclair/ir
  sharedir:    /tmp/smite-nyx
  input size:  405 bytes
  iterations:  100
  repeats:     6

  run 1/6:
  snapshot creation (first exec): 442.67 ms
  steady-state (snapshot restore + target run):
    execs/sec: 2.3
    wall time: 43.754 s
    latency:   min 426.71 ms  mean 437.54 ms  median 435.11 ms  p99 458.89 ms  max 500.49 ms
    input execution: mean 426.27 ms  median 424.01 ms   (guest runtime)
    nyx overhead:    mean 11.27 ms  median 11.18 ms   (restore + reset + ipc; 28753 dirty pages/exec)
    non-normal results: 0 / 100

  run 2/6:
  snapshot creation (first exec): 426.26 ms
  steady-state (snapshot restore + target run):
    execs/sec: 2.4
    wall time: 41.146 s
    latency:   min 379.63 ms  mean 411.46 ms  median 401.53 ms  p99 492.16 ms  max 505.94 ms
    input execution: mean 400.11 ms  median 390.04 ms   (guest runtime)
    nyx overhead:    mean 11.35 ms  median 11.29 ms   (restore + reset + ipc; 28732 dirty pages/exec)
    non-normal results: 0 / 100

  run 3/6:
  snapshot creation (first exec): 462.03 ms
  steady-state (snapshot restore + target run):
    execs/sec: 2.2
    wall time: 44.492 s
    latency:   min 406.64 ms  mean 444.92 ms  median 439.64 ms  p99 536.15 ms  max 542.38 ms
    input execution: mean 433.44 ms  median 428.76 ms   (guest runtime)
    nyx overhead:    mean 11.48 ms  median 11.27 ms   (restore + reset + ipc; 28765 dirty pages/exec)
    non-normal results: 0 / 100

  run 4/6:
  snapshot creation (first exec): 541.80 ms
  steady-state (snapshot restore + target run):
    execs/sec: 2.3
    wall time: 43.949 s
    latency:   min 405.46 ms  mean 439.49 ms  median 423.15 ms  p99 529.56 ms  max 552.07 ms
    input execution: mean 427.76 ms  median 411.30 ms   (guest runtime)
    nyx overhead:    mean 11.73 ms  median 11.47 ms   (restore + reset + ipc; 28671 dirty pages/exec)
    non-normal results: 0 / 100

  run 5/6:
  snapshot creation (first exec): 515.63 ms
  steady-state (snapshot restore + target run):
    execs/sec: 2.3
    wall time: 42.919 s
    latency:   min 394.80 ms  mean 429.19 ms  median 418.29 ms  p99 523.86 ms  max 552.63 ms
    input execution: mean 417.55 ms  median 406.38 ms   (guest runtime)
    nyx overhead:    mean 11.64 ms  median 11.49 ms   (restore + reset + ipc; 28810 dirty pages/exec)
    non-normal results: 0 / 100

  run 6/6:
  snapshot creation (first exec): 539.49 ms
  steady-state (snapshot restore + target run):
    execs/sec: 2.3
    wall time: 43.982 s
    latency:   min 412.68 ms  mean 439.82 ms  median 425.83 ms  p99 547.89 ms  max 575.46 ms
    input execution: mean 427.70 ms  median 413.65 ms   (guest runtime)
    nyx overhead:    mean 12.12 ms  median 11.90 ms   (restore + reset + ipc; 28606 dirty pages/exec)
    non-normal results: 0 / 100

  aggregate over 6 runs:
    execs/sec: mean 2.3  stddev 0.1  min 2.2  max 2.4
    snapshot:  mean 487.98 ms
    latency (mean across runs):
      min 404.32 ms  mean 433.74 ms  median 423.92 ms  p99 514.75 ms  max 538.16 ms
    input execution (mean across runs): mean 422.14 ms  median 412.36 ms
    nyx overhead (mean across runs):     mean 11.60 ms  median 11.43 ms
    non-normal results: 0 / 600
After
Smite Nyx benchmark
  target:      eclair/ir
  sharedir:    /tmp/smite-nyx
  input size:  405 bytes
  iterations:  100
  repeats:     8

  run 1/8:
  snapshot creation (first exec): 310.78 ms
  steady-state (snapshot restore + target run):
    execs/sec: 3.6
    wall time: 28.129 s
    latency:   min 267.64 ms  mean 281.29 ms  median 279.98 ms  p99 299.24 ms  max 311.10 ms
    input execution: mean 271.63 ms  median 270.80 ms   (guest runtime)
    nyx overhead:    mean 9.66 ms  median 9.50 ms   (restore + reset + ipc; 23478 dirty pages/exec)
    non-normal results: 0 / 100

  run 2/8:
  snapshot creation (first exec): 245.27 ms
  steady-state (snapshot restore + target run):
    execs/sec: 3.8
    wall time: 26.039 s
    latency:   min 228.98 ms  mean 260.39 ms  median 241.91 ms  p99 529.11 ms  max 539.95 ms
    input execution: mean 251.38 ms  median 233.55 ms   (guest runtime)
    nyx overhead:    mean 9.01 ms  median 8.65 ms   (restore + reset + ipc; 20396 dirty pages/exec)
    non-normal results: 0 / 100

  run 3/8:
  snapshot creation (first exec): 271.62 ms
  steady-state (snapshot restore + target run):
    execs/sec: 3.7
    wall time: 26.739 s
    latency:   min 249.17 ms  mean 267.39 ms  median 265.53 ms  p99 303.05 ms  max 303.55 ms
    input execution: mean 258.48 ms  median 256.52 ms   (guest runtime)
    nyx overhead:    mean 8.91 ms  median 8.84 ms   (restore + reset + ipc; 22011 dirty pages/exec)
    non-normal results: 0 / 100

  run 4/8:
  snapshot creation (first exec): 171.90 ms
  steady-state (snapshot restore + target run):
    execs/sec: 4.2
    wall time: 23.641 s
    latency:   min 203.86 ms  mean 236.41 ms  median 237.04 ms  p99 256.31 ms  max 259.80 ms
    input execution: mean 229.58 ms  median 230.32 ms   (guest runtime)
    nyx overhead:    mean 6.83 ms  median 6.66 ms   (restore + reset + ipc; 16397 dirty pages/exec)
    non-normal results: 0 / 100

  run 5/8:
  snapshot creation (first exec): 226.48 ms
  steady-state (snapshot restore + target run):
    execs/sec: 4.5
    wall time: 22.201 s
    latency:   min 214.69 ms  mean 222.01 ms  median 220.72 ms  p99 236.80 ms  max 238.02 ms
    input execution: mean 214.18 ms  median 212.95 ms   (guest runtime)
    nyx overhead:    mean 7.84 ms  median 7.74 ms   (restore + reset + ipc; 18998 dirty pages/exec)
    non-normal results: 0 / 100

  run 6/8:
  snapshot creation (first exec): 265.21 ms
  steady-state (snapshot restore + target run):
    execs/sec: 4.0
    wall time: 25.131 s
    latency:   min 237.48 ms  mean 251.31 ms  median 246.77 ms  p99 291.18 ms  max 293.20 ms
    input execution: mean 241.83 ms  median 237.33 ms   (guest runtime)
    nyx overhead:    mean 9.48 ms  median 9.38 ms   (restore + reset + ipc; 23779 dirty pages/exec)
    non-normal results: 0 / 100

  run 7/8:
  snapshot creation (first exec): 259.52 ms
  steady-state (snapshot restore + target run):
    execs/sec: 4.2
    wall time: 23.614 s
    latency:   min 222.55 ms  mean 236.13 ms  median 232.66 ms  p99 276.10 ms  max 276.50 ms
    input execution: mean 226.99 ms  median 223.75 ms   (guest runtime)
    nyx overhead:    mean 9.14 ms  median 9.11 ms   (restore + reset + ipc; 22798 dirty pages/exec)
    non-normal results: 0 / 100

  run 8/8:
  snapshot creation (first exec): 311.06 ms
  steady-state (snapshot restore + target run):
    execs/sec: 3.3
    wall time: 30.664 s
    latency:   min 283.79 ms  mean 306.64 ms  median 302.64 ms  p99 352.06 ms  max 356.74 ms
    input execution: mean 296.68 ms  median 293.30 ms   (guest runtime)
    nyx overhead:    mean 9.96 ms  median 9.59 ms   (restore + reset + ipc; 23136 dirty pages/exec)
    non-normal results: 0 / 100

  aggregate over 8 runs:
    execs/sec: mean 3.9  stddev 0.4  min 3.3  max 4.5
    snapshot:  mean 257.73 ms
    latency (mean across runs):
      min 238.52 ms  mean 257.70 ms  median 253.41 ms  p99 317.98 ms  max 322.36 ms
    input execution (mean across runs): mean 248.84 ms  median 244.81 ms
    nyx overhead (mean across runs):     mean 8.85 ms  median 8.68 ms
    non-normal results: 0 / 800

Target: eclair

1. Summary Statistics

Target Duration (h) n (Baseline) n (Exp.) Median Cov. (Baseline) Median Cov. (Exp.) Adj. p-value (Cov.) Â12 (Cov.) Median AUC (Baseline) Median AUC (Exp.) Adj. p-value (AUC) Â12 (AUC) Union Cov. (Baseline) Union Cov. (Exp.) Execs/s (Baseline) Execs/s (Exp.)
eclair 1.99194 10 10 10633 10963.7 0.00170625 0.92 20857.5 21529.5 0.000246128 0.99 11098 11627 17.165 46.165

Median Coverage Over Time

eclair Time Series

Distribution Comparisons

Final Edge Coverage Area Under Curve (Speed)
eclair Boxplot eclair AUC

Snapshot-restore fuzzing of Eclair started every iteration on a cold JVM,
so the channel path ran interpreted. Add an Eclair-specific SnapshotSetup
that, before the snapshot is taken:

- drives thousands of open_channel exchanges on throwaway connections so
  HotSpot JIT-compiles the channel path (batched under Eclair's pending-
  channel rate limit),
- waits for the compile queue to drain (polling jcmd Compiler.queue),
- freezes the JIT with a catch-all Exclude directive so no compiler
  threads run during fuzzing.

Keeps C1 only (-XX:TieredStopAtLevel=1): C2 measured ~28% slower here as
its speculation deopts on varied fuzzing inputs. The runtime image switches
to the full JDK for jcmd.

@morehouse morehouse left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a couple big issues when testing this locally:

  1. Eclair actually rejects concurrent open_channel messages, so ~90% of the open_channels we send during warmup get immediately rejected.
  2. After snapshot Eclair is in a state where all its pending channel slots are occupied. Depending on timing of the first SendOpenChannel after that, Eclair may reject it due to "rate limit reached". I think we're warming up and snapshotting too fast, so Eclair doesn't have time to process the queue of open_channels and then remove pending channels after disconnect.

Probably we should pause after each open_channel message and wait for Eclair's accept_channel, so we can be sure that Eclair's processing is done. We then also probably want a sleep or ping-pong after each disconnect so that Eclair has time to tear the actors down and free up pending slots. This will be slower, but maybe we'll need fewer iterations if we actually wait to make sure Eclair fully processes everything before moving on.

I also have some ideas to improve further, but those are for follow-up work.

  • Measure the individual effects of JIT freeze and warmup in isolation
  • Try warming up more of the funding flow (e.g., through funding_signed, mining blocks, channel_ready)
  • Try warming up gossip paths

Comment on lines +294 to +296
let directive = std::env::temp_dir().join("smite-jit-exclude-all.json");
fs::write(&directive, "[ { match: [\"*.*\"], Exclude: true } ]\n")?;
match self.run_jcmd(&["Compiler.directives_add", &directive.to_string_lossy()]) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be curious how much difference the JIT freeze makes on its own -- is this the load-bearing change or is it the warmup?

/// (<= [`WARMUP_OPENS_PER_CONNECTION`]) then drops, releasing the slots.
fn warmup<T: Target>(
target: &T,
chain_hash: [u8; 32],

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we can probably drop the chain_hash parameter and just hard-code regtest

/// Eclair rate-limits pending (half-open) channels per peer (default 99). Opens
/// stay pending until the connection drops, so batches stay well under the limit
/// to keep Eclair accepting (and JIT-compiling) each one.
const WARMUP_OPENS_PER_CONNECTION: usize = 40;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this const should go directly above warmup

Comment on lines +199 to +201
// Sync so Eclair processes (and JIT-compiles) the whole batch before conn
// drops at the end of this iteration, releasing its pending-channel slots.
ping_pong(&mut conn)?;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eclair actually processes open_channel messages async, so the only way to be sure it has processed the whole batch is to wait for all the accept_channel messages. Eclair also rejects concurrent open_channels, so we actually need to send them one at a time and wait for each response before sending the next one. A few other benefits of doing this:

  • More thorough warmup, potentially requiring much fewer iterations.
  • We can detect if our open_channel messages ever start getting rejected (e.g., due to Eclair update) and raise the alarm.

Comment on lines +143 to +145
if let Ok(opts) = std::env::var("SMITE_ECLAIR_JAVA_OPTS") {
cmd.env("JAVA_OPTS", opts);
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ends up overriding the JAVA_OPTS set in init.sh, which includes our coverage agent and the C1 compiler flag. I think we probably want to append to JAVA_OPTS here instead.

Comment on lines +104 to +105
/// per iteration. The `channel_type` is `Anchors`, the only type Eclair (the
/// sole JVM target, and thus the only one that runs warmup) accepts.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eclair accepts other channel types if they're negotiated (e.g., zero-fee-commits, scid-alias).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants