Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

<%= self %> name: Akira Matsuda GitHub: amatsuda Twitter: @a_matsuda

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Rails

Slide 5

Slide 5 text

Rails

Slide 6

Slide 6 text

Rails Ruby

Slide 7

Slide 7 text

Rails Ruby

Slide 8

Slide 8 text

Rails Ruby Haml

Slide 9

Slide 9 text

Rails Ruby Haml

Slide 10

Slide 10 text

Rails Ruby Haml Kaminari

Slide 11

Slide 11 text

Rails Ruby Haml Kaminari

Slide 12

Slide 12 text

Rails Ruby RubyKaigi Haml Kaminari

Slide 13

Slide 13 text

Rails Ruby RubyKaigi Haml Kaminari

Slide 14

Slide 14 text

http://rubykaigi.org/2014

Slide 15

Slide 15 text

http://rubykaigi.org/2014 The slides are available from the "SCHEDULE" link The videos are available from the "SCHEDULE" link Check 'em out!

Slide 16

Slide 16 text

Rails Ruby RubyKaigi Haml Kaminari

Slide 17

Slide 17 text

Rails Ruby RubyKaigi Haml Kaminari Asakusa.rb

Slide 18

Slide 18 text

Rails Ruby RubyKaigi Haml Kaminari Asakusa.rb

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

begin

Slide 21

Slide 21 text

ERB Part 1

Slide 22

Slide 22 text

Template Engines in Ruby There is the template Engine specification in Ruby

Slide 23

Slide 23 text

Which is called "eRuby"

Slide 24

Slide 24 text

eRuby

Slide 25

Slide 25 text

eRuby Do you know what it is?

Slide 26

Slide 26 text

eRuby Stands for "Embedded Ruby" (≠ mruby) The spec for embedding Ruby code into a text (mainly HTML file) Created by Matz & Shugo Maeda (@shugomaeda), who is Matz's boss at their company Inspired by ePerl spec

Slide 27

Slide 27 text

eruby

Slide 28

Slide 28 text

eruby The referential implementation of eRuby, written in C This eruby has a small "r", just as Ruby (= language) and ruby(= implementation) Created by Shugo Maeda No more maintained :<

Slide 29

Slide 29 text

ERB

Slide 30

Slide 30 text

ERB A Pure Ruby implementation of eRuby A ruby stdlib Created by Masatoshi Seki (@m_seki), a legendary Ruby guru Well documented (particularly in Japanese)

Slide 31

Slide 31 text

Erb? ERb? No, ERB! I see so many mistakes in books/ articles/documantations It used to be called ERb/ERbLight, then renamed to ERB when becoming a ruby standard library

Slide 32

Slide 32 text

Pure Ruby Only one file lib/erb.rb % wc -l #=> 1009 LOC

Slide 33

Slide 33 text

This includes documentation, comments % grep "^ *#" erb.rb | wc -l #=> 517 So, the code is actually less than 500 lines

Slide 34

Slide 34 text

Usage of ERB

Slide 35

Slide 35 text

Hello! puts ERB.new('Hello!').result #=> "Hello!"

Slide 36

Slide 36 text

Embedding Ruby code puts ERB.new('1 + 1 = <%= 1 + 1 %>').result #=> "1 + 1 = 2"

Slide 37

Slide 37 text

Embedding Ruby code puts ERB.new('<% if true %>foo<% else %>bar<% end %>').result #=> "foo"

Slide 38

Slide 38 text

How does ERB work?

Slide 39

Slide 39 text

ERB#initialize class ERB def initialize(str, safe_level=nil, trim_mode=nil, eoutvar='_erbout') ... compiler = make_compiler(trim_mode) set_eoutvar(compiler, eoutvar) @src, @enc = *compiler.compile(str) ... ennd

Slide 40

Slide 40 text

ERB#initialize Creates a compiler The compiler compiles the given template into @src

Slide 41

Slide 41 text

ERB::Compiler#compile class ERB class Compiler def compile(s) ... out = Buffer.new(self, enc) scanner = make_scanner(s) scanner.scan do |token| case token when '<%', '<%=', '<%#' add_put_cmd(out, content) if content.size > 0 ... return out.script, enc ennnd

Slide 42

Slide 42 text

ERB::Compiler#compile Prepares a buffer Scans through the template, splits the template into tokens, then executes a command for each token

Slide 43

Slide 43 text

add_put_cmd class ERB class Compiler def add_put_cmd(out, content) out.push("#{@put_cmd} #{content_dump(content)}") ennnd

Slide 44

Slide 44 text

add_put_cmd Pushes a Ruby code String into the buffer @put_cmd => method content_dump(content) => params

Slide 45

Slide 45 text

What is @put_cmd? class ERB def set_eoutvar(compiler, eoutvar = '_erbout') compiler.put_cmd = "#{eoutvar}.concat" compiler.insert_cmd = "#{eoutvar}.concat" compiler.pre_cmd = ["#{eoutvar} = ''"] compiler.post_cmd = ["#{eoutvar}.force_encoding(__ENCODING__)"] ennd

Slide 46

Slide 46 text

@put_cmd "_erbout.concat" by default

Slide 47

Slide 47 text

content_dump class ERB class Compiler def content_dump(s) ... s.dump ... ennnd

Slide 48

Slide 48 text

content_dump Just dumps the content

Slide 49

Slide 49 text

So, add_put_cmd(out, content) is equivalent to something like this out = Buffer.new(self, Encoding::UTF_8) out.push("_erbout.concat 'Hello'.dump")

Slide 50

Slide 50 text

ERB#result class ERB def result(b=new_toplevel) ... eval(@src, b, (@filename || '(erb)'), 0) ennnd

Slide 51

Slide 51 text

ERB internally executes something like this _erbout = "" buf = ERB::Compiler::Buffer.new(ERB::Comp iler.new(nil)) buf.push("_erbout.concat 'Hello'.dump") buf.close puts eval(buf.script)

Slide 52

Slide 52 text

How do we use ERB?

Slide 53

Slide 53 text

The major use case template file + ruby => HTML

Slide 54

Slide 54 text

In Ruby on Rails A component named ActionView

Slide 55

Slide 55 text

How does ActionView render? Class.new(ActionView::Base).new. render(inline: "<%= 'Hello' %>")

Slide 56

Slide 56 text

ActionView::Template#ren der module ActionView class Template def render(view, locals, buffer=nil, &block) ... compile!(view) view.send(method_name, locals, buffer, &block) ... ennnd

Slide 57

Slide 57 text

ActionView::Template#co mpile! module ActionView class Template def compile!(view) return if @compiled ... mod = view.singleton_class ... compile(mod) ... @compiled = true ... ennnd

Slide 58

Slide 58 text

ActionView::Template#co mpile module ActionView class Template def compile(mod) ... method_name = self.method_name code = @handler.call(self) source = <<-end_src def #{method_name}(local_assigns, output_buffer) _old_virtual_path, @virtual_path = @virtual_path, #{@virtual_path.inspect};_old_output_buffer = @output_buffer;#{locals_code};#{code} ensure @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer end end_src ... mod.module_eval(source, identifier, 0) ... ennnd

Slide 59

Slide 59 text

@handler.call(self)

Slide 60

Slide 60 text

ActionView::Template::Handlers::ERB# call module ActionView class Template module Handlers class ERB def call(template) ... erb = template_source.gsub(ENCODING_TAG, '') ... self.class.erb_implementation.new( erb, :escape => (self.class.escape_whitelist.include? template.type), :trim => (self.class.erb_trim_mode == "-") ).src ennnnnd

Slide 61

Slide 61 text

What ActionView render does ActionView defines a Ruby method on the view object The method body is generated by ERB.new(template).src

Slide 62

Slide 62 text

ERB#src Returns the Ruby code String to be `eval`ed

Slide 63

Slide 63 text

ERB#result class ERB def result(b=new_toplevel) ... eval(@src, b, (@filename || '(erb)'), 0) ennd

Slide 64

Slide 64 text

ERB#src ERB.new('Hello').src #=> "#coding:UTF-8\n_erbout = ''; _erbout.concat \"Hello\"; _erbout.force_encoding(__ENCODING__ )"

Slide 65

Slide 65 text

Short summary Templates are compiled to be Ruby methods Only for the first execution of the template So, compile overhead is not at all a big deal What really matters is performance of the generated ruby code

Slide 66

Slide 66 text

ActionView::Template::Handlers::ERB# call module ActionView class Template module Handlers class ERB def call(template) ... erb = template_source.gsub(ENCODING_TAG, '') ... self.class.erb_implementation.new( erb, :escape => (self.class.escape_whitelist.include? template.type), :trim => (self.class.erb_trim_mode == "-") ).src ennnnnd

Slide 67

Slide 67 text

What is self.class.erb_implementation?

Slide 68

Slide 68 text

ActionView::Template::Handlers::ERB.erb _implementation module ActionView class Template module Handlers class ERB ... class_attribute :erb_implementation self.erb_implementation = Erubis ... ennnnd

Slide 69

Slide 69 text

self.class.erb_implementa tion Defaulted to Erubis

Slide 70

Slide 70 text

Erubis

Slide 71

Slide 71 text

Erubis Yet another Pure Ruby implementation of eRuby A ruby gem Created by Makoto Kuwata (@makotokuwata) https://github.com/kwatch/erubis

Slide 72

Slide 72 text

According to its README "Very fast, almost three times faster than ERB" "Auto escaping support" "Ruby on Rails support" ... and so on

Slide 73

Slide 73 text

I said Erubis is a yet another implementation of eRuby, but, Both ERB and Erubis are eRuby implementations #=> false Erubis is ERB compatible, plus some more original features #=> true

Slide 74

Slide 74 text

Why does ActionView use Erubis by default?

Slide 75

Slide 75 text

Wanna use ERB? It should be quite easy, it's configurable! Let's try.

Slide 76

Slide 76 text

action_view/template/ handlers/erb.rb module ActionView class Template module Handlers class ERB ... class_attribute :erb_implementation - self.erb_implementation = Erubis + self.erb_implementation = ::ERB ... ennnnd

Slide 77

Slide 77 text

The ActionView tests do not pass! :bomb: % bundle ex rake 1530 runs, 3434 assertions, 17 failures, 29 errors, 1 skips

Slide 78

Slide 78 text

We've got so many errors like this... SyntaxError: test template:2: syntax error, unexpected ')' ...ple.com', :method => :put) do ).to_s); _erbout.concat "Hello...

Slide 79

Slide 79 text

This causes a syntax error output_buffer = render_erb( "<%= form_tag('http:// www.example.com') do %>Hello world! <% end %>")

Slide 80

Slide 80 text

Because this is not allowed in eRuby spec <%= form_for @article do |f| %> ... <% end %>

Slide 81

Slide 81 text

Do you remember that it had to be like this in Rails 1.x and 2.x? <% form_for @article do |f| %> ... <% end %>

Slide 82

Slide 82 text

How did things change in Rails2..Rails3? Ask the repo.

Slide 83

Slide 83 text

Between Rails 2 and 3

Slide 84

Slide 84 text

A big security improvement Introducing the SafeBuffer Strings are regarded unsafe by default

Slide 85

Slide 85 text

actionpack/lib/action_view/ template/handlers/erb.rb require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/string/output_safety' +require 'erubis' module ActionView module TemplateHandlers + class Erubis < ::Erubis::Eruby + def add_preamble(src) + src << "@output_buffer = ActionView::SafeBuffer.new;" + end + + def add_text(src, text) + src << "@output_buffer << ('" << escape_text(text) << "'.html_safe!);" + end + ... + end + class ERB < TemplateHandler ... def compile(template) - require 'erb' - magic = $1 if template.source =~ /\A(<%#.*coding[:=]\s*(\S+)\s*-?%>)/ erb = "#{magic}<% __in_erb_template=true %>#{template.source}" - ::ERB.new(erb, nil, erb_trim_mode, '@output_buffer').src + Erubis.new(erb, :trim=>(self.class.erb_trim_mode == "-")).src ennnnnd

Slide 86

Slide 86 text

/ERB/Erubis/ Why Erubis? Because Erubis had the "Auto escaping support"

Slide 87

Slide 87 text

Do you remember that Rails templates were not html_safe by default before this change? We were required to html_escape everything manually like this <%=h @user.name %>

Slide 88

Slide 88 text

Then,

Slide 89

Slide 89 text

Stopped hardcoding "Erubis"

Slide 90

Slide 90 text

actionpack/lib/action_view/ template/handlers/erb.rb module ActionView module TemplateHandlers class ERB < TemplateHandler ... + cattr_accessor :erubis_implementation + self.erubis_implementation = Erubis def compile(template) magic = $1 if template.source =~ /\A(<%#.*coding[:=]\s*(\S+) \s*-?%>)/ erb = "#{magic}<% __in_erb_template=true %>#{template.source}" - Erubis.new(erb, :trim=>(self.class.erb_trim_mode == "-")).src + self.class.erubis_implementation.new(erb, :trim=>(self.class.erb_trim_mode == "-")).src ennnnd

Slide 91

Slide 91 text

Renamed erubis_implementation to erb_implementation

Slide 92

Slide 92 text

actionpack/lib/action_view/ template/handlers/erb.rb module ActionView module TemplateHandlers class ERB < TemplateHandler ... - cattr_accessor :erubis_implementation - self.erubis_implementation = Erubis + cattr_accessor :erb_implementation + self.erb_implementation = Erubis def compile(template) ... - result = self.class.erubis_implementation.new(erb, :trim=>(self.class.erb_trim_mode == "-")).src + result = self.class.erb_implementation.new(erb, :trim=>(self.class.erb_trim_mode == "-")).src ... ennnnd

Slide 93

Slide 93 text

So, Rails' default template handler is Erubis, not ERB Because Erubis has some original extensions in addition to eRuby, and the Rails team liked it We cannot switch erb_implementation back to ERB

Slide 94

Slide 94 text

I noticed this 2 years ago, And asked Seki-san if we could extend ERB to be "Rails compatible" before Ruby 2.0 release

Slide 95

Slide 95 text

He tried,

Slide 96

Slide 96 text

Firstly he tried to add some extension points to ERB git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/ trunk@38186 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- ChangeLog | 5 +++++ lib/erb.rb | 36 ++++++++++++++++++++---------------- 2 files changed, 25 insertions(+), 16 deletions(-)

Slide 97

Slide 97 text

But gave up. Because he didn't like this part in ActionView + Erubis BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/ This code scans the template with the Regexp and detects the Ruby block, but this kind of code could be imperfect So this is not acceptable as an ERB spec, said Seki- san.

Slide 98

Slide 98 text

So what is BLOCK_EXPR? And why is this problematic? Let's carefully take a look at the Regexp.

Slide 99

Slide 99 text

action_view/template/ handlers/erb.rb module ActionView class Template module Handlers class Erubis < ::Erubis::Eruby ... BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/ def add_expr_literal(src, code) flush_newline_if_pending(src) if code =~ BLOCK_EXPR src << '@output_buffer.append= ' << code else src << '@output_buffer.append=(' << code << ');' end ennnnnd

Slide 100

Slide 100 text

BLOCK_EXPR The Regexp scans the given String token and detects if the token opens a block or not. <%= form_tag('http:// www.example.com') do %> This does match the expression.

Slide 101

Slide 101 text

How about these blocks? BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/ p BLOCK_EXPR =~ "foo('bar') do" p BLOCK_EXPR =~ "foo('bar') {" p BLOCK_EXPR =~ "foo('bar')do" p BLOCK_EXPR =~ "foo('bar'){"

Slide 102

Slide 102 text

OMG BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/ p BLOCK_EXPR =~ "foo('bar') do" #=> matches p BLOCK_EXPR =~ "foo('bar') {" #=> matches p BLOCK_EXPR =~ "foo('bar')do" #=> does not match! p BLOCK_EXPR =~ "foo('bar'){" #=> does not match!

Slide 103

Slide 103 text

I think I found a bug form_for('foo'){...} is a valid Ruby code that calls a method with a block But ActionView doesn't regard this as a block And causes a syntax error

Slide 104

Slide 104 text

But this should be the fix, obviously. -BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/ +BLOCK_EXPR = /\s*(do|\{)(\s*\|[^|]*\|)?\s*\Z/ p BLOCK_EXPR =~ "foo('bar') do" #=> matches p BLOCK_EXPR =~ "foo('bar') {" #=> matches p BLOCK_EXPR =~ "foo('bar')do" #=> matches p BLOCK_EXPR =~ "foo('bar'){" #=> matches

Slide 105

Slide 105 text

Seems good?

Slide 106

Slide 106 text

Not really :< -BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/ +BLOCK_EXPR = /\s*(do|\{)(\s*\|[^|]*\|)?\s*\Z/ p BLOCK_EXPR =~ "foo('bar') do" #=> matches p BLOCK_EXPR =~ "foo('bar') {" #=> matches p BLOCK_EXPR =~ "foo('bar')do" #=> matches p BLOCK_EXPR =~ "foo('bar'){" #=> matches p BLOCK_EXPR =~ "@todo" #=> matches!

Slide 107

Slide 107 text

If I change the BLOCK_EXPR to /\s*(do|\{)(\s*\|[^|]*\|)?\s*\Z/ <%= @todo %> Now ActionView regards this code as a block call!

Slide 108

Slide 108 text

So, The block detection code /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/ in ActionView fails to detect some valid Ruby block calls I would fix this particular case Maybe like this? /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/ But essentially, this approach is wrong! Having such a Ruby parsing code other than the Ruby parser would be a cause of such a weird bug

Slide 109

Slide 109 text

The Ruby parser is a nightmare. (@nobu) Let us keep his words in mind.

Slide 110

Slide 110 text

That is why this syntax is not acceptable in ERB <%= form_for @article do |f| %> ... <% end %>

Slide 111

Slide 111 text

You see how ERB works?

Slide 112

Slide 112 text

Anyway, "ERB or Erubis" is not a big deal We don't use neither of them after all :trollface:

Slide 113

Slide 113 text

And our choice is of course,

Slide 114

Slide 114 text

Haml Part 2

Slide 115

Slide 115 text

What is Haml? A "templating haiku" "well-indented" Implemented in Ruby Created in May 2006 http://haml.info/

Slide 116

Slide 116 text

Usage of Haml

Slide 117

Slide 117 text

Hello! Haml::Engine.new("= 'Hello!'").render #=> "Hello!\n"

Slide 118

Slide 118 text

Generating Ruby code Haml::Engine.new("='Hello!'"). compiler. precompiled_with_ambles([])

Slide 119

Slide 119 text

Demo

Slide 120

Slide 120 text

Generated Ruby code puts Haml::Engine.new("= 'Hello!'").compiler.precompiled_with_ambles([]) #=> begin;extend Haml::Helpers;_hamlout = @haml_buffer = Haml::Buffer.new(haml_buffer, {:autoclose=>["area", "base", "basefont", "br", "col", "command", "embed", "frame", "hr", "img", "input", "isindex", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"], :preserve=>["textarea", "pre", "code"], :attr_wrapper=>"'", :ugly=>false, :format=>:html5, :encoding=>"UTF-8", :escape_html=>false, :escape_attrs=>true, :hyphenate_data_attrs=>true, :cdata=>false});_erbout = _hamlout.buffer;@output_buffer = output_buffer ||= ActionView::OutputBuffer.new rescue nil;;_hamlout.push_text("#{_hamlout.format_script_false_false_fals e_false_false_true_false(( 'Hello!' ));}\n", 0, false);_erbout;ensure;@haml_buffer = @haml_buffer.upper if @haml_buffer;end;

Slide 121

Slide 121 text

Hello?

Slide 122

Slide 122 text

ERB ERB.new("<% 'Hello!' %>").src #=> "#coding:UTF-8\n_erbout = ''; 'Hello!' ; _erbout.force_encoding(__ENCODING__)"

Slide 123

Slide 123 text

Haml puts Haml::Engine.new("= 'Hello!'").compiler.precompiled_with_ambles([]) #=> begin;extend Haml::Helpers;_hamlout = @haml_buffer = Haml::Buffer.new(haml_buffer, {:autoclose=>["area", "base", "basefont", "br", "col", "command", "embed", "frame", "hr", "img", "input", "isindex", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"], :preserve=>["textarea", "pre", "code"], :attr_wrapper=>"'", :ugly=>false, :format=>:html5, :encoding=>"UTF-8", :escape_html=>false, :escape_attrs=>true, :hyphenate_data_attrs=>true, :cdata=>false});_erbout = _hamlout.buffer;@output_buffer = output_buffer ||= ActionView::OutputBuffer.new rescue nil;;_hamlout.push_text("#{_hamlout.format_script_false_false_fals e_false_false_true_false(( 'Hello!' ));}\n", 0, false);_erbout;ensure;@haml_buffer = @haml_buffer.upper if @haml_buffer;end;

Slide 124

Slide 124 text

OK, nice haiku.

Slide 125

Slide 125 text

But seriously, We don't usually have to care what is executed behind But isn't this too much? It apparently looks slow...

Slide 126

Slide 126 text

I said, compilation overhead doesn't matter But the generated Ruby code DOES matter Yes, it's time to benchmark

Slide 127

Slide 127 text

Benchmark

Slide 128

Slide 128 text

Let's start with the simplest case

Slide 129

Slide 129 text

Benchmarking the method call require 'erb' require 'haml' require 'benchmark/ips' obj = Object.new erb_src = ERB.new("<%= 'Hello!' %>").src haml_src = Haml::Engine.new("= 'Hello!'").compiler.precompiled_with_ambles([]) obj.singleton_class.module_eval("def erb() #{erb_src}; end") obj.singleton_class.module_eval("def haml() #{haml_src}; end") Benchmark.ips do |x| x.report('erb') do obj.erb end x.report('haml') do obj.haml end end

Slide 130

Slide 130 text

Result Calculating ------------------------------------- erb 69.436k i/100ms haml 6.263k i/100ms ------------------------------------------------- erb 1.795M (± 2.9%) i/s - 9.027M haml 68.364k (± 5.0%) i/s - 344.465k

Slide 131

Slide 131 text

Result Greeting "Hello!" in Haml is 26x slower than ERB But this is just a micro benchmark

Slide 132

Slide 132 text

As a little bit more realistic benchmark Let's see actual rendering speed via ActionView I'm going to skip the template file path resolution which is supposed to be heavier than printing "Hello!"

Slide 133

Slide 133 text

Directly `render`ing the template require 'erb' require 'haml' require 'action_view' require 'action_controller' require 'benchmark/ips' require 'haml/template' view = Class.new(ActionView::Base).new('.') erb_handler = ActionView::Template.handler_for_extension('erb') erb_template = ActionView::Template.new("<%= 'Hello!' %>", File.expand_path('hello.erb'), erb_handler, {}) # discarding the first execution that runs the template compilation puts erb_template.render(view, {}) haml_handler = ActionView::Template.handler_for_extension('haml') haml_template = ActionView::Template.new("= 'Hello!'", File.expand_path('hello.haml'), haml_handler, {}) puts haml_template.render(view, {}) Benchmark.ips do |x| x.report('erb') do erb_template.render(view, {}) end x.report('haml') do haml_template.render(view, {}) end end

Slide 134

Slide 134 text

Result Calculating ------------------------------------- erb 7.837k i/100ms haml 4.195k i/100ms ------------------------------------------------- erb 93.728k (± 3.5%) i/s - 470.220k haml 46.107k (± 2.7%) i/s - 230.725k

Slide 135

Slide 135 text

Result Haml is still 2x slower than ERB In a semi-micro banchmark

Slide 136

Slide 136 text

Real ActionView rendering Calling ActionView::Base#render method

Slide 137

Slide 137 text

Rendering via ActionView::Base require 'erb' require 'haml' require 'action_view' require 'action_controller' require 'benchmark/ips' module Rails def self.env ActiveSupport::StringInquirer.new('production') end end require 'haml/template' view = Class.new(ActionView::Base).new('.') puts view.render(template: 'hello', handlers: 'erb') puts view.render(template: 'hello', handlers: 'haml') Benchmark.ips do |x| x.report('erb') do view.render(template: 'hello', handlers: 'erb') end x.report('haml') do view.render(template: 'hello', handlers: 'haml') end end

Slide 138

Slide 138 text

Result Calculating ------------------------------------- erb 1.356k i/100ms haml 1.134k i/100ms ------------------------------------------------- erb 14.152k (± 2.4%) i/s - 71.868k haml 11.645k (± 2.9%) i/s - 58.968k

Slide 139

Slide 139 text

Result On the real "Hello!" app, an ERB template renders 20ʙ% faster than a Haml one

Slide 140

Slide 140 text

Why is Haml slow?

Slide 141

Slide 141 text

Why is Haml slow? Let's read through the generated haiku

Slide 142

Slide 142 text

The code begin;extend Haml::Helpers;_hamlout = @haml_buffer = Haml::Buffer.new(haml_buffer, {:autoclose=>["area", "base", "basefont", "br", "col", "command", "embed", "frame", "hr", "img", "input", "isindex", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"], :preserve=>["textarea", "pre", "code"], :attr_wrapper=>"'", :ugly=>false, :format=>:html5, :encoding=>"UTF-8", :escape_html=>false, :escape_attrs=>true, :hyphenate_data_attrs=>true, :cdata=>false});_erbout = _hamlout.buffer;@output_buffer = output_buffer ||= ActionView::OutputBuffer.new rescue nil;;_hamlout.push_text("#{_hamlout.format_script_false_f alse_false_false_false_true_false(( 'Hello!' ));}\n", 0, false);_erbout;ensure;@haml_buffer = @haml_buffer.upper if @haml_buffer;end;

Slide 143

Slide 143 text

The code begin;extend Haml::Helpers;_hamlout = @haml_buffer = Haml::Buffer.new(haml_buffer, {:autoclose=>["area", "base", "basefont", "br", "col", "command", "embed", "frame", "hr", "img", "input", "isindex", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"], :preserve=>["textarea", "pre", "code"], :attr_wrapper=>"'", :ugly=>false, :format=>:html5, :encoding=>"UTF-8", :escape_html=>false, :escape_attrs=>true, :hyphenate_data_attrs=>true, :cdata=>false});_erbout = _hamlout.buffer;@output_buffer = output_buffer ||= ActionView::OutputBuffer.new rescue nil;; _hamlout.push_text("#{_hamlout.format_script_false_false_false_fal se_false_true_false(( 'Hello!' ));}\n", 0, false); _erbout;ensure;@haml_buffer = @haml_buffer.upper if @haml_buffer;end; 4&561 )&--0 5&"3%08/

Slide 144

Slide 144 text

The first part begin;extend Haml::Helpers;_hamlout = @haml_buffer = Haml::Buffer.new(haml_buffer, {:autoclose=>["area", "base", "basefont", "br", "col", "command", "embed", "frame", "hr", "img", "input", "isindex", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"], :preserve=>["textarea", "pre", "code"], :attr_wrapper=>"'", :ugly=>false, :format=>:html5, :encoding=>"UTF-8", :escape_html=>false, :escape_attrs=>true, :hyphenate_data_attrs=>true, :cdata=>false});_erbout = _hamlout.buffer;@output_buffer = output_buffer ||= ActionView::OutputBuffer.new rescue nil;;

Slide 145

Slide 145 text

Options

Slide 146

Slide 146 text

Haml.has_many options begin;extend Haml::Helpers;_hamlout = @haml_buffer = Haml::Buffer.new(haml_buffer, {:autoclose=>["area", "base", "basefont", "br", "col", "command", "embed", "frame", "hr", "img", "input", "isindex", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"], :preserve=>["textarea", "pre", "code"], :attr_wrapper=>"'", :ugly=>false, :format=>:html5, :encoding=>"UTF-8", :escape_html=>false, :escape_attrs=>true, :hyphenate_data_attrs=>true, :cdata=>false});_erbout = _hamlout.buffer;@output_buffer = output_buffer ||= ActionView::OutputBuffer.new rescue nil;;

Slide 147

Slide 147 text

Haml has various options But who actually configures Haml? Haml buffer can be configured even in runtime! No. We don't need such a feature. Default must be just fine for everyone. Let's just remove them.

Slide 148

Slide 148 text

The code begin;extend Haml::Helpers;_hamlout = @haml_buffer = Haml::Buffer.new(haml_buffer, {:autoclose=>["area", "base", "basefont", "br", "col", "command", "embed", "frame", "hr", "img", "input", "isindex", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"], :preserve=>["textarea", "pre", "code"], :attr_wrapper=>"'", :ugly=>false, :format=>:html5, :encoding=>"UTF-8", :escape_html=>false, :escape_attrs=>true, :hyphenate_data_attrs=>true, :cdata=>false});_erbout = _hamlout.buffer;@output_buffer = output_buffer ||= ActionView::OutputBuffer.new rescue nil;;_hamlout.push_text("#{_hamlout.format_script_false_f alse_false_false_false_true_false(( 'Hello!' ));}\n", 0, false);_erbout;ensure;@haml_buffer = @haml_buffer.upper if @haml_buffer;end;

Slide 149

Slide 149 text

- options begin;extend Haml::Helpers;_hamlout = @haml_buffer = Haml::Buffer.new(haml_buffer);_erbout = _hamlout.buffer;@output_buffer = output_buffer ||= ActionView::OutputBuffer.new rescue nil;;_hamlout.push_text("#{_hamlout.forma t_script(( 'Hello!' ));}\n", 0, false);_erbout;ensure;@haml_buffer = @haml_buffer.upper if @haml_buffer;end;

Slide 150

Slide 150 text

Haml::Helpers

Slide 151

Slide 151 text

Haml.has_many Helpers % git grep "^ *def " lib/haml/helpers.rb lib/haml/helpers.rb: def initialize(method) lib/haml/helpers.rb: def to_s lib/haml/helpers.rb: def inspect lib/haml/helpers.rb: def self.action_view? lib/haml/helpers.rb: def init_haml_helpers lib/haml/helpers.rb: def non_haml lib/haml/helpers.rb: def find_and_preserve(input = nil, tags = haml_buffer.options[:preserve], &block) lib/haml/helpers.rb: def preserve(input = nil, &block) lib/haml/helpers.rb: def list_of(enum, opts={}, &block) lib/haml/helpers.rb: def html_attrs(lang = 'en-US') lib/haml/helpers.rb: def tab_up(i = 1) lib/haml/helpers.rb: def tab_down(i = 1) lib/haml/helpers.rb: def with_tabs(i) lib/haml/helpers.rb: def surround(front, back = front, &block) lib/haml/helpers.rb: def precede(str, &block) lib/haml/helpers.rb: def succeed(str, &block) lib/haml/helpers.rb: def capture_haml(*args, &block) lib/haml/helpers.rb: def haml_concat(text = "") lib/haml/helpers.rb: def haml_indent lib/haml/helpers.rb: def haml_tag(name, *rest, &block) lib/haml/helpers.rb: def haml_tag_if(condition, *tag) lib/haml/helpers.rb: def html_escape(text) lib/haml/helpers.rb: def escape_once(text) lib/haml/helpers.rb: def is_haml? lib/haml/helpers.rb: def block_is_haml?(block) lib/haml/helpers.rb: def merge_name_and_attributes(name, attributes_hash = {}) lib/haml/helpers.rb: def with_haml_buffer(buffer) lib/haml/helpers.rb: def haml_buffer lib/haml/helpers.rb: def haml_bind_proc(&proc) lib/haml/helpers.rb: def prettify(text) lib/haml/helpers.rb: def is_haml?

Slide 152

Slide 152 text

Haml.has_many Helpers Haml extends the Helpers module into each buffer instance Wow lovely, but who uses them? Again, let's just remove them.

Slide 153

Slide 153 text

The code begin;extend Haml::Helpers;_hamlout = @haml_buffer = Haml::Buffer.new(haml_buffer);_erbout = _hamlout.buffer;@output_buffer = output_buffer ||= ActionView::OutputBuffer.new rescue nil;;_hamlout.push_text("#{_hamlout.format_sc ript(( 'Hello!' ));}\n", 0, false);_erbout;ensure;@haml_buffer = @haml_buffer.upper if @haml_buffer;end;

Slide 154

Slide 154 text

- Helpers begin;_hamlout = @haml_buffer = Haml::Buffer.new(haml_buffer);_erbout = _hamlout.buffer;@output_buffer = output_buffer ||= ActionView::OutputBuffer.new rescue nil;;_hamlout.push_text("#{_hamlout.for mat_script(( 'Hello!' ));}\n", 0, false);_erbout;ensure;@haml_buffer = @haml_buffer.upper if @haml_buffer;end;

Slide 155

Slide 155 text

Buffers

Slide 156

Slide 156 text

Haml.has_many buffers We can find 6 different buffer variables in the code _hamlout @haml_buffer haml_buffer _erbout @output_buffer output_buffer Why don't we just remove unneeded ones?

Slide 157

Slide 157 text

The code begin;_hamlout = @haml_buffer = Haml::Buffer.new(haml_buffer);_erbout = _hamlout.buffer;@output_buffer = output_buffer ||= ActionView::OutputBuffer.new rescue nil;;_hamlout.push_text("#{_hamlout.for mat_script(( 'Hello!' ));}\n", 0, false);_erbout;ensure;@haml_buffer = @haml_buffer.upper if @haml_buffer;end;

Slide 158

Slide 158 text

- buffers @output_buffer = output_buffer ||= ActionView::OutputBuffer.new;@outpu t_buffer.concat("#{_hamlout.format_ script(( 'Hello!' ));}\n", 0, false);@output_buffer.to_s

Slide 159

Slide 159 text

html_safe

Slide 160

Slide 160 text

Why not directly use SafeBuffer? Requiring the SafeBuffer means that the Haml gem is going to depend on Rails Let's do so! Who uses Haml off Rails?

Slide 161

Slide 161 text

The code @output_buffer = output_buffer ||= ActionView::OutputBuffer.new;@outpu t_buffer.concat("#{_hamlout.format_ script(( 'Hello!' ));}\n", 0, false);@output_buffer.to_s

Slide 162

Slide 162 text

+ Rails @output_buffer = output_buffer ||= ActionView::OutputBuffer.new;@outpu t_buffer.concat('Hello!');@output_b uffer.to_s

Slide 163

Slide 163 text

This is the goal I want to make Haml as fast as possible But these changes would bring many incompatibilities So I'm going to fork Haml and create my own version Which is named "Haml X"

Slide 164

Slide 164 text

Haml X Haml X is still under construction Not yet published I'm working on it, and hopefully will be able to ship it soon

Slide 165

Slide 165 text

Current status of Haml X Calculating ------------------------------------- erb 69.021k i/100ms haml 6.263k i/100ms hamlx 9.026k i/100ms ------------------------------------------------- erb 1.831M (± 3.2%) i/s - 9.180M haml 68.364k (± 5.0%) i/s - 344.465k hamlx 110.208k (± 4.3%) i/s - 550.586k

Slide 166

Slide 166 text

Current status Still 16.6x slower than ERB But more than 50% faster than the original Haml

Slide 167

Slide 167 text

Slim Part 3

Slide 168

Slide 168 text

Slim Haml-like syntax "Automatic HTML escaping by default" "High performance" https://github.com/slim-template/slim

Slide 169

Slide 169 text

IMO Slim is actually slimmer than current Haml Code is slim Built on Temple Renders through Tilt Less options No helper methods

Slide 170

Slide 170 text

Temple A very cleverly written "Template compilation framework" Compiles the template into "the core abstraction" S-expression using Ruby Array

Slide 171

Slide 171 text

Tilt "Generic interface to multiple Ruby template engines" https://github.com/rtomayko/tilt

Slide 172

Slide 172 text

Slim Slim is awesome. Thank you slim, for being a good competitor :)

Slide 173

Slide 173 text

Conclusion Now you understand how templates work Haml is so slow I'm going to improve it

Slide 174

Slide 174 text

Conclusion Haml X is (hopefully) coming soon Stay tuned!

Slide 175

Slide 175 text

end