Skip to content

TimeZoneInfo incredibly slow #25075

Closed
Closed

Description

@Stoffelche

The TimeZoneInfo methods ConvertTimeToUtc, IsAmbiguousTime and IsDayLightSavingTime popped up as eating up about half of the processing time in a program importing, converting and storing lots of data in a database, As this was quite unbelievable to me, I programmed a little converter, which did these same conversions for our time zone "W. Europe Standard Time" and found it to be about 70 times as fast as the dotnet implementation.
I tried to figure out what went wrong in TimeZoneInfo implementation, but it looked pretty complicated (even messed up) to me, and as I only have limited time resources thought, that someone responsible for this code should check this out instead.
I know my comparison is only valid for our W.European timezone, but I think that most time zones are similarily easy to implement, so for these types of zones the dotnet code really should perform much better. Performance should really be an issue here, as time-related functionality is offen used in comination with engineering and measurment data, where converting between local timezones and UTC is very frequent, and needs to be fast.
I have attached my test program and also paste it here for your convenience.
Stefan

Program.zip

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TestTimeZone {
	class MyWesternEuropeConverter {
		const int YFirst = 1900;
		DateTime TFirst = new DateTime(YFirst, 1, 1), TLast = new DateTime(YFirst + 200, 1, 1);
		DateTime[] FwdTimes = new DateTime[200], BwdTimes = new DateTime[200];
		TimeZoneInfo RegularInfo;
		int StdHourDiff = 1;
		int HourOfChange = 2;
		public MyWesternEuropeConverter() {
			RegularInfo = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time");
			InitDates();
		}
		public DateTime ConvertTimeToUtcRegular(DateTime dt, out bool isAmbiguous, out bool isDayLightSavingTime, out bool isAllowed) {
			try {
				isAmbiguous = RegularInfo.IsAmbiguousTime(dt);
				dt = TimeZoneInfo.ConvertTimeToUtc(dt, RegularInfo);
				isDayLightSavingTime = RegularInfo.IsDaylightSavingTime(dt);
				isAllowed = true;
			} catch {
				isAllowed = false;
				isAmbiguous = true;
				isDayLightSavingTime = true;
			}
			return dt;
		}
		public DateTime ConvertTimeFromUtcRegular(DateTime dt) {
			return TimeZoneInfo.ConvertTimeFromUtc(dt, RegularInfo);
		}

		public DateTime ConvertTimeToUtc(DateTime dt, out bool isAmbiguous, out bool isDayLightSavingTime, out bool isAllowed) {
			int yearIdx = dt.Year - YFirst;
			if (yearIdx < 0 || yearIdx >= 200) {
				return ConvertTimeToUtcRegular(dt, out isAmbiguous, out isDayLightSavingTime, out isAllowed);
			} else {
				DateTime curFwd = FwdTimes[yearIdx];
				DateTime curBwd = BwdTimes[yearIdx];
				if (dt < curFwd || dt >= curBwd) {
					isAmbiguous = false;
					isAllowed = true;
					isDayLightSavingTime = false;
					return new DateTime(dt.Ticks - TimeSpan.TicksPerHour * StdHourDiff, DateTimeKind.Utc);
				}
				if (dt.Ticks < curFwd.Ticks + TimeSpan.TicksPerHour) {
					isAllowed = false;
					isAmbiguous = true;
					isDayLightSavingTime = true;
					return dt; // new DateTime(dt.Ticks - TimeSpan.TicksPerHour * StdHourDiff, DateTimeKind.Utc);
				}
				isAllowed = true;
				isAmbiguous = dt.Ticks >= curBwd.Ticks - TimeSpan.TicksPerHour;
				isDayLightSavingTime = !isAmbiguous;
				return new DateTime(dt.Ticks - TimeSpan.TicksPerHour * (StdHourDiff + (isAmbiguous ? 0 : 1)), DateTimeKind.Utc);
			}
		}
		public DateTime ConvertTimeFromUtc(DateTime dt) {
			DateTime utc = new DateTime(dt.Ticks + TimeSpan.TicksPerHour * StdHourDiff, DateTimeKind.Local);
			int yearIdx = utc.Year - YFirst;
			if (yearIdx < 0 || yearIdx >= 200) {
				return TimeZoneInfo.ConvertTimeFromUtc(dt, RegularInfo);
			}
			DateTime curFwd = FwdTimes[yearIdx];
			DateTime curBwd = BwdTimes[yearIdx];
			if (dt.Ticks >= curFwd.Ticks || dt.Ticks <= curBwd.Ticks - TimeSpan.TicksPerHour) dt = new DateTime(dt.Ticks + TimeSpan.TicksPerHour, DateTimeKind.Local);
			return dt;
		}
		void InitDates() {
			int yFirst = TFirst.Year;
			for (int i = 0; i < FwdTimes.Length; i++) {
				FwdTimes[i] = GetFwdDate(yFirst + i).AddHours(HourOfChange);
				BwdTimes[i] = GetBwdDate(yFirst + i).AddHours(HourOfChange + 1);
		//		System.Diagnostics.Trace.WriteLine(string.Format("{0}-{1}", FwdTimes[i], BwdTimes[i]));
			}
		}
		DateTime GetFwdDate(int year) {
			DateTime dLast = new DateTime(year, 3, 31);
			return dLast.AddDays(-(int)dLast.DayOfWeek);
		}
		DateTime GetBwdDate(int year) {
			DateTime dLast = new DateTime(year, 10, 31);
			return dLast.AddDays(-(int)dLast.DayOfWeek);
		}
	}

	class Program {
		static void Echo(string s) {
			System.Diagnostics.Trace.WriteLine(s);
			System.Console.WriteLine(s);
		}
		static string Report(DateTime t, bool isAmbiguous, bool isDayLightSaving, bool isAllowed) {
			return string.Format("{0:yyyy/MM/dd/HH:mm} ambiguous:{1} dayLightSaving:{2} allowed:{3}", t, isAmbiguous, isDayLightSaving, isAllowed);
		}
		static void Main(string[] args) {
			MyWesternEuropeConverter conv = new MyWesternEuropeConverter();
			TimeZoneInfo tzInfo = TimeZoneInfo.Local;
			DateTime tStart = new DateTime(1900, 1, 1);
			DateTime tEnd = new DateTime(2100, 1, 1);
			bool allowed1, allowed2, isAmbiguous1, isAmbiguous2, isDayLightSaving1, isDayLightSaving2;
			for (DateTime tCur = tStart; tCur < tEnd; tCur = tCur.AddMinutes(15)) {
				DateTime utc1 = conv.ConvertTimeToUtcRegular(tCur, out isAmbiguous1, out isDayLightSaving1, out allowed1);
				DateTime utc2 = conv.ConvertTimeToUtc(tCur, out isAmbiguous2, out isDayLightSaving2, out allowed2);
				if (utc1 != utc2 || isAmbiguous1 != isAmbiguous2 || isDayLightSaving1 != isDayLightSaving2 || allowed1 != allowed2)
					Echo(string.Format("Different: {0} {1}", Report(utc1, isAmbiguous1, isDayLightSaving1, allowed1), Report(utc2, isAmbiguous2, isDayLightSaving2, allowed2)));
			}
			DateTime utc = DateTime.UtcNow;
			int count = 0;
			for (DateTime tCur = tStart; tCur < tEnd; tCur = tCur.AddMinutes(15)) {
				count++;
				DateTime utc1 = conv.ConvertTimeToUtcRegular(tCur, out isAmbiguous1, out isDayLightSaving1, out allowed1);
			}
			TimeSpan tNeeded = DateTime.UtcNow - utc;
			Echo(string.Format("seconds needed for {0} ToUtc regular: {1}, ticks per call: {2}", count, tNeeded.TotalSeconds,((double) tNeeded.Ticks)/count));
			utc = DateTime.UtcNow;
			count = 0;
			for (DateTime tCur = tStart; tCur < tEnd; tCur = tCur.AddMinutes(15)) {
				count++;
				DateTime utc2 = conv.ConvertTimeToUtc(tCur, out isAmbiguous2, out isDayLightSaving2, out allowed2);
			}
			tNeeded = DateTime.UtcNow - utc;
			Echo(string.Format("seconds needed for {0} ToUtc mine: {1}, ticks per call: {2}", count, tNeeded.TotalSeconds, ((double)tNeeded.Ticks) / count));
			System.Console.Write("Ready >");
			System.Console.ReadKey();
		}
	}
}

Activity

  1. transferred this issue fromdotnet/corefxon Jan 31, 2020
  2. added this to the Future milestone on Jan 31, 2020
  3. danmoseley commented on Oct 9, 2021

    @danmoseley
    Contributor

    I ran the code above and indeed:

    seconds needed for 7012704 ToUtc regular: 2.4033412, ticks per call: 3.4271248294523766
    seconds needed for 7012704 ToUtc mine: 0.1591308, ticks per call: 0.22691789073087928
    

    @tarekgh this code is special casing a block of years (1900-2199) for which it pre populates a cache of the daylight savings transitions (2 DateTime's per year). I guess (assuming that this logic was made robust against eg daylight savings switching <> 2x a year) it also incurs the cost of the cache per timezone (perhaps, you just pick one). It's clearly helpful if code is calling this a lot. Thoughts about the core libraries doing something like this?

  4. tarekgh commented on Oct 9, 2021

    @tarekgh
    Member

    Well, creating a cache for all years inside the time zone will make it faster but the concern is the cache size for every zone. Like I know some services use many time zones in their incoming requests which will cause consuming a lot of memory for that. Also, the code here is customized to this specific zone which has a consistent daylight transition across all years (include the time of transition and what would be transition time offset). This is not the cases in all time zones. For example, Morocco time zone can have multiple transitions in the same year. and even the transition usually happens around the Lunar Ramadan month. That means the transition happen in different days and months every year. Time zone is complex thing. We can look doing some perf work there though as I believe there should be some opportunities to optimize.

  5. danmoseley commented on Oct 9, 2021

    @danmoseley
    Contributor

    Possibly we could cache just the nearest pair of transitions around the last requested date time/culture. That would help the case where the conversions are all in the current culture and approximately current datetime. (Can transition date change during process lifetime?)

    It would be good to get input here to learn whether that would be useful. Cc @Stoffelche in case after all this time they still have input!

  6. added
    help wanted[up-for-grabs] Good issue for external contributors
    on Aug 20, 2022
  7. Santas commented on Nov 20, 2023

    @Santas

    +1

  8. alex-jitbit commented on Aug 8, 2024

    @alex-jitbit

    We're also facing the same issue. Our app works in multiple timezones, and converting UTC time to/from user's local timezone is very VERY cpu-consuming, especially when DST is involved

  9. alex-jitbit commented on Jan 31, 2025

    @alex-jitbit

    Any updates about this issue? With all the incredible performance improvements published by @stephentoub every year - I really hope timezone calculation improvements will make it there one day.

    Basically, every time you display a date in user-specific/company-specific timezone in a report - you call an incredibly slow method.

    I currently use my own caching mechanism to work this around, (cache UTC-offset by timezone ID) but it lacks DST-transition support.

  10. tarekgh commented on Jan 31, 2025

    @tarekgh
    Member

    We haven't looked into this yet, but it may be prioritized and addressed at some point hopefully soon.

    every time you display a date in user-specific/company-specific timezone in a report - you can an incredibly slow method.

    Is this always involving the Local time zone? I am asking to know if optimizing the Local time zone operations can help here.

  11. alex-jitbit commented on Feb 1, 2025

    @alex-jitbit

    @tarekgh

    Is this always involving the Local time zone

    No. Our web app has users in all 24 time zones, datetimes are stored as UTC in the database and displayed in client's local time zone.

    We have hundreds of thousands of users. And this is a wild performance bottleneck.

  12. 3 remaining items

  13. danmoseley commented on Feb 2, 2025

    @danmoseley
    Contributor

    Is OS relevant here?

  14. tarekgh commented on Feb 2, 2025

    @tarekgh
    Member

    Is OS relevant here?

    It is a factor. On non-Windows platforms, the TZ adjustment rules count is much bigger, and we read it from the OS using the File IO operations. On Windows we get the rules from reading the registry and Windows doesn't carry the historical TZ data which reduces the rules count used there. But in all cases, we try to find the rule to use to do the time conversion matching the input date/time.

  15. danmoseley commented on Feb 2, 2025

    @danmoseley
    Contributor

    It might be helpful for folks encountering this to share OS.

  16. alex-jitbit commented on Feb 2, 2025

    @alex-jitbit

    @danmoseley tested in all OS'es. We use Ubuntu on production, before that we used Windows, and we develop on Macs. I used PerfView on Windows and dotnet-trace on Ubuntu to confirm TimeZone operations are indeed a bottleneck on both.

    BenchmarkDotNet results (this is on M3 Mac, results on Windows Ryzen 7650X3D look similar):

    Method Mean Error StdDev Allocated
    TimeZoneInfo_ConvertTimeToUtc 64.704 ns 0.2887 ns 0.2560 ns -
    CachedUtcOffset_ToUtc 0.3976 ns 0.0191 ns 0.0160 ns -

    I basically keep a dictionary of UTC offsets for every timezone for current year.

    public class UtcOffset
    {
    	public readonly TimeSpan StandardOffset, DstOffset;
    	public readonly TimeSpan DstStart, DstEnd; //store DST transition dates as timespans since year start - for performance
    	//...rest of the code
    }

    Looking at the (rather complicated) source code I can see that .NET iterates through all adjustments rules, until it finds the correct one. And a timezone can have 150 adjustments tules. If you have a report with 50 rows and 3 date columns thats 22500 iterations total.

    P.S. Out of curiosity I tested NodaTime library it is also 3x faster at converting timezones. Better, but not good enough for me.

  17. danmoseley commented on Feb 2, 2025

    @danmoseley
    Contributor
  18. tarekgh commented on Feb 2, 2025

    @tarekgh
    Member

    And a timezone can have 150 adjustments tules

    On Windows, you shouldn't encounter that many rules.

    @alex-jitbit

    What range of years that the Date/time values stored in your DB?

  19. alex-jitbit commented on Feb 3, 2025

    @alex-jitbit

    @tarekgh I'm not on Windows though. It's Ubuntu (prod) & MacOS (dev)

    Range of years is "current year +/- couple of years back and forth"

  20. daniatic commented on Feb 3, 2025

    @daniatic

    @stephentoub - Maybe it's worth considering performance improvements for TimeZone DateTime conversions? Could be an interesting topic for your "Performance Improvements in .NET X" blog series.

  21. modified the milestones: 10.0.0, 11.0.0 on Jul 31, 2025
  22. tarekgh commented on Oct 5, 2025

    @tarekgh
    Member

    Should be addressed by #119662

  23. alex-jitbit commented on Oct 5, 2025

    @alex-jitbit

    Should be addressed by #119662

    Only 2-3x faster? :(

  24. locked and limited conversation to collaborators on Nov 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area-System.DateTimeenhancementProduct code improvement that does NOT require public API changes/additionshelp wanted[up-for-grabs] Good issue for external contributorstenet-performancePerformance related issue

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions