Skip to content

Instantly share code, notes, and snippets.

@eidge
Created December 4, 2017 15:13
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 eidge/90186910049d8b194bc30014594266b9 to your computer and use it in GitHub Desktop.
Save eidge/90186910049d8b194bc30014594266b9 to your computer and use it in GitHub Desktop.
require 'erubi'
# # Problem
#
# Creating reports involves rendering HTML from templates. We are currently
# using Rails for this, but as Rails is meant to serve web requests, their
# template rendering engine will render templates in memory.
#
# This is a constraint for reports as they might have hundred of pages and
# therefore occupy hundreds of megabytes of memory.
#
# # Solution
#
# Compile and render templates directly into disk (eventually with a buffer in
# between to improve performance) so that we are not bound by the amount of
# available RAM to produce reports.
#
# This is a proof of concept to allow ERB templates to be rendered into an IO
# Stream. It reduces RAM usage to almost zero since everything is streamed
# directly into disk.
#
# Rendering a benchmark of 600 000 000 lines worth of HTML blows up with an
# OutOfMemory error in the default implementation but will render nicely into a
# file using the StreamingTemplate.
class StreamingTemplate
ENGINE_OPTIONS = {
bufvar: 'buffer',
preamble: '',
postamble: 'buffer',
}.freeze
def initialize(buffer: nil)
@buffer = buffer || StringIO.new
end
def buffer=(rhs)
fail "Buffer can't be nil" if rhs.nil?
@buffer = rhs
end
def template
fail NotImplementedError, 'No template provided'
end
def src
engine.src
end
def render
eval(src, binding)
end
protected
def component(component)
component.buffer = buffer
component.render
nil
end
private
def buffer
@buffer
end
def engine
@engine ||= Erubi::Engine.new(template, ENGINE_OPTIONS)
end
end
class ReportTemplate < StreamingTemplate
def template
<<-TEMPLATE
<%= component header %>
<% 600_000_000.times do %>
Hello <%= sir %>
<% end %>
TEMPLATE
end
def sir
'Nourish Care'
end
def header
OrganisationUnitHeader.new
end
end
class OrganisationUnitHeader < StreamingTemplate
def template
'<%= component header %>'
end
def header
Header.new('Best Care Homes')
end
end
class Header < StreamingTemplate
def initialize(text)
@text = text
end
attr_reader :text
def template
'<%= text %>'
end
end
# Rendering report to disk yields memory usage next to 0
#
p 'Report to disk'
file = File.open('bench.html', 'w')
ReportTemplate.new(buffer: file).render
file.close
# Rendering in memory memory usage next in the order of GBs
#
# p 'Report in memory (will probably crash or take GB of ram)'
# ReportTemplate.new.render
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment