-
-
Notifications
You must be signed in to change notification settings - Fork 638
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
lazy recomputeDependents #2827
lazy recomputeDependents #2827
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. |
Preview in LiveCodesLatest commit: c758a69
See documentations for usage instructions. |
8c9258f
to
6d9d8fe
Compare
The commit link is not found. (because of force pushing?) |
6d9d8fe
to
3e6fa5e
Compare
3e6fa5e
to
5f04a05
Compare
5f04a05
to
8f65f54
Compare
8f65f54
to
6a6b747
Compare
e9c3d5c
to
05064d6
Compare
05064d6
to
00e11ab
Compare
00e11ab
to
29b3544
Compare
29b3544
to
f3b5463
Compare
5a85a80
to
5b6c8ee
Compare
src/vanilla/store.ts
Outdated
while (pending[0].size || pending[1].size || pending[2].size) { | ||
recomputeDependents(pending, new Set(pending[0].keys())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actual change
- while (pending[1].size || pending[2].size) {
- pending[0].clear()
+ while (pending[0].size || pending[1].size || pending[2].size) {
+ recomputeDependents(pending, new Set(pending[0].keys()))
5b6c8ee
to
c0df057
Compare
c0df057
to
2b73c07
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks so much for working on it. The commit history tells how challenging this has been.
I only check the diff, and assuming that this keeps the basic logic and only defers the execution, it's pretty close to what I would do. What's blocking is the reasonable understanding of the store.test.tsx
change and failed CI tests.
You can stop it here and I can take it over, if you like.
src/vanilla/store.ts
Outdated
@@ -198,33 +207,6 @@ const addPendingFunction = (pending: Pending, fn: () => void) => { | |||
pending[2].add(fn) | |||
} | |||
|
|||
const flushPending = (pending: Pending) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I'm not sure which is better, I would try keeping it outside. I'm not sure about the future, and we may put "pending" in buildStore. In general, I prefer isolating it.
src/vanilla/store.ts
Outdated
const getter: Getter = <V>(a: Atom<V>) => | ||
returnAtomValue(readAtomState(pending, a)) | ||
const getter: Getter = <V>(a: Atom<V>) => { | ||
recomputeDependencies(pending, atom) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why this is necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps I should leave a comment here.
Before we get
any atom, we need to recompute the dependents of any of its dependencies that have been marked as dirty, to ensure we are returning the latest value. This is the exception to the lazy recomputeDependents rule.
Example:
const a = atom(0)
const b = atom(0)
const c = atom((get) => [get(a), get(b)])
const d = atom((get) => get(c))
const e = atom((get) => get(a))
const w = atom(null, (get, set) => {
get(d) // [0, 0]
// does not immediately recompute,
// marks `a` and all its dependents as dirty
set(a, 1)
// setA: dependencies of `d` are {c,a,b}
// setB: dirty atoms of setA are {a}
// setC: dependents setB are {a,c,e}
// setD: intersection of setC and setB are {a,c}
// recompute setD
get(d) // [1, 0]
// recompute remaining dependents of dirty atoms in flushPending {e}
})
store.set(w)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not convinced but maybe I need to play with it to get better understanding. It's my problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I figured out why this passes the peek test. Here's a better test. it('can read in write function without changing dependencies', () => {
const a = atom(0)
let bReadCount = 0
const b = atom(
(get) => {
++bReadCount
return get(a)
},
() => {},
)
let bMountCalled = false
b.onMount = () => {
bMountCalled = true
}
const c = atom((get) => get(a))
const w = atom(null, (get, set) => {
expect(bReadCount).toBe(0)
get(b)
expect(bReadCount).toBe(1)
set(a, 1)
expect(bReadCount).toBe(1)
})
const store = createStore()
store.sub(c, () => {}) // mounts c,a
store.set(w)
expect(bMountCalled).toBe(false) // ✅ PASSES (should fail if b atomState.m is defined
expect(bReadCount).toBe(1) // ❌ FAILS - received 2
expect(getAtomState(b)!.m).toBeUndefined() // ✅ PASSES (could fail)
expect(getAtomState(b)!.d.size).toBe(0) // ❌ FAILS - 1 (a atomState)
expect(getAtomState(a)!.m!.d.has(b)).toBe(false) // ❌ FAILS - true
}) |
2b73c07
to
9d85665
Compare
a4865f9
to
f9c3966
Compare
commit: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks so much for your effort! This is a big improvement.
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [jotai](https://github.com/pmndrs/jotai) | dependencies | minor | [`2.9.3` -> `2.11.0`](https://renovatebot.com/diffs/npm/jotai/2.9.3/2.11.0) | --- ### Release Notes <details> <summary>pmndrs/jotai (jotai)</summary> ### [`v2.11.0`](https://github.com/pmndrs/jotai/releases/tag/v2.11.0) [Compare Source](pmndrs/jotai@v2.10.4...v2.11.0) There are no public API changes, but some internal behaviors have been improved. Now, atom updates are batched in a single write, which might provide a performance benefit in certain edge cases. This feature has been requested actually for a long time, and it's finally implemented. See also [#​2782](pmndrs/jotai#2782). #### What's Changed - refactor(store): rename pending to batch by [@​dai-shi](https://github.com/dai-shi) in pmndrs/jotai#2868 - lazy recomputeDependents by [@​dmaskasky](https://github.com/dmaskasky) in pmndrs/jotai#2827 - fix(store): robust flush batch by [@​dmaskasky](https://github.com/dmaskasky) in pmndrs/jotai#2871 - fix(store): refactor batch priority by [@​dmaskasky](https://github.com/dmaskasky) in pmndrs/jotai#2875 - feat: dev store with unstable_derive by [@​dai-shi](https://github.com/dai-shi) in pmndrs/jotai#2852 #### New Contributors - [@​rainagalbiati-turngate](https://github.com/rainagalbiati-turngate) made their first contribution in pmndrs/jotai#2882 - [@​leweyse](https://github.com/leweyse) made their first contribution in pmndrs/jotai#2883 **Full Changelog**: pmndrs/jotai@v2.10.4...v2.11.0 ### [`v2.10.4`](https://github.com/pmndrs/jotai/releases/tag/v2.10.4) [Compare Source](pmndrs/jotai@v2.10.3...v2.10.4) A minor improvement for some edge cases. (For more information, see [#​2789](pmndrs/jotai#2789).) #### What's Changed - fix(store): do not recompute unmounted atoms eagerly by [@​dai-shi](https://github.com/dai-shi) in pmndrs/jotai#2849 #### New Contributors - [@​rmillio](https://github.com/rmillio) made their first contribution in pmndrs/jotai#2832 - [@​sukvvon](https://github.com/sukvvon) made their first contribution in pmndrs/jotai#2851 **Full Changelog**: pmndrs/jotai@v2.10.3...v2.10.4 ### [`v2.10.3`](https://github.com/pmndrs/jotai/releases/tag/v2.10.3) [Compare Source](pmndrs/jotai@v2.10.2...v2.10.3) This fixes various edge cases. Huge thanks to [@​dmaskasky](https://github.com/dmaskasky) ! 🎉 #### What's Changed - fix: flushPending in async write by [@​dai-shi](https://github.com/dai-shi) in pmndrs/jotai#2804 - fix: flush pending finally everywhere by [@​dmaskasky](https://github.com/dmaskasky) in pmndrs/jotai#2818 - fix: rethrow falsy errors thrown in flushPending by [@​dmaskasky](https://github.com/dmaskasky) in pmndrs/jotai#2820 - fix: setAtom uses stale pending on atom unmount by [@​dmaskasky](https://github.com/dmaskasky) in pmndrs/jotai#2811 - fix: onMount setSelf does not notify listeners by [@​dmaskasky](https://github.com/dmaskasky) in pmndrs/jotai#2815 - refactor(core): Use iterative approach in recompute dependents by [@​dmaskasky](https://github.com/dmaskasky) in pmndrs/jotai#2821 - refactor(store): prefer epoch number comparisons to indicate value change by [@​dmaskasky](https://github.com/dmaskasky) in pmndrs/jotai#2828 **Full Changelog**: pmndrs/jotai@v2.10.2...v2.10.3 ### [`v2.10.2`](https://github.com/pmndrs/jotai/releases/tag/v2.10.2) [Compare Source](pmndrs/jotai@v2.10.1...v2.10.2) Fixed some jotai/utils for a regression in v2.10.0. #### What's Changed - fix(unstable_derive): trap atom methods by [@​dai-shi](https://github.com/dai-shi) in pmndrs/jotai#2741 - Throw error on `useAtom(undefined)` or `useAtom(null)` by [@​kevinschaich](https://github.com/kevinschaich) in pmndrs/jotai#2778 - fix(utils): make 'loadable' update immediate after resolve by [@​e7h4n](https://github.com/e7h4n) in pmndrs/jotai#2790 - fix(utils): make 'unwrap' update immediate after resolve by [@​organize](https://github.com/organize) in pmndrs/jotai#2794 #### New Contributors - [@​niklasbec](https://github.com/niklasbec) made their first contribution in pmndrs/jotai#2773 - [@​romain-trotard](https://github.com/romain-trotard) made their first contribution in pmndrs/jotai#2781 - [@​kretajak](https://github.com/kretajak) made their first contribution in pmndrs/jotai#2786 - [@​Brokyeom](https://github.com/Brokyeom) made their first contribution in pmndrs/jotai#2798 - [@​ryoku4](https://github.com/ryoku4) made their first contribution in pmndrs/jotai#2802 - [@​yairEO](https://github.com/yairEO) made their first contribution in pmndrs/jotai#2805 - [@​kevinschaich](https://github.com/kevinschaich) made their first contribution in pmndrs/jotai#2778 - [@​e7h4n](https://github.com/e7h4n) made their first contribution in pmndrs/jotai#2790 - [@​organize](https://github.com/organize) made their first contribution in pmndrs/jotai#2794 **Full Changelog**: pmndrs/jotai@v2.10.1...v2.10.2 ### [`v2.10.1`](https://github.com/pmndrs/jotai/releases/tag/v2.10.1) [Compare Source](pmndrs/jotai@v2.10.0...v2.10.1) This fixes a bug in an extreme edge case. If you find this change breaks something, please report to us. #### What's Changed - fix(core): recompute dependents in edge cases by [@​dai-shi](https://github.com/dai-shi) in pmndrs/jotai#2742 #### New Contributors - [@​vangie](https://github.com/vangie) made their first contribution in pmndrs/jotai#2753 - [@​ts1994tw](https://github.com/ts1994tw) made their first contribution in pmndrs/jotai#2759 - [@​KagamiChan](https://github.com/KagamiChan) made their first contribution in pmndrs/jotai#2761 - [@​nguyenbry](https://github.com/nguyenbry) made their first contribution in pmndrs/jotai#2762 - [@​jaycho46](https://github.com/jaycho46) made their first contribution in pmndrs/jotai#2766 - [@​midzdotdev](https://github.com/midzdotdev) made their first contribution in pmndrs/jotai#2767 **Full Changelog**: pmndrs/jotai@v2.10.0...v2.10.1 ### [`v2.10.0`](https://github.com/pmndrs/jotai/releases/tag/v2.10.0) [Compare Source](pmndrs/jotai@v2.9.3...v2.10.0) It comes with another significant internal change to address some edge cases. Since v2.9.0, we've been working on some internal refactors to support more edge cases and clean up the code. Users are encouraged to update to the new versions eventually, but if you're satisfied with the current situation and prefer to avoid temporary instability, you can stick with v2.8.4 for now. #### What's Changed - breaking(core): avoid continuable promise in store api by [@​dai-shi](https://github.com/dai-shi) in pmndrs/jotai#2695 #### New Contributors - [@​sphinxrave](https://github.com/sphinxrave) made their first contribution in pmndrs/jotai#2653 - [@​mxthxngx](https://github.com/mxthxngx) made their first contribution in pmndrs/jotai#2712 - [@​hoangvu12](https://github.com/hoangvu12) made their first contribution in pmndrs/jotai#2716 - [@​YuHyeonWook](https://github.com/YuHyeonWook) made their first contribution in pmndrs/jotai#2734 **Full Changelog**: pmndrs/jotai@v2.9.3...v2.10.0 </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS41Ny40IiwidXBkYXRlZEluVmVyIjoiMzkuODIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: https://git.tristess.app/alexandresoro/ouca/pulls/380 Reviewed-by: Alexandre Soro <[email protected]> Co-authored-by: renovate <[email protected]> Co-committed-by: renovate <[email protected]>
Related Bug Reports or Discussions
#2782
Summary
Currently, recomputeDependents runs for each call to set. This causes overlapping dependents to recompute multiple times.
We still need to recompute some dependents if they are accessed with
get
. But the remaining dependents can be recomputed at the end of the transaction in flushPending.Proposed Fix
set
get
, if the atom's state is dirtyflushPending
, recompute all remaining dirty dependentsMore complex example
Suppose you write to node 17. Itself and it's dependents are
{17, 15, 16, 12, 13, 14, 9, 10, 11, 6, 3, 1}
.We save these nodes as pending recompute.
Then suppose you read from node 1. Itself and it's dependencies are
{1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 15, 17}
.The intersection of these two sets is
{1, 3, 6, 9, 12, 15, 17}
.On
get
, only these nodes will be eagerly recomputed.On
flushPending
, the remaining nodes{16, 13, 14, 10, 11}
will be recomputed.Description of Core Functions
getMountedDependents
Was previously
getDependents
. Is called bygetAllDependents
. Returns a set of all mounted dependents.getAllDependents
Is called by
markRecomputePending
andgetSortedDependents
. Returns a map of atoms to a set of their dependents (Map<AnyAtom, Set<AnyAtom>>
).markRecomputePending
Is called by
recomputeDependents
andsetter
. SetsatomState.x
to true.getSortedDependents
Is called by
recomputeDependents
. Returns a topological sorted list of atoms and its dependent atoms (deep).recomputeDependents
Is called by
recomputeDependencies
andflushPending
. Receives a set of atoms for which to recompute dependents on. Iteratively callsreadAtomState
andmountDependencies
on all atoms and their dependents (deep) whoseatomState.x
is true.recomputeDependencies
Is called by the
getter
inwriteAtomState
. Recomputes the dependents (deep) of all dependencies (deep) whoseatomState.x
is true.flushPending
Is called in the following places
readAtomState
getter
finally block ifgetter
is called async.complete
which is called when a valueOrPromise is a promise and has settled.setter
finally block ifsetter
is called async.writeAtom
finally block.setAtom
finally block ifsetAtom
is called async.subscribeAtom
unsubscribeAtom
Check List
pnpm run prettier
for formatting code and docs