facebook / hermes

A JavaScript engine optimized for running React Native.
https://hermesengine.dev/
MIT License
9.9k stars 639 forks source link

Memory consumption on iOS #878

Closed michalchudziak closed 1 year ago

michalchudziak commented 1 year ago

Bug Description

Hermes version: 0.12.0 React Native version (if any): 0.70.6 OS version (if any): iOS 16 Platform (most likely one of arm64-v8a, armeabi-v7a, x86, x86_64): iOS

Steps To Reproduce

In the app I'm currently working on, the memory consumption on iOS is significantly increased compared to JSC. The difference between using hermes and JSC differs from 20 MB to 40MB. I did memory allocation profiling using Xcode Instruments (release app on iPhone 12 Pro), and found following object allocations that are not present in the JSC builds. Unfortunately I can't provide the minimal repro project, but I'm curious wether anyone recognizes these objects.

image

The Expected Behavior

Memory consumption to be lower or equal to JSC.

neildhar commented 1 year ago

Hi @michalchudziak, thanks for reporting this.

Could you describe the memory consumption pattern in a little more detail? Does Hermes remain consistently higher than JSC, or does the difference vary significantly over time? How does the 20-40MB compare to the overall memory consumption?

Note that comparing specific allocations across the engines generally doesn't work well, since the implementations are completely different. Are you able to share a more complete memory profile? Including anonymous mappings as well will be helpful, since most of Hermes' memory is typically backed by them.

That said, looking at the top allocations you've highlighted here.

If the first category is unique to Hermes, it's likely that it is coming from Intl. Does you application make heavy use of Intl?

For the two large malloc allocations in Hermes, I'd typically look at large ArrayBuffers or strings.

Using some of the memory profilers in Chrome DevTools while attached to the RN app may also help you pinpoint where the two large allocations are coming from in JS.

tmikov commented 1 year ago

@michalchudziak Can you also confirm that you are experiencing this with the release version of the app?

michalchudziak commented 1 year ago

Hey @neildhar @tmikov thank you for the swift response.

Does Hermes remain consistently higher than JSC, or does the difference vary significantly over time?

The difference is not constant, but it's consistently higher than JSC. 20-40 is an average from tested scenarios (I ran it 10 times per scenario to reduce variance). Sometimes it exceeds that value, but I haven't spotted any leaks.

How does the 20-40MB compare to the overall memory consumption?

Note that comparing specific allocations across the engines generally doesn't work well, since the implementations are completely different. Are you able to share a more complete memory profile? Including anonymous mappings as well will be helpful, since most of Hermes' memory is typically backed by them.

I can share some logs on Discord, I'll send you a DM.

Does you application make heavy use of Intl?

We use a lot of translations and support many languages, but regarding Intl APIs, we use them only in several places.

Can you also confirm that you are experiencing this with the release version of the app?

Yes, I've been profiling the release version of the app

mingodad commented 1 year ago

Here is a comparison with an adapted test from https://github.com/ArashPartow/exprtk (see attached):

node-hermes --version
LLVM (http://llvm.org/):
  LLVH version 8.0.0svn
  Optimized build
/usr/bin/time node-hermes exprtk_functional_test.txt.js 
7445 7299
0.62user 0.12system 0:00.74elapsed 99%CPU (0avgtext+0avgdata 202504maxresident)k
0inputs+0outputs (0major+55426minor)pagefaults 0swaps
/usr/bin/time qjs exprtk_functional_test.txt.js 
7445 7299
0.07user 0.00system 0:00.08elapsed 98%CPU (0avgtext+0avgdata 6176maxresident)k
0inputs+0outputs (0major+1716minor)pagefaults 0swaps
node -v
v16.17.1
/usr/bin/time  node exprtk_functional_test.txt.js 
7445 7299
0.07user 0.02system 0:00.09elapsed 102%CPU (0avgtext+0avgdata 46368maxresident)k
0inputs+0outputs (0major+6511minor)pagefaults 0swaps

exprtk_functional_test.txt.js.zip

mirzalikic commented 1 year ago

Hey, I have the same issue in my project, I upgraded from React Native version 0.69.6 to 0.70.5 some time ago, since then I noticed that the app launch is slower than before (the splash screen is longer visible). To find the cause of this, I used Xcode's allocations profile instrument. This is where I noticed that the app loads ~50 MB extra on launch, but only when Hermes is enabled. I made a small memory comparison between the versions, each with Hermes and JSC.

The results are (Release Configuration and iPhone 14 Pro):

screens | start: login form | timeline overview: flatlist | dashboard: simple components | more: some links | profile: form | messages:flatlist -- | -- | -- | -- | -- | -- | -- 0.69.6 |   |   |   |   |   |   jsc | 48 | 46 | 56 | 68 | 74 | 82 hermes | 44 | 45 | 55 | 64 | 70 | 82   |   |   |   |   |   |   0.70.5 |   |   |   |   |   |   jsc | 46 | 45 | 55 | 64 | 69 | 86 hermes | 95 | 99 | 108 | 116 | 122 | 133 I tried to describe the app in the table. As you can see the app loads in version 0.70.5 and Hermes enabled ~50 MB extra I couldn't find a cause for this. I've tried to remove some screens and libraries, the overhead gets smaller, but it still loads more than with JSC. On another project I have an even bigger difference of 90MB between JSC and Hermes on startup. However I made a small test app with expo to reproduce this issue: https://github.com/mirzalikic/hermes-test To run this app: 1. install expo-cli (https://docs.expo.dev/) 2. yarn install 3. expo prebuild 4. go in generated ios folder and open xcwordspace 5. Product -> Profile -> Allocations Switch to JSC in app.json -> jsEngine: "jsc" than repeat step 3. This app consumes ~56MB on launch with Hermes and only ~18MB with JSC
neildhar commented 1 year ago

@mingodad node-hermes is considered experimental, and Hermes is generally not optimised for running from source. Running with hermes (after setting console.log to print) and precompiling the JS first makes the comparison much more favourable. (you can precompile with hermes -emit-binary -out foo.hbc foo.js and then run it with hermes foo.hbc)

@mirzalikic can you verify that the built iOS app contains bytecode and not JS as well? You can look at the bundle by unzipping the IPA.

mirzalikic commented 1 year ago

@neildhar just checked the build, the main.jsbundle contains minified JS code instead of bytecode. Any idea how to fix this?

neildhar commented 1 year ago

@mirzalikic that would certainly explain the memory regression. I don't know off-hand what might cause this, since RN does the bundling and compilation to bytecode.

@michalchudziak has previously mentioned that he had success patching in https://github.com/facebook/react-native/commit/03de19745eec9a0d4d1075bac48639ecf1d41352, so that may be worth trying.

mirzalikic commented 1 year ago

@neildhar thanks, this patch fixed the problem. I saw that React-Native 0.71.0 was released yesterday with this fix, so the update should solve the issue as well.

neildhar commented 1 year ago

@mirzalikic thanks for the update. To confirm, are you now observing better memory consumption with Hermes?

mirzalikic commented 1 year ago

yes i can confirm the issue is gone now, https://github.com/facebook/react-native/commit/03de19745eec9a0d4d1075bac48639ecf1d41352 this was the fix and its included in the latest React Native version

michalchudziak commented 1 year ago

@mirzalikic are you sure that the memory consumption issue is gone? It seems that the fix you've linked is addressing the bytecode issue.

mirzalikic commented 1 year ago

@michalchudziak yes memory consumption is great now, the bytecode issue was causing the high memory consumption.

the main.jsbundle only contained minified js code instead of compiled code.

tmikov commented 1 year ago

Closing, since the issue has been successfully resolved.

michalchudziak commented 1 year ago

@neildhar @tmikov Upon reviewing the profiles for version 0.71, it appears the previously identified issue persists, notably in the initial allocation pertaining to NSData mutableCopyWithZone. However, the other significant allocations associated with Hermes do not seem to be present.

For further context, I compared using a newly generated project through npx react-native init TestHermes (0.71.8), observing it with Hermes both enabled and disabled. The analysis showed a noticeable difference in the total memory allocated, approximately ~1.17 MiB, to the object under review.

image

It seems that it's a reference to JSBigString allocated here https://github.com/facebook/react-native/blob/e25c6632a2360acfb63ceb62258ee1ec18452018/packages/react-native/ReactCommon/cxxreact/Instance.cpp#L106

Do you have any context on that? Is this a conscious allocation?

tmikov commented 1 year ago

@michalchudziak the code you linked is not part of Hermes, so I am not sure what is happening there. Looks like JSBigString contains the bytecode bundle? Ideally it should be memory-mapped, but Hermes doesn't control how that is allocated.

As far as I can tell, JSBigString is an abstraction on top of mmap(), so I don't know why it is showed as malloc or what it has to do with NSData.