Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/reference-manual/native-image/BuildOutput.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,10 @@ Increase the amount of available memory to reduce the time to build the native b
#### <a name="glossary-peak-rss"></a>Peak RSS
Peak [resident set size](https://en.wikipedia.org/wiki/Resident_set_size) as reported by the operating system.
This value indicates the maximum amount of memory consumed by the build process.
If the [GC statistics](#glossary-garbage-collection) do not show any problems, the amount of available memory of the system can be reduced to a value closer to the peak RSS.
By default, the process will only use available memory, so memory that the operating system can make available without having to swap out memory used by other processes.
Therefore, consider freeing up memory if builds are slow, for example, by closing applications that you do not need.
Note that, by default, the build process will also not use more than 32GB if available.
If the [GC statistics](#glossary-garbage-collection) do not show any problems, the amount of total memory of the system can be reduced to a value closer to the peak RSS to lower operational costs.

#### <a name="glossary-cpu-load"></a>CPU load
The CPU time used by the process divided by the total process time.
Expand Down
1 change: 1 addition & 0 deletions substratevm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This changelog summarizes major changes to GraalVM Native Image.

## Version 23.1.0
* (GR-35746) Lower the default aligned chunk size from 1 MB to 512 KB for the serial and epsilon GCs, reducing memory usage and image size in many cases.
* (GR-45673) Improve the memory footprint of the Native Image build process. The builder now takes available memory into account to reduce memory pressure when many other processes are running on the same machine. It also consumes less memory in many cases and is therefore also less likely to fail due to out-of-memory errors. At the same time, we have raised its memory limit from 14GB to 32GB.

## Version 23.0.0
* (GR-40187) Report invalid use of SVM specific classes on image class- or module-path as error. As a temporary workaround, `-H:+AllowDeprecatedBuilderClassesOnImageClasspath` allows turning the error into a warning.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ public enum OS {

DARWIN("Darwin", false),
LINUX("Linux", true),
SOLARIS("Solaris", true),
WINDOWS("Windows", false);

/**
Expand Down Expand Up @@ -61,9 +60,6 @@ private static OS findCurrent() {
if (name.equals("Linux")) {
return LINUX;
}
if (name.equals("SunOS")) {
return SOLARIS;
}
if (name.equals("Mac OS X") || name.equals("Darwin")) {
return DARWIN;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/*
* Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.oracle.svm.driver;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.management.ManagementFactory;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.oracle.svm.core.OS;
import com.oracle.svm.core.util.ExitStatus;
import com.oracle.svm.driver.NativeImage.NativeImageError;

class MemoryUtil {
private static final long KiB_TO_BYTES = 1024;
private static final long MiB_TO_BYTES = 1024 * KiB_TO_BYTES;

/* Builder needs at least 512MiB for building a helloworld in a reasonable amount of time. */
private static final long MIN_HEAP_BYTES = 512 * MiB_TO_BYTES;

/*
* Builder uses at most 32GB to avoid disabling compressed oops (UseCompressedOops).
* Deliberately use GB (not GiB) to stay well below 32GiB when relative maximum is calculated.
*/
private static final long MAX_HEAP_BYTES = 32_000_000_000L;

/* Use 80% of total system memory in case available memory cannot be determined. */
private static final double FALLBACK_MAX_RAM_PERCENTAGE = 80.0;

public static List<String> determineMemoryFlags() {
return List.of(
/*
* Use MaxRAMPercentage to allow users to overwrite max heap setting with
* -XX:MaxRAMPercentage or -Xmx, and freely adjust the min heap with
* -XX:InitialRAMPercentage or -Xms.
*/
"-XX:MaxRAMPercentage=" + determineReasonableMaxRAMPercentage(),
/*
* Optimize for throughput by increasing the goal of the total time for
* garbage collection from 1% to 5% (N=19). This also reduces peak RSS.
*/
"-XX:GCTimeRatio=19", // 1/(1+N) time for GC
/*
* Let builder exit on first OutOfMemoryError to provide for shorter
* feedback loops.
*/
"-XX:+ExitOnOutOfMemoryError");
}

/**
* Returns a percentage (0.0-100.0) to be used as a value for the -XX:MaxRAMPercentage flag of
* the builder process. Prefer available memory over total memory to reduce memory pressure on
* the host machine.
*/
private static double determineReasonableMaxRAMPercentage() {
var osBean = (com.sun.management.OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
long totalMemorySize = osBean.getTotalMemorySize();
long reasonableMaxMemorySize = -1;
reasonableMaxMemorySize = switch (OS.getCurrent()) {
case LINUX -> getAvailableMemorySizeLinux();
case DARWIN -> getAvailableMemorySizeDarwin();
case WINDOWS -> getAvailableMemorySizeWindows();
};
Comment on lines +85 to +89
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use osBean.getFreeMemorySize()?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Free memory can be much lower than available memory due to OS caches etc, especially on long running developer machines.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is from a cloud machine I ran for 20min:

MiB Mem :   7645.6 total,    134.0 free,   3298.8 used,   4212.8 buff/cache

Native Image wouldn't start if we set the max heap to 134MiB.

if (reasonableMaxMemorySize < 0 || reasonableMaxMemorySize > totalMemorySize) {
return FALLBACK_MAX_RAM_PERCENTAGE;
}
if (reasonableMaxMemorySize < MIN_HEAP_BYTES) {
throw new NativeImageError(
"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."
.formatted(reasonableMaxMemorySize / MiB_TO_BYTES, MIN_HEAP_BYTES / MiB_TO_BYTES),
null, ExitStatus.OUT_OF_MEMORY.getValue());
}
reasonableMaxMemorySize = Math.min(reasonableMaxMemorySize, MAX_HEAP_BYTES);
return (double) reasonableMaxMemorySize / totalMemorySize * 100;
}

/**
* Returns the total amount of available memory in bytes on Linux based on
* <code>/proc/meminfo</code>, otherwise <code>-1</code>.
*
* @see <a href=
* "https://github.com/torvalds/linux/blob/865fdb08197e657c59e74a35fa32362b12397f58/mm/page_alloc.c#L5137">page_alloc.c#L5137</a>
*/
private static long getAvailableMemorySizeLinux() {
try {
String memAvailableLine = Files.readAllLines(Paths.get("/proc/meminfo")).stream().filter(l -> l.startsWith("MemAvailable")).findFirst().orElse("");
Matcher m = Pattern.compile("^MemAvailable:\\s+(\\d+) kB").matcher(memAvailableLine);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this metric is not container aware. I.e. on large systems, say with 1TB of memory, and a container slice of say 50GB you'd still get > 32 Gib since 50/0.8 > 32. Using the OSBean could solve this problem.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point and yes, in that case reasonableMaxMemorySize > totalMemorySize is true, so this would return the 80% fallback which actually matches the current behavior (80% of total memory). OSBean takes cgroups into account but it unfortunately only provides access to free, not available memory. Do you have any other suggestions how we could handle such cases?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on this in #6581.

if (m.matches()) {
return Long.parseLong(m.group(1)) * KiB_TO_BYTES;
}
} catch (Exception e) {
}
return -1;
}

/**
* Returns the total amount of available memory in bytes on Darwin based on
* <code>vm_stat</code>, otherwise <code>-1</code>.
*
* @see <a href=
* "https://opensource.apple.com/source/system_cmds/system_cmds-496/vm_stat.tproj/vm_stat.c.auto.html">vm_stat.c</a>
*/
private static long getAvailableMemorySizeDarwin() {
try {
Process p = Runtime.getRuntime().exec(new String[]{"vm_stat"});
try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
String line1 = reader.readLine();
if (line1 == null) {
return -1;
}
Matcher m1 = Pattern.compile("^Mach Virtual Memory Statistics: \\(page size of (\\d+) bytes\\)").matcher(line1);
long pageSize = -1;
if (m1.matches()) {
pageSize = Long.parseLong(m1.group(1));
}
if (pageSize <= 0) {
return -1;
}
String line2 = reader.readLine();
Matcher m2 = Pattern.compile("^Pages free:\\s+(\\d+).").matcher(line2);
long freePages = -1;
if (m2.matches()) {
freePages = Long.parseLong(m2.group(1));
}
if (freePages <= 0) {
return -1;
}
String line3 = reader.readLine();
if (!line3.startsWith("Pages active")) {
return -1;
}
String line4 = reader.readLine();
Matcher m4 = Pattern.compile("^Pages inactive:\\s+(\\d+).").matcher(line4);
long inactivePages = -1;
if (m4.matches()) {
inactivePages = Long.parseLong(m4.group(1));
}
if (inactivePages <= 0) {
return -1;
}
assert freePages > 0 && inactivePages > 0 && pageSize > 0;
return (freePages + inactivePages) * pageSize;
} finally {
p.waitFor();
}
} catch (Exception e) {
}
return -1;
}

/**
* Returns the total amount of available memory in bytes on Windows based on <code>wmic</code>,
* otherwise <code>-1</code>.
*
* @see <a href=
* "https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-operatingsystem">Win32_OperatingSystem
* class</a>
*/
private static long getAvailableMemorySizeWindows() {
try {
Process p = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", "wmic", "OS", "get", "FreePhysicalMemory"});
try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
String line1 = reader.readLine();
if (line1 == null || !line1.startsWith("FreePhysicalMemory")) {
return -1;
}
String line2 = reader.readLine();
if (line2 == null) {
return -1;
}
String line3 = reader.readLine();
if (line3 == null) {
return -1;
}
Matcher m = Pattern.compile("^(\\d+)\\s+").matcher(line3);
if (m.matches()) {
return Long.parseLong(m.group(1)) * KiB_TO_BYTES;
}
}
p.waitFor();
} catch (Exception e) {
}
return -1;
}
}
Loading