Skip to content

feat: build logger messages lazily #3643

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 16, 2025
Merged

Conversation

BobTheBuidler
Copy link
Contributor

@BobTheBuidler BobTheBuidler commented Mar 13, 2025

What was wrong?

When using the logging module, we currently build the log message eagerly which leads to wasted cpu if the logger is not enabled.

According to DuckDuckGo's silly AI assistant:

"Using f-strings for logging in Python is generally discouraged because they evaluate the string immediately, which can lead to unnecessary computations even if the log level is not set to display that message. Instead, it's recommended to use the traditional formatting methods, like the % operator, which defer the string formatting until it's actually needed, improving performance and avoiding potential issues."

With this change, we will build the log messages lazily so the string is never built if the log will not actually be emitted, saving compute. This is just a microoptimization but will make a difference for power users such as myself who have use cases that require making a high volume of requests over a long period.

Related to Issue # N/A
Closes # N/A

How was it fixed?

replacing f-strings with strings which will be used with str.format

Todo:

  • Clean up commit history
  • Add entry to the release notes Not applicable for typical user so I skipped this addition

Cute Animal Picture

Put a link to a cute animal picture inside the parenthesis-->

@BobTheBuidler BobTheBuidler marked this pull request as ready for review March 13, 2025 17:57
@BobTheBuidler BobTheBuidler changed the title feat: build logger strings lazily feat: build logger messages lazily Mar 13, 2025
@kclowes
Copy link
Collaborator

kclowes commented Mar 13, 2025

Thank you! Before merging, I'd like to make sure that this actually makes a 'meaningful' difference in performance before we sacrifice code consistency. Do you happen to know what magnitude of faster this is? If not, would you mind running a couple benchmarks?

@BobTheBuidler
Copy link
Contributor Author

BobTheBuidler commented Apr 5, 2025

This is on the backburner for now. I can tell you it saves about 33% of the time vs the existing code but I know you're going to want to see some data for this. I'm still going to put a benchmark together for that, when I get a chance. Stay tuned!

@BobTheBuidler
Copy link
Contributor Author

BobTheBuidler commented May 14, 2025

@kclowes Another day, another 15 minute wait period while my stuff spins up... Perfect time to put this data together:

For this benchmark I used a logger.debug statement comparable to the one found in HTTPProvider.make_request, since every single request goes thru this function.

import logging
from time import time

logger = logging.getLogger('test')

endpoint_uri = 'https://somewebsite.com/somereallylongauthkey/'
method = "eth_call"
response = {"jsonrpc": "2.0", "id": 1, "result": "0x" + "0" * 64}

def test_old_3_inputs():
    logger.debug(
        f"Getting response HTTP. URI: {endpoint_uri}, Method: {method}, Response: {response}",
    )
        
def test_new_3_inputs():
    logger.debug(
        "Getting response HTTP. URI: %s, Method: %s Response: %s",
        endpoint_uri,
        method,
        response,
    )
    

start = time()
for _ in range(50_000):
    test_old_3_inputs()
print(f"old: {time() - start}")

start = time()
for _ in range(50_000):
    test_new_3_inputs()
print(f"new: {time() - start}")
old: 0.4092729091644287
new: 0.12366008758544922

So for every ~175k requests it will save about 1s, this is not an insignificant savings for a power user.

My 33% estimate above came from my own tests with only 1 input, not 3. So turns out this is more impactful than I originally intended!

Copy link
Collaborator

@kclowes kclowes left a comment

Choose a reason for hiding this comment

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

Awesome, thank you! I left one nit, but I think that's it. Do you want to add a newsfragment or should I?

@@ -164,36 +164,34 @@ def get_endpoint_uri_or_ipc_path(self) -> str:
)

async def connect(self) -> None:
endpoint = self.get_endpoint_uri_or_ipc_path()
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍 nice!

Copy link
Collaborator

@fselmo fselmo left a comment

Choose a reason for hiding this comment

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

This lgtm too. Thanks for benchmarking!

@BobTheBuidler
Copy link
Contributor Author

I committed your nit, and added the newsfragment. I think we're good to go!

@kclowes
Copy link
Collaborator

kclowes commented May 15, 2025

oop, that nit was a bad suggestion, sorry. I didn't see that endpoint was only defined in connect

@BobTheBuidler
Copy link
Contributor Author

fixed, everything is green again

- Create 3643.performance.rst
- chore: black .
- fix: linter errors
@fselmo
Copy link
Collaborator

fselmo commented May 16, 2025

rebased with main and squashed, merging 👍🏼

@fselmo fselmo merged commit 2f3edcc into ethereum:main May 16, 2025
85 checks passed
@BobTheBuidler BobTheBuidler deleted the patch-2 branch May 16, 2025 00:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants