Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
331d45a
feat(profiling): Add OTLP profiles core infrastructure
jbachorik Dec 1, 2025
e67ea79
feat(profiling): Initial implementation of JFR->OTLP/P format
jbachorik Dec 2, 2025
1e8790b
feat(profiling): Add benchmarks for OTLP/P converter
jbachorik Dec 2, 2025
6e7eeb7
feat(profiling): Add OLTP/P format validation tests
jbachorik Dec 3, 2025
4f842e4
feat(profiling): Add JMH filtering and update benchmarks with results
jbachorik Dec 3, 2025
1d9066c
chore(profiling): revert dictionary optimization and add profiling su…
jbachorik Dec 4, 2025
7c50808
feat(profiling): Add stack trace caching optimization using JFR const…
jbachorik Dec 4, 2025
6aea20f
fix(profiling): Skip Docker-dependent validation tests gracefully whe…
jbachorik Dec 4, 2025
01b092a
feat(profiling): Add original_payload support to OTLP profiles converter
jbachorik Dec 4, 2025
a089a91
feat(profiling): Add RecordingData reference counting and OTLP config…
jbachorik Dec 4, 2025
9876e2c
feat(profiling): Implement OtlpProfileUploader for OTLP format uploads
jbachorik Dec 4, 2025
1afccd0
feat(profiling): Integrate OtlpProfileUploader with explicit referenc…
jbachorik Dec 4, 2025
2d2b208
feat(profiling): Add profiling-otel to agent-profiling shadowJar
jbachorik Dec 4, 2025
09af640
feat(profiling): Add CLI tool for converting JFR to OTLP format
jbachorik Dec 5, 2025
62a5452
feat(profiling): Add optional pretty-printing for JSON output
jbachorik Dec 5, 2025
28cd416
feat(profiling): Add profcheck integration for OTLP profile validation
jbachorik Dec 5, 2025
1e474de
fix(profiling): Fix OTLP dictionary index 0 sentinel encoding
jbachorik Dec 5, 2025
214a75e
feat(profiling): Add sample attributes support to OTLP profiles conve…
jbachorik Dec 5, 2025
d4f6085
fix(profiling): Fix timestamp validation and make profcheck mandatory
jbachorik Dec 5, 2025
8dd504f
feat(profiling): Add convenience script for JFR to OTLP conversion
jbachorik Dec 8, 2025
a661ff0
feat(profiling): Add diagnostics mode to convert-jfr.sh script
jbachorik Dec 8, 2025
0e06b50
feat(profiling): Add JFR to OTLP conversion convenience script with d…
jbachorik Dec 8, 2025
b2c0f4a
feat(profiling): Convert JFR converter script to use fat jar for 31x …
jbachorik Dec 8, 2025
8a9a667
refactor(profiling): Consolidate converter script output for clarity
jbachorik Dec 8, 2025
c835f93
Properly handle `objectClass` attribute
jbachorik Dec 8, 2025
340ae63
Adapt to master: fix API breaks from rebase
jbachorik Apr 22, 2026
ddb7035
fix(profiling): Address review findings in OTLP pipeline
jbachorik Apr 22, 2026
8ded4f2
build: bump jafar to 0.21.1
jbachorik Apr 22, 2026
633db7c
fix(profiling): fix StackTraceConversionBenchmark broken reflection
jbachorik Apr 22, 2026
b61f085
perf(profiling): optimize OTLP converter hot paths
jbachorik Apr 22, 2026
60049bd
build: bump jafar to 0.21.3
jbachorik Apr 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dd-java-agent/agent-profiling/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies {

api project(':dd-java-agent:agent-profiling:profiling-ddprof')
api project(':dd-java-agent:agent-profiling:profiling-uploader')
api project(':dd-java-agent:agent-profiling:profiling-otel')
api project(':dd-java-agent:agent-profiling:profiling-controller')
implementation project(':dd-java-agent:agent-profiling:profiling-scrubber')
api project(':dd-java-agent:agent-profiling:profiling-controller-jfr')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public RecordingInputStream getStream() throws IOException {
}

@Override
public void release() {
protected void doRelease() {
recording.close();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public RecordingInputStream getStream() throws IOException {
}

@Override
public void release() {
protected void doRelease() {
// noop
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/*
* Copyright 2025 Datadog
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.datadog.profiling.controller;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import datadog.trace.api.profiling.ProfilingSnapshot;
import datadog.trace.api.profiling.RecordingData;
import datadog.trace.api.profiling.RecordingInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Instant;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.junit.jupiter.api.Test;

/** Tests for RecordingData reference counting with multiple handlers. */
public class RecordingDataRefCountingTest {

/** Test RecordingData implementation that tracks release calls. */
private static class TestRecordingData extends RecordingData {
private final AtomicInteger releaseCount = new AtomicInteger(0);
private final CountDownLatch releaseLatch = new CountDownLatch(1);

public TestRecordingData() {
super(Instant.now(), Instant.now(), ProfilingSnapshot.Kind.PERIODIC);
}

@Nonnull
@Override
public RecordingInputStream getStream() throws IOException {
return new RecordingInputStream(new ByteArrayInputStream(new byte[0]));
}

@Override
protected void doRelease() {
releaseCount.incrementAndGet();
releaseLatch.countDown();
}

@Nullable
@Override
public Path getFile() {
return null;
}

@Override
public String getName() {
return "test-recording";
}

public int getReleaseCount() {
return releaseCount.get();
}

public boolean awaitRelease(long timeout, TimeUnit unit) throws InterruptedException {
return releaseLatch.await(timeout, unit);
}
}

@Test
public void testSingleHandler() throws InterruptedException {
TestRecordingData data = new TestRecordingData();

// Single handler: retain once, release once
data.retain();
assertEquals(0, data.getReleaseCount(), "Should not be released yet");

data.release();

assertTrue(data.awaitRelease(1, TimeUnit.SECONDS), "Release should be called");
assertEquals(1, data.getReleaseCount(), "doRelease() should be called exactly once");
}

@Test
public void testTwoHandlers() throws InterruptedException {
TestRecordingData data = new TestRecordingData();

// Two handlers (e.g., JFR + OTLP): retain twice
data.retain(); // Handler 1
data.retain(); // Handler 2
assertEquals(0, data.getReleaseCount(), "Should not be released yet");

// First handler releases
data.release();
assertEquals(0, data.getReleaseCount(), "Should not be released after first release");

// Second handler releases
data.release();

assertTrue(data.awaitRelease(1, TimeUnit.SECONDS), "Release should be called");
assertEquals(1, data.getReleaseCount(), "doRelease() should be called exactly once");
}

@Test
public void testThreeHandlers() throws InterruptedException {
TestRecordingData data = new TestRecordingData();

// Three handlers (e.g., dumper + JFR + OTLP): retain three times
data.retain(); // Handler 1
data.retain(); // Handler 2
data.retain(); // Handler 3
assertEquals(0, data.getReleaseCount(), "Should not be released yet");

// First two handlers release
data.release();
data.release();
assertEquals(0, data.getReleaseCount(), "Should not be released after two releases");

// Third handler releases
data.release();

assertTrue(data.awaitRelease(1, TimeUnit.SECONDS), "Release should be called");
assertEquals(1, data.getReleaseCount(), "doRelease() should be called exactly once");
}

@Test
public void testReleaseBeforeRetain() {
TestRecordingData data = new TestRecordingData();

// Cannot release before any retain
assertThrows(
IllegalStateException.class, data::release, "Should throw when releasing with refcount=0");
}

@Test
public void testRetainAfterFullRelease() throws InterruptedException {
TestRecordingData data = new TestRecordingData();

data.retain();
data.release();
assertTrue(data.awaitRelease(1, TimeUnit.SECONDS), "Release should be called");

// Cannot retain after full release
assertThrows(
IllegalStateException.class, data::retain, "Should throw when retaining after release");
}

@Test
public void testMultipleReleaseIdempotent() throws InterruptedException {
TestRecordingData data = new TestRecordingData();

data.retain();
data.release();
assertTrue(data.awaitRelease(1, TimeUnit.SECONDS), "Release should be called");

// Additional release calls should be no-op
data.release();
data.release();

assertEquals(1, data.getReleaseCount(), "doRelease() should still be called exactly once");
}

@Test
public void testConcurrentHandlers() throws InterruptedException {
TestRecordingData data = new TestRecordingData();
int numHandlers = 10;

// Retain for all handlers
for (int i = 0; i < numHandlers; i++) {
data.retain();
}

// Simulate concurrent release from multiple threads
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch doneLatch = new CountDownLatch(numHandlers);

for (int i = 0; i < numHandlers; i++) {
new Thread(
() -> {
try {
startLatch.await();
data.release();
doneLatch.countDown();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
})
.start();
}

// Start all threads
startLatch.countDown();

// Wait for all threads to complete
assertTrue(doneLatch.await(5, TimeUnit.SECONDS), "All threads should complete");
assertTrue(data.awaitRelease(1, TimeUnit.SECONDS), "Release should be called");
assertEquals(1, data.getReleaseCount(), "doRelease() should be called exactly once");
}

@Test
public void testRetainChaining() {
TestRecordingData data = new TestRecordingData();

// retain() should return this for chaining
RecordingData result = data.retain();
assertEquals(data, result, "retain() should return the same instance");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ public void setSpanContext(long rootSpanId, long spanId, long traceIdHigh, long
try {
profiler.setContext(rootSpanId, spanId, traceIdHigh, traceIdLow);
} catch (Throwable e) {
log.debug("Failed to clear context", e);
log.debug("Failed to set context", e);
}
}

Expand All @@ -367,7 +367,7 @@ public void clearSpanContext() {
try {
profiler.setContext(0L, 0L, 0L, 0L);
} catch (Throwable e) {
log.debug("Failed to set context", e);
log.debug("Failed to clear context", e);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public RecordingInputStream getStream() throws IOException {
}

@Override
public void release() {
protected void doRelease() {
try {
Files.deleteIfExists(recordingFile);
} catch (IOException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public void clearContextValue(ProfilingContextAttribute attribute) {

@Override
public void close() {
// ddprof 1.41.0 removed the int-encoding setter; snapshot/restore of tag
// context across nested scopes is no longer supported by the library.
// ddprof 1.41.0 removed the int-encoding setter; snapshot/restore is no longer possible.
// Context values written via setContextValue() inside this scope persist after close();
// nested scope isolation is not maintained.
}
}
Loading
Loading