diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 86bde088f4ad7..c334d0a1dbd90 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -254,6 +254,8 @@ + + diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.Android.cs b/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.Android.cs new file mode 100644 index 0000000000000..c78dadcdfc3bd --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.Android.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; + +namespace System +{ + public readonly partial struct DateTimeOffset + { + private static bool s_androidTZDataLoaded; + private static readonly object s_localUtcOffsetLock = new(); + private static Thread? s_loadAndroidTZData; + private static bool s_startNewBackgroundThread = true; + + // Now on Android does the following + // 1) quickly returning a fast path result when first called if the right AppContext data element is set + // 2) starting a background thread to load TimeZoneInfo local cache + // + // On Android, loading AndroidTZData is expensive for startup performance. + // The fast result relies on `System.TimeZoneInfo.LocalDateTimeOffset` being set + // in the App Context's properties as the appropriate local date time offset from UTC. + // monovm_initialize(_preparsed) can be leveraged to do so. + // However, to handle timezone changes during the app lifetime, AndroidTZData needs to be loaded. + // So, on first call, we return the fast path and start a background thread to load + // the TimeZoneInfo Local cache implementation which loads AndroidTZData. + public static DateTimeOffset Now + { + get + { + DateTime utcDateTime = DateTime.UtcNow; + + if (s_androidTZDataLoaded) // The background thread finished, the cache is loaded. + return ToLocalTime(utcDateTime, true); + + if (s_startNewBackgroundThread) // The cache isn't loaded and no background thread has been created + { + lock (s_localUtcOffsetLock) + { + // Now may be called multiple times before a cache is loaded and a background thread is running, + // once the lock is available, check for a cache and background thread. + if (s_androidTZDataLoaded) + return ToLocalTime(utcDateTime, true); + + if (s_loadAndroidTZData == null) + { + s_loadAndroidTZData = new Thread(() => { + // Delay the background thread to avoid impacting startup, if it still coincides after 1s, startup is already perceived as slow + Thread.Sleep(1000); + + _ = TimeZoneInfo.Local; // Load AndroidTZData + s_androidTZDataLoaded = true; + + lock (s_localUtcOffsetLock) + { + s_loadAndroidTZData = null; // Ensure thread is cleared when cache is loaded + } + }); + s_loadAndroidTZData.IsBackground = true; + } + } + + if (s_startNewBackgroundThread) + { + // Because Start does not block the calling thread, + // setting the boolean flag to false immediately after should + // prevent two calls to DateTimeOffset.Now in quick succession + // from both reaching here. + s_loadAndroidTZData.Start(); + s_startNewBackgroundThread = false; + } + } + + + object? localDateTimeOffset = AppContext.GetData("System.TimeZoneInfo.LocalDateTimeOffset"); + if (localDateTimeOffset == null) // If no offset property provided through monovm app context, default + return ToLocalTime(utcDateTime, true); + + // Fast path obtained offset incorporated into ToLocalTime(DateTime.UtcNow, true) logic + int localDateTimeOffsetSeconds = Convert.ToInt32(localDateTimeOffset); + TimeSpan offset = TimeSpan.FromSeconds(localDateTimeOffsetSeconds); + long localTicks = utcDateTime.Ticks + offset.Ticks; + if (localTicks < DateTime.MinTicks || localTicks > DateTime.MaxTicks) + throw new ArgumentException(SR.Arg_ArgumentOutOfRangeException); + + return new DateTimeOffset(localTicks, offset); + } + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.NonAndroid.cs b/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.NonAndroid.cs new file mode 100644 index 0000000000000..511e40ef6c8d1 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.NonAndroid.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System +{ + public readonly partial struct DateTimeOffset + { + // Returns a DateTimeOffset representing the current date and time. The + // resolution of the returned value depends on the system timer. + public static DateTimeOffset Now => ToLocalTime(DateTime.UtcNow, true); + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs b/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs index 3f8feead885a7..b2289d1377ace 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs @@ -34,7 +34,7 @@ namespace System [StructLayout(LayoutKind.Auto)] [Serializable] [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public readonly struct DateTimeOffset + public readonly partial struct DateTimeOffset : IComparable, ISpanFormattable, IComparable, @@ -321,10 +321,6 @@ public DateTimeOffset(int year, int month, int day, int hour, int minute, int se _dateTime = _dateTime.AddMicroseconds(microsecond); } - // Returns a DateTimeOffset representing the current date and time. The - // resolution of the returned value depends on the system timer. - public static DateTimeOffset Now => ToLocalTime(DateTime.UtcNow, true); - public static DateTimeOffset UtcNow { get diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index e03c67beaa58d..d9b46074b8e35 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -107,7 +107,7 @@ private static int ParseGMTNumericZone(string name) { return new TimeZoneInfo(id, TimeSpan.FromSeconds(0), id, name, name, null, disableDaylightSavingTime:true); } - if (name.StartsWith("GMT", StringComparison.Ordinal)) + if (name.Length >= 3 && name[0] == 'G' && name[1] == 'M' && name[2] == 'T') { return new TimeZoneInfo(id, TimeSpan.FromSeconds(ParseGMTNumericZone(name)), id, name, name, null, disableDaylightSavingTime:true); } diff --git a/src/tasks/AndroidAppBuilder/Templates/MonoRunner.java b/src/tasks/AndroidAppBuilder/Templates/MonoRunner.java index 700d95e123f07..4bd5a68d41623 100644 --- a/src/tasks/AndroidAppBuilder/Templates/MonoRunner.java +++ b/src/tasks/AndroidAppBuilder/Templates/MonoRunner.java @@ -26,6 +26,8 @@ import java.util.ArrayList; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; public class MonoRunner extends Instrumentation { @@ -88,7 +90,8 @@ public static int initialize(String entryPointLibName, String[] args, Context co unzipAssets(context, filesDir, "assets.zip"); Log.i("DOTNET", "MonoRunner initialize,, entryPointLibName=" + entryPointLibName); - return initRuntime(filesDir, cacheDir, testResultsDir, entryPointLibName, args); + int localDateTimeOffset = OffsetDateTime.now().getOffset().getTotalSeconds(); + return initRuntime(filesDir, cacheDir, testResultsDir, entryPointLibName, args, localDateTimeOffset); } @Override @@ -149,7 +152,7 @@ static void unzipAssets(Context context, String toPath, String zipName) { } } - static native int initRuntime(String libsDir, String cacheDir, String testResultsDir, String entryPointLibName, String[] args); + static native int initRuntime(String libsDir, String cacheDir, String testResultsDir, String entryPointLibName, String[] args, int local_date_time_offset); static native int setEnv(String key, String value); } diff --git a/src/tasks/AndroidAppBuilder/Templates/monodroid.c b/src/tasks/AndroidAppBuilder/Templates/monodroid.c index 1d8d3f1bd529c..a016353a0b4f3 100644 --- a/src/tasks/AndroidAppBuilder/Templates/monodroid.c +++ b/src/tasks/AndroidAppBuilder/Templates/monodroid.c @@ -203,7 +203,7 @@ cleanup_runtime_config (MonovmRuntimeConfigArguments *args, void *user_data) } int -mono_droid_runtime_init (const char* executable, int managed_argc, char* managed_argv[]) +mono_droid_runtime_init (const char* executable, int managed_argc, char* managed_argv[], int local_date_time_offset) { // NOTE: these options can be set via command line args for adb or xharness, see AndroidSampleApp.csproj @@ -225,13 +225,17 @@ mono_droid_runtime_init (const char* executable, int managed_argc, char* managed // TODO: set TRUSTED_PLATFORM_ASSEMBLIES, APP_PATHS and NATIVE_DLL_SEARCH_DIRECTORIES - const char* appctx_keys[2]; + const char* appctx_keys[3]; appctx_keys[0] = "RUNTIME_IDENTIFIER"; appctx_keys[1] = "APP_CONTEXT_BASE_DIRECTORY"; + appctx_keys[2] = "System.TimeZoneInfo.LocalDateTimeOffset"; - const char* appctx_values[2]; + const char* appctx_values[3]; appctx_values[0] = ANDROID_RUNTIME_IDENTIFIER; appctx_values[1] = bundle_path; + char local_date_time_offset_buffer[32]; + snprintf (local_date_time_offset_buffer, sizeof(local_date_time_offset_buffer), "%d", local_date_time_offset); + appctx_values[2] = strdup (local_date_time_offset_buffer); char *file_name = RUNTIMECONFIG_BIN_FILE; 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 free (file_path); } - monovm_initialize(2, appctx_keys, appctx_values); + monovm_initialize(3, appctx_keys, appctx_values); mono_debug_init (MONO_DEBUG_FORMAT_MONO); 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 } int -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) +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) { char file_dir[2048]; char cache_dir[2048]; @@ -347,7 +351,7 @@ Java_net_dot_MonoRunner_initRuntime (JNIEnv* env, jobject thiz, jstring j_files_ managed_argv[i + 1] = (*env)->GetStringUTFChars(env, j_arg, NULL); } - int res = mono_droid_runtime_init (executable, managed_argc, managed_argv); + int res = mono_droid_runtime_init (executable, managed_argc, managed_argv, current_local_time); for (int i = 0; i < args_len; ++i) {