Share
Embed
Helix: Native Extensions for Everyone - RubyConf Colombia 2017
You can write a native extension for Ruby using Rust through Helix. With Helix your code will be screaming fast, while ensuring thread and memory safety and preventing segfaults. Fast, reliable, and productive. Come learn how we can have our cake and eat it too!
Writing Faster Ruby: https://speakerdeck.com/sferik/writing-fast-ruby
hone
September 08, 2017
Transcript
64 slides
-
Ruby is Awesome • Beautiful syntax • Optimize for happiness
• Community • Mature Ecosystem -
Ruby is Slow • Usually it doesn't matter • Most
workloads are I/O bound • But occasionally it does... -
"Best of both worlds" • Native extensions • JSON gem,
nokogiri gem, etc…. • stdlib too: Date, Pathname, etc... • Very fast • Transparent to the user -
Case Study: Discourse • String#blank? Is a hotspot for Discourse
• Sam Saffron's fast_blank • 50 LoC in C • 20x speedup of String#blank? -
But C... • Unsafe • Risky - segfaults! • Maintenance
burden • Contribution barrier • Cross Platform Support -
Case Study #2: New Relic RPM • Monitoring agent built
in Ruby • Hard to minimize overhead blog.scoutapp.com/articles/2016/02/07/overhead-benchmarks-new-relic-vs-scout -
Case Study #3: Skylight Agent • Started with Ruby •
Too much overhead • Native extension! • But C… • AppSignal has written parts of their agent in Rust too -
Meet Rust • Like C: compiled, statically typed, very fast
• Unlike C: enjoyable to use, guarantees safety • "If it compiles, it doesn't crash" • Same guarantee as Ruby with memory safety • Multiplatform Support -
Cargo.toml [package] name = "hello_world" version = "0.1.0" authors =
["Your Name <[email protected]>"] [dependencies] time = "0.1.12" regex = "0.1.41" -
cargo build $ cargo build Updating registry `https://github.com/rust-lang/crates.io-index` Downloading memchr
v0.1.5 Downloading libc v0.1.10 Downloading regex-syntax v0.2.1 Downloading memchr v0.1.5 Downloading aho-corasick v0.3.0 Downloading regex v0.1.41 Compiling memchr v0.1.5 Compiling libc v0.1.10 Compiling regex-syntax v0.2.1 Compiling memchr v0.1.5 Compiling aho-corasick v0.3.0 Compiling regex v0.1.41 Compiling hello_world v0.1.0 (file:///path/to/project/hello_world) -
Cargo.lock [root] name = "hello_world" version = "0.1.0" dependencies =
[ "regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "aho-corasick" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", ] -
Rust Multiplatform Support https://blog.rust-lang.org/2016/05/13/rustup.html rustup - a toolchain manager for
Rust. "rustup works the same on Windows as it does on Unix" - rustup README Targets: $ rustup target list | wc -l 36 Including: • x86_64-apple-darwin (installed) • x86_64-pc-windows-gnu (installed) • x86_64-unknown-linux-gnu (default) • x86_64-unknown-linux-musl (installed) -
Shipping Rust in Firefox https://hacks.mozilla.org/2016/07/shipping-rust-in-firefox/ "Starting with Firefox 48, Mozilla
is shipping its first production Rust code, with more to come!" -Dave Herman, Director of Strategy at Mozilla Research -
Zero-cost abstractions™ • In Ruby: tension between abstractions and performance
• hash.keys.each vs hash.each_key • array.shuffle.first vs array.sample • In Rust: no such tradeoff • Compiler is magic -
Zero-cost abstractions™ pub fn main() { let array = [1,2,3];
let mut sum = 0; for i in 0..3 { sum += array[i] } println!("{:?}", sum); } -
Zero-cost abstractions™ pub fn main() { let array = [1,2,3];
let sum = array.iter().fold(0, |sum, &val| sum + val); println!("{:?}", sum); } -
fast_blank in Rust* pub extern "C" in fast_blank(buf: Buf) ->
bool { buf.as_slice().chars().all(|c| c.is_whitespace()) } * without boilerplate code -
fast_blank in Helix #[macro_use] extern crate helix; declare_types! { reopen
class RubyString { def is_blank(self) -> bool { self.chars().all(|c| c.is_whitespace()) } } } -
The Vision • Keep writing the Ruby you love… •
...without the fear of eventually hitting a wall • Start with Ruby • Move to Helix when appropriate -
3. Rails Generator $ bin/rails generate helix:crate text_transform Running via
Spring preloader in process 24825 create crates/text_transform/Cargo.toml create crates/text_transform/src/lib.rs create crates/text_transform/text_transform.gemspec create crates/text_transform/Gemfile create crates/text_transform/lib/tasks/helix_runtime.rake create crates/text_transform/lib/text_transform.rb create crates/text_transform/.gitignore create crates/text_transform/Rakefile append crates/text_transform/Rakefile run bundle from "./crates/text_transform" append Gemfile -
3. Rails Generator $ bin/rails generate helix:crate text_transform Running via
Spring preloader in process 24825 create crates/text_transform/Cargo.toml create crates/text_transform/src/lib.rs create crates/text_transform/text_transform.gemspec create crates/text_transform/Gemfile create crates/text_transform/lib/tasks/helix_runtime.rake create crates/text_transform/lib/text_transform.rb create crates/text_transform/.gitignore create crates/text_transform/Rakefile append crates/text_transform/Rakefile run bundle from "./crates/text_transform" append Gemfile -
crates/text_transform/src/lib.rs #[macro_use] extern crate helix; ruby! { class TextTransform {
def hello() { println!("Hello from text_transform!"); } } } -
$ bundle exec rake build $ bundle exec irb >
require 'text_transform' => true > TextTransform.hello Hello from text_transform! => nil -
4. Implement flip method def flip(text: String) -> String {
text.chars().rev().map(|char| { match char { '!' => '¡', '"' => '„', '&' => '⅋', '\'' => '‚', '(' => ')', ')' => '(', ',' => '‘', '.' => '˙', ... // Flip back ... _ => char, } }).collect() end -
4. Implement flip method def flip(text: String) -> String {
text.chars().rev().map(|char| { match char { '!' => '¡', '"' => '„', '&' => '⅋', '\'' => '‚', '(' => ')', ')' => '(', ',' => '‘', '.' => '˙', ... // Flip back ... _ => char, } }).collect() end -
4. Implement flip method def flip(text: String) -> String {
text.chars().rev().map(|char| { match char { '!' => '¡', '"' => '„', '&' => '⅋', '\'' => '‚', '(' => ')', ')' => '(', ',' => '‘', '.' => '˙', ... // Flip back ... _ => char, } }).collect() end -
crates/text_transform/spec/text_transform.rb require "text_transform" describe "TextTransform" do it "can flip text"
do expect(TextTransform.flip("Hello Aaron (@tenderlove)!")).to eq("¡(ǝʌolɹǝpuǝʇ@) uoɹɐ∀ ollǝH") end it "can flip the text back" do expect(TextTransform.flip("¡(ǝʌolɹǝpuǝʇ@) uoɹɐ∀ ollǝH")).to eq("Hello Aaron (@tenderlove)!") end end -
5. Flipping Tables! def flip(text: String) -> String { if
text == "┬──┬ ノ( ゜-゜ノ)" { return "(╯°□°)╯︵ ┻━┻".to_string(); } else if text == "(╯°□°)╯︵ ┻━┻" { return "┬──┬ ノ( ゜-゜ノ)".to_string(); } // rest of method … end -
6. Using in Rails $ bin/rails generate helix:crate text_transform Running
via Spring preloader in process 24825 create crates/text_transform/Cargo.toml create crates/text_transform/src/lib.rs create crates/text_transform/text_transform.gemspec create crates/text_transform/Gemfile create crates/text_transform/lib/tasks/helix_runtime.rake create crates/text_transform/lib/text_transform.rb create crates/text_transform/.gitignore create crates/text_transform/Rakefile append crates/text_transform/Rakefile run bundle from "./crates/text_transform" append Gemfile -
6. Using in Rails (Routes) config/routes.rb: resources :flips, path: "/",
only: [:index, :create] -
6. Using in Rails (Controller) app/controllers/flips_controller.rb: class FlipsController < ApplicationController
def index @text = params[:text] || "Hello world!" end def create @text = TextTransform.flip(params[:text]) render :index end end -
6. Using in Rails (View) app/views/flips/index.html.erb: <h1>Flipper</h1> <%= form_tag do
%> <%= text_field_tag :text, @text %> <%= submit_tag "Flip!" %> <% end %> -
7. Deploy to Heroku Setup rustc on production machine $
heroku create $ heroku buildpacks:add https://github.com/emk/heroku-buildpack-rust $ heroku buildpacks:add heroku/ruby $ git push heroku master -
7. Deploy to Heroku Setup rustc on production machine $
heroku create $ heroku buildpacks:add https://github.com/emk/heroku-buildpack-rust $ heroku buildpacks:add heroku/ruby $ git push heroku master -
Distribution (Precompile Binaries) • Use CI to build binaries and
publish to rubygems ◦ Travis/CircleCI: Linux + OS X ◦ Appveyor: Windows • Don't need rust compiler on host machine • Next version of helix CLI will generate CI config ◦ helix bootstrap hello_helix ◦ Add env var RUBYGEMS_AUTH_TOKEN to CI services to publish -
8. Deploy to Heroku $ heroku create $ git push
heroku master -----> Ruby app detected -----> Compiling Ruby/Rails -----> Using Ruby version: ruby-2.3.4 -----> Installing dependencies using bundler 1.15.2 … Installing text_transform 0.1.0 (x86_64-linux) -
Good use cases • CPU-bound • Simple inputs ◦ JSON
◦ Filenames ◦ data tables • Avoid chatty APIs (boundary cost) -
Measure # of words in Shakespeare Ruby: 3 seconds Rust
example: 30 ms -
Good use cases ruby! { class WordCount { def search(path:
String, search: String) -> i32 { let mut file = File::open(path).expect("could not open file."); // … lines.into_par_iter(). .map(|line| wc_line(line, search)) .sum() } } } -
Good use cases • Use Rust libraries • Leverage Rust
web browser tech • Mailer, Background job, Action Cable -
ruby! { class InlineCSS { def inline(html: String, css: String)
-> String { let doc = kuchiki::parse_html().one(html); let mut parser = parse_css::CSSParser::new(css); let rules = ...; for rule in rules { let matching_elements = ...; for matching_element in matching_elements { // insert style attribute } } // serialize } } } Good use cases -
Roadmap • Greenfield project • Drop-in replacement • Reopen class
• Ship to production • Non-traditional use-cases • Performance parity with C • Misc features and QoL improvements -
Let's keep writing the ruby we love without the fear
we'll hit a wall.