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
6 changes: 6 additions & 0 deletions Documentation/building/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ Overridable MSBuild properties include:
assemblies placed in the APK will be compressed in `Release` builds. `Debug`
builds are not affected.

* `$(AndroidEnableAssemblyStoreDecompressionCache)`: Defaults to `False`. When
enabled for a CoreCLR `Release` build, decompressed assemblies are cached in
the app's Android code-cache directory and mapped from there on subsequent
launches. The cache consumes additional on-device storage and is rebuilt
after app or platform updates.

## Options suitable for local development

### Native runtime (`src/native`)
Expand Down
4 changes: 4 additions & 0 deletions Documentation/project-docs/AssemblyStores.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ The header is a fixed-size structure at the beginning of each assembly store fil
- **ENTRY_COUNT** (`uint32_t`) - Number of assemblies in the store
- **INDEX_ENTRY_COUNT** (`uint32_t`) - Number of entries in the index (typically `ENTRY_COUNT * 2`)
- **INDEX_SIZE** (`uint32_t`) - Index size in bytes
- **CONTENT_ID** (`uint64_t`) - Deterministic xxHash3 of everything after the header

## [INDEX]

Expand Down Expand Up @@ -154,6 +155,7 @@ All kinds of stores share the following header format:
uint32_t entry_count;
uint32_t index_entry_count;
uint32_t index_size; // index size in bytes
uint64_t content_id;
};

Individual fields have the following meanings:
Expand All @@ -165,6 +167,7 @@ Individual fields have the following meanings:
table, see below)
- `index_entry_count`: number of entries in the index
- `index_size`: index size in bytes
- `content_id`: deterministic xxHash3 of the index, descriptors, names, and assembly data

## Assembly descriptor table

Expand Down Expand Up @@ -252,6 +255,7 @@ struct [[gnu::packed]] AssemblyStoreHeader final
uint32_t entry_count;
uint32_t index_entry_count;
uint32_t index_size; // index size in bytes
uint64_t content_id;
};
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public class GenerateNativeApplicationConfigSources : AndroidTask

public bool EnableMarshalMethods { get; set; }
public bool EnableManagedMarshalMethodsLookup { get; set; }
public bool AndroidEnableAssemblyStoreDecompressionCache { get; set; }
public string? RuntimeConfigBinFilePath { get; set; }
public string ProjectRuntimeConfigFilePath { get; set; } = String.Empty;
public string? BoundExceptionType { get; set; }
Expand Down Expand Up @@ -285,6 +286,7 @@ static bool ShouldSkipAssembly (ITaskItem assembly)
MarshalMethodsEnabled = EnableMarshalMethods,
ManagedMarshalMethodsLookupEnabled = EnableManagedMarshalMethodsLookup,
IgnoreSplitConfigs = ShouldIgnoreSplitConfigs (),
AssemblyStoreDecompressionCacheEnabled = AndroidEnableAssemblyStoreDecompressionCache,
};
} else {
appConfigAsmGen = new ApplicationConfigNativeAssemblyGenerator (envBuilder.EnvironmentVariables, envBuilder.SystemProperties, Log) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Hashing;
using System.Linq;

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using NUnit.Framework;
using Xamarin.Android.Tasks;

namespace Xamarin.Android.Build.Tests.Tasks;

[TestFixture]
public class CreateAssemblyStoreTests : BaseTest
{
[Test]
public void ContentIdMatchesStoreContents ()
{
string testDirectory = Path.Combine (Root, "temp", nameof (ContentIdMatchesStoreContents));
Directory.CreateDirectory (testDirectory);

string assemblyPath = Path.Combine (testDirectory, "Example.dll.zst");
byte [] assemblyData = [1, 3, 3, 7, 9, 11, 17, 23];
File.WriteAllBytes (assemblyPath, assemblyData);

var metadata = new Dictionary<string, string> {
["Abi"] = "arm64-v8a",
};
var task = new CreateAssemblyStore {
BuildEngine = new MockBuildEngine (TestContext.Out),
AppSharedLibrariesDir = Path.Combine (testDirectory, "stores"),
ResolvedFrameworkAssemblies = [],
ResolvedUserAssemblies = [new TaskItem (assemblyPath, metadata)],
SupportedAbis = ["arm64-v8a"],
TargetRuntime = "CoreCLR",
UseAssemblyStore = true,
};

Assert.IsTrue (task.Execute (), "CreateAssemblyStore should succeed.");

string storePath = task.AssembliesToAddToArchive.Single ().ItemSpec;
byte [] store = File.ReadAllBytes (storePath);
using var reader = new BinaryReader (new MemoryStream (store));
Assert.AreEqual (0x41424158u, reader.ReadUInt32 (), "Unexpected assembly store magic.");
Assert.AreEqual (0x80010004u, reader.ReadUInt32 (), "Unexpected arm64 assembly store version.");
reader.BaseStream.Seek (3 * sizeof (uint), SeekOrigin.Current);
ulong contentId = reader.ReadUInt64 ();

Assert.AreEqual (
XxHash3.HashToUInt64 (store.AsSpan (5 * sizeof (uint) + sizeof (ulong))),
contentId,
"The content ID should hash everything after the assembly store header."
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#nullable enable
using System.IO;

using Microsoft.Build.Utilities;
using NUnit.Framework;
using Xamarin.Android.Tasks;
using Xamarin.ProjectTools;

namespace Xamarin.Android.Build.Tests.Tasks;

[TestFixture]
public class GenerateNativeApplicationConfigSourcesTests : BaseTest
{
[TestCase (false)]
[TestCase (true)]
public void AssemblyStoreDecompressionCacheSettingIsEmitted (bool enabled)
{
string outputRoot = Path.Combine (Root, "temp", $"{nameof (AssemblyStoreDecompressionCacheSettingIsEmitted)}-{enabled}");
string monoAndroidPath = Path.Combine (TestEnvironment.MonoAndroidFrameworkDirectory, "Mono.Android.dll");
FileAssert.Exists (monoAndroidPath);

var task = new GenerateNativeApplicationConfigSources {
BuildEngine = new MockBuildEngine (TestContext.Out),
ResolvedAssemblies = [new TaskItem (monoAndroidPath)],
EnvironmentOutputDirectory = Path.Combine (outputRoot, "android"),
SupportedAbis = ["arm64-v8a"],
AndroidPackageName = "com.microsoft.android.cachetest",
EnablePreloadAssembliesDefault = false,
TargetsCLR = true,
AndroidRuntime = "CoreCLR",
UseAssemblyStore = true,
AndroidEnableAssemblyStoreDecompressionCache = enabled,
};

Assert.IsTrue (task.Execute (), "GenerateNativeApplicationConfigSources should succeed.");

var environmentFiles = EnvironmentHelper.GatherEnvironmentFiles (
outputRoot,
"arm64-v8a",
required: true,
runtime: AndroidRuntime.CoreCLR
);
var config = (EnvironmentHelper.ApplicationConfig_CoreCLR)EnvironmentHelper.ReadApplicationConfig (environmentFiles, AndroidRuntime.CoreCLR);
Assert.AreEqual (enabled, config.assembly_store_decompression_cache_enabled);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ public sealed class ApplicationConfig_CoreCLR : IApplicationConfig
public uint jni_remapping_replacement_method_index_entry_count;
public string android_package_name = String.Empty;
public bool managed_marshal_methods_lookup_enabled;
public bool assembly_store_decompression_cache_enabled;
}

const uint ApplicationConfigFieldCount_CoreCLR = 20;
const uint ApplicationConfigFieldCount_CoreCLR = 21;

// This must be identical to the ApplicationConfig structure in src/native/mono/xamarin-app-stub/xamarin-app.hh
public sealed class ApplicationConfig_MonoVM : IApplicationConfig
Expand Down Expand Up @@ -407,6 +408,11 @@ static IApplicationConfig ReadApplicationConfig_CoreCLR (EnvironmentFile envFile
AssertFieldType (envFile.Path, parser.SourceFilePath, ".byte", field [0], item.LineNumber);
ret.managed_marshal_methods_lookup_enabled = ConvertFieldToBool ("managed_marshal_methods_lookup_enabled", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
break;

case 20: // assembly_store_decompression_cache_enabled: bool / .byte
AssertFieldType (envFile.Path, parser.SourceFilePath, ".byte", field [0], item.LineNumber);
ret.assembly_store_decompression_cache_enabled = ConvertFieldToBool ("assembly_store_decompression_cache_enabled", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
break;
}
fieldCount++;
}
Expand Down Expand Up @@ -771,6 +777,7 @@ static void AssertApplicationConfigIsIdentical (ApplicationConfig_CoreCLR firstA
Assert.AreEqual (firstAppConfig.environment_variable_count, secondAppConfig.environment_variable_count, $"Field 'environment_variable_count' has different value in environment file '{secondEnvFile}' than in environment file '{firstEnvFile}'");
Assert.AreEqual (firstAppConfig.system_property_count, secondAppConfig.system_property_count, $"Field 'system_property_count' has different value in environment file '{secondEnvFile}' than in environment file '{firstEnvFile}'");
Assert.AreEqual (firstAppConfig.android_package_name, secondAppConfig.android_package_name, $"Field 'android_package_name' has different value in environment file '{secondEnvFile}' than in environment file '{firstEnvFile}'");
Assert.AreEqual (firstAppConfig.assembly_store_decompression_cache_enabled, secondAppConfig.assembly_store_decompression_cache_enabled, $"Field 'assembly_store_decompression_cache_enabled' has different value in environment file '{secondEnvFile}' than in environment file '{firstEnvFile}'");
}

static void AssertApplicationConfigIsIdentical (ApplicationConfig_MonoVM firstAppConfig, string firstEnvFile, ApplicationConfig_MonoVM secondAppConfig, string secondEnvFile)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,5 @@ sealed class ApplicationConfigCLR
public uint jni_remapping_replacement_method_index_entry_count;
public string android_package_name = String.Empty;
public bool managed_marshal_methods_lookup_enabled;
public bool assembly_store_decompression_cache_enabled;
}
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ sealed class DsoCacheState
public bool MarshalMethodsEnabled { get; set; }
public bool ManagedMarshalMethodsLookupEnabled { get; set; }
public bool IgnoreSplitConfigs { get; set; }
public bool AssemblyStoreDecompressionCacheEnabled { get; set; }

public ApplicationConfigNativeAssemblyGeneratorCLR (IDictionary<string, string> environmentVariables, IDictionary<string, string> systemProperties,
IDictionary<string, string>? runtimeProperties, TaskLoggingHelper log)
Expand Down Expand Up @@ -373,6 +374,7 @@ protected override void Construct (LlvmIrModule module)
jni_remapping_replacement_type_count = (uint)JniRemappingReplacementTypeCount,
jni_remapping_replacement_method_index_entry_count = (uint)JniRemappingReplacementMethodIndexEntryCount,
android_package_name = AndroidPackageName,
assembly_store_decompression_cache_enabled = AssemblyStoreDecompressionCacheEnabled,
};
application_config = new StructureInstance<ApplicationConfigCLR> (applicationConfigStructureInfo, app_cfg);
module.AddGlobalVariable ("application_config", application_config);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ partial class AssemblyStoreGenerator
{
sealed class AssemblyStoreHeader
{
public const uint NativeSize = 5 * sizeof (uint);
public const uint NativeSize = 5 * sizeof (uint) + sizeof (ulong);

public readonly uint magic = ASSEMBLY_STORE_MAGIC;
public readonly uint version;
Expand All @@ -14,17 +14,19 @@ sealed class AssemblyStoreHeader

// Index size in bytes
public readonly uint index_size;
public readonly ulong content_id;

public AssemblyStoreHeader (uint version, uint entry_count, uint index_entry_count, uint index_size)
public AssemblyStoreHeader (uint version, uint entry_count, uint index_entry_count, uint index_size, ulong content_id)
{
this.version = version;
this.entry_count = entry_count;
this.index_entry_count = index_entry_count;
this.index_size = index_size;
this.content_id = content_id;
}
#if XABT_TESTS
public AssemblyStoreHeader (uint magic, uint version, uint entry_count, uint index_entry_count, uint index_size)
: this (version, entry_count, index_entry_count, index_size)
public AssemblyStoreHeader (uint magic, uint version, uint entry_count, uint index_entry_count, uint index_size, ulong content_id)
: this (version, entry_count, index_entry_count, index_size, content_id)
{
this.magic = magic;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Hashing;

using Microsoft.Android.Build.Tasks;
using Microsoft.Build.Utilities;
Expand All @@ -28,6 +29,7 @@ namespace Xamarin.Android.Tasks;
// [ENTRY_COUNT] uint; number of entries in the store
// [INDEX_ENTRY_COUNT] uint; number of entries in the index
// [INDEX_SIZE] uint; index size in bytes
// [CONTENT_ID] ulong: deterministic hash of everything after the header
//
// INDEX (variable size, HEADER.ENTRY_COUNT*2 entries, for assembly names with and without the extension)
// [NAME_HASH] uint on 32-bit platforms, ulong on 64-bit platforms; xxhash of the assembly name
Expand All @@ -53,8 +55,8 @@ partial class AssemblyStoreGenerator
const uint ASSEMBLY_STORE_MAGIC = 0x41424158; // 'XABA', little-endian, must match the BUNDLED_ASSEMBLIES_BLOB_MAGIC native constant

// Bit 31 is set for 64-bit platforms, cleared for the 32-bit ones
const uint ASSEMBLY_STORE_FORMAT_VERSION_64BIT = 0x80000003; // Must match the ASSEMBLY_STORE_FORMAT_VERSION native constant
const uint ASSEMBLY_STORE_FORMAT_VERSION_32BIT = 0x00000003;
const uint ASSEMBLY_STORE_FORMAT_VERSION_64BIT = 0x80000004; // Must match the ASSEMBLY_STORE_FORMAT_VERSION native constant
const uint ASSEMBLY_STORE_FORMAT_VERSION_32BIT = 0x00000004;

const uint ASSEMBLY_STORE_ABI_AARCH64 = 0x00010000;
const uint ASSEMBLY_STORE_ABI_ARM = 0x00020000;
Expand Down Expand Up @@ -122,7 +124,7 @@ string Generate (string baseOutputDirectory, AndroidTargetArch arch, List<Assemb
ulong curPos = assemblyDataStart;

Directory.CreateDirectory (Path.GetDirectoryName (storePath));
using var fs = File.Open (storePath, FileMode.Create, FileAccess.Write, FileShare.Read);
using var fs = File.Open (storePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
fs.Seek ((long)curPos, SeekOrigin.Begin);

uint mappingIndex = 0;
Expand Down Expand Up @@ -157,7 +159,7 @@ string Generate (string baseOutputDirectory, AndroidTargetArch arch, List<Assemb
fs.Seek (0, SeekOrigin.Begin);

uint storeVersion = is64Bit ? ASSEMBLY_STORE_FORMAT_VERSION_64BIT : ASSEMBLY_STORE_FORMAT_VERSION_32BIT;
var header = new AssemblyStoreHeader (storeVersion | abiFlag, infoCount, (uint)index.Count, (uint)(index.Count * IndexEntrySize ()));
var header = new AssemblyStoreHeader (storeVersion | abiFlag, infoCount, (uint)index.Count, (uint)(index.Count * IndexEntrySize ()), content_id: 0);
using var writer = new BinaryWriter (fs);
WriteHeader (writer, header);

Expand All @@ -178,11 +180,33 @@ string Generate (string baseOutputDirectory, AndroidTargetArch arch, List<Assemb
throw new InvalidOperationException ($"Internal error: store '{storePath}' position is different than metadata size after header write");
}

writer.Flush ();
ulong contentId = ComputeContentId (fs);
header = new AssemblyStoreHeader (storeVersion | abiFlag, infoCount, (uint)index.Count, (uint)(index.Count * IndexEntrySize ()), contentId);
fs.Seek (0, SeekOrigin.Begin);
WriteHeader (writer, header);
writer.Flush ();
log.LogDebugMessage ($"Assembly store content ID: 0x{contentId:x16}");

return storePath;

uint IndexEntrySize () => is64Bit ? AssemblyStoreIndexEntry.NativeSize64 : AssemblyStoreIndexEntry.NativeSize32;
}

static ulong ComputeContentId (Stream stream)
{
stream.Seek (AssemblyStoreHeader.NativeSize, SeekOrigin.Begin);

var hash = new XxHash3 ();
byte [] buffer = new byte [64 * 1024];
int bytesRead;
while ((bytesRead = stream.Read (buffer, 0, buffer.Length)) > 0) {
hash.Append (buffer.AsSpan (0, bytesRead));
}

return hash.GetCurrentHashAsUInt64 ();
}

void CopyData (FileInfo? src, Stream dest, string storePath)
{
if (src == null) {
Expand Down Expand Up @@ -239,6 +263,7 @@ void WriteHeader (BinaryWriter writer, AssemblyStoreHeader header)
writer.Write (header.entry_count);
writer.Write (header.index_entry_count);
writer.Write (header.index_size);
writer.Write (header.content_id);
}
#if XABT_TESTS
AssemblyStoreHeader ReadHeader (BinaryReader reader)
Expand All @@ -249,8 +274,9 @@ AssemblyStoreHeader ReadHeader (BinaryReader reader)
uint entry_count = reader.ReadUInt32 ();
uint index_entry_count = reader.ReadUInt32 ();
uint index_size = reader.ReadUInt32 ();
ulong content_id = reader.ReadUInt64 ();

return new AssemblyStoreHeader (magic, version, entry_count, index_entry_count, index_size);
return new AssemblyStoreHeader (magic, version, entry_count, index_entry_count, index_size, content_id);
}
#endif

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved.
<AndroidGenerateLayoutBindings Condition=" '$(AndroidGenerateLayoutBindings)' == '' ">False</AndroidGenerateLayoutBindings>
<AndroidFragmentType Condition=" '$(AndroidFragmentType)' == '' ">Android.App.Fragment</AndroidFragmentType>
<AndroidEnableAssemblyCompression Condition=" '$(AndroidEnableAssemblyCompression)' == '' ">True</AndroidEnableAssemblyCompression>
<AndroidEnableAssemblyStoreDecompressionCache Condition=" '$(AndroidEnableAssemblyStoreDecompressionCache)' == '' ">False</AndroidEnableAssemblyStoreDecompressionCache>
<AndroidIncludeWrapSh Condition=" '$(AndroidIncludeWrapSh)' == '' ">False</AndroidIncludeWrapSh>
<_AndroidCheckedBuild Condition=" '$(_AndroidCheckedBuild)' == '' "></_AndroidCheckedBuild>

Expand Down Expand Up @@ -1802,6 +1803,7 @@ because xbuild doesn't support framework reference assemblies.
BoundExceptionType="$(AndroidBoundExceptionType)"
RuntimeConfigBinFilePath="$(_BinaryRuntimeConfigPath)"
UseAssemblyStore="$(_AndroidUseAssemblyStore)"
AndroidEnableAssemblyStoreDecompressionCache="$(AndroidEnableAssemblyStoreDecompressionCache)"
EnableMarshalMethods="$(_AndroidUseMarshalMethods)"
EnableManagedMarshalMethodsLookup="$(_AndroidUseManagedMarshalMethodsLookup)"
CustomBundleConfigFile="$(AndroidBundleConfigurationFile)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public static void LoadApplication (Context context)
String language = locale.getLanguage () + "-" + locale.getCountry ();
String filesDir = context.getFilesDir ().getAbsolutePath ();
String cacheDir = context.getCacheDir ().getAbsolutePath ();
String codeCacheDir = context.getCodeCacheDir ().getAbsolutePath ();
String dataDir = getNativeLibraryPath (context);
ClassLoader loader = context.getClassLoader ();
String runtimeDir = getNativeLibraryPath (runtimePackage);
Expand All @@ -66,7 +67,7 @@ public static void LoadApplication (Context context)
//
// Should the order change here, src/native/clr/include/constants.hh must be updated accordingly
//
String[] appDirs = new String[] {filesDir, cacheDir, dataDir};
String[] appDirs = new String[] {filesDir, cacheDir, dataDir, codeCacheDir};
boolean haveSplitApks = runtimePackage.splitSourceDirs != null && runtimePackage.splitSourceDirs.length > 0;

System.loadLibrary("monodroid");
Expand Down
Loading