Skip to content

Commit

Permalink
Implement page compilation on demand (#330)
Browse files Browse the repository at this point in the history
* Compile one module per page

And get rid of Code.eval_quoted

* WIP: load pages on demand

* use a genserver to add backpressure on page loading queue

* reduce module footprint

* make sure the page is loaded correctly and some cleanup

* fix spec

* encapsulate router table

* remove @beacon_path_params

* remove unnucessary call to :crypto.hash/2 and Base.encode16/1

* remove unnuecessary Macro.camelize/1 call

turns out it does add some calls to the stack

* remove unnecessary code and do some cleanup

* clean up

* remove unused assign
  • Loading branch information
leandrocp authored Aug 21, 2023
1 parent 6d012ab commit c781cab
Show file tree
Hide file tree
Showing 19 changed files with 316 additions and 239 deletions.
4 changes: 4 additions & 0 deletions dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,10 @@ seeds = fn ->
<p>From dynamic_helper:</p>
<%= dynamic_helper("upcase", %{name: "beacon"}) %>
</div>
<div>
<p>RANDOM:<%= Enum.random(1..100) %></p>
</div>
</main>
""",
helpers: [
Expand Down
3 changes: 1 addition & 2 deletions lib/beacon/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ defmodule Beacon.Application do
Beacon.Repo
]

# We store routes by order and length so the most visited pages will likely be in the first rows
:ets.new(:beacon_pages, [:ordered_set, :named_table, :public, read_concurrency: true])
Beacon.Router.init()

:ets.new(:beacon_assets, [:set, :named_table, :public, read_concurrency: true])

Expand Down
36 changes: 12 additions & 24 deletions lib/beacon/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ defmodule Beacon.Config do
[
{identifier :: atom(),
fun ::
(Beacon.Template.t(), Beacon.Template.LoadMetadata.t() ->
{:cont, Beacon.Template.t()} | {:halt, Beacon.Template.t()} | {:halt, Exception.t()})}
(template :: String.t(), Beacon.Template.LoadMetadata.t() ->
{:cont, String.t() | Beacon.Template.t()} | {:halt, String.t() | Beacon.Template.t()} | {:halt, Exception.t()})}
]}
]}
| {:render_template,
Expand Down Expand Up @@ -168,14 +168,8 @@ defmodule Beacon.Config do
]

@default_render_template [
{:heex,
[
eval_heex_ast: &Beacon.Template.HEEx.eval_ast/2
]},
{:markdown,
[
eval_heex_ast: &Beacon.Template.HEEx.eval_ast/2
]}
{:heex, []},
{:markdown, []}
]

@default_media_types ["image/jpeg", "image/gif", "image/png", "image/webp", ".pdf"]
Expand Down Expand Up @@ -283,15 +277,14 @@ defmodule Beacon.Config do
load_template: [
{:custom_format,
[
validate: fn template, _metadata -> MyEngine.validate(template) end
validate: fn template, _metadata -> MyEngine.validate(template) end,
build_rendered: fn template, _metadata -> %Phoenix.LiveView.Rendered{static: template} end,
]}
],
render_template: [
{:custom_format,
[
assigns: fn template, %{assigns: assigns} -> MyEngine.parse_to_html(template, assigns) end,
compile: &Beacon.Template.HEEx.compile/2,
eval: &Beacon.Template.HEEx.eval_ast/2
assigns: fn %Phoenix.LiveView.Rendered{static: template} , %{assigns: assigns} -> MyEngine.parse_to_html(template, assigns) end
]}
],
after_publish_page: [
Expand Down Expand Up @@ -329,20 +322,15 @@ defmodule Beacon.Config do
compile_heex: &Beacon.Template.HEEx.compile/2
],
custom_format: [
validate: #Function<41.3316493/2 in :erl_eval.expr/6>
validate: #Function<41.3316493/2 in :erl_eval.expr/6>,
build_rendered: #Function<41.3316494/2 in :erl_eval.expr/6>
]
],
render_template: [
heex: [
eval_heex_ast: &Beacon.Template.HEEx.eval_ast/2
],
markdown: [
eval_heex_ast: &Beacon.Template.HEEx.eval_ast/2
],
heex: [],
markdown: [],
custom_format: [
assigns: #Function<41.3316493/2 in :erl_eval.expr/6>,
compile: &Beacon.Template.HEEx.compile/2,
eval: &Beacon.Template.HEEx.eval_ast/2
assigns: #Function<41.3316493/2 in :erl_eval.expr/6>
]
],
after_create_page: [],
Expand Down
18 changes: 15 additions & 3 deletions lib/beacon/lifecycle/template.ex
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,22 @@ defmodule Beacon.Lifecycle.Template do
@doc """
Render a `page` template using the registered format used on the `page`.
This stage runs in the render callback of the LiveView responsible for displaying the page.
## Notes
- This stage runs in the render callback of the LiveView responsible for displaying the page.
- It will load and compile the page module if it wasn't not loaded yet.
"""
def render_template(site, template, format, context) do
lifecycle = Lifecycle.execute(__MODULE__, site, :render_template, template, sub_key: format, context: context)
@spec render_template(Beacon.Content.Page.t(), module(), map(), Macro.Env.t()) :: Beacon.Template.t()
def render_template(page, page_module, assigns, env) do
template =
case page_module.render(assigns) do
%Phoenix.LiveView.Rendered{} = rendered -> rendered
:not_loaded -> Beacon.Loader.PageModuleLoader.load_page_template!(page, page_module, assigns)
end

context = [path: page.path, assigns: assigns, env: env]
lifecycle = Lifecycle.execute(__MODULE__, page.site, :render_template, template, sub_key: page.format, context: context)
lifecycle.output
end
end
30 changes: 16 additions & 14 deletions lib/beacon/loader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ defmodule Beacon.Loader do
def load_page(%Content.Page{} = page) do
page = Repo.preload(page, :event_handlers)
config = Beacon.Config.fetch!(page.site)
GenServer.call(name(config.site), {:load_page, page}, 60_000)
GenServer.call(name(config.site), {:load_page, page}, 30_000)
end

@doc false
Expand Down Expand Up @@ -172,7 +172,7 @@ defmodule Beacon.Loader do
|> Content.list_published_pages()
|> Enum.map(fn page ->
Task.async(fn ->
{:ok, _ast} = Beacon.Loader.PageModuleLoader.load_page!(page)
{:ok, _module, _ast} = Beacon.Loader.PageModuleLoader.load_page!(page)
:ok
end)
end)
Expand All @@ -197,20 +197,22 @@ defmodule Beacon.Loader do
end

@doc false
def layout_module_for_site(site, layout_id) do
prefix = Macro.camelize("layout_#{layout_id}")
module_for_site(site, prefix)
def layout_module_for_site(layout_id) do
module_for_site("Layout#{layout_id}")
end

@doc false
def page_module_for_site(site, page_id) do
prefix = Macro.camelize("page_#{page_id}")
module_for_site(site, prefix)
def page_module_for_site(page_id) do
module_for_site("Page#{page_id}")
end

defp module_for_site(site, prefix) do
site_hash = :crypto.hash(:md5, Atom.to_string(site)) |> Base.encode16()
Module.concat([BeaconWeb.LiveRenderer, "#{prefix}#{site_hash}"])
defp module_for_site(resource) do
Module.concat([BeaconWeb.LiveRenderer, resource])
end

defp module_for_site(site, resource) do
site_hash = :md5 |> :crypto.hash(Atom.to_string(site)) |> Base.encode16()
Module.concat([BeaconWeb.LiveRenderer, "#{site_hash}#{resource}"])
end

# This retry logic exists because a module may be in the process of being reloaded, in which case we want to retry
Expand Down Expand Up @@ -358,8 +360,8 @@ defmodule Beacon.Loader do
:ok <- load_snippet_helpers(page.site),
{:ok, _ast} <- Beacon.Loader.LayoutModuleLoader.load_layout!(layout),
:ok <- load_stylesheets(page.site),
{:ok, _ast} <- Beacon.Loader.PageModuleLoader.load_page!(page) do
:ok = Beacon.PubSub.page_loaded(page)
{:ok, _module, _ast} <- Beacon.Loader.PageModuleLoader.load_page!(page),
:ok <- Beacon.PubSub.page_loaded(page) do
:ok
else
_ -> raise Beacon.LoaderError, message: "failed to load resources for page #{page.title} of site #{page.site}"
Expand All @@ -368,7 +370,7 @@ defmodule Beacon.Loader do

@doc false
def do_unload_page(page) do
module = page_module_for_site(page.site, page.id)
module = page_module_for_site(page.id)
:code.delete(module)
:code.purge(module)
Beacon.Router.del_page(page.site, page.path)
Expand Down
2 changes: 1 addition & 1 deletion lib/beacon/loader/layout_module_loader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ defmodule Beacon.Loader.LayoutModuleLoader do

def load_layout!(%Content.Layout{} = layout) do
component_module = Loader.component_module_for_site(layout.site)
module = Loader.layout_module_for_site(layout.site, layout.id)
module = Loader.layout_module_for_site(layout.id)
render_function = render_layout(layout)
ast = render(module, render_function, component_module)
:ok = Loader.reload_module!(module, ast)
Expand Down
Loading

0 comments on commit c781cab

Please sign in to comment.