Conversation
BenchmarksComparisonBenchmark execution time: 2026-02-10 20:31:01 Comparing candidate commit a472e65 in PR branch Found 10 performance improvements and 5 performance regressions! Performance is the same for 42 metrics, 2 unstable metrics. scenario:benching deserializing traces from msgpack to their internal representation
scenario:credit_card/is_card_number/ 378282246310005
scenario:credit_card/is_card_number/378282246310005
scenario:credit_card/is_card_number/x371413321323331
scenario:credit_card/is_card_number_no_luhn/x371413321323331
scenario:normalization/normalize_name/normalize_name/Too-Long-.Too-Long-.Too-Long-.Too-Long-.Too-Long-.Too-Lo...
scenario:normalization/normalize_name/normalize_name/bad-name
scenario:normalization/normalize_service/normalize_service/A0000000000000000000000000000000000000000000000000...
scenario:normalization/normalize_service/normalize_service/Test Conversion 0f Weird !@#$%^&**() Characters
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-unknown-linux-gnu
libdatadog-x64-windows
libdatadog-x86-windows
x86_64-alpine-linux-musl
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.
Not in trace exporter's config 🤷
There was a problem hiding this comment.
Is this what you're referring to? Because we send it as a metric, which has to be f64?
Would it be better to do the conversion to f64 internally and not expose that implementation detail in the public API? Going from u32 to f64 should be cheap in Rust.
This is non-blocking. It's not a hill I need to die on.
|
|
||
| impl From<u64> for OpCode { | ||
| fn from(val: u64) -> Self { | ||
| unsafe { std::mem::transmute(val) } |
There was a problem hiding this comment.
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.
I'd like to keep the transmute and just bounds-check it in the try_from. SGTY?
There was a problem hiding this comment.
Is there any reason to do bound-check + transmute rather than exhaust matching?
There was a problem hiding this comment.
The main reason is to avoid having all the OpCodes listed out in two places. Maybe I can macro that away? 🤔
There was a problem hiding this comment.
Maybe using a crate like num_enum or num_traits?
| use crate::span::{Span, SpanText}; | ||
|
|
||
| #[derive(Clone, Copy)] | ||
| pub struct ChangeBuffer(*const u8); |
There was a problem hiding this comment.
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.
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); |
|
|
||
| impl From<u64> for OpCode { | ||
| fn from(val: u64) -> Self { | ||
| unsafe { std::mem::transmute(val) } |
There was a problem hiding this comment.
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. |
📚 Documentation Check Results📦
|
🔒 Cargo Deny Results📦
|
5d9d55b to
210da31
Compare
| @@ -0,0 +1,397 @@ | |||
| use std::collections::HashMap; | |||
|
|
|||
| use anyhow::{anyhow, bail, Result}; | |||
There was a problem hiding this comment.
Personal opinion about anyhow. I think it's good for either prototyping things or using it in standalone binaries. On the other hand using it in libraries has its downsides, the main one being bubbling errors up to the caller, hence difficulting the SDKs consuming our libraries to do proper error handling. That said, I think that is not a problem right now and we can talk about it in subsequent PRs.
There was a problem hiding this comment.
non-blocking: +1. I don't think anyhow should be used in the public API for a library. But I'm ok with a TODO comment and taking care of it in the future.
|
|
||
| impl From<u64> for OpCode { | ||
| fn from(val: u64) -> Self { | ||
| unsafe { std::mem::transmute(val) } |
There was a problem hiding this comment.
Maybe using a crate like num_enum or num_traits?
| let slice = self.as_slice(); | ||
| let bytes = slice.get(*index..*index + size) | ||
| .ok_or(anyhow!("read_u64 out of bounds: offset={}, len={}", *index, self.len))?; | ||
| let array: [u8; 8] = bytes.try_into() |
There was a problem hiding this comment.
This method is generic over T. What happens if T isn't 8 bytes, like u32 or u128?
Do you need to convert bytes into array here? I don't think we lose any error fidelity because I don't think it's possible for try_into to fail here? We have the size, and slice.get() uses size to get exactly the correct bytes.
You could just pass bytes directly with: Ok(T::from_bytes(bytes)).
| pub fn flush_change_buffer(&mut self) -> Result<()> { | ||
| let mut index = 0; | ||
| let mut count = self.change_buffer.read::<u64>(&mut index)?; | ||
| index += 8; |
There was a problem hiding this comment.
I might be misunderstanding, but doesn't the index get incremented in read? Won't this double increment the index?
Assuming I'm setting it up correctly this test would expose the double increment and the size issue in Read.
#[cfg(test)]
mod tests {
use super::*;
use libdd_tinybytes::BytesString;
#[test]
fn test_flush_change_buffer_single_create() {
// Buffer: [count=1, opcode=Create(0), span_id, trace_id, parent_id]
let mut buf = vec![];
buf.extend_from_slice(&1u64.to_le_bytes()); // count = 1
buf.extend_from_slice(&0u64.to_le_bytes()); // opcode = Create
buf.extend_from_slice(&123u64.to_le_bytes()); // span_id
buf.extend_from_slice(&456u128.to_le_bytes()); // trace_id
buf.extend_from_slice(&0u64.to_le_bytes()); // parent_id
let change_buffer =
unsafe { ChangeBuffer::from_raw_parts(buf.as_ptr(), buf.len()) };
let mut state = ChangeBufferState::<BytesString>::new(
change_buffer,
BytesString::from_static("service"),
BytesString::from_static("rust"),
1234.0,
);
state.flush_change_buffer().unwrap();
let span = state.get_span(&123).unwrap();
assert_eq!(span.span_id, 123);
assert_eq!(span.trace_id, 456);
}
}Also, I just realized the change buffer has no unit tests.
|
|
||
| pub mod span; | ||
|
|
||
| pub mod change_buffer; |
There was a problem hiding this comment.
non-blocking: Should we put the change_buffer behind an optional feature for now while it's still experimental?
Clippy Allow Annotation ReportComparing clippy allow annotations between branches:
Summary by Rule
Annotation Counts by File
Annotation Stats by Crate
About This ReportThis report tracks Clippy allow annotations for specific rules, showing how they've changed in this PR. Decreasing the number of these annotations generally improves code quality. |
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.