-
Notifications
You must be signed in to change notification settings - Fork 80
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
Improve main cpu loop performance #19
Conversation
Excellent! Thanks for documenting your progress, it is really insightful :) |
117d8de
to
dfdf985
Compare
My intuition was partly correct, by breaking of the for loop and rescheduling execution of the This seems to indicate that Firefox may add delay when "awaiting". Probably not resuming function execution immediately when the promise resolves. This will mostly be unseen in other web app but here we await a lot so milliseconds adds up. It doesn't change anything on Chrome however, performance is the same with await or this new method. This is fascinating to me how browser implementations can influence performance even on standard JS features. It would be good if you could test the branch too, so that I'm sure the performance increase is real. When checked, if it's ok for you, I can replace the "execute" function body with the "executeNoAsync" one and improve the "ugly" |
Wow, that's great! I will check the branch on my end and report shortly |
The branch is live here if you want to check: http://info2.hackervaillant.eu/ I'm now at 105% on Firefox (Maybe had some background load for the earlier results) |
So here are my results: Firefox - around 115% (up from 48%) 👍 👍👍 |
I cleaned up a bit. I'm not sure the setTimeoutOptimized + handleMessage implementation belongs in the execute.ts file but don't know where else to put it. I also have this code at the start of the execute function to initialize nextTick on first run if (this.nextTick === 0) {
this.nextTick = this.cpu.cycles + this.workUnitCycles;
} This is the near equivalent of the previous I wonder if this could be moved in the Runner constructor because |
Perhaps we can add a utility call, something like This sort of abstraction will also allow us to adapt to the new main thread scheduling API when it will be ready. We can eventually support different implementations, depending on the run time (different browsers, node, etc.) and the available APIs
Right, not supported at the moment |
I got rid of this.nextTick since we iterate for a fixed amount each time, no need to keep track of that anymore. |
Lovely, thank you! One note - the existing code loops for 500,000 clock cycles. The new code that uses |
Complete oversight from me over this CPU cycle != instruction. I fixed the loop. It now runs for a fixed amount of clock cycles again. There is still a possible drift of some (1 to 3 ?) cycles per execution when the last executed instruction on the batch takes several CPU cycles but i guess comparing to 500000 it is not important. Just to keep in mind. I also implemented the MicroTaskScheduler. I had some problems with So i found this pattern but i don't know if it is good practice, let me know. handleMessage = (event: MessageEvent) => {
// this correctly refers to the class instance
} Since i'm completely new to Typescript don't hesitate to point bad practices or simply bad code, i'll be happy to correct 🙂 |
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.
Hi Glenn, I've left some suggestions - most of them are related to typescript specific syntax / tricks.
One more thing: Can you add a unit test for MicroTaskScheduler
?
I think i implemented all your feedback. However I'm stuck for the unit test. What blocks me is that, internally the scheduler uses window.postMessage api which is asynchronous. So i don't know how to wait for the event listener to be executed. There may be a solution around mocking window.postMessage but i'm not sure. import { MicroTaskScheduler, IMicroTaskCallback } from './task-scheduler';
describe('task-scheduler', () => {
it('should execute task', () => {
const taskScheduler = new MicroTaskScheduler();
const task = jest.fn();
taskScheduler.start();
taskScheduler.postTask(task);
// How can i wait until the internal scheduler method handleMessage is called ?
expect(task).toHaveBeenCalled();
});
}); |
So one approach would be to mock Another way would just be to wait using |
Yeah I came upon this However when i run the test i got this error: Maybe it has to do with some configuration in tsconfig ? I put the test in the demo src folder under task-scheduler.spec.ts |
I have removed
So from what i understand, current tests run in a nodejs environment. Here since i use browser features, i have to find how to run test in a "browser" context. It seems to be done using "jsdom". I'm looking at how i could use that. |
That's what's called a rabbit hole ... I fixed the previous error messages by adding "dom" lib to the tsconfig.json ("lib": ["es2015", "dom"]) /**
* @jest-environment jsdom
*/ My test is: /**
* @jest-environment jsdom
*/
import { MicroTaskScheduler } from './task-scheduler';
test('should execute task', async () => {
const taskScheduler = new MicroTaskScheduler();
const postTaskSpy = jest.spyOn(taskScheduler, 'postTask');
const handleMessageSpy = jest.spyOn(taskScheduler, 'handleMessage');
const fn = jest.fn();
taskScheduler.start();
taskScheduler.postTask(fn);
await new Promise((resolve) => setTimeout(resolve, 200));
expect(postTaskSpy).toHaveBeenCalled();
expect(handleMessageSpy).toHaveBeenCalled();
expect(fn).toHaveBeenCalled();
}); So this didn't work because jsdom implementation of "window.postMessage" doesn't fill the event.source (https://github.com/jsdom/jsdom/blob/020539ed3f46720fe526ecf55a3a2d2a889c94b4/lib/jsdom/living/post-message.js#L31) And in the handleMessage I was checking if the event came from "window" and then if it had the correct message name. After removing the On to adding other tests now ... |
yeah, that's why I was thinking that it might make more sense to mock
I think my preferred alternative would be to leave the DOM library out of /// <reference lib="dom" /> |
Ok I understand for the dom lib inclusion. I added the reference line and it works. However there is still the initial error problem. I had to remove the I didn't push the modification because it touches the config so if you have an idea on how to properly deal with this... All this configurations are a bit overwhelming 🙂 |
1e83645
to
b7a83c7
Compare
Alright Glenn, I think I figured out the correct configuration. Can you please rebase on top of my latest commit from master? |
b7a83c7
to
5683d5b
Compare
Rebased The tests are green with the new conf ! |
Hooray! Would you like to squash into one commit before I merge? You can follow the convention of the other commits with the message: "perf(demo): improve main cpu loop performance" |
5683d5b
to
c675c2a
Compare
Done 🙂 |
So I guess we can now close #18 as well? |
Yes ! |
Working branch for #18
Benchmark on Demo
baseline: setTimeout(resolve, 0))
Perf on Firefox is unchanged, which is quite disappointing
Profiling
I tried to do some profiling to spot differences between Firefox and Chrome
My main discovery is that on Chrome, "Micro tasks" each take about ~20ms to run. On Firefox "Dom Events" (which i think refers to the same thing as Chrome Micro tasks" take about ~100ms so about 5 times more.
This may be a coincidence but i have ~150% simulation time on chrome and ~30% on Firefox, 5 time less.
I have an intuition that the infinite for loop may be causing this.
So I'll have a try at refactoring the async execute function to run without using await, but by enqueuing execution task at the end of the function. Like this:
I'll see how this goes !