@@ -748,7 +748,7 @@ get_tzname(int dst)
748
748
#endif
749
749
750
750
#if defined(__APPLE__ ) && defined(ENABLE_MACOS_LOCALTIME_CACHE )
751
- static void invalidate_localtime_cache (void );
751
+ static void invalidate_offset_cache (void );
752
752
#endif
753
753
754
754
void
@@ -760,7 +760,7 @@ ruby_reset_timezone(const char *val)
760
760
#endif
761
761
ruby_reset_leap_second_info ();
762
762
#if defined(__APPLE__ ) && defined(ENABLE_MACOS_LOCALTIME_CACHE )
763
- invalidate_localtime_cache ();
763
+ invalidate_offset_cache ();
764
764
#endif
765
765
}
766
766
@@ -773,39 +773,186 @@ update_tz(void)
773
773
}
774
774
775
775
#if defined(__APPLE__ ) && defined(ENABLE_MACOS_LOCALTIME_CACHE )
776
- /* Use direct-mapped hash table for better performance with random access patterns
777
- * (e.g., when reading many timestamps from zip files) */
778
- #define LOCALTIME_CACHE_SIZE 32 /* Must be power of 2 */
779
- #define LOCALTIME_CACHE_ZONE_LENGTH 64
776
+ /* Offset-based cache: stores UTC-to-local offset per minute
777
+ * This handles leap seconds correctly since offset remains constant throughout a minute */
778
+ #define OFFSET_CACHE_SIZE 32 /* Must be power of 2 */
779
+ #define OFFSET_CACHE_ZONE_LENGTH 64
780
780
781
- /* Localtime cache structure keyed by time_t */
781
+ /* Cache key: minute precision (no seconds) */
782
782
typedef struct {
783
- time_t time ; /* the time_t value as key */
784
- struct tm local_tm ; /* cached localtime result */
783
+ int year ;
784
+ int month ;
785
+ int day ;
786
+ int hour ;
787
+ int minute ;
788
+ } offset_cache_key ;
789
+
790
+ /* Offset cache entry */
791
+ typedef struct {
792
+ offset_cache_key key ;
793
+ long gmtoff ; /* UTC to local offset in seconds */
794
+ int isdst ; /* DST flag */
785
795
int valid ;
786
796
#ifdef HAVE_STRUCT_TM_TM_ZONE
787
- char tm_zone [LOCALTIME_CACHE_ZONE_LENGTH ];
797
+ char tm_zone [OFFSET_CACHE_ZONE_LENGTH ];
788
798
#endif
789
- } localtime_cache_entry ;
799
+ } offset_cache_entry ;
790
800
791
- static localtime_cache_entry localtime_cache [ LOCALTIME_CACHE_SIZE ] = {{0 }};
801
+ static offset_cache_entry offset_cache [ OFFSET_CACHE_SIZE ] = {{0 }};
792
802
793
803
/* Invalidate the entire cache - called when timezone changes */
794
804
static void
795
- invalidate_localtime_cache (void )
805
+ invalidate_offset_cache (void )
796
806
{
797
- for (int i = 0 ; i < LOCALTIME_CACHE_SIZE ; i ++ ) {
798
- localtime_cache [i ].valid = 0 ;
807
+ for (int i = 0 ; i < OFFSET_CACHE_SIZE ; i ++ ) {
808
+ offset_cache [i ].valid = 0 ;
799
809
}
800
810
}
801
811
802
- /* Knuth multiplicative hash for time_t values */
812
+ /* Hash function for offset cache based on minute components */
803
813
static inline unsigned int
804
- localtime_cache_hash (time_t t ) {
805
- /* Knuth's multiplicative hash: multiply by golden ratio prime
806
- * For 32 entries (5 bits), shift right by 27 to get upper 5 bits */
807
- uint32_t val = (uint32_t )t ;
808
- return (uint32_t )((val * 2654435761U ) >> 27 ) & (LOCALTIME_CACHE_SIZE - 1 );
814
+ offset_cache_hash (const offset_cache_key * key )
815
+ {
816
+ /* Combine components into a single value for hashing */
817
+ uint32_t val = key -> year * 525600 + key -> month * 43200 +
818
+ key -> day * 1440 + key -> hour * 60 + key -> minute ;
819
+ /* Knuth multiplicative hash */
820
+ return (uint32_t )((val * 2654435761U ) >> 27 ) & (OFFSET_CACHE_SIZE - 1 );
821
+ }
822
+
823
+ /* Check if cache keys match */
824
+ static inline int
825
+ offset_cache_key_eq (const offset_cache_key * a , const offset_cache_key * b )
826
+ {
827
+ return a -> year == b -> year && a -> month == b -> month &&
828
+ a -> day == b -> day && a -> hour == b -> hour && a -> minute == b -> minute ;
829
+ }
830
+
831
+ /* Days in each month (non-leap year) */
832
+ static const int days_in_month [12 ] = {31 , 28 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31 };
833
+
834
+ /* Check if year is a leap year */
835
+ static inline int
836
+ is_leap_year (int year )
837
+ {
838
+ return (year % 4 == 0 && year % 100 != 0 ) || (year % 400 == 0 );
839
+ }
840
+
841
+ /* Get days in month, accounting for leap years */
842
+ static inline int
843
+ get_days_in_month (int year , int month )
844
+ {
845
+ if (month == 2 && is_leap_year (year )) {
846
+ return 29 ;
847
+ }
848
+ return days_in_month [month - 1 ];
849
+ }
850
+
851
+ /* Calculate day of week from year/month/day */
852
+ static int
853
+ calculate_wday (int year , int month , int day )
854
+ {
855
+ /* Zeller's congruence algorithm */
856
+ if (month < 3 ) {
857
+ month += 12 ;
858
+ year -- ;
859
+ }
860
+ int k = year % 100 ;
861
+ int j = year / 100 ;
862
+ int h = (day + (13 * (month + 1 )) / 5 + k + k / 4 + j / 4 - 2 * j ) % 7 ;
863
+ /* Convert to tm_wday format (Sunday = 0) */
864
+ return (h + 6 ) % 7 ;
865
+ }
866
+
867
+ /* Calculate day of year from year/month/day */
868
+ static int
869
+ calculate_yday (int year , int month , int day )
870
+ {
871
+ int yday = 0 ;
872
+ for (int m = 1 ; m < month ; m ++ ) {
873
+ yday += get_days_in_month (year , m );
874
+ }
875
+ return yday + day - 1 ; /* tm_yday is 0-based */
876
+ }
877
+
878
+ /* Apply offset to UTC time components */
879
+ static void
880
+ apply_tm_offset (struct tm * tm , long offset )
881
+ {
882
+ /* Break down offset into hours and minutes */
883
+ int offset_hours = (int )(offset / 3600 );
884
+ int offset_mins = (int )((offset % 3600 ) / 60 );
885
+ int offset_secs = (int )(offset % 60 );
886
+
887
+ /* Apply seconds offset */
888
+ tm -> tm_sec += offset_secs ;
889
+ if (tm -> tm_sec < 0 ) {
890
+ tm -> tm_sec += 60 ;
891
+ offset_mins -- ;
892
+ }
893
+ else if (tm -> tm_sec > 60 ) {
894
+ /* Preserve leap second (60) */
895
+ tm -> tm_sec -= 60 ;
896
+ offset_mins ++ ;
897
+ }
898
+
899
+ /* Apply minutes offset */
900
+ tm -> tm_min += offset_mins ;
901
+ if (tm -> tm_min < 0 ) {
902
+ tm -> tm_min += 60 ;
903
+ offset_hours -- ;
904
+ }
905
+ else if (tm -> tm_min >= 60 ) {
906
+ tm -> tm_min -= 60 ;
907
+ offset_hours ++ ;
908
+ }
909
+
910
+ /* Apply hours offset */
911
+ tm -> tm_hour += offset_hours ;
912
+ int day_delta = 0 ;
913
+ if (tm -> tm_hour < 0 ) {
914
+ day_delta = - ((23 - tm -> tm_hour ) / 24 );
915
+ tm -> tm_hour = ((tm -> tm_hour % 24 ) + 24 ) % 24 ;
916
+ }
917
+ else if (tm -> tm_hour >= 24 ) {
918
+ day_delta = tm -> tm_hour / 24 ;
919
+ tm -> tm_hour = tm -> tm_hour % 24 ;
920
+ }
921
+
922
+ /* Apply day offset if needed */
923
+ if (day_delta != 0 ) {
924
+ int year = tm -> tm_year + 1900 ;
925
+ int month = tm -> tm_mon + 1 ;
926
+ int day = tm -> tm_mday + day_delta ;
927
+
928
+ /* Handle month boundaries */
929
+ while (day < 1 ) {
930
+ month -- ;
931
+ if (month < 1 ) {
932
+ month = 12 ;
933
+ year -- ;
934
+ }
935
+ day += get_days_in_month (year , month );
936
+ }
937
+
938
+ while (day > get_days_in_month (year , month )) {
939
+ day -= get_days_in_month (year , month );
940
+ month ++ ;
941
+ if (month > 12 ) {
942
+ month = 1 ;
943
+ year ++ ;
944
+ }
945
+ }
946
+
947
+ /* Update tm structure */
948
+ tm -> tm_year = year - 1900 ;
949
+ tm -> tm_mon = month - 1 ;
950
+ tm -> tm_mday = day ;
951
+
952
+ /* Recalculate wday and yday */
953
+ tm -> tm_wday = calculate_wday (year , month , day );
954
+ tm -> tm_yday = calculate_yday (year , month , day );
955
+ }
809
956
}
810
957
#endif
811
958
@@ -817,35 +964,68 @@ rb_localtime_r(const time_t *t, struct tm *result)
817
964
#endif
818
965
819
966
#if defined(__APPLE__ ) && defined(ENABLE_MACOS_LOCALTIME_CACHE )
820
- unsigned int cache_idx = localtime_cache_hash (* t );
967
+ /* First get UTC time components */
968
+ struct tm utc_tm ;
969
+ if (!gmtime_r (t , & utc_tm )) {
970
+ return NULL ;
971
+ }
972
+
973
+ /* Create cache key from UTC time (minute precision) */
974
+ offset_cache_key key = {
975
+ .year = utc_tm .tm_year + 1900 ,
976
+ .month = utc_tm .tm_mon + 1 ,
977
+ .day = utc_tm .tm_mday ,
978
+ .hour = utc_tm .tm_hour ,
979
+ .minute = utc_tm .tm_min
980
+ };
981
+
982
+ unsigned int cache_idx = offset_cache_hash (& key );
983
+ offset_cache_entry * entry = & offset_cache [cache_idx ];
984
+
985
+ /* Check cache */
986
+ if (entry -> valid && offset_cache_key_eq (& entry -> key , & key )) {
987
+ /* Cache hit - apply cached offset to UTC time */
988
+ * result = utc_tm ; /* Start with UTC components */
989
+
990
+ /* Apply the offset directly without calling gmtime_r again */
991
+ apply_tm_offset (result , entry -> gmtoff );
821
992
822
- if (localtime_cache [cache_idx ].valid && localtime_cache [cache_idx ].time == * t ) {
823
- * result = localtime_cache [cache_idx ].local_tm ;
993
+ /* Set cached timezone info */
994
+ result -> tm_isdst = entry -> isdst ;
995
+ #ifdef HAVE_STRUCT_TM_TM_GMTOFF
996
+ result -> tm_gmtoff = entry -> gmtoff ;
997
+ #endif
824
998
#ifdef HAVE_STRUCT_TM_TM_ZONE
825
- result -> tm_zone = localtime_cache [ cache_idx ]. tm_zone ;
999
+ result -> tm_zone = entry -> tm_zone ;
826
1000
#endif
827
1001
return result ;
828
1002
}
829
1003
1004
+ /* Cache miss - call localtime_r */
830
1005
update_tz ();
831
1006
832
1007
if (!localtime_r (t , result )) {
833
1008
return NULL ;
834
1009
}
835
1010
836
- localtime_cache [cache_idx ].time = * t ;
837
- localtime_cache [cache_idx ].local_tm = * result ;
838
- localtime_cache [cache_idx ].valid = 1 ;
1011
+ /* Store in cache */
1012
+ entry -> key = key ;
1013
+ #ifdef HAVE_STRUCT_TM_TM_GMTOFF
1014
+ entry -> gmtoff = result -> tm_gmtoff ;
1015
+ #else
1016
+ /* Fallback: calculate offset if tm_gmtoff not available */
1017
+ entry -> gmtoff = mktime (result ) - * t ;
1018
+ #endif
1019
+ entry -> isdst = result -> tm_isdst ;
1020
+ entry -> valid = 1 ;
1021
+
839
1022
#ifdef HAVE_STRUCT_TM_TM_ZONE
840
- /* We must copy the timezone string because the pointer from localtime_r
841
- * points to static data that may be overwritten before we retrieve it
842
- * from the cache on a future call. */
843
1023
if (result -> tm_zone ) {
844
- strncpy (localtime_cache [ cache_idx ]. tm_zone , result -> tm_zone , LOCALTIME_CACHE_ZONE_LENGTH - 1 );
845
- localtime_cache [ cache_idx ]. tm_zone [LOCALTIME_CACHE_ZONE_LENGTH - 1 ] = '\0' ;
846
- localtime_cache [ cache_idx ]. local_tm . tm_zone = localtime_cache [ cache_idx ]. tm_zone ;
847
- } else {
848
- localtime_cache [ cache_idx ]. tm_zone [0 ] = '\0' ;
1024
+ strncpy (entry -> tm_zone , result -> tm_zone , OFFSET_CACHE_ZONE_LENGTH - 1 );
1025
+ entry -> tm_zone [OFFSET_CACHE_ZONE_LENGTH - 1 ] = '\0' ;
1026
+ }
1027
+ else {
1028
+ entry -> tm_zone [0 ] = '\0' ;
849
1029
}
850
1030
#endif
851
1031
0 commit comments