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