|
| 1 | +//! Instrumentation pass for move/copy operations. |
| 2 | +//! |
| 3 | +//! This pass modifies the source scopes of statements containing `Operand::Move` and `Operand::Copy` |
| 4 | +//! to make them appear as if they were inlined from `compiler_move()` and `compiler_copy()` intrinsic |
| 5 | +//! functions. This creates the illusion that moves/copies are function calls in debuggers and |
| 6 | +//! profilers, making them visible for performance analysis. |
| 7 | +//! |
| 8 | +//! The pass leverages the existing inlining infrastructure by creating synthetic `SourceScopeData` |
| 9 | +//! with the `inlined` field set to point to the appropriate intrinsic function. |
| 10 | +
|
| 11 | +use rustc_index::IndexVec; |
| 12 | +use rustc_middle::mir::*; |
| 13 | +use rustc_middle::ty::{self, Instance, Ty, TyCtxt, TypingEnv}; |
| 14 | +use rustc_session::config::DebugInfo; |
| 15 | +use rustc_span::sym; |
| 16 | + |
| 17 | +/// Default minimum size in bytes for move/copy operations to be instrumented. Set to 64+1 bytes |
| 18 | +/// (typical cache line size) to focus on potentially expensive operations. |
| 19 | +const DEFAULT_INSTRUMENT_MOVES_SIZE_LIMIT: u64 = 65; |
| 20 | + |
| 21 | +#[derive(Copy, Clone, Debug)] |
| 22 | +enum Operation { |
| 23 | + Move, |
| 24 | + Copy, |
| 25 | +} |
| 26 | + |
| 27 | +/// Bundle up parameters into a structure to make repeated calling neater |
| 28 | +struct Params<'a, 'tcx> { |
| 29 | + tcx: TyCtxt<'tcx>, |
| 30 | + source_scopes: &'a mut IndexVec<SourceScope, SourceScopeData<'tcx>>, |
| 31 | + local_decls: &'a IndexVec<Local, LocalDecl<'tcx>>, |
| 32 | + typing_env: TypingEnv<'tcx>, |
| 33 | + size_limit: u64, |
| 34 | +} |
| 35 | + |
| 36 | +/// MIR transform that instruments move/copy operations for profiler visibility. |
| 37 | +pub(crate) struct InstrumentMoves; |
| 38 | + |
| 39 | +impl<'tcx> crate::MirPass<'tcx> for InstrumentMoves { |
| 40 | + fn is_enabled(&self, sess: &rustc_session::Session) -> bool { |
| 41 | + sess.opts.unstable_opts.instrument_moves && sess.opts.debuginfo != DebugInfo::None |
| 42 | + } |
| 43 | + |
| 44 | + fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) { |
| 45 | + // Skip promoted MIR bodies to avoid recursion |
| 46 | + if body.source.promoted.is_some() { |
| 47 | + return; |
| 48 | + } |
| 49 | + |
| 50 | + let typing_env = body.typing_env(tcx); |
| 51 | + let size_limit = tcx |
| 52 | + .sess |
| 53 | + .opts |
| 54 | + .unstable_opts |
| 55 | + .instrument_moves_size_limit |
| 56 | + .unwrap_or(DEFAULT_INSTRUMENT_MOVES_SIZE_LIMIT); |
| 57 | + |
| 58 | + // Common params, including selectively borrowing the bits of Body we need to avoid |
| 59 | + // mut/non-mut aliasing problems. |
| 60 | + let mut params = Params { |
| 61 | + tcx, |
| 62 | + source_scopes: &mut body.source_scopes, |
| 63 | + local_decls: &body.local_decls, |
| 64 | + typing_env, |
| 65 | + size_limit, |
| 66 | + }; |
| 67 | + |
| 68 | + // Process each basic block |
| 69 | + for block_data in body.basic_blocks.as_mut() { |
| 70 | + for stmt in &mut block_data.statements { |
| 71 | + let source_info = &mut stmt.source_info; |
| 72 | + if let StatementKind::Assign(box (_, rvalue)) = &stmt.kind { |
| 73 | + match rvalue { |
| 74 | + Rvalue::Use(op) |
| 75 | + | Rvalue::Repeat(op, _) |
| 76 | + | Rvalue::Cast(_, op, _) |
| 77 | + | Rvalue::UnaryOp(_, op) => { |
| 78 | + self.annotate_move(&mut params, source_info, op); |
| 79 | + } |
| 80 | + Rvalue::BinaryOp(_, box (lop, rop)) => { |
| 81 | + self.annotate_move(&mut params, source_info, lop); |
| 82 | + self.annotate_move(&mut params, source_info, rop); |
| 83 | + } |
| 84 | + Rvalue::Aggregate(_, ops) => { |
| 85 | + for op in ops { |
| 86 | + self.annotate_move(&mut params, source_info, op); |
| 87 | + } |
| 88 | + } |
| 89 | + Rvalue::Ref(..) |
| 90 | + | Rvalue::ThreadLocalRef(..) |
| 91 | + | Rvalue::RawPtr(..) |
| 92 | + | Rvalue::NullaryOp(..) |
| 93 | + | Rvalue::Discriminant(..) |
| 94 | + | Rvalue::CopyForDeref(..) |
| 95 | + | Rvalue::ShallowInitBox(..) |
| 96 | + | Rvalue::WrapUnsafeBinder(..) => {} // No operands to instrument |
| 97 | + } |
| 98 | + } |
| 99 | + } |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + fn is_required(&self) -> bool { |
| 104 | + false // Optional optimization/instrumentation pass |
| 105 | + } |
| 106 | +} |
| 107 | + |
| 108 | +impl InstrumentMoves { |
| 109 | + /// If this is a Move or Copy of a concrete type, update its debug info to make it look like it |
| 110 | + /// was inlined from `core::intrinsics::compiler_move`/`compiler_copy`. |
| 111 | + fn annotate_move<'tcx>( |
| 112 | + &self, |
| 113 | + params: &mut Params<'_, 'tcx>, |
| 114 | + source_info: &mut SourceInfo, |
| 115 | + op: &Operand<'tcx>, |
| 116 | + ) { |
| 117 | + let (place, operation) = match op { |
| 118 | + Operand::Move(place) => (place, Operation::Move), |
| 119 | + Operand::Copy(place) => (place, Operation::Copy), |
| 120 | + _ => return, |
| 121 | + }; |
| 122 | + let Params { tcx, typing_env, local_decls, size_limit, source_scopes } = params; |
| 123 | + |
| 124 | + if let Some(type_size) = |
| 125 | + self.should_instrument_operation(*tcx, *typing_env, local_decls, place, *size_limit) |
| 126 | + { |
| 127 | + let ty = place.ty(*local_decls, *tcx).ty; |
| 128 | + source_info.scope = self.create_inlined_scope( |
| 129 | + *tcx, |
| 130 | + *typing_env, |
| 131 | + source_scopes, |
| 132 | + source_info, |
| 133 | + operation, |
| 134 | + ty, |
| 135 | + type_size, |
| 136 | + ); |
| 137 | + } |
| 138 | + } |
| 139 | + |
| 140 | + /// Determines if an operation should be instrumented based on type characteristics. |
| 141 | + /// Returns Some(size) if it should be instrumented, None otherwise. |
| 142 | + fn should_instrument_operation<'tcx>( |
| 143 | + &self, |
| 144 | + tcx: TyCtxt<'tcx>, |
| 145 | + typing_env: ty::TypingEnv<'tcx>, |
| 146 | + local_decls: &rustc_index::IndexVec<Local, LocalDecl<'tcx>>, |
| 147 | + place: &Place<'tcx>, |
| 148 | + size_limit: u64, |
| 149 | + ) -> Option<u64> { |
| 150 | + let ty = place.ty(local_decls, tcx).ty; |
| 151 | + let Ok(layout) = tcx.layout_of(typing_env.as_query_input(ty)) else { |
| 152 | + return None; |
| 153 | + }; |
| 154 | + |
| 155 | + let size = layout.size.bytes(); |
| 156 | + |
| 157 | + // 1. Skip ZST types (no actual move/copy happens) |
| 158 | + if layout.is_zst() { |
| 159 | + return None; |
| 160 | + } |
| 161 | + |
| 162 | + // 2. Check size threshold (only instrument large moves/copies) |
| 163 | + if size < size_limit { |
| 164 | + return None; |
| 165 | + } |
| 166 | + |
| 167 | + // 3. Skip scalar/vector types that won't generate memcpy |
| 168 | + match layout.layout.backend_repr { |
| 169 | + rustc_abi::BackendRepr::Scalar(_) |
| 170 | + | rustc_abi::BackendRepr::ScalarPair(_, _) |
| 171 | + | rustc_abi::BackendRepr::SimdVector { .. } => None, |
| 172 | + _ => Some(size), |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + /// Creates an inlined scope that makes operations appear to come from |
| 177 | + /// the specified compiler intrinsic function. |
| 178 | + fn create_inlined_scope<'tcx>( |
| 179 | + &self, |
| 180 | + tcx: TyCtxt<'tcx>, |
| 181 | + typing_env: TypingEnv<'tcx>, |
| 182 | + source_scopes: &mut IndexVec<SourceScope, SourceScopeData<'tcx>>, |
| 183 | + original_source_info: &SourceInfo, |
| 184 | + operation: Operation, |
| 185 | + ty: Ty<'tcx>, |
| 186 | + type_size: u64, |
| 187 | + ) -> SourceScope { |
| 188 | + let intrinsic_def_id = match operation { |
| 189 | + Operation::Move => tcx.get_diagnostic_item(sym::compiler_move), |
| 190 | + Operation::Copy => tcx.get_diagnostic_item(sym::compiler_copy), |
| 191 | + }; |
| 192 | + |
| 193 | + let Some(intrinsic_def_id) = intrinsic_def_id else { |
| 194 | + // Shouldn't happen, but just return original scope if it does |
| 195 | + return original_source_info.scope; |
| 196 | + }; |
| 197 | + |
| 198 | + // Monomorphize the intrinsic for the actual type being moved/copied + size const parameter |
| 199 | + // compiler_move<T, const SIZE: usize> or compiler_copy<T, const SIZE: usize> |
| 200 | + let size_const = ty::Const::from_target_usize(tcx, type_size); |
| 201 | + let generic_args = tcx.mk_args(&[ty.into(), size_const.into()]); |
| 202 | + let intrinsic_instance = Instance::expect_resolve( |
| 203 | + tcx, |
| 204 | + typing_env, |
| 205 | + intrinsic_def_id, |
| 206 | + generic_args, |
| 207 | + original_source_info.span, |
| 208 | + ); |
| 209 | + |
| 210 | + // Create new inlined scope that makes the operation appear to come from the intrinsic |
| 211 | + let inlined_scope_data = SourceScopeData { |
| 212 | + span: original_source_info.span, |
| 213 | + parent_scope: Some(original_source_info.scope), |
| 214 | + |
| 215 | + // Pretend this op is inlined from the intrinsic |
| 216 | + inlined: Some((intrinsic_instance, original_source_info.span)), |
| 217 | + |
| 218 | + // Proper inlined scope chaining to maintain debug info hierarchy |
| 219 | + inlined_parent_scope: { |
| 220 | + let parent_scope = &source_scopes[original_source_info.scope]; |
| 221 | + if parent_scope.inlined.is_some() { |
| 222 | + // If parent is already inlined, chain through it |
| 223 | + Some(original_source_info.scope) |
| 224 | + } else { |
| 225 | + // Otherwise, use the parent's inlined_parent_scope |
| 226 | + parent_scope.inlined_parent_scope |
| 227 | + } |
| 228 | + }, |
| 229 | + |
| 230 | + local_data: ClearCrossCrate::Clear, |
| 231 | + }; |
| 232 | + |
| 233 | + // Add the new scope |
| 234 | + source_scopes.push(inlined_scope_data) |
| 235 | + } |
| 236 | +} |
0 commit comments