-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
gh-102402: Fix floating point math issue by using time.time_ns()
in logging.LogRecord
#102412
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
Conversation
Lib/test/test_logging.py
Outdated
finally: | ||
time.time_ns = og_time_ns |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: Is there a more preferred way to accomplish this other than monkeypatching time.time_ns
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use unittest.mock.patch
as in this example:
import time
from unittest.mock import patch
for t in (123, 456, 789):
with patch('time.time_ns') as patched_ns:
patched_ns.return_value = t
value = time.time_ns()
print(f'patched: {value}')
print(f'OG: {time.time_ns()}')
which should, when run, print something like this:
patched: 123
OG: 1681278183002693921
patched: 456
OG: 1681278183003199141
patched: 789
OG: 1681278183003610801
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, my assumption was that we probably didn't want to add imports (I don't know why I assumed that...).
Updated to patch with mock
instead of manual patching.
I was able to come up with a math approach that is pretty easy to understand. It's also faster. String manipulation:
Math:
This version is a little faster:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR - minor changes requested.
Lib/test/test_logging.py
Outdated
finally: | ||
time.time_ns = og_time_ns |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use unittest.mock.patch
as in this example:
import time
from unittest.mock import patch
for t in (123, 456, 789):
with patch('time.time_ns') as patched_ns:
patched_ns.return_value = t
value = time.time_ns()
print(f'patched: {value}')
print(f'OG: {time.time_ns()}')
which should, when run, print something like this:
patched: 123
OG: 1681278183002693921
patched: 456
OG: 1681278183003199141
patched: 789
OG: 1681278183003610801
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have made the requested changes; please review again
Lib/test/test_logging.py
Outdated
finally: | ||
time.time_ns = og_time_ns |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, my assumption was that we probably didn't want to add imports (I don't know why I assumed that...).
Updated to patch with mock
instead of manual patching.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @serhiy-storchaka! I've addressed most items but would like more information on the request for additional testing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. I left only one minor suggestion, which can be ignored if you have other reasons.
Thank you for your contribution, @dougthor42. For a first time contributor, this PR has surprisingly high quality, you did not even make common errors. And I did not expect such good responsibility after months of inactivity. I will be glad to see your other PRs.
Lib/test/test_logging.py
Outdated
with patch("time.time_ns") as patched_ns: | ||
orig_modules = import_helper._save_and_remove_modules(['logging']) | ||
try: | ||
# mock for module import | ||
patched_ns.return_value = ns | ||
import logging |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that it can be out of the loop. You only need the following code in the loop:
new_ns = ns + offset_ns
patched_ns.return_value = new_ns
record = logging.makeLogRecord({'msg': 'test'})
self.assertAlmostEqual(record.created, new_ns / 1e9, places=6)
self.assertAlmostEqual(record.relativeCreated, offset_ns / 1e6, places=7)
It is just an optimization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. It may be "just an optimization" but I wouldn't consider it a premature optimization. Updated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a first time contributor, this PR has surprisingly high quality, you did not even make common errors. And I did not expect such good responsibility after months of inactivity. I will be glad to see your other PRs.
Thanks! I do hope to find more PRs that I can help with (and have the time to help with...)
Lib/test/test_logging.py
Outdated
with patch("time.time_ns") as patched_ns: | ||
orig_modules = import_helper._save_and_remove_modules(['logging']) | ||
try: | ||
# mock for module import | ||
patched_ns.return_value = ns | ||
import logging |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. It may be "just an optimization" but I wouldn't consider it a premature optimization. Updated.
Closing and reopening, as there seems to have been a spurious error. |
Thank you @dougthor42 for your patience - sorry it dropped off my radar for so long. |
…()` in `logging.LogRecord` (pythonGH-102412)
This change introduced reference leaks:
|
I suggest to not reimport the logging module in the test: def test_relativeCreated_has_higher_precision(self):
# See issue gh-102402
ns = 1_677_903_920_000_998_503 # approx. 2023-03-04 04:25:20 UTC
offsets_ns = (200, 500, 12_354, 99_999, 1_677_903_456_999_123_456)
with (patch("time.time_ns") as time_ns_mock,
support.swap_attr(logging, '_startTime', ns)):
for offset_ns in offsets_ns:
# mock for log record creation
new_ns = ns + offset_ns
time_ns_mock.return_value = new_ns
record = logging.makeLogRecord({'msg': 'test'})
self.assertAlmostEqual(record.created, new_ns / 1e9, places=6)
# After PR gh-102412, precision (places) increases from 3 to 7
self.assertAlmostEqual(record.relativeCreated, offset_ns / 1e6, places=7) |
I'm happy that my PEP 564 – Add new time functions with nanosecond resolution is used to fix very concrete time rounding issues ;-) |
Could you please open a new issue for the leak? I tried to find the cause, and still have not found it. It is a puzzle. |
I proposed a fix: #117985 |
Oh no! Sorry that my change caused more work for people 😞. TIL about |
Buildbots were broken for 1 day. I merged my fix since no better fix was proposed: 8f25cc9 |
gh-102402: Fix floating point math issue by using
time.time_ns()
inlogging.LogRecord
.logging.LogRecord
to usetime.time_ns()
instead oftime.time()
.LogRecord.msecs
to be robust against floating point / binary representation arithmetic errorsNotes / Open Questions:
LogRecord.msecs
is a float so I kept it that way. Why is it a float even though it always has.0
?LogRecord.created
as a float in seconds.logging.__init__
so I didn't add any for the new code. Would you like me to add some annotations?I opted to use string manipulation to grab themsecs
. I have an integer math-based approach, but it's, IMO, more verbose and harder to read/understand. LMK if you want me to switch back to that (there are still some minor fixes to apply to it).msecs
-related bug.Fixes #102402.