Repository navigation
PERF: improve performance with ZoneInfo timezones (get internal transitions/deltas to avoid slow python path) - #64379
Conversation
…nfo vs pytz timezone
| self.deltas = deltas | ||
|
|
||
| if typ != "pytz" and typ != "dateutil": | ||
| if typ not in ("pytz", "dateutil", "zoneinfo"): |
There was a problem hiding this comment.
nitpick: is this going to create an unnecessary python tuple object?
| # Daylight Savings | ||
|
|
||
|
|
||
| cdef object _get_zoneinfo_trans_and_deltas(tzinfo tz): |
There was a problem hiding this comment.
can we re-use some of this in the treat_tz_as_dateutil path?
|
Couple of test failures to figure out, but this looks like the right approach. |
|
@jbrockmendel Thank you for your review! I've applied your feedback. |
Co-authored-by: mv-python <[email protected]>
Co-authored-by: mv-python <[email protected]>
This surprises me. Is there a minimal example? |
| dateutil_tz : tzinfo | ||
| A dateutil timezone object with _trans_list and _trans_idx attributes. | ||
| first_offset_seconds : int64_t | ||
| The UTC offset in seconds for the period before the first transition. |
There was a problem hiding this comment.
This is needed to address the dateutil-vs-zoneinfo discrepancy?
There was a problem hiding this comment.
No, this is just extracting common logic into a single function. The discrepancy is addressed by the fallback logic in tzconversion.pyx (the part you asked me to add comments to).
@jbrockmendel from zoneinfo import ZoneInfo
from dateutil.tz import gettz
from datetime import datetime
dt = datetime(1900, 1, 1)
# Africa/Lusaka: 2-second difference
print(ZoneInfo("Africa/Lusaka").utcoffset(dt)) # -> 2:10:18
print(gettz("Africa/Lusaka").utcoffset(dt)) # -> 2:10:20
# Africa/Sao_Tome: much larger difference
print(ZoneInfo("Africa/Sao_Tome").utcoffset(dt)) # -> -1 day, 23:23:15
print(gettz("Africa/Sao_Tome").utcoffset(dt)) # -> 0:26:56Note that this discrepancy may not occur in all environments. Looking at the CI failure results, macOS passes while Ubuntu and Windows fail. |
|
@pganssle thoughts here on how to explain/handle the discrepancy? |
|
I suspect this would also close #58962 |
| return _get_trans_and_deltas_from_dateutil_tz( | ||
| dateutil_tz, first_offset_seconds | ||
| ) | ||
| else: |
There was a problem hiding this comment.
does is_fixed_offset handle/catch this case correctly?
There was a problem hiding this comment.
@jbrockmendel The Etc/GMT* timezones (where _trans_list exists but is empty) were not being handled correctly. I've fixed this and committed the change.
I haven't looked into the details, but I suspect that it might be down to the fact that That will be fixed in FWIW, my preferred mechanism to handle all of this stuff (not that I would recommend blocking PRs like this one on it) would be for someone to write a new "vectorized time zone" library for I'd be happy to champion whatever changes are necessary in |
jorisvandenbossche
left a comment
There was a problem hiding this comment.
Taking a closer look at that fixed offset detection, some questions, see inline comment
| return tz.utcoffset(obj) | ||
|
|
||
|
|
||
| cdef set _get_zoneinfo_offset_seconds(object tz_py): |
There was a problem hiding this comment.
| cdef set _get_zoneinfo_offset_seconds(object tz_py): | |
| cdef set _get_zoneinfo_fixed_offset(object tz_py): |
And let it return something like is_fixed, fixed_offset (bool, int) ?
At the moment this function gets all offsets, while in practice (where this helper is used) we only need the single offset if there is a single one (if it is a fixed offset).
For checking non-fixed zones, this will now needlessly iterate over all the offsets.
| if hasattr(tz_py, "_ttinfos"): | ||
| for info in tz_py._ttinfos: | ||
| offsets.add(int(info.utcoff.total_seconds())) | ||
| if hasattr(tz_py, "_tti_before"): | ||
| offsets.add(int(tz_py._tti_before.utcoff.total_seconds())) | ||
| if hasattr(tz_py, "_tz_after") and tz_py._tz_after is not None: | ||
| tz_after = tz_py._tz_after | ||
| if hasattr(tz_after, "std"): | ||
| offsets.add(int(tz_after.std.utcoff.total_seconds())) | ||
| offsets.add(int(tz_after.dst.utcoff.total_seconds())) | ||
| elif hasattr(tz_after, "utcoff"): | ||
| offsets.add(int(tz_after.utcoff.total_seconds())) | ||
| if hasattr(tz_after, "dstoff"): | ||
| offsets.add( | ||
| int((tz_after.utcoff + tz_after.dstoff).total_seconds()) | ||
| ) | ||
| return offsets |
There was a problem hiding this comment.
This whole part was added (originally) in a commit mentioning to fix the 32bit tests (062dc27). And I see that indeed the fixed offset test was failing only for the 32bit linux.
But can you give a bit more explanation of why you did this fix?
The timezone database on that machine has different entries that didn't get caught by the previous check for fixed offsets? (which was only checking if len(tz_py._trans_utc) == 0 and not has_future_dst)
And also, now there is a more elaborate check, is that second check still needed?
There was a problem hiding this comment.
Another alternative, since we are relying on the private API of zoneinfo anyway now, is to use tz_py._fixed_offset. That would simplify this a lot
(and then the offset can be gotten from tz_py._tz_after.utcoff, I think)
|
@kjmin622 small ping here (I would like to get this in for 3.0.3). Do you have time to take a look at my last comments? |
|
@jorisvandenbossche I've had a lot on my schedule lately and couldn’t check. Would it be okay to apply it by May 3? |
|
@jorisvandenbossche I applied your review! Thank you. |
|
@kjmin622 thanks for the update! I just pushed one small additional simplication (the two small helper functions were essentially only used once now, so I inlined them) |
jorisvandenbossche
left a comment
There was a problem hiding this comment.
Thanks @kjmin622!
Let's get this in. There is still a whatsnew missing, but I'll do that in a follow-up (and I have a follow-up PR for some other code paths as well)
…neInfo timezones (get internal transitions/deltas to avoid slow python path)) (#65480) Co-authored-by: Jeongmin Gil <[email protected]> Co-authored-by: Joris Van den Bossche <[email protected]>
| assert tz == timezone(-timedelta(hours=2, minutes=45)) | ||
|
|
||
|
|
||
| def test_zoneinfo_utc_to_local_post_2037(): |
There was a problem hiding this comment.
Sorry I have not read the full history of this PR. Is it expected that this test would fail if we used dates in 2100 instead of 2040? It seems to be the case for me locally. I am wondering if it is related to the round tripping failure we are seeing in the xarray test suite (e.g. in this CI run).
There was a problem hiding this comment.
Ah yes, that is expected at the moment. At some point in the PR review I asked about the arbitrary cut-off of 2100, but then forgot to follow-up on that.
We should probably at least go up to 2262 on the short term, i.e. the max range for when we used nanosecond unit by default (that has now changed, so it is now possible to have data beyond that, but should be less common).
And then ideally have some solution that takes into account the range of the actual data being processed.
There was a problem hiding this comment.
All good, thanks @jorisvandenbossche that sounds reasonable to me.
There was a problem hiding this comment.
FWIW, I would have expected this test to also fail in the past (before 3.0), because then we were using pytz by default, and then it stopped using DST transitions around 2037 (only with 3.0.0-3.0.2, it will have used DST transitions for all years).
But I assume this is because it is a hypothesis test, that you might not have run into that before?
…itions/deltas to avoid slow python path) (pandas-dev#64379) Co-authored-by: mv-python <[email protected]> Co-authored-by: Joris Van den Bossche <[email protected]>
doc/source/whatsnew/vX.X.X.rstfile if fixing a bug or adding a new feature.AGENTS.md.