It began during RailsConf.
My fingers were itching to code, so between sessions I started tinkering with some of the more fanciful enhancements to The Ruby Racer I'd been contemplating as well as wrestling with a number of long-standing bugs. But what started out as a small refactoring slowly but relentlessly gained momentum until things were completely out of control.
By all accounts my hands were steady and the work proceeded in an orderly fashion. Instead, it was I who was out of control. Personally and psychologically I was helpless to stop myself as line after line, class after class unravelled before me --each one more rapidly than before. Soon a critical mass was achieved and boom! Nuclear. A from-scratch rewrite was underway.
Yes it was a rewrite, but by the time I was aware of it, I was too deeply invested to turn back.
It's a tichy thing, is a rewrite. Instead of the careful, iterative approach that serves as law in our profession, you drop everything and sprint towards the cliff in the hope that you can lunge across the chasm in in one leap. Once you're in the air there's no going back, and the possibility is as thrilling as it is real that, like Icarus, you'll hurtle into the abyss... another unsung casualty of hubris.
The prize of course is a better library that is more stable, has more features and is easier to build and install, so if a rewrite it had to be, I did the only thing I could do yield myself to it utterly.
I'm happy to say that it's mostly there there and that, despite some outstanding flaws, I'll be releasing the next version shortly. But more on that later. First, what's new?
I've come to realize that to solve these memory problems in the general case would require each host Ruby platform, as well as V8, to expose a uniform interface to its garbage collector so that The Ruby Racer can traverse its object graph in order to detect cycles of garbage. In other words "not bloody likely."
As a fallback, I've decided to throw up my hands and eschew GC histrionics in favor of more sane memory management in the C extension combined with an explicit teardown mechanism for cases where cycles of garbage do occur. e.g.
context = V8::Context.new context['cycle'] = context #oh no, a vicious cycle! context.dispose() # Gordion knot is cleaved, sir!
In exchange, we get a kinder, gentler racer that works on MRI as well as Rubinius, and which you'll be able to rely on in your production processes.
Speaking of production, the upcoming release contains a much more comprehensive coverage of underlying V8 functionality than every before.
Among other things, you can query the V8 vm for heap usage by young generation, old generation as well as absolute heap size including executable memory. Not only that, but you can also place hard caps on each of these numbers. That way you can contain memory usage and keep your processes under control; even in a resource constrained environment like Heroku.
A (hopefully) kinder build
One of the sorest pain-points with The Ruby Racer was the way it built. Prior to 0.11, it had a hard dependency on the libv8 rubygem in order to provide the actual V8 library. In retrospect, this was a bad idea.
If you were unable to build the v8 contained in the libv8 gem, then you were completely hosed. You could not use therubyracer at all. Or, if you wanted to use a custom v8 patched for security or with tweaked startup data, then you were out of luck. Finally, it raised an unnecessary barrier to distributing therubyracer as a binary gem (which I would like to do more of). If you have a binary version, then why do you need to have libv8 at all?
For these reasons, the dependency on libv8 has been converted to a soft one. If a compatible version is detected in your rubygems, then it will compile and link against it. However, if for whatever reason, you do not want to use the libv8 gem to build, then that is now your prerogative. The Ruby Racer will search your system for v8 just like any other library.
The only difference is that if you want to use the libv8 gem, then you will have to explicitly require a compatible version in your Gemfile.
More bugs, but not on Rubinius
There are still some outstanding issues on MRI that emerge under a heavy stress of rapid object allocation and deallocation. As far as I can tell they are related to to the weakref implementation, wherein MRI gets confused and starts handing back references to completely unrelated objects. I say "as far as I can tell" because the subtleties involved lie beyond the frontier of my understanding of MRI internals. Given my current workload, it is unlikely I will have time to make the necessary investment in understanding required to completely quash these problems.
Despite this, I'm releasing the new version anyhow in order to support Rubinius more fully and because I think that even on MRI it will solve more problems than it introduces.
On the MRI front, I still think that closing some of the loopholes in the memory management, the enhanced extensibility, and better build make it worth releasing anyway. I've been recommending people to upgrade to the beta for almost 3 months and it is almost always the right decision. Given that I have neither the time nor the inclination to support what I now consider to be the legacy release, I think it's high time to either shit or get off the proverbial pot.
If you, or anybody you know has a deep knowledge of MRI internals and would be willing to help me address these issues as well as others that may arise, I would love to work with you to knock 'em down.
So this is why I'm releasing version 0.11.0 of The Ruby Racer today. Of course, I may
come to quickly regret it, but that's what
gem yank is for right?