-
Notifications
You must be signed in to change notification settings - Fork 564
[XABT] Scan for JLO needed wrappers in LinkAssembliesNoShrink.
#9893
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
Conversation
bb13f01 to
d533b22
Compare
src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs
Outdated
Show resolved
Hide resolved
src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindJavaObjectsStep.cs
Outdated
Show resolved
Hide resolved
|
|
||
| <GenerateJavaCallableWrappers | ||
| CodeGenerationTarget="$(_AndroidJcwCodegenTarget)" | ||
| OutputDirectory="$(IntermediateOutputPath)android\src" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are many concerns around incremental build performance. Three of which are:
- Reducing time spent parsing the
.dllfiles. - Reducing time spent generating the
.javasource - Reducing time spent compiling the
.javasource into.classfiles. (javacis slow.) - "Synchronization"
This PR addresses (1) and maybe (2) by making assembly parsing incremental: we only need to process assemblies which have changed.
Should we consider expanding upon (2), (3) and (4)?
The "Synchronization" question is rephrased as "how do we ensure that removed types are deleted"? For example, if the MyApp.dll assembly has:
class Demo : Java.Lang.Object {
}then MyApp.jlo.xml will mention Demo, and $(IntermediateOutputPath)android\src will contain crc64…/Demo.java, and $(IntermediateOutputPath)android\bin\classes will contain crc64…/Demo.class.
We then update MyApp.dll to remove the Demo type. What happens? What could or should happen?
If we don't remove src/crc64…/Demo.java and classes/crc64…/Demo.class, then we have an unsynchronized state of the world. (I think we've had previous bugs due to this; perhaps @dellis1972 remembers?)
If we delete src and classes, that means we need to regenerate .java files for everything, and recompile for everything. We maintain synchronization, but at a cost. (At least we saved on assembly parsing!)
An idea I've been kicking around (mentally and with Dean) was to have per-assembly directories. Instead of $(IntermediateOutputPath)android\src, have $(IntermediateOutputPath)android\jcw\$(AssemblyName)\src and $(IntermediateOutputPath)android\jcw\$(AssemblyName)\classes. When the assembly changes, we delete just the …\jcw\$(AssemblyName) directory, and no others. This should reduce the number of .java files we need to generate and javac invocations on incremental builds.
I don't know if this is actually viable; javac -d OUTDIR can only have one directory, so a per-assembly classes directory means we'd have N javac invocations (one per assembly) instead of 1, and then there's the question of ordering (A.dll may depend on B.dll, creating an ordering constraint on javac).
Even if per-assembly classes directories aren't viable, per-assembly src directories should be viable: delete just the per-assembly src directory and regenerate it based on the .jlo.xml file.
Which brings us to <GenerateJavaCallableWrappers/>: is there a way to make GenerateJavaCallableWrappers.OutputDirectory a per-assembly value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed that this PR only attempts to tackle (1) and that this is just a piece of the build performance puzzle. However tackling (1) is a very large effort as it also requires updating the other "_GenerateJavaStubs" pieces (marshal method rewriting, type map generation, acw map generation, android manifest merging) before we can actually get the win by removing the additional Cecil scan. As such, I think we should focus on that first.
The important note is that nothing in this PR makes those other existing performance issues worse. They remain unaffected.
Having said that and looking ahead...
Doing (2) incrementally should be ~easy, but also probably not a huge win. This step is now very cheap (<50 ms on Android template).
Doing (3) better, if possible, would be a good win. However we haven't examined it enough to know what is viable here. Even better if work here can also make D8 incremental as it is even slower than javac. (dotnet new android template FastDev build: _CompileJava: 1.83s, _CompileToDalvik: 4.07s)
(4) is just something to keep in mind for any future attempts to optimize (2) and (3). We need to ensure that incremental builds remove .java files that should no longer be generated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would check a dotnet new maui project to see the real cost of _CompileJava or _CompileToDalvik.
Once you bring their dependency tree of AndroidX libraries they get a lot slower.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would check a dotnet new maui project to see the real cost of _CompileJava or _CompileToDalvik.
Yep, 16.72s and 22.94s respectively, on dotnet new maui.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jonpryor We did have issues around deleted files not being removed when we have the enhanced fast deployment. This split the .dex files into a core runtime which was packed in the apk, and a classes.dex which was fast deployed. We had to manage classes being deleted in that scenario. All that does was stripped out though.
It should be able to calculate the list of files we expect to be generated and delete anything that is not in that list but exists in the target folder. We do something similar in Buildapk when updating an existing apk from the one aapt2 produced.
804f66e to
dbc94c8
Compare
jonathanpeppers
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rest in here looks good. 👍
Is the Java.Interop bump a branch? or main?
|
|
||
| using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) { | ||
| XmlExporter.Export (sw, wrappers, true); | ||
| Files.CopyIfStreamChanged (sw.BaseStream, destinationJLOXml); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very minor: but we could log if it wrote the file.
| // Names of assemblies which don't have Mono.Android.dll references, or are framework assemblies, but which must | ||
| // be scanned for Java types. | ||
| static readonly HashSet<string> SpecialAssemblies = new HashSet<string> (StringComparer.OrdinalIgnoreCase) { | ||
| "Java.Interop.dll", | ||
| "Mono.Android.dll", | ||
| "Mono.Android.Runtime.dll", | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does XAJavaTypeScanner also have a list of these?
| class XAJavaTypeScanner | |
| { | |
| // Names of assemblies which don't have Mono.Android.dll references, or are framework assemblies, but which must | |
| // be scanned for Java types. | |
| static readonly HashSet<string> SpecialAssemblies = new HashSet<string> (StringComparer.OrdinalIgnoreCase) { | |
| "Java.Interop.dll", | |
| "Mono.Android.dll", | |
| "Mono.Android.Runtime.dll", | |
| }; |
Maybe they could share the list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That part of XAJavaTypeScanner is only used for scanning the old way for the steps that haven't been migrated. It should go away once we complete the transition.
It's a branch, this PR needs dotnet/java-interop#1317 to go first. |
Context: dotnet/android#9893 Create XML import/export adapters for Java Callable Wrappers (JCWs). This allows us to serialize the JCWs we find in the new `FindJavaObjectsStep` linker step (dotnet/android#9893) to disk that, and later use that serialized data in `_GenerateJavaStubs`. This file also allows us to cache the assembly scanning to disk so we don't need to rerun it on incremental builds. This format is likely a "work in progress". As we move other `Java.Lang.Object`-related scanning to linker steps we will likely find additional data they require that needs to go into this file, or that this format isn't even suitable and we use something else instead. Sample JCW XML content: <types was_scanned="true"> <type name="MainActivity" package="crc645107ba1b8b6ee4d3" application_java_class="android.app.Application" mono_runtime_initialization="mono.MonoPackageManager.LoadApplication (context);" extends_type="android.app.Activity" partial_assembly_qualified_name="tempbuild.MainActivity, tempbuild"> <constructors> <constructor name="MainActivity" method="n_.ctor:()V:" jni_signature="()V" managed_parameters="" retval="void" is_dynamically_registered="True" /> </constructors> <methods> <method name="clone" method="n_clone:()Ljava/lang/Object;:GetCloneHandler" jni_signature="()Ljava/lang/Object;" retval="java.lang.Object" /> <method name="onCreate" method="n_onCreate:(Landroid/os/Bundle;)V:GetOnCreate_Landroid_os_Bundle_Handler" jni_signature="(Landroid/os/Bundle;)V" params="android.os.Bundle p0" retval="void" super_call="p0" activate_call="p0" /> </methods> </type> </types> Most XML attributes correspond to members on `CallableWrapperType`, `CallableWrapperMethod`, etc. The XML file is a *per-assembly* data store; `//types/@was_scanned` specifies whether or not the assembly was scanned for JCWs. If the assembly did not contain JCWs (e.g. `System.Private.CoreLib.dll`), then `was_scanned` will be False.
dbc94c8 to
af3be8d
Compare
Today, application builds use Cecil to scan through all Android assemblies at least twice:
_LinkAssembliesNoShrink,_GenerateJavaStubs_ILLink,_GenerateJavaStubs,_RemoveRegisterAttributeThis is a costly operation that we would like to only perform once to help speed up application builds. The long-term goal is to move the (many) steps currently performed by
_GenerateJavaStubsinto "linker" steps.This PR moves the Cecil scanning required for the first step: "generating Java Callable Wrappers".
Implementation
This does not move generating JCW Java code to the linker, it only moves scanning for the JLO information needed to generate JCWs to the linker. This information is persisted as an XML file in
/objnext to the processed assembly.Example:
obj/Debug/net10.0-android/android/assets/MyApplication.dllobj/Debug/net10.0-android/android/assets/MyApplication.jlo.xmlDoing it this way has two benefits: it helps keep the linker from getting too complex (eg: generating Java code) and it helps incremental builds. If an assembly has not changed since the last build, the JLO information in the
.jlo.xmlis still valid and will be reused. The assembly does not need to be re-scanned. (For example, the ~70 AndroidX assemblies that the MAUI template uses do not need to be re-scanned on every Debug build.)The process of actually generating the JCWs is done by a new
GenerateJavaCallableWrapperstask in the_GenerateJavaStubstarget that consumes the.jlo.xmlfiles and outputs the.javafiles.Release Builds
In an ideal Release build, we would (probably?) run this new step as part of the
ILLinkinvocation. However there are some tricky limitations to running in the linker pipeline, chiefly being that the linker cannot load external assemblies likeJava.Interop.Tool.JavaCallableWrappers.dll(and dependencies) that we need. This can be worked around by including the needed source files in the linker assembly, but this will require additional work.Instead, for now, we will add an invocation a new
_LinkAssembliesAdditionalStepstarget to linked builds that runs afterILLink. This calls the existingLinkAssembliesNoShrinktask to scan for JLOs but does not execute the other linker tasks that have already been run. This allows us to move forward with converting to "linker steps" for making Debug builds faster without the extra work required to move these steps intoILLink. A future enhancement can perform this work if desired. (There is also some question as to whether we actually want to create newILLinklinker steps.)Note this temporarily leaves the
$(_AndroidJLOCheckedBuild)flag that compares the generated Java files to the previous ones. This will likely be useful ensuring future moved steps are correct as well.