// We use the YARV bytecode constants which have a CRuby-style name #![allow(non_upper_case_globals)] use core::ffi::c_void; use std::collections::HashMap; use crate::{cruby::*, hir_type::{types::{Empty, Fixnum}, Type}, virtualmem::CodePtr}; /// Ephemeral state for profiling runtime information struct Profiler { cfp: CfpPtr, iseq: IseqPtr, insn_idx: usize, } impl Profiler { fn new(ec: EcPtr) -> Self { let cfp = unsafe { get_ec_cfp(ec) }; let iseq = unsafe { get_cfp_iseq(cfp) }; Profiler { cfp, iseq, insn_idx: unsafe { get_cfp_pc(cfp).offset_from(get_iseq_body_iseq_encoded(iseq)) as usize }, } } // Get an instruction operand that sits next to the opcode at PC. fn insn_opnd(&self, idx: usize) -> VALUE { unsafe { get_cfp_pc(self.cfp).add(1 + idx).read() } } // Peek at the nth topmost value on the Ruby stack. // Returns the topmost value when n == 0. fn peek_at_stack(&self, n: isize) -> VALUE { unsafe { let sp: *mut VALUE = get_cfp_sp(self.cfp); *(sp.offset(-1 - n)) } } } /// API called from zjit_* instruction. opcode is the bare (non-zjit_*) instruction. #[unsafe(no_mangle)] pub extern "C" fn rb_zjit_profile_insn(opcode: ruby_vminsn_type, ec: EcPtr) { with_vm_lock(src_loc!(), || { let mut profiler = Profiler::new(ec); profile_insn(&mut profiler, opcode); }); } /// Profile a YARV instruction fn profile_insn(profiler: &mut Profiler, opcode: ruby_vminsn_type) { match opcode { YARVINSN_opt_plus => profile_operands(profiler, 2), YARVINSN_opt_minus => profile_operands(profiler, 2), YARVINSN_opt_mult => profile_operands(profiler, 2), YARVINSN_opt_div => profile_operands(profiler, 2), YARVINSN_opt_mod => profile_operands(profiler, 2), YARVINSN_opt_eq => profile_operands(profiler, 2), YARVINSN_opt_neq => profile_operands(profiler, 2), YARVINSN_opt_lt => profile_operands(profiler, 2), YARVINSN_opt_le => profile_operands(profiler, 2), YARVINSN_opt_gt => profile_operands(profiler, 2), YARVINSN_opt_ge => profile_operands(profiler, 2), YARVINSN_opt_send_without_block => { let cd: *const rb_call_data = profiler.insn_opnd(0).as_ptr(); let argc = unsafe { vm_ci_argc((*cd).ci) }; // Profile all the arguments and self (+1). profile_operands(profiler, (argc + 1) as usize); } _ => {} } } /// Profile the Type of top-`n` stack operands fn profile_operands(profiler: &mut Profiler, n: usize) { let payload = get_or_create_iseq_payload(profiler.iseq); let mut types = if let Some(types) = payload.opnd_types.get(&profiler.insn_idx) { types.clone() } else { vec![Empty; n] }; for i in 0..n { let opnd_type = Type::from_value(profiler.peek_at_stack((n - i - 1) as isize)); types[i] = types[i].union(opnd_type); } payload.opnd_types.insert(profiler.insn_idx, types); } /// This is all the data ZJIT stores on an iseq. This will be dynamically allocated by C code /// C code should pass an &mut IseqPayload to us when calling into ZJIT. #[derive(Default, Debug)] pub struct IseqPayload { /// Type information of YARV instruction operands, indexed by the instruction index opnd_types: HashMap>, /// JIT code address of the first block pub start_ptr: Option, } impl IseqPayload { /// Get profiled operand types for a given instruction index pub fn get_operand_types(&self, insn_idx: usize) -> Option<&[Type]> { self.opnd_types.get(&insn_idx).map(|types| types.as_slice()) } /// Return true if top-two stack operands are Fixnums pub fn have_two_fixnums(&self, insn_idx: usize) -> bool { match self.get_operand_types(insn_idx) { Some([left, right]) => left.is_subtype(Fixnum) && right.is_subtype(Fixnum), _ => false, } } } /// Get the payload for an iseq. For safety it's up to the caller to ensure the returned `&mut` /// upholds aliasing rules and that the argument is a valid iseq. pub fn get_iseq_payload(iseq: IseqPtr) -> Option<&'static mut IseqPayload> { let payload = unsafe { rb_iseq_get_zjit_payload(iseq) }; let payload: *mut IseqPayload = payload.cast(); unsafe { payload.as_mut() } } /// Get the payload object associated with an iseq. Create one if none exists. pub fn get_or_create_iseq_payload(iseq: IseqPtr) -> &'static mut IseqPayload { type VoidPtr = *mut c_void; let payload_non_null = unsafe { let payload = rb_iseq_get_zjit_payload(iseq); if payload.is_null() { // Allocate a new payload with Box and transfer ownership to the GC. // We drop the payload with Box::from_raw when the GC frees the iseq and calls us. // NOTE(alan): Sometimes we read from an iseq without ever writing to it. // We allocate in those cases anyways. let new_payload = IseqPayload::default(); let new_payload = Box::into_raw(Box::new(new_payload)); rb_iseq_set_zjit_payload(iseq, new_payload as VoidPtr); new_payload } else { payload as *mut IseqPayload } }; // SAFETY: we should have the VM lock and all other Ruby threads should be asleep. So we have // exclusive mutable access. // Hmm, nothing seems to stop calling this on the same // iseq twice, though, which violates aliasing rules. unsafe { payload_non_null.as_mut() }.unwrap() }