Permalink
Browse files

Add a renderer that is able to cache compiled modules

  • Loading branch information...
1 parent 552f9c8 commit 896f73c6fa25eb448a59f9769559bdcbc921896b @josevalim josevalim committed Sep 7, 2012
Showing with 146 additions and 32 deletions.
  1. +5 −2 lib/dynamo/app.ex
  2. +6 −30 lib/dynamo/views.ex
  3. +127 −0 lib/dynamo/views/renderer.ex
  4. +8 −0 test/dynamo/views_test.exs
View
@@ -158,6 +158,10 @@ defmodule Dynamo.App do
filters = [Dynamo.Filters.Reloader.new(dynamo[:compile_on_demand], dynamo[:reload_modules])|filters]
end
+ if dynamo[:reload_modules] && !dynamo[:compile_on_demand] do
+ raise "Cannot have reload_modules set to true and compile_on_demand set to false"
+ end
+
filters
end
@@ -196,8 +200,7 @@ defmodule Dynamo.App do
defmacro apply_filters(_) do
quote location: :keep do
Enum.each Dynamo.App.config_filters(__MODULE__), prepend_filter(&1)
- @__reverse_filters Enum.reverse @__filters
- def filters, do: @__reverse_filters
+ def :filters, [], [], do: Macro.escape(Enum.reverse(@__filters))
end
end
View
@@ -1,5 +1,5 @@
defmodule Dynamo.Views do
- defrecord Template, identifier: nil, format: nil, handler: nil, source: nil, updated_at: nil
+ defrecord Template, identifier: nil, format: nil, handler: nil, source: nil, ref: nil, updated_at: nil
defexception TemplateNotFound, query: nil, view_paths: nil do
def message(exception) do
@@ -9,39 +9,15 @@ defmodule Dynamo.Views do
def render(query, view_paths, assigns) do
template = Enum.find_value(view_paths, fn(x) -> x.find(query) end)
- module = compile_template template, query, view_paths
-
- try do
- module.render(assigns, template)
- after
- :code.purge(module)
- :code.delete(module)
- end
+ check_template(template, query, view_paths)
+ Dynamo.Views.Renderer.render(template, Keyword.put(assigns, :template, template))
end
- defp compile_template(nil, query, view_paths) do
+ defp check_template(nil, query, view_paths) do
raise Dynamo.Views.TemplateNotFound, query: query, view_paths: view_paths
end
- defp compile_template(Dynamo.Views.Template[handler: handler] = template, _, _) do
- source = get_handler(handler).compile(template)
-
- source = quote hygiene: false do
- _ = assigns
- _ = template
- unquote(source)
- end
-
- defmodule Elixir.Dynamo.Views.CompiledTemplate do
- args = quote hygiene: false, do: [assigns, template]
- def :render, args, [], do: source
- end
-
- Dynamo.Views.CompiledTemplate
- end
-
- # TODO: Remove hardcoded handler.
- defp get_handler("eex") do
- Dynamo.Views.EEXHandler
+ defp check_template(_, _, _) do
+ :ok
end
end
@@ -0,0 +1,127 @@
+defmodule Dynamo.Views.Renderer do
+ @moduledoc false
+ @slots 1_000_000
+ @max_attempts 1_000
+
+ use GenServer.Behaviour
+ alias Dynamo.Views.Template, as: Template
+
+ @doc """
+ Starts the `Dynamo.Views.Renderer` server.
+ Usually called internally by Dynamo.
+ """
+ def start_link do
+ :gen_server.start({ :local, __MODULE__ }, __MODULE__, [], [])
+ end
+
+ @doc """
+ Stops the `Dynamo.Views.Renderer` server.
+ """
+ def stop do
+ :gen_server.call(__MODULE__, :stop)
+ end
+
+ @doc """
+ This function is responsible for rendering the templates.
+ It supports both pre-compiled and on demand compilation.
+
+ The on demand mode needs to be explicitly enabled
+ by calling start_link/0.
+ """
+ def render(Template[ref: { mod, fun }], assigns) do
+ apply mod, fun, [assigns]
+ end
+
+ def render(template, assigns) do
+ module =
+ get_cached(template) || compile(template) || raise_too_busy(template)
+
+ module.render(assigns)
+ end
+
+ ## Callbacks
+
+ @doc false
+ def init(args) do
+ { :ok, Binary.Dict.new(args) }
+ end
+
+ @doc false
+ def handle_call({ :get_cached, identifier, updated_at }, _from, dict) do
+ case Dict.get(dict, identifier) do
+ { module, cached } when updated_at > cached ->
+ spawn fn ->
+ :code.purge(module)
+ :code.delete(module)
+ end
+
+ { :reply, nil, Dict.delete(dict, identifier) }
+ { module, _ } ->
+ { :reply, module, dict }
+ nil ->
+ { :reply, nil, dict }
+ end
+ end
+
+ def handle_call({ :register, identifier, updated_at, compiled }, _from, dict) do
+ if module = generate_module(compiled, identifier, 0) do
+ { :reply, module, Dict.put(dict, identifier, { module, updated_at }) }
+ else
+ { :reply, nil, dict }
+ end
+ end
+
+ def handle_call(:stop, _from, state) do
+ { :stop, :normal, :ok, state }
+ end
+
+ def handle_call(_arg, _from, _config) do
+ super
+ end
+
+ ## Helpers
+
+ defp get_cached(Template[identifier: identifier, updated_at: updated_at]) do
+ :gen_server.call(__MODULE__, { :get_cached, identifier, updated_at })
+ end
+
+ defp compile(Template[handler: handler, identifier: identifier, updated_at: updated_at] = template) do
+ compiled = get_handler(handler).compile(template)
+ :gen_server.call(__MODULE__, { :register, identifier, updated_at, compiled })
+ end
+
+ defp raise_too_busy(Template[identifier: identifier]) do
+ raise "Compiling template #{inspect identifier} exceeded the max number of attempts #{@max_attemps}. What gives?"
+ end
+
+ # TODO: Remove hardcoded handler.
+ defp get_handler("eex") do
+ Dynamo.Views.EEXHandler
+ end
+
+ defp generate_module(source, identifier, attempts) when attempts < @max_attemps do
+ random = :random.uniform(@slots)
+ module = Module.concat(Dynamo.Views, "Template#{random}")
+
+ if :code.is_loaded(module) do
+ generate_module(source, identifier, attempts + 1)
+ else
+ source = quote hygiene: false do
+ _ = assigns
+ unquote(source)
+ end
+
+ defmodule module do
+ @file identifier
+ args = quote hygiene: false, do: [assigns]
+ def :render, args, [], do: source
+ end
+
+ module
+ end
+ end
+
+ defp generate_module(_, _, _) do
+ nil
+ end
+end
@@ -5,6 +5,14 @@ defmodule Dynamo.ViewsTest do
@view_paths [Dynamo.Views.PathFinder.new(File.expand_path("../../fixtures/views", __FILE__))]
+ def setup(_) do
+ Dynamo.Views.Renderer.start_link
+ end
+
+ def teardown(_) do
+ Dynamo.Views.Renderer.stop
+ end
+
test "renders a template" do
body = Dynamo.Views.render "hello.html", @view_paths, []
assert body == "HELLO!"

0 comments on commit 896f73c

Please sign in to comment.