Blazingly Fast JavaScript

Refactoring Collision



Blazingly Fast JavaScript

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

The "Refactoring Collision" 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 discusses the previous optimizations made, such as moving promises out of the main area and using C++ for processing frame data in WebSockets. They also introduce a new optimization technique by rethinking the update function and making it more specific to the problem at hand. They make changes to the code and test the performance again to see if the optimization has made a difference.


Transcript from the "Refactoring Collision" Lesson

>> We are going to continue on with our optimizations. So if you cannot remember where we just were, we did a third optimization merge with the second optimization, which of course is gonna be moving promises completely out of our main area. So our hotspot is now depromisified. And second thing was to move away from JavaScript processing of frame data in WebSockets and move it all into C++ using micro WebSockets, which is a very, very, very good library.

I absolutely love them. Bun uses them. Bun's been enjoying all the performance accolades. And a lot of that is just simply because they try to do as little as possible, and JavaScript, very, very good at that. So what we're gonna do now is we're just gonna actually do a continued measurement and see, do we actually see a difference if we include our update function win?

Now, remember, our update function does win, it doesn't win a whole bunch, but it does win. And so maybe we have room to even make it better, faster, stronger, we're just not doing it at this current moment. Okay, so we're gonna jump over here to our third one, and I'm gonna add this git commit.

I forgot to do this, third opt. There we go. Yeah, we don't need to push that. Git checkout, let's call it fto. So we're gonna check this out. We're gonna do fto, and we're gonna merge in the first optimization. So git merge fo. There we go, awesome. So now we have the improved update function, plus we have the removed promises, plus we have the C++ processing of frame data for WebSockets.

So I think we have as good of a program that we can have right now, because there's no more optimizations we can add. So yeah, this looks pretty good. We'll actually add some more. So let's jump over to here, and let's do one more run. And we're gonna call this one fto and we're gonna just run it at 2000.

Sounds good? Sounds great, thank you very much for saying that. All right, so let's go to cargo, 2000. This one, fantastic. Go over here, I'm gonna highlight this, and we're gonna add in fto, there we go. Does this make a difference? I'm hoping we see a bit of a difference.

So we're gonna let this thing run for a little bit. And maybe we'll check it out on the perf, see how much time is left over on the update function. Because at this point, our update function making that faster, despite it showing up as almost always a top line item, is just not meaningfully impacting our program.

A little bit, but not a lot of it. And so I'm very curious if we're gonna see something here. And so it doesn't look like we're still seeing pretty much anything at this point, which is a little too bad. I mean, it still hasn't quite hit enough. Maybe we just didn't make it fast enough.

We can check out how much time it's actually still running. Maybe we just haven't hit that point where it really starts making a difference. And so one of the big problems, of course, is that anytime you run things, you're gonna get slightly different results, especially if you're running it on localhost.

Often I find that when I restart my system, I'll just get simply different results than I got before. Because again, the same production, this is localhost, take it with a grain of salt. So we'll cut this thing down. And so we're still not seeing a whole bunch, Which is a little sad to see cuz it just feels like it should.

So we're now gonna do an inspect, and let's just see what is our opportunity potential size of the update function. All right, we'll do that, we'll do this. And let's go over to our performance side of things. And once we get a few of these running, we will go in here and we'll hit Record.

Gonna wait ten seconds again. You don't need a ton of time in here, I find that usually if I go longer, the numbers do change a little bit. It just takes longer and longer to process because it is displaying all of those little items. And so it's not necessarily that great.

So it looks like our update function is still taking a lot of time right now. And so we probably have something that we could improve. Another thing to look at is that we're still largely idle, so maybe we just haven't put enough pressure yet to really see is this thing still affecting it at maximum load?

Because our server's gotten way more efficient than it ever was before. So maybe there's some problems here. We're having a bit of a testing problem, if you will. And so let's just start. Actually, well, before I say what we're going to do, let me check my notes because I think we got some good things going.

All right, so we're gonna rethink the update function again. And this is a technique that, I mean, okay, so it's hard to call this a technique, but it is a technique. I'm gonna try to do this in a way that helps people understand, cuz I see it constantly.

You'll see this, I'm guaranteeing, you do this, I do this, we all do this. So what can we say is true about the update function? What does it do? It takes the bullets and compares them against each other to see if they've collided, and compares them against player one and player two to see if they've collided.

But if you remember during the beginning, the bullets shoot in a straight line. Only the first two bullets can ever collide. Do we ever need to check if there is a collision with the player and a bullet, if there exists two bullets, one from each player? No, because if there's two bullets, they can never hit each player.

If there's only bullets from one player, then there actually is a chance for it to be able to hit a player. And so this is called project-specific optimizations. Actually really thinking about your problem and stop trying to program the general solution. Often you'll find yourself making these generalized functions because at some point in the future, things might change.

And though that can be good, it often, a, leads you down a point of just abstraction and which causes you to not actually write as good of code. But b, you also just make things slower cuz you're planning on some future state where things are gonna change drastically.

And is that the case for this? Well, no, for the case of this, we're not changing. So let's try to think about our update function and exactly what it needs to do. And so I'm gonna make this change and we're gonna branch off of the third optimization. We're gonna call this one blazingly-fast, okay?

We're gonna make a very specific change to only address the problem we're addressing. Cuz right now, again, we are solving, if we look at the function, we are truly solving the general case. Any bullet can hit any other bullet, any bullet could hit a player, but that's not the case in reality.

Only certain conditions have to be met for things to happen. So what I'm gonna do is I'm gonna actually re-kinda create how we do PlayerState, and I'm gonna paste these in here. We're gonna do b1, b2. We're gonna have Bullet array, Bullet array. I did not do that well, Bullet array, fantastic.

We're gonna drop this bullet because we really don't need that one. And I'm gonna go b1, copy that, b2, fantastic. So we're gonna create two new arrays. And now we're gonna do the exact same thing, when we call for PlayerState, we're also gonna call for the specific bullet set for that player.

And then when we do update functions, we're only gonna check the front bullets. We don't need to check all the bullets for collision. So I'll first go all the way down here where we actually do the updating. This one, we're gonna take in a Bullet array, right? Probably should have called it bullets because it makes more sense to be called bullets.

So there we go, we have bullets. And then up here we can call this bullets and we're gonna go getBullets. I can't keep saying that phrase, it's getting really weird sounding. And we're gonna go down here and we're gonna getBullets, and this is gonna return, [LAUGH] Bullets. And that is gonna be s/b/gc.

No, yes, no, yes, no. There we go, fantastic. All right, so now we kinda have this thing effectively programmed right here. And so now we need to do the actual update function. So now we're adding player 1 bullets to play player 1 array, player 2 bullets to player 2 array.

This should hopefully get us also an additional speed up because we're no longer having one big array. So when we remove bullets, we're always removing from the front of the list. So we were shifting, say, 28 bullets. Now, we'll only be shifting sets of 14 when they collide.

Maybe it makes a difference, maybe it doesn't. Probably doesn't, but you never know. So let's go, let's think about this. The first thing we wanna do is go for, I guess we can go constant b of this.b1, all right? And go updateBullet(b, delta). Paste that in there. There we go.

We need to update all the positions of the bullets that we did before. And now let's do the actual checking. Let's go const b = this.b1[0], b2[0]. If b && b2, oopsies, then we only need to check for a collision. We don't need to check, do you collide with other players?

We don't need to check any of that because it's already kind of been solved, right?. So we can just come here and b1, b1, and we're just gonna simply shift. I know shifts, this should make you uncomfortable seeing that b1. When you see a shift, you should think, okay, we could probably do better here, cuz shifts mean that we're adjusting all the indices from the front.

It's the worst case scenario removed from an array, probably not the best way to go. But hey, we now have more information to think about for later on. So let's do a little b. If only b exists, that means we need to check against player 2. So I'll just come in here, drop that in.

Else if b2 exists, then we only need to check against player 1, right? b2, there we go. So it's really that simple. We actually don't even need these returns here. And that means all of this goes away. So now we kinda have this, well, really much more optimized way we're attempting to do things.

We're really only doing the absolute minimum for this specific problem and nothing else. And that's really my concern here. Now, just to make sure everything's working, I'm gonna do an npm run test game. We don't need to test the integration at this point, we're only changing the bullets.

As long as they collide, move, and do all the things, we've probably done something right. Hey, this doesn't work anymore. You're absolutely right, it should be b1.length + game.b2.length, fantastic. You forgot to make this also, again, right? Thank you, TypeScript, saving my bacon with all this classing. I know everybody loves classes, so there we go, we made it all work.

This is beautiful, so now this is working again. So, does this make a difference? So, of course, what's the first thing we do? We've made a small change, we've made a hypothesis, now we validate, have we actually made a real change? Well, it's a good question. I also forgot to do git status, git checkout -b blazingly-fast, okay?

Cuz we're gonna really just make this thing run as small as possible. All right, so let's go like this, a tsc, we're gonna run with inspect under bf-2000. I'm also positive if I just increased the connection amount, we'd start seeing differences, but I would love to see it if we could see it at the smaller level.

All right, so I'm just gonna hit Record again, see how we do. I'm hoping to see something good. All right, so our update function does look a touch smaller. Last time it was sitting around, what was it, 20%? We're now down to about 10% of the time. So I think we've made a good thing.

We could kinda go back and forth and test it a couple of times to see, are we actually making a difference? But to me, this looks like a big W. We've again just made the amount of time we're processing JavaScript just keep shrinking. Look at our idle time.

Our idle time is through the roof and we're at 2000 connections. I'm pretty sure we can increase the amount of connections and probably not see a huge degradation in performance. So I think there could be a there there at this point. I'm happy about what I'm seeing. So let's try the old, we'll drop the inspect.

I'm gonna go rm/tmp/bf-2000 cuz we don't want that because we had the old inspect flag on there. Which if you think about it, I mean, it's still really crazy. At the beginning of the day when we first ran our program, 500 was 32% of the time. We were having bad frames.

Now we're at 2000, significantly less than that. And so you can make even a slow thing still fast relative to itself. Now, if we were to write this in Rust, we could probably do 10,000 games all running on time. It's a different order of scale, but then you're also writing in Rust, and then you have to tell people about you writing in Rust.

Then there's a whole set of complications. Then you start tweeting about it. It's very complicated, you probably don't wanna do that. Not at your stage in career. It's really only when you wanna ride into the sunset do you do that. Okay, well, look at that, our ratio is actually also looking really, really good.

We may have actually made some sort of dent using our update function speed up, so this is really, really good. I'm very, very happy about that.

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