diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/PhysicalMemory.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/PhysicalMemory.java index 2f234bcd8548..bbb12df9e4a4 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/PhysicalMemory.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/PhysicalMemory.java @@ -24,9 +24,17 @@ */ package com.oracle.svm.core.heap; +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.locks.ReentrantLock; import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; import org.graalvm.word.UnsignedWord; import org.graalvm.word.WordFactory; @@ -38,6 +46,7 @@ import com.oracle.svm.core.thread.VMOperation; import com.oracle.svm.core.util.UnsignedUtils; import com.oracle.svm.core.util.VMError; +import com.sun.management.OperatingSystemMXBean; /** * Contains static methods to get configuration of physical memory. @@ -50,6 +59,8 @@ public interface PhysicalMemorySupport { UnsignedWord size(); } + private static final long K = 1024; + private static final ReentrantLock LOCK = new ReentrantLock(); private static final UnsignedWord UNSET_SENTINEL = UnsignedUtils.MAX_VALUE; private static UnsignedWord cachedSize = UNSET_SENTINEL; @@ -107,6 +118,73 @@ public static UnsignedWord size() { return cachedSize; } + /** Returns the amount of used physical memory in bytes, or -1 if not supported. */ + public static long usedSize() { + // Windows, macOS, and containerized Linux use the OS bean. + if (Platform.includedIn(Platform.WINDOWS.class) || + Platform.includedIn(Platform.MACOS.class) || + (Containers.isContainerized() && Containers.memoryLimitInBytes() > 0)) { + OperatingSystemMXBean osBean = (com.sun.management.OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean(); + return osBean.getTotalMemorySize() - osBean.getFreeMemorySize(); + } + + // Non-containerized Linux uses /proc/meminfo. + if (Platform.includedIn(Platform.LINUX.class)) { + return getUsedSizeFromProcMemInfo(); + } + + return -1L; + } + + // Will be removed as part of GR-51479. + private static long getUsedSizeFromProcMemInfo() { + try { + List lines = readAllLines("/proc/meminfo"); + for (String line : lines) { + if (line.contains("MemAvailable")) { + return size().rawValue() - parseFirstNumber(line) * K; + } + } + } catch (Exception e) { + /* Nothing to do. */ + } + return -1L; + } + + private static List readAllLines(String fileName) throws IOException { + List lines = new ArrayList<>(); + try (BufferedReader bufferedReader = new BufferedReader(new FileReader(fileName, StandardCharsets.UTF_8))) { + String line; + while ((line = bufferedReader.readLine()) != null) { + lines.add(line); + } + } + return lines; + } + + /** Parses the first number in the String as a long value. */ + private static long parseFirstNumber(String str) { + int firstDigit = -1; + int lastDigit = -1; + + for (int i = 0; i < str.length(); i++) { + if (Character.isDigit(str.charAt(i))) { + if (firstDigit == -1) { + firstDigit = i; + } + lastDigit = i; + } else if (firstDigit != -1) { + break; + } + } + + if (firstDigit >= 0) { + String number = str.substring(firstDigit, lastDigit + 1); + return Long.parseLong(number); + } + return -1; + } + /** * Returns the size of physical memory in bytes that has been previously cached. This method * must not be called if {@link #isInitialized()} is still false. diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/EveryChunkNativePeriodicEvents.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/EveryChunkNativePeriodicEvents.java index db0a82d9c853..254c7ef2f873 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/EveryChunkNativePeriodicEvents.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/EveryChunkNativePeriodicEvents.java @@ -51,7 +51,7 @@ public class EveryChunkNativePeriodicEvents extends Event { public static void emit() { emitJavaThreadStats(); - emitPhysicalMemory(); + emitPhysicalMemory(PhysicalMemory.usedSize()); emitClassLoadingStatistics(); emitPerThreadEvents(); } @@ -75,7 +75,7 @@ private static void emitJavaThreadStats() { } @Uninterruptible(reason = "Accesses a JFR buffer.") - private static void emitPhysicalMemory() { + private static void emitPhysicalMemory(long usedSize) { if (JfrEvent.PhysicalMemory.shouldEmit()) { JfrNativeEventWriterData data = StackValue.get(JfrNativeEventWriterData.class); JfrNativeEventWriterDataAccess.initializeThreadLocalNativeBuffer(data); @@ -83,7 +83,7 @@ private static void emitPhysicalMemory() { JfrNativeEventWriter.beginSmallEvent(data, JfrEvent.PhysicalMemory); JfrNativeEventWriter.putLong(data, JfrTicks.elapsedTicks()); JfrNativeEventWriter.putLong(data, PhysicalMemory.getCachedSize().rawValue()); - JfrNativeEventWriter.putLong(data, 0); /* used size */ + JfrNativeEventWriter.putLong(data, usedSize); /* used size */ JfrNativeEventWriter.endSmallEvent(data); } } diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEveryChunkNativePeriodicEvents.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEveryChunkNativePeriodicEvents.java new file mode 100644 index 000000000000..4189c585fe47 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestEveryChunkNativePeriodicEvents.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2024, Red Hat Inc. 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.test.jfr; + +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.junit.Test; + +import com.oracle.svm.core.jfr.JfrEvent; + +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; + +/** + * Tests the VM-level JFR events that are created periodically upon every chunk. Note that the + * events ThreadCPULoad and ThreadAllocationStatistics are not tested here since they already have + * their own individual tests. + */ +public class TestEveryChunkNativePeriodicEvents extends JfrRecordingTest { + @Test + public void test() throws Throwable { + String[] events = new String[]{JfrEvent.JavaThreadStatistics.getName(), JfrEvent.PhysicalMemory.getName(), JfrEvent.ClassLoadingStatistics.getName()}; + Recording recording = startRecording(events); + stopRecording(recording, TestEveryChunkNativePeriodicEvents::validateEvents); + } + + private static void validateEvents(List events) { + boolean foundJavaThreadStatistics = false; + boolean foundPhysicalMemory = false; + boolean foundClassLoadingStatistics = false; + + for (RecordedEvent e : events) { + String eventName = e.getEventType().getName(); + if (eventName.equals(JfrEvent.JavaThreadStatistics.getName())) { + foundJavaThreadStatistics = true; + assertTrue(e.getLong("activeCount") > 1); + assertTrue(e.getLong("daemonCount") > 0); + assertTrue(e.getLong("accumulatedCount") > 1); + assertTrue(e.getLong("peakCount") > 1); + } else if (eventName.equals(JfrEvent.PhysicalMemory.getName())) { + foundPhysicalMemory = true; + assertTrue(e.getLong("totalSize") > 0); + assertTrue(e.getLong("usedSize") > 0); + } else if (eventName.equals(JfrEvent.ClassLoadingStatistics.getName())) { + foundClassLoadingStatistics = true; + assertTrue(e.getLong("loadedClassCount") > 0); + } + } + + assertTrue(foundJavaThreadStatistics); + assertTrue(foundPhysicalMemory); + assertTrue(foundClassLoadingStatistics); + } +}