From 7a7850d5739f1e9c1dc789c65e9a02336e5e7dbc Mon Sep 17 00:00:00 2001 From: ZR74 <2401889661@qq.com> Date: Tue, 28 Jul 2026 17:19:41 +0000 Subject: [PATCH 1/2] feat(compiler): add opt-in EVM compiler observation Emit schema-versioned phase, IR, stack-lift, Phi, register-allocation, and code-size metrics only when DTVM_EVM_COMPILER_OBSERVE is enabled. Add an atomic S0/S1 collection tool and unit tests so compiler growth can be attributed instead of inferred from aggregate JIT time. --- .../README.md | 98 +++++ docs/changes/README.md | 1 + docs/modules/compiler/spec.md | 10 + docs/modules/runtime/spec.md | 7 +- docs/modules/tools/spec.md | 9 + src/compiler/cgir/pass/interference_cache.h | 2 + src/compiler/cgir/pass/phi_elimination.cpp | 15 +- src/compiler/cgir/pass/phi_elimination.h | 12 +- src/compiler/compiler.cpp | 217 +++++++--- src/compiler/compiler.h | 4 +- src/compiler/evm_compiler.cpp | 318 ++++++++++++--- src/compiler/evm_compiler.h | 3 +- src/compiler/evm_compiler_metrics.h | 139 +++++++ src/compiler/evm_frontend/evm_analyzer.h | 15 + .../evm_frontend/evm_lifted_stack_lifter.h | 45 +++ .../evm_frontend/evm_mir_compiler.cpp | 34 ++ src/compiler/evm_frontend/evm_mir_compiler.h | 16 + src/runtime/evm_module.cpp | 3 + src/runtime/evm_module.h | 40 ++ .../test_run_ssa_compiler_observation.py | 110 +++++ tools/run_ssa_compiler_observation.py | 376 ++++++++++++++++++ 21 files changed, 1371 insertions(+), 103 deletions(-) create mode 100644 docs/changes/2026-07-28-evm-compiler-observation/README.md create mode 100644 src/compiler/evm_compiler_metrics.h create mode 100644 tests/tools/test_run_ssa_compiler_observation.py create mode 100644 tools/run_ssa_compiler_observation.py diff --git a/docs/changes/2026-07-28-evm-compiler-observation/README.md b/docs/changes/2026-07-28-evm-compiler-observation/README.md new file mode 100644 index 000000000..0761caa6f --- /dev/null +++ b/docs/changes/2026-07-28-evm-compiler-observation/README.md @@ -0,0 +1,98 @@ +# Change: Add opt-in EVM compiler observation + +- **Status**: Implemented +- **Date**: 2026-07-28 +- **Tier**: Full + +## Overview + +Add an opt-in, structured observation record for the EVM multipass compiler. +The record attributes compile time and intermediate-representation growth +across frontend, MIR, CgIR, Phi elimination, register allocation, machine-code +emission, and code publication. + +## Motivation + +Total JIT time and mapped executable size cannot explain which compiler stage +caused a regression. Diagnosing stack-SSA compile growth required reproducible +per-stage timing, before/after IR snapshots, and counters for stack lifting, +Phi elimination, register allocation, fallback coverage, and emitted bytes. + +The observation must remain disabled by default and must separate its own +measurement overhead from compiler work. + +## Impact + +### Affected Modules + +- `compiler`: phase timers, MIR/CgIR snapshots, lift/Phi/RA counters, structured + record emission, and feature-coverage aggregation. +- `runtime`: retain actual emitted code bytes and dynamic-entry fallback counts + on the EVM module. +- `tools`: collect paired S0/S1 records for a deduplicated fixture set. +- `tests`: validate parsing, fingerprint selection, comparison, and summary + behavior of the observation runner. + +### Affected Contracts + +Setting `DTVM_EVM_COMPILER_OBSERVE` to a non-empty value other than `0` emits +one `[DTVM_EVM_COMPILER_OBSERVATION]` JSON line per attempted EVM compilation. +No record is emitted when the variable is unset or `0`. + +`EVMModule::getEmittedJITCodeSize()` reports actual emitted bytes, while +`getJITCodeSize()` continues to report the executable mapping size. + +### Compatibility + +The feature is additive. Default compilation, generated code, and EVM execution +semantics are unchanged. + +## Implementation Plan + +### Phase 1: Capture compiler structure + +- [x] Add opt-in phase timing with explicit observation-overhead accounting. +- [x] Capture MIR and CgIR snapshots around lowering, Phi elimination, and + register allocation. +- [x] Capture stack-lift, Phi-copy, spill, fallback, feature-coverage, and + emitted-code metrics. + +### Phase 2: Emit and consume structured records + +- [x] Emit schema-versioned records with deterministic bytecode fingerprints. +- [x] Add an atomic, resumable S0/S1 collection and comparison tool. +- [x] Add unit tests for parsing, target selection, comparison, and aggregation. + +### Phase 3: Verify + +- [x] Update affected module specifications. +- [x] Run formatting checks. +- [x] Build stack-SSA off and on configurations. +- [x] Run frontend and Python tool tests. +- [x] Verify observation-off emits no records and both modes preserve execution + output. + +### Validation + +- Strict `clang-format` dry run on every changed C++ source and header. +- `git diff --check`. +- Stack-SSA off and on release multipass builds with LLVM 15. +- `evmJitFrontendTests`: 108/108 passed in each configuration. +- `python3 -m unittest tests/tools/test_run_ssa_compiler_observation.py`: + 4/4 passed. +- `double_mod_origin.evm.hex`: successful execution with identical output in + both configurations; zero observation records by default and exactly one + schema-versioned record when enabled. + +## Compatibility Notes + +Consumers should treat unknown JSON fields as forward-compatible additions and +key records by `schema_version`. + +## Risks + +- Observation can perturb timings. The record reports observation overhead + separately, and performance measurements must run with observation disabled. +- Snapshot walks add work only when observation is enabled. +- Logging-only feature counters are reported as zero when the corresponding + build instrumentation is unavailable. diff --git a/docs/changes/README.md b/docs/changes/README.md index e35be6891..7b70c0237 100644 --- a/docs/changes/README.md +++ b/docs/changes/README.md @@ -53,6 +53,7 @@ Typical triggers: | 2026-04-14 | [from-raw-pointer-safety-checks](2026-04-14-from-raw-pointer-safety-checks/README.md) | Accepted | Light | Add null/alignment safety checks to `from_raw_pointer` in Rust bindings | | 2026-05-13 | [evm-ngram-macro-ops](2026-05-13-evm-ngram-macro-ops/README.md) | Implemented | Full | Initial EVM n-gram macro-op lowering and specialized keccak helpers for multipass JIT | | 2026-07-21 | [evm-memory-alias-and-expansion](2026-07-21-evm-memory-alias-and-expansion/README.md) | Implemented | Full | Stronger memory alias proofs, wider precheck/expansion coverage, DSE, load forwarding, grouping, and MCOPY roadmap | +| 2026-07-28 | [evm-compiler-observation](2026-07-28-evm-compiler-observation/README.md) | Implemented | Full | Add opt-in structured EVM compiler phase and IR observations | Each active proposal lives in its own subdirectory. Browse `docs/changes/*/README.md` to see all current proposals, or use: diff --git a/docs/modules/compiler/spec.md b/docs/modules/compiler/spec.md index e065b1a33..845d93142 100644 --- a/docs/modules/compiler/spec.md +++ b/docs/modules/compiler/spec.md @@ -189,6 +189,16 @@ and destination ends for expansion elision while retaining the original - Compilation start/end times are recorded in `utils::StatisticPhase::JITCompilation` - When `ZEN_ENABLE_LINUX_PERF` is enabled, perf JIT dump symbols (e.g., `EVMBB*`) are emitted for generated blocks +- `DTVM_EVM_COMPILER_OBSERVE=1` emits one schema-versioned JSON record per EVM + module. It reports phase times, explicitly separated observation overhead, + MIR/CgIR snapshots, stack-lift/Phi/register-allocation counters, feature + coverage, and emitted code bytes. The record names the existing stack-SSA and + memory-plan build gates. In a build with + `ZEN_ENABLE_MULTIPASS_JIT_LOGGING`, feature coverage includes aggregate + memory-plan and arithmetic-path counters; without logging those counters + remain zero. +- Observation is disabled by default and must not influence generated code, + compiler decisions, or deterministic execution. --- diff --git a/docs/modules/runtime/spec.md b/docs/modules/runtime/spec.md index 4545e82be..e7006114c 100644 --- a/docs/modules/runtime/spec.md +++ b/docs/modules/runtime/spec.md @@ -95,8 +95,11 @@ runtime propagates errors via `common::Error` / `common::ErrorCode`; common runt ## Compatibility Strategy 1. **Config**: `RuntimeConfig` controls RunMode (Interp / Singlepass / Multipass), WASI, statistics, JIT thread count; changes require `validate()` -2. **Compile options**: Behavior controlled by `ZEN_ENABLE_EVM`, `ZEN_ENABLE_JIT`, `ZEN_ENABLE_SINGLEPASS_JIT`, `ZEN_ENABLE_MULTIPASS_JIT`, `ZEN_ENABLE_BUILTIN_WASI`, `ZEN_ENABLE_CPU_EXCEPTION`, `ZEN_ENABLE_VIRTUAL_STACK`, `ZEN_ENABLE_DWASM`, etc. -3. **ABI**: entrypoint assembly depends on `Instance`, `MemoryInstance` layout; layout changes require offset updates in assembly +2. **EVM JIT code size**: `getEmittedJITCodeSize()` reports actual emitted + machine-code bytes; `getJITCodeSize()` retains the executable mapping size + used for runtime address bounds. +3. **Compile options**: Behavior controlled by `ZEN_ENABLE_EVM`, `ZEN_ENABLE_JIT`, `ZEN_ENABLE_SINGLEPASS_JIT`, `ZEN_ENABLE_MULTIPASS_JIT`, `ZEN_ENABLE_BUILTIN_WASI`, `ZEN_ENABLE_CPU_EXCEPTION`, `ZEN_ENABLE_VIRTUAL_STACK`, `ZEN_ENABLE_DWASM`, etc. +4. **ABI**: entrypoint assembly depends on `Instance`, `MemoryInstance` layout; layout changes require offset updates in assembly ## Cross-References diff --git a/docs/modules/tools/spec.md b/docs/modules/tools/spec.md index 9340347a3..c0aed5636 100644 --- a/docs/modules/tools/spec.md +++ b/docs/modules/tools/spec.md @@ -63,8 +63,17 @@ This module does not include: Build system (CMake), test framework (gtest/ctest) | Script | Responsibility | |--------|----------------| | function_selector.py | Compute Solidity function selector (keccak256 first 4 bytes) | +| run_ssa_compiler_observation.py | Collect and compare target-bytecode compiler phase and IR observations for S0/S1 fixtures | | requirements.txt | Python dependencies (PyYAML, rlp, eth-hash, trie, etc.) | +`run_ssa_compiler_observation.py` isolates each selected fixture, alternates +S0/S1 process order, and enables the opt-in compiler observation record. It +computes the target bytecode's deterministic FNV-1a fingerprint and requires +exactly one matching record, preventing helper or subcall modules from being +mistaken for the selected target. Output contains executable and fixture +hashes, per-code structural ratios, dominant S1 phase counts, and an atomically +resumable checkpoint. + ## External Contracts | External Dependency | Use | diff --git a/src/compiler/cgir/pass/interference_cache.h b/src/compiler/cgir/pass/interference_cache.h index 6c056bafd..262f4561e 100644 --- a/src/compiler/cgir/pass/interference_cache.h +++ b/src/compiler/cgir/pass/interference_cache.h @@ -14,6 +14,8 @@ #pragma once +#include + #include "llvm/ADT/SmallVector.h" #include "llvm/Support/Compiler.h" #include diff --git a/src/compiler/cgir/pass/phi_elimination.cpp b/src/compiler/cgir/pass/phi_elimination.cpp index 2990af917..7e7d58869 100644 --- a/src/compiler/cgir/pass/phi_elimination.cpp +++ b/src/compiler/cgir/pass/phi_elimination.cpp @@ -21,7 +21,7 @@ struct CopyEdge { class PhiEliminationImpl { public: - void runOnCgFunction(CgFunction &MF) { + CgPhiElimination::Statistics runOnCgFunction(CgFunction &MF) { this->MF = &MF; MRI = &MF.getRegInfo(); TII = &MF.getTargetInstrInfo(); @@ -35,6 +35,7 @@ class PhiEliminationImpl { for (CgBasicBlock *BB : Blocks) { eliminateBlockPhis(*BB); } + return Stats; } private: @@ -49,6 +50,7 @@ class PhiEliminationImpl { if (Phis.empty()) { return; } + Stats.PhiInstructions += Phis.size(); std::unordered_map> EdgeCopies; EdgeCopies.reserve(BB.pred_size()); @@ -58,6 +60,7 @@ class PhiEliminationImpl { (Phi->getNumOperands() % 2) == 1 && "invalid lowered PHI"); const CgRegister DstReg = Phi->getOperand(0).getReg(); + Stats.PhiIncomingEdges += (Phi->getNumOperands() - 1) / 2; for (unsigned OpIdx = 1; OpIdx < Phi->getNumOperands(); OpIdx += 2) { CgOperand &SrcOp = Phi->getOperand(OpIdx); CgOperand &PredOp = Phi->getOperand(OpIdx + 1); @@ -68,7 +71,10 @@ class PhiEliminationImpl { } for (auto &[Pred, Copies] : EdgeCopies) { + Stats.CandidateEdgeCopies += Copies.size(); + const size_t CandidateCount = Copies.size(); eraseIdentityCopies(Copies); + Stats.IdentityEdgeCopies += CandidateCount - Copies.size(); if (Copies.empty()) { continue; } @@ -79,6 +85,7 @@ class PhiEliminationImpl { } CgBasicBlock *SplitBB = createSplitEdgeBlock(*Pred, BB); + ++Stats.SplitCriticalEdges; emitParallelCopies(*SplitBB, SplitBB->end(), Copies); addUnconditionalBranch(*SplitBB, BB); } @@ -150,6 +157,7 @@ class PhiEliminationImpl { llvm::MutableArrayRef OperandRef(Operands); MF->createCgInstruction(BB, InsertPt, TII->get(llvm::TargetOpcode::COPY), OperandRef); + ++Stats.EmittedCopyInstructions; } void emitParallelCopies(CgBasicBlock &BB, CgBasicBlock::iterator InsertPt, @@ -196,11 +204,12 @@ class PhiEliminationImpl { CgFunction *MF = nullptr; CgRegisterInfo *MRI = nullptr; const llvm::TargetInstrInfo *TII = nullptr; + CgPhiElimination::Statistics Stats; }; } // namespace -void CgPhiElimination::runOnCgFunction(CgFunction &MF) { +CgPhiElimination::Statistics CgPhiElimination::runOnCgFunction(CgFunction &MF) { PhiEliminationImpl Impl; - Impl.runOnCgFunction(MF); + return Impl.runOnCgFunction(MF); } diff --git a/src/compiler/cgir/pass/phi_elimination.h b/src/compiler/cgir/pass/phi_elimination.h index 65ab9acf5..06f329421 100644 --- a/src/compiler/cgir/pass/phi_elimination.h +++ b/src/compiler/cgir/pass/phi_elimination.h @@ -4,6 +4,7 @@ #pragma once #include "compiler/common/common_defs.h" +#include namespace COMPILER { @@ -11,7 +12,16 @@ class CgFunction; class CgPhiElimination : public NonCopyable { public: - void runOnCgFunction(CgFunction &MF); + struct Statistics { + uint64_t PhiInstructions = 0; + uint64_t PhiIncomingEdges = 0; + uint64_t CandidateEdgeCopies = 0; + uint64_t IdentityEdgeCopies = 0; + uint64_t EmittedCopyInstructions = 0; + uint64_t SplitCriticalEdges = 0; + }; + + Statistics runOnCgFunction(CgFunction &MF); }; } // namespace COMPILER diff --git a/src/compiler/compiler.cpp b/src/compiler/compiler.cpp index 52392d7a5..2ce20240d 100644 --- a/src/compiler/compiler.cpp +++ b/src/compiler/compiler.cpp @@ -12,7 +12,9 @@ #include "compiler/cgir/pass/reg_alloc_basic.h" #include "compiler/cgir/pass/reg_alloc_greedy.h" #include "compiler/cgir/pass/register_coalescer.h" +#include "compiler/common/llvm_workaround.h" #include "compiler/context.h" +#include "compiler/evm_compiler_metrics.h" #include "compiler/frontend/parser.h" #include "compiler/mir/function.h" #include "compiler/mir/module.h" @@ -37,6 +39,56 @@ using namespace COMPILER; +namespace { + +EVMCompilerMIRSnapshot captureMIRSnapshot(MFunction &MF) { + EVMCompilerMIRSnapshot Snapshot; + Snapshot.BasicBlocks = MF.getNumBasicBlocks(); + Snapshot.Instructions = MF.getNumInstructions(); + Snapshot.Variables = MF.getNumVariables(); + for (MBasicBlock *BB : MF) { + for (MInstruction *Instruction : *BB) { + if (Instruction->getOpcode() != OP_phi) { + continue; + } + ++Snapshot.PhiInstructions; + Snapshot.PhiIncomingEdges += + llvm::cast(Instruction)->getNumIncoming(); + } + } + return Snapshot; +} + +EVMCompilerCgSnapshot captureCgSnapshot(CgFunction &MF, bool CountStackSlots) { + EVMCompilerCgSnapshot Snapshot; + Snapshot.BasicBlocks = MF.size(); + Snapshot.VirtualRegisters = MF.getRegInfo().getNumVirtRegs(); + const auto &TII = MF.getTargetInstrInfo(); + auto &Workaround = MF.getContext().getLLVMWorkaround(); + for (CgBasicBlock *BB : MF) { + for (const CgInstruction &Instruction : *BB) { + ++Snapshot.Instructions; + if (Instruction.isPHI()) { + ++Snapshot.PhiInstructions; + Snapshot.PhiIncomingEdges += (Instruction.getNumOperands() - 1) / 2; + } + if (!CountStackSlots) { + continue; + } + int FrameIndex = 0; + if (Workaround.isLoadFromStackSlot(TII, Instruction, FrameIndex) != 0) { + ++Snapshot.StackSlotLoads; + } + if (Workaround.isStoreToStackSlot(TII, Instruction, FrameIndex) != 0) { + ++Snapshot.StackSlotStores; + } + } + } + return Snapshot; +} + +} // namespace + // mprotect need protect by chunks(0x1000) in occulum // so align code size space to 0x1000 const size_t MPROTECT_CHUNK_SIZE = 0x1000; @@ -55,8 +107,14 @@ static inline bool isFuncNeedGreedyRA(uint32_t FuncIdx) { #endif // ZEN_ENABLE_DEBUG_GREEDY_RA void JITCompilerBase::compileMIRToCgIR(MModule &MMod, MFunction &MFunc, - CgFunction &CgFunc, - bool DisableGreedyRA) { + CgFunction &CgFunc, bool DisableGreedyRA, + EVMCompilerObservation *Observation) { + if (Observation != nullptr && Observation->Enabled) { + EVMCompilerPhaseTimer PhaseTimer(Observation, + EVMCompilerPhase::ObservationOverhead); + Observation->MIRAfterFrontend = captureMIRSnapshot(MFunc); + Observation->DisableGreedyRA = DisableGreedyRA; + } #ifdef ZEN_ENABLE_MULTIPASS_JIT_LOGGING llvm::DebugFlag = true; llvm::dbgs() << "\n########## MIR Dump ##########\n\n"; @@ -64,59 +122,111 @@ void JITCompilerBase::compileMIRToCgIR(MModule &MMod, MFunction &MFunc, #endif { + EVMCompilerPhaseTimer PhaseTimer(Observation, EVMCompilerPhase::MIRVerify); MVerifier Verifier(MMod, MFunc, llvm::errs()); if (!Verifier.verify()) { throw getError(ErrorCode::MIRVerifyingFailed); } } - DeadMBasicBlockElim MBBDCE; - MBBDCE.runOnMFunction(MFunc); + { + EVMCompilerPhaseTimer PhaseTimer(Observation, EVMCompilerPhase::MIRDCE); + DeadMBasicBlockElim MBBDCE; + MBBDCE.runOnMFunction(MFunc); + } + if (Observation != nullptr && Observation->Enabled) { + EVMCompilerPhaseTimer PhaseTimer(Observation, + EVMCompilerPhase::ObservationOverhead); + Observation->MIRAfterDCE = captureMIRSnapshot(MFunc); + } CgFunction &MF = CgFunc; - // TODO: refactor to pass - X86CgLowering CgLowering(MF); - X86CgPeephole CgPeephole(MF); - CgPhiElimination PhiElimination; - PhiElimination.runOnCgFunction(MF); + { + EVMCompilerPhaseTimer PhaseTimer(Observation, EVMCompilerPhase::CgLowering); + // TODO: refactor to pass + X86CgLowering CgLowering(MF); + X86CgPeephole CgPeephole(MF); + } + if (Observation != nullptr && Observation->Enabled) { + EVMCompilerPhaseTimer PhaseTimer(Observation, + EVMCompilerPhase::ObservationOverhead); + Observation->CgBeforePhi = captureCgSnapshot(MF, false); + } + + { + EVMCompilerPhaseTimer PhaseTimer(Observation, + EVMCompilerPhase::PhiElimination); + CgPhiElimination PhiElimination; + const auto PhiStats = PhiElimination.runOnCgFunction(MF); + if (Observation != nullptr && Observation->Enabled) { + Observation->PhiElimination.PhiInstructions = PhiStats.PhiInstructions; + Observation->PhiElimination.PhiIncomingEdges = PhiStats.PhiIncomingEdges; + Observation->PhiElimination.CandidateEdgeCopies = + PhiStats.CandidateEdgeCopies; + Observation->PhiElimination.IdentityEdgeCopies = + PhiStats.IdentityEdgeCopies; + Observation->PhiElimination.EmittedCopyInstructions = + PhiStats.EmittedCopyInstructions; + Observation->PhiElimination.SplitCriticalEdges = + PhiStats.SplitCriticalEdges; + } + } + if (Observation != nullptr && Observation->Enabled) { + EVMCompilerPhaseTimer PhaseTimer(Observation, + EVMCompilerPhase::ObservationOverhead); + Observation->CgAfterPhi = captureCgSnapshot(MF, false); + } uint32_t MFuncIdx = MFunc.getFuncIdx(); - if (DisableGreedyRA) { - ZEN_LOG_DEBUG("using fast ra for function %d", MFuncIdx); - FastRA RA(MF); - } else { -#ifdef ZEN_ENABLE_DEBUG_GREEDY_RA - if (!isFuncNeedGreedyRA(MFuncIdx)) { + { + EVMCompilerPhaseTimer PhaseTimer(Observation, + EVMCompilerPhase::RegisterAllocation); + if (DisableGreedyRA) { ZEN_LOG_DEBUG("using fast ra for function %d", MFuncIdx); FastRA RA(MF); } else { +#ifdef ZEN_ENABLE_DEBUG_GREEDY_RA + if (!isFuncNeedGreedyRA(MFuncIdx)) { + ZEN_LOG_DEBUG("using fast ra for function %d", MFuncIdx); + FastRA RA(MF); + } else { #endif // ZEN_ENABLE_DEBUG_GREEDY_RA - ZEN_LOG_DEBUG("using greedy ra for function %d", MFuncIdx); - CgDeadCgInstructionElim DCE(MF); - CgDominatorTree DomTree(MF); - CgLoopInfo Loops(MF); - CgSlotIndexes Indexes(MF); - CgLiveIntervals LIS(MF); - CgLiveStacks LSS(MF); - CgBlockFrequencyInfo MBFI(MF); - // CgRegisterCoalescer must before CgVirtRegMap - CgRegisterCoalescer Coalescer(MF); - CgVirtRegMap VRM(MF); - CgLiveRegMatrix Matrix(MF); - // RABasic ra(MF); - - CgEdgeBundles EdgeBundles(MF); - CgSpillPlacement SpillPlacer(MF); - MF.EvictAdvisor = std::unique_ptr( - createReleaseModeAdvisor()); - std::shared_ptr RA = std::make_shared(MF); - - CgVirtRegRewriter Rewriter(MF); + ZEN_LOG_DEBUG("using greedy ra for function %d", MFuncIdx); + CgDeadCgInstructionElim DCE(MF); + CgDominatorTree DomTree(MF); + CgLoopInfo Loops(MF); + CgSlotIndexes Indexes(MF); + CgLiveIntervals LIS(MF); + if (Observation != nullptr && Observation->Enabled) { + Observation->LiveIntervals = MF.getRegInfo().getNumVirtRegs(); + Observation->LiveIntervalsAvailable = true; + } + CgLiveStacks LSS(MF); + CgBlockFrequencyInfo MBFI(MF); + // CgRegisterCoalescer must before CgVirtRegMap + CgRegisterCoalescer Coalescer(MF); + CgVirtRegMap VRM(MF); + CgLiveRegMatrix Matrix(MF); + // RABasic ra(MF); + + CgEdgeBundles EdgeBundles(MF); + CgSpillPlacement SpillPlacer(MF); + MF.EvictAdvisor = std::unique_ptr( + createReleaseModeAdvisor()); + std::shared_ptr RA = std::make_shared(MF); + + CgVirtRegRewriter Rewriter(MF); #ifdef ZEN_ENABLE_DEBUG_GREEDY_RA - } + } #endif // ZEN_ENABLE_DEBUG_GREEDY_RA + } + } + if (Observation != nullptr && Observation->Enabled) { + EVMCompilerPhaseTimer PhaseTimer(Observation, + EVMCompilerPhase::ObservationOverhead); + Observation->CgAfterRA = captureCgSnapshot(MF, true); } #ifdef ZEN_ENABLE_MULTIPASS_JIT_LOGGING @@ -125,24 +235,31 @@ void JITCompilerBase::compileMIRToCgIR(MModule &MMod, MFunction &MFunc, MF.dump(); #endif - PrologEpilogInserter PEInserter; - PEInserter.runOnCgFunction(MF); + { + EVMCompilerPhaseTimer PhaseTimer(Observation, EVMCompilerPhase::PostRA); + PrologEpilogInserter PEInserter; + PEInserter.runOnCgFunction(MF); #ifdef ZEN_ENABLE_MULTIPASS_JIT_LOGGING - llvm::dbgs() << "\n########## CgIR Dump After Prologue/Epilogue Insertion " - "##########\n\n"; - MF.dump(); + llvm::dbgs() << "\n########## CgIR Dump After Prologue/Epilogue " + "Insertion ##########\n\n"; + MF.dump(); #endif - ExpandPostRAPseudos PseudosExpander; - PseudosExpander.runOnCgFunction(MF); + ExpandPostRAPseudos PseudosExpander; + PseudosExpander.runOnCgFunction(MF); #ifdef ZEN_ENABLE_MULTIPASS_JIT_LOGGING - llvm::dbgs() << "\n########## CgIR Dump After Post-RA Pseudo " - "Instruction Expansion " - "##########\n\n"; - MF.dump(); + llvm::dbgs() << "\n########## CgIR Dump After Post-RA Pseudo " + "Instruction Expansion ##########\n\n"; + MF.dump(); #endif - if (MF.EvictAdvisor) { - MF.EvictAdvisor.reset(); + if (MF.EvictAdvisor) { + MF.EvictAdvisor.reset(); + } + } + if (Observation != nullptr && Observation->Enabled) { + EVMCompilerPhaseTimer PhaseTimer(Observation, + EVMCompilerPhase::ObservationOverhead); + Observation->CgAfterPostRA = captureCgSnapshot(MF, true); } } diff --git a/src/compiler/compiler.h b/src/compiler/compiler.h index 2fb0da0a1..abd998e5a 100644 --- a/src/compiler/compiler.h +++ b/src/compiler/compiler.h @@ -14,13 +14,15 @@ class WasmFrontendContext; class MModule; class MFunction; class CgFunction; +struct EVMCompilerObservation; class JITCompilerBase : public NonCopyable { protected: virtual ~JITCompilerBase() = default; static void compileMIRToCgIR(MModule &Mod, MFunction &MFunc, - CgFunction &CgFunc, bool DisableGreedyRA); + CgFunction &CgFunc, bool DisableGreedyRA, + EVMCompilerObservation *Observation = nullptr); static void emitObjectBuffer(CompileContext *Ctx); }; diff --git a/src/compiler/evm_compiler.cpp b/src/compiler/evm_compiler.cpp index b5bff2086..08017329b 100644 --- a/src/compiler/evm_compiler.cpp +++ b/src/compiler/evm_compiler.cpp @@ -4,6 +4,7 @@ #include "compiler/evm_compiler.h" #include "common/thread_pool.h" #include "compiler/cgir/cg_function.h" +#include "compiler/evm_compiler_metrics.h" #include "compiler/mir/module.h" #include "compiler/target/x86/x86_mc_lowering.h" #include "platform/map.h" @@ -17,6 +18,12 @@ #include "llvm/Support/Debug.h" #endif // ZEN_ENABLE_MULTIPASS_JIT_LOGGING #include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Format.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include // Constants for memory protection alignment const size_t MPROTECT_CHUNK_SIZE = 0x1000; @@ -26,8 +33,163 @@ const size_t MPROTECT_CHUNK_SIZE = 0x1000; namespace COMPILER { +namespace { + +bool isCompilerObservationEnabled() { + const char *Value = std::getenv("DTVM_EVM_COMPILER_OBSERVE"); + return Value != nullptr && Value[0] != '\0' && std::strcmp(Value, "0") != 0; +} + +uint64_t fingerprintBytecode(const uint8_t *Bytecode, size_t BytecodeSize) { + constexpr uint64_t FNVOffsetBasis = 14695981039346656037ULL; + constexpr uint64_t FNVPrime = 1099511628211ULL; + uint64_t Hash = FNVOffsetBasis; + for (size_t Index = 0; Index < BytecodeSize; ++Index) { + Hash ^= Bytecode[Index]; + Hash *= FNVPrime; + } + return Hash; +} + +void emitMIRSnapshot(llvm::raw_ostream &OS, + const EVMCompilerMIRSnapshot &Snapshot) { + OS << "{\"basic_blocks\":" << Snapshot.BasicBlocks + << ",\"instructions\":" << Snapshot.Instructions + << ",\"variables\":" << Snapshot.Variables + << ",\"phi_instructions\":" << Snapshot.PhiInstructions + << ",\"phi_incoming_edges\":" << Snapshot.PhiIncomingEdges << "}"; +} + +void emitCgSnapshot(llvm::raw_ostream &OS, + const EVMCompilerCgSnapshot &Snapshot) { + OS << "{\"basic_blocks\":" << Snapshot.BasicBlocks + << ",\"instructions\":" << Snapshot.Instructions + << ",\"phi_instructions\":" << Snapshot.PhiInstructions + << ",\"phi_incoming_edges\":" << Snapshot.PhiIncomingEdges + << ",\"virtual_registers\":" << Snapshot.VirtualRegisters + << ",\"stack_slot_loads\":" << Snapshot.StackSlotLoads + << ",\"stack_slot_stores\":" << Snapshot.StackSlotStores << "}"; +} + +void emitCompilerObservation(const EVMCompilerObservation &Observation) { + static constexpr const char *PhaseNames[] = { + "context_setup", "frontend", + "mir_verify", "mir_dce", + "cg_lowering", "phi_elimination", + "register_allocation", "post_ra", + "mc_lowering", "object_emission", + "code_publish", "observation_overhead", + }; + static_assert(std::size(PhaseNames) == + static_cast(EVMCompilerPhase::Count), + "phase names must cover every EVM compiler observation phase"); + + const uint64_t AccountedNs = Observation.getAccountedPhaseNs(); + const uint64_t UnaccountedNs = + Observation.TotalNs > AccountedNs ? Observation.TotalNs - AccountedNs : 0; + const double AccountedPercent = + Observation.TotalNs == 0 ? 0.0 + : static_cast(AccountedNs) * 100.0 / + static_cast(Observation.TotalNs); + + std::string Record; + llvm::raw_string_ostream OS(Record); + OS << "{\"schema_version\":1" + << ",\"compile_succeeded\":" + << (Observation.CompileSucceeded ? "true" : "false") +#ifdef ZEN_ENABLE_EVM_STACK_SSA_LIFT + << ",\"stack_ssa_enabled\":true" +#else + << ",\"stack_ssa_enabled\":false" +#endif +#ifdef ZEN_ENABLE_EVM_MEMORY_PLAN_FRAMEWORK + << ",\"memory_plan_framework_enabled\":true" +#else + << ",\"memory_plan_framework_enabled\":false" +#endif + << ",\"ra_mode\":\"" << (Observation.DisableGreedyRA ? "fast" : "greedy") + << "\"" + << ",\"bytecode_size\":" << Observation.BytecodeSize + << ",\"bytecode_fingerprint_fnv1a64\":\"" + << llvm::format_hex(Observation.BytecodeFingerprint, 18) << "\"" + << ",\"total_ns\":" << Observation.TotalNs + << ",\"accounted_phase_ns\":" << AccountedNs + << ",\"unaccounted_ns\":" << UnaccountedNs + << ",\"accounted_percent\":" << AccountedPercent << ",\"phases_ns\":{"; + for (size_t Index = 0; Index < std::size(PhaseNames); ++Index) { + if (Index != 0) { + OS << ","; + } + OS << "\"" << PhaseNames[Index] << "\":" << Observation.PhaseNs[Index]; + } + OS << "},\"stack_lift\":{" + << "\"analyzed_blocks\":" << Observation.StackLift.AnalyzedBlocks + << ",\"lifted_blocks\":" << Observation.StackLift.LiftedBlocks + << ",\"non_lifted_blocks\":" << Observation.StackLift.NonLiftedBlocks + << ",\"merge_blocks\":" << Observation.StackLift.MergeBlocks + << ",\"merge_slots\":" << Observation.StackLift.MergeSlots + << ",\"merge_predecessor_edges\":" + << Observation.StackLift.MergePredecessorEdges + << ",\"recorded_incoming_states\":" + << Observation.StackLift.RecordedIncomingStates + << ",\"folded_merge_slots\":" << Observation.StackLift.FoldedMergeSlots + << ",\"materialization_requests\":" + << Observation.StackLift.MaterializationRequests + << ",\"protected_incoming_values\":" + << Observation.StackLift.ProtectedIncomingValues + << ",\"materialized_u256_merges\":" + << Observation.StackLift.MaterializedU256Merges << "}" + << ",\"mir_after_frontend\":"; + emitMIRSnapshot(OS, Observation.MIRAfterFrontend); + OS << ",\"mir_after_dce\":"; + emitMIRSnapshot(OS, Observation.MIRAfterDCE); + OS << ",\"cg_before_phi\":"; + emitCgSnapshot(OS, Observation.CgBeforePhi); + OS << ",\"cg_after_phi\":"; + emitCgSnapshot(OS, Observation.CgAfterPhi); + OS << ",\"cg_after_ra\":"; + emitCgSnapshot(OS, Observation.CgAfterRA); + OS << ",\"cg_after_post_ra\":"; + emitCgSnapshot(OS, Observation.CgAfterPostRA); + OS << ",\"phi_elimination\":{" + << "\"phi_instructions\":" << Observation.PhiElimination.PhiInstructions + << ",\"phi_incoming_edges\":" + << Observation.PhiElimination.PhiIncomingEdges + << ",\"candidate_edge_copies\":" + << Observation.PhiElimination.CandidateEdgeCopies + << ",\"identity_edge_copies\":" + << Observation.PhiElimination.IdentityEdgeCopies + << ",\"emitted_copy_instructions\":" + << Observation.PhiElimination.EmittedCopyInstructions + << ",\"split_critical_edges\":" + << Observation.PhiElimination.SplitCriticalEdges << "}" + << ",\"feature_coverage\":{" + << "\"memory_expansion_plans\":" + << Observation.FeatureCoverage.MemoryExpansionPlans + << ",\"memory_expansion_plan_covered_ops\":" + << Observation.FeatureCoverage.MemoryExpansionPlanCoveredOps + << ",\"memory_expansion_plan_estimated_reduced_expansions\":" + << Observation.FeatureCoverage + .MemoryExpansionPlanEstimatedReducedExpansions + << ",\"range_u64_fast_paths\":" + << Observation.FeatureCoverage.RangeU64FastPaths + << ",\"const_u64_fast_paths\":" + << Observation.FeatureCoverage.ConstU64FastPaths + << ",\"full_arithmetic_paths\":" + << Observation.FeatureCoverage.FullArithmeticPaths << "}" + << ",\"live_intervals_available\":" + << (Observation.LiveIntervalsAvailable ? "true" : "false") + << ",\"live_intervals\":" << Observation.LiveIntervals + << ",\"emitted_code_bytes\":" << Observation.EmittedCodeBytes << "}"; + OS.flush(); + llvm::errs() << "[DTVM_EVM_COMPILER_OBSERVATION] " << Record << "\n"; +} + +} // namespace + void EVMJITCompiler::compileEVMToMC(EVMFrontendContext &Ctx, MModule &Mod, - uint32_t FuncIdx, bool DisableGreedyRA) { + uint32_t FuncIdx, bool DisableGreedyRA, + EVMCompilerObservation *Observation) { if (Ctx.Inited) { // Release all memory allocated by previous function compilation Ctx.MemPool = CompileMemPool(); @@ -43,19 +205,57 @@ void EVMJITCompiler::compileEVMToMC(EVMFrontendContext &Ctx, MModule &Mod, CgFunction CgFunc(Ctx, MFunc); MFunc.setFunctionType(Mod.getFuncType(FuncIdx)); EVMMirBuilder MIRBuilder(Ctx, MFunc); - MIRBuilder.compile(&Ctx); + { + EVMCompilerPhaseTimer PhaseTimer(Observation, EVMCompilerPhase::Frontend); + MIRBuilder.compile(&Ctx); + } + if (Observation != nullptr && Observation->Enabled) { + EVMCompilerPhaseTimer PhaseTimer(Observation, + EVMCompilerPhase::ObservationOverhead); + Observation->StackLift = MIRBuilder.getStackLiftStatistics(); +#ifdef ZEN_ENABLE_MULTIPASS_JIT_LOGGING + const auto Stats = MIRBuilder.getFeatureCoverageStatistics(); + Observation->FeatureCoverage.MemoryExpansionPlans = + Stats.MemoryExpansionPlans; + Observation->FeatureCoverage.MemoryExpansionPlanCoveredOps = + Stats.MemoryExpansionPlanCoveredOps; + Observation->FeatureCoverage.MemoryExpansionPlanEstimatedReducedExpansions = + Stats.MemoryExpansionPlanEstimatedReducedExpansions; + Observation->FeatureCoverage.RangeU64FastPaths = Stats.RangeU64FastPaths; + Observation->FeatureCoverage.ConstU64FastPaths = Stats.ConstU64FastPaths; + Observation->FeatureCoverage.FullArithmeticPaths = + Stats.FullArithmeticPaths; +#endif // ZEN_ENABLE_MULTIPASS_JIT_LOGGING + } #ifdef ZEN_ENABLE_MULTIPASS_JIT_LOGGING MIRBuilder.dumpMemoryCompileStats(); #endif // ZEN_ENABLE_MULTIPASS_JIT_LOGGING // Apply MIR optimizations and generate machine code - compileMIRToCgIR(Mod, MFunc, CgFunc, DisableGreedyRA); + compileMIRToCgIR(Mod, MFunc, CgFunc, DisableGreedyRA, Observation); // Generate machine code - Ctx.getMCLowering().runOnCgFunction(CgFunc); + { + EVMCompilerPhaseTimer PhaseTimer(Observation, EVMCompilerPhase::MCLowering); + Ctx.getMCLowering().runOnCgFunction(CgFunc); + } } void EagerEVMJITCompiler::compile() { + EVMCompilerObservation Observation; + Observation.Enabled = isCompilerObservationEnabled(); + Observation.BytecodeSize = EVMMod->CodeSize; + if (Observation.Enabled) { + Observation.BytecodeFingerprint = fingerprintBytecode( + reinterpret_cast(EVMMod->Code), EVMMod->CodeSize); + } + EVMCompilerObservation *ObservationPtr = + Observation.Enabled ? &Observation : nullptr; + EVMCompilerObservation::TimePoint ObservationStart; + if (Observation.Enabled) { + ObservationStart = EVMCompilerObservation::Clock::now(); + } + // 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 @@ -80,24 +280,30 @@ void EagerEVMJITCompiler::compile() { try { EVMFrontendContext Ctx; - Ctx.setGasMeteringEnabled(Config.EnableEvmGasMetering); + { + EVMCompilerPhaseTimer PhaseTimer(ObservationPtr, + EVMCompilerPhase::ContextSetup); + 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(); - // GasChunkCostSPP is only allocated when the SPP metering pipeline runs - // (i.e. this module will be JIT-compiled). Pass nullptr when the array is - // empty so the JIT falls back to the unshifted GasChunkCost automatically. - const uint64_t *CostSPPPtr = - Cache.GasChunkCostSPP.empty() ? nullptr : Cache.GasChunkCostSPP.data(); - Ctx.setGasChunkInfo(Cache.GasChunkEnd.data(), Cache.GasChunkCost.data(), - CostSPPPtr, EVMMod->CodeSize); - Ctx.setResolvedJumpTargets(&Cache.ResolvedJumpTargets); + Ctx.setRevision(EVMMod->getRevision()); + Ctx.setBytecode(reinterpret_cast(EVMMod->Code), + EVMMod->CodeSize); + Ctx.setMemoryLinearStrideSkipLeadingZeroLimbStores( + EVMMod->getMemoryLinearStrideSkipLeadingZeroLimbStores()); + const auto &Cache = EVMMod->getBytecodeCache(); + // GasChunkCostSPP is only allocated when the SPP metering pipeline runs + // (i.e. this module will be JIT-compiled). Pass nullptr when the array is + // empty so the JIT falls back to the unshifted GasChunkCost + // automatically. + const uint64_t *CostSPPPtr = Cache.GasChunkCostSPP.empty() + ? nullptr + : Cache.GasChunkCostSPP.data(); + Ctx.setGasChunkInfo(Cache.GasChunkEnd.data(), Cache.GasChunkCost.data(), + CostSPPPtr, EVMMod->CodeSize); + Ctx.setResolvedJumpTargets(&Cache.ResolvedJumpTargets); + } MModule Mod(Ctx); buildEVMFunction(Ctx, Mod, *EVMMod); Ctx.CodeMPool = &EVMMod->getJITCodeMemPool(); @@ -115,38 +321,52 @@ void EagerEVMJITCompiler::compile() { 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); + compileEVMToMC(Ctx, Mod, 0, Config.DisableMultipassGreedyRA, + ObservationPtr); + { + EVMCompilerPhaseTimer PhaseTimer(ObservationPtr, + EVMCompilerPhase::ObjectEmission); + emitObjectBuffer(&Ctx); + } ZEN_ASSERT(Ctx.ExternRelocs.empty()); - uint8_t *JITFuncPtr = Ctx.CodePtr + Ctx.FuncOffsetMap[0]; + { + EVMCompilerPhaseTimer PhaseTimer(ObservationPtr, + EVMCompilerPhase::CodePublish); + uint8_t *JITFuncPtr = Ctx.CodePtr + Ctx.FuncOffsetMap[0]; + EVMMod->setEmittedJITCodeSize(Ctx.CodeSize); + if (Observation.Enabled) { + Observation.EmittedCodeBytes = Ctx.CodeSize; + } #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 - // 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); + // 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; + Observation.CompileSucceeded = true; } catch (const std::exception &E) { ZEN_LOG_ERROR("EVM JIT compilation failed: %s", E.what()); EVMMod->ShouldFallbackToInterp = true; @@ -154,5 +374,13 @@ void EagerEVMJITCompiler::compile() { ZEN_LOG_ERROR("EVM JIT compilation failed"); EVMMod->ShouldFallbackToInterp = true; } + if (Observation.Enabled) { + const auto ObservationEnd = EVMCompilerObservation::Clock::now(); + Observation.TotalNs = static_cast( + std::chrono::duration_cast(ObservationEnd - + ObservationStart) + .count()); + emitCompilerObservation(Observation); + } } } // namespace COMPILER diff --git a/src/compiler/evm_compiler.h b/src/compiler/evm_compiler.h index 0dac7b84d..731d6c729 100644 --- a/src/compiler/evm_compiler.h +++ b/src/compiler/evm_compiler.h @@ -19,7 +19,8 @@ class EVMJITCompiler : public JITCompilerBase { ~EVMJITCompiler() override = default; void compileEVMToMC(EVMFrontendContext &Ctx, MModule &Mod, uint32_t FuncIdx, - bool DisableGreedyRA); + bool DisableGreedyRA, + EVMCompilerObservation *Observation = nullptr); runtime::EVMModule *EVMMod; const runtime::RuntimeConfig &Config; diff --git a/src/compiler/evm_compiler_metrics.h b/src/compiler/evm_compiler_metrics.h new file mode 100644 index 000000000..47f610912 --- /dev/null +++ b/src/compiler/evm_compiler_metrics.h @@ -0,0 +1,139 @@ +// Copyright (C) 2026 the DTVM authors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#ifndef ZEN_COMPILER_EVM_COMPILER_METRICS_H +#define ZEN_COMPILER_EVM_COMPILER_METRICS_H + +#include "compiler/evm_frontend/evm_lifted_stack_lifter.h" + +#include +#include +#include +#include + +namespace COMPILER { + +enum class EVMCompilerPhase : uint8_t { + ContextSetup = 0, + Frontend, + MIRVerify, + MIRDCE, + CgLowering, + PhiElimination, + RegisterAllocation, + PostRA, + MCLowering, + ObjectEmission, + CodePublish, + ObservationOverhead, + Count, +}; + +struct EVMCompilerMIRSnapshot { + uint64_t BasicBlocks = 0; + uint64_t Instructions = 0; + uint64_t Variables = 0; + uint64_t PhiInstructions = 0; + uint64_t PhiIncomingEdges = 0; +}; + +struct EVMCompilerCgSnapshot { + uint64_t BasicBlocks = 0; + uint64_t Instructions = 0; + uint64_t PhiInstructions = 0; + uint64_t PhiIncomingEdges = 0; + uint64_t VirtualRegisters = 0; + uint64_t StackSlotLoads = 0; + uint64_t StackSlotStores = 0; +}; + +struct EVMCompilerPhiEliminationMetrics { + uint64_t PhiInstructions = 0; + uint64_t PhiIncomingEdges = 0; + uint64_t CandidateEdgeCopies = 0; + uint64_t IdentityEdgeCopies = 0; + uint64_t EmittedCopyInstructions = 0; + uint64_t SplitCriticalEdges = 0; +}; + +struct EVMCompilerFeatureCoverage { + uint64_t MemoryExpansionPlans = 0; + uint64_t MemoryExpansionPlanCoveredOps = 0; + uint64_t MemoryExpansionPlanEstimatedReducedExpansions = 0; + uint64_t RangeU64FastPaths = 0; + uint64_t ConstU64FastPaths = 0; + uint64_t FullArithmeticPaths = 0; +}; + +struct EVMCompilerObservation { + using Clock = std::chrono::steady_clock; + using TimePoint = Clock::time_point; + + bool Enabled = false; + bool CompileSucceeded = false; + bool DisableGreedyRA = false; + uint64_t BytecodeSize = 0; + uint64_t BytecodeFingerprint = 0; + uint64_t TotalNs = 0; + uint64_t EmittedCodeBytes = 0; + uint64_t LiveIntervals = 0; + bool LiveIntervalsAvailable = false; + std::array(EVMCompilerPhase::Count)> PhaseNs{}; + EVMLiftedStackStatistics StackLift; + EVMCompilerMIRSnapshot MIRAfterFrontend; + EVMCompilerMIRSnapshot MIRAfterDCE; + EVMCompilerCgSnapshot CgBeforePhi; + EVMCompilerCgSnapshot CgAfterPhi; + EVMCompilerCgSnapshot CgAfterRA; + EVMCompilerCgSnapshot CgAfterPostRA; + EVMCompilerPhiEliminationMetrics PhiElimination; + EVMCompilerFeatureCoverage FeatureCoverage; + + void addPhaseTime(EVMCompilerPhase Phase, uint64_t DurationNs) { + if (!Enabled) { + return; + } + PhaseNs[static_cast(Phase)] += DurationNs; + } + + uint64_t getAccountedPhaseNs() const { + uint64_t Total = 0; + for (uint64_t Duration : PhaseNs) { + Total += Duration; + } + return Total; + } +}; + +class EVMCompilerPhaseTimer final { +public: + EVMCompilerPhaseTimer(EVMCompilerObservation *Observation, + EVMCompilerPhase Phase) + : Observation(Observation), Phase(Phase) { + if (Observation != nullptr && Observation->Enabled) { + Start = EVMCompilerObservation::Clock::now(); + Active = true; + } + } + + ~EVMCompilerPhaseTimer() { + if (!Active) { + return; + } + const auto End = EVMCompilerObservation::Clock::now(); + const uint64_t DurationNs = static_cast( + std::chrono::duration_cast(End - Start) + .count()); + Observation->addPhaseTime(Phase, DurationNs); + } + +private: + EVMCompilerObservation *Observation = nullptr; + EVMCompilerPhase Phase = EVMCompilerPhase::ContextSetup; + EVMCompilerObservation::TimePoint Start; + bool Active = false; +}; + +} // namespace COMPILER + +#endif // ZEN_COMPILER_EVM_COMPILER_METRICS_H diff --git a/src/compiler/evm_frontend/evm_analyzer.h b/src/compiler/evm_frontend/evm_analyzer.h index cad82e20f..91707c7d9 100644 --- a/src/compiler/evm_frontend/evm_analyzer.h +++ b/src/compiler/evm_frontend/evm_analyzer.h @@ -117,6 +117,8 @@ struct JITSuitabilityResult { size_t MaxConsecutiveExpensive = 0; // longest unbroken run size_t MaxBlockExpensiveCount = 0; // max RA-expensive ops in one block size_t DupFeedbackPatternCount = 0; // DUPn immediately before RA-expensive + size_t DynamicEntryMergeFallbackBlocks = 0; + size_t DynamicEntryMergeFallbackEdges = 0; }; /// Thresholds for JIT suitability fallback. These limits bound bytecode size @@ -1527,6 +1529,8 @@ class EVMAnalyzer { // loop. const std::vector DispatchSources = collectAllDynamicJumpDispatchSourceBlocks(); + size_t DynamicEntryMergeFallbackBlocks = 0; + size_t DynamicEntryMergeFallbackEdges = 0; for (auto &[EntryPC, Info] : BlockInfos) { (void)EntryPC; bool EntryKnown = Info.IsEntryStateCompatible; @@ -1614,6 +1618,17 @@ class EVMAnalyzer { } } + for (const auto &[EntryPC, Info] : BlockInfos) { + (void)EntryPC; + if (!Info.IsDynamicJumpTargetCandidate || Info.CanLiftStack) { + continue; + } + ++DynamicEntryMergeFallbackBlocks; + DynamicEntryMergeFallbackEdges += DispatchSources.size(); + } + JITResult.DynamicEntryMergeFallbackBlocks = DynamicEntryMergeFallbackBlocks; + JITResult.DynamicEntryMergeFallbackEdges = DynamicEntryMergeFallbackEdges; + // A block with a hidden live-in prefix (absolute entry depth exceeds the // block's local entry depth) can only be lifted soundly when it never // reconciles its hidden prefix slots with the runtime stack. The lifter diff --git a/src/compiler/evm_frontend/evm_lifted_stack_lifter.h b/src/compiler/evm_frontend/evm_lifted_stack_lifter.h index de5d5686e..b7ab4697b 100644 --- a/src/compiler/evm_frontend/evm_lifted_stack_lifter.h +++ b/src/compiler/evm_frontend/evm_lifted_stack_lifter.h @@ -15,6 +15,20 @@ namespace COMPILER { +struct EVMLiftedStackStatistics { + uint64_t AnalyzedBlocks = 0; + uint64_t LiftedBlocks = 0; + uint64_t NonLiftedBlocks = 0; + uint64_t MergeBlocks = 0; + uint64_t MergeSlots = 0; + uint64_t MergePredecessorEdges = 0; + uint64_t RecordedIncomingStates = 0; + uint64_t FoldedMergeSlots = 0; + uint64_t MaterializationRequests = 0; + uint64_t ProtectedIncomingValues = 0; + uint64_t MaterializedU256Merges = 0; +}; + template class EVMLiftedStackLifter { public: using Operand = typename IRBuilder::Operand; @@ -96,6 +110,7 @@ template class EVMLiftedStackLifter { ValueIds.clear(); NextValueId = 1; NextOpaqueValueId = 1; + AnalyzedBlocks = Analyzer.getBlockInfos().size(); #ifdef ZEN_ENABLE_EVM_STACK_SSA_LIFT for (const auto &[EntryPC, BlockInfo] : Analyzer.getBlockInfos()) { if (!BlockInfo.CanLiftStack) { @@ -138,6 +153,35 @@ template class EVMLiftedStackLifter { #endif } + EVMLiftedStackStatistics getStatistics() const { + EVMLiftedStackStatistics Stats; + Stats.AnalyzedBlocks = AnalyzedBlocks; + Stats.LiftedBlocks = LiftedBlocks.size(); + Stats.NonLiftedBlocks = + AnalyzedBlocks >= LiftedBlocks.size() + ? AnalyzedBlocks - static_cast(LiftedBlocks.size()) + : 0; + for (const auto &[BlockPC, EntryState] : BlockEntryStates) { + (void)BlockPC; + Stats.RecordedIncomingStates += EntryState.IncomingStates.size(); + if (EntryState.PendingPhis.empty()) { + continue; + } + ++Stats.MergeBlocks; + Stats.MergeSlots += EntryState.PendingPhis.size(); + Stats.MergePredecessorEdges += EntryState.ExpectedIncomingCount; + for (const PendingPhi &Phi : EntryState.PendingPhis) { + if (Phi.IsComplete && + Phi.Resolution == PendingPhi::ResolutionKind::Folded) { + ++Stats.FoldedMergeSlots; + } else { + ++Stats.MaterializationRequests; + } + } + } + return Stats; + } + bool isLiftedBlock(uint64_t BlockPC) const { #ifdef ZEN_ENABLE_EVM_STACK_SSA_LIFT return LiftedBlocks.count(BlockPC) != 0; @@ -483,6 +527,7 @@ template class EVMLiftedStackLifter { std::set LiftedBlocks; std::map BlockEntryStates; std::map ValueIds; + uint64_t AnalyzedBlocks = 0; uint64_t NextValueId = 1; uint64_t NextOpaqueValueId = 1; }; diff --git a/src/compiler/evm_frontend/evm_mir_compiler.cpp b/src/compiler/evm_frontend/evm_mir_compiler.cpp index 9ff0e22f6..998533bc9 100644 --- a/src/compiler/evm_frontend/evm_mir_compiler.cpp +++ b/src/compiler/evm_frontend/evm_mir_compiler.cpp @@ -1202,6 +1202,7 @@ void EVMMirBuilder::assignStackEntryOperand(const Operand &Dest, typename EVMMirBuilder::Operand EVMMirBuilder::prepareStackPhiIncoming(const Operand &Value) { + ++StackLiftStatistics.ProtectedIncomingValues; U256Inst Prepared = {}; U256Inst Src = extractU256Operand(Value); for (size_t I = 0; I < EVM_ELEMENTS_COUNT; ++I) { @@ -1215,9 +1216,21 @@ void EVMMirBuilder::registerCurrentBlockPC(uint64_t BlockPC) { BlockEntryTable[BlockPC] = CurBB; } +void EVMMirBuilder::setStackLiftStatistics( + const EVMLiftedStackStatistics &Statistics) { + const uint64_t ProtectedIncomingValues = + StackLiftStatistics.ProtectedIncomingValues; + const uint64_t MaterializedU256Merges = + StackLiftStatistics.MaterializedU256Merges; + StackLiftStatistics = Statistics; + StackLiftStatistics.ProtectedIncomingValues = ProtectedIncomingValues; + StackLiftStatistics.MaterializedU256Merges = MaterializedU256Merges; +} + typename EVMMirBuilder::Operand EVMMirBuilder::materializeStackMergeOperand( const std::vector &PredBlockPCs, const std::vector> &IncomingValues) { + ++StackLiftStatistics.MaterializedU256Merges; std::map IncomingValueMap; for (const auto &[PredBlockPC, Value] : IncomingValues) { IncomingValueMap[PredBlockPC] = Value; @@ -7438,6 +7451,27 @@ bool EVMMirBuilder::hasArithCompileStats() const { MemStats.ModU128OpportunityCount != 0; } +EVMMirBuilder::FeatureCoverageStatistics +EVMMirBuilder::getFeatureCoverageStatistics() const { + FeatureCoverageStatistics Result; + Result.MemoryExpansionPlans = MemStats.MemoryExpansionPlanCount; + Result.MemoryExpansionPlanCoveredOps = MemStats.MemoryExpansionPlanCoveredOps; + Result.MemoryExpansionPlanEstimatedReducedExpansions = + MemStats.MemoryExpansionPlanEstimatedReducedExpansions; + Result.RangeU64FastPaths = + MemStats.AddFastRangeU64Count + MemStats.SubFastRangeU64Count + + MemStats.MulFastRangeU64Count + MemStats.DivFastRangeU64Count + + MemStats.ModFastRangeU64Count; + Result.ConstU64FastPaths = + MemStats.AddFastConstU64Count + MemStats.SubFastConstU64Count + + MemStats.MulFastConstU64Count + MemStats.DivFastConstU64Count + + MemStats.ModFastConstU64Count; + Result.FullArithmeticPaths = MemStats.AddFullCount + MemStats.SubFullCount + + MemStats.MulFullCount + MemStats.DivFullCount + + MemStats.ModFullCount; + return Result; +} + void EVMMirBuilder::noteBlockMemoryEventPC(uint64_t PC) { #ifdef ZEN_ENABLE_MULTIPASS_JIT_LOGGING if (!CurBlockMemStats.Active) { diff --git a/src/compiler/evm_frontend/evm_mir_compiler.h b/src/compiler/evm_frontend/evm_mir_compiler.h index b2c045f39..d8ceeb801 100644 --- a/src/compiler/evm_frontend/evm_mir_compiler.h +++ b/src/compiler/evm_frontend/evm_mir_compiler.h @@ -6,6 +6,7 @@ #include "action/vm_eval_stack.h" #include "compiler/context.h" +#include "compiler/evm_frontend/evm_lifted_stack_lifter.h" #include "compiler/evm_frontend/evm_memory_facts.h" #include "compiler/evm_frontend/evm_memory_grouping.h" #include "compiler/evm_frontend/evm_value_range.h" @@ -143,11 +144,21 @@ class EVMMirBuilder final { using U256ConstInt = std::array; using JumpTargetPCList = std::vector; + struct FeatureCoverageStatistics { + uint64_t MemoryExpansionPlans = 0; + uint64_t MemoryExpansionPlanCoveredOps = 0; + uint64_t MemoryExpansionPlanEstimatedReducedExpansions = 0; + uint64_t RangeU64FastPaths = 0; + uint64_t ConstU64FastPaths = 0; + uint64_t FullArithmeticPaths = 0; + }; + // Range classification for u256 operands. Narrower ranges enable // single-instruction fast paths instead of expensive multi-limb arithmetic. using ValueRange = EVMValueRange; EVMMirBuilder(CompilerContext &Context, MFunction &MFunc); + FeatureCoverageStatistics getFeatureCoverageStatistics() const; class Operand { public: @@ -358,6 +369,10 @@ class EVMMirBuilder final { Operand createStackEntryOperand(ValueRange Range = ValueRange::U256); void assignStackEntryOperand(const Operand &Dest, const Operand &Value); Operand prepareStackPhiIncoming(const Operand &Value); + void setStackLiftStatistics(const EVMLiftedStackStatistics &Statistics); + const EVMLiftedStackStatistics &getStackLiftStatistics() const { + return StackLiftStatistics; + } void registerCurrentBlockPC(uint64_t BlockPC); Operand materializeStackMergeOperand( const std::vector &PredBlockPCs, @@ -1435,6 +1450,7 @@ class EVMMirBuilder final { Variable *MemoryBaseVar = nullptr; Variable *MemorySizeVar = nullptr; uint64_t CurrentBlockPC = 0; + EVMLiftedStackStatistics StackLiftStatistics; std::map BlockEntryTable; std::map> DynamicPhiIncomingBlockTable; diff --git a/src/runtime/evm_module.cpp b/src/runtime/evm_module.cpp index d49295b85..c08a4f75d 100644 --- a/src/runtime/evm_module.cpp +++ b/src/runtime/evm_module.cpp @@ -84,6 +84,9 @@ EVMModule::newEVMModule(Runtime &RT, CodeHolderUniquePtr CodeHolder, Analyzer.analyze(reinterpret_cast(Mod->Code), Mod->CodeSize); Mod->ShouldFallbackToInterp = Analyzer.getJITSuitability().ShouldFallback; + Mod->setDynamicEntryMergeFallbackMetrics( + Analyzer.getJITSuitability().DynamicEntryMergeFallbackBlocks, + Analyzer.getJITSuitability().DynamicEntryMergeFallbackEdges); #ifdef ZEN_ENABLE_MULTIPASS_JIT if (RT.getConfig().EnableProfileGuidedJIT) { diff --git a/src/runtime/evm_module.h b/src/runtime/evm_module.h index 752d0b0ff..be7b5c0a2 100644 --- a/src/runtime/evm_module.h +++ b/src/runtime/evm_module.h @@ -60,6 +60,29 @@ class EVMModule final : public BaseModule { uint64_t getKnownCallDataLoad0Low64() const { return MemoryProfile.KnownCallDataLoad0Low64; } + size_t getDynamicEntryMergeFallbackBlocks() const { +#ifdef ZEN_ENABLE_JIT + return DynamicEntryMergeFallbackBlocks.load(std::memory_order_acquire); +#else + return 0; +#endif + } + size_t getDynamicEntryMergeFallbackEdges() const { +#ifdef ZEN_ENABLE_JIT + return DynamicEntryMergeFallbackEdges.load(std::memory_order_acquire); +#else + return 0; +#endif + } + void setDynamicEntryMergeFallbackMetrics(size_t FallbackBlocks, + size_t FallbackEdges) { +#ifdef ZEN_ENABLE_JIT + DynamicEntryMergeFallbackBlocks.store(FallbackBlocks, + std::memory_order_relaxed); + DynamicEntryMergeFallbackEdges.store(FallbackEdges, + std::memory_order_relaxed); +#endif + } static constexpr int32_t getCodeSizeOffset() { static_assert(offsetof(EVMModule, CodeSize) <= std::numeric_limits::max(), @@ -79,6 +102,20 @@ class EVMModule final : public BaseModule { return *JITCodeMemPool; } + size_t getEmittedJITCodeSize() const { +#ifdef ZEN_ENABLE_JIT + return EmittedJITCodeSize.load(std::memory_order_acquire); +#else + return 0; +#endif + } + + void setEmittedJITCodeSize(size_t Size) { +#ifdef ZEN_ENABLE_JIT + EmittedJITCodeSize.store(Size, std::memory_order_relaxed); +#endif + } + void setJITCodeAndSize(void *Code, size_t Size) { JITCodeSize.store(Size, std::memory_order_relaxed); JITCode.store(Code, std::memory_order_release); @@ -134,6 +171,9 @@ class EVMModule final : public BaseModule { std::unique_ptr JITCodeMemPool; std::atomic JITCode{nullptr}; std::atomic JITCodeSize{0}; + std::atomic EmittedJITCodeSize{0}; + std::atomic DynamicEntryMergeFallbackBlocks{0}; + std::atomic DynamicEntryMergeFallbackEdges{0}; #endif // ZEN_ENABLE_JIT }; diff --git a/tests/tools/test_run_ssa_compiler_observation.py b/tests/tools/test_run_ssa_compiler_observation.py new file mode 100644 index 000000000..e1881637c --- /dev/null +++ b/tests/tools/test_run_ssa_compiler_observation.py @@ -0,0 +1,110 @@ +import importlib.util +import json +import sys +import tempfile +import unittest +from pathlib import Path + + +SCRIPT = ( + Path(__file__).parents[2] / "tools" / "run_ssa_compiler_observation.py" +) +SPEC = importlib.util.spec_from_file_location( + "run_ssa_compiler_observation", SCRIPT +) +run_ssa_compiler_observation = importlib.util.module_from_spec(SPEC) +assert SPEC.loader is not None +sys.modules[SPEC.name] = run_ssa_compiler_observation +SPEC.loader.exec_module(run_ssa_compiler_observation) + + +def observation(value: int) -> dict: + return { + "total_ns": value * 10, + "emitted_code_bytes": value * 20, + "phases_ns": { + "frontend": value * 5, + "register_allocation": value, + "observation_overhead": value * 2, + }, + "mir_after_frontend": { + "basic_blocks": value, + "instructions": value * 2, + }, + "cg_before_phi": { + "basic_blocks": value * 3, + "instructions": value * 4, + "virtual_registers": value * 5, + }, + "cg_after_ra": { + "stack_slot_loads": value * 6, + }, + } + + +class SSACompilerObservationTest(unittest.TestCase): + def test_parse_observations(self) -> None: + output = ( + "noise\n" + f"{run_ssa_compiler_observation.OBSERVATION_PREFIX}" + '{"schema_version":1,"total_ns":12}\n' + ) + self.assertEqual( + run_ssa_compiler_observation.parse_observations(output), + [{"schema_version": 1, "total_ns": 12}], + ) + + def test_load_target_fingerprint(self) -> None: + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "fixture.json" + address = "0x" + "aa" * 20 + path.write_text( + json.dumps( + { + "MainnetReplay_x": { + "transaction": {"to": address}, + "pre": {address: {"code": "0x60016000"}}, + } + } + ), + encoding="utf-8", + ) + fingerprint, size = ( + run_ssa_compiler_observation.load_target_fingerprint( + path, "MainnetReplay_x" + ) + ) + self.assertEqual(size, 4) + self.assertEqual( + fingerprint, + f"0x{run_ssa_compiler_observation.fnv1a64(bytes.fromhex('60016000')):016x}", + ) + + def test_compare_observations_excludes_observation_overhead(self) -> None: + comparison = run_ssa_compiler_observation.compare_observations( + observation(2), observation(6) + ) + self.assertEqual(comparison["compile_time"]["ratio"], 3) + self.assertEqual(comparison["dominant_s1_phase"], "frontend") + + def test_select_target_observation_requires_unique_match(self) -> None: + expected = { + "bytecode_fingerprint_fnv1a64": "0xabc", + "bytecode_size": 4, + } + self.assertIs( + run_ssa_compiler_observation.select_target_observation( + [expected], "0xabc", 4 + ), + expected, + ) + with self.assertRaises( + run_ssa_compiler_observation.ExperimentError + ): + run_ssa_compiler_observation.select_target_observation( + [expected, expected], "0xabc", 4 + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/run_ssa_compiler_observation.py b/tools/run_ssa_compiler_observation.py new file mode 100644 index 000000000..b657791c5 --- /dev/null +++ b/tools/run_ssa_compiler_observation.py @@ -0,0 +1,376 @@ +#!/usr/bin/env python3 +"""Collect S0/S1 compiler observations on a deduplicated replay fixture set.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import os +import statistics +import subprocess +import sys +import tempfile +from pathlib import Path +from typing import Any + + +OBSERVATION_PREFIX = "[DTVM_EVM_COMPILER_OBSERVATION] " + + +class ExperimentError(RuntimeError): + pass + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def atomic_json(path: Path, value: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_name(path.name + ".part") + content = ( + json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n" + ).encode("utf-8") + with temporary.open("wb") as stream: + stream.write(content) + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, path) + + +def fnv1a64(data: bytes) -> int: + result = 14695981039346656037 + for byte in data: + result ^= byte + result = (result * 1099511628211) & ((1 << 64) - 1) + return result + + +def parse_observations(output: str) -> list[dict[str, Any]]: + observations = [] + for line in output.splitlines(): + if not line.startswith(OBSERVATION_PREFIX): + continue + try: + observation = json.loads(line[len(OBSERVATION_PREFIX) :]) + except json.JSONDecodeError as exc: + raise ExperimentError("malformed compiler observation record") from exc + if not isinstance(observation, dict): + raise ExperimentError("compiler observation record is not an object") + observations.append(observation) + return observations + + +def load_target_fingerprint( + fixture_path: Path, expected_test_name: str +) -> tuple[str, int]: + with fixture_path.open("r", encoding="utf-8") as stream: + fixture = json.load(stream) + if not isinstance(fixture, dict) or expected_test_name not in fixture: + raise ExperimentError( + f"fixture {fixture_path} has no test {expected_test_name}" + ) + case = fixture[expected_test_name] + transaction = case.get("transaction", {}) + target_address = transaction.get("to") + pre = case.get("pre", {}) + target = pre.get(target_address) + if target is None and isinstance(target_address, str): + target = pre.get(target_address.lower()) + code = target.get("code") if isinstance(target, dict) else None + if not isinstance(code, str) or not code.startswith("0x"): + raise ExperimentError(f"fixture {fixture_path} has no target bytecode") + try: + bytecode = bytes.fromhex(code[2:]) + except ValueError as exc: + raise ExperimentError(f"fixture {fixture_path} has invalid bytecode") from exc + return f"0x{fnv1a64(bytecode):016x}", len(bytecode) + + +def select_target_observation( + observations: list[dict[str, Any]], + fingerprint: str, + bytecode_size: int, +) -> dict[str, Any]: + matches = [ + item + for item in observations + if item.get("bytecode_fingerprint_fnv1a64") == fingerprint + and item.get("bytecode_size") == bytecode_size + ] + if len(matches) != 1: + raise ExperimentError( + f"expected one target observation for {fingerprint}/{bytecode_size}, " + f"found {len(matches)}" + ) + return matches[0] + + +def ratio(numerator: float, denominator: float) -> float | None: + return numerator / denominator if denominator else None + + +def compare_observations( + s0: dict[str, Any], s1: dict[str, Any] +) -> dict[str, Any]: + s0_mir = s0["mir_after_frontend"] + s1_mir = s1["mir_after_frontend"] + s0_cg = s0["cg_before_phi"] + s1_cg = s1["cg_before_phi"] + s0_ra = s0["cg_after_ra"] + s1_ra = s1["cg_after_ra"] + metrics = { + "compile_time": (s0["total_ns"], s1["total_ns"]), + "frontend_time": (s0["phases_ns"]["frontend"], s1["phases_ns"]["frontend"]), + "mir_basic_blocks": (s0_mir["basic_blocks"], s1_mir["basic_blocks"]), + "mir_instructions": (s0_mir["instructions"], s1_mir["instructions"]), + "cg_basic_blocks": (s0_cg["basic_blocks"], s1_cg["basic_blocks"]), + "cg_instructions": (s0_cg["instructions"], s1_cg["instructions"]), + "virtual_registers": ( + s0_cg["virtual_registers"], + s1_cg["virtual_registers"], + ), + "ra_stack_slot_loads": ( + s0_ra["stack_slot_loads"], + s1_ra["stack_slot_loads"], + ), + "emitted_code_bytes": ( + s0["emitted_code_bytes"], + s1["emitted_code_bytes"], + ), + } + result = {} + for name, (s0_value, s1_value) in metrics.items(): + result[name] = { + "s0": s0_value, + "s1": s1_value, + "ratio": ratio(s1_value, s0_value), + } + phases = { + name: duration + for name, duration in s1["phases_ns"].items() + if name != "observation_overhead" + } + result["dominant_s1_phase"] = max(phases, key=phases.get) + return result + + +def summarize(fixtures: list[dict[str, Any]]) -> dict[str, Any]: + metric_names = ( + "compile_time", + "frontend_time", + "mir_basic_blocks", + "mir_instructions", + "cg_basic_blocks", + "cg_instructions", + "virtual_registers", + "ra_stack_slot_loads", + "emitted_code_bytes", + ) + metrics: dict[str, Any] = {} + for name in metric_names: + values = [ + item["comparison"][name]["ratio"] + for item in fixtures + if item["comparison"][name]["ratio"] is not None + ] + metrics[name] = { + "count": len(values), + "median_ratio": statistics.median(values) if values else None, + "geometric_mean_ratio": ( + math.exp(statistics.fmean(math.log(value) for value in values)) + if values and all(value > 0 for value in values) + else None + ), + } + dominant_counts: dict[str, int] = {} + for item in fixtures: + phase = item["comparison"]["dominant_s1_phase"] + dominant_counts[phase] = dominant_counts.get(phase, 0) + 1 + return { + "completed_fixtures": len(fixtures), + "metrics": metrics, + "dominant_s1_phase_counts": dominant_counts, + } + + +def run_variant( + executable: Path, + fixture_path: Path, + test_name: str, + revision: str, + cpu: int | None, + expected_ssa: bool, +) -> dict[str, Any]: + fingerprint, bytecode_size = load_target_fingerprint(fixture_path, test_name) + with tempfile.TemporaryDirectory(prefix="dtvm-compiler-observe-") as directory: + isolated_fixture = Path(directory) / fixture_path.name + isolated_fixture.symlink_to(fixture_path.resolve()) + test_filter = f"*{test_name}_{revision}_0" + command = [ + str(executable.resolve()), + f"--gtest_filter={test_filter}", + ] + if cpu is not None: + command = ["taskset", "-c", str(cpu), *command] + environment = os.environ.copy() + environment.update( + { + "DTVM_EVM_COMPILER_OBSERVE": "1", + "DTVM_TEST_DIR": directory, + "DTVM_TEST_REVISION": revision, + "DTVM_TEST_MODE": "multipass", + } + ) + completed = subprocess.run( + command, + check=False, + env=environment, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + if completed.returncode != 0 or "[ PASSED ] 1 test." not in completed.stdout: + raise ExperimentError( + f"{executable} failed for {test_name} (exit {completed.returncode})" + ) + observation = select_target_observation( + parse_observations(completed.stdout), fingerprint, bytecode_size + ) + if observation.get("compile_succeeded") is not True: + raise ExperimentError(f"compiler observation failed for {test_name}") + if observation.get("stack_ssa_enabled") is not expected_ssa: + raise ExperimentError(f"SSA mode mismatch for {test_name}") + return observation + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--s0", type=Path, required=True) + parser.add_argument("--s1", type=Path, required=True) + parser.add_argument("--performance-set", type=Path, required=True) + parser.add_argument("--fixture-dir", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--revision", default="Osaka") + parser.add_argument("--cpu", type=int) + parser.add_argument("--resume", action="store_true") + return parser.parse_args() + + +def build_metadata(arguments: argparse.Namespace) -> dict[str, Any]: + return { + "performance_set": str(arguments.performance_set.resolve()), + "performance_set_sha256": sha256_file(arguments.performance_set), + "fixture_dir": str(arguments.fixture_dir.resolve()), + "revision": arguments.revision, + "cpu": arguments.cpu, + "executables": { + "S0": { + "path": str(arguments.s0.resolve()), + "sha256": sha256_file(arguments.s0), + }, + "S1": { + "path": str(arguments.s1.resolve()), + "sha256": sha256_file(arguments.s1), + }, + }, + } + + +def main() -> int: + arguments = parse_arguments() + for path in (arguments.s0, arguments.s1, arguments.performance_set): + if not path.is_file(): + raise ExperimentError(f"file not found: {path}") + if not arguments.fixture_dir.is_dir(): + raise ExperimentError(f"fixture directory not found: {arguments.fixture_dir}") + + with arguments.performance_set.open("r", encoding="utf-8") as stream: + manifest = json.load(stream) + records = manifest.get("fixtures") + if not isinstance(records, list): + raise ExperimentError("performance set has no fixtures array") + + metadata = build_metadata(arguments) + results: list[dict[str, Any]] = [] + if arguments.resume and arguments.output.exists(): + with arguments.output.open("r", encoding="utf-8") as stream: + previous = json.load(stream) + if previous.get("metadata") != metadata: + raise ExperimentError("cannot resume: experiment metadata changed") + results = previous.get("fixtures", []) + + completed_positions = {item["position"] for item in results} + for record_index, record in enumerate(records, start=1): + position = int(record["position"]) + if position in completed_positions: + continue + fixture_path = arguments.fixture_dir / record["fixture"] + test_name = record["test_name"] + order = ("S0", "S1") if position % 2 else ("S1", "S0") + observations: dict[str, dict[str, Any]] = {} + print( + f"[{record_index}/{len(records)}] position={position} " + f"{record['module_code_hash']} " + f"order={','.join(order)}", + flush=True, + ) + for variant in order: + observations[variant] = run_variant( + arguments.s0 if variant == "S0" else arguments.s1, + fixture_path, + test_name, + arguments.revision, + arguments.cpu, + expected_ssa=variant == "S1", + ) + result = { + "position": position, + "fixture": record["fixture"], + "fixture_sha256": sha256_file(fixture_path), + "test_name": test_name, + "transaction_hash": record.get("transaction_hash"), + "module_code_hash": record.get("module_code_hash"), + "frequency": int(record.get("frequency", 1)), + "observations": observations, + "comparison": compare_observations( + observations["S0"], observations["S1"] + ), + } + results.append(result) + results.sort(key=lambda item: item["position"]) + atomic_json( + arguments.output, + { + "schema_version": 1, + "status": "in_progress", + "metadata": metadata, + "fixtures": results, + "summary": summarize(results), + }, + ) + + final = { + "schema_version": 1, + "status": "completed", + "metadata": metadata, + "fixtures": results, + "summary": summarize(results), + } + atomic_json(arguments.output, final) + print(json.dumps(final["summary"], indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except (ExperimentError, OSError, ValueError) as exc: + print(f"error: {exc}", file=sys.stderr) + sys.exit(2) From fe4cc11e2c36f180ea17296626d4723fc730f2a9 Mon Sep 17 00:00:00 2001 From: ZR74 <2401889661@qq.com> Date: Wed, 29 Jul 2026 03:07:46 +0000 Subject: [PATCH 2/2] fix(compiler): decouple EVM metrics from frontend headers Keep stack-lift statistics in the lightweight metrics header and let the EVM frontend consume that definition. Avoid pulling EVMC-only headers into generic multipass compiler builds that do not enable EVM support. --- src/compiler/evm_compiler_metrics.h | 16 ++++++++++++++-- .../evm_frontend/evm_lifted_stack_lifter.h | 15 +-------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/compiler/evm_compiler_metrics.h b/src/compiler/evm_compiler_metrics.h index 47f610912..742e0c570 100644 --- a/src/compiler/evm_compiler_metrics.h +++ b/src/compiler/evm_compiler_metrics.h @@ -4,8 +4,6 @@ #ifndef ZEN_COMPILER_EVM_COMPILER_METRICS_H #define ZEN_COMPILER_EVM_COMPILER_METRICS_H -#include "compiler/evm_frontend/evm_lifted_stack_lifter.h" - #include #include #include @@ -13,6 +11,20 @@ namespace COMPILER { +struct EVMLiftedStackStatistics { + uint64_t AnalyzedBlocks = 0; + uint64_t LiftedBlocks = 0; + uint64_t NonLiftedBlocks = 0; + uint64_t MergeBlocks = 0; + uint64_t MergeSlots = 0; + uint64_t MergePredecessorEdges = 0; + uint64_t RecordedIncomingStates = 0; + uint64_t FoldedMergeSlots = 0; + uint64_t MaterializationRequests = 0; + uint64_t ProtectedIncomingValues = 0; + uint64_t MaterializedU256Merges = 0; +}; + enum class EVMCompilerPhase : uint8_t { ContextSetup = 0, Frontend, diff --git a/src/compiler/evm_frontend/evm_lifted_stack_lifter.h b/src/compiler/evm_frontend/evm_lifted_stack_lifter.h index b7ab4697b..edbbb87b6 100644 --- a/src/compiler/evm_frontend/evm_lifted_stack_lifter.h +++ b/src/compiler/evm_frontend/evm_lifted_stack_lifter.h @@ -4,6 +4,7 @@ #ifndef EVM_FRONTEND_EVM_LIFTED_STACK_LIFTER_H #define EVM_FRONTEND_EVM_LIFTED_STACK_LIFTER_H +#include "compiler/evm_compiler_metrics.h" #include "compiler/evm_frontend/evm_analyzer.h" #include @@ -15,20 +16,6 @@ namespace COMPILER { -struct EVMLiftedStackStatistics { - uint64_t AnalyzedBlocks = 0; - uint64_t LiftedBlocks = 0; - uint64_t NonLiftedBlocks = 0; - uint64_t MergeBlocks = 0; - uint64_t MergeSlots = 0; - uint64_t MergePredecessorEdges = 0; - uint64_t RecordedIncomingStates = 0; - uint64_t FoldedMergeSlots = 0; - uint64_t MaterializationRequests = 0; - uint64_t ProtectedIncomingValues = 0; - uint64_t MaterializedU256Merges = 0; -}; - template class EVMLiftedStackLifter { public: using Operand = typename IRBuilder::Operand;