diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index b47b0d81717..1514c649177 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -606,8 +606,9 @@ private static void startProfilingAgent(final URL bootstrapURL, final boolean is Thread.currentThread().setContextClassLoader(classLoader); final Class profilingAgentClass = classLoader.loadClass("com.datadog.profiling.agent.ProfilingAgent"); - final Method profilingInstallerMethod = profilingAgentClass.getMethod("run", Boolean.TYPE); - profilingInstallerMethod.invoke(null, isStartingFirst); + final Method profilingInstallerMethod = + profilingAgentClass.getMethod("run", Boolean.TYPE, ClassLoader.class); + profilingInstallerMethod.invoke(null, isStartingFirst, AGENT_CLASSLOADER); /* * Install the tracer hooks only when not using 'early start'. * The 'early start' is happening so early that most of the infrastructure has not been set up yet. diff --git a/dd-java-agent/agent-profiling/agent-profiling.gradle b/dd-java-agent/agent-profiling/agent-profiling.gradle index 4ed4f5ec6d3..11035867722 100644 --- a/dd-java-agent/agent-profiling/agent-profiling.gradle +++ b/dd-java-agent/agent-profiling/agent-profiling.gradle @@ -18,6 +18,7 @@ dependencies { api project(':dd-java-agent:agent-profiling:profiling-auxiliary') implementation project(path: ':dd-java-agent:agent-profiling:profiling-auxiliary-async', configuration: 'shadow') + api project(':dd-java-agent:agent-profiling:profiling-context') api project(':dd-java-agent:agent-profiling:profiling-uploader') api project(':dd-java-agent:agent-profiling:profiling-controller') api project(':dd-java-agent:agent-profiling:profiling-controller-openjdk') diff --git a/dd-java-agent/agent-profiling/profiling-auxiliary-async/profiling-auxiliary-async.gradle b/dd-java-agent/agent-profiling/profiling-auxiliary-async/profiling-auxiliary-async.gradle index 2fbc5538607..cf97bf2abfc 100644 --- a/dd-java-agent/agent-profiling/profiling-auxiliary-async/profiling-auxiliary-async.gradle +++ b/dd-java-agent/agent-profiling/profiling-auxiliary-async/profiling-auxiliary-async.gradle @@ -19,7 +19,9 @@ excludedClassesCoverage += [ 'com.datadog.profiling.auxiliary.async.AsyncProfilerRecording', 'com.datadog.profiling.auxiliary.async.AsyncProfilerRecordingData', // the config event is a simple data holder - 'com.datadog.profiling.auxiliary.async.AsyncProfilerConfigEvent' + 'com.datadog.profiling.auxiliary.async.AsyncProfilerConfigEvent', + // simple forwarder class to the native implementation + "com.datadog.profiling.auxiliary.async.ContextIntervalEventsWriter", ] dependencies { diff --git a/dd-java-agent/agent-profiling/profiling-context/profiling-context.gradle b/dd-java-agent/agent-profiling/profiling-context/profiling-context.gradle new file mode 100644 index 00000000000..ab6bdaf7d14 --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/profiling-context.gradle @@ -0,0 +1,54 @@ +apply from: "$rootDir/gradle/java.gradle" +apply plugin: 'idea' + +ext { + minimumBranchCoverage = 0.6 + minimumInstructionCoverage = 0.8 + excludedClassesCoverage = [ + // simple DTO + "com.datadog.profiling.context.PositionDecoder.*", + "com.datadog.profiling.context.JfrTimestampPatch", + // jacoco does not allow per-method excludes so, here we go + "com.datadog.profiling.context.ProfilerTracingContextTrackerFactory*", + "com.datadog.profiling.context.ProfilerTracingContextTracker.TimeTicksProvider*", + ] +} + +sourceSets { + "main_java11" { + java.srcDirs "${project.projectDir}/src/main/java11" + } +} + +dependencies { + api project(':internal-api') + + implementation deps.slf4j + main_java11CompileOnly deps.slf4j + implementation sourceSets.main_java11.output + + testImplementation deps.junit5 + testImplementation deps.mockito +} + +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 + +compileMain_java11Java.doFirst { + setJavaVersion(it, 11) + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +jar { + from sourceSets.main_java11.output +} +forbiddenApisMain_java11 { + failOnMissingClasses = false +} + +idea { + module { + jdkName = '11' + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/Allocator.java b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/Allocator.java new file mode 100644 index 00000000000..bf938c45db1 --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/Allocator.java @@ -0,0 +1,29 @@ +package com.datadog.profiling.context; + +import com.datadog.profiling.context.allocator.AllocatedBuffer; + +/** A simple allocator definition */ +public interface Allocator { + /** + * The size of the chunk the allocator opearates with + * + * @return size of the chunk the allocator opearates with + */ + int getChunkSize(); + + /** + * Allocate the {@code size} number of bytes aligned to the chunk size + * + * @param size the number of bytes to allocate + * @return the allocated byffer or {@literal null} when allocation fails + */ + AllocatedBuffer allocate(int size); + + /** + * Allocate the {@code chunks} number of chunks + * + * @param chunks the number of chunks to allocate + * @return the allocated byffer or {@literal null} when allocation fails + */ + AllocatedBuffer allocateChunks(int chunks); +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/IntervalEncoder.java b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/IntervalEncoder.java new file mode 100644 index 00000000000..e1b7373188d --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/IntervalEncoder.java @@ -0,0 +1,330 @@ +package com.datadog.profiling.context; + +import java.nio.ByteBuffer; + +/** + * Encodes the collected interval into a binary compressed format. + * + *

The binary blob can be split into a prologue and data chunk whereas the data chunk immediately + * follows the prologue.
+ * + *

The prologue consists of a header and thread map. + * + *

The header is containing information about the number of covered threads, the start timestamp + * in ticks and the frequency multiplier allowing to convert ticks to nanoseconds. + * + *

The thread map is a list of [thread id, interval count] tuples.
+ * + *

The data chunk holds the list of intervals in the form of delta encoded offsets against the + * start timestamp. + * + *

Each thread has its own delta encoded sequence - meaning that the start of the first interval + * for each thread is encoded as the delta against the blob start timestamp, whereas the rest of the + * timestamps are delta encoded against the previous timestamp. In this sequence each two subsequent + * timestamps are forming an interval entry.
+ * + *

All the timestamp delta values are further group varint + * encoded with the assumption that the most of the deltas will fit into 1 or 2 bytes.
+ * + *

The bitmap describing the size of each group varint encoded value in number of byte is located + * at the end of the data chunk.
+ *


+ * + *

Formal description

+ * + *

+ * + *

Prologue Layout

+ * + *

Header

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Attribute + * DescriptionByte SizeCompression
Data Chunk OffsetOffset to the data chunk start4 bytesRAW
Start Epoch MillisThe system epoch millis to peg the start ticks againstt1-9 bytesLEB128 Varint
Frequency MultiplierThe number of ticks per 1000ns1-5 bytesLEB128 Varint
Number of ThreadsNumber of threads with recorded intervals1-5 bytesLEB128 Varint
+ * + *
+ * + *

Thread Map

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Attribute + * DescriptionByte SizeCompression
Thread IDThe Java thread ID1-5 bytesLEB128 Varint
Number of IntervalsThe number of intervals stored for this thread1-5 bytesLEB128 Varint
+ * + *
+ * + *

Data Chunk Layout

+ * + *

Header

+ * + * + * + * + * + * + * + * + * + * + * + * + * + *
Attribute + * DescriptionByte SizeCompression
Group Varint Bitmap OffsetOffset to the group varint size bitmap4 bytesRAW
+ * + *
+ * + *

Thread interval sequence

+ * + * The following tuple is repeated for as many times as there are intervals for the given + * thread. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Attribute + * DescriptionByte SizeCompression
Start deltaDelta ticks to the end of the previous interval or to the blob start timestamp if this is the first interval1-4 bytesGroup Varint
End deltaDelta ticks to the start of this interval1-4 bytesGroup Varint
+ * + *
+ * + *

Group Varint Size Bitmap

+ * + * The group varint encoding of long (8 bytes) numbers is using a separate size map where each 8 + * subsequent long numbers are encoded in the correspondingly subsequent 3 bytes where each 3 bits + * are representing the number of bytes the referenced long number can be represented in. The value + * of those 3 bits is offset by 1 meaning that if the value is 0b000 the encoded length becomes 1 + * and 0b111 is actually 8.
+ *
+ * The following triple is repeated as many times as necessary to cover all intervals + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * * + * + * + * + * + * + * + * + *
Attribute + * DescriptionByte SizeCompression
byte 1The following long number lengths are encoded in this byte - [111_222_33]1 byteRAW
byte 2The following long number lengths are encoded in this byte - [3_444_555_6]1 byteRAW
byte 3The following long number lengths are encoded in this byte - [66_777_888]1 byteRAW
+ */ +final class IntervalEncoder { + private final ByteBuffer prologueBuffer; + private final ByteBuffer dataChunkBuffer; + private final ByteBuffer groupVarintMapBuffer; + private final int threadCount; + private int threadIndex = 0; + + private int maskPos = 0; + private int maskOffset = 0; + + private boolean encoderFinished = false; + private boolean threadInFlight = false; + private final LEB128Support leb128Support = new LEB128Support(); + + final class ThreadEncoder { + private final byte[] elements = new byte[8]; + + private long runningTimestamp; + private final long threadId; + private int intervals = 0; + + private boolean threadFinished = false; + + ThreadEncoder(long threadId) { + this.threadId = threadId; + this.runningTimestamp = 0; + } + + void recordInterval(long from, long till) { + if (threadFinished) { + throw new IllegalStateException("Illegal state: threadFinished=" + threadFinished); + } + intervals++; + putLongValue(from - runningTimestamp); + putLongValue(till - from); + runningTimestamp = till; + } + + IntervalEncoder finish() { + if (threadFinished) { + throw new IllegalStateException("Illegal state: threadFinished=" + threadFinished); + } + leb128Support.putVarint(prologueBuffer, threadId); + leb128Support.putVarint(prologueBuffer, intervals); + threadInFlight = false; + threadFinished = true; + return IntervalEncoder.this; + } + + private void putLongValue(long value) { + int size = leb128Support.longSize(value); + + int base = size - 1; + for (int i = 0; i < size; i++) { + elements[base - i] = (byte) (value & 0xff); + value = value >>> 8; + } + dataChunkBuffer.put(elements, 0, size); + storeLongValueSize(size); + } + + private void storeLongValueSize(int size) { + groupVarintMapBuffer.position(maskPos); + groupVarintMapBuffer.mark(); + int mask = groupVarintMapBuffer.getInt(); + + // record the current length + // need to do 'size - 1' to be able to squeeze the length into 3 bits -> 0 means size of 1 + // etc. + mask = mask | (((size - 1) & 0x7) << (29 - maskOffset)); + // and rewrite the mask + groupVarintMapBuffer.reset(); + groupVarintMapBuffer.putInt(mask); + if ((maskOffset += 3) > 21) { + maskOffset = 0; + maskPos += 3; + } + } + } + + /** + * @param timestampMillis the timestamp in epoch millis + * @param freqMultiplier number of ticks per 1000ns + * @param threadCount number of tracked threads + * @param maxSize max data size + */ + IntervalEncoder(long timestampMillis, long freqMultiplier, int threadCount, int maxSize) { + this.threadCount = threadCount; + + this.prologueBuffer = + ByteBuffer.allocate( + leb128Support.varintSize(timestampMillis) + + leb128Support.varintSize(freqMultiplier) + + leb128Support.varintSize(threadCount) + + threadCount * 18); + this.dataChunkBuffer = ByteBuffer.allocate(maxSize * 8 + 4); + this.groupVarintMapBuffer = + ByteBuffer.allocate(leb128Support.align((int) (Math.ceil(maxSize / 8d) * 3), 4)); + + prologueBuffer.putInt(0); // pre-allocate space for the datachunk offset + dataChunkBuffer.putInt(0); // pre-allocate space for the group varint map offset + leb128Support.putVarint(prologueBuffer, timestampMillis); + leb128Support.putVarint(prologueBuffer, freqMultiplier); + leb128Support.putVarint(prologueBuffer, threadCount); + } + + ThreadEncoder startThread(long threadId) { + if (threadInFlight || threadIndex++ >= threadCount) { + throw new IllegalStateException( + "Illegal state: threadInFlight=" + + threadInFlight + + ", threadIndex=" + + threadIndex + + ", threadCount=" + + threadCount); + } + threadInFlight = true; + return new ThreadEncoder(threadId); + } + + ByteBuffer finish() { + if (encoderFinished || threadInFlight) { + throw new IllegalStateException( + "Illegal state: encoderFinished=" + + encoderFinished + + ", threadInFlight=" + + threadInFlight); + } + encoderFinished = true; + ByteBuffer buffer = + ByteBuffer.allocate( + prologueBuffer.position() + + dataChunkBuffer.position() + + groupVarintMapBuffer.position()); + prologueBuffer.putInt(0, prologueBuffer.position()); // store the data chunk offset + dataChunkBuffer.putInt(0, dataChunkBuffer.position()); // store the group varint bitmap offset + // and now store the parts of the blob + buffer.put((ByteBuffer) prologueBuffer.flip()); // prologue + buffer.put((ByteBuffer) dataChunkBuffer.flip()); // data chunk + buffer.put((ByteBuffer) groupVarintMapBuffer.flip()); // group varint bitmap + return (ByteBuffer) buffer.flip(); + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/IntervalSequencePruner.java b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/IntervalSequencePruner.java new file mode 100644 index 00000000000..b1bff41316b --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/IntervalSequencePruner.java @@ -0,0 +1,147 @@ +package com.datadog.profiling.context; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A utility class to 'prune' the interval sequence.
+ * An interval sequence is the result of a serie of activations and deactivations of a specific span + * on a specific thread. All intervals present in the sequence are related to exactly one span and + * thread and therefore it is possible to make assumptions about time linearity and do a simple + * deduplication and simplification of the recorded raw data. + */ +final class IntervalSequencePruner { + private static final Logger log = LoggerFactory.getLogger(IntervalSequencePruner.class); + + /** + * All zero values should be removed from the sequence.
+ * {@literal 0} is used as a marker for pruned values. This possible thanks to the knowledge of + * the pruned data - there will never be a valid zero value store before pruning. + */ + private static final class PruningLongIterator implements LongIterator { + private final LongIterator wrapped; + private long cachedValue = 0L; + + PruningLongIterator(LongIterator wrapped) { + this.wrapped = wrapped; + } + + @Override + public boolean hasNext() { + if (cachedValue != 0) { + // previously computed cached value available + return true; + } + while (wrapped.hasNext()) { + long val = wrapped.next(); + if (val != 0) { + cachedValue = val; + return true; + } + } + return false; + } + + @Override + public long next() { + try { + return cachedValue; + } finally { + cachedValue = 0; + } + } + } + + /** + * Perform deduplication and simplification of the original sequence raw data. + * + *

The deduplication and simplification rules are + * + *

+ * + * @param sequence the raw data sequence + * @param timestampDelta the timestamp delta to use for the synthetic end transition if necessary + * @return a {@linkplain LongIterator} instance providing access to the pruned data + */ + LongIterator pruneIntervals(LongSequence sequence, long timestampDelta) { + int lastTransition = ProfilerTracingContextTracker.TRANSITION_NONE; + int finishIndexStart = -1; + int sequenceOffset = 0; + LongIterator iterator = sequence.iterator(); + while (iterator.hasNext()) { + long value = iterator.next(); + int transition = (int) ((value & ProfilerTracingContextTracker.TRANSITION_MASK) >>> 62); + if (transition == ProfilerTracingContextTracker.TRANSITION_STARTED) { + if (lastTransition == ProfilerTracingContextTracker.TRANSITION_STARTED) { + // skip duplicated starts + sequence.set(sequenceOffset, 0L); + } else if (lastTransition > ProfilerTracingContextTracker.TRANSITION_STARTED) { + int collapsedLength = sequenceOffset - finishIndexStart - 1; + if (collapsedLength > 0) { + for (int i = 0; i < collapsedLength; i++) { + sequence.set(finishIndexStart + i, 0L); + } + } + finishIndexStart = -1; + + if (lastTransition == ProfilerTracingContextTracker.TRANSITION_MAYBE_FINISHED) { + // skip 'maybe finished' followed by started + sequence.set(sequenceOffset - 1, 0L); + // also, ignore the start - instead just continue the previous interval + sequence.set(sequenceOffset, 0L); + } + } + } else if (transition == ProfilerTracingContextTracker.TRANSITION_MAYBE_FINISHED) { + if (lastTransition == ProfilerTracingContextTracker.TRANSITION_NONE) { + // dangling transition - remove it + log.debug("Dangling 'maybe finished' transition"); + sequence.set(sequenceOffset, 0L); + transition = lastTransition; + } else if (lastTransition == ProfilerTracingContextTracker.TRANSITION_STARTED) { + finishIndexStart = sequenceOffset; + } else if (lastTransition == ProfilerTracingContextTracker.TRANSITION_FINISHED) { + // 'finish' followed by 'maybe finished' turns into 'finished' + transition = lastTransition; + } + } else { // if (transition == ProfilerTracingContextTracker.TRANSITION_FINISHED) + if (lastTransition == ProfilerTracingContextTracker.TRANSITION_NONE) { + // dangling transition - remove it + log.debug("Dangling 'finished' transition"); + sequence.set(sequenceOffset, 0L); + transition = lastTransition; + } else if (lastTransition == ProfilerTracingContextTracker.TRANSITION_STARTED) { + // mark the start of 'finished' sequence + finishIndexStart = sequenceOffset; + } + } + lastTransition = transition; + sequenceOffset++; + } + if (finishIndexStart > -1) { + int maxIndex = sequence.size() - 1; + for (int i = finishIndexStart; i < maxIndex; i++) { + sequence.set(i, 0L); + } + } + if (lastTransition == ProfilerTracingContextTracker.TRANSITION_STARTED) { + // dangling start -> create a synthetic finished transition + log.debug( + "Dangling 'started' transition. Creating synthetic 'finished' transition @{} delta", + timestampDelta); + sequence.add(ProfilerTracingContextTracker.maskDeactivation(timestampDelta, false)); + } + return new PruningLongIterator(sequence.iterator()); + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/JfrTimestampPatch.java b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/JfrTimestampPatch.java new file mode 100644 index 00000000000..6f86e9aaabb --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/JfrTimestampPatch.java @@ -0,0 +1,7 @@ +package com.datadog.profiling.context; + +public final class JfrTimestampPatch { + public static void execute(ClassLoader agentClassLoader) { + JfrTimestampPatchImpl.execute(agentClassLoader); + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/LEB128Support.java b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/LEB128Support.java new file mode 100644 index 00000000000..07055ea6ddf --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/LEB128Support.java @@ -0,0 +1,98 @@ +package com.datadog.profiling.context; + +import java.nio.ByteBuffer; + +final class LEB128Support { + private static final int EXT_BIT = 0x80; + private static final long COMPRESSED_INT_MASK = -EXT_BIT; + + int align(int value, int alignment) { + return value == 0 ? 0 : (((value - 1) / alignment) + 1) * alignment; + } + + int varintSize(long value) { + if (value >= 0 && value < 255) { + return 1; + } + int pos = 63; + long mask = 0xFE00000000000000L; + while ((value & mask) == 0) { + pos -= 7; + mask = mask >>> 7; + } + return ((pos - 1) / 7) + 1; + } + + int longSize(long value) { + int pos = 63; + long mask = 0xFF00000000000000L; + while (pos > 0 && (value & mask) == 0) { + pos -= 8; + mask = mask >>> 8; + } + return (pos / 8) + 1; + } + + void putVarint(ByteBuffer buffer, long value) { + // if (value < 0) { + // // negative numbers are not supported + // return; + // } + if ((value & COMPRESSED_INT_MASK) == 0) { + buffer.put((byte) ((value & 0x7f))); + return; + } + buffer.put((byte) ((value & 0x7f) | EXT_BIT)); + + value >>= 7; + if ((value & COMPRESSED_INT_MASK) == 0) { + buffer.put((byte) ((value & 0x7f))); + return; + } + buffer.put((byte) ((value & 0x7f) | EXT_BIT)); + + value >>= 7; + if ((value & COMPRESSED_INT_MASK) == 0) { + buffer.put((byte) ((value & 0x7f))); + return; + } + buffer.put((byte) ((value & 0x7f) | EXT_BIT)); + + value >>= 7; + if ((value & COMPRESSED_INT_MASK) == 0) { + buffer.put((byte) ((value & 0x7f))); + return; + } + buffer.put((byte) ((value & 0x7f) | EXT_BIT)); + + value >>= 7; + if ((value & COMPRESSED_INT_MASK) == 0) { + buffer.put((byte) ((value & 0x7f))); + return; + } + buffer.put((byte) ((value & 0x7f) | EXT_BIT)); + + value >>= 7; + if ((value & COMPRESSED_INT_MASK) == 0) { + buffer.put((byte) ((value & 0x7f))); + return; + } + buffer.put((byte) ((value & 0x7f) | EXT_BIT)); + + value >>= 7; + if ((value & COMPRESSED_INT_MASK) == 0) { + buffer.put((byte) ((value & 0x7f))); + return; + } + buffer.put((byte) ((value & 0x7f) | EXT_BIT)); + + value >>= 7; + if ((value & COMPRESSED_INT_MASK) == 0) { + buffer.put((byte) ((value & 0x7f))); + return; + } + buffer.put((byte) ((value & 0x7f) | EXT_BIT)); + + buffer.put((byte) ((value >> 7) & 0x7f)); + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/LongIterator.java b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/LongIterator.java new file mode 100644 index 00000000000..43299fb19c6 --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/LongIterator.java @@ -0,0 +1,13 @@ +package com.datadog.profiling.context; + +/** A primitive long-value iterator */ +public interface LongIterator { + /** @return {@literal true} if the iterated seuqence has more elements */ + boolean hasNext(); + + /** + * @return the next long-value from the iterated sequence + * @throws IllegalStateException if this method is called and there is no next value + */ + long next(); +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/LongSequence.java b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/LongSequence.java new file mode 100644 index 00000000000..bffe864cca6 --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/LongSequence.java @@ -0,0 +1,235 @@ +package com.datadog.profiling.context; + +import com.datadog.profiling.context.allocator.AllocatedBuffer; +import java.util.concurrent.atomic.AtomicInteger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A growing sequence of long values.
+ * Allows adding new values, rewriting values at a given position, retrieving values from a position + * and creating {@linkplain LongIterator} over the sequence. + * + *

This class is not thread-safe in a generic way. The only concurrent access that is allowed is + * doing 'release' while a sequence is manipulated and vice versa. For all other combinations of + * method invocations the results are not defined. The caller should take care of proper + * synchronization, if necessary. + */ +final class LongSequence { + private static final Logger log = LoggerFactory.getLogger(LongSequence.class); + + private class LongIteratorImpl implements LongIterator { + int bufferReadSlot = 0; + int allIndex = 0; + LongIterator currentIterator = null; + + @Override + public boolean hasNext() { + if (bufferReadSlot > bufferWriteSlot || allIndex >= size) { + return false; + } + if (currentIterator == null) { + currentIterator = buffers[bufferReadSlot].iterator(); + } + if (currentIterator.hasNext()) { + return true; + } + do { + if (++bufferReadSlot > bufferWriteSlot) { + return false; + } + currentIterator = buffers[bufferReadSlot].iterator(); + } while (!currentIterator.hasNext()); + return true; + } + + @Override + public long next() { + if (currentIterator == null) { + throw new IllegalStateException(); + } + + long value = currentIterator.next(); + allIndex++; + return value; + } + } + + // with capacity increment c[n] = c[n -1] + 2 * c[n - 1] we can safely accommodate 100k elements + private AllocatedBuffer[] buffers = new AllocatedBuffer[10]; + private int[] bufferBoundaryMap = new int[10]; + + private int bufferInitSlot = 0; + private int bufferWriteSlot = 0; + private int capacity = 0; + private int capacityInChunks = 0; + private int size = 0; + private int sizeInBytes = 0; + private int threshold = 0; + private final Allocator allocator; + private final PositionDecoder positionDecoder = PositionDecoder.getInstance(); + + private static int align(int size) { + return (int) (Math.ceil(size / 8d) * 8); + } + + private AtomicInteger state = new AtomicInteger(0); + + public LongSequence(Allocator allocator) { + this.allocator = allocator; + this.bufferWriteSlot = -1; + } + + public int add(long value) { + // check'n'update the state - if it is non-negative, increment the counter and proceed, + // otherwise bail out + if (state.updateAndGet(prev -> prev >= 0 ? prev + 1 : prev) < 0) { + // bail out if this instance was already released + return -1; + } + + try { + if (bufferWriteSlot == -1) { + // first write; initialize the data structure + capacityInChunks = 1; + bufferWriteSlot = 0; + bufferInitSlot = 0; + AllocatedBuffer cBuffer = allocator.allocateChunks(capacityInChunks); + buffers[bufferWriteSlot] = cBuffer; + if (cBuffer != null) { + capacity = cBuffer.capacity(); + threshold = align((int) (this.capacity * 0.75f)); + bufferBoundaryMap[bufferWriteSlot] = capacity - 1; + } else { + capacity = 0; + threshold = -1; + } + } else { + if (threshold > -1 && sizeInBytes == threshold) { + // we hit the threshold - let's prepare the next-in-line buffer + int newCapacity = 2 * capacityInChunks; // capacity stays aligned + AllocatedBuffer cBuffer = allocator.allocateChunks(newCapacity); + if (cBuffer != null) { + bufferInitSlot = bufferWriteSlot + 1; + buffers[bufferInitSlot] = cBuffer; + capacityInChunks += newCapacity; + capacity += cBuffer.capacity(); // update the sequence capacity + threshold = align((int) (capacity * 0.75f)); // update the threshold + bufferBoundaryMap[bufferInitSlot] = capacity - 1; + } else { + threshold = -1; + } + } + } + if (bufferWriteSlot == buffers.length || buffers[bufferWriteSlot] == null) { + return 0; + } + while (!buffers[bufferWriteSlot].putLong(value)) { + bufferWriteSlot++; + if (bufferWriteSlot == buffers.length || buffers[bufferWriteSlot] == null) { + return 0; + } + } + + size += 1; + sizeInBytes += 8; + return 1; + } finally { + // check'n'update the state - if it is negative before update, perform release, otherwise just + // decrement the counter + if (state.getAndUpdate(prev -> prev > 0 ? prev - 1 : prev) < 0) { + doRelease(); + } + } + } + + public boolean set(int index, long value) { + // check'n'update the state - if it is non-negative, increment the counter and proceed, + // otherwise bail out + if (state.updateAndGet(prev -> prev >= 0 ? prev + 1 : prev) < 0) { + // bail out if this instance was already released + return false; + } + try { + PositionDecoder.Coordinates decoded = + positionDecoder.decode(index * 8, bufferBoundaryMap, bufferInitSlot + 1); + if (decoded != null) { + return buffers[decoded.slot].putLong(decoded.index, value); + } + return false; + } finally { + // check'n'update the state - if it is negative before update, perform release, otherwise just + // decrement the counter + if (state.getAndUpdate(prev -> prev > 0 ? prev - 1 : prev) < 0) { + doRelease(); + } + } + } + + public long get(int index) { + // check'n'update the state - if it is non-negative, increment the counter and proceed, + // otherwise bail out + if (state.updateAndGet(prev -> prev >= 0 ? prev + 1 : prev) < 0) { + // bail out if this instance was already released + return Long.MIN_VALUE; + } + try { + PositionDecoder.Coordinates decoded = + positionDecoder.decode(index * 8, bufferBoundaryMap, bufferInitSlot + 1); + if (decoded != null) { + return buffers[decoded.slot].getLong(decoded.index); + } + return Long.MIN_VALUE; + } finally { + // check'n'update the state - if it is negative before update, perform release, otherwise just + // decrement the counter + if (state.getAndUpdate(prev -> prev > 0 ? prev - 1 : prev) < 0) { + doRelease(); + } + } + } + + public int size() { + // check'n'update the state - if it is non-negative, increment the counter and proceed, + // otherwise bail out + if (state.updateAndGet(prev -> prev >= 0 ? prev + 1 : prev) < 0) { + // bail out if this instance was already released + return 0; + } + try { + return size; + } finally { + // check'n'update the state - if it is negative before update, perform release, otherwise just + // decrement the counter + if (state.getAndUpdate(prev -> prev > 0 ? prev - 1 : prev) < 0) { + doRelease(); + } + } + } + + public void release() { + if (state.getAndUpdate(prev -> prev > 0 ? -1 * prev : prev) == 0) { + doRelease(); + } + } + + private void doRelease() { + for (int i = 0; i < buffers.length; i++) { + AllocatedBuffer buffer = buffers[i]; + if (buffer != null) { + buffer.release(); + } + } + // clear out the buffer slots to allow the most of the retained data to be GCed + // do not clear up the buffers contents as they still may be in use by an iterator instance + buffers = null; + bufferBoundaryMap = null; + bufferInitSlot = -1; + size = 0; + state.set(-1); + } + + public LongIterator iterator() { + return new LongIteratorImpl(); + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/PositionDecoder.java b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/PositionDecoder.java new file mode 100644 index 00000000000..fb84f96c09b --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/PositionDecoder.java @@ -0,0 +1,108 @@ +package com.datadog.profiling.context; + +import java.util.Arrays; +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * A utility class to decode a position into [slot, index] coordinates in an array of indexable + * buffers. It supports each of the indexable buffers having a different size. + */ +public final class PositionDecoder { + /** Coordinates data holder */ + public static final class Coordinates { + public final int slot; + public final int index; + + public Coordinates(int slot, int index) { + this.slot = slot; + this.index = index; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Coordinates coordinates = (Coordinates) o; + return slot == coordinates.slot && index == coordinates.index; + } + + @Override + public int hashCode() { + return Objects.hash(slot, index); + } + } + + private static final class Singleton { + static final PositionDecoder INSTANCE = new PositionDecoder(); + } + + public static PositionDecoder getInstance() { + return Singleton.INSTANCE; + } + + /** + * Turn the position into a {@linkplain Coordinates} instance + * + * @param position the position + * @param bufferBoundaryMap an array of buffer boundaries - a boundary is defined as 'size - 1' + * for each buffer element + * @return decoded {@linkplain Coordinates} or {@literal null} + */ + @Nullable + public Coordinates decode(int position, int[] bufferBoundaryMap) { + return decode(position, bufferBoundaryMap, bufferBoundaryMap.length); + } + + /** + * Turn the position into a {@linkplain Coordinates} instance + * + * @param position the position + * @param bufferBoundaryMap an array of buffer boundaries - a boundary is defined as 'size - 1' + * for each buffer element + * @param bufferBoundaryMapLimit limit the buffer boundaries to be used only to the first + * {@literal bufferBoundaryMapLimit} ones + * @return decoded {@linkplain Coordinates} or {@literal null} + */ + @Nullable + public Coordinates decode( + int position, @Nonnull int[] bufferBoundaryMap, int bufferBoundaryMapLimit) { + if (bufferBoundaryMap.length == 0) { + return null; + } + if (bufferBoundaryMapLimit <= 0 || bufferBoundaryMapLimit > bufferBoundaryMap.length) { + return null; + } + + // shortcut for a position falling within the first slot + if (position <= bufferBoundaryMap[0]) { + return new Coordinates(0, position); + } + // shortcut for positions not covered by the boundary map + if (position > bufferBoundaryMap[bufferBoundaryMapLimit - 1]) { + return null; + } + + // shortcut to linear search for a small number of slots in use + if (bufferBoundaryMapLimit < 5) { + int slot = 0; + while (slot <= bufferBoundaryMapLimit && bufferBoundaryMap[slot] < position) { + slot++; + } + if (slot <= bufferBoundaryMapLimit) { + return new Coordinates(slot, position - bufferBoundaryMap[slot - 1] - 1); + } + return null; + } + + // use binary search + int slot = Arrays.binarySearch(bufferBoundaryMap, position); + if (slot > 0) { + return new Coordinates(slot, position - bufferBoundaryMap[slot - 1] - 1); + } else { + slot = -1 - slot; + return new Coordinates(slot, position - bufferBoundaryMap[slot - 1] - 1); + } + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/ProfilerTracingContextTracker.java b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/ProfilerTracingContextTracker.java new file mode 100644 index 00000000000..bc936da8201 --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/ProfilerTracingContextTracker.java @@ -0,0 +1,349 @@ +package com.datadog.profiling.context; + +import datadog.trace.api.profiling.TracingContextTracker; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.relocate.api.RatelimitedLogger; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class ProfilerTracingContextTracker implements TracingContextTracker { + private static final Logger log = LoggerFactory.getLogger(ProfilerTracingContextTracker.class); + + private static final byte[] EMPTY_DATA = new byte[0]; + + static final int TRANSITION_STARTED = 0; + static final int TRANSITION_MAYBE_FINISHED = 1; + static final int TRANSITION_NONE = -1; + static final int TRANSITION_FINISHED = 2; + + public interface TimeTicksProvider { + TimeTicksProvider SYSTEM = + new TimeTicksProvider() { + @Override + public long ticks() { + return System.nanoTime(); + } + + @Override + public long frequency() { + return 1_000_000_000L; // nanosecond frequency + } + }; + + long ticks(); + + long frequency(); + } + + static final class DelayedTrackerImpl implements TracingContextTracker.DelayedTracker { + volatile ProfilerTracingContextTracker ref; + + DelayedTrackerImpl(ProfilerTracingContextTracker ref) { + this.ref = ref; + } + + @Override + public void cleanup() { + ProfilerTracingContextTracker instance = ref; + if (instance != null) { + instance.release(); + /* + It is important to remove the reference such that the tracker, and transitively the span + is not held by the delayed instance until its timeout + */ + releaseRef(); + } + } + + void releaseRef() { + ref = null; + } + + @Override + public long getDelay(TimeUnit unit) { + ProfilerTracingContextTracker instance = ref; + if (ref != null) { + return unit.convert( + instance.lastTransitionTimestamp + instance.inactivityDelay - System.nanoTime(), + TimeUnit.NANOSECONDS); + } + // if the rerence was cleared this instance is ready for immediate removal from the queue + return -1; + } + + @Override + public int compareTo(Delayed o) { + if (o instanceof ProfilerTracingContextTracker.DelayedTrackerImpl) { + ProfilerTracingContextTracker thiz = ref; + ProfilerTracingContextTracker other = ((DelayedTrackerImpl) o).ref; + return Long.compare( + thiz != null ? thiz.lastTransitionTimestamp : -1, + other != null ? other.lastTransitionTimestamp : -1); + } + return 0; + } + } + + private static final RatelimitedLogger warnlog = + new RatelimitedLogger( + LoggerFactory.getLogger(ProfilerTracingContextTracker.class), 30, TimeUnit.SECONDS); + + private static final AtomicReferenceFieldUpdater< + ProfilerTracingContextTracker, DelayedTrackerImpl> + DELAYED_TRACKER_REF_UPDATER = + AtomicReferenceFieldUpdater.newUpdater( + ProfilerTracingContextTracker.class, DelayedTrackerImpl.class, "delayedTrackerRef"); + private static final long TRANSITION_MAYBE_FINISHED_MASK = + (long) (TRANSITION_MAYBE_FINISHED) << 62; + private static final long TRANSITION_FINISHED_MASK = (long) (TRANSITION_FINISHED) << 62; + static final long TRANSITION_MASK = TRANSITION_FINISHED_MASK | TRANSITION_MAYBE_FINISHED_MASK; + static final long TIMESTAMP_MASK = ~TRANSITION_MASK; + + private final long inactivityDelay; + + private final ConcurrentMap threadSequences = new ConcurrentHashMap<>(64); + private final long startTimestampTicks; + private final long startTimestampMillis; + private final long delayedActivationTimestamp; + private final Allocator allocator; + private final AtomicBoolean released = new AtomicBoolean(); + private final AgentSpan span; + private final TimeTicksProvider timeTicksProvider; + private final IntervalSequencePruner sequencePruner; + + private final long initialThreadId; + private final AtomicBoolean initialized = new AtomicBoolean(false); + private final AtomicReference persisted = new AtomicReference<>(null); + + private long lastTransitionTimestamp = -1; + + private volatile DelayedTrackerImpl delayedTrackerRef = null; + + ProfilerTracingContextTracker( + Allocator allocator, + AgentSpan span, + TimeTicksProvider timeTicksProvider, + IntervalSequencePruner sequencePruner) { + this(allocator, span, timeTicksProvider, sequencePruner, -1L); + } + + ProfilerTracingContextTracker( + Allocator allocator, + AgentSpan span, + TimeTicksProvider timeTicksProvider, + IntervalSequencePruner sequencePruner, + long inactivityDelay) { + this.startTimestampTicks = timeTicksProvider.ticks(); + this.startTimestampMillis = System.currentTimeMillis(); + this.timeTicksProvider = timeTicksProvider; + this.sequencePruner = sequencePruner; + this.span = span; + this.allocator = allocator; + this.initialThreadId = Thread.currentThread().getId(); + this.lastTransitionTimestamp = System.nanoTime(); + this.inactivityDelay = inactivityDelay; + this.delayedActivationTimestamp = timeTicksProvider.ticks(); + } + + @Override + public void activateContext() { + activateContext(Thread.currentThread().getId()); + } + + private void activateContext(long threadId) { + activateContext(threadId, accessTimestamp()); + } + + void activateContext(long threadId, long timestamp) { + storeDelayedActivation(); + long tsDiff = timestamp - startTimestampTicks; + long masked = maskActivation(tsDiff); + store(threadId, masked); + } + + private long accessTimestamp() { + long ts = timeTicksProvider.ticks(); + lastTransitionTimestamp = System.nanoTime(); + return ts; + } + + static long maskActivation(long value) { + return value & TIMESTAMP_MASK; + } + + static long maskDeactivation(long value, boolean maybe) { + return (value & TIMESTAMP_MASK) + | (maybe ? TRANSITION_MAYBE_FINISHED_MASK : TRANSITION_FINISHED_MASK); + } + + @Override + public void deactivateContext() { + deactivateContext(Thread.currentThread().getId(), false); + } + + @Override + public void maybeDeactivateContext() { + deactivateContext(Thread.currentThread().getId(), true); + } + + private void deactivateContext(long threadId, boolean maybe) { + deactivateContext(threadId, accessTimestamp(), maybe); + } + + void deactivateContext(long threadId, long timestamp, boolean maybe) { + storeDelayedActivation(); + long tsDiff = timestamp - startTimestampTicks; + long masked = maskDeactivation(tsDiff, maybe); + store(threadId, masked); + } + + @Override + public byte[] persist() { + byte[] data = null; + if (persisted.compareAndSet(null, EMPTY_DATA)) { + try { + if (released.get()) { + // tracker was released without persisting the data + return null; + } + ByteBuffer buffer = encodeIntervals(); + data = buffer.array(); + data = Arrays.copyOf(data, buffer.limit()); + } finally { + // make sure the other threads do not stay blocked even if there is an exception thrown + persisted.compareAndSet(EMPTY_DATA, data); + } + } else { + // busy wait for the data to become available + while ((data = persisted.get()) == EMPTY_DATA) { + Thread.yield(); + } + } + return data; + } + + LongIterator pruneIntervals(LongSequence sequence) { + return sequencePruner.pruneIntervals(sequence, timeTicksProvider.ticks() - startTimestampTicks); + } + + @Override + public boolean release() { + boolean result = releaseThreadSequences(); + releaseDelayed(); + return result; + } + + private boolean releaseThreadSequences() { + // the released flag needs to be set first such that it would prevent using the resourcese being + // released from 'persist()' method + if (released.compareAndSet(false, true)) { + log.trace("Releasing tracing context for span {}", span); + // now let's wait for any date being currently persisted + // 'persist()' method guarantees that the 'persisted' value will become != EMPTY_DATA + while (persisted.get() == EMPTY_DATA) { + Thread.yield(); + } + // it is safe to cleanup the resources + threadSequences.values().forEach(LongSequence::release); + threadSequences.clear(); + return true; + } + return false; + } + + void releaseDelayed() { + DelayedTrackerImpl delayed = DELAYED_TRACKER_REF_UPDATER.getAndSet(this, null); + if (delayed != null) { + delayed.releaseRef(); + } + } + + @Override + public int getVersion() { + return 1; + } + + @Override + public DelayedTracker asDelayed() { + return DELAYED_TRACKER_REF_UPDATER.updateAndGet( + this, prev -> prev != null ? prev : new DelayedTrackerImpl(this)); + } + + private void store(long threadId, long value) { + if (released.get()) { + return; + } + int added = 0; + LongSequence sequence = + threadSequences.computeIfAbsent(threadId, k -> new LongSequence(allocator)); + synchronized (sequence) { + added = sequence.add(value); + } + if (added == -1) { + warnlog.warn( + "Attempting to add transition to already released context - losing tracing context data"); + } else if (added == 0) { + warnlog.warn("Profiling Context Buffer is full - losing tracing context data"); + } + } + + private ByteBuffer encodeIntervals() { + int totalSequenceBufferSize = 0; + int maxSequenceSize = 0; + Set> entrySet = new HashSet<>(threadSequences.entrySet()); + for (Map.Entry entry : entrySet) { + LongSequence sequence = entry.getValue(); + maxSequenceSize = Math.max(maxSequenceSize, sequence.size()); + totalSequenceBufferSize += sequence.size(); + } + + IntervalEncoder encoder = + new IntervalEncoder( + startTimestampMillis, + timeTicksProvider.frequency() / 1_000_000L, + threadSequences.size(), + totalSequenceBufferSize); + for (Map.Entry entry : threadSequences.entrySet()) { + long threadId = entry.getKey(); + IntervalEncoder.ThreadEncoder threadEncoder = encoder.startThread(threadId); + LongSequence rawIntervals = entry.getValue(); + synchronized (rawIntervals) { + LongIterator iterator = pruneIntervals(entry.getValue()); + int sequenceIndex = 0; + while (iterator.hasNext() && sequenceIndex++ < maxSequenceSize) { + long from = iterator.next(); + long maskedFrom = (from & TIMESTAMP_MASK); + if (iterator.hasNext()) { + long till = iterator.next(); + long maskedTill = (till & TIMESTAMP_MASK); + if (maskedTill > maskedFrom) { + threadEncoder.recordInterval(maskedFrom, maskedTill); + } + } + } + } + threadEncoder.finish(); + } + ByteBuffer buffer = encoder.finish(); + return buffer; + } + + private void storeDelayedActivation() { + if (initialized.compareAndSet(false, true)) { + log.trace("Storing delayed activation for span {}", span); + activateContext(initialThreadId, delayedActivationTimestamp); + } + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/ProfilerTracingContextTrackerFactory.java b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/ProfilerTracingContextTrackerFactory.java new file mode 100644 index 00000000000..7816dc73f1d --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/ProfilerTracingContextTrackerFactory.java @@ -0,0 +1,169 @@ +package com.datadog.profiling.context; + +import com.datadog.profiling.context.allocator.Allocators; +import datadog.trace.api.config.ProfilingConfig; +import datadog.trace.api.profiling.TracingContextTracker; +import datadog.trace.api.profiling.TracingContextTrackerFactory; +import datadog.trace.bootstrap.config.provider.ConfigProvider; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.util.AgentTaskScheduler; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class ProfilerTracingContextTrackerFactory + implements TracingContextTrackerFactory.Implementation { + private static final Logger log = + LoggerFactory.getLogger(ProfilerTracingContextTrackerFactory.class); + + private static final long DEFAULT_INACTIVITY_CHECK_PERIOD_MS = 5_000L; // 5 seconds + + private final DelayQueue delayQueue = new DelayQueue<>(); + + public static void register(ConfigProvider configProvider) { + if (configProvider.getBoolean( + ProfilingConfig.PROFILING_TRACING_CONTEXT_ENABLED, + ProfilingConfig.PROFILING_TRACING_CONTEXT_ENABLED_DEFAULT)) { + long inactivityDelayNs = + TimeUnit.NANOSECONDS.convert( + configProvider.getInteger( + ProfilingConfig.PROFILING_TRACING_CONTEXT_TRACKER_INACTIVE_SEC, + ProfilingConfig.PROFILING_TRACING_CONTEXT_TRACKER_INACTIVE_DEFAULT), + TimeUnit.SECONDS); + int reservedMemorySize = + configProvider.getInteger( + ProfilingConfig.PROFILING_TRACING_CONTEXT_RESERVED_MEMORY_SIZE, + ProfilingConfig.PROFILING_TRACING_CONTEXT_RESERVED_MEMORY_SIZE_DEFAULT); + String reservedMemoryType = + configProvider.getString( + ProfilingConfig.PROFILING_TRACING_CONTEXT_RESERVED_MEMORY_TYPE, + ProfilingConfig.PROFILING_TRACING_CONTEXT_RESERVED_MEMORY_TYPE_DEFAULT); + + TracingContextTrackerFactory.registerImplementation( + new ProfilerTracingContextTrackerFactory( + inactivityDelayNs, + DEFAULT_INACTIVITY_CHECK_PERIOD_MS, + reservedMemorySize, + reservedMemoryType)); + } + } + + private void initializeInactiveTrackerCleanup(long inactivityCheckPeriodMs) { + AgentTaskScheduler.INSTANCE.scheduleAtFixedRate( + target -> { + Collection timeouts = new ArrayList<>(500); + int drained = 0; + do { + drained = target.drainTo(timeouts); + if (drained > 0) { + log.debug("Drained {} inactive trackers", drained); + } + Iterator iterator = timeouts.iterator(); + while (iterator.hasNext()) { + iterator.next().cleanup(); + iterator.remove(); + } + } while (drained > 0); + }, + delayQueue, + inactivityCheckPeriodMs, + inactivityCheckPeriodMs, + TimeUnit.MILLISECONDS); + } + + private final Allocator allocator; + private final IntervalSequencePruner sequencePruner = new IntervalSequencePruner(); + private final ProfilerTracingContextTracker.TimeTicksProvider timeTicksProvider; + private final long inactivityDelay; + + ProfilerTracingContextTrackerFactory( + long inactivityDelayNs, long inactivityCheckPeriodMs, int reservedMemorySize) { + this(inactivityDelayNs, inactivityCheckPeriodMs, reservedMemorySize, "heap"); + } + + ProfilerTracingContextTrackerFactory( + long inactivityDelayNs, + long inactivityCheckPeriodMs, + int reservedMemorySize, + String reservedMemoryType) { + timeTicksProvider = getTicksProvider(); + log.debug("Tracing Context Tracker Memory Type: {}", reservedMemoryType); + allocator = + reservedMemoryType.equalsIgnoreCase("direct") + ? Allocators.directAllocator(reservedMemorySize, 32) + : Allocators.heapAllocator(reservedMemorySize, 32); + this.inactivityDelay = inactivityDelayNs; + + if (inactivityDelay > 0) { + initializeInactiveTrackerCleanup(inactivityCheckPeriodMs); + } + } + + private static ProfilerTracingContextTracker.TimeTicksProvider getTicksProvider() { + try { + Class clz = + ProfilerTracingContextTracker.class.getClassLoader().loadClass("jdk.jfr.internal.JVM"); + MethodHandle mh; + long frequency = 0L; + try { + mh = MethodHandles.lookup().findStatic(clz, "getJVM", MethodType.methodType(clz)); + Object jvm = mh.invoke(); + mh = + MethodHandles.lookup() + .findVirtual(clz, "getTicksFrequency", MethodType.methodType(long.class)); + mh = mh.bindTo(jvm); + frequency = (long) mh.invokeWithArguments(); + } catch (NoSuchMethodException ignored) { + // the method is available since JDK11 only + } + mh = MethodHandles.lookup().findStatic(clz, "counterTime", MethodType.methodType(long.class)); + // sanity check to fail early if the method handle invocation does not work + long ticks = (long) mh.invokeExact(); + + MethodHandle fixedMh = mh; + long fixedFrequency = frequency; + + log.info("Using JFR time ticks provider"); + return new ProfilerTracingContextTracker.TimeTicksProvider() { + @Override + public long ticks() { + try { + return (long) fixedMh.invokeExact(); + } catch (Throwable ignored) { + } + return Long.MIN_VALUE; + } + + @Override + public long frequency() { + return fixedFrequency; + } + }; + } catch (Throwable t) { + if (log.isDebugEnabled()) { + log.warn("Failed to access JFR timestamps. Falling back to system nanotime.", t); + } else { + log.warn("Failed to access JFR timestamps. Falling back to system nanotime."); + } + } + return ProfilerTracingContextTracker.TimeTicksProvider.SYSTEM; + } + + @Override + public TracingContextTracker instance(AgentSpan span) { + ProfilerTracingContextTracker instance = + new ProfilerTracingContextTracker( + allocator, span, timeTicksProvider, sequencePruner, inactivityDelay); + if (inactivityDelay > 0) { + delayQueue.offer(instance.asDelayed()); + } + return instance; + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/AllocatedBuffer.java b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/AllocatedBuffer.java new file mode 100644 index 00000000000..8b83b484f4f --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/AllocatedBuffer.java @@ -0,0 +1,48 @@ +package com.datadog.profiling.context.allocator; + +import com.datadog.profiling.context.LongIterator; + +/** + * Represents the memory allocated by a {@linkplain Allocators} implementation. The buffer defines a + * few rudimentary operations to store and retrieve long values (both sequential and positional + * stores are supported) as well as a custom iterator over the long values stored in the buffer. + */ +public interface AllocatedBuffer { + /** Release the held memory */ + void release(); + + /** @return the buffer capacity in bytes */ + int capacity(); + + /** + * Add the long value to the buffer + * + * @param value the long value to store + * @return {@literal true} if the buffer was able to store the value, {@literal false} otherwise + * (eg. when the buffer capacity is exhausted) + */ + boolean putLong(long value); + + /** + * Puts the long value to the buffer at the given position + * + * @param pos the position to store the value at + * @param value the long value to store + * @return {@literal true} if the buffer was able to store the value, {@literal false} otherwise + * (eg. when the buffer capacity is exhausted) + */ + boolean putLong(int pos, long value); + + /** + * Retrieves the long value from the given position + * + * @param pos the position to retrieve the long value from; it is upon the caller to make sure the + * position was previously written to and is within the capacity + * @return the long value stored at that position; when a read from behind the capacity limit is + * attempted {@linkplain Long#MIN_VALUE} is always returned + */ + long getLong(int pos); + + /** @return a custom long-value iterator of all values stored in the buffer up to this point */ + LongIterator iterator(); +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/Allocators.java b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/Allocators.java new file mode 100644 index 00000000000..00925a5c3e4 --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/Allocators.java @@ -0,0 +1,22 @@ +package com.datadog.profiling.context.allocator; + +import com.datadog.profiling.context.Allocator; +import com.datadog.profiling.context.allocator.direct.DirectAllocator; +import com.datadog.profiling.context.allocator.heap.HeapAllocator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** A factory class for {@linkplain Allocator} instances */ +public abstract class Allocators { + private static final Logger log = LoggerFactory.getLogger(Allocators.class); + + private Allocators() {} + + public static Allocator directAllocator(int capacity, int chunk) { + return new DirectAllocator(capacity, chunk); + } + + public static Allocator heapAllocator(int capacity, int chunk) { + return new HeapAllocator(capacity, chunk); + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/direct/Chunk.java b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/direct/Chunk.java new file mode 100644 index 00000000000..2214465d9ad --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/direct/Chunk.java @@ -0,0 +1,29 @@ +package com.datadog.profiling.context.allocator.direct; + +import java.nio.ByteBuffer; + +final class Chunk { + private final DirectAllocator allocator; + final ByteBuffer buffer; + private final int ref; + private int weight = 1; + + Chunk(DirectAllocator allocator, ByteBuffer buffer, int ref) { + this.allocator = allocator; + this.buffer = buffer; + this.ref = ref; + } + + void extend() { + weight++; + buffer.limit(buffer.limit() + allocator.getChunkSize()); + } + + void release() { + allocator.release(ref, weight); + } + + int getWeight() { + return weight; + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/direct/DirectAllocatedBuffer.java b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/direct/DirectAllocatedBuffer.java new file mode 100644 index 00000000000..2dc1816b393 --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/direct/DirectAllocatedBuffer.java @@ -0,0 +1,135 @@ +package com.datadog.profiling.context.allocator.direct; + +import com.datadog.profiling.context.LongIterator; +import com.datadog.profiling.context.PositionDecoder; +import com.datadog.profiling.context.allocator.AllocatedBuffer; +import java.nio.ByteBuffer; +import java.util.Arrays; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class DirectAllocatedBuffer implements AllocatedBuffer { + private static final Logger log = LoggerFactory.getLogger(DirectAllocatedBuffer.class); + + private final Chunk[] chunks; + private final int[] chunkBoundaryMap; + private final int capacity; + private int chunkWriteIndex = 0; + private int valueWriteIndex = 0; + + private final PositionDecoder positionDecoder = PositionDecoder.getInstance(); + + DirectAllocatedBuffer(int bufferSize, int chunkSize, Chunk... chunks) { + this.chunks = chunks; + this.chunkBoundaryMap = new int[chunks.length]; + this.capacity = bufferSize; + + int boundary = 0; + for (int i = 0; i < chunks.length; i++) { + boundary += chunkSize * chunks[i].getWeight(); + chunkBoundaryMap[i] = boundary - 1; + } + } + + @Override + public void release() { + for (Chunk chunk : chunks) { + chunk.release(); + } + Arrays.fill(chunks, null); + } + + @Override + public int capacity() { + return capacity; + } + + @Override + public boolean putLong(long value) { + if (valueWriteIndex == chunks[chunkWriteIndex].buffer.limit()) { + valueWriteIndex = 0; + if (++chunkWriteIndex == chunks.length) { + return false; + } + } + chunks[chunkWriteIndex].buffer.putLong(value); + valueWriteIndex += 8; + return true; + } + + @Override + public boolean putLong(int pos, long value) { + if (pos + 8 <= capacity) { + PositionDecoder.Coordinates coordinates = positionDecoder.decode(pos, chunkBoundaryMap); + + if (coordinates.slot < chunks.length) { + chunks[coordinates.slot].buffer.putLong(coordinates.index, value); + return true; + } + } + return false; + } + + @Override + public long getLong(int pos) { + if (pos + 8 <= capacity) { + PositionDecoder.Coordinates coordinates = positionDecoder.decode(pos, chunkBoundaryMap); + + return chunks[coordinates.slot].buffer.getLong(coordinates.index); + } + return Long.MIN_VALUE; + } + + @Override + public LongIterator iterator() { + return new LongIterator() { + int valueReadIndex = 0; + int chunkReadIndex = -1; + int readBytes = 0; + byte computedHasNext = -1; + ByteBuffer currentBuffer = null; + + @Override + public boolean hasNext() { + if (computedHasNext > -1) { + return computedHasNext == 1; + } + + if (chunkReadIndex < 0) { + chunkReadIndex = 0; + currentBuffer = (ByteBuffer) chunks[chunkReadIndex].buffer.duplicate().flip(); + } + if (currentBuffer.limit() != 0) { + if (chunkReadIndex <= chunkWriteIndex) { + if (valueReadIndex >= currentBuffer.limit()) { + chunkReadIndex++; + valueReadIndex = 0; + if (chunkReadIndex < chunks.length) { + currentBuffer = (ByteBuffer) chunks[chunkReadIndex].buffer.duplicate().flip(); + if (currentBuffer.limit() != 0) { + computedHasNext = 1; + return true; + } + } + } else { + computedHasNext = 1; + return true; + } + } + } + currentBuffer = null; + computedHasNext = 0; + return false; + } + + @Override + public long next() { + long value = currentBuffer.getLong(); + valueReadIndex += 8; + readBytes += 8; + computedHasNext = -1; + return value; + } + }; + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/direct/DirectAllocator.java b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/direct/DirectAllocator.java new file mode 100644 index 00000000000..72c07a74bc2 --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/direct/DirectAllocator.java @@ -0,0 +1,271 @@ +package com.datadog.profiling.context.allocator.direct; + +import com.datadog.profiling.context.Allocator; +import com.datadog.profiling.context.allocator.AllocatedBuffer; +import datadog.trace.api.GlobalTracer; +import datadog.trace.api.StatsDClient; +import datadog.trace.api.Tracer; +import datadog.trace.relocate.api.RatelimitedLogger; +import java.lang.reflect.Field; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A naive implementation of a custom allocator using one big memory pool provided by a {@linkplain + * java.nio.DirectByteBuffer}. The allocator is splitting the provided buffer into chunks of the + * requested size and serves the allocation requests as the multiples of the chunk size.
+ * The free/occupied chunks are mapped in a single bitmap.
+ * This class is thread-safe.
+ * In order to improve the parallelism the implementation is using a version of striped locking when + * there are several subsets of chunks, each guarded by a separate lock. The number of subsets is + * set to be ~ the number of available CPU cores since there will never be more competing threads + * than that number.
+ * !!! IMPORTANT: This implementation tends to degrade in situation when the pool is almost + * exhausted. Therefore, this implementation is not suitable to be used in production as is and + * serves mostly as the starting point for a production ready version. !!! + */ +public final class DirectAllocator implements Allocator { + private static final Logger log = LoggerFactory.getLogger(DirectAllocator.class); + private static final RatelimitedLogger warnlog = new RatelimitedLogger(log, 30, TimeUnit.SECONDS); + + private static final class AllocationResult { + static final AllocationResult EMPTY = new AllocationResult(0, 0); + + final int allocatedChunks; + final int usedChunks; + + AllocationResult(int allocatedChunks, int usedChunks) { + this.allocatedChunks = allocatedChunks; + this.usedChunks = usedChunks; + } + } + + private static final int[] MASK_ARRAY = new int[8]; + + static { + int mask = 0x80; + for (int i = 0; i < 8; i++) { + MASK_ARRAY[i] = mask; + mask = mask >>> 1; + } + } + + private final StatsDClient statsDClient; + + private final ByteBuffer pool; + private final ByteBuffer memorymap; + private final int chunkSize; + private final int numChunks; + private final ReentrantLock[] memoryMapLocks; + private final int memoryMapLockCount; + private int lockSectionSize; + private final int bitmapSize; + + private final long capacity; + private final AtomicLong allocatedBytes = new AtomicLong(0); + private final AtomicInteger lockSectionOffset = new AtomicInteger(0); + + public DirectAllocator(int capacity, int chunkSize) { + log.warn( + "DirectAllocator is an experimental implementation. It should not be used in production."); + int cpus = Runtime.getRuntime().availableProcessors(); + + int targetNumChunks = (int) Math.ceil(capacity / (double) chunkSize); + this.lockSectionSize = (int) Math.ceil((targetNumChunks / (double) (cpus * 8))) * 8; + this.numChunks = + (int) (Math.ceil(targetNumChunks / (double) lockSectionSize) * lockSectionSize); + chunkSize = (int) Math.ceil(((double) capacity / (numChunks * chunkSize)) * chunkSize); + chunkSize = (int) Math.ceil(chunkSize / 8d) * 8; + int alignedCapacity = numChunks * chunkSize; + this.chunkSize = chunkSize; + this.pool = ByteBuffer.allocateDirect(alignedCapacity); + this.memorymap = ByteBuffer.allocateDirect((int) Math.ceil(numChunks / 8d)); + + this.memoryMapLockCount = (int) Math.ceil(numChunks / (double) lockSectionSize); + this.memoryMapLocks = new ReentrantLock[memoryMapLockCount]; + for (int i = 0; i < memoryMapLockCount; i++) { + memoryMapLocks[i] = new ReentrantLock(); + } + this.capacity = alignedCapacity; + this.bitmapSize = (int) Math.ceil(lockSectionSize / 8d); + StatsDClient statsd = StatsDClient.NO_OP; + try { + Tracer tracer = GlobalTracer.get(); + Field fld = tracer.getClass().getDeclaredField("statsDClient"); + fld.setAccessible(true); + statsd = (StatsDClient) fld.get(tracer); + log.debug("Set up custom StatsD Client instance {}", statsd); + } catch (Throwable t) { + t.printStackTrace(); + } + statsDClient = statsd; + } + + @Override + public int getChunkSize() { + return chunkSize; + } + + @Override + public AllocatedBuffer allocate(int bytes) { + return allocateChunks((int) Math.ceil(bytes / (double) chunkSize)); + } + + @Override + public AllocatedBuffer allocateChunks(int chunks) { + long ts = System.nanoTime(); + chunks = Math.min(chunks, numChunks); + + long delta = chunkSize * (long) chunks; + long size = allocatedBytes.addAndGet(delta); + boolean exhausted = false; + while (size > capacity) { + long overflow = size - capacity; + long newDelta = delta - overflow; + chunks = (int) (newDelta / chunkSize); + size = allocatedBytes.addAndGet(chunks * (long) chunkSize - delta); + delta = newDelta; + exhausted = delta == 0; + } + if (exhausted) { + warnlog.warn("Capacity exhausted - buffer could not be allocated"); + statsDClient.histogram("tracing.context.allocator.latency", System.nanoTime() - ts); + return null; + } else { + log.trace("Allocated {} chunks, new size={} ({})", chunks, size, this); + statsDClient.gauge("tracing.context.reserved.memory", size); + } + int lockSection = 0; + int offset = 0; + int allocated = 0; + Chunk[] chunkArray = new Chunk[chunks]; + byte[] buffer = new byte[bitmapSize]; + ReentrantLock sectionLock; + + while (allocated < chunks) { + int offsetValue = 0; + int newOffsetValue = 0; + do { + offsetValue = lockSectionOffset.get(); + newOffsetValue = + (offsetValue + 104729) + % memoryMapLocks + .length; // simple hashing using 10000th prime number and mod operation + } while (!lockSectionOffset.compareAndSet(offsetValue, newOffsetValue)); + int idx; + for (idx = 0; idx < memoryMapLocks.length; idx++) { + lockSection = offsetValue % memoryMapLocks.length; + sectionLock = null; + try { + memoryMapLocks[lockSection].lock(); + sectionLock = memoryMapLocks[lockSection]; + + int memorymapOffset = lockSection * bitmapSize; + for (int i = 0; i < bitmapSize; i++) { + buffer[i] = memorymap.get(memorymapOffset + i); + } + AllocationResult rslt = + allocateChunks(buffer, lockSection, chunkArray, chunks - allocated, offset); + offset += rslt.usedChunks; + allocated += rslt.allocatedChunks; + if (allocated == chunks) { + break; + } + } finally { + if (sectionLock != null) { + sectionLock.unlock(); + } + } + } + } + statsDClient.histogram("tracing.context.allocator.latency", System.nanoTime() - ts); + return new DirectAllocatedBuffer( + chunkSize * allocated, chunkSize, Arrays.copyOf(chunkArray, offset)); + } + + private AllocationResult allocateChunks( + byte[] bitmap, int lockSection, Chunk[] chunks, int toAllocate, int offset) { + int allocated = 0; + int chunkCounter = 0; + int overlay = 0x00; + for (int index = 0; index < bitmap.length; index++) { + int slot = bitmap[index] & 0xff; + if (slot == 0xff) { + overlay = 0; + continue; + } + if ((overlay & 0x01) != 0) { + overlay = 0x01 << 8; + } else { + overlay = 0; + } + int mask = 0x80; + int bitIndex = 0; + while (slot != 0xff && allocated < toAllocate) { + if ((slot & mask) != 0) { + bitIndex++; + mask = mask >>> 1; + continue; + } + int previousMask = mask; + int lockOffset = lockSection * (lockSectionSize / 8); + int slotOffset = lockOffset + index; + slot |= mask; + memorymap.put(slotOffset, (byte) (slot & 0xff)); + + allocated++; + overlay |= mask; + mask = mask >>> 1; + + int ref = (slotOffset * 8 + bitIndex++); + if (chunkCounter > 0) { + Chunk previous = chunks[chunkCounter + offset - 1]; + if ((overlay & (previousMask << 1)) != 0) { + // can create a contiguous chunk + previous.extend(); + continue; + } + } + pool.position(ref * chunkSize); + ByteBuffer sliced = pool.slice(); + sliced.limit(chunkSize); + chunks[chunkCounter++ + offset] = new Chunk(this, sliced, ref); + } + } + return allocated == 0 ? AllocationResult.EMPTY : new AllocationResult(allocated, chunkCounter); + } + + void release(int ref, int len) { + int chunks = len; + long delta = chunkSize * (long) len; + int offset = 0; + while (offset < len) { + int pos = (ref + offset) / 8; + int lockIndex = pos / lockSectionSize; + ReentrantLock lock = memoryMapLocks[lockIndex]; + try { + lock.lock(); + int slot = memorymap.get(pos); + int mask = 0; + int bitPos = (ref + offset) % 8; + int bitStop = Math.min(bitPos + len, 8); + for (int i = bitPos; i < bitStop; i++) { + mask |= MASK_ARRAY[i]; + } + memorymap.put(pos, (byte) ((slot & ~mask) & 0xff)); + len -= (bitStop - bitPos); + } finally { + lock.unlock(); + } + } + long size = allocatedBytes.addAndGet(-delta); + log.trace("{} allocated chunks released - new size={} ({})", chunks, size, this); + statsDClient.gauge("tracing.context.reserved.memory", size); + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/heap/HeapAllocatedBuffer.java b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/heap/HeapAllocatedBuffer.java new file mode 100644 index 00000000000..96707882456 --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/heap/HeapAllocatedBuffer.java @@ -0,0 +1,67 @@ +package com.datadog.profiling.context.allocator.heap; + +import com.datadog.profiling.context.LongIterator; +import com.datadog.profiling.context.allocator.AllocatedBuffer; +import java.nio.ByteBuffer; + +final class HeapAllocatedBuffer implements AllocatedBuffer { + private final HeapAllocator allocator; + private final ByteBuffer buffer; + + HeapAllocatedBuffer(HeapAllocator allocator, int capacity) { + this.allocator = allocator; + buffer = ByteBuffer.allocate(capacity); + } + + @Override + public void release() { + allocator.release(buffer.capacity()); + } + + @Override + public int capacity() { + return buffer.capacity(); + } + + @Override + public boolean putLong(long value) { + if (buffer.position() + 8 <= buffer.capacity()) { + buffer.putLong(value); + return true; + } + return false; + } + + @Override + public boolean putLong(int pos, long value) { + if (pos + 8 <= buffer.capacity()) { + buffer.putLong(pos, value); + return true; + } + return false; + } + + @Override + public long getLong(int pos) { + if (pos + 8 <= buffer.capacity()) { + return buffer.getLong(pos); + } + return Long.MIN_VALUE; + } + + @Override + public LongIterator iterator() { + ByteBuffer iterable = (ByteBuffer) buffer.duplicate().flip(); + return new LongIterator() { + @Override + public boolean hasNext() { + return iterable.position() < iterable.limit(); + } + + @Override + public long next() { + return iterable.getLong(); + } + }; + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/heap/HeapAllocator.java b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/heap/HeapAllocator.java new file mode 100644 index 00000000000..ab3062f927c --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/main/java/com/datadog/profiling/context/allocator/heap/HeapAllocator.java @@ -0,0 +1,97 @@ +package com.datadog.profiling.context.allocator.heap; + +import com.datadog.profiling.context.Allocator; +import com.datadog.profiling.context.allocator.AllocatedBuffer; +import datadog.trace.api.GlobalTracer; +import datadog.trace.api.StatsDClient; +import datadog.trace.api.Tracer; +import datadog.trace.relocate.api.RatelimitedLogger; +import java.lang.reflect.Field; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A straight-forward {@linkplain Allocator} implementation which satisfies the allocation requests + * by instantiating {@linkplain java.nio.HeapByteBuffer} instances. + */ +public final class HeapAllocator implements Allocator { + private static final Logger log = LoggerFactory.getLogger(HeapAllocator.class); + private static final RatelimitedLogger warnlog = new RatelimitedLogger(log, 30, TimeUnit.SECONDS); + + private final int chunkSize; + private final AtomicLong remaining; + private final long topMemory; + + private final StatsDClient statsDClient; + + public HeapAllocator(int capacity, int chunkSize) { + this.chunkSize = chunkSize; + int numChunks = (int) Math.ceil(capacity / (double) chunkSize); + this.topMemory = numChunks * (long) chunkSize; + this.remaining = new AtomicLong(topMemory); + + StatsDClient statsd = getStatsdClient(); + statsDClient = statsd; + } + + private static StatsDClient getStatsdClient() { + try { + Tracer tracer = GlobalTracer.get(); + Field fld = tracer.getClass().getDeclaredField("statsDClient"); + fld.setAccessible(true); + StatsDClient statsd = (StatsDClient) fld.get(tracer); + log.debug("Set up custom StatsD Client instance {}", statsd); + return statsd; + } catch (Throwable t) { + if (log.isDebugEnabled()) { + log.warn("Unable to obtain a StatsD client instance", t); + } else { + log.warn("Unable to obtain a StatsD client instance"); + } + return StatsDClient.NO_OP; + } + } + + @Override + public int getChunkSize() { + return chunkSize; + } + + @Override + public AllocatedBuffer allocate(int size) { + long ts = System.nanoTime(); + long newRemaining = 0; + try { + // align at chunkSize + size = (((size - 1) / chunkSize) + 1) * chunkSize; + newRemaining = remaining.addAndGet(-size); + while (newRemaining < 0) { + long restored = remaining.addAndGet(size); // restore the remaining size + size = (int) (newRemaining + size); + if (size <= chunkSize) { + warnlog.warn("Capacity exhausted - buffer could not be allocated"); + return null; + } + // align at chunkSize + size = (((size - 1) / chunkSize) + 1) * chunkSize; + newRemaining = remaining.addAndGet(-size); + } + statsDClient.gauge("tracing.context.reserved.memory", topMemory - newRemaining); + return new HeapAllocatedBuffer(this, size); + } finally { + long timeDelta = System.nanoTime() - ts; + statsDClient.histogram("tracing.context.allocator.latency", timeDelta); + } + } + + @Override + public AllocatedBuffer allocateChunks(int chunks) { + return allocate(chunks * chunkSize); + } + + void release(int size) { + remaining.addAndGet(size); + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/main/java11/com/datadog/profiling/context/JfrTimestampPatchImpl.java b/dd-java-agent/agent-profiling/profiling-context/src/main/java11/com/datadog/profiling/context/JfrTimestampPatchImpl.java new file mode 100644 index 00000000000..9f97cd56fd4 --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/main/java11/com/datadog/profiling/context/JfrTimestampPatchImpl.java @@ -0,0 +1,32 @@ +package com.datadog.profiling.context; + +import java.lang.instrument.Instrumentation; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import jdk.jfr.FlightRecorder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class JfrTimestampPatchImpl { + private static final Logger log = LoggerFactory.getLogger(JfrTimestampPatchImpl.class); + + static void execute(ClassLoader agentClassLoader) { + try { + Class installerClass = agentClassLoader.loadClass("datadog.trace.agent.tooling.Utils"); + Instrumentation instrumentation = + (Instrumentation) installerClass.getMethod("getInstrumentation").invoke(null); + Set myModules = Collections.singleton(JfrTimestampPatchImpl.class.getModule()); + Module module = FlightRecorder.class.getModule(); + instrumentation.redefineModule( + module, + Collections.emptySet(), + Map.of("jdk.jfr.internal", myModules), + Map.of("jdk.jfr.internal", myModules), + Collections.emptySet(), + Collections.emptyMap()); + } catch (Throwable t) { + log.debug("Failed to patch the access to JFR timestamp", t); + } + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/AllocatorsTest.java b/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/AllocatorsTest.java new file mode 100644 index 00000000000..7f3fb5b4cb5 --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/AllocatorsTest.java @@ -0,0 +1,169 @@ +package com.datadog.profiling.context; + +import static org.junit.jupiter.api.Assertions.*; + +import com.datadog.profiling.context.allocator.AllocatedBuffer; +import com.datadog.profiling.context.allocator.Allocators; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class AllocatorsTest { + private static final int maxChunks = 16; + private static final int chunkSize = 32; + + private static final Allocator directAllocator = + Allocators.directAllocator(chunkSize * maxChunks, chunkSize);; + private static final Allocator heapAllocator = + Allocators.heapAllocator(chunkSize * maxChunks, chunkSize); + + private AllocatedBuffer buffer = null; + + @AfterEach + void teardown() { + if (buffer != null) { + buffer.release(); + } + } + + @ParameterizedTest + @MethodSource("allocators") + void testAllocations(Allocator allocator) { + AllocatedBuffer buffer1 = null; + AllocatedBuffer buffer2 = null; + AllocatedBuffer buffer3 = null; + try { + buffer1 = allocator.allocateChunks(maxChunks - 2); + buffer2 = allocator.allocate(chunkSize); + buffer3 = allocator.allocate(chunkSize / 2); + buffer = allocator.allocate(chunkSize / 4); + + assertNotNull(buffer1); + assertNotNull(buffer2); + assertNotNull(buffer3); + assertNull(buffer); + + assertEquals(chunkSize * (maxChunks - 2), buffer1.capacity()); + assertEquals(chunkSize, buffer2.capacity()); + assertEquals(chunkSize, buffer3.capacity()); + } finally { + if (buffer1 != null) { + buffer1.release(); + } + if (buffer2 != null) { + buffer2.release(); + } + if (buffer3 != null) { + buffer3.release(); + } + } + } + + private static Stream allocators() { + return Stream.of(Arguments.of(heapAllocator), Arguments.of(directAllocator)); + } + + @Test + void testOverallocationDirect() { + buffer = directAllocator.allocateChunks(maxChunks); + assertNotNull(buffer); + assertNull(directAllocator.allocate(10)); + } + + @Test + void testOverallocationHeap() { + buffer = heapAllocator.allocateChunks(maxChunks); + assertNotNull(buffer); + assertNull(heapAllocator.allocate(10)); + } + + @Test + void testDirectBufferIterator() { + buffer = directAllocator.allocate(512); + + LongIterator iterator = buffer.iterator(); + assertFalse(iterator.hasNext()); + assertFalse(iterator.hasNext()); + + buffer.putLong(0L); + iterator = buffer.iterator(); + assertTrue(iterator.hasNext()); + assertTrue(iterator.hasNext()); + + iterator.next(); + assertFalse(iterator.hasNext()); + + // fill in data spanning multiple chunks + // there is already 1 value stored so we need to add only chunkSize other values + for (int i = 0; i < chunkSize; i++) { + buffer.putLong(i + 1); + } + iterator = buffer.iterator(); + int cnt = 0; + while (iterator.hasNext()) { + assertNotNull(iterator.next()); + cnt++; + } + assertEquals(chunkSize + 1, cnt); + } + + @Test + void testHeapBufferIterator() { + buffer = heapAllocator.allocate(256); + + LongIterator iterator = buffer.iterator(); + assertFalse(iterator.hasNext()); + assertFalse(iterator.hasNext()); + + buffer.putLong(0L); + iterator = buffer.iterator(); + assertTrue(iterator.hasNext()); + assertTrue(iterator.hasNext()); + + iterator.next(); + assertFalse(iterator.hasNext()); + } + + @ParameterizedTest + @MethodSource("allocateBufferTestParams") + void testAllocatedBuffer(Allocator allocator, int numChunks, int skid) { + buffer = + skid == 0 + ? allocator.allocateChunks(numChunks) + : allocator.allocate(chunkSize * numChunks + skid); + int iterations = (numChunks * chunkSize) / 8; + for (int i = 0; i < iterations; i++) { + assertTrue(buffer.putLong(i)); + } + assertTrue(numChunks * chunkSize + skid <= buffer.capacity()); + + for (int i = 0; i < numChunks; i++) { + assertTrue(buffer.putLong(i * chunkSize, 5)); + assertEquals(5, buffer.getLong(i * chunkSize)); + } + + if (skid == 0) { + assertFalse(buffer.putLong(6)); + } else { + assertTrue(buffer.putLong(6)); + } + assertFalse(buffer.putLong(buffer.capacity() - 7, 6)); + + assertEquals(Long.MIN_VALUE, buffer.getLong(buffer.capacity() - 7)); + } + + private static Stream allocateBufferTestParams() { + return Stream.of( + Arguments.of(directAllocator, 1, 0), + Arguments.of(directAllocator, 2, 0), + Arguments.of(directAllocator, 1, 1), + Arguments.of(directAllocator, 2, 1), + Arguments.of(heapAllocator, 1, 0), + Arguments.of(heapAllocator, 2, 0), + Arguments.of(heapAllocator, 1, 1), + Arguments.of(heapAllocator, 2, 1)); + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/IntervalEncoderTest.java b/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/IntervalEncoderTest.java new file mode 100644 index 00000000000..2a47d26d212 --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/IntervalEncoderTest.java @@ -0,0 +1,84 @@ +package com.datadog.profiling.context; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.ByteBuffer; +import java.time.Instant; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class IntervalEncoderTest { + private Instant now; + private IntervalEncoder instance; + + @BeforeEach + void setup() { + now = Instant.now(); + instance = new IntervalEncoder(now.toEpochMilli(), 1000, 2, 32); + } + + @Test + void testFinishUnstarted() { + assertNotNull(instance.finish()); + } + + @Test + void testFinishThreadInFlight() { + IntervalEncoder.ThreadEncoder encoder = instance.startThread(1); + assertThrows(IllegalStateException.class, instance::finish); + } + + @Test + void testDoubleFinish() { + instance.finish(); + assertThrows(IllegalStateException.class, instance::finish); + } + + @Test + void testStartTooManyThreads() { + instance.startThread(1).finish(); + instance.startThread(2).finish(); + assertThrows(IllegalStateException.class, () -> instance.startThread(3)); + } + + @Test + void testStartMultiThreads() { + instance.startThread(1); + assertThrows(IllegalStateException.class, () -> instance.startThread(2)); + + IntervalEncoder instance1 = new IntervalEncoder(System.currentTimeMillis(), 1000, 1, 100); + instance1.startThread(1); + assertThrows(IllegalStateException.class, () -> instance1.startThread(2)); + } + + @Test + void testEncodeSequence() { + IntervalEncoder.ThreadEncoder encoder = instance.startThread(1); + encoder.recordInterval(200, 300); + encoder.recordInterval(500, 600); + ByteBuffer data = encoder.finish().finish(); + assertNotNull(data); + assertTrue(data.limit() > 0); + + List intervals = new IntervalParser().parseIntervals(data.array()); + IntervalParser.Interval i1 = intervals.get(0); + assertEquals(now.toEpochMilli() * 1_000_000L + 200, i1.from); + } + + @Test + void testIntervalAfterFinish() { + IntervalEncoder.ThreadEncoder encoder = instance.startThread(1); + encoder.recordInterval(200, 300); + encoder.finish(); + assertThrows(IllegalStateException.class, () -> encoder.recordInterval(500, 600)); + } + + @Test + void testThreadDoubleFinish() { + IntervalEncoder.ThreadEncoder encoder = instance.startThread(1); + encoder.recordInterval(200, 300); + encoder.finish(); + assertThrows(IllegalStateException.class, encoder::finish); + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/IntervalParser.java b/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/IntervalParser.java new file mode 100644 index 00000000000..090a82d2b4f --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/IntervalParser.java @@ -0,0 +1,138 @@ +package com.datadog.profiling.context; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public final class IntervalParser { + public static final class Interval { + public final long threadId; + public final long from; + public final long till; + + public Interval(long threadId, long from, long till) { + this.threadId = threadId; + this.from = from; + this.till = till; + } + } + + private static class GroupVarintIterator { + private final ByteBuffer dataBuffer; + private final ByteBuffer bitmapBuffer; + + private int maskIndex = 0; + private int maskOffset = 0; + + GroupVarintIterator(ByteBuffer dataBuffer) { + int index = dataBuffer.getInt(); + this.dataBuffer = dataBuffer.slice(); + this.bitmapBuffer = slice(dataBuffer, index); + } + + private static ByteBuffer slice(ByteBuffer src, int index) { + /* + ByteBuffer.slice(index, size) is available only from JDK 13 + We need to emulate that by storing current position, setting the position to index, slicing and restoring the original position + */ + int currentPos = src.position(); + try { + src.position(index); + return src.slice(); + } finally { + src.position(currentPos); + } + } + + boolean hasNext() { + return dataBuffer.position() < dataBuffer.capacity() && maskIndex < bitmapBuffer.capacity(); + } + + long next() { + int mask = bitmapBuffer.getInt(maskIndex); + int size = ((mask >>> (29 - maskOffset)) & 0x7) + 1; + if ((maskOffset += 3) > 21) { + maskIndex += 3; + maskOffset = 0; + } + long value = 0L; + for (int i = 0; i < size; i++) { + int item = (int) dataBuffer.get() & 0xff; + value = (value << 8) + item; + } + return value; + } + } + + public List parseIntervals(byte[] intervalData) { + List result = new ArrayList<>(); + + parseIntervals( + intervalData, + interval -> { + result.add(interval); + return true; + }); + return result; + } + + public interface IntervalConsumer { + boolean accept(Interval interval); + } + + public void parseIntervals(byte[] intervalData, IntervalConsumer consumer) { + ByteBuffer buffer = ByteBuffer.wrap(intervalData); + int chunkDataOffset = buffer.getInt(); + long timestampMillis = getVarint(buffer); + long frequencyMultiplier = getVarint(buffer); + int numThreads = (int) getVarint(buffer); + + ByteBuffer dataChunk = + ByteBuffer.wrap(intervalData, chunkDataOffset, intervalData.length - chunkDataOffset) + .slice(); + + GroupVarintIterator iterator = new GroupVarintIterator(dataChunk); + for (int thread = 0; thread < numThreads; thread++) { + long previousTimestamp = 0; + long threadId = getVarint(buffer); + int intervals = (int) getVarint(buffer); + for (int interval = 0; interval < intervals; interval++) { + if (iterator.hasNext()) { + long startTsDelta = iterator.next(); + long endTsDelta = iterator.next(); + long startTs = previousTimestamp + startTsDelta; + long endTs = startTs + endTsDelta; + if (!consumer.accept( + new Interval( + threadId, + toEpochNanos(timestampMillis, startTs, frequencyMultiplier), + toEpochNanos(timestampMillis, endTs, frequencyMultiplier)))) { + // consumer not interested in the rest of the data + return; + } + previousTimestamp = endTs; + } else { + throw new IllegalStateException(); + } + } + } + } + + private static long toEpochNanos(long startMs, long ts, long frequencyMultiplier) { + return TimeUnit.NANOSECONDS.convert(startMs, TimeUnit.MILLISECONDS) + + ((ts * 1000) / frequencyMultiplier); + } + + private long getVarint(ByteBuffer buffer) { + long ret = 0; + for (int i = 0; i < 8; i++) { + byte b = buffer.get(); + ret += (long) ((b & 0x7FL)) << (7 * i); + if (b >= 0) { + return ret; + } + } + return ret + ((buffer.get() & 0xFFL) << 56); + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/IntervalSequencePrunerTest.java b/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/IntervalSequencePrunerTest.java new file mode 100644 index 00000000000..a6c90074533 --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/IntervalSequencePrunerTest.java @@ -0,0 +1,286 @@ +package com.datadog.profiling.context; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.datadog.profiling.context.allocator.Allocators; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class IntervalSequencePrunerTest { + private IntervalSequencePruner instance; + + @BeforeEach + void setup() { + instance = new IntervalSequencePruner(); + } + + @Test + void testPruneStartStop() { + LongSequence ls = new LongSequence(Allocators.heapAllocator(1024, 32)); + long start = ProfilerTracingContextTracker.maskActivation(100); + long stop = ProfilerTracingContextTracker.maskDeactivation(200, false); + ls.add(start); + ls.add(stop); + + LongIterator iterator = instance.pruneIntervals(ls, 0); + List values = new ArrayList<>(); + while (iterator.hasNext()) { + assertTrue(iterator.hasNext()); + values.add(iterator.next()); + } + assertFalse(iterator.hasNext()); + assertEquals(Arrays.asList(start, stop), values); + } + + @Test + void testPruneMaybeStop() { + LongSequence ls = new LongSequence(Allocators.heapAllocator(1024, 32)); + long start = ProfilerTracingContextTracker.maskActivation(100); + long maybeStop = ProfilerTracingContextTracker.maskDeactivation(200, true); + ls.add(start); + ls.add(maybeStop); + + LongIterator iterator = instance.pruneIntervals(ls, 0); + List values = new ArrayList<>(); + while (iterator.hasNext()) { + assertTrue(iterator.hasNext()); + values.add(iterator.next()); + } + assertFalse(iterator.hasNext()); + assertEquals(Arrays.asList(start, maybeStop), values); + } + + @Test + void testPruneMaybeStopWithStop() { + LongSequence ls = new LongSequence(Allocators.heapAllocator(1024, 32)); + long start = ProfilerTracingContextTracker.maskActivation(100); + long maybeStop = ProfilerTracingContextTracker.maskDeactivation(200, true); + long stop = ProfilerTracingContextTracker.maskDeactivation(300, false); + + ls.add(start); + ls.add(maybeStop); + ls.add(stop); + + LongIterator iterator = instance.pruneIntervals(ls, 0); + List values = new ArrayList<>(); + while (iterator.hasNext()) { + assertTrue(iterator.hasNext()); + values.add(iterator.next()); + } + assertFalse(iterator.hasNext()); + assertEquals(Arrays.asList(start, stop), values); + } + + @Test + void testPruneMaybeStopWithRestartAndStop() { + LongSequence ls = new LongSequence(Allocators.heapAllocator(1024, 32)); + long start = ProfilerTracingContextTracker.maskActivation(100); + long maybeStop = ProfilerTracingContextTracker.maskDeactivation(200, true); + long restart = ProfilerTracingContextTracker.maskActivation(300); + long stop = ProfilerTracingContextTracker.maskDeactivation(400, false); + + ls.add(start); + ls.add(maybeStop); + ls.add(restart); + ls.add(stop); + + LongIterator iterator = instance.pruneIntervals(ls, 0); + List values = new ArrayList<>(); + while (iterator.hasNext()) { + assertTrue(iterator.hasNext()); + values.add(iterator.next()); + } + assertFalse(iterator.hasNext()); + assertEquals(Arrays.asList(start, stop), values); + } + + @Test + void testPruneMaybeStopWithRestartDangling() { + LongSequence ls = new LongSequence(Allocators.heapAllocator(1024, 32)); + long start = ProfilerTracingContextTracker.maskActivation(100); + long maybeStop = ProfilerTracingContextTracker.maskDeactivation(200, true); + long restart = ProfilerTracingContextTracker.maskActivation(300); + + ls.add(start); + ls.add(maybeStop); + ls.add(restart); + + long finishTs = 400; + LongIterator iterator = instance.pruneIntervals(ls, finishTs); + List values = new ArrayList<>(); + while (iterator.hasNext()) { + assertTrue(iterator.hasNext()); + values.add(iterator.next()); + } + assertFalse(iterator.hasNext()); + // the deactivation at 'finishTs' is added by the pruning not to have a dangling start + assertEquals( + Arrays.asList(start, ProfilerTracingContextTracker.maskDeactivation(finishTs, false)), + values); + } + + @Test + void testPruneMaybeStopAfterStop() { + LongSequence ls = new LongSequence(Allocators.heapAllocator(1024, 32)); + long start = ProfilerTracingContextTracker.maskActivation(100); + long stop = ProfilerTracingContextTracker.maskDeactivation(200, false); + long maybeStop = ProfilerTracingContextTracker.maskDeactivation(300, true); + + ls.add(start); + ls.add(stop); + ls.add(maybeStop); + + LongIterator iterator = instance.pruneIntervals(ls, 0); + List values = new ArrayList<>(); + while (iterator.hasNext()) { + assertTrue(iterator.hasNext()); + values.add(iterator.next()); + } + assertFalse(iterator.hasNext()); + assertEquals(Arrays.asList(start, maybeStop), values); + } + + @Test + void testPruneStartAfterMultiStop() { + LongSequence ls = new LongSequence(Allocators.heapAllocator(1024, 32)); + long start = ProfilerTracingContextTracker.maskActivation(100); + long stop1 = ProfilerTracingContextTracker.maskDeactivation(200, false); + long stop2 = ProfilerTracingContextTracker.maskDeactivation(300, false); + long stop3 = ProfilerTracingContextTracker.maskDeactivation(400, false); + long start1 = ProfilerTracingContextTracker.maskActivation(500); + long stop4 = ProfilerTracingContextTracker.maskDeactivation(600, false); + + ls.add(start); + ls.add(stop1); + ls.add(stop2); + ls.add(stop3); + ls.add(start1); + ls.add(stop4); + + LongIterator iterator = instance.pruneIntervals(ls, 0); + List values = new ArrayList<>(); + while (iterator.hasNext()) { + assertTrue(iterator.hasNext()); + values.add(iterator.next()); + } + assertFalse(iterator.hasNext()); + assertEquals(Arrays.asList(start, stop3, start1, stop4), values); + } + + @Test + void testPruneMaybeStopAfterStopMulti() { + LongSequence ls = new LongSequence(Allocators.heapAllocator(1024, 32)); + long start = ProfilerTracingContextTracker.maskActivation(100); + long stop = ProfilerTracingContextTracker.maskDeactivation(200, false); + long maybeStop = ProfilerTracingContextTracker.maskDeactivation(300, true); + long stop1 = ProfilerTracingContextTracker.maskDeactivation(400, false); + long stop2 = ProfilerTracingContextTracker.maskDeactivation(500, false); + + ls.add(start); + ls.add(stop); + ls.add(maybeStop); + ls.add(stop1); + ls.add(stop2); + + LongIterator iterator = instance.pruneIntervals(ls, 0); + List values = new ArrayList<>(); + while (iterator.hasNext()) { + assertTrue(iterator.hasNext()); + values.add(iterator.next()); + } + assertFalse(iterator.hasNext()); + assertEquals(Arrays.asList(start, stop2), values); + } + + @Test + void testPruneMultiStartStop() { + LongSequence ls = new LongSequence(Allocators.heapAllocator(1024, 32)); + long start = ProfilerTracingContextTracker.maskActivation(100); + long start1 = ProfilerTracingContextTracker.maskActivation(200); + long stop = ProfilerTracingContextTracker.maskDeactivation(300, false); + + ls.add(start); + ls.add(start1); + ls.add(stop); + + LongIterator iterator = instance.pruneIntervals(ls, 0); + List values = new ArrayList<>(); + while (iterator.hasNext()) { + assertTrue(iterator.hasNext()); + values.add(iterator.next()); + } + assertFalse(iterator.hasNext()); + assertEquals(Arrays.asList(start, stop), values); + } + + @Test + void testPruneMaybeStopMulti() { + LongSequence ls = new LongSequence(Allocators.heapAllocator(1024, 32)); + long start = ProfilerTracingContextTracker.maskActivation(100); + long maybeStop = ProfilerTracingContextTracker.maskDeactivation(200, true); + long maybeStop2 = ProfilerTracingContextTracker.maskDeactivation(300, true); + long maybeStop3 = ProfilerTracingContextTracker.maskDeactivation(400, true); + + ls.add(start); + ls.add(maybeStop); + ls.add(maybeStop2); + ls.add(maybeStop3); + + LongIterator iterator = instance.pruneIntervals(ls, 0); + List values = new ArrayList<>(); + while (iterator.hasNext()) { + assertTrue(iterator.hasNext()); + values.add(iterator.next()); + } + assertFalse(iterator.hasNext()); + assertEquals(Arrays.asList(start, maybeStop3), values); + } + + @Test + void testPruneStopWithNoStart() { + LongSequence ls = new LongSequence(Allocators.heapAllocator(1024, 32)); + long stop = ProfilerTracingContextTracker.maskDeactivation(100, false); + + ls.add(stop); + + LongIterator iterator = instance.pruneIntervals(ls, 0); + assertFalse(iterator.hasNext()); + } + + @Test + void testPruneMaybeStopWithNoStart() { + LongSequence ls = new LongSequence(Allocators.heapAllocator(1024, 32)); + long maybeStop = ProfilerTracingContextTracker.maskDeactivation(100, true); + + ls.add(maybeStop); + + LongIterator iterator = instance.pruneIntervals(ls, 0); + assertFalse(iterator.hasNext()); + } + + @Test + void testPruneStartWithNoStop() { + LongSequence ls = new LongSequence(Allocators.heapAllocator(1024, 32)); + long start = ProfilerTracingContextTracker.maskActivation(100); + long stopTs = 200; + + ls.add(start); + + LongIterator iterator = instance.pruneIntervals(ls, stopTs); + List values = new ArrayList<>(); + while (iterator.hasNext()) { + assertTrue(iterator.hasNext()); + values.add(iterator.next()); + } + + assertFalse(iterator.hasNext()); + assertEquals( + Arrays.asList(start, ProfilerTracingContextTracker.maskDeactivation(stopTs, false)), + values); + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/LEB128SupportTest.java b/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/LEB128SupportTest.java new file mode 100644 index 00000000000..42da5c9d45d --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/LEB128SupportTest.java @@ -0,0 +1,75 @@ +package com.datadog.profiling.context; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.ByteBuffer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class LEB128SupportTest { + private LEB128Support instance; + + @BeforeEach + void setup() { + instance = new LEB128Support(); + } + + @Test + void align() { + assertEquals(0, instance.align(0, 3)); + assertEquals(3, instance.align(1, 3)); + assertEquals(3, instance.align(3, 3)); + } + + @Test + void varintSize() { + ByteBuffer buffer = ByteBuffer.allocate(10); + long value = 0L; + instance.putVarint(buffer, value); + int size = instance.varintSize(value); + assertEquals(1, size); + assertEquals(buffer.position(), size); + + buffer.rewind(); + value = 1L; + instance.putVarint(buffer, value); + size = instance.varintSize(value); + assertEquals(1, size); + assertEquals(buffer.position(), size); + + value = 0x40; + for (int i = 1; i < 10; i++) { + size = instance.varintSize(value); + buffer.rewind(); + instance.putVarint(buffer, value); + assertEquals(i, size); + assertEquals(buffer.position(), size); + + if (i == 9) { + value = value << 1; + size = instance.varintSize(value); + buffer.rewind(); + instance.putVarint(buffer, value); + assertEquals(i, size); + assertEquals(buffer.position(), size); + } else { + value = value << 7; + } + } + } + + @Test + void longSize() { + long value = 0L; + int size = instance.longSize(value); + assertEquals(1, size); + + value = 0x80; + for (int i = 1; i < 9; i++) { + size = instance.longSize(value); + assertEquals(i, size); + + value = value << 8; + } + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/LongSequenceTest.java b/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/LongSequenceTest.java new file mode 100644 index 00000000000..42e90866f0f --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/LongSequenceTest.java @@ -0,0 +1,96 @@ +package com.datadog.profiling.context; + +import static org.junit.jupiter.api.Assertions.*; + +import com.datadog.profiling.context.allocator.Allocators; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class LongSequenceTest { + private LongSequence instance; + + @BeforeEach + void setup() throws Exception { + instance = new LongSequence(Allocators.heapAllocator(512, 32)); + } + + @Test + void testCapacity() { + int items = 64; + for (int i = 0; i < items; i++) { + try { + assertTrue(instance.add(i) > 0); + } catch (Throwable t) { + System.out.println("===> " + i); + throw t; + } + } + assertFalse(instance.add(65) > 0); + assertEquals(items, instance.size()); + LongIterator iterator = instance.iterator(); + long value = 0; + while (iterator.hasNext()) { + long retrieved = iterator.next(); + assertEquals(value++, retrieved); + } + } + + @Test + void testDoubleRelease() { + instance.release(); + instance.release(); + } + + @Test + void testPositionalSetGet() { + for (int i = 1; i <= 3; i++) { + instance.add(i); + } + + for (int i = 1; i <= 3; i++) { + assertEquals(i, instance.get(i - 1)); + instance.set(i - 1, 2 * i); + assertEquals(2 * i, instance.get(i - 1)); + } + } + + @Test + void testGetInvalidIndex() { + assertEquals(Long.MIN_VALUE, instance.get(5)); + assertEquals(Long.MIN_VALUE, instance.get(65)); + } + + @Test + void testSetInvalidIndex() { + assertFalse(instance.set(65, 0L)); + } + + @Test + void testIteratorInvalid() { + LongIterator iterator = instance.iterator(); + assertThrows(IllegalStateException.class, iterator::next); + } + + @Test + void testIteratorEmpty() { + LongIterator iterator = instance.iterator(); + assertFalse(iterator.hasNext()); + } + + @Test + void testIterator() { + // fill in data + for (int i = 0; i < 64; i++) { + instance.add(i); + } + + long expected = 0; + LongIterator iterator = instance.iterator(); + while (iterator.hasNext()) { + assertTrue(iterator.hasNext()); + assertEquals(expected++, iterator.next()); + } + assertEquals(expected, instance.size()); + assertFalse(iterator.hasNext()); + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/PositionDecoderTest.java b/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/PositionDecoderTest.java new file mode 100644 index 00000000000..2972a1a6e0c --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/PositionDecoderTest.java @@ -0,0 +1,69 @@ +package com.datadog.profiling.context; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class PositionDecoderTest { + private PositionDecoder instance; + + @BeforeEach + void setup() { + instance = PositionDecoder.getInstance(); + } + + @Test + void decodeInvalidCapacityMap() { + int[] map = new int[0]; + + assertNull(instance.decode(0, map, -1)); + assertNull(instance.decode(0, map, 0)); + assertNull(instance.decode(0, map, 1)); + } + + @Test + void decodeInvalidCapacityMapSize() { + int[] map = new int[] {3, 10, 25}; + + assertNull(instance.decode(0, map, -1)); + assertNull(instance.decode(0, map, map.length + 1)); + } + + @Test + void decodeValid() { + int maxBoundary = 12; + int[] map = new int[] {2, 4, 6, 8, 10, maxBoundary}; + PositionDecoder.Coordinates[] expected = + new PositionDecoder.Coordinates[] { + new PositionDecoder.Coordinates(0, 0), + new PositionDecoder.Coordinates(0, 1), + new PositionDecoder.Coordinates(0, 2), + new PositionDecoder.Coordinates(1, 0), + new PositionDecoder.Coordinates(1, 1), + new PositionDecoder.Coordinates(2, 0), + new PositionDecoder.Coordinates(2, 1), + new PositionDecoder.Coordinates(3, 0), + new PositionDecoder.Coordinates(3, 1), + new PositionDecoder.Coordinates(4, 0), + new PositionDecoder.Coordinates(4, 1), + new PositionDecoder.Coordinates(5, 0), + new PositionDecoder.Coordinates(5, 1) + }; + + for (int mapIndex = map.length - 1; mapIndex >= 0; mapIndex--) { + int limit = map[mapIndex]; + for (int pos = 0; pos <= limit; pos++) { + assertEquals( + expected[pos], + instance.decode(pos, map, mapIndex + 1), + "Failed to decode position " + + pos + + " with section boundary limit set to " + + (mapIndex + 1)); + } + } + + assertNull(instance.decode(map[map.length - 1] + 1, map)); + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/ProfilerTracingContextTrackerFactoryTest.java b/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/ProfilerTracingContextTrackerFactoryTest.java new file mode 100644 index 00000000000..2f25394f957 --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/ProfilerTracingContextTrackerFactoryTest.java @@ -0,0 +1,68 @@ +package com.datadog.profiling.context; + +import static org.junit.jupiter.api.Assertions.*; + +import datadog.trace.api.profiling.TracingContextTracker; +import datadog.trace.api.profiling.TracingContextTrackerFactory; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class ProfilerTracingContextTrackerFactoryTest { + @Test + void testTracingAvailable() { + assertFalse(TracingContextTrackerFactory.isTrackingAvailable()); + + TracingContextTrackerFactory.registerImplementation( + new ProfilerTracingContextTrackerFactory( + TimeUnit.NANOSECONDS.convert(100, TimeUnit.MILLISECONDS), 10L, 512)); + assertTrue(TracingContextTrackerFactory.isTrackingAvailable()); + } + + @Test + void testReleasedAfterInactivity() throws Exception { + long inactivityMs = 100; + ProfilerTracingContextTrackerFactory instance = + new ProfilerTracingContextTrackerFactory( + TimeUnit.NANOSECONDS.convert(inactivityMs, TimeUnit.MILLISECONDS), 10L, 512); + TracingContextTracker tracker1 = instance.instance(null); + TracingContextTracker tracker2 = instance.instance(null); + + Thread.sleep((long) (inactivityMs * 1.5d)); + assertFalse( + tracker1.release(), "Tracker resources were not released within " + inactivityMs + "ms"); + assertFalse( + tracker2.release(), "Tracker resources were not released within " + inactivityMs + "ms"); + } + + @Test + void testNotReleasedBeforeInactivity() throws Exception { + long inactivityMs = 100; + ProfilerTracingContextTrackerFactory instance = + new ProfilerTracingContextTrackerFactory( + TimeUnit.NANOSECONDS.convert(inactivityMs, TimeUnit.MILLISECONDS), 10L, 512); + TracingContextTracker tracker1 = instance.instance(null); + TracingContextTracker tracker2 = instance.instance(null); + + Thread.sleep((long) (inactivityMs * 0.5d)); + assertTrue( + tracker1.release(), + "Tracker resources were erroneously released before " + inactivityMs + "ms"); + assertTrue( + tracker2.release(), + "Tracker resources were erroneously released before " + inactivityMs + "ms"); + } + + @Test + void testNoInactivityRelease() throws Exception { + long inactivityMs = 0; + ProfilerTracingContextTrackerFactory instance = + new ProfilerTracingContextTrackerFactory( + TimeUnit.NANOSECONDS.convert(inactivityMs, TimeUnit.MILLISECONDS), 10L, 512); + TracingContextTracker tracker1 = instance.instance(null); + TracingContextTracker tracker2 = instance.instance(null); + + Thread.sleep((long) (inactivityMs * 1.5d)); + assertTrue(tracker1.release(), "Tracker resources are erroneously released"); + assertTrue(tracker2.release(), "Tracker resources are erroneously released"); + } +} diff --git a/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/ProfilerTracingContextTrackerTest.java b/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/ProfilerTracingContextTrackerTest.java new file mode 100644 index 00000000000..0c3f836f559 --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-context/src/test/java/com/datadog/profiling/context/ProfilerTracingContextTrackerTest.java @@ -0,0 +1,212 @@ +package com.datadog.profiling.context; + +import static org.junit.jupiter.api.Assertions.*; + +import com.datadog.profiling.context.allocator.Allocators; +import datadog.trace.api.profiling.TracingContextTracker; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Base64; +import java.util.List; +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +class ProfilerTracingContextTrackerTest { + private static final class TestTimeTickProvider + implements ProfilerTracingContextTracker.TimeTicksProvider { + private final long step; + private final long frequency; + private final AtomicLong ts = new AtomicLong(0); + + public TestTimeTickProvider(long initial, long step, long frequency) { + this.ts.set(initial); + this.step = step; + this.frequency = frequency; + } + + @Override + public long ticks() { + return ts.getAndAdd(step); + } + + @Override + public long frequency() { + return frequency; + } + + public void set(long ticks) { + ts.set(ticks); + } + } + + private final IntervalSequencePruner sequencePruner = new IntervalSequencePruner(); + private ProfilerTracingContextTracker instance; + + @BeforeEach + void setup() throws Exception { + ProfilerTracingContextTracker.TimeTicksProvider ticker = + new TestTimeTickProvider(100_000L, 1000L, 1_000_000_000L); + instance = + new ProfilerTracingContextTracker( + Allocators.heapAllocator(32, 16), null, ticker, sequencePruner); + } + + @Test + void deactivateContext() {} + + @Test + void persist() { + TestTimeTickProvider tickProvider = new TestTimeTickProvider(100_000L, 3, 1_000_000_000L); + instance = + new ProfilerTracingContextTracker( + Allocators.directAllocator(8192, 64), null, tickProvider, sequencePruner); + for (int i = 0; i < 40; i += 4) { + instance.activateContext(1L, (i + 1) * 1_000_000L); + instance.deactivateContext(1L, (i + 2) * 1_000_000L, false); + instance.activateContext(2L, (i + 3) * 1_000_000L); + instance.deactivateContext(2L, (i + 4) * 1_000_000L, true); + } + // set the timestamp after the last transition's timestamp + tickProvider.set((44 * 1_000_000L) + 1); + + byte[] persisted = instance.persist(); + assertNotNull(persisted); + + List intervals = new IntervalParser().parseIntervals(persisted); + assertEquals(11, intervals.size()); + + byte[] encoded = Base64.getEncoder().encode(persisted); + System.out.println("===> encoded: " + encoded.length); + System.err.println("===> " + new String(encoded, StandardCharsets.UTF_8)); + } + + @Test + void testSanity() { + ProfilerTracingContextTrackerFactory instance = + new ProfilerTracingContextTrackerFactory(-1, 10L, 512); + + ProfilerTracingContextTracker tracker = + (ProfilerTracingContextTracker) instance.instance(AgentTracer.NoopAgentSpan.INSTANCE); + AtomicInteger blobCounter = new AtomicInteger(); + + tracker.activateContext(); + tracker.maybeDeactivateContext(); + tracker.deactivateContext(); + byte[] data1 = tracker.persist(); + assertNotNull(data1); + byte[] data2 = tracker.persist(); + assertNotNull(data2); + assertArrayEquals(data1, data2); + + assertTrue(tracker.release()); + assertArrayEquals(data1, tracker.persist()); // previously persisted data survives release + assertFalse(tracker.release()); // double release returns 'false' + assertArrayEquals( + data1, tracker.persist()); // previously persisted data survives double-release + } + + @Test + void delayedCompareToTest() { + ProfilerTracingContextTrackerFactory instance = + new ProfilerTracingContextTrackerFactory(-1, 10L, 512); + + ProfilerTracingContextTracker tracker1 = + (ProfilerTracingContextTracker) instance.instance(AgentTracer.NoopAgentSpan.INSTANCE); + ProfilerTracingContextTracker tracker2 = + (ProfilerTracingContextTracker) instance.instance(AgentTracer.NoopAgentSpan.INSTANCE); + + TracingContextTracker.DelayedTracker delayed1 = tracker1.asDelayed(); + TracingContextTracker.DelayedTracker delayed2 = tracker2.asDelayed(); + + Delayed other = + new Delayed() { + @Override + public long getDelay(TimeUnit unit) { + return 0; + } + + @Override + public int compareTo(Delayed o) { + return 0; + } + }; + + assertEquals(0, delayed1.compareTo(other)); + assertEquals(0, delayed1.compareTo(delayed1)); + + tracker2.activateContext(); + assertEquals(-1, delayed1.compareTo(delayed2)); + assertEquals(1, delayed2.compareTo(delayed1)); + } + + @Test + void delayedCachingTest() { + ProfilerTracingContextTrackerFactory instance = + new ProfilerTracingContextTrackerFactory(-1, 10L, 512); + + ProfilerTracingContextTracker tracker1 = + (ProfilerTracingContextTracker) instance.instance(AgentTracer.NoopAgentSpan.INSTANCE); + ProfilerTracingContextTracker tracker2 = + (ProfilerTracingContextTracker) instance.instance(AgentTracer.NoopAgentSpan.INSTANCE); + + TracingContextTracker.DelayedTracker delayed1 = tracker1.asDelayed(); + TracingContextTracker.DelayedTracker delayed2 = tracker2.asDelayed(); + + assertEquals(delayed1, tracker1.asDelayed()); + assertEquals(delayed2, tracker2.asDelayed()); + } + + @Test + void delayedReleaseTest() { + ProfilerTracingContextTrackerFactory instance = + new ProfilerTracingContextTrackerFactory(-1, 10L, 512); + + ProfilerTracingContextTracker tracker1 = + (ProfilerTracingContextTracker) instance.instance(AgentTracer.NoopAgentSpan.INSTANCE); + ProfilerTracingContextTracker tracker2 = + (ProfilerTracingContextTracker) instance.instance(AgentTracer.NoopAgentSpan.INSTANCE); + + TracingContextTracker.DelayedTracker delayed1 = tracker1.asDelayed(); + TracingContextTracker.DelayedTracker delayed2 = tracker2.asDelayed(); + + // make sure the tracker release will remove the reference from delayed to the tracker + tracker1.release(); + assertNull(((ProfilerTracingContextTracker.DelayedTrackerImpl) delayed1).ref); + assertNotEquals(delayed1, tracker1.asDelayed()); + + // make sure that the delayed cleanup will also trigger the tracker release + delayed2.cleanup(); + assertNull(((ProfilerTracingContextTracker.DelayedTrackerImpl) delayed2).ref); + assertNotEquals(delayed2, tracker2.asDelayed()); + } + + @Test + @Disabled + // a handy test to try deserializing data from spans + void testPersisted() { + String[] encodedData = + new String[] { + "AAAAI6SK7u6LAcG57+//L7gXB4IDGEYITQFPAVEBUwHgAQEAAACvCi3WjrSyLhomSh0EAQl0Lpo45iXmARamNA4qQB9qOIQw/iiKJQ5shA7WC/4dMh2mO2wXaBfGKVAXWDwQFF4eSismAQeqAjJMMl4kIg3wGSxaUBRERkjb8CSsJC48VCJqFuAMolksb/TEAVe8JXuksSoCrFqEErM8BmcapgmfBDbYWgAf/ix8C4IM7AkKFbyEhigRaODq3X69tgLxcRxQLgMaybJTKgLqZGg+ZJKJKJJJJJJJJJJKRJJJJJJJSRaaJJJJSRZZKAAAAA" + }; + for (String encoded : encodedData) { + System.out.println("===="); + byte[] decoded = Base64.getDecoder().decode(encoded); + + for (IntervalParser.Interval i : new IntervalParser().parseIntervals(decoded)) { + System.out.println( + "===> " + + Instant.ofEpochMilli(i.from / 1000000) + + " - " + + Instant.ofEpochMilli(i.till / 1000000) + + " :: " + + (i.till - i.from)); + } + } + } +} diff --git a/dd-java-agent/agent-profiling/profiling-controller-jfr/profiling-controller-jfr.gradle b/dd-java-agent/agent-profiling/profiling-controller-jfr/profiling-controller-jfr.gradle index b8b7af23425..3fb6419644a 100644 --- a/dd-java-agent/agent-profiling/profiling-controller-jfr/profiling-controller-jfr.gradle +++ b/dd-java-agent/agent-profiling/profiling-controller-jfr/profiling-controller-jfr.gradle @@ -6,7 +6,6 @@ apply from: "$rootDir/gradle/java.gradle" dependencies { api project(':dd-java-agent:agent-profiling:profiling-controller') - implementation 'tools.profiler:async-profiler:1.8.3' implementation deps.slf4j diff --git a/dd-java-agent/agent-profiling/profiling-controller-jfr/src/main/resources/jfr/dd.jfp b/dd-java-agent/agent-profiling/profiling-controller-jfr/src/main/resources/jfr/dd.jfp index 52c4b0df085..be0fcdd893c 100644 --- a/dd-java-agent/agent-profiling/profiling-controller-jfr/src/main/resources/jfr/dd.jfp +++ b/dd-java-agent/agent-profiling/profiling-controller-jfr/src/main/resources/jfr/dd.jfp @@ -246,3 +246,5 @@ datadog.Scope#threshold=10 ms datadog.ExceptionSample#enabled=true datadog.ExceptionCount#enabled=true datadog.ProfilerSetting#enabled=true +datadog.ContextInterval#enabled=true +datadog.Checkpoint#enabled=true diff --git a/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/ProfilingAgent.java b/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/ProfilingAgent.java index 279af730078..dc984cb6f79 100644 --- a/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/ProfilingAgent.java +++ b/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/ProfilingAgent.java @@ -2,6 +2,8 @@ import static datadog.trace.util.AgentThreadFactory.AGENT_THREAD_GROUP; +import com.datadog.profiling.context.JfrTimestampPatch; +import com.datadog.profiling.context.ProfilerTracingContextTrackerFactory; import com.datadog.profiling.controller.ConfigurationException; import com.datadog.profiling.controller.Controller; import com.datadog.profiling.controller.ControllerFactory; @@ -9,6 +11,7 @@ import com.datadog.profiling.controller.UnsupportedEnvironmentException; import com.datadog.profiling.uploader.ProfileUploader; import datadog.trace.api.Config; +import datadog.trace.api.Platform; import datadog.trace.bootstrap.config.provider.ConfigProvider; import java.io.IOException; import java.lang.ref.WeakReference; @@ -32,7 +35,7 @@ public class ProfilingAgent { * Main entry point into profiling Note: this must be reentrant because we may want to start * profiling before any other tool, and then attempt to start it again at normal time */ - public static synchronized void run(final boolean isStartingFirst) + public static synchronized void run(final boolean isStartingFirst, ClassLoader agentClasLoader) throws IllegalArgumentException, IOException { if (profiler == null) { final Config config = Config.get(); @@ -50,11 +53,16 @@ public static synchronized void run(final boolean isStartingFirst) "Profiling: API key doesn't match expected format, expected to get a 32 character hex string. Profiling is disabled."); return; } + if (Platform.isJavaVersionAtLeast(9)) { + JfrTimestampPatch.execute(agentClasLoader); + } try { final ConfigProvider configProvider = ConfigProvider.getInstance(); final Controller controller = ControllerFactory.createController(configProvider); + ProfilerTracingContextTrackerFactory.register(configProvider); + final ProfileUploader uploader = new ProfileUploader(config, configProvider); final Duration startupDelay = Duration.ofSeconds(config.getProfilingStartDelay()); diff --git a/dd-java-agent/dd-java-agent.gradle b/dd-java-agent/dd-java-agent.gradle index f745a9ab673..9a32f1dd426 100644 --- a/dd-java-agent/dd-java-agent.gradle +++ b/dd-java-agent/dd-java-agent.gradle @@ -194,7 +194,7 @@ tasks.register('checkAgentJarSize').configure { doLast { // Arbitrary limit to prevent unintentional increases to the agent jar size // Raise or lower as required - assert shadowJar.archiveFile.get().getAsFile().length() < 19 * 1024 * 1024 + assert shadowJar.archiveFile.get().getAsFile().length() < 20 * 1024 * 1024 } dependsOn "shadowJar" diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java index 41ac137b033..b65eb7dc9e0 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java @@ -80,6 +80,21 @@ public final class ProfilingConfig { public static final String PROFILING_ASYNC_MEMLEAK_CAPACITY = "profiling.async.memleak.capacity"; public static final int PROFILING_ASYNC_MEMLEAK_CAPACITY_DEFAULT = 1024; + public static final String PROFILING_TRACING_CONTEXT_ENABLED = + "profiling.tracing_context.enabled"; + public static final boolean PROFILING_TRACING_CONTEXT_ENABLED_DEFAULT = false; + public static final String PROFILING_TRACING_CONTEXT_TRACKER_INACTIVE_SEC = + "profiling.tracing_context.tracker.inactive.seconds"; + public static final int PROFILING_TRACING_CONTEXT_TRACKER_INACTIVE_DEFAULT = 90; + public static final String PROFILING_TRACING_CONTEXT_RESERVED_MEMORY_SIZE = + "profiling.tracing_context.memory.bytes"; + public static final int PROFILING_TRACING_CONTEXT_RESERVED_MEMORY_SIZE_DEFAULT = + 32 * 1024 * 1024; // 32MB + + public static final String PROFILING_TRACING_CONTEXT_RESERVED_MEMORY_TYPE = + "profiling.tracing_context.memory.type"; + public static final String PROFILING_TRACING_CONTEXT_RESERVED_MEMORY_TYPE_DEFAULT = "heap"; + public static final String PROFILING_LEGACY_TRACING_INTEGRATION = "profiling.legacy.tracing.integration"; public static final boolean PROFILING_LEGACY_TRACING_INTEGRATION_DEFAULT = true; diff --git a/dd-trace-core/dd-trace-core.gradle b/dd-trace-core/dd-trace-core.gradle index 0a38802cb21..c8c5d62aaba 100644 --- a/dd-trace-core/dd-trace-core.gradle +++ b/dd-trace-core/dd-trace-core.gradle @@ -20,7 +20,8 @@ excludedClassesCoverage += [ 'datadog.trace.core.PendingTraceBuffer.DelayingPendingTraceBuffer.FlushElement', 'datadog.trace.core.StatusLogger', 'datadog.trace.core.scopemanager.ContinuableScopeManager.SingleContinuation', - 'datadog.trace.core.SpanCorrelationImpl' + 'datadog.trace.core.SpanCorrelationImpl', + 'datadog.trace.core.Base64Encoder' ] apply plugin: 'org.unbroken-dome.test-sets' diff --git a/dd-trace-core/src/main/java/datadog/trace/core/Base64Encoder.java b/dd-trace-core/src/main/java/datadog/trace/core/Base64Encoder.java new file mode 100644 index 00000000000..1beb24a40d7 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/Base64Encoder.java @@ -0,0 +1,99 @@ +package datadog.trace.core; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +// TODO - can be removed when JDK7 is dropped (adapted from JDK source) +final class Base64Encoder { + + private static final char[] BASE_64 = new char[65]; + + static { + int i = 0; + for (char c = 'A'; c <= 'Z'; ++c) { + BASE_64[i++] = c; + } + for (char c = 'a'; c <= 'z'; ++c) { + BASE_64[i++] = c; + } + for (char c = '0'; c <= '9'; ++c) { + BASE_64[i++] = c; + } + BASE_64[i++] = '+'; + BASE_64[i] = '/'; + } + + public static final Base64Encoder INSTANCE = new Base64Encoder(); + + private Base64Encoder() {} + + private int outLength(int srclen) { + int n = srclen % 3; + return 4 * (srclen / 3) + (n == 0 ? 0 : n + 1); + } + + public byte[] encode(byte[] src) { + int len = outLength(src.length); // dst array size + byte[] dst = new byte[len]; + int ret = encode0(src, 0, src.length, dst); + if (ret != dst.length) return Arrays.copyOf(dst, ret); + return dst; + } + + public ByteBuffer encode(ByteBuffer buffer) { + int len = outLength(buffer.remaining()); + byte[] dst = new byte[len]; + int ret = 0; + if (buffer.hasArray()) { + ret = + encode0( + buffer.array(), + buffer.arrayOffset() + buffer.position(), + buffer.arrayOffset() + buffer.limit(), + dst); + buffer.position(buffer.limit()); + } else { + byte[] src = new byte[buffer.remaining()]; + buffer.get(src); + ret = encode0(src, 0, src.length, dst); + } + if (ret != dst.length) dst = Arrays.copyOf(dst, ret); + return ByteBuffer.wrap(dst); + } + + private void encodeBlock(byte[] src, int sp, int sl, byte[] dst, int dp) { + for (int sp0 = sp, dp0 = dp; sp0 < sl; ) { + int bits = (src[sp0++] & 0xff) << 16 | (src[sp0++] & 0xff) << 8 | (src[sp0++] & 0xff); + dst[dp0++] = (byte) BASE_64[(bits >>> 18) & 0x3f]; + dst[dp0++] = (byte) BASE_64[(bits >>> 12) & 0x3f]; + dst[dp0++] = (byte) BASE_64[(bits >>> 6) & 0x3f]; + dst[dp0++] = (byte) BASE_64[bits & 0x3f]; + } + } + + private int encode0(byte[] src, int off, int end, byte[] dst) { + int sp = off; + int slen = (end - off) / 3 * 3; + int sl = off + slen; + int dp = 0; + while (sp < sl) { + int sl0 = Math.min(sp + slen, sl); + encodeBlock(src, sp, sl0, dst, dp); + int dlen = (sl0 - sp) / 3 * 4; + dp += dlen; + sp = sl0; + } + if (sp < end) { // 1 or 2 leftover bytes + int b0 = src[sp++] & 0xff; + dst[dp++] = (byte) BASE_64[b0 >> 2]; + if (sp == end) { + dst[dp++] = (byte) BASE_64[(b0 << 4) & 0x3f]; + } else { + int b1 = src[sp++] & 0xff; + dst[dp++] = (byte) BASE_64[(b0 << 4) & 0x3f | (b1 >> 4)]; + dst[dp++] = (byte) BASE_64[(b1 << 2) & 0x3f]; + } + } + return dp; + } +} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index e71d2eaa0bb..cb045458495 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -26,6 +26,7 @@ import datadog.trace.api.gateway.RequestContext; import datadog.trace.api.interceptor.MutableSpan; import datadog.trace.api.interceptor.TraceInterceptor; +import datadog.trace.api.profiling.TracingContextTrackerFactory; import datadog.trace.api.sampling.PrioritySampling; import datadog.trace.api.sampling.SamplingMechanism; import datadog.trace.api.time.SystemTimeSource; @@ -780,6 +781,18 @@ void write(final List trace) { spanToSample.forceKeep(forceKeep); boolean published = forceKeep || sampler.sample(spanToSample); if (published) { + if (TracingContextTrackerFactory.isTrackingAvailable()) { + for (DDSpan span : writtenTrace) { + int stored = span.storeContextToTag(); + if (stored > -1) { + log.trace( + "Sending statsd metric 'tracing.context.size'={} (client={})", + stored, + statsDClient); + statsDClient.histogram("tracing.context.size", stored); + } + } + } writer.write(writtenTrace); } else { // with span streaming this won't work - it needs to be changed diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java index 0fd5732435d..7f4de132a6f 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java @@ -13,13 +13,18 @@ import datadog.trace.api.DDId; import datadog.trace.api.DDTags; import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.profiling.TracingContextTracker; +import datadog.trace.api.profiling.TracingContextTrackerFactory; +import datadog.trace.api.profiling.TransientProfilingContextHolder; import datadog.trace.api.sampling.PrioritySampling; import datadog.trace.api.sampling.SamplingMechanism; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AttachableWrapper; import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import java.io.PrintWriter; import java.io.StringWriter; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Map; import java.util.concurrent.atomic.AtomicLongFieldUpdater; @@ -35,7 +40,8 @@ *

Spans are created by the {@link CoreTracer#buildSpan}. This implementation adds some features * according to the DD agent. */ -public class DDSpan implements AgentSpan, CoreSpan, AttachableWrapper { +public class DDSpan + implements AgentSpan, CoreSpan, TransientProfilingContextHolder, AttachableWrapper { private static final Logger log = LoggerFactory.getLogger(DDSpan.class); public static final String CHECKPOINTED_TAG = "checkpointed"; @@ -49,6 +55,7 @@ static DDSpan create( final DDSpan span = new DDSpan(timestampMicro, context, emitCheckpoints); log.debug("Started span: {}", span); context.getTrace().registerSpan(span); + span.tracingContextTracker = TracingContextTrackerFactory.instance(span); return span; } @@ -101,6 +108,8 @@ private DDSpan(final long timestampMicro, @Nonnull DDSpanContext context) { this(timestampMicro, context, true); } + private volatile TracingContextTracker tracingContextTracker; + private DDSpan( final long timestampMicro, @Nonnull DDSpanContext context, boolean emitLocalCheckpoints) { this.context = context; @@ -125,6 +134,7 @@ private void finishAndAddToTrace(final long durationNano) { // ensure a min duration of 1 if (DURATION_NANO_UPDATER.compareAndSet(this, 0, Math.max(1, durationNano))) { context.getTrace().onFinish(this); + tracingContextTracker.deactivateContext(); PendingTrace.PublishState publishState = context.getTrace().onPublish(this); log.debug("Finished span ({}): {}", publishState, this); } else { @@ -237,6 +247,30 @@ public final void publish() { } } + public int storeContextToTag() { + try { + byte[] contextContent = tracingContextTracker.persist(); + if (contextContent != null) { + String contextTagName = "_dd_tracing_context_" + tracingContextTracker.getVersion(); + byte[] encoded = Base64Encoder.INSTANCE.encode(contextContent); + if (log.isTraceEnabled()) { + log.trace( + "Tracing context data for s_id:{}, tag:{}={}", + getSpanId(), + contextTagName, + new String(encoded, StandardCharsets.UTF_8)); + } + context.setTag(contextTagName, UTF8BytesString.create(encoded)); + return encoded.length; + } + } catch (Throwable t) { + log.error("", t); + } finally { + tracingContextTracker.release(); + } + return -1; + } + @Override public DDSpan setError(final boolean error) { context.setErrorFlag(error); @@ -521,6 +555,7 @@ public boolean eligibleForDropping() { @Override public void startThreadMigration() { + tracingContextTracker.maybeDeactivateContext(); if (hasCheckpoints()) { context.getTracer().onStartThreadMigration(this); } @@ -528,6 +563,7 @@ public void startThreadMigration() { @Override public void finishThreadMigration() { + tracingContextTracker.activateContext(); if (hasCheckpoints()) { context.getTracer().onFinishThreadMigration(this); } @@ -535,6 +571,7 @@ public void finishThreadMigration() { @Override public void finishWork() { + tracingContextTracker.deactivateContext(); if (hasCheckpoints()) { context.getTracer().onFinishWork(this); } diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index b3bfe071c44..efad4f3d4eb 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -29,7 +29,7 @@ final class CachedData { testcontainers: '1.15.0-rc2', jmc : "8.1.0-SNAPSHOT", autoservice : "1.0-rc7", - asyncprofiler : "2.7-DD-20220303" + asyncprofiler : "2.7-DD-20220411" ] static deps = [ diff --git a/internal-api/src/main/java/datadog/trace/api/profiling/TracingContextTracker.java b/internal-api/src/main/java/datadog/trace/api/profiling/TracingContextTracker.java new file mode 100644 index 00000000000..cbc39b3e4d9 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/profiling/TracingContextTracker.java @@ -0,0 +1,104 @@ +package datadog.trace.api.profiling; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +/** A tracing context tracker */ +public interface TracingContextTracker { + /** Provides support for a timed tracker cleanup */ + interface DelayedTracker extends Delayed { + DelayedTracker EMPTY = + new DelayedTracker() { + @Override + public void cleanup() {} + + @Override + public long getDelay(TimeUnit unit) { + return -1; + } + + @Override + public int compareTo(Delayed o) { + return -1; + } + }; + + void cleanup(); + } + + /** A no-op implementation */ + TracingContextTracker EMPTY = + new TracingContextTracker() { + @Override + public boolean release() { + return false; + } + + @Override + public void activateContext() {} + + @Override + public void deactivateContext() {} + + @Override + public void maybeDeactivateContext() {} + + @Override + public byte[] persist() { + return null; + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public DelayedTracker asDelayed() { + return DelayedTracker.EMPTY; + } + }; + + /** + * Release any resources held by the tracker + * + * @return {@literal false} if already released; {@literal true} otherwise + */ + boolean release(); + + /** Notify of the context activation */ + void activateContext(); + + /** Notify of the context deactivation */ + void deactivateContext(); + + /** + * Notify of the eventual context deactivation + * + * @param the deactivation is conditional - if it is followed by an activation the deactivation + * should be disregarded + */ + void maybeDeactivateContext(); + + /** + * Convert the sparse 'on-line' representation into a compressed binary blob + * + * @return the binary blob of the context tracking data or {@literal null} + */ + byte[] persist(); + + /** + * The tracker version - should be in sync with the binary blob format + * + * @return the tracker version + */ + int getVersion(); + + /** + * Turn this instance into a {@linkplain Delayed} one, suitable to be used in {@linkplain + * java.util.concurrent.DelayQueue} + * + * @return the delayed version of this instance + */ + DelayedTracker asDelayed(); +} diff --git a/internal-api/src/main/java/datadog/trace/api/profiling/TracingContextTrackerFactory.java b/internal-api/src/main/java/datadog/trace/api/profiling/TracingContextTrackerFactory.java new file mode 100644 index 00000000000..daa2be2ceb7 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/profiling/TracingContextTrackerFactory.java @@ -0,0 +1,64 @@ +package datadog.trace.api.profiling; + +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A factory class for {@linkplain TracingContextTracker} instances.
+ * This class is actually just a shared scaffolding and the actual implementation needs to register + * itself by calling {@linkplain + * TracingContextTrackerFactory#registerImplementation(Implementation)} + */ +public final class TracingContextTrackerFactory { + private static final Logger log = LoggerFactory.getLogger(TracingContextTrackerFactory.class); + + public interface Implementation { + Implementation EMPTY = + new Implementation() { + public TracingContextTracker instance(AgentSpan span) { + return TracingContextTracker.EMPTY; + } + }; + + TracingContextTracker instance(AgentSpan span); + } + + private static final TracingContextTrackerFactory INSTANCE = new TracingContextTrackerFactory(); + + private volatile Implementation implementation = Implementation.EMPTY; + private final AtomicReferenceFieldUpdater + implFieldUpdater = + AtomicReferenceFieldUpdater.newUpdater( + TracingContextTrackerFactory.class, Implementation.class, "implementation"); + + public static boolean registerImplementation(Implementation factoryImplementation) { + try { + return INSTANCE.implFieldUpdater.compareAndSet( + INSTANCE, Implementation.EMPTY, factoryImplementation); + } catch (Throwable t) { + log.warn("Failed to register a profiling context implementation", t); + } + return false; + } + + static void removeImplementation(Implementation implementation) { + INSTANCE.implFieldUpdater.compareAndSet(INSTANCE, implementation, Implementation.EMPTY); + } + + public static boolean isTrackingAvailable() { + // return true if a non-dummy implementatin has been registered + return INSTANCE.implementation != Implementation.EMPTY; + } + + /** + * Create a new tracing context tracker associated with the given span + * + * @param span the span to associate the tracker with + * @return a new {@linkplain TracingContextTracker} instance + */ + public static TracingContextTracker instance(AgentSpan span) { + return INSTANCE.implementation.instance(span); + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/profiling/TransientProfilingContextHolder.java b/internal-api/src/main/java/datadog/trace/api/profiling/TransientProfilingContextHolder.java new file mode 100644 index 00000000000..78e5da98033 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/profiling/TransientProfilingContextHolder.java @@ -0,0 +1,5 @@ +package datadog.trace.api.profiling; + +public interface TransientProfilingContextHolder { + int storeContextToTag(); +} diff --git a/internal-api/src/test/groovy/datadog/trace/api/profiling/TracingContextTrackerFactoryTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/profiling/TracingContextTrackerFactoryTest.groovy new file mode 100644 index 00000000000..8c3c2c9106b --- /dev/null +++ b/internal-api/src/test/groovy/datadog/trace/api/profiling/TracingContextTrackerFactoryTest.groovy @@ -0,0 +1,63 @@ +package datadog.trace.api.profiling + +import datadog.trace.test.util.DDSpecification + +class TracingContextTrackerFactoryTest extends DDSpecification { + def factory + + def cleanup() { + TracingContextTrackerFactory.removeImplementation(factory) + } + + def "sanity default"() { + expect: + TracingContextTrackerFactory.isTrackingAvailable() == false + TracingContextTrackerFactory.instance(null) == TracingContextTracker.EMPTY + } + + def "sanity empty tracker"() { + when: + def tracker = TracingContextTrackerFactory.instance(null) + + then: + tracker.version == 0 + tracker.activateContext() + tracker.deactivateContext() + tracker.maybeDeactivateContext() + tracker.persist() == null + tracker.release() == false + + when: + def delayed = tracker.asDelayed() + + then: + delayed == TracingContextTracker.DelayedTracker.EMPTY + delayed.getDelay(null) == -1 + delayed.compareTo(null) == -1 + delayed.cleanup() + } + + def "custom implementation registration"() { + setup: + factory = new TestTracingContextTrackerFactory() + + expect: + TracingContextTrackerFactory.registerImplementation(factory) == true + TracingContextTrackerFactory.registerImplementation(factory) == false + TracingContextTrackerFactory.isTrackingAvailable() == true + } + + def "custom factory usage"() { + setup: + factory = new TestTracingContextTrackerFactory() + TracingContextTrackerFactory.registerImplementation(factory) + + when: + def tracker = TracingContextTrackerFactory.instance(null) + + then: + tracker != null + tracker != TracingContextTracker.EMPTY + tracker instanceof TestTracingContextTracker + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/profiling/TestTracingContextTracker.java b/internal-api/src/test/java/datadog/trace/api/profiling/TestTracingContextTracker.java new file mode 100644 index 00000000000..23864750a20 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/profiling/TestTracingContextTracker.java @@ -0,0 +1,32 @@ +package datadog.trace.api.profiling; + +final class TestTracingContextTracker implements TracingContextTracker { + @Override + public boolean release() { + return false; + } + + @Override + public void activateContext() {} + + @Override + public void deactivateContext() {} + + @Override + public void maybeDeactivateContext() {} + + @Override + public byte[] persist() { + return new byte[0]; + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public DelayedTracker asDelayed() { + return null; + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/profiling/TestTracingContextTrackerFactory.java b/internal-api/src/test/java/datadog/trace/api/profiling/TestTracingContextTrackerFactory.java new file mode 100644 index 00000000000..146c0fcc896 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/profiling/TestTracingContextTrackerFactory.java @@ -0,0 +1,11 @@ +package datadog.trace.api.profiling; + +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; + +final class TestTracingContextTrackerFactory + implements TracingContextTrackerFactory.Implementation { + @Override + public TracingContextTracker instance(AgentSpan span) { + return new TestTracingContextTracker(); + } +} diff --git a/settings.gradle b/settings.gradle index cca6b3d8063..5425c74e5b8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -45,9 +45,11 @@ include ':dd-java-agent:agent-jmxfetch' include ':dd-java-agent:agent-logging' include ':dd-java-agent:load-generator' +// profiling include ':dd-java-agent:agent-profiling' include ':dd-java-agent:agent-profiling:profiling-auxiliary' include ':dd-java-agent:agent-profiling:profiling-auxiliary-async' +include ':dd-java-agent:agent-profiling:profiling-context' include ':dd-java-agent:agent-profiling:profiling-controller' include ':dd-java-agent:agent-profiling:profiling-controller-jfr' include ':dd-java-agent:agent-profiling:profiling-controller-openjdk'