Skip to content

gh-109653: Improve the import time of email.utils - #109824

Merged
hauntsaninja merged 12 commits into
python:mainfrom
AlexWaygood:email-message-import
Oct 12, 2023
Merged

gh-109653: Improve the import time of email.utils#109824
hauntsaninja merged 12 commits into
python:mainfrom
AlexWaygood:email-message-import

Conversation

@AlexWaygood

@AlexWaygoodAlexWaygood commented Sep 25, 2023

Copy link
Copy Markdown
Member

This patch reduces the import time of email.utils by around 46%.

Why do I care about email.utils? Well, email.utils is imported by email.message, and email.message is imported by lots of other stdlib modules: urllib.parse, mailbox, and importlib.metadata. Improving the import time of this module has a cascading effect through lots of the rest of the standard library.

A lot of the import cost of email.utils has to do with the make_msgid function, which requires the random and socket modules. This function is used by many third-party libraries, but isn't used at all by any of the stdlib modules that import email.utils. This patch moves the function into a separate submodule.

@AlexWaygoodAlexWaygood added type-feature A feature request or enhancement performance Performance or resource usage topic-email 3.13 bugs and security fixes labels Sep 25, 2023
@AlexWaygood
AlexWaygood requested a review from a team as a code ownerSeptember 25, 2023 10:03
Comment threadMisc/NEWS.d/next/Library/2023-09-25-10-47-22.gh-issue-109653.TUHrId.rst Outdated
Co-authored-by: Nikita Sobolev <mail@sobolevn.me>
Comment threadLib/email/_msgid.py Outdated
Comment threadLib/email/_msgid.py Outdated
Comment threadLib/email/utils.py Outdated
Comment threadLib/email/utils.py Outdated
Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com>
Comment threadLib/email/utils.py Outdated
if attr == "make_msgid":
from email._msgid import make_msgid
return make_msgid
raise AttributeError(f"module {__name__!r} has no attribute {attr!r}")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general I don't like performance hacks like this, but they do have their place. I can't speak to whether or not this is a worthwhile performance hack, you should seek approval from the maintainers of the impacted modules for that.

That said, the stdlib itself makes no use of make_msgid, and email.utils is not itself considered part of the new email API. Moving it into a separate module and making that part of the non-legacy public API of the email module would actually make some sense. I guess we'd just call it 'msgid'? Then this code should issue a deprecation warning pointing to the new way to import make_msgid. It feels kind of weird to have a module with just one function, but there isn't really anything else related to it that I can think of. (I wonder...maybe make_msgid actually belongs in the UUID module? Probably not. Wrong RFC.)

@hauntsaninja

Copy link
Copy Markdown
Contributor

This feels a little strange to me. Should we instead just defer the imports? Might even be faster to do so

@AlexWaygood

AlexWaygood commented Oct 3, 2023

Copy link
Copy Markdown
MemberAuthor

This feels a little strange to me. Should we instead just defer the imports? Might even be faster to do so

Yeah, that might be a better shout. Have been meaning to give it a go — just haven't got round to it yet :)

I'm not really enthused about the idea of a painful deprecation period in order to change the place where people are "meant" to import it from. This function is pretty widely used by third-party packages, and the improvement in import time doesn't seem worth the disruption to me :)

@AlexWaygood

Copy link
Copy Markdown
MemberAuthor

I've updated the PR. I abandoned the idea of adding a new submodule; now I just defer the problematic random, socket and os imports to inside make_msgid(). The reason I didn't do that originally was because I was concerned it would slow down the make_msgid() function, but local experimentation doesn't show that. (I ran python -m timeit -s "from email.utils import make_msgid" "make_msgid()" both with and without the change to test.) Shows the importance of benchmarking!

Comment threadLib/email/utils.py
Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com>

@hauntsaninjahauntsaninja left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good

Comment threadLib/email/utils.py
except UnicodeEncodeError:
if isinstance(charset, str):
# lazy import to improve module import time
from email.charset import Charset

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I like this one, only fine with it since formataddr doesn't look too widely used (a lot of the time it's nice to pay these costs upfront, predictable performance is important, e.g. don't want the first request your webserver serves to be randomly slow)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(a lot of the time it's nice to pay these costs upfront, predictable performance is important

agreed. On the other hand, though, the email package goes in quite heavily for lazy imports in some other places, so this does seem in keeping with that general philosophy:

# Some convenience routines. Don't import Parser and Message as side-effects
# of importing email since those cascadingly import most of the rest of the
# email package.
defmessage_from_string(s, *args, **kws):
"""Parse a string into a Message object model.
Optional _class and strict are passed to the Parser constructor.
"""
fromemail.parserimportParser
returnParser(*args, **kws).parsestr(s)
defmessage_from_bytes(s, *args, **kws):
"""Parse a bytes string into a Message object model.
Optional _class and strict are passed to the Parser constructor.
"""
fromemail.parserimportBytesParser
returnBytesParser(*args, **kws).parsebytes(s)
defmessage_from_file(fp, *args, **kws):
"""Read a file and parse its contents into a Message object model.
Optional _class and strict are passed to the Parser constructor.
"""
fromemail.parserimportParser
returnParser(*args, **kws).parse(fp)
defmessage_from_binary_file(fp, *args, **kws):
"""Read a binary file and parse its contents into a Message object model.
Optional _class and strict are passed to the Parser constructor.
"""
fromemail.parserimportBytesParser
returnBytesParser(*args, **kws).parse(fp)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be happy to change it so it's imported at the top of the function if you think that'd be better?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah that'd be worse, module level or here. I'm fine with this as is!

Comment threadLib/email/utils.py Outdated
@hauntsaninja
hauntsaninja merged commit aa3f419 into python:mainOct 12, 2023
@AlexWaygood
AlexWaygood deleted the email-message-import branch October 12, 2023 22:26
@AlexWaygood
AlexWaygood restored the email-message-import branch February 22, 2024 16:37
@AlexWaygood
AlexWaygood deleted the email-message-import branch March 7, 2024 14:35
Glyphack pushed a commit to Glyphack/cpython that referenced this pull request Sep 2, 2024
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3.13bugs and security fixesperformancePerformance or resource usagetopic-emailtype-featureA feature request or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@AlexWaygood@hauntsaninja@bitdancer@sobolevn@AA-Turner