1 /*
  2  * Copyright (c) 2023 SAP SE. All rights reserved.
  3  * Copyright (c) 2023, 2024, Red Hat, Inc. All rights reserved.
  4  * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
  5  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  6  *
  7  * This code is free software; you can redistribute it and/or modify it
  8  * under the terms of the GNU General Public License version 2 only, as
  9  * published by the Free Software Foundation.
 10  *
 11  * This code is distributed in the hope that it will be useful, but WITHOUT
 12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 14  * version 2 for more details (a copy is included in the LICENSE file that
 15  * accompanied this code).
 16  *
 17  * You should have received a copy of the GNU General Public License version
 18  * 2 along with this work; if not, write to the Free Software Foundation,
 19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 20  *
 21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 22  * or visit www.oracle.com if you need additional information or have any
 23  * questions.
 24  *
 25  */
 26 
 27 /*
 28  * @test id=trimNative
 29  * @requires (os.family=="linux") & !vm.musl
 30  * @modules java.base/jdk.internal.misc
 31  * @library /test/lib
 32  * @build jdk.test.whitebox.WhiteBox
 33  * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
 34  * @run driver TestTrimNative trimNative
 35  */
 36 
 37 /*
 38  * @test id=trimNativeStrict
 39  * @requires (os.family=="linux") & !vm.musl
 40  * @modules java.base/jdk.internal.misc
 41  * @library /test/lib
 42  * @build jdk.test.whitebox.WhiteBox
 43  * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
 44  * @run main/manual TestTrimNative trimNativeStrict
 45  */
 46 
 47 /*
 48  * @test id=trimNativeHighInterval
 49  * @summary High interval trimming should not even kick in for short program runtimes
 50  * @requires (os.family=="linux") & !vm.musl
 51  * @modules java.base/jdk.internal.misc
 52  * @library /test/lib
 53  * @build jdk.test.whitebox.WhiteBox
 54  * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
 55  * @run driver TestTrimNative trimNativeHighInterval
 56  */
 57 
 58 /*
 59  * @test id=trimNativeLowInterval
 60  * @summary Very low (sub-second) interval, nothing should explode
 61  * @requires (os.family=="linux") & !vm.musl
 62  * @modules java.base/jdk.internal.misc
 63  * @library /test/lib
 64  * @build jdk.test.whitebox.WhiteBox
 65  * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
 66  * @run driver TestTrimNative trimNativeLowInterval
 67  */
 68 
 69 /*
 70  * @test id=trimNativeLowIntervalStrict
 71  * @summary Very low (sub-second) interval, nothing should explode (stricter test, manual mode)
 72  * @requires (os.family=="linux") & !vm.musl
 73  * @modules java.base/jdk.internal.misc
 74  * @library /test/lib
 75  * @build jdk.test.whitebox.WhiteBox
 76  * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
 77  * @run main/manual TestTrimNative trimNativeLowIntervalStrict
 78  */
 79 
 80 /*
 81  * @test id=testOffByDefault
 82  * @summary Test that trimming is disabled by default
 83  * @requires (os.family=="linux") & !vm.musl
 84  * @modules java.base/jdk.internal.misc
 85  * @library /test/lib
 86  * @build jdk.test.whitebox.WhiteBox
 87  * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
 88  * @run driver TestTrimNative testOffByDefault
 89  */
 90 
 91 /*
 92  * @test id=testOffExplicit
 93  * @summary Test that trimming can be disabled explicitly
 94  * @requires (os.family=="linux") & !vm.musl
 95  * @modules java.base/jdk.internal.misc
 96  * @library /test/lib
 97  * @build jdk.test.whitebox.WhiteBox
 98  * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
 99  * @run driver TestTrimNative testOffExplicit
100  */
101 
102 /*
103  * @test id=testOffOnNonCompliantPlatforms
104  * @summary Test that trimming is correctly reported as unavailable if unavailable
105  * @requires (os.family!="linux") | vm.musl
106  * @modules java.base/jdk.internal.misc
107  * @library /test/lib
108  * @build jdk.test.whitebox.WhiteBox
109  * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
110  * @run driver TestTrimNative testOffOnNonCompliantPlatforms
111  */
112 
113 import jdk.test.lib.Platform;
114 import jdk.test.lib.process.OutputAnalyzer;
115 import jdk.test.lib.process.ProcessTools;
116 
117 import java.io.IOException;
118 import java.util.*;
119 import java.util.regex.Matcher;
120 import java.util.regex.Pattern;
121 
122 import jdk.test.whitebox.WhiteBox;
123 
124 public class TestTrimNative {
125 
126     // Actual RSS increase is a lot larger than 4 MB. Depends on glibc overhead, and NMT malloc headers in debug VMs.
127     // We need small-grained allocations to make sure they actually increase RSS (all touched) and to see the
128     // glibc-retaining-memory effect.
129     static final int szAllocations = 128;
130     static final int totalAllocationsSize = 128 * 1024 * 1024; // 128 MB total
131     static final int numAllocations = totalAllocationsSize / szAllocations;
132 
133     static long[] ptrs = new long[numAllocations];
134 
135     enum Unit {
136         B(1), K(1024), M(1024*1024), G(1024*1024*1024);
137         public final long size;
138         Unit(long size) { this.size = size; }
139     }
140 
141     private static String[] prepareOptions(String[] extraVMOptions, String[] programOptions) {
142         List<String> allOptions = new ArrayList<String>();
143         if (extraVMOptions != null) {
144             allOptions.addAll(Arrays.asList(extraVMOptions));
145         }
146         allOptions.add("-Xmx128m");
147         allOptions.add("-Xms128m"); // Stabilize RSS
148         allOptions.add("-XX:+AlwaysPreTouch"); // Stabilize RSS
149         allOptions.add("-XX:+UnlockDiagnosticVMOptions"); // For whitebox
150         allOptions.add("-XX:+WhiteBoxAPI");
151         allOptions.add("-Xbootclasspath/a:.");
152         allOptions.add("-XX:-ExplicitGCInvokesConcurrent"); // Invoke explicit GC on System.gc
153         allOptions.add("-Xlog:trimnative=debug");
154         allOptions.add("--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED");
155         if (programOptions != null) {
156             allOptions.addAll(Arrays.asList(programOptions));
157         }
158         return allOptions.toArray(new String[0]);
159     }
160 
161     private static OutputAnalyzer runTestWithOptions(String[] extraOptions, String[] programOptions) throws IOException {
162         ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder(prepareOptions(extraOptions, programOptions));
163         OutputAnalyzer output = new OutputAnalyzer(pb.start());
164         output.shouldHaveExitValue(0);
165         return output;
166     }
167 
168     private static void checkExpectedLogMessages(OutputAnalyzer output, boolean expectEnabled,
169                                                  int expectedInterval) {
170         if (expectEnabled) {
171             output.shouldContain("Periodic native trim enabled (interval: " + expectedInterval + " ms");
172             output.shouldContain("Native heap trimmer start");
173         } else {
174             output.shouldNotContain("Periodic native trim enabled");
175         }
176     }
177 
178     /**
179      * Given JVM output, look for one or more log lines that describes a successful negative trim. The total amount
180      * of trims should be matching about what the test program allocated.
181      * @param output
182      * @param minTrimsExpected min number of periodic trim lines expected in UL log
183      * @param maxTrimsExpected min number of periodic trim lines expected in UL log
184      * @param strict: if true, expect RSS to go down; if false, just look for trims without looking at RSS.
185      */
186     private static void parseOutputAndLookForNegativeTrim(OutputAnalyzer output, int minTrimsExpected,
187                                                           int maxTrimsExpected, boolean strict) {
188         output.reportDiagnosticSummary();
189         List<String> lines = output.asLines();
190         Pattern pat = Pattern.compile(".*\\[trimnative\\] Periodic Trim \\(\\d+\\): (\\d+)([BKMG])->(\\d+)([BKMG]).*");
191         int numTrimsFound = 0;
192         long rssReductionTotal = 0;
193         for (String line : lines) {
194             Matcher mat = pat.matcher(line);
195             if (mat.matches()) {
196                 long rss1 = Long.parseLong(mat.group(1)) * Unit.valueOf(mat.group(2)).size;
197                 long rss2 = Long.parseLong(mat.group(3)) * Unit.valueOf(mat.group(4)).size;
198                 if (rss1 > rss2) {
199                     rssReductionTotal += (rss1 - rss2);
200                 }
201                 numTrimsFound ++;
202             }
203             if (numTrimsFound > maxTrimsExpected) {
204                 throw new RuntimeException("Abnormal high number of periodic trim attempts found (more than " + maxTrimsExpected +
205                         "). Does the interval setting not work?");
206             }
207         }
208         if (numTrimsFound < minTrimsExpected) {
209             throw new RuntimeException("We found fewer (periodic) trim lines in UL log than expected (expected at least " + minTrimsExpected +
210                     ", found " + numTrimsFound + ").");
211         }
212         System.out.println("Found " + numTrimsFound + " trims. Ok.");
213         if (strict && maxTrimsExpected > 0) {
214             // This is very fuzzy. Test program malloced X bytes, then freed them again and trimmed. But the log line prints change in RSS.
215             // Which, of course, is influenced by a lot of other factors. But we expect to see *some* reasonable reduction in RSS
216             // due to trimming.
217             float fudge = 0.5f;
218             // On ppc, we see a vastly diminished return (~3M reduction instead of ~200), I suspect because of the underlying
219             // 64k pages lead to a different geometry. Manual tests with larger reclaim sizes show that autotrim works. For
220             // this test, we just reduce the fudge factor.
221             if (Platform.isPPC()) { // le and be both
222                 fudge = 0.01f;
223             }
224             long expectedMinimalReduction = (long) (totalAllocationsSize * fudge);
225             if (rssReductionTotal < expectedMinimalReduction) {
226                 throw new RuntimeException("We did not see the expected RSS reduction in the UL log. Expected (with fudge)" +
227                         " to see at least a combined reduction of " + expectedMinimalReduction + ".");
228             } else {
229                 System.out.println("Found high enough RSS reduction from trims: " + rssReductionTotal);
230             }
231         }
232     }
233 
234     static class Tester {
235         public static void main(String[] args) throws Exception {
236             long sleeptime = Long.parseLong(args[0]);
237 
238             System.out.println("Will spike now...");
239             WhiteBox wb = WhiteBox.getWhiteBox();
240             for (int i = 0; i < numAllocations; i++) {
241                 ptrs[i] = wb.NMTMalloc(szAllocations);
242                 wb.preTouchMemory(ptrs[i], szAllocations);
243             }
244             for (int i = 0; i < numAllocations; i++) {
245                 wb.NMTFree(ptrs[i]);
246             }
247             System.out.println("Done spiking.");
248 
249             System.out.println("GC...");
250             System.gc();
251 
252             // give GC time to react
253             System.out.println("Sleeping for " + sleeptime + " ms...");
254             Thread.sleep(sleeptime);
255             System.out.println("Done.");
256         }
257     }
258 
259     public static void main(String[] args) throws Exception {
260 
261         if (args.length == 0) {
262             throw new RuntimeException("Argument error");
263         }
264 
265         boolean strictTesting = args[0].endsWith("Strict");
266 
267         switch (args[0]) {
268             case "trimNative":
269             case "trimNativeStrict": {
270                 long trimInterval = 500; // twice per second
271                 long ms1 = System.currentTimeMillis();
272                 OutputAnalyzer output = runTestWithOptions(
273                         new String[] { "-XX:TrimNativeHeapInterval=" + trimInterval },
274                         new String[] { TestTrimNative.Tester.class.getName(), "5000" }
275                 );
276                 long ms2 = System.currentTimeMillis();
277                 long runtime_ms = ms2 - ms1;
278 
279                 checkExpectedLogMessages(output, true, 500);
280 
281                 long maxTrimsExpected = runtime_ms / trimInterval;
282                 long minTrimsExpected = maxTrimsExpected / 2;
283                 parseOutputAndLookForNegativeTrim(output, (int) minTrimsExpected, (int) maxTrimsExpected, strictTesting);
284             } break;
285 
286             case "trimNativeHighInterval": {
287                 OutputAnalyzer output = runTestWithOptions(
288                         new String[] { "-XX:TrimNativeHeapInterval=" + Integer.MAX_VALUE },
289                         new String[] { TestTrimNative.Tester.class.getName(), "5000" }
290                 );
291                 checkExpectedLogMessages(output, true, Integer.MAX_VALUE);
292                 // We should not see any trims since the interval would prevent them
293                 parseOutputAndLookForNegativeTrim(output, 0, 0, strictTesting);
294             } break;
295 
296             case "trimNativeLowInterval":
297             case "trimNativeLowIntervalStrict": {
298                 long ms1 = System.currentTimeMillis();
299                 OutputAnalyzer output = runTestWithOptions(
300                         new String[] { "-XX:TrimNativeHeapInterval=1" },
301                         new String[] { TestTrimNative.Tester.class.getName(), "0" }
302                 );
303                 long ms2 = System.currentTimeMillis();
304                 int maxTrimsExpected = (int)(ms2 - ms1); // 1ms trim interval
305                 checkExpectedLogMessages(output, true, 1);
306                 parseOutputAndLookForNegativeTrim(output, 1, (int)maxTrimsExpected, strictTesting);
307             } break;
308 
309             case "testOffOnNonCompliantPlatforms": {
310                 OutputAnalyzer output = runTestWithOptions(
311                         new String[] { "-XX:TrimNativeHeapInterval=1" },
312                         new String[] { "-version" }
313                 );
314                 checkExpectedLogMessages(output, false, 0);
315                 parseOutputAndLookForNegativeTrim(output, 0, 0, strictTesting);
316                 // The following output is expected to be printed with warning level, so it should not need -Xlog
317                 output.shouldContain("[warning][trimnative] Native heap trim is not supported on this platform");
318             } break;
319 
320             case "testOffExplicit": {
321                 OutputAnalyzer output = runTestWithOptions(
322                         new String[] { "-XX:TrimNativeHeapInterval=0" },
323                         new String[] { "-version" }
324                 );
325                 checkExpectedLogMessages(output, false, 0);
326                 parseOutputAndLookForNegativeTrim(output, 0, 0, strictTesting);
327             } break;
328 
329             case "testOffByDefault": {
330                 OutputAnalyzer output = runTestWithOptions(null, new String[] { "-version" } );
331                 checkExpectedLogMessages(output, false, 0);
332                 parseOutputAndLookForNegativeTrim(output, 0, 0, strictTesting);
333             } break;
334 
335             default:
336                 throw new RuntimeException("Invalid test " + args[0]);
337 
338         }
339     }
340 }