-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Conversation
Hi @thomaszdxsn! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@fb.com. Thanks! |
By the way, this change is fix my performance issue. But I don't know is it harm other issues? |
@drarmstr give a review, please |
Hi @thomaszdxsn ! Thank you so much for the contribution! I'm still on a bit of a holiday schedule so I'll try to review later this week. We do really appreciate your effort. |
@@ -700,7 +700,7 @@ function selector<T>( | |||
): void { | |||
deps.add(newDepKey); | |||
|
|||
setDepsInStore(store, state, deps, executionId); | |||
// setDepsInStore(store, state, deps, executionId); |
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.
With this simplification we can just get rid of setNewDepInStore()
and call deps.add()
directly in getRecoilValue()
.
@@ -782,6 +782,7 @@ function selector<T>( | |||
|
|||
try { | |||
result = get({get: getRecoilValue, getCallback}); | |||
setDepsInStore(store, state, deps, executionId) |
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.
Best if this is done outside of this try/catch block. We could just call it before the return, which references depValues
, so it might be a nice logical place for it.
Ok, I took a look; thanks again @thomaszdxsn ! I've actually thought an optimization like this makes sense, but we need to be careful with asynchronous cases. If a selector has pending dependencies, or is async itself, then it needs to update it's dependencies before relinquishing control; from a glance this may be done as there are calls to @csantos42 do you have thoughts? |
Ok, I will dive into internals |
Hi @thomaszdxsn, have you had a chance to dive in further? |
Sorry, I am busy until 2022-01-26. after 01-25 I will give some further progress |
@thomaszdxsn - In the meantime, #1568 and #1566 do some unrelated cleanup of the selector implementation. Hopefully it should help make it a bit more readable. |
got it, thanks |
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
Sorry, make a mess so pr is closed. The codebase is overwhelming for me. please give some mentor
|
Yeah, I'm hoping that no other promise wrapping logic changes are needed here. But we should add more unit testcases in Selector Also, we started stubbing some performance tests in |
Sorry I discover the new merged code now, Let me merge it |
@drarmstr has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
@thomaszdxsn - Thanks for the updates so far. I worked closer with your test case and updated it to cover some more cases. testRecoil('Dynamic deps will refresh', async () => {
const myAtom = atom({
key: 'selector dynamic deps atom',
default: 'DEFAULT',
});
const myAtomA = atom({
key: 'selector dynamic deps atom A',
default: new Promise(() => {}),
});
const myAtomB = atom({
key: 'selector dynamic deps atom B',
default: 'B',
});
const myAtomC = atom({
key: 'selector dynamic deps atom C',
default: new Promise(() => {}),
});
let selectorEvaluations = 0;
const mySelector = selector({
key: 'selector dynamic deps selector',
get: async ({get}) => {
selectorEvaluations++;
await Promise.resolve();
const sw = get(myAtom);
if (sw === 'A') {
return 'RESOLVED_' + get(myAtomA);
}
if (sw === 'B') {
return 'RESOLVED_' + get(myAtomB);
}
if (sw === 'C') {
return 'RESOLVED_' + get(myAtomC);
}
await new Promise(() => {});
},
});
const wrapperSelector = selector({
key: 'selector dynamic deps wrapper',
get: ({get}) => {
const loadable = get(noWait(mySelector));
return loadable.state === 'loading' ? 'loading' : loadable.contents;
},
});
let setAtom, setAtomA, setAtomB;
function ComponentSetter() {
setAtom = useSetRecoilState(myAtom);
setAtomA = useSetRecoilState(myAtomA);
setAtomB = useSetRecoilState(myAtomB);
}
const c = renderElements(
<>
<ComponentSetter />
<ReadsAtom atom={wrapperSelector} />
</>,
);
await flushPromisesAndTimers();
expect(c.textContent).toBe('"loading"');
expect(selectorEvaluations).toBe(1);
// Cause re-evaluation to pending state
act(() => setAtom('TMP'));
await flushPromisesAndTimers();
expect(c.textContent).toBe('"loading"');
expect(selectorEvaluations).toBe(2);
// Add atomA dependency, which is pending
act(() => setAtom('A'));
await flushPromisesAndTimers();
expect(c.textContent).toBe('"loading"');
expect(selectorEvaluations).toBe(3);
// change to atomB dependency
act(() => setAtom('B'));
await flushPromisesAndTimers();
await flushPromisesAndTimers();
expect(c.textContent).toBe('"RESOLVED_B"');
expect(selectorEvaluations).toBe(4);
// Set atomB
act(() => setAtomB('SETB-0'));
await flushPromisesAndTimers();
expect(c.textContent).toBe('"RESOLVED_SETB-0"');
expect(selectorEvaluations).toBe(5);
// Change back to atomA dependency
act(() => setAtom('A'));
await flushPromisesAndTimers();
expect(c.textContent).toBe('"loading"');
expect(selectorEvaluations).toBe(6);
// Setting B is currently ignored
act(() => setAtomB('SETB-IGNORE'));
await flushPromisesAndTimers();
expect(c.textContent).toBe('"loading"');
expect(selectorEvaluations).toBe(6);
// Set atomA
act(() => setAtomA('SETA'));
await flushPromisesAndTimers();
await flushPromisesAndTimers();
expect(c.textContent).toBe('"RESOLVED_SETA"');
expect(selectorEvaluations).toBe(7);
// Setting atomB is ignored
act(() => setAtomB('SETB-LATER'));
await flushPromisesAndTimers();
expect(c.textContent).toBe('"RESOLVED_SETA"');
expect(selectorEvaluations).toBe(7);
// Change to atomC, which is pending
act(() => setAtom('C'));
await flushPromisesAndTimers();
expect(c.textContent).toBe('"loading"');
expect(selectorEvaluations).toBe(8);
// Setting atomA is ignored
act(() => setAtomA('SETA-LATER'));
await flushPromisesAndTimers();
expect(c.textContent).toBe('"loading"');
expect(selectorEvaluations).toBe(8);
// change back to atomA for new value
act(() => setAtom('A'));
await flushPromisesAndTimers();
await flushPromisesAndTimers();
expect(c.textContent).toBe('"RESOLVED_SETA-LATER"');
expect(selectorEvaluations).toBe(9);
// Change back to atomB
act(() => setAtom('B'));
await flushPromisesAndTimers();
expect(c.textContent).toBe('"RESOLVED_SETB-LATER"');
expect(selectorEvaluations).toBe(10);
// Set atomB
act(() => setAtomB('SETB-1'));
await flushPromisesAndTimers();
expect(c.textContent).toBe('"RESOLVED_SETB-1"');
expect(selectorEvaluations).toBe(11);
}); You'll notice this also adds a wrapper selector using But, I still think there is hope here if we can just optimize |
Involve async things make it very complicate. actually I don't use any async in recoil(I have a little async logic, but write as I known what you meaning but don't know how to optimize |
My scenario is:
When display nodes show more than 900+, the response time is 500ms+. Because every |
@drarmstr What you mean |
Yeah, even though many have simple use cases we still need to be careful to make sure all the corner cases work for everyone. The possible optimization is perhaps just checking if the graph needs to be updated before updating it. Thinking is that the updating process, with taking multiple set differences, is more expensive than just first checking if they are already equivalent up-front.. |
@drarmstr Sorry but what is timing for |
Ah, I see, you're trying to cover the case of looping over new dependencies, not just existing dependencies. Yeah, then that idea won't be sufficient either. Hmm, I guess it's worth diving into the actual graph update algorithm in |
Add better unit test for dynamic dependencies in #1598 |
@thomaszdxsn - An idea to optimize the common case, while still supporting all cases, might be to lazily update all of the dependencies that are synchronously requested by the selector from the initial execution of the |
Can you give some pseudo code? |
It might be as easy:
|
@thomaszdxsn - Ok, I've got it implemented and it seems to work really well. You don't have to worry about updating the PR, I can take care of merging it. Thanks for all of your help here, this will be a great improvement! (Also note #1651) |
Summary: The current algorithm for updating selector dependencies in the data flow graph is expensive. Currently we update the graph every time a new dependency is added, which scales very poorly. This optimization only updates the graph with all sycnhronous dependencies after the synchronous selector evaluation completes. We still have to update the graph after every asynchronous dependency is added since we need to know if we have to re-evaluate the selector if one of them is updated before the selector completely resolves. This optimization significantly helps the common case for synchronous dependencies: ``` Improvement Number of synchronous dependencies 2x 100 4x 1,000 40x 10,000 ``` facebookexperimental#914 Please refer this issue for detail Pull Request resolved: facebookexperimental#1515 Test Plan: Unit tests from D34100807 check for proper re-evaluation of updated async dependencies before async selectors resolve. Differential Revision: https://www.internalfb.com/diff/D33825247?entry_point=27 Pulled By: drarmstr fbshipit-source-id: 6df53e7bca2a2284c350149c662d7a7e7e31ca52
You are very gorgeous man, thank you very much, I learn a lot of things from this issue |
Summary: The current algorithm for updating selector dependencies in the data flow graph is expensive. Currently we update the graph every time a new dependency is added, which scales very poorly. This optimization only updates the graph with all sycnhronous dependencies after the synchronous selector evaluation completes. We still have to update the graph after every asynchronous dependency is added since we need to know if we have to re-evaluate the selector if one of them is updated before the selector completely resolves. This optimization significantly helps the common case for synchronous dependencies: ``` Improvement Number of synchronous dependencies 2x 100 4x 1,000 40x 10,000 ``` (Note: to scale to 10,000+ also requires D34633750) facebookexperimental/Recoil#914 Please refer this issue for detail Pull Request resolved: facebookexperimental/Recoil#1515 Test Plan: Unit tests from D34100807 check for proper re-evaluation of updated async dependencies before async selectors resolve. Test performance of scalability with `Recoil_Perf-test.js` Reviewed By: mondaychen Differential Revision: D33825247 Pulled By: drarmstr fbshipit-source-id: 943b64ef371833c1f77c0185c3c13d239526ad39
Summary: The current algorithm for updating selector dependencies in the data flow graph is expensive. Currently we update the graph every time a new dependency is added, which scales very poorly. This optimization only updates the graph with all sycnhronous dependencies after the synchronous selector evaluation completes. We still have to update the graph after every asynchronous dependency is added since we need to know if we have to re-evaluate the selector if one of them is updated before the selector completely resolves. This optimization significantly helps the common case for synchronous dependencies: ``` Improvement Number of synchronous dependencies 2x 100 4x 1,000 40x 10,000 ``` (Note: to scale to 10,000+ also requires D34633750) facebookexperimental/Recoil#914 Please refer this issue for detail Pull Request resolved: facebookexperimental/Recoil#1515 Test Plan: Unit tests from D34100807 check for proper re-evaluation of updated async dependencies before async selectors resolve. Test performance of scalability with `Recoil_Perf-test.js` Reviewed By: mondaychen Differential Revision: D33825247 Pulled By: drarmstr fbshipit-source-id: 943b64ef371833c1f77c0185c3c13d239526ad39
Summary: The current algorithm for updating selector dependencies in the data flow graph is expensive. Currently we update the graph every time a new dependency is added, which scales very poorly. This optimization only updates the graph with all sycnhronous dependencies after the synchronous selector evaluation completes. We still have to update the graph after every asynchronous dependency is added since we need to know if we have to re-evaluate the selector if one of them is updated before the selector completely resolves. This optimization significantly helps the common case for synchronous dependencies: ``` Improvement Number of synchronous dependencies 2x 100 4x 1,000 40x 10,000 ``` (Note: to scale to 10,000+ also requires D34633750) facebookexperimental/Recoil#914 Please refer this issue for detail Pull Request resolved: facebookexperimental/Recoil#1515 Test Plan: Unit tests from D34100807 check for proper re-evaluation of updated async dependencies before async selectors resolve. Test performance of scalability with `Recoil_Perf-test.js` Reviewed By: mondaychen Differential Revision: D33825247 Pulled By: drarmstr fbshipit-source-id: 943b64ef371833c1f77c0185c3c13d239526ad39
setDepsInStore
every time, harm the performance when multiple call the getter#914 Please refer this issue for detail