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