Skip to content

Commit c927026

Browse files
authored
[monodroid] Embedded assemblies store (#6311)
What do we want? Faster (Release) App Startup! How do we get that? Assembly Stores! "In the beginning", assemblies were stored in the `assemblies` directory within the `.apk`. App startup would open the `.apk`, traverse all entries within the `.apk` looking for `assemblies/*.dll`, `assemblies/*.dll.config`, and `assemblies/*.pdb` files. When a "supported" `assemblies/*` entry was encountered, the entry would be **mmap**(2)'d so that it could be used; see also commit c195683. Of particular note is: 1. The need to enumerate *all* entries within the `.apk`, as there is no guarantee of entry ordering, and 2. The need for *N* `mmap()` invocations, one per assembly included in the app, *plus* additional `mmap()` invocations for the `.pdb` and `.dll.config` files, if present. Useful contextual note: a "modern" AndroidX-using app could pull in dozens to over 200 assemblies without really trying. There will be *lots* of `mmap()` invocations. Instead of adding (compressed! d236af5) data for each assembly separately, instead add a small set of "Assembly Store" files which contain the assembly & related data to use within the app: * `assemblies/assemblies.blob` * `assemblies/assemblies.[ARCHITECTURE].blob` `assemblies.[ARCHITECTURE].blob` contains architecture-specific assemblies, e.g. `System.Private.CoreLib.dll` built for x86 would be placed within `assemblies.x86.blob`. `ARCHITECTURE` is one of `x86`, `x86_64`, `armeabi_v7a`, or `arm64_v8a`; note use of `_` instead of `-`, which is different from the `lib/ARCHITECTURE` convention within `.apk` files. This is done because this is apparently what Android and `bundletool` do, e.g. creating `split_config.armeabi_v7a.apk`. Once the architecture-neutral `assemblies.blob` and appropriate (singular!) `assemblies.[ARCHITECTURE].blob` for the current architecture is found and `mmap()`'d, `.apk` entry traversal can end. There is no longer a need to parse the entire `.apk` during startup. The reduction in the number of `mmap()` system calls required can have a noticeable impact on process startup, particularly with .NET SDK for Android & MAUI; see below for timing details. The assembly store format uses the followings structures: struct AssemblyStoreHeader { uint32_t magic, version; uint32_t local_entry_count; // Number of AssemblyStoreAssemblyDescriptor entries uint32_t global_entry_count; // Number of AssemblyStoreAssemblyDescriptor entries in entire app, across all *.blob files uint32_t store_id; }; struct AssemblyStoreAssemblyDescriptor { uint32_t data_offset, data_size; // Offset from beginning of file for .dll data uint32_t debug_data_offset, debug_data_size; // Offset from beginning of file for .pdb data uint32_t config_data_offset, config_data_size; // Offset from beginning of file for .dll.config data }; struct AssemblyStoreHashEntry { union { uint64_t hash64; // 64-bit xxhash of assembly filename uint32_t hash64; // 32-bit xxhash of assembly filename }; uint32_t mapping_index, local_store_index, store_id; }; The assembly store format is roughly as follows: AssemblyStoreHeader header {…}; AssemblyStoreAssemblyDescriptor assemblies [header.local_entry_count]; // The following two entries exist only when header.store_id == 0 AssemblyStoreHashEntry hashes32[header.global_entry_count]; AssemblyStoreHashEntry hashes64[header.global_entry_count]; uint8_t data[]; Note that `AssemblyStoreFileFormat::hashes32` and `AssemblyStoreFileFormat::hashes64` are *sorted by their hash*. Further note that assembly *filenames* are not present. `EmbeddedAssemblies::blob_assemblies_open_from_bundles()` will hash the filename, then binary search the appropriate `hashes*` array to get the appropriate assembly information. As the assembly store format doesn't include assembly names, `.apk` and `.aab` files will also contain an `assemblies.manifest` file, which contains the assembly names and other information in a human- readable format; it is also used by `assembly-store-reader`: Hash 32 Hash 64 Blob ID Blob idx Name 0xa2e0939b 0x4288cfb749e4c631 000 0000 Xamarin.AndroidX.Activity … 0xad6f1e8a 0x6b0ff375198b9c17 001 0000 System.Private.CoreLib Add a new `tools/assembly-store-reader` utility which can read the new `assemblies*.blob` files: % tools/scripts/read-assembly-store path/to/app.apk Store set 'base_assemblies': Is complete set? yes Number of stores in the set: 5 Assemblies: 0: Name: Xamarin.AndroidX.Activity Store ID: 0 (shared) Hashes: 32-bit == 0xa2e0939b; 64-bit == 0x4288cfb749e4c631 Assembly image: offset == 1084; size == 14493 Debug data: absent Config file: absent … 16: Name: System.Private.CoreLib Store ID: 1 (x86) Hashes: 32-bit == 0xad6f1e8a; 64-bit == 0x6b0ff375198b9c17 Assembly image: offset == 44; size == 530029 Debug data: absent Config file: absent … On a Pixel 3 XL (arm64-v8a) running Android 12 with MAUI 6.0.101-preview.10.1952, we observe: ~~ MAUI: Displayed Time ~~ | Before ms | After ms | Δ | Notes | | ---------:| --------: | -----------: | ------------------------------------- | | 1016.800 | 892.600 | -12.21% ✓ | defaults; profiled AOT; 32-bit build | | 1016.100 | 894.700 | -11.95% ✓ | defaults; profiled AOT; 64-bit build | | 1104.200 | 922.000 | -16.50% ✓ | defaults; full AOT+LLVM; 64-bit build | | 1102.700 | 926.100 | -16.02% ✓ | defaults; full AOT; 32-bit build | | 1108.400 | 932.600 | -15.86% ✓ | defaults; full AOT; 64-bit build | | 1106.300 | 932.600 | -15.70% ✓ | defaults; full AOT+LLVM; 32-bit build | | 1292.000 | 1271.800 | -1.56% ✓ | defaults; 64-bit build | | 1307.000 | 1275.400 | -2.42% ✓ | defaults; 32-bit build | Displayed time reduces by ~12% when Profiled AOT is used. It is interesting to note that **Displayed time** is nearly identical for the default (JIT) settings case. It's most probably caused by the amount of JIT-ed code between `OnCreate()` and the time when the application screen is presented, most likely the time is spent JIT-ing MAUI rendering code. ~~ MAUI: Total native init time (before `OnCreate()`) ~~ | Before ms | After ms | Δ | Notes | | --------: | --------: | -----------: | ------------------------------------- | | 96.727 | 88.921 | -8.07% ✓ | defaults; 32-bit build | | 97.236 | 89.693 | -7.76% ✓ | defaults; 64-bit build | | 169.315 | 108.845 | -35.71% ✓ | defaults; profiled AOT; 32-bit build | | 170.061 | 109.071 | -35.86% ✓ | defaults; profiled AOT; 64-bit build | | 363.864 | 208.949 | -42.57% ✓ | defaults; full AOT; 64-bit build | | 363.629 | 209.092 | -42.50% ✓ | defaults; full AOT; 32-bit build | | 373.203 | 218.289 | -41.51% ✓ | defaults; full AOT+LLVM; 64-bit build | | 372.783 | 219.003 | -41.25% ✓ | defaults; full AOT+LLVM; 32-bit build | Note that "native init time" includes running `JNIEnv.Initialize()`, which requires loading `Mono.Android.dll` + dependencies such as `System.Private.CoreLib.dll`, which in turn means that the AOT DSOs such as `libaot-System.Private.CoreLib.dll.so` must *also* be loaded. The loading of the AOT DSOs is why JIT is fastest here (no AOT DSOs), and why Profiled AOT is faster than Full AOT (smaller DSOs). ~~ Plain Xamarin.Android: Displayed Time ~~ | Before ms | After ms | Δ | Notes | | --------: | --------: | -----------: | ------------------------------------- | | 289.300 | 251.000 | -13.24% ✓ | defaults; full AOT+LLVM; 64-bit build | | 286.300 | 252.900 | -11.67% ✓ | defaults; full AOT; 64-bit build | | 285.700 | 255.300 | -10.64% ✓ | defaults; profiled AOT; 32-bit build | | 282.900 | 255.800 | -9.58% ✓ | defaults; full AOT+LLVM; 32-bit build | | 286.100 | 256.500 | -10.35% ✓ | defaults; full AOT; 32-bit build | | 286.100 | 258.000 | -9.82% ✓ | defaults; profiled AOT; 64-bit build | | 328.900 | 310.600 | -5.56% ✓ | defaults; 32-bit build | | 319.300 | 313.000 | -1.97% ✓ | defaults; 64-bit build | ~~ Plain Xamarin.Android: Total native init time (before `OnCreate()`) ~~ | Before ms | After ms | Δ | Notes | | --------: | --------: | -----------: | ------------------------------------- | | 59.768 | 42.694 | -28.57% ✓ | defaults; profiled AOT; 64-bit build | | 60.056 | 42.990 | -28.42% ✓ | defaults; profiled AOT; 32-bit build | | 65.829 | 48.684 | -26.05% ✓ | defaults; full AOT; 64-bit build | | 65.688 | 48.713 | -25.84% ✓ | defaults; full AOT; 32-bit build | | 67.159 | 49.938 | -25.64% ✓ | defaults; full AOT+LLVM; 64-bit build | | 67.514 | 50.465 | -25.25% ✓ | defaults; full AOT+LLVM; 32-bit build | | 66.758 | 62.531 | -6.33% ✓ | defaults; 32-bit build | | 67.252 | 62.829 | -6.58% ✓ | defaults; 64-bit build |
1 parent e0f3683 commit c927026

File tree

72 files changed

+3847
-533
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+3847
-533
lines changed

Configuration.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<!-- Should correspond to the first value from `$(API_LEVELS)` in `build-tools/api-xml-adjuster/Makefile` -->
2626
<AndroidFirstFrameworkVersion Condition="'$(AndroidFirstFrameworkVersion)' == ''">v4.4</AndroidFirstFrameworkVersion>
2727
<AndroidFirstApiLevel Condition="'$(AndroidFirstApiLevel)' == ''">19</AndroidFirstApiLevel>
28+
<AndroidJavaRuntimeApiLevel Condition="'$(AndroidJavaRuntimeApiLevel)' == ''">21</AndroidJavaRuntimeApiLevel>
2829
<!-- The min API level supported by Microsoft.Android.Sdk, should refactor/remove when this value is the same as $(AndroidFirstApiLevel) -->
2930
<AndroidMinimumDotNetApiLevel Condition="'$(AndroidMinimumDotNetApiLevel)' == ''">21</AndroidMinimumDotNetApiLevel>
3031
<AndroidFirstPlatformId Condition="'$(AndroidFirstPlatformId)' == ''">$(AndroidFirstApiLevel)</AndroidFirstPlatformId>

Documentation/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
* [Submitting Bugs, Feature Requests, and Pull Requests][bugs]
1313
* [Directory Structure](project-docs/ExploringSources.md)
14+
* [Assembly store format](project-docs/AssemblyStores.md)
1415

1516
[bugs]: https://github.com/xamarin/xamarin-android/wiki/Submitting-Bugs,-Feature-Requests,-and-Pull-Requests
1617

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
2+
**Table of Contents**
3+
4+
- [Assembly Store format and purpose](#assembly-store-format-and-purpose)
5+
- [Rationale](#rationale)
6+
- [Store kinds and locations](#store-kinds-and-locations)
7+
- [Store format](#store-format)
8+
- [Common header](#common-header)
9+
- [Assembly descriptor table](#assembly-descriptor-table)
10+
- [Index store](#index-store)
11+
- [Hash table format](#hash-table-format)
12+
13+
<!-- markdown-toc end -->
14+
15+
# Assembly Store format and purpose
16+
17+
Assembly stores are binary files which contain the managed
18+
assemblies, their debug data (optionally) and the associated config
19+
file (optionally). They are placed inside the Android APK/AAB
20+
archives, replacing individual assemblies/pdb/config files.
21+
22+
Assembly stores are an optional form of assembly storage in the
23+
archive, they can be used in all build configurations **except** when
24+
Fast Deployment is in effect (in which case assemblies aren't placed
25+
in the archives at all, they are instead synchronized from the host to
26+
the device/emulator filesystem)
27+
28+
## Rationale
29+
30+
During native startup, the Xamarin.Android runtime looks inside the
31+
application APK file for the managed assemblies (and their associated
32+
pdb and config files, if applicable) in order to map them (using the
33+
`mmap(2)` call) into memory so that they can be given to the Mono
34+
runtime when it requests a given assembly is loaded. The reason for
35+
the memory mapping is that, as far as Android is concerned, managed
36+
assembly files are just data/resources and, thus, aren't extracted to
37+
the filesystem. As a result, Mono wouldn't be able to find the
38+
assemblies by scanning the filesystem - the host application
39+
(Xamarin.Android) must give it a hand in finding them.
40+
41+
Applications can contain hundreds of assemblies (for instance a Hello
42+
World MAUI application currently contains over 120 assemblies) and
43+
each of them would have to be mmapped at startup, together with its
44+
pdb and config files, if found. This not only costs time (each `mmap`
45+
invocation is a system call) but it also makes the assembly discovery
46+
an O(n) algorithm, which takes more time as more assemblies are added
47+
to the APK/AAB archive.
48+
49+
An assembly store, however, needs to be mapped only once and any
50+
further operations are merely pointer arithmetic, making the process
51+
not only faster but also reducing the algorithm complexity to O(1).
52+
53+
# Store kinds and locations
54+
55+
Each application will contain at least a single assembly store, with
56+
assemblies that are architecture-agnostics and any number of
57+
architecture-specific stores. dotnet ships with a handful of
58+
assemblies that **are** architecture-specific - those assemblies are
59+
placed in an architecture specific store, one per architecture
60+
supported by and enabled for the application. On the execution time,
61+
the Xamarin.Android runtime will always map the architecture-agnostic
62+
store and one, and **only** one, of the architecture-specific stores.
63+
64+
Stores are placed in the same location in the APK/AAB archive where the
65+
individual assemblies traditionally live, the `assemblies/` (for APK)
66+
and `base/root/assemblies/` (for AAB) folders.
67+
68+
The architecture agnostic store is always named `assemblies.blob` while
69+
the architecture-specific one is called `assemblies.[ARCH].blob`.
70+
71+
Each APK in the application (e.g. the future Feature APKs) **may**
72+
contain the above two assembly store files (some APKs may contain only
73+
resources, other may contain only native libraries etc)
74+
75+
Currently, Xamarin.Android applications will produce only one set of
76+
stores but when Xamarin.Android adds support for Android Features, each
77+
feature APK will contain its own set of stores. All of the APKs will
78+
follow the location, format and naming conventions described above.
79+
80+
# Store format
81+
82+
Each store is a structured binary file, using little-endian byte order
83+
and aligned to a byte boundary. Each store consists of a header, an
84+
assembly descriptor table and, optionally (see below), two tables with
85+
assembly name hashes. All the stores are assigned a unique ID, with
86+
the store having ID equal to `0` being the [Index store](#index-store)
87+
88+
Assemblies are stored as adjacent byte streams:
89+
90+
- **Image data**
91+
Required to be present for all assemblies, contains the actual
92+
assembly PE image.
93+
- **Debug data**
94+
Optional. Contains the assembly's PDB or MDB debug data.
95+
- **Config data**
96+
Optional. Contains the assembly's .config file. Config data
97+
**must** be terminated with a `NUL` character (`0`), this is to
98+
make runtime code slightly more efficient.
99+
100+
All the structures described here are defined in the
101+
[`xamarin-app.hh`](../../src/monodroid/jni/xamarin-app.hh) file.
102+
Should there be any difference between this document and the
103+
structures in the header file, the information from the header is the
104+
one that should be trusted.
105+
106+
## Common header
107+
108+
All kinds of stores share the following header format:
109+
110+
struct AssemblyStoreHeader
111+
{
112+
uint32_t magic;
113+
uint32_t version;
114+
uint32_t local_entry_count;
115+
uint32_t global_entry_count;
116+
uint32_t store_id;
117+
;
118+
119+
Individual fields have the following meanings:
120+
121+
- `magic`: has the value of 0x41424158 (`XABA`)
122+
- `version`: a value increased every time assembly store format changes.
123+
- `local_entry_count`: number of assemblies stored in this assembly
124+
store (also the number of entries in the assembly descriptor
125+
table, see below)
126+
- `global_entry_count`: number of entries in the index store's (see
127+
below) hash tables and, thus, the number of assemblies stored in
128+
**all** of the assembly stores across **all** of the application's
129+
APK files, all the other assembly stores have `0` in this field
130+
since they do **not** have the hash tables.
131+
- `store_id`: a unique ID of this store.
132+
133+
## Assembly descriptor table
134+
135+
Each store header is followed by a table of
136+
`AssemblyStoreHeader.local_entry_count` entries, each entry
137+
defined by the following structure:
138+
139+
struct AssemblyStoreAssemblyDescriptor
140+
{
141+
uint32_t data_offset;
142+
uint32_t data_size;
143+
uint32_t debug_data_offset;
144+
uint32_t debug_data_size;
145+
uint32_t config_data_offset;
146+
uint32_t config_data_size;
147+
};
148+
149+
Only the `data_offset` and `data_size` fields must have a non-zero
150+
value, other fields describe optional data and can be set to `0`.
151+
152+
Individual fields have the following meanings:
153+
154+
- `data_offset`: offset of the assembly image data from the
155+
beginning of the store file
156+
- `data_size`: number of bytes of the image data
157+
- `debug_data_offset`: offset of the assembly's debug data from the
158+
beginning of the store file. A value of `0` indicates there's no
159+
debug data for this assembly.
160+
- `debug_data_size`: number of bytes of debug data. Can be `0` only
161+
if `debug_data_offset` is `0`
162+
- `config_data_offset`: offset of the assembly's config file data
163+
from the beginning of the store file. A value of `0` indicates
164+
there's no config file data for this assembly.
165+
- `config_data_size`: number of bytes of config file data. Can be
166+
`0` only if `config_data_offset` is `0`
167+
168+
## Index store
169+
170+
Each application will contain exactly one store with a global index -
171+
two tables with assembly name hashes. All the other stores **do not**
172+
contain these tables. Two hash tables are necessary because hashes
173+
for 32-bit and 64-bit devices are different.
174+
175+
The hash tables follow the [Assembly descriptor
176+
table](#assembly-descriptor-table) and precede the individual assembly
177+
streams.
178+
179+
Placing the hash tables in a single index store, while "wasting" a
180+
certain amount of memory (since 32-bit devices won't use the 64-bit
181+
table and vice versa), makes for simpler and faster runtime
182+
implementation and the amount of memory wasted isn't big (1000
183+
two tables which are 8kb long each, this being the amount of memory
184+
wasted)
185+
186+
### Hash table format
187+
188+
Both tables share the same format, despite the hashes themselves being
189+
of different sizes. This is done to make handling of the tables
190+
easier on the runtime.
191+
192+
Each entry contains, among other fields, the assembly name hash. In
193+
case of satellite assemblies, the assembly culture (e.g. `en/` or
194+
`fr/`) is treated as part of the assembly name, thus resulting in a
195+
unique hash. The hash value is obtained using the
196+
[xxHash](https://cyan4973.github.io/xxHash/) algorithm and is
197+
calculated **without** including the `.dll` extension. This is done
198+
for runtime efficiency as the vast majority of Mono requests to load
199+
an assembly does not include the `.dll` suffix, thus saving us time of
200+
appending it in order to generate the hash for index lookup.
201+
202+
Each entry is represented by the following structure:
203+
204+
struct AssemblyStoreHashEntry
205+
{
206+
union {
207+
uint64_t hash64;
208+
uint32_t hash32;
209+
};
210+
uint32_t mapping_index;
211+
uint32_t local_store_index;
212+
uint32_t store_id;
213+
};
214+
215+
Individual fields have the following meanings:
216+
217+
- `hash64`/`hash32`: the 32-bit or 64-bit hash of the assembly's name
218+
**without** the `.dll` suffix
219+
- `mapping_index`: index into a compile-time generated array of
220+
assembly data pointers. This is a global index, unique across
221+
**all** the APK files comprising the application.
222+
- `local_store_index`: index into assembly store [Assembly descriptor table](#assembly-descriptor-table)
223+
describing the assembly.
224+
- `store_id`: ID of the assembly store containing the assembly

Xamarin.Android.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "decompress-assemblies", "to
148148
EndProject
149149
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "tmt", "tools\tmt\tmt.csproj", "{1A273ED2-AE84-48E9-9C23-E978C2D0CB34}"
150150
EndProject
151+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "assembly-store-reader", "tools\assembly-store-reader\assembly-store-reader.csproj", "{DA50FC92-7FE7-48B5-BDB6-CDA57B37BB51}"
152+
EndProject
151153
Global
152154
GlobalSection(SharedMSBuildProjectFiles) = preSolution
153155
src\Xamarin.Android.NamingCustomAttributes\Xamarin.Android.NamingCustomAttributes.projitems*{3f1f2f50-af1a-4a5a-bedb-193372f068d7}*SharedItemsImports = 5
@@ -408,6 +410,10 @@ Global
408410
{1FED3F23-1175-42AA-BE87-EF1E8DB52F8B}.Debug|AnyCPU.Build.0 = Debug|Any CPU
409411
{1FED3F23-1175-42AA-BE87-EF1E8DB52F8B}.Release|AnyCPU.ActiveCfg = Release|Any CPU
410412
{1FED3F23-1175-42AA-BE87-EF1E8DB52F8B}.Release|AnyCPU.Build.0 = Release|Any CPU
413+
{DA50FC92-7FE7-48B5-BDB6-CDA57B37BB51}.Debug|AnyCPU.ActiveCfg = Debug|anycpu
414+
{DA50FC92-7FE7-48B5-BDB6-CDA57B37BB51}.Debug|AnyCPU.Build.0 = Debug|anycpu
415+
{DA50FC92-7FE7-48B5-BDB6-CDA57B37BB51}.Release|AnyCPU.ActiveCfg = Release|anycpu
416+
{DA50FC92-7FE7-48B5-BDB6-CDA57B37BB51}.Release|AnyCPU.Build.0 = Release|anycpu
411417
EndGlobalSection
412418
GlobalSection(SolutionProperties) = preSolution
413419
HideSolutionNode = FALSE
@@ -474,6 +480,7 @@ Global
474480
{37FCD325-1077-4603-98E7-4509CAD648D6} = {864062D3-A415-4A6F-9324-5820237BA058}
475481
{88B746FF-8D6E-464D-9D66-FF2ECCF148E0} = {864062D3-A415-4A6F-9324-5820237BA058}
476482
{1A273ED2-AE84-48E9-9C23-E978C2D0CB34} = {864062D3-A415-4A6F-9324-5820237BA058}
483+
{DA50FC92-7FE7-48B5-BDB6-CDA57B37BB51} = {864062D3-A415-4A6F-9324-5820237BA058}
477484
{1FED3F23-1175-42AA-BE87-EF1E8DB52F8B} = {04E3E11E-B47D-4599-8AFC-50515A95E715}
478485
EndGlobalSection
479486
GlobalSection(ExtensibilityGlobals) = postSolution

build-tools/automation/azure-pipelines.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,9 @@ stages:
397397
cancelTimeoutInMinutes: 2
398398
workspace:
399399
clean: all
400+
variables:
401+
CXX: g++-10
402+
CC: gcc-10
400403
steps:
401404
- checkout: self
402405
clean: true

build-tools/installers/create-installers.targets

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@
290290
<_MSBuildFiles Include="$(MSBuildSrcDir)\Xamarin.SourceWriter.dll" />
291291
<_MSBuildFiles Include="$(MSBuildSrcDir)\Xamarin.SourceWriter.pdb" />
292292
<_MSBuildFiles Include="$(MSBuildSrcDir)\K4os.Compression.LZ4.dll" />
293+
<_MSBuildFiles Include="$(MSBuildSrcDir)\K4os.Hash.xxHash.dll" />
293294
</ItemGroup>
294295
<ItemGroup>
295296
<_MSBuildTargetsSrcFiles Include="$(MSBuildTargetsSrcDir)\Xamarin.Android.AvailableItems.targets" />

0 commit comments

Comments
 (0)