|
| 1 | +//! Breakdown of `zjit_alloc_bytes` (ZJIT's Rust heap usage) by subsystem. |
| 2 | +//! |
| 3 | +//! `zjit_alloc_bytes` is a single number produced by the global allocator |
| 4 | +//! wrapper in the `jit` crate, so it tells you *how much* ZJIT has allocated |
| 5 | +//! but not *what for*. On large applications the non-code metadata is several |
| 6 | +//! times the size of the generated code, and `--zjit-mem-size` caps the sum of |
| 7 | +//! the two, so knowing the composition is the difference between guessing and |
| 8 | +//! fixing. |
| 9 | +//! |
| 10 | +//! This module walks the structures ZJIT retains and reports the bytes each |
| 11 | +//! subsystem owns. Vec-backed sizes are exact (capacity times element size); |
| 12 | +//! hash-table sizes are close approximations of what hashbrown asks the |
| 13 | +//! allocator for (see [`hash_table_bytes`]). The residual between |
| 14 | +//! `zjit_alloc_bytes` and the sum of the categories is reported as |
| 15 | +//! `mem_unaccounted_bytes`. |
| 16 | +
|
| 17 | +use crate::cruby::{IseqPtr, for_each_iseq, rb_iseq_get_jit_payload}; |
| 18 | +use crate::payload::IseqPayload; |
| 19 | +use crate::state::ZJITState; |
| 20 | + |
| 21 | +/// Number of control bytes hashbrown allocates past the end of the bucket |
| 22 | +/// array. This is `Group::WIDTH`, 16 on x86-64 (SSE2) and aarch64 (NEON). |
| 23 | +const HASHBROWN_GROUP_WIDTH: usize = 16; |
| 24 | + |
| 25 | +/// Approximate the bytes hashbrown (the backing store of `std`'s `HashMap` and |
| 26 | +/// `HashSet`) requests from the allocator for a table whose `capacity()` is |
| 27 | +/// `capacity` and whose element type is `T`. |
| 28 | +/// |
| 29 | +/// hashbrown rounds the requested capacity up to a power-of-two bucket count |
| 30 | +/// and allocates `buckets * size_of::<T>()` for the entries plus |
| 31 | +/// `buckets + Group::WIDTH` control bytes. Padding for alignment is ignored, so |
| 32 | +/// this can be a few bytes low per table. |
| 33 | +pub fn hash_table_bytes<T>(capacity: usize) -> usize { |
| 34 | + if capacity == 0 { |
| 35 | + return 0; |
| 36 | + } |
| 37 | + // Inverse of hashbrown's capacity_to_buckets(): buckets 4 hold 3 items, |
| 38 | + // buckets 8 hold 7, and beyond that capacity is 7/8 of the bucket count. |
| 39 | + let buckets = if capacity <= 3 { |
| 40 | + 4 |
| 41 | + } else if capacity <= 7 { |
| 42 | + 8 |
| 43 | + } else { |
| 44 | + (capacity * 8 / 7).next_power_of_two() |
| 45 | + }; |
| 46 | + buckets * size_of::<T>() + buckets + HASHBROWN_GROUP_WIDTH |
| 47 | +} |
| 48 | + |
| 49 | +/// Bytes retained by each ZJIT subsystem, in the same units as |
| 50 | +/// `zjit_alloc_bytes`. |
| 51 | +#[derive(Default, Debug)] |
| 52 | +pub struct MemoryBreakdown { |
| 53 | + /// `IseqPayload` structs, one per ISEQ ZJIT has ever touched. |
| 54 | + pub iseq_payload_bytes: usize, |
| 55 | + /// Per-instruction profiling data (type/shape distributions) inside payloads. |
| 56 | + pub profile_bytes: usize, |
| 57 | + /// `IseqVersion` structs plus the `Vec` of version pointers in each payload. |
| 58 | + pub iseq_version_bytes: usize, |
| 59 | + /// GC offset tables (addresses of `VALUE`s baked into JIT code). |
| 60 | + pub gc_offset_bytes: usize, |
| 61 | + /// JIT-to-JIT call metadata: `IseqCall` allocations and the incoming and |
| 62 | + /// outgoing edge vectors that point at them. |
| 63 | + pub iseq_call_bytes: usize, |
| 64 | + /// Patch-point tables used to invalidate speculative code (`Invariants`). |
| 65 | + pub invariant_bytes: usize, |
| 66 | + /// `JITFrame`s: compile-time frame metadata plus their trailing stack maps. |
| 67 | + pub jit_frame_bytes: usize, |
| 68 | + /// `CodeBlock` bookkeeping: label tables and (with `--zjit-dump-disasm`) |
| 69 | + /// assembly comments. |
| 70 | + pub code_block_bytes: usize, |
| 71 | + /// String-keyed counter tables that only `--zjit-stats` populates. |
| 72 | + pub stats_counter_bytes: usize, |
| 73 | + /// Shape tables for ivar accesses that miss their inline guard chain. |
| 74 | + pub ivar_cache_bytes: usize, |
| 75 | + /// Class tables for send sites that dispatch over more classes than an |
| 76 | + /// inline guard chain can cover. |
| 77 | + pub send_cache_bytes: usize, |
| 78 | + /// Interpreter state for compiled side exits. Trading these bytes for |
| 79 | + /// executable ones is the point of the exercise: they used to be immediates |
| 80 | + /// in the exit stubs. See [`crate::exit_meta`]. |
| 81 | + pub exit_meta_bytes: usize, |
| 82 | + /// The deduplicated set of ISEQs the JITFrame and ExitMeta tables reference, |
| 83 | + /// which is what the GC mark phase walks in their place. See |
| 84 | + /// [`crate::gc::RootIseqs`]. |
| 85 | + pub root_iseq_bytes: usize, |
| 86 | + |
| 87 | + /// Number of ISEQ payloads walked, for per-ISEQ math. |
| 88 | + pub payload_count: usize, |
| 89 | + /// Number of `IseqVersion`s reachable from those payloads. |
| 90 | + pub version_count: usize, |
| 91 | + /// Number of per-instruction profile entries in those payloads. |
| 92 | + pub profile_entry_count: usize, |
| 93 | + /// Bytes sitting in the unused tail of profile `entries` vectors. |
| 94 | + pub profile_entry_slack_bytes: usize, |
| 95 | + /// Number of operand type distributions across all profiles. |
| 96 | + pub profile_distribution_count: usize, |
| 97 | + /// How many of those distributions saw at most one type. |
| 98 | + pub profile_monomorphic_distribution_count: usize, |
| 99 | + /// Number of patch points in `Invariants`. |
| 100 | + pub patch_point_count: usize, |
| 101 | + /// Number of live `JITFrame`s. |
| 102 | + pub jit_frame_count: usize, |
| 103 | + /// Number of ivar shape tables, i.e. distinct ivar names with one. |
| 104 | + pub ivar_cache_count: usize, |
| 105 | + /// Number of send class tables, i.e. distinct call shapes with one. |
| 106 | + pub send_cache_count: usize, |
| 107 | + /// Number of interned `ExitMeta` records. |
| 108 | + pub exit_meta_count: usize, |
| 109 | + /// Number of distinct ISEQs in the root set. The ratio against |
| 110 | + /// `jit_frame_count + 2 * exit_meta_count` is what deduplication saves the |
| 111 | + /// mark phase on every collection. |
| 112 | + pub root_iseq_count: usize, |
| 113 | + /// Number of objects in the dense arrays GC marking walks, i.e. the distinct |
| 114 | + /// objects the profiles reference. See |
| 115 | + /// [`crate::profile::IseqProfile::marked_objects`]. |
| 116 | + pub profile_marked_object_count: usize, |
| 117 | +} |
| 118 | + |
| 119 | +impl MemoryBreakdown { |
| 120 | + /// Sum of every byte category above. |
| 121 | + pub fn accounted_bytes(&self) -> usize { |
| 122 | + self.iseq_payload_bytes |
| 123 | + + self.profile_bytes |
| 124 | + + self.iseq_version_bytes |
| 125 | + + self.gc_offset_bytes |
| 126 | + + self.iseq_call_bytes |
| 127 | + + self.invariant_bytes |
| 128 | + + self.jit_frame_bytes |
| 129 | + + self.code_block_bytes |
| 130 | + + self.stats_counter_bytes |
| 131 | + + self.ivar_cache_bytes |
| 132 | + + self.send_cache_bytes |
| 133 | + + self.exit_meta_bytes |
| 134 | + + self.root_iseq_bytes |
| 135 | + } |
| 136 | +} |
| 137 | + |
| 138 | +/// Walk everything ZJIT retains on the Rust heap and attribute it to a |
| 139 | +/// subsystem. Requires the VM lock (it iterates over every live ISEQ). |
| 140 | +pub fn memory_breakdown() -> MemoryBreakdown { |
| 141 | + let mut out = MemoryBreakdown::default(); |
| 142 | + |
| 143 | + // Per-ISEQ payloads. Only ISEQs that are still alive are visited; ZJIT |
| 144 | + // currently never frees the payload of a dead ISEQ, so those bytes show up |
| 145 | + // in mem_unaccounted_bytes rather than here. |
| 146 | + for_each_iseq(|iseq: IseqPtr| { |
| 147 | + let payload = unsafe { rb_iseq_get_jit_payload(iseq) } as *const IseqPayload; |
| 148 | + if payload.is_null() { |
| 149 | + return; |
| 150 | + } |
| 151 | + let payload = unsafe { &*payload }; |
| 152 | + out.payload_count += 1; |
| 153 | + out.iseq_payload_bytes += size_of::<IseqPayload>(); |
| 154 | + |
| 155 | + let profile = payload.profile.heap_size(); |
| 156 | + out.profile_bytes += profile.bytes; |
| 157 | + out.profile_entry_count += profile.entry_count; |
| 158 | + out.profile_entry_slack_bytes += profile.entry_slack_bytes; |
| 159 | + out.profile_distribution_count += profile.distribution_count; |
| 160 | + out.profile_monomorphic_distribution_count += profile.monomorphic_distribution_count; |
| 161 | + out.profile_marked_object_count += profile.marked_object_count; |
| 162 | + |
| 163 | + out.iseq_version_bytes += payload.versions.capacity() * size_of::<crate::payload::IseqVersionRef>(); |
| 164 | + for version in payload.versions.iter() { |
| 165 | + let version = unsafe { version.as_ref() }; |
| 166 | + out.version_count += 1; |
| 167 | + out.iseq_version_bytes += size_of::<crate::payload::IseqVersion>(); |
| 168 | + out.gc_offset_bytes += version.gc_offsets.heap_size(); |
| 169 | + out.iseq_call_bytes += version.iseq_call_heap_size(); |
| 170 | + } |
| 171 | + }); |
| 172 | + |
| 173 | + let invariants = ZJITState::get_invariants(); |
| 174 | + let (invariant_bytes, patch_point_count) = invariants.heap_size(); |
| 175 | + out.invariant_bytes = invariant_bytes; |
| 176 | + out.patch_point_count = patch_point_count; |
| 177 | + |
| 178 | + let jit_frames = ZJITState::get_jit_frames(); |
| 179 | + out.jit_frame_count = jit_frames.len(); |
| 180 | + out.jit_frame_bytes = jit_frames.capacity() * size_of::<*mut crate::jit_frame::JITFrame>() |
| 181 | + + jit_frames.iter().map(|&frame| unsafe { &*frame }.heap_size()).sum::<usize>(); |
| 182 | + |
| 183 | + let exit_metas = ZJITState::get_exit_metas(); |
| 184 | + out.exit_meta_count = exit_metas.len(); |
| 185 | + out.exit_meta_bytes = exit_metas.capacity() * size_of::<crate::exit_meta::ExitMeta>(); |
| 186 | + |
| 187 | + let root_iseqs = ZJITState::get_root_iseqs(); |
| 188 | + out.root_iseq_count = root_iseqs.len(); |
| 189 | + out.root_iseq_bytes = root_iseqs.heap_size(); |
| 190 | + |
| 191 | + let ivar_caches = ZJITState::get_ivar_caches(); |
| 192 | + out.ivar_cache_count = ivar_caches.len(); |
| 193 | + out.ivar_cache_bytes = hash_table_bytes::<(crate::cruby::ID, Box<crate::ivar_cache::IvarCache>)>(ivar_caches.capacity()) |
| 194 | + + ivar_caches.values().map(|cache| size_of::<crate::ivar_cache::IvarCache>() + cache.heap_size()).sum::<usize>(); |
| 195 | + |
| 196 | + let send_caches = ZJITState::get_send_caches(); |
| 197 | + out.send_cache_count = send_caches.len(); |
| 198 | + out.send_cache_bytes = crate::send_cache::send_caches_heap_size(send_caches); |
| 199 | + |
| 200 | + out.code_block_bytes = ZJITState::get_code_block().heap_size(); |
| 201 | + out.stats_counter_bytes = ZJITState::counter_table_heap_size(); |
| 202 | + |
| 203 | + out |
| 204 | +} |
0 commit comments