Skip to content

Commit 9ce7fe1

Browse files
committed
src: compress ICU data into a shared cache file
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
1 parent 465e71b commit 9ce7fe1

6 files changed

Lines changed: 619 additions & 20 deletions

File tree

‎deps/zstd/zstd.gyp‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,5 +103,13 @@
103103
],
104104
'toolsets': ['host', 'target'],
105105
},
106+
{
107+
'target_name': 'zstd_compress',
108+
'type': 'executable',
109+
'toolsets': ['host'],
110+
'dependencies': ['zstd#host'],
111+
'include_dirs': ['lib'],
112+
'sources': ['../../tools/zstd_compress.cc'],
113+
},
106114
]
107115
}

‎node.gyp‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,9 @@
920920
'msvs_disabled_warnings!': [4244],
921921

922922
'conditions': [
923+
[ 'icu_system!="true" and v8_enable_i18n_support==1', {
924+
'defines': [ 'NODE_HAVE_EMBEDDED_ICU_ZSTD=1' ],
925+
}],
923926
[ 'openssl_default_cipher_list!=""', {
924927
'defines': [
925928
'NODE_OPENSSL_DEFAULT_CIPHER_LIST="<(openssl_default_cipher_list)"'

‎src/node_i18n.cc‎

Lines changed: 297 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@
3333
* udata_setCommonData(SMALL_ICUDATA_ENTRY_POINT,...)
3434
* to load up the english+root data.
3535
*
36-
* - when NOT in NODE_HAVE_SMALL_ICU mode, ICU is linked directly with its full
37-
* data. All of the variables and command line options for changing data at
38-
* runtime are disabled, as they wouldn't fully override the internal data.
36+
* - Full and small ICU data are stored as a zstd frame in the binary.
37+
* The first process inflates that into a file under the temp directory
38+
* and maps it read-only. Later processes map the same file, so the
39+
* pages stay clean, demand-paged, and shared. --icu-data-dir still wins
40+
* when it is set.
3941
* See: http://bugs.icu-project.org/trac/ticket/10924
4042
*/
4143

@@ -68,8 +70,28 @@
6870
#include <unicode/uversion.h>
6971
#include "nbytes.h"
7072

71-
#ifdef NODE_HAVE_SMALL_ICU
73+
#if defined(NODE_HAVE_SMALL_ICU) || defined(NODE_HAVE_EMBEDDED_ICU_ZSTD)
7274
#include <unicode/udata.h>
75+
#endif
76+
77+
#ifdef NODE_HAVE_EMBEDDED_ICU_ZSTD
78+
#include "uv.h"
79+
#include "zstd.h"
80+
81+
#include "../tools/embed_sha256.h"
82+
83+
#include <cstring>
84+
#include <string>
85+
86+
#ifdef _WIN32
87+
#include <io.h>
88+
#include <windows.h>
89+
#else
90+
#include <sys/mman.h>
91+
#endif
92+
#endif
93+
94+
#ifdef NODE_HAVE_SMALL_ICU
7395

7496
/* if this is defined, we have a 'secondary' entry point.
7597
compare following to utypes.h defs for U_ICUDATA_ENTRY_POINT */
@@ -85,6 +107,270 @@
85107
extern "C" const char U_DATA_API SMALL_ICUDATA_ENTRY_POINT[];
86108
#endif
87109

110+
#ifdef NODE_HAVE_EMBEDDED_ICU_ZSTD
111+
extern "C" const uint8_t node_icu_zstd_dat[];
112+
113+
namespace {
114+
115+
constexpr size_t kIcuHeaderSize = 52;
116+
constexpr uint64_t kMaxIcuBytes = 256 * 1024 * 1024;
117+
118+
size_t Align16(size_t size) {
119+
return (size + 15u) & ~size_t{15};
120+
}
121+
122+
// Anonymous mapping, not the malloc heap. munmap actually drops the
123+
// decompress buffer once the cache file is mapped.
124+
uint8_t* AllocRaw(size_t size) {
125+
#ifdef _WIN32
126+
return static_cast<uint8_t*>(
127+
VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
128+
#else
129+
#if !defined(MAP_ANON) && defined(MAP_ANONYMOUS)
130+
#define MAP_ANON MAP_ANONYMOUS
131+
#endif
132+
void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE,
133+
MAP_PRIVATE | MAP_ANON, -1, 0);
134+
if (ptr == MAP_FAILED) {
135+
return nullptr;
136+
}
137+
return static_cast<uint8_t*>(ptr);
138+
#endif
139+
}
140+
141+
void FreeRaw(uint8_t* ptr, size_t size) {
142+
if (ptr == nullptr) {
143+
return;
144+
}
145+
#ifdef _WIN32
146+
VirtualFree(ptr, 0, MEM_RELEASE);
147+
#else
148+
munmap(ptr, size);
149+
#endif
150+
}
151+
152+
uint64_t ReadU64LE(const uint8_t* bytes) {
153+
uint64_t value = 0;
154+
for (int i = 0; i < 8; i++) {
155+
value |= static_cast<uint64_t>(bytes[i]) << (8 * i);
156+
}
157+
return value;
158+
}
159+
160+
void AppendHex(std::string* out, const uint8_t hash[32]) {
161+
static const char kHex[] = "0123456789abcdef";
162+
for (int i = 0; i < 32; i++) {
163+
out->push_back(kHex[hash[i] >> 4]);
164+
out->push_back(kHex[hash[i] & 0xf]);
165+
}
166+
}
167+
168+
void CleanupFs(uv_fs_t* req) {
169+
uv_fs_req_cleanup(req);
170+
}
171+
172+
void CloseFd(uv_file fd) {
173+
uv_fs_t req;
174+
uv_fs_close(nullptr, &req, fd, nullptr);
175+
CleanupFs(&req);
176+
}
177+
178+
bool CachePath(const uint8_t hash[32], std::string* out) {
179+
char tmp[4096];
180+
size_t len = sizeof(tmp);
181+
if (uv_os_tmpdir(tmp, &len) != 0) {
182+
return false;
183+
}
184+
std::string path(tmp);
185+
if (path.empty()) {
186+
return false;
187+
}
188+
char tail = path.back();
189+
if (tail != '/' && tail != '\\') {
190+
#ifdef _WIN32
191+
path.push_back('\\');
192+
#else
193+
path.push_back('/');
194+
#endif
195+
}
196+
path += "node-icu-";
197+
AppendHex(&path, hash);
198+
path += ".dat";
199+
*out = path;
200+
return true;
201+
}
202+
203+
// Map the cache file read-only. Clean file pages stay shared across
204+
// processes of this user and are faulted only when ICU touches them.
205+
uint8_t* MapIfSize(const std::string& path, size_t size) {
206+
uv_fs_t req;
207+
int fd = uv_fs_open(nullptr, &req, path.c_str(), UV_FS_O_RDONLY, 0, nullptr);
208+
CleanupFs(&req);
209+
if (fd < 0) {
210+
return nullptr;
211+
}
212+
int st = uv_fs_fstat(nullptr, &req, fd, nullptr);
213+
uint64_t file_size = st == 0 ? req.statbuf.st_size : 0;
214+
CleanupFs(&req);
215+
if (st != 0 || file_size != size) {
216+
CloseFd(fd);
217+
return nullptr;
218+
}
219+
#ifdef _WIN32
220+
intptr_t osf = _get_osfhandle(fd);
221+
if (osf == -1) {
222+
CloseFd(fd);
223+
return nullptr;
224+
}
225+
HANDLE mapping = CreateFileMappingW(reinterpret_cast<HANDLE>(osf),
226+
nullptr,
227+
PAGE_READONLY,
228+
0,
229+
0,
230+
nullptr);
231+
if (mapping == nullptr) {
232+
CloseFd(fd);
233+
return nullptr;
234+
}
235+
void* view = MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, size);
236+
CloseFd(fd);
237+
if (view == nullptr) {
238+
CloseHandle(mapping);
239+
return nullptr;
240+
}
241+
// ICU holds pointers into this view for the process lifetime.
242+
static HANDLE keep_mapping = nullptr;
243+
keep_mapping = mapping;
244+
if (keep_mapping == nullptr) {
245+
return nullptr;
246+
}
247+
return static_cast<uint8_t*>(view);
248+
#else
249+
void* view = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0);
250+
CloseFd(fd);
251+
if (view == MAP_FAILED) {
252+
return nullptr;
253+
}
254+
return static_cast<uint8_t*>(view);
255+
#endif
256+
}
257+
258+
bool WriteAll(uv_file fd, const uint8_t* data, size_t size) {
259+
size_t off = 0;
260+
while (off < size) {
261+
size_t remain = size - off;
262+
unsigned int chunk = remain > 0x40000000u
263+
? 0x40000000u
264+
: static_cast<unsigned int>(remain);
265+
uv_buf_t buf = uv_buf_init(
266+
const_cast<char*>(reinterpret_cast<const char*>(data + off)), chunk);
267+
uv_fs_t req;
268+
int n = uv_fs_write(nullptr, &req, fd, &buf, 1,
269+
static_cast<int64_t>(off), nullptr);
270+
CleanupFs(&req);
271+
if (n <= 0) {
272+
return false;
273+
}
274+
off += static_cast<size_t>(n);
275+
}
276+
return true;
277+
}
278+
279+
uint8_t* PublishCache(const uint8_t* data,
280+
size_t size,
281+
const uint8_t hash[32]) {
282+
std::string path;
283+
if (!CachePath(hash, &path)) {
284+
return nullptr;
285+
}
286+
uint8_t* existing = MapIfSize(path, size);
287+
if (existing != nullptr) {
288+
return existing;
289+
}
290+
std::string tmp = path + ".tmp." + std::to_string(uv_os_getpid());
291+
uv_fs_t req;
292+
int fd = uv_fs_open(nullptr, &req, tmp.c_str(),
293+
UV_FS_O_CREAT | UV_FS_O_EXCL | UV_FS_O_WRONLY, 0600,
294+
nullptr);
295+
CleanupFs(&req);
296+
if (fd < 0) {
297+
return MapIfSize(path, size);
298+
}
299+
bool wrote = WriteAll(fd, data, size);
300+
if (wrote) {
301+
int sync = uv_fs_fsync(nullptr, &req, fd, nullptr);
302+
CleanupFs(&req);
303+
wrote = sync == 0;
304+
}
305+
CloseFd(fd);
306+
if (!wrote) {
307+
uv_fs_unlink(nullptr, &req, tmp.c_str(), nullptr);
308+
CleanupFs(&req);
309+
return nullptr;
310+
}
311+
int renamed = uv_fs_rename(nullptr, &req, tmp.c_str(), path.c_str(), nullptr);
312+
CleanupFs(&req);
313+
if (renamed != 0) {
314+
uv_fs_unlink(nullptr, &req, tmp.c_str(), nullptr);
315+
CleanupFs(&req);
316+
}
317+
return MapIfSize(path, size);
318+
}
319+
320+
uint8_t* LoadEmbeddedICU(std::string* error) {
321+
const uint8_t* bytes = node_icu_zstd_dat;
322+
if (memcmp(bytes, "ICUZ", 4) != 0) {
323+
*error = "embedded ICU data header is invalid";
324+
return nullptr;
325+
}
326+
uint64_t raw_size = ReadU64LE(bytes + 4);
327+
uint64_t compressed_size = ReadU64LE(bytes + 12);
328+
const uint8_t* expect_hash = bytes + 20;
329+
if (raw_size == 0 || raw_size > kMaxIcuBytes || compressed_size == 0 ||
330+
compressed_size > raw_size) {
331+
*error = "embedded ICU data header is invalid";
332+
return nullptr;
333+
}
334+
std::string path;
335+
if (CachePath(expect_hash, &path)) {
336+
uint8_t* cached = MapIfSize(path, static_cast<size_t>(raw_size));
337+
if (cached != nullptr) {
338+
return cached;
339+
}
340+
}
341+
size_t raw = static_cast<size_t>(raw_size);
342+
size_t alloc = Align16(raw);
343+
uint8_t* data = AllocRaw(alloc);
344+
if (data == nullptr) {
345+
*error = "failed to decompress embedded ICU data";
346+
return nullptr;
347+
}
348+
size_t got = ZSTD_decompress(
349+
data, raw, bytes + kIcuHeaderSize, static_cast<size_t>(compressed_size));
350+
if (ZSTD_isError(got) || got != raw) {
351+
FreeRaw(data, alloc);
352+
*error = "failed to decompress embedded ICU data";
353+
return nullptr;
354+
}
355+
uint8_t hash[32];
356+
EmbedSha256(data, got, hash);
357+
if (memcmp(hash, expect_hash, 32) != 0) {
358+
FreeRaw(data, alloc);
359+
*error = "embedded ICU data hash mismatch";
360+
return nullptr;
361+
}
362+
uint8_t* mapped = PublishCache(data, got, hash);
363+
if (mapped != nullptr) {
364+
FreeRaw(data, alloc);
365+
return mapped;
366+
}
367+
// No writable temp directory. Keep the private buffer so ICU still works.
368+
return data;
369+
}
370+
371+
} // namespace
372+
#endif // NODE_HAVE_EMBEDDED_ICU_ZSTD
373+
88374
namespace node {
89375

90376
using v8::Context;
@@ -555,7 +841,13 @@ ConverterObject::ConverterObject(
555841
bool InitializeICUDirectory(const std::string& path, std::string* error) {
556842
UErrorCode status = U_ZERO_ERROR;
557843
if (path.empty()) {
558-
#ifdef NODE_HAVE_SMALL_ICU
844+
#ifdef NODE_HAVE_EMBEDDED_ICU_ZSTD
845+
static uint8_t* icu_data = LoadEmbeddedICU(error);
846+
if (icu_data == nullptr) {
847+
return false;
848+
}
849+
udata_setCommonData(icu_data, &status);
850+
#elif defined(NODE_HAVE_SMALL_ICU)
559851
// install the 'small' data.
560852
udata_setCommonData(&SMALL_ICUDATA_ENTRY_POINT, &status);
561853
#else // !NODE_HAVE_SMALL_ICU

0 commit comments

Comments
 (0)