Skip to main content

javm_exec/
gas_const.rs

1//! Shared gas / resource cost constants and the category-#2 memory
2//! footprint multiplier.
3//!
4//! Single source of truth linked by **both** execution engines (the
5//! interpreter and the x86 recompiler) so their charges cannot drift.
6//! See `~/docs/spec-staging/gas-cost.md`.
7//!
8//! NOTE: the category-#3 constants ([`PAGE_IN_COST`], [`COW_COST`],
9//! [`COMPILE_COST_PER_PAGE`], [`CALL_FRAME_COST`], [`HOST_CALL_FLOOR`],
10//! [`MGMT_OP_COST`]) are **placeholders**. TODO(gas-calibration): calibrate
11//! against the kernel page-fault / call-setup handler; these values are
12//! subject to change. [`MAX_PAGES_PER_ACCESS`] is an invariant, not a tunable.
13
14/// Base load/store latency at the smallest footprint tier (×1).
15/// Same value as [`crate::gas_cost::DEFAULT_MEM_CYCLES`]; the #2 tier
16/// scales it (see [`mem_cycles_for`]).
17pub const MEM_CYCLES_BASE: u8 = 25;
18
19/// #3 read-only page-in, charged **per declared 2 MiB unit at the CALL**
20/// (no longer per touched unit at a fault): the cost of admitting one
21/// read-only unit — code or a pinned DataCap intersected with a 2 MiB
22/// cluster — into the working set. Folded into [`call_frame_cost`]; a
23/// read at a fault charges nothing.
24/// TODO(gas-calibration): placeholder, subject to change.
25pub const PAGE_IN_COST: u64 = 64;
26
27/// #3 copy-on-write (first write of a page): allocate + copy + map RW. The
28/// **only** fault-driven #3 charge (read-only page-in moved to the CALL).
29/// TODO(gas-calibration): placeholder (~4 KiB copy), subject to change.
30pub const COW_COST: u64 = 256;
31
32/// #3 JIT-compile cost per 4 KiB of callee code, charged at the CALL that
33/// first materializes a callee Image (`O(code)`, bounded by `MAX_CODE_SIZE`).
34/// Folded into [`call_frame_cost`]. Charged in full on every CALL — the
35/// compiled image is memoized for *work*, never for gas — so a re-CALL into
36/// a warm Image pays the same and gas stays independent of the node-local
37/// compile cache. TODO(gas-calibration): placeholder, subject to change.
38pub const COMPILE_COST_PER_PAGE: u64 = 512;
39
40/// Max consensus 4 KiB pages a single scalar access can span.
41///
42/// **Invariant, not a tunable.** The widest PVM2 memory access is an
43/// 8-byte scalar (`ld`/`sd`) and `8 < 4096`, so a (possibly misaligned,
44/// via Zicclsm) access touches at most `ceil(8 / 4096) + 1 = 2`
45/// consensus pages. Adding a wider access (e.g. a vector load/store)
46/// MUST revisit this constant, or the worst-case-#3 reserve undercounts.
47pub const MAX_PAGES_PER_ACCESS: u64 = 2;
48
49/// #3 call-frame **base**: the fixed per-CALL frame-setup cost (callee
50/// address-space page table + dispatch table + frame push), independent of
51/// code size. The code-size (compile) and read-only-page-in components are
52/// added on top by [`call_frame_cost`]. Charged at an in-kernel CALL
53/// (`ecall.jar` OP_HOST_CALL).
54/// TODO(gas-calibration): placeholder, subject to change.
55pub const CALL_FRAME_COST: u64 = 1024;
56
57/// Dynamic floor charged for a bubbled host call (`ecalli imm`).
58/// TODO(gas-calibration): placeholder, subject to change.
59pub const HOST_CALL_FLOOR: u64 = 100;
60
61/// Dynamic cost of an in-kernel MGMT op (MOVE / COPY / DROP / ...):
62/// O(1) content-addressed handle work.
63/// TODO(gas-calibration): placeholder, subject to change.
64pub const MGMT_OP_COST: u64 = 100;
65
66/// Dynamic **floor** charged at every `ecall` block (the per-op base),
67/// keyed only on the instruction type — which both engines know at the
68/// ecall, so they charge identically: `ecalli` (host call) →
69/// [`HOST_CALL_FLOOR`]; `ecall.jar` (MGMT / CALL) → [`MGMT_OP_COST`].
70///
71/// An in-kernel CALL (`ecall.jar` OP_HOST_CALL) pays this floor **plus**
72/// [`call_frame_cost`] (compile + eager read-only page-in + frame setup),
73/// charged by the kernel CALL dispatch once it has resolved the callee
74/// Image. TODO(gas-calibration): placeholder. Both engines must keep using
75/// this one function for the floor.
76#[inline]
77pub fn ecall_dynamic_cost(is_ecalli: bool) -> u64 {
78    if is_ecalli {
79        HOST_CALL_FLOOR
80    } else {
81        MGMT_OP_COST
82    }
83}
84
85/// Category-#3 cost of materializing a callee sub-invocation at an
86/// in-kernel CALL, charged to the **caller's** meter **in addition to** the
87/// [`ecall_dynamic_cost`] floor, and computed **statically from the callee
88/// Image** so both engines agree:
89///
90/// - **JIT compile** — `O(code)`: `ceil(code_len / PAGE_SIZE)` pages ×
91///   [`COMPILE_COST_PER_PAGE`].
92/// - **Eager read-only page-in** — one [`PAGE_IN_COST`] per declared 2 MiB
93///   read-only `unit` (the callee's code region plus its pinned mappings) —
94///   the cost that used to be charged lazily per touched unit at a fault.
95/// - **Frame-setup base** — [`CALL_FRAME_COST`].
96///
97/// Always charged in full: the compiled image and its page table are
98/// memoized as a node-local **performance** optimization, never a gas
99/// discount — so a re-CALL into a warm Image pays identically and gas stays
100/// independent of the cache (the architectural "eager compile + eager
101/// RO-map at CALL" model; the implementation may stay lazy/demand-paged
102/// without changing this charge). TODO(gas-calibration): placeholder
103/// coefficients.
104#[inline]
105pub fn call_frame_cost(code_len: u32, ro_units: u32) -> u64 {
106    let code_pages = (code_len as u64).div_ceil(crate::mem::PAGE_SIZE as u64);
107    CALL_FRAME_COST
108        .saturating_add(code_pages.saturating_mul(COMPILE_COST_PER_PAGE))
109        .saturating_add((ro_units as u64).saturating_mul(PAGE_IN_COST))
110}
111
112/// Category-#2 memory-access-latency footprint multiplier (×1..4),
113/// chosen from the Instance's total accessible 4 KiB page count. Tiers
114/// from `~/docs/memory-gas.md` (mem_seq / mem_rand benchmarks). The
115/// multiplier is static (resolved once at compile time) and folded into
116/// each block's #1 cost, so it has zero runtime metering cost.
117#[inline]
118pub fn compute_scale(accessible_pages: u32) -> u8 {
119    match accessible_pages {
120        0..=2048 => 1,     // ≤ 8 MiB — fits L2/L3
121        2049..=8192 => 2,  // ≤ 32 MiB — L3 edge
122        8193..=65536 => 3, // ≤ 256 MiB — DRAM
123        _ => 4,            // > 256 MiB — DRAM + headroom
124    }
125}
126
127/// Effective per-load/store base latency after #2 footprint scaling:
128/// `MEM_CYCLES_BASE × compute_scale(accessible_pages)` (saturating).
129/// This is the `mem_cycles` value threaded into predecode / the gas
130/// simulator in place of the flat [`MEM_CYCLES_BASE`].
131#[inline]
132pub fn mem_cycles_for(accessible_pages: u32) -> u8 {
133    MEM_CYCLES_BASE.saturating_mul(compute_scale(accessible_pages))
134}
135
136/// Accessible 4 KiB page count for an Instance whose data extent is
137/// `[data_base, mem_size)` — the #2 footprint. `mem_size` is the
138/// high-water-mark over the Image's `memory_mappings`
139/// (`javm_cap::image::ImageCap::data_overlays` — the single source of
140/// truth both engines derive from), so the two engines compute an
141/// identical page count. `data_base` is `javm_cap::layout::DATA_BASE`
142/// (passed in — this crate has no `javm-cap` dependency).
143#[inline]
144pub fn accessible_pages(mem_size: u32, data_base: u32) -> u32 {
145    mem_size.saturating_sub(data_base) / crate::mem::PAGE_SIZE
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn scale_tier_boundaries() {
154        // Inclusive upper bounds per tier; comparison is by page count.
155        assert_eq!(compute_scale(0), 1);
156        assert_eq!(compute_scale(2048), 1);
157        assert_eq!(compute_scale(2049), 2);
158        assert_eq!(compute_scale(8192), 2);
159        assert_eq!(compute_scale(8193), 3);
160        assert_eq!(compute_scale(65536), 3);
161        assert_eq!(compute_scale(65537), 4);
162        assert_eq!(compute_scale(u32::MAX), 4);
163    }
164
165    #[test]
166    fn mem_cycles_scales_with_footprint() {
167        assert_eq!(mem_cycles_for(0), 25);
168        assert_eq!(mem_cycles_for(2049), 50);
169        assert_eq!(mem_cycles_for(8193), 75);
170        assert_eq!(mem_cycles_for(65537), 100);
171    }
172
173    #[test]
174    fn base_matches_gas_cost_default() {
175        assert_eq!(MEM_CYCLES_BASE, crate::gas_cost::DEFAULT_MEM_CYCLES);
176    }
177}