Skip to content

Commit 1426f7a

Browse files
committed
Attempt to improve memory usage of builder.
1 parent 3e9f44b commit 1426f7a

File tree

4 files changed

+200
-21
lines changed

4 files changed

+200
-21
lines changed

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

+189-19
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,21 @@
2424
*/
2525
package com.oracle.svm.driver;
2626

27+
import java.io.BufferedReader;
28+
import java.io.InputStreamReader;
2729
import java.lang.management.ManagementFactory;
30+
import java.nio.file.Files;
31+
import java.nio.file.Paths;
2832
import java.util.ArrayList;
2933
import java.util.List;
34+
import java.util.function.Function;
35+
import java.util.regex.Matcher;
36+
import java.util.regex.Pattern;
3037

38+
import com.oracle.svm.core.OS;
3139
import com.oracle.svm.core.util.ExitStatus;
3240
import com.oracle.svm.driver.NativeImage.NativeImageError;
41+
import com.oracle.svm.hosted.NativeImageOptions;
3342

3443
class MemoryUtil {
3544
private static final long KiB_TO_BYTES = 1024L;
@@ -39,17 +48,22 @@ class MemoryUtil {
3948
/* Builder needs at least 512MiB for building a helloworld in a reasonable amount of time. */
4049
private static final long MIN_HEAP_BYTES = 512L * MiB_TO_BYTES;
4150

42-
/* If free memory is below 8GiB, use 85% of total system memory (e.g., 7GiB * 85% ~ 6GiB). */
43-
private static final long DEDICATED_MODE_THRESHOLD = 8L * GiB_TO_BYTES;
51+
/* Use 85% of total system memory (e.g., 7GiB * 85% ~ 6GiB) in dedicated mode. */
4452
private static final double DEDICATED_MODE_TOTAL_MEMORY_RATIO = 0.85D;
4553

54+
/* If available memory is below 8GiB, fall back to dedicated mode. */
55+
private static final long MIN_AVAILABLE_MEMORY_THRESHOLD = 8L * GiB_TO_BYTES;
56+
4657
/*
4758
* Builder uses at most 32GB to avoid disabling compressed oops (UseCompressedOops).
4859
* Deliberately use GB (not GiB) to stay well below 32GiB when relative maximum is calculated.
4960
*/
5061
private static final long MAX_HEAP_BYTES = 32_000_000_000L;
5162

52-
public static List<String> determineMemoryFlags(NativeImage.HostFlags hostFlags) {
63+
// Use another static field because NativeImageOptions are hosted only
64+
private static final String BUILDER_RESOURCE_USAGE_TEXT_PROPERTY = NativeImageOptions.BUILDER_RESOURCE_USAGE_TEXT_PROPERTY;
65+
66+
public static List<String> determineMemoryFlags(NativeImage.HostFlags hostFlags, String resourceUsageValue) {
5367
List<String> flags = new ArrayList<>();
5468
if (hostFlags.hasUseParallelGC()) {
5569
// native image generation is a throughput-oriented task
@@ -61,9 +75,9 @@ public static List<String> determineMemoryFlags(NativeImage.HostFlags hostFlags)
6175
* -XX:InitialRAMPercentage or -Xms.
6276
*/
6377
if (hostFlags.hasMaxRAMPercentage()) {
64-
flags.add("-XX:MaxRAMPercentage=" + determineReasonableMaxRAMPercentage());
78+
flags.addAll(determineReasonableMaxRAMPercentage(resourceUsageValue, value -> "-XX:MaxRAMPercentage=" + value));
6579
} else if (hostFlags.hasMaximumHeapSizePercent()) {
66-
flags.add("-XX:MaximumHeapSizePercent=" + (int) determineReasonableMaxRAMPercentage());
80+
flags.addAll(determineReasonableMaxRAMPercentage(resourceUsageValue, value -> "-XX:MaximumHeapSizePercent=" + value.intValue()));
6781
}
6882
if (hostFlags.hasGCTimeRatio()) {
6983
/*
@@ -82,23 +96,46 @@ public static List<String> determineMemoryFlags(NativeImage.HostFlags hostFlags)
8296
}
8397

8498
/**
85-
* Returns a percentage (0.0-100.0) to be used as a value for the -XX:MaxRAMPercentage flag of
86-
* the builder process. Prefer free memory over total memory to reduce memory pressure on the
87-
* host machine. Note that this method uses OperatingSystemMXBean, which is container-aware.
99+
* Returns a percentage (0.0-100.0) to be used as a value for the -XX:MaxRAMPercentage or
100+
* -XX:MaximumHeapSizePercent flags of the builder process. Dedicated mode uses a fixed
101+
* percentage of total memory and is the default in containers. Shared mode tries to use
102+
* available memory to reduce memory pressure on the host machine. Note that this method uses
103+
* OperatingSystemMXBean, which is container-aware.
88104
*/
89-
private static double determineReasonableMaxRAMPercentage() {
105+
private static List<String> determineReasonableMaxRAMPercentage(String resourceUsageValue, Function<Double, String> toMemoryFlag) {
90106
var osBean = (com.sun.management.OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
91107
final double totalMemorySize = osBean.getTotalMemorySize();
92-
double reasonableMaxMemorySize = osBean.getFreeMemorySize();
108+
final double dedicatedMemorySize = totalMemorySize * DEDICATED_MODE_TOTAL_MEMORY_RATIO;
93109

94-
if (reasonableMaxMemorySize < DEDICATED_MODE_THRESHOLD) {
95-
/*
96-
* When free memory is low, for example in memory-constrained environments or when a
97-
* good amount of memory is used for caching, use a fixed percentage of total memory
98-
* rather than free memory. In containerized environments, builds are expected to run
99-
* more or less exclusively (builder + driver + optional Gradle/Maven process).
100-
*/
101-
reasonableMaxMemorySize = totalMemorySize * DEDICATED_MODE_TOTAL_MEMORY_RATIO;
110+
String dedicatedModeReason;
111+
boolean isDedicatedResourceUsage;
112+
boolean isExplicitlySetByUser = resourceUsageValue != null;
113+
if (isExplicitlySetByUser) {
114+
isDedicatedResourceUsage = resourceUsageValue.equals("dedicated");
115+
dedicatedModeReason = "selected explicitly";
116+
} else {
117+
isDedicatedResourceUsage = isContainerized();
118+
dedicatedModeReason = isDedicatedResourceUsage ? "in container" : "not in container";
119+
}
120+
121+
double reasonableMaxMemorySize;
122+
if (isDedicatedResourceUsage) {
123+
reasonableMaxMemorySize = dedicatedMemorySize;
124+
} else {
125+
reasonableMaxMemorySize = getAvailableMemorySize();
126+
if (!isExplicitlySetByUser && reasonableMaxMemorySize < MIN_AVAILABLE_MEMORY_THRESHOLD) {
127+
// fall back to dedicated mode (unless explicitly set by user)
128+
isDedicatedResourceUsage = true;
129+
reasonableMaxMemorySize = dedicatedMemorySize;
130+
dedicatedModeReason = "insufficient available memory";
131+
}
132+
}
133+
134+
final String resourceUsageText;
135+
if (isDedicatedResourceUsage) {
136+
resourceUsageText = "dedicated mode: " + dedicatedModeReason;
137+
} else {
138+
resourceUsageText = "shared mode: sufficient available memory";
102139
}
103140

104141
if (reasonableMaxMemorySize < MIN_HEAP_BYTES) {
@@ -111,6 +148,139 @@ private static double determineReasonableMaxRAMPercentage() {
111148
/* Ensure max memory size does not exceed upper limit. */
112149
reasonableMaxMemorySize = Math.min(reasonableMaxMemorySize, MAX_HEAP_BYTES);
113150

114-
return reasonableMaxMemorySize / totalMemorySize * 100;
151+
double reasonableMaxRamPercentage = reasonableMaxMemorySize / totalMemorySize * 100;
152+
153+
return List.of(toMemoryFlag.apply(reasonableMaxRamPercentage), "-D" + BUILDER_RESOURCE_USAGE_TEXT_PROPERTY + "=" + resourceUsageText);
154+
}
155+
156+
private static boolean isContainerized() {
157+
if (!OS.LINUX.isCurrent()) {
158+
return false;
159+
}
160+
try {
161+
return Files.readAllLines(Paths.get("/proc/self/cgroup")).stream().anyMatch(
162+
s -> s.contains("docker") || s.contains("kubepods") || s.contains("containerd"));
163+
} catch (Exception e) {
164+
}
165+
return false;
166+
}
167+
168+
private static double getAvailableMemorySize() {
169+
return switch (OS.getCurrent()) {
170+
case LINUX -> getAvailableMemorySizeLinux();
171+
case DARWIN -> getAvailableMemorySizeDarwin();
172+
case WINDOWS -> getAvailableMemorySizeWindows();
173+
};
174+
}
175+
176+
/**
177+
* Returns the total amount of available memory in bytes on Linux based on
178+
* <code>/proc/meminfo</code>, otherwise <code>-1</code>. Note that this metric is not
179+
* container-aware (does not take cgroups into account) and may report available memory of the
180+
* host.
181+
*
182+
* @see <a href=
183+
* "https://github.com/torvalds/linux/blob/865fdb08197e657c59e74a35fa32362b12397f58/mm/page_alloc.c#L5137">page_alloc.c#L5137</a>
184+
*/
185+
private static long getAvailableMemorySizeLinux() {
186+
try {
187+
String memAvailableLine = Files.readAllLines(Paths.get("/proc/meminfo")).stream().filter(l -> l.startsWith("MemAvailable")).findFirst().orElse("");
188+
Matcher m = Pattern.compile("^MemAvailable:\\s+(\\d+) kB").matcher(memAvailableLine);
189+
if (m.matches()) {
190+
return Long.parseLong(m.group(1)) * KiB_TO_BYTES;
191+
}
192+
} catch (Exception e) {
193+
}
194+
return -1;
195+
}
196+
197+
/**
198+
* Returns the total amount of available memory in bytes on Darwin based on
199+
* <code>vm_stat</code>, otherwise <code>-1</code>.
200+
*
201+
* @see <a href=
202+
* "https://opensource.apple.com/source/system_cmds/system_cmds-496/vm_stat.tproj/vm_stat.c.auto.html">vm_stat.c</a>
203+
*/
204+
private static long getAvailableMemorySizeDarwin() {
205+
try {
206+
Process p = Runtime.getRuntime().exec(new String[]{"vm_stat"});
207+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
208+
String line1 = reader.readLine();
209+
if (line1 == null) {
210+
return -1;
211+
}
212+
Matcher m1 = Pattern.compile("^Mach Virtual Memory Statistics: \\(page size of (\\d+) bytes\\)").matcher(line1);
213+
long pageSize = -1;
214+
if (m1.matches()) {
215+
pageSize = Long.parseLong(m1.group(1));
216+
}
217+
if (pageSize <= 0) {
218+
return -1;
219+
}
220+
String line2 = reader.readLine();
221+
Matcher m2 = Pattern.compile("^Pages free:\\s+(\\d+).").matcher(line2);
222+
long freePages = -1;
223+
if (m2.matches()) {
224+
freePages = Long.parseLong(m2.group(1));
225+
}
226+
if (freePages <= 0) {
227+
return -1;
228+
}
229+
String line3 = reader.readLine();
230+
if (!line3.startsWith("Pages active")) {
231+
return -1;
232+
}
233+
String line4 = reader.readLine();
234+
Matcher m4 = Pattern.compile("^Pages inactive:\\s+(\\d+).").matcher(line4);
235+
long inactivePages = -1;
236+
if (m4.matches()) {
237+
inactivePages = Long.parseLong(m4.group(1));
238+
}
239+
if (inactivePages <= 0) {
240+
return -1;
241+
}
242+
assert freePages > 0 && inactivePages > 0 && pageSize > 0;
243+
return (freePages + inactivePages) * pageSize;
244+
} finally {
245+
p.waitFor();
246+
}
247+
} catch (Exception e) {
248+
}
249+
return -1;
250+
}
251+
252+
/**
253+
* Returns the total amount of available memory in bytes on Windows based on <code>wmic</code>,
254+
* otherwise <code>-1</code>.
255+
*
256+
* @see <a href=
257+
* "https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-operatingsystem">Win32_OperatingSystem
258+
* class</a>
259+
*/
260+
private static long getAvailableMemorySizeWindows() {
261+
try {
262+
Process p = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", "wmic", "OS", "get", "FreePhysicalMemory"});
263+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
264+
String line1 = reader.readLine();
265+
if (line1 == null || !line1.startsWith("FreePhysicalMemory")) {
266+
return -1;
267+
}
268+
String line2 = reader.readLine();
269+
if (line2 == null) {
270+
return -1;
271+
}
272+
String line3 = reader.readLine();
273+
if (line3 == null) {
274+
return -1;
275+
}
276+
Matcher m = Pattern.compile("^(\\d+)\\s+").matcher(line3);
277+
if (m.matches()) {
278+
return Long.parseLong(m.group(1)) * KiB_TO_BYTES;
279+
}
280+
}
281+
p.waitFor();
282+
} catch (Exception e) {
283+
}
284+
return -1;
115285
}
116286
}

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
import com.oracle.svm.driver.metainf.NativeImageMetaInfResourceProcessor;
9999
import com.oracle.svm.driver.metainf.NativeImageMetaInfWalker;
100100
import com.oracle.svm.hosted.NativeImageGeneratorRunner;
101+
import com.oracle.svm.hosted.NativeImageOptions;
101102
import com.oracle.svm.hosted.NativeImageSystemClassLoader;
102103
import com.oracle.svm.hosted.util.JDKArgsUtils;
103104
import com.oracle.svm.util.LogUtils;
@@ -983,7 +984,8 @@ static void ensureDirectoryExists(Path dir) {
983984

984985
private void prepareImageBuildArgs() {
985986
addImageBuilderJavaArgs("-Xss10m");
986-
addImageBuilderJavaArgs(MemoryUtil.determineMemoryFlags(config.getHostFlags()));
987+
String resourceUsageValue = getHostedOptionArgumentValue(imageBuilderArgs, oH(NativeImageOptions.BuilderResourceUsage));
988+
addImageBuilderJavaArgs(MemoryUtil.determineMemoryFlags(config.getHostFlags(), resourceUsageValue));
987989

988990
/* Prevent JVM that runs the image builder to steal focus. */
989991
addImageBuilderJavaArgs("-Djava.awt.headless=true");

substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageOptions.java

+7
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import jdk.graal.compiler.options.Option;
5353
import jdk.graal.compiler.options.OptionKey;
5454
import jdk.graal.compiler.options.OptionStability;
55+
import jdk.graal.compiler.options.OptionType;
5556
import jdk.graal.compiler.options.OptionValues;
5657
import jdk.graal.compiler.serviceprovider.GraalServices;
5758

@@ -185,6 +186,12 @@ public static CStandards getCStandard() {
185186
}
186187
}
187188

189+
@APIOption(name = "resource-usage", extra = true)//
190+
@Option(help = "TBD: Either 'shared' or 'dedicated'.", type = OptionType.User)//
191+
public static final HostedOptionKey<String> BuilderResourceUsage = new HostedOptionKey<>(null);
192+
193+
public static final String BUILDER_RESOURCE_USAGE_TEXT_PROPERTY = "svm.builder.resourceUsageText";
194+
188195
/**
189196
* Configures the number of threads of the common pool (see driver).
190197
*/

substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ProgressReporter.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ private void printResourceInfo() {
425425

426426
List<String> inputArguments = ManagementFactory.getRuntimeMXBean().getInputArguments();
427427
List<String> maxRAMPrecentageValues = inputArguments.stream().filter(arg -> arg.startsWith("-XX:MaxRAMPercentage")).toList();
428-
String maxHeapSuffix = "determined at start";
428+
String maxHeapSuffix = System.getProperty(NativeImageOptions.BUILDER_RESOURCE_USAGE_TEXT_PROPERTY, "unknown mode");
429429
if (maxRAMPrecentageValues.size() > 1) { // The driver sets this option once
430430
maxHeapSuffix = "set via '%s'".formatted(maxRAMPrecentageValues.get(maxRAMPrecentageValues.size() - 1));
431431
}

0 commit comments

Comments
 (0)