From e30ca001efa861cc13259ca8287837174b24e679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 16 Apr 2011 10:28:47 +0200 Subject: Yo dawg, I heard you like streaming. So I put a fiber, inside a block, inside a body, so you can stream. --- actionpack/lib/action_view.rb | 12 +++- actionpack/lib/action_view/base.rb | 47 +------------ actionpack/lib/action_view/buffers.rb | 43 ++++++++++++ actionpack/lib/action_view/flows.rb | 64 ++++++++++++++++++ .../lib/action_view/helpers/capture_helper.rb | 11 +++ .../renderer/fibered_template_renderer.rb | 35 ---------- .../lib/action_view/renderer/partial_renderer.rb | 2 - .../renderer/streaming_template_renderer.rb | 79 ++++++++++++++++++++++ .../lib/action_view/renderer/template_renderer.rb | 1 - actionpack/lib/action_view/rendering.rb | 26 ++++--- actionpack/lib/action_view/template.rb | 6 ++ .../lib/action_view/template/handlers/erb.rb | 18 ++--- actionpack/test/template/capture_helper_test.rb | 2 +- 13 files changed, 235 insertions(+), 111 deletions(-) create mode 100644 actionpack/lib/action_view/buffers.rb create mode 100644 actionpack/lib/action_view/flows.rb delete mode 100644 actionpack/lib/action_view/renderer/fibered_template_renderer.rb create mode 100644 actionpack/lib/action_view/renderer/streaming_template_renderer.rb diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index 9b8b694646..4547aceb28 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -44,7 +44,7 @@ module ActionView autoload :AbstractRenderer autoload :PartialRenderer autoload :TemplateRenderer - autoload :FiberedTemplateRenderer + autoload :StreamingTemplateRenderer end autoload_at "action_view/template/resolver" do @@ -54,6 +54,16 @@ module ActionView autoload :FallbackFileSystemResolver end + autoload_at "action_view/buffers" do + autoload :OutputBuffer + autoload :StreamingBuffer + end + + autoload_at "action_view/flows" do + autoload :OutputFlow + autoload :StreamingFlow + end + autoload_at "action_view/template/error" do autoload :MissingTemplate autoload :ActionViewError diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 10a523eeac..513080ae54 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -182,7 +182,7 @@ module ActionView #:nodoc: @_config = {} @_virtual_path = nil - @_view_flow = Flow.new + @_view_flow = OutputFlow.new @output_buffer = nil if @_controller = controller @@ -205,49 +205,4 @@ module ActionView #:nodoc: ActiveSupport.run_load_hooks(:action_view, self) end - - class Flow - attr_reader :content - - def initialize - @content = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new } - end - - def get(key) - @content[key] - end - - def set(key, value) - @content[key] = value - end - - def append(key, value) - @content[key] << value - end - end - - class FiberedFlow < Flow - def initialize(flow, fiber) - @content = flow.content - @fiber = fiber - end - - def get(key) - return super if @content.key?(key) - - begin - @waiting_for = key - Fiber.yield - ensure - @waiting_for = nil - end - - super - end - - def set(key, value) - super - @fiber.resume if @waiting_for == key - end - end end diff --git a/actionpack/lib/action_view/buffers.rb b/actionpack/lib/action_view/buffers.rb new file mode 100644 index 0000000000..2e2b39e4a2 --- /dev/null +++ b/actionpack/lib/action_view/buffers.rb @@ -0,0 +1,43 @@ +require 'active_support/core_ext/string/output_safety' + +module ActionView + class OutputBuffer < ActiveSupport::SafeBuffer + def initialize(*) + super + encode! if encoding_aware? + end + + def <<(value) + super(value.to_s) + end + alias :append= :<< + alias :safe_append= :safe_concat + end + + class StreamingBuffer + def initialize(block) + @block = block + end + + def <<(value) + value = value.to_s + value = ERB::Util.h(value) unless value.html_safe? + @block.call(value) + end + alias :concat :<< + alias :append= :<< + + def safe_concat(value) + @block.call(value.to_s) + end + alias :safe_append= :safe_concat + + def html_safe? + true + end + + def html_safe + self + end + end +end \ No newline at end of file diff --git a/actionpack/lib/action_view/flows.rb b/actionpack/lib/action_view/flows.rb new file mode 100644 index 0000000000..1ac62961d1 --- /dev/null +++ b/actionpack/lib/action_view/flows.rb @@ -0,0 +1,64 @@ +require 'active_support/core_ext/string/output_safety' + +module ActionView + class OutputFlow + attr_reader :content + + def initialize + @content = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new } + end + + def get(key) + @content[key] + end + + def set(key, value) + @content[key] = value + end + + def append(key, value) + @content[key] << value + end + end + + class StreamingFlow < OutputFlow + def initialize(flow, fiber) + @content = flow.content + @fiber = fiber + @root = Fiber.current.object_id + end + + # Try to get an stored content. If the content + # is not available and we are inside the layout + # fiber, we set that we are waiting for the given + # key and yield. + def get(key) + return super if @content.key?(key) + + if inside_fiber? + begin + @waiting_for = key + Fiber.yield + ensure + @waiting_for = nil + end + end + + super + end + + # Set the contents for the given key. This is called + # by provides and resumes back to the fiber if it is + # the key it is waiting for. + def set(key, value) + super + @fiber.resume if @waiting_for == key + end + + private + + def inside_fiber? + Fiber.current.object_id != @root + end + end +end \ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index f8b5605ed9..148d814ac7 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -139,6 +139,17 @@ module ActionView result unless content end + # The same as +content_for+ but when used with streaming flushes + # straight back to the layout. In other words, if you want to + # concatenate several times to the same buffer when rendering a given + # template, you should use +content_for+, if not, use +provide+ as it + # has better streaming support. + def provide(name, content = nil, &block) + content = capture(&block) if block_given? + @_view_flow.set(name, content) if content + content + end + # content_for? simply checks whether any content has been captured yet using content_for # Useful to render parts of your layout differently based on what is in your views. # diff --git a/actionpack/lib/action_view/renderer/fibered_template_renderer.rb b/actionpack/lib/action_view/renderer/fibered_template_renderer.rb deleted file mode 100644 index 45f48cab76..0000000000 --- a/actionpack/lib/action_view/renderer/fibered_template_renderer.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'action_view/renderer/template_renderer' -require 'fiber' - -module ActionView - class FiberedTemplateRenderer < TemplateRenderer #:nodoc: - # Renders the given template. An string representing the layout can be - # supplied as well. - def render_template(template, layout_name = nil, locals = {}) #:nodoc: - view, locals = @view, locals || {} - - final = nil - layout = layout_name && find_layout(layout_name, locals.keys) - yielder = lambda { |*name| view._layout_for(*name) } - - instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do - @fiber = Fiber.new do - final = if layout - layout.render(view, locals, &yielder) - else - view._layout_for - end - end - - @view._view_flow = FiberedFlow.new(view._view_flow, @fiber) - @fiber.resume - - content = template.render(view, locals, &yielder) - view._view_flow.set(:layout, content) - @fiber.resume while @fiber.alive? - end - - final - end - end -end diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb index 180afc27ac..10cd37d56f 100644 --- a/actionpack/lib/action_view/renderer/partial_renderer.rb +++ b/actionpack/lib/action_view/renderer/partial_renderer.rb @@ -1,5 +1,3 @@ -require 'action_view/renderer/abstract_renderer' - module ActionView class PartialRenderer < AbstractRenderer #:nodoc: PARTIAL_NAMES = Hash.new {|h,k| h[k] = {} } diff --git a/actionpack/lib/action_view/renderer/streaming_template_renderer.rb b/actionpack/lib/action_view/renderer/streaming_template_renderer.rb new file mode 100644 index 0000000000..a8c6ecbde1 --- /dev/null +++ b/actionpack/lib/action_view/renderer/streaming_template_renderer.rb @@ -0,0 +1,79 @@ +require 'fiber' + +module ActionView + class StreamingTemplateRenderer < TemplateRenderer #:nodoc: + # A valid Rack::Body (i.e. it responds to each). + # It is initialized with a block that, when called, starts + # rendering the template. + class Body #:nodoc: + def initialize(&start) + @start = start + end + + def each(&block) + @start.call(block) + self + end + end + + # For streaming, instead of rendering a given a template, we return a Body + # object that responds to each. This object is initialized with a block + # that knows how to render the template. + def render_template(template, layout_name = nil, locals = {}) #:nodoc: + return [super] unless template.supports_streaming? + + locals ||= {} + layout = layout_name && find_layout(layout_name, locals.keys) + + Body.new do |buffer| + delayed_render(buffer, template, layout, @view, locals) + end + end + + private + + def delayed_render(buffer, template, layout, view, locals) + # Wrap the given buffer in the StreamingBuffer and pass it to the + # underlying template handler. Now, everytime something is concatenated + # to the buffer, it is not appended to an array, but streamed straight + # to the client. + output = ActionView::StreamingBuffer.new(buffer) + yielder = lambda { |*name| view._layout_for(*name) } + + instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do + fiber = Fiber.new do + if layout + layout.render(view, locals, output, &yielder) + else + # If you don't have a layout, just render the thing + # and concatenate the final result. This is the same + # as a layout with just <%= yield %> + output.safe_concat view._layout_for + end + end + + # Set the view flow to support streaming. It will be aware + # when to stop rendering the layout because it needs to search + # something in the template and vice-versa. + view._view_flow = StreamingFlow.new(view._view_flow, fiber) + + # Yo! Start the fiber! + fiber.resume + + # If the fiber is still alive, it means we need something + # from the template, so start rendering it. If not, it means + # the layout exited without requiring anything from the template. + if fiber.alive? + content = template.render(view, locals, &yielder) + + # Once rendering the template is done, sets its content in the :layout key. + view._view_flow.set(:layout, content) + + # In case the layout continues yielding, we need to resume + # the fiber until all yields are handled. + fiber.resume while fiber.alive? + end + end + end + end +end diff --git a/actionpack/lib/action_view/renderer/template_renderer.rb b/actionpack/lib/action_view/renderer/template_renderer.rb index 10d8cc3b87..6b5ead463f 100644 --- a/actionpack/lib/action_view/renderer/template_renderer.rb +++ b/actionpack/lib/action_view/renderer/template_renderer.rb @@ -1,6 +1,5 @@ require 'active_support/core_ext/object/try' require 'active_support/core_ext/array/wrap' -require 'action_view/renderer/abstract_renderer' module ActionView class TemplateRenderer < AbstractRenderer #:nodoc: diff --git a/actionpack/lib/action_view/rendering.rb b/actionpack/lib/action_view/rendering.rb index e3568e70e5..2bce2fb045 100644 --- a/actionpack/lib/action_view/rendering.rb +++ b/actionpack/lib/action_view/rendering.rb @@ -27,6 +27,19 @@ module ActionView end end + # Render but returns a valid Rack body. If fibers are defined, we return + # a streaming body that renders the template piece by piece. + # + # Note that partials are not supported to be rendered with streaming, + # so in such cases, we just wrap them in an array. + def render_body(options) + if options.key?(:partial) + [_render_partial(options)] + else + StreamingTemplateRenderer.new(self).render(options) + end + end + # Returns the contents that are yielded to a layout, given a name or a block. # # You can think of a layout as a method that is called with a block. If the user calls @@ -79,7 +92,7 @@ module ActionView @_view_flow.get(name).html_safe end - # Returns the content from the Flow unless we have a block. + # Handle layout for calls from partials that supports blocks. def _block_layout_for(*args, &block) name = args.first @@ -91,20 +104,11 @@ module ActionView end def _render_template(options) #:nodoc: - if @magic_medicine - _fibered_template_renderer.render(options) - else - _template_renderer.render(options) - end + _template_renderer.render(options) end def _template_renderer #:nodoc: @_template_renderer ||= TemplateRenderer.new(self) end - - def _fibered_template_renderer #:nodoc: - @_fibered_template_renderer ||= FiberedTemplateRenderer.new(self) - end - end end diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index 17e549a1c2..6dfc4f68ae 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -126,6 +126,12 @@ module ActionView @formats = Array.wrap(format).map { |f| f.is_a?(Mime::Type) ? f.ref : f } end + # Returns if the underlying handler supports streaming. If so, + # a streaming buffer *may* be passed when it start rendering. + def supports_streaming? + handler.respond_to?(:supports_streaming?) && handler.supports_streaming? + end + # Render a template. If the template was not compiled yet, it is done # exactly before rendering. # diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb index 4af576a688..7e9e4e518a 100644 --- a/actionpack/lib/action_view/template/handlers/erb.rb +++ b/actionpack/lib/action_view/template/handlers/erb.rb @@ -1,23 +1,9 @@ require 'active_support/core_ext/class/attribute_accessors' -require 'active_support/core_ext/string/output_safety' require 'action_view/template' require 'action_view/template/handler' require 'erubis' module ActionView - class OutputBuffer < ActiveSupport::SafeBuffer - def initialize(*) - super - encode! if encoding_aware? - end - - def <<(value) - super(value.to_s) - end - alias :append= :<< - alias :safe_append= :safe_concat - end - class Template module Handlers class Erubis < ::Erubis::Eruby @@ -73,6 +59,10 @@ module ActionView new.call(template) end + def supports_streaming? + true + end + def handles_encoding? true end diff --git a/actionpack/test/template/capture_helper_test.rb b/actionpack/test/template/capture_helper_test.rb index ec252fa117..3ebcb8165f 100644 --- a/actionpack/test/template/capture_helper_test.rb +++ b/actionpack/test/template/capture_helper_test.rb @@ -4,7 +4,7 @@ class CaptureHelperTest < ActionView::TestCase def setup super @av = ActionView::Base.new - @_view_flow = ActionView::Flow.new + @_view_flow = ActionView::OutputFlow.new end def test_capture_captures_the_temporary_output_buffer_in_its_block -- cgit v1.2.3