From c6ba0137fb8100f0f24ca096be1c33f0fee11cd9 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Wed, 5 Aug 2026 15:49:00 +0200 Subject: [PATCH 1/7] Replace JMC FlightRecordingLoader with lightweight jafar-backed JfrEvents API JMC's loader can hang forever if an internal parallel chunk-parsing worker dies from OOM (root cause of BoundMethodHandleProfilerTest CI hangs); jafar's synchronous UntypedJafarParser surfaces failures instead. JfrEvents.load() collapses jafar's resolved values in place and supports an early-exit predicate so verifyStackTraces() stops parsing once every pattern is matched, instead of materializing the whole event set. Co-Authored-By: Claude Sonnet 5 --- ddprof-test/build.gradle.kts | 1 + .../profiler/AbstractProfilerTest.java | 222 +++++++----------- .../ContendedCallTraceStorageTest.java | 62 +++-- .../java/com/datadoghq/profiler/JfrEvent.java | 219 +++++++++++++++++ .../com/datadoghq/profiler/JfrEvents.java | 196 ++++++++++++++++ .../java/com/datadoghq/profiler/JfrFrame.java | 61 +++++ .../com/datadoghq/profiler/JfrStackTrace.java | 62 +++++ .../alloc/AllocationProfilerTest.java | 15 +- .../context/AllNativeContextSamplingTest.java | 27 +-- .../CustomContextAttributeSamplingTest.java | 62 ++--- .../profiler/cpu/CTimerSamplerTest.java | 19 +- .../profiler/cpu/ClinitResolutionTest.java | 33 +-- .../profiler/cpu/ContextCpuTest.java | 72 +++--- .../cpu/LightweightContextCpuTest.java | 38 ++- .../profiler/cpu/RemoteSymbolicationTest.java | 149 +++++------- .../datadoghq/profiler/cpu/SmokeCpuTest.java | 53 ++--- .../profiler/cpu/VtableReceiverFrameTest.java | 47 ++-- .../profiler/endpoints/EndpointTest.java | 39 +-- .../profiler/memleak/GCGenerationsTest.java | 3 - .../memleak/LivenessTrackingTest.java | 40 ++-- .../profiler/memleak/MemleakProfilerTest.java | 18 +- .../metadata/DictionaryRotationTest.java | 24 +- .../metadata/MetadataNormalisationTest.java | 22 +- .../nativelibs/NativeLibrariesTest.java | 39 ++- .../nativemem/NativememProfilerTest.java | 51 ++-- .../NativememSampledProfilerTest.java | 39 ++- .../NativeSocketBytesAccuracyTest.java | 42 +--- .../NativeSocketDisabledTest.java | 4 +- .../nativesocket/NativeSocketEnabledTest.java | 4 +- .../NativeSocketEventFieldsTest.java | 130 ++++------ .../NativeSocketEventThreadTest.java | 26 +- .../NativeSocketMacOsNoOpTest.java | 4 +- .../NativeSocketRateLimitTest.java | 31 +-- .../NativeSocketRemoteAddressTest.java | 38 ++- .../nativesocket/NativeSocketRestartTest.java | 4 +- .../NativeSocketSendRecvSeparateTest.java | 24 +- .../NativeSocketStackTraceTest.java | 26 +- .../NativeSocketUdpExcludedTest.java | 4 +- .../nativethread/DynamicNativeThread.java | 25 +- .../nativethread/NativeThreadTest.java | 44 ++-- .../ThreadEntryDetectionTest.java | 42 ++-- .../profiler/queue/QueueTimeTest.java | 67 ++---- .../settings/DatadogSettingsTest.java | 56 ++--- .../wallclock/BaseContextWallClockTest.java | 122 +++++----- .../wallclock/CollapsingSleepTest.java | 14 +- .../ContendedWallclockSamplesTest.java | 27 +-- .../wallclock/MegamorphicCallTest.java | 22 +- .../wallclock/PrecheckEfficiencyTest.java | 118 ++++------ .../profiler/wallclock/PrecheckTest.java | 38 +-- .../profiler/wallclock/SleepTest.java | 3 +- .../profiler/wallclock/SmokeWallTest.java | 18 +- .../wallclock/VirtualThreadWallClockTest.java | 82 +++---- .../wallclock/WallClockThreadFilterTest.java | 26 +- .../WallclockMitigationsCombinedTest.java | 24 +- gradle/libs.versions.toml | 5 + 55 files changed, 1350 insertions(+), 1333 deletions(-) create mode 100644 ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvent.java create mode 100644 ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvents.java create mode 100644 ddprof-test/src/test/java/com/datadoghq/profiler/JfrFrame.java create mode 100644 ddprof-test/src/test/java/com/datadoghq/profiler/JfrStackTrace.java 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..87ea17a660 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/AbstractProfilerTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/AbstractProfilerTest.java @@ -12,33 +12,15 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; 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 +29,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 +244,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 +350,44 @@ 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); } } - public final IItemCollection verifyEvents(String eventType) { + 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; } } @@ -445,15 +398,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 +413,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 +438,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..19a20e8af8 --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvent.java @@ -0,0 +1,219 @@ +/* + * 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; + } + + 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); + } + + public boolean has(String field) { + return value.containsKey(field); + } + + 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; + } + + public long getLong(String field, long defaultValue) { + Long v = getLong(field); + return v != null ? v : defaultValue; + } + + public Double getDouble(String field) { + Object v = value.get(field); + return v instanceof Number ? ((Number) v).doubleValue() : null; + } + + public double getDouble(String field, double defaultValue) { + Double v = getDouble(field); + return v != null ? v : defaultValue; + } + + 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"); + } + + 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"); + } + + 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(); + for (JfrFrame frame : getStackTrace(field).frames()) { + String className = frame.className(); + String methodName = frame.methodName(); + if (className != null) { + sb.append(className).append('.'); + } + sb.append(methodName != null ? methodName : "").append("()\n"); + } + 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"); + if (name instanceof Map) { + Object s = ((Map) name).get("string"); + return s != null ? s.toString() : null; + } + return name != null ? name.toString() : 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..d9ff79bf0d --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvents.java @@ -0,0 +1,196 @@ +/* + * 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.Predicate; +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. + */ + 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<>(); + 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))); + 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; + } + + /** + * 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; + } + + public boolean hasItems() { + return !events.isEmpty(); + } + + 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)); + } + + public JfrEvents filter(Predicate predicate) { + List filtered = new ArrayList<>(); + for (JfrEvent e : events) { + if (predicate.test(e)) { + filtered.add(e); + } + } + return new JfrEvents(filtered); + } + + 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..d5a01256fd --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrFrame.java @@ -0,0 +1,61 @@ +/* + * 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; + } + return symbolString(((Map) type).get("name")); + } +} 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..03284f20d5 --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrStackTrace.java @@ -0,0 +1,62 @@ +/* + * 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; + } + + @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); + } + + public List frames() { + return frames; + } + + public boolean isEmpty() { + return frames.isEmpty(); + } + + 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..fa9faa6217 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,11 @@ package com.datadoghq.profiler.alloc; 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.Aggregators; -import org.openjdk.jmc.common.item.IItemCollection; import org.openjdk.jol.info.GraphLayout; import java.util.Random; @@ -35,7 +35,7 @@ public void shouldGetObjectAllocationSamples() throws InterruptedException { AllocatingTarget target1 = new AllocatingTarget(); AllocatingTarget target2 = new AllocatingTarget(); runTests(target1, target2); - IItemCollection allocations = verifyEvents("datadog.ObjectSample"); + JfrEvents allocations = verifyEvents("datadog.ObjectSample"); // FIXME when more tests are ported to this structure if (!Platform.isMusl()) { // JOL on musl seems to be locking up randomly @@ -44,14 +44,17 @@ public void shouldGetObjectAllocationSamples() throws InterruptedException { } } - private static void assertAllocations(IItemCollection allocations, Class clazz, AllocatingTarget... targets) { + private static void assertAllocations(JfrEvents allocations, Class clazz, AllocatingTarget... targets) { long allocated = 0; for (AllocatingTarget target : targets) { allocated += target.getAllocated(clazz); } - IItemCollection allocationsByType = allocations.apply(allocatedTypeFilter(clazz.getCanonicalName())); + JfrEvents allocationsByType = allocations.filter(allocatedTypeFilter(clazz.getCanonicalName())); assertTrue(allocationsByType.hasItems()); - long recorded = allocationsByType.getAggregate(Aggregators.sum(SCALED_SIZE)).longValue(); + long recorded = 0; + for (JfrEvent item : allocationsByType) { + recorded += (long) scaledSize(item); + } 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..f1954a0cc0 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 @@ -13,12 +13,8 @@ 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 com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import java.util.Map; import java.util.Set; @@ -54,14 +50,11 @@ public void test(@CStack String cstack) throws ExecutionException, InterruptedEx verifyCStackSettings(); - IItemCollection events = verifyEvents("datadog.ExecutionSample"); + JfrEvents 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")); - } + for (JfrEvent sample : events) { + String stackTrace = sample.getStackTraceString(); + assertFalse(stackTrace.contains("jvmtiError")); } } 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..069a111ca9 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 @@ -16,12 +16,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 +51,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 +61,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; } - totalWeight++; + } 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; } + 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..5787e0185a 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 @@ -5,12 +5,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 +36,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/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..84d0c26703 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 @@ -7,11 +7,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 +34,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 +61,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..c41d0eafa7 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 @@ -4,11 +4,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 java.util.concurrent.ThreadLocalRandom; @@ -62,32 +59,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..21e12f7722 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,9 @@ 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 +14,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 +37,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..4bb22ae6ca 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 @@ -5,9 +5,6 @@ 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; 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..0c41e4dff4 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 @@ -2,12 +2,11 @@ import com.datadoghq.profiler.Platform; import com.datadoghq.profiler.AbstractProfilerTest; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; 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; @@ -33,20 +32,23 @@ 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"); + JfrEvents allocations = verifyEvents("datadog.HeapLiveObject"); + JfrEvents 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) { + private static void assertAllocations(JfrEvents allocations, Class clazz, MemLeakTarget... targets) { long allocated = 0; for (MemLeakTarget target : targets) { allocated += target.getAllocated(clazz); } - IItemCollection allocationsByType = allocations.apply(allocatedTypeFilter(clazz.getCanonicalName())); + JfrEvents allocationsByType = allocations.filter(allocatedTypeFilter(clazz.getCanonicalName())); assertTrue(allocationsByType.hasItems()); - long recorded = allocationsByType.getAggregate(Aggregators.sum(SCALED_SIZE)).longValue(); + long recorded = 0; + for (JfrEvent item : allocationsByType) { + recorded += (long) scaledSize(item); + } 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)", 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..f6b82b4198 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,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.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 +42,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 +50,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..4ca70b6e05 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 @@ -10,10 +10,7 @@ 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 com.datadoghq.profiler.JfrEvent; import org.xerial.snappy.Snappy; import java.io.IOException; @@ -68,26 +65,22 @@ public void test() { 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(); + for (JfrEvent item : verifyEvents("datadog.ExecutionSample")) { + String stacktrace = item.getStackTraceString(); + String mode = item.getEnumName(THREAD_EXECUTION_MODE); + 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(); } } assertTrue(modeCounters.containsKey("JVM"), "no JVM samples"); 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..c4fa5d6296 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,8 @@ 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 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; @@ -63,25 +60,21 @@ public void shouldEmitWeightedMallocSamples() throws InterruptedException { stopProfiler(); - IItemCollection events = verifyEvents("datadog.NativeMemoryAllocation"); + JfrEvents 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++; - } + for (JfrEvent item : events) { + 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 + ")"); + sampleCount++; } // With ~20M bytes allocated and a 512-byte interval we expect plenty of samples. 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..baeb6ad1e5 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 @@ -4,14 +4,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; @@ -35,11 +29,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 +54,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..727d32db32 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,9 @@ 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 +31,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..d7e66752d8 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 @@ -2,8 +2,8 @@ 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 +22,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..4cd1870e14 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 @@ -3,17 +3,9 @@ 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 +16,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 +28,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..7c1ffbffc3 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 @@ -4,12 +4,8 @@ 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 +40,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..1f66d5ab18 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,9 @@ 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 +38,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..6449578b85 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 @@ -4,13 +4,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.UnitLookup; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import java.io.IOException; import java.io.InputStream; @@ -25,8 +20,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 +44,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..8d3a375331 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,9 @@ 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 +36,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..5b000450a5 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 @@ -4,10 +4,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 com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; import java.io.IOException; import java.io.InputStream; @@ -46,22 +44,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..82245b1461 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 @@ -7,11 +7,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 +59,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..41b6ab5a1f 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,10 @@ 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 +42,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..a4a2b2464e 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 @@ -2,25 +2,14 @@ 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 +51,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..a6cc4597c8 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,15 @@ 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 +27,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..a96488adad 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 @@ -6,12 +6,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 +79,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 +87,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); } - 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; + 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; + } + 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..99d4e4253a 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 @@ -3,10 +3,10 @@ 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 +25,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..a7f957bba1 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 @@ -7,10 +7,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 +72,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..9b3f887efe 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 @@ -4,11 +4,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 java.util.ArrayList; import java.util.List; @@ -86,16 +83,13 @@ 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()); + JfrEvents events = verifyEvents("datadog.MethodSample"); + System.err.println(events.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); - } + for (JfrEvent sample : events) { + String stackTrace = sample.getStackTraceString(); + if (stackTrace.contains(".itable stub()")) { + itableStubStacktraces.add(stackTrace); } } assertFalse(itableStubStacktraces.isEmpty()); 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..6053d6b55e 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 @@ -4,11 +4,8 @@ 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 +96,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; } - for (IItem item : batch) { - if (threadNameAccessor != null) { - String threadName = threadNameAccessor.getMember(item); - if (EFFICIENCY_SLEEPING.equals(threadName)) { + if (EFFICIENCY_PARKED.equals(threadName)) { + parkSamples++; + continue; + } + 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 +244,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..1ac87a5141 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 @@ -2,7 +2,6 @@ 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 +20,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..8e4e784bd9 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 @@ -7,11 +7,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 +38,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..b7c5b0b703 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 @@ -5,11 +5,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 +73,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 +114,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..8f75597290 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 @@ -3,14 +3,10 @@ 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 +17,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" } From a6a187803f8fe026f29794a148512fdff4f8a931 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Wed, 5 Aug 2026 16:42:35 +0200 Subject: [PATCH 2/7] Stream high-volume JFR event checks instead of materializing them BoundMethodHandleProfilerTest, NativeLibrariesTest and NativememSampledProfilerTest OOM'd the 512MB test heap because they collected every matching event into a list just to run per-event checks/counts or a presence check. Adds JfrEvents.forEach/reduce and a stopWhen-aborting presence check, and switches these three tests to them, so at most one event is resolved in memory at a time. Co-Authored-By: Claude Sonnet 5 --- .../profiler/AbstractProfilerTest.java | 59 +++++++++++++++ .../com/datadoghq/profiler/JfrEvents.java | 73 +++++++++++++++++++ .../BoundMethodHandleProfilerTest.java | 2 +- .../nativelibs/NativeLibrariesTest.java | 33 +++++---- .../NativememSampledProfilerTest.java | 12 ++- 5 files changed, 157 insertions(+), 22 deletions(-) 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 87ea17a660..2b069e2755 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/AbstractProfilerTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/AbstractProfilerTest.java @@ -12,7 +12,10 @@ 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; @@ -369,6 +372,25 @@ protected void verifyEventsPresent(Path recording, String... expectedEventTypes) } } + /** + * 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 JfrEvents verifyEvents(String eventType) { return verifyEvents(eventType, true); } @@ -392,6 +414,43 @@ protected JfrEvents verifyEvents(Path recording, String eventType, boolean failO } } + /** + * 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; + } + } + protected final void verifyCStackSettings() { String cstack = (String)testParams.get("cstack"); if (cstack == null) { diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvents.java b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvents.java index d9ff79bf0d..8d250beba6 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvents.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvents.java @@ -16,7 +16,10 @@ 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; /** @@ -115,6 +118,76 @@ public static JfrEvents load(Path recording, Predicate typeFilter, Predi 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. + */ + 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}; + 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))); + 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. + */ + 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(); + 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))); + 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 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..86ec5babf6 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 @@ -39,7 +39,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/nativelibs/NativeLibrariesTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativelibs/NativeLibrariesTest.java index 4ca70b6e05..5e46d161c2 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 @@ -10,7 +10,6 @@ import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.RetryingTest; -import com.datadoghq.profiler.JfrEvent; import org.xerial.snappy.Snappy; import java.io.IOException; @@ -63,12 +62,13 @@ public void test() { } stopProfiler(); assertTrue(blackhole != 0); - Map modeCounters = new HashMap<>(); - Map libraryCounters = new HashMap<>(); - for (JfrEvent item : verifyEvents("datadog.ExecutionSample")) { + // 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); - modeCounters.computeIfAbsent(mode, x -> new AtomicInteger()).incrementAndGet(); + c.modeCounters.computeIfAbsent(mode, x -> new AtomicInteger()).incrementAndGet(); if ("NATIVE".equals(mode)) { String library = ""; if (stacktrace.contains("LZ4JNI") || stacktrace.contains(".LZ4HC_")) { @@ -80,18 +80,18 @@ public void test() { } else if (stacktrace.contains("Compile")) { library = "JIT"; } - libraryCounters.computeIfAbsent(library, x -> new AtomicInteger()).incrementAndGet(); + 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())); } @@ -188,6 +188,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/NativememSampledProfilerTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/nativemem/NativememSampledProfilerTest.java index c4fa5d6296..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,8 +12,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.params.provider.ValueSource; -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; @@ -60,9 +58,10 @@ public void shouldEmitWeightedMallocSamples() throws InterruptedException { stopProfiler(); - JfrEvents events = verifyEvents("datadog.NativeMemoryAllocation"); - int sampleCount = 0; - for (JfrEvent item : events) { + // 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); @@ -74,8 +73,7 @@ public void shouldEmitWeightedMallocSamples() throws InterruptedException { assertTrue(weight >= 1.0, "weight must be >= 1.0 on the sampled path, got " + weight + " (size=" + size + ")"); - sampleCount++; - } + }); // 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 From 476968ec18182ca0b2235274c4abd4db3ba0bc35 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Wed, 5 Aug 2026 17:10:25 +0200 Subject: [PATCH 3/7] Port NativeThreadPrimingTest to the streaming JfrEvents API Merging origin/main pulled in this test written against the old JMC-based verifyEvents(), which no longer compiles now that it returns JfrEvents. Uses streamEvents() instead. Co-Authored-By: Claude Sonnet 5 --- .../profiler/cpu/NativeThreadPrimingTest.java | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) 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); } } From 1813c798c6c7ac15ea487c0d216ba2b026790de5 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Thu, 6 Aug 2026 09:43:52 +0200 Subject: [PATCH 4/7] Fix JfrFrame/JfrEvent to return dot-separated class names JVM-internal slash-separated names broke tests comparing against getClass().getName(), failing CI across the full test matrix. Co-Authored-By: Claude Sonnet 5 --- .../src/test/java/com/datadoghq/profiler/JfrEvent.java | 9 ++++++--- .../src/test/java/com/datadoghq/profiler/JfrFrame.java | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvent.java b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvent.java index 19a20e8af8..44d1d04d51 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvent.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvent.java @@ -192,11 +192,14 @@ public String getClassName(String field) { return null; } Object name = ((Map) c).get("name"); + String s; if (name instanceof Map) { - Object s = ((Map) name).get("string"); - return s != null ? s.toString() : null; + Object v = ((Map) name).get("string"); + s = v != null ? v.toString() : null; + } else { + s = name != null ? name.toString() : null; } - return name != null ? name.toString() : null; + return s != null ? s.replace('/', '.') : null; } @Override diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/JfrFrame.java b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrFrame.java index d5a01256fd..37da36ad22 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/JfrFrame.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrFrame.java @@ -56,6 +56,7 @@ public String className() { if (!(type instanceof Map)) { return null; } - return symbolString(((Map) type).get("name")); + String name = symbolString(((Map) type).get("name")); + return name != null ? name.replace('/', '.') : null; } } From 4a1a732ad0d0a864c348128f9393c64d8b6a5277 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Thu, 6 Aug 2026 11:49:17 +0200 Subject: [PATCH 5/7] Stream remaining high-volume JFR event checks instead of materializing them AllocationProfilerTest, CTimerSamplerTest, MegamorphicCallTest, GCGenerationsTest, and MemleakProfilerTest each materialized every matching JFR event (deep-resolved, full stack traces) into memory, which OOMs the test heap under heavy sampling load. Also fixes AllocationProfilerTest comparing array class names against Class.getCanonicalName() ("int[]") instead of getClassName()'s Class.getName()-style format ("[I"), which silently zeroed out recorded allocation sizes. Co-Authored-By: Claude Sonnet 5 --- .../alloc/AllocationProfilerTest.java | 38 +++++++++++++----- .../profiler/cpu/CTimerSamplerTest.java | 12 +++--- .../profiler/memleak/GCGenerationsTest.java | 6 ++- .../profiler/memleak/MemleakProfilerTest.java | 8 +++- .../wallclock/MegamorphicCallTest.java | 40 ++++++++++--------- 5 files changed, 65 insertions(+), 39 deletions(-) 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 fa9faa6217..e4888a7a5c 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 @@ -2,7 +2,6 @@ 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; @@ -35,26 +34,43 @@ public void shouldGetObjectAllocationSamples() throws InterruptedException { AllocatingTarget target1 = new AllocatingTarget(); AllocatingTarget target2 = new AllocatingTarget(); runTests(target1, target2); - JfrEvents 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(JfrEvents 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); } - JfrEvents allocationsByType = allocations.filter(allocatedTypeFilter(clazz.getCanonicalName())); - assertTrue(allocationsByType.hasItems()); - long recorded = 0; - for (JfrEvent item : allocationsByType) { - recorded += (long) scaledSize(item); - } + 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/cpu/CTimerSamplerTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/cpu/CTimerSamplerTest.java index f1954a0cc0..380a9eb4c2 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 @@ -13,14 +13,13 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.junitpioneer.jupiter.RetryingTest; -import com.datadoghq.profiler.JfrEvent; -import com.datadoghq.profiler.JfrEvents; 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; @@ -50,12 +49,13 @@ public void test(@CStack String cstack) throws ExecutionException, InterruptedEx verifyCStackSettings(); - JfrEvents events = verifyEvents("datadog.ExecutionSample"); - - for (JfrEvent sample : events) { + // 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/memleak/GCGenerationsTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/memleak/GCGenerationsTest.java index 4bb22ae6ca..1d8fdf343c 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 @@ -30,7 +30,11 @@ public void shouldGetLiveObjectSamples() throws InterruptedException { MemLeakTarget target1 = new MemLeakTarget(); MemLeakTarget target2 = new MemLeakTarget(); runTests(target1, target2); - verifyEvents("datadog.HeapLiveObject"); + // Streamed rather than materialized: 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. + long sampleCount = streamEvents("datadog.HeapLiveObject", e -> {}); + assertTrue(sampleCount > 0, "datadog.HeapLiveObject was empty"); } public static class MemLeakTarget extends ClassValue implements Runnable { 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 0c41e4dff4..26605b9f01 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 @@ -32,8 +32,12 @@ public void shouldGetLiveObjectSamples() throws InterruptedException { MemLeakTarget target1 = new MemLeakTarget(); MemLeakTarget target2 = new MemLeakTarget(); runTests(target1, target2); - JfrEvents allocations = verifyEvents("datadog.HeapLiveObject"); - JfrEvents heapUsage = verifyEvents("datadog.HeapUsage"); + // Streamed rather than materialized: 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. + long sampleCount = streamEvents("datadog.HeapLiveObject", e -> {}); + assertTrue(sampleCount > 0, "datadog.HeapLiveObject was empty"); + verifyEvents("datadog.HeapUsage"); // assertAllocations(allocations, int[].class, target1, target2); // assertAllocations(allocations, Integer[].class, target1, target2); } 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 9b3f887efe..414d2b30c3 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 @@ -4,14 +4,9 @@ import com.datadoghq.profiler.Platform; import org.junit.jupiter.api.Assumptions; import org.junitpioneer.jupiter.RetryingTest; -import com.datadoghq.profiler.JfrEvent; -import com.datadoghq.profiler.JfrEvents; -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 { @@ -83,22 +78,29 @@ public void testITableStubs() { int result = profiledWork(iterations, new Calculator1(), new Calculator2(), new Calculator3()); System.err.println(result); stopProfiler(); - JfrEvents events = verifyEvents("datadog.MethodSample"); - System.err.println(events.count()); - List itableStubStacktraces = new ArrayList<>(); - for (JfrEvent sample : events) { + + // 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()")) { - itableStubStacktraces.add(stackTrace); + 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; } } From 77ba5ae6f0d3fbe4bc08850dcfd57615adcbf8ad Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Thu, 6 Aug 2026 12:36:07 +0200 Subject: [PATCH 6/7] Clean up streaming JFR event checks: dead code, reinvented helper, javadoc Use verifyEventPresent instead of streamEvents+assert for presence-only checks, drop dead allocation-comparison code left over from the materialization fix, and correct a verifyEvents javadoc overclaim about stack traces. Co-Authored-By: Claude Sonnet 5 --- .../profiler/AbstractProfilerTest.java | 6 +++ .../profiler/memleak/GCGenerationsTest.java | 10 ++--- .../profiler/memleak/MemleakProfilerTest.java | 37 +++---------------- 3 files changed, 16 insertions(+), 37 deletions(-) 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 2b069e2755..1c32de3b72 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/AbstractProfilerTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/AbstractProfilerTest.java @@ -391,6 +391,12 @@ protected void verifyEventPresent(Path recording, 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); } 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 1d8fdf343c..cb423a9098 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 @@ -11,7 +11,6 @@ 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 { @@ -30,11 +29,10 @@ public void shouldGetLiveObjectSamples() throws InterruptedException { MemLeakTarget target1 = new MemLeakTarget(); MemLeakTarget target2 = new MemLeakTarget(); runTests(target1, target2); - // Streamed rather than materialized: 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. - long sampleCount = streamEvents("datadog.HeapLiveObject", e -> {}); - assertTrue(sampleCount > 0, "datadog.HeapLiveObject was empty"); + // 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/MemleakProfilerTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/memleak/MemleakProfilerTest.java index 26605b9f01..af11c5ce2c 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 @@ -2,8 +2,6 @@ import com.datadoghq.profiler.Platform; import com.datadoghq.profiler.AbstractProfilerTest; -import com.datadoghq.profiler.JfrEvent; -import com.datadoghq.profiler.JfrEvents; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.RetryingTest; @@ -13,7 +11,6 @@ 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 { @@ -32,31 +29,13 @@ public void shouldGetLiveObjectSamples() throws InterruptedException { MemLeakTarget target1 = new MemLeakTarget(); MemLeakTarget target2 = new MemLeakTarget(); runTests(target1, target2); - // Streamed rather than materialized: 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. - long sampleCount = streamEvents("datadog.HeapLiveObject", e -> {}); - assertTrue(sampleCount > 0, "datadog.HeapLiveObject was empty"); + // 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"); -// assertAllocations(allocations, int[].class, target1, target2); -// assertAllocations(allocations, Integer[].class, target1, target2); - } - - private static void assertAllocations(JfrEvents allocations, Class clazz, MemLeakTarget... targets) { - long allocated = 0; - for (MemLeakTarget target : targets) { - allocated += target.getAllocated(clazz); - } - JfrEvents allocationsByType = allocations.filter(allocatedTypeFilter(clazz.getCanonicalName())); - assertTrue(allocationsByType.hasItems()); - long recorded = 0; - for (JfrEvent item : allocationsByType) { - recorded += (long) scaledSize(item); - } - 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)); } public static class MemLeakTarget extends ClassValue implements Runnable { @@ -78,10 +57,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); From 27bc85130c242924808afee0a213c63c46fb9eb2 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Thu, 6 Aug 2026 14:30:58 +0200 Subject: [PATCH 7/7] Fix multi-chunk JFR streaming race, trailing-newline bug, and add missing headers/javadoc JfrEvents.load/forEach/reduce read events from jafar's per-chunk parser callbacks, which run concurrently across chunks for multi-chunk recordings; synchronize the shared list/counter/accumulator mutations to avoid lost updates and nondeterministic verification. getStackTraceString appended a trailing newline after the last frame, breaking endsWith-style checks. Also adds missing copyright headers and public-method javadoc across the touched test files. Co-Authored-By: Claude Sonnet 5 --- .../profiler/AbstractProfilerTest.java | 5 +++ .../java/com/datadoghq/profiler/JfrEvent.java | 16 ++++++- .../com/datadoghq/profiler/JfrEvents.java | 44 ++++++++++++++++--- .../com/datadoghq/profiler/JfrStackTrace.java | 9 ++++ .../alloc/AllocationProfilerTest.java | 5 +++ .../profiler/cpu/CTimerSamplerTest.java | 5 +++ .../profiler/cpu/ContextCpuTest.java | 5 +++ .../cpu/LightweightContextCpuTest.java | 5 +++ .../datadoghq/profiler/cpu/SmokeCpuTest.java | 5 +++ .../profiler/cpu/VtableReceiverFrameTest.java | 5 +++ .../profiler/endpoints/EndpointTest.java | 5 +++ .../profiler/memleak/GCGenerationsTest.java | 5 +++ .../profiler/memleak/MemleakProfilerTest.java | 5 +++ .../BoundMethodHandleProfilerTest.java | 5 +++ .../metadata/MetadataNormalisationTest.java | 5 +++ .../nativelibs/NativeLibrariesTest.java | 5 +++ .../NativeSocketBytesAccuracyTest.java | 5 +++ .../NativeSocketDisabledTest.java | 5 +++ .../nativesocket/NativeSocketEnabledTest.java | 5 +++ .../NativeSocketEventFieldsTest.java | 5 +++ .../NativeSocketEventThreadTest.java | 5 +++ .../NativeSocketMacOsNoOpTest.java | 5 +++ .../NativeSocketRemoteAddressTest.java | 5 +++ .../nativesocket/NativeSocketRestartTest.java | 5 +++ .../NativeSocketSendRecvSeparateTest.java | 5 +++ .../NativeSocketStackTraceTest.java | 5 +++ .../NativeSocketUdpExcludedTest.java | 5 +++ .../profiler/queue/QueueTimeTest.java | 5 +++ .../settings/DatadogSettingsTest.java | 5 +++ .../wallclock/BaseContextWallClockTest.java | 5 +++ .../wallclock/CollapsingSleepTest.java | 5 +++ .../ContendedWallclockSamplesTest.java | 5 +++ .../wallclock/MegamorphicCallTest.java | 5 +++ .../wallclock/PrecheckEfficiencyTest.java | 5 +++ .../profiler/wallclock/SleepTest.java | 5 +++ .../profiler/wallclock/SmokeWallTest.java | 5 +++ .../wallclock/VirtualThreadWallClockTest.java | 5 +++ .../wallclock/WallClockThreadFilterTest.java | 5 +++ 38 files changed, 237 insertions(+), 7 deletions(-) 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 1c32de3b72..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; diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvent.java b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvent.java index 44d1d04d51..7974de74ae 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvent.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvent.java @@ -30,6 +30,7 @@ public final class JfrEvent { this.value = value; } + /** The JFR event type name (e.g. {@code "datadog.ExecutionSample"}). */ public String typeName() { return typeName; } @@ -44,10 +45,12 @@ 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; @@ -70,21 +73,25 @@ public Long getLong(String 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; @@ -105,6 +112,7 @@ 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)); } @@ -128,6 +136,7 @@ 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 @@ -136,13 +145,18 @@ public String getStackTraceString(String 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("()\n"); + sb.append(methodName != null ? methodName : "").append("()"); } return sb.toString(); } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvents.java b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvents.java index 8d250beba6..da140e13f3 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvents.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrEvents.java @@ -88,6 +88,13 @@ public static JfrEvents load(Path recording, Predicate typeFilter) throw * 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 { @@ -97,13 +104,16 @@ public static JfrEvents load(Path recording, Predicate typeFilter, Predi 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))); - events.add(event); - if (stopWhen.test(event)) { - ctl.abort(); + synchronized (lock) { + events.add(event); + if (stopWhen.test(event)) { + ctl.abort(); + } } } }); @@ -127,6 +137,12 @@ public static JfrEvents load(Path recording, Predicate typeFilter, Predi * 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 { @@ -136,12 +152,15 @@ public static long forEach(Path recording, Predicate typeFilter, Consume 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))); - consumer.accept(event); - count[0]++; + synchronized (lock) { + consumer.accept(event); + count[0]++; + } } }); parser.run(); @@ -161,6 +180,12 @@ public static long forEach(Path recording, Predicate typeFilter, Consume * 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 { @@ -170,11 +195,14 @@ public static T reduce(Path recording, Predicate typeFilter, Supplie 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))); - accumulator.accept(acc, event); + synchronized (lock) { + accumulator.accept(acc, event); + } } }); parser.run(); @@ -235,10 +263,12 @@ private static Object collapseValue(Object v) { 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(); } @@ -248,6 +278,7 @@ 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) { @@ -258,6 +289,7 @@ public JfrEvents filter(Predicate predicate) { return new JfrEvents(filtered); } + /** A {@link Stream} over this collection's events, for callers preferring stream-style access. */ public Stream stream() { return events.stream(); } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/JfrStackTrace.java b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrStackTrace.java index 03284f20d5..548230701c 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/JfrStackTrace.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/JfrStackTrace.java @@ -24,6 +24,12 @@ private JfrStackTrace(List frames, boolean truncated) { 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)) { @@ -48,14 +54,17 @@ static JfrStackTrace of(Object rawStackTrace) { 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 e4888a7a5c..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.alloc; import com.datadoghq.profiler.AbstractProfilerTest; 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 380a9eb4c2..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; 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 069a111ca9..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; 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 5787e0185a..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; 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 84d0c26703..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; 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 c41d0eafa7..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.cpu; import com.datadoghq.profiler.AbstractProfilerTest; 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 21e12f7722..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.endpoints; import com.datadoghq.profiler.AbstractProfilerTest; 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 cb423a9098..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; 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 af11c5ce2c..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; 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 86ec5babf6..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; 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 f6b82b4198..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.metadata; import com.datadoghq.profiler.AbstractProfilerTest; 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 5e46d161c2..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; 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 baeb6ad1e5..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; import com.datadoghq.profiler.AbstractProfilerTest; 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 727d32db32..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; import com.datadoghq.profiler.JfrEvents; 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 d7e66752d8..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; import org.junit.jupiter.api.Assumptions; 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 4cd1870e14..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; import com.datadoghq.profiler.Platform; 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 7c1ffbffc3..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; import com.datadoghq.profiler.AbstractProfilerTest; 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 1f66d5ab18..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; import com.datadoghq.profiler.JfrEvents; 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 6449578b85..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; import com.datadoghq.profiler.AbstractProfilerTest; 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 8d3a375331..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; import com.datadoghq.profiler.JfrEvents; 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 5b000450a5..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; import com.datadoghq.profiler.AbstractProfilerTest; 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 82245b1461..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; 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 41b6ab5a1f..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.nativesocket; import com.datadoghq.profiler.AbstractProfilerTest; 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 a4a2b2464e..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.queue; import com.datadoghq.profiler.AbstractProfilerTest; 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 a6cc4597c8..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.settings; import com.datadoghq.profiler.AbstractProfilerTest; 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 a96488adad..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; 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 99d4e4253a..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.wallclock; import static org.junit.jupiter.api.Assertions.assertTrue; 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 a7f957bba1..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; 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 414d2b30c3..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.wallclock; import com.datadoghq.profiler.AbstractProfilerTest; 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 6053d6b55e..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.wallclock; import com.datadoghq.profiler.AbstractProfilerTest; 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 1ac87a5141..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.wallclock; import com.datadoghq.profiler.AbstractProfilerTest; 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 8e4e784bd9..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; 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 b7c5b0b703..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; 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 8f75597290..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,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.datadoghq.profiler.wallclock; import static org.junit.jupiter.api.Assertions.assertEquals;