Skip to content

Commit 95277cf

Browse files
committed
fix(perfetto): Introduces memory/size limits for perfetto conversion
1 parent 0206ff2 commit 95277cf

8 files changed

Lines changed: 602 additions & 102 deletions

File tree

relay-event-schema/src/protocol/debugmeta.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ macro_rules! impl_traits {
208208
};
209209
}
210210

211-
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
211+
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
212212
pub struct DebugId(pub debugid::DebugId);
213213

214214
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]

relay-profiling/src/debug_image.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use uuid::{Error as UuidError, Uuid};
77
use crate::utils;
88

99
/// The type of a debug image.
10-
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
10+
#[derive(Debug, Serialize, Deserialize, PartialEq, Copy, Clone)]
1111
#[serde(rename_all = "lowercase")]
1212
pub enum ImageType {
1313
MachO,
Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
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

Comments
 (0)