Created
December 4, 2017 15:13
-
-
Save eidge/90186910049d8b194bc30014594266b9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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