comparison piecrust/rendering.py @ 415:0e9a94b7fdfa

bake: Improve bake record information. * Store things in the bake record that require less interaction between the master process and the workers. For instance, don't store the paginator object in the render pass info -- instead, just store whether pagination was used, and whether it had more items. * Simplify information passing between workers and bake passes by saving the rendering info to the JSON cache. This means the "render first sub" job doesn't have to return anything except errors now. * Add more performance counter info.
author Ludovic Chabant <ludovic@chabant.com>
date Sat, 20 Jun 2015 19:23:16 -0700
parents e7b865f8f335
children 6801ad5aa1d4
comparison
equal deleted inserted replaced
414:c4b3a7fd2f87 415:0e9a94b7fdfa
1 import re 1 import re
2 import os.path 2 import os.path
3 import copy
3 import logging 4 import logging
4 from werkzeug.utils import cached_property 5 from werkzeug.utils import cached_property
5 from piecrust.data.builder import ( 6 from piecrust.data.builder import (
6 DataBuildingContext, build_page_data, build_layout_data) 7 DataBuildingContext, build_page_data, build_layout_data)
7 from piecrust.data.filters import ( 8 from piecrust.data.filters import (
38 39
39 def __getattr__(self, name): 40 def __getattr__(self, name):
40 return getattr(self.page, name) 41 return getattr(self.page, name)
41 42
42 43
44 class RenderedSegments(object):
45 def __init__(self, segments, render_pass_info):
46 self.segments = segments
47 self.render_pass_info = render_pass_info
48
49
50 class RenderedLayout(object):
51 def __init__(self, content, render_pass_info):
52 self.content = content
53 self.render_pass_info = render_pass_info
54
55
43 class RenderedPage(object): 56 class RenderedPage(object):
44 def __init__(self, page, uri, num=1): 57 def __init__(self, page, uri, num=1):
45 self.page = page 58 self.page = page
46 self.uri = uri 59 self.uri = uri
47 self.num = num 60 self.num = num
48 self.data = None 61 self.data = None
49 self.content = None 62 self.content = None
63 self.render_info = None
50 64
51 @property 65 @property
52 def app(self): 66 def app(self):
53 return self.page.app 67 return self.page.app
68
69 def copyRenderInfo(self):
70 return copy.deepcopy(self.render_info)
54 71
55 72
56 PASS_NONE = 0 73 PASS_NONE = 0
57 PASS_FORMATTING = 1 74 PASS_FORMATTING = 1
58 PASS_RENDERING = 2 75 PASS_RENDERING = 2
63 80
64 class RenderPassInfo(object): 81 class RenderPassInfo(object):
65 def __init__(self): 82 def __init__(self):
66 self.used_source_names = set() 83 self.used_source_names = set()
67 self.used_taxonomy_terms = set() 84 self.used_taxonomy_terms = set()
85 self.used_pagination = False
86 self.pagination_has_more = False
87 self.used_assets = False
88
89 def merge(self, other):
90 self.used_source_names |= other.used_source_names
91 self.used_taxonomy_terms |= other.used_taxonomy_terms
92 self.used_pagination = self.used_pagination or other.used_pagination
93 self.pagination_has_more = (self.pagination_has_more or
94 other.pagination_has_more)
95 self.used_assets = self.used_assets or other.used_assets
96
97 def _toJson(self):
98 data = {
99 'used_source_names': list(self.used_source_names),
100 'used_taxonomy_terms': list(self.used_taxonomy_terms),
101 'used_pagination': self.used_pagination,
102 'pagination_has_more': self.pagination_has_more,
103 'used_assets': self.used_assets}
104 return data
105
106 @staticmethod
107 def _fromJson(data):
108 assert data is not None
109 rpi = RenderPassInfo()
110 rpi.used_source_names = set(data['used_source_names'])
111 for i in data['used_taxonomy_terms']:
112 terms = i[2]
113 if isinstance(terms, list):
114 terms = tuple(terms)
115 rpi.used_taxonomy_terms.add((i[0], i[1], terms))
116 rpi.used_pagination = data['used_pagination']
117 rpi.pagination_has_more = data['pagination_has_more']
118 rpi.used_assets = data['used_assets']
119 return rpi
68 120
69 121
70 class PageRenderingContext(object): 122 class PageRenderingContext(object):
71 def __init__(self, qualified_page, page_num=1, force_render=False): 123 def __init__(self, qualified_page, page_num=1, force_render=False):
72 self.page = qualified_page 124 self.page = qualified_page
76 self.pagination_filter = None 128 self.pagination_filter = None
77 self.custom_data = None 129 self.custom_data = None
78 self._current_pass = PASS_NONE 130 self._current_pass = PASS_NONE
79 131
80 self.render_passes = {} 132 self.render_passes = {}
81 self.used_pagination = None
82 self.used_assets = None
83 133
84 @property 134 @property
85 def app(self): 135 def app(self):
86 return self.page.app 136 return self.page.app
87 137
90 return self.page.source_metadata 140 return self.page.source_metadata
91 141
92 @cached_property 142 @cached_property
93 def uri(self): 143 def uri(self):
94 return self.page.getUri(self.page_num) 144 return self.page.getUri(self.page_num)
95
96 @property
97 def pagination_has_more(self):
98 if self.used_pagination is None:
99 return False
100 return self.used_pagination.has_more
101 145
102 @property 146 @property
103 def current_pass_info(self): 147 def current_pass_info(self):
104 return self.render_passes.get(self._current_pass) 148 return self.render_passes.get(self._current_pass)
105 149
108 self.render_passes.setdefault(rdr_pass, RenderPassInfo()) 152 self.render_passes.setdefault(rdr_pass, RenderPassInfo())
109 self._current_pass = rdr_pass 153 self._current_pass = rdr_pass
110 154
111 def setPagination(self, paginator): 155 def setPagination(self, paginator):
112 self._raiseIfNoCurrentPass() 156 self._raiseIfNoCurrentPass()
113 if self.used_pagination is not None: 157 pass_info = self.current_pass_info
158 if pass_info.used_pagination:
114 raise Exception("Pagination has already been used.") 159 raise Exception("Pagination has already been used.")
115 self.used_pagination = paginator 160 assert paginator.is_loaded
161 pass_info.used_pagination = True
162 pass_info.pagination_has_more = paginator.has_more
116 self.addUsedSource(paginator._source) 163 self.addUsedSource(paginator._source)
117 164
118 def addUsedSource(self, source): 165 def addUsedSource(self, source):
119 self._raiseIfNoCurrentPass() 166 self._raiseIfNoCurrentPass()
120 if isinstance(source, PageSource): 167 if isinstance(source, PageSource):
121 pass_info = self.render_passes[self._current_pass] 168 pass_info = self.current_pass_info
122 pass_info.used_source_names.add(source.name) 169 pass_info.used_source_names.add(source.name)
123 170
124 def setTaxonomyFilter(self, taxonomy, term_value): 171 def setTaxonomyFilter(self, taxonomy, term_value):
125 is_combination = isinstance(term_value, tuple) 172 is_combination = isinstance(term_value, tuple)
126 flt = PaginationFilter(value_accessor=page_value_accessor) 173 flt = PaginationFilter(value_accessor=page_value_accessor)
148 195
149 def render_page(ctx): 196 def render_page(ctx):
150 eis = ctx.app.env.exec_info_stack 197 eis = ctx.app.env.exec_info_stack
151 eis.pushPage(ctx.page, ctx) 198 eis.pushPage(ctx.page, ctx)
152 try: 199 try:
153 page = ctx.page
154
155 # Build the data for both segment and layout rendering. 200 # Build the data for both segment and layout rendering.
156 data_ctx = DataBuildingContext(page, page_num=ctx.page_num) 201 page_data = _build_render_data(ctx)
157 data_ctx.pagination_source = ctx.pagination_source
158 data_ctx.pagination_filter = ctx.pagination_filter
159 page_data = build_page_data(data_ctx)
160 if ctx.custom_data:
161 page_data.update(ctx.custom_data)
162 202
163 # Render content segments. 203 # Render content segments.
164 ctx.setCurrentPass(PASS_FORMATTING) 204 ctx.setCurrentPass(PASS_FORMATTING)
165 repo = ctx.app.env.rendered_segments_repository 205 repo = ctx.app.env.rendered_segments_repository
166 save_to_fs = True 206 save_to_fs = True
167 if ctx.app.env.fs_cache_only_for_main_page and not eis.is_main_page: 207 if ctx.app.env.fs_cache_only_for_main_page and not eis.is_main_page:
168 save_to_fs = False 208 save_to_fs = False
169 if repo and not ctx.force_render: 209 if repo and not ctx.force_render:
170 contents = repo.get( 210 render_result = repo.get(
171 ctx.uri, 211 ctx.uri,
172 lambda: _do_render_page_segments(page, page_data), 212 lambda: _do_render_page_segments(ctx.page, page_data),
173 fs_cache_time=page.path_mtime, 213 fs_cache_time=ctx.page.path_mtime,
174 save_to_fs=save_to_fs) 214 save_to_fs=save_to_fs)
175 else: 215 else:
176 contents = _do_render_page_segments(page, page_data) 216 render_result = _do_render_page_segments(ctx.page, page_data)
177 if repo: 217 if repo:
178 repo.put(ctx.uri, contents, save_to_fs) 218 repo.put(ctx.uri, render_result, save_to_fs)
179 219
180 # Render layout. 220 # Render layout.
221 page = ctx.page
181 ctx.setCurrentPass(PASS_RENDERING) 222 ctx.setCurrentPass(PASS_RENDERING)
182 layout_name = page.config.get('layout') 223 layout_name = page.config.get('layout')
183 if layout_name is None: 224 if layout_name is None:
184 layout_name = page.source.config.get('default_layout', 'default') 225 layout_name = page.source.config.get('default_layout', 'default')
185 null_names = ['', 'none', 'nil'] 226 null_names = ['', 'none', 'nil']
186 if layout_name not in null_names: 227 if layout_name not in null_names:
187 build_layout_data(page, page_data, contents) 228 build_layout_data(page, page_data, render_result['segments'])
188 output = render_layout(layout_name, page, page_data) 229 layout_result = _do_render_layout(layout_name, page, page_data)
189 else: 230 else:
190 output = contents['content'] 231 layout_result = {
232 'content': render_result['segments']['content'],
233 'pass_info': None}
191 234
192 rp = RenderedPage(page, ctx.uri, ctx.page_num) 235 rp = RenderedPage(page, ctx.uri, ctx.page_num)
193 rp.data = page_data 236 rp.data = page_data
194 rp.content = output 237 rp.content = layout_result['content']
238 rp.render_info = {
239 PASS_FORMATTING: RenderPassInfo._fromJson(
240 render_result['pass_info'])}
241 if layout_result['pass_info'] is not None:
242 rp.render_info[PASS_RENDERING] = RenderPassInfo._fromJson(
243 layout_result['pass_info'])
195 return rp 244 return rp
196 finally: 245 finally:
197 ctx.setCurrentPass(PASS_NONE) 246 ctx.setCurrentPass(PASS_NONE)
198 eis.popPage() 247 eis.popPage()
199 248
200 249
201 def render_page_segments(ctx): 250 def render_page_segments(ctx):
202 repo = ctx.app.env.rendered_segments_repository
203 if repo:
204 cache_key = ctx.uri
205 return repo.get(
206 cache_key,
207 lambda: _do_render_page_segments_from_ctx(ctx),
208 fs_cache_time=ctx.page.path_mtime)
209
210 return _do_render_page_segments_from_ctx(ctx)
211
212
213 def _do_render_page_segments_from_ctx(ctx):
214 eis = ctx.app.env.exec_info_stack 251 eis = ctx.app.env.exec_info_stack
215 eis.pushPage(ctx.page, ctx) 252 eis.pushPage(ctx.page, ctx)
216 ctx.setCurrentPass(PASS_FORMATTING)
217 try: 253 try:
218 data_ctx = DataBuildingContext(ctx.page, page_num=ctx.page_num) 254 page_data = _build_render_data(ctx)
219 page_data = build_page_data(data_ctx) 255
220 return _do_render_page_segments(ctx.page, page_data) 256 ctx.setCurrentPass(PASS_FORMATTING)
257 repo = ctx.app.env.rendered_segments_repository
258 save_to_fs = True
259 if ctx.app.env.fs_cache_only_for_main_page and not eis.is_main_page:
260 save_to_fs = False
261 if repo and not ctx.force_render:
262 render_result = repo.get(
263 ctx.uri,
264 lambda: _do_render_page_segments(ctx.page, page_data),
265 fs_cache_time=ctx.page.path_mtime,
266 save_to_fs=save_to_fs)
267 else:
268 render_result = _do_render_page_segments(ctx.page, page_data)
269 if repo:
270 repo.put(ctx.uri, render_result, save_to_fs)
221 finally: 271 finally:
222 ctx.setCurrentPass(PASS_NONE) 272 ctx.setCurrentPass(PASS_NONE)
223 eis.popPage() 273 eis.popPage()
224 274
275 rs = RenderedSegments(
276 render_result['segments'],
277 RenderPassInfo._fromJson(render_result['pass_info']))
278 return rs
279
280
281 def _build_render_data(ctx):
282 data_ctx = DataBuildingContext(ctx.page, page_num=ctx.page_num)
283 data_ctx.pagination_source = ctx.pagination_source
284 data_ctx.pagination_filter = ctx.pagination_filter
285 page_data = build_page_data(data_ctx)
286 if ctx.custom_data:
287 page_data.update(ctx.custom_data)
288 return page_data
289
225 290
226 def _do_render_page_segments(page, page_data): 291 def _do_render_page_segments(page, page_data):
227 app = page.app 292 app = page.app
293
294 cpi = app.env.exec_info_stack.current_page_info
295 assert cpi is not None
296 assert cpi.page == page
297
228 engine_name = page.config.get('template_engine') 298 engine_name = page.config.get('template_engine')
229 format_name = page.config.get('format') 299 format_name = page.config.get('format')
230 300
231 engine = get_template_engine(app, engine_name) 301 engine = get_template_engine(app, engine_name)
232 302
233 formatted_content = {} 303 formatted_segments = {}
234 for seg_name, seg in page.raw_content.items(): 304 for seg_name, seg in page.raw_content.items():
235 seg_text = '' 305 seg_text = ''
236 for seg_part in seg.parts: 306 for seg_part in seg.parts:
237 part_format = seg_part.fmt or format_name 307 part_format = seg_part.fmt or format_name
238 try: 308 try:
244 err.lineno += seg_part.line 314 err.lineno += seg_part.line
245 raise err 315 raise err
246 316
247 part_text = format_text(app, part_format, part_text) 317 part_text = format_text(app, part_format, part_text)
248 seg_text += part_text 318 seg_text += part_text
249 formatted_content[seg_name] = seg_text 319 formatted_segments[seg_name] = seg_text
250 320
251 if seg_name == 'content': 321 if seg_name == 'content':
252 m = content_abstract_re.search(seg_text) 322 m = content_abstract_re.search(seg_text)
253 if m: 323 if m:
254 offset = m.start() 324 offset = m.start()
255 content_abstract = seg_text[:offset] 325 content_abstract = seg_text[:offset]
256 formatted_content['content.abstract'] = content_abstract 326 formatted_segments['content.abstract'] = content_abstract
257 327
258 return formatted_content 328 pass_info = cpi.render_ctx.render_passes.get(PASS_FORMATTING)
259 329 res = {
260 330 'segments': formatted_segments,
261 def render_layout(layout_name, page, layout_data): 331 'pass_info': pass_info._toJson()}
332 return res
333
334
335 def _do_render_layout(layout_name, page, layout_data):
336 cpi = page.app.env.exec_info_stack.current_page_info
337 assert cpi is not None
338 assert cpi.page == page
339
262 names = layout_name.split(',') 340 names = layout_name.split(',')
263 default_template_engine = get_template_engine(page.app, None) 341 default_template_engine = get_template_engine(page.app, None)
264 default_exts = ['.' + e.lstrip('.') 342 default_exts = ['.' + e.lstrip('.')
265 for e in default_template_engine.EXTENSIONS] 343 for e in default_template_engine.EXTENSIONS]
266 full_names = [] 344 full_names = []
279 output = engine.renderFile(full_names, layout_data) 357 output = engine.renderFile(full_names, layout_data)
280 except TemplateNotFoundError as ex: 358 except TemplateNotFoundError as ex:
281 msg = "Can't find template for page: %s\n" % page.path 359 msg = "Can't find template for page: %s\n" % page.path
282 msg += "Looked for: %s" % ', '.join(full_names) 360 msg += "Looked for: %s" % ', '.join(full_names)
283 raise Exception(msg) from ex 361 raise Exception(msg) from ex
284 return output 362
363 pass_info = cpi.render_ctx.render_passes.get(PASS_RENDERING)
364 res = {'content': output, 'pass_info': pass_info._toJson()}
365 return res
285 366
286 367
287 def get_template_engine(app, engine_name): 368 def get_template_engine(app, engine_name):
288 if engine_name == 'html': 369 if engine_name == 'html':
289 engine_name = None 370 engine_name = None