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