Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions src/action/compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,8 @@ void performEVMJITCompile(runtime::EVMModule &Mod) {
switch (Mod.getRuntime()->getConfig().Mode) {
#ifdef ZEN_ENABLE_MULTIPASS_JIT
case common::RunMode::MultipassMode: {
if (Mod.getRuntime()->getConfig().EnableMultipassLazy) {
ZEN_LOG_WARN("EVMJIT does not support lazy compilation now");
} else {
COMPILER::EagerEVMJITCompiler ECompiler(&Mod);
ECompiler.compile();
}
COMPILER::EagerEVMJITCompiler ECompiler(&Mod);
ECompiler.compile();
break;
}
#endif
Expand Down
5 changes: 4 additions & 1 deletion src/cli/dtvm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,10 @@ int main(int argc, char *argv[]) {
"determination)")
->excludes(DMMOption);
CLIParser->add_flag("--enable-multipass-lazy", Config.EnableMultipassLazy,
"Enable multipass lazy mode(on request compile)");
"Enable multipass lazy mode (on request compile)");
CLIParser->add_flag("--enable-profile-guided-jit",
Config.EnableProfileGuidedJIT,
"Enable profile-guided JIT mode");
CLIParser->add_option("--entry-hint", EntryHint, "Entry function hint");
#ifdef ZEN_ENABLE_EVM
CLIParser->add_flag("--enable-evm-gas", Config.EnableEvmGasMetering,
Expand Down
113 changes: 74 additions & 39 deletions src/compiler/evm_compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,61 +56,96 @@ void EVMJITCompiler::compileEVMToMC(EVMFrontendContext &Ctx, MModule &Mod,
}

void EagerEVMJITCompiler::compile() {
// Start the timer outside the try-block so a scope guard can always release
// the in-flight TimerPair, even if the body throws. On the success path we
// set Committed = true and the guard switches to stopRecord(); on any
// exception path it falls back to revertRecord() and avoids leaking the
// stack entry maintained by Statistics.
auto Timer = Stats.startRecord(zen::utils::StatisticPhase::JITCompilation);

EVMFrontendContext Ctx;
Ctx.setGasMeteringEnabled(Config.EnableEvmGasMetering);
bool Committed = false;
// Capture by reference so the destructor sees the final Committed value.
// StatisticTimer is a private type alias, so we keep the guard's captures
// generic via auto/templated lambda + a small RAII shim.
auto Finalize = [&Stats = this->Stats, Timer, &Committed]() noexcept {
if (Committed) {
Stats.stopRecord(Timer);
} else {
Stats.revertRecord(Timer);
}
};
struct TimerScopeGuard {
decltype(Finalize) F;
~TimerScopeGuard() { F(); }
} TimerGuard{Finalize};

Comment on lines 58 to 80
try {
EVMFrontendContext Ctx;
Ctx.setGasMeteringEnabled(Config.EnableEvmGasMetering);
#ifdef ZEN_ENABLE_EVM_GAS_REGISTER
Ctx.setGasRegisterEnabled(true);
Ctx.setGasRegisterEnabled(true);
#endif
Ctx.setRevision(EVMMod->getRevision());
Ctx.setBytecode(reinterpret_cast<const Byte *>(EVMMod->Code),
EVMMod->CodeSize);
Ctx.setMemoryLinearStrideSkipLeadingZeroLimbStores(
EVMMod->getMemoryLinearStrideSkipLeadingZeroLimbStores());
const auto &Cache = EVMMod->getBytecodeCache();
Ctx.setGasChunkInfo(Cache.GasChunkEnd.data(), Cache.GasChunkCost.data(),
EVMMod->CodeSize);

MModule Mod(Ctx);
buildEVMFunction(Ctx, Mod, *EVMMod);
Ctx.CodeMPool = &EVMMod->getJITCodeMemPool();
Ctx.setRevision(EVMMod->getRevision());
Ctx.setBytecode(reinterpret_cast<const Byte *>(EVMMod->Code),
EVMMod->CodeSize);
Ctx.setMemoryLinearStrideSkipLeadingZeroLimbStores(
EVMMod->getMemoryLinearStrideSkipLeadingZeroLimbStores());
const auto &Cache = EVMMod->getBytecodeCache();
Ctx.setGasChunkInfo(Cache.GasChunkEnd.data(), Cache.GasChunkCost.data(),
EVMMod->CodeSize);

MModule Mod(Ctx);
buildEVMFunction(Ctx, Mod, *EVMMod);
Ctx.CodeMPool = &EVMMod->getJITCodeMemPool();

#ifdef ZEN_ENABLE_LINUX_PERF
utils::JitDumpWriter JitDumpWriter;
utils::JitDumpWriter JitDumpWriter;
#define JIT_DUMP_WRITE_FUNC(FuncName, FuncAddr, FuncSize) \
JitDumpWriter.writeFunc(FuncName, reinterpret_cast<uint64_t>(FuncAddr), \
FuncSize)
#else
#define JIT_DUMP_WRITE_FUNC(...)
#endif // ZEN_ENABLE_LINUX_PERF

auto &CodeMPool = EVMMod->getJITCodeMemPool();
uint8_t *JITCode = const_cast<uint8_t *>(CodeMPool.getMemStart());
auto &CodeMPool = EVMMod->getJITCodeMemPool();
uint8_t *JITCode = const_cast<uint8_t *>(CodeMPool.getMemStart());

// EVM has only 1 function, use direct single-threaded compilation
compileEVMToMC(Ctx, Mod, 0, Config.DisableMultipassGreedyRA);
emitObjectBuffer(&Ctx);
ZEN_ASSERT(Ctx.ExternRelocs.empty());
// EVM has only 1 function, use direct single-threaded compilation
compileEVMToMC(Ctx, Mod, 0, Config.DisableMultipassGreedyRA);
emitObjectBuffer(&Ctx);
ZEN_ASSERT(Ctx.ExternRelocs.empty());

uint8_t *JITFuncPtr = Ctx.CodePtr + Ctx.FuncOffsetMap[0];
EVMMod->setJITCodeAndSize(JITFuncPtr, Ctx.CodeSize);
uint8_t *JITFuncPtr = Ctx.CodePtr + Ctx.FuncOffsetMap[0];
#ifdef ZEN_ENABLE_LINUX_PERF
// Write block symbols instead of EVM_Main
// JIT_DUMP_WRITE_FUNC("EVM_Main", JITFuncPtr, Ctx.FuncSizeMap[0]);
for (const auto &[BBIdx, BBSymOffset] : Ctx.FuncOffsetMap) {
if (BBIdx == 0) {
continue;
// Write block symbols instead of EVM_Main
// JIT_DUMP_WRITE_FUNC("EVM_Main", JITFuncPtr, Ctx.FuncSizeMap[0]);
for (const auto &[BBIdx, BBSymOffset] : Ctx.FuncOffsetMap) {
if (BBIdx == 0) {
continue;
}
uint8_t *BBCode = Ctx.CodePtr + BBSymOffset;
JIT_DUMP_WRITE_FUNC(Ctx.FuncNameMap[BBIdx], BBCode,
Ctx.FuncSizeMap[BBIdx]);
}
uint8_t *BBCode = Ctx.CodePtr + BBSymOffset;
JIT_DUMP_WRITE_FUNC(Ctx.FuncNameMap[BBIdx], BBCode, Ctx.FuncSizeMap[BBIdx]);
}
#endif
size_t CodeSize = CodeMPool.getMemEnd() - JITCode;
platform::mprotect(JITCode, TO_MPROTECT_CODE_SIZE(CodeSize),
PROT_READ | PROT_EXEC);
EVMMod->setJITCodeAndSize(JITCode, CodeSize);

Stats.stopRecord(Timer);
// mprotect must cover the whole code mempool starting from JITCode (the
// page-aligned mempool start) so the entire executable buffer becomes RX.
// The size we publish, however, must be measured from JITFuncPtr (the
// actual function entry we hand to the runtime); otherwise consumers that
// compute getJITCode() + getJITCodeSize() — e.g. trap handlers — would
// walk past the end of the allocation.
size_t MProtectSize = CodeMPool.getMemEnd() - JITCode;
platform::mprotect(JITCode, TO_MPROTECT_CODE_SIZE(MProtectSize),
PROT_READ | PROT_EXEC);
size_t PublishedCodeSize = CodeMPool.getMemEnd() - JITFuncPtr;
// Publish JITFuncPtr only after mprotect — atomic release ensures the
// interpreter thread sees fully executable code.
EVMMod->setJITCodeAndSize(JITFuncPtr, PublishedCodeSize);

Committed = true;
} catch (const std::exception &E) {
ZEN_LOG_ERROR("EVM JIT compilation failed: %s", E.what());
} catch (...) {
ZEN_LOG_ERROR("EVM JIT compilation failed");
}
}
} // namespace COMPILER
2 changes: 2 additions & 0 deletions src/compiler/evm_frontend/evm_mir_compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,8 @@ void EVMMirBuilder::initEVM(CompilerContext *Context) {
ReturnBB = createBasicBlock();
loadEVMInstanceAttr();

// Normal execution continues from here (bytecode at PC=0)

GasChunkEnd = EvmCtx->getGasChunkEnd();
GasChunkCost = EvmCtx->getGasChunkCost();
GasChunkSize = EvmCtx->getGasChunkSize();
Expand Down
25 changes: 24 additions & 1 deletion src/runtime/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,22 @@ struct RuntimeConfig {
bool DisableMultipassMultithread = false;
// Number of threads for multipass JIT if DisableMultipassMultithread is false
uint32_t NumMultipassThreads = 8;
// Enable multipass lazy mode(on request compile)
// Enable WASM multipass lazy compilation (segment-based lazy compile)
bool EnableMultipassLazy = false;
// Enable profile-guided JIT:
// contracts start in interpreter mode; runtime profiling (sliding window
// of recent calls) determines which contracts are hot enough to trigger
// background JIT compilation. Modules are NOT compiled at load time.
bool EnableProfileGuidedJIT = false;
// Maximum number of concurrent background JIT compilation threads.
uint32_t NumJITCompileThreads = 10;
Comment on lines +42 to +48
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

NumJITCompileThreads can be set to 0 programmatically, but JITCompilePool(0) will accept tasks that never run, and shutdown/destruction can then block forever waiting for unfinished futures. Add validation (e.g., in RuntimeConfig::validate) to ensure NumJITCompileThreads >= 1 (and ideally cap it consistently with set_option’s limit) when profile-guided JIT is enabled.

Copilot uses AI. Check for mistakes.
// Profile-guided JIT trigger thresholds (configurable for testing).
// Defaults match profile::JIT_TRIGGER_CALL_COUNT / JIT_TRIGGER_TOTAL_GAS.
uint64_t JITTriggerCallCount = 32;
uint64_t JITTriggerTotalGas = 100000;
// Eager JIT: synchronously compile every contract on first call instead
// of waiting for the profiling window to fill. For testing only.
bool EnableEagerJIT = false;
#endif // ZEN_ENABLE_MULTIPASS_JIT

bool validate() {
Expand Down Expand Up @@ -67,6 +81,15 @@ struct RuntimeConfig {
"multipass JIT multithread enabled but thread number is 0");
return false;
}
if (EnableProfileGuidedJIT && NumJITCompileThreads == 0) {
// A zero-sized JITCompilePool would silently accept tasks that
// never run, and shutdown/destruction would block forever waiting
// for unfinished futures. Require at least one worker thread when
// profile-guided JIT is enabled.
ZEN_LOG_FATAL(
"profile-guided JIT enabled but NumJITCompileThreads is 0");
return false;
}
#else
ZEN_LOG_FATAL("enable multipass JIT but not supported, please recompile "
"with -DZEN_ENABLE_MULTIPASS_JIT=ON");
Expand Down
35 changes: 31 additions & 4 deletions src/runtime/evm_module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ EVMModule::EVMModule(Runtime *RT)
}

EVMModule::~EVMModule() {
#ifdef ZEN_ENABLE_JIT
if (JITCompileFuture.valid()) {
// Use a bounded wait to avoid hanging forever if the compilation
// thread is stuck. 30 seconds is generous for any single contract
// compilation; if it hasn't finished by then, something is wrong.
auto Status = JITCompileFuture.wait_for(std::chrono::seconds(30));
if (Status == std::future_status::timeout) {
ZEN_LOG_ERROR("JIT compilation timed out during module destruction; "
"leaking compile thread to avoid deadlock");
}
}
#endif

if (Name) {
this->freeSymbol(Name);
Name = common::WASM_SYMBOL_NULL;
Expand Down Expand Up @@ -103,18 +116,32 @@ EVMModule::newEVMModule(Runtime &RT, CodeHolderUniquePtr CodeHolder,
if (RT.getConfig().Mode != common::RunMode::InterpMode) {
#ifdef ZEN_ENABLE_JIT_PRECOMPILE_FALLBACK
// Run the EVMAnalyzer once at module creation to determine if this
// contract should fall back to interpreter. This avoids per-call O(n)
// bytecode scans in the execute() hot path.
// contract should fall back to interpreter. We do this even in
// profile-guided JIT mode so the background trigger can rely on the
// same persisted decision instead of re-evaluating per call.
COMPILER::EVMAnalyzer Analyzer(Rev);
Analyzer.analyze(reinterpret_cast<const uint8_t *>(Mod->Code),
Mod->CodeSize);
Mod->ShouldFallbackToInterp =
Analyzer.getJITSuitability().ShouldFallback ||
hasUnresolvedCompatibleDynamicReturnTrampoline(Analyzer);
if (!Mod->ShouldFallbackToInterp)
#endif // ZEN_ENABLE_JIT_PRECOMPILE_FALLBACK

#ifdef ZEN_ENABLE_MULTIPASS_JIT
if (RT.getConfig().EnableProfileGuidedJIT) {
// Profile-guided JIT: skip JIT compilation at load time.
// JIT will be triggered later by the profiling logic in execute().
// Eagerly init bytecode cache for interpreter use.
(void)Mod->getBytecodeCache();
} else
Comment on lines 116 to +136
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

When EnableProfileGuidedJIT is true, module creation skips the EVMAnalyzer and never sets ShouldFallbackToInterp. That means contracts that should permanently fall back to interpreter mode can still get JIT-triggered later, and the execute() hot path comment about the flag being set at creation becomes false. Consider running the analyzer even in profile-guided mode to set ShouldFallbackToInterp (without compiling), or otherwise persist the rejection decision onto the module so execute() can reliably avoid JIT for unsupported contracts.

Copilot uses AI. Check for mistakes.
#endif
{
action::performEVMJITCompile(*Mod);
#ifdef ZEN_ENABLE_JIT_PRECOMPILE_FALLBACK
if (!Mod->ShouldFallbackToInterp)
#endif // ZEN_ENABLE_JIT_PRECOMPILE_FALLBACK
{
action::performEVMJITCompile(*Mod);
}
}
}

Expand Down
14 changes: 11 additions & 3 deletions src/runtime/evm_module.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#include "evmc/evmc.hpp"
#include "runtime/evm_memory_specialization.h"
#include "runtime/module.h"
#include <atomic>
#include <future>
#include <limits>
#include <memory>

Expand Down Expand Up @@ -80,14 +82,20 @@ class EVMModule final : public BaseModule<EVMModule> {
return *JITCodeMemPool;
}

void *getJITCode() const { return JITCode; }
void *getJITCode() const { return JITCode.load(std::memory_order_acquire); }

size_t getJITCodeSize() const { return JITCodeSize; }

void setJITCodeAndSize(void *Code, size_t Size) {
JITCode = Code;
JITCodeSize = Size;
JITCode.store(Code, std::memory_order_release);
}
// Future for background JIT compilation (managed by JITCompilePool).
std::future<void> JITCompileFuture;

// Per-module execution statistics for profile-guided JIT diagnostics.
uint64_t ModuleExecuteCount = 0;
uint64_t ModuleFirstJITAtCall = 0;
#endif // ZEN_ENABLE_JIT

private:
Expand All @@ -109,7 +117,7 @@ class EVMModule final : public BaseModule<EVMModule> {

#ifdef ZEN_ENABLE_JIT
std::unique_ptr<common::CodeMemPool> JITCodeMemPool;
void *JITCode = nullptr;
std::atomic<void *> JITCode{nullptr};
size_t JITCodeSize = 0;
#endif // ZEN_ENABLE_JIT
};
Expand Down
15 changes: 10 additions & 5 deletions src/runtime/runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -724,14 +724,19 @@ void Runtime::callEVMMainOnPhysStack(EVMInstance &Inst, evmc_message &Msg,
MsgWithCode.code_size = Inst.getModule()->CodeSize;
Inst.setExeResult(evmc::Result{EVMC_SUCCESS, 0, 0});
Inst.pushMessage(&MsgWithCode);
if (getConfig().Mode == RunMode::InterpMode) {
callEVMInInterpMode(Inst, MsgWithCode, Result);
} else {

bool UseJIT = false;
#ifdef ZEN_ENABLE_JIT
UseJIT = (getConfig().Mode != RunMode::InterpMode) &&
Inst.getModule()->getJITCode();
#endif

if (UseJIT) {
#ifdef ZEN_ENABLE_JIT
callEVMInJITMode(Inst, MsgWithCode, Result);
#else
ZEN_UNREACHABLE();
#endif
} else {
callEVMInInterpMode(Inst, MsgWithCode, Result);
}
Result.gas_left = Inst.getGas();
}
Expand Down
14 changes: 12 additions & 2 deletions src/tests/solidity_contract_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,18 @@ GTEST_API_ int main(int argc, char **argv) {
"Number of threads for multipass JIT(set 0 for automatic "
"determination)")
->excludes(DMMOption);
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

This test binary previously exposed --enable-multipass-lazy like the other CLIs/tests, but that option is removed here. If existing CI scripts or local workflows pass --enable-multipass-lazy to this binary, they'll now fail argument parsing. Consider keeping the flag for parity/backward-compatibility (even if unused for solidity tests) alongside the new --enable-profile-guided-jit option.

Suggested change
->excludes(DMMOption);
->excludes(DMMOption);
bool EnableMultipassLazyCompatibility = false;
CLIParser.add_flag(
"--enable-multipass-lazy", EnableMultipassLazyCompatibility,
"Deprecated compatibility flag accepted for parity with other "
"multipass JIT CLIs; ignored by solidity contract tests");

Copilot uses AI. Check for mistakes.
CLIParser.add_flag("--enable-multipass-lazy", Config.EnableMultipassLazy,
"Enable multipass lazy mode(on request compile)");
CLIParser.add_flag("--enable-profile-guided-jit",
Config.EnableProfileGuidedJIT,
"Enable profile-guided JIT mode");
// Deprecated compatibility flag: kept for parity with other multipass JIT
// CLIs/test binaries so existing CI scripts that pass
// --enable-multipass-lazy do not fail argument parsing. The flag has no
// effect on solidity contract tests.
bool EnableMultipassLazyCompatibility = false;
CLIParser.add_flag(
"--enable-multipass-lazy", EnableMultipassLazyCompatibility,
"Deprecated compatibility flag accepted for parity with other "
"multipass JIT CLIs; ignored by solidity contract tests");
#endif // ZEN_ENABLE_MULTIPASS_JIT
CLI11_PARSE(CLIParser, argc, argv);

Expand Down
3 changes: 3 additions & 0 deletions src/tests/spec_unit_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ GTEST_API_ int main(int argc, char *argv[]) {
->excludes(DMMOption);
CLIParser.add_flag("--enable-multipass-lazy", Config.EnableMultipassLazy,
"Enable multipass lazy mode(on request compile)");
CLIParser.add_flag("--enable-profile-guided-jit",
Config.EnableProfileGuidedJIT,
"Enable profile-guided JIT mode");
#endif // ZEN_ENABLE_MULTIPASS_JIT

CLI11_PARSE(CLIParser, argc, argv);
Expand Down
Loading
Loading