Skip to content

Commit f646506

Browse files
authored
feat(profiling): Profile::{try_new2,try_add_sample2} (#1351)
feat(profiling): parallel set and string set test: exercise thread safety feat(profiling): add ProfilesDictionary Merge branch 'main' into levi/profiles_dictionary feat(profiling): Profile::try_add_sample2 test: new functions test: write the test in a more obvious way docs: improve ProfilesDictionary and related types Merge branch 'main' into levi/profiles_dictionary Merge branch 'levi/profiles_dictionary' into levi/try_add_sample2 refactor: try_new2 -> try_new_with_dictionary fix: bench too test: v1 and v2 have compat reprs refactor: move transmute into From Merge branch 'levi/profiles_dictionary' into levi/try_add_sample2 refactor: more cautious around transmutes docs: repr(C) Function/Mapping Merge branch 'main' into levi/profiles_dictionary Merge branch 'levi/profiles_dictionary' into levi/try_add_sample2 refactor: move transmute into From Merge branch 'levi/profiles_dictionary' into levi/try_add_sample2 refactor: expand unsafe and better document it docs: try_reserve_exact optimization for Box<[]> style: remove unused import Merge branch 'levi/profiles_dictionary' into levi/try_add_sample2 style: remove unused import refactor: impl Default for internal::Function bench: fix filename assignment Merge branch 'main' into levi/try_add_sample2 Merge branch 'main' into levi/try_add_sample2 Co-authored-by: levi.morrison <[email protected]>
1 parent 6b26318 commit f646506

File tree

15 files changed

+814
-14
lines changed

15 files changed

+814
-14
lines changed

libdd-profiling-ffi/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
#![cfg_attr(not(test), deny(clippy::todo))]
88
#![cfg_attr(not(test), deny(clippy::unimplemented))]
99

10-
#[cfg(all(feature = "symbolizer", not(target_os = "windows")))]
11-
pub use symbolizer_ffi::*;
12-
1310
mod exporter;
1411
mod profiles;
1512
mod string_storage;
1613

14+
#[cfg(all(feature = "symbolizer", not(target_os = "windows")))]
15+
pub use symbolizer_ffi::*;
16+
1717
// re-export crashtracker ffi
1818
#[cfg(feature = "crashtracker-ffi")]
1919
pub use libdd_crashtracker_ffi::*;

libdd-profiling-ffi/src/profiles/datatypes.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ use anyhow::Context;
66
use function_name::named;
77
use libdd_common_ffi::slice::{AsBytes, ByteSlice, CharSlice, Slice};
88
use libdd_common_ffi::{wrap_with_ffi_result, Error, Handle, Timespec, ToInner};
9-
use libdd_profiling::api;
10-
use libdd_profiling::api::ManagedStringId;
9+
use libdd_profiling::api::{self, ManagedStringId};
1110
use libdd_profiling::internal;
1211
use std::num::NonZeroI64;
1312
use std::str::Utf8Error;
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Copyright 2025-Present Datadog, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use criterion::*;
5+
use libdd_profiling::api2::Location2;
6+
use libdd_profiling::profiles::datatypes::{Function, FunctionId2, MappingId2};
7+
use libdd_profiling::{self as profiling, api, api2};
8+
9+
fn make_sample_types() -> Vec<api::ValueType<'static>> {
10+
vec![api::ValueType::new("samples", "count")]
11+
}
12+
13+
fn make_stack_api(frames: &[Frame]) -> (Vec<api::Location<'static>>, Vec<i64>) {
14+
// No mappings in Ruby, but the v1 API requires it.
15+
let mapping = api::Mapping::default();
16+
let mut locations = Vec::with_capacity(frames.len());
17+
for frame in frames {
18+
locations.push(api::Location {
19+
mapping,
20+
function: api::Function {
21+
name: frame.function_name,
22+
filename: frame.file_name,
23+
..Default::default()
24+
},
25+
line: frame.line_number as i64,
26+
..Default::default()
27+
});
28+
}
29+
let values = vec![1i64];
30+
(locations, values)
31+
}
32+
33+
fn make_stack_api2(frames: &[Frame2]) -> (Vec<Location2>, Vec<i64>) {
34+
let mut locations = Vec::with_capacity(frames.len());
35+
36+
for frame in frames {
37+
locations.push(Location2 {
38+
mapping: MappingId2::default(),
39+
function: frame.function,
40+
address: 0,
41+
line: frame.line_number as i64,
42+
});
43+
}
44+
45+
let values = vec![1i64];
46+
(locations, values)
47+
}
48+
49+
#[derive(Clone, Copy)]
50+
struct Frame {
51+
file_name: &'static str,
52+
line_number: u32,
53+
function_name: &'static str,
54+
}
55+
56+
impl Frame {
57+
pub const fn new(
58+
file_name: &'static str,
59+
line_number: u32,
60+
function_name: &'static str,
61+
) -> Self {
62+
Self {
63+
file_name,
64+
line_number,
65+
function_name,
66+
}
67+
}
68+
}
69+
70+
#[derive(Clone, Copy)]
71+
struct Frame2 {
72+
function: FunctionId2,
73+
line_number: u32,
74+
}
75+
76+
pub fn bench_add_sample_vs_add2(c: &mut Criterion) {
77+
let sample_types = make_sample_types();
78+
let dict = profiling::profiles::datatypes::ProfilesDictionary::try_new().unwrap();
79+
80+
// This is root-to-leaf, instead of leaf-to-root. We'll reverse it below.
81+
// Taken from a Ruby app, everything here is source-available.
82+
let mut frames = [
83+
Frame::new("/usr/local/bundle/gems/logging-2.4.0/lib/logging/diagnostic_context.rb", 474, "create_with_logging_context"),
84+
Frame::new("/usr/local/bundle/gems/puma-6.4.3/lib/puma/thread_pool.rb", 155, "spawn_thread"),
85+
Frame::new("/usr/local/bundle/gems/puma-6.4.3/lib/puma/server.rb", 245, "run"),
86+
Frame::new("/usr/local/bundle/gems/puma-6.4.3/lib/puma/server.rb", 464, "process_client"),
87+
Frame::new("/usr/local/bundle/gems/puma-6.4.3/lib/puma/request.rb", 99, "handle_request"),
88+
Frame::new("/usr/local/bundle/gems/puma-6.4.3/lib/puma/thread_pool.rb", 378, "with_force_shutdown"),
89+
Frame::new("/usr/local/bundle/gems/puma-6.4.3/lib/puma/request.rb", 100, "handle_request"),
90+
Frame::new("/usr/local/bundle/gems/puma-6.4.3/lib/puma/configuration.rb", 272, "call"),
91+
Frame::new("/usr/local/bundle/gems/railties-7.0.8.7/lib/rails/engine.rb", 530, "call"),
92+
Frame::new("/usr/local/bundle/gems/datadog-2.18.0/lib/datadog/tracing/contrib/rack/middlewares.rb", 474, "call"),
93+
Frame::new("/usr/local/bundle/gems/datadog-2.18.0/lib/datadog/tracing/contrib/rack/trace_proxy_middleware.rb", 17, "call"),
94+
Frame::new("/usr/local/bundle/gems/datadog-2.18.0/lib/datadog/tracing/contrib/rack/middlewares.rb", 70, "call"),
95+
Frame::new("/usr/local/bundle/gems/datadog-2.18.0/lib/datadog/appsec/contrib/rack/request_middleware.rb", 82, "call"),
96+
Frame::new("/usr/local/lib/libruby.so.3.3", 0, "catch"),
97+
Frame::new("/usr/local/bundle/gems/datadog-2.18.0/lib/datadog/appsec/contrib/rack/request_middleware.rb", 85, "catch"),
98+
Frame::new("/usr/local/bundle/gems/datadog-2.18.0/lib/datadog/appsec/instrumentation/gateway.rb", 41, "push"),
99+
Frame::new("/usr/local/bundle/gems/datadog-2.18.0/lib/datadog/appsec/instrumentation/gateway.rb", 37, "push"),
100+
Frame::new("/usr/local/bundle/gems/datadog-2.18.0/lib/datadog/appsec/instrumentation/gateway/middleware.rb", 18, "call"),
101+
];
102+
frames.reverse();
103+
104+
let strings = dict.strings();
105+
let functions = dict.functions();
106+
107+
let frames2 = frames.map(|f| {
108+
let set_id = functions
109+
.try_insert(Function {
110+
name: strings.try_insert(f.file_name).unwrap(),
111+
system_name: Default::default(),
112+
file_name: strings.try_insert(f.file_name).unwrap(),
113+
})
114+
.unwrap();
115+
Frame2 {
116+
function: FunctionId2::from(set_id),
117+
line_number: f.line_number,
118+
}
119+
});
120+
let dict = profiling::profiles::collections::Arc::try_new(dict).unwrap();
121+
122+
c.bench_function("profile_add_sample_frames_x1000", |b| {
123+
b.iter(|| {
124+
let mut profile = profiling::internal::Profile::try_new(&sample_types, None).unwrap();
125+
let (locations, values) = make_stack_api(frames.as_slice());
126+
for _ in 0..1000 {
127+
let sample = api::Sample {
128+
locations: locations.clone(),
129+
values: &values,
130+
labels: vec![],
131+
};
132+
black_box(profile.try_add_sample(sample, None)).unwrap();
133+
}
134+
black_box(profile.only_for_testing_num_aggregated_samples())
135+
})
136+
});
137+
138+
c.bench_function("profile_add_sample2_frames_x1000", |b| {
139+
b.iter(|| {
140+
let mut profile = profiling::internal::Profile::try_new_with_dictionary(
141+
&sample_types,
142+
None,
143+
dict.try_clone().unwrap(),
144+
)
145+
.unwrap();
146+
let (locations, values) = make_stack_api2(frames2.as_slice());
147+
for _ in 0..1000 {
148+
// Provide an empty iterator for labels conversion path
149+
let labels_iter = std::iter::empty::<anyhow::Result<api2::Label>>();
150+
// SAFETY: all ids come from the profile's dictionary.
151+
black_box(unsafe {
152+
profile.try_add_sample2(&locations, &values, labels_iter, None)
153+
})
154+
.unwrap();
155+
}
156+
black_box(profile.only_for_testing_num_aggregated_samples())
157+
})
158+
});
159+
}
160+
161+
criterion_group!(benches, bench_add_sample_vs_add2);

libdd-profiling/benches/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
use criterion::criterion_main;
55

6+
mod add_samples;
67
mod interning_strings;
78

8-
criterion_main!(interning_strings::benches);
9+
criterion_main!(interning_strings::benches, add_samples::benches);

libdd-profiling/src/api2.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use crate::profiles::datatypes::{FunctionId2, MappingId2, StringId2};
5+
6+
#[derive(Copy, Clone, Debug, Default)]
7+
#[repr(C)]
8+
pub struct Location2 {
9+
pub mapping: MappingId2,
10+
pub function: FunctionId2,
11+
12+
/// The instruction address for this location, if available. It
13+
/// should be within [Mapping.memory_start...Mapping.memory_limit]
14+
/// for the corresponding mapping. A non-leaf address may be in the
15+
/// middle of a call instruction. It is up to display tools to find
16+
/// the beginning of the instruction if necessary.
17+
pub address: u64,
18+
pub line: i64,
19+
}
20+
21+
#[derive(Copy, Clone, Debug, Default)]
22+
pub struct Label<'a> {
23+
pub key: StringId2,
24+
25+
/// At most one of `.str` and `.num` should not be empty.
26+
pub str: &'a str,
27+
pub num: i64,
28+
29+
/// Should only be present when num is present.
30+
/// Specifies the units of num.
31+
/// Use arbitrary string (for example, "requests") as a custom count unit.
32+
/// If no unit is specified, consumer may apply heuristic to deduce the unit.
33+
/// Consumers may also interpret units like "bytes" and "kilobytes" as memory
34+
/// units and units like "seconds" and "nanoseconds" as time units,
35+
/// and apply appropriate unit conversions to these.
36+
pub num_unit: &'a str,
37+
}
38+
39+
impl<'a> Label<'a> {
40+
pub const fn str(key: StringId2, str: &'a str) -> Label<'a> {
41+
Label {
42+
key,
43+
str,
44+
num: 0,
45+
num_unit: "",
46+
}
47+
}
48+
49+
pub const fn num(key: StringId2, num: i64, num_unit: &'a str) -> Label<'a> {
50+
Label {
51+
key,
52+
str: "",
53+
num,
54+
num_unit,
55+
}
56+
}
57+
}

libdd-profiling/src/internal/function.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use super::*;
66
/// Represents a [pprof::Function] with some space-saving changes:
77
/// - The id is not stored on the struct. It's stored in the container that holds the struct.
88
/// - ids for linked objects use 32-bit numbers instead of 64 bit ones.
9-
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
9+
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash, PartialOrd, Ord)]
1010
pub struct Function {
1111
pub name: StringId,
1212
pub system_name: StringId,

libdd-profiling/src/internal/owned_types/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ pub struct Period {
3333
pub value: i64,
3434
}
3535

36+
impl<'a> From<api::Period<'a>> for Period {
37+
#[inline]
38+
fn from(period: api::Period<'a>) -> Self {
39+
Period::from(&period)
40+
}
41+
}
42+
3643
impl<'a> From<&'a api::Period<'a>> for Period {
3744
#[inline]
3845
fn from(period: &'a api::Period<'a>) -> Self {

0 commit comments

Comments
 (0)