1 /*
  2  * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 /**
 25  * @test
 26  * @requires vm.debug
 27  * @modules java.base/jdk.internal.value
 28  * @library /test/lib
 29  * @modules java.base/jdk.internal.vm.annotation java.base/jdk.internal.value
 30  * @enablePreview
 31  * @run main/othervm HeapDump
 32  */
 33 
 34 import java.io.File;
 35 import java.lang.ref.Reference;
 36 import java.lang.reflect.Array;
 37 import java.lang.reflect.Field;
 38 import java.lang.reflect.Modifier;
 39 import java.util.concurrent.TimeUnit;
 40 import java.util.Enumeration;
 41 import jdk.internal.value.ValueClass;
 42 import jdk.internal.vm.annotation.NullRestricted;
 43 import jdk.internal.vm.annotation.Strict;
 44 
 45 import jdk.test.lib.apps.LingeredApp;
 46 import jdk.test.lib.JDKToolLauncher;
 47 import jdk.test.lib.hprof.model.HackJavaValue;
 48 import jdk.test.lib.hprof.model.JavaByte;
 49 import jdk.test.lib.hprof.model.JavaClass;
 50 import jdk.test.lib.hprof.model.JavaField;
 51 import jdk.test.lib.hprof.model.JavaHeapObject;
 52 import jdk.test.lib.hprof.model.JavaObject;
 53 import jdk.test.lib.hprof.model.JavaObjectArray;
 54 import jdk.test.lib.hprof.model.JavaStatic;
 55 import jdk.test.lib.hprof.model.JavaThing;
 56 import jdk.test.lib.hprof.model.JavaValueArray;
 57 import jdk.test.lib.hprof.model.Snapshot;
 58 import jdk.test.lib.hprof.parser.Reader;
 59 import jdk.test.lib.process.ProcessTools;
 60 
 61 
 62 value class TestClass {
 63 
 64     public static value class Value0 {
 65         public int value0_int;
 66         public Value0(int i) { value0_int = i; }
 67     }
 68 
 69     // value class with the only value filed
 70     // offset of flattened value_value0 is the same as offset of flattened Value
 71     public static value class Value {
 72         public Value0 value_value0;
 73 
 74         public Value(int i) {
 75             value_value0 = i == 0 ? null : new Value0(i);
 76         }
 77     }
 78 
 79     public static value class ValueHolder {
 80         public Value holder_value;
 81 
 82         public ValueHolder(int i) {
 83             holder_value = new Value(i);
 84         }
 85     }
 86 
 87     // value class with reference (String)
 88     public static value class ValueRef {
 89         public Value ref_value;
 90         public String ref_str;
 91 
 92         public ValueRef(int i) {
 93             ref_value = new Value(i);
 94             ref_str = "#" + String.valueOf(i);
 95         }
 96     }
 97 
 98     // nullable value
 99     public Value nullableValue = new Value(1);
100     // null restricted value
101     @Strict
102     @NullRestricted
103     public Value nullRestrictedValue = new Value(11);
104     // null value
105     public Value nullValue = null;
106     // value object with reference
107     public ValueRef refValue = new ValueRef(21);
108 
109     // nullable array
110     public Value[] nullableArray = createNullableArray(101);
111     // null restricted array
112     public Value[] nullRestrictedArray = createNullRestrictedArray(201);
113 
114     // static value field (for sanity, cannot be flat)
115     public static Value staticValue = new Value(31);
116 
117     public TestClass() {
118     }
119 
120     static Value[] createNullableArray(int seed) {
121         Value[] arr = (Value[])ValueClass.newNullableAtomicArray(Value.class, 5);
122         for (int i = 0; i < arr.length; i++) {
123             arr[i] = (i % 2 != 0) ? null : new Value(seed + i);
124         }
125         return arr;
126     }
127 
128     static Value[] createNullRestrictedArray(int seed) {
129         Value defValue = new Value(0);
130         Value[] arr = (Value[])ValueClass.newNullRestrictedNonAtomicArray(Value.class, 5, defValue);
131         for (int i = 0; i < arr.length; i++) {
132             arr[i] = new Value(seed + i);
133         }
134         return arr;
135     }
136 }
137 
138 class HeapDumpTarg extends LingeredApp {
139     public static void main(String[] args) {
140         TestClass testObj = new TestClass();
141         LingeredApp.main(args);
142         Reference.reachabilityFence(testObj);
143     }
144 }
145 
146 public class HeapDump {
147     public static void main(String[] args) throws Throwable {
148         LingeredApp theApp = null;
149         String hprogFile = new File(System.getProperty("test.classes") + "/Myheapdump.hprof").getAbsolutePath();
150         try {
151             theApp = new HeapDumpTarg();
152 
153             // -XX:+PrintInlineLayout is debug-only arg
154             LingeredApp.startApp(theApp, "--enable-preview", "-XX:+UnlockDiagnosticVMOptions",
155                                 "-XX:+PrintInlineLayout", "-XX:+PrintFlatArrayLayout",
156                                  "--add-modules=java.base",
157                                  "--add-exports=java.base/jdk.internal.value=ALL-UNNAMED");
158 
159             // jcmd <pid> GC.heap_dump
160             JDKToolLauncher launcher = JDKToolLauncher
161                                         .createUsingTestJDK("jcmd")
162                                         .addToolArg(Long.toString(theApp.getPid()))
163                                         .addToolArg("GC.heap_dump")
164                                         .addToolArg(hprogFile);
165             Process jcmd = ProcessTools.startProcess("jcmd", new ProcessBuilder(launcher.getCommand()));
166             // If something goes wrong with heap dumping most likely we'll get crash of the target VM
167             while (!jcmd.waitFor(5, TimeUnit.SECONDS)) {
168                 if (!theApp.getProcess().isAlive()) {
169                     log("ERROR: target VM died, killing jcmd...");
170                     jcmd.destroyForcibly();
171                     throw new Exception("Target VM died");
172                 }
173             }
174 
175             if (jcmd.exitValue() != 0) {
176                 throw new Exception("Jcmd exited with code " + jcmd.exitValue());
177             }
178         } finally {
179             LingeredApp.stopApp(theApp);
180         }
181 
182         // test object to compare
183         TestClass testObj = new TestClass();
184 
185         log("Reading " + hprogFile + "...");
186         try (Snapshot snapshot = Reader.readFile(hprogFile,true, 0)) {
187             log("Snapshot read, resolving...");
188             snapshot.resolve(true);
189             log("Snapshot resolved.");
190 
191             JavaObject dumpObj = findObject(snapshot, testObj.getClass().getName());
192 
193             log("");
194             print(dumpObj);
195 
196             log("");
197             log("Verifying object " + testObj.getClass().getName() + " (dumped object " + dumpObj + ")");
198             compareObjectFields("  ", testObj, dumpObj);
199         }
200     }
201 
202     private static JavaObject findObject(Snapshot snapshot, String className) throws Exception {
203         log("looking for " + className + "...");
204         JavaClass jClass = snapshot.findClass(className);
205         if (jClass == null) {
206             throw new Exception("'" + className + "' not found");
207         }
208         Enumeration<JavaHeapObject> objects = jClass.getInstances(false);
209         if (!objects.hasMoreElements()) {
210             throw new Exception("No '" + className + "' instances found");
211         }
212         JavaHeapObject heapObj = objects.nextElement();
213         if (objects.hasMoreElements()) {
214             throw new Exception("More than 1 instances of '" + className + "' found");
215         }
216         if (!(heapObj instanceof JavaObject)) {
217             throw new Exception("'" + className + "' instance is not JavaObject (" + heapObj.getClass() + ")");
218         }
219         return (JavaObject)heapObj;
220     }
221 
222     // description of the current object for logging and error reporting
223     private static String objDescr;
224     private static boolean errorReported = false;
225 
226     private static void compareObjects(String logPrefix, Object testObj, JavaThing dumpObj) throws Exception {
227         if (testObj == null) {
228             if (!isNullValue(dumpObj)) {
229                 throw new Exception("null expected, but dumped object is " + dumpObj);
230             }
231             log(logPrefix + objDescr + ": null");
232         } else if (dumpObj instanceof JavaObject obj) {
233             // special handling for Strings
234             if (testObj instanceof String testStr) {
235                 objDescr += " (String, " + obj.getIdString() + ")";
236                 if (!obj.getClazz().isString()) {
237                     throw new Exception("value (" + obj + ")"
238                             + " is not String (" + obj.getClazz() + ")");
239                 }
240                 String dumpStr = getStringValue(obj);
241                 if (!testStr.equals(dumpStr)) {
242                     throw new Exception("different values:"
243                             + " expected \"" + testStr + "\", actual \"" + dumpStr + "\"");
244                 }
245                 log(logPrefix + objDescr + ": \"" + testStr + "\" ( == \"" + dumpStr + "\")");
246             } else {
247                 // other Object
248                 log(logPrefix + objDescr + ": Object " + obj);
249                 if (isTestClass(obj.getClazz().getName())) {
250                     compareObjectFields(logPrefix + "  ", testObj, obj);
251                 }
252             }
253         } else {
254             throw new Exception("Object expected, but the value (" + dumpObj + ")"
255                     + " is not JavaObject (" + dumpObj.getClass() + ")");
256         }
257     }
258 
259     private static void compareObjectFields(String logPrefix, Object testObj, JavaObject dumpObj) throws Exception {
260         Field[] fields = testObj.getClass().getDeclaredFields();
261         for (Field testField : fields) {
262             boolean isStatic = Modifier.isStatic(testField.getModifiers());
263             testField.setAccessible(true);
264             objDescr = "- " + (isStatic ? "(static) " : "")
265                     + testField.getName() + " ('" + testField.getType().descriptorString() + "')";
266             try {
267                 Object testValue = testField.get(testObj);
268 
269                 JavaField dumpField = getField(dumpObj, testField.getName(), isStatic);
270                 JavaThing dumpValue = isStatic
271                         ? dumpObj.getClazz().getStaticField(dumpField.getName())
272                         : dumpObj.getField(dumpField.getName());
273 
274                 objDescr += ", dump signature '" + dumpField.getSignature() + "'";
275 
276                 compareType(testField, dumpField);
277 
278                 if (testValue == null) {
279                     if (!isNullValue(dumpValue)) {
280                         throw new Exception("null expected, but dumped object is " + dumpValue);
281                     }
282                     log(logPrefix + objDescr + ": null");
283                 } else {
284                     switch (testField.getType().descriptorString().charAt(0)) {
285                     case 'L':
286                         compareObjects(logPrefix, testValue, dumpValue);
287                         break;
288                     case '[':
289                         int testLength = Array.getLength(testValue);
290                         objDescr += " (Array of '" + testField.getType().getComponentType() + "'"
291                                 + ", length = " + testLength + ", " + dumpValue + ")";
292                         if (dumpValue instanceof JavaValueArray arr) {
293                             // array of primitive type
294                             char testElementType = testField.getType().getComponentType().descriptorString().charAt(0);
295                             if ((char)arr.getElementType() != testElementType) {
296                                 throw new Exception("wrong element type: '" + (char)arr.getElementType() + "'");
297                             }
298                             int dumpLength = arr.getLength();
299                             if (dumpLength != testLength) {
300                                 throw new Exception("wrong array size: " + dumpLength);
301                             }
302                             JavaThing[] dumpElements = arr.getElements();
303                             log(logPrefix + objDescr);
304                             for (int j = 0; j < testLength; j++) {
305                                 Object elementValue = Array.get(testValue, j);
306                                 objDescr = "[" + j + "]";
307                                 comparePrimitiveValues(elementValue, dumpElements[j]);
308                                 log(logPrefix + "  [" + j + "]: " + elementValue + " ( == " + dumpElements[j] + ")");
309                             }
310                         } else if (dumpValue instanceof JavaObjectArray arr) {
311                             int dumpLength = arr.getLength();
312                             if (dumpLength != testLength) {
313                                 throw new Exception("wrong array size: " + dumpLength);
314                             }
315                             JavaThing[] dumpElements = arr.getElements();
316                             log(logPrefix + objDescr);
317                             for (int j = 0; j < testLength; j++) {
318                                 Object elementValue = Array.get(testValue, j);
319                                 objDescr = "[" + j + "]";
320                                 compareObjects(logPrefix + "  ", elementValue, dumpElements[j]);
321                             }
322                         } else {
323                             throw new Exception("Array expected, but the value (" + dumpValue + ")"
324                                     + " is neither JavaValueArray nor JavaObjectArray"
325                                     + " (" + dumpValue.getClass() + ")");
326                         }
327                         break;
328                     default:
329                         comparePrimitiveValues(testValue, dumpValue);
330                         log(logPrefix + objDescr + ": " + testValue + " ( == " + dumpValue + ")");
331                         break;
332                     }
333                 }
334             } catch (Exception ex) {
335                 if (!errorReported) {
336                     log(logPrefix + objDescr + ": ERROR - " + ex.getMessage());
337                     errorReported = true;
338                 }
339                 throw ex;
340             }
341         }
342     }
343 
344     private static JavaField getField(JavaObject obj, String fieldName, boolean isStatic) throws Exception {
345         if (isStatic) {
346             JavaStatic[] statics = obj.getClazz().getStatics();
347             for (JavaStatic st: statics) {
348                 if (st.getField().getName().equals(fieldName)) {
349                     return st.getField();
350                 }
351             }
352         } else {
353             JavaField[] fields = obj.getClazz().getFields();
354             for (JavaField field : fields) {
355                 if (fieldName.equals(field.getName())) {
356                     return field;
357                 }
358             }
359         }
360         throw new Exception("field '" + fieldName + "' not found");
361     }
362 
363     private static void compareType(Field field, JavaField dumpField) throws Exception {
364         String sig = field.getType().descriptorString();
365         char type = sig.charAt(0);
366         if (type == '[') {
367             type = 'L';
368         }
369         char dumpType = dumpField.getSignature().charAt(0);
370         if (dumpType != type) {
371             throw new Exception("type mismatch:"
372                     + " expected '" + type + "' (" + sig + ")"
373                     + ", found '" + dumpField.getSignature().charAt(0) + "' (" + dumpField.getSignature() + ")");
374         }
375     }
376 
377     private static void comparePrimitiveValues(Object testValue, JavaThing dumpValue) throws Exception {
378         // JavaByte.toString() returns hex
379         String testStr = testValue instanceof Byte byteValue
380                 ? (new JavaByte(byteValue)).toString()
381                 : String.valueOf(testValue);
382         String dumpStr = dumpValue.toString();
383         if (!testStr.equals(dumpStr)) {
384             throw new Exception("Wrong value: expected " + testStr + ", actual " + dumpStr);
385         }
386     }
387 
388     private static boolean isNullValue(JavaThing value) {
389         return value == null
390                 // dumped value is HackJavaValue with string representation "<null>"
391                 || (value instanceof HackJavaValue &&  "<null>".equals(value.toString()));
392     }
393 
394     private static String getStringValue(JavaObject value) {
395         JavaThing valueObj = value.getField("value");
396         if (valueObj instanceof JavaValueArray valueArr) {
397             try {
398                 if (valueArr.getElementType() == 'B') {
399                     Field valueField = JavaByte.class.getDeclaredField("value");
400                     valueField.setAccessible(true);
401                     JavaThing[] things = valueArr.getElements();
402                     byte[] bytes = new byte[things.length];
403                     for (int i = 0; i < things.length; i++) {
404                         bytes[i] = valueField.getByte(things[i]);
405                     }
406                     return new String(bytes);
407                 }
408             } catch (Exception ignored) {
409             }
410             return valueArr.valueString();
411         } else {
412             return null;
413         }
414     }
415 
416     private static void print(JavaObject dumpObject) {
417         log("Dumped object " + dumpObject + ":");
418         print("", dumpObject);
419     }
420 
421     private static void print(String prefix, JavaObject dumpObject) {
422         JavaClass clazz = dumpObject.getClazz();
423         // print only test classes
424         if (!isTestClass(clazz.getName())) {
425             return;
426         }
427 
428         JavaField[] fields = clazz.getFields();
429         for (JavaField field : fields) {
430             printFieldValue(prefix, field, false, dumpObject.getField(field.getName()));
431         }
432 
433         JavaStatic[] statics = clazz.getStatics();
434         for (JavaStatic st: statics) {
435             printFieldValue(prefix, st.getField(), true, st.getValue());
436         }
437     }
438 
439     private static void printFieldValue(String prefix, JavaField field, boolean isStatic, JavaThing value) {
440         String logPrefix = prefix + "- " + (isStatic ? "(static) " : "")
441                 + field.getName() + " ('" + field.getSignature() + "'): ";
442         if (isNullValue(value)) {
443             log(logPrefix + "null");
444         } else {
445             if (value instanceof JavaObject obj) {
446                 logPrefix += "(class '" + obj.getClazz().getName() + "'): ";
447                 if (obj.getClazz().isString()) {
448                     String dumpStr = getStringValue(obj);
449                     log(logPrefix + "\"" + dumpStr + "\"");
450                 } else {
451                     log(logPrefix + "object " + obj);
452                     print(prefix + "  ", obj);
453                 }
454             } else if (value instanceof JavaObjectArray arr) {
455                 log(logPrefix + " array " + arr + " length: " + arr.getLength());
456                 JavaThing[] values = arr.getValues();
457                 for (int v = 0; v < values.length; v++) {
458                     log(prefix + "  [" + v + "]: " + values[v]);
459                     if (values[v] instanceof JavaObject obj) {
460                         print(prefix + "    ", obj);
461                     }
462                 }
463             } else if (value instanceof JavaValueArray arr) {
464                 log(logPrefix + "(array of '" + (char)arr.getElementType() + "')" + ": " + arr.valueString());
465             } else {
466                 log(logPrefix + "(" + value.getClass() +  ")" + value.toString());
467             }
468         }
469     }
470 
471     private static boolean isTestClass(String className) {
472         return className.startsWith("TestClass");
473     }
474 
475     private static void log(String msg) {
476         System.out.println(msg);
477         System.out.flush();
478     }
479 
480 }