Skip to content

Commit b397545

Browse files
committed
[GR-67680] Clearer Native Image output for how much memory it uses
PullRequest: graal/21487
2 parents 0ae2183 + dff3f64 commit b397545

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-
- 28.45GB of memory (42.5% of system memory, using available memory)
33+
- 14.69GiB of memory (47.0% of system memory, using all available memory)
3434
- 20 thread(s) (100.0% of 20 available processor(s), determined at start)
3535
[2/8] Performing analysis... [******] (3.4s @ 0.40GB)
3636
3,297 types, 3,733 fields, and 15,247 methods found reachable
@@ -145,11 +145,13 @@ The memory limit and number of threads used by the build process.
145145

146146
More precisely, the memory limit of the Java heap, so actual memory consumption can be higher.
147147
Please check the [peak RSS](#glossary-peak-rss) reported at the end of the build to understand how much memory was actually used.
148-
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.
149-
Otherwise, it tries to use available memory to avoid memory pressure on developer machines (shared mode).
148+
The actual memory consumption can also be lower than the limit set, as the GC only commits memory that it needs.
149+
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.
150+
Otherwise, it uses shared mode, which uses the available memory to avoid memory pressure on developer machines.
150151
If less than 8GB of memory are available, the build process falls back to the dedicated mode.
151152
Therefore, consider freeing up memory if your machine is slow during a build, for example, by closing applications that you do not need.
152153
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`.
154+
`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.
153155

154156
By default, the build process uses all available processors to maximize speed, but not more than 32 threads.
155157
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
@@ -340,6 +340,10 @@ private static <T> String oR(OptionKey<T> option) {
340340
BundleSupport bundleSupport;
341341
private final ArchiveSupport archiveSupport;
342342

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

353379
protected static class BuildConfiguration {
@@ -997,7 +1023,7 @@ static void ensureDirectoryExists(Path dir) {
9971023

9981024
private void prepareImageBuildArgs() {
9991025
addImageBuilderJavaArgs("-Xss10m");
1000-
addImageBuilderJavaArgs(MemoryUtil.determineMemoryFlags(config.getHostFlags()));
1026+
addImageBuilderJavaArgs(config.getHostFlags().defaultMemoryFlags());
10011027

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

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

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

12791316
imageBuilderJavaArgs.addAll(getAgentArguments());

0 commit comments

Comments
 (0)