From 0b89021d90cae494a90daf97c26d8d496a89b7d4 Mon Sep 17 00:00:00 2001 From: Outcry <843648230@qq.com> Date: Sat, 9 May 2026 08:25:10 +0000 Subject: [PATCH 1/5] feat(evm): add profile-guided JIT with thread pool and sliding window profiling --- src/action/compiler.cpp | 8 +- src/cli/dtvm.cpp | 5 +- src/compiler/evm_compiler.cpp | 113 ++++-- .../evm_frontend/evm_mir_compiler.cpp | 2 + src/runtime/config.h | 18 +- src/runtime/evm_module.cpp | 35 +- src/runtime/evm_module.h | 10 +- src/runtime/runtime.cpp | 15 +- src/tests/solidity_contract_tests.cpp | 14 +- src/tests/spec_unit_tests.cpp | 3 + src/vm/dt_evmc_vm.cpp | 364 +++++++++++++----- src/vm/jit_profile.h | 166 ++++++++ 12 files changed, 598 insertions(+), 155 deletions(-) create mode 100644 src/vm/jit_profile.h diff --git a/src/action/compiler.cpp b/src/action/compiler.cpp index 33317e03e..69126438b 100644 --- a/src/action/compiler.cpp +++ b/src/action/compiler.cpp @@ -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 diff --git a/src/cli/dtvm.cpp b/src/cli/dtvm.cpp index a2dce7b0c..feeee89d3 100644 --- a/src/cli/dtvm.cpp +++ b/src/cli/dtvm.cpp @@ -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, diff --git a/src/compiler/evm_compiler.cpp b/src/compiler/evm_compiler.cpp index f7b908c7a..c990c18f5 100644 --- a/src/compiler/evm_compiler.cpp +++ b/src/compiler/evm_compiler.cpp @@ -56,28 +56,49 @@ 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}; + + 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(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(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(FuncAddr), \ FuncSize) @@ -85,32 +106,46 @@ void EagerEVMJITCompiler::compile() { #define JIT_DUMP_WRITE_FUNC(...) #endif // ZEN_ENABLE_LINUX_PERF - auto &CodeMPool = EVMMod->getJITCodeMemPool(); - uint8_t *JITCode = const_cast(CodeMPool.getMemStart()); + auto &CodeMPool = EVMMod->getJITCodeMemPool(); + uint8_t *JITCode = const_cast(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 diff --git a/src/compiler/evm_frontend/evm_mir_compiler.cpp b/src/compiler/evm_frontend/evm_mir_compiler.cpp index 3b04a5784..b616d1d3a 100644 --- a/src/compiler/evm_frontend/evm_mir_compiler.cpp +++ b/src/compiler/evm_frontend/evm_mir_compiler.cpp @@ -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(); diff --git a/src/runtime/config.h b/src/runtime/config.h index c8a9b301f..3d0b635f3 100644 --- a/src/runtime/config.h +++ b/src/runtime/config.h @@ -37,8 +37,15 @@ 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; #endif // ZEN_ENABLE_MULTIPASS_JIT bool validate() { @@ -67,6 +74,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"); diff --git a/src/runtime/evm_module.cpp b/src/runtime/evm_module.cpp index a3e3177f3..56b8c2afa 100644 --- a/src/runtime/evm_module.cpp +++ b/src/runtime/evm_module.cpp @@ -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; @@ -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(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 +#endif { - action::performEVMJITCompile(*Mod); +#ifdef ZEN_ENABLE_JIT_PRECOMPILE_FALLBACK + if (!Mod->ShouldFallbackToInterp) +#endif // ZEN_ENABLE_JIT_PRECOMPILE_FALLBACK + { + action::performEVMJITCompile(*Mod); + } } } diff --git a/src/runtime/evm_module.h b/src/runtime/evm_module.h index 60ea5b62d..4f0904e41 100644 --- a/src/runtime/evm_module.h +++ b/src/runtime/evm_module.h @@ -8,6 +8,8 @@ #include "evmc/evmc.hpp" #include "runtime/evm_memory_specialization.h" #include "runtime/module.h" +#include +#include #include #include @@ -80,14 +82,16 @@ class EVMModule final : public BaseModule { 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 JITCompileFuture; #endif // ZEN_ENABLE_JIT private: @@ -109,7 +113,7 @@ class EVMModule final : public BaseModule { #ifdef ZEN_ENABLE_JIT std::unique_ptr JITCodeMemPool; - void *JITCode = nullptr; + std::atomic JITCode{nullptr}; size_t JITCodeSize = 0; #endif // ZEN_ENABLE_JIT }; diff --git a/src/runtime/runtime.cpp b/src/runtime/runtime.cpp index 078988533..6e2434adf 100644 --- a/src/runtime/runtime.cpp +++ b/src/runtime/runtime.cpp @@ -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(); } diff --git a/src/tests/solidity_contract_tests.cpp b/src/tests/solidity_contract_tests.cpp index 1b329f4f1..36a64d805 100644 --- a/src/tests/solidity_contract_tests.cpp +++ b/src/tests/solidity_contract_tests.cpp @@ -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); - 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); diff --git a/src/tests/spec_unit_tests.cpp b/src/tests/spec_unit_tests.cpp index d6adce389..1f1fed5cf 100644 --- a/src/tests/spec_unit_tests.cpp +++ b/src/tests/spec_unit_tests.cpp @@ -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); diff --git a/src/vm/dt_evmc_vm.cpp b/src/vm/dt_evmc_vm.cpp index 17961c8ea..45b44dd12 100644 --- a/src/vm/dt_evmc_vm.cpp +++ b/src/vm/dt_evmc_vm.cpp @@ -2,23 +2,31 @@ // SPDX-License-Identifier: Apache-2.0 #include "dt_evmc_vm.h" +#include "action/compiler.h" #include "common/enums.h" #include "common/errors.h" #include "evm/interpreter.h" #include "evm/opcode_handlers.h" +#include "jit_profile.h" #include "runtime/config.h" #include "runtime/evm_instance.h" #include "runtime/isolation.h" #include "runtime/runtime.h" #include "wrapped_host.h" +#ifdef ZEN_ENABLE_JIT_PRECOMPILE_FALLBACK +#include "compiler/evm_frontend/evm_analyzer.h" +#endif // ZEN_ENABLE_JIT_PRECOMPILE_FALLBACK +#include #include #include #include -#include +#include #include #include +#include +#include #ifdef ZEN_ENABLE_VIRTUAL_STACK #include "utils/virtual_stack.h" @@ -29,6 +37,11 @@ namespace { using namespace zen::runtime; using namespace zen::common; +using zen::vm::CallRecord; +using zen::vm::CallRingBuffer; +using zen::vm::ContractProfile; +using zen::vm::JITCompilePool; +namespace profile = zen::vm::profile; // RAII helper for temporarily changing runtime configuration class ScopedConfig { public: @@ -178,7 +191,9 @@ bool parseBoolEnvValue(const char *Value, bool &ParsedValue) { struct DTVM : evmc_vm { DTVM(); ~DTVM() { - // Clean up cached instance first (before modules it may reference) + // Drain the JIT compile thread pool first: wait for all in-flight + // compilation tasks to finish before unloading modules they reference. + CompilePool.reset(); if (CachedMainInst && Iso) { Iso->deleteEVMInstance(CachedMainInst); CachedMainInst = nullptr; @@ -248,6 +263,15 @@ struct DTVM : evmc_vm { // Instance pool for depth > 0 std::vector CacheInsts; + // ---- Profile-guided JIT state ---- + // Keyed by code_address (20 bytes) instead of a heap-allocated hex string; + // evmc ships a std::hash specialisation so the map works + // out of the box and we save an allocation per call on the hot path. + std::unordered_map ProfileStore; + CallRingBuffer RingBuffer{profile::RING_BUFFER_CAPACITY}; + // Thread pool for background JIT compilation (lazily initialized). + std::unique_ptr CompilePool; + bool isModuleInUse(const EVMModule *Mod) const { if (CachedMainInst && CachedMainInst->getModule() == Mod) return true; @@ -302,6 +326,22 @@ enum evmc_set_option_result set_option(evmc_vm *VMInstance, const char *Name, } else { return EVMC_SET_OPTION_INVALID_VALUE; } +#ifdef ZEN_ENABLE_MULTIPASS_JIT + } else if (std::strcmp(Name, "num_jit_compile_threads") == 0) { + int Parsed = std::atoi(Value); + if (Parsed > 0 && Parsed <= 256) { + VM->Config.NumJITCompileThreads = static_cast(Parsed); + return EVMC_SET_OPTION_SUCCESS; + } + return EVMC_SET_OPTION_INVALID_VALUE; + } else if (std::strcmp(Name, "profile_guided_jit") == 0) { + bool Parsed = false; + if (parseBoolEnvValue(Value, Parsed)) { + VM->Config.EnableProfileGuidedJIT = Parsed; + return EVMC_SET_OPTION_SUCCESS; + } + return EVMC_SET_OPTION_INVALID_VALUE; +#endif } return EVMC_SET_OPTION_INVALID_NAME; } @@ -321,6 +361,15 @@ bool ensureRuntimeAndIsolation(DTVM *VM) { return true; } +/// Lazily initialize the JIT compile thread pool. +JITCompilePool &getOrCreateCompilePool(DTVM *VM) { + if (!VM->CompilePool) { + VM->CompilePool = + std::make_unique(VM->Config.NumJITCompileThreads); + } + return *VM->CompilePool; +} + bool shouldUsePersistentModuleCache(const evmc_message *Msg) { // CREATE/CREATE2 initcode must not be cached: the same address can receive // different initcode across transactions, and initcode is one-shot. @@ -432,6 +481,7 @@ EVMModule *findModuleCached(DTVM *VM, const uint8_t *Code, size_t CodeSize, if (!ModRet) return nullptr; Mod = *ModRet; + VM->LRUOrder.push_front(AddrKey); VM->AddrCache[AddrKey] = {Mod, VM->LRUOrder.begin()}; } @@ -440,7 +490,8 @@ EVMModule *findModuleCached(DTVM *VM, const uint8_t *Code, size_t CodeSize, // these state variables for two reasons: // 1. Eviction tracking: If a stale L1 entry is replaced, we need to // invalidate - // L0Mod if it pointed to the old module (done in the eviction path above). + // L0Mod if it pointed to the old module (done in the eviction path + // above). // 2. Future extensibility: It keeps the door open for re-enabling L0 later // with a safer validation scheme (e.g., pointer + size + hash). VM->LastCodePtr = Code; @@ -501,38 +552,17 @@ EVMInstance *getOrCreateInstance(DTVM *VM, EVMModule *Mod, evmc_revision Rev, return TheInst; } -/// Fast path for interpreter mode: reuse cached instance, call interpreter -/// directly. This avoids per-call EVMInstance alloc/free and bypasses -/// Runtime::callEVMMain overhead. -evmc_result executeInterpreterFastPath(DTVM *VM, - const evmc_host_interface *Host, - evmc_host_context *Context, - evmc_revision Rev, - const evmc_message *Msg, - const uint8_t *Code, size_t CodeSize) { - // RAII guard for host context save/restore (exception safety) - HostContextScope HostScope(VM->ExecHost.get(), Host, Context); - - // Ensure runtime and isolation exist - if (!ensureRuntimeAndIsolation(VM)) { - return evmc_make_result(EVMC_FAILURE, 0, 0, nullptr, 0); - } - - // Module lookup: L1 address-based cache -> Cold load - bool IsTransientMod = false; - EVMModule *Mod = - findModuleCached(VM, Code, CodeSize, Rev, Msg, IsTransientMod); - if (!Mod) { - return evmc_make_result(EVMC_FAILURE, 0, 0, nullptr, 0); - } - ModuleGuard ModGuard(VM, Mod, IsTransientMod); - - // Instance reuse (shared only for cacheable top-level calls) - EVMInstance *TheInst = getOrCreateInstance(VM, Mod, Rev, Msg->depth); - if (!TheInst) { - return evmc_make_result(EVMC_FAILURE, 0, 0, nullptr, 0); - } - +/// Run the interpreter on an already-resolved (Mod, TheInst) pair. This is the +/// shared core used by both the public interpreter fast path and the +/// profile-guided JIT fall-back path inside the JIT-mode execute(): both +/// callers have just looked up the module and instance, and both want exactly +/// the same Ctx-pooling + interpret + result-extraction sequence. Keeping it +/// in one helper avoids the duplicated, drift-prone copy that lived in +/// execute() prior to PR #481 round-2 review. +evmc_result runInterpreterOnResolvedInstance(DTVM *VM, EVMModule *Mod, + EVMInstance *TheInst, + const evmc_message *Msg, + bool IsTransientMod) { // Trigger bytecodeCache build if not yet done (lazy, cached on module) (void)Mod->getBytecodeCache(); @@ -592,28 +622,15 @@ evmc_result executeInterpreterFastPath(DTVM *VM, return Result.release_raw(); } -#ifdef ZEN_ENABLE_JIT - -#ifdef ZEN_ENABLE_VIRTUAL_STACK -/// Virtual stack callback for JIT fast path: invoked with RSP on the virtual -/// stack. Follows the same pattern as callEVMFuncFromVirtualStack in -/// runtime.cpp. -static void callJITFromVirtualStack(zen::utils::VirtualStackInfo *StackInfo) { - auto *Inst = static_cast(StackInfo->SavedPtr1); - auto *Msg = static_cast(StackInfo->SavedPtr2); - auto *Result = static_cast(StackInfo->SavedPtr3); - Inst->getRuntime()->callEVMInJITMode(*Inst, *Msg, *Result); -} -#endif // ZEN_ENABLE_VIRTUAL_STACK - -/// Fast path for multipass JIT mode: reuse cached instance, call JIT code -/// directly. This avoids per-call callEVMMain overhead while delegating -/// actual JIT execution to Runtime::callEVMInJITMode (single source of truth -/// for CPU exception handling, error mapping, etc.). -evmc_result executeMultipassFastPath(DTVM *VM, const evmc_host_interface *Host, - evmc_host_context *Context, - evmc_revision Rev, const evmc_message *Msg, - const uint8_t *Code, size_t CodeSize) { +/// Fast path for interpreter mode: reuse cached instance, call interpreter +/// directly. This avoids per-call EVMInstance alloc/free and bypasses +/// Runtime::callEVMMain overhead. +evmc_result executeInterpreterFastPath(DTVM *VM, + const evmc_host_interface *Host, + evmc_host_context *Context, + evmc_revision Rev, + const evmc_message *Msg, + const uint8_t *Code, size_t CodeSize) { // RAII guard for host context save/restore (exception safety) HostContextScope HostScope(VM->ExecHost.get(), Host, Context); @@ -631,52 +648,121 @@ evmc_result executeMultipassFastPath(DTVM *VM, const evmc_host_interface *Host, } ModuleGuard ModGuard(VM, Mod, IsTransientMod); -#ifdef ZEN_ENABLE_JIT_PRECOMPILE_FALLBACK - // O(1) flag check replaces per-call O(n) EVMAnalyzer scan. - // The flag was set once at module creation in EVMModule::newEVMModule(). - if (Mod->ShouldFallbackToInterp) { - return executeInterpreterFastPath(VM, Host, Context, Rev, Msg, Code, - CodeSize); - } -#endif // ZEN_ENABLE_JIT_PRECOMPILE_FALLBACK - // Instance reuse (shared only for cacheable top-level calls) EVMInstance *TheInst = getOrCreateInstance(VM, Mod, Rev, Msg->depth); if (!TheInst) { return evmc_make_result(EVMC_FAILURE, 0, 0, nullptr, 0); } - // Setup message with code pointers (same pattern as interpreter fast path) - evmc_message MsgWithCode = *Msg; - MsgWithCode.code = reinterpret_cast(Mod->Code); - MsgWithCode.code_size = Mod->CodeSize; - TheInst->setExeResult(evmc::Result{EVMC_SUCCESS, 0, 0}); - TheInst->pushMessage(&MsgWithCode); + return runInterpreterOnResolvedInstance(VM, Mod, TheInst, Msg, + IsTransientMod); +} - evmc::Result Result; +/// Profile-guided JIT: record call and check if JIT should be triggered. +/// Called after execution completes with the result available. +#ifdef ZEN_ENABLE_MULTIPASS_JIT +void updateProfileAndMaybeTriggerJIT(DTVM *VM, const evmc_message *Msg, + const evmc_result &Result, + EVMModule *Mod) { + const evmc::address &ModAddr = Msg->code_address; + // evmc gas fields are signed (int64_t); a faulting call can leave + // gas_left == -1 or otherwise greater than Msg->gas, which would wrap + // to a huge uint64_t when cast and corrupt WindowGasUsed. Clamp to 0 + // and saturate to Msg->gas to keep the sliding-window accounting sane. + int64_t GasLeft = Result.gas_left; + if (GasLeft < 0) { + GasLeft = 0; + } else if (GasLeft > Msg->gas) { + GasLeft = Msg->gas; + } + uint64_t GasUsed = static_cast(Msg->gas - GasLeft); + + // 1. Push new record; may evict the oldest record. + auto Evicted = VM->RingBuffer.push(CallRecord{ModAddr, GasUsed}); + + // 2. Process evicted record: decrement its profile counters. + if (Evicted) { + auto EvictIt = VM->ProfileStore.find(Evicted->ModAddr); + if (EvictIt != VM->ProfileStore.end()) { + auto &OldProfile = EvictIt->second; + if (OldProfile.WindowCallCount > 0) + OldProfile.WindowCallCount--; + if (OldProfile.WindowGasUsed >= Evicted->GasUsed) + OldProfile.WindowGasUsed -= Evicted->GasUsed; + else + OldProfile.WindowGasUsed = 0; + + // Clean up zero-count profiles to bound ProfileStore memory. + // This only removes the *profiling statistics*; the module itself + // (and any installed JIT code) lives in AddrCache with its own LRU + // lifecycle, so erasing a profile entry does not invalidate cached + // modules or compiled code. + if (OldProfile.WindowCallCount == 0) { + VM->ProfileStore.erase(EvictIt); + } + } + } -#ifdef ZEN_ENABLE_VIRTUAL_STACK - if (Msg->depth == 0) { - // depth==0: set up virtual stack for stack overflow protection via guard - // pages. The virtual stack switches RSP to a separate mmap'd region. - zen::utils::VirtualStackInfo StackInfo; - StackInfo.SavedPtr1 = TheInst; - StackInfo.SavedPtr2 = &MsgWithCode; - StackInfo.SavedPtr3 = &Result; - TheInst->pushVirtualStack(&StackInfo); - StackInfo.runInVirtualStack(&callJITFromVirtualStack); - TheInst->popVirtualStack(); - } else { - // depth>0: re-entered via EVMC host callback, already on physical stack - VM->RT->callEVMInJITMode(*TheInst, MsgWithCode, Result); + // 3. Skip profile update for contracts already evaluated. + auto ExistIt = VM->ProfileStore.find(ModAddr); + if (ExistIt != VM->ProfileStore.end() && + (ExistIt->second.JITTriggered || ExistIt->second.JITRejected)) { + return; } -#else - VM->RT->callEVMInJITMode(*TheInst, MsgWithCode, Result); -#endif // ZEN_ENABLE_VIRTUAL_STACK - Result.gas_left = TheInst->getGas(); - return Result.release_raw(); + // 4. Update current contract profile (increment). + auto &CurrentProfile = VM->ProfileStore[ModAddr]; + CurrentProfile.WindowCallCount++; + CurrentProfile.WindowGasUsed += GasUsed; + + // 5. Check JIT trigger conditions. + if (CurrentProfile.WindowCallCount < profile::JIT_TRIGGER_CALL_COUNT || + CurrentProfile.WindowGasUsed < profile::JIT_TRIGGER_TOTAL_GAS) { + return; + } + +#ifdef ZEN_ENABLE_JIT_PRECOMPILE_FALLBACK + // Reuse the persisted suitability decision from EVMModule::newEVMModule() + // (which uses the full predicate: ShouldFallback || + // hasUnresolvedCompatibleDynamicReturnTrampoline). This keeps PGJIT's + // gating consistent with eager compilation and avoids per-trigger O(n) + // bytecode scans. + if (Mod->ShouldFallbackToInterp) { + CurrentProfile.JITRejected = true; + return; + } +#endif // ZEN_ENABLE_JIT_PRECOMPILE_FALLBACK + + // Avoid duplicate JIT triggers. + if ((Mod->JITCompileFuture.valid() && + Mod->JITCompileFuture.wait_for(std::chrono::seconds(0)) != + std::future_status::ready) || + Mod->getJITCode()) { + CurrentProfile.JITTriggered = true; + return; + } + + // Trigger background JIT compilation via thread pool. + CurrentProfile.JITTriggered = true; + auto &Pool = getOrCreateCompilePool(VM); + Mod->JITCompileFuture = + Pool.submit([Mod]() { zen::action::performEVMJITCompile(*Mod); }); +} +#endif // ZEN_ENABLE_MULTIPASS_JIT + +#ifdef ZEN_ENABLE_JIT + +#ifdef ZEN_ENABLE_VIRTUAL_STACK +/// Virtual stack callback for JIT fast path: invoked with RSP on the virtual +/// stack. Follows the same pattern as callEVMFuncFromVirtualStack in +/// runtime.cpp. +static void callJITFromVirtualStack(zen::utils::VirtualStackInfo *StackInfo) { + auto *Inst = static_cast(StackInfo->SavedPtr1); + auto *Msg = static_cast(StackInfo->SavedPtr2); + auto *Result = static_cast(StackInfo->SavedPtr3); + Inst->getRuntime()->callEVMInJITMode(*Inst, *Msg, *Result); } +#endif // ZEN_ENABLE_VIRTUAL_STACK #endif // ZEN_ENABLE_JIT /// The implementation of the evmc_vm::execute() method. @@ -693,8 +779,98 @@ evmc_result execute(evmc_vm *EVMInstance, const evmc_host_interface *Host, } #ifdef ZEN_ENABLE_JIT - // JIT mode: use optimized fast path (bypasses callEVMMain/virtual stack) - return executeMultipassFastPath(VM, Host, Context, Rev, Msg, Code, CodeSize); + { + // RAII guard for host context save/restore (exception safety) + HostContextScope HostScope(VM->ExecHost.get(), Host, Context); + + if (!ensureRuntimeAndIsolation(VM)) { + return evmc_make_result(EVMC_FAILURE, 0, 0, nullptr, 0); + } + + // Module lookup: L1 address-based cache -> Cold load + bool IsTransientMod = false; + EVMModule *Mod = + findModuleCached(VM, Code, CodeSize, Rev, Msg, IsTransientMod); + if (!Mod) { + return evmc_make_result(EVMC_FAILURE, 0, 0, nullptr, 0); + } + ModuleGuard ModGuard(VM, Mod, IsTransientMod); + +#ifdef ZEN_ENABLE_JIT_PRECOMPILE_FALLBACK + // O(1) flag check replaces per-call O(n) EVMAnalyzer scan. + // The flag was set once at module creation in EVMModule::newEVMModule(). + if (Mod->ShouldFallbackToInterp) { + return executeInterpreterFastPath(VM, Host, Context, Rev, Msg, Code, + CodeSize); + } +#endif // ZEN_ENABLE_JIT_PRECOMPILE_FALLBACK + + // Instance reuse (shared only for cacheable top-level calls) + auto *TheInst = getOrCreateInstance(VM, Mod, Rev, Msg->depth); + if (!TheInst) { + return evmc_make_result(EVMC_FAILURE, 0, 0, nullptr, 0); + } + +#ifdef ZEN_ENABLE_MULTIPASS_JIT + // Profile-guided JIT: while the JIT is not yet ready for this module, + // run the interpreter and (only for cacheable top-level invocations) + // record the call into the sliding-window profile. Once the background + // compile installs JITCode, subsequent calls naturally fall through to + // the JIT fast path below. Reusing runInterpreterOnResolvedInstance + // keeps the Ctx pooling and pre/post bookkeeping in lock-step with the + // public interpreter fast path, so the two paths can never drift. + if (!Mod->getJITCode() && VM->Config.EnableProfileGuidedJIT) { + evmc_result RawResult = runInterpreterOnResolvedInstance( + VM, Mod, TheInst, Msg, IsTransientMod); + if (shouldUsePersistentModuleCache(Msg)) { + updateProfileAndMaybeTriggerJIT(VM, Msg, RawResult, Mod); + } + return RawResult; + } +#endif // ZEN_ENABLE_MULTIPASS_JIT + + // JIT fast path: used by both non-PGJ mode and PGJ mode after JIT is + // ready. Module and instance are already resolved above. + // + // Safety: EagerEVMJITCompiler::compile() now catches exceptions and only + // logs on failure (see PR #481), so Mod->getJITCode() can legitimately be + // nullptr here. Fall back to the interpreter fast path in that case + // instead of jumping through a null function pointer. + if (!Mod->getJITCode()) { + return executeInterpreterFastPath(VM, Host, Context, Rev, Msg, Code, + CodeSize); + } + + evmc_message MsgWithCode = *Msg; + MsgWithCode.code = reinterpret_cast(Mod->Code); + MsgWithCode.code_size = Mod->CodeSize; + TheInst->setExeResult(evmc::Result{EVMC_SUCCESS, 0, 0}); + TheInst->pushMessage(&MsgWithCode); + + evmc::Result Result; + +#ifdef ZEN_ENABLE_VIRTUAL_STACK + if (Msg->depth == 0) { + // depth==0: set up virtual stack for stack overflow protection via guard + // pages. The virtual stack switches RSP to a separate mmap'd region. + zen::utils::VirtualStackInfo StackInfo; + StackInfo.SavedPtr1 = TheInst; + StackInfo.SavedPtr2 = &MsgWithCode; + StackInfo.SavedPtr3 = &Result; + TheInst->pushVirtualStack(&StackInfo); + StackInfo.runInVirtualStack(&callJITFromVirtualStack); + TheInst->popVirtualStack(); + } else { + // depth>0: re-entered via EVMC host callback, already on physical stack + VM->RT->callEVMInJITMode(*TheInst, MsgWithCode, Result); + } +#else + VM->RT->callEVMInJITMode(*TheInst, MsgWithCode, Result); +#endif // ZEN_ENABLE_VIRTUAL_STACK + + Result.gas_left = TheInst->getGas(); + return Result.release_raw(); + } #else return evmc_make_result(EVMC_FAILURE, 0, 0, nullptr, 0); #endif // ZEN_ENABLE_JIT diff --git a/src/vm/jit_profile.h b/src/vm/jit_profile.h new file mode 100644 index 000000000..dc6723988 --- /dev/null +++ b/src/vm/jit_profile.h @@ -0,0 +1,166 @@ +// Copyright (C) 2025 the DTVM authors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Profile-guided JIT: data structures and thread pool. +// +// Extracted from dt_evmc_vm.cpp so that the VM entry point file stays +// focused on the EVMC interface while profiling/compilation types live +// in their own header. + +#ifndef ZEN_VM_JIT_PROFILE_H +#define ZEN_VM_JIT_PROFILE_H + +#include "utils/logging.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace zen::vm { + +// ---- Profile-Guided JIT: tunable parameters ---- +namespace profile { +// Ring buffer capacity: number of recent global contract call records. +static constexpr size_t RING_BUFFER_CAPACITY = 10000; +// JIT trigger conditions: both must be met within the sliding window. +static constexpr uint64_t JIT_TRIGGER_CALL_COUNT = 32; +static constexpr uint64_t JIT_TRIGGER_TOTAL_GAS = 100000; +} // namespace profile + +// ---- Profile-Guided JIT: data structures ---- + +struct CallRecord { + // Use the 20-byte EVM address directly as the profile key. A + // string-formatted hex name was previously allocated on every call + // (~46 bytes + heap alloc), which showed up on the hot path of every EVM + // execution. evmc::address is POD-like, trivially copyable, and has a + // std::hash specialization shipped by evmc, so it works as an + // unordered_map key without any extra plumbing. + evmc::address ModAddr; + uint64_t GasUsed = 0; +}; + +struct ContractProfile { + uint64_t WindowCallCount = 0; + uint64_t WindowGasUsed = 0; + bool JITTriggered = false; + bool JITRejected = false; +}; + +struct CallRingBuffer { + std::vector Buffer; + size_t Head = 0; + size_t Count = 0; + size_t Capacity; + + explicit CallRingBuffer(size_t Cap) : Buffer(Cap), Capacity(Cap) {} + + std::optional push(CallRecord NewRecord) { + std::optional Evicted; + if (Count == Capacity) { + Evicted = std::move(Buffer[Head]); + } else { + Count++; + } + Buffer[Head] = std::move(NewRecord); + Head = (Head + 1) % Capacity; + return Evicted; + } +}; + +// ---- JIT Compile Thread Pool ---- +// A simple fixed-size thread pool for background JIT compilation tasks. +// Limits the number of concurrent compilations to avoid resource exhaustion. +class JITCompilePool { +public: + explicit JITCompilePool(uint32_t NumThreads) : Shutdown(false) { + for (uint32_t I = 0; I < NumThreads; ++I) { + Workers.emplace_back([this]() { workerLoop(); }); + } + } + + ~JITCompilePool() { + { + std::lock_guard Lock(QueueMutex); + Shutdown = true; + } + QueueCV.notify_all(); + for (auto &Worker : Workers) { + Worker.join(); + } + } + + // Maximum number of pending tasks. When the queue is full, submit() + // returns an invalid future and the caller should skip JIT for this + // contract. This prevents unbounded memory growth when many distinct + // contracts trigger JIT simultaneously. + static constexpr size_t MAX_TASK_QUEUE_SIZE = 256; + + // Submit a compilation task. Returns a future that completes when + // the task finishes. Returns an invalid (default-constructed) future + // if the queue is full -- callers must check future.valid() before + // calling wait(). + std::future submit(std::function Task) { + auto Promise = std::make_shared>(); + std::future Future = Promise->get_future(); + { + std::lock_guard Lock(QueueMutex); + if (TaskQueue.size() >= MAX_TASK_QUEUE_SIZE) { + ZEN_LOG_WARN("JIT compile queue full (%zu tasks), dropping request", + TaskQueue.size()); + return std::future(); + } + TaskQueue.push( + [Task = std::move(Task), Promise = std::move(Promise)]() mutable { + try { + Task(); + Promise->set_value(); + } catch (...) { + Promise->set_exception(std::current_exception()); + } + }); + } + QueueCV.notify_one(); + return Future; + } + + // Non-copyable, non-movable. + JITCompilePool(const JITCompilePool &) = delete; + JITCompilePool &operator=(const JITCompilePool &) = delete; + +private: + void workerLoop() { + while (true) { + std::function Task; + { + std::unique_lock Lock(QueueMutex); + QueueCV.wait(Lock, [this]() { return Shutdown || !TaskQueue.empty(); }); + if (Shutdown && TaskQueue.empty()) + return; + Task = std::move(TaskQueue.front()); + TaskQueue.pop(); + } + Task(); + } + } + + std::vector Workers; + std::queue> TaskQueue; + std::mutex QueueMutex; + std::condition_variable QueueCV; + bool Shutdown; +}; + +} // namespace zen::vm + +#endif // ZEN_VM_JIT_PROFILE_H From 0391880994fa0486e8e0ed86141cd7785d684685 Mon Sep 17 00:00:00 2001 From: Outcry <843648230@qq.com> Date: Wed, 13 May 2026 06:30:03 +0000 Subject: [PATCH 2/5] feat(evm): test for analyze background jit --- src/runtime/config.h | 7 ++++ src/runtime/evm_module.h | 4 ++ src/vm/dt_evmc_vm.cpp | 87 +++++++++++++++++++++++++++++++++++----- 3 files changed, 88 insertions(+), 10 deletions(-) diff --git a/src/runtime/config.h b/src/runtime/config.h index 3d0b635f3..753828a2a 100644 --- a/src/runtime/config.h +++ b/src/runtime/config.h @@ -46,6 +46,13 @@ struct RuntimeConfig { bool EnableProfileGuidedJIT = false; // Maximum number of concurrent background JIT compilation threads. uint32_t NumJITCompileThreads = 10; + // 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() { diff --git a/src/runtime/evm_module.h b/src/runtime/evm_module.h index 4f0904e41..e47dc856d 100644 --- a/src/runtime/evm_module.h +++ b/src/runtime/evm_module.h @@ -92,6 +92,10 @@ class EVMModule final : public BaseModule { } // Future for background JIT compilation (managed by JITCompilePool). std::future JITCompileFuture; + + // Per-module execution statistics for profile-guided JIT diagnostics. + uint64_t ModuleExecuteCount = 0; + uint64_t ModuleFirstJITAtCall = 0; #endif // ZEN_ENABLE_JIT private: diff --git a/src/vm/dt_evmc_vm.cpp b/src/vm/dt_evmc_vm.cpp index 45b44dd12..f222e4e78 100644 --- a/src/vm/dt_evmc_vm.cpp +++ b/src/vm/dt_evmc_vm.cpp @@ -191,6 +191,14 @@ bool parseBoolEnvValue(const char *Value, bool &ParsedValue) { struct DTVM : evmc_vm { DTVM(); ~DTVM() { + if (Config.EnableProfileGuidedJIT) { + fprintf(stderr, + "[DTVM] Profile-guided JIT stats: total_executions=%lu, " + "calls=%zu, profiled_contracts=%zu, jit_triggered=%lu, " + "first_jit_at_call=#%lu\n", + ExecuteCallCount, RingBuffer.Count, ProfileStore.size(), + BackgroundJITTriggerCount, FirstJITCallNumber); + } // Drain the JIT compile thread pool first: wait for all in-flight // compilation tasks to finish before unloading modules they reference. CompilePool.reset(); @@ -271,6 +279,11 @@ struct DTVM : evmc_vm { CallRingBuffer RingBuffer{profile::RING_BUFFER_CAPACITY}; // Thread pool for background JIT compilation (lazily initialized). std::unique_ptr CompilePool; + // Statistics: number of background JIT compilations actually triggered. + uint64_t BackgroundJITTriggerCount = 0; + // Statistics: total execute() call count and first JIT execution call number. + uint64_t ExecuteCallCount = 0; + uint64_t FirstJITCallNumber = 0; bool isModuleInUse(const EVMModule *Mod) const { if (CachedMainInst && CachedMainInst->getModule() == Mod) @@ -341,6 +354,27 @@ enum evmc_set_option_result set_option(evmc_vm *VMInstance, const char *Name, return EVMC_SET_OPTION_SUCCESS; } return EVMC_SET_OPTION_INVALID_VALUE; + } else if (std::strcmp(Name, "jit_trigger_calls") == 0) { + int Parsed = std::atoi(Value); + if (Parsed > 0) { + VM->Config.JITTriggerCallCount = static_cast(Parsed); + return EVMC_SET_OPTION_SUCCESS; + } + return EVMC_SET_OPTION_INVALID_VALUE; + } else if (std::strcmp(Name, "jit_trigger_gas") == 0) { + int Parsed = std::atoi(Value); + if (Parsed > 0) { + VM->Config.JITTriggerTotalGas = static_cast(Parsed); + return EVMC_SET_OPTION_SUCCESS; + } + return EVMC_SET_OPTION_INVALID_VALUE; + } else if (std::strcmp(Name, "jit_eager") == 0) { + bool Parsed = false; + if (parseBoolEnvValue(Value, Parsed)) { + VM->Config.EnableEagerJIT = Parsed; + return EVMC_SET_OPTION_SUCCESS; + } + return EVMC_SET_OPTION_INVALID_VALUE; #endif } return EVMC_SET_OPTION_INVALID_NAME; @@ -716,8 +750,8 @@ void updateProfileAndMaybeTriggerJIT(DTVM *VM, const evmc_message *Msg, CurrentProfile.WindowGasUsed += GasUsed; // 5. Check JIT trigger conditions. - if (CurrentProfile.WindowCallCount < profile::JIT_TRIGGER_CALL_COUNT || - CurrentProfile.WindowGasUsed < profile::JIT_TRIGGER_TOTAL_GAS) { + if (CurrentProfile.WindowCallCount < VM->Config.JITTriggerCallCount || + CurrentProfile.WindowGasUsed < VM->Config.JITTriggerTotalGas) { return; } @@ -744,6 +778,9 @@ void updateProfileAndMaybeTriggerJIT(DTVM *VM, const evmc_message *Msg, // Trigger background JIT compilation via thread pool. CurrentProfile.JITTriggered = true; + VM->BackgroundJITTriggerCount++; + ZEN_LOG_DEBUG("Background JIT triggered (#%lu) for contract", + VM->BackgroundJITTriggerCount); auto &Pool = getOrCreateCompilePool(VM); Mod->JITCompileFuture = Pool.submit([Mod]() { zen::action::performEVMJITCompile(*Mod); }); @@ -771,6 +808,7 @@ evmc_result execute(evmc_vm *EVMInstance, const evmc_host_interface *Host, const evmc_message *Msg, const uint8_t *Code, size_t CodeSize) { auto *VM = static_cast(EVMInstance); + VM->ExecuteCallCount++; // Interpreter mode: use optimized fast path (bypasses callEVMMain) if (VM->Config.Mode == RunMode::InterpMode) { @@ -795,6 +833,7 @@ evmc_result execute(evmc_vm *EVMInstance, const evmc_host_interface *Host, return evmc_make_result(EVMC_FAILURE, 0, 0, nullptr, 0); } ModuleGuard ModGuard(VM, Mod, IsTransientMod); + Mod->ModuleExecuteCount++; #ifdef ZEN_ENABLE_JIT_PRECOMPILE_FALLBACK // O(1) flag check replaces per-call O(n) EVMAnalyzer scan. @@ -820,12 +859,24 @@ evmc_result execute(evmc_vm *EVMInstance, const evmc_host_interface *Host, // keeps the Ctx pooling and pre/post bookkeeping in lock-step with the // public interpreter fast path, so the two paths can never drift. if (!Mod->getJITCode() && VM->Config.EnableProfileGuidedJIT) { - evmc_result RawResult = runInterpreterOnResolvedInstance( - VM, Mod, TheInst, Msg, IsTransientMod); - if (shouldUsePersistentModuleCache(Msg)) { - updateProfileAndMaybeTriggerJIT(VM, Msg, RawResult, Mod); + if (VM->Config.EnableEagerJIT) { + // Eager JIT: synchronously compile on first encounter, then fall + // through to the JIT fast path below instead of interpreting. + zen::action::performEVMJITCompile(*Mod); + // If compilation failed, fall back to interpreter. + if (!Mod->getJITCode()) { + return executeInterpreterFastPath(VM, Host, Context, Rev, Msg, Code, + CodeSize); + } + // Fall through to JIT fast path. + } else { + evmc_result RawResult = runInterpreterOnResolvedInstance( + VM, Mod, TheInst, Msg, IsTransientMod); + if (shouldUsePersistentModuleCache(Msg)) { + updateProfileAndMaybeTriggerJIT(VM, Msg, RawResult, Mod); + } + return RawResult; } - return RawResult; } #endif // ZEN_ENABLE_MULTIPASS_JIT @@ -836,9 +887,25 @@ evmc_result execute(evmc_vm *EVMInstance, const evmc_host_interface *Host, // logs on failure (see PR #481), so Mod->getJITCode() can legitimately be // nullptr here. Fall back to the interpreter fast path in that case // instead of jumping through a null function pointer. - if (!Mod->getJITCode()) { - return executeInterpreterFastPath(VM, Host, Context, Rev, Msg, Code, - CodeSize); + // if (!Mod->getJITCode()) { + return executeInterpreterFastPath(VM, Host, Context, Rev, Msg, Code, + CodeSize); + // } + + // Log when this module first executes via JIT. + if (Mod->ModuleFirstJITAtCall == 0) { + Mod->ModuleFirstJITAtCall = Mod->ModuleExecuteCount; + // Build a short hex prefix from the contract bytecode for identification. + char codePrefix[16] = {0}; + size_t prefixLen = Mod->CodeSize < 4 ? Mod->CodeSize : 4; + for (size_t i = 0; i < prefixLen; ++i) + snprintf(codePrefix + i * 2, sizeof(codePrefix) - i * 2, "%02x", + static_cast(static_cast(Mod->Code[i]))); + fprintf(stderr, + "[DTVM] Contract first JIT execution at module call #%lu " + "(global call #%lu), contract size=%zu, code_prefix=0x%s\n", + Mod->ModuleExecuteCount, VM->ExecuteCallCount, Mod->CodeSize, + codePrefix); } evmc_message MsgWithCode = *Msg; From 38ea7843ce60580c651e5a05f350277ecc3d9856 Mon Sep 17 00:00:00 2001 From: Outcry <843648230@qq.com> Date: Thu, 14 May 2026 12:23:10 +0000 Subject: [PATCH 3/5] feat(evm): add test identifier --- src/vm/dt_evmc_vm.cpp | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/vm/dt_evmc_vm.cpp b/src/vm/dt_evmc_vm.cpp index f222e4e78..98fbc8dfe 100644 --- a/src/vm/dt_evmc_vm.cpp +++ b/src/vm/dt_evmc_vm.cpp @@ -812,6 +812,7 @@ evmc_result execute(evmc_vm *EVMInstance, const evmc_host_interface *Host, // Interpreter mode: use optimized fast path (bypasses callEVMMain) if (VM->Config.Mode == RunMode::InterpMode) { + fprintf(stderr, "[DTVM] call should Interpreter"); return executeInterpreterFastPath(VM, Host, Context, Rev, Msg, Code, CodeSize); } @@ -865,6 +866,8 @@ evmc_result execute(evmc_vm *EVMInstance, const evmc_host_interface *Host, zen::action::performEVMJITCompile(*Mod); // If compilation failed, fall back to interpreter. if (!Mod->getJITCode()) { + + fprintf(stderr, "[DTVM] call Interpreter"); return executeInterpreterFastPath(VM, Host, Context, Rev, Msg, Code, CodeSize); } @@ -887,27 +890,29 @@ evmc_result execute(evmc_vm *EVMInstance, const evmc_host_interface *Host, // logs on failure (see PR #481), so Mod->getJITCode() can legitimately be // nullptr here. Fall back to the interpreter fast path in that case // instead of jumping through a null function pointer. - // if (!Mod->getJITCode()) { - return executeInterpreterFastPath(VM, Host, Context, Rev, Msg, Code, - CodeSize); - // } + if (!Mod->getJITCode()) { - // Log when this module first executes via JIT. - if (Mod->ModuleFirstJITAtCall == 0) { - Mod->ModuleFirstJITAtCall = Mod->ModuleExecuteCount; - // Build a short hex prefix from the contract bytecode for identification. - char codePrefix[16] = {0}; - size_t prefixLen = Mod->CodeSize < 4 ? Mod->CodeSize : 4; - for (size_t i = 0; i < prefixLen; ++i) - snprintf(codePrefix + i * 2, sizeof(codePrefix) - i * 2, "%02x", - static_cast(static_cast(Mod->Code[i]))); - fprintf(stderr, - "[DTVM] Contract first JIT execution at module call #%lu " - "(global call #%lu), contract size=%zu, code_prefix=0x%s\n", - Mod->ModuleExecuteCount, VM->ExecuteCallCount, Mod->CodeSize, - codePrefix); + fprintf(stderr, "[DTVM] call Interpreter2"); + return executeInterpreterFastPath(VM, Host, Context, Rev, Msg, Code, + CodeSize); } + // Log when this module first executes via JIT. + // if (Mod->ModuleFirstJITAtCall == 0) { + // Mod->ModuleFirstJITAtCall = Mod->ModuleExecuteCount; + // // Build a short hex prefix from the contract bytecode for + // identification. char codePrefix[16] = {0}; size_t prefixLen = + // Mod->CodeSize < 4 ? Mod->CodeSize : 4; for (size_t i = 0; i < + // prefixLen; ++i) + // snprintf(codePrefix + i * 2, sizeof(codePrefix) - i * 2, "%02x", + // static_cast(static_cast(Mod->Code[i]))); + // fprintf(stderr, + // "[DTVM] Contract first JIT execution at module call #%lu " + // "(global call #%lu), contract size=%zu, code_prefix=0x%s\n", + // Mod->ModuleExecuteCount, VM->ExecuteCallCount, Mod->CodeSize, + // codePrefix); + // } + evmc_message MsgWithCode = *Msg; MsgWithCode.code = reinterpret_cast(Mod->Code); MsgWithCode.code_size = Mod->CodeSize; @@ -932,6 +937,7 @@ evmc_result execute(evmc_vm *EVMInstance, const evmc_host_interface *Host, VM->RT->callEVMInJITMode(*TheInst, MsgWithCode, Result); } #else + fprintf(stderr, "[DTVM] call JIT"); VM->RT->callEVMInJITMode(*TheInst, MsgWithCode, Result); #endif // ZEN_ENABLE_VIRTUAL_STACK From d05e39861638f8c7963d55d7225dcee3c8bbf922 Mon Sep 17 00:00:00 2001 From: Outcry <843648230@qq.com> Date: Fri, 15 May 2026 01:24:07 +0000 Subject: [PATCH 4/5] feat(evm): add in compile identifier --- src/vm/dt_evmc_vm.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vm/dt_evmc_vm.cpp b/src/vm/dt_evmc_vm.cpp index 98fbc8dfe..f71358417 100644 --- a/src/vm/dt_evmc_vm.cpp +++ b/src/vm/dt_evmc_vm.cpp @@ -860,7 +860,10 @@ evmc_result execute(evmc_vm *EVMInstance, const evmc_host_interface *Host, // keeps the Ctx pooling and pre/post bookkeeping in lock-step with the // public interpreter fast path, so the two paths can never drift. if (!Mod->getJITCode() && VM->Config.EnableProfileGuidedJIT) { + if (VM->Config.EnableEagerJIT) { + + fprintf(stderr, "[DTVM] EnableEagerJIT compile start"); // Eager JIT: synchronously compile on first encounter, then fall // through to the JIT fast path below instead of interpreting. zen::action::performEVMJITCompile(*Mod); @@ -873,6 +876,8 @@ evmc_result execute(evmc_vm *EVMInstance, const evmc_host_interface *Host, } // Fall through to JIT fast path. } else { + + fprintf(stderr, "[DTVM] EnableProfileGuidedJIT compile start"); evmc_result RawResult = runInterpreterOnResolvedInstance( VM, Mod, TheInst, Msg, IsTransientMod); if (shouldUsePersistentModuleCache(Msg)) { From 415cc1bb4dd0781e3788a4b3d537cc78b68ad7fd Mon Sep 17 00:00:00 2001 From: Outcry <843648230@qq.com> Date: Fri, 15 May 2026 10:12:32 +0000 Subject: [PATCH 5/5] feat: for test new key --- src/vm/dt_evmc_vm.cpp | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/vm/dt_evmc_vm.cpp b/src/vm/dt_evmc_vm.cpp index f71358417..b0a325215 100644 --- a/src/vm/dt_evmc_vm.cpp +++ b/src/vm/dt_evmc_vm.cpp @@ -104,11 +104,19 @@ struct HostContextScope { // ---- Address-based module cache types ---- static constexpr size_t MAX_MODULE_CACHE_SIZE = 4096; +static constexpr size_t CODE_PREFIX_SIZE = 8; // Store first 8 bytes of code struct CodeAddrRevKey { evmc_address Addr; evmc_revision Rev; zen::runtime::EVMMemorySpecializationProfile MemoryProfile = {}; + uint8_t + CodePrefix[CODE_PREFIX_SIZE]; // First 8 bytes of code (no hash collision) + size_t CodeSize = 0; // Code size for exact match + + CodeAddrRevKey() : Addr{}, Rev{}, MemoryProfile{}, CodeSize(0) { + std::memset(CodePrefix, 0, CODE_PREFIX_SIZE); + } }; struct CodeAddrRevHash { @@ -117,6 +125,11 @@ struct CodeAddrRevHash { getEVMMemorySpecializationCodegenKey(K.MemoryProfile); uint64_t H; std::memcpy(&H, K.Addr.bytes + 12, sizeof(H)); + // Mix in code prefix (first 8 bytes) and size for better distribution + uint64_t PrefixHash; + std::memcpy(&PrefixHash, K.CodePrefix, sizeof(PrefixHash)); + H ^= PrefixHash; + H ^= static_cast(K.CodeSize) * 2654435761ULL; return H ^ (static_cast(K.Rev) * 2654435761u) ^ (static_cast(CodegenKey.SkipLeadingZeroLimbStores) << 20); } @@ -135,7 +148,9 @@ struct CodeAddrRevEqual { return A.Rev == B.Rev && ACodegenKey.SkipLeadingZeroLimbStores == BCodegenKey.SkipLeadingZeroLimbStores && - std::memcmp(A.Addr.bytes, B.Addr.bytes, sizeof(A.Addr.bytes)) == 0; + std::memcmp(A.Addr.bytes, B.Addr.bytes, sizeof(A.Addr.bytes)) == 0 && + std::memcmp(A.CodePrefix, B.CodePrefix, CODE_PREFIX_SIZE) == 0 && + A.CodeSize == B.CodeSize; } }; @@ -472,7 +487,18 @@ EVMModule *findModuleCached(DTVM *VM, const uint8_t *Code, size_t CodeSize, EVMModule *Mod = nullptr; // L1: Address-based LRU cache lookup - CodeAddrRevKey AddrKey{Msg->code_address, Rev, Profile}; + // Copy first 8 bytes of code for cache key (no hash collision, fast memcmp) + uint8_t CodePrefix[CODE_PREFIX_SIZE] = {}; + const size_t CopySize = std::min(CodeSize, CODE_PREFIX_SIZE); + if (CopySize > 0 && Code != nullptr) { + std::memcpy(CodePrefix, Code, CopySize); + } + CodeAddrRevKey AddrKey; + AddrKey.Addr = Msg->code_address; + AddrKey.Rev = Rev; + AddrKey.MemoryProfile = Profile; + std::memcpy(AddrKey.CodePrefix, CodePrefix, CODE_PREFIX_SIZE); + AddrKey.CodeSize = CodeSize; auto It = VM->AddrCache.find(AddrKey); if (It != VM->AddrCache.end() && validateCodeMatch(Code, CodeSize, It->second.first,