Navigation Menu

Skip to content

Commit

Permalink
Remove locals dependency from template.
Browse files Browse the repository at this point in the history
This means that templates does not need to store its source anymore, allowing us to reduce the ammount of memory taken by our Rails processes. Naively speaking, if your app/views contains 2MB of files, each of your processes (after being hit by a bunch of requests) will take 2MB less of memory after this commit.

This is extremely important for the upcoming features. Since Rails will also render CSS and JS files, their source won't be stored as well allowing us to decrease the ammount of memory taken.
  • Loading branch information
josevalim committed Oct 7, 2010
1 parent ad13eb7 commit b2600bf
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 85 deletions.
16 changes: 8 additions & 8 deletions actionpack/lib/action_view/lookup_context.rb
Expand Up @@ -77,17 +77,17 @@ def view_paths=(paths)
@view_paths = ActionView::Base.process_view_paths(paths) @view_paths = ActionView::Base.process_view_paths(paths)
end end


def find(name, prefix = nil, partial = false) def find(name, prefix = nil, partial = false, keys = [])
@view_paths.find(*args_for_lookup(name, prefix, partial)) @view_paths.find(*args_for_lookup(name, prefix, partial, keys))
end end
alias :find_template :find alias :find_template :find


def find_all(name, prefix = nil, partial = false) def find_all(name, prefix = nil, partial = false, keys = [])
@view_paths.find_all(*args_for_lookup(name, prefix, partial)) @view_paths.find_all(*args_for_lookup(name, prefix, partial, keys))
end end


def exists?(name, prefix = nil, partial = false) def exists?(name, prefix = nil, partial = false, keys = [])
@view_paths.exists?(*args_for_lookup(name, prefix, partial)) @view_paths.exists?(*args_for_lookup(name, prefix, partial, keys))
end end
alias :template_exists? :exists? alias :template_exists? :exists?


Expand All @@ -106,9 +106,9 @@ def with_fallbacks


protected protected


def args_for_lookup(name, prefix, partial) #:nodoc: def args_for_lookup(name, prefix, partial, keys) #:nodoc:
name, prefix = normalize_name(name, prefix) name, prefix = normalize_name(name, prefix)
[name, prefix, partial || false, @details, details_key] [name, prefix, partial || false, @details, keys, details_key]
end end


# Support legacy foo.erb names even though we now ignore .erb # Support legacy foo.erb names even though we now ignore .erb
Expand Down
4 changes: 2 additions & 2 deletions actionpack/lib/action_view/paths.rb
Expand Up @@ -10,8 +10,8 @@ def #{method}(*args)
METHOD METHOD
end end


def find(path, prefix = nil, partial = false, details = {}, key = nil) def find(path, prefix = nil, partial = false, details = {}, keys = [], key = nil)
template = find_all(path, prefix, partial, details, key).first template = find_all(path, prefix, partial, details, keys, key).first
raise MissingTemplate.new(self, "#{prefix}/#{path}", details, partial) unless template raise MissingTemplate.new(self, "#{prefix}/#{path}", details, partial) unless template
template template
end end
Expand Down
4 changes: 2 additions & 2 deletions actionpack/lib/action_view/render/layouts.rb
Expand Up @@ -62,11 +62,11 @@ def _layout_for(*args, &block) #:nodoc:
# This is the method which actually finds the layout using details in the lookup # This is the method which actually finds the layout using details in the lookup
# context object. If no layout is found, it checks if at least a layout with # context object. If no layout is found, it checks if at least a layout with
# the given name exists across all details before raising the error. # the given name exists across all details before raising the error.
def find_layout(layout) def find_layout(layout, keys)
begin begin
with_layout_format do with_layout_format do
layout =~ /^\// ? layout =~ /^\// ?
with_fallbacks { find_template(layout) } : find_template(layout) with_fallbacks { find_template(layout, nil, false, keys) } : find_template(layout, nil, false, keys)
end end
rescue ActionView::MissingTemplate => e rescue ActionView::MissingTemplate => e
update_details(:formats => nil) do update_details(:formats => nil) do
Expand Down
80 changes: 45 additions & 35 deletions actionpack/lib/action_view/render/partials.rb
Expand Up @@ -218,7 +218,6 @@ class PartialRenderer
def initialize(view_context, options, block) def initialize(view_context, options, block)
@view = view_context @view = view_context
@partial_names = PARTIAL_NAMES[@view.controller.class.name] @partial_names = PARTIAL_NAMES[@view.controller.class.name]

setup(options, block) setup(options, block)
end end


Expand All @@ -237,32 +236,45 @@ def setup(options, block)
@object = partial @object = partial


if @collection = collection if @collection = collection
paths = @collection_paths = @collection.map { |o| partial_path(o) } paths = @collection_data = @collection.map { |o| partial_path(o) }
@path = paths.uniq.size == 1 ? paths.first : nil @path = paths.uniq.size == 1 ? paths.first : nil
else else
@path = partial_path @path = partial_path
end end
end end

if @path
@variable, @variable_counter = retrieve_variable(@path)
else
paths.map! { |path| retrieve_variable(path).unshift(path) }
end
end

def retrieve_variable(path)
variable = @options[:as] || path[%r'_?(\w+)(\.\w+)*$', 1].to_sym
variable_counter = :"#{variable}_counter" if @collection
[variable, variable_counter]
end end


def render def render
identifier = ((@template = find_template) ? @template.identifier : @path) identifier = ((@template = find_partial) ? @template.identifier : @path)


if @collection if @collection
ActiveSupport::Notifications.instrument("render_collection.action_view", ActiveSupport::Notifications.instrument("render_collection.action_view",
:identifier => identifier || "collection", :count => @collection.size) do :identifier => identifier || "collection", :count => @collection.size) do
render_collection render_collection
end end
else else
if !@block && (layout = @options[:layout])
layout = find_template(layout)
end

content = ActiveSupport::Notifications.instrument("render_partial.action_view", content = ActiveSupport::Notifications.instrument("render_partial.action_view",
:identifier => identifier) do :identifier => identifier) do
render_partial render_partial
end end


if !@block && (layout = @options[:layout]) content = @view._render_layout(layout, @locals){ content } if layout
content = @view._render_layout(find_template(layout), @locals){ content }
end

content content
end end
end end
Expand All @@ -278,16 +290,9 @@ def render_collection
result.join(spacer).html_safe result.join(spacer).html_safe
end end


def collection_with_template(template = @template) def collection_with_template
segments, locals, template = [], @locals, @template segments, locals, template = [], @locals, @template

as, counter = @variable, @variable_counter
if @options[:as]
as = @options[:as]
counter = "#{as}_counter".to_sym
else
as = template.variable_name
counter = template.counter_name
end


locals[counter] = -1 locals[counter] = -1


Expand All @@ -300,34 +305,31 @@ def collection_with_template(template = @template)
segments segments
end end


def collection_without_template(collection_paths = @collection_paths) def collection_without_template
segments, locals = [], @locals segments, locals, collection_data = [], @locals, @collection_data
index, template = -1, nil index, template = -1, nil

keys = @locals.keys
if @options[:as]
as = @options[:as]
counter = "#{as}_counter"
end


@collection.each_with_index do |object, i| @collection.each_with_index do |object, i|
template = find_template(collection_paths[i]) path, *data = collection_data[i]
locals[as || template.variable_name] = object template = find_template(path, keys + data)
locals[counter || template.counter_name] = (index += 1) locals[data[0]] = object

locals[data[1]] = (index += 1)
segments << template.render(@view, locals) segments << template.render(@view, locals)
end end


@template = template @template = template
segments segments
end end


def render_partial(object = @object) def render_partial
locals, view, template = @locals, @view, @template locals, view = @locals, @view
object, as = @object, @variable


object ||= locals[template.variable_name] object ||= locals[as]
locals[@options[:as] || template.variable_name] = object locals[as] = object


template.render(view, locals) do |*name| @template.render(view, locals) do |*name|
view._layout_for(*name, &@block) view._layout_for(*name, &@block)
end end
end end
Expand All @@ -342,10 +344,18 @@ def collection
end end
end end


def find_template(path=@path) def find_partial
return path unless path.is_a?(String) if path = @path
locals = @locals.keys
locals << @variable
locals << @variable_counter if @collection
find_template(path, locals)
end
end

def find_template(path=@path, locals=@locals.keys)
prefix = @view.controller_path unless path.include?(?/) prefix = @view.controller_path unless path.include?(?/)
@view.find_template(path, prefix, true) @view.find_template(path, prefix, true, locals)
end end


def partial_path(object = @object) def partial_path(object = @object)
Expand Down
10 changes: 6 additions & 4 deletions actionpack/lib/action_view/render/rendering.rb
Expand Up @@ -34,24 +34,26 @@ def render(options = {}, locals = {}, &block)


# Determine the template to be rendered using the given options. # Determine the template to be rendered using the given options.
def _determine_template(options) #:nodoc: def _determine_template(options) #:nodoc:
keys = (options[:locals] ||= {}).keys

if options.key?(:inline) if options.key?(:inline)
handler = Template.handler_class_for_extension(options[:type] || "erb") handler = Template.handler_class_for_extension(options[:type] || "erb")
Template.new(options[:inline], "inline template", handler, {}) Template.new(options[:inline], "inline template", handler, { :locals => keys })
elsif options.key?(:text) elsif options.key?(:text)
Template::Text.new(options[:text], formats.try(:first)) Template::Text.new(options[:text], formats.try(:first))
elsif options.key?(:file) elsif options.key?(:file)
with_fallbacks { find_template(options[:file], options[:prefix]) } with_fallbacks { find_template(options[:file], options[:prefix], false, keys) }
elsif options.key?(:template) elsif options.key?(:template)
options[:template].respond_to?(:render) ? options[:template].respond_to?(:render) ?
options[:template] : find_template(options[:template], options[:prefix]) options[:template] : find_template(options[:template], options[:prefix], false, keys)
end end
end end


# Renders the given template. An string representing the layout can be # Renders the given template. An string representing the layout can be
# supplied as well. # supplied as well.
def _render_template(template, layout = nil, options = {}) #:nodoc: def _render_template(template, layout = nil, options = {}) #:nodoc:
locals = options[:locals] || {} locals = options[:locals] || {}
layout = find_layout(layout) if layout layout = find_layout(layout, locals.keys) if layout


ActiveSupport::Notifications.instrument("render_template.action_view", ActiveSupport::Notifications.instrument("render_template.action_view",
:identifier => template.identifier, :layout => layout.try(:virtual_path)) do :identifier => template.identifier, :layout => layout.try(:virtual_path)) do
Expand Down
66 changes: 43 additions & 23 deletions actionpack/lib/action_view/template.rb
Expand Up @@ -98,6 +98,8 @@ class Template


extend Template::Handlers extend Template::Handlers


attr_accessor :locals

attr_reader :source, :identifier, :handler, :virtual_path, :formats, attr_reader :source, :identifier, :handler, :virtual_path, :formats,
:original_encoding :original_encoding


Expand All @@ -117,42 +119,38 @@ def initialize(source, identifier, handler, details)
@handler = handler @handler = handler
@original_encoding = nil @original_encoding = nil
@method_names = {} @method_names = {}

@locals = details[:locals] || []
format = details[:format] || :html @formats = Array.wrap(details[:format] || :html).map(&:to_sym)
@formats = Array.wrap(format).map(&:to_sym) @virtual_path = details[:virtual_path].try(:sub, ".#{@formats.first}", "")
@virtual_path = details[:virtual_path].try(:sub, ".#{format}", "") @compiled = false
end end


def render(view, locals, &block) def render(view, locals, &block)
# Notice that we use a bang in this instrumentation because you don't want to # Notice that we use a bang in this instrumentation because you don't want to
# consume this in production. This is only slow if it's being listened to. # consume this in production. This is only slow if it's being listened to.
ActiveSupport::Notifications.instrument("!render_template.action_view", :virtual_path => @virtual_path) do ActiveSupport::Notifications.instrument("!render_template.action_view", :virtual_path => @virtual_path) do
if view.is_a?(ActionView::CompiledTemplates) compile!(view)
mod = ActionView::CompiledTemplates
else
mod = view.singleton_class
end

method_name = compile(locals, view, mod)
view.send(method_name, locals, &block) view.send(method_name, locals, &block)
end end
rescue Exception => e rescue Exception => e
if e.is_a?(Template::Error) if e.is_a?(Template::Error)
e.sub_template_of(self) e.sub_template_of(self)
raise e raise e
else else
raise Template::Error.new(self, view.respond_to?(:assigns) ? view.assigns : {}, e) raise Template::Error.new(refresh(view) || self, view.respond_to?(:assigns) ? view.assigns : {}, e)
end end
end end


def mime_type def mime_type
@mime_type ||= Mime::Type.lookup_by_extension(@formats.first.to_s) if @formats.first @mime_type ||= Mime::Type.lookup_by_extension(@formats.first.to_s) if @formats.first
end end


# TODO Remove me
def variable_name def variable_name
@variable_name ||= @virtual_path[%r'_?(\w+)(\.\w+)*$', 1].to_sym @variable_name ||= @virtual_path[%r'_?(\w+)(\.\w+)*$', 1].to_sym
end end


# TODO Remove me
def counter_name def counter_name
@counter_name ||= "#{variable_name}_counter".to_sym @counter_name ||= "#{variable_name}_counter".to_sym
end end
Expand All @@ -166,7 +164,25 @@ def inspect
end end
end end


private def compile!(view)
return if @compiled

if view.is_a?(ActionView::CompiledTemplates)
mod = ActionView::CompiledTemplates
else
mod = view.singleton_class
end

compile(view, mod)

# Just discard the source if we have a virtual path. This
# means we can get the template back.
@source = nil if @virtual_path
@compiled = true
end

protected

# Among other things, this method is responsible for properly setting # Among other things, this method is responsible for properly setting
# the encoding of the source. Until this point, we assume that the # the encoding of the source. Until this point, we assume that the
# source is BINARY data. If no additional information is supplied, # source is BINARY data. If no additional information is supplied,
Expand All @@ -187,11 +203,9 @@ def inspect
# encode the source into Encoding.default_internal. In general, # encode the source into Encoding.default_internal. In general,
# this means that templates will be UTF-8 inside of Rails, # this means that templates will be UTF-8 inside of Rails,
# regardless of the original source encoding. # regardless of the original source encoding.
def compile(locals, view, mod) def compile(view, mod)
method_name = build_method_name(locals) method_name = self.method_name
return method_name if view.respond_to?(method_name) locals_code = @locals.map { |key| "#{key} = local_assigns[:#{key}];" }.join

locals_code = locals.keys.map! { |key| "#{key} = local_assigns[:#{key}];" }.join


if source.encoding_aware? if source.encoding_aware?
# Look for # encoding: *. If we find one, we'll encode the # Look for # encoding: *. If we find one, we'll encode the
Expand Down Expand Up @@ -256,8 +270,6 @@ def #{method_name}(local_assigns)
begin begin
mod.module_eval(source, identifier, 0) mod.module_eval(source, identifier, 0)
ObjectSpace.define_finalizer(self, Finalizer[method_name, mod]) ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])

method_name
rescue Exception => e # errors from template code rescue Exception => e # errors from template code
if logger = (view && view.logger) if logger = (view && view.logger)
logger.debug "ERROR: compiling #{method_name} RAISED #{e}" logger.debug "ERROR: compiling #{method_name} RAISED #{e}"
Expand All @@ -269,12 +281,20 @@ def #{method_name}(local_assigns)
end end
end end


def build_method_name(locals) def refresh(view)
@method_names[locals.keys.hash] ||= "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}_#{locals.keys.hash}".gsub('-', "_") return unless @virtual_path
pieces = @virtual_path.split("/")
name = pieces.pop
partial = name.sub!(/^_/, "")
view.find_template(name, pieces.join, partial || false, @locals)
end

def method_name
@method_name ||= "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}".gsub('-', "_")
end end


def identifier_method_name def identifier_method_name
@identifier_method_name ||= inspect.gsub(/[^a-z_]/, '_') inspect.gsub(/[^a-z_]/, '_')
end end
end end
end end

0 comments on commit b2600bf

Please sign in to comment.