Skip to content

Replace AssemblyStore compression algorithm LZ4 with Zstd#11730

Open
Copilot wants to merge 9 commits into
mainfrom
copilot/replace-lz4-with-zstd
Open

Replace AssemblyStore compression algorithm LZ4 with Zstd#11730
Copilot wants to merge 9 commits into
mainfrom
copilot/replace-lz4-with-zstd

Conversation

Copilot AI commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Switches the per-assembly AssemblyStore compression from LZ4 (dotnet/lz4 + the K4os managed wrapper) to Zstandard, using the ZSTD_* entry points already exported by libSystem.IO.Compression.Native in the .NET runtime pack. This drops an external dependency and trades for a better compression ratio.

The 12-byte compressed-blob header (magic / descriptor index / uncompressed length) is unchanged; only the algorithm and magic value change. No FORMAT_VERSION bump is needed since the store and runtime are always built together by the same SDK.

Managed (build-time)

  • New Zstd.cs P/Invoke wrapper over libSystem.IO.Compression.Native (ZSTD_compress2 via a ZSTD_CCtx) using zstd's default compression level (3).
  • AssemblyCompression.cs compresses through Zstd instead of LZ4Codec; intermediate output extension .lz4.zst.
  • Follow-up hardening from review feedback:
    • CallingConvention.Cdecl on ZSTD_* imports.
    • Graceful handling of missing native entry points/libraries and ZSTD_* error returns.
    • Safer output-bound handling (MaximumOutputSize) and controlled encoding failure paths.

Magic value

  • COMPRESSED_DATA_MAGIC / managic constant XALZ (0x5A4C4158) → XAZS (0x535A4158), applied consistently across managed code, both native headers, and the diagnostic tools.

Native (load-time)

  • New runtime-base/zstd.hh declares the minimal entry points (ZSTD_decompress, ZSTD_isError, ZSTD_getErrorName) instead of pulling in zstd.h.
  • CLR (assembly-store.cc) and Mono (embedded-assemblies.cc) decompress via ZSTD_decompress, with error handling on ZSTD_isError.
size_t result = ZSTD_decompress (out, cad.uncompressed_file_size, in, data_size - sizeof (header));
if (ZSTD_isError (result)) { /* ZSTD_getErrorName (result) */ }

Build wiring

  • Runtimes link -lSystem.IO.Compression.Native instead of xa::lz4; removed HAVE_LZ4, the common/lz4 CMake library, the libxa-lz4 archive in NativeRuntimeComponents, and the LZ4 source references in Configuration.props / native.targets.

Tooling

  • decompress-assemblies and tmt detect the new magic and decompress using System.IO.Compression.ZstandardDecoder; unused K4os.Compression.LZ4 references removed from the affected tool projects.
  • tmt follow-up fixes from review feedback:
    • full-buffer reads for compressed payloads (no single-read assumption),
    • pooled-buffer return on all failure paths.

Out of scope

  • fastdev (FastDeploy / xamarin.sync) still uses LZ4 for fast-deploy-over-adb. As a result external/lz4 and the external/xamarin-android-tools K4os reference are intentionally retained; fully removing the LZ4 dependency requires a follow-up to migrate fastdev.

Validation notes

  • CI exercised the CoreCLR app-build wiring; the release arm64 CoreCLR size baselines (BuildReleaseArm64{Simple,XForms}DotNet.CoreCLR.apkdesc) were refreshed from the CI-produced APK descriptors. Versus the LZ4 baselines on main, Zstd shrinks libassembly-store.so (Simple: 3,461,344 → 2,903,016 bytes; XForms: 14,137,920 → 11,658,648 bytes).
  • The standalone diagnostic tools now target net11.0 and use System.IO.Compression.ZstandardDecoder for Zstd payloads, avoiding native Zstd P/Invokes in the tools.

Benchmark: .NET MAUI app on device

Measured a Release .NET MAUI app (Microsoft.Maui.Controls 11.0.0-preview.2.26152.10, net11.0-android, android-arm64, CoreCLR, profiled AOT) built against two locally-built SDKs: this PR (Zstd) vs its merge-base on main (LZ4). Only the AssemblyStore compression differs. Device: Samsung Galaxy A16 (SM-A165F), Android 16 (API 36), arm64-v8a.

Metric LZ4 (main) Zstd (this PR) Δ
libassembly-store.so (uncompressed) 16,771,520 B 13,744,600 B −3,026,920 B (−18.0%)
APK size (extractNativeLibs=false) 35,195,691 B 32,099,115 B −3,096,576 B (−8.8%)
Cold start TotalTime, median 831 ms 851 ms +20 ms (+2.4%)
Cold start TotalTime, mean 823 ms 850 ms +27 ms (+3.3%)
  • Startup is 29 interleaved adb shell am start -S -W samples per variant (alternating launches to neutralise thermal drift; first launch discarded as warmup). Zstd's slightly slower cold start reflects Zstd's higher decompression cost vs LZ4, traded for the ~3 MB smaller store.
  • Sizes use extractNativeLibs=false, which stores the store .so uncompressed and page-aligned — what Google Play serves for .aab on API 26+. With the default extractNativeLibs=true, the raw .apk is a poor proxy: the APK zip re-Deflates the store, and Deflate shrinks the more-redundant LZ4 payload further, so the naive .apk size can look larger for Zstd despite the smaller on-device footprint.
  • Store magic verified in the built APKs: XALZ (LZ4, main) vs XAZS (Zstd, this PR).

Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Copilot AI changed the title [WIP] Replace AssemblyStore compression algorithm LZ4 with Zstd Replace AssemblyStore compression algorithm LZ4 with Zstd Jun 24, 2026
Copilot AI requested a review from simonrozsival June 24, 2026 16:18
Comment thread src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs
Refresh BuildReleaseArm64 CoreCLR APK descriptors from PR CI after switching assembly-store compression to Zstd.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

Switch the net10.0 diagnostic tools from DllImport to source-generated LibraryImport for Zstd decompression entry points.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

Retarget the diagnostic tools to net11.0 so they can use System.IO.Compression.ZstandardDecoder instead of native Zstd P/Invokes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

Remove the small decompress-assemblies helper and call ZstandardDecoder.TryDecompress directly at the compressed payload site.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival marked this pull request as ready for review June 25, 2026 12:12
Copilot AI review requested due to automatic review settings June 25, 2026 12:12
@simonrozsival

Copy link
Copy Markdown
Member

/review

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown

Android PR Reviewer completed successfully!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the AssemblyStore per-assembly compression pipeline from LZ4 to Zstandard (Zstd), removing the managed LZ4 dependency and updating managed tooling + native runtimes to read/decompress the new payloads.

Changes:

  • Switched build-time assembly compression to Zstd and updated compressed blob magic (XALZXAZS) and intermediate file extension (.lz4.zst).
  • Updated MonoVM/CoreCLR native runtime code paths to decompress assemblies via ZSTD_decompress from libSystem.IO.Compression.Native, and removed the internal LZ4 native library wiring.
  • Updated diagnostic tools (tmt, decompress-assemblies) to detect the new magic and decompress using System.IO.Compression.ZstandardDecoder; refreshed APK descriptor baselines.
Show a summary per file
File Description
tools/tmt/tmt.csproj Moves tool to $(DotNetTargetFramework) and removes K4os LZ4 dependency.
tools/tmt/ApkManagedTypeResolver.cs Updates magic and decompression implementation to Zstd decoder.
tools/decompress-assemblies/main.cs Updates magic and decompression implementation to Zstd decoder.
tools/decompress-assemblies/decompress-assemblies.csproj Moves tool to $(DotNetTargetFramework) and removes K4os LZ4 dependency.
tools/assembly-store-reader/assembly-store-reader.csproj Removes K4os LZ4 package reference.
tools/assembly-store-reader-mk2/assembly-store-reader.csproj Removes K4os LZ4 package reference.
src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets Routes compression intermediate output to android\\zstd\\ and updates target comment.
src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs Introduces managed P/Invoke wrapper for Zstd entry points in System.IO.Compression.Native.
src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.cs Drops libxa-lz4 archive from runtime component list.
src/Xamarin.Android.Build.Tasks/Utilities/AssemblyStoreAssemblyInfo.cs Updates filename normalization for .zst compressed artifacts.
src/Xamarin.Android.Build.Tasks/Utilities/AssemblyCompression.cs Uses Zstd wrapper for compression and emits .zst outputs + new magic.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc Updates expected APK contents/sizes after runtime + store changes.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc Updates expected APK contents/sizes after runtime + store changes.
src/Xamarin.Android.Build.Tasks/Tasks/CompressAssemblies.cs Updates task summary comment to reflect Zstd.
src/native/native.targets Removes LZ4 CMake and source inputs from native build wiring.
src/native/mono/xamarin-app-stub/xamarin-app.hh Updates COMPRESSED_DATA_MAGIC constant to XAZS.
src/native/mono/monodroid/embedded-assemblies.cc Switches runtime decompression from LZ4 to Zstd and removes HAVE_LZ4 gating.
src/native/mono/monodroid/CMakeLists.txt Removes HAVE_LZ4 define and links -lSystem.IO.Compression.Native.
src/native/common/lz4/CMakeLists.txt Deletes internal LZ4 static library definition.
src/native/common/include/runtime-base/zstd.hh Adds minimal Zstd entry point declarations for native runtime usage.
src/native/common/include/runtime-base/timing-internal.hh Updates timing description string to “Zstd decompression”.
src/native/CMakeLists.txt Removes add_subdirectory(common/lz4).
src/native/clr/include/xamarin-app.hh Updates COMPRESSED_DATA_MAGIC constant to XAZS.
src/native/clr/host/CMakeLists.txt Removes HAVE_LZ4 define and links -lSystem.IO.Compression.Native.
src/native/clr/host/assembly-store.cc Switches runtime decompression from LZ4 to Zstd and removes HAVE_LZ4 gating.
Configuration.props Removes LZ4 source directory/full-path properties.

Copilot's findings

  • Files reviewed: 26/26 changed files
  • Comments generated: 6

Comment thread src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets Outdated
Comment thread tools/tmt/ApkManagedTypeResolver.cs Outdated
Comment thread src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs Outdated
Comment thread src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs
Comment thread src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs
Comment thread src/Xamarin.Android.Build.Tasks/Utilities/AssemblyCompression.cs Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Android PR Review — Replace AssemblyStore LZ4 with Zstd

Overall this is a clean, well-executed change. I formed an independent assessment and verified the load-bearing assumptions:

  • The ZSTD exports are real on .NET 11. libSystem.IO.Compression.Native in the .NET 11 runtime pack exports the raw, unprefixed ZSTD_* entry points — dotnet/runtime's own Interop.Zstd.cs P/Invokes the same symbols from System.IO.Compression.Native, and System.IO.Compression.ZstandardDecoder exists. The TFM bumps for decompress-assemblies/tmt (DotNetStableTargetFrameworkDotNetTargetFramework) are required for that decoder and are correct. (Note: this firmly ties the feature to .NET 11+; .NET 10 does not export these.)
  • Magic value change is correct and consistent. XALZ (0x5A4C4158) → XAZS (0x535A4158), applied across the managed writer, both native headers (CLR + mono stub), and the tools. The 12-byte header layout is unchanged.
  • Native decompression is sound. Both assembly-store.cc (CLR) and embedded-assemblies.cc (Mono) feed data_size - sizeof(CompressedAssemblyHeader) as the compressed size, check ZSTD_isError, surface ZSTD_getErrorName, and validate the produced size against cad.uncompressed_file_size. The #if defined(HAVE_LZ4) && defined(RELEASE)#if defined(RELEASE) guard change tracks the HAVE_LZ4 removal.
  • LZ4 removal is clean. No dangling K4os/LZ4Codec references remain in the changed scope; the assembly-store-reader* tools only had unused package refs removed, and external/lz4 is correctly retained for FastDev.

A few non-blocking 💡 suggestions (posted inline on Zstd.cs):

  1. Add a fast round-trip unit test for the new Zstd interop so an ABI break is caught quickly in CI rather than only in RELEASE apkdesc/device runs.
  2. Check the ZSTD_CCtx_setParameter return value so a failed level-set doesn't silently fall back to the default level.
  3. Use a file-scoped namespace in the new file to match its siblings.
  4. Confirm the build-time cost of the ultra compression level (22) on the Release hot path.

CI: builds were green / in-progress at review time with no failures attributable to these changes; the documented Release size-threshold artifacts reflect the smaller libassembly-store.so, not build breaks. Worth a final green CI before merge.

Nice work — the approach is correct and the diff is tidy. None of the above is blocking.

Generated by Android PR Reviewer for issue #11730 · 1.7K AIC · ⌖ 48.6 AIC · ⊞ 37.9K
Comment /review to run again

Comment thread src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs Outdated
Comment thread src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs
Comment thread src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs Outdated
Comment thread src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs
@simonrozsival

Copy link
Copy Markdown
Member

@copilot address code review - let's reduce the quality level to reduce time needed for decompression. I also like the suggestions regarding testing and code organization. Make sure you address all of the comments.

Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>

Copilot AI commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

@copilot address code review - let's reduce the quality level to reduce time needed for decompression. I also like the suggestions regarding testing and code organization. Make sure you address all of...

Addressed in commit 78d6a04.

Implemented all review-thread fixes:

  • Reduced Zstd compression level and hardened Zstd.cs (CallingConvention.Cdecl, parameter/error checks, missing-entrypoint handling).
  • Added guards in AssemblyCompression for full input reads and invalid max output size.
  • Fixed tmt decompression robustness (full reads + pooled buffer return on all paths).
  • Fixed _AssembliesToCompress comment typo/reference in Xamarin.Android.Common.targets.

Validation run: affected projects build successfully; targeted Xamarin.Android.Build.Tests filter run was attempted but fails in this environment due pre-existing test environment setup (TestEnvironment.DotNetPreviewDirectory null).

@simonrozsival

Copy link
Copy Markdown
Member

/review

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

Android PR Reviewer completed successfully!

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Solid, well-scoped swap from LZ4 to Zstandard — and nice hardening in response to the earlier review round (Cdecl on the P/Invokes, -1/EncodingFailed instead of crashing on missing exports, pooled-buffer return + ReadFully).

Verified

  • Header layout unchanged; the magic/extension swap (XALZXAZS, 0x5A4C41580x535A4158, .lz4.zst) is applied consistently across AssemblyCompression.cs, the CLR/Mono native readers (assembly-store.cc, embedded-assemblies.cc), and the tmt/decompress-assemblies tools. No stale XALZ/LZ4 references remain outside the intentionally-retained fastdev path.
  • Both native readers map ZSTD_decompress (dst, dstCapacity = uncompressed_length, src, srcSize = compressed_size) correctly and check ZSTD_isError before use.
  • The managed Zstd P/Invokes and the native -lSystem.IO.Compression.Native link target the raw, unprefixed ZSTD_* exports of the runtime pack — matching how dotnet/runtime's own Interop.Zstd binds them — so the mechanism is sound. ZstandardDecoder.TryDecompress (net11) is used correctly, and the netstandard2.0 build-task wrapper is justified (the BCL Zstandard types aren't reachable from netstandard2.0).

Comments

  • ⚠️ Inline (Zstd.cs): the Compress XML doc still says "maximum compression level" while the code uses level 3 (zstd's default). Please reconcile the doc with the chosen level.
  • 💡 I couldn't find a fast managed round-trip test (Zstd.CompressZstandardDecoder.TryDecompress) — re-raising the earlier suggestion, since binding to internal runtime-pack exports means an ABI break would otherwise only surface late in a Release apkdesc or device run.

CI: Azure DevOps build 1482888 is still in_progress across all 38 checks, so this isn't validated green yet — worth confirming the Release apkdesc diffs and device suites pass before merge.

Submitting as comments only (not approving, per review policy).

Generated by Android PR Reviewer for issue #11730 · 1.9K AIC · ⌖ 48 AIC · ⊞ 40K
Comment /review to run again

Comment thread src/Xamarin.Android.Build.Tasks/Utilities/Zstd.cs Outdated
simonrozsival and others added 2 commits June 26, 2026 16:41
The size-regression baselines committed for the Zstd switch were generated
in a different environment and did not match the artifacts produced by CI,
causing BuildReleaseArm64 (CoreCLR) to fail the apkdiff regression check.

Replace the Simple and XForms CoreCLR .apkdesc files with the descriptors
produced by the failing CI run (build 1482888). Compared to the LZ4
baselines on main, Zstd still shrinks libassembly-store.so (Simple:
3,461,344 -> 2,903,016; XForms: 14,137,920 -> 11,658,648); the previous
committed values were simply too optimistic.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The XML doc said compression used "the maximum compression level," but
Compress sets ZstdCompressionLevel = 3, which is zstd's default level
(not its maximum, 22). This was a leftover from an earlier ZSTD_maxCLevel ()
version. Update the doc to describe the actual behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival added copilot `copilot-cli` or other AIs were used to author this ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). labels Jun 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

copilot `copilot-cli` or other AIs were used to author this ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants