Compare commits
5 commits
main
...
lemire/com
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f3e25689a | ||
|
|
a60c80d06c | ||
|
|
a6273b1f4a | ||
|
|
f427fb358c |
||
|
|
fb86fb925a |
11 changed files with 760 additions and 68 deletions
72
benchmark/misc/compile-cache-timing.js
Normal file
72
benchmark/misc/compile-cache-timing.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
'use strict';
|
||||
|
||||
// Startup benchmark for the compile cache (including the zstd dictionary).
|
||||
// Compares no-cache / cold-cache / warm-cache for two workloads:
|
||||
// big - one large module (the typescript.js fixture)
|
||||
// many - many small modules (generated here, side-effect-free)
|
||||
// The modules are generated into a temp dir so the benchmark is self-contained
|
||||
// and reproducible, and never executes unrelated code.
|
||||
|
||||
const common = require('../common.js');
|
||||
const { spawnSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const bench = common.createBenchmark(main, {
|
||||
workload: ['big', 'many'],
|
||||
cache: ['none', 'cold', 'warm'],
|
||||
n: [30],
|
||||
});
|
||||
|
||||
const BIG = path.resolve(__dirname, '../../test/fixtures/snapshot/typescript.js');
|
||||
|
||||
// Generate `count` small, side-effect-free modules and return the require()
|
||||
// code that loads them all in one child.
|
||||
function makeManyModules(dir, count) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const reqs = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const file = path.join(dir, `mod-${i}.js`);
|
||||
fs.writeFileSync(
|
||||
file,
|
||||
`'use strict';\n` +
|
||||
`module.exports = function value${i}(a, b) {\n` +
|
||||
` const sum = a + b + ${i};\n` +
|
||||
` return { id: ${i}, sum, label: 'module-${i}' };\n` +
|
||||
`};\n`);
|
||||
reqs.push(`require(${JSON.stringify(file)});`);
|
||||
}
|
||||
return reqs.join('');
|
||||
}
|
||||
|
||||
function run(cmd, args, cacheDir) {
|
||||
const env = { ...process.env };
|
||||
if (cacheDir) env.NODE_COMPILE_CACHE = cacheDir;
|
||||
else delete env.NODE_COMPILE_CACHE;
|
||||
const child = spawnSync(cmd, args, { env, stdio: 'ignore' });
|
||||
if (child.error) throw child.error;
|
||||
}
|
||||
|
||||
function main({ n, workload, cache }) {
|
||||
const cmd = process.execPath || process.argv[0];
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-bench-'));
|
||||
const args = workload === 'big' ?
|
||||
[BIG] :
|
||||
['-e', makeManyModules(path.join(tmp, 'mods'), 120)];
|
||||
const cacheDir = cache === 'none' ? null : path.join(tmp, 'cache');
|
||||
|
||||
try {
|
||||
if (cache === 'warm') run(cmd, args, cacheDir); // populate once
|
||||
bench.start();
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (cache === 'cold' && cacheDir) {
|
||||
fs.rmSync(cacheDir, { recursive: true, force: true });
|
||||
}
|
||||
run(cmd, args, cacheDir);
|
||||
}
|
||||
bench.end(n);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
17
node.gyp
17
node.gyp
|
|
@ -1110,6 +1110,22 @@
|
|||
'<@(linked_module_files)',
|
||||
],
|
||||
},
|
||||
{
|
||||
'action_name': 'generate_compile_cache_zstd_dict',
|
||||
'inputs': [
|
||||
'src/compile_cache_zstd.dict',
|
||||
'tools/generate_compile_cache_dict.py',
|
||||
],
|
||||
'outputs': [
|
||||
'<(SHARED_INTERMEDIATE_DIR)/compile_cache_zstd_dict.h',
|
||||
],
|
||||
'action': [
|
||||
'<(python)',
|
||||
'tools/generate_compile_cache_dict.py',
|
||||
'src/compile_cache_zstd.dict',
|
||||
'<@(_outputs)',
|
||||
],
|
||||
},
|
||||
],
|
||||
}, # node_base
|
||||
{
|
||||
|
|
@ -1123,6 +1139,7 @@
|
|||
'src',
|
||||
'deps/v8/include',
|
||||
'deps/uv/include',
|
||||
'<(SHARED_INTERMEDIATE_DIR)', # for compile_cache_zstd_dict.h etc.
|
||||
],
|
||||
|
||||
'dependencies': [
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
#include "compile_cache.h"
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include "debug_utils-inl.h"
|
||||
#include "env-inl.h"
|
||||
|
|
@ -8,6 +10,11 @@
|
|||
#include "path.h"
|
||||
#include "util.h"
|
||||
#include "zlib.h"
|
||||
#include "zstd.h"
|
||||
// kCompileCacheZstdDict + kCompileCacheZstdDictSize come from the header
|
||||
// generated at build time by the GYP action (from src/compile_cache_zstd.dict).
|
||||
// The include directory (SHARED_INTERMEDIATE_DIR) is added by node.gyp.
|
||||
#include "compile_cache_zstd_dict.h"
|
||||
|
||||
#ifdef NODE_IMPLEMENTS_POSIX_CREDENTIALS
|
||||
#include <unistd.h> // getuid
|
||||
|
|
@ -25,6 +32,29 @@ using v8::ScriptCompiler;
|
|||
using v8::String;
|
||||
|
||||
namespace {
|
||||
// The compile-cache zstd dictionary is immutable and embedded in the binary,
|
||||
// so the prepared CDict/DDict are created once and shared across all handlers
|
||||
// (and all Environments/Workers) instead of per handler. They live for the
|
||||
// lifetime of the process. Returns nullptr if preparation fails, in which
|
||||
// case callers fall back to plain (dictionary-less) zstd.
|
||||
ZSTD_CDict* GetCompileCacheCDict() {
|
||||
static ZSTD_CDict* cdict =
|
||||
ZSTD_createCDict(kCompileCacheZstdDict, kCompileCacheZstdDictSize, 1);
|
||||
return cdict;
|
||||
}
|
||||
|
||||
ZSTD_DDict* GetCompileCacheDDict() {
|
||||
static ZSTD_DDict* ddict =
|
||||
ZSTD_createDDict(kCompileCacheZstdDict, kCompileCacheZstdDictSize);
|
||||
return ddict;
|
||||
}
|
||||
|
||||
// The dictionary only helps small/medium caches; for larger inputs zstd's own
|
||||
// adaptive model dominates and the dictionary never wins, so we skip the
|
||||
// (otherwise wasted) second compression above this raw size. Decompression is
|
||||
// unaffected: a single DDict decodes both dict-assisted and plain frames.
|
||||
constexpr uint32_t kCompileCacheDictMaxRawSize = 256 * 1024;
|
||||
|
||||
std::string Uint32ToHex(uint32_t crc) {
|
||||
std::string str;
|
||||
str.reserve(8);
|
||||
|
|
@ -75,18 +105,21 @@ inline void CompileCacheHandler::Debug(const char* format,
|
|||
}
|
||||
}
|
||||
|
||||
ScriptCompiler::CachedData* CompileCacheEntry::CopyCache() const {
|
||||
ScriptCompiler::CachedData* CompileCacheEntry::WrapCache() const {
|
||||
DCHECK_NOT_NULL(cache);
|
||||
int cache_size = cache->length;
|
||||
uint8_t* data = new uint8_t[cache_size];
|
||||
memcpy(data, cache->data, cache_size);
|
||||
// The returned CachedData does not own the buffer - it's a view into
|
||||
// the buffer owned by this entry, which outlives the synchronous
|
||||
// consumption of the cache during compilation, so no copy is necessary.
|
||||
return new ScriptCompiler::CachedData(
|
||||
data, cache_size, ScriptCompiler::CachedData::BufferOwned);
|
||||
cache->data, cache->length, ScriptCompiler::CachedData::BufferNotOwned);
|
||||
}
|
||||
|
||||
// Used for identifying and verifying a file is a compile cache file.
|
||||
// See comments in CompileCacheHandler::Persist().
|
||||
constexpr uint32_t kCacheMagicNumber = 0x8adfdbb2;
|
||||
// The last byte is bumped whenever the format of the cache file changes
|
||||
// so that files in an older format are discarded as cache misses and
|
||||
// then overwritten with the new format.
|
||||
constexpr uint32_t kCacheMagicNumber = 0x8adfdbb3;
|
||||
|
||||
const char* CompileCacheEntry::type_name() const {
|
||||
switch (type) {
|
||||
|
|
@ -124,10 +157,21 @@ void CompileCacheHandler::ReadCacheFile(CompileCacheEntry* entry) {
|
|||
uv_fs_req_cleanup(&close_req);
|
||||
});
|
||||
|
||||
// Get the file size upfront so that the cache can be read with a single
|
||||
// exactly-sized read, and truncated or trailing data can be detected
|
||||
// without additional read attempts.
|
||||
int err = uv_fs_fstat(nullptr, &req, file, nullptr);
|
||||
if (err < 0) {
|
||||
Debug("fstat failed, %s\n", uv_strerror(err));
|
||||
return;
|
||||
}
|
||||
uint64_t file_size = req.statbuf.st_size;
|
||||
uv_fs_req_cleanup(&req);
|
||||
|
||||
// Read the headers.
|
||||
std::vector<uint32_t> headers(kHeaderCount);
|
||||
uv_buf_t headers_buf = uv_buf_init(reinterpret_cast<char*>(headers.data()),
|
||||
kHeaderCount * sizeof(uint32_t));
|
||||
std::array<uint32_t, kHeaderCount> headers;
|
||||
uv_buf_t headers_buf =
|
||||
uv_buf_init(reinterpret_cast<char*>(headers.data()), kHeaderSize);
|
||||
const int r = uv_fs_read(nullptr, &req, file, &headers_buf, 1, 0, nullptr);
|
||||
if (r != static_cast<int>(headers_buf.len)) {
|
||||
Debug("reading header failed, bytes read %d", r);
|
||||
|
|
@ -137,13 +181,15 @@ void CompileCacheHandler::ReadCacheFile(CompileCacheEntry* entry) {
|
|||
Debug("\n");
|
||||
return;
|
||||
}
|
||||
uv_fs_req_cleanup(&req);
|
||||
|
||||
Debug("[%d %d %d %d %d]...",
|
||||
Debug("[%d %d %d %d %d %d]...",
|
||||
headers[kMagicNumberOffset],
|
||||
headers[kCodeSizeOffset],
|
||||
headers[kCacheSizeOffset],
|
||||
headers[kCodeHashOffset],
|
||||
headers[kCacheHashOffset]);
|
||||
headers[kCacheHashOffset],
|
||||
headers[kCacheRawSizeOffset]);
|
||||
|
||||
if (headers[kMagicNumberOffset] != kCacheMagicNumber) {
|
||||
Debug("magic number mismatch: expected %d, actual %d\n",
|
||||
|
|
@ -166,50 +212,56 @@ void CompileCacheHandler::ReadCacheFile(CompileCacheEntry* entry) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Read the cache, grow the buffer exponentially whenever it fills up.
|
||||
size_t offset = headers_buf.len;
|
||||
size_t capacity = 4096; // Initial buffer capacity
|
||||
uint32_t cache_size = headers[kCacheSizeOffset];
|
||||
uint32_t raw_size = headers[kCacheRawSizeOffset];
|
||||
|
||||
// Check the cache size. The headers were read successfully, so
|
||||
// file_size >= kHeaderSize here. The file must contain exactly the
|
||||
// headers followed by cache_size bytes of cache content.
|
||||
if (file_size - kHeaderSize != cache_size) {
|
||||
Debug("cache size mismatch: expected %d, actual %d\n",
|
||||
cache_size,
|
||||
file_size - kHeaderSize);
|
||||
return;
|
||||
}
|
||||
|
||||
// The cache content is stored uncompressed when cache_size == raw_size,
|
||||
// and zstd-compressed when cache_size < raw_size (see
|
||||
// CompileCacheHandler::Persist()). Anything else is invalid.
|
||||
if (cache_size > raw_size) {
|
||||
Debug(
|
||||
"invalid cache size %d > uncompressed size %d\n", cache_size, raw_size);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the cache content in one go with an exactly-sized buffer,
|
||||
// looping only in case of short reads.
|
||||
std::unique_ptr<uint8_t[]> disk_data(new uint8_t[cache_size]);
|
||||
size_t total_read = 0;
|
||||
uint8_t* buffer = new uint8_t[capacity];
|
||||
|
||||
while (true) {
|
||||
// If there is not enough space to read more data, do a simple
|
||||
// realloc here (we don't actually realloc because V8 requires
|
||||
// the underlying buffer to be delete[]-able).
|
||||
if (total_read == capacity) {
|
||||
size_t new_capacity = capacity * 2;
|
||||
auto* new_buffer = new uint8_t[new_capacity];
|
||||
memcpy(new_buffer, buffer, capacity);
|
||||
delete[] buffer;
|
||||
buffer = new_buffer;
|
||||
capacity = new_capacity;
|
||||
}
|
||||
|
||||
uv_buf_t iov = uv_buf_init(reinterpret_cast<char*>(buffer + total_read),
|
||||
capacity - total_read);
|
||||
int bytes_read =
|
||||
uv_fs_read(nullptr, &req, file, &iov, 1, offset + total_read, nullptr);
|
||||
while (total_read < cache_size) {
|
||||
uv_buf_t iov =
|
||||
uv_buf_init(reinterpret_cast<char*>(disk_data.get() + total_read),
|
||||
cache_size - total_read);
|
||||
int bytes_read = uv_fs_read(
|
||||
nullptr, &req, file, &iov, 1, kHeaderSize + total_read, nullptr);
|
||||
if (req.result < 0) { // Error.
|
||||
// req will be cleaned up by scope leave.
|
||||
delete[] buffer;
|
||||
Debug(" %s\n", uv_strerror(req.result));
|
||||
return;
|
||||
}
|
||||
uv_fs_req_cleanup(&req);
|
||||
if (bytes_read <= 0) {
|
||||
break;
|
||||
if (bytes_read == 0) { // Unexpected EOF - the file shrank under us.
|
||||
Debug("cache size mismatch: expected %d, actual %d\n",
|
||||
cache_size,
|
||||
total_read);
|
||||
return;
|
||||
}
|
||||
total_read += bytes_read;
|
||||
}
|
||||
|
||||
// Check the cache size and hash.
|
||||
if (headers[kCacheSizeOffset] != total_read) {
|
||||
Debug("cache size mismatch: expected %d, actual %d\n",
|
||||
headers[kCacheSizeOffset],
|
||||
total_read);
|
||||
return;
|
||||
}
|
||||
uint32_t cache_hash = GetHash(reinterpret_cast<char*>(buffer), total_read);
|
||||
// Check the cache hash of the on-disk content before decompressing.
|
||||
uint32_t cache_hash =
|
||||
GetHash(reinterpret_cast<char*>(disk_data.get()), cache_size);
|
||||
if (headers[kCacheHashOffset] != cache_hash) {
|
||||
Debug("cache hash mismatch: expected %d, actual %d\n",
|
||||
headers[kCacheHashOffset],
|
||||
|
|
@ -217,9 +269,58 @@ void CompileCacheHandler::ReadCacheFile(CompileCacheEntry* entry) {
|
|||
return;
|
||||
}
|
||||
|
||||
entry->cache.reset(new ScriptCompiler::CachedData(
|
||||
buffer, total_read, ScriptCompiler::CachedData::BufferOwned));
|
||||
Debug(" success, size=%d\n", total_read);
|
||||
if (cache_size == raw_size) {
|
||||
// Stored uncompressed - hand the buffer to V8 directly.
|
||||
entry->cache.reset(new ScriptCompiler::CachedData(
|
||||
disk_data.release(),
|
||||
raw_size,
|
||||
ScriptCompiler::CachedData::BufferOwned));
|
||||
} else {
|
||||
// Cross-check the content size embedded in the zstd frame before
|
||||
// allocating, in case the headers are corrupted.
|
||||
unsigned long long content_size = // NOLINT(runtime/int)
|
||||
ZSTD_getFrameContentSize(disk_data.get(), cache_size);
|
||||
if (content_size != raw_size) {
|
||||
Debug("uncompressed size mismatch: expected %d, actual %d\n",
|
||||
raw_size,
|
||||
content_size);
|
||||
return;
|
||||
}
|
||||
// Lazily create the decompression context on first use and reuse it
|
||||
// for subsequent reads - recreating its workspace for every file
|
||||
// costs more than the decompression itself for small caches.
|
||||
if (zstd_dctx_ == nullptr && (zstd_dctx_ = ZSTD_createDCtx()) == nullptr) {
|
||||
Debug("failed to create zstd context\n");
|
||||
return;
|
||||
}
|
||||
// Decompress directly into the buffer handed to V8. The embedded
|
||||
// dictionary is referenced via a shared, prepared DDict; plain frames
|
||||
// (which carry no dictID) decompress correctly with it as well.
|
||||
std::unique_ptr<uint8_t[]> raw_data(new uint8_t[raw_size]);
|
||||
ZSTD_DDict* ddict = GetCompileCacheDDict();
|
||||
size_t decompressed_size;
|
||||
if (ddict != nullptr) {
|
||||
decompressed_size = ZSTD_decompress_usingDDict(
|
||||
zstd_dctx_, raw_data.get(), raw_size, disk_data.get(), cache_size,
|
||||
ddict);
|
||||
} else {
|
||||
decompressed_size = ZSTD_decompressDCtx(
|
||||
zstd_dctx_, raw_data.get(), raw_size, disk_data.get(), cache_size);
|
||||
}
|
||||
if (ZSTD_isError(decompressed_size)) {
|
||||
Debug("decompression failed: %s\n", ZSTD_getErrorName(decompressed_size));
|
||||
return;
|
||||
}
|
||||
if (decompressed_size != raw_size) {
|
||||
Debug("decompressed size mismatch: expected %d, actual %d\n",
|
||||
raw_size,
|
||||
decompressed_size);
|
||||
return;
|
||||
}
|
||||
entry->cache.reset(new ScriptCompiler::CachedData(
|
||||
raw_data.release(), raw_size, ScriptCompiler::CachedData::BufferOwned));
|
||||
}
|
||||
Debug(" success, size=%d\n", raw_size);
|
||||
}
|
||||
|
||||
static std::string GetRelativePath(std::string_view path,
|
||||
|
|
@ -280,11 +381,18 @@ CompileCacheEntry* CompileCacheHandler::GetOrInsert(Local<String> code,
|
|||
return loaded->second.get();
|
||||
}
|
||||
|
||||
// If the code hash mismatches, the code has changed, discard the stale entry
|
||||
// and create a new one.
|
||||
auto emplaced =
|
||||
compiler_cache_store_.emplace(key, std::make_unique<CompileCacheEntry>());
|
||||
auto* result = emplaced.first->second.get();
|
||||
// If the code hash mismatches, the code has changed, reset the stale
|
||||
// entry in place. Otherwise insert a new one.
|
||||
CompileCacheEntry* result;
|
||||
if (loaded != compiler_cache_store_.end()) {
|
||||
result = loaded->second.get();
|
||||
result->refreshed = false;
|
||||
result->persisted = false;
|
||||
} else {
|
||||
result = compiler_cache_store_
|
||||
.emplace(key, std::make_unique<CompileCacheEntry>())
|
||||
.first->second.get();
|
||||
}
|
||||
|
||||
result->code_hash = code_hash;
|
||||
result->code_size = code_utf8.length();
|
||||
|
|
@ -391,6 +499,16 @@ void CompileCacheHandler::Persist() {
|
|||
// finished. In that case, the off-thread writes should finish long
|
||||
// before any attempt of flushing is made so the method would then only
|
||||
// incur a negligible overhead from thread synchronization.
|
||||
|
||||
// The compression context is created lazily when there is anything to
|
||||
// compress and reused for all the entries in this invocation.
|
||||
ZSTD_CCtx* cctx = nullptr;
|
||||
auto cleanup_cctx = OnScopeLeave([&cctx]() {
|
||||
if (cctx != nullptr) {
|
||||
ZSTD_freeCCtx(cctx);
|
||||
}
|
||||
});
|
||||
|
||||
for (auto& pair : compiler_cache_store_) {
|
||||
auto* entry = pair.second.get();
|
||||
const char* type_name = entry->type_name();
|
||||
|
|
@ -418,18 +536,71 @@ void CompileCacheHandler::Persist() {
|
|||
|
||||
DCHECK_EQ(entry->cache->buffer_policy,
|
||||
ScriptCompiler::CachedData::BufferOwned);
|
||||
char* cache_ptr =
|
||||
char* raw_ptr =
|
||||
reinterpret_cast<char*>(const_cast<uint8_t*>(entry->cache->data));
|
||||
uint32_t cache_size = static_cast<uint32_t>(entry->cache->length);
|
||||
uint32_t raw_size = static_cast<uint32_t>(entry->cache->length);
|
||||
|
||||
// Compress the cache with zstd to reduce the size on disk. Compression
|
||||
// level 1 prioritizes speed - persistence usually happens on process
|
||||
// shutdown and should add as little overhead as possible. If the data
|
||||
// is not compressible, store it uncompressed, which is indicated by
|
||||
// the cache size being equal to the uncompressed size in the headers.
|
||||
//
|
||||
// We also try the embedded trained dictionary and keep whichever frame is
|
||||
// smaller (still subject to the "only store if < raw" policy). The
|
||||
// dictionary mainly helps the small/medium caches that dominate real
|
||||
// compile cache usage; for inputs where plain zstd already wins we keep
|
||||
// the plain frame.
|
||||
char* cache_ptr = raw_ptr;
|
||||
uint32_t cache_size = raw_size;
|
||||
std::unique_ptr<uint8_t[]> compressed;
|
||||
std::unique_ptr<uint8_t[]> compressed_dict;
|
||||
if (cctx != nullptr || (cctx = ZSTD_createCCtx()) != nullptr) {
|
||||
size_t compressed_bound = ZSTD_compressBound(raw_size);
|
||||
compressed.reset(new uint8_t[compressed_bound]);
|
||||
size_t compressed_size = ZSTD_compressCCtx(
|
||||
cctx, compressed.get(), compressed_bound, raw_ptr, raw_size, 1);
|
||||
char* best_ptr = reinterpret_cast<char*>(compressed.get());
|
||||
// Only attempt the dictionary for small/medium entries (see
|
||||
// kCompileCacheDictMaxRawSize); for large blobs it never wins and the
|
||||
// extra compression would be wasted work.
|
||||
ZSTD_CDict* cdict = raw_size <= kCompileCacheDictMaxRawSize
|
||||
? GetCompileCacheCDict()
|
||||
: nullptr;
|
||||
if (cdict != nullptr) {
|
||||
// Compress into a separate buffer so the selected frame's bytes and
|
||||
// size always stay in sync (the plain buffer is left untouched).
|
||||
compressed_dict.reset(new uint8_t[compressed_bound]);
|
||||
size_t dict_size = ZSTD_compress_usingCDict(
|
||||
cctx, compressed_dict.get(), compressed_bound, raw_ptr, raw_size,
|
||||
cdict);
|
||||
if (!ZSTD_isError(dict_size) &&
|
||||
(ZSTD_isError(compressed_size) || dict_size < compressed_size)) {
|
||||
compressed_size = dict_size;
|
||||
best_ptr = reinterpret_cast<char*>(compressed_dict.get());
|
||||
}
|
||||
}
|
||||
if (!ZSTD_isError(compressed_size) && compressed_size < raw_size) {
|
||||
cache_ptr = best_ptr;
|
||||
cache_size = static_cast<uint32_t>(compressed_size);
|
||||
}
|
||||
}
|
||||
Debug("[compile cache] compressed cache for %s %s: %d -> %d bytes\n",
|
||||
type_name,
|
||||
entry->source_filename,
|
||||
raw_size,
|
||||
cache_size);
|
||||
|
||||
uint32_t cache_hash = GetHash(cache_ptr, cache_size);
|
||||
|
||||
// Generating headers.
|
||||
std::vector<uint32_t> headers(kHeaderCount);
|
||||
std::array<uint32_t, kHeaderCount> headers;
|
||||
headers[kMagicNumberOffset] = kCacheMagicNumber;
|
||||
headers[kCodeSizeOffset] = entry->code_size;
|
||||
headers[kCacheSizeOffset] = cache_size;
|
||||
headers[kCodeHashOffset] = entry->code_hash;
|
||||
headers[kCacheHashOffset] = cache_hash;
|
||||
headers[kCacheRawSizeOffset] = raw_size;
|
||||
|
||||
// Generate the temporary filename.
|
||||
// The temporary file should be placed in a location like:
|
||||
|
|
@ -459,7 +630,7 @@ void CompileCacheHandler::Persist() {
|
|||
Debug(" -> %s\n", mkstemp_req.path);
|
||||
Debug("[compile cache] writing cache for %s %s to temporary file %s [%d "
|
||||
"%d %d "
|
||||
"%d %d]...",
|
||||
"%d %d %d]...",
|
||||
type_name,
|
||||
entry->source_filename,
|
||||
mkstemp_req.path,
|
||||
|
|
@ -467,12 +638,13 @@ void CompileCacheHandler::Persist() {
|
|||
headers[kCodeSizeOffset],
|
||||
headers[kCacheSizeOffset],
|
||||
headers[kCodeHashOffset],
|
||||
headers[kCacheHashOffset]);
|
||||
headers[kCacheHashOffset],
|
||||
headers[kCacheRawSizeOffset]);
|
||||
|
||||
// Write to the temporary file.
|
||||
uv_buf_t headers_buf = uv_buf_init(reinterpret_cast<char*>(headers.data()),
|
||||
headers.size() * sizeof(uint32_t));
|
||||
uv_buf_t data_buf = uv_buf_init(cache_ptr, entry->cache->length);
|
||||
uv_buf_t headers_buf =
|
||||
uv_buf_init(reinterpret_cast<char*>(headers.data()), kHeaderSize);
|
||||
uv_buf_t data_buf = uv_buf_init(cache_ptr, cache_size);
|
||||
uv_buf_t bufs[] = {headers_buf, data_buf};
|
||||
|
||||
uv_fs_t write_req;
|
||||
|
|
@ -529,6 +701,12 @@ CompileCacheHandler::CompileCacheHandler(Environment* env)
|
|||
is_debug_(
|
||||
env->enabled_debug_list()->enabled(DebugCategory::COMPILE_CACHE)) {}
|
||||
|
||||
CompileCacheHandler::~CompileCacheHandler() {
|
||||
if (zstd_dctx_ != nullptr) {
|
||||
ZSTD_freeDCtx(zstd_dctx_);
|
||||
}
|
||||
}
|
||||
|
||||
// Directory structure:
|
||||
// - Compile cache directory (from NODE_COMPILE_CACHE)
|
||||
// - $NODE_VERSION-$ARCH-$CACHE_DATA_VERSION_TAG-$UID
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
#include <unordered_map>
|
||||
#include "v8.h"
|
||||
|
||||
struct ZSTD_DCtx_s;
|
||||
|
||||
namespace node {
|
||||
class Environment;
|
||||
|
||||
|
|
@ -36,9 +38,11 @@ struct CompileCacheEntry {
|
|||
bool refreshed = false;
|
||||
bool persisted = false;
|
||||
|
||||
// Copy the cache into a new store for V8 to consume. Caller takes
|
||||
// ownership.
|
||||
v8::ScriptCompiler::CachedData* CopyCache() const;
|
||||
// Wrap the cache into a non-owning CachedData for V8 to consume.
|
||||
// The caller takes ownership of the returned wrapper object, while
|
||||
// the underlying buffer remains owned by this entry and must outlive
|
||||
// the consumption of the wrapper.
|
||||
v8::ScriptCompiler::CachedData* WrapCache() const;
|
||||
const char* type_name() const;
|
||||
};
|
||||
|
||||
|
|
@ -65,6 +69,7 @@ enum class EnableOption : uint8_t { DEFAULT, PORTABLE };
|
|||
class CompileCacheHandler {
|
||||
public:
|
||||
explicit CompileCacheHandler(Environment* env);
|
||||
~CompileCacheHandler();
|
||||
CompileCacheEnableResult Enable(Environment* env,
|
||||
const std::string& dir,
|
||||
EnableOption option = EnableOption::DEFAULT);
|
||||
|
|
@ -99,7 +104,9 @@ class CompileCacheHandler {
|
|||
static constexpr size_t kCacheSizeOffset = 2;
|
||||
static constexpr size_t kCodeHashOffset = 3;
|
||||
static constexpr size_t kCacheHashOffset = 4;
|
||||
static constexpr size_t kHeaderCount = 5;
|
||||
static constexpr size_t kCacheRawSizeOffset = 5;
|
||||
static constexpr size_t kHeaderCount = 6;
|
||||
static constexpr size_t kHeaderSize = kHeaderCount * sizeof(uint32_t);
|
||||
|
||||
v8::Isolate* isolate_ = nullptr;
|
||||
bool is_debug_ = false;
|
||||
|
|
@ -109,6 +116,9 @@ class CompileCacheHandler {
|
|||
EnableOption portable_ = EnableOption::DEFAULT;
|
||||
std::unordered_map<uint32_t, std::unique_ptr<CompileCacheEntry>>
|
||||
compiler_cache_store_;
|
||||
// Lazily created zstd decompression context, reused across cache reads
|
||||
// to avoid recreating its workspace for every file.
|
||||
ZSTD_DCtx_s* zstd_dctx_ = nullptr;
|
||||
};
|
||||
} // namespace node
|
||||
|
||||
|
|
|
|||
BIN
src/compile_cache_zstd.dict
Normal file
BIN
src/compile_cache_zstd.dict
Normal file
Binary file not shown.
|
|
@ -521,8 +521,9 @@ MaybeLocal<Module> ModuleWrap::CompileSourceTextModule(
|
|||
}
|
||||
|
||||
if (cache_entry != nullptr && cache_entry->cache != nullptr) {
|
||||
// source will take ownership of cached_data.
|
||||
cached_data = cache_entry->CopyCache();
|
||||
// source only takes ownership of the cached_data wrapper - the
|
||||
// underlying buffer is still owned by the compile cache entry.
|
||||
cached_data = cache_entry->WrapCache();
|
||||
}
|
||||
|
||||
ScriptCompiler::Source source(source_text, origin, cached_data);
|
||||
|
|
|
|||
|
|
@ -1675,8 +1675,9 @@ static MaybeLocal<Function> CompileFunctionForCJSLoader(
|
|||
code, filename, CachedCodeType::kCommonJS);
|
||||
}
|
||||
if (cache_entry != nullptr && cache_entry->cache != nullptr) {
|
||||
// source will take ownership of cached_data.
|
||||
cached_data = cache_entry->CopyCache();
|
||||
// source only takes ownership of the cached_data wrapper - the
|
||||
// underlying buffer is still owned by the compile cache entry.
|
||||
cached_data = cache_entry->WrapCache();
|
||||
}
|
||||
|
||||
ScriptCompiler::Source source(code, origin, cached_data);
|
||||
|
|
|
|||
128
test/parallel/test-compile-cache-corrupted.js
Normal file
128
test/parallel/test-compile-cache-corrupted.js
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
'use strict';
|
||||
|
||||
// This tests that NODE_COMPILE_CACHE gracefully discards corrupted
|
||||
// cache files and regenerates them.
|
||||
|
||||
require('../common');
|
||||
const { spawnSyncAndAssert } = require('../common/child_process');
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
|
||||
// Offsets into the cache file headers (see src/compile_cache.h).
|
||||
const kHeaderSize = 6 * 4;
|
||||
const kCacheRawSizeOffset = 5 * 4;
|
||||
|
||||
function readU32(buf, offset) {
|
||||
return os.endianness() === 'LE' ?
|
||||
buf.readUInt32LE(offset) : buf.readUInt32BE(offset);
|
||||
}
|
||||
|
||||
function writeU32(buf, value, offset) {
|
||||
if (os.endianness() === 'LE') {
|
||||
buf.writeUInt32LE(value, offset);
|
||||
} else {
|
||||
buf.writeUInt32BE(value, offset);
|
||||
}
|
||||
}
|
||||
|
||||
tmpdir.refresh();
|
||||
const dir = tmpdir.resolve('.compile_cache_dir');
|
||||
const script = tmpdir.resolve('script.js');
|
||||
fs.writeFileSync(script, 'const foo = 1;', 'utf-8');
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: dir,
|
||||
};
|
||||
|
||||
function getCacheFile() {
|
||||
const subdirs = fs.readdirSync(dir);
|
||||
assert.strictEqual(subdirs.length, 1);
|
||||
const entries = fs.readdirSync(path.join(dir, subdirs[0]));
|
||||
assert.strictEqual(entries.length, 1);
|
||||
return path.join(dir, subdirs[0], entries[0]);
|
||||
}
|
||||
|
||||
// Runs the script and expects the corrupted cache to be discarded
|
||||
// with the given debug message and then regenerated.
|
||||
function expectRecovery(mismatchRE) {
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[script],
|
||||
{ env, cwd: tmpdir.path },
|
||||
{
|
||||
stderr(output) {
|
||||
console.log(output); // Logging for debugging.
|
||||
assert.match(output, mismatchRE);
|
||||
assert.match(output, /writing cache for .*script\.js.*success/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Warm the cache.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[script],
|
||||
{ env, cwd: tmpdir.path },
|
||||
{
|
||||
stderr(output) {
|
||||
console.log(output); // Logging for debugging.
|
||||
assert.match(output, /writing cache for .*script\.js.*success/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
const cacheFile = getCacheFile();
|
||||
assert(fs.readFileSync(cacheFile).length > kHeaderSize);
|
||||
|
||||
{
|
||||
// Corrupt the magic number.
|
||||
const data = fs.readFileSync(cacheFile);
|
||||
for (let i = 0; i < 4; i++) data[i] ^= 0xff;
|
||||
fs.writeFileSync(cacheFile, data);
|
||||
expectRecovery(
|
||||
/reading cache from .* for CommonJS .*script\.js.*magic number mismatch/);
|
||||
}
|
||||
|
||||
{
|
||||
// Truncate the cache content.
|
||||
const data = fs.readFileSync(cacheFile);
|
||||
fs.writeFileSync(cacheFile, data.subarray(0, data.length - 3));
|
||||
expectRecovery(
|
||||
/reading cache from .* for CommonJS .*script\.js.*cache size mismatch/);
|
||||
}
|
||||
|
||||
{
|
||||
// Flip a byte in the middle of the cache content.
|
||||
const data = fs.readFileSync(cacheFile);
|
||||
data[kHeaderSize + Math.floor((data.length - kHeaderSize) / 2)] ^= 0xff;
|
||||
fs.writeFileSync(cacheFile, data);
|
||||
expectRecovery(
|
||||
/reading cache from .* for CommonJS .*script\.js.*cache hash mismatch/);
|
||||
}
|
||||
|
||||
{
|
||||
// Corrupt the uncompressed size field in the headers.
|
||||
const data = fs.readFileSync(cacheFile);
|
||||
writeU32(data, readU32(data, kCacheRawSizeOffset) + 1, kCacheRawSizeOffset);
|
||||
fs.writeFileSync(cacheFile, data);
|
||||
expectRecovery(
|
||||
/reading cache from .* for CommonJS .*script\.js.*uncompressed size mismatch/);
|
||||
}
|
||||
|
||||
// After the last recovery the cache should be consumed just fine.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[script],
|
||||
{ env, cwd: tmpdir.path },
|
||||
{
|
||||
stderr(output) {
|
||||
console.log(output); // Logging for debugging.
|
||||
assert.match(output, /cache for .*script\.js was accepted/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
|
@ -64,3 +64,83 @@ const path = require('path');
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Exercise the dictionary-compressed path (added on top of #63861) for many
|
||||
// small modules, which is where the embedded dictionary helps most. We write
|
||||
// the cache, then read it back and assert every entry is accepted - this
|
||||
// proves each dict-compressed frame decompresses to exactly the bytes that
|
||||
// were persisted.
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const dir = tmpdir.resolve('.compile_cache_dir');
|
||||
|
||||
// Generate a handful of small modules so the dictionary path is exercised.
|
||||
const count = 8;
|
||||
const modules = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const file = tmpdir.resolve(`mod-${i}.js`);
|
||||
fs.writeFileSync(
|
||||
file,
|
||||
`'use strict';\n` +
|
||||
`module.exports = function value${i}(a, b) {\n` +
|
||||
` const sum = a + b + ${i};\n` +
|
||||
` return { id: ${i}, sum, label: 'module-${i}' };\n` +
|
||||
`};\n`);
|
||||
modules.push(file);
|
||||
}
|
||||
const reqCode = modules.map((m) => `require(${JSON.stringify(m)});`).join('');
|
||||
|
||||
// First run writes the cache for every module.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
['-e', reqCode],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: dir
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
stderr(output) {
|
||||
for (const m of modules) {
|
||||
const name = path.basename(m).replace(/[.]/g, '\\.');
|
||||
assert.match(output, new RegExp(`writing cache for .*${name}.*success`));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
const cacheDirs = fs.readdirSync(dir);
|
||||
assert.strictEqual(cacheDirs.length, 1);
|
||||
// At least one entry per module (the `-e` runner is cached too).
|
||||
const entries = fs.readdirSync(path.join(dir, cacheDirs[0]));
|
||||
assert(entries.length >= count, `expected >= ${count} entries, got ${entries.length}`);
|
||||
|
||||
// Second run reads every cached entry back; "was accepted" only happens when
|
||||
// the decompressed bytes match the freshly produced in-memory cache, so this
|
||||
// is a full roundtrip check of the dictionary-compressed entries.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
['-e', reqCode],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: dir
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
stderr(output) {
|
||||
for (const m of modules) {
|
||||
const name = path.basename(m).replace(/[.]/g, '\\.');
|
||||
assert.match(
|
||||
output,
|
||||
new RegExp(`cache for .*${name} was accepted, keeping the in-memory entry`));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
37
tools/generate_compile_cache_dict.py
Normal file
37
tools/generate_compile_cache_dict.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env python
|
||||
"""Generate compile_cache_zstd_dict.h from a trained zstd .dict file.
|
||||
|
||||
Invoked by the GYP action in node.gyp at build time. Only the small binary
|
||||
.dict (src/compile_cache_zstd.dict) is checked into the repository; the C
|
||||
array it produces is generated into SHARED_INTERMEDIATE_DIR.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main(dict_path, out_path):
|
||||
with open(dict_path, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
lines = [
|
||||
'// Generated by tools/generate_compile_cache_dict.py',
|
||||
'// from %s' % os.path.basename(dict_path),
|
||||
'// The .dict file is the source of truth; do not edit by hand.',
|
||||
'',
|
||||
'static const unsigned char kCompileCacheZstdDict[] = {',
|
||||
]
|
||||
for i in range(0, len(data), 12):
|
||||
chunk = data[i:i + 12]
|
||||
lines.append(' %s,' % ', '.join('0x%02x' % b for b in chunk))
|
||||
lines.append('};')
|
||||
lines.append('static const size_t kCompileCacheZstdDictSize = %d;' %
|
||||
len(data))
|
||||
|
||||
with open(out_path, 'w') as f:
|
||||
f.write('\n'.join(lines) + '\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 3:
|
||||
sys.exit('Usage: %s <input.dict> <output.h>' % sys.argv[0])
|
||||
main(sys.argv[1], sys.argv[2])
|
||||
168
tools/train_compile_cache_dict.mjs
Normal file
168
tools/train_compile_cache_dict.mjs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
#!/usr/bin/env node
|
||||
// =============================================================================
|
||||
// train_compile_cache_dict.mjs
|
||||
//
|
||||
// Maintainer tool that regenerates src/compile_cache_zstd.dict, the zstd
|
||||
// dictionary embedded in the node binary and used to shrink compile-cache
|
||||
// entries on disk (see src/compile_cache.cc).
|
||||
//
|
||||
// -----------------------------------------------------------------------------
|
||||
// What it does
|
||||
// -----------------------------------------------------------------------------
|
||||
// The node compile cache stores V8 code caches (the bytecode/metadata blob V8
|
||||
// produces when it compiles a module) so later runs can skip recompilation.
|
||||
// These blobs share a lot of structure across files, so we zstd-compress them
|
||||
// with a trained dictionary to cut the cache's on-disk footprint.
|
||||
//
|
||||
// This script builds that dictionary end to end:
|
||||
//
|
||||
// 1. Walks a fixed in-tree corpus (CORPUS below) in sorted order.
|
||||
// 2. For each .js file, harvests a V8 code cache via vm.compileFunction with
|
||||
// produceCachedData — the same shape the CommonJS loader produces at
|
||||
// runtime — and writes each blob to a temp directory.
|
||||
// 3. Feeds all the harvested blobs to `zstd --train`, capping the result at
|
||||
// MAXDICT (16 KiB) bytes.
|
||||
// 4. Overwrites src/compile_cache_zstd.dict with the trained dictionary.
|
||||
//
|
||||
// The shipped src/compile_cache_zstd.dict is the source of truth; this script
|
||||
// exists to document and reproduce exactly how that file was made, so a future
|
||||
// maintainer can regenerate it (e.g. after a V8/corpus change) and review the
|
||||
// resulting diff.
|
||||
//
|
||||
// -----------------------------------------------------------------------------
|
||||
// Usage
|
||||
// -----------------------------------------------------------------------------
|
||||
// node tools/train_compile_cache_dict.mjs
|
||||
//
|
||||
// Run from anywhere (paths are resolved relative to this file). The output is
|
||||
// written in place to src/compile_cache_zstd.dict; inspect the diff afterwards
|
||||
// and commit it if it looks right. Progress is printed to stderr.
|
||||
//
|
||||
// Prerequisites:
|
||||
// * the `zstd` CLI on PATH, version REQUIRED_ZSTD (matching deps/zstd).
|
||||
// * a built node on PATH (this is the node you invoke it with).
|
||||
//
|
||||
// Note: --predictable is added automatically (see below) — you do not need to
|
||||
// pass it yourself.
|
||||
//
|
||||
// -----------------------------------------------------------------------------
|
||||
// Reproducibility
|
||||
// -----------------------------------------------------------------------------
|
||||
// The output is byte-for-byte stable only if all of the following are pinned:
|
||||
//
|
||||
// * node is run with --predictable. V8 otherwise randomizes the string hash
|
||||
// seed per process, and that seed leaks into vm.compileFunction cachedData,
|
||||
// so every harvest (and therefore every trained dictionary) would differ
|
||||
// run-to-run even on the same machine. This script re-executes itself with
|
||||
// --predictable if needed.
|
||||
// * the corpus and its order are fixed (CORPUS below, walked sorted).
|
||||
// * the node build is fixed: cachedData embeds the V8 version and build
|
||||
// flags, so a different node produces a different (still valid) dictionary.
|
||||
// * the zstd CLI matches deps/zstd (currently 1.5.7). ZDICT training defaults
|
||||
// change between zstd releases; this script refuses to run on a mismatch.
|
||||
//
|
||||
// Training on --predictable caches is fine even though the runtime consumes
|
||||
// non-predictable caches: the dictionary only supplies shared substrings, and
|
||||
// Persist() keeps min(plain, dict) per entry, so a less-than-ideal dictionary
|
||||
// can never make any entry larger.
|
||||
// =============================================================================
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { readFileSync, writeFileSync, mkdtempSync, readdirSync,
|
||||
rmSync } from 'node:fs';
|
||||
import { join, relative, dirname } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const OUT = join(ROOT, 'src', 'compile_cache_zstd.dict');
|
||||
const MAXDICT = 16384;
|
||||
const REQUIRED_ZSTD = '1.5.7'; // keep in sync with deps/zstd/lib/zstd.h
|
||||
|
||||
// Fixed corpus, relative to the repo root. Chosen to be diverse, always
|
||||
// present in a checkout, and disjoint from the held-out measurement corpora
|
||||
// (e.g. test/parallel) used to report size numbers.
|
||||
const CORPUS = ['lib', 'tools', 'deps/npm/node_modules'];
|
||||
|
||||
// V8 randomizes the hash seed per process; re-exec under --predictable so the
|
||||
// harvested caches — and the trained dictionary — are deterministic.
|
||||
if (!process.execArgv.includes('--predictable')) {
|
||||
const r = spawnSync(process.execPath,
|
||||
['--predictable', fileURLToPath(import.meta.url), ...process.argv.slice(2)],
|
||||
{ stdio: 'inherit' });
|
||||
process.exit(r.status ?? 1);
|
||||
}
|
||||
|
||||
function checkZstd() {
|
||||
const r = spawnSync('zstd', ['--version'], { encoding: 'utf8' });
|
||||
if (r.status !== 0) {
|
||||
console.error('error: zstd CLI not found on PATH'); process.exit(1);
|
||||
}
|
||||
const m = r.stdout.match(/v(\d+\.\d+\.\d+)/);
|
||||
if (!m || m[1] !== REQUIRED_ZSTD) {
|
||||
console.error(`error: zstd ${REQUIRED_ZSTD} required (matching deps/zstd), ` +
|
||||
`found ${m ? m[1] : 'unknown'}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const PARAMS = ['exports', 'require', 'module', '__filename', '__dirname'];
|
||||
|
||||
function* walk(dir) {
|
||||
let ents;
|
||||
try { ents = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
||||
for (const e of ents.sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
||||
const p = join(dir, e.name);
|
||||
if (e.isDirectory()) yield* walk(p);
|
||||
else if (e.isFile() && p.endsWith('.js')) yield p;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
checkZstd();
|
||||
const { default: vm } = await import('node:vm');
|
||||
|
||||
const files = [];
|
||||
for (const root of CORPUS) for (const f of walk(join(ROOT, root))) files.push(f);
|
||||
files.sort();
|
||||
|
||||
const samples = mkdtempSync(join(tmpdir(), 'cc-dict-'));
|
||||
const sampleFiles = [];
|
||||
let ok = 0;
|
||||
let r;
|
||||
try {
|
||||
for (const f of files) {
|
||||
let code;
|
||||
try { code = readFileSync(f, 'utf8'); } catch { continue; }
|
||||
try {
|
||||
const fn = vm.compileFunction(code, PARAMS,
|
||||
{ filename: f, produceCachedData: true });
|
||||
const cd = fn.cachedData;
|
||||
if (cd && cd.length > 0) {
|
||||
const name = relative(ROOT, f).replace(/[\\/]/g, '_') + '.cache';
|
||||
const out = join(samples, name);
|
||||
writeFileSync(out, cd);
|
||||
sampleFiles.push(out);
|
||||
ok++;
|
||||
}
|
||||
} catch { /* skip modules V8 can't compile standalone */ }
|
||||
}
|
||||
sampleFiles.sort();
|
||||
console.error(`harvested ${ok}/${files.length} code caches from ` +
|
||||
`${CORPUS.join(', ')}`);
|
||||
|
||||
// One sample path per file is spread on argv; the fixed corpus (~1.4k
|
||||
// files, a few hundred KB of paths) stays well under ARG_MAX.
|
||||
r = spawnSync('zstd',
|
||||
['--train', ...sampleFiles, `--maxdict=${MAXDICT}`, '-f', '-o', OUT],
|
||||
{ stdio: ['ignore', 'ignore', 'inherit'] });
|
||||
} finally {
|
||||
rmSync(samples, { recursive: true, force: true });
|
||||
}
|
||||
if (r.status !== 0) { console.error('error: zstd --train failed'); process.exit(1); }
|
||||
|
||||
const size = readFileSync(OUT).length;
|
||||
console.error(`wrote ${relative(ROOT, OUT)} (${size} bytes)`);
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error(e); process.exit(1); });
|
||||
Loading…
Add table
Add a link
Reference in a new issue