facebook / react-native

A framework for building native applications using React
https://reactnative.dev
MIT License
119.28k stars 24.35k forks source link

Fetching large json data results in OutOfMemoryError #32134

Open curtisy1 opened 3 years ago

curtisy1 commented 3 years ago

Description

Using fetch to get several Megabytes (currently happens for me at around 80Mb) of JSON data causes Android to panic and throw an OutOfMemoryError. This is due to the fact that the whole response is being read as bytes, quickly filling up the heap.

Stacktrace:

09-01 21:32:21.035  5480  5555 E AndroidRuntime: java.lang.OutOfMemoryError: Failed to allocate a 98836368 byte allocation with 25165824 free bytes and 88MB until OOM, target footprint 133385952, growth limit 201326592
09-01 21:32:21.035  5480  5555 E AndroidRuntime:    at okio.Buffer.readByteArray(Buffer.kt:1429)
09-01 21:32:21.035  5480  5555 E AndroidRuntime:    at okio.Buffer.readByteArray(Buffer.kt:1424)
09-01 21:32:21.035  5480  5555 E AndroidRuntime:    at okio.RealBufferedSource.readByteArray(RealBufferedSource.kt:238)
09-01 21:32:21.035  5480  5555 E AndroidRuntime:    at okhttp3.ResponseBody.bytes(ResponseBody.kt:124)
09-01 21:32:21.035  5480  5555 E AndroidRuntime:    at com.facebook.react.modules.blob.BlobModule$4.toResponseData(BlobModule.java:134)
09-01 21:32:21.035  5480  5555 E AndroidRuntime:    at com.facebook.react.modules.network.NetworkingModule$2.onResponse(NetworkingModule.java:512)
09-01 21:32:21.035  5480  5555 E AndroidRuntime:    at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
09-01 21:32:21.035  5480  5555 E AndroidRuntime:    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
09-01 21:32:21.035  5480  5555 E AndroidRuntime:    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
09-01 21:32:21.035  5480  5555 E AndroidRuntime:    at java.lang.Thread.run(Thread.java:923)

React Native version:

System:
    OS: Linux 5.13 Solus 4.3
    CPU: (8) x64 Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
    Memory: 1.39 GB / 15.52 GB
    Shell: 5.1.8 - /bin/bash
  Binaries:
    Node: 14.17.5 - /usr/bin/node
    Yarn: 1.22.10 - /usr/bin/yarn
    npm: 6.14.14 - /usr/bin/npm
    Watchman: Not Found
  SDKs:
    Android SDK: Not Found
  IDEs:
    Android Studio: Not Found
  Languages:
    Java: 1.8.0_302-solus - /usr/lib64/openjdk-8/bin/javac
  npmPackages:
    @react-native-community/cli: Not Found
    react: 17.0.2 => 17.0.2 
    react-native: 0.65.1 => 0.65.1 
  npmGlobalPackages:
    *react-native*: Not Found

Steps To Reproduce

I created a test repository that shows the aforementioned behaviour. It also includes a 100Mb JSON file for testing, that you can serve locally or access via GitHub directly. Steps are as follows

  1. Tap on the load data button
  2. Wait for the app to crash

Expected Results

Getting an out of memory error shouldn't really happen with this size of data in my opinion. Sure 100Mb sounds a lot at first, but in enterprise-grade apps this is probably a common scenario. In any case I think there should be a possibility to dynamically switch to streaming the response since okhttp offers bytestream and charstream as well. I'm not that well versed in Java but it would probably help reduce all of the 150Mb landing on the heap at once?

Snack, code example, screenshot, or link to a repository:

https://github.com/curtisy1/ReactNativeFetchRepro

452MJ commented 3 years ago

Try add android:largeHeap label to your AndroidManifest.xml.


<application
   ...
+    android:largeHeap="true"
   ...
```/>
curtisy1 commented 3 years ago

Thanks for trying to help @452MJ!

I am aware of that option but that's pretty much my last resort as I think there must be another way.

Simply put, there's probably thousands of apps - and among those a few RN apps as well - on the market fetching something (once, hence not directly streaming) with a size over 100Mb and I highly doubt they all have android:largeHeap set to true. Not to mention Google also does not directly recommend it as a fix to increasing performance or OOM errors.

There's also a very informative StackOverflow question with the general answer being "do not use if you absolutely have to"

452MJ commented 3 years ago

I think this is a must for RN apps. Some of the apps I worked on in the past sometimes caused OOM crash due to downloading web images. Unlike native projects whose memory footprint rarely reaches 100MB, it is common for RN projects to hit 200-300MB. This is also a weak term in RN. So I set largeHeap to true for each project.

Maybe you can try run on Hermes jsengine.😬

curtisy1 commented 3 years ago

Unlike native projects whose memory footprint rarely reaches 100MB, it is common for RN projects to hit 200-300MB

I completely agree with this one. But most of it is probably the JS overhead and not any native implementation. In this case, however, the issue is that the bytes are accessed directly, so we get the total size of our response added to our heap. OkHttp actually warns about this and advises streaming the response. I'm not experienced enough in native Android development to tell if this would be applicable in this case, but it sounds better than having an OOM or a large heap that might cause other side effects.

That's actually why I'm interested in what the react-native team has to say about this, because setting largeHeap seems like some workaround for a bigger issue to me

EDIT: I tried setting android:largeHeap=true and enabling Hermes but the OOM still occurs.

452MJ commented 3 years ago

Maybe you can try using axios to replace fetch

curtisy1 commented 3 years ago

This probably wouldn't change anything as both fetch and axios work by sending XMLHttpRequests under the hood, which then get converted to a native call to OkHttp, causing the behavior above. The only thing it would probably change is that the JSON parsing would be up to the JS side of things. But if the request in itself fails (or more accurately, the initial response conversion on Android's native side), I won't even get to parsing any JSON.

stale[bot] commented 2 years ago

Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may also label this issue as a "Discussion" or add it to the "Backlog" and I will leave it open. Thank you for your contributions.

curtisy1 commented 2 years ago

Definitely not stale. But if I have the freedom to label it, I'll gladly add it to the backlog 😉

TommysG commented 2 years ago

Exactly the same problem here. I have offline-first architecture in my app and i need to fetch some big data on startup. @curtisy1 did you find any solution to this problem?

curtisy1 commented 2 years ago

@TommysG We didn't find any solution to this. What we tried doing was reduce the size of our responses and optimize them in a way that this issue is more unlikely to happen for our use case. That is more of a bandaid solution, however and I'm fairly certain we will easily reach that cap in production where larger data is quite common

curtisy1 commented 2 years ago

Seems like we are now also facing this in production.

@shergin Seeing as you're not on the React Native Core team anymore but still the codeowner according to the codeowners file. Do you know who's the right person to mention here, so this issue gets some love maybe?

curtisy1 commented 1 year ago

Very sorry to necro-bump this but it's been two years with the only reply being from another react-native user trying to help and one having the same issue. Since then, the CODEOWNERS got removed as well, for staleness like dear stale bot once tried to do with this issue (until... well it was removed as well).

To clarify, this is probably a fairly niche issue but I don't really consider leaving an issue alone for two years a good thing. In my attempt to get someone to actually look at this, I, as a user don't have any other choice but randomly bump this and ping people I think might know how to help.

For future reference, maybe you folks could implement some kind of policy on what "Triage" actually means for you? When I filter for the tag, I get 46 pages with almost 1.3k issues back, the oldest being from 2020 (that's ~81% of all open issues!). I doubt any issue would be triaged this long considering you have tags for pretty much anything.

And before you ask, yes, I know this is a community project but it still runs under the Facebook/Meta umbrella so I would expect at least a bit of dedication towards issues opened by the community. Especially when I take the time to catch a stack trace and fill out the issue template.

Microsoft does a lot better than you in that regard, so please step up your game and actually respond to issues :smile: (fwiw I'd also accept a won't fix with a detailed explanation and way to move forward).

Ping @cortinico because you accepted the CODEOWNERS removal, so you're the only straw I can hold onto for this year

cortinico commented 1 year ago

Hey @curtisy1 I can mention that our team looks at new issues every day. I personally go over most of them on a daily basis. The reality is that our team is just so small, and we can't answer to everyone, and also we can't fix every bug. We try to prioritize those that have most upvotes or are affecting the highest number of people.

Issues that gets picked up by the community and receive a PR, are the ones that most likely to get fixed and end in a release.

As for your specific issue, the problem you're having is that your response gets processed by the BlobModule, which is loading the full response in memory. I guess the problem is that you should not be using the BlobModule at for this specific request as it should be of type json. I'm unsure why this is happening and would need further investigation.

tkserver commented 4 days ago

Same issue for me. I'm reading large JSON files up to 150mb. On ios all works fine. I can load the entire JSON file and work with it. On anrdoid, anything over 15-20MB produces out of memory errors. Also looking to resolve this!