Skip to main content

javm_bench/
lib.rs

1//! Shared runners for `benches/pvm_bench.rs` and `benches/stark_bench.rs`.
2//!
3//! The bench measures the full per-invocation lifecycle:
4//!   * `Nub::put_cap_with_hash` for each cap the invocation requires
5//!     (Data blobs the Image references, the Image itself, the empty
6//!     root cnode, the Instance). Each put is a single
7//!     `BTreeMap::get + refcount.fetch_add(1)` after warm-up — i.e. a
8//!     few tens of nanoseconds per cap.
9//!   * `Nub::invoke_cached(instance_hash, endpoint, args, gas)`.
10//!
11//! - `run_interpreter` — `Nub::new_local()` drives the byte-PVM
12//!   interpreter (`javm-exec`) in-process.
13//! - `run_recompiler` — a long-lived `Nub::new_hyperlight()` sandbox
14//!   (cached in a `OnceLock`) drives the in-kernel JIT path through
15//!   the same `invoke_cached` API.
16//!
17//! `BuiltCaps` holds the pre-built `Cap` graph + its precomputed
18//! hashes. Construction happens once per workload at bench warm-up via
19//! [`BuiltCaps::for_image`]; the iter loop reuses the resulting handles.
20//!
21//! Linux x86-64 only — `nub` pulls the Hyperlight host stack
22//! unconditionally.
23
24#![cfg_attr(not(all(target_os = "linux", target_arch = "x86_64")), allow(unused))]
25#![cfg(all(target_os = "linux", target_arch = "x86_64"))]
26
27use javm_cap::NUM_REGS;
28use javm_cap::image::{Image, PinnedCap};
29use javm_cap::slot::SlotIdx;
30use javm_cap::{Cap, CapHash};
31use nub::{InvocationResult, Nub};
32use std::sync::{Mutex, OnceLock};
33
34/// HostCall(0) — the trampoline halt all bench programs end on
35/// (`ecalli 0`). Both backends surface it as `exit_reason=4,
36/// exit_arg=0`.
37const EXIT_HOSTCALL: u32 = 4;
38
39/// Default initial-gas budget for the bench.
40const INITIAL_GAS: u64 = 100_000_000_000;
41
42/// Pre-built `Cap` graph for one (image, endpoint) bench cell.
43///
44/// Built once at warm-up via [`Self::for_image`]; the iter loop puts each
45/// cap with its precomputed hash and invokes. The first iter pays the
46/// full deep-clone cost (caps move into the Nub's cache allocator);
47/// subsequent iters hit the idempotent fast path (refcount bump only).
48pub struct BuiltCaps {
49    /// Cap::Data blobs for each pinned-slot Data + each initial-slot Data,
50    /// paired with their content hashes.
51    pub data_caps: Vec<(CapHash, Cap)>,
52    /// Cap::Image referencing the data_caps above by hash.
53    pub image_cap: Cap,
54    pub image_hash: CapHash,
55    /// Empty Cap::CNode (V1 has no per-instance slot bindings).
56    pub cnode_cap: Cap,
57    pub cnode_hash: CapHash,
58    /// Cap::Instance with the bench's flat (ro, rw) overlay layout.
59    pub instance_cap: Cap,
60    pub instance_hash: CapHash,
61    pub endpoint_idx: u8,
62}
63
64impl BuiltCaps {
65    /// Build the full `Cap` graph for `image[endpoint_idx]`. All
66    /// hashes are precomputed once here.
67    pub fn for_image(image: &Image, endpoint_idx: u8) -> Self {
68        let endpoint = image
69            .endpoints
70            .get(&endpoint_idx)
71            .unwrap_or_else(|| panic!("endpoint {endpoint_idx} not declared"));
72
73        // 1. Build a Cap::Data per non-empty pinned/initial slot. Track
74        //    each slot's resolved CapHash so the Image can reference them.
75        let mut data_caps: Vec<(CapHash, Cap)> = Vec::new();
76        let mut pinned_hashes: Vec<(SlotIdx, CapHash)> = Vec::new();
77        let mut initial_hashes: Vec<(SlotIdx, CapHash)> = Vec::new();
78
79        for (slot, pinned) in &image.pinned_slots {
80            let (h, cap) = match pinned {
81                PinnedCap::Data { content, size } => {
82                    let cap = Cap::data_inline_with_size(content, *size);
83                    let h = ssz::hash_tree_root(&cap);
84                    (h, Some(cap))
85                }
86                PinnedCap::Image { content_hash } => {
87                    // Sub-Image hash assumed already-published; carry it
88                    // through to the image_with_slots builder.
89                    (*content_hash, None)
90                }
91            };
92            pinned_hashes.push((*slot, h));
93            if let Some(c) = cap {
94                data_caps.push((h, c));
95            }
96        }
97        for (slot, init) in &image.initial_slots {
98            let cap = Cap::data_inline_with_size(&init.content, init.size);
99            let h = ssz::hash_tree_root(&cap);
100            initial_hashes.push((*slot, h));
101            data_caps.push((h, cap));
102        }
103
104        // 2. Build the Cap::Image referencing the data caps by hash.
105        let image_cap = Cap::image_with_slots(image, &pinned_hashes, &initial_hashes)
106            .expect("image_with_slots");
107        let image_hash = ssz::hash_tree_root(&image_cap);
108
109        // 3. Empty root CNode (V1: no per-instance slot bindings).
110        let cnode_cap = Cap::empty_cnode(0).expect("empty_cnode");
111        let cnode_hash = ssz::hash_tree_root(&cnode_cap);
112
113        // 4. Build the Instance with the bench's flat overlay layout.
114        let (mem_size, overlays) = build_overlays(image);
115        let overlay_slices: Vec<(u32, &[u8])> = overlays
116            .iter()
117            .map(|(start, bytes)| (*start, bytes.as_slice()))
118            .collect();
119
120        let mut regs = [0u64; NUM_REGS];
121        for (&i, &v) in &endpoint.initial_regs {
122            if let Some(slot) = regs.get_mut(i as usize) {
123                *slot = v;
124            }
125        }
126
127        let instance_cap = Cap::instance_with_overlays(
128            [0u8; 32],
129            image_hash,
130            cnode_hash,
131            &overlay_slices,
132            mem_size,
133            regs,
134            0,
135            0,
136        );
137        let instance_hash = ssz::hash_tree_root(&instance_cap);
138
139        BuiltCaps {
140            data_caps,
141            image_cap,
142            image_hash,
143            cnode_cap,
144            cnode_hash,
145            instance_cap,
146            instance_hash,
147            endpoint_idx,
148        }
149    }
150
151    /// Put every cap into `nub`'s cache via `put_cap_with_hash`.
152    /// Idempotent re-puts after the first call are refcount bumps only.
153    fn put_into(&self, nub: &mut Nub) {
154        for (h, cap) in &self.data_caps {
155            nub.put_cap_with_hash(*h, cap)
156                .unwrap_or_else(|e| panic!("put_cap_with_hash data: {e}"));
157        }
158        nub.put_cap_with_hash(self.image_hash, &self.image_cap)
159            .unwrap_or_else(|e| panic!("put_cap_with_hash image: {e}"));
160        nub.put_cap_with_hash(self.cnode_hash, &self.cnode_cap)
161            .unwrap_or_else(|e| panic!("put_cap_with_hash cnode: {e}"));
162        nub.put_cap_with_hash(self.instance_hash, &self.instance_cap)
163            .unwrap_or_else(|e| panic!("put_cap_with_hash instance: {e}"));
164    }
165}
166
167/// Drive `built[endpoint_idx]` through the byte-PVM interpreter via a
168/// fresh `Nub::new_local()` (the Local backend has no per-invocation
169/// state, so a fresh Nub each call is fine — and matches the chain's
170/// per-event allocation model).
171pub fn run_interpreter(built: &BuiltCaps) -> (u64, u64) {
172    let mut nub = Nub::new_local();
173    built.put_into(&mut nub);
174    let result = nub
175        .invoke_cached(built.instance_hash, built.endpoint_idx, [0; 4], INITIAL_GAS)
176        .unwrap_or_else(|e| panic!("interpreter invoke_cached: {e}"));
177    finish(&result)
178}
179
180/// Drive `built[endpoint_idx]` through the in-kernel JIT via the long-
181/// lived Hyperlight `Nub`.
182pub fn run_recompiler(built: &BuiltCaps) -> (u64, u64) {
183    let mut nub = nub_hyperlight().lock().expect("nub mutex");
184    built.put_into(&mut nub);
185    let result = nub
186        .invoke_cached(built.instance_hash, built.endpoint_idx, [0; 4], INITIAL_GAS)
187        .unwrap_or_else(|e| panic!("recompiler invoke_cached: {e}"));
188    finish(&result)
189}
190
191fn finish(result: &InvocationResult) -> (u64, u64) {
192    assert_eq!(
193        result.exit_reason, EXIT_HOSTCALL,
194        "unexpected exit_reason {} (exit_arg={})",
195        result.exit_reason, result.exit_arg,
196    );
197    assert_eq!(
198        result.exit_arg, 0,
199        "expected HostCall(0) trampoline halt, got HostCall({})",
200        result.exit_arg,
201    );
202    let gas_used = INITIAL_GAS.saturating_sub(result.gas_remaining);
203    (result.return_value, gas_used)
204}
205
206/// Long-lived Hyperlight sandbox shared across bench iterations.
207fn nub_hyperlight() -> &'static Mutex<Nub> {
208    static NUB: OnceLock<Mutex<Nub>> = OnceLock::new();
209    NUB.get_or_init(|| Mutex::new(Nub::new_hyperlight().expect("Hyperlight sandbox")))
210}
211
212/// Walk the Image's memory mappings + slot contents and produce
213/// `(mem_size, overlays)` for the InstanceCap. Each non-empty content
214/// becomes one `(start, bytes)` overlay; stack/heap are empty inside
215/// `mem_size` as zero-init RW pages.
216fn build_overlays(image: &Image) -> (u32, Vec<(u32, Vec<u8>)>) {
217    let mut mem_size: u32 = 0;
218    let mut overlays: Vec<(u32, Vec<u8>)> = Vec::new();
219
220    for mapping in &image.memory_mappings {
221        let end = (mapping.start + mapping.size) as u32;
222        if end > mem_size {
223            mem_size = end;
224        }
225
226        let target = mapping.source.target();
227        if let Some(PinnedCap::Data { content, .. }) = image.pinned_slots.get(&target) {
228            if !content.is_empty() {
229                overlays.push((mapping.start as u32, content.clone()));
230            }
231        } else if let Some(init) = image.initial_slots.get(&target)
232            && !init.content.is_empty()
233        {
234            overlays.push((mapping.start as u32, init.content.clone()));
235        }
236    }
237
238    (mem_size, overlays)
239}