Conversation
Store the full ICU data file, one-byte builtin sources, and the V8 startup snapshot plus code caches as zstd frames instead of raw bytes. Decompress each once at startup and keep it for the process lifetime. On macOS Release, link the executable with -dead_strip, -x, and -S. A Release arm64 macOS binary dropped from 140 MB to 59 MB. Intl, builtins, the snapshot, and code cache behavior are unchanged. Startup RSS rose by about 40 MB because those blobs are private heap instead of file-backed pages. Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
Wrap lines past 80 columns and store the zstd frame size in uint64_t. Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
-Wl,-dead_strip on the Release executable removed napi_* and other globals that nothing in the executable calls. Addons resolve those symbols from the host, so doc-kit's lightningcss addon called a null pointer inside napi_register_module_v1 and macOS CI died with SIGSEGV while generating docs. -x and -S stay; they only drop local and debug symbols. Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com> Assisted-by: Grok
udata_setCommonData() stores the package address and later lookups return pointers into that storage, so the decompressed buffer cannot be freed. Inflating the full ICU data file at startup turned every page into private dirty memory. Map the original data from the executable again so unused pages stay demand-paged, clean, and shareable across processes. Builtin sources and the startup snapshot remain compressed. Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com> Assisted-by: Grok
The ICU data file is a zstd frame in the binary. The first process inflates it into a file under the temp directory and maps that file read-only. Later processes map the same file, so the pages stay clean, demand-paged, and shared instead of a private dirty copy. If the temp directory cannot be written, startup keeps the private buffer so ICU still works. Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com> Assisted-by: Grok
Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com> Assisted-by: Grok
|
Review requested:
|
| '<(PRODUCT_DIR)/zstd_compress<(EXECUTABLE_SUFFIX)', | ||
| '<(icu_data_in)', | ||
| ], | ||
| 'outputs': [ '<(SHARED_INTERMEDIATE_DIR)/node_icu_zstd.dat' ], |
There was a problem hiding this comment.
Don't drop the version number and endianness here.
Instead name the file icudt78l.dat.z or something (add a suffix).
This will cause a lot of harm trying to figure out which version of the data file is being used.
| '-d', '<(SHARED_INTERMEDIATE_DIR)', | ||
| '-n', 'icudata', | ||
| '-e', 'icudt<(icu_ver_major)', | ||
| '-e', 'node_icu_zstd', |
There was a problem hiding this comment.
disruptive and unnecessary to change the entry point here, making it unrelated to icu
| '-e', 'node_icu_zstd', | |
| '-e', 'icudt<(icu_ver_major)<(icu_endianness)_dat_zstd', |
|
Repeating (here) my suggestion to discuss this upstream with ICU, that's where it should be implemented. See https://unicode-org.atlassian.net/browse/ICU-6429 for example. I would support that. As I noted, I'm working on an adhoc on platform cost of supporting more and more languages in CLDR consumers (such as ICU, such as CLDR). |
| return nullptr; | ||
| } | ||
| std::string path; | ||
| if (CachePath(expect_hash, &path)) { |
There was a problem hiding this comment.
Am I missing something, or is the integrity of the cached data on disk not checked before it's mapped in?
I noticed it's created mode 0600, but processes run by the user could still tamper with it to unexpected results, right? Or does the ICU have internal integrity checks for udata_setCommonData data?
There was a problem hiding this comment.
Magic numbers are checked and sizes checked, but no the data is not checksummed etc. That would be part of an install process.
There was a problem hiding this comment.
But this file is written here by node into a temp directory, right? No install process there...
IOW, what I'm thinking is:
- node starts, notices no cache file, decompresses ICU data into cache file, does whatever, exits
- something tampers with the cache file
- node starts, notices cache file, maps it into memory... and ICU is confused and crashes or worse
There was a problem hiding this comment.
Always possible but given the node.js trust model (any code that runs is trusted) that really wouldn't be considered a vulnerability. Given how ICU data is used, it wouldn't be considered a high risk.
Packagers pass --with-icu-compress. The default build keeps the data file mapped from the binary. The compressed blob is named icudt<version><endian>.dat.z and its symbol stays an ICU data name. The cache file is checked against the hash stored in the binary before ICU is pointed at it. A rewritten file is discarded and built again. Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com> Assisted-by: Grok
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #66211 +/- ##
==========================================
- Coverage 90.28% 90.26% -0.03%
==========================================
Files 790 791 +1
Lines 272044 272900 +856
Branches 51949 52119 +170
==========================================
+ Hits 245625 246328 +703
- Misses 16915 17023 +108
- Partials 9504 9549 +45
🚀 New features to boost your workflow:
|
srl295
left a comment
There was a problem hiding this comment.
It's conditional.. but i don't think this is mergeable. Let's say it's an interesting experiment.
| if options.with_icu_compress and with_intl not in ('small-icu', 'full-icu'): | ||
| error('--with-icu-compress requires --with-intl=full-icu or small-icu') |
There was a problem hiding this comment.
This would be better done in the if system-icu branch.
| 'toolsets': ['host'], | ||
| 'dependencies': ['zstd#host'], | ||
| 'include_dirs': ['lib'], | ||
| 'sources': ['../../tools/zstd_compress.cc'], |
There was a problem hiding this comment.
I do wonder if bringing in a whole new implementation file is the best option. Isn't there any compression anywhere linked into node already?
| * - when NOT in NODE_HAVE_SMALL_ICU mode, ICU is linked directly with its full | ||
| * data. All of the variables and command line options for changing data at | ||
| * runtime are disabled, as they wouldn't fully override the internal data. | ||
| * - Full and small ICU data are stored as a zstd frame in the binary. |
There was a problem hiding this comment.
Out of date, needs to mention the 'if'
| 'WARNING_CFLAGS': [ '-Werror' ], | ||
| }, | ||
| }], | ||
| # The Release executable's local symbol table and STABS dominate |
| #if !defined(MAP_ANON) && defined(MAP_ANONYMOUS) | ||
| #define MAP_ANON MAP_ANONYMOUS | ||
| #endif | ||
| void* ptr = mmap( |
There was a problem hiding this comment.
I'm sorry, this is very messy within Node. This should be handled upstream in ICU, which already has its own abstraction for mmap.
| #include "node_v8_platform-inl.h" | ||
| #include "simdjson.h" | ||
| #include "timers.h" | ||
| #include "zstd.h" |
Depends on #66186. The ICU-only diff is compress-embedded-blobs...compress-icu-data.
Why
Full ICU data is 33 MB of clean, shareable pages in the binary. Those pages should stay that way. The bytes stored in the executable do not have to be the raw file. Whether that tradeoff is worth it is a packager decision, so it is off unless requested.
What
./configure --with-icu-compressstores the data file as a zstd frame namedicudt<version><endian>.dat.z(33,105,536 bytes down to 9,223,488, header included). The entry point stays an ICU data symbol,icudt<version><endian>_dat_zstd_dat.The first process inflates it into
icudt78l-<sha256>.datunder the temp directory, mode 0600, and maps that file read-only. Later processes map the same file. Before the mapping is handed to ICU, the bytes are checked against the SHA-256 stored in the binary. A file that does not match is deleted and written again. If the temp directory cannot be written, startup keeps the private buffer so ICU still runs.--icu-data-dirstill wins when it is set.Without the flag, ICU data stays the normal linked file.