Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

getEntityRecords resolver crashes on Android when fetching multiple items #29928

Closed
fluiddot opened this issue Mar 17, 2021 · 18 comments
Closed
Labels
Mobile App - i.e. Android or iOS Native mobile impl of the block editor. (Note: used in scripts, ping mobile folks to change) [Type] Bug An existing feature does not function as intended

Comments

@fluiddot
Copy link
Contributor

fluiddot commented Mar 17, 2021

Description

When the editor is opened, the reusable blocks are fetched from the site via this selector in the useBlockEditorSettings hook.

The selector used in this case is getEntityRecords that has a resolver associated which executes the fetch request and updates the state with the response.

This resolver apart from fetching, it also updates the entity cache for each item, this way if the selector getEntityRecord is called for getting one of the reusable blocks, it will get it from the cache instead of doing a fetch request as we already fetched that item in getEntityRecords.

The cache update is done by calling the actions START_RESOLUTION and FINISH_RESOLUTION for each item, these actions are the same that would be triggered when the item is fetched via the getEntityRecord resolver (related code). Initially this shouldn't be a problem but I realised that each action, as well as any other routine (control or promise), called from the resolver is nested which could end up with a long call stack.

As an example, in the following logs you can see how many routines are nested when fetching 30 reusable blocks.

These logs have been obtained by adding logs in the function (createRuntime) of the Redux middleware for generator coroutines package, that handles the routines execution..

Logs

The log format is <ROUTINE_TYPE> => <CALLSTACK_DEPTH> <ROUTINE_ARGS>

  • ROUTINE_TYPE: There're three types, control, promise and unhandledActionControl (more info here).
  • CALLSTACK_DEPTH: The depth of the routine in the call stack.
  • ROUTINE_ARGS: The arguments of the routine.
[getEntityRecords resolver]

control =>                  {"depth": 1}    {"action": "@@data/SELECT", "selectorName": "hasStartedResolution"}
control =>                  {"depth": 2}    {"action": "@@data/SELECT", "selectorName": "getEntitiesByKind"}
unhandledActionControl =>   {"depth": 3}    {"action": "ENQUEUE_LOCK_REQUEST"}
unhandledActionControl =>   {"depth": 4}    {"action": "PROCESS_PENDING_LOCK_REQUESTS"}
control =>                  {"depth": 5}    {"action": "@@data/SELECT", "selectorName": "__unstableGetPendingLockRequests"}
control =>                  {"depth": 6}    {"action": "@@data/SELECT", "selectorName": "__unstableIsLockAvailable"}
unhandledActionControl =>   {"depth": 7}    {"action": "GRANT_LOCK_REQUEST"}
promise =>                  {"depth": 8}    {"action": "AWAIT_PROMISE"}
promise =>                  {"depth": 9}    {"action": "API_FETCH"}
unhandledActionControl =>   {"depth": 10}   {"action": "RELEASE_LOCK"}
unhandledActionControl =>   {"depth": 11}   {"action": "PROCESS_PENDING_LOCK_REQUESTS"}
control =>                  {"depth": 12}   {"action": "@@data/SELECT", "selectorName": "__unstableGetPendingLockRequests"}
unhandledActionControl =>   {"depth": 13}   {"action": "RECEIVE_ITEMS"}
unhandledActionControl =>   {"depth": 14}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 15}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 16}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 17}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 18}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 19}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 20}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 21}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 22}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 23}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 24}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 25}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 26}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 27}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 28}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 29}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 30}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 31}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 32}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 33}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 34}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 35}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 36}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 37}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 38}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 39}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 40}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 41}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 42}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 43}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 44}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 45}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 46}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 47}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 48}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 49}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 50}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 51}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 52}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 53}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 54}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 55}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 56}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 57}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 58}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 59}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 60}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 61}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 62}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 63}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 64}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 65}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 66}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 67}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 68}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 69}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 70}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 71}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 72}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 73}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 74}   {"action": "START_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 75}   {"action": "FINISH_RESOLUTION", "selectorName": "getEntityRecord"}
unhandledActionControl =>   {"depth": 76}   {"action": "RELEASE_LOCK"}
unhandledActionControl =>   {"depth": 77}   {"action": "PROCESS_PENDING_LOCK_REQUESTS"}
control =>                  {"depth": 78}   {"action": "@@data/SELECT", "selectorName": "__unstableGetPendingLockRequests"}

As you can see depending on the number of items fetched, more START_RESOLUTION and FINISH_RESOLUTION actions are executed. The problem here is that on Android when there's more than 27 items, this exceeds the maximum call stack size, in the following error stacktrace you can see the error.

Error stacktrace
RangeError: Maximum call stack size exceeded (native stack depth)
    at getValuePair (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:159649:17)
    at get (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:159758:37)
    at isRunning (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:158751:62)
    at fulfillSelector$ (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:159009:49)
    at call (native)
    at tryCatch (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:26169:23)
    at invoke (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:26342:32)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:26212:30)
    at call (native)
    at tryCatch (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:26169:23)
    at invoke (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:26242:30)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:26272:19)
    at tryCallTwo (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:27531:9)
    at doResolve (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:27695:25)
    at Promise (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:27554:14)
    at callInvokeWithMethodAndArg (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:26271:33)
    at enqueue (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:26276:157)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:26212:30)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:26293:69)
    at fulfillSelector (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:159003:44)
    at apply (native)
    at selectorResolver (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:159057:30)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:285359:51)
    at apply (native)
    at selector (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160757:62)
    at getCurrentPostAttribute (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:285413:34)
    at getEditedPostAttribute (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:285442:37)
    at apply (native)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:158815:34)
    at apply (native)
    at runSelector (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:158923:38)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:294268:36)
    at mapSelect (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:161460:34)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:163325:45)
    at call (native)
    at __experimentalMarkListeningStores (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:158583:33)
    at apply (native)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:158620:37)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:163281:56)
    at onStoreChange (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:163324:42)
    at onChange (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:163351:24)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:158855:23)
    at dispatch (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:159256:17)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:159836:24)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160790:20)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160836:22)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:158936:46)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:273428:21)
    at commitHookEffectList (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:20962:38)
    at commitPassiveHookEffects (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:20995:37)
    ... skipping 488 frames
    at next (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160212:24)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160204:19)
    at any (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160265:14)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160213:27)
    at some (native)
    at next (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160212:24)
    at unhandledActionControl (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:159929:11)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160213:27)
    at some (native)
    at next (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160212:24)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160204:19)
    at any (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160265:14)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160213:27)
    at some (native)
    at next (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160212:24)
    at unhandledActionControl (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:159929:11)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160213:27)
    at some (native)
    at next (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160212:24)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160204:19)
    at any (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160265:14)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160213:27)
    at some (native)
    at next (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160212:24)
    at unhandledActionControl (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:159929:11)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160213:27)
    at some (native)
    at next (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160212:24)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160204:19)
    at any (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160265:14)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160213:27)
    at some (native)
    at next (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160212:24)
    at unhandledActionControl (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:159929:11)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160213:27)
    at some (native)
    at next (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160212:24)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:160204:19)
    at tryCallOne (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:27522:16)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:27623:27)
    at apply (native)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:31760:26)
    at _callTimer (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:31649:17)
    at _callImmediatesPass (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:31685:19)
    at callImmediates (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:31904:33)
    at __callImmediates (native)
    at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:3151:34)
    at __guard (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:3357:15)
    at flushedQueue (http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false:3150:21)
    at invokeCallbackAndReturnFlushedQueue (native)

This error is not happening on the web and iOS version because most likely the threshold of the call stack size is higher than on Android. On Android we use Hermes (a JavaScript engine optimized for React Native), the code related to this value is located here.

Solution

Add batched actions (START_RESOLUTIONS and FINISH_RESOLUTIONS)

Instead of yield a start and finish action for each item, we would batch them and yield only two actions each time a page of results is received.

These actions would accept an array of args arrays to mark as resolving/resolved. Such batched actions would also prevent firing too many change events. Now, when receiving a page of 100 items, the store fires 200+ change events.

Thanks @jsnajdr for proposing this solution in this comment.

Other potential solutions

Prevent the call stack to grow excessively when executing routines

I was thinking about the option of changing the way that next routines are called when running an unhandled action controls. Instead of calling the function we could use Microtasks.

Another option would be to use tasks directly and wrapping the call with setTimeout.

I'm not very familiar with the Redux middleware for generator coroutines package so I'd appreciate feedback for this option.

Increase native stack depth on Hermes

This solution would also work but it would imply to maintain and build our version of Hermes which could be overkilling. I found out some documentation about this.

Prevent getEntityRecords resolver to update the entity cache for each item

This would also work because we would ensure that just a small number of routines are executed. However this would be a temporary workaround because if in the future we have another resolver that executes a lot of routines, we would have a new crash.

For this option we could use the _fields query parameter or add a condition in the resolver that skips this part if the platform is native.

Step-by-step reproduction instructions

  1. Add more than 27 reusable blocks to a site (28 items should trigger the exception), this has to be done in the web version because creating reusable blocks from the app is not yet available.
  2. Open a post from the WPAndroid app.
  3. Observe that no crash or exception is produced.

Expected behaviour

The editor shouldn't crash when it's opened.

Actual behaviour

The editor crashes when is opened.

Screenshots or screen recording (optional)

N/A

WordPress information

  • WordPress version: 16.9
  • Gutenberg version: N/A
  • Are all plugins except Gutenberg deactivated? N/A
  • Are you using a default theme (e.g. Twenty Twenty-One)? N/A

Device information

  • Device: Samsung Galaxy S20 FE 5G
  • Operating system: Android 10
  • WordPress app version: 16.9
@fluiddot fluiddot added [Type] Bug An existing feature does not function as intended Mobile App - i.e. Android or iOS Native mobile impl of the block editor. (Note: used in scripts, ping mobile folks to change) labels Mar 17, 2021
@fluiddot
Copy link
Contributor Author

This crash has been prevented with a workaround in this PR.

@fluiddot
Copy link
Contributor Author

@youknowriad I was wondering if you could help me by providing some feedback regarding the solution "Prevent recursion when executing actions" as I saw you already worked in the @wordpress/redux-routine package and you probably have good insights about it.

Thank you very much for the help 🙇 !

@youknowriad
Copy link
Contributor

Hi There! reading the report above, it's not clear to me where the recursion happens (where the depth increases), those "START" / "FINISH" resolution actions don't trigger recursion as far as I know?

@fluiddot
Copy link
Contributor Author

Hi There! reading the report above, it's not clear to me where the recursion happens (where the depth increases), those "START" / "FINISH" resolution actions don't trigger recursion as far as I know?

Yeah, the "START" / "FINISH" resolution doesn't trigger other actions.

The problem is more related to how the unhandled action controls are executed in @wordpress/redux-routine when they're triggered one after the other. In this case, each yield call with an action object like "START" or "FINISH" increases one level the depth.

Let me show it better with a stacktrace that I took adding a breakpoint when the last item in the for loop was executed:

Stacktrace (with comments)
getEntityRecords$ (resolvers.js:226)
tryCatch (runtime.js:63)
invoke (runtime.js:293)
(anonymous) (runtime.js:118)
(anonymous) (create.js:32)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50) => This action's type is FINISH_RESOLUTION (triggered by the last item in the loop)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50) => This action's type is START_RESOLUTION (triggered by the last item in the loop)

[This sequence of the stracktrace is repeated for each item in the loop, in this case 28 times]
================================
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
================================

(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50) => This action's type is FINISH_RESOLUTION (triggered by the second item in the loop)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50) => This action's type is START_RESOLUTION (triggered by the second item in the loop)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50) => This action's type is FINISH_RESOLUTION (triggered by the first item in the loop) 
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50) => This action's type is START_RESOLUTION (triggered by the first item in the loop)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50) => This action's type is RECEIVE_ITEMS
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
tryCallOne (core.js:37)
(anonymous) (core.js:123)
(anonymous) (JSTimers.js:289)
_callTimer (JSTimers.js:146)
_callImmediatesPass (JSTimers.js:194)
callImmediates (JSTimers.js:458)
__callImmediates (MessageQueue.js:407)
(anonymous) (MessageQueue.js:143)
__guard (MessageQueue.js:384)
flushedQueue (MessageQueue.js:142)
invokeCallbackAndReturnFlushedQueue (MessageQueue.js:138)
(anonymous) (debuggerWorker.js:69)
Worker.postMessage (async)
(anonymous) (index.js:183)
h (runtime.js:45)
(anonymous) (runtime.js:271)
t.<computed> (runtime.js:97)
n (asyncToGenerator.js:3)
c (asyncToGenerator.js:25)
(anonymous) (asyncToGenerator.js:32)
(anonymous) (asyncToGenerator.js:21)
(anonymous) (index.js:158)
Full stacktrace (1420 lines)
getEntityRecords$ (resolvers.js:226)
tryCatch (runtime.js:63)
invoke (runtime.js:293)
(anonymous) (runtime.js:118)
(anonymous) (create.js:32)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
any (builtin.js:15)
(anonymous) (create.js:47)
next (create.js:46)
unhandledActionControl (runtime.js:50)
(anonymous) (create.js:47)
next (create.js:46)
(anonymous) (create.js:38)
tryCallOne (core.js:37)
(anonymous) (core.js:123)
(anonymous) (JSTimers.js:289)
_callTimer (JSTimers.js:146)
_callImmediatesPass (JSTimers.js:194)
callImmediates (JSTimers.js:458)
__callImmediates (MessageQueue.js:407)
(anonymous) (MessageQueue.js:143)
__guard (MessageQueue.js:384)
flushedQueue (MessageQueue.js:142)
invokeCallbackAndReturnFlushedQueue (MessageQueue.js:138)
(anonymous) (debuggerWorker.js:69)
Worker.postMessage (async)
(anonymous) (index.js:183)
h (runtime.js:45)
(anonymous) (runtime.js:271)
t.<computed> (runtime.js:97)
n (asyncToGenerator.js:3)
c (asyncToGenerator.js:25)
(anonymous) (asyncToGenerator.js:32)
(anonymous) (asyncToGenerator.js:21)
(anonymous) (index.js:158)

As you can see in the above stacktrace, it gets bigger for each item in the loop, unfortunately this on Android ends up exceeding the call stack size.

@youknowriad
Copy link
Contributor

So if I'm understanding properly, there's no recursion, it's just that Android has a small stack size right?

  • In which case we could try to refactor the "rungen" code to be more "flat" but it may not be easy to do so.
  • Or just increase the size in Android.

@fluiddot
Copy link
Contributor Author

fluiddot commented Mar 18, 2021

So if I'm understanding properly, there's no recursion, it's just that Android has a small stack size right?

Yes, probably I shouldn't have used the word "recursion" because it's a bit misleading since it's not actually happening (I'll rename it).

EDIT: I've just renamed the solution "Prevent recursion when executing actions" to "Prevent the call stack to grow excessively when executing routines".

  • In which case we could try to refactor the "rungen" code to be more "flat" but it may not be easy to do so.

I was wondering how other libraries handle this case, maybe we can check Redux-saga in case it gives us some ideas.

  • Or just increase the size in Android.

This is one of the potential solutions I added to the issue, but it's neither easy to do because the configuration is not exposed so we should build a custom version.

@jsnajdr
Copy link
Member

jsnajdr commented Mar 18, 2021

I tried to reproduce the growing stack with a minimalistic example that also dispatches a lot of actions inside a generator:

const { dispatch, registerStore } = require( '@wordpress/data' );

registerStore( 'recurse', {
  reducer: ( state = 0, action ) => state,
  actions: {
    curse: function* () {
      for ( let i = 0; i < 100; i++ ) {
        yield { type: 'CURSE' };
        console.trace();
      }
    },
  },
} );

dispatch( 'recurse' ).curse();

But running this doesn't show a stack growing. Each of the 100 console.trace() calls reports the same flat stacktrace:

Trace
    at curse (/Users/jsnajdr/src/gutenberg/genstack.js:9:13)
    at curse.next (<anonymous>)
    at /Users/jsnajdr/src/gutenberg/node_modules/rungen/dist/create.js:32:55
    at any (/Users/jsnajdr/src/gutenberg/node_modules/rungen/dist/controls/builtin.js:15:3)
    at /Users/jsnajdr/src/gutenberg/node_modules/rungen/dist/create.js:47:18
    at Array.some (<anonymous>)
    at next (/Users/jsnajdr/src/gutenberg/node_modules/rungen/dist/create.js:46:18)
    at unhandledActionControl (/Users/jsnajdr/src/gutenberg/packages/redux-routine/build/runtime.js:62:5)
    at /Users/jsnajdr/src/gutenberg/node_modules/rungen/dist/create.js:47:18
    at Array.some (<anonymous>)

So, what does the code that dispatches START_RESOLUTION in a loop, for each record, do differently?

@jsnajdr
Copy link
Member

jsnajdr commented Mar 18, 2021

Each of the 100 console.trace() calls reports the same flat stacktrace:

This seems to be some quirk in how console.trace() prints the stack. When running the code in debugger, I see a growing call stack on each loop iteration indeed.

It happens because rungen runs the generator using a tail recursion. A minimal example is this:

function* curse( times ) {
  for ( let i = 0; i < times; i++ ) {
    yield `curse ${ i }`;
  }
}

function process( value, next ) {
  console.log( value );
  next();
}

function rungen( iter ) {
  function next() {
    const n = iter.next();
    if ( ! n.done ) {
      process( n.value, next );
    }
  }

  next();
}

rungen( curse( 10000 ) );

Running this program will terminate with stack overflow after 5652 iterations.

I'm afraid this is a fundamental shortcoming in rungen that can't be avoided. The continuation-style next callbacks are there to allow async behavior. Note that process() doesn't need to call next synchronously, but the call can be asynchronous, too. Only that synchronous calls grow the call stack.

I hope this shortcoming will be removed once we migrate the rungen controls to async thunk functions.

@fluiddot
Copy link
Contributor Author

This seems to be some quirk in how console.trace() prints the stack. When running the code in debugger, I see a growing call stack on each loop iteration indeed.

Running this program will terminate with stack overflow after 5652 iterations.

That's exactly what I was experiencing, unfortunately on Android the threshold for triggering the stack overflow is quite lower (around 1200 iterations).

I'm afraid this is a fundamental shortcoming in rungen that can't be avoided. The continuation-style next callbacks are there to allow async behavior. Note that process() doesn't need to call next synchronously, but the call can be asynchronous, too. Only that synchronous calls grow the call stack.

Yeah, actually in the case I exposed in this issue, the problem comes from a for loop that runs unhandled action controls which are synchronous calls and therefore make the call stack grow.

I hope this shortcoming will be removed once we migrate the rungen controls to async thunk functions.

Oh that's very interesting, I didn't know that we were migrating it.

Thank you very much @jsnajdr for checking this and providing the example, it's very descriptive 🙇 !

@jsnajdr
Copy link
Member

jsnajdr commented Mar 18, 2021

A brief look at Redux Saga shows me that they have the same problem. They also have a next function that advances the iterator and processes the return value with a handler, passing next as a continuation callback.

This is probably the fate of all generator-based effect runtimes that try to be sync and async at the same time.

It could be solved if JavaScript could do tail call optimizations, but most engines don't.

@fluiddot
Copy link
Contributor Author

A brief look at Redux Saga shows me that they have the same problem. They also have a next function that advances the iterator and processes the return value with a handler, passing next as a continuation callback.

This is probably the fate of all generator-based effect runtimes that try to be sync and async at the same time.

It could be solved if JavaScript could do tail call optimizations, but most engines don't.

I also wanted to check how Redux Saga handles this so thanks for exploring it beforehand 🙇 . I'm surprised that they have the same problem 😞 .

Thinking out loud, I wonder if a potential solution would be to use microtasks although it's still not fully supported. Ideally, as you commented, this problem would be addressed if the JS engine do tail call optimizations but at least with this option we would defer the execution of next and prevent the call stack to grow.

@jsnajdr
Copy link
Member

jsnajdr commented Mar 19, 2021

Thinking out loud, I wonder if a potential solution would be to use microtasks although it's still not fully supported.

Queuing a microtask is already possible with Promise.resolve().then( task ). But that would make all rungen tasks asynchronous, even if they were originally synchronous. Potentially introducing race conditions and other subtle bugs.

@hypest
Copy link
Contributor

hypest commented Mar 19, 2021

if the selector getEntityRecord is called for getting one of the reusable blocks, it will get it from the cache instead of doing a fetch request as we already fetched that item in getEntityRecords.

I wonder, the list of defined reusable blocks is not related to the post/page currently open in the editor so, sounds to me like data that we should fetch in the parent mobile app. Like, fetch it like we fetch other site data and keep it in local DB. The mobile apps act as a glorified cache anyway so, maybe populating the reusable blocks cache can be moved there. It won't be a "real" solution to the call stack depth limit problem, but still makes sense architecturally. Am I perhaps missing something? 🤔

@jsnajdr
Copy link
Member

jsnajdr commented Mar 19, 2021

A reasonable solution would be to introduce batched actions START_RESOLUTIONS and FINISH_RESOLUTIONS that would accept an array of args arrays to mark as resolving/resolved. Then we would always yield only two actions each time a page of results is received.

Such batched actions would also prevent firing too many change events. Now, when receiving a page of 100 items, the store fires 200+ change events.

@fluiddot
Copy link
Contributor Author

I wonder, the list of defined reusable blocks is not related to the post/page currently open in the editor so, sounds to me like data that we should fetch in the parent mobile app. Like, fetch it like we fetch other site data and keep it in local DB. The mobile apps act as a glorified cache anyway so, maybe populating the reusable blocks cache can be moved there. It won't be a "real" solution to the call stack depth limit problem, but still makes sense architecturally. Am I perhaps missing something? 🤔

Yes, the list of defined reusable blocks is not related to the post/page but to the site.

This is an interesting solution I didn't consider, my approach for the reusable blocks was to follow the same flow as we have in the web version. However if architecturally makes more sense to rely on the parent mobile app for fetching data, this is a solution that I definitely I'd like to explore.

Thanks @hypest for pointing this solution out 🙇 .

@fluiddot
Copy link
Contributor Author

A reasonable solution would be to introduce batched actions START_RESOLUTIONS and FINISH_RESOLUTIONS that would accept an array of args arrays to mark as resolving/resolved. Then we would always yield only two actions each time a page of results is received.

Such batched actions would also prevent firing too many change events. Now, when receiving a page of 100 items, the store fires 200+ change events.

This is a great idea @jsnajdr! Add the batched actions (START_RESOLUTIONS and FINISH_RESOLUTIONS) would solve the problem for sure and as you commented, moreover it would reduce the store change events which is a good performance improvement. I'm going to add this solution to the description and mark it as the optimal one for fixing this issue.

Regarding the problem with the reusable blocks, I'm going to consider both options (the one about moving the logic to the mobile app and this one). So I’m going to keep this issue open because this would still be a potential problem if we use getEntityRecords on somewhere else.

@fluiddot
Copy link
Contributor Author

👋 Reviving this issue:

I worked on adding the batched actions as @jsnajdr pointed in a previous comment. I created a draft PR that adds these new actions and applies them into the getEntityRecords resolver, so far looks like it's working nice 🎊 .

Before proceeding, I'd appreciate it if someone could do an initial review just in case I'm overlooking something important.

@youknowriad @jsnajdr could you help me with this? If not please let me know who would be the right people to assign as reviewers, thanks 🙇 !

@fluiddot
Copy link
Contributor Author

I'm closing this issue since it has been fixed via #31005 🎊 .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Mobile App - i.e. Android or iOS Native mobile impl of the block editor. (Note: used in scripts, ping mobile folks to change) [Type] Bug An existing feature does not function as intended
Projects
None yet
Development

No branches or pull requests

4 participants