-
Notifications
You must be signed in to change notification settings - Fork 2k
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
enhancement: remove extra ticks #3754
Conversation
✅ Deploy Preview for compassionate-pike-271cb3 ready!
To edit notification comments on pull requests, go to your Netlify site settings. |
7575d65
to
e1074d8
Compare
70d4963
to
1fb60f2
Compare
This PR has been pruned to only include the changes the result in tick decreases -- and is now ready for review! |
In terms of comparing the number of ticks, see the following, adapted from tc39/ecma262#2770 const result = Promise.resolve(1);
function dummyAsync(n) {
return Promise.resolve(n+1);
}
async function asyncHelperWith2Awaits(p) {
try {
const r = await p;
return await dummyAsync(r);
} catch (err) {
return 0;
}
}
function returnAsyncWith2Awaits() {
return asyncHelperWith2Awaits(result);
}
function returnPromiseWithThen() {
return result.then(r => dummyAsync(r));
}
function returnPromiseWithThenAndCatch() {
return result.then(r => dummyAsync(r)).then(undefined, () => 0);
}
const resolved = Promise.resolve();
async function test(fn) {
let done = false;
let count = 0;
fn().then(() => { done = true; });
function counter() {
if (done) {
console.log(`${fn.name} took ${count} ticks`);
} else {
resolved.then(() => {
count++;
counter();
});
}
}
counter();
}
async function tests() {
await resolved;
await test(returnAsyncWith2Awaits);
await test(returnPromiseWithThen);
await test(returnPromiseWithThenAndCatch);
}
tests(); which emits on my instance of chrome:
Additional async helpers may be of use elsewhere; I have focused here so far on value completion. |
I added 89521b3 to "optimize" |
The "optimization" does not seem to be necessary for const result = Promise.resolve(1);
async function direct() {
try {
return await result;
} catch (err) {
return 0;
}
}
async function asyncHelper(p) {
try {
return await p;
} catch (err) {
return 0;
}
}
function returnAsync() {
return asyncHelper(result);
}
function returnPromiseWithCatch() {
return result.then(undefined, () => 0);
}
const resolved = Promise.resolve();
async function test(fn) {
let done = false;
let count = 0;
fn().then(() => { done = true; });
function counter() {
if (done) {
console.log(`${fn.name} took ${count} ticks`);
} else {
resolved.then(() => {
count++;
counter();
});
}
}
counter();
}
async function tests() {
await resolved;
await test(direct);
await test(returnAsync);
await test(returnPromiseWithCatch);
}
tests(); yields:
89521b3 removed! |
1fb60f2
to
68b229e
Compare
This comment has been minimized.
This comment has been minimized.
@yaacovCR Please, see benchmark results here: https://github.com/graphql/graphql-js/actions/runs/3297051171/jobs/5437417123#step:6:1 |
I removed the sync helper as it doesn't really help anything. |
If I'm running this right, tests are a bit noisy (running on WSL on my laptop), but looks like a speed bump of 58%.... Which is good! Note that this comes entire from this PR rather than #3793 which is |
#3796 formalizes the approach in this PR across the code base using eslint rules to prefer async functions over then/catch. It removes another extra tick. |
2030515
to
5f90143
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.
left, a name suggestion.
As far as every else, it is ready to merge.
use it within executeField, completeListItemValue, executeStreamField this takes advantage of some quirks involving promises/await
...integrating review feedback
This comment has been minimized.
This comment has been minimized.
@yaacovCR The latest changes of this PR are available on NPM as Also you can depend on latest version built from this PR: |
See edited comment above, the performance benefit with this PR does not have to in the end with faster promise adoption; it seems to have to do with subtracting a |
This PR no longer results in a performance savings on main. With the help of Reverting this PR from the commit right before #3886 yields the following performance drop: But with #3886, reverting this PR, actually leads to a performance improvement! Why? ..... |
clearly this does not have to do with ticks and presumably has something to do with JIT changes ??? |
I have played around with deopt-explorer trying to explore what optimization changes There are definite changes in what is optimized/deoptimized with this functions. In terms of profiling, however, I could not see how this was tied to performance, because using deopt-explorer, it was never faster with this optimization, for reasons unknown. These benchmarks are very synthetic => we use the same operation repeatedly, have a single field, and a single rootValue. My expectation is that v8's ability to optimize would be challenged with a mixture of complex operations, and root values. All of this is internal behavior, and reworking graphql-js in ways that make the code more complex to trick v8 into producing a set of more optimized functions for a synthetic test case that may not influence real world performance does not seem like the best idea. So I am much more bearish on this PR from the get-go. My inclination even at this point is to revert it prior to the v17 release, especially as it no longer seems to produce the desired synthetic performance benefit. |
UPDATE:
After much testing related to #3796, I am unable to generalize the speed savings from this change. Although it is true that there is a single extra PromiseJob created when returning a promise from an
onFulfilled
handler with.then(...)
(as opposed to using an async function) -- this extra tick in the general case does not seem to incur a real-world significant performance cost. In particular, when directly using our benchmark suite on the async version without an extra tick versus the.then(...)
version with an extra tick, there is no performance benefit.So why did this PR demonstrate a significant performance benefit? Probably because in the case where item completion requires completing a promise, we used to call
.then(...)
twice, first to callcompleteValue
, and then to install an error handler. It seems that the "extra" tick might not matter, but double-calling.then(...)
does. In fact, we only get the true savings when thecompleteValue
call withincompletePromisedValue
does not return a promise. We used to have a second.then(...)
anyway for the error handler, but now we can return directly with a single await. If completion does return a promise, i.e. if there is a nested field that itself returns a promise, the entire performance benefit disappears.So the performance benefit with this PR does not have to do with the proposal below for faster promise adoption.
Promise resolution order changes in some instances, resulting in different orders for some errors within the errors array, as well as in different values of hasNext within incremental delivery payloads.
This PR introduces an async
completePromisedValue
helper function rather than using a promise chain (see below links).https://github.com/tc39/proposal-faster-promise-adoption
tc39/ecma262#2770
tc39/ecma262#2772
tc39/ecma262#1250
https://v8.dev/blog/fast-async