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
=== modified file 'lib/lp/translations/browser/tests/test_pofile_view.py'
--- lib/lp/translations/browser/tests/test_pofile_view.py 2014-06-10 11:25:51 +0000
+++ lib/lp/translations/browser/tests/test_pofile_view.py 2014-07-02 06:12:24 +0000
@@ -3,20 +3,91 @@
33
4__metaclass__ = type4__metaclass__ = type
55
6from storm.store import Store
7from testtools.matchers import Equals
8from zope.component import getUtility
9
6from lp.app.errors import UnexpectedFormData10from lp.app.errors import UnexpectedFormData
11from lp.app.interfaces.launchpad import ILaunchpadCelebrities
12from lp.services.webapp.interfaces import ILaunchBag
7from lp.services.webapp.servers import LaunchpadTestRequest13from lp.services.webapp.servers import LaunchpadTestRequest
8from lp.testing import (14from lp.testing import (
9 BrowserTestCase,15 BrowserTestCase,
10 login,16 login,
17 login_person,
11 person_logged_in,18 person_logged_in,
19 record_two_runs,
12 TestCaseWithFactory,20 TestCaseWithFactory,
13 )21 )
14from lp.testing.layers import (22from lp.testing.layers import (
15 DatabaseFunctionalLayer,23 DatabaseFunctionalLayer,
16 ZopelessDatabaseLayer,24 ZopelessDatabaseLayer,
17 )25 )
26from lp.testing.matchers import HasQueryCount
27from lp.testing.views import create_initialized_view
18from lp.translations.browser.pofile import POFileTranslateView28from lp.translations.browser.pofile import POFileTranslateView
19from lp.translations.enums import TranslationPermission29from lp.translations.enums import TranslationPermission
30from lp.translations.interfaces.potemplate import IPOTemplateSet
31from lp.translations.interfaces.translationsperson import ITranslationsPerson
32from lp.translations.model.pofiletranslator import POFileTranslator
33
34
35class TestQueryCount(TestCaseWithFactory):
36
37 layer = DatabaseFunctionalLayer
38
39 def test_query_count(self):
40 person = self.factory.makePerson()
41 login_person(person)
42 ITranslationsPerson(person).translations_relicensing_agreement = True
43 product = self.factory.makeProduct(owner=person)
44 product.translationpermission = TranslationPermission.OPEN
45 pofile = self.factory.makePOFile(
46 potemplate=self.factory.makePOTemplate(
47 productseries=product.series[0]))
48 pofile.potemplate.productseries.product
49 potmsgsets = [
50 self.factory.makePOTMsgSet(pofile.potemplate) for i in range(10)]
51
52 # Preload a few transaction-crossing caches that would give
53 # extra queries to the first request.
54 getUtility(ILaunchBag).time_zone
55 getUtility(ILaunchpadCelebrities).ubuntu
56 person.inTeam(getUtility(ILaunchpadCelebrities).admin)
57 person.inTeam(getUtility(ILaunchpadCelebrities).rosetta_experts)
58
59 def create_suggestions():
60 for potmsgset in potmsgsets:
61 pot = self.factory.makePOTemplate()
62 self.factory.makeCurrentTranslationMessage(
63 potmsgset=self.factory.makePOTMsgSet(
64 singular=potmsgset.msgid_singular.msgid,
65 potemplate=pot),
66 language=pofile.language,
67 translations=[self.factory.getUniqueUnicode()])
68 # A suggestion only shows up if it's actually in a
69 # POFile.
70 self.factory.makePOFile(
71 potemplate=pot, language=pofile.language)
72 self.factory.makeSuggestion(
73 pofile=pofile, potmsgset=potmsgset)
74
75 # Ensure that these are valid suggestions.
76 templateset = getUtility(IPOTemplateSet)
77 templateset.wipeSuggestivePOTemplatesCache()
78 templateset.populateSuggestivePOTemplatesCache()
79
80 # And ensure that the credits string is empty, as that's
81 # not currently constant.
82 Store.of(pofile).find(POFileTranslator, pofile=pofile).set(
83 pofileID=self.factory.makePOFile().id)
84
85 nb_objects = 2
86 recorder1, recorder2 = record_two_runs(
87 lambda: create_initialized_view(
88 pofile, '+translate', principal=person)(),
89 create_suggestions, nb_objects)
90 self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
2091
2192
22class TestPOFileTranslateViewInvalidFiltering(TestCaseWithFactory):93class TestPOFileTranslateViewInvalidFiltering(TestCaseWithFactory):
2394
=== modified file 'lib/lp/translations/browser/translationmessage.py'
--- lib/lp/translations/browser/translationmessage.py 2014-07-02 04:41:13 +0000
+++ lib/lp/translations/browser/translationmessage.py 2014-07-02 06:12:24 +0000
@@ -1212,24 +1212,6 @@
1212 else:1212 else:
1213 self.can_confirm_and_dismiss = True1213 self.can_confirm_and_dismiss = True
12141214
1215 def _setOnePOFile(self, messages):
1216 """Return a list of messages that all have a browser_pofile set.
1217
1218 If a pofile cannot be found for a message, it is not included in
1219 the resulting list.
1220 """
1221 result = []
1222 for message in messages:
1223 if message.browser_pofile is None:
1224 pofile = message.getOnePOFile()
1225 if pofile is None:
1226 # Do not include in result.
1227 continue
1228 else:
1229 message.setPOFile(pofile)
1230 result.append(message)
1231 return result
1232
1233 def _buildAllSuggestions(self):1215 def _buildAllSuggestions(self):
1234 """Builds all suggestions and puts them into suggestions_block.1216 """Builds all suggestions and puts them into suggestions_block.
12351217
@@ -1277,8 +1259,8 @@
12771259
1278 self._set_dismiss_flags(local, other)1260 self._set_dismiss_flags(local, other)
12791261
1280 for suggestion in local:1262 getUtility(ITranslationMessageSet).preloadDetails(
1281 suggestion.setPOFile(self.pofile)1263 local, need_potranslation=True, need_people=True)
12821264
1283 # Get a list of translations which are _used_ as translations1265 # Get a list of translations which are _used_ as translations
1284 # for this same message in a different translation template.1266 # for this same message in a different translation template.
@@ -1289,19 +1271,36 @@
1289 potmsgset.getExternallySuggestedOrUsedTranslationMessages(1271 potmsgset.getExternallySuggestedOrUsedTranslationMessages(
1290 suggested_languages=[language],1272 suggested_languages=[language],
1291 used_languages=used_languages))1273 used_languages=used_languages))
1274
1275 # Suggestions from other templates need full preloading,
1276 # including picking a POFile. preloadDetails requires that
1277 # all messages have the same language, so we invoke it
1278 # separately for the alternate suggestion language.
1279 preload_groups = [
1280 translations[language].used + translations[language].suggested,
1281 translations[self.sec_lang].used,
1282 ]
1283 for group in preload_groups:
1284 getUtility(ITranslationMessageSet).preloadDetails(
1285 group, need_pofile=True, need_potemplate=True,
1286 need_potemplate_context=True,
1287 need_potranslation=True, need_potmsgset=True,
1288 need_people=True)
1289
1292 alt_external = translations[self.sec_lang].used1290 alt_external = translations[self.sec_lang].used
1293 externally_used = self._setOnePOFile(sorted(1291 externally_used = sorted(
1294 translations[language].used,1292 [m for m in translations[language].used if m.browser_pofile],
1295 key=operator.attrgetter("date_created"),1293 key=operator.attrgetter("date_created"),
1296 reverse=True))1294 reverse=True)
12971295
1298 # Get a list of translations which are suggested as1296 # Get a list of translations which are suggested as
1299 # translations for this same message in a different translation1297 # translations for this same message in a different translation
1300 # template, but are not used.1298 # template, but are not used.
1301 externally_suggested = self._setOnePOFile(sorted(1299 externally_suggested = sorted(
1302 translations[language].suggested,1300 [m for m in translations[language].suggested
1301 if m.browser_pofile],
1303 key=operator.attrgetter("date_created"),1302 key=operator.attrgetter("date_created"),
1304 reverse=True))1303 reverse=True)
1305 else:1304 else:
1306 # Don't show suggestions for anonymous users.1305 # Don't show suggestions for anonymous users.
1307 local = externally_used = externally_suggested = []1306 local = externally_used = externally_suggested = []
13081307
=== modified file 'lib/lp/translations/interfaces/potemplate.py'
--- lib/lp/translations/interfaces/potemplate.py 2013-10-21 04:58:19 +0000
+++ lib/lp/translations/interfaces/potemplate.py 2014-07-02 06:12:24 +0000
@@ -698,6 +698,9 @@
698 Return None if there is no such `IPOTemplate`.698 Return None if there is no such `IPOTemplate`.
699 """699 """
700700
701 def preloadPOTemplateContexts(templates):
702 """Preload context objects for a sequence of POTemplates."""
703
701 def wipeSuggestivePOTemplatesCache():704 def wipeSuggestivePOTemplatesCache():
702 """Erase suggestive-templates cache.705 """Erase suggestive-templates cache.
703706
704707
=== modified file 'lib/lp/translations/interfaces/translationmessage.py'
--- lib/lp/translations/interfaces/translationmessage.py 2013-01-07 02:40:55 +0000
+++ lib/lp/translations/interfaces/translationmessage.py 2014-07-02 06:12:24 +0000
@@ -343,3 +343,25 @@
343 return.343 return.
344 :param order_by: An SQL ORDER BY clause.344 :param order_by: An SQL ORDER BY clause.
345 """345 """
346
347 def preloadDetails(messages, pofile=None, need_pofile=False,
348 need_potemplate=False, need_potemplate_context=False,
349 need_potranslation=False, need_potmsgset=False,
350 need_people=False):
351 """Preload lots of details for `TranslationMessage`s.
352
353 If need_pofile is True, pofile may be left None to indicate that
354 an arbitrary `POFile` containing that message may be picked, or
355 set to a specific `POFile` in which all the messages must exist.
356 In either case, all messages must be for the same language.
357 """
358
359 def preloadPOFilesAndSequences(messages, pofile=None):
360 """Preload browser_pofile and sequence for `TranslationMessage`s.
361
362 All messages must be for the same language.
363
364 If pofile is None, an arbitrary POFile containing each message
365 will be used. Otherwise, each message must exist in the given
366 POFile.
367 """
346368
=== modified file 'lib/lp/translations/model/potemplate.py'
--- lib/lp/translations/model/potemplate.py 2013-10-21 04:58:19 +0000
+++ lib/lp/translations/model/potemplate.py 2014-07-02 06:12:24 +0000
@@ -1347,6 +1347,14 @@
1347 len(preferred_matches))1347 len(preferred_matches))
1348 return None1348 return None
13491349
1350 def preloadPOTemplateContexts(self, templates):
1351 """See `IPOTemplateSet`."""
1352 from lp.registry.model.product import Product
1353 from lp.registry.model.productseries import ProductSeries
1354 pses = load_related(ProductSeries, templates, ['productseriesID'])
1355 load_related(Product, pses, ['productID'])
1356 load_related(SourcePackageName, templates, ['sourcepackagenameID'])
1357
1350 def wipeSuggestivePOTemplatesCache(self):1358 def wipeSuggestivePOTemplatesCache(self):
1351 """See `IPOTemplateSet`."""1359 """See `IPOTemplateSet`."""
1352 return IMasterStore(POTemplate).execute(1360 return IMasterStore(POTemplate).execute(
13531361
=== modified file 'lib/lp/translations/model/translationmessage.py'
--- lib/lp/translations/model/translationmessage.py 2014-06-13 07:43:41 +0000
+++ lib/lp/translations/model/translationmessage.py 2014-07-02 06:12:24 +0000
@@ -22,9 +22,18 @@
22from storm.expr import And22from storm.expr import And
23from storm.locals import SQL23from storm.locals import SQL
24from storm.store import Store24from storm.store import Store
25from zope.component import getUtility
25from zope.interface import implements26from zope.interface import implements
27from zope.security.proxy import removeSecurityProxy
2628
27from lp.registry.interfaces.person import validate_public_person29from lp.registry.interfaces.person import (
30 IPersonSet,
31 validate_public_person,
32 )
33from lp.services.database.bulk import (
34 load,
35 load_related,
36 )
28from lp.services.database.constants import (37from lp.services.database.constants import (
29 DEFAULT,38 DEFAULT,
30 UTC_NOW,39 UTC_NOW,
@@ -41,6 +50,7 @@
41 cachedproperty,50 cachedproperty,
42 get_property_cache,51 get_property_cache,
43 )52 )
53from lp.translations.interfaces.potemplate import IPOTemplateSet
44from lp.translations.interfaces.side import TranslationSide54from lp.translations.interfaces.side import TranslationSide
45from lp.translations.interfaces.translationmessage import (55from lp.translations.interfaces.translationmessage import (
46 ITranslationMessage,56 ITranslationMessage,
@@ -49,6 +59,7 @@
49 TranslationValidationStatus,59 TranslationValidationStatus,
50 )60 )
51from lp.translations.interfaces.translations import TranslationConstants61from lp.translations.interfaces.translations import TranslationConstants
62from lp.translations.model.potranslation import POTranslation
5263
5364
54UTC = pytz.timezone('UTC')65UTC = pytz.timezone('UTC')
@@ -113,10 +124,13 @@
113 elements.append(suffix)124 elements.append(suffix)
114 return self.potmsgset.makeHTMLID('_'.join(elements))125 return self.potmsgset.makeHTMLID('_'.join(elements))
115126
116 def setPOFile(self, pofile):127 def setPOFile(self, pofile, sequence=None):
117 """See `ITranslationMessage`."""128 """See `ITranslationMessage`."""
118 self.browser_pofile = pofile129 self.browser_pofile = pofile
119 del get_property_cache(self).sequence130 if sequence is not None:
131 get_property_cache(self).sequence = sequence
132 else:
133 del get_property_cache(self).sequence
120134
121 @cachedproperty135 @cachedproperty
122 def sequence(self):136 def sequence(self):
@@ -396,7 +410,7 @@
396 """(SELECT potemplate410 """(SELECT potemplate
397 FROM TranslationTemplateItem411 FROM TranslationTemplateItem
398 WHERE potmsgset = %s AND sequence > 0412 WHERE potmsgset = %s AND sequence > 0
399 LIMIT 1)""" % sqlvalues(self.potmsgset)),413 LIMIT 1)""" % sqlvalues(self.potmsgsetID)),
400 POFile.language == self.language).one()414 POFile.language == self.language).one()
401 return pofile415 return pofile
402416
@@ -530,3 +544,67 @@
530 def selectDirect(self, where=None, order_by=None):544 def selectDirect(self, where=None, order_by=None):
531 """See `ITranslationMessageSet`."""545 """See `ITranslationMessageSet`."""
532 return TranslationMessage.select(where, orderBy=order_by)546 return TranslationMessage.select(where, orderBy=order_by)
547
548 def preloadDetails(self, messages, pofile=None, need_pofile=False,
549 need_potemplate=False, need_potemplate_context=False,
550 need_potranslation=False, need_potmsgset=False,
551 need_people=False):
552 """See `ITranslationMessageSet`."""
553 from lp.translations.model.potemplate import POTemplate
554 from lp.translations.model.potmsgset import POTMsgSet
555 assert need_pofile or not need_potemplate
556 assert need_potemplate or not need_potemplate_context
557 tms = [removeSecurityProxy(tm) for tm in messages]
558 if need_pofile:
559 self.preloadPOFilesAndSequences(tms, pofile)
560 if need_potemplate:
561 pofiles = [tm.browser_pofile for tm in tms]
562 pots = load_related(
563 POTemplate,
564 (removeSecurityProxy(pofile) for pofile in pofiles),
565 ['potemplateID'])
566 if need_potemplate_context:
567 getUtility(IPOTemplateSet).preloadPOTemplateContexts(pots)
568 if need_potranslation:
569 load_related(
570 POTranslation, tms,
571 ['msgstr%dID' % form
572 for form in xrange(TranslationConstants.MAX_PLURAL_FORMS)])
573 if need_potmsgset:
574 load_related(POTMsgSet, tms, ['potmsgsetID'])
575 if need_people:
576 list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
577 [tm.submitterID for tm in tms]
578 + [tm.reviewerID for tm in tms]))
579
580 def preloadPOFilesAndSequences(self, messages, pofile=None):
581 """See `ITranslationMessageSet`."""
582 from lp.translations.model.pofile import POFile
583 from lp.translations.model.translationtemplateitem import (
584 TranslationTemplateItem,
585 )
586
587 if len(messages) == 0:
588 return
589 language = messages[0].language
590 if pofile is not None:
591 pofile_constraints = [POFile.id == pofile.id]
592 else:
593 pofile_constraints = [POFile.language == language]
594 results = IStore(POFile).find(
595 (TranslationTemplateItem.potmsgsetID, POFile.id,
596 TranslationTemplateItem.sequence),
597 TranslationTemplateItem.potmsgsetID.is_in(
598 message.potmsgsetID for message in messages),
599 POFile.potemplateID == TranslationTemplateItem.potemplateID,
600 *pofile_constraints
601 ).config(distinct=(TranslationTemplateItem.potmsgsetID,))
602 potmsgset_map = dict(
603 (potmsgset_id, (pofile_id, sequence))
604 for potmsgset_id, pofile_id, sequence in results)
605 load(POFile, (pofile_id for pofile_id, _ in potmsgset_map.values()))
606 for message in messages:
607 assert message.language == language
608 pofile_id, sequence = potmsgset_map.get(
609 message.potmsgsetID, (None, None))
610 message.setPOFile(IStore(POFile).get(POFile, pofile_id), sequence)