Skip to content

Commit 81f8e64

Browse files
authored
[Android][libs] Introduce GetLocalUtcOffset temporary fast result (#74459)
* Avoid ICU loading via StringComparison.Ordinal * Pass OffsetDateTime and monotonic clock through monovm * WIP Leverage monovm_initialize_preparsed * Initial perf improvement implementation * Cleanup GetLocalUtcOffset * Remove Kernel Monotonic Time app context key * Cleanup GetLocalUtcOffset * Sleep background thread first, then start loading AndroidTZData * Conform Runtime Config Property name * Revert "WIP Leverage monovm_initialize_preparsed" This reverts commit 1edaeee. * Address feedback * Fix thread clearing * Move implementation to DateTimeOffset.Now * Address feedback * Consolidate into DateTimeOffset.Now and avoid calling DateTime.UtcNow multiple times * Introduce small performance gains Leverage a boolean flag instead of && Start the background thread to avoid deadlocking * Bump boolean flag outside of background thread body
1 parent 5e7cfbf commit 81f8e64

File tree

7 files changed

+120
-14
lines changed

7 files changed

+120
-14
lines changed

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: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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+
private static bool s_androidTZDataLoaded;
11+
private static readonly object s_localUtcOffsetLock = new();
12+
private static Thread? s_loadAndroidTZData;
13+
private static bool s_startNewBackgroundThread = true;
14+
15+
// Now on Android does the following
16+
// 1) quickly returning a fast path result when first called if the right AppContext data element is set
17+
// 2) starting a background thread to load TimeZoneInfo local cache
18+
//
19+
// On Android, loading AndroidTZData is expensive for startup performance.
20+
// The fast result relies on `System.TimeZoneInfo.LocalDateTimeOffset` being set
21+
// in the App Context's properties as the appropriate local date time offset from UTC.
22+
// monovm_initialize(_preparsed) can be leveraged to do so.
23+
// However, to handle timezone changes during the app lifetime, AndroidTZData needs to be loaded.
24+
// So, on first call, we return the fast path and start a background thread to load
25+
// the TimeZoneInfo Local cache implementation which loads AndroidTZData.
26+
public static DateTimeOffset Now
27+
{
28+
get
29+
{
30+
DateTime utcDateTime = DateTime.UtcNow;
31+
32+
if (s_androidTZDataLoaded) // The background thread finished, the cache is loaded.
33+
return ToLocalTime(utcDateTime, true);
34+
35+
if (s_startNewBackgroundThread) // The cache isn't loaded and no background thread has been created
36+
{
37+
lock (s_localUtcOffsetLock)
38+
{
39+
// Now may be called multiple times before a cache is loaded and a background thread is running,
40+
// once the lock is available, check for a cache and background thread.
41+
if (s_androidTZDataLoaded)
42+
return ToLocalTime(utcDateTime, true);
43+
44+
if (s_loadAndroidTZData == null)
45+
{
46+
s_loadAndroidTZData = new Thread(() => {
47+
// Delay the background thread to avoid impacting startup, if it still coincides after 1s, startup is already perceived as slow
48+
Thread.Sleep(1000);
49+
50+
_ = TimeZoneInfo.Local; // Load AndroidTZData
51+
s_androidTZDataLoaded = true;
52+
53+
lock (s_localUtcOffsetLock)
54+
{
55+
s_loadAndroidTZData = null; // Ensure thread is cleared when cache is loaded
56+
}
57+
});
58+
s_loadAndroidTZData.IsBackground = true;
59+
}
60+
}
61+
62+
if (s_startNewBackgroundThread)
63+
{
64+
// Because Start does not block the calling thread,
65+
// setting the boolean flag to false immediately after should
66+
// prevent two calls to DateTimeOffset.Now in quick succession
67+
// from both reaching here.
68+
s_loadAndroidTZData.Start();
69+
s_startNewBackgroundThread = false;
70+
}
71+
}
72+
73+
74+
object? localDateTimeOffset = AppContext.GetData("System.TimeZoneInfo.LocalDateTimeOffset");
75+
if (localDateTimeOffset == null) // If no offset property provided through monovm app context, default
76+
return ToLocalTime(utcDateTime, true);
77+
78+
// Fast path obtained offset incorporated into ToLocalTime(DateTime.UtcNow, true) logic
79+
int localDateTimeOffsetSeconds = Convert.ToInt32(localDateTimeOffset);
80+
TimeSpan offset = TimeSpan.FromSeconds(localDateTimeOffsetSeconds);
81+
long localTicks = utcDateTime.Ticks + offset.Ticks;
82+
if (localTicks < DateTime.MinTicks || localTicks > DateTime.MaxTicks)
83+
throw new ArgumentException(SR.Arg_ArgumentOutOfRangeException);
84+
85+
return new DateTimeOffset(localTicks, offset);
86+
}
87+
}
88+
}
89+
}
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: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import java.util.ArrayList;
2727
import java.util.zip.ZipEntry;
2828
import java.util.zip.ZipInputStream;
29+
import java.time.OffsetDateTime;
30+
import java.time.ZoneOffset;
2931

3032
public class MonoRunner extends Instrumentation
3133
{
@@ -88,7 +90,8 @@ public static int initialize(String entryPointLibName, String[] args, Context co
8890
unzipAssets(context, filesDir, "assets.zip");
8991

9092
Log.i("DOTNET", "MonoRunner initialize,, entryPointLibName=" + entryPointLibName);
91-
return initRuntime(filesDir, cacheDir, testResultsDir, entryPointLibName, args);
93+
int localDateTimeOffset = OffsetDateTime.now().getOffset().getTotalSeconds();
94+
return initRuntime(filesDir, cacheDir, testResultsDir, entryPointLibName, args, localDateTimeOffset);
9295
}
9396

9497
@Override
@@ -149,7 +152,7 @@ static void unzipAssets(Context context, String toPath, String zipName) {
149152
}
150153
}
151154

152-
static native int initRuntime(String libsDir, String cacheDir, String testResultsDir, String entryPointLibName, String[] args);
155+
static native int initRuntime(String libsDir, String cacheDir, String testResultsDir, String entryPointLibName, String[] args, int local_date_time_offset);
153156

154157
static native int setEnv(String key, String value);
155158
}

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)