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

Async scheduling of batched updates #34295

Closed
wants to merge 5 commits into from

Conversation

adamziel
Copy link
Contributor

@adamziel adamziel commented Aug 25, 2021

Description

Calling __experimentalBatch() with a few dozens requests causes the UI to freeze like that:

130773120-a0390dec-9620-48d4-9112-44a4aae2bd7a

This is because the saving state hinges on isSavingEntityRecord() which changes only once the appropriate redux actions are dispatched. Currently we do it in a blocking way like this:

// this calls dispatch().saveEditedEntityRecord and such
const resultPromises = requests.map( ( request ) => request( api ) ); 

Which is fine for a small number of requests. Unfortunately, for many requests it takes some seconds:

+ console.time( "Before add requests" )
const resultPromises = requests.map( ( request ) => request( api ) );
+ console.timeEnd( "Before add requests" )

Zrzut ekranu 2021-08-25 o 13 19 24

In this PR I propose an async way of enqueuing updates. After every request( api ) call, we await the next frame. This allows the browser to move on to other tasks, update the UI, switch to the "Saving..." state etc.

Note that this is a one-off fix for something that I believe is a symptom of using redux-rungen instead of async/await. I explored refactoring that with @jsnajdr – it would make core-data async by default and potentially alleviate this problem. I'd love to pick up that work again – see #33201.

How has this been tested?

  1. Go to the widgets editor
  2. Add 50 widgets
  3. Click "Update"
  4. Confirm that it changes to "Saving..." pretty much immediately

Types of changes

Bug fix (non-breaking change which fixes an issue)

@adamziel adamziel added the [Package] Core data /packages/core-data label Aug 25, 2021
@adamziel adamziel self-assigned this Aug 25, 2021
@adamziel adamziel requested a review from nerrad as a code owner August 25, 2021 11:27
@adamziel adamziel force-pushed the update/async-batch-scheduling branch from 1818673 to d9b7bb9 Compare August 25, 2021 11:28
@github-actions
Copy link

github-actions bot commented Aug 25, 2021

Size Change: +94 B (0%)

Total Size: 1.04 MB

Filename Size Change
build/block-editor/index.min.js 118 kB +22 B (0%)
build/components/index.min.js 209 kB +38 B (0%)
build/core-data/index.min.js 12.4 kB +38 B (0%)
build/editor/index.min.js 37.7 kB -4 B (0%)
ℹ️ View Unchanged
Filename Size
build/a11y/index.min.js 931 B
build/admin-manifest/index.min.js 1.09 kB
build/annotations/index.min.js 2.7 kB
build/api-fetch/index.min.js 2.19 kB
build/autop/index.min.js 2.08 kB
build/blob/index.min.js 459 B
build/block-directory/index.min.js 6.2 kB
build/block-directory/style-rtl.css 1.01 kB
build/block-directory/style.css 1.01 kB
build/block-editor/style-rtl.css 13.8 kB
build/block-editor/style.css 13.8 kB
build/block-library/blocks/archives/editor-rtl.css 61 B
build/block-library/blocks/archives/editor.css 60 B
build/block-library/blocks/archives/style-rtl.css 65 B
build/block-library/blocks/archives/style.css 65 B
build/block-library/blocks/audio/editor-rtl.css 58 B
build/block-library/blocks/audio/editor.css 58 B
build/block-library/blocks/audio/style-rtl.css 111 B
build/block-library/blocks/audio/style.css 111 B
build/block-library/blocks/audio/theme-rtl.css 125 B
build/block-library/blocks/audio/theme.css 125 B
build/block-library/blocks/block/editor-rtl.css 161 B
build/block-library/blocks/block/editor.css 161 B
build/block-library/blocks/button/editor-rtl.css 474 B
build/block-library/blocks/button/editor.css 474 B
build/block-library/blocks/button/style-rtl.css 605 B
build/block-library/blocks/button/style.css 604 B
build/block-library/blocks/buttons/editor-rtl.css 315 B
build/block-library/blocks/buttons/editor.css 315 B
build/block-library/blocks/buttons/style-rtl.css 370 B
build/block-library/blocks/buttons/style.css 370 B
build/block-library/blocks/calendar/style-rtl.css 207 B
build/block-library/blocks/calendar/style.css 207 B
build/block-library/blocks/categories/editor-rtl.css 84 B
build/block-library/blocks/categories/editor.css 83 B
build/block-library/blocks/categories/style-rtl.css 79 B
build/block-library/blocks/categories/style.css 79 B
build/block-library/blocks/code/style-rtl.css 90 B
build/block-library/blocks/code/style.css 90 B
build/block-library/blocks/code/theme-rtl.css 131 B
build/block-library/blocks/code/theme.css 131 B
build/block-library/blocks/columns/editor-rtl.css 194 B
build/block-library/blocks/columns/editor.css 193 B
build/block-library/blocks/columns/style-rtl.css 474 B
build/block-library/blocks/columns/style.css 475 B
build/block-library/blocks/cover/editor-rtl.css 666 B
build/block-library/blocks/cover/editor.css 670 B
build/block-library/blocks/cover/style-rtl.css 1.23 kB
build/block-library/blocks/cover/style.css 1.23 kB
build/block-library/blocks/embed/editor-rtl.css 488 B
build/block-library/blocks/embed/editor.css 488 B
build/block-library/blocks/embed/style-rtl.css 400 B
build/block-library/blocks/embed/style.css 400 B
build/block-library/blocks/embed/theme-rtl.css 124 B
build/block-library/blocks/embed/theme.css 124 B
build/block-library/blocks/file/editor-rtl.css 300 B
build/block-library/blocks/file/editor.css 300 B
build/block-library/blocks/file/style-rtl.css 255 B
build/block-library/blocks/file/style.css 255 B
build/block-library/blocks/file/view.min.js 322 B
build/block-library/blocks/freeform/editor-rtl.css 2.44 kB
build/block-library/blocks/freeform/editor.css 2.44 kB
build/block-library/blocks/gallery/editor-rtl.css 879 B
build/block-library/blocks/gallery/editor.css 876 B
build/block-library/blocks/gallery/style-rtl.css 1.7 kB
build/block-library/blocks/gallery/style.css 1.7 kB
build/block-library/blocks/gallery/theme-rtl.css 122 B
build/block-library/blocks/gallery/theme.css 122 B
build/block-library/blocks/group/editor-rtl.css 159 B
build/block-library/blocks/group/editor.css 159 B
build/block-library/blocks/group/style-rtl.css 57 B
build/block-library/blocks/group/style.css 57 B
build/block-library/blocks/group/theme-rtl.css 70 B
build/block-library/blocks/group/theme.css 70 B
build/block-library/blocks/heading/editor-rtl.css 152 B
build/block-library/blocks/heading/editor.css 152 B
build/block-library/blocks/heading/style-rtl.css 76 B
build/block-library/blocks/heading/style.css 76 B
build/block-library/blocks/home-link/style-rtl.css 247 B
build/block-library/blocks/home-link/style.css 247 B
build/block-library/blocks/html/editor-rtl.css 283 B
build/block-library/blocks/html/editor.css 284 B
build/block-library/blocks/image/editor-rtl.css 728 B
build/block-library/blocks/image/editor.css 728 B
build/block-library/blocks/image/style-rtl.css 482 B
build/block-library/blocks/image/style.css 487 B
build/block-library/blocks/image/theme-rtl.css 124 B
build/block-library/blocks/image/theme.css 124 B
build/block-library/blocks/latest-comments/style-rtl.css 284 B
build/block-library/blocks/latest-comments/style.css 284 B
build/block-library/blocks/latest-posts/editor-rtl.css 137 B
build/block-library/blocks/latest-posts/editor.css 137 B
build/block-library/blocks/latest-posts/style-rtl.css 528 B
build/block-library/blocks/latest-posts/style.css 527 B
build/block-library/blocks/list/style-rtl.css 63 B
build/block-library/blocks/list/style.css 63 B
build/block-library/blocks/media-text/editor-rtl.css 266 B
build/block-library/blocks/media-text/editor.css 263 B
build/block-library/blocks/media-text/style-rtl.css 488 B
build/block-library/blocks/media-text/style.css 485 B
build/block-library/blocks/more/editor-rtl.css 431 B
build/block-library/blocks/more/editor.css 431 B
build/block-library/blocks/navigation-link/editor-rtl.css 474 B
build/block-library/blocks/navigation-link/editor.css 474 B
build/block-library/blocks/navigation-link/style-rtl.css 94 B
build/block-library/blocks/navigation-link/style.css 94 B
build/block-library/blocks/navigation/editor-rtl.css 1.69 kB
build/block-library/blocks/navigation/editor.css 1.69 kB
build/block-library/blocks/navigation/style-rtl.css 1.68 kB
build/block-library/blocks/navigation/style.css 1.67 kB
build/block-library/blocks/navigation/view.min.js 2.52 kB
build/block-library/blocks/nextpage/editor-rtl.css 395 B
build/block-library/blocks/nextpage/editor.css 395 B
build/block-library/blocks/page-list/editor-rtl.css 310 B
build/block-library/blocks/page-list/editor.css 310 B
build/block-library/blocks/page-list/style-rtl.css 242 B
build/block-library/blocks/page-list/style.css 242 B
build/block-library/blocks/paragraph/editor-rtl.css 157 B
build/block-library/blocks/paragraph/editor.css 157 B
build/block-library/blocks/paragraph/style-rtl.css 248 B
build/block-library/blocks/paragraph/style.css 248 B
build/block-library/blocks/post-author/editor-rtl.css 210 B
build/block-library/blocks/post-author/editor.css 210 B
build/block-library/blocks/post-author/style-rtl.css 182 B
build/block-library/blocks/post-author/style.css 181 B
build/block-library/blocks/post-comments-form/style-rtl.css 140 B
build/block-library/blocks/post-comments-form/style.css 140 B
build/block-library/blocks/post-comments/style-rtl.css 360 B
build/block-library/blocks/post-comments/style.css 359 B
build/block-library/blocks/post-content/editor-rtl.css 138 B
build/block-library/blocks/post-content/editor.css 138 B
build/block-library/blocks/post-excerpt/editor-rtl.css 73 B
build/block-library/blocks/post-excerpt/editor.css 73 B
build/block-library/blocks/post-excerpt/style-rtl.css 69 B
build/block-library/blocks/post-excerpt/style.css 69 B
build/block-library/blocks/post-featured-image/editor-rtl.css 398 B
build/block-library/blocks/post-featured-image/editor.css 398 B
build/block-library/blocks/post-featured-image/style-rtl.css 143 B
build/block-library/blocks/post-featured-image/style.css 143 B
build/block-library/blocks/post-template/editor-rtl.css 99 B
build/block-library/blocks/post-template/editor.css 98 B
build/block-library/blocks/post-template/style-rtl.css 378 B
build/block-library/blocks/post-template/style.css 379 B
build/block-library/blocks/post-terms/style-rtl.css 73 B
build/block-library/blocks/post-terms/style.css 73 B
build/block-library/blocks/post-title/style-rtl.css 60 B
build/block-library/blocks/post-title/style.css 60 B
build/block-library/blocks/preformatted/style-rtl.css 103 B
build/block-library/blocks/preformatted/style.css 103 B
build/block-library/blocks/pullquote/editor-rtl.css 198 B
build/block-library/blocks/pullquote/editor.css 198 B
build/block-library/blocks/pullquote/style-rtl.css 361 B
build/block-library/blocks/pullquote/style.css 360 B
build/block-library/blocks/pullquote/theme-rtl.css 167 B
build/block-library/blocks/pullquote/theme.css 167 B
build/block-library/blocks/query-pagination-numbers/editor-rtl.css 122 B
build/block-library/blocks/query-pagination-numbers/editor.css 121 B
build/block-library/blocks/query-pagination/editor-rtl.css 270 B
build/block-library/blocks/query-pagination/editor.css 262 B
build/block-library/blocks/query-pagination/style-rtl.css 168 B
build/block-library/blocks/query-pagination/style.css 168 B
build/block-library/blocks/query-title/editor-rtl.css 85 B
build/block-library/blocks/query-title/editor.css 85 B
build/block-library/blocks/query/editor-rtl.css 131 B
build/block-library/blocks/query/editor.css 132 B
build/block-library/blocks/quote/style-rtl.css 169 B
build/block-library/blocks/quote/style.css 169 B
build/block-library/blocks/quote/theme-rtl.css 220 B
build/block-library/blocks/quote/theme.css 222 B
build/block-library/blocks/rss/editor-rtl.css 202 B
build/block-library/blocks/rss/editor.css 204 B
build/block-library/blocks/rss/style-rtl.css 289 B
build/block-library/blocks/rss/style.css 288 B
build/block-library/blocks/search/editor-rtl.css 165 B
build/block-library/blocks/search/editor.css 165 B
build/block-library/blocks/search/style-rtl.css 374 B
build/block-library/blocks/search/style.css 375 B
build/block-library/blocks/search/theme-rtl.css 64 B
build/block-library/blocks/search/theme.css 64 B
build/block-library/blocks/separator/editor-rtl.css 99 B
build/block-library/blocks/separator/editor.css 99 B
build/block-library/blocks/separator/style-rtl.css 250 B
build/block-library/blocks/separator/style.css 250 B
build/block-library/blocks/separator/theme-rtl.css 172 B
build/block-library/blocks/separator/theme.css 172 B
build/block-library/blocks/shortcode/editor-rtl.css 474 B
build/block-library/blocks/shortcode/editor.css 474 B
build/block-library/blocks/site-logo/editor-rtl.css 462 B
build/block-library/blocks/site-logo/editor.css 464 B
build/block-library/blocks/site-logo/style-rtl.css 153 B
build/block-library/blocks/site-logo/style.css 153 B
build/block-library/blocks/site-tagline/editor-rtl.css 86 B
build/block-library/blocks/site-tagline/editor.css 86 B
build/block-library/blocks/site-title/editor-rtl.css 84 B
build/block-library/blocks/site-title/editor.css 84 B
build/block-library/blocks/social-link/editor-rtl.css 165 B
build/block-library/blocks/social-link/editor.css 165 B
build/block-library/blocks/social-links/editor-rtl.css 812 B
build/block-library/blocks/social-links/editor.css 811 B
build/block-library/blocks/social-links/style-rtl.css 1.33 kB
build/block-library/blocks/social-links/style.css 1.33 kB
build/block-library/blocks/spacer/editor-rtl.css 307 B
build/block-library/blocks/spacer/editor.css 307 B
build/block-library/blocks/spacer/style-rtl.css 48 B
build/block-library/blocks/spacer/style.css 48 B
build/block-library/blocks/table/editor-rtl.css 471 B
build/block-library/blocks/table/editor.css 472 B
build/block-library/blocks/table/style-rtl.css 481 B
build/block-library/blocks/table/style.css 481 B
build/block-library/blocks/table/theme-rtl.css 188 B
build/block-library/blocks/table/theme.css 188 B
build/block-library/blocks/tag-cloud/style-rtl.css 146 B
build/block-library/blocks/tag-cloud/style.css 146 B
build/block-library/blocks/template-part/editor-rtl.css 636 B
build/block-library/blocks/template-part/editor.css 635 B
build/block-library/blocks/template-part/theme-rtl.css 101 B
build/block-library/blocks/template-part/theme.css 101 B
build/block-library/blocks/term-description/editor-rtl.css 90 B
build/block-library/blocks/term-description/editor.css 90 B
build/block-library/blocks/text-columns/editor-rtl.css 95 B
build/block-library/blocks/text-columns/editor.css 95 B
build/block-library/blocks/text-columns/style-rtl.css 166 B
build/block-library/blocks/text-columns/style.css 166 B
build/block-library/blocks/verse/style-rtl.css 87 B
build/block-library/blocks/verse/style.css 87 B
build/block-library/blocks/video/editor-rtl.css 571 B
build/block-library/blocks/video/editor.css 572 B
build/block-library/blocks/video/style-rtl.css 173 B
build/block-library/blocks/video/style.css 173 B
build/block-library/blocks/video/theme-rtl.css 124 B
build/block-library/blocks/video/theme.css 124 B
build/block-library/common-rtl.css 1.29 kB
build/block-library/common.css 1.29 kB
build/block-library/editor-rtl.css 9.95 kB
build/block-library/editor.css 9.93 kB
build/block-library/index.min.js 150 kB
build/block-library/reset-rtl.css 527 B
build/block-library/reset.css 527 B
build/block-library/style-rtl.css 11 kB
build/block-library/style.css 11 kB
build/block-library/theme-rtl.css 658 B
build/block-library/theme.css 663 B
build/block-serialization-default-parser/index.min.js 1.09 kB
build/block-serialization-spec-parser/index.min.js 2.79 kB
build/blocks/index.min.js 47 kB
build/components/style-rtl.css 15.7 kB
build/components/style.css 15.8 kB
build/compose/index.min.js 10.2 kB
build/customize-widgets/index.min.js 11.1 kB
build/customize-widgets/style-rtl.css 1.5 kB
build/customize-widgets/style.css 1.49 kB
build/data-controls/index.min.js 614 B
build/data/index.min.js 7.1 kB
build/date/index.min.js 31.5 kB
build/deprecated/index.min.js 428 B
build/dom-ready/index.min.js 304 B
build/dom/index.min.js 4.53 kB
build/edit-navigation/index.min.js 13.6 kB
build/edit-navigation/style-rtl.css 3.15 kB
build/edit-navigation/style.css 3.14 kB
build/edit-post/classic-rtl.css 492 B
build/edit-post/classic.css 494 B
build/edit-post/index.min.js 28.8 kB
build/edit-post/style-rtl.css 7.2 kB
build/edit-post/style.css 7.2 kB
build/edit-site/index.min.js 26.2 kB
build/edit-site/style-rtl.css 5.07 kB
build/edit-site/style.css 5.07 kB
build/edit-widgets/index.min.js 16 kB
build/edit-widgets/style-rtl.css 4.06 kB
build/edit-widgets/style.css 4.06 kB
build/editor/style-rtl.css 3.74 kB
build/editor/style.css 3.73 kB
build/element/index.min.js 3.17 kB
build/escape-html/index.min.js 517 B
build/format-library/index.min.js 5.36 kB
build/format-library/style-rtl.css 668 B
build/format-library/style.css 669 B
build/hooks/index.min.js 1.55 kB
build/html-entities/index.min.js 424 B
build/i18n/index.min.js 3.59 kB
build/is-shallow-equal/index.min.js 501 B
build/keyboard-shortcuts/index.min.js 1.49 kB
build/keycodes/index.min.js 1.25 kB
build/list-reusable-blocks/index.min.js 1.85 kB
build/list-reusable-blocks/style-rtl.css 838 B
build/list-reusable-blocks/style.css 838 B
build/media-utils/index.min.js 2.88 kB
build/notices/index.min.js 845 B
build/nux/index.min.js 2.03 kB
build/nux/style-rtl.css 747 B
build/nux/style.css 743 B
build/plugins/index.min.js 1.83 kB
build/primitives/index.min.js 921 B
build/priority-queue/index.min.js 582 B
build/react-i18n/index.min.js 671 B
build/redux-routine/index.min.js 2.63 kB
build/reusable-blocks/index.min.js 2.28 kB
build/reusable-blocks/style-rtl.css 256 B
build/reusable-blocks/style.css 256 B
build/rich-text/index.min.js 10.6 kB
build/server-side-render/index.min.js 1.32 kB
build/shortcode/index.min.js 1.48 kB
build/token-list/index.min.js 562 B
build/url/index.min.js 1.72 kB
build/viewport/index.min.js 1.02 kB
build/warning/index.min.js 248 B
build/widgets/index.min.js 6.27 kB
build/widgets/style-rtl.css 1.05 kB
build/widgets/style.css 1.05 kB
build/wordcount/index.min.js 1.04 kB

compressed-size-action

Copy link
Member

@kevin940726 kevin940726 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm okay if it's just a temporary solution. Maybe add a comment stating that explicitly though?

packages/core-data/src/actions.js Outdated Show resolved Hide resolved

// Each request( api ) is pretty fast, but when there's a lot of them it may block the browser for a few
// seconds. Let's split this long, blocking task into bite-sized pieces scheduled separately to give the
// browser a space for processing other tasks.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you clarify more what request calls are doing here? Can you share examples? I have trouble understanding why this would be blocking the UI.

@noisysocks
Copy link
Member

noisysocks commented Aug 26, 2021

Why does it take four seconds to invoke all of the calls to saveEditedEntityRecord ? Is it the locking mechanism, maybe? Feels like this is the root issue. I wouldn't expect it to take so long even with 50 entities.

Note that this is a one-off fix for something that I believe is a symptom of using redux-rungen instead of async/await. I explored refactoring that with @jsnajdr – it would make core-data async by default and potentially alleviate this problem. I'd love to pick up that work again – see #33201.

Yeah we should finish this 🙂

@adamziel
Copy link
Contributor Author

adamziel commented Aug 26, 2021

Here's a screenshot from the profiler @youknowriad @noisysocks:

Zrzut ekranu 2021-08-26 o 15 11 38

All the blocking comes from what happens inside saveEditedEntityRecord. Basically what happens is this:

  1. __experimentalBatch calls saveEditedEntityRecord from core-data in a loop, once for each record. This call is through a few layers of abstraction including the request( api ) @youknowriad asked about above.
  2. Every call dispatches a bunch of redux actions, some of them change the store leading to calling onStoreChange() and runSelector().
  3. About half of these store changes causes the WidgetAreasBlockEditorProvider to re-render, that could also be a case for a few other components.
  4. Because we use redux-rungen, everything so far happens synchronously and there are no breaks between updating the store to say "this is being saved" and adding the last record to the batch.

The fix here is three-fold:

  1. Don't re-render any components when we don't need to – this would get us only half-way there though looking at the profiler output.
  2. Don't process everything in the same tick, break down the task into multiple ticks – that's what this PR (and Refactor saveEntityRecord from redux-rungen to async thunks #33201) are about.
  3. Don't process all the widgets we have, instead only update the ones that were actually changed (Widgets editor saves all the widgets instead of just the changed ones #34335)

@youknowriad
Copy link
Contributor

Because we use redux-rungen, everything so far happens synchronously and there are no breaks between updating the store to say "this is being saved" and adding the last record to the batch.

This is the part that is a bit concerning to me and also unexpected. If I call a function that returns a promise, why this is happening synchronously? Do you know more here maybe there's something to be fixed low level (without refactoring all actions to thunks) to solve this?

@youknowriad
Copy link
Contributor

Every call dispatches a bunch of redux actions, some of them change the store leading to calling onStoreChange() and runSelector().

To solve this kind of issues I recently introduced registry.batch function to wrap several sequential dispatch calls (it results in a single onStoreChange call), that said, it's more suited for sequential dispatch calls, not dispatch calls happening as response of promises. I think we should avoid that in this case because it will also block rerendering for any other interactions (like typing) while the batch is in progress. So for me, we should focus on the first problem above. (why things are synchronous when they are supposed to be asynchronous)

@adamziel
Copy link
Contributor Author

This is the part that is a bit concerning to me and also unexpected. If I call a function that returns a promise, why this is happening synchronously? Do you know more here maybe there's something to be fixed low level (without refactoring all actions to thunks) to solve this?

@youknowriad I'd have to confirm that, but I believe this is what happens:

  1. saveEntityRecord is synchronous up to the apiFetch call when a promise is awaited for. The part after apiFetch then resumes on the next tick
  2. We call saveEntityRecord e.g. 50 times here: requests.map( ( request ) => request( api ) );
  3. Therefore we have a lot of synchronous work before things become async

An even shorter example would be:

function fun() {
    for( let i = 0; i < 1000000; i++) {
        document.createElement("div");
    }
    return Promise.resolve("success");
}

for(let i = 0; i < 10; i++ ) {
    fun().then(console.log);
    console.log("fun() called")
}

// 10x fun() called
// 10x success

Even though a promise is returned each time, there is still a lot of synchronous work to do in a single tick.

@adamziel
Copy link
Contributor Author

@youknowriad
Copy link
Contributor

@adamziel that make sense, if this synchronous work done before API fetch involve multiple "dispatch" calls, we may consider using registry.batch to only rerender once there. but I guess it's not easy since it's scattered towards 50 different function calls and we don't want to keep the registry "paused" during the apiFetch calls right.

The solution in this PR is a bit weird though: you're basically doing this:

 - run sync work for dispatch 1 (which can include multiple rerendering)
 - pause a bit to kind of unblock the browser
 - run sync work for dispatch 2
 - pause a bit to kind of unblock the browser
 ....
 - once all the sync work is done for trigger batch api endoin
 - once the endpoint resolves, call all the batches and resume their execution which can also involve multiple rendering.

So what you're doing is to split the sync work in chunks (which has nothing to do with rungen really).

For me, I fear this indicates the way "batching is performed right now is not great and too clever, I understand the reasoning about the generic createBatch... approach but it's just not great, ideally for me it seems that we better have something like:

saveEntityRecords( recordsToSave ) {
   registry.batch( () => {
      // call all the sync dispatches before apiFetch 
   } );
   await apiFetch( batchRequest);
   registry.batch( () => {
      // call all the sync dispatches post apiFetch 
   } );
}

@youknowriad
Copy link
Contributor

Rungen seems to be synchronous-first:
https://github.com/youknowriad/rungen/blob/c62da308455485e256a187adf250f68b0fbfa2fe/src/create.js#L8-L32

That's on purpose and not the real issue here, the rerendering on each dispatch is likely the issue.

@youknowriad
Copy link
Contributor

youknowriad commented Aug 26, 2021

Actually since calling saveEntityRecord returns a promise synchronously, you can do something like that while retaining the createBatch thing:

registry.batch( () => {
   const resultPromises = requests.map( ( request ) => request( api ) );
} );

// do the batch request here

registry.batch( () => {
    // Resume the blocked requests (resultPromises)
} );

@youknowriad
Copy link
Contributor

@adamziel The solution I shared above will probably stop working if saveEntityRecord becomes fully async which will likely result in a worse experience.

Copy link
Member

@jsnajdr jsnajdr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The synchronous dispatches are caused by the __unstableAcquireStoreLock call, the very first thing every saveEntityRecord does, and the ENQUEUE_LOCK_REQUEST action dispatch it performs.

Every ENQUEUE_LOCK_REQUEST dispatch adds an item to the state.locks.requests array in core-data, and causes the core-data store to fire an update event. That's a lot of useSelect React updates that, in case of locking, are guaranteed to be noops.

I think the fact that the locking machine's state is part or core-data state and fires subscriber updates is very inefficient. The state.locks state is 100% private, as there are no public selectors that would look at it.

Only the acquire/release actions look at the state, and even these are private to the core-data store.

The locking machine doesn't need to be implemented with Redux at all. It could be a plain object with internal state with two methods:

interface Locks {
  acquire( path, exclusive ): Promise<Lock>;
  release( lock: Lock ): void;
}

const awaitNextFrame = () =>
__unstableAwaitPromise(
new Promise( ( resolve ) =>
window.requestAnimationFrame( () => resolve() )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requestAnimationFrame is not the best function to call here. It always waits 16ms for the next frame, even if the browser is not busy with anything else. Dispatching 60 parallel requests will be spread over exactly one second. setTimeout or setImmediate would be better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this could only happen after the first saveEntityRecord which would already trigger the progress indicator.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setTimeout might not be better either, some browsers will only fire the callback after a certain amount of time (mostly 1 second) if the tab is in the background. setImmediate is non-standard, so probably not the best either. I wonder if using Promise.resolve() to schedule a microtask work here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promise.resolve() wouldn't prevent freezing of the UI. It schedules a microtask right after the current script finishes running, and no other event, like user input or an animation frame, can squeeze in between them.

setImmediate, appropriately polyfilled, is probably the best. Just be careful to not choose a polyfill that uses queueMicrotask or something equivalent. The popular async package has a polyfill that does exactly that, so the danger is real.

@youknowriad
Copy link
Contributor

I think the fact that the locking machine's state is part or core-data state and fires subscriber updates is very inefficient. The state.locks state is 100% private, as there are no public selectors that would look at it.

This makes sense to me, I'm not sure locking should be done using "dispatch" actions. We could still have a "lock container" that is specific per registry/store though to avoid globals.

@jsnajdr
Copy link
Member

jsnajdr commented Aug 27, 2021

This makes sense to me, I'm not sure locking should be done using "dispatch" actions. We could still have a "lock container" that is specific per registry/store though to avoid globals.

I'm working on a PR that moves the locks away from the core-data state. Good point, though, about the locks being store/registry specific. As the locks are used only by core-data, store-specificity is currently not an issue, but my current implemenation is be shared among registries ☹️

@youknowriad
Copy link
Contributor

As the locks are used only by core-data, store-specificity is currently not an issue

store specific is good enough but core-data can be instantiated multiple times on a page in theory

@adamziel
Copy link
Contributor Author

adamziel commented Aug 27, 2021

For me, I fear this indicates the way "batching is performed right now is not great and too clever, I understand the reasoning about the generic createBatch... approach but it's just not great, ideally for me it seems that we better have something like:

Yeah I agree, I'd rather use the registry.batch in some way too so that we can avoid all the store updates. cc @noisysocks too

As we mentioned in this PR, there is plenty of related work going on. Depending on where it takes us, we may have to take different approaches (e.g. registry.batch won't play nicely with async saveEntityRecord). Let's put this one on pause until the dust settles.

@adamziel
Copy link
Contributor Author

adamziel commented Aug 27, 2021

ideally for me it seems that we better have something like:

saveEntityRecords( recordsToSave ) {
   registry.batch( () => {
      // call all the sync dispatches before apiFetch 
   } );
   await apiFetch( batchRequest);
   registry.batch( () => {
      // call all the sync dispatches post apiFetch 
   } );
}

One note there: I would still like to immediately propagate store updates from the very first saveEntityRecord call to make sure that progress indicator is being displayed, even considering that in the perfect world the batched blocking time wouldn't even be noticeable here. Everything after the first one – let's batch it!

@adamziel
Copy link
Contributor Author

We no longer use rungen and the locking machine also no longer depends on Redux which makes this PR outdated. I'm going to close it then. The freeze is much less noticeable these days as well.

@adamziel adamziel closed this Jul 25, 2022
@youknowriad youknowriad deleted the update/async-batch-scheduling branch September 7, 2022 09:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Core data /packages/core-data
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants