Fix available memory extraction on Linux (#26764) (#26938)
authoragoretsky <agoretsky15@gmail.com>
Mon, 14 Oct 2019 17:03:12 +0000 (20:03 +0300)
committerWilliam Godbe <wigodbe@microsoft.com>
Mon, 14 Oct 2019 17:03:12 +0000 (10:03 -0700)
* Fix available memory extraction on Linux

The GlobalMemoryStatusEx in PAL is returning number of free physical pages in
the ullAvailPhys member. But there are additional pages that are allocated
as buffers and caches that get released when there is a memory pressure and
thus they are effectively available too.

This change extracts the available memory on Linux from the /proc/meminfo
MemAvailable row, which is reported by the kernel as the most precise
amount of available memory.

src/gc/unix/config.h.in
src/gc/unix/configure.cmake
src/gc/unix/gcenv.unix.cpp
src/pal/src/misc/sysinfo.cpp

index ae33c02..699135c 100644 (file)
 #cmakedefine01 HAVE_PTHREAD_GETAFFINITY_NP
 #cmakedefine01 HAVE_PTHREAD_NP_H
 #cmakedefine01 HAVE_CPUSET_T
+#cmakedefine01 HAVE__SC_AVPHYS_PAGES
+#cmakedefine01 HAVE__SC_PHYS_PAGES
+#cmakedefine01 HAVE_SYSCONF
+#cmakedefine01 HAVE_SYSCTL
+#cmakedefine01 HAVE_SYSINFO
+#cmakedefine01 HAVE_SYSINFO_WITH_MEM_UNIT
+#cmakedefine01 HAVE_XSW_USAGE
+#cmakedefine01 HAVE_XSWDEV
+
 
 #endif // __CONFIG_H__
index 859ffa4..dafb0cd 100644 (file)
@@ -95,4 +95,34 @@ endif()
 
 check_library_exists(${PTHREAD_LIBRARY} pthread_getaffinity_np "" HAVE_PTHREAD_GETAFFINITY_NP)
 
+check_cxx_symbol_exists(_SC_PHYS_PAGES unistd.h HAVE__SC_PHYS_PAGES)
+check_cxx_symbol_exists(_SC_AVPHYS_PAGES unistd.h HAVE__SC_AVPHYS_PAGES)
+check_function_exists(sysctl HAVE_SYSCTL)
+check_function_exists(sysinfo HAVE_SYSINFO)
+check_function_exists(sysconf HAVE_SYSCONF)
+check_struct_has_member ("struct sysinfo" mem_unit "sys/sysinfo.h" HAVE_SYSINFO_WITH_MEM_UNIT)
+
+check_cxx_source_compiles("
+#include <sys/param.h>
+#include <sys/sysctl.h>
+#include <vm/vm_param.h>
+
+int main(int argc, char **argv)
+{
+    struct xswdev xsw;
+
+    return 0;
+}" HAVE_XSWDEV)
+
+check_cxx_source_compiles("
+#include <sys/param.h>
+#include <sys/sysctl.h>
+
+int main(int argc, char **argv)
+{
+    struct xsw_usage xsu;
+
+    return 0;
+}" HAVE_XSW_USAGE)
+
 configure_file(${CMAKE_CURRENT_LIST_DIR}/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h)
index 4a48a4a..52dd673 100644 (file)
@@ -2,9 +2,13 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 // See the LICENSE file in the project root for more information.
 
+#define _WITH_GETLINE
 #include <cstdint>
 #include <cstddef>
+#include <cstdio>
 #include <cassert>
+#define __STDC_FORMAT_MACROS
+#include <cinttypes>
 #include <memory>
 #include <pthread.h>
 #include <signal.h>
  #error "sys/mman.h required by GC PAL"
 #endif // HAVE_SYS_MMAN_H
 
+#if HAVE_SYSINFO
+#include <sys/sysinfo.h>
+#endif
+
+#if HAVE_XSWDEV
+#include <vm/vm_param.h>
+#endif // HAVE_XSWDEV
+
+#ifdef __APPLE__
+#include <mach/vm_types.h>
+#include <mach/vm_param.h>
+#include <mach/mach_port.h>
+#include <mach/mach_host.h>
+#endif // __APPLE__
+
+#if HAVE_SYSCTL
+#include <sys/sysctl.h>
+#endif
+
 #ifdef __linux__
 #include <sys/syscall.h> // __NR_membarrier
 // Ensure __NR_membarrier is defined for portable builds.
@@ -63,6 +86,16 @@ typedef cpuset_t cpu_set_t;
 #include "globals.h"
 #include "cgroup.h"
 
+#ifndef __APPLE__
+#if HAVE_SYSCONF && HAVE__SC_AVPHYS_PAGES
+#define SYSCONF_PAGES _SC_AVPHYS_PAGES
+#elif HAVE_SYSCONF && HAVE__SC_PHYS_PAGES
+#define SYSCONF_PAGES _SC_PHYS_PAGES
+#else
+#error Dont know how to get page-size on this architecture!
+#endif
+#endif // __APPLE__
+
 #if HAVE_NUMA_H
 
 #include <numa.h>
@@ -678,6 +711,58 @@ bool GCToOSInterface::GetWriteWatch(bool resetState, void* address, size_t size,
     return false;
 }
 
+// Get memory size multiplier based on the passed in units (k = kilo, m = mega, g = giga)
+static uint64_t GetMemorySizeMultiplier(char units)
+{
+    switch(units)
+    {
+        case 'g':
+        case 'G': return 1024 * 1024 * 1024;
+        case 'm':
+        case 'M': return 1024 * 1024;
+        case 'k':
+        case 'K': return 1024;
+    }
+
+    // No units multiplier
+    return 1;
+}
+
+#ifndef __APPLE__
+// Try to read the MemAvailable entry from /proc/meminfo.
+// Return true if the /proc/meminfo existed, the entry was present and we were able to parse it.
+static bool ReadMemAvailable(uint64_t* memAvailable)
+{
+    bool foundMemAvailable = false;
+    FILE* memInfoFile = fopen("/proc/meminfo", "r");
+    if (memInfoFile != NULL)
+    {
+        char *line = nullptr;
+        size_t lineLen = 0;
+
+        while (getline(&line, &lineLen, memInfoFile) != -1)
+        {
+            char units = '\0';
+            uint64_t available;
+            int fieldsParsed = sscanf(line, "MemAvailable: %" SCNu64 " %cB", &available, &units);
+
+            if (fieldsParsed >= 1)
+            {
+                uint64_t multiplier = GetMemorySizeMultiplier(units);
+                *memAvailable = available * multiplier;
+                foundMemAvailable = true;
+                break;
+            }
+        }
+
+        free(line);
+        fclose(memInfoFile);
+    }
+
+    return foundMemAvailable;
+}
+#endif // __APPLE__
+
 // Get size of the largest cache on the processor die
 // Parameters:
 //  trueSize - true to return true cache size, false to return scaled up size based on
@@ -799,6 +884,8 @@ uint64_t GCToOSInterface::GetPhysicalMemoryLimit(bool* is_restricted)
         return restricted_limit;
     }
 
+    // Get the physical memory size
+#if HAVE_SYSCONF && HAVE__SC_PHYS_PAGES
     long pages = sysconf(_SC_PHYS_PAGES);
     if (pages == -1) 
     {
@@ -812,6 +899,124 @@ uint64_t GCToOSInterface::GetPhysicalMemoryLimit(bool* is_restricted)
     }
 
     return pages * pageSize;
+#elif HAVE_SYSCTL
+    int mib[3];
+    mib[0] = CTL_HW;
+    mib[1] = HW_MEMSIZE;
+    int64_t physical_memory = 0;
+    size_t length = sizeof(INT64);
+    int rc = sysctl(mib, 2, &physical_memory, &length, NULL, 0);
+    assert(rc == 0);
+
+    return physical_memory;
+#else // HAVE_SYSCTL
+#error "Don't know how to get total physical memory on this platform"
+#endif // HAVE_SYSCTL
+}
+
+// Get amount of physical memory available for use in the system
+uint64_t GetAvailablePhysicalMemory()
+{
+    uint64_t available = 0;
+
+    // Get the physical memory available.
+#ifndef __APPLE__
+    static volatile bool tryReadMemInfo = true;
+
+    if (tryReadMemInfo)
+    {
+        // Ensure that we don't try to read the /proc/meminfo in successive calls to the GlobalMemoryStatusEx
+        // if we have failed to access the file or the file didn't contain the MemAvailable value.
+        tryReadMemInfo = ReadMemAvailable(&available);
+    }
+
+    if (!tryReadMemInfo)
+    {
+        // The /proc/meminfo doesn't exist or it doesn't contain the MemAvailable row or the format of the row is invalid
+        // Fall back to getting the available pages using sysconf.
+        available = sysconf(SYSCONF_PAGES) * sysconf(_SC_PAGE_SIZE);
+    }
+#else // __APPLE__
+    vm_size_t page_size;
+    mach_port_t mach_port;
+    mach_msg_type_number_t count;
+    vm_statistics_data_t vm_stats;
+    mach_port = mach_host_self();
+    count = sizeof(vm_stats) / sizeof(natural_t);
+    if (KERN_SUCCESS == host_page_size(mach_port, &page_size))
+    {
+        if (KERN_SUCCESS == host_statistics(mach_port, HOST_VM_INFO, (host_info_t)&vm_stats, &count))
+        {
+            available = (int64_t)vm_stats.free_count * (int64_t)page_size;
+        }
+    }
+    mach_port_deallocate(mach_task_self(), mach_port);
+#endif // __APPLE__
+
+    return available;
+}
+
+// Get the amount of available swap space
+uint64_t GetAvailablePageFile()
+{
+    uint64_t available = 0;
+
+    int mib[3];
+    int rc;
+
+    // Get swap file size
+#if HAVE_XSW_USAGE
+    // This is available on OSX
+    struct xsw_usage xsu;
+    mib[0] = CTL_VM;
+    mib[1] = VM_SWAPUSAGE;
+    size_t length = sizeof(xsu);
+    rc = sysctl(mib, 2, &xsu, &length, NULL, 0);
+    if (rc == 0)
+    {
+        available = xsu.xsu_avail;
+    }
+#elif HAVE_XSWDEV
+    // E.g. FreeBSD
+    struct xswdev xsw;
+
+    size_t length = 2;
+    rc = sysctlnametomib("vm.swap_info", mib, &length);
+    if (rc == 0)
+    {
+        int pagesize = getpagesize();
+        // Aggregate the information for all swap files on the system
+        for (mib[2] = 0; ; mib[2]++)
+        {
+            length = sizeof(xsw);
+            rc = sysctl(mib, 3, &xsw, &length, NULL, 0);
+            if ((rc < 0) || (xsw.xsw_version != XSWDEV_VERSION))
+            {
+                // All the swap files were processed or coreclr was built against
+                // a version of headers not compatible with the current XSWDEV_VERSION.
+                break;
+            }
+
+            uint64_t avail = xsw.xsw_nblks - xsw.xsw_used;
+            available += avail * pagesize;
+        }
+    }
+#elif HAVE_SYSINFO
+    // Linux
+    struct sysinfo info;
+    rc = sysinfo(&info);
+    if (rc == 0)
+    {
+        available = info.freeswap;
+#if HAVE_SYSINFO_WITH_MEM_UNIT
+        // A newer version of the sysinfo structure represents all the sizes
+        // in mem_unit instead of bytes
+        available *= info.mem_unit;
+#endif // HAVE_SYSINFO_WITH_MEM_UNIT
+    }
+#endif // HAVE_SYSINFO
+
+    return available;
 }
 
 // Get memory status
@@ -822,30 +1027,49 @@ uint64_t GCToOSInterface::GetPhysicalMemoryLimit(bool* is_restricted)
 //  available_page_file - The maximum amount of memory the current process can commit, in bytes.
 void GCToOSInterface::GetMemoryStatus(uint32_t* memory_load, uint64_t* available_physical, uint64_t* available_page_file)
 {
+    uint64_t available = 0;
+    uint32_t load = 0;
+
     if (memory_load != nullptr || available_physical != nullptr)
     {
-        uint64_t total = GetPhysicalMemoryLimit();
-
-        uint64_t available = 0;
-        uint32_t load = 0;
         size_t used;
-
-        // Get the physical memory in use - from it, we can get the physical memory available.
-        // We do this only when we have the total physical memory available.
-        if (total > 0 && GetPhysicalMemoryUsed(&used))
+        bool isRestricted;
+        uint64_t total = GetPhysicalMemoryLimit(&isRestricted);
+        if (isRestricted)
         {
-            available = total > used ? total-used : 0; 
-            load = (uint32_t)(((float)used * 100) / (float)total);
+            // Get the physical memory in use - from it, we can get the physical memory available.
+            // We do this only when we have the total physical memory available.
+            if (GetPhysicalMemoryUsed(&used))
+            {
+                available = total > used ? total-used : 0; 
+                load = (uint32_t)(((float)used * 100) / (float)total);
+            }
         }
+        else
+        {
+            available = GetAvailablePhysicalMemory();
 
-        if (memory_load != nullptr)
-            *memory_load = load;
-        if (available_physical != nullptr)
-            *available_physical = available;
+            if (memory_load != NULL)
+            {
+                uint32_t load = 0;
+                if (total > available)
+                {
+                    used = total - available;
+                    load = (uint32_t)(((float)used * 100) / (float)total);
+                }
+            }
+        }
     }
 
+    if (available_physical != NULL)
+        *available_physical = available;
+
+    if (memory_load != nullptr)
+        *memory_load = load;
+
     if (available_page_file != nullptr)
-        *available_page_file = 0;
+        *available_page_file = GetAvailablePageFile();
+
 }
 
 // Get a high precision performance counter
index e3d001b..52e8ec0 100644 (file)
@@ -25,6 +25,8 @@ Revision History:
 #include <sched.h>
 #include <errno.h>
 #include <unistd.h>
+#define __STDC_FORMAT_MACROS
+#include <inttypes.h>
 #include <sys/types.h>
 #if HAVE_SYSCTL
 #include <sys/sysctl.h>
@@ -239,6 +241,58 @@ GetSystemInfo(
     PERF_EXIT(GetSystemInfo);
 }
 
+// Get memory size multiplier based on the passed in units (k = kilo, m = mega, g = giga)
+static uint64_t GetMemorySizeMultiplier(char units)
+{
+    switch(units)
+    {
+        case 'g':
+        case 'G': return 1024 * 1024 * 1024;
+        case 'm':
+        case 'M': return 1024 * 1024;
+        case 'k':
+        case 'K': return 1024;
+    }
+
+    // No units multiplier
+    return 1;
+}
+
+#ifndef __APPLE__
+// Try to read the MemAvailable entry from /proc/meminfo.
+// Return true if the /proc/meminfo existed, the entry was present and we were able to parse it.
+static bool ReadMemAvailable(uint64_t* memAvailable)
+{
+    bool foundMemAvailable = false;
+    FILE* memInfoFile = fopen("/proc/meminfo", "r");
+    if (memInfoFile != NULL)
+    {
+        char *line = nullptr;
+        size_t lineLen = 0;
+
+        while (getline(&line, &lineLen, memInfoFile) != -1)
+        {
+            char units = '\0';
+            uint64_t available;
+            int fieldsParsed = sscanf(line, "MemAvailable: %" SCNu64 " %cB", &available, &units);
+
+            if (fieldsParsed >= 1)
+            {
+                uint64_t multiplier = GetMemorySizeMultiplier(units);
+                *memAvailable = available * multiplier;
+                foundMemAvailable = true;
+                break;
+            }
+        }
+
+        free(line);
+        fclose(memInfoFile);
+    }
+
+    return foundMemAvailable;
+}
+#endif // __APPLE__
+
 /*++
 Function:
   GlobalMemoryStatusEx
@@ -365,7 +419,22 @@ GlobalMemoryStatusEx(
     if (lpBuffer->ullTotalPhys > 0)
     {
 #ifndef __APPLE__
-        lpBuffer->ullAvailPhys = sysconf(SYSCONF_PAGES) * sysconf(_SC_PAGE_SIZE);
+        static volatile bool tryReadMemInfo = true;
+
+        if (tryReadMemInfo)
+        {
+            // Ensure that we don't try to read the /proc/meminfo in successive calls to the GlobalMemoryStatusEx
+            // if we have failed to access the file or the file didn't contain the MemAvailable value.
+            tryReadMemInfo = ReadMemAvailable(&lpBuffer->ullAvailPhys);
+        }
+
+        if (!tryReadMemInfo)
+        {
+            // The /proc/meminfo doesn't exist or it doesn't contain the MemAvailable row or the format of the row is invalid
+            // Fall back to getting the available pages using sysconf.
+            lpBuffer->ullAvailPhys = sysconf(SYSCONF_PAGES) * sysconf(_SC_PAGE_SIZE);
+        }
+
         INT64 used_memory = lpBuffer->ullTotalPhys - lpBuffer->ullAvailPhys;
         lpBuffer->dwMemoryLoad = (DWORD)((used_memory * 100) / lpBuffer->ullTotalPhys);
 #else
@@ -445,17 +514,7 @@ ReadMemoryValueFromFile(const char* filename, size_t* val)
     if (errno != 0)
         goto done;
 
-    multiplier = 1;
-    switch(*endptr)
-    {
-        case 'g':
-        case 'G': multiplier = 1024;
-        case 'm':
-        case 'M': multiplier = multiplier*1024;
-        case 'k':
-        case 'K': multiplier = multiplier*1024;
-    }
-
+    multiplier = GetMemorySizeMultiplier(*endptr);
     *val = num * multiplier;
     result = true;
     if (*val/multiplier != num)