Skip to content
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

Add support for compressed assemblies in APK #4686

Merged
merged 2 commits into from
May 26, 2020

Commits on May 26, 2020

  1. Add support for compressed assemblies in APK

    Currently, `Xamarin.Android` supports managed assembly compression in
    the APK archive if application is bundled (with Mono's `mkbundle`) into
    a native shared library.  Managed assemblies are compressed using gzip
    compression and placed in an array inside the data section of the shared
    library.  However, support for `mkbundle` is possibly going to be
    removed and we realized it is a feature some developers appreciate since
    the produced APKs are smaller and the impact on startup time isn't big
    enough to worry.
    
    This commit aims to be a replacement for `mkbundle` with a handful of
    improvements thrown in.  First of all, the compression is performed
    using the [managed implementation][0] of the excellent [LZ4][1]
    algorithm.  This gives us a decent compression ratio and a much faster
    (de)compression speed than gzip/zlib offer.  Also, assemblies are stored
    directly in the APK in their usual directory, which allows us to `mmap`
    them on the runtime directly from the APK.  The build process calculates
    the size required to store the decompressed assemblies and adds a data
    section to `libxamarin-app.so` which makes Android allocate all the
    required memory when the DSO is loaded, thus removing the need of
    dynamic memory allocation and making the startup faster.
    
    Compression is supported only in `Release` builds and is enabled by
    default, but it can be turned off by setting the
    `$(AndroidEnableAssemblyCompression)` MSBuild property to `False`. If
    there's a need to turn compression off for an individual assembly by
    adding the `AndroidSkipCompression` metadata item to the assembly in
    question using code similar to this, in the application's project file:
    
        <AndroidCustomMetaDataForReferences Include="MyAssembly.dll">
           <AndroidSkipCompression>true</AssemblySkipCompression>
        </AndroidCustomMetaDataForReferences>
    
    The compressed assemblies still use their original name (e.g.
    `Mono.Android.dll`) so that we don't have to perform any string matching
    on the runtime in order to detect whether the assembly we are asked to
    load is compressed or not.  Instead, the compression code prepends a
    short header to each .dll file (in pseudo-code):
    
        uint32 magic = 0x5A4C4158; // 'XALZ', little-endian
        uint32 index; // Index into an internal assembly descriptor table
        uint32 uncompressed_length;
    
    The decompression code looks at the mmapped data and checks whether the
    above header is present.  If yes, the assembly is decompressed,
    otherwise it's loaded as-is.
    
    It is important to remember that the assemblies are compressed on the
    build time using LZ4 block compression which requires assembly data to
    be entirely loaded into memory (we do this instead of using the LZ4
    frame format to make decompression on the run time faster) before
    compression.  The compression output also requires a separate buffer,
    thus memory consumption will roughly be 1.5x the assembly size.
    However, since we use a byte buffer pool, memory consumption will not be
    a sum of all the assemblies but rather the size of the biggest one in
    the set.
    
    ~ Application Size ~
    
    A Xamarin.Forms "Hello World" application APK shrunk by 27% with this
    commit:
    
    | Before   | After    | Δ         |
    |----------|----------|-----------|
    | 23305194 | 16813034 | -27,85%   |
    
    Size comparison between this commit and APKs created with
    `$(BundleAssemblies) == True` depends on the number of enabled ABI
    targets in the application. For each ABI, `$(BundleAssemblies) == True`
    creates a separate shared library, so the amount of space consumed
    increases by the size of the bundle shared library.  The new compression
    scheme shares the compressed assemblies among all the enabled ABIs, thus
    effectively creating smaller multi-ABI APKs.
    
    In the tables below, `Before` refers to the APK created with
    `$(BundleAssemblies) == True`, `After` refers to the APK build with the
    new compression scheme.
    
    All ABIs enabled:
    
    | Before   | After    | Δ         |
    |----------|----------|-----------|
    | 27130240 | 16813034 | -38,03%   |
    
    Single ABI enabled:
    
    | Before   | After    | Δ         |
    |----------|----------|-----------|
    | 7783449  | 8746878  | +11,01%   |
    
    ~ Startup Performance ~
    
    Startup time of the same application isn't affected too much by
    decompression (comparison between uncompressed application and one
    compressed using the new scheme):
    
    ~ Before ~
    
    App configuration: **Release**
    
    Xamarin.Android
      - Version: **10.4.100-12**
      - Branch: **master**
      - Commit: **3f438e46d7b166a3a3ef54c9ffafb5f426760468**
    
    ~ After ~
    
    App configuration: **Release**
    
    Xamarin.Android
      - Version: **10.4.100-18**
      - Branch: **compress-assemblies**
      - Commit: **cec90e936478f9afbbc31b43e52164ecd5182c79**
    
    Device
      - Model: **Pixel 3 XL**
      - Native architecture: **arm64-v8a**
      - SDK version: **29**
    
    ~ Application Displayed Time ~
    
    | Before  | After   | Δ        | Notes                          |
    | ------- | ------- | -------- | ------------------------------ |
    | 795.800 | 793.800 | -0.25% ✓ | preload enabled; 32-bit build  |
    | 777.100 | 780.500 | +0.44% ✗ | preload disabled; 32-bit build |
    | 779.000 | 791.500 | +1.58% ✗ | preload enabled; 64-bit build  |
    | 776.000 | 781.400 | +0.69% ✗ | preload disabled; 64-bit build |
    
    Comparison of startup times between the `$(BundleAssemblies) == True`
    scheme and the new one with the same device and application as
    above (once again `Before` refers to the `$(BundleAssemblies)`
    application):
    
    | Before  | After   | Δ        | Notes                          |
    | ------- | ------- | -------- | ------------------------------ |
    | 855.600 | 793.800 | -7.22% ✓ | preload enabled; 32-bit build  |
    | 843.000 | 780.500 | -7.41% ✓ | preload disabled; 32-bit build |
    | 849.400 | 791.500 | -6.82% ✓ | preload enabled; 64-bit build  |
    | 841.600 | 781.400 | -7.15% ✓ | preload disabled; 64-bit build |
    
    [0]: https://www.nuget.org/packages/K4os.Compression.LZ4/
    [1]: https://github.com/lz4/lz4
    [2]: https://quixdb.github.io/squash-benchmark/#results-table
    grendello committed May 26, 2020
    Configuration menu
    Copy the full SHA
    f29332d View commit details
    Browse the repository at this point in the history
  2. Fix indentation

    Weird intermixing of spaces following tabs…
    jonpryor authored May 26, 2020
    Configuration menu
    Copy the full SHA
    e2a2541 View commit details
    Browse the repository at this point in the history