You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[build] Optimize managed <-> java type lookups (#3992)
Typemap data is used to correlate JNI type names to .NET Assembly-
Qualified Type Names, and vice versa:
java/lang/Object <=> Java.Lang.Object, Mono.Android
Typemap data is used from `JNIEnv.GetJniName()` for managed-to-JNI
lookups, and from `TypeManager.GetJavaToManagedType()` for
JNI-to-managed lookups.
When [typemap files were first introduced][0], they relied on:
1. A string-oriented mapping from Java type names to .NET Assembly
Qualified names and vice versa; and
2. A binary search via **bsearch**(3) over this table to find the
associated type, using the source type as the "key".
(The introduction of `libxamarin-app.so` (decfbcc) merely moved the
(formerly separate) typemap data into `libxamarin-app.so` for Release
config builds -- Debug builds continued using separate typemap files --
but didn't otherwise change how these mappings work.)
This approach works very well at the expense of data size -- shorter
strings are 0-padded to a common width -- and slightly degraded
performance because of the requirement to perform string comparisons.
Furthermore, the managed-to-JNI lookup required that Reflection is
used to obtain the Assembly Qualified type name
(`Type.AssemblyQualifiedName`), while the JNI-to-managed lookup
likewise requires some Reflection to obtain `Type` instances (via
`Type.GetType()`).
Rework the typemap data in an effort to reduce Reflection use:
For the managed-to-JNI mapping, use the combination of
`type.Module.ModuleVersionId` and `Type.MetadataToken` -- a GUID
and an int -- instead of using `Type.AssemblyQualifiedName`. This
allows us to perform the binary search over a set of 20 bytes (16
bytes for the UUID and 4 bytes for the token ID).
JNI-to-managed lookups still need to rely on a binary search across
strings, but instead of mapping the JNI name to an Assembly-Qualified
Type Name and using `Type.GetType()`, we instead map the JNI name to
the same GUID+token pair via a new internal call which uses
`mono_class_get()` & `mono_type_get_object()` to return the `Type`.
As a result of this fundamental change, `libxamarin-app.so` decreases
in size, and app startup time is reduced. For a Release configuration
build of `tests/Xamarin.Forms-Performance-Integration`,
`libs/arm64-v8a/libxamarin-app.so` shrinks from 377KB to 104KB (!),
and on a Pixel 3 XL app the `ActivityTaskManager: Displayed` time was
reduced from 805ms to 789ms (`$(AndroidEnablePreloadAssemblies)`=True),
a nearly 10% improvement.
Build time is also minimally impacted; `<GenerateJavaStubs/>` task
time is reduced from 389ms to 247ms for the Xamarin.Forms build.
~~ Fast Deployment ~~
When Xamarin.Android Fast Deployment is *not* used for Debug builds
(which is the case for OSS builds of xamarin-android), the typemap
generation and deployment is identical for both Release and Debug
builds: `libxamarin-app.so` contains the new typemap information.
In commercial Xamarin.Android builds which use Fast Deployment, the
typemap data is instead stored in two sets of files:
* `typemap.index`: stores the mapping from module GUIDs to
assembly filenames.
* `*.typemap`: One file per .NET *module*, contain both the JNI-to-
managed and managed-to-JNI maps, the latter using indexes into
the Java to managed maps.
All of these files are loaded during Debug app startup and used to
construct a dataset which is then searched during all the lookups.
~~ File Formats ~~
All data in all file formats is little-endian.
The `typemap.index` file stores the mapping from GUIDs to module
filenames such as `Mono.Android.dll`. The file format in pseudo-C++:
struct TypemapIndexHeader {
byte magic [4]; // "XATI"
uint32_t format_version;
uint32_t entry_count;
uint32_t module_filename_width;
TypemapIndexEntry entries [entry_count];
};
struct TypemapIndexEntry {
UUID module_uuid; // 16 bytes
byte file_name [TypemapIndexHeader::module_filename_width];
};
`TypemapIndexHeader::module_filename_width` is the maximum filename
length of any entry within `TypemapIndexEntry::file_name` + 1 for a
terminating `NUL`.
There is no order required within `TypemapIndexHeader::entries`.
`TypemapIndexEntry::file_name` is `NUL` padded, filling the entire
array until the next `TypemapIndexEntry` entry.
The `*.typemap` file stores the mappings from JNI type names to
module GUID and type token pairs. The file format in pseudo-C++:
struct TypemapFileHeader {
byte magic [4]; // "XATM"
uint32_t format_version;
GUID module_uuid;
uint32_t entry_count;
uint32_t duplicate_count;
uint32_t jni_name_width;
uint32_t assembly_name_size;
byte assembly_name [assembly_name_size];
TypemapFileJavaToManagedEntry java_to_managed [entry_count];
TypemapFileManagedToJavaEntry managed_to_java [entry_count];
TypemapFileManagedToJavaEntry duplicates [duplicate_count];
};
struct TypemapFileJavaToManagedEntry {
byte jni_name [TypemapFileHeader::jni_name_width];
uint32_t managed_type_token;
};
struct TypemapFileManagedToJavaEntry {
uint32_t managed_type_token;
uint32_t java_to_managed_index;
};
`TypemapFileHeader::duplicate_count` may be 0.
`TypemapFileJavaToManagedEntry::jni_name` is `NUL` padded.
`TypemapFileJavaToManagedEntry::managed_type_token` is the value of
`Type.MetadataToken`.
`TypemapFileManagedToJavaEntry::java_to_managed_index` is the index
within `TypemapFileHeader::java_to_managed` that contains the JNI name.
[0]: xamarin/monodroid@e69b76e
[1]: https://github.com/xamarin/java.interop/blob/3226a4b57ad84574a69a151a310b077cfe69ee19/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/TypeNameMapGenerator.cs#L16-L56
monodroid_log(LogLevel.Warn,LogCategories.Default,$"typemap: failed to map managed type to Java type: {type.AssemblyQualifiedName} (Module ID: {type.Module.ModuleVersionId}; Type token: {type.MetadataToken})");
0 commit comments