mirror of
https://github.com/ruby/ruby.git
synced 2025-08-15 13:39:04 +02:00
Merge c026384cc6
into a04555c8ab
This commit is contained in:
commit
400f7105a4
5 changed files with 514 additions and 0 deletions
33
benchmark/time_localtime.yml
Normal file
33
benchmark/time_localtime.yml
Normal file
|
@ -0,0 +1,33 @@
|
|||
prelude: |
|
||||
# frozen_string_literal: true
|
||||
# Benchmark for localtime cache performance
|
||||
# Tests various access patterns that benefit from caching
|
||||
|
||||
# Sequential timestamps (common in log processing)
|
||||
sequential_times = Array.new(100) { |i| Time.at(1609459200 + i) }
|
||||
|
||||
# Random timestamps (common in zip file processing)
|
||||
random_times = Array.new(100) { |i| Time.at(1609459200 + (i * 1103515245 + 12345) % 86400) }
|
||||
|
||||
# Repeated timestamp (common in batch processing)
|
||||
repeated_time = Time.at(1609459200)
|
||||
|
||||
benchmark:
|
||||
# Sequential access pattern - benefits from cache
|
||||
sequential: |
|
||||
sequential_times.each { |t| t.localtime }
|
||||
|
||||
# Random access pattern - tests cache distribution
|
||||
random: |
|
||||
random_times.each { |t| t.localtime }
|
||||
|
||||
# Repeated access - maximum cache benefit
|
||||
repeated: |
|
||||
100.times { repeated_time.localtime }
|
||||
|
||||
# Mixed pattern - realistic workload
|
||||
mixed: |
|
||||
50.times do |i|
|
||||
Time.at(1609459200 + i).localtime
|
||||
Time.at(1609459200 + i * 3600).localtime
|
||||
end
|
27
benchmark/time_localtime_fork.yml
Normal file
27
benchmark/time_localtime_fork.yml
Normal file
|
@ -0,0 +1,27 @@
|
|||
prelude: |
|
||||
# frozen_string_literal: true
|
||||
# Benchmark for localtime cache performance in forked processes
|
||||
# This specifically tests the macOS fork performance issue
|
||||
|
||||
require 'benchmark'
|
||||
|
||||
def benchmark_localtime(count)
|
||||
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
count.times do |i|
|
||||
Time.at(1609459200 + i % 10).localtime
|
||||
end
|
||||
Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
||||
end
|
||||
|
||||
benchmark:
|
||||
# Direct localtime calls (parent process)
|
||||
parent_process: |
|
||||
1000.times { |i| Time.at(1609459200 + i % 10).localtime }
|
||||
|
||||
# Localtime calls after fork (where cache provides most benefit)
|
||||
forked_process: |
|
||||
pid = fork do
|
||||
# Perform localtime calls in child process
|
||||
1000.times { |i| Time.at(1609459200 + i % 10).localtime }
|
||||
end
|
||||
Process.wait(pid)
|
14
configure.ac
14
configure.ac
|
@ -2254,6 +2254,20 @@ AC_CHECK_FUNCS(waitpid)
|
|||
AC_CHECK_FUNCS(__cospi)
|
||||
AC_CHECK_FUNCS(__sinpi)
|
||||
|
||||
AC_ARG_ENABLE(macos-localtime-cache,
|
||||
AS_HELP_STRING([--enable-macos-localtime-cache],
|
||||
[enable localtime cache optimization for macOS to improve forked process performance]),
|
||||
[macos_localtime_cache=$enableval], [macos_localtime_cache=yes])
|
||||
AS_IF([test "$macos_localtime_cache" = yes], [
|
||||
AS_CASE(["$target_os"],
|
||||
[darwin*], [
|
||||
AC_DEFINE(ENABLE_MACOS_LOCALTIME_CACHE, 1, [Enable macOS localtime cache optimization])
|
||||
AC_MSG_NOTICE([macOS localtime cache optimization enabled])
|
||||
],
|
||||
[AC_MSG_WARN([--enable-macos-localtime-cache is only supported on macOS, ignoring])]
|
||||
)
|
||||
])
|
||||
|
||||
AS_IF([test "x$ac_cv_member_struct_statx_stx_btime" = xyes],
|
||||
[AC_CHECK_FUNCS(statx)])
|
||||
|
||||
|
|
|
@ -206,6 +206,247 @@ class TestTimeTZ < Test::Unit::TestCase
|
|||
}
|
||||
end if has_lisbon_tz
|
||||
|
||||
def test_dublin_mean_time
|
||||
with_tz(tz="Europe/Dublin") {
|
||||
# Dublin Mean Time had an offset of -00:25:21 (25 minutes 21 seconds behind UTC)
|
||||
# from 1880 to 1916. Test creating times with this historical offset.
|
||||
t = Time.new(1910, 1, 1, 0, 0, 0, "-00:25:21")
|
||||
assert_equal(-1521, t.utc_offset, "UTC offset should be -1521 seconds")
|
||||
assert_equal("-00:25:21", t.strftime("%::z"), "Formatted offset should be -00:25:21")
|
||||
|
||||
# Test that Ruby correctly handles the conversion to/from UTC
|
||||
utc = Time.utc(1910, 1, 1, 0, 25, 21)
|
||||
dublin = utc.getlocal("-00:25:21")
|
||||
assert_equal(1910, dublin.year)
|
||||
assert_equal(1, dublin.month)
|
||||
assert_equal(1, dublin.day)
|
||||
assert_equal(0, dublin.hour)
|
||||
assert_equal(0, dublin.min)
|
||||
assert_equal(0, dublin.sec)
|
||||
assert_equal(-1521, dublin.utc_offset)
|
||||
|
||||
# Test arithmetic preserves the offset
|
||||
t2 = t + 3600
|
||||
assert_equal(-1521, t2.utc_offset, "Offset should be preserved after arithmetic")
|
||||
}
|
||||
end
|
||||
|
||||
def test_non_15minute_boundaries
|
||||
# Test various times around non-15-minute boundaries with unusual offsets
|
||||
# These test the caching mechanism's ability to handle offsets that don't
|
||||
# align with the 15-minute cache boundaries
|
||||
|
||||
# Test -00:25:21 offset at various minutes/seconds
|
||||
# This tests times that would fall into different 15-minute cache buckets
|
||||
with_tz(tz="Europe/Dublin") {
|
||||
# Test at 0:00 (cache bucket 0)
|
||||
t1 = Time.new(1910, 1, 1, 0, 0, 0, "-00:25:21")
|
||||
assert_equal(-1521, t1.utc_offset)
|
||||
|
||||
# Test at 0:14:59 (still cache bucket 0)
|
||||
t2 = Time.new(1910, 1, 1, 0, 14, 59, "-00:25:21")
|
||||
assert_equal(-1521, t2.utc_offset)
|
||||
|
||||
# Test at 0:15:00 (cache bucket 1)
|
||||
t3 = Time.new(1910, 1, 1, 0, 15, 0, "-00:25:21")
|
||||
assert_equal(-1521, t3.utc_offset)
|
||||
|
||||
# Test at 0:29:59 (still cache bucket 1)
|
||||
t4 = Time.new(1910, 1, 1, 0, 29, 59, "-00:25:21")
|
||||
assert_equal(-1521, t4.utc_offset)
|
||||
|
||||
# Test at 0:30:00 (cache bucket 2)
|
||||
t5 = Time.new(1910, 1, 1, 0, 30, 0, "-00:25:21")
|
||||
assert_equal(-1521, t5.utc_offset)
|
||||
|
||||
# Test at 0:44:59 (still cache bucket 2)
|
||||
t6 = Time.new(1910, 1, 1, 0, 44, 59, "-00:25:21")
|
||||
assert_equal(-1521, t6.utc_offset)
|
||||
|
||||
# Test at 0:45:00 (cache bucket 3)
|
||||
t7 = Time.new(1910, 1, 1, 0, 45, 0, "-00:25:21")
|
||||
assert_equal(-1521, t7.utc_offset)
|
||||
|
||||
# Test at 0:59:59 (still cache bucket 3)
|
||||
t8 = Time.new(1910, 1, 1, 0, 59, 59, "-00:25:21")
|
||||
assert_equal(-1521, t8.utc_offset)
|
||||
}
|
||||
|
||||
# Test some other unusual historical offsets
|
||||
# Amsterdam had +00:19:32 before 1892
|
||||
t_ams = Time.new(1890, 6, 15, 12, 30, 45, "+00:19:32")
|
||||
assert_equal(1172, t_ams.utc_offset, "Amsterdam offset should be 1172 seconds")
|
||||
|
||||
# Test arithmetic across cache boundaries with unusual offset
|
||||
t_start = Time.new(1910, 1, 1, 0, 14, 30, "-00:25:21")
|
||||
t_end = t_start + 90 # Move from bucket 0 to bucket 1
|
||||
assert_equal(-1521, t_start.utc_offset)
|
||||
assert_equal(-1521, t_end.utc_offset)
|
||||
assert_equal(16, t_end.min)
|
||||
assert_equal(0, t_end.sec)
|
||||
end
|
||||
|
||||
def test_dublin_dst_1916_transition
|
||||
# Test the critical DST transition in Dublin on May 21, 1916
|
||||
# Before: -0:25:21 (DMT)
|
||||
# After: -0:25:21 + 1:00 DST = +0:34:39 (IST)
|
||||
with_tz(tz="Europe/Dublin") {
|
||||
# Test before DST transition - different cache buckets
|
||||
t1 = Time.local(1916, 5, 21, 1, 0, 0)
|
||||
t2 = Time.local(1916, 5, 21, 1, 14, 59)
|
||||
t3 = Time.local(1916, 5, 21, 1, 15, 0)
|
||||
t4 = Time.local(1916, 5, 21, 1, 30, 0)
|
||||
t5 = Time.local(1916, 5, 21, 1, 45, 0)
|
||||
t6 = Time.local(1916, 5, 21, 1, 59, 59)
|
||||
|
||||
# All should have -0:25:21 offset before transition
|
||||
assert_equal(-1521, t1.utc_offset, "1:00 should be -0:25:21")
|
||||
assert_equal(-1521, t2.utc_offset, "1:14:59 should be -0:25:21")
|
||||
assert_equal(-1521, t3.utc_offset, "1:15 should be -0:25:21")
|
||||
assert_equal(-1521, t4.utc_offset, "1:30 should be -0:25:21")
|
||||
assert_equal(-1521, t5.utc_offset, "1:45 should be -0:25:21")
|
||||
assert_equal(-1521, t6.utc_offset, "1:59:59 should be -0:25:21")
|
||||
|
||||
# Test after DST transition - different cache buckets
|
||||
t7 = Time.local(1916, 5, 21, 3, 0, 0)
|
||||
t8 = Time.local(1916, 5, 21, 3, 14, 59)
|
||||
t9 = Time.local(1916, 5, 21, 3, 15, 0)
|
||||
t10 = Time.local(1916, 5, 21, 3, 30, 0)
|
||||
t11 = Time.local(1916, 5, 21, 3, 45, 0)
|
||||
t12 = Time.local(1916, 5, 21, 3, 59, 59)
|
||||
|
||||
# All should have +0:34:39 offset after transition
|
||||
expected_offset = 2079 # -1521 + 3600 = 2079 seconds
|
||||
assert_equal(expected_offset, t7.utc_offset, "3:00 should be +0:34:39")
|
||||
assert_equal(expected_offset, t8.utc_offset, "3:14:59 should be +0:34:39")
|
||||
assert_equal(expected_offset, t9.utc_offset, "3:15 should be +0:34:39")
|
||||
assert_equal(expected_offset, t10.utc_offset, "3:30 should be +0:34:39")
|
||||
assert_equal(expected_offset, t11.utc_offset, "3:45 should be +0:34:39")
|
||||
assert_equal(expected_offset, t12.utc_offset, "3:59:59 should be +0:34:39")
|
||||
|
||||
# Verify DST flag
|
||||
assert_equal(false, t1.dst?, "Before transition should not be DST")
|
||||
assert_equal(true, t7.dst?, "After transition should be DST")
|
||||
|
||||
# Test times in October when DST ends
|
||||
t13 = Time.local(1916, 10, 1, 1, 0, 0)
|
||||
assert_equal(expected_offset, t13.utc_offset, "October 1 01:00 should still be DST")
|
||||
assert_equal(true, t13.dst?, "October 1 01:00 should still be DST")
|
||||
|
||||
# After DST ends - Dublin transitions to GMT (0 offset), not back to DMT
|
||||
t14 = Time.local(1916, 10, 1, 3, 0, 0)
|
||||
assert_equal(0, t14.utc_offset, "October 1 03:00 should be GMT")
|
||||
assert_equal(false, t14.dst?, "October 1 03:00 should not be DST")
|
||||
}
|
||||
end
|
||||
|
||||
def test_sub_minute_transitions
|
||||
# Test timezone transitions that occur at non-standard seconds
|
||||
# Initially we thought these would break the 15-minute cache, but the cache
|
||||
# is actually keyed by UTC time, not local time, so it handles these correctly!
|
||||
|
||||
# Dublin October 1, 1916: Transitions at 02:25:21 local time
|
||||
# From IST (+00:34:39) to GMT (+00:00:00)
|
||||
# Times fall back from 02:59:59 IST to 02:25:21 GMT
|
||||
with_tz(tz="Europe/Dublin") {
|
||||
# The problematic case: times in the same 15-minute bucket with different offsets
|
||||
# During the "fall back", times from 02:25:21 to 02:59:59 occur twice:
|
||||
# - First as IST (before transition)
|
||||
# - Then as GMT (after transition)
|
||||
|
||||
# Test the repeated hour during fall back
|
||||
# 02:30:00 can be either IST or GMT
|
||||
t_ist = Time.new(1916, 10, 1, 2, 30, 0, :dst) # Force DST=true (IST)
|
||||
t_gmt = Time.new(1916, 10, 1, 2, 30, 0, :std) # Force DST=false (GMT)
|
||||
|
||||
# These have different offsets and fall in different UTC cache buckets:
|
||||
# 02:30:00 IST = 01:55:21 UTC (bucket 01:45-01:59)
|
||||
# 02:30:00 GMT = 02:30:00 UTC (bucket 02:30-02:44)
|
||||
assert_equal(2079, t_ist.utc_offset, "02:30:00 IST should have offset +2079")
|
||||
assert_equal(0, t_gmt.utc_offset, "02:30:00 GMT should have offset 0")
|
||||
|
||||
# Test more times in the same bucket
|
||||
t_ist2 = Time.new(1916, 10, 1, 2, 44, 59, :dst) # Last second of bucket, IST
|
||||
t_gmt2 = Time.new(1916, 10, 1, 2, 44, 59, :std) # Last second of bucket, GMT
|
||||
|
||||
assert_equal(2079, t_ist2.utc_offset, "02:44:59 IST should have offset +2079")
|
||||
assert_equal(0, t_gmt2.utc_offset, "02:44:59 GMT should have offset 0")
|
||||
}
|
||||
|
||||
# Monrovia January 7, 1972: Transitions at 00:44:30 local time
|
||||
# From MMT (-00:44:30) to GMT (+00:00:00)
|
||||
with_tz(tz="Africa/Monrovia") {
|
||||
# Create UTC times that correspond to just before and after the transition
|
||||
# 23:59:59 MMT (Jan 6) = 00:44:29 UTC (Jan 7) (offset -2670)
|
||||
# 00:44:30 GMT (Jan 7) = 00:44:30 UTC (Jan 7) (offset 0)
|
||||
utc_before = Time.utc(1972, 1, 7, 0, 44, 29)
|
||||
utc_after = Time.utc(1972, 1, 7, 0, 44, 30)
|
||||
|
||||
# Convert to local times
|
||||
local_before = utc_before.getlocal
|
||||
local_after = utc_after.getlocal
|
||||
|
||||
# Check the times
|
||||
assert_equal(6, local_before.day, "Before transition should be Jan 6")
|
||||
assert_equal(23, local_before.hour, "Before transition should be hour 23")
|
||||
assert_equal(59, local_before.min, "Before transition should be minute 59")
|
||||
assert_equal(59, local_before.sec, "Before transition should be second 59")
|
||||
|
||||
assert_equal(7, local_after.day, "After transition should be Jan 7")
|
||||
assert_equal(0, local_after.hour, "After transition should be hour 0")
|
||||
assert_equal(44, local_after.min, "After transition should be minute 44")
|
||||
assert_equal(30, local_after.sec, "After transition should be second 30")
|
||||
|
||||
# Check offsets
|
||||
assert_equal(-2670, local_before.utc_offset, "23:59:59 should have MMT offset -2670")
|
||||
assert_equal(0, local_after.utc_offset, "00:44:30 should have GMT offset 0")
|
||||
|
||||
# Test creating local times directly
|
||||
# These are in the same cache bucket (00:30:00 - 00:44:59)
|
||||
t1 = Time.local(1972, 1, 7, 0, 44, 29)
|
||||
t2 = Time.local(1972, 1, 7, 0, 44, 30)
|
||||
|
||||
# With a 15-minute cache, these might incorrectly return the same offset
|
||||
# The correct behavior is:
|
||||
# Note: 00:44:29 doesn't exist in local time (it's during the skipped period)
|
||||
# so Ruby might adjust it
|
||||
assert_equal(0, t2.utc_offset, "Local 00:44:30 should have GMT offset 0")
|
||||
}
|
||||
end
|
||||
|
||||
def test_europe_astrakhan
|
||||
# Test the Europe/Astrakhan timezone transition on Apr 30, 1924
|
||||
# LMT offset: +3:12:12 (11532 seconds)
|
||||
# At Wed Apr 30 20:47:47 1924 UT, time changes from LMT to +03
|
||||
with_tz(tz="Europe/Astrakhan") {
|
||||
# Test right before the transition
|
||||
# Wed Apr 30 20:47:47 1924 UT = Wed Apr 30 23:59:59 1924 LMT
|
||||
utc_before = Time.utc(1924, 4, 30, 20, 47, 47)
|
||||
local_before = utc_before.getlocal
|
||||
|
||||
assert_equal(30, local_before.day)
|
||||
assert_equal(4, local_before.month)
|
||||
assert_equal(1924, local_before.year)
|
||||
assert_equal(23, local_before.hour)
|
||||
assert_equal(59, local_before.min)
|
||||
assert_equal(59, local_before.sec)
|
||||
assert_equal(11532, local_before.utc_offset, "Before transition should have LMT offset +3:12:12 (11532 seconds)")
|
||||
|
||||
# Test right after the transition
|
||||
# Wed Apr 30 20:47:48 1924 UT = Wed Apr 30 23:47:48 1924 +03
|
||||
utc_after = Time.utc(1924, 4, 30, 20, 47, 48)
|
||||
local_after = utc_after.getlocal
|
||||
|
||||
assert_equal(30, local_after.day)
|
||||
assert_equal(4, local_after.month)
|
||||
assert_equal(1924, local_after.year)
|
||||
assert_equal(23, local_after.hour)
|
||||
assert_equal(47, local_after.min)
|
||||
assert_equal(48, local_after.sec)
|
||||
assert_equal(10800, local_after.utc_offset, "After transition should have +03 offset (10800 seconds)")
|
||||
}
|
||||
end
|
||||
|
||||
def test_pacific_kiritimati
|
||||
with_tz(tz="Pacific/Kiritimati") {
|
||||
assert_time_constructor(tz, "1994-12-30 00:00:00 -1000", :local, [1994,12,30,0,0,0])
|
||||
|
|
199
time.c
199
time.c
|
@ -18,6 +18,7 @@
|
|||
#include <math.h>
|
||||
#include <time.h>
|
||||
#include <sys/types.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef HAVE_UNISTD_H
|
||||
# include <unistd.h>
|
||||
|
@ -703,6 +704,12 @@ static struct vtm *localtimew(wideval_t timew, struct vtm *result);
|
|||
|
||||
static int leap_year_p(long y);
|
||||
#define leap_year_v_p(y) leap_year_p(NUM2LONG(modv((y), INT2FIX(400))))
|
||||
static int calc_tm_yday(long tm_year, int tm_mon, int tm_mday);
|
||||
static int calc_wday(int year_mod400, int month, int day);
|
||||
|
||||
#if defined(__APPLE__) && defined(ENABLE_MACOS_LOCALTIME_CACHE)
|
||||
static void apply_tm_offset(struct tm *tm, long offset);
|
||||
#endif
|
||||
|
||||
static VALUE tm_from_time(VALUE klass, VALUE time);
|
||||
|
||||
|
@ -746,6 +753,10 @@ get_tzname(int dst)
|
|||
}
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__) && defined(ENABLE_MACOS_LOCALTIME_CACHE)
|
||||
static void invalidate_offset_cache(void);
|
||||
#endif
|
||||
|
||||
void
|
||||
ruby_reset_timezone(const char *val)
|
||||
{
|
||||
|
@ -754,6 +765,9 @@ ruby_reset_timezone(const char *val)
|
|||
w32_tz.use_tzkey = !val || !*val;
|
||||
#endif
|
||||
ruby_reset_leap_second_info();
|
||||
#if defined(__APPLE__) && defined(ENABLE_MACOS_LOCALTIME_CACHE)
|
||||
invalidate_offset_cache();
|
||||
#endif
|
||||
}
|
||||
|
||||
static void
|
||||
|
@ -764,12 +778,113 @@ update_tz(void)
|
|||
tzset();
|
||||
}
|
||||
|
||||
#if defined(__APPLE__) && defined(ENABLE_MACOS_LOCALTIME_CACHE)
|
||||
/* Offset-based cache: stores UTC-to-local offset per 15-minute interval
|
||||
* This handles leap seconds correctly since offset remains constant throughout a minute
|
||||
* All timezone offsets are on 15-minute boundaries (00, 15, 30, 45) which improves
|
||||
* cache effectiveness by 15x compared to per-minute caching */
|
||||
#define OFFSET_CACHE_SIZE 64 /* Must be power of 2 - increased for better coverage */
|
||||
#define OFFSET_CACHE_ZONE_LENGTH 64
|
||||
|
||||
/* Offset cache entry */
|
||||
typedef struct {
|
||||
time_t key;
|
||||
long gmtoff; /* UTC to local offset in seconds */
|
||||
int isdst; /* DST flag */
|
||||
int valid;
|
||||
#ifdef HAVE_STRUCT_TM_TM_ZONE
|
||||
char* tm_zone;
|
||||
#endif
|
||||
} offset_cache_entry;
|
||||
|
||||
static offset_cache_entry offset_cache[OFFSET_CACHE_SIZE] = {{0}};
|
||||
|
||||
/* Invalidate the entire cache - called when timezone changes */
|
||||
static void
|
||||
invalidate_offset_cache(void)
|
||||
{
|
||||
for (int i = 0; i < OFFSET_CACHE_SIZE; i++) {
|
||||
offset_cache[i].valid = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hash function for offset cache based on 15-minute intervals */
|
||||
static inline unsigned int
|
||||
offset_cache_hash(const time_t key)
|
||||
{
|
||||
/* Knuth multiplicative hash */
|
||||
return (uint32_t)((key * 2654435761U) >> 26) & (OFFSET_CACHE_SIZE - 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
static struct tm *
|
||||
rb_localtime_r(const time_t *t, struct tm *result)
|
||||
{
|
||||
#if defined __APPLE__ && defined __LP64__
|
||||
if (*t != (time_t)(int)*t) return NULL;
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__) && defined(ENABLE_MACOS_LOCALTIME_CACHE)
|
||||
/* First get UTC time components */
|
||||
struct tm utc_tm;
|
||||
if (!gmtime_r(t, &utc_tm)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Create cache key from UTC time aligned to one-minute boundaries */
|
||||
time_t key = (*t) / (60);
|
||||
if (*t < 0 && (*t) % (60) != 0) {
|
||||
key--; /* Floor division adjustment for negative timestamps */
|
||||
}
|
||||
|
||||
unsigned int cache_idx = offset_cache_hash(key);
|
||||
offset_cache_entry *entry = &offset_cache[cache_idx];
|
||||
|
||||
/* Check cache */
|
||||
if (entry->valid && entry->key == key) {
|
||||
/* Cache hit - apply cached offset to UTC time */
|
||||
*result = utc_tm; /* Start with UTC components */
|
||||
apply_tm_offset(result, entry->gmtoff);
|
||||
|
||||
/* Set cached timezone info */
|
||||
result->tm_isdst = entry->isdst;
|
||||
#ifdef HAVE_STRUCT_TM_TM_GMTOFF
|
||||
result->tm_gmtoff = entry->gmtoff;
|
||||
#endif
|
||||
#ifdef HAVE_STRUCT_TM_TM_ZONE
|
||||
result->tm_zone = entry->tm_zone;
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Cache miss - call localtime_r */
|
||||
update_tz();
|
||||
|
||||
if (!localtime_r(t, result)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#ifdef HAVE_STRUCT_TM_TM_GMTOFF
|
||||
long gmtoff = result->tm_gmtoff;
|
||||
#else
|
||||
/* Fallback: calculate offset if tm_gmtoff not available */
|
||||
long gmtoff = mktime(result) - *t;
|
||||
#endif
|
||||
|
||||
/* Store in cache as long as offset is a multiple of one minute */
|
||||
if ((gmtoff % (60)) == 0) {
|
||||
entry->key = key;
|
||||
entry->gmtoff = gmtoff;
|
||||
entry->isdst = result->tm_isdst;
|
||||
entry->valid = 1;
|
||||
|
||||
#ifdef HAVE_STRUCT_TM_TM_ZONE
|
||||
entry->tm_zone = result->tm_zone;
|
||||
#endif
|
||||
}
|
||||
|
||||
return result;
|
||||
#else
|
||||
update_tz();
|
||||
#ifdef HAVE_GMTIME_R
|
||||
result = localtime_r(t, result);
|
||||
|
@ -779,6 +894,7 @@ rb_localtime_r(const time_t *t, struct tm *result)
|
|||
if (tmp) *result = *tmp;
|
||||
}
|
||||
#endif
|
||||
#endif /* __APPLE__ */
|
||||
#if defined(HAVE_MKTIME) && defined(LOCALTIME_OVERFLOW_PROBLEM)
|
||||
if (result) {
|
||||
long gmtoff1 = 0;
|
||||
|
@ -860,6 +976,88 @@ static const int8_t leap_year_days_in_month[] = {
|
|||
#define days_in_month_in(y) days_in_month_of(leap_year_p(y))
|
||||
#define days_in_month_in_v(y) days_in_month_of(leap_year_v_p(y))
|
||||
|
||||
#if defined(__APPLE__) && defined(ENABLE_MACOS_LOCALTIME_CACHE)
|
||||
/* Apply offset to UTC time components */
|
||||
static void
|
||||
apply_tm_offset(struct tm *tm, long offset)
|
||||
{
|
||||
/* Break down offset into hours and minutes */
|
||||
int offset_hours = (int)(offset / 3600);
|
||||
int offset_mins = (int)((offset % 3600) / 60);
|
||||
int offset_secs = (int)(offset % 60);
|
||||
|
||||
/* Apply seconds offset */
|
||||
tm->tm_sec += offset_secs;
|
||||
if (tm->tm_sec < 0) {
|
||||
tm->tm_sec += 60;
|
||||
offset_mins--;
|
||||
}
|
||||
else if (tm->tm_sec > 60) {
|
||||
/* Preserve leap second (60) */
|
||||
tm->tm_sec -= 60;
|
||||
offset_mins++;
|
||||
}
|
||||
|
||||
/* Apply minutes offset */
|
||||
tm->tm_min += offset_mins;
|
||||
if (tm->tm_min < 0) {
|
||||
tm->tm_min += 60;
|
||||
offset_hours--;
|
||||
}
|
||||
else if (tm->tm_min >= 60) {
|
||||
tm->tm_min -= 60;
|
||||
offset_hours++;
|
||||
}
|
||||
|
||||
/* Apply hours offset */
|
||||
tm->tm_hour += offset_hours;
|
||||
int day_delta = 0;
|
||||
if (tm->tm_hour < 0) {
|
||||
day_delta = -((23 - tm->tm_hour) / 24);
|
||||
tm->tm_hour = ((tm->tm_hour % 24) + 24) % 24;
|
||||
}
|
||||
else if (tm->tm_hour >= 24) {
|
||||
day_delta = tm->tm_hour / 24;
|
||||
tm->tm_hour = tm->tm_hour % 24;
|
||||
}
|
||||
|
||||
/* Apply day offset if needed */
|
||||
if (day_delta != 0) {
|
||||
int year = tm->tm_year + 1900;
|
||||
int month = tm->tm_mon + 1;
|
||||
int day = tm->tm_mday + day_delta;
|
||||
|
||||
/* Handle month boundaries */
|
||||
while (day < 1) {
|
||||
month--;
|
||||
if (month < 1) {
|
||||
month = 12;
|
||||
year--;
|
||||
}
|
||||
day += days_in_month_in(year)[month - 1];
|
||||
}
|
||||
|
||||
while (day > days_in_month_in(year)[month - 1]) {
|
||||
day -= days_in_month_in(year)[month - 1];
|
||||
month++;
|
||||
if (month > 12) {
|
||||
month = 1;
|
||||
year++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Update tm structure */
|
||||
tm->tm_year = year - 1900;
|
||||
tm->tm_mon = month - 1;
|
||||
tm->tm_mday = day;
|
||||
|
||||
/* Recalculate wday and yday */
|
||||
tm->tm_wday = calc_wday(year % 400, month, day);
|
||||
tm->tm_yday = calc_tm_yday(tm->tm_year, tm->tm_mon, tm->tm_mday);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#define M28(m) \
|
||||
(m),(m),(m),(m),(m),(m),(m),(m),(m),(m), \
|
||||
(m),(m),(m),(m),(m),(m),(m),(m),(m),(m), \
|
||||
|
@ -951,6 +1149,7 @@ timegmw_noleapsecond(struct vtm *vtm)
|
|||
divmodv(year1900, INT2FIX(400), &q400, &r400);
|
||||
year_mod400 = NUM2INT(r400);
|
||||
|
||||
/* TODO - should this be year1900? */
|
||||
yday = calc_tm_yday(year_mod400, vtm->mon-1, vtm->mday);
|
||||
|
||||
/*
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue