-
Notifications
You must be signed in to change notification settings - Fork 16
Change Buffer #1453
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
base: main
Are you sure you want to change the base?
Change Buffer #1453
Conversation
BenchmarksComparisonBenchmark execution time: 2026-01-20 20:09:29 Comparing candidate commit 5a6f809 in PR branch Found 0 performance improvements and 1 performance regressions! Performance is the same for 56 metrics, 2 unstable metrics. scenario:profile_add_sample2_frames_x1000
CandidateCandidate benchmark detailsGroup 1
Group 2
Group 3
Group 4
Group 5
Group 6
Group 7
Group 8
Group 9
Group 10
Group 11
Group 12
Group 13
Group 14
Group 15
Group 16
Group 17
Group 18
Group 19
BaselineOmitted due to size. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1453 +/- ##
==========================================
- Coverage 71.29% 70.93% -0.36%
==========================================
Files 416 418 +2
Lines 66872 67233 +361
==========================================
+ Hits 47676 47694 +18
- Misses 19196 19539 +343
🚀 New features to boost your workflow:
|
ae7b0d1 to
4c76607
Compare
Artifact Size Benchmark Reportaarch64-alpine-linux-musl
aarch64-apple-darwin
aarch64-unknown-linux-gnu
libdatadog-x64-windows
libdatadog-x86-windows
x86_64-alpine-linux-musl
x86_64-apple-darwin
x86_64-unknown-linux-gnu
|
| trace_span_counts: HashMap<u128, usize>, | ||
| tracer_service: T, | ||
| tracer_language: T, | ||
| pid: f64, |
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.
Aren't PIDs integers?
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.
Not in trace exporter's config 🤷
|
|
||
| impl From<u64> for OpCode { | ||
| fn from(val: u64) -> Self { | ||
| unsafe { std::mem::transmute(val) } |
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 UB if someone sends an invalid opcode. Could you use try_from instead?
Something like...
impl TryFrom<u64> for OpCode {
type Error = anyhow::Error;
fn try_from(val: u64) -> Result<Self, Self::Error> {
match val {
0 => Ok(OpCode::Create),
...
_ => Err(anyhow!("invalid opcode: {}", val))
}
}
}You'll need to update BufferedOperation::from_buf and flush_change_buffer to handle the Result, but both of those functions return Results anyway, so that shouldn't be a problem.
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'd like to keep the transmute and just bounds-check it in the try_from. SGTY?
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.
Is there any reason to do bound-check + transmute rather than exhaust matching?
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.
The main reason is to avoid having all the OpCodes listed out in two places. Maybe I can macro that away? 🤔
| use crate::span::{Span, SpanText}; | ||
|
|
||
| #[derive(Clone, Copy)] | ||
| pub struct ChangeBuffer(*const u8); |
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.
Instead of a raw pointer, can the buffer be &mut [u8]? If we can do that, I think you can move the unsafe code to the FFI layer / caller where it better belongs. The caller would need to do something like
let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
let change_buffer = ChangeBuffer::new(slice); If we can do that, then we won't need the unsafe code in get_num_raw where every read has the potential for UB if we get the pointer math wrong. I think you'd also be able to get rid of the unsafe Send and Sync definitions.
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.
The problem is lifetimes. In napi.rs, the Buffer object we get from Node.js will have a lifetime as long as the function call. That said, the informal promise is that the Buffer will persist on the JS side forever, as good as 'static. Raw pointers allowed me to get around that.
I think I can take it a step further and convert that back to a slice with new ownership so that we can avoid all the unsafe. I'll give it a try.
| .insert(T::from_static_str("_dd.rule_psr"), rule); | ||
| } | ||
| if let Some(rule) = trace.sampling_limit_decision { | ||
| span.metrics.insert(T::from_static_str("_dd.;_psr"), rule); |
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.
Is _dd.;_psr correct?
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.
Correct.
| use crate::span::{Span, SpanText}; | ||
|
|
||
| #[derive(Clone, Copy)] | ||
| pub struct ChangeBuffer(*const u8); |
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.
If you get into trouble with lifetimes using a slice from the managed side maybe you can add an length field. That way you can enclose all the unsafe operations to just the constructor and a handful of private helpers to turn it into a slice. Something like:
pub struct ChangeBuffer {
ptr: *mut u8,
len: usize,
}
impl ChangeBuffer {
pub unsafe fn from_raw_parts(ptr: *const u8, len: usize) -> Self {
Self { ptr: ptr as *mut u8, len }
}
fn as_slice(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
}
fn as_mut_slice(&mut self) -> &mut [u8] {
unsafe { std::slice::from_raw_parts_mut(self.ptr, self.len) }
}
pub fn read_u64(&self, offset: usize) -> Result<u64> {
let slice = self.as_slice();
let bytes = slice.get(offset..offset + 8)
.context(format!("read_u64 out of bounds: offset={}, len={}", offset, self.len))?;
let array: [u8; 8] = bytes.try_into()
.map_err(|_| anyhow!("failed to convert slice to [u8; 8]"))?;
Ok(u64::from_le_bytes(array))
}
pub fn write_u64(&mut self, offset: usize, value: u64) -> Result<()> {
let slice = self.as_mut_slice();
let target = slice.get_mut(offset..offset + 8)
.context(format!("write_u64 out of bounds: offset={}, len={}", offset, self.len))?;
target.copy_from_slice(&value.to_le_bytes());
Ok(())
}
pub fn clear_count(&mut self) -> Result<()> {
self.write_u64(0, 0)
.context("failed to clear buffer count")
}
| #[derive(Clone, Copy)] | ||
| pub struct ChangeBuffer(*const u8); | ||
| unsafe impl Send for ChangeBuffer {} | ||
| unsafe impl Sync for ChangeBuffer {} |
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.
Do you really have to implement Sync? Is this required by NapiRS? In that case I think it will be good to document the constraints and point out that synchronization must be provided externally.
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.
IIRC napi.rs needed it, but for a reason that does not apply to our usage. I'll try again without.
|
|
||
| impl From<u64> for OpCode { | ||
| fn from(val: u64) -> Self { | ||
| unsafe { std::mem::transmute(val) } |
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.
Is there any reason to do bound-check + transmute rather than exhaust matching?
|
This is more of a general comment. Since trace-utils is too crowded and we talked about refactoring it. Would it make sense to place this implmentation in a new crate? I don't say that I has to happen now. Maybe in a later PR when we split trace-utils. |
| } | ||
|
|
||
| #[derive(Default)] | ||
| pub struct ChangeBufferState<T: SpanText + 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.
If having a non-cryptogrhapic hasher is an option you can use FxHashMap from rustc-hash that way you can squeeze some performance from the lookups.
| fn copy_in_chunk_tags(&self, span: &mut Span<T>) { | ||
| if let Some(trace) = self.traces.get(&span.trace_id) { | ||
| span.meta.reserve(trace.meta.len()); | ||
| span.meta.extend(trace.meta.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.
I belive that clone will create a temproray hashmap so in order to save that extra allocation it will probably better to iterate over the hashmap just cloning the elements rather than cloning the whole thing.
What does this PR do?
A brief description of the change being made with this pull request.
Motivation
What inspired you to submit this pull request?
Additional Notes
Anything else we should know when reviewing?
How to test the change?
Describe here in detail how the change can be validated.