Skip to content

Commit 3e379a7

Browse files
authored
Merge branch 'main' into jb/vt_churn
2 parents 0bfeed0 + 4011bf7 commit 3e379a7

18 files changed

Lines changed: 1910 additions & 343 deletions

File tree

.gitlab/reliability/.gitlab-ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ variables:
4141
- out.log
4242
reports:
4343
dotenv: build.env
44-
expire_in: 1 day
44+
expire_in: 7 days
4545

4646
reliability-amd64:
4747
extends: .reliability_job
@@ -99,7 +99,7 @@ reliability-aarch64:
9999
- out.log
100100
reports:
101101
dotenv: build.env
102-
expire_in: 1 day
102+
expire_in: 7 days
103103

104104
reliability-chaos-amd64:
105105
extends: .reliability_chaos_job

AGENTS.md

Lines changed: 45 additions & 336 deletions
Large diffs are not rendered by default.

build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ object ConfigurationPresets {
5151
configureTsan(this, currentPlatform, currentArch, version, rootDir, compiler)
5252
}
5353
register("fuzzer") {
54-
configureFuzzer(this, currentPlatform, currentArch, version, rootDir)
54+
configureFuzzer(this, currentPlatform, currentArch, version, rootDir, compiler)
5555
}
5656
}
5757

@@ -313,7 +313,8 @@ object ConfigurationPresets {
313313
platform: Platform,
314314
architecture: Architecture,
315315
version: String,
316-
rootDir: File
316+
rootDir: File,
317+
compiler: String = "gcc"
317318
) {
318319
config.platform.set(platform)
319320
config.architecture.set(architecture)
@@ -339,7 +340,28 @@ object ConfigurationPresets {
339340
when (platform) {
340341
Platform.LINUX -> {
341342
config.compilerArgs.set(fuzzerCompilerArgs + commonLinuxCompilerArgs(version))
342-
config.linkerArgs.set(commonLinuxLinkerArgs() + fuzzerLinkerArgs)
343+
344+
// commonLinuxLinkerArgs() carries -Wl,-z,defs, which forbids the
345+
// undefined __asan_*/__ubsan_* symbols the instrumented objects
346+
// reference. -fsanitize=address only links the runtime into
347+
// executables, not shared libraries, so the fuzzer .so needs the
348+
// runtime linked explicitly — same treatment (and rationale) as the
349+
// asan config. On clang this resolves both __asan_* and __ubsan_*
350+
// from one clang_rt.asan runtime; on gcc it adds -lubsan.
351+
val libasan = PlatformUtils.locateLibasan(compiler)
352+
val fuzzerRuntimeArgs = if (libasan != null) {
353+
val asanLibDir = File(libasan).parent
354+
val asanLibName = File(libasan).nameWithoutExtension.removePrefix("lib")
355+
val ubsanLibs = if (asanLibName.startsWith("clang_rt")) emptyList()
356+
else listOf("-lubsan")
357+
listOf("-L$asanLibDir", "-l$asanLibName",
358+
"-Wl,-rpath,$asanLibDir") +
359+
ubsanLibs +
360+
fuzzerLinkerArgs
361+
} else {
362+
fuzzerLinkerArgs
363+
}
364+
config.linkerArgs.set(commonLinuxLinkerArgs() + fuzzerRuntimeArgs)
343365

344366
config.testEnvironment.apply {
345367
put("ASAN_OPTIONS", "allocator_may_return_null=1:detect_stack_use_after_return=0:handle_segv=0:abort_on_error=1:symbolize=1:suppressions=$rootDir/gradle/sanitizers/asan.supp")

ddprof-lib/src/main/cpp/javaApi.cpp

Lines changed: 311 additions & 0 deletions
Large diffs are not rendered by default.

ddprof-lib/src/main/cpp/profiler.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,6 +1302,9 @@ Error Profiler::start(Arguments &args, bool reset) {
13021302
}
13031303
_string_label_map.clearAll();
13041304
_context_value_map.clearAll();
1305+
// Signal the Java layer that context-value encodings have been reassigned so it can drop its
1306+
// process-wide ContextValueCache (consumed in JavaProfiler.execute after this start returns).
1307+
_context_value_dict_reset.store(true, std::memory_order_release);
13051308

13061309
// Reset call trace storage
13071310
if (!_omit_stacktraces) {

ddprof-lib/src/main/cpp/profiler.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ class alignas(alignof(SpinLock)) Profiler {
8585
StringDictionary _class_map{1};
8686
StringDictionary _string_label_map{2};
8787
StringDictionary _context_value_map{3};
88+
// Set when a fresh start resets _context_value_map (clearAll), reassigning encodings. Consumed by
89+
// the Java layer to drop its process-wide ContextValueCache so no stale encoding is reused. See
90+
// JavaProfiler.execute / ContextValueCache.
91+
std::atomic<bool> _context_value_dict_reset{false};
8892
ThreadFilter _thread_filter;
8993
CallTraceStorage _call_trace_storage;
9094
FlightRecorder _jfr;
@@ -261,6 +265,10 @@ class alignas(alignof(SpinLock)) Profiler {
261265
SharedLockGuard classMapSharedGuard() { return SharedLockGuard(&_class_map_lock); }
262266
StringDictionary *stringLabelMap() { return &_string_label_map; }
263267
StringDictionary *contextValueMap() { return &_context_value_map; }
268+
// Atomically reads and clears the "context value dictionary was reset" flag (see the member).
269+
bool consumeContextValueDictReset() {
270+
return _context_value_dict_reset.exchange(false, std::memory_order_acq_rel);
271+
}
264272
u32 numContextAttributes() { return _num_context_attributes; }
265273
ThreadFilter *threadFilter() { return &_thread_filter; }
266274

ddprof-lib/src/main/java/com/datadoghq/profiler/ContextSetter.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@
2121
import java.util.List;
2222
import java.util.Set;
2323

24+
/**
25+
* DirectByteBuffer context wrapper.
26+
*
27+
* @deprecated Superseded by {@link JavaProfiler#setTraceContext} /
28+
* {@link JavaProfiler#setContextValue} (all-native). Removed in phase 3.
29+
*/
30+
@Deprecated
2431
public class ContextSetter {
2532

2633
private final List<String> attributes;
@@ -62,6 +69,11 @@ public int offsetOf(String attribute) {
6269
return attributes.indexOf(attribute);
6370
}
6471

72+
/** Number of (deduplicated, truncated) context attribute slots. Pure Java; no native/DBB read. */
73+
public int size() {
74+
return attributes.size();
75+
}
76+
6577
public boolean setContextValue(String attribute, String value) {
6678
return setContextValue(offsetOf(attribute), value);
6779
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright 2026, Datadog, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.datadoghq.profiler;
17+
18+
import java.nio.charset.StandardCharsets;
19+
import java.util.concurrent.atomic.AtomicReferenceArray;
20+
21+
/**
22+
* Process-wide cache resolving an attribute value to its {@code (encoding, utf8)} pair for the
23+
* all-native context write path, avoiding a JNI {@code registerConstant0} call on every write.
24+
*
25+
* <p>Within a single recording session the encoding is a <em>stable</em> ID: {@code registerConstant0}
26+
* interns the value in the native Dictionary and returns the same ID for the life of that session.
27+
* That stability is what makes a single shared, lock-free cache correct without per-thread copies:
28+
* <ul>
29+
* <li>Entries are immutable and published atomically (one array slot store), so a concurrent
30+
* reader never sees a torn {@code (key, encoding, utf8)}.</li>
31+
* <li>A miss just calls {@code registerConstant0} again — idempotent, returns the same encoding —
32+
* and re-stores an equivalent entry; racing writers converge (last write wins, all correct).</li>
33+
* <li>Direct-mapped by {@code value.hashCode()}; a hash collision evicts the previous value, which
34+
* is simply re-resolved on its next use.</li>
35+
* </ul>
36+
*
37+
* <p><b>Session boundary:</b> the encoding is stable only <em>within</em> a recording session. A
38+
* fresh {@code start} (native {@code Profiler::start} with reset) calls
39+
* {@code StringDictionary::clearAll()}, which resets the ID counter, so encodings from the previous
40+
* session become stale. {@link JavaProfiler} therefore calls {@link #clear()} on every {@code start}
41+
* command; a subsequent {@link #resolve} re-registers the value and re-caches its new encoding.
42+
*
43+
* <p>Replaces the per-{@link ThreadContext} value cache: in the all-native model there is no
44+
* per-thread {@code ThreadContext} instance to host it, and a global cache avoids duplicating the
45+
* table across every carrier / virtual thread.
46+
*/
47+
final class ContextValueCache {
48+
49+
/** Max UTF-8 byte length for a value (the OTEP attrs_data entry length field is one byte). */
50+
static final int MAX_VALUE_BYTES = 255;
51+
52+
private static final int SIZE = 256;
53+
private static final int MASK = SIZE - 1;
54+
55+
/** Immutable resolved value; published as a single atomic array-slot store. */
56+
static final class Entry {
57+
final String key;
58+
final int encoding;
59+
final byte[] utf8;
60+
61+
Entry(String key, int encoding, byte[] utf8) {
62+
this.key = key;
63+
this.encoding = encoding;
64+
this.utf8 = utf8;
65+
}
66+
}
67+
68+
private final AtomicReferenceArray<Entry> table = new AtomicReferenceArray<>(SIZE);
69+
70+
/**
71+
* Resolves {@code value} to its cached {@code (encoding, utf8)} pair, registering it on a miss.
72+
*
73+
* @return the entry, or {@code null} if the value cannot be represented — {@code null} input, a
74+
* UTF-8 encoding longer than {@value #MAX_VALUE_BYTES} bytes, or a full native Dictionary
75+
* (encoding {@code < 0}). Callers treat {@code null} as "no attribute" (skip / clear).
76+
*/
77+
Entry resolve(String value) {
78+
if (value == null) {
79+
return null;
80+
}
81+
int slot = value.hashCode() & MASK;
82+
Entry e = table.get(slot);
83+
if (e != null && value.equals(e.key)) {
84+
return e; // hit — no JNI
85+
}
86+
// Miss: encode + validate size before touching the Dictionary (a rejected value must not
87+
// create an orphan Dictionary entry).
88+
byte[] utf8 = value.getBytes(StandardCharsets.UTF_8);
89+
if (utf8.length > MAX_VALUE_BYTES) {
90+
return null;
91+
}
92+
int encoding = ThreadContext.registerConstant0(value);
93+
if (encoding < 0) {
94+
return null; // Dictionary full
95+
}
96+
Entry ne = new Entry(value, encoding, utf8);
97+
table.set(slot, ne); // benign race: converges on an equivalent entry
98+
return ne;
99+
}
100+
101+
/**
102+
* Drops all cached entries. Called when a fresh recording session starts and the native
103+
* Dictionary is reset, so stale encodings from the previous session are not reused. A concurrent
104+
* {@link #resolve} racing this simply observes a miss and re-registers the value against the new
105+
* Dictionary — correct, just an extra {@code registerConstant0} call.
106+
*/
107+
void clear() {
108+
for (int i = 0; i < SIZE; i++) {
109+
table.set(i, null);
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)