-
Notifications
You must be signed in to change notification settings - Fork 93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(profile): Introduce a common sample format #1462
Conversation
One worry about indexing stacks that I have is that for some environments (node for example), we do not have control over sample scheduling meaning we only get access to the entire profile once the profile stop method has been called at which point creating an index of stacks may add a non negligible amount of CPU and memory overhead. In any case, I think we need to benchmark this format to see how it impacts our overhead. As an alternative, could we maybe think about supporting both indexed and non indexed stacks? It could be a progressive enhancement where we just distinguish between samples that are |
You don't need to use this feature if you don't want to. Push each stack as a unique one and give it the latest ID. |
@phacops does that mean we no longer index frames thought? That could be a pretty big hit to the profile size |
relay-profiling/src/lib.rs
Outdated
match payload { | ||
Ok(payload) => Ok(vec![payload]), | ||
Err(err) => Err(err), | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
match payload { | |
Ok(payload) => Ok(vec![payload]), | |
Err(err) => Err(err), | |
} | |
Ok(vec![payload?]) |
This might be simpler and there is no need for another match
here
relay-profiling/src/sample.rs
Outdated
fn remove_single_samples_per_thread(&mut self) { | ||
let mut sample_count_by_thread_id: HashMap<u64, u32> = HashMap::new(); | ||
|
||
for sample in self.profile.samples.iter() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for sample in self.profile.samples.iter() { | |
for sample in self.profile.samples { |
you shouldn't have to use .iter()
explicitly here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed it but I needed to borrow.
It wasn't there in the first commit but I added it in a different commit. It would be a shame not to have it. |
relay-profiling/src/lib.rs
Outdated
Err(payload) => Err(payload), | ||
let profile: MinimalProfile = minimal_profile_from_json(payload)?; | ||
match profile.version { | ||
Some(_) => expand_sample_profile(payload), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would match on v1
explicitly here. That would make the intent clearer, and if there's ever a v2
, an old Relay instance would not try to expand it as a sample profile.
let mut items: Vec<Vec<u8>> = Vec::new(); | ||
|
||
for transaction in &profile.transactions { | ||
let mut new_profile = profile.clone(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the purpose of splitting each transaction into a separate profile? A doc comment would help on this function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left a comment, let me know if it's clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, thank you 👍
let mut new_profile = profile.clone(); | ||
|
||
new_profile.transactions.clear(); | ||
new_profile.transactions.push(transaction.clone()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be possible to consume profile.transactions
and prevent the clone()
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how to do this. I need to be able to copy the profile but Profile
doesn't implement Copy
since I have a bunch of Vec
and String
in the struct
. So I explicitely call clone
. Then, this is what the compiler suggests:
error[E0382]: borrow of partially moved value: `profile`
--> relay-profiling/src/sample.rs:205:31
|
204 | for transaction in profile.transactions {
| -------------------- `profile.transactions` partially moved due to this implicit call to `.into_iter()`
205 | let mut new_profile = profile.clone();
| ^^^^^^^^^^^^^^^ value borrowed here after partial move
|
note: this function takes ownership of the receiver `self`, which moves `profile.transactions`
= note: partial move occurs because `profile.transactions` has type `Vec<transaction_metadata::TransactionMetadata>`, which does not implement the `Copy` trait
help: consider iterating over a slice of the `Vec<transaction_metadata::TransactionMetadata>`'s content to avoid moving into the `for` loop
|
204 | for transaction in &profile.transactions {
And from there, it also makes me clone the transaction. What would you do differently?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is indeed trickier than I thought, but what should work is
let mut profile = parse_profile(payload)?;
// [...]
// Replace profile.transactions with an empty vector
// (side benefit: makes calling .clear() unnecessary).
let transactions = std::mem::take(&mut profile.transactions);
for transaction in transactions {
// [...]
new_profile.profile.samples.retain_mut(|sample| {
// [...]
});
new_profile.transactions.push(transaction);
relay-profiling/src/sample.rs
Outdated
return Err(ProfileError::NoTransactionAssociated); | ||
} | ||
|
||
profile.profile.frames.retain(|frame| frame.valid()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could frames
be empty after this? If so, should an error be returned in that case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, since frames are indexed (and stacks as well), I can't remove any of them, valid or invalid. I'll remove this.
@@ -30,6 +30,10 @@ impl TransactionMetadata { | |||
&& self.relative_start_ns < self.relative_end_ns | |||
&& self.relative_cpu_start_ms <= self.relative_cpu_end_ms | |||
} | |||
|
|||
pub fn duration_ns(&self) -> u64 { | |||
self.relative_end_ns - self.relative_start_ns |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could panic if end < start
, you might want to use saturating_sub.
…ad ID the transaction is started on
let mut new_profile = profile.clone(); | ||
|
||
new_profile.transactions.clear(); | ||
new_profile.transactions.push(transaction.clone()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is indeed trickier than I thought, but what should work is
let mut profile = parse_profile(payload)?;
// [...]
// Replace profile.transactions with an empty vector
// (side benefit: makes calling .clear() unnecessary).
let transactions = std::mem::take(&mut profile.transactions);
for transaction in transactions {
// [...]
new_profile.profile.samples.retain_mut(|sample| {
// [...]
});
new_profile.transactions.push(transaction);
let mut items: Vec<Vec<u8>> = Vec::new(); | ||
|
||
for transaction in &profile.transactions { | ||
let mut new_profile = profile.clone(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, thank you 👍
deserialize_with = "deserialize_number_from_string", | ||
skip_serializing_if = "is_zero" | ||
)] | ||
pub thread_id: u64, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@phacops could we call thread_id
active_thread_id
instead?
This way it's easier to figure out what it refers to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@phacops I saw, that when you replaced is_active
in thread_metadata with active_thread_id
in transaction_metadata you've also removed is_main
.
Is it intentional not to have that field anymore?
Yes, it's intentional to not have The objective with Once we live the mobile SDKs world, main thread and active thread become different. At the begining, adding a second metadata field to indicate the active thread seemed like a natural thing to do but it turns out we are not interested in the main thread, we're interested into the active thread. So |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a few small questions, but otherwise LGTM!
&& self.relative_cpu_start_ms <= self.relative_cpu_end_ms | ||
&& self.relative_start_ns < self.relative_end_ns |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does relative_start_ns
use <
but relative_cpu_start_ms
use <=
? Are 0 duration profiles allowed when using relative_cpu_start_ms
(this is android only right?)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's to avoid considering the profile invalid when we don't have a value (fields will be defaulted to 0). Ideally we'd do a check based on the platform but we don't have access to that. We might also have to align both to check for <=
because we might have CPU time only anyway.
@@ -1036,6 +1036,7 @@ impl EnvelopeProcessorService { | |||
match relay_profiling::expand_profile(&item.payload()[..]) { | |||
Ok(payloads) => new_profiles.extend(payloads), | |||
Err(err) => { | |||
relay_log::debug!("invalid profile: {:#?}", err); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are we able to see these logs in prod somewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, that's local only.
#[serde(default, skip_serializing_if = "Option::is_none")] | ||
instruction_addr: Option<Addr>, | ||
#[serde(default, skip_serializing_if = "Option::is_none")] | ||
name: Option<String>, | ||
#[serde(default, skip_serializing_if = "Option::is_none")] | ||
line: Option<u32>, | ||
#[serde(default, skip_serializing_if = "Option::is_none")] | ||
file: Option<String>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since all the attributes are optional, is it possible the SDK sends an empty dict as the frame? Should we invalidate this case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be possible yes. Since we do frame indexing, I wouldn't be really able to remove the frame if it's invalid. We could reject the whole profile after checking if frames referenced in a sample are all valid or not though.
"stack_id": 0, | ||
"thread_id": "1", | ||
"queue_address": "0x0000000102adc700", | ||
"relative_timestamp_ns": "10500500" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@phacops since this is not really a timestamp, should we call it elapsed_ns instead to be more accurate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's call it elapsed_since_start_ns
, makes it very clear that way. @viglia @armcknight
We have several formats for each platform based around sampling. This format is meant to consolidate all of them into one common format, the
Sample
format.Compared to current formats, this one lets us deduplicate the stacks of frames to reduce the size of the profile and standardize common metadata for all platforms and in returns, in the rest of the pipeline, the logic to transform into calltrees will be adapted for this format only instead of having to be adapted for each platform for which it is missing (
rust
,python
andnode
so far). Some of the fields were added or renamed to match what is in theevent
format as well (for exampletimestamp
orrelease
).There are still some fields specific to
cocoa
. It's not great since it's meant to be a more generic format but we still need some things that exists only forcocoa
. Not sure if we can do so much better here. I toyed with the idea of adding a generictags
field so specific metadata but we'd still have to manually validate it since that metadata is mandatory in our data model right now. I think once we'll adapt our data model to be more generic, we'd be able to update this format as well.