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

Tune bundled assemblies #6188

Merged
merged 2 commits into from
Aug 18, 2021
Merged

Conversation

grendello
Copy link
Contributor

Bundled assemblies are now stored on run time in an array pre-allocated
during build. This allows us to avoid any memory allocation during
startup. At the same time, a provision is made for adding more
assemblies than those counted and placed in the .apk during the build.
This may come handy when Xamarin.Android supports split
applications (with components placed in different APKs) and also as a
simple fail-safe feature should the build miscalculate something.

Debug information for assemblies is no longer registered when reading
the APK, but rather whenever an assembly load is requested by Mono.

Neither the assemblies nor their debug information are mmapped when
reading the APK anymore. This is done lazily when assembly load is
requested by Mono. Mapping of files after the startup phase is protected
with a POSIX semaphore.

Additionally, a handful of simple optimizations are implemented:

  • we no longer query the device's memory page size whenever we mmap a
    file, the value is instead queried and cached in the Util
    constructor.
  • current app domain is no longer queried when getting a Mono object's
    type under NET6
  • a number of log statements during startup no longer run by
    default

The changes reduce startup time for a simple (one control in a layout)
plain Xamarin.Android application by around 4ms measured for Displayed
time, and around 2ms when measured for native runtime startup.

For the Hello Maui sample from the maui-samples repository, the
startup time is reduced by ~200ms measured for Displayed time (from
~1.6s to ~1.4s), with the same speed up for the native runtime as in the
plain Xamarin.Android test above.

Performance was measured on a Pixel 3XL phone running Android 12 beta -
the Displayed measurements were found to be very unstable during
testing (with variance between runs reaching 600ms sometimes).

@grendello grendello force-pushed the tune-bundled-assemblies branch 3 times, most recently from 8932a07 to 30cd53e Compare August 17, 2021 08:51
@grendello
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@grendello grendello force-pushed the tune-bundled-assemblies branch from 30cd53e to ec58ab2 Compare August 17, 2021 16:46
@grendello
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@@ -295,6 +321,7 @@ void AddEnvironment ()
JniAddNativeMethodRegistrationAttributePresent = appConfState != null ? appConfState.JniAddNativeMethodRegistrationAttributePresent : false,
HaveRuntimeConfigBlob = haveRuntimeConfigBlob,
NumberOfAssembliesInApk = assemblyCount,
BundledAssemblyNameWidth = assemblyNameWidth + 1,
Copy link
Member

Choose a reason for hiding this comment

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

Why the +1 here? Terminating NUL? If so, should ApplicationConfigNativeAssemblyGenerator do that addition instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, ApplicationConfigNativeAssemblyGenerator doesn't calculate the value - its task is just to write it.


int64_t assembly_count = application_config.number_of_assemblies_in_apk;
bool bundled_assemblies_slow_path = bundled_assembly_index >= application_config.number_of_assemblies_in_apk;
uint32_t max_assembly_name_size = application_config.bundled_assembly_name_width - 1;
Copy link
Member

Choose a reason for hiding this comment

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

This is "differently" amusing: we have to do +1 to support the terminating NUL, yet when we hit native code we just -1 that value.

Do we actually need the terminating NUL?

Probably, actually?

The set_entry_data<false>() method uses max_assembly_name_size as the len parameter to strncpy(3), and strncpy() won't read or write past len, it feels like there could be a situation where XamarinAndroidBundledAssembly::name isn't properly NUL-terminated?

Copy link
Contributor Author

@grendello grendello Aug 18, 2021

Choose a reason for hiding this comment

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

The NUL is just a failsafe, just as is max_assembly_name_size. See this comment for more explanation.

if constexpr (NeedsNameAlloc) {
entry.name = utils.strdup_new (entry_name.get () + prefix_len);
} else {
strncpy (entry.name, entry_name.get () + prefix_len, max_name_size);
Copy link
Member

Choose a reason for hiding this comment

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

Should this also explicitly set entry.name[max_name_size]=0 or entry.name[max_name_size-1]=0?

Given the +1/-1 operations elsewhere, I think libxamarin-app.so will allocate the XamarinAndroidBundledAssembly::name entry to be of length char[max_name_size+1] (due to the -1 in libmonodroid.so, and thus it'll come "pre-NUL-terminated", but it's taken me more mental effort than I'd like to admit to come to this conclusion.

Copy link
Contributor Author

@grendello grendello Aug 18, 2021

Choose a reason for hiding this comment

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

strncpy copies at most n bytes (in this case max_name_size) which may not include the terminating NUL byte, true, but since this line is used only with pre-allocated buffers, the string is terminated properly even if the name takes the entire max_name_size characters (since the name buffer is always max_name_size + 1 long). strncpy also stops copying at the first \0 it encounters and copies it to the destination buffer, if it's found within the first n bytes of the source string.

Action<ITaskItem> updateNameWidth = (ITaskItem assembly) => {
string assemblyName = Path.GetFileName (assembly.ItemSpec);
if (assemblyName.Length > assemblyNameWidth) {
assemblyNameWidth = assemblyName.Length;
Copy link
Member

Choose a reason for hiding this comment

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

I like the idea, but this isn't "Unicode"-aware: for better or worse, this is entirely valid:

% csc /out:😀.exe hw.cs
% mono 😀.exe               
Hello, world!

or for the .csproj-inclined:

  <PropertyGroup>
    <AssemblyName>😀</AssemblyName>
  </PropertyGroup>

On macOS, what is 😀?

% ls -l | tail -1 | hexdump -C
00000000  2d 72 77 2d 72 2d 2d 72  2d 2d 20 20 20 20 31 20  |-rw-r--r--    1 |
00000010  6a 6f 6e 20 20 73 74 61  66 66 20 20 20 20 20 20  |jon  staff      |
00000020  33 35 38 34 20 41 75 67  20 31 37 20 31 39 3a 33  |3584 Aug 17 19:3|
00000030  32 20 f0 9f 98 80 2e 65  78 65 0a                 |2 .....exe.|

i.e. 😀 is { f0 9f 98 80 }, which is 4 bytes, not 1.

You shouldn't assume that assembly names are restricted to ASCII.

Is this even possible in Xamarin.Android? I don't know about emoji, but we do use umlauts within directory names: https://github.com/xamarin/xamarin-android/blob/f7d32102cc3ada67aa12cc0beed2cf97226f41b3/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AotTests.cs#L159

…which means we should improve that test (and others?) so that assemblies also contain umlauts. Pity we missed that opportunity.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, I keep forgetting that people can do weird things (especially with emoji) :)

@@ -90,6 +92,9 @@ protected override void WriteSymbols (StreamWriter output)
WriteCommentLine (output, "number_of_assemblies_in_apk");
size += WriteData (output, NumberOfAssembliesInApk);

WriteCommentLine (output, "bundled_assembly_name_width");
size += WriteData (output, BundledAssemblyNameWidth);
Copy link
Member

Choose a reason for hiding this comment

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

e.g. perhaps this should contain +1 /* NUL */?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, as said above, this class merely generates the native assembly - value calculation is not its concern.

var name_labels = new List<string> ();
for (int i = 0; i < NumberOfAssembliesInApk; i++) {
string bufferLabel = GetBufferLabel ();
WriteBufferAllocation (output, bufferLabel, (uint)BundledAssemblyNameWidth);
Copy link
Member

Choose a reason for hiding this comment

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

Ditto here (uint) BundledAssemblyNameWidth+1 /* NUL */?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ditto :)

@grendello grendello force-pushed the tune-bundled-assemblies branch from ec58ab2 to 25a2032 Compare August 18, 2021 08:01
@grendello
Copy link
Contributor Author

The test failures don't appear to be caused by this PR. I managed to reproduce them locally using the tip of main. The WebSocket tests fail because they can't reach the echo.websocket.org server (using ws://echo.websocket.org) and the interpreter crash appears to be caused by an issue in the legacy Mono runtime:

08-18 12:39:20.475 25293 25312 F libc    : Fatal signal 4 (SIGILL), code 0 (SI_USER) in tid 25312 (Instrumentation), pid 25293 (o.Android_Tests)
08-18 12:39:20.548  3468  3533 D DeviceStateHelper: Audio mode: 0
08-18 12:39:20.586 25323 25323 I crash_dump32: obtaining output fd from tombstoned, type: kDebuggerdTombstoneProto
08-18 12:39:20.587   876   876 I tombstoned: received crash request for pid 25312
08-18 12:39:20.588 25323 25323 I crash_dump32: performing dump of process 25293 (target tid = 25312)
08-18 12:39:20.609 25323 25323 E DEBUG   : failed to read /proc/uptime: Permission denied
08-18 12:39:20.745 25323 25323 W unwind  : Failed to initialize DEX file support: dlopen failed: library "libdexfile.so" not found
08-18 12:39:20.860 25323 25323 F DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
08-18 12:39:20.860 25323 25323 F DEBUG   : Build fingerprint: 'google/crosshatch/crosshatch:12/SPB4.210715.011/7608474:user/release-keys'
08-18 12:39:20.860 25323 25323 F DEBUG   : Revision: 'MP1.0'
08-18 12:39:20.860 25323 25323 F DEBUG   : ABI: 'arm'
08-18 12:39:20.860 25323 25323 F DEBUG   : Timestamp: 2021-08-18 12:39:20.608873094+0200
08-18 12:39:20.860 25323 25323 F DEBUG   : Process uptime: 0s
08-18 12:39:20.860 25323 25323 F DEBUG   : Cmdline: Mono.Android_Tests
08-18 12:39:20.860 25323 25323 F DEBUG   : pid: 25293, tid: 25312, name: Instrumentation  >>> Mono.Android_Tests <<<
08-18 12:39:20.860 25323 25323 F DEBUG   : uid: 10517
08-18 12:39:20.860 25323 25323 F DEBUG   : signal 4 (SIGILL), code 0 (SI_USER), fault addr --------
08-18 12:39:20.860 25323 25323 F DEBUG   :     r0  00000000  r1  00000000  r2  00000030  r3  b9efbc50
08-18 12:39:20.860 25323 25323 F DEBUG   :     r4  b9ff6840  r5  b9c76326  r6  00000000  r7  bbe5d564
08-18 12:39:20.861 25323 25323 F DEBUG   :     r8  b9efbcfc  r9  b9ff6828  r10 e8055eb0  r11 b9efbc68
08-18 12:39:20.861 25323 25323 F DEBUG   :     ip  00000000  sp  b9efba90  lr  bbdaec80  pc  bbdae968
08-18 12:39:20.861 25323 25323 F DEBUG   : backtrace:
08-18 12:39:20.861 25323 25323 F DEBUG   :       #00 pc 0012b968  /data/app/~~ra-9GTek0k9wm1fjca-eLw==/Mono.Android_Tests-CI3wvNJ2tvljF1F-oWIDsw==/lib/arm/libmonosgen-2.0.so (interp_exec_method_full+48928)

I have no idea why the designer tests crash though... They shouldn't be affected by this PR, which is about code that runs on Android device.

@grendello
Copy link
Contributor Author

WebSocket tests will be disabled by this PR

C++ features:

 * Use C++20 concepts to make compile-time correctness verification
   stronger
 * Use `std::vector` for dynamic heap buffers
 * Use `std::array` for static stack buffers
 * Use `std::unique_ptr` instead of the home-grown equivalent (also for
   C-allocated memory)
 * Use C++20 structure designators

Optimizations:

 * Add string routines (`ends_with` and `find_last`) optimized for the
   stack string types (derived from `string_base`)
 * Count assemblies at the build time and pre-allocate enough memory (in
   the form of `std::vector` instance) to store information about the
   bundled assemblies, thus avoiding 2 allocations per assembly (one
   `realloc` and one `malloc`) making the bundled assemblies loading
   algorithm closer to O(1) instead of the previous O(n*2)
 * Use character arrays instead of "plain" string literals to optimize
   some string operations.

These changes improve plain XA test app startup time by around 11%
Bundled assemblies are now stored on run time in an array pre-allocated
during build. This allows us to avoid any memory allocation during
startup. At the same time, a provision is made for adding more
assemblies than those counted and placed in the .apk during the build.
This may come handy when Xamarin.Android supports split
applications (with components placed in different APKs) and also as a
simple fail-safe feature should the build miscalculate something.

Debug information for assemblies is no longer registered when reading
the APK, but rather whenever an assembly load is requested by Mono.

Neither the assemblies nor their debug information are mmapped when
reading the APK anymore. This is done lazily when assembly load is
requested by Mono. Mapping of files after the startup phase is protected
with a POSIX semaphore.

Additionally, a handful of simple optimizations are implemented:

  * we no longer query the device's memory page size whenever we mmap a
    file, the value is instead queried and cached in the `Util`
    constructor.
  * current app domain is no longer queried when getting a Mono object's
    type under NET6
  * a number of log statements during startup no longer run by
    default

The changes reduce startup time for a simple (one control in a layout)
plain Xamarin.Android application by around 4ms measured for `Displayed`
time, and around 2ms when measured for native runtime startup.

For the `Hello Maui` sample from the `maui-samples` repository, the
startup time is reduced by ~200ms measured for `Displayed` time (from
~1.6s to ~1.4s), with the same speed up for the native runtime as in the
plain Xamarin.Android test above.

Performance was measured on a Pixel 3XL phone running Android 12 beta -
the `Displayed` measurements were found to be **very** unstable during
testing (with variance between runs reaching 600ms sometimes).
@grendello grendello force-pushed the tune-bundled-assemblies branch from 25a2032 to 466f40c Compare August 18, 2021 17:29
@jonpryor
Copy link
Member

When Embedded Assemblies are used (`$(EmbedAssembliesIntoApk)`=True),
assembly names are detected during `.apk` content traversal,
duplicated, and stored into `bundled_assemblies` and
`MonoBundledAssembly::name`.

Reduce memory allocations by updating `libxamarin-app.so` to contain
a `bundled_assembly_names` array, a'la:

	struct XamarinAndroidBundledAssembly {
	    int32_t     apk_fd;
	    uint32_t    data_offset;
	    uint32_t    data_size;
	    uint8_t    *data;
	    uint32_t    name_length;
	    char       *name;
	};
	constexpr size_t AssemblyNameWidth;
	static char assembly_name_1 [AssemblyNameWidth];
	static char assembly_name_2 [AssemblyNameWidth];
	// …

	extern "C" XamarinAndroidBundledAssembly bundled_assemblies[] = {
	    {
	        .apk_fd         = -1,
	        .data_offset    = 0,
	        .data_size      = 0,
	        .name_length	= 0,
	        .name           = assembly_name_1,
	    },
	    {
	        .apk_fd         = -1,
	        .data_offset    = 0,
	        .data_size      = 0,
	        .name_length	= 0,
	        .name           = assembly_name_2,
	    },
	    // …
	}

The `bundled_assemblies` array *doesn't* contain the assembly names
detected during packaging time.  Rather, it contains *buffers*, each
large enough to hold any assembly name.

This removes the need to allocate additional memory for assembly
names during process startup.  (We'll still need to allocate memory
for other entries within the `.apk`, such as `.pdb` files.)

Additionally, a provision is made for adding more assemblies than
those counted and placed in the .`apk` during the build.  This may
come handy when Xamarin.Android supports split applications (with
components placed in different `.apk` files), and also as a simple
fail-safe feature should the build miscalculate something.

Debug information for assemblies is no longer registered when reading
the APK, but rather whenever an assembly load is requested by Mono.

Neither the assemblies nor their debug information are mmapped when
reading the APK anymore.  This is done lazily when assembly load is
requested by Mono.  Mapping of files after the startup phase is
protected with a POSIX semaphore.

Additionally, a handful of simple optimizations are implemented:

  * We no longer query the device's memory page size whenever we
    `mmap()` a file; the value is instead queried and cached in the
    `Util` constructor.
  * The current AppDomain is no longer queried when getting a Mono
    object's type under .NET 6
  * Many log statements are no longer run by default during startup.

These changes reduce startup time for a simple plain
Xamarin.Android application (one control in a layout) by around 4ms
measured for `Displayed` time, and by around 2ms when measured for
native runtime startup.

For the `Hello Maui` sample from the `maui-samples` repository, the
startup time is reduced by ~200ms measured for `Displayed` time (from
~1.6s to ~1.4s), with the same speed up for the native runtime as in
the plain Xamarin.Android test above.

Performance was measured on a Pixel 3XL phone running Android 12 beta.
*Note*: the `Displayed` measurements were found to be **very** unstable
during testing on Android 12, with variance between runs reaching 600ms
sometimes.

@jonpryor jonpryor merged commit 69289d7 into dotnet:main Aug 18, 2021
@grendello grendello deleted the tune-bundled-assemblies branch August 18, 2021 18:51
void prepare_for_multiple_threads () noexcept
{
int ret = sem_init (&assembly_mmap_semaphore, 0 /* pshared */, 1 /* value */);
abort_unless (ret == 0, "Failed to initialize assembly mapping semaphore. %s", strerror (errno));
Copy link
Member

@jonathanpeppers jonathanpeppers Aug 19, 2021

Choose a reason for hiding this comment

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

I think this abort is happening during the Designer tests since this got merged?

I got this from their log, is it useful?

[2021-08-18 23:45:32.5] Renderer (error) >> ../../../jni/embedded-assemblies.hh:89 (prepare_for_multiple_threads): Failed to initialize assembly mapping semaphore. Function not implemented
[2021-08-18 23:45:32.5] Renderer >> 
[2021-08-18 23:45:32.5] Renderer >> =================================================================
[2021-08-18 23:45:32.5] Renderer >> 	Native Crash Reporting
[2021-08-18 23:45:32.5] Renderer >> =================================================================
[2021-08-18 23:45:32.5] Renderer >> Got a abrt while executing native code. This usually indicates
[2021-08-18 23:45:32.5] Renderer >> a fatal error in the mono runtime or one of the native libraries 
[2021-08-18 23:45:32.5] Renderer >> used by your application.
[2021-08-18 23:45:32.5] Renderer >> =================================================================
[2021-08-18 23:45:32.5] Renderer >> 
[2021-08-18 23:45:32.5] Renderer >> =================================================================
[2021-08-18 23:45:32.5] Renderer >> 	Native stacktrace:
[2021-08-18 23:45:32.5] Renderer >> =================================================================
[2021-08-18 23:45:32.5] Renderer >> 	0x110bfc9d8 - /Library/Frameworks/Xamarin.Android.framework/Versions/11.4.99.108/lib/xamarin.android/xbuild/Xamarin/Android/lib/host-Darwin/libmonosgen-2.0.dylib : mono_dump_native_crash_info
[2021-08-18 23:45:32.5] Renderer >> 	0x110b9738e - /Library/Frameworks/Xamarin.Android.framework/Versions/11.4.99.108/lib/xamarin.android/xbuild/Xamarin/Android/lib/host-Darwin/libmonosgen-2.0.dylib : mono_handle_native_crash
[2021-08-18 23:45:32.5] Renderer >> 	0x110bfbfdf - /Library/Frameworks/Xamarin.Android.framework/Versions/11.4.99.108/lib/xamarin.android/xbuild/Xamarin/Android/lib/host-Darwin/libmonosgen-2.0.dylib : sigabrt_signal_handler
[2021-08-18 23:45:32.5] Renderer >> 	0x7fff20438d7d - /usr/lib/system/libsystem_platform.dylib : _sigtramp
[2021-08-18 23:45:32.5] Renderer >> 	0x1123c1468 - Unknown
[2021-08-18 23:45:32.5] Renderer >> 	0x7fff20348406 - /usr/lib/system/libsystem_c.dylib : abort
[2021-08-18 23:45:32.5] Renderer >> 	0x110ab2cbd - /Library/Frameworks/Xamarin.Android.framework/Versions/11.4.99.108/lib/xamarin.android/xbuild/Xamarin/Android/lib/host-Darwin/libmono-android.debug.dylib : _ZL15do_abort_unlessbPKcz
[2021-08-18 23:45:32.5] Renderer >> 	0x110aafa7a - /Library/Frameworks/Xamarin.Android.framework/Versions/11.4.99.108/lib/xamarin.android/xbuild/Xamarin/Android/lib/host-Darwin/libmono-android.debug.dylib : _ZN7xamarin7android8internal16MonodroidRuntime38Java_mono_android_Runtime_initInternalEP7JNIEnv_P7_jclassP8_jstringP13_jobjectArrayS8_SA_P8_jobjectSA_ih
[2021-08-18 23:45:32.5] Renderer >> 	0x110aaf005 - /Library/Frameworks/Xamarin.Android.framework/Versions/11.4.99.108/lib/xamarin.android/xbuild/Xamarin/Android/lib/host-Darwin/libmono-android.debug.dylib : Java_mono_android_Runtime_init
[2021-08-18 23:45:32.5] Renderer >> 	0x1124ca3a7 - Unknown
[2021-08-18 23:45:32.5] Renderer >> 	0x1124b9ffd - Unknown
[2021-08-18 23:45:32.5] Renderer >> 	0x1124b97d0 - Unknown
[2021-08-18 23:45:32.5] Renderer >> 
[2021-08-18 23:45:32.5] Renderer >> =================================================================
[2021-08-18 23:45:32.5] Renderer >> 	Basic Fault Address Reporting
[2021-08-18 23:45:32.5] Renderer >> =================================================================
[2021-08-18 23:45:32.5] Renderer >> Memory around native instruction pointer (0x7fff203c492e):0x7fff203c491e  ff ff c3 90 90 90 b8 48 01 00 02 49 89 ca 0f 05  .......H...I....
[2021-08-18 23:45:32.5] Renderer >> 0x7fff203c492e  73 08 48 89 c7 e9 a1 a1 ff ff c3 90 90 90 b8 53  s.H............S
[2021-08-18 23:45:32.5] Renderer >> 0x7fff203c493e  00 00 02 49 89 ca 0f 05 73 08 48 89 c7 e9 89 a1  ...I....s.H.....
[2021-08-18 23:45:32.5] Renderer >> 0x7fff203c494e  ff ff c3 90 90 90 b8 83 01 00 02 49 89 ca 0f 05  ...........I....
[2021-08-18 23:45:32.5] Renderer >> 
[2021-08-18 23:45:32.5] Renderer >> =================================================================

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It does help, thanks!

@github-actions github-actions bot locked and limited conversation to collaborators Jan 24, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants