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#[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 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 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 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}