Skip to content

Commit eecff95

Browse files
jbachorikclaude
andcommitted
Migrate ReferenceChainTrackingTest to the JfrEvents API
Fixes the CodeQL Autobuild failure: this test still used JMC's IItemCollection/IItem after AbstractProfilerTest's verifyEvents() switched to the jafar-backed JfrEvents API. Adds a JfrEvents overload of ReferenceChainAssertions.findMatchForClass alongside the existing IItemCollection one, which LeakingCacheScenario/ReferenceChainJfrParserTest still use unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 34b95e1 commit eecff95

2 files changed

Lines changed: 91 additions & 34 deletions

File tree

ddprof-test/src/test/java/com/datadoghq/profiler/referencechains/ReferenceChainAssertions.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
package com.datadoghq.profiler.referencechains;
77

8+
import com.datadoghq.profiler.JfrEvent;
9+
import com.datadoghq.profiler.JfrEvents;
810
import org.openjdk.jmc.common.IMCType;
911
import org.openjdk.jmc.common.item.IAccessorKey;
1012
import org.openjdk.jmc.common.item.IItem;
@@ -16,13 +18,21 @@
1618

1719
import java.util.ArrayList;
1820
import java.util.List;
21+
import java.util.Map;
1922

2023
/**
2124
* Shared {@code datadog.ReferenceChain} JFR-parsing helpers, extracted out of
2225
* {@code ReferenceChainTrackingTest} so both that in-process JUnit test and
2326
* {@link LeakingCacheScenario} (run inside a genuinely separate child JVM by
2427
* {@code ExternalProcessReferenceChainTest}) can reuse the exact same JMC-accessor logic
2528
* rather than maintaining two copies.
29+
*
30+
* <p>{@link #findMatchForClass(JfrEvents, Class)} is a separate, jafar-backed counterpart to
31+
* {@link #findMatchForClass(IItemCollection, Class)} for {@code ReferenceChainTrackingTest}, which
32+
* loads recordings via {@code AbstractProfilerTest}'s {@code JfrEvents}-returning helpers
33+
* (jb/jfr-lightweight-query-api). {@link LeakingCacheScenario} and {@code ReferenceChainJfrParserTest}
34+
* load recordings directly via JMC's {@code JfrLoaderToolkit} and keep using the
35+
* {@code IItemCollection} overload unchanged.
2636
*/
2737
public final class ReferenceChainAssertions {
2838
private ReferenceChainAssertions() {}
@@ -40,6 +50,19 @@ public static final class ChainMatch {
4050
}
4151
}
4252

53+
/** Result of {@link #findMatchForClass(JfrEvents, Class)}: one resolved chain event's fields. */
54+
public static final class JfrChainMatch {
55+
public final List<String> chain;
56+
public final long targetTag;
57+
public final int depth;
58+
59+
JfrChainMatch(List<String> chain, long targetTag, int depth) {
60+
this.chain = chain;
61+
this.targetTag = targetTag;
62+
this.depth = depth;
63+
}
64+
}
65+
4366
/**
4467
* Scans {@code events} for a {@code datadog.ReferenceChain} item whose {@code chain[0]} is
4568
* {@code targetClass} specifically, ignoring any events for other klasses this same
@@ -82,6 +105,57 @@ public static ChainMatch findMatchForClass(IItemCollection events, Class<?> targ
82105
return null;
83106
}
84107

108+
/**
109+
* jafar/{@code JfrEvents}-backed counterpart to {@link #findMatchForClass(IItemCollection, Class)} -
110+
* see this class's own header comment for why these are two separate overloads rather than one.
111+
*/
112+
public static JfrChainMatch findMatchForClass(JfrEvents events, Class<?> targetClass) {
113+
if (events == null || !events.hasItems()) {
114+
return null;
115+
}
116+
for (JfrEvent item : events) {
117+
Object chainValue = item.get("chain");
118+
if (!(chainValue instanceof Object[])) {
119+
throw new IllegalStateException(
120+
"'chain' field resolved to " + chainValue + ", expected an array");
121+
}
122+
Object[] rawChain = (Object[]) chainValue;
123+
if (rawChain.length == 0 || !targetClass.getName().equals(classFullName(rawChain[0]))) {
124+
continue;
125+
}
126+
List<String> chain = new ArrayList<>(rawChain.length);
127+
for (Object element : rawChain) {
128+
chain.add(classFullName(element));
129+
}
130+
long targetTag = item.getLong("targetTag", -1);
131+
int depth = (int) item.getLong("depth", -1);
132+
return new JfrChainMatch(chain, targetTag, depth);
133+
}
134+
return null;
135+
}
136+
137+
/**
138+
* The full name (e.g. {@code java.lang.String}) of a resolved {@code chain[]} array element -
139+
* mirrors {@code JfrEvent.getClassName(String)}'s own class-reference-map unwrapping, applied
140+
* to an array element rather than a named field.
141+
*/
142+
@SuppressWarnings("unchecked")
143+
private static String classFullName(Object element) {
144+
if (!(element instanceof Map)) {
145+
throw new IllegalStateException(
146+
"chain[] element resolved to " + element + ", expected a class reference map");
147+
}
148+
Object name = ((Map<String, Object>) element).get("name");
149+
String s;
150+
if (name instanceof Map) {
151+
Object v = ((Map<String, Object>) name).get("string");
152+
s = v != null ? v.toString() : null;
153+
} else {
154+
s = name != null ? name.toString() : null;
155+
}
156+
return s != null ? s.replace('/', '.') : null;
157+
}
158+
85159
/**
86160
* Looks up a field's accessor by identifier rather than via {@code Attribute.attr(...)}: JMC's
87161
* v1 chunk parser (internal.parser.v1.ValueReaders.ArrayReader#getContentType()) registers

ddprof-test/src/test/java/com/datadoghq/profiler/referencechains/ReferenceChainTrackingTest.java

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,15 @@
77

88
import com.datadoghq.profiler.AbstractProfilerTest;
99
import com.datadoghq.profiler.JavaProfiler;
10+
import com.datadoghq.profiler.JfrEvent;
11+
import com.datadoghq.profiler.JfrEvents;
1012
import com.datadoghq.profiler.Platform;
1113
import org.junit.jupiter.api.MethodOrderer;
1214
import org.junit.jupiter.api.Order;
1315
import org.junit.jupiter.api.Tag;
1416
import org.junit.jupiter.api.Test;
1517
import org.junit.jupiter.api.TestMethodOrder;
1618
import org.junitpioneer.jupiter.RetryingTest;
17-
import org.openjdk.jmc.common.IMCType;
18-
import org.openjdk.jmc.common.item.IAttribute;
19-
import org.openjdk.jmc.common.item.IItem;
20-
import org.openjdk.jmc.common.item.IItemCollection;
21-
import org.openjdk.jmc.common.item.IItemIterable;
22-
import org.openjdk.jmc.common.item.IMemberAccessor;
2319

2420
import java.nio.file.Files;
2521
import java.nio.file.Path;
@@ -32,8 +28,6 @@
3228
import static org.junit.jupiter.api.Assertions.assertEquals;
3329
import static org.junit.jupiter.api.Assertions.assertNotNull;
3430
import static org.junit.jupiter.api.Assertions.assertTrue;
35-
import static org.openjdk.jmc.common.item.Attribute.attr;
36-
import static org.openjdk.jmc.common.unit.UnitLookup.PLAIN_TEXT;
3731

3832
/**
3933
* PROF-15341 (+ lifecycle-wiring follow-up, + the Remaining Work Plan's target-selection bridging,
@@ -92,9 +86,6 @@
9286
@Tag("slow")
9387
public class ReferenceChainTrackingTest extends AbstractProfilerTest {
9488

95-
private static final IAttribute<String> SETTING_NAME = attr("name", "", "", PLAIN_TEXT);
96-
private static final IAttribute<String> SETTING_VALUE = attr("value", "", "", PLAIN_TEXT);
97-
9889
// Arbitrary, test-chosen klass ids for the debug-only population-seeding seams below (see
9990
// ReferenceChainTestSeamsTest's own comment: LivenessTracker's population table treats these as
10091
// opaque keys, so they need not resolve to any real class). Distinct per test/from
@@ -224,19 +215,11 @@ protected boolean isPlatformSupported() {
224215
@RetryingTest(5)
225216
public void shouldExposeReferenceChainsSettingWhenEnabled() {
226217
stopProfiler();
227-
IItemCollection settings = verifyEvents("jdk.ActiveSetting");
218+
JfrEvents settings = verifyEvents("jdk.ActiveSetting");
228219
boolean sawEnabledSetting = false;
229-
for (IItemIterable iterable : settings) {
230-
IMemberAccessor<String, IItem> nameAccessor = SETTING_NAME.getAccessor(iterable.getType());
231-
IMemberAccessor<String, IItem> valueAccessor = SETTING_VALUE.getAccessor(iterable.getType());
232-
if (nameAccessor == null || valueAccessor == null) {
233-
continue;
234-
}
235-
for (IItem item : iterable) {
236-
if ("enabled".equals(nameAccessor.getMember(item))
237-
&& "true".equals(valueAccessor.getMember(item))) {
238-
sawEnabledSetting = true;
239-
}
220+
for (JfrEvent item : settings) {
221+
if ("enabled".equals(item.getString("name")) && "true".equals(item.getString("value"))) {
222+
sawEnabledSetting = true;
240223
}
241224
}
242225
assertTrue(sawEnabledSetting, "datadog.ReferenceChain#enabled setting was not found");
@@ -334,7 +317,7 @@ public void shouldReconstructReferrerChainToGcRoot() throws Exception {
334317
// flag as leak candidates - this test's own assertions below look for ChainLink specifically
335318
// among however many datadog.ReferenceChain events actually appear, rather than assuming it
336319
// is the only one.
337-
ReferenceChainAssertions.ChainMatch match = null;
320+
ReferenceChainAssertions.JfrChainMatch match = null;
338321
boolean seededTestKlassTrend = false;
339322
int totalRounds = 16;
340323
for (int round = 1; round <= totalRounds && match == null; round++) {
@@ -436,7 +419,7 @@ public void shouldReconstructReferrerChainToGcRoot() throws Exception {
436419
// keep as its representative. Everything above chain[0] reflects real JDK-internal
437420
// collection representation (e.g. ArrayList's backing array) rather than anything this test
438421
// controls, so it is deliberately not asserted beyond "at least one hop was reconstructed".
439-
assertEquals(ChainLink.class.getName(), match.chain.get(0).getFullName());
422+
assertEquals(ChainLink.class.getName(), match.chain.get(0));
440423
assertTrue(match.targetTag > 0, "targetTag should be a valid, non-zero JVMTI tag");
441424
assertTrue(match.depth >= 0, "depth should be a non-negative hop count");
442425
assertTrue(!gcRootHolder.isEmpty()); // keeps every allocated ChainLink reachable until here
@@ -515,7 +498,7 @@ public void shouldReconstructReferrerChainThroughUnboundedCacheLeak() throws Exc
515498
// floor rather than this test's own "memory=64" request, why totalRounds is capped at
516499
// 16 rather than a larger margin above the 10-round minimum (shared-fork heap headroom),
517500
// and why per-round growth itself is clamped to round 10 (Math.min(round, 10) below).
518-
ReferenceChainAssertions.ChainMatch match = null;
501+
ReferenceChainAssertions.JfrChainMatch match = null;
519502
boolean seededTestKlassTrend = false;
520503
int totalRounds = 16;
521504

@@ -559,7 +542,7 @@ public void shouldReconstructReferrerChainThroughUnboundedCacheLeak() throws Exc
559542
nextKey += newEntries;
560543
System.gc();
561544
dump(scratchDumpPath);
562-
IItemCollection events1 = verifyEvents(scratchDumpPath, "datadog.ReferenceChain", false);
545+
JfrEvents events1 = verifyEvents(scratchDumpPath, "datadog.ReferenceChain", false);
563546
match = ReferenceChainAssertions.findMatchForClass(events1, CachedPayload.class);
564547

565548
if (match == null && "debug".equals(System.getProperty("ddprof_test.config"))) {
@@ -581,7 +564,7 @@ public void shouldReconstructReferrerChainThroughUnboundedCacheLeak() throws Exc
581564
JavaProfiler.setKlassPopulationRepresentativeForTest0(CACHED_PAYLOAD_TEST_KLASS_ID, cache.get(keys[0]));
582565
JavaProfiler.pollReferenceChainTargets0();
583566
dump(scratchDumpPath);
584-
IItemCollection events2 = verifyEvents(scratchDumpPath, "datadog.ReferenceChain", false);
567+
JfrEvents events2 = verifyEvents(scratchDumpPath, "datadog.ReferenceChain", false);
585568
match = ReferenceChainAssertions.findMatchForClass(events2, CachedPayload.class);
586569
}
587570

@@ -591,32 +574,32 @@ public void shouldReconstructReferrerChainThroughUnboundedCacheLeak() throws Exc
591574
// slot against Profiler::dump()'s own exclusive hold.
592575
Thread.sleep(300);
593576
dump(scratchDumpPath);
594-
IItemCollection events3 = verifyEvents(scratchDumpPath, "datadog.ReferenceChain", false);
577+
JfrEvents events3 = verifyEvents(scratchDumpPath, "datadog.ReferenceChain", false);
595578
match = ReferenceChainAssertions.findMatchForClass(events3, CachedPayload.class);
596579
}
597580
}
598581

599582
for (int attempt = 0; match == null && attempt < 5; attempt++) {
600583
Thread.sleep(1000);
601584
dump(scratchDumpPath);
602-
IItemCollection events4 = verifyEvents(scratchDumpPath, "datadog.ReferenceChain", false);
585+
JfrEvents events4 = verifyEvents(scratchDumpPath, "datadog.ReferenceChain", false);
603586
match = ReferenceChainAssertions.findMatchForClass(events4, CachedPayload.class);
604587
}
605588

606589
assertNotNull(match,
607590
"Never observed a datadog.ReferenceChain event whose chain[0] is " + CachedPayload.class
608591
+ " after " + cache.size() + " cached entries across up to " + totalRounds
609592
+ " population-growth rounds plus a grace period");
610-
assertEquals(CachedPayload.class.getName(), match.chain.get(0).getFullName());
593+
assertEquals(CachedPayload.class.getName(), match.chain.get(0));
611594
assertTrue(match.targetTag > 0, "targetTag should be a valid, non-zero JVMTI tag");
612595
assertTrue(match.depth >= 0, "depth should be a non-negative hop count");
613596

614597
// The point of this test over shouldReconstructReferrerChainToGcRoot(): confirm the walk
615598
// actually passed through the cache's own internal storage, not some other, coincidental
616599
// retainer - cache is the only thing keeping any CachedPayload instance reachable.
617600
boolean sawHashMapInternals = false;
618-
for (IMCType type : match.chain) {
619-
if (type.getFullName().startsWith("java.util.HashMap")) {
601+
for (String type : match.chain) {
602+
if (type.startsWith("java.util.HashMap")) {
620603
sawHashMapInternals = true;
621604
break;
622605
}
@@ -685,7 +668,7 @@ public void shouldReportAbandonedSearchOnTinyFrontierCap() throws Exception {
685668
Path dumpPath = Paths.get("referencechains-abandoned-test.jfr");
686669
try {
687670
dump(dumpPath);
688-
IItemCollection abandoned = verifyEvents(dumpPath, "datadog.ReferenceChainAbandoned", true);
671+
JfrEvents abandoned = verifyEvents(dumpPath, "datadog.ReferenceChainAbandoned", true);
689672
assertTrue(abandoned.hasItems(), "Expected at least one datadog.ReferenceChainAbandoned event");
690673
} finally {
691674
Files.deleteIfExists(dumpPath);

0 commit comments

Comments
 (0)