Skip to content

Commit e8595b9

Browse files
committed
implement offset based cache
1 parent fb37fc9 commit e8595b9

File tree

1 file changed

+216
-36
lines changed

1 file changed

+216
-36
lines changed

time.c

Lines changed: 216 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,7 @@ get_tzname(int dst)
748748
#endif
749749

750750
#if defined(__APPLE__) && defined(ENABLE_MACOS_LOCALTIME_CACHE)
751-
static void invalidate_localtime_cache(void);
751+
static void invalidate_offset_cache(void);
752752
#endif
753753

754754
void
@@ -760,7 +760,7 @@ ruby_reset_timezone(const char *val)
760760
#endif
761761
ruby_reset_leap_second_info();
762762
#if defined(__APPLE__) && defined(ENABLE_MACOS_LOCALTIME_CACHE)
763-
invalidate_localtime_cache();
763+
invalidate_offset_cache();
764764
#endif
765765
}
766766

@@ -773,39 +773,186 @@ update_tz(void)
773773
}
774774

775775
#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
780780

781-
/* Localtime cache structure keyed by time_t */
781+
/* Cache key: minute precision (no seconds) */
782782
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 */
785795
int valid;
786796
#ifdef HAVE_STRUCT_TM_TM_ZONE
787-
char tm_zone[LOCALTIME_CACHE_ZONE_LENGTH];
797+
char tm_zone[OFFSET_CACHE_ZONE_LENGTH];
788798
#endif
789-
} localtime_cache_entry;
799+
} offset_cache_entry;
790800

791-
static localtime_cache_entry localtime_cache[LOCALTIME_CACHE_SIZE] = {{0}};
801+
static offset_cache_entry offset_cache[OFFSET_CACHE_SIZE] = {{0}};
792802

793803
/* Invalidate the entire cache - called when timezone changes */
794804
static void
795-
invalidate_localtime_cache(void)
805+
invalidate_offset_cache(void)
796806
{
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;
799809
}
800810
}
801811

802-
/* Knuth multiplicative hash for time_t values */
812+
/* Hash function for offset cache based on minute components */
803813
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+
}
809956
}
810957
#endif
811958

@@ -817,35 +964,68 @@ rb_localtime_r(const time_t *t, struct tm *result)
817964
#endif
818965

819966
#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);
821992

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
824998
#ifdef HAVE_STRUCT_TM_TM_ZONE
825-
result->tm_zone = localtime_cache[cache_idx].tm_zone;
999+
result->tm_zone = entry->tm_zone;
8261000
#endif
8271001
return result;
8281002
}
8291003

1004+
/* Cache miss - call localtime_r */
8301005
update_tz();
8311006

8321007
if (!localtime_r(t, result)) {
8331008
return NULL;
8341009
}
8351010

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+
8391022
#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. */
8431023
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';
8491029
}
8501030
#endif
8511031

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy