Repository navigation
TimeZoneInfo incredibly slow #25075
Description
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
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
danmoseley commentedon Oct 9, 2021
I ran the code above and indeed:
@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?
tarekgh commentedon Oct 9, 2021
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.
danmoseley commentedon Oct 9, 2021
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!
Santas commentedon Nov 20, 2023
+1
alex-jitbit commentedon Aug 8, 2024
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
alex-jitbit commentedon Jan 31, 2025
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.
tarekgh commentedon Jan 31, 2025
We haven't looked into this yet, but it may be prioritized and addressed at some point hopefully soon.
Is this always involving the Local time zone? I am asking to know if optimizing the Local time zone operations can help here.
alex-jitbit commentedon Feb 1, 2025
@tarekgh
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.
3 remaining items
danmoseley commentedon Feb 2, 2025
Is OS relevant here?
tarekgh commentedon Feb 2, 2025
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.
danmoseley commentedon Feb 2, 2025
It might be helpful for folks encountering this to share OS.
alex-jitbit commentedon Feb 2, 2025
@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):
I basically keep a dictionary of UTC offsets for every timezone for current year.
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.
danmoseley commentedon Feb 2, 2025
Thanks @alex-jitbit
tarekgh commentedon Feb 2, 2025
On Windows, you shouldn't encounter that many rules.
@alex-jitbit
What range of years that the Date/time values stored in your DB?
alex-jitbit commentedon Feb 3, 2025
@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"
daniatic commentedon Feb 3, 2025
@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.
tarekgh commentedon Oct 5, 2025
Should be addressed by #119662
alex-jitbit commentedon Oct 5, 2025
Only 2-3x faster? :(