diff --git a/ddprof-test/build.gradle.kts b/ddprof-test/build.gradle.kts index 035e13037e..d4d3f631a2 100644 --- a/ddprof-test/build.gradle.kts +++ b/ddprof-test/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { // Test dependencies "testCommon"(libs.bundles.testing) "testCommon"(libs.bundles.profiler.runtime) + "testCommon"(libs.jafar.parser) "testCommon"(libs.asm) // Main/application dependencies diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/AbstractProfilerTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/AbstractProfilerTest.java index 1da7b59bf8..b4e988c582 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/AbstractProfilerTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/AbstractProfilerTest.java @@ -1,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler; import java.nio.file.Files; @@ -12,33 +17,18 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInfo; -import org.openjdk.jmc.common.IMCStackTrace; -import org.openjdk.jmc.common.item.Attribute; import static org.junit.jupiter.api.Assertions.*; -import static org.openjdk.jmc.common.item.Attribute.attr; -import static org.openjdk.jmc.common.unit.UnitLookup.*; - -import org.openjdk.jmc.common.IMCType; -import org.openjdk.jmc.common.item.IAttribute; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.common.item.IItemFilter; -import org.openjdk.jmc.common.item.ItemFilters; -import org.openjdk.jmc.common.item.IType; -import org.openjdk.jmc.common.unit.IQuantity; -import org.openjdk.jmc.common.unit.QuantityConversionException; -import org.openjdk.jmc.common.unit.UnitLookup; -import org.openjdk.jmc.flightrecorder.JfrLoaderToolkit; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; public abstract class AbstractProfilerTest { private static final boolean ALLOW_NATIVE_CSTACKS = true; @@ -47,52 +37,38 @@ public abstract class AbstractProfilerTest { private Map sanitizerLogSizesBefore = new HashMap<>(); public static final String LAMBDA_QUALIFIER = Platform.isJavaVersionAtLeast(21) ? "$$Lambda." : "$$Lambda$"; - public static final IQuantity ZERO_BYTES = BYTE.quantity(0); - public static final IAttribute SIZE = attr("size", "size", "", BYTE.getContentType()); - public static final IAttribute WEIGHT = attr("weight", "weight", "weight", NUMBER); - - public static final IAttribute SCALED_SIZE = new Attribute("scaledSize", "scaled size", "", BYTE.getContentType()) { - @Override - public IMemberAccessor customAccessor(IType type) { - IMemberAccessor sizeAccessor = SIZE.getAccessor(type); - IMemberAccessor weightAccessor = WEIGHT.getAccessor(type); - if (sizeAccessor == null || weightAccessor == null) { - return i -> ZERO_BYTES; - } - return i -> sizeAccessor.getMember(i).multiply(weightAccessor.getMember(i).doubleValue()); - } - }; - - public static final IAttribute LOCAL_ROOT_SPAN_ID = attr("localRootSpanId", "localRootSpanId", - "localRootSpanId", NUMBER); - public static final IAttribute SPAN_ID = attr("spanId", "spanId", - "spanId", NUMBER); - - public static final IAttribute OPERATION = attr("operation", "operation", - "operation", PLAIN_TEXT); - - - - public static final IAttribute THREAD_STATE = - attr("state", "state", "Thread State", PLAIN_TEXT); - public static final IAttribute THREAD_EXECUTION_MODE = - attr("mode", "mode", "Execution Mode", PLAIN_TEXT); + // Field-name constants for the JFR fields tests commonly read via JfrEvent.getXxx(...). + // These replace the old JMC-typed IAttribute constants of the same names one-for-one, so + // subclasses keep referencing AbstractProfilerTest.SPAN_ID etc. unchanged - only the call + // pattern around them (JfrEvent.getLong(SPAN_ID) instead of SPAN_ID.getAccessor(type).getMember(item)) + // changes at each site. + public static final String SIZE = "size"; + public static final String WEIGHT = "weight"; + public static final String LOCAL_ROOT_SPAN_ID = "localRootSpanId"; + public static final String SPAN_ID = "spanId"; + public static final String OPERATION = "operation"; + public static final String THREAD_STATE = "state"; + public static final String THREAD_EXECUTION_MODE = "mode"; + public static final String TAG_1 = "tag1"; + public static final String TAG_2 = "tag2"; + public static final String TAG_3 = "tag3"; + public static final String STACK_TRACE = "stackTrace"; + public static final String CPU_INTERVAL = "cpuInterval"; + public static final String CPU_ENGINE = "cpuEngine"; + public static final String WALL_INTERVAL = "wallInterval"; + public static final String NAME = "name"; + public static final String COUNT = "count"; - public static final IAttribute TAG_1 = attr("tag1", "", "", PLAIN_TEXT); - public static final IAttribute TAG_2 = attr("tag2", "", "", PLAIN_TEXT); - public static final IAttribute TAG_3 = attr("tag3", "", "", PLAIN_TEXT); - - public static final IAttribute STACK_TRACE = attr("stackTrace", "stackTrace", "", UnitLookup.STACKTRACE); - - public static final IAttribute CPU_INTERVAL = attr("cpuInterval", "cpuInterval", "", TIMESPAN); - public static final IAttribute CPU_ENGINE = attr("cpuEngine", "", "", PLAIN_TEXT); - - public static final IAttribute WALL_INTERVAL = attr("wallInterval", "wallInterval", "", TIMESPAN); - - public static final IAttribute NAME = attr("name", "", "", PLAIN_TEXT); - - public static final IAttribute COUNT = attr("count", "", "", NUMBER); + /** + * {@code size * weight}: an estimated true byte contribution of a subsampled allocation/ + * liveness event, replacing JMC's computed {@code SCALED_SIZE} attribute. {@code weight} is + * stored as either an integer or float field depending on event type (see jfrMetadata.cpp), + * hence the {@code double} read. + */ + public static double scaledSize(JfrEvent item) { + return item.getLong(SIZE, 0) * item.getDouble(WEIGHT, 1.0); + } protected JavaProfiler profiler; private Path jfrDump; @@ -276,40 +252,28 @@ public static final boolean isInCI() { } private void checkConfig() { - try { - IItemCollection profilerConfig = verifyEvents("datadog.DatadogProfilerConfig"); - for (IItemIterable items : profilerConfig) { - IMemberAccessor cpuIntervalAccessor = CPU_INTERVAL.getAccessor(items.getType()); - IMemberAccessor wallIntervalAccessor = WALL_INTERVAL.getAccessor(items.getType()); - for (IItem item : items) { - long cpuIntervalMillis = cpuIntervalAccessor.getMember(item).longValueIn(MILLISECOND); - long wallIntervalMillis = wallIntervalAccessor.getMember(item).longValueIn(MILLISECOND); - if (!Platform.isJ9() && Platform.isJavaVersionAtLeast(11)) { - // fixme J9 engine have weird defaults and need fixing - // Only assert intervals that were explicitly requested in the profiler - // command; engines not requested carry default intervals that do not - // match the (absent) command value. - if (cpuInterval.toMillis() > 0) { - assertEquals(cpuInterval.toMillis(), cpuIntervalMillis); - } - if (wallInterval.toMillis() > 0) { - assertEquals(wallInterval.toMillis(), wallIntervalMillis); - } - } + JfrEvents profilerConfig = verifyEvents("datadog.DatadogProfilerConfig"); + for (JfrEvent item : profilerConfig) { + long cpuIntervalMillis = item.getLong(CPU_INTERVAL, 0); + long wallIntervalMillis = item.getLong(WALL_INTERVAL, 0); + if (!Platform.isJ9() && Platform.isJavaVersionAtLeast(11)) { + // fixme J9 engine have weird defaults and need fixing + // Only assert intervals that were explicitly requested in the profiler + // command; engines not requested carry default intervals that do not + // match the (absent) command value. + if (cpuInterval.toMillis() > 0) { + assertEquals(cpuInterval.toMillis(), cpuIntervalMillis); + } + if (wallInterval.toMillis() > 0) { + assertEquals(wallInterval.toMillis(), wallIntervalMillis); } } - } catch (QuantityConversionException e) { - Assertions.fail(e.getMessage()); } } - protected static IItemFilter allocatedTypeFilter(String className) { - return type -> { - IMemberAccessor accessor = JdkAttributes.OBJECT_CLASS.getAccessor(type); - return iItem -> { - return accessor != null && accessor.getMember(iItem).getFullName().equals(className); - }; - }; + /** Matches allocation/liveness events whose sampled object's class full name equals {@code className}. */ + protected static Predicate allocatedTypeFilter(String className) { + return item -> className.equals(item.getClassName("objectClass")); } protected void runTests(Runnable... runnables) throws InterruptedException { @@ -394,47 +358,106 @@ private String getAmendedProfilerCommand() { protected abstract String getProfilerCommand(); - + protected void verifyEventsPresent(String... expectedEventTypes) { verifyEventsPresent(jfrDump, expectedEventTypes); } protected void verifyEventsPresent(Path recording, String... expectedEventTypes) { try { - IItemCollection events = JfrLoaderToolkit.loadEvents(Files.newInputStream(recording)); - assertTrue(events.hasItems()); + JfrEvents events = JfrEvents.load(recording, new HashSet<>(Arrays.asList(expectedEventTypes))::contains); for (String expectedEventType : expectedEventTypes) { - IItemCollection filtered = events.apply(ItemFilters.type(expectedEventType)); + JfrEvents filtered = events.byType(expectedEventType); assertTrue(filtered.hasItems(), expectedEventType + " was empty for " + getAmendedProfilerCommand()); - System.out.println(expectedEventType + " count: " + filtered.stream().count()); + System.out.println(expectedEventType + " count: " + filtered.count()); } } catch (Throwable t) { - fail(getProfilerCommand() + " " + t.getMessage()); + fail(getProfilerCommand() + " " + t.getMessage(), t); + } + } + + /** + * Like {@link #verifyEventsPresent}, but for a single event type where the caller doesn't need + * the materialized collection back — stops parsing as soon as one matching event is found + * (see {@link JfrEvents#load(Path, Predicate, Predicate)}), instead of resolving every event of + * a possibly high-volume type just to confirm it's non-empty. + */ + protected void verifyEventPresent(String eventType) { + verifyEventPresent(jfrDump, eventType); + } + + protected void verifyEventPresent(Path recording, String eventType) { + try { + JfrEvents events = JfrEvents.load(recording, eventType::equals, item -> true); + assertTrue(events.hasItems(), eventType + " was empty for " + getAmendedProfilerCommand()); + } catch (Throwable t) { + fail(getProfilerCommand() + " " + t, t); } } - public final IItemCollection verifyEvents(String eventType) { + /** + * Materializes every matching event, deep-resolved (including its stack trace, if the event + * type has one), into memory. For high-volume event types, prefer {@link #verifyEventPresent} + * (presence only), {@link #streamEvents} (per-event checks) or {@link #reduceEvents} (folding into an + * accumulator) to avoid exhausting the test heap. + */ + public final JfrEvents verifyEvents(String eventType) { return verifyEvents(eventType, true); } - protected IItemCollection verifyEvents(String eventType, boolean failOnEmpty) { + protected JfrEvents verifyEvents(String eventType, boolean failOnEmpty) { return verifyEvents(jfrDump, eventType, failOnEmpty); } - protected IItemCollection verifyEvents(Path recording, String eventType, boolean failOnEmpty) { + protected JfrEvents verifyEvents(Path recording, String eventType, boolean failOnEmpty) { try { - IItemCollection events = JfrLoaderToolkit.loadEvents(Files.newInputStream(recording)); - assertTrue(events.hasItems()); - IItemCollection collection = events.apply(ItemFilters.type(eventType)); - System.out.println(eventType + " count: " + collection.stream().flatMap(IItemIterable::stream).count()); + JfrEvents collection = JfrEvents.load(recording, eventType); + System.out.println(eventType + " count: " + collection.count()); if (failOnEmpty) { assertTrue(collection.hasItems(), eventType + " was empty for " + getAmendedProfilerCommand()); } return collection; } catch (Throwable t) { - fail(getProfilerCommand() + " " + t); + fail(getProfilerCommand() + " " + t, t); + return null; + } + } + + /** + * Like {@link #verifyEvents(String)}, but for callers that only need per-event checks and/or a + * count (e.g. {@code NativememSampledProfilerTest}'s per-sample field validation) rather than + * the materialized {@link JfrEvents} collection — never holds more than one event in memory at + * a time. See {@link JfrEvents#forEach} for the idempotency requirement on {@code consumer}. + */ + protected long streamEvents(String eventType, Consumer consumer) { + return streamEvents(jfrDump, eventType, consumer); + } + + protected long streamEvents(Path recording, String eventType, Consumer consumer) { + try { + return JfrEvents.forEach(recording, eventType::equals, consumer); + } catch (Throwable t) { + fail(getProfilerCommand() + " " + t, t); + return 0; + } + } + + /** + * Like {@link #streamEvents}, but for callers that fold matching events into an accumulator + * (e.g. {@code NativeLibrariesTest}'s per-mode/per-library sample counts) instead of running + * independent per-event checks. See {@link JfrEvents#reduce} for the per-attempt reset contract. + */ + protected T reduceEvents(String eventType, Supplier initial, BiConsumer accumulator) { + return reduceEvents(jfrDump, eventType, initial, accumulator); + } + + protected T reduceEvents(Path recording, String eventType, Supplier initial, BiConsumer accumulator) { + try { + return JfrEvents.reduce(recording, eventType::equals, initial, accumulator); + } catch (Throwable t) { + fail(getProfilerCommand() + " " + t, t); return null; } } @@ -445,15 +468,11 @@ protected final void verifyCStackSettings() { // not a forced cstack mode return; } - IItemCollection settings = verifyEvents("jdk.ActiveSetting"); - for (IItemIterable settingEvents : settings) { - IMemberAccessor nameAccessor = JdkAttributes.REC_SETTING_NAME.getAccessor(settingEvents.getType()); - IMemberAccessor valueAccessor = JdkAttributes.REC_SETTING_VALUE.getAccessor(settingEvents.getType()); - for (IItem item : settingEvents) { - String name = nameAccessor.getMember(item); - if (name.equals("cstack")) { - assertEquals(cstack, valueAccessor.getMember(item)); - } + JfrEvents settings = verifyEvents("jdk.ActiveSetting"); + for (JfrEvent item : settings) { + String name = item.getString("name"); + if ("cstack".equals(name)) { + assertEquals(cstack, item.getString("value")); } } } @@ -464,21 +483,20 @@ protected void verifyStackTraces(String eventType, String... patterns) { protected void verifyStackTraces(Path recording, String eventType, String... patterns) { Set unmatched = new HashSet<>(Arrays.asList(patterns)); - long cumulatedEvents = 0; - outer: for (IItemIterable sample : verifyEvents(recording, eventType, false)) { - cumulatedEvents += sample.getItemCount(); - IMemberAccessor stackTraceAccessor = JdkAttributes.STACK_TRACE_STRING.getAccessor(sample.getType()); - for (IItem item : sample) { - String stackTrace = stackTraceAccessor.getMember(item); - if (stackTrace != null) { - unmatched.removeIf(stackTrace::contains); - if (unmatched.isEmpty()) { - break outer; - } - } - } + long[] cumulatedEvents = {0}; + try { + // Stops parsing once every pattern has matched, instead of materializing every event of + // eventType up front — see JfrEvents.load(Path, Predicate, Predicate). + JfrEvents.load(recording, eventType::equals, item -> { + cumulatedEvents[0]++; + String stackTrace = item.getStackTraceString(); + unmatched.removeIf(stackTrace::contains); + return unmatched.isEmpty(); + }); + } catch (Throwable t) { + fail(getProfilerCommand() + " " + t, t); } - assertNotEquals(0, cumulatedEvents, "no events found for " + eventType); + assertNotEquals(0, cumulatedEvents[0], "no events found for " + eventType); assertTrue(unmatched.isEmpty(), "couldn't find " + eventType + " with " + unmatched); } @@ -490,17 +508,13 @@ protected void verifyStackTraces(Path recording, String eventType, String... pat * @return the counter value, or -1 if no matching event is found */ public long getRecordedCounterValue(String counterName) { - IItemCollection events = verifyEvents("datadog.ProfilerCounter", false); - for (IItemIterable iterable : events) { - IMemberAccessor nameAccessor = NAME.getAccessor(iterable.getType()); - IMemberAccessor countAccessor = COUNT.getAccessor(iterable.getType()); - if (nameAccessor == null || countAccessor == null) continue; - for (IItem item : iterable) { - if (counterName.equals(nameAccessor.getMember(item))) { - return countAccessor.getMember(item).longValue(); - } + JfrEvents events = verifyEvents("datadog.ProfilerCounter", false); + for (JfrEvent item : events) { + String name = item.getString(NAME); + if (counterName.equals(name)) { + return item.getLong(COUNT, -1); } } return -1; } -} \ No newline at end of file +} diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/ContendedCallTraceStorageTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/ContendedCallTraceStorageTest.java index b95b58d60f..8d25b89c9a 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/ContendedCallTraceStorageTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/ContendedCallTraceStorageTest.java @@ -6,19 +6,14 @@ package com.datadoghq.profiler; import org.junit.jupiter.api.Test; -import org.openjdk.jmc.common.IMCStackTrace; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.ItemFilters; -import org.openjdk.jmc.flightrecorder.JfrLoaderToolkit; -import org.openjdk.jmc.flightrecorder.CouldNotLoadRecordingException; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; @@ -138,14 +133,15 @@ private List measureContention() throws Exception { } } - private List analyzeContentionFromJFR(List recordings) throws IOException, CouldNotLoadRecordingException { + private List analyzeContentionFromJFR(List recordings) throws Exception { List results = new ArrayList<>(); for (Path jfrFile : recordings) { - IItemCollection events = JfrLoaderToolkit.loadEvents(Files.newInputStream(jfrFile)); + JfrEvents events = JfrEvents.load(jfrFile, new HashSet<>(Arrays.asList( + "datadog.ExecutionSample", "jdk.ObjectAllocationInNewTLAB"))::contains); // Count profiling events - represents successful put() operations - IItemCollection cpuEvents = events.apply(ItemFilters.type("datadog.ExecutionSample")); - IItemCollection allocationEvents = events.apply(ItemFilters.type("jdk.ObjectAllocationInNewTLAB")); + JfrEvents cpuEvents = events.byType("datadog.ExecutionSample"); + JfrEvents allocationEvents = events.byType("jdk.ObjectAllocationInNewTLAB"); // Count events with regular stack traces vs dropped traces long cpuWithRegularStack = countEventsWithRegularStackTrace(cpuEvents); @@ -169,39 +165,35 @@ private List analyzeContentionFromJFR(List recordings) t return results; } - private long countEventsWithRegularStackTrace(IItemCollection events) { + private long countEventsWithRegularStackTrace(JfrEvents events) { if (!events.hasItems()) return 0; - + long count = 0; - for (IItemIterable iterable : events) { - for (IItem item : iterable) { - IMCStackTrace stackTrace = STACK_TRACE.getAccessor(iterable.getType()).getMember(item); - if (stackTrace != null && !stackTrace.getFrames().isEmpty()) { - // Check if this is NOT the dropped trace (contains method with "dropped") - String topMethodName = stackTrace.getFrames().get(0).getMethod().getMethodName(); - if (!topMethodName.contains("dropped")) { - count++; - } + for (JfrEvent item : events) { + JfrStackTrace stackTrace = item.getStackTrace(STACK_TRACE); + if (stackTrace != null && !stackTrace.isEmpty()) { + // Check if this is NOT the dropped trace (contains method with "dropped") + String topMethodName = stackTrace.frames().get(0).methodName(); + if (!topMethodName.contains("dropped")) { + count++; } } } return count; } - - private long countEventsWithDroppedStackTrace(IItemCollection events) { + + private long countEventsWithDroppedStackTrace(JfrEvents events) { if (!events.hasItems()) return 0; - + long count = 0; - for (IItemIterable iterable : events) { - for (IItem item : iterable) { - IMCStackTrace stackTrace = STACK_TRACE.getAccessor(iterable.getType()).getMember(item); - if (stackTrace != null && !stackTrace.getFrames().isEmpty()) { - // Check if this is the special dropped trace (single frame with "dropped" method) - if (stackTrace.getFrames().size() == 1) { - String methodName = stackTrace.getFrames().get(0).getMethod().getMethodName(); - if (methodName.contains("dropped")) { - count++; - } + for (JfrEvent item : events) { + JfrStackTrace stackTrace = item.getStackTrace(STACK_TRACE); + if (stackTrace != null && !stackTrace.isEmpty()) { + // Check if this is the special dropped trace (single frame with "dropped" method) + if (stackTrace.frames().size() == 1) { + String methodName = stackTrace.frames().get(0).methodName(); + if (methodName.contains("dropped")) { + count++; } } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvent.java b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvent.java new file mode 100644 index 0000000000..7974de74ae --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvent.java @@ -0,0 +1,236 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.datadoghq.profiler; + +import java.util.Map; +import java.util.Objects; + +/** + * One parsed JFR event: a type name plus its fully-resolved field map (no jafar + * {@code ComplexType}/{@code ArrayType} wrappers remain — {@link JfrEvents} resolves them eagerly + * at load time via {@code Values.resolvedDeep()}, so every nested value here is a plain + * {@code Map}/{@code Object[]}/boxed-primitive/{@code String}). + * + *

Numeric fields annotated {@code @Timestamp(TICKS)}/{@code @Timespan(TICKS)} (the standard JDK + * convention, used for e.g. {@code startTime}/{@code duration}) are already normalized to epoch/duration + * nanoseconds by jafar's parser before this class ever sees them — see + * {@code io.jafar.parser.impl.TemporalNormalizer}. Fields with an explicit non-TICKS unit + * (e.g. this profiler's own {@code cpuInterval}/{@code wallInterval}, annotated + * {@code @Timespan("MILLISECONDS")}) are left as the raw value in that already-correct unit. + */ +public final class JfrEvent { + private final String typeName; + private final Map value; + + JfrEvent(String typeName, Map value) { + this.typeName = typeName; + this.value = value; + } + + /** The JFR event type name (e.g. {@code "datadog.ExecutionSample"}). */ + public String typeName() { + return typeName; + } + + /** Escape hatch: the raw, fully-resolved field map for this event. */ + public Map raw() { + return value; + } + + /** The raw field value at {@code field}, or {@code null} if absent. */ + public Object get(String field) { + return value.get(field); + } + + /** {@code true} if {@code field} is present in this event's field map. */ + public boolean has(String field) { + return value.containsKey(field); + } + + /** The value at {@code field} converted via {@link Object#toString()}, or {@code null} if absent. */ + public String getString(String field) { + Object v = value.get(field); + return v != null ? v.toString() : null; + } + + /** + * The name of a constant-pool-referenced enum-like type (e.g. this profiler's + * {@code jdk.types.ThreadState}/{@code datadog.types.ExecutionMode} fields {@code state}/ + * {@code mode}). {@link JfrEvents#load} already collapses these single-field wrapper types to + * their scalar value, so this is just {@link #getString(String)} under a name that documents + * intent at enum-like call sites. + */ + public String getEnumName(String field) { + return getString(field); + } + + /** Boxed so callers can distinguish "field absent" (null) from a real 0. */ + public Long getLong(String field) { + Object v = value.get(field); + return v instanceof Number ? ((Number) v).longValue() : null; + } + + /** Like {@link #getLong(String)}, but returns {@code defaultValue} instead of {@code null} if absent. */ + public long getLong(String field, long defaultValue) { + Long v = getLong(field); + return v != null ? v : defaultValue; + } + + /** Boxed so callers can distinguish "field absent" (null) from a real {@code 0.0}. */ + public Double getDouble(String field) { + Object v = value.get(field); + return v instanceof Number ? ((Number) v).doubleValue() : null; + } + + /** Like {@link #getDouble(String)}, but returns {@code defaultValue} instead of {@code null} if absent. */ + public double getDouble(String field, double defaultValue) { + Double v = getDouble(field); + return v != null ? v : defaultValue; + } + + /** Boxed so callers can distinguish "field absent" (null) from a real {@code false}. */ + public Boolean getBoolean(String field) { + Object v = value.get(field); + return v instanceof Boolean ? (Boolean) v : null; + } + + /** + * Converts a nanosecond field (e.g. a {@code @Timestamp(TICKS)}-normalized {@code startTime}, + * or a {@code @Timespan(TICKS)}-normalized {@code duration}) to epoch/duration milliseconds. + * Returns {@code null} if the field is absent. + */ + public Long getNanosAsMillis(String field) { + Long nanos = getLong(field); + return nanos != null ? nanos / 1_000_000L : null; + } + + /** The stack trace at {@code field} (default: {@code "stackTrace"}), never {@code null} (empty if absent). */ + public JfrStackTrace getStackTrace() { + return getStackTrace("stackTrace"); + } + + /** Like {@link #getStackTrace()}, but for a caller-specified {@code field} name. */ + public JfrStackTrace getStackTrace(String field) { + return JfrStackTrace.of(value.get(field)); + } + + /** + * The stack trace at {@code field} (default: {@code "stackTrace"}) formatted as one + * newline-joined string, one {@code className.methodName()} call-site per frame — for + * substring pattern matching against expected frames, matching how tests used JMC's + * {@code JdkAttributes.STACK_TRACE_STRING} synthetic attribute. + * + *

{@code Lookup::fillNativeMethodInfo} (flightRecorder.cpp) gives every native/stub frame + * (e.g. {@code vtable stub}/{@code itable stub}) an explicit empty-string class, never a + * missing one — so the dot is printed whenever a class field is present at all, even if its + * name is empty, producing e.g. {@code .vtable stub()} to match what tests substring-match + * against. Only a truly absent class (no {@code type} field/map, {@link JfrFrame#className()} + * returns {@code null}) skips the dot. The trailing {@code ()} is likewise always appended, + * even though the raw JFR method name never includes it (confirmed via jafar MCP inspection + * of a real recording: the vtable-stub frame's name is literally {@code "vtable stub"}). + */ + public String getStackTraceString() { + return getStackTraceString("stackTrace"); + } + + /** Like {@link #getStackTraceString()}, but for a caller-specified {@code field} name. */ + public String getStackTraceString(String field) { + // Mirrors IMemberAccessor.getMember()'s null-on-absent contract: some events + // (e.g. lightweight-mode CPU samples) have no stackTrace field at all, which + // callers distinguish from "present but empty" via a null check. + if (!has(field)) { + return null; + } + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (JfrFrame frame : getStackTrace(field).frames()) { + if (!first) { + sb.append('\n'); + } + first = false; + String className = frame.className(); + String methodName = frame.methodName(); + if (className != null) { + sb.append(className).append('.'); + } + sb.append(methodName != null ? methodName : "").append("()"); + } + return sb.toString(); + } + + /** + * The name of a JFR thread-reference field (e.g. {@code "eventThread"}), preferring + * {@code osName} (what this profiler's own attribution and most JDK events key on) and + * falling back to {@code javaName}. Returns {@code null} if the field/thread is absent. + */ + @SuppressWarnings("unchecked") + public String getThreadName(String field) { + Object t = value.get(field); + if (!(t instanceof Map)) { + return null; + } + Map thread = (Map) t; + Object osName = thread.get("osName"); + if (osName != null && !osName.toString().isEmpty()) { + return osName.toString(); + } + Object javaName = thread.get("javaName"); + return javaName != null ? javaName.toString() : null; + } + + /** + * The Java thread id of a JFR thread-reference field, or {@code null} if the field/thread + * is absent. + */ + @SuppressWarnings("unchecked") + public Long getThreadJavaId(String field) { + Object t = value.get(field); + if (!(t instanceof Map)) { + return null; + } + Object id = ((Map) t).get("javaThreadId"); + return id instanceof Number ? ((Number) id).longValue() : null; + } + + /** + * The full name (e.g. {@code java.lang.String}) of a JFR class-reference field + * (e.g. {@code "objectClass"}, {@code "task"}), or {@code null} if the field/class is absent. + */ + @SuppressWarnings("unchecked") + public String getClassName(String field) { + Object c = value.get(field); + if (!(c instanceof Map)) { + return null; + } + Object name = ((Map) c).get("name"); + String s; + if (name instanceof Map) { + Object v = ((Map) name).get("string"); + s = v != null ? v.toString() : null; + } else { + s = name != null ? name.toString() : null; + } + return s != null ? s.replace('/', '.') : null; + } + + @Override + public String toString() { + return "JfrEvent{" + typeName + ", " + value + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof JfrEvent)) return false; + JfrEvent other = (JfrEvent) o; + return typeName.equals(other.typeName) && value.equals(other.value); + } + + @Override + public int hashCode() { + return Objects.hash(typeName, value); + } +} diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvents.java b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvents.java new file mode 100644 index 0000000000..da140e13f3 --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvents.java @@ -0,0 +1,301 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.datadoghq.profiler; + +import io.jafar.parser.api.JafarParser; +import io.jafar.parser.api.UntypedJafarParser; +import io.jafar.parser.api.Values; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; + +/** + * A queryable collection of parsed JFR events, backed by the jafar parser instead of JDK + * Mission Control's {@code FlightRecordingLoader}. + * + *

JMC's loader parses chunks on an internal thread pool via an + * {@code ExecutorCompletionService}; if a worker dies from an uncaught {@link OutOfMemoryError} + * mid-parse, the main thread's {@code take()} blocks forever waiting for a result that will never + * arrive (confirmed root cause of CI hangs in {@code BoundMethodHandleProfilerTest} on + * aarch64/JDK25 debug builds). jafar's {@code UntypedJafarParser.run()} is synchronous and single- + * threaded from the caller's perspective — a worker failure surfaces as a propagated exception + * from {@code run()}, never a silent hang. + */ +public final class JfrEvents implements Iterable { + private final List events; + + private JfrEvents(List events) { + this.events = events; + } + + /** + * Parses every event in {@code recording} into memory. Prefer {@link #load(Path, String)} or + * {@link #load(Path, Predicate)} when only specific event types are needed: this loads and + * deep-resolves every event of every type in the recording, which for a high-volume + * recording (e.g. allocation/GC-heavy runs) can exceed a constrained test heap even when the + * caller only cares about one small event type. + */ + public static JfrEvents load(Path recording) throws Exception { + return load(recording, typeName -> true); + } + + /** Parses only events whose type name equals {@code eventType}, skipping resolution of the rest. */ + public static JfrEvents load(Path recording, String eventType) throws Exception { + return load(recording, eventType::equals); + } + + /** Retries for a recording read right after {@code JavaProfiler.stop()}/{@code dump()} racing the JFR chunk flush. */ + private static final int LOAD_RETRIES = 5; + private static final long LOAD_RETRY_BACKOFF_MILLIS = 20; + + /** + * Parses only events whose type name matches {@code typeFilter}, skipping resolution of the + * rest. For more than one type, pass a {@code Set::contains} or an {@code + * Arrays.asList(...)::contains}. Checking {@code type.getName()} against the filter before + * calling {@code Values.resolvedDeep()} is what keeps this streaming rather than + * materializing the whole recording: the expensive deep per-field walk only runs for events + * we actually keep. + * + *

Retries with a small linear backoff on {@link IOException}: a test reading the recording + * immediately after {@code stopProfiler()}/{@code dump()} can race the native side finishing + * its last chunk flush, which jafar's single-pass streaming parser surfaces as a hard parse + * failure rather than tolerating (confirmed by re-opening the exact same file moments later — + * it parses cleanly once the flush has landed). + */ + public static JfrEvents load(Path recording, Predicate typeFilter) throws Exception { + return load(recording, typeFilter, e -> false); + } + + /** + * Like {@link #load(Path, Predicate)}, but stops parsing early once {@code stopWhen} returns + * {@code true} for a kept event — via jafar's {@code Control.abort()}, checked by + * {@code EventStream.onEvent()} right after our handler returns. This skips the rest of the + * current chunk only: {@code StreamingChunkParser.parse()} submits every chunk's + * parsing task up front, so any chunk already queued still runs to completion. For the + * single-{@code dump()} recordings this test suite produces (one dump is one chunk in + * practice), that makes {@code stopWhen} an effective full early-exit — useful for callers + * like {@code verifyStackTraces()} that only need a prefix of a high-volume event type. + * + *

A multi-chunk recording has each chunk's {@code parser.handle()} callback invoked from a + * different worker of jafar's internal chunk-parsing thread pool ({@code + * StreamingChunkParser} submits one parsing task per chunk before joining any of them), so the + * per-event handling below is wrapped in a {@code synchronized} block to serialize the shared + * {@code events} list mutation (and the {@code stopWhen}/{@code ctl.abort()} pair, so a + * concurrent chunk can't abort mid-check) across chunks. + */ + public static JfrEvents load(Path recording, Predicate typeFilter, Predicate stopWhen) + throws Exception { + IOException lastFailure = null; + for (int attempt = 0; attempt < LOAD_RETRIES; attempt++) { + if (attempt > 0) { + Thread.sleep(LOAD_RETRY_BACKOFF_MILLIS * attempt); + } + List events = new ArrayList<>(); + Object lock = new Object(); + try (UntypedJafarParser parser = JafarParser.newUntypedParser(recording)) { + parser.handle((type, value, ctl) -> { + if (typeFilter.test(type.getName())) { + JfrEvent event = new JfrEvent(type.getName(), collapseSimpleTypes(Values.resolvedDeep(value))); + synchronized (lock) { + events.add(event); + if (stopWhen.test(event)) { + ctl.abort(); + } + } + } + }); + parser.run(); + return new JfrEvents(events); + } catch (IOException e) { + lastFailure = e; + System.err.println("JfrEvents.load: retrying " + recording + " after parse failure (attempt " + + (attempt + 1) + "/" + LOAD_RETRIES + "): " + e); + } + } + throw lastFailure; + } + + /** + * Streams matching events through {@code consumer} one at a time without ever retaining more + * than one in memory, for callers that only need per-event checks and/or a count (e.g. + * {@code NativememSampledProfilerTest}'s per-sample field validation) rather than the + * materialized collection {@link #load} returns. Retries the whole parse on {@link IOException} + * exactly like {@link #load}, so {@code consumer} may see a subset of a failed attempt's events + * before a retry starts over from zero — it must be idempotent (e.g. plain per-event + * assertions), not accumulate state that would double-count across attempts. Returns the + * number of events streamed from the attempt that ultimately succeeded. + * + *

As with {@link #load(Path, Predicate, Predicate)}, a multi-chunk recording can invoke this + * per-event callback from several jafar chunk-parsing worker threads at once, so the call to + * {@code consumer} and the shared counter update are wrapped in a {@code synchronized} block: + * {@code consumer} is thus never invoked concurrently, and only needs to be idempotent across + * retries (see above), not thread-safe. + */ + public static long forEach(Path recording, Predicate typeFilter, Consumer consumer) + throws Exception { + IOException lastFailure = null; + for (int attempt = 0; attempt < LOAD_RETRIES; attempt++) { + if (attempt > 0) { + Thread.sleep(LOAD_RETRY_BACKOFF_MILLIS * attempt); + } + long[] count = {0}; + Object lock = new Object(); + try (UntypedJafarParser parser = JafarParser.newUntypedParser(recording)) { + parser.handle((type, value, ctl) -> { + if (typeFilter.test(type.getName())) { + JfrEvent event = new JfrEvent(type.getName(), collapseSimpleTypes(Values.resolvedDeep(value))); + synchronized (lock) { + consumer.accept(event); + count[0]++; + } + } + }); + parser.run(); + return count[0]; + } catch (IOException e) { + lastFailure = e; + System.err.println("JfrEvents.forEach: retrying " + recording + " after parse failure (attempt " + + (attempt + 1) + "/" + LOAD_RETRIES + "): " + e); + } + } + throw lastFailure; + } + + /** + * Like {@link #forEach}, but for callers that fold matching events into an accumulator (e.g. + * {@code NativeLibrariesTest}'s per-mode/per-library sample counts) instead of running + * independent per-event checks. {@code initial} is called fresh at the start of every attempt + * so a retry after an {@link IOException} starts accumulation over, rather than double-counting + * events an earlier, failed attempt already folded in. + * + *

As with {@link #load(Path, Predicate, Predicate)}, a multi-chunk recording can invoke this + * per-event callback from several jafar chunk-parsing worker threads at once, so the call to + * {@code accumulator} is wrapped in a {@code synchronized} block: {@code accumulator} is thus + * never invoked concurrently, so folding into {@code acc} (e.g. a plain {@code HashMap}) is + * safe without {@code acc} itself needing to be a concurrent collection. + */ + public static T reduce(Path recording, Predicate typeFilter, Supplier initial, + BiConsumer accumulator) throws Exception { + IOException lastFailure = null; + for (int attempt = 0; attempt < LOAD_RETRIES; attempt++) { + if (attempt > 0) { + Thread.sleep(LOAD_RETRY_BACKOFF_MILLIS * attempt); + } + T acc = initial.get(); + Object lock = new Object(); + try (UntypedJafarParser parser = JafarParser.newUntypedParser(recording)) { + parser.handle((type, value, ctl) -> { + if (typeFilter.test(type.getName())) { + JfrEvent event = new JfrEvent(type.getName(), collapseSimpleTypes(Values.resolvedDeep(value))); + synchronized (lock) { + accumulator.accept(acc, event); + } + } + }); + parser.run(); + return acc; + } catch (IOException e) { + lastFailure = e; + System.err.println("JfrEvents.reduce: retrying " + recording + " after parse failure (attempt " + + (attempt + 1) + "/" + LOAD_RETRIES + "): " + e); + } + } + throw lastFailure; + } + + /** + * Collapses any single-entry {@code Map} produced by {@code Values.resolvedDeep()} down to + * its lone value, recursively. JFR's own metadata marks certain constant-pool-referenced + * types "simple" (JDK's own {@code jdk.types.Symbol}/{@code java.lang.String} CPOOL entries, + * and this profiler's own {@code jdk.types.ThreadState}/{@code datadog.types.ExecutionMode}/ + * {@code profiler.types.CounterName}/{@code profiler.types.AttributeValue} single-field + * wrapper types) meaning they should read as a plain scalar, not a wrapper struct. jafar's + * {@code MetadataClass.isSimpleType()} tracks this internally, but the untyped/{@code Values} + * API doesn't expose or apply it — so we collapse structurally instead: every "simple" type + * this profiler emits happens to be single-field, and no genuine multi-field JFR type is + * ever single-field, so "exactly one entry" is an exact (not heuristic) match for "simple". + */ + private static Map collapseSimpleTypes(Map root) { + for (Map.Entry e : root.entrySet()) { + e.setValue(collapseValue(e.getValue())); + } + return root; + } + + /** + * Mutates {@code v} in place rather than building a parallel copy: {@code Values.resolvedDeep()} + * (see its source) always allocates fresh, exclusively-owned {@code Map}/{@code Object[]} + * instances at every recursion level, so nothing else retains a reference to them and + * overwriting entries/elements here is safe. + */ + @SuppressWarnings("unchecked") + private static Object collapseValue(Object v) { + if (v instanceof Map) { + Map m = (Map) v; + if (m.size() == 1) { + return collapseValue(m.values().iterator().next()); + } + for (Map.Entry e : m.entrySet()) { + e.setValue(collapseValue(e.getValue())); + } + return m; + } + if (v instanceof Object[]) { + Object[] arr = (Object[]) v; + for (int i = 0; i < arr.length; i++) { + arr[i] = collapseValue(arr[i]); + } + return arr; + } + return v; + } + + /** {@code true} if this collection contains at least one event. */ + public boolean hasItems() { + return !events.isEmpty(); + } + + /** The number of events in this collection. */ + public long count() { + return events.size(); + } + + /** A new collection containing only events whose type name equals {@code typeName}. */ + public JfrEvents byType(String typeName) { + return filter(e -> e.typeName().equals(typeName)); + } + + /** A new collection containing only the events for which {@code predicate} returns {@code true}. */ + public JfrEvents filter(Predicate predicate) { + List filtered = new ArrayList<>(); + for (JfrEvent e : events) { + if (predicate.test(e)) { + filtered.add(e); + } + } + return new JfrEvents(filtered); + } + + /** A {@link Stream} over this collection's events, for callers preferring stream-style access. */ + public Stream stream() { + return events.stream(); + } + + @Override + public Iterator iterator() { + return Collections.unmodifiableList(events).iterator(); + } +} diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/JfrFrame.java b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrFrame.java new file mode 100644 index 0000000000..37da36ad22 --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrFrame.java @@ -0,0 +1,62 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.datadoghq.profiler; + +import java.util.Map; + +/** + * One frame of a {@link JfrStackTrace}, wrapping a resolved {@code jdk.types.StackFrame} map: + * {@code {method: {type: {name: {string: ...}}, name: {string: ...}}, lineNumber, bytecodeIndex, type}}. + */ +public final class JfrFrame { + private final Map value; + + JfrFrame(Map value) { + this.value = value; + } + + @SuppressWarnings("unchecked") + private static String symbolString(Object symbolOrMap) { + if (symbolOrMap instanceof Map) { + Object s = ((Map) symbolOrMap).get("string"); + return s != null ? s.toString() : null; + } + return symbolOrMap != null ? symbolOrMap.toString() : null; + } + + @SuppressWarnings("unchecked") + private Map method() { + Object m = value.get("method"); + return m instanceof Map ? (Map) m : null; + } + + /** The frame's method name, or {@code null} if unavailable. */ + public String methodName() { + Map method = method(); + return method != null ? symbolString(method.get("name")) : null; + } + + /** The method's descriptor (JVM signature string), or {@code null} if unavailable. */ + public String methodDescriptor() { + Map method = method(); + return method != null ? symbolString(method.get("descriptor")) : null; + } + + /** The full name (e.g. {@code java.lang.String}) of the method's declaring type, or {@code null}. */ + @SuppressWarnings("unchecked") + public String className() { + Map method = method(); + if (method == null) { + return null; + } + Object type = method.get("type"); + if (!(type instanceof Map)) { + return null; + } + String name = symbolString(((Map) type).get("name")); + return name != null ? name.replace('/', '.') : null; + } +} diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/JfrStackTrace.java b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrStackTrace.java new file mode 100644 index 0000000000..548230701c --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrStackTrace.java @@ -0,0 +1,71 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.datadoghq.profiler; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Wraps a resolved {@code jdk.types.StackTrace} value: {@code {frames: [...], truncated: boolean}}. + */ +public final class JfrStackTrace { + private final List frames; + private final boolean truncated; + + private static final JfrStackTrace EMPTY = new JfrStackTrace(Collections.emptyList(), false); + + private JfrStackTrace(List frames, boolean truncated) { + this.frames = frames; + this.truncated = truncated; + } + + /** + * Converts a raw resolved {@code jdk.types.StackTrace} value (a {@code Map} with + * {@code frames}/{@code truncated} entries, as produced by {@code Values.resolvedDeep()}) into + * a {@link JfrStackTrace}. Returns {@link #EMPTY} if {@code rawStackTrace} isn't such a map + * (e.g. the field was absent from the event). + */ + @SuppressWarnings("unchecked") + static JfrStackTrace of(Object rawStackTrace) { + if (!(rawStackTrace instanceof Map)) { + return EMPTY; + } + Map map = (Map) rawStackTrace; + Object framesVal = map.get("frames"); + List frames; + if (framesVal instanceof Object[]) { + Object[] arr = (Object[]) framesVal; + frames = new ArrayList<>(arr.length); + for (Object frame : arr) { + if (frame instanceof Map) { + frames.add(new JfrFrame((Map) frame)); + } + } + } else { + frames = Collections.emptyList(); + } + Object truncatedVal = map.get("truncated"); + boolean truncated = Boolean.TRUE.equals(truncatedVal); + return new JfrStackTrace(frames, truncated); + } + + /** This stack trace's frames, outermost (root) frame first. */ + public List frames() { + return frames; + } + + /** {@code true} if this stack trace has no frames (e.g. the field was absent from the event). */ + public boolean isEmpty() { + return frames.isEmpty(); + } + + /** {@code true} if the JVM truncated this stack trace (frame count exceeded the configured depth). */ + public boolean isTruncated() { + return truncated; + } +} diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/alloc/AllocationProfilerTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/alloc/AllocationProfilerTest.java index d995d065f4..c9e78aa1ba 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/alloc/AllocationProfilerTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/alloc/AllocationProfilerTest.java @@ -1,11 +1,15 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.alloc; import com.datadoghq.profiler.AbstractProfilerTest; +import com.datadoghq.profiler.JfrEvent; import com.datadoghq.profiler.Platform; import org.junit.jupiter.api.Assumptions; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.Aggregators; -import org.openjdk.jmc.common.item.IItemCollection; import org.openjdk.jol.info.GraphLayout; import java.util.Random; @@ -35,23 +39,43 @@ public void shouldGetObjectAllocationSamples() throws InterruptedException { AllocatingTarget target1 = new AllocatingTarget(); AllocatingTarget target2 = new AllocatingTarget(); runTests(target1, target2); - IItemCollection allocations = verifyEvents("datadog.ObjectSample"); + + // A million-iteration allocation loop produces millions of datadog.ObjectSample events; + // materializing all of them (each deep-resolved down to its full stack trace) OOMs the test + // heap, so this reduces per-event in a single streaming pass instead. + String intArrayName = int[].class.getName(); + String integerArrayName = Integer[].class.getName(); + RecordedSizes recorded = reduceEvents("datadog.ObjectSample", RecordedSizes::new, (acc, item) -> { + acc.total++; + String className = item.getClassName("objectClass"); + if (intArrayName.equals(className)) { + acc.intArray += (long) scaledSize(item); + } else if (integerArrayName.equals(className)) { + acc.integerArray += (long) scaledSize(item); + } + }); + assertTrue(recorded.total > 0, "datadog.ObjectSample was empty"); + // FIXME when more tests are ported to this structure if (!Platform.isMusl()) { // JOL on musl seems to be locking up randomly - assertAllocations(allocations, int[].class, target1, target2); - assertAllocations(allocations, Integer[].class, target1, target2); + assertAllocations(recorded.intArray, int[].class, target1, target2); + assertAllocations(recorded.integerArray, Integer[].class, target1, target2); } } - private static void assertAllocations(IItemCollection allocations, Class clazz, AllocatingTarget... targets) { + private static final class RecordedSizes { + long total; + long intArray; + long integerArray; + } + + private static void assertAllocations(long recorded, Class clazz, AllocatingTarget... targets) { long allocated = 0; for (AllocatingTarget target : targets) { allocated += target.getAllocated(clazz); } - IItemCollection allocationsByType = allocations.apply(allocatedTypeFilter(clazz.getCanonicalName())); - assertTrue(allocationsByType.hasItems()); - long recorded = allocationsByType.getAggregate(Aggregators.sum(SCALED_SIZE)).longValue(); + assertTrue(recorded > 0, "no allocation samples recorded for " + clazz.getCanonicalName()); double error = Math.abs(recorded - allocated) / (double)allocated; assertTrue(error <= 0.50, String.format("allocation samples should be within 10pct tolerance of allocated memory (recorded %d, allocated %d :: %4.2f)", diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/context/AllNativeContextSamplingTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/context/AllNativeContextSamplingTest.java index 80b81c1b7b..1d597be113 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/context/AllNativeContextSamplingTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/context/AllNativeContextSamplingTest.java @@ -16,14 +16,11 @@ package com.datadoghq.profiler.context; import com.datadoghq.profiler.AbstractProfilerTest; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import com.datadoghq.profiler.Platform; import org.junit.jupiter.api.Assumptions; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.common.unit.IQuantity; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -67,22 +64,12 @@ public void nativeOnlyContextIsVisibleToSampler() throws Exception { profiler.clearTraceContext(); stopProfiler(); - IItemCollection events = verifyEvents("datadog.MethodSample"); + JfrEvents events = verifyEvents("datadog.MethodSample"); boolean found = false; - for (IItemIterable samples : events) { - IMemberAccessor spanIdAccessor = SPAN_ID.getAccessor(samples.getType()); - IMemberAccessor rootSpanIdAccessor = LOCAL_ROOT_SPAN_ID.getAccessor(samples.getType()); - if (spanIdAccessor == null || rootSpanIdAccessor == null) { - continue; - } - for (IItem sample : samples) { - if (spanIdAccessor.getMember(sample).longValue() == EXPECTED_SPAN_ID - && rootSpanIdAccessor.getMember(sample).longValue() == EXPECTED_LOCAL_ROOT_SPAN_ID) { - found = true; - break; - } - } - if (found) { + for (JfrEvent sample : events) { + if (sample.getLong(SPAN_ID, -1) == EXPECTED_SPAN_ID + && sample.getLong(LOCAL_ROOT_SPAN_ID, -1) == EXPECTED_LOCAL_ROOT_SPAN_ID) { + found = true; break; } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/context/CustomContextAttributeSamplingTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/context/CustomContextAttributeSamplingTest.java index 0a5b43c8f8..84784a32ff 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/context/CustomContextAttributeSamplingTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/context/CustomContextAttributeSamplingTest.java @@ -17,16 +17,12 @@ import com.datadoghq.profiler.AbstractProfilerTest; import com.datadoghq.profiler.ContextSetter; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import com.datadoghq.profiler.Platform; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.common.unit.IQuantity; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; import java.util.Arrays; import java.util.HashMap; @@ -83,31 +79,21 @@ public void customAttributeValueSurfacesInJfr() throws InterruptedException { } stopProfiler(); - IItemCollection events = verifyEvents("datadog.MethodSample"); + JfrEvents events = verifyEvents("datadog.MethodSample"); Map weightsByTagValue = new HashMap<>(); - for (IItemIterable samples : events) { - IMemberAccessor weightAccessor = WEIGHT.getAccessor(samples.getType()); - IMemberAccessor tag1Accessor = TAG_1.getAccessor(samples.getType()); - IMemberAccessor tag2Accessor = TAG_2.getAccessor(samples.getType()); - if (tag1Accessor == null || tag2Accessor == null) { + for (JfrEvent sample : events) { + String stacktrace = sample.getStackTraceString(); + if (stacktrace == null || !stacktrace.contains("sleep") || stacktrace.contains("")) { + // Only count samples taken while the context was definitely active. continue; } - IMemberAccessor stacktraceAccessor = - JdkAttributes.STACK_TRACE_STRING.getAccessor(samples.getType()); - for (IItem sample : samples) { - String stacktrace = stacktraceAccessor.getMember(sample); - if (!stacktrace.contains("sleep") || stacktrace.contains("")) { - // Only count samples taken while the context was definitely active. - continue; - } - String tag = tag1Accessor.getMember(sample); - if (tag == null) { - continue; - } - weightsByTagValue.computeIfAbsent(tag, v -> new AtomicLong()) - .addAndGet(weightAccessor.getMember(sample).longValue()); - assertNull(tag2Accessor.getMember(sample), "tag2 was never set for this thread"); + String tag = sample.getString(TAG_1); + if (tag == null) { + continue; } + weightsByTagValue.computeIfAbsent(tag, v -> new AtomicLong()) + .addAndGet(sample.getLong(WEIGHT, 0)); + assertNull(sample.getString(TAG_2), "tag2 was never set for this thread"); } for (String value : values) { assertNotNull(weightsByTagValue.get(value), @@ -116,17 +102,11 @@ public void customAttributeValueSurfacesInJfr() throws InterruptedException { // jdk.ActiveSetting must enumerate the configured attribute names, unbundling the // dynamic-column config into the recording. - IItemCollection activeSettings = verifyEvents("jdk.ActiveSetting"); + JfrEvents activeSettings = verifyEvents("jdk.ActiveSetting"); Set recordedContextAttributes = new HashSet<>(); - for (IItemIterable activeSetting : activeSettings) { - IMemberAccessor nameAccessor = - JdkAttributes.REC_SETTING_NAME.getAccessor(activeSetting.getType()); - IMemberAccessor valueAccessor = - JdkAttributes.REC_SETTING_VALUE.getAccessor(activeSetting.getType()); - for (IItem item : activeSetting) { - if ("contextattribute".equals(nameAccessor.getMember(item))) { - recordedContextAttributes.add(valueAccessor.getMember(item)); - } + for (JfrEvent item : activeSettings) { + if ("contextattribute".equals(item.getString("name"))) { + recordedContextAttributes.add(item.getString("value")); } } assertEquals(3, recordedContextAttributes.size()); @@ -136,12 +116,8 @@ public void customAttributeValueSurfacesInJfr() throws InterruptedException { // dictionary_context_keys must match the number of distinct values registered above. Map jfrCounters = new HashMap<>(); - for (IItemIterable counterEvent : verifyEvents("datadog.ProfilerCounter")) { - IMemberAccessor nameAccessor = NAME.getAccessor(counterEvent.getType()); - IMemberAccessor countAccessor = COUNT.getAccessor(counterEvent.getType()); - for (IItem item : counterEvent) { - jfrCounters.put(nameAccessor.getMember(item), countAccessor.getMember(item).longValue()); - } + for (JfrEvent item : verifyEvents("datadog.ProfilerCounter")) { + jfrCounters.put(item.getString(NAME), item.getLong(COUNT, 0)); } assertFalse(jfrCounters.isEmpty()); assertEquals(values.length, jfrCounters.get("dictionary_context_keys")); diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/CTimerSamplerTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/CTimerSamplerTest.java index 1dabea65b1..6fe6fe3073 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/CTimerSamplerTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/CTimerSamplerTest.java @@ -1,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.cpu; import com.datadoghq.profiler.AbstractProfilerTest; @@ -13,18 +18,13 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.common.unit.IQuantity; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeFalse; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -54,15 +54,13 @@ public void test(@CStack String cstack) throws ExecutionException, InterruptedEx verifyCStackSettings(); - IItemCollection events = verifyEvents("datadog.ExecutionSample"); - - for (IItemIterable cpuSamples : events) { - IMemberAccessor frameAccessor = JdkAttributes.STACK_TRACE_STRING.getAccessor(cpuSamples.getType()); - for (IItem sample : cpuSamples) { - String stackTrace = frameAccessor.getMember(sample); - assertFalse(stackTrace.contains("jvmtiError")); - } - } + // Streamed rather than materialized: cpu=100us over this workload can produce tens of + // thousands of samples, and every check here is per-event with no need to retain them. + long sampleCount = streamEvents("datadog.ExecutionSample", sample -> { + String stackTrace = sample.getStackTraceString(); + assertFalse(stackTrace.contains("jvmtiError")); + }); + assertTrue(sampleCount > 0, "datadog.ExecutionSample was empty"); } @Override diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/ClinitResolutionTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/ClinitResolutionTest.java index b4c5d0c516..10ca084fd7 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/ClinitResolutionTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/ClinitResolutionTest.java @@ -9,11 +9,8 @@ import com.datadoghq.profiler.Platform; import org.junit.jupiter.api.Assumptions; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -87,23 +84,17 @@ public void testClinitFrameIsResolvedNotUnknown() throws Exception { // SpinningClinit — i.e. both the class name and the method name are present, // not replaced by "unknown". // - // JMC's STACK_TRACE_STRING HTML-escapes angle brackets, so appears as - // "<clinit>". Matching on "clinit" catches both representations. - IItemCollection events = verifyEvents("datadog.ExecutionSample"); + // Matching on the bare "clinit" token is robust regardless of how the class + // initializer's method name is escaped/rendered. + JfrEvents events = verifyEvents("datadog.ExecutionSample"); boolean foundClinit = false; - outer: - for (IItemIterable cpuSamples : events) { - IMemberAccessor frameAccessor = - JdkAttributes.STACK_TRACE_STRING.getAccessor(cpuSamples.getType()); - if (frameAccessor == null) continue; - for (IItem sample : cpuSamples) { - String stackTrace = frameAccessor.getMember(sample); - if (stackTrace != null - && stackTrace.contains("SpinningClinit") - && stackTrace.contains("clinit")) { - foundClinit = true; - break outer; - } + for (JfrEvent sample : events) { + String stackTrace = sample.getStackTraceString(); + if (stackTrace != null + && stackTrace.contains("SpinningClinit") + && stackTrace.contains("clinit")) { + foundClinit = true; + break; } } assertTrue(foundClinit, diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/ContextCpuTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/ContextCpuTest.java index 3afd598cb3..ec46ac8a16 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/ContextCpuTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/ContextCpuTest.java @@ -1,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.cpu; import java.util.Map; @@ -16,12 +21,8 @@ import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.provider.ValueSource; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.common.unit.IQuantity; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import com.datadoghq.profiler.AbstractProfilerTest; import static com.datadoghq.profiler.MoreAssertions.assertInRange; @@ -55,7 +56,7 @@ public void test(@CStack String cstack) throws ExecutionException, InterruptedEx Set method1SpanIds = profiledCode.spanIdsForMethod("method1Impl"); Set method2SpanIds = profiledCode.spanIdsForMethod("method2Impl"); Set method3SpanIds = profiledCode.spanIdsForMethod("method3Impl"); - IItemCollection events = verifyEvents("datadog.ExecutionSample"); + JfrEvents events = verifyEvents("datadog.ExecutionSample"); // on mac the usage of itimer to drive the sampling provides very unreliable outputs if (!Platform.isMac()) { @@ -65,42 +66,36 @@ public void test(@CStack String cstack) throws ExecutionException, InterruptedEx long method2Weight = 0; long method3Weight = 0; long totalWeight = 0; - for (IItemIterable cpuSamples : events) { - IMemberAccessor frameAccessor = JdkAttributes.STACK_TRACE_STRING.getAccessor(cpuSamples.getType()); - IMemberAccessor spanIdAccessor = SPAN_ID.getAccessor(cpuSamples.getType()); - IMemberAccessor rootSpanIdAccessor = LOCAL_ROOT_SPAN_ID.getAccessor(cpuSamples.getType()); - IMemberAccessor stateAccessor = THREAD_STATE.getAccessor(cpuSamples.getType()); - for (IItem sample : cpuSamples) { - String stackTrace = frameAccessor.getMember(sample); - long spanId = spanIdAccessor.getMember(sample).longValue(); - long rootSpanId = rootSpanIdAccessor.getMember(sample).longValue(); - String state = stateAccessor.getMember(sample); - assertDoesNotThrow(() -> Thread.State.valueOf(state)); - assertEquals(Thread.State.RUNNABLE, Thread.State.valueOf(state)); - if (stackTrace.contains("method3Impl")) { - // method3 is scheduled after method2, and method1 blocks on it, so spanId == rootSpanId + 2 - if (spanId > 0) { - assertEquals(rootSpanId + 2, spanId, stackTrace); - assertTrue(method3SpanIds.contains(spanId), stackTrace); - method3Weight += 1; - } - } else if (stackTrace.contains("method2Impl")) { - // method2 is called next, so spanId == rootSpanId + 1 - if (spanId > 0) { - assertEquals(rootSpanId + 1, spanId, stackTrace); - assertTrue(method2SpanIds.contains(spanId), stackTrace); - method2Weight += 1; - } - } else if (stackTrace.contains("method1Impl") - && !stackTrace.contains("method2") && !stackTrace.contains("method3")) { - // need to check this after method2 because method1 calls method2 - // it's the root so spanId == rootSpanId - assertEquals(rootSpanId, spanId, stackTrace); - assertTrue(spanId == 0 || method1SpanIds.contains(spanId), stackTrace); - method1Weight += 1; + for (JfrEvent sample : events) { + String stackTrace = sample.getStackTraceString(); + long spanId = sample.getLong(SPAN_ID, 0); + long rootSpanId = sample.getLong(LOCAL_ROOT_SPAN_ID, 0); + String state = sample.getEnumName(THREAD_STATE); + assertDoesNotThrow(() -> Thread.State.valueOf(state)); + assertEquals(Thread.State.RUNNABLE, Thread.State.valueOf(state)); + if (stackTrace.contains("method3Impl")) { + // method3 is scheduled after method2, and method1 blocks on it, so spanId == rootSpanId + 2 + if (spanId > 0) { + assertEquals(rootSpanId + 2, spanId, stackTrace); + assertTrue(method3SpanIds.contains(spanId), stackTrace); + method3Weight += 1; + } + } else if (stackTrace.contains("method2Impl")) { + // method2 is called next, so spanId == rootSpanId + 1 + if (spanId > 0) { + assertEquals(rootSpanId + 1, spanId, stackTrace); + assertTrue(method2SpanIds.contains(spanId), stackTrace); + method2Weight += 1; } - totalWeight++; + } else if (stackTrace.contains("method1Impl") + && !stackTrace.contains("method2") && !stackTrace.contains("method3")) { + // need to check this after method2 because method1 calls method2 + // it's the root so spanId == rootSpanId + assertEquals(rootSpanId, spanId, stackTrace); + assertTrue(spanId == 0 || method1SpanIds.contains(spanId), stackTrace); + method1Weight += 1; } + totalWeight++; } assertInRange(method1Weight / (double) totalWeight, 0.1, 0.6); assertInRange(method2Weight / (double) totalWeight, 0.1, 0.6); diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/LightweightContextCpuTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/LightweightContextCpuTest.java index 8498738fa8..73160d1891 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/LightweightContextCpuTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/LightweightContextCpuTest.java @@ -1,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.cpu; import com.datadoghq.profiler.AbstractProfilerTest; @@ -5,12 +10,8 @@ import com.datadoghq.profiler.context.ContextExecutor; import com.datadoghq.profiler.context.Tracing; import org.junit.jupiter.api.Assumptions; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.common.unit.IQuantity; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import java.util.HashSet; import java.util.List; @@ -40,27 +41,21 @@ public void test() throws ExecutionException, InterruptedException { } stopProfiler(); Set sampledSpanIds = profiledCode.allSampledSpanIds(); - IItemCollection events = verifyEvents("datadog.ExecutionSample"); + JfrEvents events = verifyEvents("datadog.ExecutionSample"); int numNonZeroContexts = 0; - for (IItemIterable cpuSamples : events) { - IMemberAccessor frameAccessor = JdkAttributes.STACK_TRACE_STRING.getAccessor(cpuSamples.getType()); - IMemberAccessor spanIdAccessor = SPAN_ID.getAccessor(cpuSamples.getType()); - IMemberAccessor rootSpanIdAccessor = LOCAL_ROOT_SPAN_ID.getAccessor(cpuSamples.getType()); - IMemberAccessor stateAccessor = THREAD_STATE.getAccessor(cpuSamples.getType()); - for (IItem sample : cpuSamples) { - String stackTrace = frameAccessor.getMember(sample); - assertNull(stackTrace); - long spanId = spanIdAccessor.getMember(sample).longValue(); - long rootSpanId = rootSpanIdAccessor.getMember(sample).longValue(); - numNonZeroContexts += (spanId != 0 && rootSpanId != 0 ? 1 : 0); - if (spanId > 0) { - assertTrue(sampledSpanIds.contains(spanId)); - } - String state = stateAccessor.getMember(sample); - assertDoesNotThrow(() -> Thread.State.valueOf(state)); - assertEquals(Thread.State.RUNNABLE, Thread.State.valueOf(state)); + for (JfrEvent sample : events) { + String stackTrace = sample.getStackTraceString(); + assertNull(stackTrace); + long spanId = sample.getLong(SPAN_ID, 0); + long rootSpanId = sample.getLong(LOCAL_ROOT_SPAN_ID, 0); + numNonZeroContexts += (spanId != 0 && rootSpanId != 0 ? 1 : 0); + if (spanId > 0) { + assertTrue(sampledSpanIds.contains(spanId)); } + String state = sample.getEnumName(THREAD_STATE); + assertDoesNotThrow(() -> Thread.State.valueOf(state)); + assertEquals(Thread.State.RUNNABLE, Thread.State.valueOf(state)); } assertTrue(numNonZeroContexts > 0, "no context"); Map debugCounters = profiler.getDebugCounters(); diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/NativeThreadPrimingTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/NativeThreadPrimingTest.java index a231ee4dbb..97e0dcd2af 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/NativeThreadPrimingTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/NativeThreadPrimingTest.java @@ -10,12 +10,8 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.params.provider.ValueSource; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; +import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -46,26 +42,19 @@ public void testPreExistingNativeThreadsHaveUsableFrames() throws Exception { } stopProfiler(); - IItemCollection events = verifyEvents("datadog.ExecutionSample"); - int syntheticSamples = 0; - int totalSamples = 0; - for (IItemIterable cpuSamples : events) { - IMemberAccessor frameAccessor = - JdkAttributes.STACK_TRACE_STRING.getAccessor(cpuSamples.getType()); - for (IItem sample : cpuSamples) { - totalSamples++; - String stackTrace = frameAccessor.getMember(sample); - if (stackTrace.contains(UNKNOWN_NATIVE_THREAD_FRAME) - || NO_JAVA_FRAME_ONLY.matcher(stackTrace).matches()) { - syntheticSamples++; - } + AtomicInteger syntheticSamples = new AtomicInteger(); + long totalSamples = streamEvents("datadog.ExecutionSample", item -> { + String stackTrace = item.getStackTraceString(); + if (stackTrace.contains(UNKNOWN_NATIVE_THREAD_FRAME) + || NO_JAVA_FRAME_ONLY.matcher(stackTrace).matches()) { + syntheticSamples.incrementAndGet(); } - } + }); - assertTrue(syntheticSamples <= MAX_SYNTHETIC_NATIVE_THREAD_SAMPLES, + assertTrue(syntheticSamples.get() <= MAX_SYNTHETIC_NATIVE_THREAD_SAMPLES, "Expected at most " + MAX_SYNTHETIC_NATIVE_THREAD_SAMPLES + " samples with a synthetic native-thread frame, got " - + syntheticSamples + " of " + totalSamples); + + syntheticSamples.get() + " of " + totalSamples); } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/RemoteSymbolicationTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/RemoteSymbolicationTest.java index eb3e852bd4..89e07023bb 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/RemoteSymbolicationTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/RemoteSymbolicationTest.java @@ -14,24 +14,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.params.provider.ValueSource; -import org.openjdk.jmc.common.IMCFrame; -import org.openjdk.jmc.common.IMCMethod; -import org.openjdk.jmc.common.IMCStackTrace; -import org.openjdk.jmc.common.IMCType; -import org.openjdk.jmc.common.item.Attribute; -import org.openjdk.jmc.common.item.IAttribute; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; - -import java.util.List; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; +import com.datadoghq.profiler.JfrFrame; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import static org.openjdk.jmc.common.unit.UnitLookup.PLAIN_TEXT; /** * Integration test for remote symbolication feature. @@ -78,31 +67,22 @@ public void testRemoteSymbolicationEnabled(@CStack String cstack) throws Excepti // First verify that our test library (libddproftest) has a build-id // We use the extended jdk.NativeLibrary event which now includes buildId and loadBias fields - IItemCollection libraryEvents = verifyEvents("jdk.NativeLibrary"); + JfrEvents libraryEvents = verifyEvents("jdk.NativeLibrary"); String testLibBuildId = null; boolean foundTestLib = false; - // Create attributes for the custom fields we added to jdk.NativeLibrary - IAttribute buildIdAttr = Attribute.attr("buildId", "buildId", "GNU Build ID", PLAIN_TEXT); - IAttribute nameAttr = Attribute.attr("name", "name", "Name", PLAIN_TEXT); + for (JfrEvent libItem : libraryEvents) { + String name = libItem.getString("name"); + String buildId = libItem.getString("buildId"); - for (IItemIterable libItems : libraryEvents) { - IMemberAccessor buildIdAccessor = buildIdAttr.getAccessor(libItems.getType()); - IMemberAccessor nameAccessor = nameAttr.getAccessor(libItems.getType()); + System.out.println("Library: " + name + " -> build-id: " + + (buildId != null && !buildId.isEmpty() ? buildId : "")); - for (IItem libItem : libItems) { - String name = nameAccessor.getMember(libItem); - String buildId = buildIdAccessor.getMember(libItem); - - System.out.println("Library: " + name + " -> build-id: " + - (buildId != null && !buildId.isEmpty() ? buildId : "")); - - // Check if this is our test library - if (name != null && name.contains("libddproftest")) { - foundTestLib = true; - testLibBuildId = buildId; - System.out.println("Found test library: " + name + " with build-id: " + buildId); - } + // Check if this is our test library + if (name != null && name.contains("libddproftest")) { + foundTestLib = true; + testLibBuildId = buildId; + System.out.println("Found test library: " + name + " with build-id: " + buildId); } } @@ -114,7 +94,7 @@ public void testRemoteSymbolicationEnabled(@CStack String cstack) throws Excepti "Test library libddproftest found but has no build-id. " + "Cannot test remote symbolication without build-id."); - IItemCollection events = verifyEvents("datadog.ExecutionSample"); + JfrEvents events = verifyEvents("datadog.ExecutionSample"); boolean foundTestLibFrame = false; boolean foundTestLibRemoteFrame = false; @@ -122,68 +102,53 @@ public void testRemoteSymbolicationEnabled(@CStack String cstack) throws Excepti int printCount = 0; int testLibFrameCount = 0; - for (IItemIterable cpuSamples : events) { - IMemberAccessor stackTraceAccessor = - STACK_TRACE.getAccessor(cpuSamples.getType()); - - for (IItem sample : cpuSamples) { - IMCStackTrace stackTrace = stackTraceAccessor.getMember(sample); - if (stackTrace == null) { - continue; + for (JfrEvent sample : events) { + if (!sample.has("stackTrace")) { + continue; + } + sampleCount++; + + // Iterate through frames to check for test library frames + for (JfrFrame frame : sample.getStackTrace().frames()) { + // Check for jvmtiError in method name + String methodName = frame.methodName(); + if (methodName != null && methodName.contains("jvmtiError")) { + fail("Found jvmtiError in frame method name: " + methodName); } - sampleCount++; - - // Iterate through frames to check for test library frames - List frames = stackTrace.getFrames(); - - for (IMCFrame frame : frames) { - IMCMethod method = frame.getMethod(); - if (method == null) { - continue; - } - - // Check for jvmtiError in method name - String methodName = method.getMethodName(); - if (methodName != null && methodName.contains("jvmtiError")) { - fail("Found jvmtiError in frame method name: " + methodName); - } - - // Get class name (contains build-id for remote symbolication frames) - IMCType type = method.getType(); - String className = type != null ? type.getFullName() : null; - - // Check if this is a remote symbolication frame from our test library - // Format in JFR: type.name = build-ID (bare, no suffix), method.name = "" - if (methodName != null && methodName.equals("") && - className != null && className.equals(testLibBuildId)) { - foundTestLibRemoteFrame = true; - testLibFrameCount++; - foundTestLibFrame = true; - - // Print first remote frame for debugging - if (printCount == 0) { - System.out.println("=== First remote symbolication frame ==="); - System.out.println("Class: " + className); - System.out.println("Method: " + methodName); - System.out.println("Signature: " + (method.getFormalDescriptor() != null ? method.getFormalDescriptor() : "null")); - printCount++; - } + // Get class name (contains build-id for remote symbolication frames) + String className = frame.className(); + + // Check if this is a remote symbolication frame from our test library + // Format in JFR: type.name = build-ID (bare, no suffix), method.name = "" + if (methodName != null && methodName.equals("") && + className != null && className.equals(testLibBuildId)) { + foundTestLibRemoteFrame = true; + testLibFrameCount++; + foundTestLibFrame = true; + + // Print first remote frame for debugging + if (printCount == 0) { + System.out.println("=== First remote symbolication frame ==="); + System.out.println("Class: " + className); + System.out.println("Method: " + methodName); + System.out.println("Signature: " + (frame.methodDescriptor() != null ? frame.methodDescriptor() : "null")); + printCount++; } + } - // With remote symbolication, we should see method names, not resolved symbols - // Log a warning if we find resolved symbols (indicates remote symbolication didn't work for this frame) - if (methodName != null && (methodName.equals("burn_cpu") || methodName.equals("compute_fibonacci"))) { - System.out.println("WARNING: Found resolved symbol instead of remote frame: " + methodName + " (class: " + className + ")"); - } + // With remote symbolication, we should see method names, not resolved symbols + // Log a warning if we find resolved symbols (indicates remote symbolication didn't work for this frame) + if (methodName != null && (methodName.equals("burn_cpu") || methodName.equals("compute_fibonacci"))) { + System.out.println("WARNING: Found resolved symbol instead of remote frame: " + methodName + " (class: " + className + ")"); + } - // Also count frames with resolved symbols from libddproftest - // (for fallback case or if library name appears in class name) - if ((methodName != null && (methodName.contains("burn_cpu") || methodName.contains("compute_fibonacci"))) || - (className != null && className.contains("libddproftest"))) { - foundTestLibFrame = true; - // Don't increment testLibFrameCount here to avoid double-counting - } + // Also count frames with resolved symbols from libddproftest + // (for fallback case or if library name appears in class name) + if ((methodName != null && (methodName.contains("burn_cpu") || methodName.contains("compute_fibonacci"))) || + (className != null && className.contains("libddproftest"))) { + foundTestLibFrame = true; + // Don't increment testLibFrameCount here to avoid double-counting } } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/SmokeCpuTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/SmokeCpuTest.java index fd065bd149..74a0ddf2b9 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/SmokeCpuTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/SmokeCpuTest.java @@ -1,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.cpu; import com.datadoghq.profiler.AbstractProfilerTest; @@ -7,11 +12,8 @@ import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.params.provider.ValueSource; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import java.util.concurrent.ExecutionException; @@ -37,21 +39,18 @@ public void testComputations(@CStack String cstack) throws Exception { verifyCStackSettings(); - IItemCollection events = verifyEvents("datadog.ExecutionSample"); + JfrEvents events = verifyEvents("datadog.ExecutionSample"); // on mac the usage of itimer to drive the sampling provides very unreliable outputs - for (IItemIterable cpuSamples : events) { - IMemberAccessor frameAccessor = JdkAttributes.STACK_TRACE_STRING.getAccessor(cpuSamples.getType()); - for (IItem sample : cpuSamples) { - String stackTrace = frameAccessor.getMember(sample); - assertFalse(stackTrace.contains("jvmtiError")); - if ("vmx".equals(stackTrace)) { - // extra checks to make sure we see the mixed stacktraces - assertTrue(stackTrace.contains("JavaCalls::call_virtual()"), - "JavaCalls::call_virtual() (above call_stub) found in stack trace"); - assertTrue(stackTrace.contains("call_stub()"), - "call_stub() (sentinel value used to halt unwinding) found in stack trace"); - } + for (JfrEvent sample : events) { + String stackTrace = sample.getStackTraceString(); + assertFalse(stackTrace.contains("jvmtiError")); + if ("vmx".equals(stackTrace)) { + // extra checks to make sure we see the mixed stacktraces + assertTrue(stackTrace.contains("JavaCalls::call_virtual()"), + "JavaCalls::call_virtual() (above call_stub) found in stack trace"); + assertTrue(stackTrace.contains("call_stub()"), + "call_stub() (sentinel value used to halt unwinding) found in stack trace"); } } } @@ -67,21 +66,18 @@ public void testIOBound(@CStack String cstack) throws Exception { verifyCStackSettings(); - IItemCollection events = verifyEvents("datadog.ExecutionSample"); + JfrEvents events = verifyEvents("datadog.ExecutionSample"); // on mac the usage of itimer to drive the sampling provides very unreliable outputs - for (IItemIterable cpuSamples : events) { - IMemberAccessor frameAccessor = JdkAttributes.STACK_TRACE_STRING.getAccessor(cpuSamples.getType()); - for (IItem sample : cpuSamples) { - String stackTrace = frameAccessor.getMember(sample); - assertFalse(stackTrace.contains("jvmtiError")); - if ("vmx".equals(stackTrace)) { - // extra checks to make sure we see the mixed stacktraces - assertTrue(stackTrace.contains("JavaCalls::call_virtual()"), - "JavaCalls::call_virtual() (above call_stub) found in stack trace"); - assertTrue(stackTrace.contains("call_stub()"), - "call_stub() (sentinel value used to halt unwinding) found in stack trace"); - } + for (JfrEvent sample : events) { + String stackTrace = sample.getStackTraceString(); + assertFalse(stackTrace.contains("jvmtiError")); + if ("vmx".equals(stackTrace)) { + // extra checks to make sure we see the mixed stacktraces + assertTrue(stackTrace.contains("JavaCalls::call_virtual()"), + "JavaCalls::call_virtual() (above call_stub) found in stack trace"); + assertTrue(stackTrace.contains("call_stub()"), + "call_stub() (sentinel value used to halt unwinding) found in stack trace"); } } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/VtableReceiverFrameTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/VtableReceiverFrameTest.java index e3c4dfd1f9..84d1775b9d 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/VtableReceiverFrameTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/VtableReceiverFrameTest.java @@ -1,14 +1,16 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.cpu; import com.datadoghq.profiler.AbstractProfilerTest; import com.datadoghq.profiler.Platform; import org.junit.jupiter.api.Assumptions; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import java.util.concurrent.ThreadLocalRandom; @@ -62,32 +64,24 @@ public void testVtableReceiverFrameInCpuSamples() throws Exception { System.err.println(result); stopProfiler(); - IItemCollection events = verifyEvents("datadog.ExecutionSample"); + JfrEvents events = verifyEvents("datadog.ExecutionSample"); boolean foundVtableWithReceiver = false; - for (IItemIterable cpuSamples : events) { - IMemberAccessor frameAccessor = - JdkAttributes.STACK_TRACE_STRING.getAccessor(cpuSamples.getType()); - if (frameAccessor == null) continue; - for (IItem sample : cpuSamples) { - String stackTrace = frameAccessor.getMember(sample); - if (stackTrace != null && stackTrace.contains(".vtable stub()")) { - System.err.println("=VTABLE STUB TRACE=\n" + stackTrace + "\n=END="); - } - // JMC's STACK_TRACE_STRING HTML-escapes angle brackets in method - // names (it does the same for /), so the synthetic - // method appears as "<vtable_receiver>" in the rendered string. - // Match on the bare token so the test is robust to either form. - if (stackTrace != null - && stackTrace.contains(".vtable stub()") - && stackTrace.contains("vtable_receiver") - && (stackTrace.contains("Circle") - || stackTrace.contains("Square") - || stackTrace.contains("Triangle"))) { - foundVtableWithReceiver = true; - break; - } + for (JfrEvent sample : events) { + String stackTrace = sample.getStackTraceString(); + if (stackTrace != null && stackTrace.contains(".vtable stub()")) { + System.err.println("=VTABLE STUB TRACE=\n" + stackTrace + "\n=END="); + } + // Match on the bare token so the test is robust regardless of how the + // synthetic method name is escaped/rendered. + if (stackTrace != null + && stackTrace.contains(".vtable stub()") + && stackTrace.contains("vtable_receiver") + && (stackTrace.contains("Circle") + || stackTrace.contains("Square") + || stackTrace.contains("Triangle"))) { + foundVtableWithReceiver = true; + break; } - if (foundVtableWithReceiver) break; } assertTrue(foundVtableWithReceiver, "No CPU sample contained a vtable stub frame, a vtable_receiver synthetic frame, " + diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/endpoints/EndpointTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/endpoints/EndpointTest.java index 97baba6e26..c73bfaf9bd 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/endpoints/EndpointTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/endpoints/EndpointTest.java @@ -1,13 +1,14 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.endpoints; import com.datadoghq.profiler.AbstractProfilerTest; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import org.junit.jupiter.api.Test; -import org.openjdk.jmc.common.item.IAttribute; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.common.unit.IQuantity; import java.util.Arrays; import java.util.BitSet; @@ -18,8 +19,6 @@ import static com.datadoghq.profiler.MoreAssertions.assertBoundedBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.openjdk.jmc.common.item.Attribute.attr; -import static org.openjdk.jmc.common.unit.UnitLookup.PLAIN_TEXT; public class EndpointTest extends AbstractProfilerTest { @@ -43,25 +42,18 @@ public void testEndpoints() { Map debugCounters = profiler.getDebugCounters(); assertEquals(endpoints.length, debugCounters.get("dictionary_endpoints_keys")); stopProfiler(); - IItemCollection events = verifyEvents("datadog.Endpoint"); - IAttribute endpointAttribute = attr("endpoint", "endpoint", "endpoint", - PLAIN_TEXT); + JfrEvents events = verifyEvents("datadog.Endpoint"); BitSet recovered = new BitSet(); - for (IItemIterable it : events) { - IMemberAccessor endpointAccessor = endpointAttribute.getAccessor(it.getType()); - IMemberAccessor rootSpanIdAccessor = LOCAL_ROOT_SPAN_ID.getAccessor(it.getType()); - IMemberAccessor operationAccessor = OPERATION.getAccessor(it.getType()); - for (IItem event : it) { - long rootSpanId = rootSpanIdAccessor.getMember(event).longValue(); - String operation = operationAccessor.getMember(event); - Endpoint endpoint = endpoints[(int) rootSpanId]; - recovered.set((int) rootSpanId); - String message = endpoint.toString(); - String recordedEndpoint = endpointAccessor.getMember(event); - assertEquals(endpoint.endpoint, recordedEndpoint, message); - assertEquals(endpoint.rootSpanId, rootSpanId, message); - assertEquals(endpoint.operation, operation, message); - } + for (JfrEvent event : events) { + long rootSpanId = event.getLong(LOCAL_ROOT_SPAN_ID, -1); + String operation = event.getString(OPERATION); + Endpoint endpoint = endpoints[(int) rootSpanId]; + recovered.set((int) rootSpanId); + String message = endpoint.toString(); + String recordedEndpoint = event.getString("endpoint"); + assertEquals(endpoint.endpoint, recordedEndpoint, message); + assertEquals(endpoint.rootSpanId, rootSpanId, message); + assertEquals(endpoint.operation, operation, message); } for (int i = 0; i < endpoints.length; i++) { assertTrue(recovered.get(i), i + " not tested"); diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/memleak/GCGenerationsTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/memleak/GCGenerationsTest.java index a7bcb5cf6b..152f1b0c17 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/memleak/GCGenerationsTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/memleak/GCGenerationsTest.java @@ -1,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.memleak; import com.datadoghq.profiler.Platform; @@ -5,16 +10,12 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.Aggregators; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.ItemFilters; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicLong; -import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Assumptions; public class GCGenerationsTest extends AbstractProfilerTest { @@ -33,7 +34,10 @@ public void shouldGetLiveObjectSamples() throws InterruptedException { MemLeakTarget target1 = new MemLeakTarget(); MemLeakTarget target2 = new MemLeakTarget(); runTests(target1, target2); - verifyEvents("datadog.HeapLiveObject"); + // With "generations" tracking, every retained survivor is re-reported on each flush + // cycle for the rest of the run, which can drive the event count well past what's safe + // to hold fully resolved in memory, so only presence is checked, not materialized. + verifyEventPresent("datadog.HeapLiveObject"); } public static class MemLeakTarget extends ClassValue implements Runnable { diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/memleak/LivenessTrackingTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/memleak/LivenessTrackingTest.java index 4fd9b1e78a..be9494986a 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/memleak/LivenessTrackingTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/memleak/LivenessTrackingTest.java @@ -7,15 +7,11 @@ import com.datadoghq.profiler.Platform; import com.datadoghq.profiler.AbstractProfilerTest; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; +import com.datadoghq.profiler.JfrStackTrace; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.IMCStackTrace; -import org.openjdk.jmc.common.item.Aggregators; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.ItemFilters; -import org.openjdk.jmc.flightrecorder.JfrLoaderToolkit; import java.nio.file.Files; import java.nio.file.Path; @@ -78,12 +74,10 @@ public void shouldPreserveLiveObjectTracesAcrossJFRDumps() throws Exception { assertTrue(Files.exists(firstDump), "First JFR dump should be created"); // Parse first recording and verify liveness samples - IItemCollection firstRecording = JfrLoaderToolkit.loadEvents(Files.newInputStream(firstDump)); - IItemCollection firstLiveObjects = firstRecording.apply( - ItemFilters.type("datadog.HeapLiveObject")); + JfrEvents firstLiveObjects = JfrEvents.load(firstDump, "datadog.HeapLiveObject"); assertTrue(firstLiveObjects.hasItems(), "First recording should contain live object samples"); - long firstSampleCount = firstLiveObjects.getAggregate(Aggregators.count()).longValue(); + long firstSampleCount = firstLiveObjects.count(); assertTrue(firstSampleCount > 0, "First recording should have liveness samples"); // Verify all live object samples have stack traces with at least one frame @@ -116,12 +110,10 @@ public void shouldPreserveLiveObjectTracesAcrossJFRDumps() throws Exception { assertTrue(Files.exists(secondDump), "Second JFR dump should be created"); // Parse second recording and verify reduced liveness samples - IItemCollection secondRecording = JfrLoaderToolkit.loadEvents(Files.newInputStream(secondDump)); - IItemCollection secondLiveObjects = secondRecording.apply( - ItemFilters.type("datadog.HeapLiveObject")); + JfrEvents secondLiveObjects = JfrEvents.load(secondDump, "datadog.HeapLiveObject"); assertTrue(secondLiveObjects.hasItems(), "Second recording should contain live object samples"); - long secondSampleCount = secondLiveObjects.getAggregate(Aggregators.count()).longValue(); + long secondSampleCount = secondLiveObjects.count(); // Verify all live object samples have stack traces with at least one frame verifyStackTracesPresent(secondLiveObjects); @@ -154,21 +146,19 @@ public void shouldPreserveLiveObjectTracesAcrossJFRDumps() throws Exception { * Verify that liveness samples have valid stack traces with at least one frame * Allow some tolerance for profiling timing issues */ - private void verifyStackTracesPresent(IItemCollection liveObjects) { + private void verifyStackTracesPresent(JfrEvents liveObjects) { AtomicInteger samplesWithoutStackTrace = new AtomicInteger(0); AtomicInteger samplesWithEmptyStackTrace = new AtomicInteger(0); AtomicInteger totalSamples = new AtomicInteger(0); - for (IItemIterable iterable : liveObjects) { - for (IItem item : iterable) { - totalSamples.incrementAndGet(); + for (JfrEvent item : liveObjects) { + totalSamples.incrementAndGet(); - IMCStackTrace stackTrace = STACK_TRACE.getAccessor(iterable.getType()).getMember(item); - if (stackTrace == null) { - samplesWithoutStackTrace.incrementAndGet(); - } else if (stackTrace.getFrames().isEmpty()) { - samplesWithEmptyStackTrace.incrementAndGet(); - } + JfrStackTrace stackTrace = item.getStackTrace(STACK_TRACE); + if (stackTrace == null) { + samplesWithoutStackTrace.incrementAndGet(); + } else if (stackTrace.isEmpty()) { + samplesWithEmptyStackTrace.incrementAndGet(); } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/memleak/MemleakProfilerTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/memleak/MemleakProfilerTest.java index adbca5ab61..2b908789c7 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/memleak/MemleakProfilerTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/memleak/MemleakProfilerTest.java @@ -1,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.memleak; import com.datadoghq.profiler.Platform; @@ -5,16 +10,12 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.Aggregators; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.ItemFilters; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicLong; -import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Assumptions; public class MemleakProfilerTest extends AbstractProfilerTest { @@ -33,24 +34,13 @@ public void shouldGetLiveObjectSamples() throws InterruptedException { MemLeakTarget target1 = new MemLeakTarget(); MemLeakTarget target2 = new MemLeakTarget(); runTests(target1, target2); - IItemCollection allocations = verifyEvents("datadog.HeapLiveObject"); - IItemCollection heapUsage = verifyEvents("datadog.HeapUsage"); -// assertAllocations(allocations, int[].class, target1, target2); -// assertAllocations(allocations, Integer[].class, target1, target2); - } - - private static void assertAllocations(IItemCollection allocations, Class clazz, MemLeakTarget... targets) { - long allocated = 0; - for (MemLeakTarget target : targets) { - allocated += target.getAllocated(clazz); - } - IItemCollection allocationsByType = allocations.apply(allocatedTypeFilter(clazz.getCanonicalName())); - assertTrue(allocationsByType.hasItems()); - long recorded = allocationsByType.getAggregate(Aggregators.sum(SCALED_SIZE)).longValue(); - long absoluteError = Math.abs(recorded - allocated); - assertTrue(absoluteError < allocated / 10, - String.format("allocation samples should be within 10pct tolerance of allocated memory (recorded %d, allocated %d)", - recorded, allocated)); + // Every retained survivor is re-reported on each flush cycle for the rest of the run, + // which can drive the event count well past what's safe to hold fully resolved in + // memory, so only presence is checked, not materialized. + verifyEventPresent("datadog.HeapLiveObject"); + // HeapUsage is a low-frequency periodic gauge event, unlike HeapLiveObject above, so + // materializing it is safe. + verifyEvents("datadog.HeapUsage"); } public static class MemLeakTarget extends ClassValue implements Runnable { @@ -72,10 +62,6 @@ public void run() { } } - long getAllocated(Class clazz) { - return get(clazz).get(); - } - private static void allocate(ThreadLocalRandom random, int depth) { if (depth > 0) { allocate(random, depth - 1); diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/metadata/BoundMethodHandleProfilerTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/metadata/BoundMethodHandleProfilerTest.java index bf2e4985f3..5899bc6242 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/metadata/BoundMethodHandleProfilerTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/metadata/BoundMethodHandleProfilerTest.java @@ -1,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.metadata; import com.datadoghq.profiler.Platform; @@ -39,7 +44,7 @@ public void test() throws Throwable { int x = generateBoundMethodHandles(numBoundMethodHandles); assertTrue(x != 0); stopProfiler(); - verifyEvents("datadog.MethodSample"); + verifyEventPresent("datadog.MethodSample"); Map counters = profiler.getDebugCounters(); assertFalse(counters.isEmpty(), "profiler debug counters must not be empty after BoundMethodHandle workload"); } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/metadata/DictionaryRotationTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/metadata/DictionaryRotationTest.java index 6b8c404152..7971a0031c 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/metadata/DictionaryRotationTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/metadata/DictionaryRotationTest.java @@ -16,12 +16,9 @@ package com.datadoghq.profiler.metadata; import com.datadoghq.profiler.AbstractProfilerTest; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import org.junit.jupiter.api.Test; -import org.openjdk.jmc.common.item.IAttribute; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; import java.nio.file.Files; import java.nio.file.Path; @@ -30,8 +27,6 @@ import java.util.Set; import static org.junit.jupiter.api.Assertions.*; -import static org.openjdk.jmc.common.item.Attribute.attr; -import static org.openjdk.jmc.common.unit.UnitLookup.PLAIN_TEXT; /** * Verifies that the dictionary rotate+clearStandby cycle correctly: @@ -41,9 +36,6 @@ */ public class DictionaryRotationTest extends AbstractProfilerTest { - private static final IAttribute ENDPOINT_ATTR = - attr("endpoint", "endpoint", "endpoint", PLAIN_TEXT); - @Test public void dumpCycleSeparatesPreAndPostDumpEntries() throws Exception { String[] preDump = { "ep_pre_0", "ep_pre_1", "ep_pre_2" }; @@ -103,15 +95,11 @@ protected String getProfilerCommand() { return "wall=~1ms"; } - private static Set endpointNames(IItemCollection events) { + private static Set endpointNames(JfrEvents events) { Set names = new HashSet<>(); - for (IItemIterable it : events) { - IMemberAccessor accessor = ENDPOINT_ATTR.getAccessor(it.getType()); - if (accessor == null) continue; - for (IItem item : it) { - String v = accessor.getMember(item); - if (v != null) names.add(v); - } + for (JfrEvent item : events) { + String v = item.getString("endpoint"); + if (v != null) names.add(v); } return names; } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/metadata/MetadataNormalisationTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/metadata/MetadataNormalisationTest.java index 08b6eafa3e..df7629984c 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/metadata/MetadataNormalisationTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/metadata/MetadataNormalisationTest.java @@ -1,12 +1,14 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.metadata; import com.datadoghq.profiler.AbstractProfilerTest; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import org.junit.jupiter.api.Test; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; import java.lang.reflect.Constructor; import java.lang.reflect.Method; @@ -45,7 +47,7 @@ public void test() throws Exception { } System.out.println(count); stopProfiler(); - IItemCollection executionSamples = verifyEvents("datadog.ExecutionSample"); + JfrEvents executionSamples = verifyEvents("datadog.ExecutionSample"); Matcher[] forbiddenPatternMatchers = Stream.of( "MH.*0x[A-Fa-f0-9]{3}", // method handles "GeneratedConstructorAccessor\\d+", @@ -53,14 +55,11 @@ public void test() throws Exception { ) .map(regex -> Pattern.compile(regex).matcher("")) .toArray(Matcher[]::new); - for (IItemIterable samples : executionSamples) { - IMemberAccessor stacktraceAccessor = JdkAttributes.STACK_TRACE_STRING.getAccessor(samples.getType()); - for (IItem item : samples) { - String stacktrace = stacktraceAccessor.getMember(item); - for (Matcher matcher : forbiddenPatternMatchers) { - matcher.reset(stacktrace); - assertFalse(matcher.find(), () -> matcher.pattern() + "\n" + stacktrace); - } + for (JfrEvent item : executionSamples) { + String stacktrace = item.getStackTraceString(); + for (Matcher matcher : forbiddenPatternMatchers) { + matcher.reset(stacktrace); + assertFalse(matcher.find(), () -> matcher.pattern() + "\n" + stacktrace); } } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/nativelibs/NativeLibrariesTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativelibs/NativeLibrariesTest.java index 5cd62c1b66..6b0ab82823 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/nativelibs/NativeLibrariesTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/nativelibs/NativeLibrariesTest.java @@ -1,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativelibs; import com.datadoghq.profiler.AbstractProfilerTest; @@ -10,10 +15,6 @@ import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; import org.xerial.snappy.Snappy; import java.io.IOException; @@ -66,39 +67,36 @@ public void test() { } stopProfiler(); assertTrue(blackhole != 0); - Map modeCounters = new HashMap<>(); - Map libraryCounters = new HashMap<>(); - for (IItemIterable cpuSamples : verifyEvents("datadog.ExecutionSample")) { - IMemberAccessor stacktraceAccessor = JdkAttributes.STACK_TRACE_STRING.getAccessor(cpuSamples.getType()); - IMemberAccessor modeAccessor = THREAD_EXECUTION_MODE.getAccessor(cpuSamples.getType()); - for (IItem item : cpuSamples) { - String stacktrace = stacktraceAccessor.getMember(item); - String mode = modeAccessor.getMember(item); - modeCounters.computeIfAbsent(mode, x -> new AtomicInteger()).incrementAndGet(); - if ("NATIVE".equals(mode)) { - String library = ""; - if (stacktrace.contains("LZ4JNI") || stacktrace.contains(".LZ4HC_")) { - library = "LZ4"; - } else if (stacktrace.contains("Java_org_xerial_snappy_SnappyNative") || stacktrace.contains("libsnappyjava")) { - library = "SNAPPY"; - } else if (stacktrace.contains("Java_com_github_luben_zstd") || stacktrace.contains(".ZSTD_")) { - library = "ZSTD"; - } else if (stacktrace.contains("Compile")) { - library = "JIT"; - } - libraryCounters.computeIfAbsent(library, x -> new AtomicInteger()).incrementAndGet(); + // Folded rather than materialized: a 1ms-CPU-sampled recording of this workload can carry + // far more datadog.ExecutionSample events (each with a native stack) than fit comfortably + // in the test heap if collected into a list first, and only the counts below are needed. + Counters counters = reduceEvents("datadog.ExecutionSample", Counters::new, (c, item) -> { + String stacktrace = item.getStackTraceString(); + String mode = item.getEnumName(THREAD_EXECUTION_MODE); + c.modeCounters.computeIfAbsent(mode, x -> new AtomicInteger()).incrementAndGet(); + if ("NATIVE".equals(mode)) { + String library = ""; + if (stacktrace.contains("LZ4JNI") || stacktrace.contains(".LZ4HC_")) { + library = "LZ4"; + } else if (stacktrace.contains("Java_org_xerial_snappy_SnappyNative") || stacktrace.contains("libsnappyjava")) { + library = "SNAPPY"; + } else if (stacktrace.contains("Java_com_github_luben_zstd") || stacktrace.contains(".ZSTD_")) { + library = "ZSTD"; + } else if (stacktrace.contains("Compile")) { + library = "JIT"; } + c.libraryCounters.computeIfAbsent(library, x -> new AtomicInteger()).incrementAndGet(); } - } - assertTrue(modeCounters.containsKey("JVM"), "no JVM samples"); - assertTrue(modeCounters.containsKey("NATIVE"), "no NATIVE samples"); - assertTrue(libraryCounters.containsKey("LZ4"), "no lz4-java samples"); + }); + assertTrue(counters.modeCounters.containsKey("JVM"), "no JVM samples"); + assertTrue(counters.modeCounters.containsKey("NATIVE"), "no NATIVE samples"); + assertTrue(counters.libraryCounters.containsKey("LZ4"), "no lz4-java samples"); // snappy is problematic on musl; we are not running it // for some reason it is not also appearing in sanitized runs - assertTrue(isMusl || isSanitizer || libraryCounters.containsKey("SNAPPY"), "no snappy-java samples"); - assertTrue(libraryCounters.containsKey("ZSTD"), "no zstd-jni samples"); - modeCounters.forEach((mode, count) -> System.err.println(mode + ": " + count.get())); - libraryCounters.forEach((lib, count) -> System.err.println(lib + ": " + count.get())); + assertTrue(isMusl || isSanitizer || counters.libraryCounters.containsKey("SNAPPY"), "no snappy-java samples"); + assertTrue(counters.libraryCounters.containsKey("ZSTD"), "no zstd-jni samples"); + counters.modeCounters.forEach((mode, count) -> System.err.println(mode + ": " + count.get())); + counters.libraryCounters.forEach((lib, count) -> System.err.println(lib + ": " + count.get())); } @@ -195,6 +193,11 @@ private int snappyJava() { return blackhole; } + private static class Counters { + final Map modeCounters = new HashMap<>(); + final Map libraryCounters = new HashMap<>(); + } + ByteBuffer fill(ByteBuffer buffer) { byte[] bytes = new byte[buffer.limit()]; ThreadLocalRandom.current().nextBytes(bytes); diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/nativemem/NativememProfilerTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativemem/NativememProfilerTest.java index 9f2092a5f0..ddfc5e8468 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/nativemem/NativememProfilerTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/nativemem/NativememProfilerTest.java @@ -12,17 +12,11 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.params.provider.ValueSource; -import org.openjdk.jmc.common.item.IAttribute; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.common.unit.IQuantity; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.openjdk.jmc.common.item.Attribute.attr; -import static org.openjdk.jmc.common.unit.UnitLookup.ADDRESS; /** * Smoke tests for native memory (malloc) profiling. @@ -36,7 +30,7 @@ */ public class NativememProfilerTest extends CStackAwareAbstractProfilerTest { - private static final IAttribute MALLOC_ADDRESS = attr("address", "address", "", ADDRESS); + private static final String MALLOC_ADDRESS = "address"; @BeforeAll static void preloadNativeLib() { @@ -73,32 +67,27 @@ public void shouldRecordMallocSamples() throws InterruptedException { stopProfiler(); - IItemCollection events = verifyEvents("datadog.NativeMemoryAllocation"); + JfrEvents events = verifyEvents("datadog.NativeMemoryAllocation"); boolean foundMinSize = false; - for (IItemIterable items : events) { - IMemberAccessor sizeAccessor = SIZE.getAccessor(items.getType()); - IMemberAccessor weightAccessor = WEIGHT.getAccessor(items.getType()); - IMemberAccessor addrAccessor = MALLOC_ADDRESS.getAccessor(items.getType()); - if (sizeAccessor == null) { + for (JfrEvent item : events) { + if (!item.has(SIZE)) { continue; } - assertNotNull(addrAccessor, "datadog.NativeMemoryAllocation events must carry an address field"); - assertNotNull(weightAccessor, "datadog.NativeMemoryAllocation events must carry a weight field"); - for (IItem item : items) { - IQuantity size = sizeAccessor.getMember(item); - assertNotNull(size, "datadog.NativeMemoryAllocation event must have a non-null size field"); - assertTrue(size.longValue() > 0, "allocation size must be positive"); - if (size.longValue() >= 1024) { - foundMinSize = true; - } - IQuantity addr = addrAccessor.getMember(item); - assertTrue(addr == null || addr.longValue() != 0, "malloc address must not be zero"); - // nativemem=0 samples every allocation; weight must be exactly 1.0. - IQuantity weight = weightAccessor.getMember(item); - assertNotNull(weight, "datadog.NativeMemoryAllocation event must have a non-null weight field"); - assertTrue(Math.abs(weight.doubleValue() - 1.0) < 1e-6, - "weight must be 1.0 for nativemem=0 (all allocations sampled), got " + weight.doubleValue()); + assertTrue(item.has(MALLOC_ADDRESS), "datadog.NativeMemoryAllocation events must carry an address field"); + assertTrue(item.has(WEIGHT), "datadog.NativeMemoryAllocation events must carry a weight field"); + Long size = item.getLong(SIZE); + assertNotNull(size, "datadog.NativeMemoryAllocation event must have a non-null size field"); + assertTrue(size > 0, "allocation size must be positive"); + if (size >= 1024) { + foundMinSize = true; } + Long addr = item.getLong(MALLOC_ADDRESS); + assertTrue(addr == null || addr != 0, "malloc address must not be zero"); + // nativemem=0 samples every allocation; weight must be exactly 1.0. + Double weight = item.getDouble(WEIGHT); + assertNotNull(weight, "datadog.NativeMemoryAllocation event must have a non-null weight field"); + assertTrue(Math.abs(weight - 1.0) < 1e-6, + "weight must be 1.0 for nativemem=0 (all allocations sampled), got " + weight); } assertTrue(foundMinSize, "expected at least one malloc event with size >= 1024 bytes"); diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/nativemem/NativememSampledProfilerTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativemem/NativememSampledProfilerTest.java index 03a86cc3c2..232564dc22 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/nativemem/NativememSampledProfilerTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/nativemem/NativememSampledProfilerTest.java @@ -12,11 +12,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.params.provider.ValueSource; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.common.unit.IQuantity; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -63,26 +58,22 @@ public void shouldEmitWeightedMallocSamples() throws InterruptedException { stopProfiler(); - IItemCollection events = verifyEvents("datadog.NativeMemoryAllocation"); - int sampleCount = 0; - for (IItemIterable items : events) { - IMemberAccessor sizeAccessor = SIZE.getAccessor(items.getType()); - IMemberAccessor weightAccessor = WEIGHT.getAccessor(items.getType()); - assertNotNull(sizeAccessor, "datadog.NativeMemoryAllocation events must carry a size field"); - assertNotNull(weightAccessor, "datadog.NativeMemoryAllocation events must carry a weight field"); - for (IItem item : items) { - IQuantity size = sizeAccessor.getMember(item); - IQuantity weight = weightAccessor.getMember(item); - assertNotNull(size, "datadog.NativeMemoryAllocation event must have a non-null size field"); - assertNotNull(weight, "datadog.NativeMemoryAllocation event must have a non-null weight field"); - // Weight is 1 / (1 - exp(-size/interval)); that function is strictly > 1 - // for all positive sizes, so any Poisson-sampled event must carry weight >= 1. - assertTrue(weight.doubleValue() >= 1.0, - "weight must be >= 1.0 on the sampled path, got " + weight.doubleValue() - + " (size=" + size.longValue() + ")"); - sampleCount++; - } - } + // Streamed rather than materialized: ~40k deep-native-stack events at this volume + // can exceed the test heap if collected into a list first, and every check here is + // per-event with no need to retain the collection afterward. + long sampleCount = streamEvents("datadog.NativeMemoryAllocation", item -> { + assertTrue(item.has(SIZE), "datadog.NativeMemoryAllocation events must carry a size field"); + assertTrue(item.has(WEIGHT), "datadog.NativeMemoryAllocation events must carry a weight field"); + Long size = item.getLong(SIZE); + Double weight = item.getDouble(WEIGHT); + assertNotNull(size, "datadog.NativeMemoryAllocation event must have a non-null size field"); + assertNotNull(weight, "datadog.NativeMemoryAllocation event must have a non-null weight field"); + // Weight is 1 / (1 - exp(-size/interval)); that function is strictly > 1 + // for all positive sizes, so any Poisson-sampled event must carry weight >= 1. + assertTrue(weight >= 1.0, + "weight must be >= 1.0 on the sampled path, got " + weight + + " (size=" + size + ")"); + }); // With ~20M bytes allocated and a 512-byte interval we expect plenty of samples. // The assertion is loose to tolerate CI variance but tight enough to catch diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketBytesAccuracyTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketBytesAccuracyTest.java index 92347ca032..3159718bc1 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketBytesAccuracyTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketBytesAccuracyTest.java @@ -1,17 +1,16 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; import com.datadoghq.profiler.AbstractProfilerTest; import com.datadoghq.profiler.Platform; import org.junit.jupiter.api.Assumptions; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.Attribute; -import org.openjdk.jmc.common.item.IAttribute; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.common.unit.IQuantity; -import org.openjdk.jmc.common.unit.UnitLookup; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import java.io.IOException; import java.io.InputStream; @@ -35,11 +34,6 @@ */ public class NativeSocketBytesAccuracyTest extends AbstractProfilerTest { - private static final IAttribute DURATION_ATTR = - Attribute.attr("duration", "duration", "Duration", UnitLookup.TIMESPAN); - private static final IAttribute WEIGHT_ATTR = - Attribute.attr("weight", "weight", "weight", UnitLookup.NUMBER); - @Override protected boolean isPlatformSupported() { return Platform.isLinux() && !Platform.isMusl(); @@ -65,27 +59,20 @@ public void timeWeightedEstimateIsWithinReasonableBounds() throws Exception { stopProfiler(); - IItemCollection events = verifyEvents("datadog.NativeSocketEvent"); + JfrEvents events = verifyEvents("datadog.NativeSocketEvent"); assertTrue(events.hasItems(), "No NativeSocketEvent events found"); double scaledDurationNs = 0.0; long sendEventCount = 0; - for (IItemIterable items : events) { - IMemberAccessor opAccessor = OPERATION.getAccessor(items.getType()); - IMemberAccessor durationAccessor = DURATION_ATTR.getAccessor(items.getType()); - IMemberAccessor weightAccessor = WEIGHT_ATTR.getAccessor(items.getType()); - if (opAccessor == null || durationAccessor == null || weightAccessor == null) continue; - for (IItem item : items) { - String op = opAccessor.getMember(item); - // Outbound direction: SEND (send syscall) or WRITE (write syscall on socket fd). - if ("SEND".equals(op) || "WRITE".equals(op)) { - IQuantity dur = durationAccessor.getMember(item); - IQuantity weight = weightAccessor.getMember(item); - if (dur != null && weight != null) { - double durationNs = dur.doubleValueIn(UnitLookup.NANOSECOND); - scaledDurationNs += durationNs * weight.doubleValue(); - sendEventCount++; - } + for (JfrEvent item : events) { + String op = item.getString(OPERATION); + // Outbound direction: SEND (send syscall) or WRITE (write syscall on socket fd). + if ("SEND".equals(op) || "WRITE".equals(op)) { + Long dur = item.getLong("duration"); + Double weight = item.getDouble(WEIGHT); + if (dur != null && weight != null) { + scaledDurationNs += dur * weight; + sendEventCount++; } } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketDisabledTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketDisabledTest.java index 0db60ec2a1..c26545de23 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketDisabledTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketDisabledTest.java @@ -1,9 +1,14 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; +import com.datadoghq.profiler.JfrEvents; import com.datadoghq.profiler.Platform; import org.junit.jupiter.api.Assumptions; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.IItemCollection; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -31,7 +36,7 @@ public void noSocketEventsWithoutFeatureEnabled() throws Exception { stopProfiler(); - IItemCollection events = verifyEvents("datadog.NativeSocketEvent", false); + JfrEvents events = verifyEvents("datadog.NativeSocketEvent", false); assertFalse(events.hasItems(), "NativeSocketEvent events must not appear when nativesocket argument is absent"); } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketEnabledTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketEnabledTest.java index 5f0f3e7f63..b5fd3c7bcd 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketEnabledTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketEnabledTest.java @@ -1,9 +1,14 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; import org.junit.jupiter.api.Assumptions; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.IItemCollection; +import com.datadoghq.profiler.JfrEvents; import com.datadoghq.profiler.Platform; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -22,7 +27,7 @@ public void socketEventsProducedWhenFeatureEnabled() throws Exception { stopProfiler(); - IItemCollection events = verifyEvents("datadog.NativeSocketEvent"); + JfrEvents events = verifyEvents("datadog.NativeSocketEvent"); assertTrue(events.hasItems(), "Expected NativeSocketEvent events to be present in JFR recording"); } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketEventFieldsTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketEventFieldsTest.java index ac5a0271b8..c243819870 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketEventFieldsTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketEventFieldsTest.java @@ -1,19 +1,16 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; import com.datadoghq.profiler.Platform; import org.junit.jupiter.api.Assumptions; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.IMCThread; -import org.openjdk.jmc.common.IMCStackTrace; -import org.openjdk.jmc.common.item.Attribute; -import org.openjdk.jmc.common.item.IAttribute; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.common.unit.IQuantity; -import org.openjdk.jmc.common.unit.UnitLookup; -import org.openjdk.jmc.flightrecorder.JfrAttributes; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; +import com.datadoghq.profiler.JfrStackTrace; import static org.junit.jupiter.api.Assertions.*; @@ -24,12 +21,9 @@ */ public class NativeSocketEventFieldsTest extends NativeSocketTestBase { - private static final IAttribute REMOTE_ADDRESS = - Attribute.attr("remoteAddress", "remoteAddress", "Remote address", UnitLookup.PLAIN_TEXT); - private static final IAttribute BYTES_TRANSFERRED = - Attribute.attr("bytesTransferred", "bytesTransferred", "Bytes transferred", UnitLookup.MEMORY); - private static final IAttribute DURATION = - Attribute.attr("duration", "duration", "Duration", UnitLookup.TIMESPAN); + private static final String REMOTE_ADDRESS = "remoteAddress"; + private static final String BYTES_TRANSFERRED = "bytesTransferred"; + private static final String DURATION = "duration"; @RetryingTest(3) public void allRequiredFieldsPresentAndValid() throws Exception { @@ -39,74 +33,57 @@ public void allRequiredFieldsPresentAndValid() throws Exception { stopProfiler(); - IItemCollection events = verifyEvents("datadog.NativeSocketEvent"); + JfrEvents events = verifyEvents("datadog.NativeSocketEvent"); assertTrue(events.hasItems(), "No NativeSocketEvent events found"); boolean foundSend = false; boolean foundRecv = false; - for (IItemIterable items : events) { - IMemberAccessor operationAccessor = - OPERATION.getAccessor(items.getType()); - IMemberAccessor remoteAddressAccessor = - REMOTE_ADDRESS.getAccessor(items.getType()); - IMemberAccessor bytesAccessor = - BYTES_TRANSFERRED.getAccessor(items.getType()); - IMemberAccessor weightAccessor = - WEIGHT.getAccessor(items.getType()); - IMemberAccessor durationAccessor = - DURATION.getAccessor(items.getType()); - IMemberAccessor threadAccessor = - JfrAttributes.EVENT_THREAD.getAccessor(items.getType()); - IMemberAccessor stackTraceAccessor = - STACK_TRACE.getAccessor(items.getType()); - - assertNotNull(operationAccessor, "operation field accessor must be present"); - assertNotNull(remoteAddressAccessor, "remoteAddress field accessor must be present"); - assertNotNull(bytesAccessor, "bytesTransferred field accessor must be present"); - assertNotNull(weightAccessor, "weight field accessor must be present"); - assertNotNull(durationAccessor, "duration field accessor must be present"); - assertNotNull(threadAccessor, "eventThread field accessor must be present"); - assertNotNull(stackTraceAccessor, "stackTrace field accessor must be present"); - - for (IItem item : items) { - String operation = operationAccessor.getMember(item); - assertNotNull(operation, "operation must not be null"); - // op encodes the underlying syscall: SEND/RECV are emitted by send_hook/recv_hook; - // WRITE/READ are emitted by write_hook/read_hook. Java sockets typically reach - // libc via write()/read(), so foundSend covers SEND and WRITE, foundRecv covers - // RECV and READ — both directions must be observed. - assertTrue(operation.equals("SEND") || operation.equals("RECV") - || operation.equals("WRITE") || operation.equals("READ"), - "operation must be one of SEND/RECV/WRITE/READ, got: " + operation); - if ("SEND".equals(operation) || "WRITE".equals(operation)) foundSend = true; - if ("RECV".equals(operation) || "READ".equals(operation)) foundRecv = true; - - String remoteAddress = remoteAddressAccessor.getMember(item); - assertNotNull(remoteAddress, "remoteAddress must not be null"); - // AF_UNIX SOCK_STREAM sockets produce an empty remoteAddress; skip - // the ip:port format check for those events. - if (!remoteAddress.isEmpty()) { - assertTrue(remoteAddress.contains(":"), - "remoteAddress must be in ip:port format, got: " + remoteAddress); - } - - IQuantity bytes = bytesAccessor.getMember(item); - assertNotNull(bytes, "bytesTransferred must not be null"); - assertTrue(bytes.longValue() > 0, - "bytesTransferred must be > 0, got: " + bytes); - - IQuantity weight = weightAccessor.getMember(item); - assertNotNull(weight, "weight must not be null"); - assertTrue(weight.doubleValue() > 0.0, - "weight must be > 0, got: " + weight); - - IQuantity duration = durationAccessor.getMember(item); - assertNotNull(duration, "duration must not be null"); - - IMCThread thread = threadAccessor.getMember(item); - assertNotNull(thread, "eventThread must not be null"); + for (JfrEvent item : events) { + assertTrue(item.has(OPERATION), "operation field must be present"); + assertTrue(item.has(REMOTE_ADDRESS), "remoteAddress field must be present"); + assertTrue(item.has(BYTES_TRANSFERRED), "bytesTransferred field must be present"); + assertTrue(item.has(WEIGHT), "weight field must be present"); + assertTrue(item.has(DURATION), "duration field must be present"); + assertTrue(item.has("eventThread"), "eventThread field must be present"); + assertTrue(item.has(STACK_TRACE), "stackTrace field must be present"); + + String operation = item.getString(OPERATION); + assertNotNull(operation, "operation must not be null"); + // op encodes the underlying syscall: SEND/RECV are emitted by send_hook/recv_hook; + // WRITE/READ are emitted by write_hook/read_hook. Java sockets typically reach + // libc via write()/read(), so foundSend covers SEND and WRITE, foundRecv covers + // RECV and READ — both directions must be observed. + assertTrue(operation.equals("SEND") || operation.equals("RECV") + || operation.equals("WRITE") || operation.equals("READ"), + "operation must be one of SEND/RECV/WRITE/READ, got: " + operation); + if ("SEND".equals(operation) || "WRITE".equals(operation)) foundSend = true; + if ("RECV".equals(operation) || "READ".equals(operation)) foundRecv = true; + + String remoteAddress = item.getString(REMOTE_ADDRESS); + assertNotNull(remoteAddress, "remoteAddress must not be null"); + // AF_UNIX SOCK_STREAM sockets produce an empty remoteAddress; skip + // the ip:port format check for those events. + if (!remoteAddress.isEmpty()) { + assertTrue(remoteAddress.contains(":"), + "remoteAddress must be in ip:port format, got: " + remoteAddress); } + + Long bytes = item.getLong(BYTES_TRANSFERRED); + assertNotNull(bytes, "bytesTransferred must not be null"); + assertTrue(bytes > 0, + "bytesTransferred must be > 0, got: " + bytes); + + Double weight = item.getDouble(WEIGHT); + assertNotNull(weight, "weight must not be null"); + assertTrue(weight > 0.0, + "weight must be > 0, got: " + weight); + + Long duration = item.getLong(DURATION); + assertNotNull(duration, "duration must not be null"); + + String threadName = item.getThreadName("eventThread"); + assertNotNull(threadName, "eventThread must not be null"); } assertTrue(foundSend, "Expected at least one SEND event"); diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketEventThreadTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketEventThreadTest.java index 0aa1d45c4c..97b0198ec5 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketEventThreadTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketEventThreadTest.java @@ -1,15 +1,16 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; import com.datadoghq.profiler.AbstractProfilerTest; import com.datadoghq.profiler.Platform; import org.junit.jupiter.api.Assumptions; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.IMCThread; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.flightrecorder.JfrAttributes; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import java.io.IOException; import java.io.InputStream; @@ -44,20 +45,14 @@ public void eventThreadIsPopulated() throws Exception { stopProfiler(); - IItemCollection events = verifyEvents("datadog.NativeSocketEvent"); + JfrEvents events = verifyEvents("datadog.NativeSocketEvent"); assertTrue(events.hasItems(), "No NativeSocketEvent events found"); - for (IItemIterable items : events) { - IMemberAccessor threadAccessor = - JfrAttributes.EVENT_THREAD.getAccessor(items.getType()); - assertNotNull(threadAccessor, "eventThread accessor must be present"); - for (IItem item : items) { - IMCThread thread = threadAccessor.getMember(item); - assertNotNull(thread, "eventThread must not be null"); - String name = thread.getThreadName(); - assertNotNull(name, "thread name must not be null"); - assertFalse(name.isEmpty(), "thread name must not be empty"); - } + for (JfrEvent item : events) { + assertTrue(item.has("eventThread"), "eventThread field must be present"); + String name = item.getThreadName("eventThread"); + assertNotNull(name, "thread name must not be null"); + assertFalse(name.isEmpty(), "thread name must not be empty"); } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketMacOsNoOpTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketMacOsNoOpTest.java index 9ad59ad872..63ebf50523 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketMacOsNoOpTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketMacOsNoOpTest.java @@ -1,9 +1,14 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; +import com.datadoghq.profiler.JfrEvents; import com.datadoghq.profiler.Platform; import org.junit.jupiter.api.Assumptions; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.IItemCollection; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -38,7 +43,7 @@ public void noEventsOnMacOS() throws Exception { stopProfiler(); - IItemCollection events = verifyEvents("datadog.NativeSocketEvent", false); + JfrEvents events = verifyEvents("datadog.NativeSocketEvent", false); assertNotNull(events); assertFalse(events.hasItems(), "NativeSocketEvent must not be emitted on macOS (no-op stub)"); diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketRateLimitTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketRateLimitTest.java index 1c3426a6ac..9620e35f60 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketRateLimitTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketRateLimitTest.java @@ -20,14 +20,8 @@ import com.datadoghq.profiler.Platform; import org.junit.jupiter.api.Assumptions; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.Attribute; -import org.openjdk.jmc.common.item.IAttribute; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.common.unit.IQuantity; -import org.openjdk.jmc.common.unit.UnitLookup; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import java.io.IOException; import java.io.InputStream; @@ -53,9 +47,6 @@ */ public class NativeSocketRateLimitTest extends AbstractProfilerTest { - private static final IAttribute WEIGHT_ATTR = - Attribute.attr("weight", "weight", "weight", UnitLookup.NUMBER); - @Override protected boolean isPlatformSupported() { return Platform.isLinux() && !Platform.isMusl(); @@ -77,23 +68,17 @@ public void eventCountIsSubstantiallyLessThanOperationCount() throws Exception { stopProfiler(); - IItemCollection events = verifyEvents("datadog.NativeSocketEvent"); + JfrEvents events = verifyEvents("datadog.NativeSocketEvent"); assertTrue(events.hasItems(), "No NativeSocketEvent events found"); long eventCount = 0; boolean foundWeightAboveOne = false; - for (IItemIterable items : events) { - IMemberAccessor weightAccessor = - WEIGHT_ATTR.getAccessor(items.getType()); - for (IItem item : items) { - eventCount++; - if (weightAccessor != null) { - IQuantity w = weightAccessor.getMember(item); - if (w != null && w.doubleValue() > 1.0) { - foundWeightAboveOne = true; - } - } + for (JfrEvent item : events) { + eventCount++; + Double w = item.getDouble(WEIGHT); + if (w != null && w > 1.0) { + foundWeightAboveOne = true; } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketRemoteAddressTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketRemoteAddressTest.java index 04b46e6e8d..b56080559f 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketRemoteAddressTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketRemoteAddressTest.java @@ -1,16 +1,16 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; import com.datadoghq.profiler.AbstractProfilerTest; import com.datadoghq.profiler.Platform; import org.junit.jupiter.api.Assumptions; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.Attribute; -import org.openjdk.jmc.common.item.IAttribute; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.common.unit.UnitLookup; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import java.io.IOException; import java.io.InputStream; @@ -25,8 +25,7 @@ */ public class NativeSocketRemoteAddressTest extends AbstractProfilerTest { - private static final IAttribute REMOTE_ADDRESS = - Attribute.attr("remoteAddress", "remoteAddress", "Remote address", UnitLookup.PLAIN_TEXT); + private static final String REMOTE_ADDRESS = "remoteAddress"; @Override protected boolean isPlatformSupported() { @@ -50,24 +49,20 @@ public void remoteAddressIsIpColonPort() throws Exception { stopProfiler(); - IItemCollection events = verifyEvents("datadog.NativeSocketEvent"); + JfrEvents events = verifyEvents("datadog.NativeSocketEvent"); assertTrue(events.hasItems(), "No NativeSocketEvent events found"); boolean foundMatchingAddress = false; - for (IItemIterable items : events) { - IMemberAccessor addrAccessor = - REMOTE_ADDRESS.getAccessor(items.getType()); - assertNotNull(addrAccessor, "remoteAddress accessor must exist"); - for (IItem item : items) { - String addr = addrAccessor.getMember(item); - assertNotNull(addr, "remoteAddress must not be null"); - assertFalse(addr.isEmpty(), "remoteAddress must not be empty"); - // Must match ip:port pattern - assertTrue(addr.matches("^[\\d.]+:\\d+$") || addr.matches("^\\[.*\\]:\\d+$"), - "remoteAddress '" + addr + "' does not match expected ip:port format"); - if (addr.endsWith(":" + serverPort)) { - foundMatchingAddress = true; - } + for (JfrEvent item : events) { + assertTrue(item.has(REMOTE_ADDRESS), "remoteAddress field must exist"); + String addr = item.getString(REMOTE_ADDRESS); + assertNotNull(addr, "remoteAddress must not be null"); + assertFalse(addr.isEmpty(), "remoteAddress must not be empty"); + // Must match ip:port pattern + assertTrue(addr.matches("^[\\d.]+:\\d+$") || addr.matches("^\\[.*\\]:\\d+$"), + "remoteAddress '" + addr + "' does not match expected ip:port format"); + if (addr.endsWith(":" + serverPort)) { + foundMatchingAddress = true; } } assertTrue(foundMatchingAddress, diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketRestartTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketRestartTest.java index 375bc01401..4d0999f036 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketRestartTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketRestartTest.java @@ -1,9 +1,14 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; +import com.datadoghq.profiler.JfrEvents; import com.datadoghq.profiler.Platform; import org.junit.jupiter.api.Assumptions; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.IItemCollection; import java.nio.file.Files; import java.nio.file.Path; @@ -36,7 +41,7 @@ public void testNativeSocketProfilerRestart() throws Exception { profiler.stop(); - IItemCollection events = verifyEvents(jfr2, "datadog.NativeSocketEvent", true); + JfrEvents events = verifyEvents(jfr2, "datadog.NativeSocketEvent", true); assertTrue(events.hasItems(), "NativeSocketEvent events must be recorded in the second profiling session"); } finally { diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketSendRecvSeparateTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketSendRecvSeparateTest.java index 7eb7d688e5..919a15917c 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketSendRecvSeparateTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketSendRecvSeparateTest.java @@ -1,13 +1,16 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; import com.datadoghq.profiler.AbstractProfilerTest; import com.datadoghq.profiler.Platform; import org.junit.jupiter.api.Assumptions; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import java.io.IOException; import java.io.InputStream; @@ -46,22 +49,18 @@ public void sendAndRecvTrackedWithSeparateCounts() throws Exception { stopProfiler(); - IItemCollection events = verifyEvents("datadog.NativeSocketEvent"); + JfrEvents events = verifyEvents("datadog.NativeSocketEvent"); assertTrue(events.hasItems(), "No NativeSocketEvent events found"); long sendCount = 0; long recvCount = 0; - for (IItemIterable items : events) { - IMemberAccessor opAccessor = OPERATION.getAccessor(items.getType()); - assertNotNull(opAccessor); - for (IItem item : items) { - String op = opAccessor.getMember(item); - // Java sockets reach libc via write()/read(); send()/recv() also possible. - // Group by direction: outbound (SEND, WRITE) vs inbound (RECV, READ). - if ("SEND".equals(op) || "WRITE".equals(op)) sendCount++; - else if ("RECV".equals(op) || "READ".equals(op)) recvCount++; - } + for (JfrEvent item : events) { + String op = item.getString(OPERATION); + // Java sockets reach libc via write()/read(); send()/recv() also possible. + // Group by direction: outbound (SEND, WRITE) vs inbound (RECV, READ). + if ("SEND".equals(op) || "WRITE".equals(op)) sendCount++; + else if ("RECV".equals(op) || "READ".equals(op)) recvCount++; } System.out.println("Outbound (SEND/WRITE) events: " + sendCount diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketStackTraceTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketStackTraceTest.java index fc6ba9344f..2f3becdd97 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketStackTraceTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketStackTraceTest.java @@ -1,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; import com.datadoghq.profiler.CStackAwareAbstractProfilerTest; @@ -7,11 +12,8 @@ import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.params.provider.ValueSource; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import java.io.IOException; import java.io.InputStream; @@ -62,18 +64,13 @@ public void stackTraceIsCapturedForSocketEvents() throws Exception { // whatever frames happened to be on the stack. verifyStackTraces("datadog.NativeSocketEvent", "doTcpTransfer"); - IItemCollection events = verifyEvents("datadog.NativeSocketEvent"); - for (IItemIterable items : events) { - IMemberAccessor stackTraceAccessor = - JdkAttributes.STACK_TRACE_STRING.getAccessor(items.getType()); - if (stackTraceAccessor == null) continue; - for (IItem item : items) { - String st = stackTraceAccessor.getMember(item); - if (st == null) continue; - for (String hookFrame : new String[] {"send_hook", "recv_hook", "write_hook", "read_hook"}) { - assertFalse(st.contains(hookFrame), - "profiler-internal hook frame " + hookFrame + " leaked into stack trace: " + st); - } + JfrEvents events = verifyEvents("datadog.NativeSocketEvent"); + for (JfrEvent item : events) { + String st = item.getStackTraceString(); + if (st == null) continue; + for (String hookFrame : new String[] {"send_hook", "recv_hook", "write_hook", "read_hook"}) { + assertFalse(st.contains(hookFrame), + "profiler-internal hook frame " + hookFrame + " leaked into stack trace: " + st); } } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketUdpExcludedTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketUdpExcludedTest.java index 3007d4f56d..59af649757 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketUdpExcludedTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/nativesocket/NativeSocketUdpExcludedTest.java @@ -1,10 +1,15 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; import com.datadoghq.profiler.AbstractProfilerTest; +import com.datadoghq.profiler.JfrEvents; import com.datadoghq.profiler.Platform; import org.junit.jupiter.api.Assumptions; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.IItemCollection; import java.net.DatagramPacket; import java.net.DatagramSocket; @@ -42,7 +47,7 @@ public void udpTransfersProduceNoSocketEvents() throws Exception { stopProfiler(); - IItemCollection events = verifyEvents("datadog.NativeSocketEvent", false); + JfrEvents events = verifyEvents("datadog.NativeSocketEvent", false); assertNotNull(events); assertFalse(events.hasItems(), "NativeSocketEvent must not be produced for UDP (sendto/recvfrom) transfers"); diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/nativethread/DynamicNativeThread.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativethread/DynamicNativeThread.java index ddea4f8c1f..232de7fa62 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/nativethread/DynamicNativeThread.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/nativethread/DynamicNativeThread.java @@ -20,10 +20,7 @@ import com.datadoghq.profiler.nativethread.NativeThreadCreator; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; +import com.datadoghq.profiler.JfrEvent; import java.util.HashMap; import java.util.Map; @@ -62,19 +59,15 @@ public void test() { stopProfiler(); int count = 0; boolean stacktrace_printed = false; - for (IItemIterable cpuSamples : verifyEvents("datadog.ExecutionSample")) { - IMemberAccessor stacktraceAccessor = JdkAttributes.STACK_TRACE_STRING.getAccessor(cpuSamples.getType()); - IMemberAccessor modeAccessor = THREAD_EXECUTION_MODE.getAccessor(cpuSamples.getType()); - for (IItem item : cpuSamples) { - String stacktrace = stacktraceAccessor.getMember(item); - if (stacktrace.indexOf("do_primes()") != -1) { - if (!stacktrace_printed) { - stacktrace_printed = true; - System.out.println("Native thread stack:"); - System.out.println(stacktrace); - } - count++; + for (JfrEvent item : verifyEvents("datadog.ExecutionSample")) { + String stacktrace = item.getStackTraceString(); + if (stacktrace != null && stacktrace.indexOf("do_primes()") != -1) { + if (!stacktrace_printed) { + stacktrace_printed = true; + System.out.println("Native thread stack:"); + System.out.println(stacktrace); } + count++; } } assertTrue(count > 0, "no native thread sample"); diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/nativethread/NativeThreadTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativethread/NativeThreadTest.java index 5547b84d76..d6609c41db 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/nativethread/NativeThreadTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/nativethread/NativeThreadTest.java @@ -20,10 +20,7 @@ import com.datadoghq.profiler.nativethread.NativeThreadCreator; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; +import com.datadoghq.profiler.JfrEvent; import java.util.HashMap; import java.util.Map; @@ -61,28 +58,23 @@ public void test() { int totalSamples = 0; boolean stacktrace_printed = false; - for (IItemIterable cpuSamples : verifyEvents("datadog.ExecutionSample")) { - IMemberAccessor stacktraceAccessor = JdkAttributes.STACK_TRACE_STRING.getAccessor(cpuSamples.getType()); - IMemberAccessor modeAccessor = THREAD_EXECUTION_MODE.getAccessor(cpuSamples.getType()); - - for (IItem item : cpuSamples) { - String stacktrace = stacktraceAccessor.getMember(item); - totalSamples++; - - if (stacktrace.indexOf("do_primes()") != -1) { - // Native thread sample: must not contain break_no_anchor - // (non-Java threads have no JavaFrameAnchor — that error is inapplicable) - // break_no_symbol is expected when DWARF CFI is incomplete - assertFalse(stacktrace.contains("break_no_anchor"), - "Found break_no_anchor in native thread sample: " + stacktrace); - - if (!stacktrace_printed) { - stacktrace_printed = true; - System.out.println("Native thread stack:"); - System.out.println(stacktrace); - } - count++; - } + for (JfrEvent item : verifyEvents("datadog.ExecutionSample")) { + String stacktrace = item.getStackTraceString(); + totalSamples++; + + if (stacktrace != null && stacktrace.indexOf("do_primes()") != -1) { + // Native thread sample: must not contain break_no_anchor + // (non-Java threads have no JavaFrameAnchor — that error is inapplicable) + // break_no_symbol is expected when DWARF CFI is incomplete + assertFalse(stacktrace.contains("break_no_anchor"), + "Found break_no_anchor in native thread sample: " + stacktrace); + + if (!stacktrace_printed) { + stacktrace_printed = true; + System.out.println("Native thread stack:"); + System.out.println(stacktrace); + } + count++; } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/nativethread/ThreadEntryDetectionTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativethread/ThreadEntryDetectionTest.java index 76e5e08ac2..afac047083 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/nativethread/ThreadEntryDetectionTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/nativethread/ThreadEntryDetectionTest.java @@ -18,11 +18,8 @@ import com.datadoghq.profiler.AbstractProfilerTest; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -65,7 +62,7 @@ public void testThreadEntryDetection() throws Exception { stopProfiler(); // Verify events - IItemCollection events = verifyEvents("datadog.ExecutionSample"); + JfrEvents events = verifyEvents("datadog.ExecutionSample"); assertNoErrorFrames(events); // Verify we captured some samples @@ -80,23 +77,18 @@ public void testThreadEntryDetection() throws Exception { * for pure pthreads. break_no_symbol is acceptable — it means the DWARF * unwind hit an unresolvable PC, expected when CFI is incomplete. */ - private void assertNoErrorFrames(IItemCollection events) { + private void assertNoErrorFrames(JfrEvents events) { int samplesChecked = 0; - for (IItemIterable samples : events) { - IMemberAccessor stackTraceAccessor = - JdkAttributes.STACK_TRACE_STRING.getAccessor(samples.getType()); + for (JfrEvent sample : events) { + String stackTrace = sample.getStackTraceString(); + samplesChecked++; - for (IItem sample : samples) { - String stackTrace = stackTraceAccessor.getMember(sample); - samplesChecked++; - - if (stackTrace.contains("do_primes()")) { - // Native thread sample: must not be misclassified as break_no_anchor - assertFalse(stackTrace.contains("break_no_anchor"), - String.format("Found break_no_anchor in native thread sample %d:\n%s", - samplesChecked, stackTrace)); - } + if (stackTrace != null && stackTrace.contains("do_primes()")) { + // Native thread sample: must not be misclassified as break_no_anchor + assertFalse(stackTrace.contains("break_no_anchor"), + String.format("Found break_no_anchor in native thread sample %d:\n%s", + samplesChecked, stackTrace)); } } @@ -106,13 +98,7 @@ private void assertNoErrorFrames(IItemCollection events) { /** * Counts total number of samples in the event collection. */ - private int countTotalSamples(IItemCollection events) { - int count = 0; - for (IItemIterable samples : events) { - for (IItem sample : samples) { - count++; - } - } - return count; + private int countTotalSamples(JfrEvents events) { + return (int) events.count(); } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/queue/QueueTimeTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/queue/QueueTimeTest.java index 03d4a6505f..c113dcffde 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/queue/QueueTimeTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/queue/QueueTimeTest.java @@ -1,26 +1,20 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.queue; import com.datadoghq.profiler.AbstractProfilerTest; import com.datadoghq.profiler.JavaProfiler; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import org.junit.jupiter.api.Test; -import org.openjdk.jmc.common.IMCThread; -import org.openjdk.jmc.common.IMCType; -import org.openjdk.jmc.common.item.IAttribute; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.common.unit.IQuantity; -import org.openjdk.jmc.common.unit.IRange; -import org.openjdk.jmc.flightrecorder.JfrAttributes; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; import java.util.concurrent.ArrayBlockingQueue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.openjdk.jmc.common.item.Attribute.attr; -import static org.openjdk.jmc.common.unit.UnitLookup.*; public class QueueTimeTest extends AbstractProfilerTest { @Override @@ -62,41 +56,29 @@ public void testRecordQueueTime() throws Exception { thread.join(); stopProfiler(); - IAttribute startTimeAttr = attr("startTime", "", "", TIMESTAMP); - IAttribute originAttr = attr("origin", "", "", THREAD); - IAttribute taskAttr = attr("task", "", "", CLASS); - IAttribute schedulerAttr = attr("scheduler", "", "", CLASS); - IAttribute queueTypeAttr = attr("queueType", "", "", CLASS); - IAttribute queueLengthAttr = attr("queueLength", "", "", NUMBER); - - IItemCollection activeSettings = verifyEvents("jdk.ActiveSetting"); - for (IItemIterable activeSetting : activeSettings) { - IMemberAccessor nameAccessor = JdkAttributes.REC_SETTING_NAME.getAccessor(activeSetting.getType()); - IMemberAccessor valueAccessor = JdkAttributes.REC_SETTING_VALUE.getAccessor(activeSetting.getType()); - for (IItem item : activeSetting) { - String name = nameAccessor.getMember(item); - if ("tscfrequency".equals(name)) { - String frequency = valueAccessor.getMember(item); - assertTrue(Long.valueOf(frequency) > 0, frequency); - } + JfrEvents activeSettings = verifyEvents("jdk.ActiveSetting"); + for (JfrEvent item : activeSettings) { + String name = item.getString("name"); + if ("tscfrequency".equals(name)) { + String frequency = item.getString("value"); + assertTrue(Long.valueOf(frequency) > 0, frequency); } } - IItemCollection events = verifyEvents("datadog.QueueTime"); - for (IItemIterable it : events) { - for (IItem item : it) { - assertTrue(startTimeAttr.getAccessor(it.getType()).getMember(item).longValueIn(EPOCH_NS) > 0); - IRange lifetime = JfrAttributes.LIFETIME.getAccessor(it.getType()).getMember(item); - long duration = lifetime.getEnd().longValueIn(EPOCH_MS) - lifetime.getStart().longValueIn(EPOCH_MS); - assertTrue(duration >= 9); - assertEquals(task.getClass().getName(), taskAttr.getAccessor(it.getType()).getMember(item).getTypeName()); - assertEquals(getClass().getName(), schedulerAttr.getAccessor(it.getType()).getMember(item).getTypeName()); - assertEquals(1, SPAN_ID.getAccessor(it.getType()).getMember(item).longValue()); - assertEquals(2, LOCAL_ROOT_SPAN_ID.getAccessor(it.getType()).getMember(item).longValue()); - assertEquals("origin", originAttr.getAccessor(it.getType()).getMember(item).getThreadName()); - assertEquals(ArrayBlockingQueue.class.getName(), queueTypeAttr.getAccessor(it.getType()).getMember(item).getTypeName()); - assertEquals(10, queueLengthAttr.getAccessor(it.getType()).getMember(item).longValue()); - } + JfrEvents events = verifyEvents("datadog.QueueTime"); + for (JfrEvent item : events) { + assertTrue(item.getLong("startTime") > 0); + // startTime/duration are TICKS-annotated so jafar auto-converts them to + // epoch nanos / duration nanos respectively. + long durationMillis = item.getLong("duration") / 1_000_000; + assertTrue(durationMillis >= 9); + assertEquals(task.getClass().getName(), item.getClassName("task")); + assertEquals(getClass().getName(), item.getClassName("scheduler")); + assertEquals(1, item.getLong(SPAN_ID)); + assertEquals(2, item.getLong(LOCAL_ROOT_SPAN_ID)); + assertEquals("origin", item.getThreadName("origin")); + assertEquals(ArrayBlockingQueue.class.getName(), item.getClassName("queueType")); + assertEquals(10, item.getLong("queueLength")); } } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/settings/DatadogSettingsTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/settings/DatadogSettingsTest.java index ac3064eff5..eaa3228428 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/settings/DatadogSettingsTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/settings/DatadogSettingsTest.java @@ -1,20 +1,20 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.settings; import com.datadoghq.profiler.AbstractProfilerTest; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.IAttribute; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; import java.util.Arrays; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.openjdk.jmc.common.item.Attribute.attr; -import static org.openjdk.jmc.common.unit.UnitLookup.PLAIN_TEXT; public class DatadogSettingsTest extends AbstractProfilerTest { @Override @@ -32,38 +32,27 @@ public void testRecordDatadogSetting() { profiler.recordSetting("long value " + i, new String(longValueBytes)); } stopProfiler(); - IItemCollection events = verifyEvents("datadog.ProfilerSetting"); - final IAttribute nameAttr = - attr("name", "", "", PLAIN_TEXT); - final IAttribute valueAttr = - attr("value", "", "", PLAIN_TEXT); - final IAttribute unitAttr = - attr("unit", "", "", PLAIN_TEXT); + JfrEvents events = verifyEvents("datadog.ProfilerSetting"); boolean dimensionlessChecked = false; boolean withUnitChecked = false; int longValuesChecked = 0; - for (IItemIterable settings : events) { - IMemberAccessor nameAccessor = nameAttr.getAccessor(settings.getType()); - IMemberAccessor valueAccessor = valueAttr.getAccessor(settings.getType()); - IMemberAccessor unitAccessor = unitAttr.getAccessor(settings.getType()); - for (IItem setting : settings) { - String name = nameAccessor.getMember(setting); - String value = valueAccessor.getMember(setting); - String unit = unitAccessor.getMember(setting); - if (!dimensionlessChecked && name.equals("dimensionless")) { - assertEquals("value", value); - assertEquals("", unit); - dimensionlessChecked = true; - } else if (!withUnitChecked && "withUnit".equals(name)) { - assertEquals("60", value); - assertEquals("seconds", unit); - withUnitChecked = true; - } else { - assertTrue(name.startsWith("long value")); - assertEquals(longValueBytes.length, value.length()); - assertEquals("", unit); - longValuesChecked++; - } + for (JfrEvent setting : events) { + String name = setting.getString("name"); + String value = setting.getString("value"); + String unit = setting.getString("unit"); + if (!dimensionlessChecked && name.equals("dimensionless")) { + assertEquals("value", value); + assertEquals("", unit); + dimensionlessChecked = true; + } else if (!withUnitChecked && "withUnit".equals(name)) { + assertEquals("60", value); + assertEquals("seconds", unit); + withUnitChecked = true; + } else { + assertTrue(name.startsWith("long value")); + assertEquals(longValueBytes.length, value.length()); + assertEquals("", unit); + longValuesChecked++; } } assertTrue(dimensionlessChecked); diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/BaseContextWallClockTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/BaseContextWallClockTest.java index 1ff6bbb7c0..10719177dc 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/BaseContextWallClockTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/BaseContextWallClockTest.java @@ -1,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.wallclock; import com.datadoghq.profiler.AbstractProfilerTest; @@ -6,12 +11,8 @@ import com.datadoghq.profiler.context.ContextExecutor; import com.datadoghq.profiler.context.Tracing; import org.junit.jupiter.api.Assumptions; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.common.unit.IQuantity; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import java.util.HashSet; import java.util.List; @@ -83,7 +84,7 @@ void test(AbstractProfilerTest test, boolean assertContext, String cstack) throw Set method1SpanIds = new HashSet<>(methodsToSpanIds.get("method1Impl")); Set method2SpanIds = new HashSet<>(methodsToSpanIds.get("method2Impl")); Set method3SpanIds = new HashSet<>(methodsToSpanIds.get("method3Impl")); - IItemCollection events = test.verifyEvents("datadog.MethodSample"); + JfrEvents events = test.verifyEvents("datadog.MethodSample"); Set states = new HashSet<>(); Set modes = new HashSet<>(); // we have 100 method1, method2, and method3 calls, but can't guarantee we sampled them all @@ -91,68 +92,60 @@ void test(AbstractProfilerTest test, boolean assertContext, String cstack) throw long method2Weight = 0; long method3Weight = 0; long unattributedWeight = 0; - for (IItemIterable wallclockSamples : events) { - IMemberAccessor frameAccessor = JdkAttributes.STACK_TRACE_STRING.getAccessor(wallclockSamples.getType()); - IMemberAccessor spanIdAccessor = SPAN_ID.getAccessor(wallclockSamples.getType()); - IMemberAccessor rootSpanIdAccessor = LOCAL_ROOT_SPAN_ID.getAccessor(wallclockSamples.getType()); - IMemberAccessor weightAccessor = WEIGHT.getAccessor(wallclockSamples.getType()); - IMemberAccessor stateAccessor = THREAD_STATE.getAccessor(wallclockSamples.getType()); - IMemberAccessor modeAccessor = THREAD_EXECUTION_MODE.getAccessor(wallclockSamples.getType()); - for (IItem sample : wallclockSamples) { - String stackTrace = frameAccessor.getMember(sample); - long spanId = spanIdAccessor.getMember(sample).longValue(); - long rootSpanId = rootSpanIdAccessor.getMember(sample).longValue(); - long weight = weightAccessor.getMember(sample).longValue(); - modes.add(modeAccessor.getMember(sample)); - String state = stateAccessor.getMember(sample); - assertNotNull(state); - states.add(state); - - // a lot fo care needs to be taken here with samples that fall between a context activation and - // a method call. E.g. not finding method2Impl in the stack trace doesn't mean the sample wasn't - // taken in the part of method2 between activation and invoking method2Impl, which complicates - // assertions when we only find method1Impl - boolean attributed = false; - if (stackTrace.contains("method3Impl")) { - if (assertContext) { - // method3 is scheduled after method2, and method1 blocks on it, so spanId == rootSpanId + 2 - assertEquals(rootSpanId + 2, spanId, stackTrace); - assertTrue(spanId == 0 || method3SpanIds.contains(spanId), stackTrace); - } - method3Weight += weight; - attributed = true; - } else if (stackTrace.contains("method2Impl")) { - if (assertContext) { - // method2 is called next, so spanId == rootSpanId + 1 - assertEquals(rootSpanId + 1, spanId, stackTrace); - assertTrue(spanId == 0 || method2SpanIds.contains(spanId), stackTrace); - } - method2Weight += weight; - attributed = true; - } else if (stackTrace.contains("method1Impl") - && !stackTrace.contains("method2") && !stackTrace.contains("method3") - && !stackTrace.contains("Object.wait")) { - // Exclude Object.wait frames: while method1Impl is blocked in monitor.wait(), - // method3 runs concurrently on the executor thread. The wall-clock profiler - // samples all threads, so that same window produces method3Weight samples on - // the executor AND method1Weight samples on the main thread. Counting the - // main-thread double-dip inflates method1's share to ~40-55% instead of ~33%. - if (assertContext) { - // need to check this after method2 because method1 calls method2 - // it's the root so spanId == rootSpanId - assertEquals(rootSpanId, spanId, stackTrace); - assertTrue(spanId == 0 || method1SpanIds.contains(spanId), stackTrace); - } - method1Weight += weight; - attributed = true; + for (JfrEvent sample : events) { + String stackTrace = sample.getStackTraceString(); + long spanId = sample.getLong(SPAN_ID, 0); + long rootSpanId = sample.getLong(LOCAL_ROOT_SPAN_ID, 0); + long weight = sample.getLong(WEIGHT, 0); + modes.add(sample.getEnumName(THREAD_EXECUTION_MODE)); + String state = sample.getEnumName(THREAD_STATE); + assertNotNull(state); + states.add(state); + + // a lot fo care needs to be taken here with samples that fall between a context activation and + // a method call. E.g. not finding method2Impl in the stack trace doesn't mean the sample wasn't + // taken in the part of method2 between activation and invoking method2Impl, which complicates + // assertions when we only find method1Impl + boolean attributed = false; + if (stackTrace.contains("method3Impl")) { + if (assertContext) { + // method3 is scheduled after method2, and method1 blocks on it, so spanId == rootSpanId + 2 + assertEquals(rootSpanId + 2, spanId, stackTrace); + assertTrue(spanId == 0 || method3SpanIds.contains(spanId), stackTrace); + } + method3Weight += weight; + attributed = true; + } else if (stackTrace.contains("method2Impl")) { + if (assertContext) { + // method2 is called next, so spanId == rootSpanId + 1 + assertEquals(rootSpanId + 1, spanId, stackTrace); + assertTrue(spanId == 0 || method2SpanIds.contains(spanId), stackTrace); } - assertTrue(weight <= 10 && weight > 0); - // Only count as unattributed if spanId is 0 AND we couldn't attribute by stack trace - // This prevents double-counting samples that have valid stack traces but no context - // (e.g., JVMTI samples when using TLS context which can't be read cross-thread) - if (spanId == 0 && !attributed) { - unattributedWeight += weight; + method2Weight += weight; + attributed = true; + } else if (stackTrace.contains("method1Impl") + && !stackTrace.contains("method2") && !stackTrace.contains("method3") + && !stackTrace.contains("Object.wait")) { + // Exclude Object.wait frames: while method1Impl is blocked in monitor.wait(), + // method3 runs concurrently on the executor thread. The wall-clock profiler + // samples all threads, so that same window produces method3Weight samples on + // the executor AND method1Weight samples on the main thread. Counting the + // main-thread double-dip inflates method1's share to ~40-55% instead of ~33%. + if (assertContext) { + // need to check this after method2 because method1 calls method2 + // it's the root so spanId == rootSpanId + assertEquals(rootSpanId, spanId, stackTrace); + assertTrue(spanId == 0 || method1SpanIds.contains(spanId), stackTrace); } + method1Weight += weight; + attributed = true; + } + assertTrue(weight <= 10 && weight > 0); + // Only count as unattributed if spanId is 0 AND we couldn't attribute by stack trace + // This prevents double-counting samples that have valid stack traces but no context + // (e.g., JVMTI samples when using TLS context which can't be read cross-thread) + if (spanId == 0 && !attributed) { + unattributedWeight += weight; } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/CollapsingSleepTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/CollapsingSleepTest.java index ff362085f2..d089c26757 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/CollapsingSleepTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/CollapsingSleepTest.java @@ -1,12 +1,17 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.wallclock; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; -import org.openjdk.jmc.common.item.Aggregators; -import org.openjdk.jmc.common.item.IItemCollection; import com.datadoghq.profiler.AbstractProfilerTest; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import com.datadoghq.profiler.Platform; import java.util.concurrent.locks.LockSupport; @@ -25,10 +30,14 @@ public void testSleep() { ts = System.nanoTime(); } while (waitTime > 1_000); stopProfiler(); - IItemCollection events = verifyEvents("datadog.MethodSample"); + JfrEvents events = verifyEvents("datadog.MethodSample"); assertTrue(events.hasItems()); - assertTrue(events.getAggregate(Aggregators.sum(WEIGHT)).longValue() > 700); - assertTrue(events.getAggregate(Aggregators.count()).longValue() > 9); + long totalWeight = 0; + for (JfrEvent item : events) { + totalWeight += item.getLong(WEIGHT, 0); + } + assertTrue(totalWeight > 700); + assertTrue(events.count() > 9); } @Override diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/ContendedWallclockSamplesTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/ContendedWallclockSamplesTest.java index bf8591ca6d..b92ab864f0 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/ContendedWallclockSamplesTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/ContendedWallclockSamplesTest.java @@ -1,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.wallclock; import com.datadoghq.profiler.CStackAwareAbstractProfilerTest; @@ -7,10 +12,7 @@ import com.datadoghq.profiler.junit.RetryTest; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.params.provider.ValueSource; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; +import com.datadoghq.profiler.JfrEvent; import java.time.Duration; import java.util.ArrayList; @@ -75,19 +77,15 @@ public void test(@CStack String cstack) { String lambdaName = getClass().getName() + LAMBDA_QUALIFIER; String lambdaStateName = getClass().getName() + ".lambda$pingPong$"; - for (IItemIterable wallclockSamples : verifyEvents("datadog.MethodSample")) { - IMemberAccessor frameAccessor = JdkAttributes.STACK_TRACE_STRING.getAccessor(wallclockSamples.getType()); - IMemberAccessor stateAccessor = THREAD_STATE.getAccessor(wallclockSamples.getType()); - for (IItem sample : wallclockSamples) { - String state = stateAccessor.getMember(sample); - if ("CONTENDED".equals(state)) { - String stackTrace = frameAccessor.getMember(sample); - if (!stackTrace.endsWith(".GC_active()")) { - // shortcut the assertions for sanitized runs - // the samples are not that good, but it still makes sense to run this load under sanitizers - assertTrue(isSanitizer || stackTrace.contains(lambdaStateName), () -> stackTrace + " missing " + lambdaStateName); - assertTrue(isSanitizer || stackTrace.contains(lambdaName), () -> stackTrace + " missing " + lambdaName); - } + for (JfrEvent sample : verifyEvents("datadog.MethodSample")) { + String state = sample.getEnumName(THREAD_STATE); + if ("CONTENDED".equals(state)) { + String stackTrace = sample.getStackTraceString(); + if (!stackTrace.endsWith(".GC_active()")) { + // shortcut the assertions for sanitized runs + // the samples are not that good, but it still makes sense to run this load under sanitizers + assertTrue(isSanitizer || stackTrace.contains(lambdaStateName), () -> stackTrace + " missing " + lambdaStateName); + assertTrue(isSanitizer || stackTrace.contains(lambdaName), () -> stackTrace + " missing " + lambdaName); } } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/MegamorphicCallTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/MegamorphicCallTest.java index 763994ebad..d55bd4f524 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/MegamorphicCallTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/MegamorphicCallTest.java @@ -1,20 +1,17 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.wallclock; import com.datadoghq.profiler.AbstractProfilerTest; import com.datadoghq.profiler.Platform; import org.junit.jupiter.api.Assumptions; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; - -import java.util.ArrayList; -import java.util.List; + import java.util.concurrent.ThreadLocalRandom; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class MegamorphicCallTest extends AbstractProfilerTest { @@ -86,25 +83,29 @@ public void testITableStubs() { int result = profiledWork(iterations, new Calculator1(), new Calculator2(), new Calculator3()); System.err.println(result); stopProfiler(); - IItemCollection events = verifyEvents("datadog.MethodSample"); - System.err.println(events.stream().count()); - List itableStubStacktraces = new ArrayList<>(); - for (IItemIterable cpuSamples : events) { - IMemberAccessor frameAccessor = JdkAttributes.STACK_TRACE_STRING.getAccessor(cpuSamples.getType()); - for (IItem sample : cpuSamples) { - String stackTrace = frameAccessor.getMember(sample); - if (stackTrace.contains(".itable stub()")) { - itableStubStacktraces.add(stackTrace); + + // Streamed rather than materialized: wall=100us over this workload produces up to + // hundreds of thousands of samples, and every check here is per-event (a running + // "did we see X" flag) with no need to retain the stack-trace strings afterward. + StubSearch found = reduceEvents("datadog.MethodSample", StubSearch::new, (acc, sample) -> { + acc.total++; + String stackTrace = sample.getStackTraceString(); + if (stackTrace.contains(".itable stub()")) { + acc.foundItableStub = true; + if (stackTrace.contains("MegamorphicCallTest.profiledWork")) { + acc.foundProfiledWork = true; } } - } - assertFalse(itableStubStacktraces.isEmpty()); - boolean foundProfiledWork = false; - for (String stacktrace : itableStubStacktraces) { - foundProfiledWork = stacktrace.contains("MegamorphicCallTest.profiledWork"); - if (foundProfiledWork) - break; - } - assertTrue(foundProfiledWork); + }); + System.err.println(found.total); + assertTrue(found.total > 0, "datadog.MethodSample was empty"); + assertTrue(found.foundItableStub); + assertTrue(found.foundProfiledWork); + } + + private static final class StubSearch { + long total; + boolean foundItableStub; + boolean foundProfiledWork; } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/PrecheckEfficiencyTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/PrecheckEfficiencyTest.java index 3582cf74d1..050d762c4f 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/PrecheckEfficiencyTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/PrecheckEfficiencyTest.java @@ -1,14 +1,16 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.wallclock; import com.datadoghq.profiler.AbstractProfilerTest; import com.datadoghq.profiler.Platform; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -99,67 +101,56 @@ public void compareSuppressionRates() throws Exception { stopProfiler(); - IItemCollection events = verifyEvents("datadog.MethodSample", false); + JfrEvents events = verifyEvents("datadog.MethodSample", false); long sleepSamples = 0, parkSamples = 0, objectWaitSamples = 0, runnableSamples = 0; - for (IItemIterable batch : events) { - IMemberAccessor stackAccessor = JdkAttributes.STACK_TRACE_STRING.getAccessor(batch.getType()); - IMemberAccessor stateAccessor = THREAD_STATE.getAccessor(batch.getType()); - IMemberAccessor threadNameAccessor = - JdkAttributes.EVENT_THREAD_NAME.getAccessor(batch.getType()); - if (stackAccessor == null && stateAccessor == null && threadNameAccessor == null) { + for (JfrEvent item : events) { + String threadName = item.getThreadName("eventThread"); + if (EFFICIENCY_SLEEPING.equals(threadName)) { + sleepSamples++; + continue; + } + if (EFFICIENCY_PARKED.equals(threadName)) { + parkSamples++; continue; } - for (IItem item : batch) { - if (threadNameAccessor != null) { - String threadName = threadNameAccessor.getMember(item); - if (EFFICIENCY_SLEEPING.equals(threadName)) { + if (EFFICIENCY_WAITING.equals(threadName)) { + objectWaitSamples++; + continue; + } + if (EFFICIENCY_WORKING.equals(threadName)) { + runnableSamples++; + continue; + } + String state = item.getEnumName(THREAD_STATE); + // CONDVAR_WAIT is written as "PARKED" in JFR metadata. + if (state != null && !state.isEmpty()) { + switch (state) { + case "SLEEPING": sleepSamples++; continue; - } - if (EFFICIENCY_PARKED.equals(threadName)) { + case "PARKED": parkSamples++; continue; - } - if (EFFICIENCY_WAITING.equals(threadName)) { + case "WAITING": objectWaitSamples++; continue; - } - if (EFFICIENCY_WORKING.equals(threadName)) { - runnableSamples++; - continue; - } - } - String state = stateAccessor != null ? stateAccessor.getMember(item) : null; - // CONDVAR_WAIT is written as "PARKED" in JFR metadata. - if (state != null && !state.isEmpty()) { - switch (state) { - case "SLEEPING": - sleepSamples++; - continue; - case "PARKED": - parkSamples++; - continue; - case "WAITING": - objectWaitSamples++; - continue; - default: - break; - } - } - String stack = stackAccessor != null ? stackAccessor.getMember(item) : null; - if (stack != null && (stack.contains("Thread.sleep") || stack.contains("sleep0"))) { - sleepSamples++; - } else if (stack != null && (stack.contains("LockSupport.park") || stack.contains("Unsafe.park") - || stack.contains("parkNanos"))) { - parkSamples++; - } else if (stack != null && (stack.contains("Object.wait") || stack.contains("wait0"))) { - objectWaitSamples++; - } else { - runnableSamples++; + default: + break; } } + String stack = item.getStackTraceString(); + if (stack != null && (stack.contains("Thread.sleep") || stack.contains("sleep0"))) { + sleepSamples++; + } else if (stack != null && (stack.contains("LockSupport.park") || stack.contains("Unsafe.park") + || stack.contains("parkNanos"))) { + parkSamples++; + } else if (stack != null && (stack.contains("Object.wait") || stack.contains("wait0"))) { + objectWaitSamples++; + } else { + runnableSamples++; + } } long total = sleepSamples + parkSamples + objectWaitSamples + runnableSamples; @@ -258,24 +249,20 @@ public void realisticServiceWorkload() throws Exception { stopProfiler(); - IItemCollection events = verifyEvents("datadog.MethodSample", false); + JfrEvents events = verifyEvents("datadog.MethodSample", false); long sleepSamples = 0, parkSamples = 0, otherSamples = 0; - for (IItemIterable batch : events) { - IMemberAccessor stackAccessor = JdkAttributes.STACK_TRACE_STRING.getAccessor(batch.getType()); - if (stackAccessor == null) continue; - for (IItem item : batch) { - String stack = stackAccessor.getMember(item); - if (stack == null) { - otherSamples++; - } else if (stack.contains("Thread.sleep") || stack.contains("sleep0")) { - sleepSamples++; - } else if (stack.contains("LockSupport.park") || stack.contains("Unsafe.park")) { - parkSamples++; - } else { - otherSamples++; - } + for (JfrEvent item : events) { + String stack = item.getStackTraceString(); + if (stack == null) { + otherSamples++; + } else if (stack.contains("Thread.sleep") || stack.contains("sleep0")) { + sleepSamples++; + } else if (stack.contains("LockSupport.park") || stack.contains("Unsafe.park")) { + parkSamples++; + } else { + otherSamples++; } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/PrecheckTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/PrecheckTest.java index e646762ecd..10b7da59df 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/PrecheckTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/PrecheckTest.java @@ -10,16 +10,8 @@ import com.datadoghq.profiler.ProfilerOwnedBlockHooks; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; -import org.openjdk.jmc.common.item.Attribute; -import org.openjdk.jmc.common.item.IAttribute; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.common.item.Aggregators; -import org.openjdk.jmc.common.unit.IQuantity; -import org.openjdk.jmc.common.unit.UnitLookup; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import java.nio.file.Files; import java.nio.file.Path; @@ -40,8 +32,6 @@ public class PrecheckTest extends AbstractProfilerTest { private static final int TAIL_WEIGHT_ITERATIONS = 50; private static final int TAIL_WEIGHT_SLEEP_MILLIS = 6; private static final long TAIL_WEIGHT_RUNNABLE_NANOS = 2_000_000L; - private static final IAttribute WEIGHT = - Attribute.attr("weight", "Sample weight", UnitLookup.NUMBER); private static volatile int tailWeightSpinSink; @Test @@ -62,7 +52,7 @@ public void testSleepingThreadIsNotSampled() throws InterruptedException { stopProfiler(); long sampleCount = verifyEvents("datadog.MethodSample", false) - .getAggregate(Aggregators.count()).longValue(); + .count(); // Explicitly owned once-per-run filter: entry signal emits, subsequent signals are // suppressed until blockExit clears the owned run. assertTrue(sampleCount < 10, @@ -87,7 +77,7 @@ public void unownedSleepingThreadIsNotExactOncePerRunSuppressed() throws Excepti stopProfiler(); long sampleCount = verifyEvents("datadog.MethodSample", false) - .getAggregate(Aggregators.count()).longValue(); + .count(); assertTrue(sampleCount >= 10, "Unowned Thread.sleep must not be exact once-per-run suppressed; got: " + sampleCount); } @@ -144,7 +134,7 @@ public void tracedSleepingThreadIsSampled() throws InterruptedException { stopProfiler(); long sampleCount = verifyEvents("datadog.MethodSample", false) - .getAggregate(Aggregators.count()).longValue(); + .count(); assertTrue(sampleCount >= 10, "Expected normal MethodSample volume for traced sleep, got: " + sampleCount); @@ -207,19 +197,11 @@ protected String getPrecheckDisabledProfilerCommand() { private WeightedSamples weightedSamplesForThread(String threadName) { long count = 0; long weight = 0; - IItemCollection events = verifyEvents("datadog.MethodSample", false); - for (IItemIterable batch : events) { - IMemberAccessor threadNameAccessor = - JdkAttributes.EVENT_THREAD_NAME.getAccessor(batch.getType()); - IMemberAccessor weightAccessor = WEIGHT.getAccessor(batch.getType()); - if (threadNameAccessor == null || weightAccessor == null) { - continue; - } - for (IItem item : batch) { - if (threadName.equals(threadNameAccessor.getMember(item))) { - count++; - weight += weightAccessor.getMember(item).longValue(); - } + JfrEvents events = verifyEvents("datadog.MethodSample", false); + for (JfrEvent item : events) { + if (threadName.equals(item.getThreadName("eventThread"))) { + count++; + weight += item.getLong(WEIGHT, 0); } } return new WeightedSamples(count, weight); diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/SleepTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/SleepTest.java index 5cc63689ec..badbaf9acb 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/SleepTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/SleepTest.java @@ -1,8 +1,12 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.wallclock; import com.datadoghq.profiler.AbstractProfilerTest; import org.junit.jupiter.api.Test; -import org.openjdk.jmc.common.item.Aggregators; import java.util.concurrent.locks.LockSupport; @@ -21,7 +25,7 @@ public void testSleep() { ts = System.nanoTime(); } while (waitTime > 1_000); stopProfiler(); - assertTrue(verifyEvents("datadog.MethodSample").getAggregate(Aggregators.count()).longValue() > 90); + assertTrue(verifyEvents("datadog.MethodSample").count() > 90); } @Override diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/SmokeWallTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/SmokeWallTest.java index cbcea4ea87..91a717efea 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/SmokeWallTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/SmokeWallTest.java @@ -1,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.wallclock; import com.datadoghq.profiler.CStackAwareAbstractProfilerTest; @@ -7,11 +12,8 @@ import com.datadoghq.profiler.junit.RetryTest; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.params.provider.ValueSource; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import java.util.concurrent.ExecutionException; @@ -41,14 +43,11 @@ public void test(@CStack String cstack) throws ExecutionException, InterruptedEx verifyCStackSettings(); - IItemCollection events = verifyEvents("datadog.MethodSample"); + JfrEvents events = verifyEvents("datadog.MethodSample"); - for (IItemIterable cpuSamples : events) { - IMemberAccessor frameAccessor = JdkAttributes.STACK_TRACE_STRING.getAccessor(cpuSamples.getType()); - for (IItem sample : cpuSamples) { - String stackTrace = frameAccessor.getMember(sample); - assertFalse(stackTrace.contains("jvmtiError")); - } + for (JfrEvent sample : events) { + String stackTrace = sample.getStackTraceString(); + assertFalse(stackTrace.contains("jvmtiError")); } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/VirtualThreadWallClockTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/VirtualThreadWallClockTest.java index e4a51ac3c3..f9c0f0e3b6 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/VirtualThreadWallClockTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/VirtualThreadWallClockTest.java @@ -1,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.wallclock; import com.datadoghq.profiler.CStackAwareAbstractProfilerTest; @@ -5,11 +10,8 @@ import com.datadoghq.profiler.junit.CStack; import com.datadoghq.profiler.junit.RetryTest; import org.junit.jupiter.api.TestTemplate; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import org.junit.jupiter.params.provider.ValueSource; @@ -76,56 +78,40 @@ private static Thread startVirtualThread(Runnable task) throws Exception { * Asserts that carrier frames (ForkJoinWorkerThread) are visible in the stack traces, * confirming that continuation unwinding is working correctly. */ - private void assertCarrierFramesVisible(IItemCollection events) { + private void assertCarrierFramesVisible(JfrEvents events) { boolean carrierVisible = false; - for (IItemIterable samples : events) { - IMemberAccessor frameAccessor = - JdkAttributes.STACK_TRACE_STRING.getAccessor(samples.getType()); - if (frameAccessor == null) continue; - for (IItem sample : samples) { - String stackTrace = frameAccessor.getMember(sample); - if (stackTrace == null || !stackTrace.contains("VirtualThreadWallClockTest")) continue; - // Standard JDK VTs run on ForkJoinWorkerThread carriers. - // If the JVM ever changes the default carrier pool this check must be updated. - if (stackTrace.contains("ForkJoinWorkerThread")) { - carrierVisible = true; - break; - } + for (JfrEvent sample : events) { + String stackTrace = sample.getStackTraceString(); + if (stackTrace == null || !stackTrace.contains("VirtualThreadWallClockTest")) continue; + // Standard JDK VTs run on ForkJoinWorkerThread carriers. + // If the JVM ever changes the default carrier pool this check must be updated. + if (stackTrace.contains("ForkJoinWorkerThread")) { + carrierVisible = true; + break; } - if (carrierVisible) break; } if (!carrierVisible) { System.out.println("=== MISSING CARRIER — sample stack traces ==="); int printed = 0; outer: - for (IItemIterable dump : events) { - IMemberAccessor fa = - JdkAttributes.STACK_TRACE_STRING.getAccessor(dump.getType()); - if (fa == null) continue; - for (IItem sample : dump) { - String st = fa.getMember(sample); - if (st != null && st.contains("VirtualThreadWallClockTest")) { - System.out.println("--- vt sample " + (++printed) + " ---"); - System.out.println(st); - if (printed >= 5) break outer; - } + for (JfrEvent sample : events) { + String st = sample.getStackTraceString(); + if (st != null && st.contains("VirtualThreadWallClockTest")) { + System.out.println("--- vt sample " + (++printed) + " ---"); + System.out.println(st); + if (printed >= 5) break outer; } } // Carrier-only samples: ForkJoinWorkerThread without VT frames int carrierPrinted = 0; System.out.println("=== CARRIER-ONLY samples (no VT frames) ==="); outer2: - for (IItemIterable dump : events) { - IMemberAccessor fa = - JdkAttributes.STACK_TRACE_STRING.getAccessor(dump.getType()); - if (fa == null) continue; - for (IItem sample : dump) { - String st = fa.getMember(sample); - if (st != null && st.contains("ForkJoinWorkerThread") && !st.contains("VirtualThreadWallClockTest")) { - System.out.println("--- carrier " + (++carrierPrinted) + " ---"); - System.out.println(st); - if (carrierPrinted >= 3) break outer2; - } + for (JfrEvent sample : events) { + String st = sample.getStackTraceString(); + if (st != null && st.contains("ForkJoinWorkerThread") && !st.contains("VirtualThreadWallClockTest")) { + System.out.println("--- carrier " + (++carrierPrinted) + " ---"); + System.out.println(st); + if (carrierPrinted >= 3) break outer2; } } if (carrierPrinted == 0) { @@ -133,17 +119,12 @@ private void assertCarrierFramesVisible(IItemCollection events) { System.out.println("=== No carrier samples — first 3 arbitrary samples ==="); int anyPrinted = 0; outer3: - for (IItemIterable dump : events) { - IMemberAccessor fa = - JdkAttributes.STACK_TRACE_STRING.getAccessor(dump.getType()); - if (fa == null) continue; - for (IItem sample : dump) { - String st = fa.getMember(sample); - if (st != null && !st.isEmpty()) { - System.out.println("--- any " + (++anyPrinted) + " ---"); - System.out.println(st); - if (anyPrinted >= 3) break outer3; - } + for (JfrEvent sample : events) { + String st = sample.getStackTraceString(); + if (st != null && !st.isEmpty()) { + System.out.println("--- any " + (++anyPrinted) + " ---"); + System.out.println(st); + if (anyPrinted >= 3) break outer3; } } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/WallClockThreadFilterTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/WallClockThreadFilterTest.java index 3a7ad72088..d136dae320 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/WallClockThreadFilterTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/WallClockThreadFilterTest.java @@ -1,16 +1,17 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.wallclock; import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Assumptions; import org.junitpioneer.jupiter.RetryingTest; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.common.unit.IQuantity; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; import com.datadoghq.profiler.AbstractProfilerTest; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import com.datadoghq.profiler.Platform; public class WallClockThreadFilterTest extends AbstractProfilerTest { @@ -21,18 +22,12 @@ public void test() throws InterruptedException { registerCurrentThreadForWallClockProfiling(); Thread.sleep(100); stopProfiler(); - IItemCollection events = verifyEvents("datadog.MethodSample"); - for (IItemIterable wallclockSamples : events) { - IMemberAccessor javaThreadNameAccessor = JdkAttributes.EVENT_THREAD_NAME - .getAccessor(wallclockSamples.getType()); - IMemberAccessor javaThreadIdAccessor = JdkAttributes.EVENT_THREAD_ID - .getAccessor(wallclockSamples.getType()); - for (IItem sample : wallclockSamples) { - String javaThreadName = javaThreadNameAccessor.getMember(sample); - assertEquals(Thread.currentThread().getName(), javaThreadName); - long javaThreadId = javaThreadIdAccessor.getMember(sample).longValue(); - assertEquals(Thread.currentThread().getId(), javaThreadId); - } + JfrEvents events = verifyEvents("datadog.MethodSample"); + for (JfrEvent sample : events) { + String javaThreadName = sample.getThreadName("eventThread"); + assertEquals(Thread.currentThread().getName(), javaThreadName); + long javaThreadId = sample.getThreadJavaId("eventThread"); + assertEquals(Thread.currentThread().getId(), javaThreadId); } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/WallclockMitigationsCombinedTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/WallclockMitigationsCombinedTest.java index 7ae94290db..00b51d9ba2 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/WallclockMitigationsCombinedTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/WallclockMitigationsCombinedTest.java @@ -12,11 +12,8 @@ import com.datadoghq.profiler.ProfilerOwnedBlockHooks; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; -import org.openjdk.jmc.common.item.IItem; -import org.openjdk.jmc.common.item.IItemCollection; -import org.openjdk.jmc.common.item.IItemIterable; -import org.openjdk.jmc.common.item.IMemberAccessor; -import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import java.util.HashMap; import java.util.Map; @@ -132,18 +129,11 @@ protected String getProfilerCommand() { private Map samplesByThreadName() { Map samplesByThread = new HashMap<>(); - IItemCollection events = verifyEvents("datadog.MethodSample", false); - for (IItemIterable batch : events) { - IMemberAccessor threadNameAccessor = - JdkAttributes.EVENT_THREAD_NAME.getAccessor(batch.getType()); - if (threadNameAccessor == null) { - continue; - } - for (IItem item : batch) { - String threadName = threadNameAccessor.getMember(item); - if (threadName != null) { - samplesByThread.merge(threadName, 1L, Long::sum); - } + JfrEvents events = verifyEvents("datadog.MethodSample", false); + for (JfrEvent item : events) { + String threadName = item.getThreadName("eventThread"); + if (threadName != null) { + samplesByThread.merge(threadName, 1L, Long::sum); } } return samplesByThread; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9fa214489c..94ce4e6a8f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,10 @@ lz4 = "1.11.1" snappy = "1.1.10.8" zstd = "1.5.7-12" +# JFR parsing for test verification (replaces JMC's FlightRecordingLoader, which can hang +# forever if one of its internal parallel chunk-parsing workers dies from OOM) +jafar = "0.26.2" + # Code analysis asm = "9.10.1" @@ -43,6 +47,7 @@ slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" } # JFR and memory analysis jmc-flightrecorder = { module = "org.openjdk.jmc:flightrecorder", version.ref = "jmc" } jol-core = { module = "org.openjdk.jol:jol-core", version.ref = "jol" } +jafar-parser = { module = "io.btrace:jafar-parser", version.ref = "jafar" } # Compression libraries lz4 = { module = "at.yawk.lz4:lz4-java", version.ref = "lz4" }