hermes: Garbage Collector not picking up high memory pressure when memory is allocated in native HostObjects

Bug Description

Background

There are multiple reports about memory issues in Hermes that seems to be caused by or be related to the garbage collector in Hermes not picking up native allocations done in C++ based HostObjects.

A host object can allocate memory using malloc or other mechanisms, and the Garbage won’t see this memory. This again causes the host object to be considered too small to be garbage collected and for performance reasons will be kept alive.

We’re seeing this extensively in our React Native library @shopify/react-native-skia where the Skia API is implemented in C++ as Host Objects using JSI.

Example

An example is our image component where we allocate and load images that can be of any size. The Hermes Garbage Collector will only see our image component (which is only pointed to by the Javascript engine, we don’t keep any internal registries of objects - they’re always returned and kept alive by JS) as a super small JS object and never deallocate this.

We’ve used some tricks like providing hooks that cleans up the allocated memory to make this work, but some of our objects are not exposed through hooks and will therefore not be cleaned up by GC.

Testing

We’ve tested these things switching back and forth between JSC and Hermes which verifies that this is an issue.

References

Here are some references to other issues in the Hermes repo that might be related:

We’ve also seen some references in other repositories like this one from @Kudo in react-native-v8:

In @shopify/react-native-skia we have multiple issues regarding this and are constantly fixing new ones (and trying to change anti-patterns that allocate too many times like in animations):

Alternative Solutions

There are several ways to move forward to solve this issue, the first one being us library developers being better at not using anti-patterns where we’re not cleaning up after us. This is however not always easy, and a lot of the native code implementing HostObjects are doing so with the assumption that if you let Javascript / Hermes manage an object’s lifespan everything should be good.

We can also look into expanding the jsi::HostObject interface to expose information about how much memory an object has allocated which can then be picked up by the garbage collector and used when calculating which objects we should free.

The garbage collector might need to look into not only its own allocated memory, but also the total memory used by the current process.

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 6
  • Comments: 23 (10 by maintainers)

Most upvoted comments

Looks like this one addresses this issue with a solution that will be exactly what we need: https://github.com/facebook/hermes/commit/aae2c4260781178d7b2ca169811b3bfca9f924d2

Awesome!

@tmikov JSC seems to run the garbage collector more frequently, and also seems to take the total memory pressure of the app into consideration when doing garbage collection (if I’m not mistaken totally). We’re not using any JSC-specific APIs - we observe in Xcode and Android Studio that when running the app under JSC it will run garbage collection more often.

We have a test where we allocate an image of a specific size on each frame - where the allocation is happening in C++ from inside a jsi::HostObject. Under JSC the memory is stable (not increasing) when run at 60fps - while on Hermes the memory is growing linearly and eventually causes the app to crash with an OOM error.

When inspecting this in Xcode’s memory graph we can verify a high number of image wrapping jsi::HostObjects not being released on Hermes, while they are released when using JSC.

The only thing we do to compare this is to switch on/off hermes in the React Native application.