redux-persist icon indicating copy to clipboard operation
redux-persist copied to clipboard

Seeing outofmemory errors on ios in asyncstorage code

Open dchersey opened this issue 5 years ago • 24 comments

Hi, using react-native-persist 5.10.0 and react-native 0.59.9, I am seeing several crashes lately in our production app like:

Error · Out of memory
[native code]stringify	
node_modules/redux-persist/lib/createPersistoid.js:90:57writeStagedState	
node_modules/redux-persist/lib/createPersistoid.js:78:6processNextKey	
node_modules/react-native/Libraries/Core/Timers/JSTimers.js:152:6_callTimer	
node_modules/react-native/Libraries/Core/Timers/JSTimers.js:414:17callTimers	
node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:366:47value	
node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:106:26	
node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:314:8value	
node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:105:17value

Could this be due to trying to store too much data in AsyncStorage? I am thinking of trying out https://github.com/bhanuc/react-native-fs-store at the RN level to drop-in this replacement for AsyncStorage ... do you know if redux-persist would work with it?

dchersey avatar Feb 04 '20 00:02 dchersey

Same here, does anyone knows a solution?

MateusDantas avatar Feb 04 '20 22:02 MateusDantas

Facing same issue, but I am also using redux-persist-expo-filesystem as storage engine, I thought it did not had any size limitations. anyone having an idea whats it about or how to deal with it?

summerkiflain avatar Feb 26 '20 06:02 summerkiflain

@summerkiflain I am dealing with the same issue as you in my production app, using redux-persist-expo-filesystem. Any luck?

brianchenault avatar Mar 30 '20 18:03 brianchenault

I'm still chasing this .. I updated everything to RN 0.61 (because aysnc storage was updated) and the symptoms changed (redux persist is no longer in the stack trace) but it does seem still related because I am updating state with every keystroke to a persisted reducer. My working theory is while doing this memory is consumed and not released fast enough (XCode profiling bears this out, although there does not appear to be a leak, just a surge in memory usage).

Just tried updating to redux-persist 6.0.0 and adding a throttle of 3000 to that reducer and explicitly flushing it when the state is cleared (to avoid it coming back due to throttle delay later). Testing now ... it takes a while because this is not reliably reproducible.

Hope this helps; I will let you know what happens.

dchersey avatar Mar 31 '20 13:03 dchersey

@dchersey thanks for the update. I'm extremely curious how this works out for you so, yes, please keep us posted. Thank you!

brianchenault avatar Mar 31 '20 14:03 brianchenault

@brianchenault @dchersey Yeah still not resolved, sentry reported it happening to two users yesterday, for me sentry reports with stracktrace saying error in node_modules/redux-persist/lib/createPersistoid.js in writeStagedState at line 98:48: which is this one:

writePromise = storage.setItem(storageKey, serialize(stagedState)).catch(onWriteFail);

summerkiflain avatar Mar 31 '20 19:03 summerkiflain

@dchersey @summerkiflain just to follow up here - out of curiosity, I have tried swapping out redux-persist-expo-filesystem with SqlLite storage (redux-persist-sqlite-storage), and I am still seeing this error, so I know it's not specific to the storage mechanism now. I'm on Expo SDK37 and redux-persist 5.10.0.

brianchenault avatar May 14 '20 21:05 brianchenault

Same issue here. It seems like this problem happens when a device is really stressed by some large payload.

I think JSON.stringify used inside defaultSerialize function here https://github.com/rt2zz/redux-persist/blob/master/src/createPersistoid.js#L140

function defaultSerialize(data) {
  return JSON.stringify(data)
}

is the cause. When the data to serialize is too big, it fails.

My current workaround is to use kind of "best effort" strategy to serialize. It will not store new data or remove the existing data, just to avoid the app from crashing.

// try to serialize but quit if it was too much.
const serialize = (data) => {
  try{
    return JSON.stringify(data)
  }catch(err){
    captureHandledException(err) // for sentry
    return JSON.stringify({})
  }
}

const persistConfig = {
  key: 'root',
  version: 1,
  storage: AsyncStorage,
  serialize,
}

I use redux-persist 6.0.0, Expo SDK36, AsyncStorage from react-native.

I also check the stored data when initializing screen and remove excessive data, because this error was happening when there was too much data in the store.

Since this error is quite hard to replicate, so I haven't tested this solution in the production yet. But I will update when I found out it fixed it or not.

I hope someone will come up with better solution though. Any suggestion is helpful.

foloinfo avatar Jun 04 '20 08:06 foloinfo

Thanks for the update @foloinfo - keep us posted. Similar scenario here - I'm dealing in large datasets.

brianchenault avatar Jun 08 '20 15:06 brianchenault

Look like it also happened with serialize process, anyway to stream JSON data ?

nguyenhose avatar Jul 05 '20 11:07 nguyenhose

Experiencing the same/similar issue.

redux-persist 6.0.0 AsyncStorage 1.11.0

I have a socket connection delivering messages once per second. Each delivery results in a write to storage. While profiling using Safari connected to a live device I noticed that my redux store is being retained on every write. This results in approx 600k being added on each message :-/ In the memory snapshot list of functions the store string is being referenced by setItem(), pointing at the Promise().

`setItem: function setItem(key, value, callback) { return new Promise(function (resolve, reject) { checkValidInput(key, value);

    _RCTAsyncStorage.default.multiSet([[key, value]], function (errors) {
      var errs = convertErrors(errors);
      callback && callback(errs && errs[0]);

      if (errs) {
        reject(errs[0]);
      } else {
        resolve(null);
      }
    });
  });
},`

My store string is exceptionally escaped as well ..

"{\"networkStatus\":\"{\\\"isFetching\\\":false,\\\"error\\\":\\\"\\\",\\\"data\\\":{\\\"connected\\\":true, ...

What other info can I provide? Any workarounds or fixes for this?

EDIT: tried throttling the socket data to once every 3s. No change.

ajp8164 avatar Jul 07 '20 21:07 ajp8164

I resolve this problem by using rn-fetch-blob to writeStream data to file. If you still want to use redux-perist, custom your storage and serialize function, in setItems function, split data and stringify at that time.

nguyenhose avatar Jul 09 '20 17:07 nguyenhose

I resolve this problem by using rn-fetch-blob to writeStream data to file. If you still want to use redux-perist, custom your storage and serialize function, in setItems function, split data and stringify at that time.

Can you elaborate on "split data" - do you mean just arbitrarily split by json string (say in half) and persist each half (then re-combine on read)?

ajp8164 avatar Jul 09 '20 17:07 ajp8164

@ajp8164 it's depend on data you have, for me it's just a large array (8 thousand records), so I can easily split it to [[array1], [arrays2]...] and each array have 200 items which able to stringify.

// data is a list of array
writeStreamData: (data, callback) => {
   return RNFetchBlob.fs.writeStream(pathFile, 'utf8').then((stream) => {
    // put a unique key so you can split it again when retrieving data
     return Promise.all([data.map(d => stream.write(JSON.stringify(d)+'unique_key'))])
   }).then(([stream]) => stream.close())
   .catch(error => {
     if (!callback) {
       throw error;
     }
   });
 },

Update: An example code ;)

nguyenhose avatar Jul 10 '20 06:07 nguyenhose

I think this is a complex problem that cant be fixed in one place. But I can identify at least one place that effectively double memory consumption at persist times.

Here:

  1. let stagedState = {} This is an object that contains already serialized slices of state for each top-level key. They are not cleared, only set.
  2. stagedState[key] = serialize(endState) So after first write, application already have 2 copies of data - real data and serialized in stagedState, than it create 3rd copy by call serialize again and oops, OutOfMemory.

We in our application have state which persistent part contained in 4-5Mb json file. And sometimes we seen these crashes.

vovkasm avatar Aug 21 '20 21:08 vovkasm

Thanks for the clues, @vovkasm. Here's a workaround that removes the extra layer of serialization in stagedState by doing all the serialization in the storage layer instead:

import AsyncStorage from '@react-native-community/async-storage';

const storage = {
  ...AsyncStorage,
  getItem: async (key: string) => {
    const value = await AsyncStorage.getItem(key);
    if (value === null) {
      return null;
    }
    return JSON.parse(value);
  },
  setItem: async (key: string, value: mixed) =>
    AsyncStorage.setItem(key, JSON.stringify(value)),
};

const persistConfig = {
  key: 'root',
  version: 1,
  storage,
  serialize: false,
  deserialize: false,
};

I'm hoping this will reduce the out-of-memory errors I see in production.

brsaylor2 avatar Sep 02 '20 23:09 brsaylor2

@brsaylor2 did that fix the issue for you?

Also I don't understand how moving the serialization over to your async storage wrapper would fix the issue? The problem seems to be that we're trying to serialize too much data at once. Am I missing something?

Thanks in advance for sharing your thoughts.

SudoPlz avatar Sep 17 '20 13:09 SudoPlz

Thanks to all the comments above.

It seems like the number of issues are related. Especially as @vovkasm mentioned, multiple serializations cause large memory usage.

A solution from @brsaylor2 gave me an idea of how I can split data to store/rehydrate.

import rootReducer from 'reducer' // whereever you define, I use `combineReducers()`
const topLevelKeys = Object.keys(rootReducer({}, {}))

const storage = {
  ...AsyncStorage,
  getItem: async (baseKey) => {
    const data = {}
    await Promise.all(topLevelKeys.map(async key => {
      const value = await AsyncStorage.getItem(`${baseKey}:${key}`)
      if(value){
        data[key] = JSON.parse(value)
      }
    }))
    return data
  },
  setItem: async (baseKey, value) => {
    topLevelKeys.map(async key => {
      if(value[key]){
        AsyncStorage.setItem(
          `${baseKey}:${key}`,
          JSON.stringify(value[key])
        )
      }
    })
  }
}

Keep in mind this solution could slow your app especially if you have many top-level keys. Also, you might need to further split by keys if you have large dataset inside of the objects.

foloinfo avatar Oct 15 '20 06:10 foloinfo

@brsaylor2 did that fix the issue for you?

Also I don't understand how moving the serialization over to your async storage wrapper would fix the issue? The problem seems to be that we're trying to serialize too much data at once. Am I missing something?

It seems to have fixed the issue. Moving the serialization to the AsyncStorage wrapper is just a way to prevent redux-persist from doing two serialization passes, where the second pass is processing a bunch of large already-serialized strings.

brsaylor2 avatar Oct 21 '20 22:10 brsaylor2

@brsaylor2 did your solution still fixed the issue?

jwLoginnove avatar Feb 24 '21 21:02 jwLoginnove

@brsaylor2 did your solution still fixed the issue?

Yes, I haven't had any reports of this crash since implementing that workaround.

brsaylor2 avatar Feb 24 '21 21:02 brsaylor2

import AsyncStorage from '@react-native-community/async-storage';

const storage = {
  ...AsyncStorage,
  getItem: async (key: string) => {
    const value = await AsyncStorage.getItem(key);
    if (value === null) {
      return null;
    }
    return JSON.parse(value);
  },
  setItem: async (key: string, value: mixed) =>
    AsyncStorage.setItem(key, JSON.stringify(value)),
};

const persistConfig = {
  key: 'root',
  version: 1,
  storage,
  serialize: false,
  deserialize: false,
};

I'm hoping this will reduce the out-of-memory errors I see in production.

I implemented this but after checking my previously persisted state was gone, had to persist everything from the start... any idea why is that happening? i changed my code like this: Screen Shot 2021-02-25 at 1 10 42 PM

summerkiflain avatar Feb 25 '21 08:02 summerkiflain

@summerkiflain when you set serialize & deserialize to false then you have to handle case in which existing data which is already deserialize

export const storage = {
    setItem: async (key, value) => {
      return set(key, value);
    },
    getItem: async key => {
      let value = await get(key);
      
      //for backward compatibility
      if (typeof value === 'string') {
        value = JSON.parse(value);
      }
  
      return value;
    },
    removeItem: async key => {
      await remove(key);
    },
};
  

smali-kazmi avatar Jan 19 '22 12:01 smali-kazmi

It causes my app to crash too. Here's the error I got: image react": "18.1.0 react-native": "0.70.5 react-native-mmkv": "^2.5.1 reduxjs-toolkit-persist": "^7.2.1 react-redux": "^8.0.5

Device: Pixel 4 API 30 Android 11.0 Google Play | x86

const persistConfig = {
  key: 'root',
  storage: storage,
  blacklist: ['user'],
  stateReconciler: autoMergeLevel1,
};

const reducers = combineReducers({
  splash: splashReducer,
  search: searchReducer,
  user: userReducer,
  team: teamReducer,
  point: pointReducer,
  fetching: fetchingReducer,
  route: routeReducer,
  screenOrientation: screenOrientationReducer,
  tabBar: tabBarReducer,
  navigation: navigationReducer,
  prize: prizeReducer,
  quiz: quizReducer,
  question: questionReducer,
  news: newsReducer,
  event: eventReducer,
  ranking: rankingReducer,
  push_notification: pushNotificationReducer,
  fcm: fcmReducer,
  [apiSlice.reducerPath]: apiSlice.reducer,
});

const _persistedReducer = persistReducer(persistConfig, reducers);

export const store = configureStore({
  reducer: _persistedReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      immutableCheck: false,
      serializableCheck: false,
    }).concat([apiSlice.middleware]),
});

setupListeners(store.dispatch);

And I'm also using this function wait(timeout); a lot in my react native app is this what it triggers it or it's the storage? To get rid of memory leak I use clearTimeout is this true?

const wait = (timeout) => {
  return new Promise((resolve) => {
    const timer = setTimeout(() => {resolve(timer)}, timeout);
  });
};

const timer = await wait(1000);
clearTimeout(timer);

hildebrandjp avatar May 11 '23 03:05 hildebrandjp