mirror of
https://github.com/nodejs/node.git
synced 2026-10-14 10:26:26 -04:00
module: implement NODE_COMPILE_CACHE for automatic on-disk code caching
This patch implements automatic on-disk code caching that can be enabled
via an environment variable NODE_COMPILE_CACHE.
When set, whenever Node.js compiles a CommonJS or a ECMAScript Module,
it will use on-disk [V8 code cache][] persisted in the specified
directory to speed up the compilation. This may slow down the first
load of a module graph, but subsequent loads of the same module graph
may get a significant speedup if the contents of the modules do not
change. Locally, this speeds up loading of
test/fixtures/snapshot/typescript.js from ~130ms to ~80ms.
To clean up the generated code cache, simply remove the directory.
It will be recreated the next time the same directory is used for
`NODE_COMPILE_CACHE`.
Compilation cache generated by one version of Node.js may not be used
by a different version of Node.js. Cache generated by different versions
of Node.js will be stored separately if the same directory is used
to persist the cache, so they can co-exist.
Caveat: currently when using this with V8 JavaScript code coverage, the
coverage being collected by V8 may be less precise in functions that are
deserialized from the code cache. It's recommended to turn this off when
running tests to generate precise coverage.
Implementation details:
There is one cache file per module on disk. The directory layout
is:
- Compile cache directory (from NODE_COMPILE_CACHE)
- 8b23c8fe: CRC32 hash of CachedDataVersionTag + NODE_VERESION
- 2ea3424d:
- 10860e5a: CRC32 hash of filename + module type
- 431e9adc: ...
- ...
Inside the cache file, there is a header followed by the actual
cache content:
```
[uint32_t] code size
[uint32_t] code hash
[uint32_t] cache size
[uint32_t] cache hash
... compile cache content ...
```
When reading the cache file, we'll also check if the code size
and code hash match the code that the module loader is loading
and whether the cache size and cache hash match the file content
read. If they don't match, or if V8 rejects the cache passed,
we'll ignore the mismatch cache, and regenerate the cache after
compilation succeeds and rewrite it to disk.
PR-URL: https://github.com/nodejs/node/pull/52535
Refs: https://github.com/nodejs/node/issues/47472
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io>
Reviewed-By: Mohammed Keyvanzadeh <mohammadkeyvanzade94@gmail.com>
This commit is contained in:
1 parent
9a44059055
commit
4d77fd2c46
25 files changed
+1203
-10
No files matched your search
@@ -2501,6 +2501,34 @@ Any other value will result in colorized output being disabled.
|
||||
[`NO_COLOR`][] is an alias for `NODE_DISABLE_COLORS`. The value of the
|
||||
environment variable is arbitrary.
|
||||
|
||||
### `NODE_COMPILE_CACHE=dir`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
> Stability: 1.1 - Active Development
|
||||
|
||||
When set, whenever Node.js compiles a CommonJS or a ECMAScript Module,
|
||||
it will use on-disk [V8 code cache][] persisted in the specified directory
|
||||
to speed up the compilation. This may slow down the first load of a
|
||||
module graph, but subsequent loads of the same module graph may get
|
||||
a significant speedup if the contents of the modules do not change.
|
||||
|
||||
To clean up the generated code cache, simply remove the directory.
|
||||
It will be recreated the next time the same directory is used for
|
||||
`NODE_COMPILE_CACHE`.
|
||||
|
||||
Compilation cache generated by one version of Node.js may not be used
|
||||
by a different version of Node.js. Cache generated by different versions
|
||||
of Node.js will be stored separately if the same directory is used
|
||||
to persist the cache, so they can co-exist.
|
||||
|
||||
Caveat: currently when using this with [V8 JavaScript code coverage][], the
|
||||
coverage being collected by V8 may be less precise in functions that are
|
||||
deserialized from the code cache. It's recommended to turn this off when
|
||||
running tests to generate precise coverage.
|
||||
|
||||
### `NODE_DEBUG=module[,…]`
|
||||
|
||||
<!-- YAML
|
||||
@@ -3146,6 +3174,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
|
||||
[Source Map]: https://sourcemaps.info/spec.html
|
||||
[Subresource Integrity]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
|
||||
[V8 JavaScript code coverage]: https://v8project.blogspot.com/2017/12/javascript-code-coverage.html
|
||||
[V8 code cache]: https://v8.dev/blog/code-caching-for-devs
|
||||
[Web Crypto API]: webcrypto.md
|
||||
[`"type"`]: packages.md#type
|
||||
[`--allow-child-process`]: #--allow-child-process
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
'src/base_object.cc',
|
||||
'src/cares_wrap.cc',
|
||||
'src/cleanup_queue.cc',
|
||||
'src/compile_cache.cc',
|
||||
'src/connect_wrap.cc',
|
||||
'src/connection_wrap.cc',
|
||||
'src/dataqueue/queue.cc',
|
||||
@@ -190,6 +191,7 @@
|
||||
'src/callback_queue-inl.h',
|
||||
'src/cleanup_queue.h',
|
||||
'src/cleanup_queue-inl.h',
|
||||
'src/compile_cache.h',
|
||||
'src/connect_wrap.h',
|
||||
'src/connection_wrap.h',
|
||||
'src/dataqueue/queue.h',
|
||||
|
||||
@@ -546,6 +546,7 @@ MaybeLocal<Value> LoadEnvironment(Environment* env,
|
||||
if (preload) {
|
||||
env->set_embedder_preload(std::move(preload));
|
||||
}
|
||||
env->InitializeCompileCache();
|
||||
|
||||
return StartExecution(env, cb);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
#include "compile_cache.h"
|
||||
#include "debug_utils-inl.h"
|
||||
#include "env-inl.h"
|
||||
#include "node_file.h"
|
||||
#include "node_internals.h"
|
||||
#include "node_version.h"
|
||||
#include "path.h"
|
||||
#include "zlib.h"
|
||||
|
||||
namespace node {
|
||||
std::string Uint32ToHex(uint32_t crc) {
|
||||
std::string str;
|
||||
str.reserve(8);
|
||||
|
||||
for (int i = 28; i >= 0; i -= 4) {
|
||||
char digit = (crc >> i) & 0xF;
|
||||
digit += digit < 10 ? '0' : 'a' - 10;
|
||||
str.push_back(digit);
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
// TODO(joyeecheung): use other hashes?
|
||||
uint32_t GetHash(const char* data, size_t size) {
|
||||
uLong crc = crc32(0L, Z_NULL, 0);
|
||||
return crc32(crc, reinterpret_cast<const Bytef*>(data), size);
|
||||
}
|
||||
|
||||
uint32_t GetCacheVersionTag() {
|
||||
std::string_view node_version(NODE_VERSION);
|
||||
uint32_t v8_tag = v8::ScriptCompiler::CachedDataVersionTag();
|
||||
uLong crc = crc32(0L, Z_NULL, 0);
|
||||
crc = crc32(crc, reinterpret_cast<const Bytef*>(&v8_tag), sizeof(uint32_t));
|
||||
crc = crc32(crc,
|
||||
reinterpret_cast<const Bytef*>(node_version.data()),
|
||||
node_version.size());
|
||||
return crc;
|
||||
}
|
||||
|
||||
uint32_t GetCacheKey(std::string_view filename, CachedCodeType type) {
|
||||
uLong crc = crc32(0L, Z_NULL, 0);
|
||||
crc = crc32(crc, reinterpret_cast<const Bytef*>(&type), sizeof(type));
|
||||
crc = crc32(
|
||||
crc, reinterpret_cast<const Bytef*>(filename.data()), filename.length());
|
||||
return crc;
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
inline void CompileCacheHandler::Debug(const char* format,
|
||||
Args&&... args) const {
|
||||
if (UNLIKELY(is_debug_)) {
|
||||
FPrintF(stderr, format, std::forward<Args>(args)...);
|
||||
}
|
||||
}
|
||||
|
||||
v8::ScriptCompiler::CachedData* CompileCacheEntry::CopyCache() const {
|
||||
DCHECK_NOT_NULL(cache);
|
||||
int cache_size = cache->length;
|
||||
uint8_t* data = new uint8_t[cache_size];
|
||||
memcpy(data, cache->data, cache_size);
|
||||
return new v8::ScriptCompiler::CachedData(
|
||||
data, cache_size, v8::ScriptCompiler::CachedData::BufferOwned);
|
||||
}
|
||||
|
||||
void CompileCacheHandler::ReadCacheFile(CompileCacheEntry* entry) {
|
||||
Debug("[compile cache] reading cache from %s for %s %s...",
|
||||
entry->cache_filename,
|
||||
entry->type == CachedCodeType::kCommonJS ? "CommonJS" : "ESM",
|
||||
entry->source_filename);
|
||||
|
||||
uv_fs_t req;
|
||||
auto defer_req_cleanup = OnScopeLeave([&req]() { uv_fs_req_cleanup(&req); });
|
||||
const char* path = entry->cache_filename.c_str();
|
||||
uv_file file = uv_fs_open(nullptr, &req, path, O_RDONLY, 0, nullptr);
|
||||
if (req.result < 0) {
|
||||
// req will be cleaned up by scope leave.
|
||||
Debug(" %s\n", uv_strerror(req.result));
|
||||
return;
|
||||
}
|
||||
uv_fs_req_cleanup(&req);
|
||||
|
||||
auto defer_close = OnScopeLeave([file]() {
|
||||
uv_fs_t close_req;
|
||||
CHECK_EQ(0, uv_fs_close(nullptr, &close_req, file, nullptr));
|
||||
uv_fs_req_cleanup(&close_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));
|
||||
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);
|
||||
if (req.result < 0 && is_debug_) {
|
||||
Debug(", %s", uv_strerror(req.result));
|
||||
}
|
||||
Debug("\n");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug("[%d %d %d %d]...",
|
||||
headers[kCodeSizeOffset],
|
||||
headers[kCacheSizeOffset],
|
||||
headers[kCodeHashOffset],
|
||||
headers[kCacheHashOffset]);
|
||||
|
||||
// Check the code size and hash which are already computed.
|
||||
if (headers[kCodeSizeOffset] != entry->code_size) {
|
||||
Debug("code size mismatch: expected %d, actual %d\n",
|
||||
entry->code_size,
|
||||
headers[kCodeSizeOffset]);
|
||||
return;
|
||||
}
|
||||
if (headers[kCodeHashOffset] != entry->code_hash) {
|
||||
Debug("code hash mismatch: expected %d, actual %d\n",
|
||||
entry->code_hash,
|
||||
headers[kCodeHashOffset]);
|
||||
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
|
||||
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);
|
||||
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;
|
||||
}
|
||||
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);
|
||||
if (headers[kCacheHashOffset] != cache_hash) {
|
||||
Debug("cache hash mismatch: expected %d, actual %d\n",
|
||||
headers[kCacheHashOffset],
|
||||
cache_hash);
|
||||
return;
|
||||
}
|
||||
|
||||
entry->cache.reset(new v8::ScriptCompiler::CachedData(
|
||||
buffer, total_read, v8::ScriptCompiler::CachedData::BufferOwned));
|
||||
Debug(" success, size=%d\n", total_read);
|
||||
}
|
||||
|
||||
CompileCacheEntry* CompileCacheHandler::GetOrInsert(
|
||||
v8::Local<v8::String> code,
|
||||
v8::Local<v8::String> filename,
|
||||
CachedCodeType type) {
|
||||
DCHECK(!compile_cache_dir_.empty());
|
||||
|
||||
Utf8Value filename_utf8(isolate_, filename);
|
||||
uint32_t key = GetCacheKey(filename_utf8.ToStringView(), type);
|
||||
|
||||
// TODO(joyeecheung): don't encode this again into UTF8. If we read the
|
||||
// UTF8 content on disk as raw buffer (from the JS layer, while watching out
|
||||
// for monkey patching), we can just hash it directly.
|
||||
Utf8Value code_utf8(isolate_, code);
|
||||
uint32_t code_hash = GetHash(code_utf8.out(), code_utf8.length());
|
||||
auto loaded = compiler_cache_store_.find(key);
|
||||
|
||||
// TODO(joyeecheung): let V8's in-isolate compilation cache take precedence.
|
||||
if (loaded != compiler_cache_store_.end() &&
|
||||
loaded->second->code_hash == code_hash) {
|
||||
return loaded->second.get();
|
||||
}
|
||||
|
||||
auto emplaced =
|
||||
compiler_cache_store_.emplace(key, std::make_unique<CompileCacheEntry>());
|
||||
auto* result = emplaced.first->second.get();
|
||||
|
||||
result->code_hash = code_hash;
|
||||
result->code_size = code_utf8.length();
|
||||
result->cache_key = key;
|
||||
result->cache_filename =
|
||||
compile_cache_dir_ + kPathSeparator + Uint32ToHex(result->cache_key);
|
||||
result->source_filename = filename_utf8.ToString();
|
||||
result->cache = nullptr;
|
||||
result->type = type;
|
||||
|
||||
// TODO(joyeecheung): if we fail enough times, stop trying for any future
|
||||
// files.
|
||||
ReadCacheFile(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
v8::ScriptCompiler::CachedData* SerializeCodeCache(
|
||||
v8::Local<v8::Function> func) {
|
||||
return v8::ScriptCompiler::CreateCodeCacheForFunction(func);
|
||||
}
|
||||
|
||||
v8::ScriptCompiler::CachedData* SerializeCodeCache(v8::Local<v8::Module> mod) {
|
||||
return v8::ScriptCompiler::CreateCodeCache(mod->GetUnboundModuleScript());
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void CompileCacheHandler::MaybeSaveImpl(CompileCacheEntry* entry,
|
||||
v8::Local<T> func_or_mod,
|
||||
bool rejected) {
|
||||
DCHECK_NOT_NULL(entry);
|
||||
Debug("[compile cache] cache for %s was %s, ",
|
||||
entry->source_filename,
|
||||
rejected ? "rejected"
|
||||
: (entry->cache == nullptr) ? "not initialized"
|
||||
: "accepted");
|
||||
if (entry->cache != nullptr && !rejected) { // accepted
|
||||
Debug("keeping the in-memory entry\n");
|
||||
return;
|
||||
}
|
||||
Debug("%s the in-memory entry\n",
|
||||
entry->cache == nullptr ? "initializing" : "refreshing");
|
||||
|
||||
v8::ScriptCompiler::CachedData* data = SerializeCodeCache(func_or_mod);
|
||||
DCHECK_EQ(data->buffer_policy, v8::ScriptCompiler::CachedData::BufferOwned);
|
||||
entry->refreshed = true;
|
||||
entry->cache.reset(data);
|
||||
}
|
||||
|
||||
void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry,
|
||||
v8::Local<v8::Module> mod,
|
||||
bool rejected) {
|
||||
DCHECK(mod->IsSourceTextModule());
|
||||
MaybeSaveImpl(entry, mod, rejected);
|
||||
}
|
||||
|
||||
void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry,
|
||||
v8::Local<v8::Function> func,
|
||||
bool rejected) {
|
||||
MaybeSaveImpl(entry, func, rejected);
|
||||
}
|
||||
|
||||
// Layout of a cache file:
|
||||
// [uint32_t] code size
|
||||
// [uint32_t] code hash
|
||||
// [uint32_t] cache size
|
||||
// [uint32_t] cache hash
|
||||
// .... compile cache content ....
|
||||
void CompileCacheHandler::Persist() {
|
||||
DCHECK(!compile_cache_dir_.empty());
|
||||
|
||||
// NOTE(joyeecheung): in most circumstances the code caching reading
|
||||
// writing logic is lenient enough that it's fine even if someone
|
||||
// overwrites the cache (that leads to either size or hash mismatch
|
||||
// in subsequent loads and the overwritten cache will be ignored).
|
||||
// Also in most use cases users should not change the files on disk
|
||||
// too rapidly. Therefore locking is not currently implemented to
|
||||
// avoid the cost.
|
||||
for (auto& pair : compiler_cache_store_) {
|
||||
auto* entry = pair.second.get();
|
||||
if (entry->cache == nullptr) {
|
||||
Debug("[compile cache] skip %s because the cache was not initialized\n",
|
||||
entry->source_filename);
|
||||
continue;
|
||||
}
|
||||
if (entry->refreshed == false) {
|
||||
Debug("[compile cache] skip %s because cache was the same\n",
|
||||
entry->source_filename);
|
||||
continue;
|
||||
}
|
||||
|
||||
DCHECK_EQ(entry->cache->buffer_policy,
|
||||
v8::ScriptCompiler::CachedData::BufferOwned);
|
||||
char* cache_ptr =
|
||||
reinterpret_cast<char*>(const_cast<uint8_t*>(entry->cache->data));
|
||||
uint32_t cache_size = static_cast<uint32_t>(entry->cache->length);
|
||||
uint32_t cache_hash = GetHash(cache_ptr, cache_size);
|
||||
|
||||
// Generating headers.
|
||||
std::vector<uint32_t> headers(kHeaderCount);
|
||||
headers[kCodeSizeOffset] = entry->code_size;
|
||||
headers[kCacheSizeOffset] = cache_size;
|
||||
headers[kCodeHashOffset] = entry->code_hash;
|
||||
headers[kCacheHashOffset] = cache_hash;
|
||||
|
||||
Debug("[compile cache] writing cache for %s in %s [%d %d %d %d]...",
|
||||
entry->source_filename,
|
||||
entry->cache_filename,
|
||||
headers[kCodeSizeOffset],
|
||||
headers[kCacheSizeOffset],
|
||||
headers[kCodeHashOffset],
|
||||
headers[kCacheHashOffset]);
|
||||
|
||||
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 bufs[] = {headers_buf, data_buf};
|
||||
|
||||
int err = WriteFileSync(entry->cache_filename.c_str(), bufs, 2);
|
||||
if (err < 0) {
|
||||
Debug("failed: %s\n", uv_strerror(err));
|
||||
} else {
|
||||
Debug("success\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CompileCacheHandler::CompileCacheHandler(Environment* env)
|
||||
: isolate_(env->isolate()),
|
||||
is_debug_(
|
||||
env->enabled_debug_list()->enabled(DebugCategory::COMPILE_CACHE)) {}
|
||||
|
||||
// Directory structure:
|
||||
// - Compile cache directory (from NODE_COMPILE_CACHE)
|
||||
// - <cache_version_tag_1>: hash of CachedDataVersionTag + NODE_VERESION
|
||||
// - <cache_version_tag_2>
|
||||
// - <cache_version_tag_3>
|
||||
// - <cache_file_1>: a hash of filename + module type
|
||||
// - <cache_file_2>
|
||||
// - <cache_file_3>
|
||||
bool CompileCacheHandler::InitializeDirectory(Environment* env,
|
||||
const std::string& dir) {
|
||||
compiler_cache_key_ = GetCacheVersionTag();
|
||||
std::string compiler_cache_key_string = Uint32ToHex(compiler_cache_key_);
|
||||
std::vector<std::string_view> paths = {dir, compiler_cache_key_string};
|
||||
std::string cache_dir = PathResolve(env, paths);
|
||||
|
||||
Debug("[compile cache] resolved path %s + %s -> %s\n",
|
||||
dir,
|
||||
compiler_cache_key_string,
|
||||
cache_dir);
|
||||
|
||||
if (UNLIKELY(!env->permission()->is_granted(
|
||||
permission::PermissionScope::kFileSystemWrite, cache_dir))) {
|
||||
Debug("[compile cache] skipping cache because write permission for %s "
|
||||
"is not granted\n",
|
||||
cache_dir);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (UNLIKELY(!env->permission()->is_granted(
|
||||
permission::PermissionScope::kFileSystemRead, cache_dir))) {
|
||||
Debug("[compile cache] skipping cache because read permission for %s "
|
||||
"is not granted\n",
|
||||
cache_dir);
|
||||
return false;
|
||||
}
|
||||
|
||||
fs::FSReqWrapSync req_wrap;
|
||||
int err = fs::MKDirpSync(nullptr, &(req_wrap.req), cache_dir, 0777, nullptr);
|
||||
if (is_debug_) {
|
||||
Debug("[compile cache] creating cache directory %s...%s\n",
|
||||
cache_dir,
|
||||
err < 0 ? uv_strerror(err) : "success");
|
||||
}
|
||||
if (err != 0 && err != UV_EEXIST) {
|
||||
return false;
|
||||
}
|
||||
|
||||
compile_cache_dir_ = cache_dir;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace node
|
||||
@@ -0,0 +1,84 @@
|
||||
#ifndef SRC_COMPILE_CACHE_H_
|
||||
#define SRC_COMPILE_CACHE_H_
|
||||
|
||||
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
|
||||
#include <cinttypes>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include "v8.h"
|
||||
|
||||
namespace node {
|
||||
class Environment;
|
||||
|
||||
// TODO(joyeecheung): move it into a CacheHandler class.
|
||||
enum class CachedCodeType : uint8_t {
|
||||
kCommonJS = 0,
|
||||
kESM,
|
||||
};
|
||||
|
||||
struct CompileCacheEntry {
|
||||
std::unique_ptr<v8::ScriptCompiler::CachedData> cache{nullptr};
|
||||
uint32_t cache_key;
|
||||
uint32_t code_hash;
|
||||
uint32_t code_size;
|
||||
|
||||
std::string cache_filename;
|
||||
std::string source_filename;
|
||||
CachedCodeType type;
|
||||
bool refreshed = false;
|
||||
// Copy the cache into a new store for V8 to consume. Caller takes
|
||||
// ownership.
|
||||
v8::ScriptCompiler::CachedData* CopyCache() const;
|
||||
};
|
||||
|
||||
class CompileCacheHandler {
|
||||
public:
|
||||
explicit CompileCacheHandler(Environment* env);
|
||||
bool InitializeDirectory(Environment* env, const std::string& dir);
|
||||
|
||||
void Persist();
|
||||
|
||||
CompileCacheEntry* GetOrInsert(v8::Local<v8::String> code,
|
||||
v8::Local<v8::String> filename,
|
||||
CachedCodeType type);
|
||||
void MaybeSave(CompileCacheEntry* entry,
|
||||
v8::Local<v8::Function> func,
|
||||
bool rejected);
|
||||
void MaybeSave(CompileCacheEntry* entry,
|
||||
v8::Local<v8::Module> mod,
|
||||
bool rejected);
|
||||
|
||||
private:
|
||||
void ReadCacheFile(CompileCacheEntry* entry);
|
||||
|
||||
template <typename T>
|
||||
void MaybeSaveImpl(CompileCacheEntry* entry,
|
||||
v8::Local<T> func_or_mod,
|
||||
bool rejected);
|
||||
|
||||
template <typename... Args>
|
||||
inline void Debug(const char* format, Args&&... args) const;
|
||||
|
||||
static constexpr size_t kCodeSizeOffset = 0;
|
||||
static constexpr size_t kCacheSizeOffset = 1;
|
||||
static constexpr size_t kCodeHashOffset = 2;
|
||||
static constexpr size_t kCacheHashOffset = 3;
|
||||
static constexpr size_t kHeaderCount = 4;
|
||||
|
||||
v8::Isolate* isolate_ = nullptr;
|
||||
bool is_debug_ = false;
|
||||
|
||||
std::string compile_cache_dir_;
|
||||
// The compile cache is stored in a directory whose name is the hex string of
|
||||
// compiler_cache_key_.
|
||||
uint32_t compiler_cache_key_ = 0;
|
||||
std::unordered_map<uint32_t, std::unique_ptr<CompileCacheEntry>>
|
||||
compiler_cache_store_;
|
||||
};
|
||||
} // namespace node
|
||||
|
||||
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
|
||||
#endif // SRC_COMPILE_CACHE_H_
|
||||
@@ -42,6 +42,7 @@ void NODE_EXTERN_PRIVATE FWrite(FILE* file, const std::string& str);
|
||||
// from a provider type to a debug category.
|
||||
#define DEBUG_CATEGORY_NAMES(V) \
|
||||
NODE_ASYNC_PROVIDER_TYPES(V) \
|
||||
V(COMPILE_CACHE) \
|
||||
V(DIAGNOSTICS) \
|
||||
V(HUGEPAGES) \
|
||||
V(INSPECTOR_SERVER) \
|
||||
|
||||
@@ -480,6 +480,16 @@ inline const std::string& Environment::exec_path() const {
|
||||
return exec_path_;
|
||||
}
|
||||
|
||||
inline CompileCacheHandler* Environment::compile_cache_handler() {
|
||||
auto* result = compile_cache_handler_.get();
|
||||
DCHECK_NOT_NULL(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
inline bool Environment::use_compile_cache() const {
|
||||
return compile_cache_handler_.get() != nullptr;
|
||||
}
|
||||
|
||||
#if HAVE_INSPECTOR
|
||||
inline void Environment::set_coverage_directory(const char* dir) {
|
||||
coverage_directory_ = std::string(dir);
|
||||
|
||||
+18
@@ -1089,6 +1089,24 @@ void Environment::InitializeLibuv() {
|
||||
StartProfilerIdleNotifier();
|
||||
}
|
||||
|
||||
void Environment::InitializeCompileCache() {
|
||||
std::string dir_from_env;
|
||||
if (!credentials::SafeGetenv(
|
||||
"NODE_COMPILE_CACHE", &dir_from_env, env_vars()) ||
|
||||
dir_from_env.empty()) {
|
||||
return;
|
||||
}
|
||||
auto handler = std::make_unique<CompileCacheHandler>(this);
|
||||
if (handler->InitializeDirectory(this, dir_from_env)) {
|
||||
compile_cache_handler_ = std::move(handler);
|
||||
AtExit(
|
||||
[](void* env) {
|
||||
static_cast<Environment*>(env)->compile_cache_handler()->Persist();
|
||||
},
|
||||
this);
|
||||
}
|
||||
}
|
||||
|
||||
void Environment::ExitEnv(StopFlags::Flags flags) {
|
||||
// Should not access non-thread-safe methods here.
|
||||
set_stopping(true);
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#endif
|
||||
#include "callback_queue.h"
|
||||
#include "cleanup_queue-inl.h"
|
||||
#include "compile_cache.h"
|
||||
#include "debug_utils.h"
|
||||
#include "env_properties.h"
|
||||
#include "handle_wrap.h"
|
||||
@@ -1010,6 +1011,10 @@ class Environment : public MemoryRetainer {
|
||||
inline void set_process_exit_handler(
|
||||
std::function<void(Environment*, ExitCode)>&& handler);
|
||||
|
||||
inline CompileCacheHandler* compile_cache_handler();
|
||||
inline bool use_compile_cache() const;
|
||||
void InitializeCompileCache();
|
||||
|
||||
void RunAndClearNativeImmediates(bool only_refed = false);
|
||||
void RunAndClearInterrupts();
|
||||
|
||||
@@ -1100,6 +1105,7 @@ class Environment : public MemoryRetainer {
|
||||
uint64_t heap_prof_interval_;
|
||||
#endif // HAVE_INSPECTOR
|
||||
|
||||
std::unique_ptr<CompileCacheHandler> compile_cache_handler_;
|
||||
std::shared_ptr<EnvironmentOptions> options_;
|
||||
// options_ contains debug options parsed from CLI arguments,
|
||||
// while inspector_host_port_ stores the actual inspector host
|
||||
|
||||
+31
-3
@@ -174,7 +174,7 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
|
||||
int column_offset = 0;
|
||||
|
||||
bool synthetic = args[2]->IsArray();
|
||||
|
||||
bool can_use_builtin_cache = false;
|
||||
Local<PrimitiveArray> host_defined_options =
|
||||
PrimitiveArray::New(isolate, HostDefinedOptions::kLength);
|
||||
Local<Symbol> id_symbol;
|
||||
@@ -193,6 +193,9 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
|
||||
column_offset = args[4].As<Int32>()->Value();
|
||||
if (args[5]->IsSymbol()) {
|
||||
id_symbol = args[5].As<Symbol>();
|
||||
can_use_builtin_cache =
|
||||
(id_symbol ==
|
||||
realm->isolate_data()->source_text_module_default_hdo());
|
||||
} else {
|
||||
id_symbol = Symbol::New(isolate, url);
|
||||
}
|
||||
@@ -232,7 +235,10 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
|
||||
isolate, url, span, SyntheticModuleEvaluationStepsCallback);
|
||||
} else {
|
||||
ScriptCompiler::CachedData* cached_data = nullptr;
|
||||
bool used_cache_from_user = false;
|
||||
if (args[5]->IsArrayBufferView()) {
|
||||
DCHECK(!can_use_builtin_cache); // We don't use this option internally.
|
||||
used_cache_from_user = true;
|
||||
Local<ArrayBufferView> cached_data_buf = args[5].As<ArrayBufferView>();
|
||||
uint8_t* data =
|
||||
static_cast<uint8_t*>(cached_data_buf->Buffer()->Data());
|
||||
@@ -253,6 +259,17 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
|
||||
false, // is WASM
|
||||
true, // is ES Module
|
||||
host_defined_options);
|
||||
|
||||
CompileCacheEntry* cache_entry = nullptr;
|
||||
if (can_use_builtin_cache && realm->env()->use_compile_cache()) {
|
||||
cache_entry = realm->env()->compile_cache_handler()->GetOrInsert(
|
||||
source_text, url, CachedCodeType::kESM);
|
||||
}
|
||||
if (cache_entry != nullptr && cache_entry->cache != nullptr) {
|
||||
// source will take ownership of cached_data.
|
||||
cached_data = cache_entry->CopyCache();
|
||||
}
|
||||
|
||||
ScriptCompiler::Source source(source_text, origin, cached_data);
|
||||
ScriptCompiler::CompileOptions options;
|
||||
if (source.GetCachedData() == nullptr) {
|
||||
@@ -273,8 +290,19 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (options == ScriptCompiler::kConsumeCodeCache &&
|
||||
source.GetCachedData()->rejected) {
|
||||
|
||||
bool cache_rejected = false;
|
||||
if (options == ScriptCompiler::kConsumeCodeCache) {
|
||||
cache_rejected = source.GetCachedData()->rejected;
|
||||
}
|
||||
|
||||
if (cache_entry != nullptr) {
|
||||
realm->env()->compile_cache_handler()->MaybeSave(
|
||||
cache_entry, module, cache_rejected);
|
||||
}
|
||||
|
||||
// If the cache comes from builtin compile cache, fail silently.
|
||||
if (cache_rejected && used_cache_from_user) {
|
||||
THROW_ERR_VM_MODULE_CACHED_DATA_REJECTED(
|
||||
realm, "cachedData buffer was rejected");
|
||||
try_catch.ReThrow();
|
||||
|
||||
+23
-6
@@ -1603,8 +1603,8 @@ static void CompileFunctionForCJSLoader(
|
||||
hdo);
|
||||
ScriptCompiler::CachedData* cached_data = nullptr;
|
||||
|
||||
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
|
||||
bool used_cache_from_sea = false;
|
||||
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
|
||||
if (sea::IsSingleExecutable()) {
|
||||
sea::SeaResource sea = sea::FindSingleExecutableResource();
|
||||
if (sea.use_code_cache()) {
|
||||
@@ -1617,7 +1617,24 @@ static void CompileFunctionForCJSLoader(
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
CompileCacheEntry* cache_entry = nullptr;
|
||||
if (!used_cache_from_sea && env->use_compile_cache()) {
|
||||
cache_entry = env->compile_cache_handler()->GetOrInsert(
|
||||
code, filename, CachedCodeType::kCommonJS);
|
||||
}
|
||||
if (cache_entry != nullptr && cache_entry->cache != nullptr) {
|
||||
// source will take ownership of cached_data.
|
||||
cached_data = cache_entry->CopyCache();
|
||||
}
|
||||
|
||||
ScriptCompiler::Source source(code, origin, cached_data);
|
||||
ScriptCompiler::CompileOptions options;
|
||||
if (cached_data == nullptr) {
|
||||
options = ScriptCompiler::kNoCompileOptions;
|
||||
} else {
|
||||
options = ScriptCompiler::kConsumeCodeCache;
|
||||
}
|
||||
|
||||
TryCatchScope try_catch(env);
|
||||
|
||||
@@ -1631,8 +1648,7 @@ static void CompileFunctionForCJSLoader(
|
||||
0, /* context extensions size */
|
||||
nullptr, /* context extensions data */
|
||||
// TODO(joyeecheung): allow optional eager compilation.
|
||||
cached_data == nullptr ? ScriptCompiler::kNoCompileOptions
|
||||
: ScriptCompiler::kConsumeCodeCache,
|
||||
options,
|
||||
v8::ScriptCompiler::NoCacheReason::kNoCacheNoReason);
|
||||
|
||||
Local<Function> fn;
|
||||
@@ -1647,11 +1663,12 @@ static void CompileFunctionForCJSLoader(
|
||||
}
|
||||
|
||||
bool cache_rejected = false;
|
||||
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
|
||||
if (used_cache_from_sea) {
|
||||
if (options == ScriptCompiler::kConsumeCodeCache) {
|
||||
cache_rejected = source.GetCachedData()->rejected;
|
||||
}
|
||||
#endif
|
||||
if (cache_entry != nullptr) {
|
||||
env->compile_cache_handler()->MaybeSave(cache_entry, fn, cache_rejected);
|
||||
}
|
||||
|
||||
std::vector<Local<Name>> names = {
|
||||
env->cached_data_rejected_string(),
|
||||
|
||||
@@ -370,6 +370,7 @@ typedef struct tm TIME_TYPE;
|
||||
#endif
|
||||
|
||||
double GetCurrentTimeInMicroseconds();
|
||||
int WriteFileSync(const char* path, uv_buf_t* bufs, size_t buf_count);
|
||||
int WriteFileSync(const char* path, uv_buf_t buf);
|
||||
int WriteFileSync(v8::Isolate* isolate,
|
||||
const char* path,
|
||||
|
||||
+5
-1
@@ -228,6 +228,10 @@ double GetCurrentTimeInMicroseconds() {
|
||||
}
|
||||
|
||||
int WriteFileSync(const char* path, uv_buf_t buf) {
|
||||
return WriteFileSync(path, &buf, 1);
|
||||
}
|
||||
|
||||
int WriteFileSync(const char* path, uv_buf_t* bufs, size_t buf_count) {
|
||||
uv_fs_t req;
|
||||
int fd = uv_fs_open(nullptr,
|
||||
&req,
|
||||
@@ -240,7 +244,7 @@ int WriteFileSync(const char* path, uv_buf_t buf) {
|
||||
return fd;
|
||||
}
|
||||
|
||||
int err = uv_fs_write(nullptr, &req, fd, &buf, 1, 0, nullptr);
|
||||
int err = uv_fs_write(nullptr, &req, fd, bufs, buf_count, 0, nullptr);
|
||||
uv_fs_req_cleanup(&req);
|
||||
if (err < 0) {
|
||||
return err;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
import('deps').then(mod => { console.log('hello ' + mod.hello); });
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
import('deps').then(mod => { console.log('hello ' + mod.hello); });
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const hello = 'world';
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "module",
|
||||
"main": "mod.js"
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
'use strict';
|
||||
|
||||
// This tests NODE_COMPILE_CACHE works.
|
||||
|
||||
require('../common');
|
||||
const { spawnSyncAndExit } = require('../common/child_process');
|
||||
const fixtures = require('../common/fixtures');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
{
|
||||
// Test that it throws if the script fails to parse, and no cache is created.
|
||||
tmpdir.refresh();
|
||||
const dir = tmpdir.resolve('.compile_cache_dir');
|
||||
|
||||
spawnSyncAndExit(
|
||||
process.execPath,
|
||||
[fixtures.path('syntax', 'bad_syntax.js')],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: dir
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
status: 1,
|
||||
stderr: /skip .*bad_syntax\.js because the cache was not initialized/,
|
||||
});
|
||||
|
||||
const cacheDir = fs.readdirSync(dir);
|
||||
assert.strictEqual(cacheDir.length, 1);
|
||||
const entries = fs.readdirSync(path.join(dir, cacheDir[0]));
|
||||
assert.strictEqual(entries.length, 0);
|
||||
|
||||
spawnSyncAndExit(
|
||||
process.execPath,
|
||||
[fixtures.path('syntax', 'bad_syntax.mjs')],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: dir
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
status: 1,
|
||||
stderr: /skip .*bad_syntax\.mjs because the cache was not initialized/,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
'use strict';
|
||||
|
||||
// This tests NODE_COMPILE_CACHE works in existing directory.
|
||||
|
||||
require('../common');
|
||||
const { spawnSyncAndAssert } = require('../common/child_process');
|
||||
const assert = require('assert');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
const fixtures = require('../common/fixtures');
|
||||
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const dir = tmpdir.resolve('.compile_cache_dir');
|
||||
const cjs = fixtures.path('es-modules', 'dynamic-import', 'import.cjs');
|
||||
const mjs = fixtures.path('es-modules', 'dynamic-import', 'import.mjs');
|
||||
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[cjs],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: dir
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
trim: true,
|
||||
stdout: 'hello world',
|
||||
stderr(output) {
|
||||
assert.match(output, /reading cache from .* for ESM .*mod\.js/);
|
||||
assert.match(output, /mod\.js was not initialized, initializing the in-memory entry/);
|
||||
assert.match(output, /writing cache for .*mod\.js.*success/);
|
||||
assert.match(output, /reading cache .* for CommonJS .*import\.cjs/);
|
||||
assert.match(output, /import\.cjs was not initialized, initializing the in-memory entry/);
|
||||
assert.match(output, /writing cache for .*import\.cjs.*success/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[cjs],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: dir
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
trim: true,
|
||||
stdout: 'hello world',
|
||||
stderr(output) {
|
||||
assert.match(output, /cache for .*mod\.js was accepted, keeping the in-memory entry/);
|
||||
assert.match(output, /.*skip .*mod\.js because cache was the same/);
|
||||
assert.match(output, /cache for .*import\.cjs was accepted, keeping the in-memory entry/);
|
||||
assert.match(output, /.*skip .*import\.cjs because cache was the same/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[mjs],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: dir
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
trim: true,
|
||||
stdout: 'hello world',
|
||||
stderr(output) {
|
||||
assert.match(output, /cache for .*mod\.js was accepted, keeping the in-memory entry/);
|
||||
assert.match(output, /.*skip .*mod\.js because cache was the same/);
|
||||
assert.match(output, /reading cache .* for ESM .*import\.mjs/);
|
||||
assert.match(output, /import\.mjs was not initialized, initializing the in-memory entry/);
|
||||
assert.match(output, /writing cache for .*import\.mjs.*success/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[mjs],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: dir
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
trim: true,
|
||||
stdout: 'hello world',
|
||||
stderr(output) {
|
||||
assert.match(output, /cache for .*mod\.js was accepted, keeping the in-memory entry/);
|
||||
assert.match(output, /.*skip .*mod\.js because cache was the same/);
|
||||
assert.match(output, /cache for .*import\.mjs was accepted, keeping the in-memory entry/);
|
||||
assert.match(output, /.*skip .*import\.mjs because cache was the same/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
// This tests NODE_COMPILE_CACHE works in existing directory.
|
||||
|
||||
require('../common');
|
||||
const { spawnSyncAndAssert } = require('../common/child_process');
|
||||
const assert = require('assert');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
const fixtures = require('../common/fixtures');
|
||||
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const dir = tmpdir.resolve('.compile_cache_dir');
|
||||
const script = fixtures.path('es-modules', 'message.mjs');
|
||||
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[script],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: dir
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
stderr(output) {
|
||||
assert.match(output, /message\.mjs was not initialized, initializing the in-memory entry/);
|
||||
assert.match(output, /writing cache for .*message\.mjs.*success/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[script],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: dir
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
stderr(output) {
|
||||
assert.match(output, /cache for .*message\.mjs was accepted, keeping the in-memory entry/);
|
||||
assert.match(output, /.*skip .*message\.mjs because cache was the same/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
'use strict';
|
||||
|
||||
// This tests NODE_COMPILE_CACHE works in existing directory.
|
||||
|
||||
require('../common');
|
||||
const { spawnSyncAndAssert } = require('../common/child_process');
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const fixtures = require('../common/fixtures');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const dir = tmpdir.resolve('.compile_cache_dir');
|
||||
fs.mkdirSync(dir);
|
||||
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[fixtures.path('empty.js')],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: dir
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
stderr(output) {
|
||||
assert.match(output, /empty\.js was not initialized, initializing the in-memory entry/);
|
||||
assert.match(output, /writing cache for .*empty\.js.*success/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Second run reads the cache.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[fixtures.path('empty.js')],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: dir
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
stderr(output) {
|
||||
assert.match(output, /cache for .*empty\.js was accepted, keeping the in-memory entry/);
|
||||
assert.match(output, /.*skip .*empty\.js because cache was the same/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
'use strict';
|
||||
|
||||
// This tests NODE_COMPILE_CACHE works in existing directory.
|
||||
|
||||
require('../common');
|
||||
const { spawnSyncAndAssert } = require('../common/child_process');
|
||||
const assert = require('assert');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
const fixtures = require('../common/fixtures');
|
||||
const fs = require('fs');
|
||||
|
||||
function testAllowed(readDir, writeDir, envDir) {
|
||||
console.log(readDir, writeDir, envDir); // Logging for debugging.
|
||||
|
||||
tmpdir.refresh();
|
||||
const dummyDir = tmpdir.resolve('dummy');
|
||||
fs.mkdirSync(dummyDir);
|
||||
const script = tmpdir.resolve(dummyDir, 'empty.js');
|
||||
fs.copyFileSync(fixtures.path('empty.js'), script);
|
||||
// If the directory doesn't exist, permission will just be disallowed.
|
||||
fs.mkdirSync(tmpdir.resolve(envDir));
|
||||
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--experimental-permission',
|
||||
`--allow-fs-read=${dummyDir}`,
|
||||
`--allow-fs-read=${readDir}`,
|
||||
`--allow-fs-write=${writeDir}`,
|
||||
script,
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: `${envDir}`
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
stderr(output) {
|
||||
assert.match(output, /writing cache for .*empty\.js.*success/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--experimental-permission',
|
||||
`--allow-fs-read=${dummyDir}`,
|
||||
`--allow-fs-read=${readDir}`,
|
||||
`--allow-fs-write=${writeDir}`,
|
||||
script,
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: `${envDir}`
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
stderr(output) {
|
||||
assert.match(output, /cache for .*empty\.js was accepted/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
testAllowed(tmpdir.resolve('.compile_cache'), tmpdir.resolve('.compile_cache'), '.compile_cache');
|
||||
testAllowed(tmpdir.resolve('.compile_cache'), tmpdir.resolve('.compile_cache'), tmpdir.resolve('.compile_cache'));
|
||||
testAllowed('*', '*', '.compile_cache');
|
||||
testAllowed('*', tmpdir.resolve('.compile_cache'), '.compile_cache');
|
||||
testAllowed(tmpdir.resolve('.compile_cache'), '*', '.compile_cache');
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
'use strict';
|
||||
|
||||
// This tests NODE_COMPILE_CACHE works in existing directory.
|
||||
|
||||
require('../common');
|
||||
const { spawnSyncAndAssert } = require('../common/child_process');
|
||||
const assert = require('assert');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
const fixtures = require('../common/fixtures');
|
||||
const fs = require('fs');
|
||||
|
||||
function testDisallowed(dummyDir, cacheDirInPermission, cacheDirInEnv) {
|
||||
console.log(dummyDir, cacheDirInPermission, cacheDirInEnv); // Logging for debugging.
|
||||
|
||||
tmpdir.refresh();
|
||||
const script = tmpdir.resolve(dummyDir, 'empty.js');
|
||||
fs.mkdirSync(tmpdir.resolve(dummyDir));
|
||||
fs.copyFileSync(fixtures.path('empty.js'), script);
|
||||
// If the directory doesn't exist, permission will just be disallowed.
|
||||
if (cacheDirInPermission !== '*') {
|
||||
fs.mkdirSync(tmpdir.resolve(cacheDirInPermission));
|
||||
}
|
||||
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--experimental-permission',
|
||||
`--allow-fs-read=${dummyDir}`, // No read or write permission for cache dir.
|
||||
`--allow-fs-write=${dummyDir}`,
|
||||
script,
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: `${cacheDirInEnv}`
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
stderr(output) {
|
||||
assert.match(output, /skipping cache because write permission for .* is not granted/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--experimental-permission',
|
||||
`--allow-fs-read=${dummyDir}`,
|
||||
`--allow-fs-read=${cacheDirInPermission}`, // Read-only
|
||||
`--allow-fs-write=${dummyDir}`,
|
||||
script,
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: `${cacheDirInEnv}`
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
stderr(output) {
|
||||
assert.match(output, /skipping cache because write permission for .* is not granted/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--experimental-permission',
|
||||
`--allow-fs-read=${dummyDir}`,
|
||||
`--allow-fs-write=${cacheDirInPermission}`, // Write-only
|
||||
script,
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: `${cacheDirInEnv}`
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
stderr(output) {
|
||||
assert.match(output, /skipping cache because read permission for .* is not granted/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
testDisallowed(tmpdir.resolve('dummy'), tmpdir.resolve('.compile_cache') + '/', '.compile_cache');
|
||||
testDisallowed(tmpdir.resolve('dummy'), tmpdir.resolve('.compile_cache/') + '/', tmpdir.resolve('.compile_cache'));
|
||||
testDisallowed(tmpdir.resolve('dummy'), '*', '.compile_cache');
|
||||
testDisallowed(tmpdir.resolve('dummy'), '*', tmpdir.resolve('.compile_cache'));
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
'use strict';
|
||||
|
||||
// This tests NODE_COMPILE_CACHE works.
|
||||
|
||||
require('../common');
|
||||
const { spawnSyncAndAssert } = require('../common/child_process');
|
||||
const assert = require('assert');
|
||||
const fixtures = require('../common/fixtures');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
{
|
||||
// Test that it works with non-existent directory.
|
||||
tmpdir.refresh();
|
||||
const dir = tmpdir.resolve('.compile_cache_dir');
|
||||
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[fixtures.path('snapshot', 'typescript.js')],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: dir
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
stderr(output) {
|
||||
console.log(output); // Logging for debugging.
|
||||
assert.match(output, /typescript\.js was not initialized, initializing the in-memory entry/);
|
||||
assert.match(output, /writing cache for .*typescript\.js.*success/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
const cacheDir = fs.readdirSync(dir);
|
||||
assert.strictEqual(cacheDir.length, 1);
|
||||
const entries = fs.readdirSync(path.join(dir, cacheDir[0]));
|
||||
assert.strictEqual(entries.length, 1);
|
||||
const cacheFile = path.join(dir, cacheDir[0], entries[0]);
|
||||
const data = fs.readFileSync(cacheFile);
|
||||
console.log(`Header in ${cacheFile}`, new Uint32Array(data.buffer, data.byteOffset, 4));
|
||||
|
||||
// Second run reads the cache, but no need to re-write because it didn't change.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[fixtures.path('snapshot', 'typescript.js')],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: dir
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
stderr(output) {
|
||||
console.log(output); // Logging for debugging.
|
||||
assert.match(output, /cache for .*typescript\.js was accepted, keeping the in-memory entry/);
|
||||
assert.match(output, /.*skip .*typescript\.js because cache was the same/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
'use strict';
|
||||
|
||||
// This tests NODE_COMPILE_CACHE works in existing directory.
|
||||
|
||||
require('../common');
|
||||
const { spawnSyncAndAssert } = require('../common/child_process');
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const dir = tmpdir.resolve('.compile_cache_dir');
|
||||
const script = tmpdir.resolve('script.js');
|
||||
fs.writeFileSync(script, 'const foo = 1;', 'utf-8');
|
||||
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[script],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: dir
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
stderr(output) {
|
||||
assert.match(output, /script\.js was not initialized, initializing the in-memory entry/);
|
||||
assert.match(output, /writing cache for .*script\.js.*success/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Update the content.
|
||||
fs.writeFileSync(script, 'const foo = 2;', 'utf-8');
|
||||
|
||||
// Another run regenerates it.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[script],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: dir
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
stderr(output) {
|
||||
assert.match(output, /reading cache from .* for .*script\.js.*code hash mismatch:/);
|
||||
assert.match(output, /writing cache for .*script\.js.*success/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Then it's consumed just fine.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[script],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
|
||||
NODE_COMPILE_CACHE: dir
|
||||
},
|
||||
cwd: tmpdir.path
|
||||
},
|
||||
{
|
||||
stderr(output) {
|
||||
assert.match(output, /cache for .*script\.js was accepted, keeping the in-memory entry/);
|
||||
assert.match(output, /.*skip .*script\.js because cache was the same/);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user