Skip to content

new trimmer feature System.TimeZoneInfo.Invariant #111215

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion docs/design/features/timezone-invariant-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,28 @@ Therefore dotnet bundles the timezone database as binary as part of the runtime.
That makes download size larger and application startup slower.
If your application doesn't need to work with time zone information, you could use this feature to make the runtime about 200KB smaller.

You enable it in project file:
## Enabling the invariant mode

Applications can enable the invariant mode by either of the following:

1. in project file:

```xml
<PropertyGroup>
<InvariantTimezone>true</InvariantTimezone>
</PropertyGroup>
```

2. in `runtimeconfig.json` file:

```json
{
"runtimeOptions": {
"configProperties": {
"System.TimeZoneInfo.Invariant": true
}
}
}
```

3. setting environment variable value `DOTNET_SYSTEM_TIMEZONE_INVARIANT` to `true` or `1`.
1 change: 1 addition & 0 deletions eng/testing/linker/project.csproj.template
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
<Target Name="RemoveInvariantGlobalization" BeforeTargets="_SetWasmBuildNativeDefaults" Condition="'$(TargetArchitecture)' == 'wasm'">
<ItemGroup>
<_PropertiesThatTriggerRelinking Remove="InvariantGlobalization" />
<_PropertiesThatTriggerRelinking Remove="InvariantTimezone" />
</ItemGroup>
</Target>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,6 @@
<PlatformManifestFileEntry Include="wasi-link.rsp" IsNative="true" />
<PlatformManifestFileEntry Include="ILLink.Substitutions.WasmIntrinsics.xml" IsNative="true" />
<PlatformManifestFileEntry Include="ILLink.Substitutions.NoWasmIntrinsics.xml" IsNative="true" />
<PlatformManifestFileEntry Include="ILLink.Substitutions.LegacyJsInterop.xml" IsNative="true" />
<!-- wasi specific -->
<PlatformManifestFileEntry Include="main.c" IsNative="true" />
<PlatformManifestFileEntry Include="driver.h" IsNative="true" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ private static unsafe bool TryConvertIanaIdToWindowsId(string ianaId, bool alloc
windowsId = null;
return false;
#else
if (GlobalizationMode.Invariant ||
if (Invariant ||
GlobalizationMode.Invariant ||
GlobalizationMode.UseNls ||
ianaId is null ||
ianaId.AsSpan().ContainsAny('\\', '\n', '\r')) // ICU uses these characters as a separator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ private static int ParseGMTNumericZone(string name)
// Defaults to Utc if local time zone cannot be found
private static TimeZoneInfo GetLocalTimeZoneCore()
{
if (Invariant) return Utc;

string? id = Interop.Sys.GetDefaultTimeZone();
if (!string.IsNullOrEmpty(id))
{
Expand All @@ -144,6 +146,7 @@ private static TimeZoneInfo GetLocalTimeZoneCore()

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

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

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

public string[] GetTimeZoneIds()
{
if (Invariant)
{
return new string[] { "UTC" };
}

int numTimeZoneIDs = _isBackwards.AsSpan(0, _ids.Length).Count(false);
string[] nonBackwardsTZIDs = new string[numTimeZoneIDs];
var index = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
Expand All @@ -25,6 +26,8 @@ public sealed partial class TimeZoneInfo

private static TimeZoneInfo GetLocalTimeZoneCore()
{
if (Invariant) return Utc;

// Without Registry support, create the TimeZoneInfo from a TZ file
return GetLocalTimeZoneFromTzFile();
}
Expand Down Expand Up @@ -69,6 +72,8 @@ private static bool IdContainsAnyDisallowedChars(string zoneId)

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

value = null;
e = null;

Expand Down Expand Up @@ -147,6 +152,11 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id,
/// </remarks>
private static IEnumerable<string> GetTimeZoneIds()
{
if (Invariant)
{
return new string[] { "UTC" };
}

try
{
var fileName = Path.Combine(GetTimeZoneDirectory(), TimeZoneFileName);
Expand Down Expand Up @@ -403,6 +413,8 @@ private static bool CompareTimeZoneFile(string filePath, byte[] buffer, byte[] r
/// </summary>
private static string FindTimeZoneId(byte[] rawData)
{
Debug.Assert(!Invariant);

// default to "Local" if we can't find the right tzfile
string id = LocalId;
string timeZoneDirectory = GetTimeZoneDirectory();
Expand Down Expand Up @@ -443,6 +455,8 @@ private static string FindTimeZoneId(byte[] rawData)

private static bool TryLoadTzFile(string tzFilePath, [NotNullWhen(true)] ref byte[]? rawData, [NotNullWhen(true)] ref string? id)
{
Debug.Assert(!Invariant);

if (File.Exists(tzFilePath))
{
try
Expand All @@ -469,6 +483,8 @@ private static bool TryLoadTzFile(string tzFilePath, [NotNullWhen(true)] ref byt
#if TARGET_WASI || TARGET_BROWSER
private static bool TryLoadEmbeddedTzFile(string name, [NotNullWhen(true)] out byte[]? rawData)
{
Debug.Assert(!Invariant);

IntPtr bytes = Interop.Sys.GetTimeZoneData(name, out int length);
if (bytes == IntPtr.Zero)
{
Expand Down Expand Up @@ -502,8 +518,11 @@ private static bool TryLoadEmbeddedTzFile(string name, [NotNullWhen(true)] out b
/// </summary>
private static bool TryGetLocalTzFile([NotNullWhen(true)] out byte[]? rawData, [NotNullWhen(true)] out string? id)
{
Debug.Assert(!Invariant);

rawData = null;
id = null;

string? tzVariable = GetTzEnvironmentVariable();

// If the env var is null, on iOS/tvOS, grab the default tz from the device.
Expand Down Expand Up @@ -573,6 +592,8 @@ private static bool TryGetLocalTzFile([NotNullWhen(true)] out byte[]? rawData, [
/// </summary>
private static TimeZoneInfo GetLocalTimeZoneFromTzFile()
{
Debug.Assert(!Invariant);

byte[]? rawData;
string? id;
if (TryGetLocalTzFile(out rawData, out id))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,16 @@ private static void PopulateAllSystemTimeZones(CachedData cachedData)
{
Debug.Assert(Monitor.IsEntered(cachedData));

cachedData._systemTimeZones ??= new Dictionary<string, TimeZoneInfo>(StringComparer.OrdinalIgnoreCase)
{
{ UtcId, s_utcTimeZone }
};

if (Invariant)
{
return;
}

foreach (string timeZoneId in GetTimeZoneIds())
{
TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ private static void PopulateAllSystemTimeZones(CachedData cachedData)
{
Debug.Assert(Monitor.IsEntered(cachedData));

cachedData._systemTimeZones ??= new Dictionary<string, TimeZoneInfo>(StringComparer.OrdinalIgnoreCase)
{
{ UtcId, s_utcTimeZone }
};

if (Invariant)
{
return;
}

using (RegistryKey? reg = Registry.LocalMachine.OpenSubKey(TimeZonesRegistryHive, writable: false))
{
if (reg != null)
Expand Down Expand Up @@ -145,7 +155,7 @@ private TimeZoneInfo(in TIME_ZONE_INFORMATION zone, bool dstDisabled)
}
_baseUtcOffset = new TimeSpan(0, -(zone.Bias), 0);

if (!dstDisabled)
if (!Invariant && !dstDisabled)
{
// only create the adjustment rule if DST is enabled
REG_TZI_FORMAT regZone = new REG_TZI_FORMAT(zone);
Expand Down Expand Up @@ -230,6 +240,8 @@ private static bool CheckDaylightSavingTimeNotSupported(in TIME_ZONE_INFORMATION
/// </summary>
private static string? FindIdFromTimeZoneInformation(in TIME_ZONE_INFORMATION timeZone, out bool dstDisabled)
{
Debug.Assert(!Invariant);

dstDisabled = false;

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

if (Invariant)
{
return Utc;
}

//
// Try using the "kernel32!GetDynamicTimeZoneInformation" API to get the "id"
//
Expand Down Expand Up @@ -347,6 +364,12 @@ private static TimeZoneInfoResult TryGetTimeZone(string id, out TimeZoneInfo? ti
internal static TimeSpan GetDateTimeNowUtcOffsetFromUtc(DateTime time, out bool isAmbiguousLocalDst)
{
isAmbiguousLocalDst = false;

if (Invariant)
{
return TimeSpan.Zero;
}

int timeYear = time.Year;

OffsetAndRule match = s_cachedData.GetOneYearLocalFromUtc(timeYear);
Expand Down Expand Up @@ -490,6 +513,8 @@ private static bool TransitionTimeFromTimeZoneInformation(in REG_TZI_FORMAT time
/// </summary>
private static bool TryCreateAdjustmentRules(string id, in REG_TZI_FORMAT defaultTimeZoneInformation, out AdjustmentRule[]? rules, out Exception? e, int defaultBaseUtcOffset)
{
Debug.Assert(!Invariant);

rules = null;
e = null;

Expand Down Expand Up @@ -720,7 +745,7 @@ private static bool TryCompareTimeZoneInformationToRegistry(in TIME_ZONE_INFORMA
/// </summary>
private static string GetLocalizedNameByMuiNativeResource(string resource)
{
if (string.IsNullOrEmpty(resource) || (GlobalizationMode.Invariant && GlobalizationMode.PredefinedCulturesOnly))
if (string.IsNullOrEmpty(resource) || Invariant || (GlobalizationMode.Invariant && GlobalizationMode.PredefinedCulturesOnly))
{
return string.Empty;
}
Expand Down Expand Up @@ -785,6 +810,8 @@ private static string GetLocalizedNameByMuiNativeResource(string resource)
/// </summary>
private static unsafe string GetLocalizedNameByNativeResource(string filePath, int resource)
{
Debug.Assert(!Invariant);

IntPtr handle = IntPtr.Zero;
try
{
Expand Down Expand Up @@ -821,6 +848,8 @@ private static unsafe string GetLocalizedNameByNativeResource(string filePath, i
/// </summary>
private static void GetLocalizedNamesByRegistryKey(RegistryKey key, out string? displayName, out string? standardName, out string? daylightName)
{
Debug.Assert(!Invariant);

displayName = string.Empty;
standardName = string.Empty;
daylightName = string.Empty;
Expand Down Expand Up @@ -867,6 +896,8 @@ private static void GetLocalizedNamesByRegistryKey(RegistryKey key, out string?
/// </summary>
private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out TimeZoneInfo? value, out Exception? e)
{
Debug.Assert(!Invariant);

e = null;

// Standard Time Zone Registry Data
Expand Down Expand Up @@ -950,6 +981,11 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out
// Helper function to get the standard display name for the UTC static time zone instance
private static string GetUtcStandardDisplayName()
{
if (Invariant)
{
return InvariantUtcStandardDisplayName;
}

// Don't bother looking up the name for invariant or English cultures
CultureInfo uiCulture = CultureInfo.CurrentUICulture;
if (uiCulture.Name.Length == 0 || uiCulture.TwoLetterISOLanguageName == "en")
Expand Down
17 changes: 17 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ private enum TimeZoneInfoResult
private static readonly TimeZoneInfo s_utcTimeZone = CreateUtcTimeZone();
private static CachedData s_cachedData = new CachedData();

[FeatureSwitchDefinition("System.TimeZoneInfo.Invariant")]
internal static bool Invariant { get; } = AppContextConfigHelper.GetBooleanConfig("System.TimeZoneInfo.Invariant", "DOTNET_SYSTEM_TIMEZONE_INVARIANT");

//
// All cached data are encapsulated in a helper class to allow consistent view even when the data are refreshed using ClearCachedData()
//
Expand Down Expand Up @@ -2054,6 +2057,12 @@ private static TimeZoneInfoResult TryGetTimeZoneUsingId(string id, bool dstDisab
TimeZoneInfoResult result = TimeZoneInfoResult.Success;
e = null;

if (Invariant && !cachedData._allSystemTimeZonesRead)
{
PopulateAllSystemTimeZones(cachedData);
cachedData._allSystemTimeZonesRead = true;
}

// check the cache
if (cachedData._systemTimeZones != null)
{
Expand All @@ -2069,6 +2078,12 @@ private static TimeZoneInfoResult TryGetTimeZoneUsingId(string id, bool dstDisab
}
}

if (Invariant)
{
value = null;
return TimeZoneInfoResult.TimeZoneNotFoundException;
}

if (cachedData._timeZonesUsingAlternativeIds != null)
{
if (cachedData._timeZonesUsingAlternativeIds.TryGetValue(id, out value))
Expand Down Expand Up @@ -2105,6 +2120,8 @@ private static TimeZoneInfoResult TryGetTimeZoneUsingId(string id, bool dstDisab

private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, bool dstDisabled, out TimeZoneInfo? value, out Exception? e, CachedData cachedData)
{
Debug.Assert(!Invariant);

TimeZoneInfoResult result;

result = TryGetTimeZoneFromLocalMachine(id, out value, out e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<ItemGroup>
<Compile Include="..\System\TimeZoneInfoTests.cs" />
<Compile Include="..\System\TimeZoneInfoTests.Common.cs" />
<Compile Include="..\System\TimeZoneTests.cs" />
<Compile Include="..\System\TimeZoneNotFoundExceptionTests.cs" />
<Compile Include="..\System\Text\StringBuilderTests.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<TestRuntime>true</TestRuntime>
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
<InvariantTimezone>true</InvariantTimezone>
</PropertyGroup>

<PropertyGroup>
<WithoutCategories>$(WithoutCategories);AdditionalTimezoneChecks</WithoutCategories>
</PropertyGroup>

<ItemGroup>

<!-- can be removed after https://github.com/dotnet/sdk/pull/45792/ -->
<RuntimeHostConfigurationOption Include="System.TimeZoneInfo.Invariant"
Condition="'$(InvariantTimezone)' != ''"
Value="$(InvariantTimezone)"
Trim="true" />

<Compile Include="..\System\TimeZoneInfoTests.Common.cs" />
<Compile Include="..\System\TimeZoneInfoTests.Invariant.cs" />
<Compile Include="..\System\TimeZoneTests.cs" />
<Compile Include="..\System\TimeZoneNotFoundExceptionTests.cs" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
<Compile Include="System\TimeoutExceptionTests.cs" />
<Compile Include="System\TimeSpanTests.cs" />
<Compile Include="System\TimeZoneInfoTests.cs" />
<Compile Include="System\TimeZoneInfoTests.Common.cs" />
<Compile Include="System\TimeZoneTests.cs" />
<Compile Include="System\TimeZoneNotFoundExceptionTests.cs" />
<Compile Include="System\TypedReferenceTests.cs" />
Expand Down
Loading
Loading