Skip to content

Commit 54e978c

Browse files
[release/7.0] [Android][libs] Introduce DateTimeOffset.Now temporary fast result (#74965)
Backport of #74459 to release/7.0 Introduces fast path for DateTimeOffset.Now on Android that allows us to load tzdata in the background. This improves performance from around 227ms to 21.81ms.
1 parent e8649de commit 54e978c

7 files changed

Lines changed: 115 additions & 14 deletions

File tree

src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,8 @@
254254
<Compile Include="$(MSBuildThisFileDirectory)System\DateTime.cs" />
255255
<Compile Include="$(MSBuildThisFileDirectory)System\DateTimeKind.cs" />
256256
<Compile Include="$(MSBuildThisFileDirectory)System\DateTimeOffset.cs" />
257+
<Compile Include="$(MSBuildThisFileDirectory)System\DateTimeOffset.NonAndroid.cs" Condition="'$(TargetsAndroid)' != 'true'" />
258+
<Compile Include="$(MSBuildThisFileDirectory)System\DateTimeOffset.Android.cs" Condition="'$(TargetsAndroid)' == 'true'" />
257259
<Compile Include="$(MSBuildThisFileDirectory)System\DayOfWeek.cs" />
258260
<Compile Include="$(MSBuildThisFileDirectory)System\DBNull.cs" />
259261
<Compile Include="$(MSBuildThisFileDirectory)System\Decimal.cs" />
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Threading;
5+
6+
namespace System
7+
{
8+
public readonly partial struct DateTimeOffset
9+
{
10+
// 0 == in process of being loaded, 1 == loaded
11+
private static volatile int s_androidTZDataLoaded = -1;
12+
13+
// Now on Android does the following
14+
// 1) quickly returning a fast path result when first called if the right AppContext data element is set
15+
// 2) starting a background thread to load TimeZoneInfo local cache
16+
//
17+
// On Android, loading AndroidTZData is expensive for startup performance.
18+
// The fast result relies on `System.TimeZoneInfo.LocalDateTimeOffset` being set
19+
// in the App Context's properties as the appropriate local date time offset from UTC.
20+
// monovm_initialize(_preparsed) can be leveraged to do so.
21+
// However, to handle timezone changes during the app lifetime, AndroidTZData needs to be loaded.
22+
// So, on first call, we return the fast path and start a background thread to load
23+
// the TimeZoneInfo Local cache implementation which loads AndroidTZData.
24+
public static DateTimeOffset Now
25+
{
26+
get
27+
{
28+
DateTime utcDateTime = DateTime.UtcNow;
29+
30+
if (s_androidTZDataLoaded == 1) // The background thread finished, the cache is loaded.
31+
{
32+
return ToLocalTime(utcDateTime, true);
33+
}
34+
35+
object? localDateTimeOffset = AppContext.GetData("System.TimeZoneInfo.LocalDateTimeOffset");
36+
if (localDateTimeOffset == null) // If no offset property provided through monovm app context, default
37+
{
38+
// no need to create the thread, load tzdata now
39+
s_androidTZDataLoaded = 1;
40+
return ToLocalTime(utcDateTime, true);
41+
}
42+
43+
// The cache isn't loaded yet.
44+
if (Interlocked.CompareExchange(ref s_androidTZDataLoaded, 0, -1) == -1)
45+
{
46+
new Thread(() =>
47+
{
48+
try
49+
{
50+
// Delay the background thread to avoid impacting startup, if it still coincides after 1s, startup is already perceived as slow
51+
Thread.Sleep(1000);
52+
53+
_ = TimeZoneInfo.Local; // Load AndroidTZData
54+
}
55+
finally
56+
{
57+
s_androidTZDataLoaded = 1;
58+
}
59+
}) { IsBackground = true }.Start();
60+
}
61+
62+
// Fast path obtained offset incorporated into ToLocalTime(DateTime.UtcNow, true) logic
63+
int localDateTimeOffsetSeconds = Convert.ToInt32(localDateTimeOffset);
64+
TimeSpan offset = TimeSpan.FromSeconds(localDateTimeOffsetSeconds);
65+
long localTicks = utcDateTime.Ticks + offset.Ticks;
66+
if (localTicks < DateTime.MinTicks || localTicks > DateTime.MaxTicks)
67+
throw new ArgumentException(SR.Arg_ArgumentOutOfRangeException);
68+
69+
return new DateTimeOffset(localTicks, offset);
70+
}
71+
}
72+
}
73+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace System
5+
{
6+
public readonly partial struct DateTimeOffset
7+
{
8+
// Returns a DateTimeOffset representing the current date and time. The
9+
// resolution of the returned value depends on the system timer.
10+
public static DateTimeOffset Now => ToLocalTime(DateTime.UtcNow, true);
11+
}
12+
}

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ namespace System
3434
[StructLayout(LayoutKind.Auto)]
3535
[Serializable]
3636
[TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
37-
public readonly struct DateTimeOffset
37+
public readonly partial struct DateTimeOffset
3838
: IComparable,
3939
ISpanFormattable,
4040
IComparable<DateTimeOffset>,
@@ -321,10 +321,6 @@ public DateTimeOffset(int year, int month, int day, int hour, int minute, int se
321321
_dateTime = _dateTime.AddMicroseconds(microsecond);
322322
}
323323

324-
// Returns a DateTimeOffset representing the current date and time. The
325-
// resolution of the returned value depends on the system timer.
326-
public static DateTimeOffset Now => ToLocalTime(DateTime.UtcNow, true);
327-
328324
public static DateTimeOffset UtcNow
329325
{
330326
get

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ private static int ParseGMTNumericZone(string name)
107107
{
108108
return new TimeZoneInfo(id, TimeSpan.FromSeconds(0), id, name, name, null, disableDaylightSavingTime:true);
109109
}
110-
if (name.StartsWith("GMT", StringComparison.Ordinal))
110+
if (name.Length >= 3 && name[0] == 'G' && name[1] == 'M' && name[2] == 'T')
111111
{
112112
return new TimeZoneInfo(id, TimeSpan.FromSeconds(ParseGMTNumericZone(name)), id, name, name, null, disableDaylightSavingTime:true);
113113
}

src/tasks/AndroidAppBuilder/Templates/MonoRunner.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@
2424
import java.io.OutputStream;
2525
import java.io.BufferedInputStream;
2626
import java.util.ArrayList;
27+
import java.util.Calendar;
2728
import java.util.zip.ZipEntry;
2829
import java.util.zip.ZipInputStream;
30+
import java.time.OffsetDateTime;
31+
import java.time.ZoneOffset;
2932

3033
public class MonoRunner extends Instrumentation
3134
{
@@ -88,7 +91,8 @@ public static int initialize(String entryPointLibName, String[] args, Context co
8891
unzipAssets(context, filesDir, "assets.zip");
8992

9093
Log.i("DOTNET", "MonoRunner initialize,, entryPointLibName=" + entryPointLibName);
91-
return initRuntime(filesDir, cacheDir, testResultsDir, entryPointLibName, args);
94+
int localDateTimeOffset = getLocalDateTimeOffset();
95+
return initRuntime(filesDir, cacheDir, testResultsDir, entryPointLibName, args, localDateTimeOffset);
9296
}
9397

9498
@Override
@@ -149,7 +153,17 @@ static void unzipAssets(Context context, String toPath, String zipName) {
149153
}
150154
}
151155

152-
static native int initRuntime(String libsDir, String cacheDir, String testResultsDir, String entryPointLibName, String[] args);
156+
static int getLocalDateTimeOffset() {
157+
if (android.os.Build.VERSION.SDK_INT >= 26) {
158+
return OffsetDateTime.now().getOffset().getTotalSeconds();
159+
}
160+
else {
161+
int offsetInMillis = Calendar.getInstance().getTimeZone().getRawOffset();
162+
return offsetInMillis / 1000;
163+
}
164+
}
165+
166+
static native int initRuntime(String libsDir, String cacheDir, String testResultsDir, String entryPointLibName, String[] args, int local_date_time_offset);
153167

154168
static native int setEnv(String key, String value);
155169
}

src/tasks/AndroidAppBuilder/Templates/monodroid.c

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ cleanup_runtime_config (MonovmRuntimeConfigArguments *args, void *user_data)
203203
}
204204

205205
int
206-
mono_droid_runtime_init (const char* executable, int managed_argc, char* managed_argv[])
206+
mono_droid_runtime_init (const char* executable, int managed_argc, char* managed_argv[], int local_date_time_offset)
207207
{
208208
// NOTE: these options can be set via command line args for adb or xharness, see AndroidSampleApp.csproj
209209

@@ -225,13 +225,17 @@ mono_droid_runtime_init (const char* executable, int managed_argc, char* managed
225225

226226
// TODO: set TRUSTED_PLATFORM_ASSEMBLIES, APP_PATHS and NATIVE_DLL_SEARCH_DIRECTORIES
227227

228-
const char* appctx_keys[2];
228+
const char* appctx_keys[3];
229229
appctx_keys[0] = "RUNTIME_IDENTIFIER";
230230
appctx_keys[1] = "APP_CONTEXT_BASE_DIRECTORY";
231+
appctx_keys[2] = "System.TimeZoneInfo.LocalDateTimeOffset";
231232

232-
const char* appctx_values[2];
233+
const char* appctx_values[3];
233234
appctx_values[0] = ANDROID_RUNTIME_IDENTIFIER;
234235
appctx_values[1] = bundle_path;
236+
char local_date_time_offset_buffer[32];
237+
snprintf (local_date_time_offset_buffer, sizeof(local_date_time_offset_buffer), "%d", local_date_time_offset);
238+
appctx_values[2] = strdup (local_date_time_offset_buffer);
235239

236240
char *file_name = RUNTIMECONFIG_BIN_FILE;
237241
int str_len = strlen (bundle_path) + strlen (file_name) + 1; // +1 is for the "/"
@@ -251,7 +255,7 @@ mono_droid_runtime_init (const char* executable, int managed_argc, char* managed
251255
free (file_path);
252256
}
253257

254-
monovm_initialize(2, appctx_keys, appctx_values);
258+
monovm_initialize(3, appctx_keys, appctx_values);
255259

256260
mono_debug_init (MONO_DEBUG_FORMAT_MONO);
257261
mono_install_assembly_preload_hook (mono_droid_assembly_preload_hook, NULL);
@@ -318,7 +322,7 @@ Java_net_dot_MonoRunner_setEnv (JNIEnv* env, jobject thiz, jstring j_key, jstrin
318322
}
319323

320324
int
321-
Java_net_dot_MonoRunner_initRuntime (JNIEnv* env, jobject thiz, jstring j_files_dir, jstring j_cache_dir, jstring j_testresults_dir, jstring j_entryPointLibName, jobjectArray j_args)
325+
Java_net_dot_MonoRunner_initRuntime (JNIEnv* env, jobject thiz, jstring j_files_dir, jstring j_cache_dir, jstring j_testresults_dir, jstring j_entryPointLibName, jobjectArray j_args, long current_local_time)
322326
{
323327
char file_dir[2048];
324328
char cache_dir[2048];
@@ -347,7 +351,7 @@ Java_net_dot_MonoRunner_initRuntime (JNIEnv* env, jobject thiz, jstring j_files_
347351
managed_argv[i + 1] = (*env)->GetStringUTFChars(env, j_arg, NULL);
348352
}
349353

350-
int res = mono_droid_runtime_init (executable, managed_argc, managed_argv);
354+
int res = mono_droid_runtime_init (executable, managed_argc, managed_argv, current_local_time);
351355

352356
for (int i = 0; i < args_len; ++i)
353357
{

0 commit comments

Comments
 (0)