Skip to content

Commit 65d3677

Browse files
committed
[re-land] Support time zone change notifications on linux
https://bugs.webkit.org/show_bug.cgi?id=314414 Reviewed by Yusuke Suzuki. We add a timezone change observer for linux that watches both dbus and /etc/localtime. We add some testing helpers and some generated tests too. This uncovered an existing bug with DST time changes. Re-land: I added a missing header and some additional clean-ups. Canonical link: https://commits.webkit.org/316629@main
1 parent e2cab1a commit 65d3677

34 files changed

Lines changed: 1645 additions & 62 deletions
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
//@ skip if $hostOS == "playstation"
2+
3+
// Verifies that every DateCache cache entry returns spec-correct values for
4+
// dates straddling a DST transition in the same time zone. After bug 314414,
5+
// these caches persist across VM entries on non-Cocoa platforms, so they must
6+
// answer correctly on both sides of a DST boundary without being flushed.
7+
//
8+
// America/Los_Angeles 2024:
9+
// - Spring forward: 2024-03-10 02:00 PST -> 03:00 PDT (UTC-8 -> UTC-7)
10+
// - Fall back: 2024-11-03 02:00 PDT -> 01:00 PST (UTC-7 -> UTC-8)
11+
12+
function expect(label, got, want)
13+
{
14+
if (got !== want)
15+
throw new Error(`${label}: expected ${JSON.stringify(want)}, got ${JSON.stringify(got)}`);
16+
}
17+
18+
function expectContains(label, haystack, needle)
19+
{
20+
if (typeof haystack !== "string" || haystack.indexOf(needle) < 0)
21+
throw new Error(`${label}: expected ${JSON.stringify(haystack)} to contain ${JSON.stringify(needle)}`);
22+
}
23+
24+
if (!$vm.setHostTimeZone("America/Los_Angeles"))
25+
throw new Error("Failed to set host time zone to America/Los_Angeles");
26+
27+
// Reach a new VMEntryScope so the time zone change takes effect.
28+
setTimeout(() => {
29+
expect("tz applied",
30+
new Intl.DateTimeFormat().resolvedOptions().timeZone,
31+
"America/Los_Angeles");
32+
33+
// UTC instants either side of the spring-forward transition.
34+
const pstUTC = new Date(Date.UTC(2024, 2, 10, 9, 30)); // -> 01:30 PST
35+
const pdtUTC = new Date(Date.UTC(2024, 2, 10, 11, 30)); // -> 04:30 PDT
36+
37+
// --- DSTCache (m_caches[UTCTime]) ---
38+
// Either side of the transition should give the spec-correct local offset,
39+
// both on cache miss and on cache hit.
40+
expect("PST offset", pstUTC.getTimezoneOffset(), 480);
41+
expect("PDT offset", pdtUTC.getTimezoneOffset(), 420);
42+
expect("PST offset (hit)", pstUTC.getTimezoneOffset(), 480);
43+
expect("PDT offset (hit)", pdtUTC.getTimezoneOffset(), 420);
44+
45+
// --- DateInstanceCache (m_dateInstanceCache) ---
46+
// Re-querying the same Date should be consistent.
47+
expect("PST hours", pstUTC.getHours(), 1);
48+
expect("PST hours (hit)", pstUTC.getHours(), 1);
49+
expect("PDT hours", pdtUTC.getHours(), 4);
50+
expect("PDT hours (hit)", pdtUTC.getHours(), 4);
51+
// A fresh Date object with the same ms hits the cache directly.
52+
expect("PDT hours (new instance hit)",
53+
new Date(pdtUTC.getTime()).getHours(), 4);
54+
55+
// --- YearMonthDayCache (m_yearMonthDayCache) ---
56+
// Three distinct Dates, all 16:00 UTC, on consecutive local days bracketing
57+
// the DST transition. The cache stores (days, year, month, day); querying
58+
// adjacent days exercises the +/-1 fast path.
59+
const day9 = new Date(Date.UTC(2024, 2, 9, 16, 0)); // 2024-03-09 08:00 PST
60+
const day10 = new Date(Date.UTC(2024, 2, 10, 16, 0)); // 2024-03-10 09:00 PDT
61+
const day11 = new Date(Date.UTC(2024, 2, 11, 16, 0)); // 2024-03-11 09:00 PDT
62+
expect("day9 year", day9.getFullYear(), 2024);
63+
expect("day10 year", day10.getFullYear(), 2024);
64+
expect("day11 year", day11.getFullYear(), 2024);
65+
expect("day9 month", day9.getMonth(), 2);
66+
expect("day10 month", day10.getMonth(), 2);
67+
expect("day11 month", day11.getMonth(), 2);
68+
expect("day9 date", day9.getDate(), 9);
69+
expect("day10 date", day10.getDate(), 10);
70+
expect("day11 date", day11.getDate(), 11);
71+
expect("day9 hours PST", day9.getHours(), 8);
72+
expect("day10 hours PDT", day10.getHours(), 9);
73+
expect("day11 hours PDT", day11.getHours(), 9);
74+
75+
// --- parseDate cache (m_cachedDateString / m_cachedDateStringValue) ---
76+
// ES2015+ : ISO date-time strings without a tz designator are local time.
77+
// The parser subtracts the local offset, so DST must be respected.
78+
expect("parse PST", Date.parse("2024-03-10T01:30:00"),
79+
Date.UTC(2024, 2, 10, 9, 30));
80+
expect("parse PDT", Date.parse("2024-03-10T04:30:00"),
81+
Date.UTC(2024, 2, 10, 11, 30));
82+
// Cache hit (same string twice in a row should bypass parsing).
83+
expect("parse PDT (hit)", Date.parse("2024-03-10T04:30:00"),
84+
Date.UTC(2024, 2, 10, 11, 30));
85+
expect("parse PST (hit)", Date.parse("2024-03-10T01:30:00"),
86+
Date.UTC(2024, 2, 10, 9, 30));
87+
88+
// --- m_timeZoneStandardDisplayNameCache / m_timeZoneDSTDisplayNameCache ---
89+
// Date#toString embeds "(Pacific Standard Time)" or "(Pacific Daylight
90+
// Time)" depending on whether the instant is in DST. Both caches are
91+
// populated on the first call; subsequent calls hit the cache.
92+
const stdStr1 = pstUTC.toString();
93+
const dstStr1 = pdtUTC.toString();
94+
expectContains("PST display name", stdStr1, "(Pacific Standard Time)");
95+
expectContains("PDT display name", dstStr1, "(Pacific Daylight Time)");
96+
expectContains("PST GMT offset", stdStr1, "GMT-0800");
97+
expectContains("PDT GMT offset", dstStr1, "GMT-0700");
98+
// Cache hits.
99+
expectContains("PST display name (hit)",
100+
new Date(pstUTC.getTime()).toString(), "(Pacific Standard Time)");
101+
expectContains("PDT display name (hit)",
102+
new Date(pdtUTC.getTime()).toString(), "(Pacific Daylight Time)");
103+
104+
// --- DSTCache (m_caches[LocalTime]) ---
105+
// Constructing a Date from local components uses the LocalTime cache. Pick
106+
// dates well away from the discontinuity so the local->UTC mapping is
107+
// unambiguous.
108+
const winterLocal = new Date(2024, 0, 15, 12, 0); // 2024-01-15 12:00 PST
109+
const summerLocal = new Date(2024, 5, 15, 12, 0); // 2024-06-15 12:00 PDT
110+
expect("local PST -> UTC ms",
111+
winterLocal.getTime(), Date.UTC(2024, 0, 15, 20, 0));
112+
expect("local PDT -> UTC ms",
113+
summerLocal.getTime(), Date.UTC(2024, 5, 15, 19, 0));
114+
expect("local PST offset", winterLocal.getTimezoneOffset(), 480);
115+
expect("local PDT offset", summerLocal.getTimezoneOffset(), 420);
116+
117+
// --- Intl.DateTimeFormat (separate code path from Date#toString) ---
118+
// The IntlDateTimeFormat instance caches its resolved time zone; it relies
119+
// on ICU's per-instant DST awareness, not on DateCache's display-name caches.
120+
const dtfShort = new Intl.DateTimeFormat("en-US", {
121+
timeZone: "America/Los_Angeles", hour: "2-digit", minute: "2-digit",
122+
hour12: false, timeZoneName: "short",
123+
});
124+
const dtfLong = new Intl.DateTimeFormat("en-US", {
125+
timeZone: "America/Los_Angeles", hour: "2-digit", minute: "2-digit",
126+
hour12: false, timeZoneName: "long",
127+
});
128+
expectContains("DTF short PST", dtfShort.format(pstUTC), "PST");
129+
expectContains("DTF short PDT", dtfShort.format(pdtUTC), "PDT");
130+
expectContains("DTF long PST", dtfLong.format(pstUTC), "Pacific Standard Time");
131+
expectContains("DTF long PDT", dtfLong.format(pdtUTC), "Pacific Daylight Time");
132+
133+
// formatToParts surfaces the timeZoneName as a discrete part.
134+
const findPart = (parts, type) => parts.find(p => p.type === type)?.value;
135+
const partsPST = dtfShort.formatToParts(pstUTC);
136+
const partsPDT = dtfShort.formatToParts(pdtUTC);
137+
expect("DTF parts PST", findPart(partsPST, "timeZoneName"), "PST");
138+
expect("DTF parts PDT", findPart(partsPDT, "timeZoneName"), "PDT");
139+
140+
// --- Date#toLocaleString (Intl pathway reached via the Date API) ---
141+
const localePST = pstUTC.toLocaleString("en-US", { timeZoneName: "short" });
142+
const localePDT = pdtUTC.toLocaleString("en-US", { timeZoneName: "short" });
143+
expectContains("toLocaleString PST", localePST, "PST");
144+
expectContains("toLocaleString PDT", localePDT, "PDT");
145+
146+
// --- setters: read-local-modify-write-back-to-UTC roundtrip ---
147+
// Date#setHours decodes the receiver's ms to local components (UTCTime
148+
// cache), mutates one field, then re-encodes (LocalTime cache). A single
149+
// call exercises both directions in one step.
150+
const pstSetter = new Date(Date.UTC(2024, 2, 10, 9, 30)); // 01:30 PST
151+
pstSetter.setHours(0); // 00:30 PST -> UTC 08:30
152+
expect("setHours PST roundtrip",
153+
pstSetter.getTime(), Date.UTC(2024, 2, 10, 8, 30));
154+
155+
const pdtSetter = new Date(Date.UTC(2024, 2, 10, 11, 30)); // 04:30 PDT
156+
pdtSetter.setHours(6); // 06:30 PDT -> UTC 13:30
157+
expect("setHours PDT roundtrip",
158+
pdtSetter.getTime(), Date.UTC(2024, 2, 10, 13, 30));
159+
160+
// setDate that walks the receiver across the spring-forward boundary: the
161+
// local time-of-day must be preserved (10:00 local) while UTC ms shifts by
162+
// 23h, not 24h, because the day in between is only 23 wall-clock hours.
163+
const walker = new Date(Date.UTC(2024, 2, 9, 18, 0)); // 2024-03-09 10:00 PST
164+
expect("walker pre hours", walker.getHours(), 10);
165+
expect("walker pre offset", walker.getTimezoneOffset(), 480);
166+
walker.setDate(10); // 2024-03-10 10:00 PDT
167+
expect("walker post hours", walker.getHours(), 10);
168+
expect("walker post offset", walker.getTimezoneOffset(), 420);
169+
expect("walker post UTC ms", walker.getTime(), Date.UTC(2024, 2, 10, 17, 0));
170+
171+
// setMonth across DST: jump from a PST instant to a PDT month, preserving
172+
// local time-of-day.
173+
const jumper = new Date(Date.UTC(2024, 0, 15, 20, 0)); // 2024-01-15 12:00 PST
174+
jumper.setMonth(5); // 2024-06-15 12:00 PDT
175+
expect("setMonth hours preserved", jumper.getHours(), 12);
176+
expect("setMonth offset PDT", jumper.getTimezoneOffset(), 420);
177+
expect("setMonth UTC ms", jumper.getTime(), Date.UTC(2024, 5, 15, 19, 0));
178+
179+
// --- getDay (day-of-week sentinel) ---
180+
// Across DST, day-of-week must reflect the local day. Pick instants whose
181+
// UTC day differs from their local day to make the assertion meaningful.
182+
// 2024-03-10 06:00 UTC = 2024-03-09 22:00 PST -> Saturday (6)
183+
// 2024-03-10 08:00 UTC = 2024-03-10 00:00 PST -> Sunday (0)
184+
// 2024-03-10 11:00 UTC = 2024-03-10 04:00 PDT -> Sunday (0)
185+
expect("getDay Sat PST",
186+
new Date(Date.UTC(2024, 2, 10, 6, 0)).getDay(), 6);
187+
expect("getDay Sun PST",
188+
new Date(Date.UTC(2024, 2, 10, 8, 0)).getDay(), 0);
189+
expect("getDay Sun PDT",
190+
new Date(Date.UTC(2024, 2, 10, 11, 0)).getDay(), 0);
191+
192+
// --- Fall back: DST -> standard transition in the same direction ---
193+
// 2024-11-03 08:30 UTC = 01:30 PDT (still summer time)
194+
// 2024-11-03 10:30 UTC = 02:30 PST (after fall back)
195+
const fbPDT = new Date(Date.UTC(2024, 10, 3, 8, 30));
196+
const fbPST = new Date(Date.UTC(2024, 10, 3, 10, 30));
197+
expect("fallBack PDT offset", fbPDT.getTimezoneOffset(), 420);
198+
expect("fallBack PST offset", fbPST.getTimezoneOffset(), 480);
199+
expect("fallBack PDT hours", fbPDT.getHours(), 1);
200+
expect("fallBack PST hours", fbPST.getHours(), 2);
201+
expectContains("fallBack PDT display name",
202+
fbPDT.toString(), "(Pacific Daylight Time)");
203+
expectContains("fallBack PST display name",
204+
fbPST.toString(), "(Pacific Standard Time)");
205+
}, 0);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//@ skip if $hostOS == "playstation"
2+
3+
// Verifies that an Intl.DateTimeFormat host time zone change is observed
4+
//
5+
// ECMA-262 leaves the *timing* of when a host zone change becomes observable
6+
// implementation-defined, and ECMA-402 only requires that observations stay
7+
// consistent.
8+
function expect(label, got, want)
9+
{
10+
if (got !== want)
11+
throw new Error(`${label}: expected "${want}", got "${got}"`);
12+
}
13+
14+
function cacheableTZ()
15+
{
16+
return new Intl.DateTimeFormat().resolvedOptions().timeZone;
17+
}
18+
19+
function slowTZ()
20+
{
21+
// Passing an options object (even an empty one) bypasses the fast cache
22+
// path, so this resolves the zone through the slow path.
23+
return new Intl.DateTimeFormat(undefined, {}).resolvedOptions().timeZone;
24+
}
25+
26+
function testTZ(timezones, index)
27+
{
28+
if (index >= timezones.length)
29+
return;
30+
31+
const tz = timezones[index];
32+
const before = cacheableTZ();
33+
expect("baseline: cacheable matches slow", before, slowTZ());
34+
35+
if (!$vm.setHostTimeZone(tz))
36+
throw new Error("Failed to set host time zone to " + tz);
37+
38+
// Get outside the VMEntryScope so clearForTimeZoneChange() runs.
39+
setTimeout(() => {
40+
const afterInvalidate = cacheableTZ();
41+
const afterInvalidateSlow = slowTZ();
42+
expect("after notification fires: cacheable matches slow", afterInvalidate, afterInvalidateSlow);
43+
expect("after notification fires: new TZ in effect", afterInvalidate, tz);
44+
testTZ(timezones, index + 1);
45+
}, 0);
46+
}
47+
48+
testTZ(["UTC", "Pacific/Kiritimati", "America/Los_Angeles"], 0);
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//@ skip if $hostOS == "playstation"
2+
3+
// Regression coverage for stale-cached-GregorianDateTime on a DateInstance
4+
// across a time zone change.
5+
//
6+
// DateInstance lazily fetches a DateInstanceData from m_dateInstanceCache on
7+
// the first accessor call and stores it in m_data. Subsequent accessors (in
8+
// both the C++ slow path and the DFG/FTL inline fast paths) only re-compute
9+
// when the receiver's ms changes (`m_gregorianDateTimeCachedForMS != milli`).
10+
// After a TZ change, DateCache::clearForTimeZoneChange poisons that sentinel on
11+
// the DateInstanceData still resident in the cache slots, so a DateInstance
12+
// pointing at one of them takes a forced miss and recomputes against the new
13+
// local zone.
14+
15+
// Spec: ECMA-262 LocalTime(t) uses "the LocalTZA of the current host
16+
// environment", so the same Date must reflect the new TZ after the change.
17+
// SpiderMonkey 115 agrees on every assertion below.
18+
19+
function expect(label, got, want)
20+
{
21+
if (got !== want)
22+
throw new Error(`${label}: expected ${JSON.stringify(want)}, got ${JSON.stringify(got)}`);
23+
}
24+
25+
if (!$vm.setHostTimeZone("America/Los_Angeles"))
26+
throw new Error("Failed to set host time zone to America/Los_Angeles");
27+
28+
setTimeout(() => {
29+
expect("initial tz",
30+
new Intl.DateTimeFormat().resolvedOptions().timeZone,
31+
"America/Los_Angeles");
32+
33+
// 2024-06-15 00:00 UTC.
34+
// In LA (PDT, UTC-7): 2024-06-14 17:00 PDT.
35+
// In JST (UTC+9): 2024-06-15 09:00 JST.
36+
const heldDate = new Date(Date.UTC(2024, 5, 15, 0, 0));
37+
38+
// Populate the DateInstance's m_data with LA-relative cached values.
39+
expect("LA hours", heldDate.getHours(), 17);
40+
expect("LA offset", heldDate.getTimezoneOffset(), 420);
41+
expect("LA date", heldDate.getDate(), 14);
42+
expect("LA day", heldDate.getDay(), 5); // Friday
43+
44+
// Flip TZ.
45+
if (!$vm.setHostTimeZone("Asia/Tokyo"))
46+
throw new Error("Failed to set host time zone to Asia/Tokyo");
47+
48+
setTimeout(() => {
49+
expect("post-change tz",
50+
new Intl.DateTimeFormat().resolvedOptions().timeZone,
51+
"Asia/Tokyo");
52+
53+
// A *fresh* Date with the same ms correctly reflects Tokyo.
54+
const freshDate = new Date(Date.UTC(2024, 5, 15, 0, 0));
55+
expect("fresh JST hours", freshDate.getHours(), 9);
56+
expect("fresh JST offset", freshDate.getTimezoneOffset(), -540);
57+
expect("fresh JST date", freshDate.getDate(), 15);
58+
expect("fresh JST day", freshDate.getDay(), 6); // Saturday
59+
60+
// The held Date must agree (spec-correct), even though its m_data was
61+
// populated under LA TZ.
62+
expect("held JST hours", heldDate.getHours(), 9);
63+
expect("held JST offset", heldDate.getTimezoneOffset(), -540);
64+
expect("held JST date", heldDate.getDate(), 15);
65+
expect("held JST day", heldDate.getDay(), 6);
66+
}, 0);
67+
}, 0);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//@ skip
2+
// FIXME: https://bugs.webkit.org/show_bug.cgi?id=318362
3+
// Skipped: exposes a known, deferred bug (see the FIXME in
4+
// DateInstanceCache::reset). Companion to
5+
// intl-datetimeformat-stale-data-across-tz-change.js, which covers the case
6+
// where the DateInstance's DateInstanceData is still resident in a cache slot
7+
// when the time zone changes. This one covers the eviction case: the
8+
// DateInstanceData is evicted from its slot (its slot overwritten by another
9+
// ms) but stays alive via DateInstance::m_data, so DateCache::reset() never
10+
// reaches it and the held Date keeps its stale local time.
11+
//
12+
// Remove the //@ skip (and add the "skip if $hostOS == playstation" guard the
13+
// sibling tests use) once the eviction case is fixed.
14+
15+
function expect(label, got, want)
16+
{
17+
if (got !== want)
18+
throw new Error(`${label}: expected ${JSON.stringify(want)}, got ${JSON.stringify(got)}`);
19+
}
20+
21+
if (!$vm.setHostTimeZone("America/Los_Angeles"))
22+
throw new Error("Failed to set host time zone to America/Los_Angeles");
23+
24+
setTimeout(() => {
25+
// 2024-06-15 00:00 UTC.
26+
// In LA (PDT, UTC-7): 2024-06-14 17:00 PDT.
27+
// In JST (UTC+9): 2024-06-15 09:00 JST.
28+
const heldMs = Date.UTC(2024, 5, 15, 0, 0);
29+
const heldDate = new Date(heldMs);
30+
31+
// Populate heldDate.m_data with LA-relative cached values.
32+
expect("LA hours", heldDate.getHours(), 17);
33+
expect("LA offset", heldDate.getTimezoneOffset(), 420);
34+
expect("LA date", heldDate.getDate(), 14);
35+
expect("LA day", heldDate.getDay(), 5); // Friday
36+
37+
// Flood the 16-slot direct-mapped DateInstanceCache with distinct ms so
38+
// heldDate's slot is overwritten. Its DateInstanceData is now evicted from
39+
// the cache but still referenced by heldDate.m_data.
40+
for (let i = 1; i <= 256; i++)
41+
new Date(heldMs + i * 86400000).getHours();
42+
43+
if (!$vm.setHostTimeZone("Asia/Tokyo"))
44+
throw new Error("Failed to set host time zone to Asia/Tokyo");
45+
46+
setTimeout(() => {
47+
expect("post-change tz",
48+
new Intl.DateTimeFormat().resolvedOptions().timeZone, "Asia/Tokyo");
49+
50+
// A fresh Date with the same ms correctly reflects Tokyo.
51+
const freshDate = new Date(heldMs);
52+
expect("fresh JST hours", freshDate.getHours(), 9);
53+
expect("fresh JST offset", freshDate.getTimezoneOffset(), -540);
54+
55+
// The held Date must agree (spec-correct), even though its evicted
56+
// m_data was populated under LA TZ.
57+
expect("held JST hours", heldDate.getHours(), 9);
58+
expect("held JST offset", heldDate.getTimezoneOffset(), -540);
59+
expect("held JST date", heldDate.getDate(), 15);
60+
expect("held JST day", heldDate.getDay(), 6); // Saturday
61+
}, 0);
62+
}, 0);

0 commit comments

Comments
 (0)