[mono] Decompress ICU data during iOS/tvOS app startup (#64967)
authorimhameed <imhameed@microsoft.com>
Tue, 1 Mar 2022 21:15:35 +0000 (16:15 -0500)
committerGitHub <noreply@github.com>
Tue, 1 Mar 2022 21:15:35 +0000 (16:15 -0500)
commit002647fce853a359a826461c06f5f530e2bb939e
tree32b4f473fbbcfe0154dbe066c30f781c9d130d51
parent2dd232a53c38ac874b15fe504df275b660988294
[mono] Decompress ICU data during iOS/tvOS app startup (#64967)

This change adds support for decompressing ICU data files using the lzfse
decompressor built into Apple OSes.

If the data file path passed to `GlobalizationNative_LoadICUData` does not have
`.lzfse` as a suffix, then the contents of this file will be mapped into
shareable memory and passed directly to ICU.

Otherwise, the contents are decompressed using a fixed-size working buffer and
are stored inside a cache file with a name that contains a decimal encoded
representation of the originating compressed file's inode number, modification
time, and size. This filesystem metadata is extremely likely to change if the
contents of the source file ever changes, so there's no need to compute a
checksum of the data to determine if the cache is still valid. If a cache file
with an appropriate name is already present then it is mapped into shareable
memory and passed to ICU. Stale cache files (defined here to be any file with a
filename that ends with "-icudt.dat.uncompressed" that doesn't exactly match
the desired cache file name) are purged on startup.

`icudt.dat` for mobile is 2126 KiB right now; when compressed with lzfse it
shrinks to 675 KiB. On an iPhone SE 1st gen, this takes 4ms to decompress.

The "decompression framework" also supports lz4, zlib, and lzma. They are all
worse either in decompression time (lzma especially + zlib) or compression
ratio (lz4):

| Algorithm | icudt.dat compressed size | decompression time |
| --------- | ------------------------- | ------------------ |
| lz4       | 1031 KiB                  | 2.41 ms            |
| lzfse     | 675 KiB                   | 4.20 ms            |
| zlib      | 659 KiB                   | 9.61 ms            |
| lzma      | 427 KiB                   | 49.20 ms           |

I am not comfortable adding 50ms to app startup time. On this same
iPhone SE 1st gen, the "Contacts" app takes 166.7ms to display app-generated
pixels. This is end-to-end latency, from the first indication that iOS has
recognized my finger tap, to the display displaying any non-placeholder
content. This was measured with with a 240fps camera. A barebones
xamarin-macios app takes 125ms to display a "Hello world!" message.

This uses the filename's suffix to control decompression for simplicity, but it
would also be possible to instead frame compressed data with a very simple
header consisting of a long-enough magic number and a decompressed payload size
that can be used as a buffer sizing hint to the decompression loop.

Miscellany:
- On iOS, the cache-directory appears to be app-specific. Example: `/var/mobile/Containers/Data/Application/0C22E0D1-26CD-46CB-9EBC-6CF55B513ED1/Library/Caches/`.
- https://www.usenix.org/system/files/conference/osdi14/osdi14-paper-pillai.pdf
src/mono/mono/mini/CMakeLists.txt
src/native/libs/System.Globalization.Native/pal_icushim_static.c
src/tasks/AppleAppBuilder/Templates/CMakeLists.txt.template