Skip to content

Commit f78b050

Browse files
refactor: Streamline draft aliases api (ietf-tools#7607)
* chore: update add-django-cprofile-filter.patch * fix: only use "draft" state when making aliases * refactor: eliminate repeated get_state_slug() On dev, reduces time for a draft-aliases api call by by 10-15% * refactor: only annotate inactive drafts * refactor: de-lint * refactor: speed up get_draft_authors_emails Another 20% or so improvement in response time * fix: guard against null person
1 parent 0dcccea commit f78b050

2 files changed

Lines changed: 131 additions & 62 deletions

File tree

ietf/doc/utils.py

Lines changed: 75 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from django.conf import settings
2121
from django.contrib import messages
22+
from django.db.models import OuterRef
2223
from django.forms import ValidationError
2324
from django.http import Http404
2425
from django.template.loader import render_to_string
@@ -39,7 +40,7 @@
3940
from ietf.name.models import DocReminderTypeName, DocRelationshipName
4041
from ietf.group.models import Role, Group, GroupFeatures
4142
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, is_individual_draft_author, is_bofreq_editor
42-
from ietf.person.models import Person
43+
from ietf.person.models import Email, Person
4344
from ietf.review.models import ReviewWish
4445
from ietf.utils import draft, log
4546
from ietf.utils.mail import parseaddr, send_mail
@@ -1301,9 +1302,13 @@ def get_draft_shepherd_email(self, doc):
13011302
def get_draft_authors_emails(self, doc):
13021303
"""Get list of authors for the given draft."""
13031304
author_emails = set()
1304-
for author in doc.documentauthor_set.all():
1305-
if author.email and author.email.email_address():
1306-
author_emails.add(author.email.email_address())
1305+
for email in Email.objects.filter(documentauthor__document=doc):
1306+
if email.active:
1307+
author_emails.add(email.address)
1308+
elif email.person:
1309+
person_email = email.person.email_address()
1310+
if person_email:
1311+
author_emails.add(person_email)
13071312
return author_emails
13081313

13091314
def get_draft_notify_emails(self, doc):
@@ -1336,59 +1341,82 @@ def get_draft_notify_emails(self, doc):
13361341
notify_emails.add(email)
13371342
return notify_emails
13381343

1344+
def _yield_aliases_for_draft(self, doc)-> Iterator[tuple[str, list[str]]]:
1345+
alias = doc.name
1346+
all = set()
1347+
1348+
# no suffix and .authors are the same list
1349+
emails = self.get_draft_authors_emails(doc)
1350+
all.update(emails)
1351+
if emails:
1352+
yield alias, list(emails)
1353+
yield alias + ".authors", list(emails)
1354+
1355+
# .chairs = group chairs
1356+
emails = self.get_draft_chair_emails(doc)
1357+
if emails:
1358+
all.update(emails)
1359+
yield alias + ".chairs", list(emails)
1360+
1361+
# .ad = sponsoring AD / WG AD (WG document)
1362+
emails = self.get_draft_ad_emails(doc)
1363+
if emails:
1364+
all.update(emails)
1365+
yield alias + ".ad", list(emails)
1366+
1367+
# .notify = notify email list from the Document
1368+
emails = self.get_draft_notify_emails(doc)
1369+
if emails:
1370+
all.update(emails)
1371+
yield alias + ".notify", list(emails)
1372+
1373+
# .shepherd = shepherd email from the Document
1374+
emails = self.get_draft_shepherd_email(doc)
1375+
if emails:
1376+
all.update(emails)
1377+
yield alias + ".shepherd", list(emails)
1378+
1379+
# .all = everything from above
1380+
if all:
1381+
yield alias + ".all", list(all)
1382+
13391383
def __iter__(self) -> Iterator[tuple[str, list[str]]]:
13401384
# Internet-Drafts with active status or expired within self.days
13411385
show_since = timezone.now() - datetime.timedelta(days=self.days)
13421386
drafts = self.draft_queryset
1343-
active_drafts = drafts.filter(states__slug='active')
1344-
inactive_recent_drafts = drafts.exclude(states__slug='active').filter(expires__gte=show_since)
1345-
interesting_drafts = active_drafts | inactive_recent_drafts
13461387

1347-
for this_draft in interesting_drafts.distinct().iterator():
1388+
# Look up the draft-active state properly. Doing this with
1389+
# states__type_id, states__slug directly in the `filter()`
1390+
# works, but it does not work as expected in `exclude()`.
1391+
active_state = State.objects.get(type_id="draft", slug="active")
1392+
active_drafts = drafts.filter(states=active_state)
1393+
for this_draft in active_drafts:
1394+
for alias, addresses in self._yield_aliases_for_draft(this_draft):
1395+
yield alias, addresses
1396+
1397+
# Annotate with the draft state slug so we can check for drafts that
1398+
# have become RFCs
1399+
inactive_recent_drafts = (
1400+
drafts.exclude(states=active_state)
1401+
.filter(expires__gte=show_since)
1402+
.annotate(
1403+
# Why _default_manager instead of objects? See:
1404+
# https://docs.djangoproject.com/en/4.2/topics/db/managers/#django.db.models.Model._default_manager
1405+
draft_state_slug=Document.states.through._default_manager.filter(
1406+
document__pk=OuterRef("pk"),
1407+
state__type_id="draft"
1408+
).values("state__slug"),
1409+
)
1410+
)
1411+
for this_draft in inactive_recent_drafts:
13481412
# Omit drafts that became RFCs, unless they were published in the last DEFAULT_YEARS
1349-
if this_draft.get_state_slug() == "rfc":
1413+
if this_draft.draft_state_slug == "rfc":
13501414
rfc = this_draft.became_rfc()
13511415
log.assertion("rfc is not None")
13521416
if rfc.latest_event(type='published_rfc').time < show_since:
13531417
continue
1354-
1355-
alias = this_draft.name
1356-
all = set()
1357-
1358-
# no suffix and .authors are the same list
1359-
emails = self.get_draft_authors_emails(this_draft)
1360-
all.update(emails)
1361-
if emails:
1362-
yield alias, list(emails)
1363-
yield alias + ".authors", list(emails)
1364-
1365-
# .chairs = group chairs
1366-
emails = self.get_draft_chair_emails(this_draft)
1367-
if emails:
1368-
all.update(emails)
1369-
yield alias + ".chairs", list(emails)
1370-
1371-
# .ad = sponsoring AD / WG AD (WG document)
1372-
emails = self.get_draft_ad_emails(this_draft)
1373-
if emails:
1374-
all.update(emails)
1375-
yield alias + ".ad", list(emails)
1376-
1377-
# .notify = notify email list from the Document
1378-
emails = self.get_draft_notify_emails(this_draft)
1379-
if emails:
1380-
all.update(emails)
1381-
yield alias + ".notify", list(emails)
1382-
1383-
# .shepherd = shepherd email from the Document
1384-
emails = self.get_draft_shepherd_email(this_draft)
1385-
if emails:
1386-
all.update(emails)
1387-
yield alias + ".shepherd", list(emails)
1388-
1389-
# .all = everything from above
1390-
if all:
1391-
yield alias + ".all", list(all)
1418+
for alias, addresses in self._yield_aliases_for_draft(this_draft):
1419+
yield alias, addresses
13921420

13931421

13941422
def get_doc_email_aliases(name: Optional[str] = None):

patch/add-django-cprofile-filter.patch

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
1-
--- django_cprofile_middleware/middleware.py.old 2018-04-04 06:32:29.282187502 -0700
2-
+++ django_cprofile_middleware/middleware.py 2018-04-06 10:11:18.936855634 -0700
3-
@@ -1,4 +1,5 @@
4-
import pstats
5-
+import re
6-
7-
try:
8-
import cProfile as profile
9-
@@ -14,6 +15,15 @@
10-
from django.utils.deprecation import MiddlewareMixin
11-
12-
1+
--- django_cprofile_middleware/middleware.py.old 2024-06-27 21:03:56.975128007 +0000
2+
+++ django_cprofile_middleware/middleware.py 2024-06-27 23:45:59.421683008 +0000
3+
@@ -19,6 +19,16 @@
4+
from django_cprofile_middleware.utils import MiddlewareMixin
5+
6+
137
+class Stats(pstats.Stats):
148
+ def filter_stats(self, regex):
159
+ oldstats = self.stats
@@ -18,23 +12,70 @@
1812
+ for func, (cc, nc, tt, ct, callers) in oldstats.iteritems():
1913
+ if filter.search(pstats.func_std_string(func)):
2014
+ newstats[func] = (cc, nc, tt, ct, callers)
15+
+
2116
+
2217
class ProfilerMiddleware(MiddlewareMixin):
2318
"""
2419
Simple profile middleware to profile django views. To run it, add ?prof to
25-
@@ -62,8 +72,13 @@
20+
@@ -38,9 +48,11 @@
21+
?download => Download profile file suitable for visualization. For example
22+
in snakeviz or RunSnakeRun
23+
24+
- This is adapted from an example found here:
25+
- http://www.slideshare.net/zeeg/django-con-high-performance-django-presentation.
26+
+ Patched with https://github.com/omarish/django-cprofile-middleware/pull/23
27+
+ for operation with Django 4.2.5+
28+
"""
29+
+ PROFILER_REQUEST_ATTR_NAME = '_django_cprofile_middleware_profiler'
30+
+
31+
def can(self, request):
32+
requires_staff = getattr(
33+
settings, "DJANGO_CPROFILE_MIDDLEWARE_REQUIRE_STAFF", True)
34+
@@ -52,10 +64,11 @@
35+
36+
def process_view(self, request, callback, callback_args, callback_kwargs):
37+
if self.can(request):
38+
- self.profiler = profile.Profile()
39+
+ profiler = profile.Profile()
40+
+ setattr(request, self.PROFILER_REQUEST_ATTR_NAME, profiler)
41+
args = (request,) + callback_args
42+
try:
43+
- return self.profiler.runcall(
44+
+ return profiler.runcall(
45+
callback, *args, **callback_kwargs)
46+
except Exception:
47+
# we want the process_exception middleware to fire
48+
@@ -63,12 +76,13 @@
49+
return
50+
51+
def process_response(self, request, response):
52+
- if self.can(request):
53+
- self.profiler.create_stats()
54+
+ if hasattr(request, self.PROFILER_REQUEST_ATTR_NAME):
55+
+ profiler = getattr(request, self.PROFILER_REQUEST_ATTR_NAME)
56+
+ profiler.create_stats()
57+
if 'download' in request.GET:
58+
import marshal
59+
60+
- output = marshal.dumps(self.profiler.stats)
61+
+ output = marshal.dumps(profiler.stats)
62+
response = HttpResponse(
63+
output, content_type='application/octet-stream')
64+
response['Content-Disposition'] = 'attachment;' \
65+
@@ -76,9 +90,14 @@
2666
response['Content-Length'] = len(output)
2767
else:
2868
io = StringIO()
2969
- stats = pstats.Stats(self.profiler, stream=io)
70+
+ stats = Stats(profiler, stream=io)
71+
3072
- stats.strip_dirs().sort_stats(request.GET.get('sort', 'time'))
31-
+ stats = Stats(self.profiler, stream=io)
3273
+ if request.GET.get('stripdirs', False):
3374
+ stats = stats.strip_dirs()
3475
+ filter = request.GET.get('filter', None)
3576
+ if filter:
3677
+ stats.filter_stats(filter)
3778
+ stats.sort_stats(request.GET.get('psort') or 'time')
3879
stats.print_stats(int(request.GET.get('count', 100)))
80+
3981
response = HttpResponse('<pre>%s</pre>' % io.getvalue())
40-
return response

0 commit comments

Comments
 (0)