fix: issue #3 slow rendering
This commit is contained in:
@@ -5,9 +5,11 @@ defmodule BDS.Generation.Outputs do
|
||||
import BDS.Generation.Renderers
|
||||
import BDS.Generation.Sitemap, only: [render_feed: 3, render_atom: 3, render_calendar: 1]
|
||||
|
||||
alias BDS.Rendering.RenderContext
|
||||
alias BDS.Rendering.TemplateSelection
|
||||
|
||||
@type output_callback :: ({String.t(), iodata()} -> any()) | nil
|
||||
@type render_target :: RenderContext.t() | String.t()
|
||||
|
||||
@spec additional_languages(map()) :: [String.t()]
|
||||
def additional_languages(plan) do
|
||||
@@ -209,7 +211,7 @@ defmodule BDS.Generation.Outputs do
|
||||
{String.t(), iodata()}
|
||||
]
|
||||
def build_category_outputs(plan, posts_by_category, languages, on_output \\ nil) do
|
||||
Enum.flat_map(posts_by_category, fn {category, posts} ->
|
||||
concurrent_flat_map(posts_by_category, fn {category, posts} ->
|
||||
paginated_posts = Enum.chunk_every(posts, max(plan.max_posts_per_page, 1))
|
||||
category_slug = archive_route_segment(category)
|
||||
total_pages = length(paginated_posts)
|
||||
@@ -267,7 +269,7 @@ defmodule BDS.Generation.Outputs do
|
||||
{String.t(), iodata()}
|
||||
]
|
||||
def build_tag_outputs(plan, posts_by_tag, languages, on_output \\ nil) do
|
||||
Enum.flat_map(posts_by_tag, fn {tag, posts} ->
|
||||
concurrent_flat_map(posts_by_tag, fn {tag, posts} ->
|
||||
tag_slug = archive_route_segment(tag)
|
||||
|
||||
build_paginated_archive_outputs(
|
||||
@@ -288,7 +290,7 @@ defmodule BDS.Generation.Outputs do
|
||||
]
|
||||
def build_date_outputs(plan, post_index, languages, on_output \\ nil) do
|
||||
year_outputs =
|
||||
Enum.flat_map(post_index.posts_by_year, fn {year, posts} ->
|
||||
concurrent_flat_map(post_index.posts_by_year, fn {year, posts} ->
|
||||
build_paginated_archive_outputs(
|
||||
plan,
|
||||
languages,
|
||||
@@ -309,7 +311,7 @@ defmodule BDS.Generation.Outputs do
|
||||
end)
|
||||
|
||||
month_outputs =
|
||||
Enum.flat_map(post_index.posts_by_year_month, fn {year_month, posts} ->
|
||||
concurrent_flat_map(post_index.posts_by_year_month, fn {year_month, posts} ->
|
||||
[year, month] = String.split(year_month, "/", parts: 2)
|
||||
|
||||
build_paginated_archive_outputs(
|
||||
@@ -332,7 +334,7 @@ defmodule BDS.Generation.Outputs do
|
||||
end)
|
||||
|
||||
day_outputs =
|
||||
Enum.flat_map(post_index.posts_by_year_month_day, fn {year_month_day, posts} ->
|
||||
concurrent_flat_map(post_index.posts_by_year_month_day, fn {year_month_day, posts} ->
|
||||
[year, month, day] = String.split(year_month_day, "/", parts: 3)
|
||||
|
||||
build_paginated_archive_outputs(
|
||||
@@ -366,7 +368,7 @@ defmodule BDS.Generation.Outputs do
|
||||
def build_core_outputs(plan, published_posts, localized_posts_by_language, on_output \\ nil) do
|
||||
language = plan.language
|
||||
additional_languages = Enum.reject(plan.blog_languages, &(&1 == language))
|
||||
main_posts = build_list_posts(plan.base_url, published_posts, nil)
|
||||
main_posts = build_list_posts(plan, published_posts, nil)
|
||||
|
||||
main_static_outputs =
|
||||
[
|
||||
@@ -384,7 +386,7 @@ defmodule BDS.Generation.Outputs do
|
||||
localized_source_posts = Map.get(localized_posts_by_language, localized_language, [])
|
||||
|
||||
localized_posts =
|
||||
build_list_posts(plan.base_url, localized_source_posts, localized_prefix)
|
||||
build_list_posts(plan, localized_source_posts, localized_prefix)
|
||||
|
||||
# `build_root_outputs` is called without `on_output` here because the
|
||||
# combined list (roots + static files) is reported exactly once by the
|
||||
@@ -404,11 +406,12 @@ defmodule BDS.Generation.Outputs do
|
||||
end)
|
||||
end
|
||||
|
||||
@spec build_page_outputs(String.t(), String.t(), [map()], map(), map(), output_callback()) :: [
|
||||
{String.t(), iodata()}
|
||||
]
|
||||
@spec build_page_outputs(render_target(), String.t(), [map()], map(), map(), output_callback()) ::
|
||||
[
|
||||
{String.t(), iodata()}
|
||||
]
|
||||
def build_page_outputs(
|
||||
project_id,
|
||||
render_target,
|
||||
main_language,
|
||||
published_posts,
|
||||
translations_by_post_language,
|
||||
@@ -418,17 +421,17 @@ defmodule BDS.Generation.Outputs do
|
||||
page_outputs =
|
||||
published_posts
|
||||
|> Enum.filter(&("page" in (&1.categories || [])))
|
||||
|> Enum.map(fn post ->
|
||||
|> concurrent_map(fn post ->
|
||||
canonical_variant = Map.get(translations_by_post_language, {post.id, main_language}, post)
|
||||
render_language = effective_render_language(canonical_variant.language, main_language)
|
||||
body = load_body(project_id, canonical_variant.file_path, canonical_variant.content)
|
||||
body = load_body(render_target, canonical_variant.file_path, canonical_variant.content)
|
||||
|
||||
effective_slug = effective_template_slug(project_id, post)
|
||||
effective_slug = effective_template_slug(render_target, post)
|
||||
|
||||
output =
|
||||
{page_output_path(post.slug, nil),
|
||||
render_post_output(
|
||||
project_id,
|
||||
render_target,
|
||||
effective_slug,
|
||||
%{
|
||||
id: canonical_variant.id,
|
||||
@@ -457,16 +460,16 @@ defmodule BDS.Generation.Outputs do
|
||||
|> Enum.flat_map(fn {language, posts} ->
|
||||
posts
|
||||
|> Enum.filter(&("page" in (&1.categories || [])))
|
||||
|> Enum.map(fn post ->
|
||||
|> concurrent_map(fn post ->
|
||||
render_language = effective_render_language(post.language, language)
|
||||
body = load_body(project_id, post.file_path, post.content)
|
||||
body = load_body(render_target, post.file_path, post.content)
|
||||
|
||||
effective_slug = effective_template_slug(project_id, post)
|
||||
effective_slug = effective_template_slug(render_target, post)
|
||||
|
||||
output =
|
||||
{page_output_path(post.slug, language),
|
||||
render_post_output(
|
||||
project_id,
|
||||
render_target,
|
||||
effective_slug,
|
||||
%{
|
||||
id: post.id,
|
||||
@@ -497,7 +500,7 @@ defmodule BDS.Generation.Outputs do
|
||||
posts
|
||||
|> paginate_posts(plan.max_posts_per_page)
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.map(fn {page_posts, page_number} ->
|
||||
|> concurrent_map(fn {page_posts, page_number} ->
|
||||
route_language = route_language(plan.language, language)
|
||||
|
||||
output =
|
||||
@@ -570,12 +573,12 @@ defmodule BDS.Generation.Outputs do
|
||||
end)
|
||||
end
|
||||
|
||||
@spec build_single_outputs(String.t(), String.t(), [map()], map(), map(), output_callback()) ::
|
||||
@spec build_single_outputs(render_target(), String.t(), [map()], map(), map(), output_callback()) ::
|
||||
[
|
||||
{String.t(), iodata()}
|
||||
]
|
||||
def build_single_outputs(
|
||||
project_id,
|
||||
render_target,
|
||||
main_language,
|
||||
published_posts,
|
||||
translations_by_post_language,
|
||||
@@ -583,17 +586,17 @@ defmodule BDS.Generation.Outputs do
|
||||
on_output \\ nil
|
||||
) do
|
||||
post_outputs =
|
||||
Enum.map(published_posts, fn post ->
|
||||
concurrent_map(published_posts, fn post ->
|
||||
canonical_variant = Map.get(translations_by_post_language, {post.id, main_language}, post)
|
||||
render_language = effective_render_language(canonical_variant.language, main_language)
|
||||
body = load_body(project_id, canonical_variant.file_path, canonical_variant.content)
|
||||
body = load_body(render_target, canonical_variant.file_path, canonical_variant.content)
|
||||
|
||||
effective_slug = effective_template_slug(project_id, post)
|
||||
effective_slug = effective_template_slug(render_target, post)
|
||||
|
||||
output =
|
||||
{post_output_path(post),
|
||||
render_post_output(
|
||||
project_id,
|
||||
render_target,
|
||||
effective_slug,
|
||||
%{
|
||||
id: canonical_variant.id,
|
||||
@@ -620,16 +623,16 @@ defmodule BDS.Generation.Outputs do
|
||||
translation_outputs =
|
||||
localized_posts_by_language
|
||||
|> Enum.flat_map(fn {language, posts} ->
|
||||
Enum.map(posts, fn post ->
|
||||
concurrent_map(posts, fn post ->
|
||||
render_language = effective_render_language(post.language, language)
|
||||
body = load_body(project_id, post.file_path, post.content)
|
||||
body = load_body(render_target, post.file_path, post.content)
|
||||
|
||||
effective_slug = effective_template_slug(project_id, post)
|
||||
effective_slug = effective_template_slug(render_target, post)
|
||||
|
||||
output =
|
||||
{post_output_path(post, language),
|
||||
render_post_output(
|
||||
project_id,
|
||||
render_target,
|
||||
effective_slug,
|
||||
%{
|
||||
id: post.id,
|
||||
@@ -650,14 +653,14 @@ defmodule BDS.Generation.Outputs do
|
||||
post_outputs ++ translation_outputs
|
||||
end
|
||||
|
||||
defp effective_template_slug(project_id, post) do
|
||||
defp effective_template_slug(render_target, post) do
|
||||
slug = Map.get(post, :template_slug)
|
||||
|
||||
if is_binary(slug) and slug != "" do
|
||||
slug
|
||||
else
|
||||
TemplateSelection.resolve_post_template_slug(
|
||||
project_id,
|
||||
render_target,
|
||||
Map.get(post, :tags) || [],
|
||||
Map.get(post, :categories) || []
|
||||
)
|
||||
@@ -682,4 +685,23 @@ defmodule BDS.Generation.Outputs do
|
||||
on_output.(output)
|
||||
output
|
||||
end
|
||||
|
||||
# Render pages on all cores, like the old app's worker threads — safe because
|
||||
# the render context is immutable data and the output callback only touches
|
||||
# cross-process-safe state (file writes, ETS, counters). Order is preserved.
|
||||
defp concurrent_map(enum, fun) do
|
||||
enum
|
||||
|> Task.async_stream(fun,
|
||||
timeout: :infinity,
|
||||
max_concurrency: System.schedulers_online(),
|
||||
ordered: true
|
||||
)
|
||||
|> Enum.map(fn {:ok, value} -> value end)
|
||||
end
|
||||
|
||||
defp concurrent_flat_map(enum, fun) do
|
||||
enum
|
||||
|> concurrent_map(fun)
|
||||
|> Enum.concat()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,6 +4,7 @@ defmodule BDS.Generation.Renderers do
|
||||
alias BDS.Generation.Paths
|
||||
alias BDS.Projects
|
||||
alias BDS.Rendering
|
||||
alias BDS.Rendering.RenderContext
|
||||
|
||||
@doc "Render the home page (HTML) using the project's template engine."
|
||||
@spec render_home(map(), String.t() | nil) :: String.t()
|
||||
@@ -80,7 +81,7 @@ defmodule BDS.Generation.Renderers do
|
||||
plan,
|
||||
language,
|
||||
label,
|
||||
build_list_posts(plan.base_url, posts, Paths.route_language(plan.language, language)),
|
||||
build_list_posts(plan, posts, Paths.route_language(plan.language, language)),
|
||||
archive_context,
|
||||
pagination,
|
||||
fallback
|
||||
@@ -88,9 +89,13 @@ defmodule BDS.Generation.Renderers do
|
||||
end
|
||||
|
||||
@doc "Try the project's post template; on error, fall back to the inline `fallback` thunk."
|
||||
@spec render_post_output(String.t(), String.t() | nil, map(), (-> String.t())) :: String.t()
|
||||
def render_post_output(project_id, template_slug, assigns, fallback) do
|
||||
render_with_fallback(fn -> Rendering.render_post_page(project_id, template_slug, assigns) end, fallback)
|
||||
@spec render_post_output(RenderContext.t() | String.t(), String.t() | nil, map(), (-> String.t())) ::
|
||||
String.t()
|
||||
def render_post_output(render_target, template_slug, assigns, fallback) do
|
||||
render_with_fallback(
|
||||
fn -> Rendering.render_post_page(render_target, template_slug, assigns) end,
|
||||
fallback
|
||||
)
|
||||
end
|
||||
|
||||
@doc "Render a list/archive page through the project template, falling back to inline."
|
||||
@@ -105,7 +110,7 @@ defmodule BDS.Generation.Renderers do
|
||||
) ::
|
||||
String.t()
|
||||
def render_list_output(
|
||||
%{project_id: project_id, language: main_language},
|
||||
%{project_id: project_id, language: main_language} = plan,
|
||||
language,
|
||||
page_title,
|
||||
posts,
|
||||
@@ -116,7 +121,7 @@ defmodule BDS.Generation.Renderers do
|
||||
when is_binary(project_id) do
|
||||
render_with_fallback(
|
||||
fn ->
|
||||
Rendering.render_list_page(project_id, %{
|
||||
Rendering.render_list_page(plan_render_target(plan), %{
|
||||
language: language,
|
||||
language_prefix: Paths.language_prefix(language, main_language),
|
||||
page_title: page_title,
|
||||
@@ -131,11 +136,14 @@ defmodule BDS.Generation.Renderers do
|
||||
|
||||
@doc "Render the project's 404 page via its template, falling back to a static page."
|
||||
@spec render_not_found_output(map(), String.t() | nil) :: String.t()
|
||||
def render_not_found_output(%{project_id: project_id, language: main_language}, language)
|
||||
def render_not_found_output(
|
||||
%{project_id: project_id, language: main_language} = plan,
|
||||
language
|
||||
)
|
||||
when is_binary(project_id) do
|
||||
render_with_fallback(
|
||||
fn ->
|
||||
Rendering.render_not_found_page(project_id, %{
|
||||
Rendering.render_not_found_page(plan_render_target(plan), %{
|
||||
language: language,
|
||||
language_prefix: Paths.language_prefix(language, main_language)
|
||||
})
|
||||
@@ -144,6 +152,15 @@ defmodule BDS.Generation.Renderers do
|
||||
)
|
||||
end
|
||||
|
||||
@doc "The render context stashed in the plan, or the bare project id when absent."
|
||||
@spec plan_render_target(map()) :: RenderContext.t() | String.t()
|
||||
def plan_render_target(plan) do
|
||||
case Map.get(plan, :render_context) do
|
||||
%RenderContext{} = ctx -> ctx
|
||||
_other -> plan.project_id
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Static fallback HTML for a 404 page."
|
||||
@spec render_not_found_page(String.t() | nil) :: String.t()
|
||||
def render_not_found_page(language) do
|
||||
@@ -156,41 +173,49 @@ defmodule BDS.Generation.Renderers do
|
||||
end
|
||||
|
||||
@doc "Build the list-of-posts payload (with hrefs and bodies) for archive/list templates."
|
||||
@spec build_list_posts(String.t() | nil, [map()], String.t() | nil) :: [map()]
|
||||
def build_list_posts(base_url, posts, language_prefix) do
|
||||
@spec build_list_posts(map(), [map()], String.t() | nil) :: [map()]
|
||||
def build_list_posts(plan, posts, language_prefix) do
|
||||
render_target = plan_render_target(plan)
|
||||
|
||||
Enum.map(posts, fn post ->
|
||||
%{
|
||||
id: post.id,
|
||||
slug: post.slug,
|
||||
title: post.title,
|
||||
href: Paths.url_for_output(base_url, Paths.post_output_path(post, language_prefix)),
|
||||
href: Paths.url_for_output(plan.base_url, Paths.post_output_path(post, language_prefix)),
|
||||
excerpt: post.excerpt,
|
||||
content: load_body(post.project_id, post.file_path, post.content)
|
||||
content: load_body(render_target, post.file_path, post.content)
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc "Load the post body from disk (or pass-through inline content) for list rendering."
|
||||
@spec load_body(String.t() | nil, String.t() | nil, String.t() | nil) :: String.t()
|
||||
def load_body(_project_id, _file_path, inline_content) when is_binary(inline_content),
|
||||
@spec load_body(RenderContext.t() | String.t() | nil, String.t() | nil, String.t() | nil) ::
|
||||
String.t()
|
||||
def load_body(_render_target, _file_path, inline_content) when is_binary(inline_content),
|
||||
do: inline_content
|
||||
|
||||
def load_body(project_id, file_path, _inline_content) do
|
||||
case file_path do
|
||||
nil ->
|
||||
""
|
||||
def load_body(%RenderContext{} = ctx, file_path, _inline_content)
|
||||
when is_binary(file_path) and file_path != "" do
|
||||
RenderContext.memoize(ctx, {:post_body, file_path}, fn ->
|
||||
read_body_file(Path.expand(file_path, ctx.project_data_dir))
|
||||
end)
|
||||
end
|
||||
|
||||
"" ->
|
||||
""
|
||||
def load_body(project_id, file_path, _inline_content)
|
||||
when is_binary(project_id) and is_binary(file_path) and file_path != "" do
|
||||
project_id
|
||||
|> Projects.get_project!()
|
||||
|> Projects.project_data_dir()
|
||||
|> then(&read_body_file(Path.expand(file_path, &1)))
|
||||
end
|
||||
|
||||
value ->
|
||||
project_path =
|
||||
Path.expand(value, Projects.project_data_dir(Projects.get_project!(project_id)))
|
||||
def load_body(_render_target, _file_path, _inline_content), do: ""
|
||||
|
||||
case File.read(project_path) do
|
||||
{:ok, contents} -> parse_frontmatter_body(contents)
|
||||
{:error, _reason} -> ""
|
||||
end
|
||||
defp read_body_file(full_path) do
|
||||
case File.read(full_path) do
|
||||
{:ok, contents} -> parse_frontmatter_body(contents)
|
||||
{:error, _reason} -> ""
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user