Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release/7.0] [Android][libs] Introduce DateTimeOffset.Now temporary fast result #74965

Merged
merged 22 commits into from
Sep 12, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@
<Compile Include="$(MSBuildThisFileDirectory)System\DateTime.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\DateTimeKind.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\DateTimeOffset.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\DateTimeOffset.NonAndroid.cs" Condition="'$(TargetsAndroid)' != 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\DateTimeOffset.Android.cs" Condition="'$(TargetsAndroid)' == 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\DayOfWeek.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\DBNull.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Decimal.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
steveisok marked this conversation as resolved.
Show resolved Hide resolved

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
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why store the thread at all? You can use s_startNewBackgroundThread or equivalent for all coordination between threads.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was in response to review on the original PR, here.

});
s_loadAndroidTZData.IsBackground = true;
steveisok marked this conversation as resolved.
Show resolved Hide resolved
}
}

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What prevents the race condition where two threads both read s_startNewBackgroundThread as true? Seems like it'd be safer to have a local that determines whether this thread is the one that gets to start it, and that local is set appropriately inside of the above lock.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we made a change based on feedback from @grendello. I don't recall the specifics as to why.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The feedback was about the previous state of the code, where the thread was started within the same lock that the thread then takes, which could end in a deadlock. The assumption here is that since .Start() doesn't block the current thread, the flag is set likely before the thread actually is started. Even in the unlikely situation when two threads would attempt to start the worker thread, the only harm would be a little time wasted inside the worker thread. This code could use a semaphore, for instance, but we felt that would be an overkill.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even in the unlikely situation when two threads would attempt to start the worker thread, the only harm would be a little time wasted inside the worker thread

Calling Start on a thread that's already started will result in an exception. So the concern I'm raising above is that this could lead to reliability problems, not just performance ones.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a very unlikely event, but sure, it is better to wrap it in in try/catch then and ignore the potential exception. Performance was the main point of this PR, so it would be a shame to sacrifice it to handle a very unlikely case with semaphores or similar mechanisms.

}
}


object? localDateTimeOffset = AppContext.GetData("System.TimeZoneInfo.LocalDateTimeOffset");
if (localDateTimeOffset == null) // If no offset property provided through monovm app context, default
return ToLocalTime(utcDateTime, true);
steveisok marked this conversation as resolved.
Show resolved Hide resolved

// 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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<DateTimeOffset>,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious why this changed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be a perf win if we are able to delay loading ICU until after startup. This change doesn't guarantee that, it just helps.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would StartsWith("GMT", StringComparison.Ordinal) load ICU?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently, it's fairly easy to get to

if (!GlobalizationMode.Invariant)
{
if (TryGetAppLocalIcuSwitchValue(out string? icuSuffixAndVersion))
{
LoadAppLocalIcu(icuSuffixAndVersion);
}
else
{
int loaded = LoadICU();
if (loaded == 0)
{
Environment.FailFast(GetIcuLoadFailureMessage());
}
}
}
to trigger the load.

{
return new TimeZoneInfo(id, TimeSpan.FromSeconds(ParseGMTNumericZone(name)), id, name, name, null, disableDaylightSavingTime:true);
}
Expand Down
7 changes: 5 additions & 2 deletions src/tasks/AndroidAppBuilder/Templates/MonoRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
16 changes: 10 additions & 6 deletions src/tasks/AndroidAppBuilder/Templates/monodroid.c
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 "/"
Expand All @@ -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);
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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)
{
Expand Down