24
24
*/
25
25
package com .oracle .svm .driver ;
26
26
27
+ import java .io .BufferedReader ;
28
+ import java .io .InputStreamReader ;
27
29
import java .lang .management .ManagementFactory ;
30
+ import java .nio .file .Files ;
31
+ import java .nio .file .Paths ;
28
32
import java .util .ArrayList ;
29
33
import java .util .List ;
34
+ import java .util .function .Function ;
35
+ import java .util .regex .Matcher ;
36
+ import java .util .regex .Pattern ;
30
37
38
+ import com .oracle .svm .core .OS ;
31
39
import com .oracle .svm .core .util .ExitStatus ;
32
40
import com .oracle .svm .driver .NativeImage .NativeImageError ;
41
+ import com .oracle .svm .hosted .NativeImageOptions ;
33
42
34
43
class MemoryUtil {
35
44
private static final long KiB_TO_BYTES = 1024L ;
@@ -39,17 +48,22 @@ class MemoryUtil {
39
48
/* Builder needs at least 512MiB for building a helloworld in a reasonable amount of time. */
40
49
private static final long MIN_HEAP_BYTES = 512L * MiB_TO_BYTES ;
41
50
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. */
44
52
private static final double DEDICATED_MODE_TOTAL_MEMORY_RATIO = 0.85D ;
45
53
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
+
46
57
/*
47
58
* Builder uses at most 32GB to avoid disabling compressed oops (UseCompressedOops).
48
59
* Deliberately use GB (not GiB) to stay well below 32GiB when relative maximum is calculated.
49
60
*/
50
61
private static final long MAX_HEAP_BYTES = 32_000_000_000L ;
51
62
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 ) {
53
67
List <String > flags = new ArrayList <>();
54
68
if (hostFlags .hasUseParallelGC ()) {
55
69
// native image generation is a throughput-oriented task
@@ -61,9 +75,9 @@ public static List<String> determineMemoryFlags(NativeImage.HostFlags hostFlags)
61
75
* -XX:InitialRAMPercentage or -Xms.
62
76
*/
63
77
if (hostFlags .hasMaxRAMPercentage ()) {
64
- flags .add ( "-XX:MaxRAMPercentage=" + determineReasonableMaxRAMPercentage ( ));
78
+ flags .addAll ( determineReasonableMaxRAMPercentage ( resourceUsageValue , value -> "-XX:MaxRAMPercentage=" + value ));
65
79
} else if (hostFlags .hasMaximumHeapSizePercent ()) {
66
- flags .add ( "-XX:MaximumHeapSizePercent=" + ( int ) determineReasonableMaxRAMPercentage ( ));
80
+ flags .addAll ( determineReasonableMaxRAMPercentage ( resourceUsageValue , value -> "-XX:MaximumHeapSizePercent=" + value . intValue () ));
67
81
}
68
82
if (hostFlags .hasGCTimeRatio ()) {
69
83
/*
@@ -82,23 +96,46 @@ public static List<String> determineMemoryFlags(NativeImage.HostFlags hostFlags)
82
96
}
83
97
84
98
/**
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.
88
104
*/
89
- private static double determineReasonableMaxRAMPercentage () {
105
+ private static List < String > determineReasonableMaxRAMPercentage (String resourceUsageValue , Function < Double , String > toMemoryFlag ) {
90
106
var osBean = (com .sun .management .OperatingSystemMXBean ) ManagementFactory .getOperatingSystemMXBean ();
91
107
final double totalMemorySize = osBean .getTotalMemorySize ();
92
- double reasonableMaxMemorySize = osBean . getFreeMemorySize () ;
108
+ final double dedicatedMemorySize = totalMemorySize * DEDICATED_MODE_TOTAL_MEMORY_RATIO ;
93
109
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" ;
102
139
}
103
140
104
141
if (reasonableMaxMemorySize < MIN_HEAP_BYTES ) {
@@ -111,6 +148,139 @@ private static double determineReasonableMaxRAMPercentage() {
111
148
/* Ensure max memory size does not exceed upper limit. */
112
149
reasonableMaxMemorySize = Math .min (reasonableMaxMemorySize , MAX_HEAP_BYTES );
113
150
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 ;
115
285
}
116
286
}
0 commit comments