perf(core): reuse persistent worker subprocesses across translation units#524
Open
hedgar2017 wants to merge 4 commits into
Open
perf(core): reuse persistent worker subprocesses across translation units#524hedgar2017 wants to merge 4 commits into
hedgar2017 wants to merge 4 commits into
Conversation
f9371c4 to
2f00823
Compare
hedgar2017
commented
Jul 12, 2026
There was a problem hiding this comment.
Pull request overview
This PR refactors solx-core’s recursive subprocess compilation into a pool of persistent worker subprocesses using a framed CBOR protocol, aiming to eliminate per-translation-unit fork/exec and reduce IPC overhead while keeping outputs byte-identical.
Changes:
- Introduces a framed CBOR channel and splits compilation payload into
Session(sent once) + per-unitJob. - Adds a parent-side
Pool/Workerfor persistent subprocess reuse and a child-side long-lived worker loop. - Updates compilation pipeline call sites and LLVM option handling to avoid cross-job option leakage.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| solx/tests/cli/recursive_process.rs | Updates CLI expectations for the new worker protocol/argument behavior. |
| solx-core/src/project/mod.rs | Switches multi-pass compilation to dispatch jobs through the persistent worker pool. |
| solx-core/src/project/contract/mod.rs | Adjusts compile_to_evm API to take output_selection by reference; removes per-unit stack handler install. |
| solx-core/src/process/mod.rs | Replaces single-shot subprocess model with new process module structure for persistent workers. |
| solx-core/src/process/channel.rs | Adds framed CBOR send/recv helpers shared by parent/child. |
| solx-core/src/process/session.rs | Defines session payload shared across jobs and sent once per worker. |
| solx-core/src/process/job.rs | Defines per-translation-unit job payload. |
| solx-core/src/process/pool.rs | Adds worker pooling, reuse policy, and retry-on-reused-worker-death behavior. |
| solx-core/src/process/worker.rs | Implements parent-side subprocess spawn + framed I/O. |
| solx-core/src/process/child.rs | Implements child-side long-lived worker loop and stack-too-deep handler response path. |
| solx-core/src/process/output.rs | Minor documentation cleanup for the output type. |
| solx-core/src/lib.rs | Updates exports to expose new pool/session/job and subprocess entrypoint. |
| solx-core/src/compiler.rs | Reorders initialization to avoid building rayon pool in worker subprocess mode. |
| solx-core/src/arguments.rs | Updates --recursive-process semantics/docs and argument validation expectations. |
| solx-codegen-evm/src/target_machine.rs | Ensures -evm-metadata-size is always passed to heal stale process-global LLVM options. |
| solx-codegen-evm/src/codegen/context/mod.rs | Adjusts size-fallback flag handling and propagates metadata sizing into fallback settings. |
…nits Replace the fork/exec-per-unit model with a pool of persistent workers fed over pipes. The per-unit CBOR payload is split into a Session (project-wide data, sent once per worker) and a slim per-unit Job, and idle workers are held in a std Mutex<Vec> and reused. The process module is one entity per file: channel (the length-prefixed CBOR frame codec, via FrameRead/FrameWrite extension traits), session, job, output, pool (parent-side scheduler), worker (subprocess handle), and child (the subprocess loop). The child runs one long-lived stack-sized thread that owns the deserialized Session and compiles jobs until stdin closes, instead of spawning a thread and re-cloning the session per job. Worker stderr is inherited, so subprocess diagnostics stream straight to the parent. Because LLVM command-line options are process-global and now survive across units in one worker, always emit -evm-metadata-size to heal a stale value, and carry metadata_size into the size-fallback settings; reset IS_SIZE_FALLBACK per job. A worker is retired when its job set a spill-area size (occurrence- gated cl-option) or carried extra llvm_options, and never reused after a stack-too-deep response since its handler has already exited the process. When a reused worker dies mid-job, the job is retried once on a fresh worker so the death is not mis-attributed to the contract. Install the stack-error handler once per worker process instead of per unit, and build the rayon pool only in the parent, after the recursive-process branch, since workers compile a single unit and never use it.
2f00823 to
8276ba7
Compare
|
📊 solx Tester Report ➡️ Download |
|
📊 Hardhat Projects Report ➡️ Download |
Worker::execute now returns a plain crate::Result<EVMOutput> — a subprocess I/O failure folds into Error::Generic — so the ? drops a dead or errored worker and the pool is just pop-or-spawn, run, and return. This removes the retry, the WorkerError enum, dispatch, and worker_failed. Whether a worker may be reused lives on the data owners: Session::allows_worker_reuse (no extra llvm_options) and Job::allows_worker_reuse (no stack-too-deep spill area), each documenting the process-global cl-option it guards against. The cached reuse_workers field is removed. run_multi_pass_pipeline breaks the loop with its value instead of a declare-then-assign, and no longer threads the contract name.
Compile-time benchmark (
|
| binary | mean ± σ | min … max | vs baseline |
|---|---|---|---|
| pr | 2.949 s ± 0.015 s | 2.934 s … 2.963 s | 1.004× |
| main | 2.937 s ± 0.032 s | 2.916 s … 2.973 s | 1.000× |
| release | 6.635 s ± 0.064 s | 6.565 s … 6.689 s | 2.259× |
openzeppelin-contracts-0.34--solx-legacy-dwarf
| binary | mean ± σ | min … max | vs baseline |
|---|---|---|---|
| pr | 32.938 s ± 0.175 s | 32.743 s … 33.082 s | 0.959× |
| main | 34.345 s ± 0.218 s | 34.163 s … 34.586 s | 1.000× |
| release | 241.584 s ± 15.953 s | 227.797 s … 259.058 s | 7.034× |
uniswap-v4-core-solx--solx-legacy-dwarf
| binary | mean ± σ | min … max | vs baseline |
|---|---|---|---|
| pr | 16.970 s ± 0.048 s | 16.914 s … 17.002 s | 0.997× |
| main | 17.029 s ± 0.032 s | 16.992 s … 17.049 s | 1.000× |
| release | 84.769 s ± 4.238 s | 80.623 s … 89.093 s | 4.978× |
Reset the process-global LLVM command-line option occurrences before every parse (new LLVMResetAllOptionOccurrences, exposed through llvm-sys and inkwell), so a translation unit never inherits an option a previous one set in the same persistent worker. This removes the whole option-leak layer: the -evm-metadata-size=0 heal is gone, and the pool reuses a worker unconditionally on success — Session/Job::allows_worker_reuse and the spill/llvm_options gating are deleted. Points solx-llvm, llvm-sys, and inkwell at their az-reset-option-occurrences branches. Byte-identical on the EVMLA smoke; CLI suite 397/397.
|
📊 Foundry Projects Report ➡️ Download |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the fork/exec-per-translation-unit subprocess model with a pool of persistent worker subprocesses fed over pipes, and slims the per-unit IPC payload. Previously every translation unit fork/exec'd the 55 MB LLVM-linked binary, CBOR-encoded the whole payload, and each child built an unused N-thread rayon pool.
Local results with the raw openzeppelin standard JSON (average of 5 runs):
solx-old)solx-new)