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 Runner with Message Channel #458

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

flashdesignory
Copy link
Contributor

@flashdesignory flashdesignory commented Nov 20, 2024

As discussed in our sync, here is a combination of the AsyncRunner and Message Channel pr.

Relevant code snippet:

class AsyncRAFTestInvoker extends BaseRAFTestInvoker {
    _scheduleCallbacks(resolve) {
        requestAnimationFrame(async () => {
            await this._syncCallback();

            let gotTimer = false;
            let gotMessage = false;

            const tryTriggerAsyncCallback = () => {
                if (!gotTimer || !gotMessage)
                    return;

                this._asyncCallback();
                setTimeout(async () => {
                    await this._reportCallback();
                    resolve();
                }, 0);
            };

            setTimeout(() => {
                gotTimer = true;
                tryTriggerAsyncCallback();
            });

            const mc = new MessageChannel();
            mc.port1.onmessage = () => {
                mc.port1.close();
                mc.port2.close();

                gotMessage = true;
                tryTriggerAsyncCallback();
            };
            mc.port2.postMessage("speedometer");
        });
    }
}

For testing purposes I kept an explicit type of "async" in the test file, but I opted all default suites into using it.
For now, I still kept the AsyncTimerClass, which can get removed once we settle on the code snippet above.

Initial testing shows that it should also fix the react specific issue.


export const ASYNC_TEST_INVOKER_LOOKUP = {
__proto__: null,
timer: AsyncTimerTestInvoker,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we don't want an AsyncTimerTestInvoker, I could simply assign the same AsyncRAFTestInvoker..

@flashdesignory flashdesignory marked this pull request as ready for review November 21, 2024 02:15
class AsyncRAFTestInvoker extends BaseRAFTestInvoker {
_scheduleCallbacks(resolve) {
requestAnimationFrame(async () => {
await this._syncCallback();
Copy link

@smaug---- smaug---- Nov 22, 2024

Choose a reason for hiding this comment

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

_syncCallback and _asyncCallback should be renamed to something else, since, well, the patch makes _syncCallback rather asynchronous :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

LOL, we had the same convo in a similar pr... what do you think about:

class TestInvoker {
    constructor(captureTest, captureLayout, reportResults) {
        this._captureTest = captureTest;
        this._captureLayout = captureLayout;
        this._reportResults = reportResults;
    }
} 

tryTriggerAsyncCallback();
});

const mc = new MessageChannel();

Choose a reason for hiding this comment

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

It might be nice to use the same channel for all the tests, just to avoid extra garbage created by the runner. Not sure where there channel could live. Perhaps a static property on the class?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

are you thinking maybe something like:

class AsyncRAFTestInvoker extends BaseRAFTestInvoker {
    static mc = new MessageChannel();
    _scheduleCallbacks(resolve) {
        requestAnimationFrame(async () => {
            await this._syncCallback();

            let gotTimer = false;
            let gotMessage = false;

            const tryTriggerAsyncCallback = () => {
                if (!gotTimer || !gotMessage)
                    return;

                this._asyncCallback();
                setTimeout(async () => {
                    await this._reportCallback();
                    resolve();
                }, 0);
            };

            const handleMessage = () => {
                AsyncRAFTestInvoker.mc.port1.removeEventListener("message", handleMessage);
                gotMessage = true;
                tryTriggerAsyncCallback();
            };

            setTimeout(() => {
                gotTimer = true;
                tryTriggerAsyncCallback();
            });

            AsyncRAFTestInvoker.mc.port1.addEventListener("message", handleMessage);
            AsyncRAFTestInvoker.mc.port1.start();
            AsyncRAFTestInvoker.mc.port2.postMessage("speedometer");
        });
    }
}

setTimeout(() => {
gotTimer = true;
tryTriggerAsyncCallback();
});

Choose a reason for hiding this comment

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

oh, hmm, does this non-double rAF setup have a bug after all. If the promise callbacks create new promises which get resolve and those callbacks then trigger setTimeout or postMessage, does anything guarantee that test runners setTimeout/messageport gets handled after those?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We are stopping and waiting for await this._syncCallback();.
The setTimeout / messageport happens after that resolved, correct?
The reason of having the boolean flags is that we can't guarantee in which order setTimeout / postMessage gets called. At least that's how I understood our conversation 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did test it with the current tests as well as with a test that had an artificial promise resolving after a long period of time in each step.

Choose a reason for hiding this comment

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

But await this._syncCallback(); might return in the same task, if one just does effectively
return new Promise(r => r());

In my example using MessageChannel there were still two rAFs.

I thought we were thinking to use the same runner for async tests and non-async, and I'm worried that not having two rafs would broke non-async tests (meaning that some time wouldn't be measured correctly). But I could be missing something (I think I did when I said on Slack couple of days ago that this approach is ok :)).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if we don't want nested rafs, this might be a way maybe:

_scheduleCallbacks(resolve) {
        let gotTimer = false;
        let gotMessage = false;
        let gotPromise = false;

        const tryTriggerAsyncCallback = () => {
            if (!gotTimer || !gotMessage || !gotPromise)
                return;

            this._asyncCallback();
            setTimeout(async () => {
                await this._reportCallback();
                resolve();
            }, 0);
        };

        requestAnimationFrame(async () => {
            await this._syncCallback();
            gotPromise = true;
            tryTriggerAsyncCallback();
        });

        requestAnimationFrame(() => {
            setTimeout(() => {
                gotTimer = true;
                tryTriggerAsyncCallback();
            });

            const mc = new MessageChannel();
            mc.port1.onmessage = () => {
                mc.port1.close();
                mc.port2.close();

                gotMessage = true;
                tryTriggerAsyncCallback();
            };
            mc.port2.postMessage("speedometer");
        });
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants