Skip to content

Commit aa92b9b

Browse files
committed
add test
1 parent f7fe9bf commit aa92b9b

1 file changed

Lines changed: 107 additions & 0 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.datadoghq.profiler.cpu;
2+
3+
import com.datadoghq.profiler.AbstractProfilerTest;
4+
import com.datadoghq.profiler.Platform;
5+
import org.junit.jupiter.api.Assumptions;
6+
import org.junitpioneer.jupiter.RetryingTest;
7+
import org.openjdk.jmc.common.item.IItem;
8+
import org.openjdk.jmc.common.item.IItemCollection;
9+
import org.openjdk.jmc.common.item.IItemIterable;
10+
import org.openjdk.jmc.common.item.IMemberAccessor;
11+
import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes;
12+
13+
import static org.junit.jupiter.api.Assertions.assertTrue;
14+
15+
/**
16+
* Verifies that cstack=vm recordings correctly resolve {@code <clinit>} frames instead of
17+
* reporting them as "unknown".
18+
*
19+
* <p>With cstack=vm, jmethodID preloading is disabled for all classes. walkVM stores
20+
* method frames as raw VMMethod* pointers when no jmethodID has been allocated.
21+
* At dump time, HotspotSupport::resolve() tries JNI GetMethodID / GetStaticMethodID to
22+
* obtain the jmethodID, but JNI intentionally hides class initializers from callers.
23+
* Without the fix both calls return null and the frame is serialised as "unknown".
24+
* With the fix, a JVMTI GetClassMethods fallback forces jmethodID-slot allocation for
25+
* all methods in the class (including {@code <clinit>}), and re-reading from VM metadata
26+
* returns the correct identifier.
27+
*/
28+
public class ClinitResolutionTest extends AbstractProfilerTest {
29+
30+
// SpinningClinit must NOT be referenced anywhere except inside the test body so that
31+
// the class is not loaded (and therefore not initialised) until the profiler is active.
32+
// It is a named static inner class rather than ASM-generated bytes so that FindClass
33+
// can locate it via the system/application class loader, which is the exact code path
34+
// exercised by the fix (system-loader classes skip jmethodID preloading but ARE
35+
// reachable via FindClass at dump time).
36+
static class SpinningClinit {
37+
// Spin for ~2 s in the static initialiser so the cpu=1ms profiler gets
38+
// thousands of sample opportunities inside this <clinit>.
39+
// System.nanoTime() calls prevent the JIT from eliminating the loop body,
40+
// and the loop count is high enough that HotSpot's C1/C2 compilation kicks in
41+
// — but crucially, HotSpot's JIT does NOT allocate jmethodID slots; only JNI
42+
// or JVMTI callers do that. The jmethodID slot therefore stays null until
43+
// HotspotSupport::resolve() runs at dump time, which is exactly the scenario
44+
// that requires the JVMTI fallback for <clinit>.
45+
static {
46+
long deadline = System.nanoTime() + 2_000_000_000L;
47+
while (System.nanoTime() < deadline) {
48+
// intentionally empty
49+
}
50+
}
51+
52+
// Static method whose invocation triggers class initialization.
53+
static void touch() {}
54+
}
55+
56+
@Override
57+
protected String getProfilerCommand() {
58+
// cstack=vm routes all Java-frame collection through HotspotSupport::walkVM and
59+
// disables jmethodID preloading, producing raw VMMethod* frames that are resolved
60+
// by HotspotSupport::resolve() at dump time.
61+
return "cpu=1ms,cstack=vm";
62+
}
63+
64+
@RetryingTest(5)
65+
public void testClinitFrameIsResolvedNotUnknown() throws Exception {
66+
// HotspotSupport::walkVM and the raw-pointer resolve path are Hotspot-only.
67+
Assumptions.assumeFalse(Platform.isZing() || Platform.isJ9() || Platform.isGraal());
68+
69+
waitForProfilerReady(2000);
70+
71+
// Trigger class loading + initialisation on this thread.
72+
// SpinningClinit.<clinit> spins for ~2 seconds; during that time the profiler
73+
// collects cpu samples whose top Java frame is the class initialiser.
74+
SpinningClinit.touch();
75+
76+
stopProfiler();
77+
78+
// Assert that at least one CPU sample contains a resolved <clinit> frame for
79+
// SpinningClinit — i.e. both the class name and the method name are present,
80+
// not replaced by "unknown".
81+
//
82+
// JMC's STACK_TRACE_STRING HTML-escapes angle brackets, so <clinit> appears as
83+
// "&lt;clinit&gt;". Matching on "clinit" catches both representations.
84+
IItemCollection events = verifyEvents("datadog.ExecutionSample");
85+
boolean foundClinit = false;
86+
outer:
87+
for (IItemIterable cpuSamples : events) {
88+
IMemberAccessor<String, IItem> frameAccessor =
89+
JdkAttributes.STACK_TRACE_STRING.getAccessor(cpuSamples.getType());
90+
if (frameAccessor == null) continue;
91+
for (IItem sample : cpuSamples) {
92+
String stackTrace = frameAccessor.getMember(sample);
93+
if (stackTrace != null
94+
&& stackTrace.contains("SpinningClinit")
95+
&& stackTrace.contains("clinit")) {
96+
System.err.println("=CLINIT TRACE=\n" + stackTrace + "\n=END=");
97+
foundClinit = true;
98+
break outer;
99+
}
100+
}
101+
}
102+
assertTrue(foundClinit,
103+
"No CPU sample contained a resolved <clinit> frame for SpinningClinit. "
104+
+ "HotspotSupport::resolve() must fall back to JVMTI GetClassMethods for "
105+
+ "<clinit> because JNI GetMethodID/GetStaticMethodID cannot look it up.");
106+
}
107+
}

0 commit comments

Comments
 (0)