Blazingly Fast JavaScript

Refactoring Timer to Remove Promises



Blazingly Fast JavaScript

Check out a free preview of the full Blazingly Fast JavaScript course

The "Refactoring Timer to Remove Promises" Lesson is part of the full, Blazingly Fast JavaScript course featured in this preview video. Here's what you'd learn in this lesson:

ThePrimeagen makes changes to improve the performance of the game loop. They remove the use of timers and instead create a ticker class that returns the next time the game loop should run. They also create a map of expiry callbacks and update the game loop function to use callback recursion instead of an async loop. The goal is to reduce the number of promises created and improve memory and CPU efficiency.


Transcript from the "Refactoring Timer to Remove Promises" Lesson

>> So let's make some changes, no more crazy timers. We just have so many of them, potentially 120,000 a second out of 1,000 concurrent games running. Let's try to make something that's a little bit more clever. So what we're gonna do is we're gonna pull out the timer, and we're gonna make it only return numbers, which will be the next time in which you need to wake up.

The next step is that we should create a map of expiry call backs. At this point, these call backs need to be called, and so that way we don't have to go to the process event loop unless if we want to go there, and we're gonna make this pretty tight.

Next, we're gonna have to update the game loop function so that it's no longer an async loop, but that delicious callback recursion. Everyone loves callback hell. And then hopefully at the end, we'll have some profit, less memory, hopefully less CPU. Added the word lisp in there cuz I just had a lot of parentheses, felt right at the time.

So let's do that right now. This one's gonna be a pretty fun change because it's a very different change. Now, notice that what I didn't do is try to improve. Everything that I just suggested wasn't trying to just kind of change this. We're really rethinking the problem, like how can we create something that's completely different or radical?

How can we change 120,000 promises created, say, down to just 60 or maybe less than 60, maybe 30 per second, just some very small number amount. So what I'm gonna do is first I'm gonna highlight this ticker and I'm gonna delete it. And I'm gonna create a file called ticker.ts.

I'll paste that in there and we're gonna export this thing. All right, awesome. I'll make sure that we have everything assigned. And so we kinda need to thin this out. We don't need nearly as much information in here. We don't need to do the async thing. And like all problems you're going to encounter, what I'm doing exactly right here will probably not be applicable to you.

But the fact that we looked at memory, saw that this was a good portion of our memory, we looked at the CPU, we saw this was a good portion. This is likely going to be a significantly larger win, cuz we have two dimensions in which we're making the program more efficient, not just a singular dimension.

So all right, I'm gonna go down here, and we don't need the remaining. We don't need the promise, resolve. We don't need the end now. We don't need the extras. Let's just do that or erase that. And we're just going to return flooredNext. So that's when we're close to execute next.

So every time this thing is called, we return our next value we should go to, and then increment our next to the next point. Next time it calls, it does it again. Next time it calls, it does it again. So every single time we do this, it should just be a nice consistent timer.

Notice that there's no more async. I'm gonna remove async from that function. It's now just a very simple function that's just returning numbers. Now, we could test this. But this is a class about performance, so ain't nobody gonna test this, okay? We're gonna do this the old-fashioned way, which is not test it and just hope everything goes well.

I'm getting we have an integration test. We should know if this thing fails. No TDD is going on here, so now I'm gonna do something which everybody is gonna be super happy about. We're gonna create a class. Everyone loves classes, don't you? And in this class, remember what we're gonna do.

We're gonna do a map of some time in the future to a list of callbacks, cuz we could have several games at the exact same millisecond that need to be woken up. Look at how beautiful this timer is, and it's also completely worthless? So we're gonna go private, callbacks will be a map.

Let's define a callback type, callback will just be a standard callback, right? So I'll have a number, the time in the future when it should happen, and a callback, right? For whatever reason, I don't like creating stuff right there, it just bothers me, I like to create it in the constructor, I don't know why.

Then we're gonna have a private last update time, which will be a number. And then we'll have a nice little constructor here, and it will have equals a new map, and this.lastUpdateTime does not equal 0 but equals So this is the last time that these things happened.

Okay, good. So let's create an add function. We need to be able to add to these callbacks. And because our goal, again, is to go from 120,000 potential promises down to somewhere less than 60. That's our goal. All right, so I'm gonna go add, and it's gonna take in a callback rate, that's not the right number, we're gonna go like this, when.

When in the future do you need to be called? And that's what this whole ticker is, is that it sets some time in the future for you to be called. Okay, fantastic! And let's do this, we're gonna go when callback, right? I'm kidding, we can't do that.

We've gotta do this whole like, all right, callback equals get this, if not cbs, then we need to go cbs equals an array. And then go up here and forget to do that whole thing because I forget things, and then do this whole cbs.set when this thing, and then cbs.push callback.

All right, there we go, we got a nice little callback adding, right? So every place we're gonna have a bunch of callbacks that potentially could be added at this number. Fantastic, we're looking, feeling good. Okay, next, we just have to return out. We have to return out nothing.

Okay, fantastic! Now we need something that runs the actual time. This is where we're gonna do the actual loop, where we're trying to aggressively reduce the amount of promises or memory created. Should be pretty easy here. So the first thing I'm gonna do is I'm gonna go like this.

Let's do a const start = We need to know when we started cuz there's gonna be an important point. Because remember, what are we doing here? We're controlling when the process event loop takes control again. So we need to be judicious about how we do it. We can't just infinitely spin processing things, because you can imagine that it could get really bad where we could end up spending 30 milliseconds, 40 milliseconds doing something.

So we got to be a bit more judicious here. So I'm gonna go like this, while this.LastUpdate is less than start, so we're gonna only process the things up until this point. If minus start is greater than two milliseconds, let's just break. Let's get out of here.

So we'll only process up to two milliseconds worth of data and that is that. So however many updates we can do, fantastic. So we're gonna grab the last callbacks at this time. And then we're gonna go for, let's go like this, if callbacks, if there's a callback at this time, we're gonna do a little nice little callback right there.

Look at how great that is, thanks Copilot. After that, we need to go this.callbacks.delete last time, and then we're gonna increment last time by one value. So all we're doing is we're literally walking per millisecond and executing all the callbacks synchronously. No more hooks, no more a bunch of other things happening, no more going to the process event loop, no more anything.

It's just in JavaScript constantly running. And then after that, we just need to be able to call ourselves again and run sometime in the future. So we can do something like setTmeout,, and we'll do it pretty much as soon as possible. What this is gonna allow, is that any data coming in from web sockets will be able to be processed.

We're letting somebody else run for a moment and then we're gonna go back and try to keep processing data. So pretty straightforward stuff right here. But this is where my intensity comes in. I just hate the fact that when I look at this, I'm creating this little function right here every time just to call this function.

Because we're already trying to be so efficient, so I wanna just put this on the shelf and make it maximally efficient. So I'm gonna go this, private boundRun, which is going to be that, and I'm just gonna go this.boundrun equals this.runbind this, right? And so then I can just do this beauty right here, this.boundrun, 0.

There we go. Look at that, doesn't that just feel better? I just feel more emotionally less in pain. I just love making things as efficient as possible, and so I feel very happy about this. We have effectively removed a bunch of memory being created, a bunch of promises.

It looks pretty good, but now how do we start this? How do we return this? Hell, this is where the fun is gonna get here. This is where people are gonna really love me after this. I'm gonna just do a quick export function, getTimer. Whoa, did you see that, it said getTicker too?

That is beautiful. So I'm gonna have a singleton, but we still have to call run. So what I will do instead is I'll do a nice little static function. Isn't this just beautiful? Create const timer = new Timer(), timer run, return timer. There we go. There we go.

It's a singular item, little state container runs by itself. I could have done this all that module level stuff. But if at some point I wanted to test this, it'd be nice to be able to kind of export all these things, expose it and be able to run and actually test that we're doing what I want to do.

And I also just love creating classes to trigger a JavaScript people. I think it's just tons of fun. So there we go. Great times, great times. So I think this will all just work first try. I assume I'm smart enough to be able to do that. And so now all we have to do is go back to our game and we need to make these updates.

So, this is not really available anymore, so I'll import this in. Now remember, this just gives us the next time to run. It doesn't give us anything other than that. So that means this needs to change. We can no longer use the Lord's loop, a do while loop, we need to use something a little bit different.

So I'm gonna have a function and we're gonna to call it run. And that's all we're gonna use. And I'm gonna also have let lastRunTime = So that way I can kind of keep track of how long it took me to run now. So I'm gonna move this logic somewhere else.

All right, so that means ticks no longer equals this. Ticks = minus lastRunTime. So that's how long it took for us to call this thing. So there we go, kind of makes sense, right? We're kind of walking through this. So all this stuff, still the same, still the same except for right here.

We can no longer do the while loop. So I have to move this into an if statement. And the best part is I get it, what is it? Modus ponens? Is that what we need to apply here? I always forget my logic name. I need to convert all the ampersands into that and then convert all the bangs into that.

So there we go. So if the game has ended, or a socket's been closed, or a socket has been errored, we need to do something different. So I'll call finish. Else, we need to call run, but we don't wanna just call run directly. We wanna go getTimer(), add, we're gonna add ourself, at the game ticker, next tip.

So there we go. Add us into the future. Let us run at some point. We'll be very happy about this, fantastic. So there we go. That looks pretty good. All right, fantastic. Now we have this whole problem down here, which is just now what we want to do, right?

This is kind of problematic because this will just run right away. So that's why I created this little finish function, so I can go function finish, there we go. And do that. That kind of made goofiness, so I'll just go, why is that doing that? Why is that doing it?

Trust me, Vim is super cool! All right, there we go. Now we have this little finish function that's gonna do all the same finishing we did before. If we stop with one or two, we'll stop it, else we'll send, hey, opponent disconnected, or we're gonna do this right here.

We're gonna send out the messages like, hey, the game stopped, everyone's done. And then we're gonna keep track of everything. So therefore we're now kind of keeping track of everything that has happened. There's one more small tiny, teeny, tiny problem that I don't know if anyone's really thought about, which is going to be the classic issue, which is inside of our ticker.

We do everything right except for one thing. What happen if we run into the future and it's taken too long to process everything, and then one of our timers tries to call this function in the past, right? Because our ticker only adds 16 milliseconds, which is probably not that great.

So we kinda have to do something either inside of here or inside of here. Now, I would probably recommend, let's just fix it in the ticker. If we run so fast that we're now into the past, we're gonna just move ourselves forward and cause a starter, cause a larger tick.

Does that sound fine? It's probably the best way to do, which means our update function will get called multiple times. I think that's probably fine. And so we're gonna go math.max, which is giving me now plus 1 or flooredNext. All right, which we could do that one, which maybe that would be good, but then of course we haven't done this thing right here.

So we do need to update this a little bit more properly. So we're gonna do a little bit of this. We're gonna go, if now is greater than flooredNext, then we're gonna go flooredNext equals now plus 1. So we'll put it slightly into the future. Hey, you're gonna happen one moment into the future.

And then we'll do next equals flooredNext plus rate, so that's our next time. And our previous now remain the same. Else, we'll just do this. And now we can just redo the whole four days. All right, does that look good? By the way, during this presentation, I only realized that this was a bug during all the times I was doing things right now.

And so this was kind of a live coding update that may or may not be 100% well thought through, but this should just work. Hashtag that out if anyone's wondering. All right, fantastic, looks good to me. Looks good to me too, I just wanna double check this. If now is greater than flooredNext, then obviously we need to move it forward.

We need to move forward next by that, else we do this. Previous now equals now. Perfect, okay, I think that's correct. I think we're looking good on that one. You should never admit your downfalls, by the way, publicly, but I'm okay with it. It's a shameful moment for me to actually have this happen.

So what I'm gonna do is, I think everything is working, so I'll just do a quick tsc to make sure, okay, all my types are working. We're probably pretty close here. So let's do a quick run test. Obviously, the first four tests should just work. I would be very surprised if the first four tests broke because they're only involving the game and making sure the bullets and everything are shooting correctly.

If you're ever wondering, if you're using this program and you're trying to implement this test yourself, and you're wondering, is integration doing anything, you can always tail test log. And so notice that we're not doing anything. So my guess is that we actually broke something right here. It's not running.

We're not doing something. So what the heck has happened? No, this is always the worst possible answers when things break live. This is the one I was hoping wouldn't break live. So what did we do wrong here? So sorry, we got to do a quick moment of debugging.

Let's make sure everything looks good. When is into the future. Start is right there, fantastic. Well, last update is less than start. Okay, fantastic, given now minus start is greater than 2, then we do that. We get the last one of those, we do a little four const.

I know live demo. Of course live demo, we delete any of the previous time, we do this, and then we rerun it again. Okay, so I mean, I feel like this all looks pretty good. Let's go back to our game just to make sure that our game is actually doing what it's supposed to be doing.

Of course, I am so stupid. Are we running? No, we didn't run, we didn't run. We didn't even run the thing. So here, we've got to make sure we start the game loop. All right, man, I'm a genius. All right, so we rerun this thing. We jump over here.

Is the thing actually gonna start running? There we go. We saw some things run. Nice! All right, so what went wrong here? Luckily I prepared for this one. I actually made this mistake earlier. So we didn't update last run time. Just lucky moves going on right now. There we go.

Just in case, I know it's happening, there we go. Now we're actually properly calculating the game loop out. Okay, fantastic! These kind of changes can be quite difficult to make. But we've made them. We're feeling successful right now. And so, let's do this one. We're gonna kill this, we're gonna do this.

Remember, my server sometimes leaks. My integration test sometimes leaks out zombie processes, just part of life, just as just a little thing we get to do here. There we go. All right, this looks good. I believe we're gonna play ourselves a correct thing. We'll let this thing play out.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now