Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
12 changes: 12 additions & 0 deletions src/Xamarin.Android.Build.Tasks/Tasks/CompileNativeAssembly.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ sealed class Config
public string? AssemblerPath;
public string? AssemblerOptions;
public string? InputSource;
public string? OutputFile;
}

[Required]
Expand All @@ -43,6 +44,14 @@ public override System.Threading.Tasks.Task RunTaskAsync ()

void RunAssembler (Config config)
{
if (config.OutputFile is not null && config.InputSource is not null && File.Exists (config.OutputFile)) {
string sourceFile = Path.Combine (WorkingDirectory, Path.GetFileName (config.InputSource));
if (File.Exists (sourceFile) && File.GetLastWriteTimeUtc (config.OutputFile) >= File.GetLastWriteTimeUtc (sourceFile)) {
LogDebugMessage ($"[LLVM llc] Skipping '{Path.GetFileName (config.InputSource)}' because '{Path.GetFileName (config.OutputFile)}' is up to date");
return;
}
}

var stdout_completed = new ManualResetEvent (false);
var stderr_completed = new ManualResetEvent (false);
var psi = new ProcessStartInfo () {
Expand Down Expand Up @@ -118,10 +127,13 @@ IEnumerable<Config> GetAssemblerConfigs ()
string executableDir = Path.GetDirectoryName (llcPath);
string executableName = MonoAndroidHelper.GetExecutablePath (executableDir, Path.GetFileName (llcPath));

string outputFilePath = Path.Combine (WorkingDirectory, sourceFile.Replace (".ll", ".o"));

yield return new Config {
InputSource = item.ItemSpec,
AssemblerPath = Path.Combine (executableDir, executableName),
AssemblerOptions = $"{assemblerOptions} -o={outputFile} {QuoteFileName (sourceFile)}",
OutputFile = outputFilePath,
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1289,6 +1289,37 @@ public void GenerateJavaStubsAndAssembly ([Values] bool isRelease, [Values] Andr
}
}

[Test]
public void CompileNativeAssemblySourcesSkipsUnchangedFiles ([Values (AndroidRuntime.CoreCLR)] AndroidRuntime runtime)
{
if (IgnoreUnsupportedConfiguration (runtime, release: false)) {
return;
}

var proj = new XamarinAndroidApplicationProject ();
proj.SetRuntime (runtime);

string abi = "arm64-v8a";
proj.SetRuntimeIdentifier (abi);

using (var b = CreateApkBuilder ()) {
b.Verbosity = LoggerVerbosity.Detailed;
Assert.IsTrue (b.Build (proj), "first build should have succeeded.");

// Modify MainActivity to trigger recompilation of typemap sources
proj.MainActivity = proj.DefaultMainActivity + Environment.NewLine + "// test comment";
proj.Touch ("MainActivity.cs");
Assert.IsTrue (b.Build (proj), "second build should have succeeded.");

Assert.IsFalse (b.Output.IsTargetSkipped ("_CompileNativeAssemblySources"), "`_CompileNativeAssemblySources` should *not* be skipped!");

// At least one .ll file should have been skipped as up to date (e.g., environment.arm64-v8a.ll)
StringAssertEx.ContainsRegex (@"\[LLVM llc\] Skipping.*up to date", b.LastBuildOutput,
message: "Expected at least one .ll file to be skipped as up to date"
);
}
}

readonly string [] ExpectedAssemblyFiles = new [] {
Path.Combine ("android", "environment.@ABI@.o"),
Path.Combine ("android", "environment.@ABI@.ll"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,6 @@ public override string GetComment (object data, string fieldName)
{
var entry = EnsureType<TypeMapAssembly> (data);

if (MonoAndroidHelper.StringEquals ("mvid_hash", fieldName)) {
return $" MVID: {entry.MVID}";
}

if (MonoAndroidHelper.StringEquals ("name_offset", fieldName)) {
return $" {entry.Name}";
}
Expand Down Expand Up @@ -171,11 +167,6 @@ sealed class TypeMapAssembly
[NativeAssembler (Ignore = true)]
public string Name = String.Empty;

[NativeAssembler (Ignore = true)]
public Guid MVID;

[NativeAssembler (UsesDataProvider = true, NumberFormat = LlvmIrVariableNumberFormat.Hexadecimal)]
public ulong mvid_hash;
public ulong name_length;

[NativeAssembler (UsesDataProvider = true)]
Expand Down Expand Up @@ -274,21 +265,26 @@ protected override void Construct (LlvmIrModule module)
Log.LogMessage ("Managed-to-java typemaps will use string-based matching.");
}

// Sort assemblies by name before building the blob so that both the blob offsets
// and the uniqueAssemblies array are in a deterministic order that is stable across
// incremental builds (assembly names don't change, unlike MVIDs).
data.UniqueAssemblies.Sort ((a, b) => StringComparer.Ordinal.Compare (a.Name, b.Name));

var assemblyNamesBlob = new LlvmIrStringBlob ();
foreach (TypeMapGenerator.TypeMapDebugAssembly asm in data.UniqueAssemblies) {
(int assemblyNameOffset, int assemblyNameLength) = assemblyNamesBlob.Add (asm.Name);

var entry = new TypeMapAssembly {
Name = asm.Name,
MVID = asm.MVID,

mvid_hash = MonoAndroidHelper.GetXxHash (asm.MVIDBytes, is64Bit: true),
name_length = (ulong)assemblyNameLength, // without the trailing NUL
name_offset = (ulong)assemblyNameOffset,
};
uniqueAssemblies.Add (new StructureInstance<TypeMapAssembly> (typeMapAssemblyStructureInfo, entry));
}

// Sort by assembly name for deterministic output. This ensures the .ll content
// is stable across incremental builds when only MVIDs change.
uniqueAssemblies.Sort ((StructureInstance<TypeMapAssembly> a, StructureInstance<TypeMapAssembly> b) => {
if (a.Instance == null) {
return b.Instance == null ? 0 : -1;
Expand All @@ -298,7 +294,7 @@ protected override void Construct (LlvmIrModule module)
return 1;
}

return a.Instance.mvid_hash.CompareTo (b.Instance.mvid_hash);
return StringComparer.Ordinal.Compare (a.Instance.Name, b.Instance.Name);
});

var managedTypeInfos = new List<StructureInstance<TypeMapManagedTypeInfo>> ();
Expand Down
41 changes: 22 additions & 19 deletions src/native/clr/host/typemap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -141,30 +141,33 @@ auto TypeMapper::index_to_name (ssize_t idx, const char* typeName, const TypeMap
}

[[gnu::always_inline, gnu::flatten]]
auto TypeMapper::managed_to_java_debug (const char *typeName, const uint8_t *mvid) noexcept -> const char*
auto TypeMapper::managed_to_java_debug (const char *typeName, [[maybe_unused]] const uint8_t *mvid) noexcept -> const char*
{
dynamic_local_path_string full_type_name;
full_type_name.append (typeName);

hash_t mvid_hash = xxhash::hash (mvid, 16z); // we must hope managed land called us with valid data

auto equal = [](TypeMapAssembly const& entry, hash_t key) -> bool { return entry.mvid_hash == key; };
auto less_than = [](TypeMapAssembly const& entry, hash_t key) -> bool { return entry.mvid_hash < key; };
ssize_t idx = Search::binary_search<TypeMapAssembly, hash_t, equal, less_than> (mvid_hash, type_map_unique_assemblies, type_map.unique_assemblies_count);

if (idx >= 0) [[likely]] {
TypeMapAssembly const& assm = type_map_unique_assemblies[idx];
// type_map_unique_assemblies is sorted by assembly name for stable build output (no
// build-specific data like MVIDs). We iterate through assemblies to find which one
// contains this type by trying each "TypeName, AssemblyName" candidate against the
// managed-to-java map. The array is small (~80-100 entries), so this is negligible.
for (size_t i = 0; i < type_map.unique_assemblies_count; i++) {
TypeMapAssembly const& assm = type_map_unique_assemblies[i];

dynamic_local_path_string full_type_name;
full_type_name.append (typeName);
full_type_name.append (", "sv);

// We explicitly trust the build process here, with regards to validity of offsets
full_type_name.append (&type_map_assembly_names[assm.name_offset], assm.name_length);
} else {
log_warn (LOG_ASSEMBLY, "typemap: unable to look up assembly name for type '{}', trying without it."sv, typeName);

ssize_t idx = find_index_by_hash (full_type_name.get (), type_map.managed_to_java, type_map_managed_type_names, MANAGED, JAVA);
if (idx >= 0) {
return index_to_name (idx, full_type_name.get (), type_map.managed_to_java, type_map_java_type_names, MANAGED, JAVA);
}
}

// If hashes are used for matching, the type names array is not used. If, however, string-based matching is in
// effect, the managed type name is looked up and then...
idx = find_index_by_hash (full_type_name.get (), type_map.managed_to_java, type_map_managed_type_names, MANAGED, JAVA);
// Fallback: try without assembly name
dynamic_local_path_string full_type_name;
full_type_name.append (typeName);

log_warn (LOG_ASSEMBLY, "typemap: unable to look up assembly name for type '{}', trying without it."sv, typeName);

ssize_t idx = find_index_by_hash (full_type_name.get (), type_map.managed_to_java, type_map_managed_type_names, MANAGED, JAVA);

// ...either method gives us index into the Java type names array
return index_to_name (idx, full_type_name.get (), type_map.managed_to_java, type_map_java_type_names, MANAGED, JAVA);
Expand Down
1 change: 0 additions & 1 deletion src/native/clr/include/xamarin-app.hh
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ struct TypeMap
// MUST match src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs
struct TypeMapAssembly
{
xamarin::android::hash_t mvid_hash;
uint64_t name_length;
uint64_t name_offset; // into the assembly names blob
};
Expand Down
Loading