-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
GcMemoryInfo returns incorrect values in .NET 8 on macOS 14 Sonoma #94846
Comments
Tagging subscribers to this area: @dotnet/gc Issue DetailsDescriptionWe build a library that uses the ratio between GCMemoryInfo.MemoryLoadBytes/GCMemoryInfo.TotalAvailableMemoryBytes returned form the GC.GetGCMemoryInfo API to monitor memory consumption an compact a cache when consumption runs above thresholds. We now have a customer report that the cache compact is constantly triggered and it turn out it is because this ratio is seemingly always at 99% when running in .NET 8 on Mac OS 14. Running on .NET 7 on the same machine returns the actual value (corresponding to what is seen in system tools). The value is also correct on Windows and in Linux containers in .NET 8. Reproduction StepsNote: I don't actually have Mac to reproduce this on, so this is a bit hypothetical:
Expected behaviorThe output memory load ratio is roughly the same as can be seen in the OS tools. Actual behaviorThe output memory load is always basically 100% memory utilization, regardless of the actual memory pressure as can be seen in OS tools. Note: This only happens on Mac OS X 14, on Windows and Linux (container) the output is the expected. Regression?Yes, in .NET 7 the returned value is correct. Known WorkaroundsNo response Configuration.NET 8 RTM Other informationNo response
|
This issue is very easy to reproduce. Here is a var format = new System.Globalization.NumberFormatInfo()
{
NumberGroupSeparator = ","
};
GC.Collect();
var info = GC.GetGCMemoryInfo();
Console.WriteLine("Framework version: " + Environment.Version);
Console.WriteLine("MemoryLoadBytes: " + info.MemoryLoadBytes.ToString("N0", format));
Console.WriteLine("TotalAvailableMemoryBytes: " + info.TotalAvailableMemoryBytes.ToString("N0", format)); And a <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>memory_info_issue</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project> The output will of course depend on the machine where the code is run. Here is the output of
If
In both instances, the macOS Activity Monitor reported around 18 GB of “Memory Used” while the code was run. |
I can add the .NET 8.0 output of the issue reproduction code under a Linux virtual machine –
And under Windows 11 (Arm64) in a virtual machine – also with 4 GB of memory:
|
@janvorli mentioned to me that the difference between 7 and 8 is in 8 we are doing "available memory - free" whereas in 7 we are doing "active + inactive + wired". I'm wondering if the discrepancy comes from the compressed memory. would you mind showing the output of |
@Maoni0 thank you for your comment. Here is the output of the issue reproduction code and vm_stat (macOS Sonoma 14.2.1, M2, 24 GB RAM): % dotnet run
Framework version: 8.0.0
MemoryLoadBytes: 25,512,105,738
TotalAvailableMemoryBytes: 25,769,803,776
% vm_stat
Mach Virtual Memory Statistics: (page size of 16384 bytes)
Pages free: 19099.
Pages active: 374368.
Pages inactive: 373569.
Pages speculative: 174.
Pages throttled: 0.
Pages wired down: 179640.
Pages purgeable: 10066.
"Translation faults": 2766402827.
Pages copy-on-write: 174174584.
Pages zero filled: 1201795858.
Pages reactivated: 153602038.
Pages purged: 115606586.
File-backed pages: 227356.
Anonymous pages: 520755.
Pages stored in compressor: 2912073.
Pages occupied by compressor: 576560.
Decompressions: 188999306.
Compressions: 239359971.
Pageins: 101231079.
Pageouts: 1199101.
Swapins: 1934303.
Swapouts: 3082680. The Activity Monitor shows these figures: ![]() |
thanks so much @OttoG! yeah so it certainly looks like "Pages occupied by compressor" is part of the physical memory. @mrahl, is it possible that you could also run vm_stat and see if the compressed memory is the culprit (ie, it basically just takes up whatever that would otherwise be free memory. if so obviously we should not include it)? |
I found this discussion that has some information on the topic: https://apple.stackexchange.com/questions/423717/ It perhaps raises more questions than it answers, but it seems to suggest that the most useful value might actually be total installed memory multiplied by the memory pressure as reported by the It also gives a formula for “Memory Used” from Activity Monitor that unfortunately underestimates the number by about a gigabyte in my case. Here is the output of a simplified version of the script from the answer (only using values from % dotnet run
Framework version: 8.0.0
MemoryLoadBytes: 25,512,105,738
TotalAvailableMemoryBytes: 25,769,803,776
% vm_stat | awk -F '[:.]' '{ s[$1] = $2; } END {
gsub(/[^0-9]/, "", s["Mach Virtual Memory Statistics"]);
gb = s["Mach Virtual Memory Statistics"] / (1024 * 1024 * 1024);
print "Numbers from vm_stat in GB";
app_mem = s["Anonymous pages"] - s["Pages purgeable"];
print "App Memory", app_mem * gb;
print "Wired Memory", s["Pages wired down"] * gb;
print "Compressed", s["Pages occupied by compressor"] * gb;
print "Memory Used", (app_mem + s["Pages wired down"] + s["Pages occupied by compressor"]) * gb;
print "Cached Files", (s["File-backed pages"] + s["Pages purgeable"]) * gb;
print "Total", (app_mem + s["Pages wired down"] + s["Pages occupied by compressor"] + s["File-backed pages"] + s["Pages purgeable"] + s["Pages free"]) * gb;
}'
Numbers from vm_stat in GB
App Memory 9.03206
Wired Memory 2.8353
Compressed 5.54578
Memory Used 17.4131
Cached Files 5.59967
Total 23.2451 |
hmm, it actually reports 63% free? in your case based on the calculation from vm_stat, it's more like 63% in use (17.4 / 24 = 72.5%). I dunno how memory_pressure does its calculation. perhaps it breaks down app memory by active and inactive and only use the active part? |
Hi @Maoni0 it seems to be quite opaque how the memory pressure percentage is calculated. In the discussion I quoted earlier, it was simply stated that (emphasis and comments in brackets are mine): Memory Pressure is just a number which provides a simple way of indicating memory load. It can be calculated from At https://support.apple.com/en-gb/guide/activity-monitor/actmntr1004/mac it is simply stated very loosely by Apple that: Memory pressure is determined by the amount of free memory, swap rate, wired memory and file cached memory. Still, there seems to be wide agreement in various discussions online that it is a relevant number and that it is the best indicator of memory pressure that Apple has come up with. This is emphasized by the fact that the There are two easy ways of retrieving this number (as a memory free percentage, i.e., 100% minus the actual memory pressure percentage) from the command line: % memory_pressure | grep System-wide
# Sample output:
# System-wide memory free percentage: 68%
% sysctl kern.memorystatus_level
# Sample output:
# kern.memorystatus_level: 68 These results consistently match what I see in the memory pressure chart of the Activity Monitor as I launch or quit apps. I also wrote a simple C program ( #include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/sysctl.h>
/*
# Test of the kern.memorystatus_level system call -- the output is expected to match the output of the following two shell commands:
memory_pressure | grep System-wide
sysctl kern.memorystatus_level
# Build with the following command (Xcode command-line tools must be installed):
clang memory-pressure-test.c -o memory-pressure-test
# Run with:
./memory-pressure-test
*/
int main() {
const char* mem_free_name = "kern.memorystatus_level";
int i;
uint32_t mem_free = 0;
size_t mem_free_length = sizeof(uint32_t);
int* mib;
size_t mib_length = 0;
int64_t mem_size, mem_load_bytes;
size_t mem_size_length = sizeof(int64_t);
/*
Simple method -- use sysctlbyname() and supply the name in cleartext
*/
sysctlbyname(mem_free_name, &mem_free, &mem_free_length, NULL, 0);
printf("Memory free (%s) fetched using sysctlbyname(): %u%%\n", mem_free_name, mem_free);
/*
More efficient method if the value will be checked repeatedly -- three times faster according to the SYSCTL(3) man page:
-- Use sysctlnametomib() to check the length of the MIB (Management Information Base) array needed to specify the call
-- Allocate memory for the MIB array
-- Use sysctlnametomib() to look up and fetch the MIB array
-- Use sysctl() to fetch the memory free percentage
*/
mem_free = 0;
sysctlnametomib(mem_free_name, NULL, &mib_length);
mib = calloc(mib_length, sizeof(int));
sysctlnametomib(mem_free_name, mib, &mib_length);
sysctl(mib, mib_length, &mem_free, &mem_free_length, NULL, 0);
printf("Memory free (%s, MIB array ", mem_free_name);
for (i = 0; i < mib_length; i++) {
printf("%s%d", i ? ", " : "", mib[i]);
}
printf(") fetched using sysctlnametomib() and sysctl(): %u%%\n", mem_free);
/*
Output formats that are more relevant to the definition of GCMemoryInfo.MemoryLoadBytes in .NET 8
*/
printf("Memory pressure: %u%%\n", 100 - mem_free);
sysctlbyname("hw.memsize", &mem_size, &mem_size_length, NULL, 0);
mem_load_bytes = mem_size * (100 - mem_free) / 100;
printf("Memory load bytes: %lld (%.2lf GB)\n", mem_load_bytes, (double)mem_load_bytes / (1024 * 1024 * 1024));
return 0;
} Sample output of the program (again, on a machine with 24 GB RAM):
In my view, after now having done some research, the most useful definition of |
We were using the `vm_statistics_data_t::free_count` as the available memory reported to the GC. It turned out this value is only a small portion of the available memory and that the appropriate value should be based on the kern.memorystatus_level value obtained using sysctl. That value represents percentual amount of available memory, so multiplying it by the total memory bytes gets the available memory bytes. Close dotnet#94846
* Fix computing available memory on OSX for GC We were using the `vm_statistics_data_t::free_count` as the available memory reported to the GC. It turned out this value is only a small portion of the available memory and that the appropriate value should be based on the kern.memorystatus_level value obtained using sysctl. That value represents percentual amount of available memory, so multiplying it by the total memory bytes gets the available memory bytes. Close #94846 * Reflect PR feeback and move total memory computation to init
Description
We build a library that uses the ratio between GCMemoryInfo.MemoryLoadBytes/GCMemoryInfo.TotalAvailableMemoryBytes returned form the GC.GetGCMemoryInfo API to monitor memory consumption an compact a cache when consumption runs above thresholds. We now have a customer report that the cache compact is constantly triggered and it turn out it is because this ratio is seemingly always at 99% when running in .NET 8 on Mac OS 14. Running on .NET 7 on the same machine returns the actual value (corresponding to what is seen in system tools). The value is also correct on Windows and in Linux containers in .NET 8.
Reproduction Steps
Note: I don't actually have Mac to reproduce this on, so this is a bit hypothetical:
Expected behavior
The output memory load ratio is roughly the same as can be seen in the OS tools.
Actual behavior
The output memory load is always basically 100% memory utilization, regardless of the actual memory pressure as can be seen in OS tools.
Note: This only happens on Mac OS X 14, on Windows and Linux (container) the output is the expected.
Regression?
Yes, in .NET 7 the returned value is correct.
Known Workarounds
No response
Configuration
.NET 8 RTM
Mac OS 14
ASP.NET Core application
Other information
No response
The text was updated successfully, but these errors were encountered: