Skip to main content

hara_native/jit/
runtime.rs

1use std::collections::{HashMap, HashSet};
2
3#[cfg(all(feature = "native-jit", not(target_arch = "wasm32")))]
4use super::{native::NativeTrace, NativeBackend};
5#[cfg(any(not(feature = "native-jit"), target_arch = "wasm32"))]
6use super::{CheckedBackend, Trace};
7use super::{
8    ExitReason, ExitSnapshot, Hotness, JitConfig, LoopKey, TraceBackend, TraceOutcome,
9    TraceRecorder, TraceValue,
10};
11use crate::vm::{Instruction, Program};
12
13/// Per-program tracing-JIT counters. They make it possible to distinguish a
14/// cold loop, an unsupported trace, and a trace that is compiled but exits.
15#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
16pub struct JitTelemetry {
17    pub backedges: u64,
18    pub compile_attempts: u64,
19    pub compiled: u64,
20    pub rejected: u64,
21    pub entries: u64,
22    pub completed_iterations: u64,
23    pub side_exits: u64,
24    pub recording_starts: u64,
25    pub recording_completed: u64,
26    pub recording_aborts: u64,
27    pub trace_paths: u64,
28    pub branch_exits: u64,
29    pub type_exits: u64,
30    pub error_exits: u64,
31    pub disabled_loops: u64,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Hash)]
35struct TracePathKey {
36    loop_key: LoopKey,
37    path: Vec<u32>,
38}
39
40struct CachedTrace<T> {
41    path: Vec<u32>,
42    compiled: T,
43}
44
45#[derive(Default)]
46struct LoopProfile {
47    iterations: u64,
48    branch_exits: u32,
49}
50
51pub(crate) struct JitRuntime {
52    hotness: Hotness,
53    recorder: TraceRecorder,
54    #[cfg(any(not(feature = "native-jit"), target_arch = "wasm32"))]
55    backend: CheckedBackend,
56    #[cfg(any(not(feature = "native-jit"), target_arch = "wasm32"))]
57    traces: HashMap<LoopKey, Vec<CachedTrace<Trace>>>,
58    #[cfg(all(feature = "native-jit", not(target_arch = "wasm32")))]
59    backend: NativeBackend,
60    #[cfg(all(feature = "native-jit", not(target_arch = "wasm32")))]
61    traces: HashMap<LoopKey, Vec<CachedTrace<NativeTrace>>>,
62    candidates: HashMap<TracePathKey, u32>,
63    rejected: HashSet<TracePathKey>,
64    disabled: HashSet<LoopKey>,
65    profiles: HashMap<LoopKey, LoopProfile>,
66    config: JitConfig,
67    batch_iterations: u32,
68    telemetry: JitTelemetry,
69}
70
71impl Default for JitRuntime {
72    fn default() -> Self {
73        Self::new(JitConfig::default())
74    }
75}
76
77impl JitRuntime {
78    pub(crate) fn new(config: JitConfig) -> Self {
79        Self {
80            hotness: Hotness::new(config),
81            recorder: TraceRecorder::new(config.max_trace_operations),
82            backend: Default::default(),
83            traces: HashMap::new(),
84            candidates: HashMap::new(),
85            rejected: HashSet::new(),
86            disabled: HashSet::new(),
87            profiles: HashMap::new(),
88            config,
89            // Keep ordinary benchmark/application loops inside one native
90            // entry. Guards still side-exit at the exact guest iteration, so
91            // this changes boundary frequency rather than semantics.
92            batch_iterations: 16_384,
93            telemetry: JitTelemetry::default(),
94        }
95    }
96
97    pub(crate) fn backedge(
98        &mut self,
99        program: &Program,
100        function: u16,
101        _from: u32,
102        header: u32,
103        path: &[u32],
104        recording_locals: &[TraceValue],
105        locals: &mut [TraceValue],
106    ) -> Option<ExitSnapshot> {
107        let key = LoopKey { function, header };
108        let path_key = TracePathKey {
109            loop_key: key,
110            path: path.to_vec(),
111        };
112        self.telemetry.backedges += 1;
113        if self.disabled.contains(&key) {
114            return None;
115        }
116        if self.rejected.contains(&path_key) {
117            return None;
118        }
119        let existing = self
120            .traces
121            .get(&key)
122            .and_then(|traces| traces.iter().position(|trace| trace.path == path));
123        let trace_count = self.traces.get(&key).map_or(0, Vec::len);
124        let should_compile = if existing.is_some() || trace_count >= self.config.max_traces_per_loop
125        {
126            false
127        } else if trace_count == 0 {
128            let hot = self.hotness.backedge(key);
129            hot || (self.hotness.count(key) == 1 && program.function_has_i64_parameters(function))
130        } else {
131            let count = self.candidates.entry(path_key.clone()).or_default();
132            *count = count.saturating_add(1);
133            *count == self.config.side_trace_threshold
134        };
135        if should_compile {
136            self.telemetry.compile_attempts += 1;
137            self.telemetry.recording_starts += 1;
138            match self
139                .recorder
140                .record_path(program, function, header, path, recording_locals)
141            {
142                Ok(trace) => match self.backend.compile(&trace) {
143                    Ok(compiled) => {
144                        self.traces.entry(key).or_default().push(CachedTrace {
145                            path: path.to_vec(),
146                            compiled,
147                        });
148                        self.candidates.remove(&path_key);
149                        self.telemetry.compiled += 1;
150                        self.telemetry.recording_completed += 1;
151                        self.telemetry.trace_paths += 1;
152                    }
153                    Err(_) => {
154                        self.rejected.insert(path_key);
155                        self.telemetry.rejected += 1;
156                        self.telemetry.recording_aborts += 1;
157                        // A primary path that the backend cannot compile is a
158                        // structural dead end for this loop.  Disable further
159                        // tracing so the interpreter does not keep collecting
160                        // the same path on every backedge.  Side-path failures
161                        // remain path-local when a usable trace already exists.
162                        if trace_count == 0 {
163                            self.disabled.insert(key);
164                            self.telemetry.disabled_loops += 1;
165                        }
166                        return None;
167                    }
168                },
169                Err(_) => {
170                    self.rejected.insert(path_key);
171                    self.telemetry.rejected += 1;
172                    self.telemetry.recording_aborts += 1;
173                    if trace_count == 0 {
174                        self.disabled.insert(key);
175                        self.telemetry.disabled_loops += 1;
176                    }
177                    return None;
178                }
179            }
180        }
181        let Some(traces) = self.traces.get_mut(&key) else {
182            return None;
183        };
184        let entry = locals.to_vec();
185        let preferred = traces.iter().position(|trace| trace.path == path);
186        let order = preferred
187            .into_iter()
188            .chain((0..traces.len()).filter(|index| Some(*index) != preferred))
189            .collect::<Vec<_>>();
190        for index in order {
191            locals.clone_from_slice(&entry);
192            self.telemetry.entries += 1;
193            match self
194                .backend
195                .enter(&mut traces[index].compiled, locals, self.batch_iterations)
196            {
197                TraceOutcome::Completed { iterations } => {
198                    self.telemetry.completed_iterations += u64::from(iterations);
199                    self.profiles.entry(key).or_default().iterations += u64::from(iterations);
200                    return Some(ExitSnapshot {
201                        function,
202                        instruction: header,
203                        locals: locals.to_vec(),
204                        stack: Vec::new(),
205                    });
206                }
207                TraceOutcome::SideExit {
208                    reason,
209                    iterations,
210                    snapshot,
211                } => {
212                    self.telemetry.side_exits += 1;
213                    self.telemetry.completed_iterations += u64::from(iterations);
214                    let profile = self.profiles.entry(key).or_default();
215                    profile.iterations += u64::from(iterations);
216                    match reason {
217                        ExitReason::BranchChanged => {
218                            self.telemetry.branch_exits += 1;
219                            profile.branch_exits = profile.branch_exits.saturating_add(1);
220                        }
221                        ExitReason::WrongTag => self.telemetry.type_exits += 1,
222                        _ => self.telemetry.error_exits += 1,
223                    }
224                    if profile.branch_exits >= self.config.max_branch_exits_before_bailout
225                        && profile.iterations
226                            < u64::from(profile.branch_exits)
227                                * u64::from(self.config.min_iterations_per_branch_exit)
228                    {
229                        self.disabled.insert(key);
230                        self.telemetry.disabled_loops += 1;
231                        return Some(snapshot);
232                    }
233                    if reason == ExitReason::BranchChanged && snapshot.locals == entry {
234                        continue;
235                    }
236                    return Some(snapshot);
237                }
238            }
239        }
240        locals.clone_from_slice(&entry);
241        None
242    }
243
244    #[cfg(test)]
245    pub(crate) fn compiled_count(&self) -> usize {
246        self.traces.values().map(Vec::len).sum()
247    }
248
249    pub(crate) fn telemetry(&self) -> JitTelemetry {
250        self.telemetry
251    }
252
253    pub(crate) fn is_disabled(&self, function: u16, header: u32) -> bool {
254        self.disabled.contains(&LoopKey { function, header })
255    }
256
257    /// True when the function has at least one loop and every loop backedge
258    /// targets a header that has been permanently disabled.  A fresh Machine
259    /// can then skip tracing from its first instruction instead of rebuilding
260    /// a rejected path before it reaches the cached backedge.
261    pub(crate) fn function_is_fully_disabled(&self, program: &Program, function: u16) -> bool {
262        let Some(prototype) = program.functions.get(usize::from(function)) else {
263            return false;
264        };
265        let mut found = false;
266        for (instruction, opcode) in prototype.code.iter().enumerate() {
267            let target = match opcode {
268                Instruction::Jump(target) | Instruction::JumpIfFalse(target)
269                    if usize::try_from(*target).is_ok_and(|target| target <= instruction) =>
270                {
271                    *target
272                }
273                _ => continue,
274            };
275            found = true;
276            if !self.is_disabled(function, target) {
277                return false;
278            }
279        }
280        found
281    }
282}