00:00:11.240
Hello! Should we get started? Are we ready for this? Okay, let's do it.
00:00:16.480
Um, welcome to Kansas. I have always been very excited about this RailsConf because I always wanted to come to Kansas.
00:00:23.599
I've always heard about this movie called 'Wizard of Oz,' and as you can tell, it's a little bit ahead of my time. But I watched it before I came here in preparation for the conference.
00:00:31.359
But as we learned in Jeremy's keynote the other day, it turns out these lines on the maps are state lines. Kansas is on the left, and the other half is Missouri.
00:00:38.200
And Kansas City is actually not in Kansas, so I guess we're not in Kansas anymore. Thank you for that.
00:00:45.000
I had to redo all my slides to add colors to them, so if they don't look very good, that's probably why. I am Godfrey. You can find me on the internet as SheninCode, and I'm very excited to welcome you to your new employee orientation.
00:01:09.479
I hope you're at the right place. If you're looking for RailsCon, I'm afraid that you've moved... just kidding, this is RailsConf.
00:01:15.400
Welcome to RailsConf, and thank you for coming. I always say there is a very personal connection for me regarding RailsConf, and I love coming back.
00:01:31.040
This is actually my fifth RailsConf. Five years ago in Austin was my first RailsConf that I attended on a student scholarship. So if you were there, thank you for chipping in for my ticket.
00:01:53.240
Thank you for giving me the opportunity to be part of this community. The next year, I went back to RailsConf Portland, and for some reason, I suddenly had the courage to go up and say, 'Hey, I have a pull request. Can you merge it?' And for some reason, they ended up merging it.
00:02:13.080
So, I had my first commit in Rails, and soon after, at the next RailsConf in Chicago, I joined the Rails core team. The following year, I had the pleasure to speak at RailsConf for the first time, which was last year.
00:02:30.879
This year, I am very honored to be part of the program committee for RailsConf and helped create the 'Behind the Magic' and 'New in Rails 5' tracks.
00:02:49.319
So, if you went to those talks and liked those tracks, well, that's where they came from. And, as Jeremy said the other day, Rails exists because of its community, so thank you for being part of this community.
00:03:08.560
Following Aaron's lead, I would like to announce some new Rails 5 features. As you learned today, Rails 5 will come with PHP support, and I would like to announce JavaScript support for Rails 5.
00:03:27.200
In fact, you don't even have to wait for Rails 5; you can get it today by running `gem install javascripts`. Once you have installed the gem, all you need to do is require JavaScript at the top of your Ruby file.
00:03:44.400
You can wrap anything in a JavaScript block and write your JavaScript code there. It even supports things like functions, which is very handy because what is everyone's favorite JavaScript feature? Of course, that's callbacks. And what's everyone's favorite Rails feature? Of course, that's also callbacks.
00:04:25.880
The JavaScript gem lets you combine the best of both worlds. For example, here's the Active Record model; you can have your favorite `before_create` callback and write that in JavaScript. Just like the Ruby gem, this is a real thing you can use.
00:04:34.360
It requires an insane amount of engineering, but if you want to learn more about that, I suggest you watch my talk at Guko called 'Dropping Down C', which you can find on YouTube.
00:05:04.800
I have another thing to plug: if you went to the lightning talk from yesterday, you would already know this, but I helped coordinate a newsletter called 'This Week in Rails', where you can find the latest commits, pull requests, and other updates from Rails each week.
00:05:36.960
Here is a sample from last week, including sensational headlines like 'Local Scientists Discover New Method to Manipulate Time' and 'Faster Code Found to Perform Better on Load'. If you haven't already, go to bitly.com/railsweekly to subscribe to this newsletter. The next issue will be coming out in a few hours, and you'll probably learn something new.
00:06:02.560
Speaking of newsletters, I'm also part of another newsletter, which is a product newsletter. If you are not already signed up for Skylight, you should do so by visiting skylight.io and selecting the 'Almost Daily Insider' email preference.
00:06:28.680
We usually write about our experiences writing and building Skylight, often discussing problems we've encountered and solutions offered by our customers. So, if you're interested, you should go sign up, and if you have pen and paper, you might want to write down that secret URL that is actually my personal referral link.
00:06:50.320
I heard we have plans to distribute bonuses this year based on referral credits in our account, so please help me out. Anyway, let's talk about Ruby. Ruby is great. We all love Ruby, and that's why we're here.
00:07:12.240
Ruby has many nice features; its metaprogramming capabilities are pretty awesome. However, there's a problem with Ruby—it's pretty slow. Most of the time, this doesn't really matter, but occasionally you might hit a wall when trying to accomplish something in Ruby, and it's just too slow for your use case.
00:07:35.840
On the other end of the spectrum is C, which is a low-level, super-fast language—it's as close to the metal as you can get without writing assembly. That speed is great, but it's also dangerous. You can easily write code that crashes your program at runtime in an unexpected way.
00:07:50.840
There are a lot of concepts that are a little bit hard to grasp. In Ruby, we have a feature called native extensions that give you the best of both worlds. For example, when you run `gem install json`, what you're getting is actually two different things for the price of one.
00:08:14.440
By default, you will get something called 'json pure', which is a pure Ruby implementation of the JSON encoder. But if you're on a supported platform, you'll also get a thing called 'json_ext', which is a native extension.
00:08:29.640
That's the same JSON encoder API written in C, and it's super fast. As a user, you don't even notice the difference; you just call the regular Ruby class and method, and under the hood, it transparently calls a C method to do the work for you.
00:08:56.240
Chances are you're already using the native version without knowing it. So, native extensions are great, but why don't we write more of them? Well, there is a catch.
00:09:05.760
While it's indeed the best of both worlds, it's only the best of both worlds if you're the end user using the gem. This is fine. David, who created Rails, once said something like this to me: 'We will jump through whatever hoops on the implementation side to make the user-facing API nicer.'
00:09:30.640
I think I would personally extrapolate that user-facing API to developer experience in general, and I think that's a good goal to have: make the experience for your developer users as nice as possible—with a beautiful interface on the outside and whatever you need on the inside to make it work.
00:09:50.360
A good example of this is Sam Saffron, who you might have heard of. He did a lot of Ruby performance work and was a Ruby Hero from last year. He noticed something in Rails, particularly in ActiveSupport. It turns out that there is a method called `string.blank?`, which is called a lot both inside the Rails framework and also in user code.
00:10:15.040
This is also the implementation of the flip side called `present?`. You might have seen things like `User.present?` or `params[:user].present?`. This is actually the method we're talking about. As of a few weeks ago, the implementation in ActiveSupport looked like this.
00:10:35.200
It's pretty short—it basically reopens the string class and checks if the string consists of all whitespace characters. It's a one-liner that reads beautifully in your Ruby code, and it's evidently a very useful method because we use it enough in our application and Rails to make it a performance hotspot.
00:10:56.440
According to Sam, he made a gem called 'FastBlank' that reimplemented the same blank method in C. This C implementation is up to 20 times faster than the Ruby version we saw on the other slide.
00:11:17.080
In some applications, you can achieve up to 5% performance improvement on average. As I said, there have been some recent improvements to the Rails version, but for the purpose of this presentation, that doesn't matter much because the optimization in Rails is about the edge cases or common cases when the string is empty.
00:11:37.119
But as a user, you don't really need to know the difference. You just get the FastBlank gem in your app, and all of your code works seamlessly because it provides the same `string.blank?` method, just with a different implementation under the hood.
00:12:10.560
You can get the performance you want and the user experience you want. So, that seems great—are we done here? Well, there is a problem, though: the problem is me.
As a developer, I know just enough C to be dangerous. If you give me a C program with a variable called PTR, the first thing I will try is probably to add a star to it, and if that doesn't work, I will try adding more stars.
00:12:30.640
If that still doesn't fix it, I will try the ampersand. The problem is, as I mentioned before, if you're like me and your C code looks like this, your program can crash at runtime. That's a significant risk because if you're embedding your C code inside a Ruby process, it will crash the entire Ruby process.
00:13:02.760
When this happens, there's not a regular exception that you can handle. At Scala, we have a similar problem—by the way, this is where I work. We are a performance analyzer for Rails apps.
00:13:14.640
To do that, we have to have an agent that we put inside your app to collect performance data in production, and we want to make sure that our agent is as lightweight as possible. We don't want the thing that's supposed to measure your performance to become a bottleneck.
00:13:36.560
So, we could write that agent in C or C++, and fortunately, most of our engineers are smarter than me; they don’t randomly add stars and ampersands to variables in C. Even then, we don’t feel confident enough that we can write and maintain our own C code that goes inside all of our customers’ apps.
00:13:55.960
There are a lot of native extensions in the Ruby community—like Nokogiri or the JSON gem. In their early days, they all had various runtime issues. Those people are way smarter than me, and if even after careful writing they still occasionally crash, then we definitely won't want to recommend our customers put something like that in their app.
00:14:17.560
So, what is the alternative? At the time when we started this project, Rust announced that they had made very good improvements. We thought, 'Hey, we'll perhaps give that a try.' What makes Rust different from writing your native extension in C or C++? Let's look at the Rust website.
00:14:51.279
Rust is a system programming language that runs blazingly fast, prevents segfaults, and guarantees memory safety. Those are a lot of words that don't mean much yet, but they also have another slogan that puts the same thing in different words: 'Hack Without Fear.'
00:15:07.440
The goal of the Rust project is to make system programming more accessible to more programmers, and it achieves this by having a compiler that can find most of these errors that could cause your program to crash at runtime and flag them at compile time.
00:15:26.640
If you don't satisfy the Rust compiler that your program is sound, it simply won't compile. Therefore, you don’t have a thing to run at runtime, and it cannot crash at runtime.
00:15:44.360
There are features in Rust that make that possible, but I can't go into a lot of details today. This talk isn't about teaching you Rust, but I'll describe them at a very high level so you can understand them without seeing actual code.
00:16:10.560
First, Rust manages the safety of your memory without using a garbage collector. Ruby also offers memory safety guarantees. In Ruby, you cannot write code that accesses random locations of memory, causing your program to crash at runtime.
00:16:34.320
This may sound crazy, but your Ruby program cannot crash at runtime if you write it correctly. The difference is that Ruby manages this using a garbage collector, while Rust tracks the lifetime of your variables at compile time.
00:16:50.400
This means Rust knows when and where to locate things and when it needs to clean them up, so it doesn't have to pause the program to clean up with a garbage collector periodically.
00:17:09.120
It also allows you to do concurrency without data races or race conditions, but I don't have time to get into that right now. The final feature that's particularly relevant to us is that it has this concept of zero-cost abstractions.
00:17:23.840
In Ruby, or in most other languages, you always have to make a trade-off between abstracting your code and performance. You might notice that you're repeating steps and want to extract that into a method. That's fine, but it incurs the cost of an extra method invocation.
00:17:39.440
When you're discussing really performance-sensitive code, you have to choose how much to abstract your code versus how performant you want your code to be. In Rails, for example, we have a lot of modules, and we cost super often and that's how we chose to abstract our code ideally.
00:18:05.040
But there might be cases where we realize this is a really hot path, and we prefer not to incur that cost. In Rust, this is one of the biggest features; you don't have to make that trade-off because the Rust compiler is smart enough to notice that this method can be inlined into another.
00:18:22.960
In fact, often if you use higher-level constructs in Rust like iterators, you're giving the compiler more information about how it can optimize your code.
00:18:38.200
So it actually makes your program run faster. For example, in Ruby, you might want to write things in an `each` loop, and we often do, and that's fine, but that incurs the extra cost of calling the `each` method.
00:18:55.440
On the other hand, in Rust, if you use an iterator, it actually makes it faster than writing a hand-rolled loop because the compiler knows that this is going to be a safe iteration, so it can remove some of the bounds checks for each iteration.
00:19:09.560
I can't get into a lot more details, but Yahuda gave a talk on Rust at RailsConf last year. If you are new to Rust and are curious about why you might want to look into this language, you can look up his talk from last year.
00:19:31.440
Now, let's get back to FastBlank. I guess we'll look at the FastBlank implementation quickly. This is the FastBlank body; you probably cannot read it, but it's fine—we'll walk through it step by step.
00:19:57.320
At the top, you have the method signature and some boilerplate to extract some pointers. If the string is empty, then return right away so you can avoid doing a bunch of extra work. The main part of the method loops through all the characters inside the string.
00:20:21.760
If you encounter a whitespace character, you keep looping, and if you encounter a non-whitespace character, you know that this string is not blank and can return false immediately. If you get to the end of the loop, you know that all characters are whitespace.
00:20:37.640
This probably looks a little bit scarier than it is, only because it's on a slide, but this is about 50 lines of code, and it’s not particularly difficult to reason about. So if we can get up to 20 times faster performance writing 50 lines of C code, it seems worth it.
00:21:01.760
Next, let's look at the equivalent FastBlank implementation in Rust. Here it is—this is literally a one-liner function in Rust that does exactly the same thing and handles all the unique edge cases correctly.
00:21:22.040
Let’s walk through it—basically, you define an ‘extern C’ function; this tells Rust, 'Please, I know this code is going to be called from a C program, so keep the C function calling convention when compiling my program.'
00:21:37.800
That part is not particularly important, except to illustrate that Rust is a pretty low-level system programming language designed to interact with other C programs.
00:21:53.360
The Ruby implementation you're probably using, MRI or C implementation, is a C program, so they work nicely together. The Rust compiler cares a lot about safety, so you have to do a bit of extra work to annotate your code and give it information it needs.
00:22:10.320
Here, we’re telling the compiler what the type of the input to this function will be. The specific type we use here isn't particularly important, but you need to tell the compiler which type each variable is going to be.
00:22:28.520
That also helps the Rust compiler figure out how to allocate memory since Rust tries to allocate things on the stack to make cleaning up faster, which is another key to Rust's performance.
00:22:45.280
Here, we’re annotating that this method will return a Boolean value because the `blank?` method is expected to return either true or false, depending on whether the string is blank.
00:23:04.080
As for the body of the method, it resembles the Ruby code you're used to writing. Here we get all the characters from the string as an iterator and use high-level combinators like `all`, which is equivalent to the `array.all?` method.
00:23:20.720
Inside, you're checking whether each character is a whitespace character. The Rust library has a method for that; it knows how to perform the necessary checks correctly.
00:23:34.680
This ultimately illustrates how Rust can accomplish the same tasks with fewer lines of code compared to C. It's quite surprising how much the code has similar readability to Ruby.
00:23:50.440
Given Rust's high-level features, is this going to sacrifice performance compared to the C version? We ran some benchmarks, and although I won't present them in detail, the Rust version is actually slightly faster than the C version.
00:24:10.960
I’m not being scientific here; I'm just trying to illustrate that you can write high-level code without sacrificing performance. It puts your code in the same ballpark as the C equivalent.
00:24:32.840
However, I must mention that there's a catch I haven't told you about. There is a lot of glue code that I didn't show you here. If you look at the full Rust versus C extension, you'll notice that the red part is the one-liner I showed you, while the C function body is much larger.
00:24:54.520
However, if you consider all the boilerplate code around it, in terms of line count, they're roughly the same size. Not all is lost! I should point out that on the Rust side, a lot of this is common shared abstractions, while on the C side, that's literally your business logic.
00:25:14.720
So, you must write that amount of code every time you create an extension like this. In Rust, the oneliner is the only thing specific to your extension.
00:25:35.120
Yehuda and I are working on a project called Helix, and our goal is to eliminate much of that boilerplate code and leverage Rust features like zero-cost abstractions, allowing you to focus on writing your code without all the boilerplate.
00:26:05.040
Here's the entire FastBlank extension written in Helix. We are using Rust's macro feature, which lets you write things similar to Ruby DSLs. At the top, we import a library and declare our Ruby types.
00:26:23.280
The first thing we do is try to reopen the Ruby string class and add a method called `blank?`. You might find this syntax familiar. Finally, in the code, we have the one-liner we saw earlier, which is all the code you need to write to create a FastBlank extension in Rust.
00:26:44.960
My personal goal going forward is to experiment with implementing some Rails features using Helix. We already have an extensive test suite in Rails, and there are many modular parts like `string.blank?` that can be swapped out for a Rust extension without affecting the user-facing API.
00:27:03.680
This enables us to iterate quickly on the pure Ruby code while taking advantage of the existing Rails test suite to experiment with those lower-level implementations.
00:27:26.160
If we keep running against the Rails test suite, we can be fairly confident that everything is in parity, and it would be an optional feature that you can install, just like FastBlank.
00:27:48.960
If your platform supports it and you trust it, it might offer substantial performance gains. There are many low-hanging fruits in smaller modules, like the ActiveSupport Duration class, which you can optimize.
00:28:06.560
For example, when you call `1.day`, that's the ActiveSupport Duration class that's created. There are likely many smaller pieces that we can experiment with using Helix. Eventually, we can work on bigger components, perhaps even the core routing library in Rust someday.
00:28:28.720
While we’re working on this project, we have a friend who works at Cesti, a catering company in San Francisco. They have a Ruby stack and are trying to deliver organized meals for companies in the Bay Area.
00:29:01.600
They have a meal matching algorithm with constraints, such as allergies that restrict certain ingredients or preferences that require meals to be vegan or gluten-free. The problem boils down to checking if the tags in the meal fully satisfy all requirements in the preferences array.
00:29:27.680
This is the set containment problem. The reason they care is that this algorithm is currently implemented in Ruby and has long execution times—sometimes ranging up to 30 minutes.
00:29:53.040
After measuring performance, they found that a lot of the time is spent in this set containment algorithm. It’s interesting to see what we can achieve by implementing this algorithm in Rust.
00:30:15.440
There’s a known trick for testing set containment. On platforms like Stack Overflow, you might find a solution. In essence, to check whether one array fully contains another, you can perform an intersection between the two.
00:30:41.920
Create a new array that only contains the items found in both original arrays, and then check if that new array equals the preferences array. This is extremely readable and expresses the logic well.
00:31:12.240
They had implemented this method and run it against their existing test suite, confirming that it indeed worked as expected. But we can do better.
00:31:29.280
After thinking very hard, we realized that all the numbers in the arrays are unique and sorted, which allows us to write a more efficient algorithm in pure Ruby that will check these conditions.
00:31:42.880
First, check for edge cases; if either array is empty, you can quickly determine one way or the other. Track the position within the other array while looping through all elements in the longer array, trying to advance the pointer.
00:32:06.760
If you get to the end of the loop and find items in the second array that aren’t present in the original, you’ll know that previous items did not meet the requirements. We benchmarked this and found it runs up to 7 times faster.
00:32:16.400
This algorithm behaves well under most circumstances, achieving about a 2X speedup on average. The fastest scenarios can yield up to 7X better performance.
00:32:36.960
Next, we wanted to see what would happen if we implemented that in Rust. Again, we’re using Helix, with the declarative types macro. We're reopening the array class and defining the 'fully contains' method with type annotations.
00:33:00.120
Here, we have similar edge-case checks and the same boilerplate for tracking the index. Interestingly, in Rust, as I previously mentioned, if you use iterators, the compiler can perform optimizations.
00:33:20.760
Here, we use an iterator instead of writing a hand-rolled loop. Because we're using an iterator, the loop body looks slightly better than the Ruby version in my opinion.
00:33:42.080
After implementing it this way and running benchmarks, we found it performs up to 173 times faster than the Ruby version, depending on the workload. I plotted this data, so you can show the performance differences visually.
00:34:05.680
You may not see all the details, but the performance of the pure Ruby implementation is near the bottom, while the fast algorithm in Ruby and the Rust implementation are performing significantly better.
00:34:31.120
Here is the summary of what we have observed: while the Rust implementation behaves much better, you should see it's usually 10 to 173 times faster than the Ruby implementation on average.
00:34:55.120
With time almost out, I want to present where you can find the code for the Helix project. It's still a work in progress, but we're on the right track.
00:35:13.920
If you're interested in helping out with this project, please come talk to us afterward.
00:35:29.960
Finally, I'll close with this: historically, scripting languages were slow, and they are still relatively slow today.
00:35:45.040
Because of this slowness, they were mainly used as a coordination layer, delegating heavy tasks to system-level languages like C.
00:36:05.240
However, it turns out many operations we are doing are IO-bound, which makes the performance difference less critical than expected.
00:36:24.760
Since web applications are so IO-heavy, this has worked out wonderfully for frameworks like Rails.
00:36:43.840
The ability to run multiple Unicorn processes on a powerful machine serves as good empirical evidence.
00:37:04.960
Despite this, we still encounter computationally heavy operations in our applications. Business logic defines the uniqueness of your application.
00:37:25.680
I think we're entering a new era where we have taken advantages from scripting languages, especially in terms of ergonomics, and moved that into system languages like Rust.
00:37:47.240
Helix allows you to move computationally heavy parts of your applications back to Rust. You don’t need to entirely switch your stack to something like Go to achieve good performance.
00:38:10.720
In my opinion, Rust is particularly suited for this task in Rails teams because of the safety guarantees offered by the Rust compiler.
00:38:30.640
More of your team may be able to tinker and experiment with the Rust code without worrying about causing runtime issues. If it doesn't compile, the worst outcome is just failing to run.
00:38:53.920
In our team, everyone wound up picking up enough Rust to fix bugs or make minor tweaks to our Rust agent. We believe that will continue to be the case.
00:39:15.840
So, the goal of the Helix project is to make this even more accessible to Ruby teams so they can continue writing most of their code in their favorite language without fearing it will be too slow.
00:39:39.040
You can always drop down to Rust if and when you need to. One more time, that's all I have today.
00:39:51.280
You can find me on the internet as SheninCode. Thank you for your time. Let’s make Ruby great again! Thank you.