Skip to content

Commit 16b69a4

Browse files
eregonban-miolpaw
committed
[GR-67680] Clearer Native Image output for how much memory it uses
* Example outputs: 15.00GiB of memory (93.8% of system memory, using all available memory) 15.00GiB of memory (93.8% of system memory, using all available memory, user flags: '-Xms4g') 29.80GiB of memory (46.6% of system memory, capped at 32GB) 13.60GiB of memory (85% of system memory because $CI set to 'true') 13.60GiB of memory (85% of system memory because in container) 13.60GiB of memory (85% of system memory because less than 8GiB available) 9.00GiB of memory (56.3% of system memory, set via '-Xmx9g') 8.00GiB of memory (50.0% of system memory, set via '-XX:MaxRAMPercentage=25 -Xms5g -Xmx8g -Xms6g') Co-Authored-By: Betty Mann <[email protected]> Co-Authored-By: Paul Wögerer <[email protected]>
1 parent 2c57cba commit 16b69a4

File tree

4 files changed

+221
-76
lines changed

4 files changed

+221
-76
lines changed

docs/reference-manual/native-image/BuildOutput.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ GraalVM Native Image: Generating 'helloworld' (executable)...
3030
Garbage collector: Serial GC (max heap size: 80% of RAM)
3131
--------------------------------------------------------------------------------
3232
Build resources:
33-
- 13.24GB of memory (42.7% of system memory, using available memory)
33+
- 13.66GiB of memory (42.7% of system memory, using all available memory)
3434
- 16 thread(s) (100.0% of 16 available processor(s), determined at start)
3535
[2/8] Performing analysis... [****] (4.5s @ 0.54GB)
3636
3,158 types, 3,625 fields, and 14,804 methods found reachable
@@ -143,11 +143,13 @@ The memory limit and number of threads used by the build process.
143143

144144
More precisely, the memory limit of the Java heap, so actual memory consumption can be higher.
145145
Please check the [peak RSS](#glossary-peak-rss) reported at the end of the build to understand how much memory was actually used.
146-
By default, the build process uses the dedicated mode (up to 85% of system memory) in containers or CI environments (when the `$CI` environment variable is set to `true`), but never more than 32GB of memory.
147-
Otherwise, it tries to use available memory to avoid memory pressure on developer machines (shared mode).
146+
The actual memory consumption can also be lower than the limit set, as the GC only commits memory that it needs.
147+
By default, the build process uses the dedicated mode (which uses 85% of system memory) in containers or CI environments (when the `$CI` environment variable is set to `true`), but never more than 32GB of memory.
148+
Otherwise, it uses shared mode, which uses the available memory to avoid memory pressure on developer machines.
148149
If less than 8GB of memory are available, the build process falls back to the dedicated mode.
149150
Therefore, consider freeing up memory if your machine is slow during a build, for example, by closing applications that you do not need.
150151
It is possible to override the default behavior and set relative or absolute memory limits, for example with `-J-XX:MaxRAMPercentage=60.0` or `-J-Xmx16g`.
152+
`Xms` (for example, `-J-Xms9g`) can also be used to ensure a minimum for the limit, if you know the image needs at least that much memory to build.
151153

152154
By default, the build process uses all available processors to maximize speed, but not more than 32 threads.
153155
Use the `--parallelism` option to set the number of threads explicitly (for example, `--parallelism=4`).

substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/MemoryUtil.java

Lines changed: 176 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,29 @@
2929
import java.lang.management.ManagementFactory;
3030
import java.nio.file.Files;
3131
import java.nio.file.Paths;
32-
import java.util.ArrayList;
3332
import java.util.List;
3433
import java.util.function.Function;
34+
import java.util.function.Supplier;
3535
import java.util.regex.Matcher;
3636
import java.util.regex.Pattern;
37+
import java.util.stream.Stream;
3738

3839
import com.oracle.svm.core.OS;
3940
import com.oracle.svm.core.SubstrateOptions;
4041
import com.oracle.svm.core.SubstrateUtil;
42+
import com.oracle.svm.core.util.BasedOnJDKFile;
4143
import com.oracle.svm.core.util.ExitStatus;
44+
import com.oracle.svm.driver.NativeImage.HostFlags;
4245
import com.oracle.svm.driver.NativeImage.NativeImageError;
4346

4447
import jdk.jfr.internal.JVM;
48+
import org.graalvm.collections.Pair;
4549

46-
class MemoryUtil {
47-
private static final long KiB_TO_BYTES = 1024L;
48-
private static final long MiB_TO_BYTES = 1024L * KiB_TO_BYTES;
49-
private static final long GiB_TO_BYTES = 1024L * MiB_TO_BYTES;
50+
public final class MemoryUtil {
51+
public static final long KiB_TO_BYTES = 1024L;
52+
public static final long MiB_TO_BYTES = 1024L * KiB_TO_BYTES;
53+
public static final long GiB_TO_BYTES = 1024L * MiB_TO_BYTES;
54+
public static final long TiB_TO_BYTES = 1024L * GiB_TO_BYTES;
5055

5156
/* Builder needs at least 512MiB for building a helloworld in a reasonable amount of time. */
5257
private static final long MIN_HEAP_BYTES = 512L * MiB_TO_BYTES;
@@ -61,89 +66,201 @@ class MemoryUtil {
6166
* Builder uses at most 32GB to avoid disabling compressed oops (UseCompressedOops).
6267
* Deliberately use GB (not GiB) to stay well below 32GiB when relative maximum is calculated.
6368
*/
64-
private static final long MAX_HEAP_BYTES = 32_000_000_000L;
69+
public static final long MAX_HEAP_BYTES = 32_000_000_000L;
6570

66-
public static List<String> determineMemoryFlags(NativeImage.HostFlags hostFlags) {
67-
List<String> flags = new ArrayList<>();
68-
if (hostFlags.hasUseParallelGC()) {
69-
// native image generation is a throughput-oriented task
70-
flags.add("-XX:+UseParallelGC");
71-
}
71+
public static List<String> heuristicMemoryFlags(HostFlags hostFlags, List<String> memoryFlags) {
7272
/*
7373
* Use MaxRAMPercentage to allow users to overwrite max heap setting with
74-
* -XX:MaxRAMPercentage or -Xmx, and freely adjust the min heap with
75-
* -XX:InitialRAMPercentage or -Xms.
74+
* -XX:MaxRAMPercentage or -Xmx (though determineMemoryUsageFlags will detect that case and
75+
* not add any flag), and freely adjust the min heap with -XX:InitialRAMPercentage or -Xms.
7676
*/
7777
if (hostFlags.hasMaxRAMPercentage()) {
78-
flags.addAll(determineMemoryUsageFlags(value -> "-XX:MaxRAMPercentage=" + value));
78+
return determineMemoryUsageFlags(memoryFlags, value -> "-XX:MaxRAMPercentage=" + value);
7979
} else if (hostFlags.hasMaximumHeapSizePercent()) {
80-
flags.addAll(determineMemoryUsageFlags(value -> "-XX:MaximumHeapSizePercent=" + value.intValue()));
81-
}
82-
if (hostFlags.hasGCTimeRatio()) {
83-
/*
84-
* Optimize for throughput by increasing the goal of the total time for garbage
85-
* collection from 1% to 10% (N=9). This also reduces peak RSS.
86-
*/
87-
flags.add("-XX:GCTimeRatio=9"); // 1/(1+N) time for GC
88-
}
89-
if (hostFlags.hasExitOnOutOfMemoryError()) {
90-
/*
91-
* Let builder exit on first OutOfMemoryError to provide for shorter feedback loops.
92-
*/
93-
flags.add("-XX:+ExitOnOutOfMemoryError");
80+
return determineMemoryUsageFlags(memoryFlags, value -> "-XX:MaximumHeapSizePercent=" + value.intValue());
81+
} else {
82+
throw new Error("Neither -XX:MaxRAMPercentage= nor -XX:MaximumHeapSizePercent= are available");
9483
}
95-
return flags;
9684
}
9785

86+
// A String in the memory reason to indicate that user memory flags overrode the heuristic
87+
private static final String SET_VIA = ", set via '";
88+
9889
/**
9990
* Returns memory usage flags for the build process. Dedicated mode uses a fixed percentage of
10091
* total memory and is the default in containers. Shared mode tries to use available memory to
10192
* reduce memory pressure on the host machine. Note that this method uses OperatingSystemMXBean,
10293
* which is container-aware.
10394
*/
104-
private static List<String> determineMemoryUsageFlags(Function<Double, String> toMemoryFlag) {
95+
private static List<String> determineMemoryUsageFlags(List<String> memoryFlags, Function<Double, String> toMemoryFlag) {
10596
var osBean = (com.sun.management.OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
106-
final double totalMemorySize = osBean.getTotalMemorySize();
107-
final double dedicatedMemorySize = totalMemorySize * DEDICATED_MODE_TOTAL_MEMORY_RATIO;
108-
109-
String memoryUsageReason = "unknown";
110-
final boolean isDedicatedMemoryUsage;
111-
if (SubstrateUtil.isCISetToTrue()) {
112-
isDedicatedMemoryUsage = true;
113-
memoryUsageReason = "$CI set to 'true'";
114-
} else if (isContainerized()) {
115-
isDedicatedMemoryUsage = true;
116-
memoryUsageReason = "in container";
97+
final long totalMemorySize = osBean.getTotalMemorySize();
98+
99+
var maxMemoryAndUsageText = maxMemoryHeuristic(totalMemorySize, SubstrateUtil.isCISetToTrue(), isContainerized(), MemoryUtil::getAvailableMemorySize, memoryFlags);
100+
long maxMemory = maxMemoryAndUsageText.getLeft();
101+
String memoryUsageText = maxMemoryAndUsageText.getRight();
102+
String memoryUsageReason = "-D" + SubstrateOptions.BUILD_MEMORY_USAGE_REASON_TEXT_PROPERTY + "=" + memoryUsageText;
103+
104+
if (memoryUsageText.contains(SET_VIA)) {
105+
return List.of(memoryUsageReason);
117106
} else {
118-
isDedicatedMemoryUsage = false;
107+
double maxRamPercentage = ((double) maxMemory) / totalMemorySize * 100.0;
108+
String memoryFlag = toMemoryFlag.apply(maxRamPercentage);
109+
return List.of(memoryFlag, memoryUsageReason);
119110
}
111+
}
112+
113+
/**
114+
* Returns the max memory (decided by the heuristic or by the user memory flags) in bytes and
115+
* the reason.
116+
*/
117+
public static Pair<Long, String> maxMemoryHeuristic(long totalMemorySize, boolean isCISetToTrue, boolean isContainerized, Supplier<Long> getAvailableMemorySize, List<String> memoryFlags) {
118+
final long dedicatedMemorySize = (long) (totalMemorySize * DEDICATED_MODE_TOTAL_MEMORY_RATIO);
120119

121-
double reasonableMaxMemorySize;
122-
if (isDedicatedMemoryUsage) {
123-
reasonableMaxMemorySize = dedicatedMemorySize;
120+
long maxMemory;
121+
String reason;
122+
if (isCISetToTrue) {
123+
reason = "85% of system memory because $CI set to 'true'";
124+
maxMemory = dedicatedMemorySize;
125+
} else if (isContainerized) {
126+
reason = "85% of system memory because in container";
127+
maxMemory = dedicatedMemorySize;
124128
} else {
125-
reasonableMaxMemorySize = getAvailableMemorySize();
126-
if (reasonableMaxMemorySize >= MIN_AVAILABLE_MEMORY_THRESHOLD_GB * GiB_TO_BYTES) {
127-
memoryUsageReason = "using available memory";
129+
long availableMemorySize = getAvailableMemorySize.get();
130+
if (availableMemorySize >= MIN_AVAILABLE_MEMORY_THRESHOLD_GB * GiB_TO_BYTES) {
131+
reason = percentageOfSystemMemoryText(availableMemorySize, totalMemorySize) + ", using all available memory";
132+
maxMemory = availableMemorySize;
128133
} else { // fall back to dedicated mode
129-
memoryUsageReason = "less than " + MIN_AVAILABLE_MEMORY_THRESHOLD_GB + "GB of memory available";
130-
reasonableMaxMemorySize = dedicatedMemorySize;
134+
reason = "85%% of system memory because less than %dGiB available".formatted(MIN_AVAILABLE_MEMORY_THRESHOLD_GB);
135+
maxMemory = dedicatedMemorySize;
131136
}
132137
}
133138

134-
if (reasonableMaxMemorySize < MIN_HEAP_BYTES) {
139+
if (maxMemory < MIN_HEAP_BYTES) {
135140
throw new NativeImageError(
136141
"There is not enough memory available on the system (got %sMiB, need at least %sMiB). Consider freeing up memory if builds are slow, for example, by closing applications that you do not need."
137-
.formatted(reasonableMaxMemorySize / MiB_TO_BYTES, MIN_HEAP_BYTES / MiB_TO_BYTES),
142+
.formatted(maxMemory / MiB_TO_BYTES, MIN_HEAP_BYTES / MiB_TO_BYTES),
138143
null, ExitStatus.OUT_OF_MEMORY.getValue());
139144
}
140145

141-
/* Ensure max memory size does not exceed upper limit. */
142-
reasonableMaxMemorySize = Math.min(reasonableMaxMemorySize, MAX_HEAP_BYTES);
146+
// Ensure max memory size does not exceed upper limit
147+
if (maxMemory > MAX_HEAP_BYTES) {
148+
maxMemory = MAX_HEAP_BYTES;
149+
reason = percentageOfSystemMemoryText(maxMemory, totalMemorySize) + ", capped at 32GB";
150+
}
151+
152+
// Handle memory flags
153+
if (!memoryFlags.isEmpty()) {
154+
long newMaxMemory = determineMaxHeapBasedOnMemoryFlags(memoryFlags, maxMemory, totalMemorySize);
155+
if (newMaxMemory > 0) {
156+
reason = percentageOfSystemMemoryText(newMaxMemory, totalMemorySize) + SET_VIA + String.join(" ", memoryFlags) + "'";
157+
maxMemory = newMaxMemory;
158+
} else {
159+
reason += ", user flags: '%s'".formatted(String.join(" ", memoryFlags));
160+
}
161+
}
162+
163+
double maxMemoryGiB = (double) maxMemory / GiB_TO_BYTES;
164+
String memoryUsageText = "%.2fGiB of memory (%s)".formatted(maxMemoryGiB, reason);
165+
166+
return Pair.create(maxMemory, memoryUsageText);
167+
}
168+
169+
static boolean isMemoryFlag(String flag) {
170+
return Stream.of("-Xmx", "-Xms", "-XX:MaxRAMPercentage=", "-XX:MaximumHeapSizePercent=").anyMatch(flag::startsWith);
171+
}
172+
173+
@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+10/src/hotspot/share/runtime/arguments.cpp#L1530-L1532")
174+
private static long determineMaxHeapBasedOnMemoryFlags(List<String> memoryFlags, long heuristicMaxMemory, long totalMemory) {
175+
// Priority: Xmx, MaxRAMPercentage, MaximumHeapSizePercent
176+
var xmx = getMaxMemoryFlagValue("-Xmx", memoryFlags, totalMemory);
177+
var maxRAMPercentage = getMaxMemoryFlagValue("-XX:MaxRAMPercentage=", memoryFlags, totalMemory);
178+
var maximumHeapSizePercent = getMaxMemoryFlagValue("-XX:MaximumHeapSizePercent=", memoryFlags, totalMemory);
179+
var xms = getMaxMemoryFlagValue("-Xms", memoryFlags, totalMemory);
180+
long newMaxMemory = 0;
181+
if (xmx > 0) {
182+
newMaxMemory = xmx;
183+
} else if (maxRAMPercentage > 0) {
184+
newMaxMemory = maxRAMPercentage;
185+
} else if (maximumHeapSizePercent > 0) {
186+
newMaxMemory = maximumHeapSizePercent;
187+
}
188+
189+
if (newMaxMemory == 0 ? xms > heuristicMaxMemory : xms > newMaxMemory) {
190+
// Xms only affects max memory if the value is higher than the current max memory value
191+
newMaxMemory = xms;
192+
}
193+
return newMaxMemory;
194+
}
195+
196+
private static String percentageOfSystemMemoryText(long maxMemory, long totalMemory) {
197+
return "%.1f%% of system memory".formatted(toPercentage(maxMemory, totalMemory));
198+
}
199+
200+
private static long getMaxMemoryFlagValue(String prefix, List<String> memoryFlags, long totalMemory) {
201+
long max = 0;
202+
for (String flag : memoryFlags) {
203+
if (flag.startsWith(prefix)) {
204+
long value = parseMemoryFlagValue(flag, totalMemory);
205+
if (value > max) {
206+
max = value;
207+
}
208+
}
209+
}
210+
return max;
211+
}
212+
213+
@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-26+10/src/hotspot/share/utilities/parseInteger.hpp#L105-L160")
214+
public static long parseMemoryFlagValue(String flag, long totalMemory) {
215+
if (flag.startsWith("-Xmx") || flag.startsWith("-Xms")) {
216+
String valuePart = flag.substring(4);
217+
if (valuePart.isEmpty()) {
218+
throw new Error("Invalid value for: " + flag);
219+
}
220+
char unit = valuePart.charAt(valuePart.length() - 1);
221+
long multiplier = switch (unit) {
222+
case 'T', 't' -> TiB_TO_BYTES;
223+
case 'G', 'g' -> GiB_TO_BYTES;
224+
case 'M', 'm' -> MiB_TO_BYTES;
225+
case 'K', 'k' -> KiB_TO_BYTES;
226+
default -> 1;
227+
};
228+
if (multiplier != 1) {
229+
valuePart = valuePart.substring(0, valuePart.length() - 1);
230+
}
231+
long value = parseLongOrFlagError(flag, valuePart);
232+
return value * multiplier;
233+
} else if (flag.startsWith("-XX:MaxRAMPercentage=")) {
234+
String valuePart = flag.substring("-XX:MaxRAMPercentage=".length());
235+
double value = parseDoubleOrFlagError(flag, valuePart);
236+
return (long) (value / 100.0 * totalMemory);
237+
} else if (flag.startsWith("-XX:MaximumHeapSizePercent=")) {
238+
String valuePart = flag.substring("-XX:MaximumHeapSizePercent=".length());
239+
double value = parseLongOrFlagError(flag, valuePart);
240+
return (long) (value / 100.0 * totalMemory);
241+
} else {
242+
throw new Error("Unknown flag: " + flag);
243+
}
244+
}
245+
246+
private static long parseLongOrFlagError(String flag, String valuePart) {
247+
try {
248+
return Long.parseLong(valuePart);
249+
} catch (NumberFormatException e) {
250+
throw new Error("Invalid value for: " + flag);
251+
}
252+
}
253+
254+
private static double parseDoubleOrFlagError(String flag, String valuePart) {
255+
try {
256+
return Double.parseDouble(valuePart);
257+
} catch (NumberFormatException e) {
258+
throw new Error("Invalid value for: " + flag);
259+
}
260+
}
143261

144-
double reasonableMaxRamPercentage = reasonableMaxMemorySize / totalMemorySize * 100;
145-
return List.of(toMemoryFlag.apply(reasonableMaxRamPercentage),
146-
"-D" + SubstrateOptions.BUILD_MEMORY_USAGE_REASON_TEXT_PROPERTY + "=" + memoryUsageReason);
262+
private static double toPercentage(long part, long total) {
263+
return part / (double) total * 100;
147264
}
148265

149266
private static boolean isContainerized() {
@@ -153,7 +270,7 @@ private static boolean isContainerized() {
153270
return JVM.isContainerized();
154271
}
155272

156-
private static double getAvailableMemorySize() {
273+
private static long getAvailableMemorySize() {
157274
return switch (OS.getCurrent()) {
158275
case LINUX -> getAvailableMemorySizeLinux();
159276
case DARWIN -> getAvailableMemorySizeDarwin();

substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/NativeImage.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,10 @@ private static <T> String oR(OptionKey<T> option) {
339339
BundleSupport bundleSupport;
340340
private final ArchiveSupport archiveSupport;
341341

342+
/**
343+
* When running the Native Image Driver on Espresso with SVM, the available VM flags differ from
344+
* those on HotSpot. This accounts for that.
345+
*/
342346
public record HostFlags(
343347
boolean useJVMCINativeLibrary,
344348
boolean hasUseJVMCICompiler,
@@ -347,6 +351,28 @@ public record HostFlags(
347351
boolean hasExitOnOutOfMemoryError,
348352
boolean hasMaximumHeapSizePercent,
349353
boolean hasUseParallelGC) {
354+
355+
public List<String> defaultMemoryFlags() {
356+
List<String> flags = new ArrayList<>();
357+
if (hasUseParallelGC) {
358+
// native image generation is a throughput-oriented task
359+
flags.add("-XX:+UseParallelGC");
360+
}
361+
if (hasGCTimeRatio) {
362+
/*
363+
* Optimize for throughput by increasing the goal of the total time for garbage
364+
* collection from 1% to 10% (N=9). This also reduces peak RSS.
365+
*/
366+
flags.add("-XX:GCTimeRatio=9"); // 1/(1+N) time for GC
367+
}
368+
if (hasExitOnOutOfMemoryError) {
369+
/*
370+
* Let the builder exit on first OutOfMemoryError to have shorter feedback loops.
371+
*/
372+
flags.add("-XX:+ExitOnOutOfMemoryError");
373+
}
374+
return flags;
375+
}
350376
}
351377

352378
protected static class BuildConfiguration {
@@ -996,7 +1022,7 @@ static void ensureDirectoryExists(Path dir) {
9961022

9971023
private void prepareImageBuildArgs() {
9981024
addImageBuilderJavaArgs("-Xss10m");
999-
addImageBuilderJavaArgs(MemoryUtil.determineMemoryFlags(config.getHostFlags()));
1025+
addImageBuilderJavaArgs(config.getHostFlags().defaultMemoryFlags());
10001026

10011027
/* Prevent JVM that runs the image builder to steal focus. */
10021028
addImageBuilderJavaArgs("-Djava.awt.headless=true");
@@ -1273,6 +1299,17 @@ private int completeImageBuild() {
12731299

12741300
addImageBuilderJavaArgs(customJavaArgs.toArray(new String[0]));
12751301

1302+
List<String> userMemoryFlags = new ArrayList<>();
1303+
for (String arg : imageBuilderJavaArgs) {
1304+
if (MemoryUtil.isMemoryFlag(arg)) {
1305+
userMemoryFlags.add(arg);
1306+
}
1307+
}
1308+
List<String> memoryFlagsToAdd = MemoryUtil.heuristicMemoryFlags(config.getHostFlags(), userMemoryFlags);
1309+
for (String memoryFlag : memoryFlagsToAdd.reversed()) {
1310+
imageBuilderJavaArgs.addFirst(memoryFlag);
1311+
}
1312+
12761313
/* Perform option consolidation of imageBuilderArgs */
12771314

12781315
imageBuilderJavaArgs.addAll(getAgentArguments());

0 commit comments

Comments
 (0)