|
| 1 | +use std::cell::Cell; |
| 2 | +use std::rc::Rc; |
| 3 | + |
| 4 | +use hashbrown::HashMap; |
| 5 | +use relay_event_schema::protocol::{Addr, DebugId, NativeImagePath}; |
| 6 | + |
| 7 | +use crate::ProfileError; |
| 8 | +use crate::debug_image::{DebugImage, ImageType}; |
| 9 | +use crate::perfetto::proto; |
| 10 | +use crate::sample::Frame; |
| 11 | +use crate::sample::v2::{FrameId, Sample, StackId}; |
| 12 | + |
| 13 | +/// Use as an error when the configured [`MemoryBudget`] limit is exceeded. |
| 14 | +#[derive(Debug, Clone, Copy)] |
| 15 | +pub struct BudgetExceeded; |
| 16 | + |
| 17 | +impl From<BudgetExceeded> for ProfileError { |
| 18 | + fn from(_: BudgetExceeded) -> Self { |
| 19 | + Self::ExceedSizeLimit |
| 20 | + } |
| 21 | +} |
| 22 | + |
| 23 | +/// A convenient tracker for tracking memory limits. |
| 24 | +/// |
| 25 | +/// The tracker can be cloned, all of the cloned instances will share the same limit. |
| 26 | +#[derive(Debug, Clone)] |
| 27 | +pub struct MemoryBudget { |
| 28 | + remaining: Rc<Cell<usize>>, |
| 29 | +} |
| 30 | + |
| 31 | +impl MemoryBudget { |
| 32 | + /// Creates a new tracker, allowing up to `limit` bytes to be used. |
| 33 | + pub fn new(limit: usize) -> Self { |
| 34 | + Self { |
| 35 | + remaining: Rc::new(Cell::new(limit)), |
| 36 | + } |
| 37 | + } |
| 38 | + |
| 39 | + /// Derives a new [`LocalBudget`]. |
| 40 | + /// |
| 41 | + /// The local budget will contribute to the budget of this tracker. |
| 42 | + pub fn local(&self) -> LocalBudget { |
| 43 | + LocalBudget { |
| 44 | + budget: self.clone(), |
| 45 | + local_usage: 0, |
| 46 | + } |
| 47 | + } |
| 48 | + |
| 49 | + /// Creates a budgeted vector. |
| 50 | + pub fn vec<T>(&self) -> BudgetVec<T> { |
| 51 | + BudgetVec { |
| 52 | + budget: self.local(), |
| 53 | + inner: Default::default(), |
| 54 | + } |
| 55 | + } |
| 56 | + |
| 57 | + /// Creates a budgeted map. |
| 58 | + pub fn map<K, V>(&self) -> BudgetMap<K, V> { |
| 59 | + BudgetMap { |
| 60 | + budget: self.local(), |
| 61 | + inner: Default::default(), |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + /// Attempts to add `size` to the current usage, if the usage is exceeded returns [`BudgetExceeded`]. |
| 66 | + pub fn try_add(&self, size: usize) -> Result<(), BudgetExceeded> { |
| 67 | + let current = self.remaining.get(); |
| 68 | + let new = current.checked_sub(size).ok_or(BudgetExceeded)?; |
| 69 | + self.remaining.set(new); |
| 70 | + Ok(()) |
| 71 | + } |
| 72 | + |
| 73 | + /// Removes `size` from the current usage. |
| 74 | + pub fn remove(&self, size: usize) { |
| 75 | + self.remaining.set(self.remaining.get() + size); |
| 76 | + } |
| 77 | +} |
| 78 | + |
| 79 | +/// A [`LocalBudget`] is like a [`MemoryBudget`], but it also keeps track of its local usage |
| 80 | +/// and refunds it on drop. |
| 81 | +#[derive(Debug)] |
| 82 | +pub struct LocalBudget { |
| 83 | + budget: MemoryBudget, |
| 84 | + local_usage: usize, |
| 85 | +} |
| 86 | + |
| 87 | +impl LocalBudget { |
| 88 | + /// Attempts to add `size` to the current usage, if the usage is exceeded returns [`BudgetExceeded`]. |
| 89 | + pub fn try_add(&mut self, size: usize) -> Result<(), BudgetExceeded> { |
| 90 | + self.budget.try_add(size)?; |
| 91 | + self.local_usage += size; |
| 92 | + Ok(()) |
| 93 | + } |
| 94 | + |
| 95 | + /// Removes `size` from the current usage. |
| 96 | + pub fn remove(&mut self, size: usize) { |
| 97 | + self.budget.remove(size); |
| 98 | + self.local_usage = self.local_usage.saturating_sub(size); |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +impl Drop for LocalBudget { |
| 103 | + fn drop(&mut self) { |
| 104 | + self.budget.remove(self.local_usage); |
| 105 | + } |
| 106 | +} |
| 107 | + |
| 108 | +/// A minimal [`Vec`] which keeps track of its internal memory usage. |
| 109 | +#[derive(Debug)] |
| 110 | +pub struct BudgetVec<T> { |
| 111 | + budget: LocalBudget, |
| 112 | + inner: Vec<T>, |
| 113 | +} |
| 114 | + |
| 115 | +impl<T> BudgetVec<T> { |
| 116 | + /// Returns the inner [`Vec`]. |
| 117 | + pub fn into_inner(self) -> Vec<T> { |
| 118 | + self.inner |
| 119 | + } |
| 120 | +} |
| 121 | + |
| 122 | +impl<T> BudgetVec<T> |
| 123 | +where |
| 124 | + T: BudgetSize, |
| 125 | +{ |
| 126 | + /// Appends an `item` to the vector. |
| 127 | + /// |
| 128 | + /// Returns [`BudgetExceeded`] if the memory budget is exhausted. |
| 129 | + pub fn push(&mut self, item: T) -> Result<(), BudgetExceeded> { |
| 130 | + self.budget.try_add(item.size())?; |
| 131 | + self.inner.push(item); |
| 132 | + Ok(()) |
| 133 | + } |
| 134 | +} |
| 135 | + |
| 136 | +impl<T> std::ops::Deref for BudgetVec<T> { |
| 137 | + type Target = Vec<T>; |
| 138 | + |
| 139 | + fn deref(&self) -> &Self::Target { |
| 140 | + &self.inner |
| 141 | + } |
| 142 | +} |
| 143 | + |
| 144 | +#[derive(Debug)] |
| 145 | +pub struct BudgetMap<K, V> { |
| 146 | + budget: LocalBudget, |
| 147 | + inner: HashMap<K, V>, |
| 148 | +} |
| 149 | + |
| 150 | +impl<K, V> BudgetMap<K, V> |
| 151 | +where |
| 152 | + K: Eq + std::hash::Hash + BudgetSize, |
| 153 | + V: BudgetSize, |
| 154 | +{ |
| 155 | + /// Inserts an entry into the map. |
| 156 | + /// |
| 157 | + /// Returns [`BudgetExceeded`] if the memory budget is exhausted. |
| 158 | + pub fn insert(&mut self, k: K, v: V) -> Result<Option<V>, BudgetExceeded> { |
| 159 | + let key_size = k.size(); |
| 160 | + self.budget.try_add(key_size + v.size())?; |
| 161 | + Ok(match self.inner.insert(k, v) { |
| 162 | + Some(prev) => { |
| 163 | + // Key was already stored, remove one count to not double count it. |
| 164 | + self.budget.remove(key_size + prev.size()); |
| 165 | + Some(prev) |
| 166 | + } |
| 167 | + None => None, |
| 168 | + }) |
| 169 | + } |
| 170 | + |
| 171 | + /// Inserts an iterator of entries into the map. |
| 172 | + /// |
| 173 | + /// Returns [`BudgetExceeded`] if the memory budget is exhausted. |
| 174 | + pub fn extend<T: IntoIterator<Item = (K, V)>>( |
| 175 | + &mut self, |
| 176 | + iter: T, |
| 177 | + ) -> Result<(), BudgetExceeded> { |
| 178 | + for (k, v) in iter { |
| 179 | + self.insert(k, v)?; |
| 180 | + } |
| 181 | + |
| 182 | + Ok(()) |
| 183 | + } |
| 184 | +} |
| 185 | + |
| 186 | +impl<K, V> std::ops::Deref for BudgetMap<K, V> { |
| 187 | + type Target = HashMap<K, V>; |
| 188 | + |
| 189 | + fn deref(&self) -> &Self::Target { |
| 190 | + &self.inner |
| 191 | + } |
| 192 | +} |
| 193 | + |
| 194 | +/// Helper trait which returns an estimated memory usage of an item. |
| 195 | +pub trait BudgetSize { |
| 196 | + fn size(&self) -> usize; |
| 197 | +} |
| 198 | + |
| 199 | +// Various trait impls, hidden behind a module for convenience. |
| 200 | +mod impls { |
| 201 | + use super::*; |
| 202 | + |
| 203 | + macro_rules! impl_budget_size_copy { |
| 204 | + ($T:ty) => { |
| 205 | + impl BudgetSize for $T |
| 206 | + where |
| 207 | + $T: Copy, |
| 208 | + { |
| 209 | + fn size(&self) -> usize { |
| 210 | + std::mem::size_of::<$T>() |
| 211 | + } |
| 212 | + } |
| 213 | + }; |
| 214 | + } |
| 215 | + |
| 216 | + impl_budget_size_copy!(bool); |
| 217 | + impl_budget_size_copy!(u32); |
| 218 | + impl_budget_size_copy!(u64); |
| 219 | + impl_budget_size_copy!(proto::Frame); |
| 220 | + impl_budget_size_copy!(Addr); |
| 221 | + impl_budget_size_copy!(uuid::Uuid); |
| 222 | + impl_budget_size_copy!(DebugId); |
| 223 | + impl_budget_size_copy!(ImageType); |
| 224 | + impl_budget_size_copy!(StackId); |
| 225 | + impl_budget_size_copy!(FrameId); |
| 226 | + |
| 227 | + macro_rules! impl_budget_size_vec_copy { |
| 228 | + ($T:ty) => { |
| 229 | + impl BudgetSize for Vec<$T> |
| 230 | + where |
| 231 | + $T: Copy, |
| 232 | + { |
| 233 | + fn size(&self) -> usize { |
| 234 | + std::mem::size_of::<Vec<$T>>() + (self.len() * std::mem::size_of::<$T>()) |
| 235 | + } |
| 236 | + } |
| 237 | + }; |
| 238 | + } |
| 239 | + |
| 240 | + impl_budget_size_vec_copy!(u8); |
| 241 | + impl_budget_size_vec_copy!(u64); |
| 242 | + impl_budget_size_vec_copy!(FrameId); |
| 243 | + |
| 244 | + impl BudgetSize for String { |
| 245 | + fn size(&self) -> usize { |
| 246 | + std::mem::size_of::<String>() + self.len() |
| 247 | + } |
| 248 | + } |
| 249 | + |
| 250 | + impl<T> BudgetSize for Option<T> |
| 251 | + where |
| 252 | + T: BudgetSize, |
| 253 | + { |
| 254 | + fn size(&self) -> usize { |
| 255 | + let total = std::mem::size_of::<Self>(); |
| 256 | + let inner = std::mem::size_of::<T>(); |
| 257 | + match self { |
| 258 | + Some(v) => v.size() + total - inner, |
| 259 | + None => total, |
| 260 | + } |
| 261 | + } |
| 262 | + } |
| 263 | + |
| 264 | + impl BudgetSize for Sample { |
| 265 | + fn size(&self) -> usize { |
| 266 | + let Sample { |
| 267 | + timestamp, |
| 268 | + stack_id, |
| 269 | + thread_id, |
| 270 | + } = self; |
| 271 | + |
| 272 | + std::mem::size_of_val(timestamp) + stack_id.size() + thread_id.size() |
| 273 | + } |
| 274 | + } |
| 275 | + |
| 276 | + impl BudgetSize for NativeImagePath { |
| 277 | + fn size(&self) -> usize { |
| 278 | + self.0.size() |
| 279 | + } |
| 280 | + } |
| 281 | + |
| 282 | + impl BudgetSize for DebugImage { |
| 283 | + fn size(&self) -> usize { |
| 284 | + let DebugImage { |
| 285 | + code_file, |
| 286 | + debug_id, |
| 287 | + image_type, |
| 288 | + image_addr, |
| 289 | + image_vmaddr, |
| 290 | + image_size, |
| 291 | + uuid, |
| 292 | + } = self; |
| 293 | + |
| 294 | + code_file.size() |
| 295 | + + debug_id.size() |
| 296 | + + image_type.size() |
| 297 | + + image_addr.size() |
| 298 | + + image_vmaddr.size() |
| 299 | + + image_size.size() |
| 300 | + + uuid.size() |
| 301 | + } |
| 302 | + } |
| 303 | + |
| 304 | + impl BudgetSize for Frame { |
| 305 | + fn size(&self) -> usize { |
| 306 | + let Frame { |
| 307 | + abs_path, |
| 308 | + colno, |
| 309 | + filename, |
| 310 | + function, |
| 311 | + in_app, |
| 312 | + instruction_addr, |
| 313 | + lineno, |
| 314 | + module, |
| 315 | + package, |
| 316 | + platform, |
| 317 | + } = self; |
| 318 | + |
| 319 | + abs_path.size() |
| 320 | + + colno.size() |
| 321 | + + filename.size() |
| 322 | + + function.size() |
| 323 | + + in_app.size() |
| 324 | + + instruction_addr.size() |
| 325 | + + lineno.size() |
| 326 | + + module.size() |
| 327 | + + package.size() |
| 328 | + + platform.size() |
| 329 | + } |
| 330 | + } |
| 331 | + |
| 332 | + impl BudgetSize for proto::Callstack { |
| 333 | + fn size(&self) -> usize { |
| 334 | + let ty = std::mem::size_of::<Self>(); |
| 335 | + let array = self.frame_ids.len() * std::mem::size_of::<u64>(); |
| 336 | + ty + array |
| 337 | + } |
| 338 | + } |
| 339 | + |
| 340 | + impl BudgetSize for proto::Mapping { |
| 341 | + fn size(&self) -> usize { |
| 342 | + let ty = std::mem::size_of::<Self>(); |
| 343 | + let array = self.path_string_ids.len() * std::mem::size_of::<u64>(); |
| 344 | + ty + array |
| 345 | + } |
| 346 | + } |
| 347 | +} |
0 commit comments