livefront / bridge

An Android library for avoiding TransactionTooLargeException during state saving and restoration

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Out of memory when saving state

elye opened this issue · comments

commented

Can't really replicate this issue, but reported quite a bit of instance error log

Fatal Exception: java.lang.OutOfMemoryError: Failed to allocate a 9740048 byte allocation with 7449136 free bytes and 7MB until OOM
       at java.lang.StringFactory.newStringFromChars + 218(StringFactory.java:218)
       at java.lang.StringFactory.newStringFromBytes + 203(StringFactory.java:203)
       at java.lang.StringFactory.newStringFromBytes + 63(StringFactory.java:63)
       at android.util.Base64.encodeToString + 456(Base64.java:456)
       at com.livefront.bridge.BridgeDelegate.writeToDisk + 171(BridgeDelegate.java:171)
       at com.livefront.bridge.BridgeDelegate.saveInstanceState + 164(BridgeDelegate.java:164)
       at com.livefront.bridge.Bridge.saveInstanceState + 80(Bridge.java:80)

Interesting. Thanks for reporting that. I'll investigate if there is safer way to do the encoding in chunks perhaps.

@elye I just wanted to check in with you on this. I managed to reproduce this in cases where extremely large amounts of data (50 - 100MB or more) were being saved by going into the generateNoisyStripedBitmapInternal method of the sample app and changing val size = 800 to something like val size = 800 * 6. I experimented with a variety of solutions and at the moment I haven't found anything that solves those cases and its quite possible the only way to deal with data that large is to ditch SharedPreferences as a storage mechanism and actually write the data to a File using a FileOutputStream. That would be a pretty big change but it is something I'd like to look into longer-term.

That being said, in the one particular case you have above at least looks like the allocation in question is quite a bit smaller and it makes me wonder if there is still some room for making some marginal improvements to the current setup. I will keep investigating that.

Hey @byencho . I'm working on the same project as @elye . Basically I found the crash is probably caused by the serialisation of bundle to string:

In BridgeDelegate.java

private void writeToDisk(@NonNull String uuid,
                             @NonNull Bundle bundle) {
        String encodedString = BundleUtil.toEncodedString(bundle);
        mSharedPreferences.edit()
                .putString(getKeyForEncodedBundle(uuid), encodedString)
                .apply();
    }

and in BundleUtil.java

public static String toEncodedString(@NonNull Bundle bundle) {
        return Base64.encodeToString(toBytes(bundle), 0);
}

This attempts to create a String from byte array and that causes OOM exception when the data is bigger than the available memory.

I'm looking for an option to overcome this issue. Wondering if you have any idea, apart from FileOutputStream?

@huan-nguyen Right, that is the spot in the library that is causing the issue. As I mentioned above, I've tried a variety of solutions already that still make use of the Base64.encodeToString call but I haven't found anything that gets around the issue for very large Bundle instances. I'm pretty sure the only way around it is to go the FileOutputStream route (if that would even fix the issue) but that's going to be a bigger change that I'll need to take up at some time later in the future.

From my testing, though, I really only saw this issue with extremely large amounts of data (50 - 100MB or more). The TransactionTooLargeException crash that Bridge is built to avoid often happens around 0.5MB, so we're talking about data at least 100 times larger than the limit set by the OS. Unless you can reproduce this issue for much smaller amounts of data I would try to look into things on your end to see if you're attempting to save extremely large data like this (such as a large Bitmap) and simply find a different way to manage its storage. Otherwise you'll have to wait until I can look into FileOutputStream approach.

Thanks for your reply @byencho.

Actually, we don't cache bitmap, just data. it's quite interesting that the crash happened with relatively smaller size of data as well. We actually see a number of crashes on old devices (most are Samsung) running Android 7 and below. Sometimes the data size just needs to be 10Mb, or even less like 3Mb to cause a crash. It's quite an edge case, but happened to occur a lot of those devices. I guess part of the reason is those devices normally have less memory than newer ones and memory management isn't great there somehow.

commented

Hi @byencho

Good day.
Just for experiment, I have fork your bridge library to play with it a little explore the effect of saving to file instead of using SharedPreference to store the bundle.

When we use val size = 800 * 6 Bitmap, the crash happens when it Encodes to 64 bits String.
The entire process of converting from Bundle to Bytes[] is already memory consuming. And converting Bytes[] to Base64 String, requires another chunk of memory. Hence when saving the bundle to file, 2 peaks of memory involve and hence crash OOM during the save process.

To overcome this a little. We could avoid encoding to Base64 bits String, by saving the Bytes[] (converted from Bundle) into File directly. I have created a branch in my Fork with the relevant codes. https://github.com/elye/bridge/tree/feature/add-file-save-option.

This logic retain everything as it is except that it will save the Bundle directly to file, instead of Encoding to Base64 String and saved in SharedPreference. I still used the SharedPreference to save the name of the file for future retrieval, and cleanup purposes. To switch back to SharedPreference approach, it is simply by setting the newly introduced sSaveToFile static boolean to false

With this in place, using the File saving approach, I could now save the val size = 800 * 6 Bitmap without crashing.

To test the different memory utilization, I use val size = 800 * 5 Bitmap.

For the SharedPreference approach, we could see the peak of the memory hit 408MB at the moment the saving is completed. (diagram below) - note upon Bitmap created, the memory is about 130MB, the conversion and saving process raise another ~270MB

Screen Shot 2020-01-10 at 6 56 55 PM

For the Saved to File approach, we could see the peak of the memory hit 249MB at the moment the saving is completed. (diagram below) - note upon Bitmap created, the memory is about 130MB, the conversion and saving process raise another ~120MB

Screen Shot 2020-01-10 at 7 02 04 PM

This clearly shows that saving File is more memory efficient than the SharedPreference approach.

The code in place takes care of WRITE, READ and DELETE. All tested working as far as I tried it out.

I didn't make a Pull Request, as there might be various ways to save to file, and you might have some preferred way of doing so. Beside this is a relatively big change that you have preference of how you like to organize the code file structure. Hence I just leave this Branch for your reference if you would like to consider adding such enhancement to your library.

Thanks.

Hi @elye . Thanks for investigating that! Yes that is pretty much the exact approach I had in mind when I mentioned using File / FileOutputStream as a larger change to get around the issue. This is definitely something I want to pursue further. I'd say for now it looks like you can just go ahead and use your own fork until I can get around to getting a version of this change into the main library.

commented

Thanks. When you're making the change, it would be even if you could find a way to avoid the parcel.writeBundle(bundle); as that's the next place the crash OOM.

Well the parcel.writeBundle(bundle) call can't be avoided: its absolutely key to the disk-writing functionality. There is no other way I am aware of to write a generic Bundle to bytes other than the procedure I'm currently using. So while I can improve the bytes-to-disk step, I'm unaware of a way to improve the Bundle-to-bytes step. The only thing I could do at that point is to catch the OOM exceptions and just not store the Bundle to disk. It's not great behavior, but maybe better than the crash. I'll have to think about that.

@elye The fix to avoid Base64-encoding the data to get around the OOM issue has been released with v1.3.0. Sorry for the delay on this! Please let me now know if you have more questions. I'm going to close the issue for now. As for the OOM problem when calling parcel.writeBundle(bundle), the best I would be able to do is catch the exception and fail to save the state. If that's a fix you'd be interested in, feel free to open a separate issue for that and I can look into it.