DateTime.h (17222B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * vim: set ts=8 sts=2 et sw=2 tw=80: 3 * This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #ifndef vm_DateTime_h 8 #define vm_DateTime_h 9 10 #include "mozilla/Atomics.h" 11 #include "mozilla/RefPtr.h" 12 #include "mozilla/UniquePtr.h" 13 14 #include <stdint.h> 15 16 #include "js/AllocPolicy.h" 17 #include "js/RealmOptions.h" 18 #include "js/Utility.h" 19 #include "js/Vector.h" 20 #include "threading/ExclusiveData.h" 21 22 #if JS_HAS_INTL_API 23 # include "mozilla/intl/ICUError.h" 24 # include "mozilla/intl/TimeZone.h" 25 #endif 26 27 namespace JS { 28 class Realm; 29 } 30 31 namespace js { 32 33 /** 34 * 21.4.1.2 Time-related Constants 35 * 36 * ES2025 draft rev 76814cbd5d7842c2a99d28e6e8c7833f1de5bee0 37 */ 38 constexpr int32_t HoursPerDay = 24; 39 constexpr int32_t MinutesPerHour = 60; 40 constexpr int32_t SecondsPerMinute = 60; 41 constexpr int32_t msPerSecond = 1000; 42 constexpr int32_t msPerMinute = msPerSecond * SecondsPerMinute; 43 constexpr int32_t msPerHour = msPerMinute * MinutesPerHour; 44 constexpr int32_t msPerDay = msPerHour * HoursPerDay; 45 46 /* 47 * Additional quantities not mentioned in the spec. 48 */ 49 constexpr int32_t SecondsPerHour = 60 * 60; 50 constexpr int32_t SecondsPerDay = SecondsPerHour * 24; 51 52 constexpr double StartOfTime = -8.64e15; 53 constexpr double EndOfTime = 8.64e15; 54 55 extern bool InitDateTimeState(); 56 57 extern void FinishDateTimeState(); 58 59 enum class ResetTimeZoneMode : bool { 60 DontResetIfOffsetUnchanged, 61 ResetEvenIfOffsetUnchanged, 62 }; 63 64 /** 65 * Engine-internal variant of JS::ResetTimeZone with an additional flag to 66 * control whether to forcibly reset all time zone data (this is the default 67 * behavior when calling JS::ResetTimeZone) or to try to reuse the previous 68 * time zone data. 69 */ 70 extern void ResetTimeZoneInternal(ResetTimeZoneMode mode); 71 72 using TimeZoneDisplayNameVector = Vector<char16_t, 100, SystemAllocPolicy>; 73 74 #if JS_HAS_INTL_API 75 using TimeZoneIdentifierVector = 76 Vector<char, mozilla::intl::TimeZone::TimeZoneIdentifierLength, 77 SystemAllocPolicy>; 78 #endif 79 80 /** 81 * Stores date/time information, particularly concerning the current local 82 * time zone, and implements a small cache for daylight saving time offset 83 * computation. 84 * 85 * The basic idea is premised upon this fact: the DST offset never changes more 86 * than once in any thirty-day period. If we know the offset at t_0 is o_0, 87 * the offset at [t_1, t_2] is also o_0, where t_1 + 3_0 days == t_2, 88 * t_1 <= t_0, and t0 <= t2. (In other words, t_0 is always somewhere within a 89 * thirty-day range where the DST offset is constant: DST changes never occur 90 * more than once in any thirty-day period.) Therefore, if we intelligently 91 * retain knowledge of the offset for a range of dates (which may vary over 92 * time), and if requests are usually for dates within that range, we can often 93 * provide a response without repeated offset calculation. 94 * 95 * Our caching strategy is as follows: on the first request at date t_0 compute 96 * the requested offset o_0. Save { start: t_0, end: t_0, offset: o_0 } as the 97 * cache's state. Subsequent requests within that range are straightforwardly 98 * handled. If a request for t_i is far outside the range (more than thirty 99 * days), compute o_i = dstOffset(t_i) and save { start: t_i, end: t_i, 100 * offset: t_i }. Otherwise attempt to *overextend* the range to either 101 * [start - 30d, end] or [start, end + 30d] as appropriate to encompass 102 * t_i. If the offset o_i30 is the same as the cached offset, extend the 103 * range. Otherwise the over-guess crossed a DST change -- compute 104 * o_i = dstOffset(t_i) and either extend the original range (if o_i == offset) 105 * or start a new one beneath/above the current one with o_i30 as the offset. 106 * 107 * This cache strategy results in 0 to 2 DST offset computations. The naive 108 * always-compute strategy is 1 computation, and since cache maintenance is a 109 * handful of integer arithmetic instructions the speed difference between 110 * always-1 and 1-with-cache is negligible. Caching loses if two computations 111 * happen: when the date is within 30 days of the cached range and when that 112 * 30-day range crosses a DST change. This is relatively uncommon. Further, 113 * instances of such are often dominated by in-range hits, so caching is an 114 * overall slight win. 115 * 116 * Why 30 days? For correctness the duration must be smaller than any possible 117 * duration between DST changes. Past that, note that 1) a large duration 118 * increases the likelihood of crossing a DST change while reducing the number 119 * of cache misses, and 2) a small duration decreases the size of the cached 120 * range while producing more misses. Using a month as the interval change is 121 * a balance between these two that tries to optimize for the calendar month at 122 * a time that a site might display. (One could imagine an adaptive duration 123 * that accommodates near-DST-change dates better; we don't believe the 124 * potential win from better caching offsets the loss from extra complexity.) 125 */ 126 class DateTimeInfo { 127 // DateTimeInfo for the default time zone. 128 static ExclusiveData<DateTimeInfo>* instance; 129 130 static constexpr int32_t InvalidOffset = INT32_MIN; 131 132 // Additional cache to avoid the mutex overhead. Uses "relaxed" semantics 133 // because it's acceptable if time zone offset changes aren't propagated right 134 // away to all other threads. 135 static inline mozilla::Atomic<int32_t, mozilla::Relaxed> 136 utcToLocalOffsetSeconds{InvalidOffset}; 137 138 friend class ExclusiveData<DateTimeInfo>; 139 140 friend bool InitDateTimeState(); 141 friend void FinishDateTimeState(); 142 143 DateTimeInfo(); 144 145 static auto acquireLockWithValidTimeZone() { 146 auto guard = instance->lock(); 147 if (guard->timeZoneStatus_ != TimeZoneStatus::Valid) { 148 guard->updateTimeZone(); 149 } 150 return guard; 151 } 152 153 public: 154 #if JS_HAS_INTL_API 155 explicit DateTimeInfo(RefPtr<JS::TimeZoneString> timeZone); 156 #endif 157 ~DateTimeInfo(); 158 159 // The spec implicitly assumes DST and time zone adjustment information 160 // never change in the course of a function -- sometimes even across 161 // reentrancy. So make critical sections as narrow as possible. 162 163 /** 164 * Get the DST offset in milliseconds at a UTC time. This is usually 165 * either 0 or |msPerSecond * SecondsPerHour|, but at least one exotic time 166 * zone (Lord Howe Island, Australia) has a fractional-hour offset, just to 167 * keep things interesting. 168 */ 169 static int32_t getDSTOffsetMilliseconds(DateTimeInfo* dtInfo, 170 int64_t utcMilliseconds) { 171 if (MOZ_UNLIKELY(dtInfo)) { 172 return dtInfo->internalGetDSTOffsetMilliseconds(utcMilliseconds); 173 } 174 auto guard = acquireLockWithValidTimeZone(); 175 return guard->internalGetDSTOffsetMilliseconds(utcMilliseconds); 176 } 177 178 /** 179 * The offset in seconds from the current UTC time to the current local 180 * standard time (i.e. not including any offset due to DST) as computed by the 181 * operating system. 182 */ 183 static int32_t utcToLocalStandardOffsetSeconds() { 184 // First try the cached offset to avoid any mutex overhead. 185 int32_t offset = utcToLocalOffsetSeconds; 186 if (offset != InvalidOffset) { 187 return offset; 188 } 189 190 // If that fails, use the mutex-synchronized code path. 191 auto guard = acquireLockWithValidTimeZone(); 192 offset = guard->utcToLocalStandardOffsetSeconds_; 193 utcToLocalOffsetSeconds = offset; 194 return offset; 195 } 196 197 /** 198 * Cache key for this date-time info. Returns a different value when the 199 * time zone changed. 200 */ 201 static int32_t timeZoneCacheKey(DateTimeInfo* dtInfo) { 202 if (MOZ_UNLIKELY(dtInfo)) { 203 // |utcToLocalStandardOffsetSeconds_| is incremented when the time zone 204 // override is modified. 205 return dtInfo->utcToLocalStandardOffsetSeconds_; 206 } 207 208 // Use the offset as the cache key for the default time zone. 209 return utcToLocalStandardOffsetSeconds(); 210 } 211 212 enum class TimeZoneOffset { UTC, Local }; 213 214 #if JS_HAS_INTL_API 215 /** 216 * Return the time zone offset, including DST, in milliseconds at the 217 * given time. The input time can be either at UTC or at local time. 218 */ 219 static int32_t getOffsetMilliseconds(DateTimeInfo* dtInfo, 220 int64_t milliseconds, 221 TimeZoneOffset offset) { 222 if (MOZ_UNLIKELY(dtInfo)) { 223 return dtInfo->internalGetOffsetMilliseconds(milliseconds, offset); 224 } 225 auto guard = acquireLockWithValidTimeZone(); 226 return guard->internalGetOffsetMilliseconds(milliseconds, offset); 227 } 228 229 /** 230 * Copy the display name for the current time zone at the given time, 231 * localized for the specified locale, into the supplied vector. 232 */ 233 static bool timeZoneDisplayName(DateTimeInfo* dtInfo, 234 TimeZoneDisplayNameVector& result, 235 int64_t utcMilliseconds, const char* locale) { 236 if (MOZ_UNLIKELY(dtInfo)) { 237 return dtInfo->internalTimeZoneDisplayName(result, utcMilliseconds, 238 locale); 239 } 240 auto guard = acquireLockWithValidTimeZone(); 241 return guard->internalTimeZoneDisplayName(result, utcMilliseconds, locale); 242 } 243 244 /** 245 * Copy the identifier for the current time zone into the supplied vector. 246 */ 247 static bool timeZoneId(DateTimeInfo* dtInfo, 248 TimeZoneIdentifierVector& result) { 249 if (MOZ_UNLIKELY(dtInfo)) { 250 return dtInfo->internalTimeZoneId(result); 251 } 252 auto guard = acquireLockWithValidTimeZone(); 253 return guard->internalTimeZoneId(result); 254 } 255 256 /** 257 * A number indicating the raw offset from GMT in milliseconds. 258 */ 259 static mozilla::Result<int32_t, mozilla::intl::ICUError> getRawOffsetMs( 260 DateTimeInfo* dtInfo) { 261 if (MOZ_UNLIKELY(dtInfo)) { 262 return dtInfo->timeZone()->GetRawOffsetMs(); 263 } 264 auto guard = acquireLockWithValidTimeZone(); 265 return guard->timeZone()->GetRawOffsetMs(); 266 } 267 #else 268 /** 269 * Return the local time zone adjustment (ES2019 20.3.1.7) as computed by 270 * the operating system. 271 */ 272 static int32_t localTZA() { 273 return utcToLocalStandardOffsetSeconds() * msPerSecond; 274 } 275 #endif /* JS_HAS_INTL_API */ 276 277 // JIT access. 278 static const void* addressOfUTCToLocalOffsetSeconds() { 279 static_assert(sizeof(decltype(utcToLocalOffsetSeconds)) == sizeof(int32_t)); 280 return &DateTimeInfo::utcToLocalOffsetSeconds; 281 } 282 283 #if JS_HAS_INTL_API 284 void updateTimeZoneOverride(RefPtr<JS::TimeZoneString> timeZone); 285 #endif 286 287 private: 288 // The method below should only be called via js::ResetTimeZoneInternal(). 289 friend void js::ResetTimeZoneInternal(ResetTimeZoneMode); 290 291 static void resetTimeZone(ResetTimeZoneMode mode) { 292 auto guard = instance->lock(); 293 guard->internalResetTimeZone(mode); 294 295 // Mark the cached value as invalid. 296 utcToLocalOffsetSeconds = InvalidOffset; 297 } 298 299 struct RangeCache { 300 // Start and end offsets in seconds describing the current and the 301 // last cached range. 302 int64_t startSeconds, endSeconds; 303 int64_t oldStartSeconds, oldEndSeconds; 304 305 // The current and the last cached offset in milliseconds. 306 int32_t offsetMilliseconds; 307 int32_t oldOffsetMilliseconds; 308 309 void reset(); 310 311 void sanityCheck(); 312 }; 313 314 enum class TimeZoneStatus : uint8_t { Valid, NeedsUpdate, UpdateIfChanged }; 315 316 TimeZoneStatus timeZoneStatus_; 317 318 /** 319 * The offset in seconds from the current UTC time to the current local 320 * standard time (i.e. not including any offset due to DST) as computed by the 321 * operating system. 322 * 323 * Cached because retrieving this dynamically is Slow, and a certain venerable 324 * benchmark which shall not be named depends on it being fast. 325 * 326 * SpiderMonkey occasionally and arbitrarily updates this value from the 327 * system time zone to attempt to keep this reasonably up-to-date. If 328 * temporary inaccuracy can't be tolerated, JSAPI clients may call 329 * JS::ResetTimeZone to forcibly sync this with the system time zone. 330 * 331 * In most cases this value is consistent with the raw time zone offset as 332 * returned by the ICU default time zone (`icu::TimeZone::getRawOffset()`), 333 * but it is possible to create cases where the operating system default time 334 * zone differs from the ICU default time zone. For example ICU doesn't 335 * support the full range of TZ environment variable settings, which can 336 * result in <ctime> returning a different time zone than what's returned by 337 * ICU. One example is "TZ=WGT3WGST,M3.5.0/-2,M10.5.0/-1", where <ctime> 338 * returns -3 hours as the local offset, but ICU flat out rejects the TZ value 339 * and instead infers the default time zone via "/etc/localtime" (on Unix). 340 * This offset can also differ from ICU when the operating system and ICU use 341 * different tzdata versions and the time zone rules of the current system 342 * time zone have changed. Or, on Windows, when the Windows default time zone 343 * can't be mapped to a IANA time zone, see for example 344 * <https://unicode-org.atlassian.net/browse/ICU-13845>. 345 * 346 * When ICU is exclusively used for time zone computations, that means when 347 * |JS_HAS_INTL_API| is true, this field is only used to detect system default 348 * time zone changes. It must not be used to convert between local and UTC 349 * time, because, as outlined above, this could lead to different results when 350 * compared to ICU. 351 * 352 * If |timeZoneOverride_| is non-null, i.e. when not using the default time 353 * zone, this field is reused as the time zone cache key. See also 354 * |timeZoneCacheKey()| and |updateTimeZoneOverride()|. 355 */ 356 int32_t utcToLocalStandardOffsetSeconds_; 357 358 RangeCache dstRange_; // UTC-based ranges 359 360 #if JS_HAS_INTL_API 361 // Use the full date-time range when we can use mozilla::intl::TimeZone. 362 static constexpr int64_t MinTimeT = 363 static_cast<int64_t>(StartOfTime / msPerSecond); 364 static constexpr int64_t MaxTimeT = 365 static_cast<int64_t>(EndOfTime / msPerSecond); 366 367 RangeCache utcRange_; // localtime-based ranges 368 RangeCache localRange_; // UTC-based ranges 369 370 /** 371 * Time zone override for realms with non-default time zone. 372 */ 373 RefPtr<JS::TimeZoneString> timeZoneOverride_; 374 375 /** 376 * The current time zone. Lazily constructed to avoid potential I/O access 377 * when initializing this class. 378 */ 379 mozilla::UniquePtr<mozilla::intl::TimeZone> timeZone_; 380 381 /** 382 * Cached time zone id. 383 */ 384 JS::UniqueChars timeZoneId_; 385 386 /** 387 * Cached names of the standard and daylight savings display names of the 388 * current time zone for the default locale. 389 */ 390 JS::UniqueChars locale_; 391 JS::UniqueTwoByteChars standardName_; 392 JS::UniqueTwoByteChars daylightSavingsName_; 393 #else 394 // Restrict the data-time range to the minimum required time_t range as 395 // specified in POSIX. Most operating systems support 64-bit time_t 396 // values, but we currently still have some configurations which use 397 // 32-bit time_t, e.g. the ARM simulator on 32-bit Linux (bug 1406993). 398 // Bug 1406992 explores to use 64-bit time_t when supported by the 399 // underlying operating system. 400 static constexpr int64_t MinTimeT = 0; /* time_t 01/01/1970 */ 401 static constexpr int64_t MaxTimeT = 2145830400; /* time_t 12/31/2037 */ 402 #endif /* JS_HAS_INTL_API */ 403 404 static constexpr int64_t RangeExpansionAmount = 30 * SecondsPerDay; 405 406 void internalResetTimeZone(ResetTimeZoneMode mode); 407 408 void resetState(); 409 410 void updateTimeZone(); 411 412 void internalResyncICUDefaultTimeZone(); 413 414 static int64_t toClampedSeconds(int64_t milliseconds); 415 416 using ComputeFn = int32_t (DateTimeInfo::*)(int64_t); 417 418 /** 419 * Get or compute an offset value for the requested seconds value. 420 */ 421 int32_t getOrComputeValue(RangeCache& range, int64_t seconds, 422 ComputeFn compute); 423 424 /** 425 * Compute the DST offset at the given UTC time in seconds from the epoch. 426 * (getDSTOffsetMilliseconds attempts to return a cached value from the 427 * dstRange_ member, but in case of a cache miss it calls this method.) 428 */ 429 int32_t computeDSTOffsetMilliseconds(int64_t utcSeconds); 430 431 int32_t internalGetDSTOffsetMilliseconds(int64_t utcMilliseconds); 432 433 #if JS_HAS_INTL_API 434 /** 435 * Compute the UTC offset in milliseconds for the given local time. Called 436 * by internalGetOffsetMilliseconds on a cache miss. 437 */ 438 int32_t computeUTCOffsetMilliseconds(int64_t localSeconds); 439 440 /** 441 * Compute the local time offset in milliseconds for the given UTC time. 442 * Called by internalGetOffsetMilliseconds on a cache miss. 443 */ 444 int32_t computeLocalOffsetMilliseconds(int64_t utcSeconds); 445 446 int32_t internalGetOffsetMilliseconds(int64_t milliseconds, 447 TimeZoneOffset offset); 448 449 bool internalTimeZoneDisplayName(TimeZoneDisplayNameVector& result, 450 int64_t utcMilliseconds, const char* locale); 451 452 bool internalTimeZoneId(TimeZoneIdentifierVector& result); 453 454 mozilla::intl::TimeZone* timeZone(); 455 #endif /* JS_HAS_INTL_API */ 456 }; 457 458 } /* namespace js */ 459 460 #endif /* vm_DateTime_h */