Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.

Add property HardLimitBytes to GCMemoryInfo #25437

Merged
merged 7 commits into from Jul 8, 2019
Merged

Add property HardLimitBytes to GCMemoryInfo #25437

merged 7 commits into from Jul 8, 2019

Conversation

ghost
Copy link

@ghost ghost commented Jun 26, 2019

This adds a new property HardLimitBytes.

Unlike TotalAvailableMemoryBytes,
this will reflect an explicitly set COMPLUS_GCHeapHardLimit.

It will also reflect the fraction of a container's size that we use,
where TotalAvailableMemoryBytes is the total container size.

Normally, though, it is equal to TotalAvailableMemoryBytes.

Fix dotnet/corefx#38821

This adds a new property HardLimitBytes.

Unlike TotalAvailableMemoryBytes,
this will reflect an explicitly set COMPLUS_GCHeapHardLimit.

It will also reflect the fraction of a container's size that we use,
where TotalAvailableMemoryBytes is the total container size.

Normally, though, it is equal to TotalAvailableMemoryBytes.

Fix #38821
@ghost
Copy link
Author

ghost commented Jun 26, 2019

I've tested this out and here are the results (my notes in parentheses):

Running normally:

bin/Debug/netcoreapp3.0/win-x64/publish/cs.exe
TotalAvailableMemoryBytes: 137323081728
HardLimitBytes: 137323081728 (normally equal)

Running in a container:

C:/Users/anhans/work/dotnet-gc-infra/c_scripts/run_in_job.exe --memory-mb 123 -- bin/Debug/netcoreapp3.0/win-x64/publish/cs.exe
TotalAvailableMemoryBytes: 128974848 (full container size)
HardLimitBytes: 96731136 (75% of container size)

Running with a hard limit :

$env:COMPLUS_GCHeapHardLimit="75bcd15" # value is hexadecimal for 123456789
bin/Debug/netcoreapp3.0/win-x64/publish/cs.exe
TotalAvailableMemoryBytes: 137323081728 (the machine's physical memory)
HardLimitBytes: 123456789 (what I specified)

@jkotas
Copy link
Member

jkotas commented Jun 26, 2019

I do not think we should be introducing this API. Instead, we should fix TotalAvailableMemoryBytes to return the amount of memory that the GC can play with.

I do not think it makes sense for GCMemoryInfo.TotalAvailableMemoryBytes to return total amount of memory on the machine that has nothing to do with the GC.

@sergiy-k
Copy link

@Maoni0, @andy-ms, I agree with @jkotas. Why TotalAvailableMemoryBytes cannot be fixed to return the Hard Limit value if it has been specified? Is there a technical reason for that?

@ghost
Copy link
Author

ghost commented Jun 28, 2019

Added a new commit which makes TotalAvailableMemoryBytes have the behavior that HardLimitBytes had.

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

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

LGTM modulo nit.

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

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

Actually, I have just notice that this value is used to convert highMemLoadThreshold and lastRecordedMemLoad to bytes. Is this conversion still valid?

@ghost
Copy link
Author

ghost commented Jul 2, 2019

You're right, in GCToOSInterface::GetMemoryStatus (called by get_memory_info which is what we use to get the memory load):

uint64_t restricted_limit = GetRestrictedPhysicalMemoryLimit();
...
*memory_load = (uint32_t)((float)workingSetSize * 100.0 / (float)restricted_limit);

GetRestrictedPhysicalMemoryLimit() appears to handle containers but not an explicit hard limit. So with an explicit hard limit set, memory_load_th basically has no effect as it will likely be bigger than the hard limit. This may be an oversight, but better to keep these numbers accurate, so I'll return both physical and hard limits from c++ and have c# choose between them.

@jkotas
Copy link
Member

jkotas commented Jul 2, 2019

I think it would be best to return the numbers in bytes from the GC API, so there are no further adjustments needed in C#.

@Maoni0
Copy link
Member

Maoni0 commented Jul 2, 2019

@andy-ms

GetRestrictedPhysicalMemoryLimit() appears to handle containers but not an explicit hard limit.

GetRestrictedPhysicalMemoryLimit() is not supposed to read what you set with the hardlimit configs. it's supposed to return the true physical memory limit for the process.

@davidfowl
Copy link
Member

I think it would be best to return the numbers in bytes from the GC API, so there are no further adjustments needed in C#.

+1

Andy Hanson added 2 commits July 1, 2019 20:16
so we can compute highMemoryLoadThresholdBytes and memoryLoadBytes
src/gc/gc.cpp Outdated
*highMemLoadThresholdBytes = (uint64_t) (((double)gc_heap::high_memory_load_th) / 100 * gc_heap::total_physical_mem);
*totalAvailableMemoryBytes = gc_heap::heap_hard_limit != 0 ? gc_heap::heap_hard_limit : gc_heap::total_physical_mem;
*lastRecordedMemLoadBytes = (uint64_t) (((double)gc_heap::last_gc_memory_load) / 100 * gc_heap::total_physical_mem);
*lastRecordedMemLoadPct = gc_heap::last_gc_memory_load;
Copy link
Member

@jkotas jkotas Jul 2, 2019

Choose a reason for hiding this comment

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

Is the last_gc_memory_load relative to total_physical_mem; or relative to *totalAvailableMemoryBytes ?

Either this or the previous line looks wrong. When memory load is 100%, I think loadBytes should be equal totalAvailableMemoryBytes.

It may be best to get rid of the *lastRecordedMemLoadPct argument, and just divide lastRecordedMemLoadBytes and totalAvailableMemoryBytes on the CoreLib side.

Copy link
Member

Choose a reason for hiding this comment

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

last_gc_memory_load is relative to total_physical_mem. the problem is we do have a GetMemoryLoad that needs the actual memory load.

I would really prefer our original API proposal that had both the total physical memory and the hardlimit for the GC heap 'cause we do care about both. and I don't think it's unreasonable for the users to know about both. either that or we have another API (internal) that gets the actual physical memory load.

Copy link
Author

Choose a reason for hiding this comment

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

last_gc_memory_load is relative to total_physical_mem. If totalAvailableMemoryBytes is less than total_physical_mem, then it will be impossible for last_gc_memory_load to ever reach 100% -- it would be at most totalAvailableMemoryBytes / total_physical_mem * 100.

Copy link
Member

@jkotas jkotas Jul 2, 2019

Choose a reason for hiding this comment

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

The primary reason why we have introduced the GCMemoryInfo API is to allow folks to build self-tuning caches.

Could you please review the places where this API is used currently (our TlsOverPerCoreLockedStacksArrayPool in particular - not sure whether there is anything else) and validate that it works correctly in all cases (regular VMs, containers, hardlimit)?

The current code for this is here:

. Is this code correct for all situations? If not, what is the code that we want people to write to implement this logic? Once we know what code we want people to write here, we should be clear about what these APIs needs to do.

GetMemoryLoad that needs the actual memory load.

This is used by the internal experimental MemoryLoadChangeNotification APIs. Is this API used by anything?

Copy link
Member

Choose a reason for hiding this comment

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

the impl in TlsOverPerCoreLockedStacksArrayPool.cs looks correct - it could be improved (for example it could say only trim if the size it trims could actually make a difference in memory pressure) but I don't think we are doing that for 3.0.

it seems awkward to have to fit the 2 numbers (actual physical memory and hardlimit) into 1. and if we don't want to have to call the diagnostics API to retrieve the physical memory (which also seems awkward for GC) our original proposal seems more sensible to me.

Copy link
Member

Choose a reason for hiding this comment

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

What logic do we think would need to look at physical memory rather than the hard limit?

Copy link
Member

@jkotas jkotas Jul 3, 2019

Choose a reason for hiding this comment

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

We have received a lot of feedback in 2.x about ArrayPool keeping too much memory over time. We have seen people taking extreme measures like poking the ArrayPool via private reflection to make it release the memory. We have implemented the trimming at the last minute to mitigate the situation.

If the ArrayPool trimming does not take the hardlimit into account, I expect that we are going to hear the same feedback again from folks who try to use the hardlimit.

Copy link
Member

Choose a reason for hiding this comment

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

Right I'm agreeing with your assessment. I'm mostly wondering when you'd need the actual physical memory vs the hard limit (the other should probably be exposed where we have the other process specific APIs Process.*)

Copy link
Member

Choose a reason for hiding this comment

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

(My reply was for Maoni's comment.)

I agree that the actual physical memory does not seem to be necessary to build decent trimming heuristic.

I think that there are 4 numbers to feed into a decent basic trimming heuristic:

  • The GC heap size: current value and limit that it should stay under. I believe it is HeapSizeBytes and TotalAvailableMemoryBytes in the current API.
  • The memory load (ie the GC heap size plus everything else): current value and limit that it should stay under. I believe it is MemoryLoadBytes and HighMemoryLoadThresholdBytes in the current API.

When either of these gets too close to its limit, it is a signal to trim caches.

Copy link
Member

Choose a reason for hiding this comment

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

I have opened https://github.com/dotnet/coreclr/issues/25552 on ArrayPool not respecting GCHardLimit.

virtual void GetMemoryInfo(uint64_t* highMemLoadThresholdBytes,
uint64_t* totalPhysicalMemoryBytes,
uint64_t* lastRecordedMemLoadBytes,
uint32_t* lastRecordedMemLoadPct,
size_t* lastRecordedHeapSize,
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Add Bytes suffix to these two args; or delete it from the other args here.

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

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

As I have said, it would be nice to get rid of the lastRecordedMemLoadPct because of it is redudant piece of data and change MemoryLoadChangeNotification to use the public GCMemoryInfo properties. I think it should be using MemoryLoadBytes / HighPressureThreshold just like ArrayPool.

The MemoryLoadChangeNotification is internal experimental API that is not used by anything. As far as I can tell, it is broken current and nobody can use it for anything useful. So passing lastRecordedMemLoadPct through may be confusing, but it is not actively harmful.

The rest of the changes looks like an improvement to me, so I would be ok with merging this (either as is; or with cleaned up lastRecordedMemLoadPct).

@jkotas jkotas merged commit 11137fb into dotnet:master Jul 8, 2019
@jkotas
Copy link
Member

jkotas commented Jul 8, 2019

Thank you everyone!

@ghost ghost deleted the just_hardlimit_change branch July 8, 2019 23:14
@ghost ghost requested a review from MeiChin-Tsai July 8, 2019 23:14
@MeiChin-Tsai
Copy link

Yes, I like the change. It is good to make intension explicit.

size_t* lastRecordedFragmentationBytes)
{
*highMemLoadThresholdBytes = (uint64_t) (((double)gc_heap::high_memory_load_th) / 100 * gc_heap::total_physical_mem);
*totalAvailableMemoryBytes = gc_heap::heap_hard_limit != 0 ? gc_heap::heap_hard_limit : gc_heap::total_physical_mem;
Copy link

Choose a reason for hiding this comment

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

Is it fine to potentially return heap_hard_limit here, which could be larger than total_physical_mem if inproperly configured?

Copy link
Member

Choose a reason for hiding this comment

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

If you misconfigure the hard limit, the GC itself won't work quite right either. You get what you asked for. We are not trying to guess whether you meant it or not.

picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
…tnet/coreclr#25437)

* Add property HardLimitBytes to GCMemoryInfo

This adds a new property HardLimitBytes.

Unlike TotalAvailableMemoryBytes,
this will reflect an explicitly set COMPLUS_GCHeapHardLimit.

It will also reflect the fraction of a container's size that we use,
where TotalAvailableMemoryBytes is the total container size.

Normally, though, it is equal to TotalAvailableMemoryBytes.

Fix dotnet/coreclr#38821

* Remove HardLimitBytes; have TotalAvailableMemoryBytes take on its behavior

* Fix typos

* Separate total_physical_mem and heap_hard_limit
so we can compute highMemoryLoadThresholdBytes and memoryLoadBytes

* Do more work in gc.cpp instead of Gc.cs

* Consistently end names in "Bytes"


Commit migrated from dotnet/coreclr@11137fb
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Proposal: Add hard limit to MemoryInfo API
6 participants