Merge lp:~wgrant/launchpad/tm-suggest-constant into lp:launchpad

Proposed by William Grant
Status: Merged
Merged at revision: 17086
Proposed branch: lp:~wgrant/launchpad/tm-suggest-constant
Merge into: lp:launchpad
Diff against target: 374 lines (+211/-30)
6 files modified
lib/lp/translations/browser/tests/test_pofile_view.py (+71/-0)
lib/lp/translations/browser/translationmessage.py (+25/-26)
lib/lp/translations/interfaces/potemplate.py (+3/-0)
lib/lp/translations/interfaces/translationmessage.py (+22/-0)
lib/lp/translations/model/potemplate.py (+8/-0)
lib/lp/translations/model/translationmessage.py (+82/-4)
To merge this branch: bzr merge lp:~wgrant/launchpad/tm-suggest-constant
Reviewer Review Type Date Requested Status
Celso Providelo (community) Approve
Review via email: mp+225260@code.launchpad.net

Commit message

Preload suggestion details in POFile:+translate to eliminate per-suggestion queries. But there are still lots of per-message queries.

Description of the change

Drop POFile:+translate from O(messages + suggestions) queries to O(messages), by doing lots of pretty boring preloading. The only thing with significant complexity is preloadPOFilesAndSequences, which reimplements getOnePOFile in a bulk manner.

We still do the suggestion calculation inside the per-message subviews, so the preloading is done once for each message. But it's a good step in reducing the query count of this page, and we can pull the suggestion calculation and preloading into the upper view later.

I've added a query count test for POFile:+translate, which tests all the preloading I've done so far. It doesn't currently add new messages, just new suggestions, since extra messages still generate extra queries.

To post a comment you must log in.
Revision history for this message
Celso Providelo (cprov) wrote :

Nice change, introducing preloadPOFilesAndSequence really solves the problem for loading suggestions for a given message.

We can land it ASAP.

(thinking out loud here, feel free to ignore me)

Although, I am not seeing a clear path for going further and pre-loading this for multiple messages. Maybe we won't need it (this step will bring enough benefits) or we could try to investigate something at the model level for making it possible.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/translations/browser/tests/test_pofile_view.py'
2--- lib/lp/translations/browser/tests/test_pofile_view.py 2014-06-10 11:25:51 +0000
3+++ lib/lp/translations/browser/tests/test_pofile_view.py 2014-07-02 06:12:24 +0000
4@@ -3,20 +3,91 @@
5
6 __metaclass__ = type
7
8+from storm.store import Store
9+from testtools.matchers import Equals
10+from zope.component import getUtility
11+
12 from lp.app.errors import UnexpectedFormData
13+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
14+from lp.services.webapp.interfaces import ILaunchBag
15 from lp.services.webapp.servers import LaunchpadTestRequest
16 from lp.testing import (
17 BrowserTestCase,
18 login,
19+ login_person,
20 person_logged_in,
21+ record_two_runs,
22 TestCaseWithFactory,
23 )
24 from lp.testing.layers import (
25 DatabaseFunctionalLayer,
26 ZopelessDatabaseLayer,
27 )
28+from lp.testing.matchers import HasQueryCount
29+from lp.testing.views import create_initialized_view
30 from lp.translations.browser.pofile import POFileTranslateView
31 from lp.translations.enums import TranslationPermission
32+from lp.translations.interfaces.potemplate import IPOTemplateSet
33+from lp.translations.interfaces.translationsperson import ITranslationsPerson
34+from lp.translations.model.pofiletranslator import POFileTranslator
35+
36+
37+class TestQueryCount(TestCaseWithFactory):
38+
39+ layer = DatabaseFunctionalLayer
40+
41+ def test_query_count(self):
42+ person = self.factory.makePerson()
43+ login_person(person)
44+ ITranslationsPerson(person).translations_relicensing_agreement = True
45+ product = self.factory.makeProduct(owner=person)
46+ product.translationpermission = TranslationPermission.OPEN
47+ pofile = self.factory.makePOFile(
48+ potemplate=self.factory.makePOTemplate(
49+ productseries=product.series[0]))
50+ pofile.potemplate.productseries.product
51+ potmsgsets = [
52+ self.factory.makePOTMsgSet(pofile.potemplate) for i in range(10)]
53+
54+ # Preload a few transaction-crossing caches that would give
55+ # extra queries to the first request.
56+ getUtility(ILaunchBag).time_zone
57+ getUtility(ILaunchpadCelebrities).ubuntu
58+ person.inTeam(getUtility(ILaunchpadCelebrities).admin)
59+ person.inTeam(getUtility(ILaunchpadCelebrities).rosetta_experts)
60+
61+ def create_suggestions():
62+ for potmsgset in potmsgsets:
63+ pot = self.factory.makePOTemplate()
64+ self.factory.makeCurrentTranslationMessage(
65+ potmsgset=self.factory.makePOTMsgSet(
66+ singular=potmsgset.msgid_singular.msgid,
67+ potemplate=pot),
68+ language=pofile.language,
69+ translations=[self.factory.getUniqueUnicode()])
70+ # A suggestion only shows up if it's actually in a
71+ # POFile.
72+ self.factory.makePOFile(
73+ potemplate=pot, language=pofile.language)
74+ self.factory.makeSuggestion(
75+ pofile=pofile, potmsgset=potmsgset)
76+
77+ # Ensure that these are valid suggestions.
78+ templateset = getUtility(IPOTemplateSet)
79+ templateset.wipeSuggestivePOTemplatesCache()
80+ templateset.populateSuggestivePOTemplatesCache()
81+
82+ # And ensure that the credits string is empty, as that's
83+ # not currently constant.
84+ Store.of(pofile).find(POFileTranslator, pofile=pofile).set(
85+ pofileID=self.factory.makePOFile().id)
86+
87+ nb_objects = 2
88+ recorder1, recorder2 = record_two_runs(
89+ lambda: create_initialized_view(
90+ pofile, '+translate', principal=person)(),
91+ create_suggestions, nb_objects)
92+ self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
93
94
95 class TestPOFileTranslateViewInvalidFiltering(TestCaseWithFactory):
96
97=== modified file 'lib/lp/translations/browser/translationmessage.py'
98--- lib/lp/translations/browser/translationmessage.py 2014-07-02 04:41:13 +0000
99+++ lib/lp/translations/browser/translationmessage.py 2014-07-02 06:12:24 +0000
100@@ -1212,24 +1212,6 @@
101 else:
102 self.can_confirm_and_dismiss = True
103
104- def _setOnePOFile(self, messages):
105- """Return a list of messages that all have a browser_pofile set.
106-
107- If a pofile cannot be found for a message, it is not included in
108- the resulting list.
109- """
110- result = []
111- for message in messages:
112- if message.browser_pofile is None:
113- pofile = message.getOnePOFile()
114- if pofile is None:
115- # Do not include in result.
116- continue
117- else:
118- message.setPOFile(pofile)
119- result.append(message)
120- return result
121-
122 def _buildAllSuggestions(self):
123 """Builds all suggestions and puts them into suggestions_block.
124
125@@ -1277,8 +1259,8 @@
126
127 self._set_dismiss_flags(local, other)
128
129- for suggestion in local:
130- suggestion.setPOFile(self.pofile)
131+ getUtility(ITranslationMessageSet).preloadDetails(
132+ local, need_potranslation=True, need_people=True)
133
134 # Get a list of translations which are _used_ as translations
135 # for this same message in a different translation template.
136@@ -1289,19 +1271,36 @@
137 potmsgset.getExternallySuggestedOrUsedTranslationMessages(
138 suggested_languages=[language],
139 used_languages=used_languages))
140+
141+ # Suggestions from other templates need full preloading,
142+ # including picking a POFile. preloadDetails requires that
143+ # all messages have the same language, so we invoke it
144+ # separately for the alternate suggestion language.
145+ preload_groups = [
146+ translations[language].used + translations[language].suggested,
147+ translations[self.sec_lang].used,
148+ ]
149+ for group in preload_groups:
150+ getUtility(ITranslationMessageSet).preloadDetails(
151+ group, need_pofile=True, need_potemplate=True,
152+ need_potemplate_context=True,
153+ need_potranslation=True, need_potmsgset=True,
154+ need_people=True)
155+
156 alt_external = translations[self.sec_lang].used
157- externally_used = self._setOnePOFile(sorted(
158- translations[language].used,
159+ externally_used = sorted(
160+ [m for m in translations[language].used if m.browser_pofile],
161 key=operator.attrgetter("date_created"),
162- reverse=True))
163+ reverse=True)
164
165 # Get a list of translations which are suggested as
166 # translations for this same message in a different translation
167 # template, but are not used.
168- externally_suggested = self._setOnePOFile(sorted(
169- translations[language].suggested,
170+ externally_suggested = sorted(
171+ [m for m in translations[language].suggested
172+ if m.browser_pofile],
173 key=operator.attrgetter("date_created"),
174- reverse=True))
175+ reverse=True)
176 else:
177 # Don't show suggestions for anonymous users.
178 local = externally_used = externally_suggested = []
179
180=== modified file 'lib/lp/translations/interfaces/potemplate.py'
181--- lib/lp/translations/interfaces/potemplate.py 2013-10-21 04:58:19 +0000
182+++ lib/lp/translations/interfaces/potemplate.py 2014-07-02 06:12:24 +0000
183@@ -698,6 +698,9 @@
184 Return None if there is no such `IPOTemplate`.
185 """
186
187+ def preloadPOTemplateContexts(templates):
188+ """Preload context objects for a sequence of POTemplates."""
189+
190 def wipeSuggestivePOTemplatesCache():
191 """Erase suggestive-templates cache.
192
193
194=== modified file 'lib/lp/translations/interfaces/translationmessage.py'
195--- lib/lp/translations/interfaces/translationmessage.py 2013-01-07 02:40:55 +0000
196+++ lib/lp/translations/interfaces/translationmessage.py 2014-07-02 06:12:24 +0000
197@@ -343,3 +343,25 @@
198 return.
199 :param order_by: An SQL ORDER BY clause.
200 """
201+
202+ def preloadDetails(messages, pofile=None, need_pofile=False,
203+ need_potemplate=False, need_potemplate_context=False,
204+ need_potranslation=False, need_potmsgset=False,
205+ need_people=False):
206+ """Preload lots of details for `TranslationMessage`s.
207+
208+ If need_pofile is True, pofile may be left None to indicate that
209+ an arbitrary `POFile` containing that message may be picked, or
210+ set to a specific `POFile` in which all the messages must exist.
211+ In either case, all messages must be for the same language.
212+ """
213+
214+ def preloadPOFilesAndSequences(messages, pofile=None):
215+ """Preload browser_pofile and sequence for `TranslationMessage`s.
216+
217+ All messages must be for the same language.
218+
219+ If pofile is None, an arbitrary POFile containing each message
220+ will be used. Otherwise, each message must exist in the given
221+ POFile.
222+ """
223
224=== modified file 'lib/lp/translations/model/potemplate.py'
225--- lib/lp/translations/model/potemplate.py 2013-10-21 04:58:19 +0000
226+++ lib/lp/translations/model/potemplate.py 2014-07-02 06:12:24 +0000
227@@ -1347,6 +1347,14 @@
228 len(preferred_matches))
229 return None
230
231+ def preloadPOTemplateContexts(self, templates):
232+ """See `IPOTemplateSet`."""
233+ from lp.registry.model.product import Product
234+ from lp.registry.model.productseries import ProductSeries
235+ pses = load_related(ProductSeries, templates, ['productseriesID'])
236+ load_related(Product, pses, ['productID'])
237+ load_related(SourcePackageName, templates, ['sourcepackagenameID'])
238+
239 def wipeSuggestivePOTemplatesCache(self):
240 """See `IPOTemplateSet`."""
241 return IMasterStore(POTemplate).execute(
242
243=== modified file 'lib/lp/translations/model/translationmessage.py'
244--- lib/lp/translations/model/translationmessage.py 2014-06-13 07:43:41 +0000
245+++ lib/lp/translations/model/translationmessage.py 2014-07-02 06:12:24 +0000
246@@ -22,9 +22,18 @@
247 from storm.expr import And
248 from storm.locals import SQL
249 from storm.store import Store
250+from zope.component import getUtility
251 from zope.interface import implements
252+from zope.security.proxy import removeSecurityProxy
253
254-from lp.registry.interfaces.person import validate_public_person
255+from lp.registry.interfaces.person import (
256+ IPersonSet,
257+ validate_public_person,
258+ )
259+from lp.services.database.bulk import (
260+ load,
261+ load_related,
262+ )
263 from lp.services.database.constants import (
264 DEFAULT,
265 UTC_NOW,
266@@ -41,6 +50,7 @@
267 cachedproperty,
268 get_property_cache,
269 )
270+from lp.translations.interfaces.potemplate import IPOTemplateSet
271 from lp.translations.interfaces.side import TranslationSide
272 from lp.translations.interfaces.translationmessage import (
273 ITranslationMessage,
274@@ -49,6 +59,7 @@
275 TranslationValidationStatus,
276 )
277 from lp.translations.interfaces.translations import TranslationConstants
278+from lp.translations.model.potranslation import POTranslation
279
280
281 UTC = pytz.timezone('UTC')
282@@ -113,10 +124,13 @@
283 elements.append(suffix)
284 return self.potmsgset.makeHTMLID('_'.join(elements))
285
286- def setPOFile(self, pofile):
287+ def setPOFile(self, pofile, sequence=None):
288 """See `ITranslationMessage`."""
289 self.browser_pofile = pofile
290- del get_property_cache(self).sequence
291+ if sequence is not None:
292+ get_property_cache(self).sequence = sequence
293+ else:
294+ del get_property_cache(self).sequence
295
296 @cachedproperty
297 def sequence(self):
298@@ -396,7 +410,7 @@
299 """(SELECT potemplate
300 FROM TranslationTemplateItem
301 WHERE potmsgset = %s AND sequence > 0
302- LIMIT 1)""" % sqlvalues(self.potmsgset)),
303+ LIMIT 1)""" % sqlvalues(self.potmsgsetID)),
304 POFile.language == self.language).one()
305 return pofile
306
307@@ -530,3 +544,67 @@
308 def selectDirect(self, where=None, order_by=None):
309 """See `ITranslationMessageSet`."""
310 return TranslationMessage.select(where, orderBy=order_by)
311+
312+ def preloadDetails(self, messages, pofile=None, need_pofile=False,
313+ need_potemplate=False, need_potemplate_context=False,
314+ need_potranslation=False, need_potmsgset=False,
315+ need_people=False):
316+ """See `ITranslationMessageSet`."""
317+ from lp.translations.model.potemplate import POTemplate
318+ from lp.translations.model.potmsgset import POTMsgSet
319+ assert need_pofile or not need_potemplate
320+ assert need_potemplate or not need_potemplate_context
321+ tms = [removeSecurityProxy(tm) for tm in messages]
322+ if need_pofile:
323+ self.preloadPOFilesAndSequences(tms, pofile)
324+ if need_potemplate:
325+ pofiles = [tm.browser_pofile for tm in tms]
326+ pots = load_related(
327+ POTemplate,
328+ (removeSecurityProxy(pofile) for pofile in pofiles),
329+ ['potemplateID'])
330+ if need_potemplate_context:
331+ getUtility(IPOTemplateSet).preloadPOTemplateContexts(pots)
332+ if need_potranslation:
333+ load_related(
334+ POTranslation, tms,
335+ ['msgstr%dID' % form
336+ for form in xrange(TranslationConstants.MAX_PLURAL_FORMS)])
337+ if need_potmsgset:
338+ load_related(POTMsgSet, tms, ['potmsgsetID'])
339+ if need_people:
340+ list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
341+ [tm.submitterID for tm in tms]
342+ + [tm.reviewerID for tm in tms]))
343+
344+ def preloadPOFilesAndSequences(self, messages, pofile=None):
345+ """See `ITranslationMessageSet`."""
346+ from lp.translations.model.pofile import POFile
347+ from lp.translations.model.translationtemplateitem import (
348+ TranslationTemplateItem,
349+ )
350+
351+ if len(messages) == 0:
352+ return
353+ language = messages[0].language
354+ if pofile is not None:
355+ pofile_constraints = [POFile.id == pofile.id]
356+ else:
357+ pofile_constraints = [POFile.language == language]
358+ results = IStore(POFile).find(
359+ (TranslationTemplateItem.potmsgsetID, POFile.id,
360+ TranslationTemplateItem.sequence),
361+ TranslationTemplateItem.potmsgsetID.is_in(
362+ message.potmsgsetID for message in messages),
363+ POFile.potemplateID == TranslationTemplateItem.potemplateID,
364+ *pofile_constraints
365+ ).config(distinct=(TranslationTemplateItem.potmsgsetID,))
366+ potmsgset_map = dict(
367+ (potmsgset_id, (pofile_id, sequence))
368+ for potmsgset_id, pofile_id, sequence in results)
369+ load(POFile, (pofile_id for pofile_id, _ in potmsgset_map.values()))
370+ for message in messages:
371+ assert message.language == language
372+ pofile_id, sequence = potmsgset_map.get(
373+ message.potmsgsetID, (None, None))
374+ message.setPOFile(IStore(POFile).get(POFile, pofile_id), sequence)