Skip to content

Commit a772aa3

Browse files
authored
[libs][Unix][perf] Lazily initialize TimeZoneInfo names and order GetSystemTimeZones by Ids (#88368)
* [libs] Remove unnecessary assignment TZifHead * [libs][perf] Add lazy initialization for TimeZoneInfo names * Reduce comparisons for UTC alias and remove static array allocation * [libs] Lazy init display names for utc aliases * [libs][perf] Order system time zones by id * Directly compare numerical value * Fix internal field naming * Remove TryPopulateTimeZoneDisplayNamesFromGlobalizationData * Make lazy initialization methods static on windows * Revert "[libs][perf] Order system time zones by id" This reverts commit 580a765. * Fix lazy initialization for Minimal Globalization Data * Avoid lazy initialization where internal display name fields are set to null * Fix CreateLocal not preserving lazy initialized names * Prevent unintended lazy initialization in CreateCustomTimeZone * Make UICulture a property * Substitute null name properties with empty string in default constructor * Assert not reached in Invariant mode
1 parent fc3e403 commit a772aa3

File tree

5 files changed

+185
-70
lines changed

5 files changed

+185
-70
lines changed

src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.Unix.cs

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Globalization;
5+
using System.Threading;
6+
using System.Diagnostics;
57

68
namespace System
79
{
@@ -21,24 +23,7 @@ public sealed partial class TimeZoneInfo
2123
"Pacific/Pitcairn" // Prefer "Pitcairn Islands Time" over "Pitcairn Time"
2224
};
2325

24-
// Main function that is called during construction to populate the three display names
25-
private static void TryPopulateTimeZoneDisplayNamesFromGlobalizationData(string timeZoneId, TimeSpan baseUtcOffset, ref string? standardDisplayName, ref string? daylightDisplayName, ref string? displayName)
26-
{
27-
if (GlobalizationMode.Invariant)
28-
{
29-
return;
30-
}
31-
32-
// Determine the culture to use
33-
CultureInfo uiCulture = CultureInfo.CurrentUICulture;
34-
if (uiCulture.Name.Length == 0)
35-
uiCulture = CultureInfo.GetCultureInfo(FallbackCultureName); // ICU doesn't work nicely with InvariantCulture
36-
37-
// Attempt to populate the fields backing the StandardName, DaylightName, and DisplayName from globalization data.
38-
GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.Standard, uiCulture.Name, ref standardDisplayName);
39-
GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.DaylightSavings, uiCulture.Name, ref daylightDisplayName);
40-
GetFullValueForDisplayNameField(timeZoneId, baseUtcOffset, uiCulture, ref displayName);
41-
}
26+
private static CultureInfo? _uiCulture;
4227

4328
// Helper function to get the standard display name for the UTC static time zone instance
4429
private static string GetUtcStandardDisplayName()
@@ -67,6 +52,35 @@ private static string GetUtcFullDisplayName(string timeZoneId, string standardDi
6752
}
6853
#pragma warning restore IDE0060
6954

55+
private static CultureInfo UICulture
56+
{
57+
get
58+
{
59+
if (_uiCulture == null)
60+
{
61+
Debug.Assert(!GlobalizationMode.Invariant);
62+
// Determine the culture to use
63+
CultureInfo uiCulture = CultureInfo.CurrentUICulture;
64+
if (uiCulture.Name.Length == 0)
65+
uiCulture = CultureInfo.GetCultureInfo(FallbackCultureName); // ICU doesn't work nicely with InvariantCulture
66+
67+
Interlocked.CompareExchange(ref _uiCulture, uiCulture, null);
68+
}
69+
70+
return _uiCulture;
71+
}
72+
}
73+
74+
private static void GetStandardDisplayName(string timeZoneId, ref string? displayName)
75+
{
76+
GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.Standard, UICulture.Name, ref displayName);
77+
}
78+
79+
private static void GetDaylightDisplayName(string timeZoneId, ref string? displayName)
80+
{
81+
GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.DaylightSavings, UICulture.Name, ref displayName);
82+
}
83+
7084
// Helper function that retrieves various forms of time zone display names from ICU
7185
private static unsafe void GetDisplayName(string timeZoneId, Interop.Globalization.TimeZoneDisplayNameType nameType, string uiCulture, ref string? displayName)
7286
{
@@ -115,14 +129,15 @@ private static unsafe void GetDisplayName(string timeZoneId, Interop.Globalizati
115129
}
116130

117131
// Helper function that builds the value backing the DisplayName field from globalization data.
118-
private static void GetFullValueForDisplayNameField(string timeZoneId, TimeSpan baseUtcOffset, CultureInfo uiCulture, ref string? displayName)
132+
private static void GetFullValueForDisplayNameField(string timeZoneId, TimeSpan baseUtcOffset, ref string? displayName)
119133
{
120134
// There are a few diffent ways we might show the display name depending on the data.
121135
// The algorithm used below should avoid duplicating the same words while still achieving the
122136
// goal of providing a unique, discoverable, and intuitive name.
123137

124138
// Try to get the generic name for this time zone.
125139
string? genericName = null;
140+
CultureInfo uiCulture = UICulture;
126141
GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.Generic, uiCulture.Name, ref genericName);
127142
if (genericName == null)
128143
{

src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.MinimalGlobalizationData.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ namespace System
66
public sealed partial class TimeZoneInfo
77
{
88
#pragma warning disable IDE0060
9-
static partial void TryPopulateTimeZoneDisplayNamesFromGlobalizationData(string timeZoneId, TimeSpan baseUtcOffset, ref string? standardDisplayName, ref string? daylightDisplayName, ref string? displayName);
9+
static partial void GetFullValueForDisplayNameField(string timeZoneId, TimeSpan baseUtcOffset, ref string? displayName);
10+
11+
static partial void GetStandardDisplayName(string timeZoneId, ref string? displayName);
12+
13+
static partial void GetDaylightDisplayName(string timeZoneId, ref string? displayName);
1014

1115
private static string GetUtcStandardDisplayName()
1216
{

src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs

Lines changed: 80 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,48 +18,58 @@ public sealed partial class TimeZoneInfo
1818
{
1919
private const string DefaultTimeZoneDirectory = "/usr/share/zoneinfo/";
2020

21-
// UTC aliases per https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml
21+
// Set fallback values using abbreviations, base offset, and id
22+
// These are expected in environments without time zone globalization data
23+
private string? _standardAbbrevName;
24+
private string? _daylightAbbrevName;
25+
26+
// Handle UTC and its aliases per https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml
2227
// Hard-coded because we need to treat all aliases of UTC the same even when globalization data is not available.
2328
// (This list is not likely to change.)
24-
private static readonly string[] s_UtcAliases = new[] {
25-
"Etc/UTC",
26-
"Etc/UCT",
27-
"Etc/Universal",
28-
"Etc/Zulu",
29-
"UCT",
30-
"UTC",
31-
"Universal",
32-
"Zulu"
33-
};
29+
private static bool IsUtcAlias (string id)
30+
{
31+
switch ((ushort)id[0])
32+
{
33+
case 69: // e
34+
case 101: // E
35+
return string.Equals(id, "Etc/UTC", StringComparison.OrdinalIgnoreCase) ||
36+
string.Equals(id, "Etc/Universal", StringComparison.OrdinalIgnoreCase) ||
37+
string.Equals(id, "Etc/UTC", StringComparison.OrdinalIgnoreCase) ||
38+
string.Equals(id, "Etc/Zulu", StringComparison.OrdinalIgnoreCase);
39+
case 85: // u
40+
case 117: // U
41+
return string.Equals(id, "UCT", StringComparison.OrdinalIgnoreCase) ||
42+
string.Equals(id, "UTC", StringComparison.OrdinalIgnoreCase) ||
43+
string.Equals(id, "Universal", StringComparison.OrdinalIgnoreCase);
44+
case 90: // z
45+
case 122: // Z
46+
return string.Equals(id, "Zulu", StringComparison.OrdinalIgnoreCase);
47+
}
48+
49+
return false;
50+
}
3451

3552
private TimeZoneInfo(byte[] data, string id, bool dstDisabled)
3653
{
3754
_id = id;
3855

3956
HasIanaId = true;
4057

41-
// Handle UTC and its aliases
42-
if (StringArrayContains(_id, s_UtcAliases, StringComparison.OrdinalIgnoreCase))
58+
if (IsUtcAlias(id))
4359
{
44-
_standardDisplayName = GetUtcStandardDisplayName();
45-
_daylightDisplayName = _standardDisplayName;
46-
_displayName = GetUtcFullDisplayName(_id, _standardDisplayName);
4760
_baseUtcOffset = TimeSpan.Zero;
4861
_adjustmentRules = Array.Empty<AdjustmentRule>();
4962
return;
5063
}
5164

52-
TZifHead t;
5365
DateTime[] dts;
5466
byte[] typeOfLocalTime;
5567
TZifType[] transitionType;
5668
string zoneAbbreviations;
5769
string? futureTransitionsPosixFormat;
58-
string? standardAbbrevName = null;
59-
string? daylightAbbrevName = null;
6070

6171
// parse the raw TZif bytes; this method can throw ArgumentException when the data is malformed.
62-
TZif_ParseRaw(data, out t, out dts, out typeOfLocalTime, out transitionType, out zoneAbbreviations, out futureTransitionsPosixFormat);
72+
TZif_ParseRaw(data, out dts, out typeOfLocalTime, out transitionType, out zoneAbbreviations, out futureTransitionsPosixFormat);
6373

6474
// find the best matching baseUtcOffset and display strings based on the current utcNow value.
6575
// NOTE: read the Standard and Daylight display strings from the tzfile now in case they can't be loaded later
@@ -71,11 +81,11 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled)
7181
if (!transitionType[type].IsDst)
7282
{
7383
_baseUtcOffset = transitionType[type].UtcOffset;
74-
standardAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex);
84+
_standardAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex);
7585
}
7686
else
7787
{
78-
daylightAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex);
88+
_daylightAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex);
7989
}
8090
}
8191

@@ -88,24 +98,15 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled)
8898
if (!transitionType[i].IsDst)
8999
{
90100
_baseUtcOffset = transitionType[i].UtcOffset;
91-
standardAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex);
101+
_standardAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex);
92102
}
93103
else
94104
{
95-
daylightAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex);
105+
_daylightAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex);
96106
}
97107
}
98108
}
99109

100-
// Set fallback values using abbreviations, base offset, and id
101-
// These are expected in environments without time zone globalization data
102-
_standardDisplayName = standardAbbrevName;
103-
_daylightDisplayName = daylightAbbrevName ?? standardAbbrevName;
104-
_displayName = string.Create(null, stackalloc char[256], $"(UTC{(_baseUtcOffset >= TimeSpan.Zero ? '+' : '-')}{_baseUtcOffset:hh\\:mm}) {_id}");
105-
106-
// Try to populate the display names from the globalization data
107-
TryPopulateTimeZoneDisplayNamesFromGlobalizationData(_id, _baseUtcOffset, ref _standardDisplayName, ref _daylightDisplayName, ref _displayName);
108-
109110
// TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns
110111
// with DateTimeOffset, SQL Server, and the W3C XML Specification
111112
if (_baseUtcOffset.Ticks % TimeSpan.TicksPerMinute != 0)
@@ -219,6 +220,50 @@ public AdjustmentRule[] GetAdjustmentRules()
219220
return rulesList.ToArray();
220221
}
221222

223+
private string? PopulateDisplayName()
224+
{
225+
if (IsUtcAlias(Id))
226+
return GetUtcFullDisplayName(Id, StandardName);
227+
228+
// Set fallback value using abbreviations, base offset, and id
229+
// These are expected in environments without time zone globalization data
230+
string? displayName = string.Create(null, stackalloc char[256], $"(UTC{(_baseUtcOffset >= TimeSpan.Zero ? '+' : '-')}{_baseUtcOffset:hh\\:mm}) {_id}");
231+
if (GlobalizationMode.Invariant)
232+
return displayName;
233+
234+
GetFullValueForDisplayNameField(Id, BaseUtcOffset, ref displayName);
235+
236+
return displayName;
237+
}
238+
239+
private string? PopulateStandardDisplayName()
240+
{
241+
if (IsUtcAlias(Id))
242+
return GetUtcStandardDisplayName();
243+
244+
string? standardDisplayName = _standardAbbrevName;
245+
if (GlobalizationMode.Invariant)
246+
return standardDisplayName;
247+
248+
GetStandardDisplayName(Id, ref standardDisplayName);
249+
250+
return standardDisplayName;
251+
}
252+
253+
private string? PopulateDaylightDisplayName()
254+
{
255+
if (IsUtcAlias(Id))
256+
return StandardName;
257+
258+
string? daylightDisplayName = _daylightAbbrevName ?? _standardAbbrevName;
259+
if (GlobalizationMode.Invariant)
260+
return daylightDisplayName;
261+
262+
GetDaylightDisplayName(Id, ref daylightDisplayName);
263+
264+
return daylightDisplayName;
265+
}
266+
222267
private static void PopulateAllSystemTimeZones(CachedData cachedData)
223268
{
224269
Debug.Assert(Monitor.IsEntered(cachedData));
@@ -1065,15 +1110,15 @@ private static DateTime TZif_UnixTimeToDateTime(long unixTime) =>
10651110
unixTime > DateTimeOffset.UnixMaxSeconds ? DateTime.MaxValue :
10661111
DateTimeOffset.FromUnixTimeSeconds(unixTime).UtcDateTime;
10671112

1068-
private static void TZif_ParseRaw(byte[] data, out TZifHead t, out DateTime[] dts, out byte[] typeOfLocalTime, out TZifType[] transitionType,
1113+
private static void TZif_ParseRaw(byte[] data, out DateTime[] dts, out byte[] typeOfLocalTime, out TZifType[] transitionType,
10691114
out string zoneAbbreviations, out string? futureTransitionsPosixFormat)
10701115
{
10711116
futureTransitionsPosixFormat = null;
10721117

10731118
// read in the 44-byte TZ header containing the count/length fields
10741119
//
10751120
int index = 0;
1076-
t = new TZifHead(data, index);
1121+
TZifHead t = new TZifHead(data, index);
10771122
index += TZifHead.Length;
10781123

10791124
int timeValuesLength = 4; // the first version uses 4-bytes to specify times

src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,30 @@ public AdjustmentRule[] GetAdjustmentRules()
8888
return (AdjustmentRule[])_adjustmentRules.Clone();
8989
}
9090

91+
private static string? PopulateDisplayName()
92+
{
93+
// Keep window's implementation to populate via constructor
94+
// This should not be reached
95+
Debug.Assert(false);
96+
return null;
97+
}
98+
99+
private static string? PopulateStandardDisplayName()
100+
{
101+
// Keep window's implementation to populate via constructor
102+
// This should not be reached
103+
Debug.Assert(false);
104+
return null;
105+
}
106+
107+
private static string? PopulateDaylightDisplayName()
108+
{
109+
// Keep window's implementation to populate via constructor
110+
// This should not be reached
111+
Debug.Assert(false);
112+
return null;
113+
}
114+
91115
private static void PopulateAllSystemTimeZones(CachedData cachedData)
92116
{
93117
Debug.Assert(Monitor.IsEntered(cachedData));
@@ -900,9 +924,9 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out
900924
value = new TimeZoneInfo(
901925
id,
902926
new TimeSpan(0, -(defaultTimeZoneInformation.Bias), 0),
903-
displayName,
904-
standardName,
905-
daylightName,
927+
displayName ?? string.Empty,
928+
standardName ?? string.Empty,
929+
daylightName ?? string.Empty,
906930
adjustmentRules,
907931
disableDaylightSavingTime: false);
908932

0 commit comments

Comments
 (0)