And so, obviously, there was some very unnecessarily dumb stuff that we learned when we fixed this. One is, like, okay, so there's this one class that has a documentation that says it closes all open resources held by this REPL instance, and then it does not close or did not previously close one of the core resources that it held. Which is like, yeah, duh, that's going to leak your memory. But then, yes, there's also really tricky to detect, and that's kind of like the stuff that I was talking about, right? Obviously there's this extra line, which is also part of the issue here, but, like, replacing eval with this contextless eval function, and contextless eval is really just eval put into a separate file so it cannot capture any variables. That is all it is. That is the core of the bug that we fixed here, because otherwise this eval call that we had to do couldn't, like, obviously eval is evil, and we know that, and we shouldn't be doing this, but in our case this is just what we have to do. This was actually capturing enough of the outer scope that it was keeping objects in memory for way longer than they should have been.
And then, as with any good story, there's a Node.js core bug that we ended up fixing. So, like, we learned that the listener handling in the Node.js core REPL actually had a very similar issue, where it was calling process of prepaint listener, so basically adding an event listener, and it was passing a callback. And that callback did not depend on any variables in the scope, but it was enough still to capture the entire outer scope, so it kept the REPL instance that created it alive, even though that was completely unnecessary. And the fix kind of is just, like, to pull this to the top level so that it doesn't, it's not the closure that captures the actual REPL instance that adds it anymore. It's a, yeah, a fairly easy thing to miss. But luckily we're allowed to do open-source contributions to Node.js core, and we can just fix this.
Anyway, and coming to the closing in on the end of this, you might wonder, how do you actually test this? Obviously we fixed all these bugs and then our CI was green, but, like, how do you make sure that you do not regress on this, right? That's tricky. Because, like, obviously you might still, you know, notice if things go really bad, and you introduce similar memory leaks to the ones that we previously saw, but if something goes a little bad, you're not going to notice immediately. And part of the core of the issue is that garbage collection in JavaScript is unobservable, right? Or at least this used to be true for a long, long time. It's not anymore. So some of the newer stuff, this isn't that new, this is, like, a couple years old at this point, but, like, there's something called finalization registry in JavaScript now. And using it is incredibly hard. And I would... Or not using it, using it correctly is incredibly hard. Using it is pretty easy, actually. But yeah, so this is tricky to get right, but you can observe the finalization of objects through this API. Like, you can get a callback that is called when an object is removed from the JavaScript heap. So perfect. There are some Node.js specific APIs that you can use. There is Vieta query objects, which is pretty cool. I would encourage you to just try it out, one of your applications at some point, where you can pass a constructor to this function. And it will give you all the objects in memory that are created from this constructor. And so this is, like, an easy way to tell whether, you know, there is something that is still in memory that shouldn't be.
Comments