Skip to content

Instantly share code, notes, and snippets.

@jeremyevans
Created June 14, 2019 23:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeremyevans/6ed5a19b559395de63aa2cb34db091e6 to your computer and use it in GitHub Desktop.
Save jeremyevans/6ed5a19b559395de63aa2cb34db091e6 to your computer and use it in GitHub Desktop.
Compiled Templates Support for Roda
From 4b00364417def26dd5aa941dcbd0156bbfc4cdee Mon Sep 17 00:00:00 2001
From: Jeremy Evans <code@jeremyevans.net>
Date: Fri, 14 Jun 2019 15:17:51 -0700
Subject: [PATCH] Improve render performance up to 4x in the default case by
calling compiled template methods directly
This takes the UnboundMethods that Tilt prepares for templates,
and defines real methods using them in a module included in the
Roda class (so it works with frozen Roda apps).
When render/view is called with a single Symbol or String argument,
lookup that argument in the template method cache. If it exists,
call the method using send. If it doesn't exist, but a single
Symbol or String argument is given, flag to update the template
method cache with the compiled template method.
This optimizes the common case of calling render/view with a
single argument. It can be 2.5x faster to render and 4x
faster to view for simple templates, in terms of time spent
in Roda.
This depends on Ruby 2.1+ and Tilt 1.2+ in order to work correctly,
so only use the compilation in those cases.
Most plugins that use render don't benefit directly from this
optimization. The render_locals plugin turns the optimization
off completely, because it can inject locals even when none are
passed directly when calling render/view, and the optimization
does not work in that case.
If using the view_options plugin, the set_view_subdir and
append_view_subdir can work with the optimization, but
set_view_options and set_layout_options disables the optimization.
Some less commonly used tilt template engines do not support
compilation. For those template engines, if compiling the
template raises a NotImplementedError, rescue it and mark
to not try compiling the template in the future.
To avoid problems when subclassing, make sure to define methods
that include the class object id in the template method cache key.
---
CHANGELOG | 4 +
lib/roda/plugins/render.rb | 137 +++++++++++++++++++++++---
lib/roda/plugins/render_locals.rb | 10 ++
lib/roda/plugins/view_options.rb | 28 ++++++
spec/plugin/render_spec.rb | 157 ++++++++++++++++++++++++++++++
spec/plugin/view_options_spec.rb | 18 ++++
spec/views/a.rdoc | 2 +
spec/views/about/comp_test.erb | 1 +
spec/views/comp_layout.erb | 1 +
spec/views/comp_test.erb | 1 +
10 files changed, 344 insertions(+), 15 deletions(-)
create mode 100644 spec/views/a.rdoc
create mode 100644 spec/views/about/comp_test.erb
create mode 100644 spec/views/comp_layout.erb
create mode 100644 spec/views/comp_test.erb
diff --git a/CHANGELOG b/CHANGELOG
index 2ca62d9..0e7023d 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,7 @@
+= master
+
+* Improve render performance up to 4x in the default case by calling compiled template methods directly (jeremyevans)
+
= 3.21.0 (2019-06-14)
* Cache compiled templates in development mode, until the template files are modified (jeremyevans)
diff --git a/lib/roda/plugins/render.rb b/lib/roda/plugins/render.rb
index 3ae541e..dd2bcf3 100644
--- a/lib/roda/plugins/render.rb
+++ b/lib/roda/plugins/render.rb
@@ -139,10 +139,15 @@ class Roda
# rendering faster by specifying +:cache_key+ inside the +:layout_opts+
# plugin option.
module Render
+ # Support for using compiled methods directly requires Ruby 2.1 for the
+ # method binding to work, and Tilt 1.2 for Tilt::Template#compiled_method.
+ COMPILED_METHOD_SUPPORT = RUBY_VERSION >= '2.1' && defined?(Tilt::VERSION) && Tilt::VERSION >= '1.2'
+
# Setup default rendering options. See Render for details.
def self.configure(app, opts=OPTS)
if app.opts[:render]
orig_cache = app.opts[:render][:cache]
+ orig_method_cache = app.opts[:render][:template_method_cache]
opts = app.opts[:render][:orig_opts].merge(opts)
end
app.opts[:render] = opts.dup
@@ -163,6 +168,20 @@ class Roda
end
end
+ if opts[:check_template_mtime]
+ opts.delete(:template_method_cache)
+ elsif COMPILED_METHOD_SUPPORT
+ opts[:template_method_cache] = orig_method_cache || (opts[:cache_class] || RodaCache).new
+ begin
+ app.const_get(:RodaCompiledTemplates, false)
+ rescue NameError
+ compiled_templates_module = Module.new
+ app.send(:include, compiled_templates_module)
+ app.const_set(:RodaCompiledTemplates, compiled_templates_module)
+ end
+ opts[:template_method_cache] = orig_method_cache || (opts[:cache_class] || RodaCache).new
+ end
+
opts[:cache] = orig_cache || (opts[:cache_class] || RodaCache).new
opts[:layout_opts] = (opts[:layout_opts] || {}).dup
@@ -182,6 +201,8 @@ class Roda
else
opts[:layout_opts][:template] = layout
end
+
+ opts[:optimize_layout] = (opts[:layout_opts][:template] if opts[:layout_opts].keys.sort == [:_is_layout, :template])
end
opts[:layout_opts].freeze
@@ -249,6 +270,9 @@ class Roda
def inherited(subclass)
super
opts = subclass.opts[:render] = subclass.opts[:render].dup
+ if COMPILED_METHOD_SUPPORT
+ opts[:template_method_cache] = (opts[:cache_class] || RodaCache).new
+ end
opts[:cache] = opts[:cache].dup
opts.freeze
end
@@ -261,9 +285,13 @@ class Roda
module InstanceMethods
# Render the given template. See Render for details.
- def render(template, opts = OPTS, &block)
- opts = render_template_opts(template, opts)
- retrieve_template(opts).render((opts[:scope]||self), (opts[:locals]||OPTS), &block)
+ def render(template, opts = (optimized_template = _cached_template_method(template); OPTS), &block)
+ if optimized_template
+ send(optimized_template, {}, &block)
+ else
+ opts = render_template_opts(template, opts)
+ retrieve_template(opts).render((opts[:scope]||self), (opts[:locals]||OPTS), &block)
+ end
end
# Return the render options for the instance's class.
@@ -274,9 +302,26 @@ class Roda
# Render the given template. If there is a default layout
# for the class, take the result of the template rendering
# and render it inside the layout. See Render for details.
- def view(template, opts=OPTS)
- opts = parse_template_opts(template, opts)
- content = opts[:content] || render_template(opts)
+ def view(template, opts = (optimized_template = _cached_template_method(template); OPTS))
+ if optimized_template
+ content = send(optimized_template, {})
+
+ render_opts = self.class.opts[:render]
+ if layout_template = render_opts[:optimize_layout]
+ method_cache = render_opts[:template_method_cache]
+ unless layout_method = method_cache[:_roda_layout]
+ retrieve_template(:template=>layout_template, :cache_key=>nil, :template_method_cache_key => :_roda_layout)
+ layout_method = method_cache[:_roda_layout]
+ end
+
+ if layout_method
+ return send(layout_method, {}){content}
+ end
+ end
+ else
+ opts = parse_template_opts(template, opts)
+ content = opts[:content] || render_template(opts)
+ end
if layout_opts = view_layout_opts(opts)
content = render_template(layout_opts){content}
@@ -287,6 +332,41 @@ class Roda
private
+ if COMPILED_METHOD_SUPPORT
+ # If there is an instance method for the template, return the instance
+ # method symbol. This optimization is only used for render/view calls
+ # with a single string or symbol argument.
+ def _cached_template_method(template)
+ case template
+ when String, Symbol
+ if (method_cache = render_opts[:template_method_cache])
+ _cached_template_method_lookup(method_cache, template)
+ end
+ end
+ end
+
+ # The key to use in the template method cache for the given template.
+ def _cached_template_method_key(template)
+ template
+ end
+
+ # Return the instance method symbol for the template in the method cache.
+ def _cached_template_method_lookup(method_cache, template)
+ method_cache[template]
+ end
+ else
+ # :nocov:
+ def _cached_template_method(template)
+ nil
+ end
+
+ def _cached_template_method_key(template)
+ nil
+ end
+ # :nocov:
+ end
+
+
# Convert template options to single hash when rendering templates using render.
def render_template_opts(template, opts)
parse_template_opts(template, opts)
@@ -313,7 +393,7 @@ class Roda
# Given the template name and options, set the template class, template path/content,
# template block, and locals to use for the render in the passed options.
def find_template(opts)
- render_opts = render_opts()
+ render_opts = self.class.opts[:render]
engine_override = opts[:engine]
engine = opts[:engine] ||= render_opts[:engine]
if content = opts[:inline]
@@ -332,13 +412,15 @@ class Roda
end
if cache
- template_block = opts[:template_block] unless content
- template_opts = opts[:template_opts]
-
- opts[:cache_key] ||= if template_class || engine_override || template_opts || template_block
- [path, template_class, engine_override, template_opts, template_block]
- else
- path
+ unless opts.has_key?(:cache_key)
+ template_block = opts[:template_block] unless content
+ template_opts = opts[:template_opts]
+
+ opts[:cache_key] = if template_class || engine_override || template_opts || template_block
+ [path, template_class, engine_override, template_opts, template_block]
+ else
+ path
+ end
end
else
opts.delete(:cache_key)
@@ -353,6 +435,9 @@ class Roda
if template.is_a?(Hash)
opts.merge!(template)
else
+ if opts.empty? && (key = _cached_template_method_key(template))
+ opts[:template_method_cache_key] = key
+ end
opts[:template] = template
opts
end
@@ -372,6 +457,7 @@ class Roda
end
cached_template(opts) do
opts = found_template_opts || find_template(opts)
+ render_opts = self.class.opts[:render]
template_opts = render_opts[:template_opts]
if engine_opts = render_opts[:engine_opts][opts[:engine]]
template_opts = template_opts.merge(engine_opts)
@@ -383,7 +469,28 @@ class Roda
if render_opts[:check_template_mtime] && !opts[:template_block] && !cache
TemplateMtimeWrapper.new(opts[:template_class], opts[:path], 1, template_opts)
else
- opts[:template_class].new(opts[:path], 1, template_opts, &opts[:template_block])
+ template = opts[:template_class].new(opts[:path], 1, template_opts, &opts[:template_block])
+
+ if COMPILED_METHOD_SUPPORT &&
+ (method_cache_key = opts[:template_method_cache_key]) &&
+ (method_cache = render_opts[:template_method_cache]) &&
+ (method_cache[method_cache_key] != false) &&
+ !opts[:inline] &&
+ cache != false
+
+ begin
+ unbound_method = template.send(:compiled_method, OPTS)
+ rescue ::NotImplementedError
+ method_cache[method_cache_key] = false
+ else
+ method_name = :"_roda_template_#{self.class.object_id}_#{method_cache_key}"
+ self.class::RodaCompiledTemplates.send(:define_method, method_name, unbound_method)
+ self.class::RodaCompiledTemplates.send(:private, method_name)
+ method_cache[method_cache_key] = method_name
+ end
+ end
+
+ template
end
end
end
diff --git a/lib/roda/plugins/render_locals.rb b/lib/roda/plugins/render_locals.rb
index 749366d..1927215 100644
--- a/lib/roda/plugins/render_locals.rb
+++ b/lib/roda/plugins/render_locals.rb
@@ -1,5 +1,7 @@
# frozen-string-literal: true
+require_relative 'render'
+
#
class Roda
module RodaPlugins
@@ -41,6 +43,14 @@ class Roda
module InstanceMethods
private
+ if Render::COMPILED_METHOD_SUPPORT
+ # Disable use of cached templates, since it assumes a render/view call with no
+ # options will have no locals.
+ def _cached_template_method(template)
+ nil
+ end
+ end
+
def render_locals
opts[:render_locals]
end
diff --git a/lib/roda/plugins/view_options.rb b/lib/roda/plugins/view_options.rb
index 540c025..8c52b42 100644
--- a/lib/roda/plugins/view_options.rb
+++ b/lib/roda/plugins/view_options.rb
@@ -1,5 +1,7 @@
# frozen-string-literal: true
+require_relative 'render'
+
#
class Roda
module RodaPlugins
@@ -124,6 +126,32 @@ class Roda
private
+ if Render::COMPILED_METHOD_SUPPORT
+ # Return nil if using custom view or layout options.
+ # If using a view subdir, prefix the template key with the subdir.
+ def _cached_template_method_key(template)
+ return if @_view_options || @_layout_options
+
+ if subdir = @_view_subdir
+ template = "#{subdir}\0#{template}"
+ end
+
+ super
+ end
+
+ # Return nil if using custom view or layout options.
+ # If using a view subdir, prefix the template key with the subdir.
+ def _cached_template_method_lookup(method_cache, template)
+ return if @_view_options || @_layout_options
+
+ if subdir = @_view_subdir
+ template = "#{subdir}\0#{template}"
+ end
+
+ super
+ end
+ end
+
# If view options or locals have been set and this
# template isn't a layout template, merge the options
# and locals into the returned hash.
diff --git a/spec/plugin/render_spec.rb b/spec/plugin/render_spec.rb
index 50d8896..3b7c134 100644
--- a/spec/plugin/render_spec.rb
+++ b/spec/plugin/render_spec.rb
@@ -1,6 +1,7 @@
require_relative "../spec_helper"
begin
+ require 'tilt'
require 'tilt/erb'
require 'tilt/string'
rescue LoadError
@@ -258,6 +259,162 @@ describe "render plugin" do
app.render_opts[:views].must_equal File.join(Dir.pwd, 'bar')
end
+ if RUBY_VERSION >= '2.1' && defined?(Tilt::VERSION) && Tilt::VERSION >= '1.2'
+ it "does not cache template renders when using a template library that doesn't support it" do
+ begin
+ require 'tilt/rdoc'
+ rescue
+ next
+ end
+
+ app(:bare) do
+ plugin :render, :views=>'spec/views', :engine=>'rdoc'
+ route do
+ render('a')
+ end
+ end
+
+ app.render_opts[:template_method_cache]['a'].must_be_nil
+ body.strip.must_equal "<p># a # * b</p>"
+ app.render_opts[:template_method_cache]['a'].must_equal false
+ body.strip.must_equal "<p># a # * b</p>"
+ app.render_opts[:template_method_cache]['a'].must_equal false
+ body.strip.must_equal "<p># a # * b</p>"
+ app.render_opts[:template_method_cache]['a'].must_equal false
+ app::RodaCompiledTemplates.private_instance_methods.length.must_equal 0
+ end
+
+ ['comp_test', :comp_test].each do |template|
+ it "does not cache template renders when given a hash" do
+ app(:bare) do
+ plugin :render, :views=>'spec/views'
+ route do
+ render(:template=>template)
+ end
+ end
+
+ app.render_opts[:template_method_cache][template].must_be_nil
+ body.strip.must_equal "ct"
+ app.render_opts[:template_method_cache][template].must_be_nil
+ body.strip.must_equal "ct"
+ app.render_opts[:template_method_cache][template].must_be_nil
+ body.strip.must_equal "ct"
+ app.render_opts[:template_method_cache][template].must_be_nil
+ app::RodaCompiledTemplates.private_instance_methods.length.must_equal 0
+ end
+
+ it "caches template renders when given a #{template.class}" do
+ app(:bare) do
+ plugin :render, :views=>'spec/views'
+ route do
+ render(template)
+ end
+ end
+
+ app.render_opts[:template_method_cache][template].must_be_nil
+ body.strip.must_equal "ct"
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol)
+ body.strip.must_equal "ct"
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol)
+ body.strip.must_equal "ct"
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol)
+ app::RodaCompiledTemplates.private_instance_methods.length.must_equal 1
+ end
+
+ it "does not cache template views or layout when given a hash" do
+ app(:bare) do
+ layout = template.to_s.sub('test', 'layout')
+ layout = layout.to_sym if template.is_a?(Symbol)
+ plugin :render, :views=>'spec/views', :layout=>layout
+ route do
+ view(:template=>template)
+ end
+ end
+
+ app.render_opts[:template_method_cache][template].must_be_nil
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil
+ body.strip.must_equal "act\nb"
+ app.render_opts[:template_method_cache][template].must_be_nil
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil
+ body.strip.must_equal "act\nb"
+ app.render_opts[:template_method_cache][template].must_be_nil
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil
+ body.strip.must_equal "act\nb"
+ app.render_opts[:template_method_cache][template].must_be_nil
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil
+ app::RodaCompiledTemplates.private_instance_methods.length.must_equal 0
+ end
+
+ it "caches template views with layout when given a #{template.class}" do
+ app(:bare) do
+ layout = template.to_s.sub('test', 'layout')
+ layout = layout.to_sym if template.is_a?(Symbol)
+ plugin :render, :views=>'spec/views', :layout=>layout
+ route do
+ view(template)
+ end
+ end
+
+ app.render_opts[:template_method_cache][template].must_be_nil
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil
+ body.strip.must_equal "act\nb"
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol)
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil
+ body.strip.must_equal "act\nb"
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol)
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_kind_of(Symbol)
+ body.strip.must_equal "act\nb"
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol)
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_kind_of(Symbol)
+ app::RodaCompiledTemplates.private_instance_methods.length.must_equal 2
+ end
+
+ it "caches template views without layout when additional layout options given when given a #{template.class}" do
+ app(:bare) do
+ plugin :render, :views=>'spec/views', :layout=>nil
+ route do
+ view(template)
+ end
+ end
+
+ app.render_opts[:template_method_cache][template].must_be_nil
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil
+ body.strip.must_equal "ct"
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol)
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil
+ body.strip.must_equal "ct"
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol)
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil
+ body.strip.must_equal "ct"
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol)
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil
+ app::RodaCompiledTemplates.private_instance_methods.length.must_equal 1
+ end
+
+ it "caches template views without layout when additional layout options given when given a #{template.class}" do
+ app(:bare) do
+ plugin :render, :views=>'spec/views', :layout_opts=>{:locals=>{:title=>"Home"}}
+ route do
+ view(template)
+ end
+ end
+
+ app.render_opts[:template_method_cache][template].must_be_nil
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil
+ body.strip.must_equal "<title>Roda: Home</title>\nct"
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol)
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil
+ body.strip.must_equal "<title>Roda: Home</title>\nct"
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol)
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil
+ body.strip.must_equal "<title>Roda: Home</title>\nct"
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol)
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil
+ app::RodaCompiledTemplates.private_instance_methods.length.must_equal 1
+ end
+ end
+ end
+
it "inline layouts and inline views" do
app(:render) do
view({:inline=>'bar'}, :layout=>{:inline=>'Foo: <%= yield %>'})
diff --git a/spec/plugin/view_options_spec.rb b/spec/plugin/view_options_spec.rb
index baabe28..97e9eb0 100644
--- a/spec/plugin/view_options_spec.rb
+++ b/spec/plugin/view_options_spec.rb
@@ -21,12 +21,21 @@ describe "view_options plugin view subdirs" do
r.on "about" do
append_view_subdir 'views'
+ r.is 'test' do
+ append_view_subdir 'about'
+ render("comp_test")
+ end
render("about", :locals=>{:title => "About Roda"})
end
r.on "path" do
render('spec/views/about', :locals=>{:title => "Path"}, :layout_opts=>{:locals=>{:title=>"Home"}})
end
+
+ r.is 'test' do
+ set_view_subdir 'spec/views'
+ render("comp_test")
+ end
end
end
end
@@ -42,6 +51,15 @@ describe "view_options plugin view subdirs" do
it "should not change behavior when subdir is not set" do
body("/path").strip.must_equal "<h1>Path</h1>"
end
+
+ it "should handle template compilation correctly" do
+ 3.times do
+ body("/test").strip.must_equal "ct"
+ end
+ 3.times do
+ body("/about/test").strip.must_equal "about-ct"
+ end
+ end
end
describe "view_options plugin" do
diff --git a/spec/views/a.rdoc b/spec/views/a.rdoc
new file mode 100644
index 0000000..1f7d854
--- /dev/null
+++ b/spec/views/a.rdoc
@@ -0,0 +1,2 @@
+# a
+# * b
diff --git a/spec/views/about/comp_test.erb b/spec/views/about/comp_test.erb
new file mode 100644
index 0000000..114a59d
--- /dev/null
+++ b/spec/views/about/comp_test.erb
@@ -0,0 +1 @@
+about-ct
diff --git a/spec/views/comp_layout.erb b/spec/views/comp_layout.erb
new file mode 100644
index 0000000..89d2654
--- /dev/null
+++ b/spec/views/comp_layout.erb
@@ -0,0 +1 @@
+a<%= yield %>b
diff --git a/spec/views/comp_test.erb b/spec/views/comp_test.erb
new file mode 100644
index 0000000..ef616ac
--- /dev/null
+++ b/spec/views/comp_test.erb
@@ -0,0 +1 @@
+ct
--
2.21.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment