comparison piecrust/baking/records.py @ 411:e7b865f8f335

bake: Enable multiprocess baking. Baking is now done by running a worker per CPU, and sending jobs to them. This changes several things across the codebase: * Ability to not cache things related to pages other than the 'main' page (i.e. the page at the bottom of the execution stack). * Decouple the baking process from the bake records, so only the main process keeps track (and modifies) the bake record. * Remove the need for 'batch page getters' and loading a page directly from the page factories. There are various smaller changes too included here, including support for scope performance timers that are saved with the bake record and can be printed out to the console. Yes I got carried away. For testing, the in-memory 'mock' file-system doesn't work anymore, since we're spawning processes, so this is replaced by a 'tmpfs' file-system which is saved in temporary files on disk and deleted after tests have run.
author Ludovic Chabant <ludovic@chabant.com>
date Fri, 12 Jun 2015 17:09:19 -0700
parents c12ee6936b8c
children 0e9a94b7fdfa
comparison
equal deleted inserted replaced
410:d1a472464e57 411:e7b865f8f335
1 import copy 1 import copy
2 import os.path 2 import os.path
3 import hashlib
3 import logging 4 import logging
4 from piecrust.records import Record, TransitionalRecord 5 from piecrust.records import Record, TransitionalRecord
5 6
6 7
7 logger = logging.getLogger(__name__) 8 logger = logging.getLogger(__name__)
8 9
9 10
10 def _get_transition_key(source_name, rel_path, taxonomy_info=None): 11 def _get_transition_key(path, taxonomy_info=None):
11 key = '%s:%s' % (source_name, rel_path) 12 key = path
12 if taxonomy_info: 13 if taxonomy_info:
13 taxonomy_name, taxonomy_term, taxonomy_source_name = taxonomy_info 14 key += '+%s:%s=' % (taxonomy_info.source_name,
14 key += ';%s:%s=' % (taxonomy_source_name, taxonomy_name) 15 taxonomy_info.taxonomy_name)
15 if isinstance(taxonomy_term, tuple): 16 if isinstance(taxonomy_info.term, tuple):
16 key += '/'.join(taxonomy_term) 17 key += '/'.join(taxonomy_info.term)
17 else: 18 else:
18 key += taxonomy_term 19 key += taxonomy_info.term
19 return key 20 return hashlib.md5(key.encode('utf8')).hexdigest()
20 21
21 22
22 class BakeRecord(Record): 23 class BakeRecord(Record):
23 RECORD_VERSION = 12 24 RECORD_VERSION = 14
24 25
25 def __init__(self): 26 def __init__(self):
26 super(BakeRecord, self).__init__() 27 super(BakeRecord, self).__init__()
27 self.out_dir = None 28 self.out_dir = None
28 self.bake_time = None 29 self.bake_time = None
30 self.timers = None
29 self.success = True 31 self.success = True
30 32
31 33
32 class BakeRecordPassInfo(object): 34 class BakePassInfo(object):
33 def __init__(self): 35 def __init__(self):
34 self.used_source_names = set() 36 self.used_source_names = set()
35 self.used_taxonomy_terms = set() 37 self.used_taxonomy_terms = set()
36 38
37 39
38 class BakeRecordSubPageEntry(object): 40 class SubPageBakeInfo(object):
39 FLAG_NONE = 0 41 FLAG_NONE = 0
40 FLAG_BAKED = 2**0 42 FLAG_BAKED = 2**0
41 FLAG_FORCED_BY_SOURCE = 2**1 43 FLAG_FORCED_BY_SOURCE = 2**1
42 FLAG_FORCED_BY_NO_PREVIOUS = 2**2 44 FLAG_FORCED_BY_NO_PREVIOUS = 2**2
43 FLAG_FORCED_BY_PREVIOUS_ERRORS = 2**3 45 FLAG_FORCED_BY_PREVIOUS_ERRORS = 2**3
66 for p, pinfo in self.render_passes.items(): 68 for p, pinfo in self.render_passes.items():
67 if p not in other.render_passes: 69 if p not in other.render_passes:
68 other.render_passes[p] = copy.deepcopy(pinfo) 70 other.render_passes[p] = copy.deepcopy(pinfo)
69 71
70 72
71 class BakeRecordPageEntry(object): 73 class PageBakeInfo(object):
74 def __init__(self):
75 self.subs = []
76 self.assets = []
77
78
79 class FirstRenderInfo(object):
80 def __init__(self):
81 self.assets = []
82 self.used_pagination = False
83 self.pagination_has_more = False
84
85
86 class TaxonomyInfo(object):
87 def __init__(self, taxonomy_name, source_name, term):
88 self.taxonomy_name = taxonomy_name
89 self.source_name = source_name
90 self.term = term
91
92
93 class BakeRecordEntry(object):
72 """ An entry in the bake record. 94 """ An entry in the bake record.
73 95
74 The `taxonomy_info` attribute should be a tuple of the form: 96 The `taxonomy_info` attribute should be a tuple of the form:
75 (taxonomy name, term, source name) 97 (taxonomy name, term, source name)
76 """ 98 """
77 FLAG_NONE = 0 99 FLAG_NONE = 0
78 FLAG_NEW = 2**0 100 FLAG_NEW = 2**0
79 FLAG_SOURCE_MODIFIED = 2**1 101 FLAG_SOURCE_MODIFIED = 2**1
80 FLAG_OVERRIDEN = 2**2 102 FLAG_OVERRIDEN = 2**2
81 103
82 def __init__(self, source_name, rel_path, path, taxonomy_info=None): 104 def __init__(self, source_name, path, taxonomy_info=None):
83 self.source_name = source_name 105 self.source_name = source_name
84 self.rel_path = rel_path
85 self.path = path 106 self.path = path
86 self.taxonomy_info = taxonomy_info 107 self.taxonomy_info = taxonomy_info
87 self.flags = self.FLAG_NONE 108 self.flags = self.FLAG_NONE
88 self.config = None 109 self.config = None
89 self.subs = []
90 self.assets = []
91 self.errors = [] 110 self.errors = []
111 self.bake_info = None
112 self.first_render_info = None
92 113
93 @property 114 @property
94 def path_mtime(self): 115 def path_mtime(self):
95 return os.path.getmtime(self.path) 116 return os.path.getmtime(self.path)
96 117
98 def was_overriden(self): 119 def was_overriden(self):
99 return (self.flags & self.FLAG_OVERRIDEN) != 0 120 return (self.flags & self.FLAG_OVERRIDEN) != 0
100 121
101 @property 122 @property
102 def num_subs(self): 123 def num_subs(self):
103 return len(self.subs) 124 if self.bake_info is None:
125 return 0
126 return len(self.bake_info.subs)
104 127
105 @property 128 @property
106 def was_any_sub_baked(self): 129 def was_any_sub_baked(self):
107 for o in self.subs: 130 if self.bake_info is not None:
108 if o.was_baked: 131 for o in self.bake_info.subs:
109 return True 132 if o.was_baked:
133 return True
110 return False 134 return False
111 135
136 @property
137 def subs(self):
138 if self.bake_info is not None:
139 return self.bake_info.subs
140 return []
141
142 @property
143 def has_any_error(self):
144 if len(self.errors) > 0:
145 return True
146 if self.bake_info is not None:
147 for o in self.bake_info.subs:
148 if len(o.errors) > 0:
149 return True
150 return False
151
112 def getSub(self, sub_index): 152 def getSub(self, sub_index):
113 return self.subs[sub_index - 1] 153 if self.bake_info is None:
154 raise Exception("No bake info available on this entry.")
155 return self.bake_info.subs[sub_index - 1]
114 156
115 def getAllErrors(self): 157 def getAllErrors(self):
116 yield from self.errors 158 yield from self.errors
117 for o in self.subs: 159 if self.bake_info is not None:
118 yield from o.errors 160 for o in self.bake_info.subs:
161 yield from o.errors
119 162
120 def getAllUsedSourceNames(self): 163 def getAllUsedSourceNames(self):
121 res = set() 164 res = set()
122 for o in self.subs: 165 if self.bake_info is not None:
123 for p, pinfo in o.render_passes.items(): 166 for o in self.bake_info.subs:
124 res |= pinfo.used_source_names 167 for p, pinfo in o.render_passes.items():
168 res |= pinfo.used_source_names
125 return res 169 return res
126 170
127 def getAllUsedTaxonomyTerms(self): 171 def getAllUsedTaxonomyTerms(self):
128 res = set() 172 res = set()
129 for o in self.subs: 173 if self.bake_info is not None:
130 for p, pinfo in o.render_passes.items(): 174 for o in self.bake_info.subs:
131 res |= pinfo.used_taxonomy_terms 175 for p, pinfo in o.render_passes.items():
176 res |= pinfo.used_taxonomy_terms
132 return res 177 return res
133 178
134 179
135 class TransitionalBakeRecord(TransitionalRecord): 180 class TransitionalBakeRecord(TransitionalRecord):
136 def __init__(self, previous_path=None): 181 def __init__(self, previous_path=None):
139 self.dirty_source_names = set() 184 self.dirty_source_names = set()
140 185
141 def addEntry(self, entry): 186 def addEntry(self, entry):
142 if (self.previous.bake_time and 187 if (self.previous.bake_time and
143 entry.path_mtime >= self.previous.bake_time): 188 entry.path_mtime >= self.previous.bake_time):
144 entry.flags |= BakeRecordPageEntry.FLAG_SOURCE_MODIFIED 189 entry.flags |= BakeRecordEntry.FLAG_SOURCE_MODIFIED
145 self.dirty_source_names.add(entry.source_name) 190 self.dirty_source_names.add(entry.source_name)
146 super(TransitionalBakeRecord, self).addEntry(entry) 191 super(TransitionalBakeRecord, self).addEntry(entry)
147 192
148 def getTransitionKey(self, entry): 193 def getTransitionKey(self, entry):
149 return _get_transition_key(entry.source_name, entry.rel_path, 194 return _get_transition_key(entry.path, entry.taxonomy_info)
150 entry.taxonomy_info) 195
151 196 def getPreviousAndCurrentEntries(self, path, taxonomy_info=None):
152 def getOverrideEntry(self, factory, uri): 197 key = _get_transition_key(path, taxonomy_info)
198 pair = self.transitions.get(key)
199 return pair
200
201 def getOverrideEntry(self, path, uri):
153 for pair in self.transitions.values(): 202 for pair in self.transitions.values():
154 cur = pair[1] 203 cur = pair[1]
155 if (cur and 204 if cur and cur.path != path:
156 (cur.source_name != factory.source.name or 205 for o in cur.subs:
157 cur.rel_path != factory.rel_path)): 206 if o.out_uri == uri:
158 for o in cur.subs: 207 return cur
159 if o.out_uri == uri:
160 return cur
161 return None 208 return None
162 209
163 def getPreviousEntry(self, source_name, rel_path, taxonomy_info=None): 210 def getPreviousEntry(self, path, taxonomy_info=None):
164 key = _get_transition_key(source_name, rel_path, taxonomy_info) 211 pair = self.getPreviousAndCurrentEntries(path, taxonomy_info)
165 pair = self.transitions.get(key)
166 if pair is not None: 212 if pair is not None:
167 return pair[0] 213 return pair[0]
168 return None 214 return None
169 215
216 def getCurrentEntry(self, path, taxonomy_info=None):
217 pair = self.getPreviousAndCurrentEntries(path, taxonomy_info)
218 if pair is not None:
219 return pair[1]
220 return None
221
170 def collapseEntry(self, prev_entry): 222 def collapseEntry(self, prev_entry):
171 cur_entry = copy.deepcopy(prev_entry) 223 cur_entry = copy.deepcopy(prev_entry)
172 cur_entry.flags = BakeRecordPageEntry.FLAG_NONE 224 cur_entry.flags = BakeRecordEntry.FLAG_NONE
173 for o in cur_entry.subs: 225 for o in cur_entry.subs:
174 o.flags = BakeRecordSubPageEntry.FLAG_NONE 226 o.flags = SubPageBakeInfo.FLAG_NONE
175 self.addEntry(cur_entry) 227 self.addEntry(cur_entry)
176 228
177 def getDeletions(self): 229 def getDeletions(self):
178 for prev, cur in self.transitions.values(): 230 for prev, cur in self.transitions.values():
179 if prev and not cur: 231 if prev and not cur:
185 diff = set(prev_out_paths) - set(cur_out_paths) 237 diff = set(prev_out_paths) - set(cur_out_paths)
186 for p in diff: 238 for p in diff:
187 yield (p, 'source file changed outputs') 239 yield (p, 'source file changed outputs')
188 240
189 def _onNewEntryAdded(self, entry): 241 def _onNewEntryAdded(self, entry):
190 entry.flags |= BakeRecordPageEntry.FLAG_NEW 242 entry.flags |= BakeRecordEntry.FLAG_NEW
191 243