Skip to content
Merged
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 @@ -111,6 +111,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 @@ -168,6 +169,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 @@ -179,6 +181,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 @@ -284,6 +287,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 @@ -286,6 +287,7 @@ static bool ShouldSkipAssembly (ITaskItem assembly)
ManagedMarshalMethodsLookupEnabled = EnableManagedMarshalMethodsLookup,
IgnoreSplitConfigs = ShouldIgnoreSplitConfigs (),
HaveAssemblyStore = UseAssemblyStore,
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
Expand Up @@ -42,4 +42,37 @@ public void HaveAssemblyStoreIsEmittedForCoreCLR (bool haveAssemblyStore)
var config = (EnvironmentHelper.ApplicationConfig_CoreCLR)EnvironmentHelper.ReadApplicationConfig (environmentFiles, AndroidRuntime.CoreCLR);
Assert.AreEqual (haveAssemblyStore, config.have_assembly_store);
}

[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 string android_package_name = String.Empty;
public bool managed_marshal_methods_lookup_enabled;
public bool have_assembly_store;
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.have_assembly_store = ConvertFieldToBool ("have_assembly_store", 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 @@ -772,6 +778,7 @@ static void AssertApplicationConfigIsIdentical (ApplicationConfig_CoreCLR firstA
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.have_assembly_store, secondAppConfig.have_assembly_store, $"Field 'have_assembly_store' 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 string android_package_name = String.Empty;
public bool managed_marshal_methods_lookup_enabled;
public bool have_assembly_store;
public bool assembly_store_decompression_cache_enabled;
}
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ sealed class DsoCacheState
public bool ManagedMarshalMethodsLookupEnabled { get; set; }
public bool IgnoreSplitConfigs { get; set; }
public bool HaveAssemblyStore { 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 @@ -284,6 +285,7 @@ protected override void Construct (LlvmIrModule module)
jni_remapping_replacement_method_index_entry_count = (uint)JniRemappingReplacementMethodIndexEntryCount,
android_package_name = AndroidPackageName,
have_assembly_store = HaveAssemblyStore,
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);

Comment thread
simonrozsival marked this conversation as resolved.
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 CRC32 for CoreCLR; uint/ulong xxhash for MonoVM depending on target bitness
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_MONOVM_64BIT = 0x80000003; // Must match the ASSEMBLY_STORE_FORMAT_VERSION native constant
const uint ASSEMBLY_STORE_FORMAT_VERSION_MONOVM_32BIT = 0x00000003;
const uint ASSEMBLY_STORE_FORMAT_VERSION_MONOVM_64BIT = 0x80000004; // Must match the ASSEMBLY_STORE_FORMAT_VERSION native constant
const uint ASSEMBLY_STORE_FORMAT_VERSION_MONOVM_32BIT = 0x00000004;
const uint ASSEMBLY_STORE_FORMAT_VERSION_CORECLR_64BIT = 0x80000004; // Must match the ASSEMBLY_STORE_FORMAT_VERSION native constant
const uint ASSEMBLY_STORE_FORMAT_VERSION_CORECLR_32BIT = 0x00000004;

Expand Down Expand Up @@ -129,7 +131,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 @@ -164,7 +166,7 @@ string Generate (string baseOutputDirectory, AndroidTargetArch arch, List<Assemb
fs.Seek (0, SeekOrigin.Begin);

uint storeVersion = GetAssemblyStoreFormatVersion (is64Bit);
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 @@ -185,6 +187,13 @@ string Generate (string baseOutputDirectory, AndroidTargetArch arch, List<Assemb
throw new InvalidOperationException ($"Internal error: store '{storePath}' position is different than metadata size after header write");
}

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;
}

Expand All @@ -206,6 +215,20 @@ uint GetAssemblyStoreFormatVersion (bool is64Bit)
return is64Bit ? ASSEMBLY_STORE_FORMAT_VERSION_MONOVM_64BIT : ASSEMBLY_STORE_FORMAT_VERSION_MONOVM_32BIT;
}

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 @@ -262,6 +285,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 @@ -272,8 +296,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 @@ -167,6 +167,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved.
<AndroidFragmentType Condition=" '$(AndroidFragmentType)' == '' ">Android.App.Fragment</AndroidFragmentType>
<AndroidEnableAssemblyCompression Condition=" '$(AndroidEnableAssemblyCompression)' == '' ">True</AndroidEnableAssemblyCompression>
<_AndroidAssemblyStoreCompressionLevel Condition=" '$(_AndroidAssemblyStoreCompressionLevel)' == '' ">3</_AndroidAssemblyStoreCompressionLevel>
<AndroidEnableAssemblyStoreDecompressionCache Condition=" '$(AndroidEnableAssemblyStoreDecompressionCache)' == '' ">False</AndroidEnableAssemblyStoreDecompressionCache>
<AndroidIncludeWrapSh Condition=" '$(AndroidIncludeWrapSh)' == '' ">False</AndroidIncludeWrapSh>
<_AndroidCheckedBuild Condition=" '$(_AndroidCheckedBuild)' == '' "></_AndroidCheckedBuild>

Expand Down Expand Up @@ -1809,6 +1810,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
Loading