Skip to content

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

Open
erickcestari wants to merge 2 commits into
lnfuzz:masterfrom
erickcestari:optimize-eclair-exec
Open

smite-scenarios: warm up and freeze Eclair's JIT before the snapshot#161
erickcestari wants to merge 2 commits into
lnfuzz: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

@morehouse morehouse left a comment

Copy link
Copy Markdown
Collaborator

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
Collaborator

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll try to also bench it to see the results.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've done the bench. You can see the results below.

Comment thread smite-scenarios/src/scenarios/setup.rs Outdated
Comment thread smite-scenarios/src/scenarios/setup.rs Outdated
Comment thread smite-scenarios/src/scenarios/setup.rs Outdated
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
Collaborator

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nice, I've fixed that. Now it waits for the accept_channel and then sends an error message for eclair be able to clean the state.

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
Collaborator

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The idea is to override even the JAVA_OPTS that exists at init.sh so it's easier to test different options without the need to build a new docker image.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have updated the docs to make it more clear.

Comment thread smite-scenarios/src/scenarios/setup.rs Outdated
@erickcestari

Copy link
Copy Markdown
Contributor Author

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.

Sorry. 🤦 That explains why it was so fast. I'm working on fixing it, applying your suggestions and trying new approaches to improve the exec/s even more.

@erickcestari
erickcestari force-pushed the optimize-eclair-exec branch from 337789d to cc6c54e Compare August 6, 2026 22:39
@erickcestari

erickcestari commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Applied an input that is able to finish the open_channel flow using the update version of bench-exec #160

before:

Smite Nyx benchmark
  target:      eclair/ir
  sharedir:    /tmp/smite-nyx
  input size:  405 bytes
  input buffer:1048576 bytes
  iterations:  100
  repeats:     10

  aggregate over 10 runs:
    execs/sec: mean 2.7  stddev 0.1  min 2.5  max 2.9
    boot + snapshot: mean 13.207 s
    latency (mean across runs):
      min 347.71 ms  mean 368.01 ms  median 364.07 ms  p99 386.50 ms  max 388.43 ms
    input execution (mean across runs): mean 358.29 ms  median 354.37 ms
    nyx overhead (mean across runs):     mean 9.72 ms  median 9.70 ms
    failed iterations: 0 / 1000
    coverage determinism: 75.7% stable (28616 / 117850 edges fluctuated, summed over runs)

after:

Smite Nyx benchmark
  target:      eclair/ir
  sharedir:    /tmp/smite-nyx
  input size:  405 bytes
  input buffer:1048576 bytes
  iterations:  100
  repeats:     10

  aggregate over 10 runs:
    execs/sec: mean 5.5  stddev 0.8  min 4.4  max 6.4
    boot + snapshot: mean 15.194 s
    latency (mean across runs):
      min 176.30 ms  mean 184.48 ms  median 184.39 ms  p99 200.72 ms  max 211.76 ms
    input execution (mean across runs): mean 177.47 ms  median 177.36 ms
    nyx overhead (mean across runs):     mean 7.02 ms  median 7.02 ms
    failed iterations: 0 / 1000
    coverage determinism: 78.3% stable (24436 / 112817 edges fluctuated, summed over runs)

after without jit_freeze:

Smite Nyx benchmark
  target:      eclair/ir
  sharedir:    /tmp/smite-nyx
  input size:  405 bytes
  input buffer:1048576 bytes
  iterations:  100
  repeats:     10

  aggregate over 10 runs:
    execs/sec: mean 3.0  stddev 0.3  min 2.7  max 3.5
    boot + snapshot: mean 13.444 s
    latency (mean across runs):
      min 267.18 ms  mean 330.86 ms  median 334.45 ms  p99 351.79 ms  max 366.28 ms
    input execution (mean across runs): mean 316.70 ms  median 320.09 ms
    nyx overhead (mean across runs):     mean 14.16 ms  median 14.38 ms
    failed iterations: 0 / 1000
    coverage determinism: 71.8% stable (32724 / 116125 edges fluctuated, summed over runs)

@erickcestari

Copy link
Copy Markdown
Contributor Author

The second commit is needed since reducing the Nyx VM from 4096 MB to 2048 MB halves the tmpfs cap on /tmp, leaving too little space to unpack container.tar (which was increased by the full JDK final build image). tar fails with ENOSPC and the guest aborts before the target ever starts.

@erickcestari
erickcestari requested a review from morehouse August 6, 2026 22:56
Eclair's channel path is still interpreted when the Nyx snapshot is taken,
so every restored execution re-pays the cold-JVM cost. Drive open_channel
exchanges before the snapshot so HotSpot compiles that path, then freeze
the JIT with a catch-all Exclude directive via jcmd, so no compiler
threads run during fuzzing. The image ships the full JDK for jcmd, and the
JVM runs with -XX:TieredStopAtLevel=1: C2 measured worse here, since its
speculation is tuned to the warmup path and varied fuzz inputs trip
uncommon traps and deoptimize on every restore.

Opens are strictly sequential and each is retired with an error for its
temporary channel id. Eclair evaluates opens in a single interceptor actor
per peer and rejects anything arriving while it is busy, so pipelined
opens warm nothing; the error closes the channel actor and frees the
pending-channel slot, which is otherwise only released on disconnect.
A rejected open aborts the setup rather than being retried: it means the
channel path never ran, and the cause is not transient.

Worth roughly 2x execs/sec against no warmup, measured with smitebot
bench-exec.
The Nyx guest has no disk: /tmp is a tmpfs that must hold the container
tarball, the tree extracted from it, and every byte the target writes at
runtime. Its default cap is half the VM's RAM, so unpacking a 600 MB image
needs a 3 GB VM; below that, tar fails with ENOSPC and the agent aborts with
an empty message that says nothing about what went wrong.

Raise the cap to 90% and drop the tarball once it is unpacked, so NYX_MEM_MB
is the only knob that governs how much memory the target actually gets. The
default 2048 MB now boots and snapshots Eclair with JIT warmup enabled, where
it previously required 4096.
@erickcestari
erickcestari force-pushed the optimize-eclair-exec branch from cc6c54e to 5acee05 Compare August 7, 2026 00:04
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