Mercurial > piecrust2
comparison piecrust/rendering.py @ 989:8adc27285d93
bake: Big pass on bake performance.
- Reduce the amount of data passed between processes.
- Make inter-process data simple objects to make it easier to test with
alternatives to pickle.
- Make sources have the basic requirement to be able to find a content item
from an item spec (path).
- Make Hoedown the default Markdown formatter.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Sun, 19 Nov 2017 14:29:17 -0800 |
parents | 45ad976712ec |
children | 1857dbd4580f |
comparison
equal
deleted
inserted
replaced
988:f83ae0a5d793 | 989:8adc27285d93 |
---|---|
1 import re | 1 import re |
2 import os.path | 2 import os.path |
3 import copy | |
4 import logging | 3 import logging |
5 from piecrust.data.builder import ( | 4 from piecrust.data.builder import ( |
6 DataBuildingContext, build_page_data, add_layout_data) | 5 DataBuildingContext, build_page_data, add_layout_data) |
7 from piecrust.templating.base import TemplateNotFoundError, TemplatingError | 6 from piecrust.templating.base import TemplateNotFoundError, TemplatingError |
8 from piecrust.sources.base import AbortedSourceUseError | 7 from piecrust.sources.base import AbortedSourceUseError |
22 class TemplateEngineNotFound(Exception): | 21 class TemplateEngineNotFound(Exception): |
23 pass | 22 pass |
24 | 23 |
25 | 24 |
26 class RenderedSegments(object): | 25 class RenderedSegments(object): |
27 def __init__(self, segments, render_pass_info): | 26 def __init__(self, segments, used_templating=False): |
28 self.segments = segments | 27 self.segments = segments |
29 self.render_pass_info = render_pass_info | 28 self.used_templating = used_templating |
30 | 29 |
31 | 30 |
32 class RenderedLayout(object): | 31 class RenderedLayout(object): |
33 def __init__(self, content, render_pass_info): | 32 def __init__(self, content): |
34 self.content = content | 33 self.content = content |
35 self.render_pass_info = render_pass_info | |
36 | 34 |
37 | 35 |
38 class RenderedPage(object): | 36 class RenderedPage(object): |
39 def __init__(self, page, sub_num): | 37 def __init__(self, page, sub_num): |
40 self.page = page | 38 self.page = page |
41 self.sub_num = sub_num | 39 self.sub_num = sub_num |
42 self.data = None | 40 self.data = None |
43 self.content = None | 41 self.content = None |
44 self.render_info = [None, None] | 42 self.render_info = {} |
45 | 43 |
46 @property | 44 @property |
47 def app(self): | 45 def app(self): |
48 return self.page.app | 46 return self.page.app |
49 | 47 |
50 def copyRenderInfo(self): | 48 |
51 return copy.deepcopy(self.render_info) | 49 def create_render_info(): |
52 | 50 """ Creates a bag of rendering properties. It's a dictionary because |
53 | 51 it will be passed between workers during the bake process, and |
54 PASS_NONE = -1 | 52 saved to records. |
55 PASS_FORMATTING = 0 | 53 """ |
56 PASS_RENDERING = 1 | 54 return { |
57 | 55 'used_source_names': set(), |
58 | 56 'used_pagination': False, |
59 RENDER_PASSES = [PASS_FORMATTING, PASS_RENDERING] | 57 'pagination_has_more': False, |
60 | 58 'used_assets': False, |
61 | 59 } |
62 class RenderPassInfo(object): | |
63 def __init__(self): | |
64 self.used_source_names = set() | |
65 self.used_pagination = False | |
66 self.pagination_has_more = False | |
67 self.used_assets = False | |
68 self._custom_info = {} | |
69 | |
70 def setCustomInfo(self, key, info): | |
71 self._custom_info[key] = info | |
72 | |
73 def getCustomInfo(self, key, default=None): | |
74 return self._custom_info.get(key, default) | |
75 | 60 |
76 | 61 |
77 class RenderingContext(object): | 62 class RenderingContext(object): |
78 def __init__(self, page, *, sub_num=1, force_render=False): | 63 def __init__(self, page, *, sub_num=1, force_render=False): |
79 self.page = page | 64 self.page = page |
80 self.sub_num = sub_num | 65 self.sub_num = sub_num |
81 self.force_render = force_render | 66 self.force_render = force_render |
82 self.pagination_source = None | 67 self.pagination_source = None |
83 self.pagination_filter = None | 68 self.pagination_filter = None |
69 self.render_info = create_render_info() | |
84 self.custom_data = {} | 70 self.custom_data = {} |
85 self.render_passes = [None, None] # Same length as RENDER_PASSES | |
86 self._current_pass = PASS_NONE | |
87 | 71 |
88 @property | 72 @property |
89 def app(self): | 73 def app(self): |
90 return self.page.app | 74 return self.page.app |
91 | 75 |
92 @property | |
93 def current_pass_info(self): | |
94 if self._current_pass != PASS_NONE: | |
95 return self.render_passes[self._current_pass] | |
96 return None | |
97 | |
98 def setCurrentPass(self, rdr_pass): | |
99 if rdr_pass != PASS_NONE: | |
100 self.render_passes[rdr_pass] = RenderPassInfo() | |
101 self._current_pass = rdr_pass | |
102 | |
103 def setPagination(self, paginator): | 76 def setPagination(self, paginator): |
104 self._raiseIfNoCurrentPass() | 77 ri = self.render_info |
105 pass_info = self.current_pass_info | 78 if ri.get('used_pagination'): |
106 if pass_info.used_pagination: | |
107 raise Exception("Pagination has already been used.") | 79 raise Exception("Pagination has already been used.") |
108 assert paginator.is_loaded | 80 assert paginator.is_loaded |
109 pass_info.used_pagination = True | 81 ri['used_pagination'] = True |
110 pass_info.pagination_has_more = paginator.has_more | 82 ri['pagination_has_more'] = paginator.has_more |
111 self.addUsedSource(paginator._source) | 83 self.addUsedSource(paginator._source) |
112 | 84 |
113 def addUsedSource(self, source): | 85 def addUsedSource(self, source): |
114 self._raiseIfNoCurrentPass() | 86 ri = self.render_info |
115 pass_info = self.current_pass_info | 87 ri['used_source_names'].add(source.name) |
116 pass_info.used_source_names.add(source.name) | |
117 | |
118 def _raiseIfNoCurrentPass(self): | |
119 if self._current_pass == PASS_NONE: | |
120 raise Exception("No rendering pass is currently active.") | |
121 | 88 |
122 | 89 |
123 class RenderingContextStack(object): | 90 class RenderingContextStack(object): |
124 def __init__(self): | 91 def __init__(self): |
125 self._ctx_stack = [] | 92 self._ctx_stack = [] |
171 # Build the data for both segment and layout rendering. | 138 # Build the data for both segment and layout rendering. |
172 with stats.timerScope("BuildRenderData"): | 139 with stats.timerScope("BuildRenderData"): |
173 page_data = _build_render_data(ctx) | 140 page_data = _build_render_data(ctx) |
174 | 141 |
175 # Render content segments. | 142 # Render content segments. |
176 ctx.setCurrentPass(PASS_FORMATTING) | |
177 repo = env.rendered_segments_repository | 143 repo = env.rendered_segments_repository |
178 save_to_fs = True | 144 save_to_fs = True |
179 if env.fs_cache_only_for_main_page and not stack.is_main_ctx: | 145 if env.fs_cache_only_for_main_page and not stack.is_main_ctx: |
180 save_to_fs = False | 146 save_to_fs = False |
181 with stats.timerScope("PageRenderSegments"): | 147 with stats.timerScope("PageRenderSegments"): |
189 render_result = _do_render_page_segments(ctx, page_data) | 155 render_result = _do_render_page_segments(ctx, page_data) |
190 if repo: | 156 if repo: |
191 repo.put(page_uri, render_result, save_to_fs) | 157 repo.put(page_uri, render_result, save_to_fs) |
192 | 158 |
193 # Render layout. | 159 # Render layout. |
194 ctx.setCurrentPass(PASS_RENDERING) | |
195 layout_name = page.config.get('layout') | 160 layout_name = page.config.get('layout') |
196 if layout_name is None: | 161 if layout_name is None: |
197 layout_name = page.source.config.get( | 162 layout_name = page.source.config.get( |
198 'default_layout', 'default') | 163 'default_layout', 'default') |
199 null_names = ['', 'none', 'nil'] | 164 null_names = ['', 'none', 'nil'] |
204 with stats.timerScope("PageRenderLayout"): | 169 with stats.timerScope("PageRenderLayout"): |
205 layout_result = _do_render_layout( | 170 layout_result = _do_render_layout( |
206 layout_name, page, page_data) | 171 layout_name, page, page_data) |
207 else: | 172 else: |
208 layout_result = RenderedLayout( | 173 layout_result = RenderedLayout( |
209 render_result.segments['content'], None) | 174 render_result.segments['content']) |
210 | 175 |
211 rp = RenderedPage(page, ctx.sub_num) | 176 rp = RenderedPage(page, ctx.sub_num) |
212 rp.data = page_data | 177 rp.data = page_data |
213 rp.content = layout_result.content | 178 rp.content = layout_result.content |
214 rp.render_info[PASS_FORMATTING] = render_result.render_pass_info | 179 rp.render_info = ctx.render_info |
215 rp.render_info[PASS_RENDERING] = layout_result.render_pass_info | |
216 return rp | 180 return rp |
217 | 181 |
218 except AbortedSourceUseError: | 182 except AbortedSourceUseError: |
219 raise | 183 raise |
220 except Exception as ex: | 184 except Exception as ex: |
223 logger.exception(ex) | 187 logger.exception(ex) |
224 raise Exception("Error rendering page: %s" % | 188 raise Exception("Error rendering page: %s" % |
225 ctx.page.content_spec) from ex | 189 ctx.page.content_spec) from ex |
226 | 190 |
227 finally: | 191 finally: |
228 ctx.setCurrentPass(PASS_NONE) | |
229 stack.popCtx() | 192 stack.popCtx() |
230 | 193 |
231 | 194 |
232 def render_page_segments(ctx): | 195 def render_page_segments(ctx): |
233 env = ctx.app.env | 196 env = ctx.app.env |
246 | 209 |
247 page = ctx.page | 210 page = ctx.page |
248 page_uri = page.getUri(ctx.sub_num) | 211 page_uri = page.getUri(ctx.sub_num) |
249 | 212 |
250 try: | 213 try: |
251 ctx.setCurrentPass(PASS_FORMATTING) | |
252 repo = env.rendered_segments_repository | 214 repo = env.rendered_segments_repository |
253 | 215 |
254 save_to_fs = True | 216 save_to_fs = True |
255 if env.fs_cache_only_for_main_page and not stack.is_main_ctx: | 217 if env.fs_cache_only_for_main_page and not stack.is_main_ctx: |
256 save_to_fs = False | 218 save_to_fs = False |
265 else: | 227 else: |
266 render_result = _do_render_page_segments_from_ctx(ctx) | 228 render_result = _do_render_page_segments_from_ctx(ctx) |
267 if repo: | 229 if repo: |
268 repo.put(page_uri, render_result, save_to_fs) | 230 repo.put(page_uri, render_result, save_to_fs) |
269 finally: | 231 finally: |
270 ctx.setCurrentPass(PASS_NONE) | |
271 stack.popCtx() | 232 stack.popCtx() |
272 | 233 |
273 return render_result | 234 return render_result |
274 | 235 |
275 | 236 |
295 engine_name = page.config.get('template_engine') | 256 engine_name = page.config.get('template_engine') |
296 format_name = page.config.get('format') | 257 format_name = page.config.get('format') |
297 | 258 |
298 engine = get_template_engine(app, engine_name) | 259 engine = get_template_engine(app, engine_name) |
299 | 260 |
261 used_templating = False | |
300 formatted_segments = {} | 262 formatted_segments = {} |
301 for seg_name, seg in page.segments.items(): | 263 for seg_name, seg in page.segments.items(): |
302 try: | 264 try: |
303 with app.env.stats.timerScope( | 265 with app.env.stats.timerScope( |
304 engine.__class__.__name__ + '_segment'): | 266 engine.__class__.__name__ + '_segment'): |
305 seg_text = engine.renderSegment( | 267 seg_text, was_rendered = engine.renderSegment( |
306 page.content_spec, seg, page_data) | 268 page.content_spec, seg, page_data) |
269 if was_rendered: | |
270 used_templating = True | |
307 except TemplatingError as err: | 271 except TemplatingError as err: |
308 err.lineno += seg.line | 272 err.lineno += seg.line |
309 raise err | 273 raise err |
310 | 274 |
311 seg_format = seg.fmt or format_name | 275 seg_format = seg.fmt or format_name |
317 if m: | 281 if m: |
318 offset = m.start() | 282 offset = m.start() |
319 content_abstract = seg_text[:offset] | 283 content_abstract = seg_text[:offset] |
320 formatted_segments['content.abstract'] = content_abstract | 284 formatted_segments['content.abstract'] = content_abstract |
321 | 285 |
322 pass_info = ctx.render_passes[PASS_FORMATTING] | 286 res = RenderedSegments(formatted_segments, used_templating) |
323 res = RenderedSegments(formatted_segments, pass_info) | |
324 | 287 |
325 app.env.stats.stepCounter('PageRenderSegments') | 288 app.env.stats.stepCounter('PageRenderSegments') |
326 | 289 |
327 return res | 290 return res |
328 | 291 |
353 logger.exception(ex) | 316 logger.exception(ex) |
354 msg = "Can't find template for page: %s\n" % page.content_item.spec | 317 msg = "Can't find template for page: %s\n" % page.content_item.spec |
355 msg += "Looked for: %s" % ', '.join(full_names) | 318 msg += "Looked for: %s" % ', '.join(full_names) |
356 raise Exception(msg) from ex | 319 raise Exception(msg) from ex |
357 | 320 |
358 pass_info = cur_ctx.render_passes[PASS_RENDERING] | 321 res = RenderedLayout(output) |
359 res = RenderedLayout(output, pass_info) | |
360 | 322 |
361 app.env.stats.stepCounter('PageRenderLayout') | 323 app.env.stats.stepCounter('PageRenderLayout') |
362 | 324 |
363 return res | 325 return res |
364 | 326 |