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             LingeredApp.startApp(theApp, "--enable-preview",
153                                  "-XX:+UnlockDiagnosticVMOptions",
154                                  "-XX:+PrintInlineLayout", "-XX:+PrintFlatArrayLayout",
155                                  "--add-modules=java.base",
156                                  "--add-exports=java.base/jdk.internal.value=ALL-UNNAMED");
157 
158             // jcmd <pid> GC.heap_dump
159             JDKToolLauncher launcher = JDKToolLauncher
160                                         .createUsingTestJDK("jcmd")
161                                         .addToolArg(Long.toString(theApp.getPid()))
162                                         .addToolArg("GC.heap_dump")
163                                         .addToolArg(hprogFile);
164             Process jcmd = ProcessTools.startProcess("jcmd", new ProcessBuilder(launcher.getCommand()));
165             // If something goes wrong with heap dumping most likely we'll get crash of the target VM
166             while (!jcmd.waitFor(5, TimeUnit.SECONDS)) {
167                 if (!theApp.getProcess().isAlive()) {
168                     log("ERROR: target VM died, killing jcmd...");
169                     jcmd.destroyForcibly();
170                     throw new Exception("Target VM died");
171                 }
172             }
173 
174             if (jcmd.exitValue() != 0) {
175                 throw new Exception("Jcmd exited with code " + jcmd.exitValue());
176             }
177         } finally {
178             LingeredApp.stopApp(theApp);
179         }
180 
181         // test object to compare
182         TestClass testObj = new TestClass();
183 
184         log("Reading " + hprogFile + "...");
185         try (Snapshot snapshot = Reader.readFile(hprogFile,true, 0)) {
186             log("Snapshot read, resolving...");
187             snapshot.resolve(true);
188             log("Snapshot resolved.");
189 
190             JavaObject dumpObj = findObject(snapshot, testObj.getClass().getName());
191 
192             log("");
193             print(dumpObj);
194 
195             log("");
196             log("Verifying object " + testObj.getClass().getName() + " (dumped object " + dumpObj + ")");
197             compareObjectFields("  ", testObj, dumpObj);
198         }
199     }
200 
201     private static JavaObject findObject(Snapshot snapshot, String className) throws Exception {
202         log("looking for " + className + "...");
203         JavaClass jClass = snapshot.findClass(className);
204         if (jClass == null) {
205             throw new Exception("'" + className + "' not found");
206         }
207         Enumeration<JavaHeapObject> objects = jClass.getInstances(false);
208         if (!objects.hasMoreElements()) {
209             throw new Exception("No '" + className + "' instances found");
210         }
211         JavaHeapObject heapObj = objects.nextElement();
212         if (objects.hasMoreElements()) {
213             throw new Exception("More than 1 instances of '" + className + "' found");
214         }
215         if (!(heapObj instanceof JavaObject)) {
216             throw new Exception("'" + className + "' instance is not JavaObject (" + heapObj.getClass() + ")");
217         }
218         return (JavaObject)heapObj;
219     }
220 
221     // description of the current object for logging and error reporting
222     private static String objDescr;
223     private static boolean errorReported = false;
224 
225     private static void compareObjects(String logPrefix, Object testObj, JavaThing dumpObj) throws Exception {
226         if (testObj == null) {
227             if (!isNullValue(dumpObj)) {
228                 throw new Exception("null expected, but dumped object is " + dumpObj);
229             }
230             log(logPrefix + objDescr + ": null");
231         } else if (dumpObj instanceof JavaObject obj) {
232             // special handling for Strings
233             if (testObj instanceof String testStr) {
234                 objDescr += " (String, " + obj.getIdString() + ")";
235                 if (!obj.getClazz().isString()) {
236                     throw new Exception("value (" + obj + ")"
237                             + " is not String (" + obj.getClazz() + ")");
238                 }
239                 String dumpStr = getStringValue(obj);
240                 if (!testStr.equals(dumpStr)) {
241                     throw new Exception("different values:"
242                             + " expected \"" + testStr + "\", actual \"" + dumpStr + "\"");
243                 }
244                 log(logPrefix + objDescr + ": \"" + testStr + "\" ( == \"" + dumpStr + "\")");
245             } else {
246                 // other Object
247                 log(logPrefix + objDescr + ": Object " + obj);
248                 if (isTestClass(obj.getClazz().getName())) {
249                     compareObjectFields(logPrefix + "  ", testObj, obj);
250                 }
251             }
252         } else {
253             throw new Exception("Object expected, but the value (" + dumpObj + ")"
254                     + " is not JavaObject (" + dumpObj.getClass() + ")");
255         }
256     }
257 
258     private static void compareObjectFields(String logPrefix, Object testObj, JavaObject dumpObj) throws Exception {
259         Field[] fields = testObj.getClass().getDeclaredFields();
260         for (Field testField : fields) {
261             boolean isStatic = Modifier.isStatic(testField.getModifiers());
262             testField.setAccessible(true);
263             objDescr = "- " + (isStatic ? "(static) " : "")
264                     + testField.getName() + " ('" + testField.getType().descriptorString() + "')";
265             try {
266                 Object testValue = testField.get(testObj);
267 
268                 JavaField dumpField = getField(dumpObj, testField.getName(), isStatic);
269                 JavaThing dumpValue = isStatic
270                         ? dumpObj.getClazz().getStaticField(dumpField.getName())
271                         : dumpObj.getField(dumpField.getName());
272 
273                 objDescr += ", dump signature '" + dumpField.getSignature() + "'";
274 
275                 compareType(testField, dumpField);
276 
277                 if (testValue == null) {
278                     if (!isNullValue(dumpValue)) {
279                         throw new Exception("null expected, but dumped object is " + dumpValue);
280                     }
281                     log(logPrefix + objDescr + ": null");
282                 } else {
283                     switch (testField.getType().descriptorString().charAt(0)) {
284                     case 'L':
285                         compareObjects(logPrefix, testValue, dumpValue);
286                         break;
287                     case '[':
288                         int testLength = Array.getLength(testValue);
289                         objDescr += " (Array of '" + testField.getType().getComponentType() + "'"
290                                 + ", length = " + testLength + ", " + dumpValue + ")";
291                         if (dumpValue instanceof JavaValueArray arr) {
292                             // array of primitive type
293                             char testElementType = testField.getType().getComponentType().descriptorString().charAt(0);
294                             if ((char)arr.getElementType() != testElementType) {
295                                 throw new Exception("wrong element type: '" + (char)arr.getElementType() + "'");
296                             }
297                             int dumpLength = arr.getLength();
298                             if (dumpLength != testLength) {
299                                 throw new Exception("wrong array size: " + dumpLength);
300                             }
301                             JavaThing[] dumpElements = arr.getElements();
302                             log(logPrefix + objDescr);
303                             for (int j = 0; j < testLength; j++) {
304                                 Object elementValue = Array.get(testValue, j);
305                                 objDescr = "[" + j + "]";
306                                 comparePrimitiveValues(elementValue, dumpElements[j]);
307                                 log(logPrefix + "  [" + j + "]: " + elementValue + " ( == " + dumpElements[j] + ")");
308                             }
309                         } else if (dumpValue instanceof JavaObjectArray arr) {
310                             int dumpLength = arr.getLength();
311                             if (dumpLength != testLength) {
312                                 throw new Exception("wrong array size: " + dumpLength);
313                             }
314                             JavaThing[] dumpElements = arr.getElements();
315                             log(logPrefix + objDescr);
316                             for (int j = 0; j < testLength; j++) {
317                                 Object elementValue = Array.get(testValue, j);
318                                 objDescr = "[" + j + "]";
319                                 compareObjects(logPrefix + "  ", elementValue, dumpElements[j]);
320                             }
321                         } else {
322                             throw new Exception("Array expected, but the value (" + dumpValue + ")"
323                                     + " is neither JavaValueArray nor JavaObjectArray"
324                                     + " (" + dumpValue.getClass() + ")");
325                         }
326                         break;
327                     default:
328                         comparePrimitiveValues(testValue, dumpValue);
329                         log(logPrefix + objDescr + ": " + testValue + " ( == " + dumpValue + ")");
330                         break;
331                     }
332                 }
333             } catch (Exception ex) {
334                 if (!errorReported) {
335                     log(logPrefix + objDescr + ": ERROR - " + ex.getMessage());
336                     errorReported = true;
337                 }
338                 throw ex;
339             }
340         }
341     }
342 
343     private static JavaField getField(JavaObject obj, String fieldName, boolean isStatic) throws Exception {
344         if (isStatic) {
345             JavaStatic[] statics = obj.getClazz().getStatics();
346             for (JavaStatic st: statics) {
347                 if (st.getField().getName().equals(fieldName)) {
348                     return st.getField();
349                 }
350             }
351         } else {
352             JavaField[] fields = obj.getClazz().getFields();
353             for (JavaField field : fields) {
354                 if (fieldName.equals(field.getName())) {
355                     return field;
356                 }
357             }
358         }
359         throw new Exception("field '" + fieldName + "' not found");
360     }
361 
362     private static void compareType(Field field, JavaField dumpField) throws Exception {
363         String sig = field.getType().descriptorString();
364         char type = sig.charAt(0);
365         if (type == '[') {
366             type = 'L';
367         }
368         char dumpType = dumpField.getSignature().charAt(0);
369         if (dumpType != type) {
370             throw new Exception("type mismatch:"
371                     + " expected '" + type + "' (" + sig + ")"
372                     + ", found '" + dumpField.getSignature().charAt(0) + "' (" + dumpField.getSignature() + ")");
373         }
374     }
375 
376     private static void comparePrimitiveValues(Object testValue, JavaThing dumpValue) throws Exception {
377         // JavaByte.toString() returns hex
378         String testStr = testValue instanceof Byte byteValue
379                 ? (new JavaByte(byteValue)).toString()
380                 : String.valueOf(testValue);
381         String dumpStr = dumpValue.toString();
382         if (!testStr.equals(dumpStr)) {
383             throw new Exception("Wrong value: expected " + testStr + ", actual " + dumpStr);
384         }
385     }
386 
387     private static boolean isNullValue(JavaThing value) {
388         return value == null
389                 // dumped value is HackJavaValue with string representation "<null>"
390                 || (value instanceof HackJavaValue &&  "<null>".equals(value.toString()));
391     }
392 
393     private static String getStringValue(JavaObject value) {
394         JavaThing valueObj = value.getField("value");
395         if (valueObj instanceof JavaValueArray valueArr) {
396             try {
397                 if (valueArr.getElementType() == 'B') {
398                     Field valueField = JavaByte.class.getDeclaredField("value");
399                     valueField.setAccessible(true);
400                     JavaThing[] things = valueArr.getElements();
401                     byte[] bytes = new byte[things.length];
402                     for (int i = 0; i < things.length; i++) {
403                         bytes[i] = valueField.getByte(things[i]);
404                     }
405                     return new String(bytes);
406                 }
407             } catch (Exception ignored) {
408             }
409             return valueArr.valueString();
410         } else {
411             return null;
412         }
413     }
414 
415     private static void print(JavaObject dumpObject) {
416         log("Dumped object " + dumpObject + ":");
417         print("", dumpObject);
418     }
419 
420     private static void print(String prefix, JavaObject dumpObject) {
421         JavaClass clazz = dumpObject.getClazz();
422         // print only test classes
423         if (!isTestClass(clazz.getName())) {
424             return;
425         }
426 
427         JavaField[] fields = clazz.getFields();
428         for (JavaField field : fields) {
429             printFieldValue(prefix, field, false, dumpObject.getField(field.getName()));
430         }
431 
432         JavaStatic[] statics = clazz.getStatics();
433         for (JavaStatic st: statics) {
434             printFieldValue(prefix, st.getField(), true, st.getValue());
435         }
436     }
437 
438     private static void printFieldValue(String prefix, JavaField field, boolean isStatic, JavaThing value) {
439         String logPrefix = prefix + "- " + (isStatic ? "(static) " : "")
440                 + field.getName() + " ('" + field.getSignature() + "'): ";
441         if (isNullValue(value)) {
442             log(logPrefix + "null");
443         } else {
444             if (value instanceof JavaObject obj) {
445                 logPrefix += "(class '" + obj.getClazz().getName() + "'): ";
446                 if (obj.getClazz().isString()) {
447                     String dumpStr = getStringValue(obj);
448                     log(logPrefix + "\"" + dumpStr + "\"");
449                 } else {
450                     log(logPrefix + "object " + obj);
451                     print(prefix + "  ", obj);
452                 }
453             } else if (value instanceof JavaObjectArray arr) {
454                 log(logPrefix + " array " + arr + " length: " + arr.getLength());
455                 JavaThing[] values = arr.getValues();
456                 for (int v = 0; v < values.length; v++) {
457                     log(prefix + "  [" + v + "]: " + values[v]);
458                     if (values[v] instanceof JavaObject obj) {
459                         print(prefix + "    ", obj);
460                     }
461                 }
462             } else if (value instanceof JavaValueArray arr) {
463                 log(logPrefix + "(array of '" + (char)arr.getElementType() + "')" + ": " + arr.valueString());
464             } else {
465                 log(logPrefix + "(" + value.getClass() +  ")" + value.toString());
466             }
467         }
468     }
469 
470     private static boolean isTestClass(String className) {
471         return className.startsWith("TestClass");
472     }
473 
474     private static void log(String msg) {
475         System.out.println(msg);
476         System.out.flush();
477     }
478 
479 }