Skip to content

Commit 10f0bbf

Browse files
authored
new trimmer feature System.TimeZoneInfo.Invariant (#111215)
1 parent 2eb6459 commit 10f0bbf

File tree

20 files changed

+1084
-690
lines changed

20 files changed

+1084
-690
lines changed

docs/design/features/timezone-invariant-mode.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,28 @@ Therefore dotnet bundles the timezone database as binary as part of the runtime.
88
That makes download size larger and application startup slower.
99
If your application doesn't need to work with time zone information, you could use this feature to make the runtime about 200KB smaller.
1010

11-
You enable it in project file:
11+
## Enabling the invariant mode
12+
13+
Applications can enable the invariant mode by either of the following:
14+
15+
1. in project file:
16+
1217
```xml
1318
<PropertyGroup>
1419
<InvariantTimezone>true</InvariantTimezone>
1520
</PropertyGroup>
1621
```
22+
23+
2. in `runtimeconfig.json` file:
24+
25+
```json
26+
{
27+
"runtimeOptions": {
28+
"configProperties": {
29+
"System.TimeZoneInfo.Invariant": true
30+
}
31+
}
32+
}
33+
```
34+
35+
3. setting environment variable value `DOTNET_SYSTEM_TIMEZONE_INVARIANT` to `true` or `1`.

eng/testing/linker/project.csproj.template

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
<Target Name="RemoveInvariantGlobalization" BeforeTargets="_SetWasmBuildNativeDefaults" Condition="'$(TargetArchitecture)' == 'wasm'">
7777
<ItemGroup>
7878
<_PropertiesThatTriggerRelinking Remove="InvariantGlobalization" />
79+
<_PropertiesThatTriggerRelinking Remove="InvariantTimezone" />
7980
</ItemGroup>
8081
</Target>
8182

src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,6 @@
254254
<PlatformManifestFileEntry Include="wasi-link.rsp" IsNative="true" />
255255
<PlatformManifestFileEntry Include="ILLink.Substitutions.WasmIntrinsics.xml" IsNative="true" />
256256
<PlatformManifestFileEntry Include="ILLink.Substitutions.NoWasmIntrinsics.xml" IsNative="true" />
257-
<PlatformManifestFileEntry Include="ILLink.Substitutions.LegacyJsInterop.xml" IsNative="true" />
258257
<!-- wasi specific -->
259258
<PlatformManifestFileEntry Include="main.c" IsNative="true" />
260259
<PlatformManifestFileEntry Include="driver.h" IsNative="true" />

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ private static unsafe bool TryConvertIanaIdToWindowsId(string ianaId, bool alloc
1313
windowsId = null;
1414
return false;
1515
#else
16-
if (GlobalizationMode.Invariant ||
16+
if (Invariant ||
17+
GlobalizationMode.Invariant ||
1718
GlobalizationMode.UseNls ||
1819
ianaId is null ||
1920
ianaId.AsSpan().ContainsAny('\\', '\n', '\r')) // ICU uses these characters as a separator

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ private static int ParseGMTNumericZone(string name)
128128
// Defaults to Utc if local time zone cannot be found
129129
private static TimeZoneInfo GetLocalTimeZoneCore()
130130
{
131+
if (Invariant) return Utc;
132+
131133
string? id = Interop.Sys.GetDefaultTimeZone();
132134
if (!string.IsNullOrEmpty(id))
133135
{
@@ -144,6 +146,7 @@ private static TimeZoneInfo GetLocalTimeZoneCore()
144146

145147
private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, out TimeZoneInfo? value, out Exception? e)
146148
{
149+
Debug.Assert(!Invariant);
147150

148151
value = id == LocalId ? GetLocalTimeZoneCore() : GetTimeZone(id, id);
149152

@@ -441,6 +444,11 @@ private void LoadEntryAt(Stream fs, long position, out string id, out int byteOf
441444

442445
public string[] GetTimeZoneIds()
443446
{
447+
if (Invariant)
448+
{
449+
return new string[] { "UTC" };
450+
}
451+
444452
int numTimeZoneIDs = _isBackwards.AsSpan(0, _ids.Length).Count(false);
445453
string[] nonBackwardsTZIDs = new string[numTimeZoneIDs];
446454
var index = 0;

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Buffers;
55
using System.Collections.Generic;
6+
using System.Diagnostics;
67
using System.Diagnostics.CodeAnalysis;
78
using System.IO;
89
using System.Runtime.InteropServices;
@@ -25,6 +26,8 @@ public sealed partial class TimeZoneInfo
2526

2627
private static TimeZoneInfo GetLocalTimeZoneCore()
2728
{
29+
if (Invariant) return Utc;
30+
2831
// Without Registry support, create the TimeZoneInfo from a TZ file
2932
return GetLocalTimeZoneFromTzFile();
3033
}
@@ -69,6 +72,8 @@ private static bool IdContainsAnyDisallowedChars(string zoneId)
6972

7073
private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, out TimeZoneInfo? value, out Exception? e)
7174
{
75+
Debug.Assert(!Invariant);
76+
7277
value = null;
7378
e = null;
7479

@@ -147,6 +152,11 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id,
147152
/// </remarks>
148153
private static IEnumerable<string> GetTimeZoneIds()
149154
{
155+
if (Invariant)
156+
{
157+
return new string[] { "UTC" };
158+
}
159+
150160
try
151161
{
152162
var fileName = Path.Combine(GetTimeZoneDirectory(), TimeZoneFileName);
@@ -403,6 +413,8 @@ private static bool CompareTimeZoneFile(string filePath, byte[] buffer, byte[] r
403413
/// </summary>
404414
private static string FindTimeZoneId(byte[] rawData)
405415
{
416+
Debug.Assert(!Invariant);
417+
406418
// default to "Local" if we can't find the right tzfile
407419
string id = LocalId;
408420
string timeZoneDirectory = GetTimeZoneDirectory();
@@ -443,6 +455,8 @@ private static string FindTimeZoneId(byte[] rawData)
443455

444456
private static bool TryLoadTzFile(string tzFilePath, [NotNullWhen(true)] ref byte[]? rawData, [NotNullWhen(true)] ref string? id)
445457
{
458+
Debug.Assert(!Invariant);
459+
446460
if (File.Exists(tzFilePath))
447461
{
448462
try
@@ -469,6 +483,8 @@ private static bool TryLoadTzFile(string tzFilePath, [NotNullWhen(true)] ref byt
469483
#if TARGET_WASI || TARGET_BROWSER
470484
private static bool TryLoadEmbeddedTzFile(string name, [NotNullWhen(true)] out byte[]? rawData)
471485
{
486+
Debug.Assert(!Invariant);
487+
472488
IntPtr bytes = Interop.Sys.GetTimeZoneData(name, out int length);
473489
if (bytes == IntPtr.Zero)
474490
{
@@ -502,8 +518,11 @@ private static bool TryLoadEmbeddedTzFile(string name, [NotNullWhen(true)] out b
502518
/// </summary>
503519
private static bool TryGetLocalTzFile([NotNullWhen(true)] out byte[]? rawData, [NotNullWhen(true)] out string? id)
504520
{
521+
Debug.Assert(!Invariant);
522+
505523
rawData = null;
506524
id = null;
525+
507526
string? tzVariable = GetTzEnvironmentVariable();
508527

509528
// If the env var is null, on iOS/tvOS, grab the default tz from the device.
@@ -573,6 +592,8 @@ private static bool TryGetLocalTzFile([NotNullWhen(true)] out byte[]? rawData, [
573592
/// </summary>
574593
private static TimeZoneInfo GetLocalTimeZoneFromTzFile()
575594
{
595+
Debug.Assert(!Invariant);
596+
576597
byte[]? rawData;
577598
string? id;
578599
if (TryGetLocalTzFile(out rawData, out id))

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,16 @@ private static void PopulateAllSystemTimeZones(CachedData cachedData)
288288
{
289289
Debug.Assert(Monitor.IsEntered(cachedData));
290290

291+
cachedData._systemTimeZones ??= new Dictionary<string, TimeZoneInfo>(StringComparer.OrdinalIgnoreCase)
292+
{
293+
{ UtcId, s_utcTimeZone }
294+
};
295+
296+
if (Invariant)
297+
{
298+
return;
299+
}
300+
291301
foreach (string timeZoneId in GetTimeZoneIds())
292302
{
293303
TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache

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

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,16 @@ private static void PopulateAllSystemTimeZones(CachedData cachedData)
114114
{
115115
Debug.Assert(Monitor.IsEntered(cachedData));
116116

117+
cachedData._systemTimeZones ??= new Dictionary<string, TimeZoneInfo>(StringComparer.OrdinalIgnoreCase)
118+
{
119+
{ UtcId, s_utcTimeZone }
120+
};
121+
122+
if (Invariant)
123+
{
124+
return;
125+
}
126+
117127
using (RegistryKey? reg = Registry.LocalMachine.OpenSubKey(TimeZonesRegistryHive, writable: false))
118128
{
119129
if (reg != null)
@@ -145,7 +155,7 @@ private TimeZoneInfo(in TIME_ZONE_INFORMATION zone, bool dstDisabled)
145155
}
146156
_baseUtcOffset = new TimeSpan(0, -(zone.Bias), 0);
147157

148-
if (!dstDisabled)
158+
if (!Invariant && !dstDisabled)
149159
{
150160
// only create the adjustment rule if DST is enabled
151161
REG_TZI_FORMAT regZone = new REG_TZI_FORMAT(zone);
@@ -230,6 +240,8 @@ private static bool CheckDaylightSavingTimeNotSupported(in TIME_ZONE_INFORMATION
230240
/// </summary>
231241
private static string? FindIdFromTimeZoneInformation(in TIME_ZONE_INFORMATION timeZone, out bool dstDisabled)
232242
{
243+
Debug.Assert(!Invariant);
244+
233245
dstDisabled = false;
234246

235247
using (RegistryKey? key = Registry.LocalMachine.OpenSubKey(TimeZonesRegistryHive, writable: false))
@@ -261,6 +273,11 @@ private static TimeZoneInfo GetLocalTimeZone(CachedData cachedData)
261273
{
262274
Debug.Assert(Monitor.IsEntered(cachedData));
263275

276+
if (Invariant)
277+
{
278+
return Utc;
279+
}
280+
264281
//
265282
// Try using the "kernel32!GetDynamicTimeZoneInformation" API to get the "id"
266283
//
@@ -347,6 +364,12 @@ private static TimeZoneInfoResult TryGetTimeZone(string id, out TimeZoneInfo? ti
347364
internal static TimeSpan GetDateTimeNowUtcOffsetFromUtc(DateTime time, out bool isAmbiguousLocalDst)
348365
{
349366
isAmbiguousLocalDst = false;
367+
368+
if (Invariant)
369+
{
370+
return TimeSpan.Zero;
371+
}
372+
350373
int timeYear = time.Year;
351374

352375
OffsetAndRule match = s_cachedData.GetOneYearLocalFromUtc(timeYear);
@@ -490,6 +513,8 @@ private static bool TransitionTimeFromTimeZoneInformation(in REG_TZI_FORMAT time
490513
/// </summary>
491514
private static bool TryCreateAdjustmentRules(string id, in REG_TZI_FORMAT defaultTimeZoneInformation, out AdjustmentRule[]? rules, out Exception? e, int defaultBaseUtcOffset)
492515
{
516+
Debug.Assert(!Invariant);
517+
493518
rules = null;
494519
e = null;
495520

@@ -720,7 +745,7 @@ private static bool TryCompareTimeZoneInformationToRegistry(in TIME_ZONE_INFORMA
720745
/// </summary>
721746
private static string GetLocalizedNameByMuiNativeResource(string resource)
722747
{
723-
if (string.IsNullOrEmpty(resource) || (GlobalizationMode.Invariant && GlobalizationMode.PredefinedCulturesOnly))
748+
if (string.IsNullOrEmpty(resource) || Invariant || (GlobalizationMode.Invariant && GlobalizationMode.PredefinedCulturesOnly))
724749
{
725750
return string.Empty;
726751
}
@@ -785,6 +810,8 @@ private static string GetLocalizedNameByMuiNativeResource(string resource)
785810
/// </summary>
786811
private static unsafe string GetLocalizedNameByNativeResource(string filePath, int resource)
787812
{
813+
Debug.Assert(!Invariant);
814+
788815
IntPtr handle = IntPtr.Zero;
789816
try
790817
{
@@ -821,6 +848,8 @@ private static unsafe string GetLocalizedNameByNativeResource(string filePath, i
821848
/// </summary>
822849
private static void GetLocalizedNamesByRegistryKey(RegistryKey key, out string? displayName, out string? standardName, out string? daylightName)
823850
{
851+
Debug.Assert(!Invariant);
852+
824853
displayName = string.Empty;
825854
standardName = string.Empty;
826855
daylightName = string.Empty;
@@ -867,6 +896,8 @@ private static void GetLocalizedNamesByRegistryKey(RegistryKey key, out string?
867896
/// </summary>
868897
private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out TimeZoneInfo? value, out Exception? e)
869898
{
899+
Debug.Assert(!Invariant);
900+
870901
e = null;
871902

872903
// Standard Time Zone Registry Data
@@ -950,6 +981,11 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out
950981
// Helper function to get the standard display name for the UTC static time zone instance
951982
private static string GetUtcStandardDisplayName()
952983
{
984+
if (Invariant)
985+
{
986+
return InvariantUtcStandardDisplayName;
987+
}
988+
953989
// Don't bother looking up the name for invariant or English cultures
954990
CultureInfo uiCulture = CultureInfo.CurrentUICulture;
955991
if (uiCulture.Name.Length == 0 || uiCulture.TwoLetterISOLanguageName == "en")

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ private enum TimeZoneInfoResult
6060
private static readonly TimeZoneInfo s_utcTimeZone = CreateUtcTimeZone();
6161
private static CachedData s_cachedData = new CachedData();
6262

63+
[FeatureSwitchDefinition("System.TimeZoneInfo.Invariant")]
64+
internal static bool Invariant { get; } = AppContextConfigHelper.GetBooleanConfig("System.TimeZoneInfo.Invariant", "DOTNET_SYSTEM_TIMEZONE_INVARIANT");
65+
6366
//
6467
// All cached data are encapsulated in a helper class to allow consistent view even when the data are refreshed using ClearCachedData()
6568
//
@@ -2054,6 +2057,12 @@ private static TimeZoneInfoResult TryGetTimeZoneUsingId(string id, bool dstDisab
20542057
TimeZoneInfoResult result = TimeZoneInfoResult.Success;
20552058
e = null;
20562059

2060+
if (Invariant && !cachedData._allSystemTimeZonesRead)
2061+
{
2062+
PopulateAllSystemTimeZones(cachedData);
2063+
cachedData._allSystemTimeZonesRead = true;
2064+
}
2065+
20572066
// check the cache
20582067
if (cachedData._systemTimeZones != null)
20592068
{
@@ -2069,6 +2078,12 @@ private static TimeZoneInfoResult TryGetTimeZoneUsingId(string id, bool dstDisab
20692078
}
20702079
}
20712080

2081+
if (Invariant)
2082+
{
2083+
value = null;
2084+
return TimeZoneInfoResult.TimeZoneNotFoundException;
2085+
}
2086+
20722087
if (cachedData._timeZonesUsingAlternativeIds != null)
20732088
{
20742089
if (cachedData._timeZonesUsingAlternativeIds.TryGetValue(id, out value))
@@ -2105,6 +2120,8 @@ private static TimeZoneInfoResult TryGetTimeZoneUsingId(string id, bool dstDisab
21052120

21062121
private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, bool dstDisabled, out TimeZoneInfo? value, out Exception? e, CachedData cachedData)
21072122
{
2123+
Debug.Assert(!Invariant);
2124+
21082125
TimeZoneInfoResult result;
21092126

21102127
result = TryGetTimeZoneFromLocalMachine(id, out value, out e);

src/libraries/System.Runtime/tests/System.Runtime.Tests/Hybrid/System.Runtime.IOS.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
<ItemGroup>
1515
<Compile Include="..\System\TimeZoneInfoTests.cs" />
16+
<Compile Include="..\System\TimeZoneInfoTests.Common.cs" />
1617
<Compile Include="..\System\TimeZoneTests.cs" />
1718
<Compile Include="..\System\TimeZoneNotFoundExceptionTests.cs" />
1819
<Compile Include="..\System\Text\StringBuilderTests.cs" />

0 commit comments

Comments
 (0)