Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -3284,6 +3284,11 @@ added: v22.1.0
Enable the [module compile cache][] for the Node.js instance. See the documentation of
[module compile cache][] for details.

### `NODE_COMPILE_CACHE_PORTABLE=1`

When set to 1, the [module compile cache][] can be reused across different directory
locations as long as the module layout relative to the cache directory remains the same.

### `NODE_DEBUG=module[,…]`

<!-- YAML
Expand Down
23 changes: 23 additions & 0 deletions doc/api/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,28 @@ the [`NODE_COMPILE_CACHE=dir`][] environment variable if it's set, or defaults
to `path.join(os.tmpdir(), 'node-compile-cache')` otherwise. To locate the compile cache
directory used by a running Node.js instance, use [`module.getCompileCacheDir()`][].

By default, caches are invalidated when the absolute paths of the modules being
cached are changed. To keep the cache working after moving the
project directory, enable portable compile cache. This allows previously compiled
modules to be reused across different directory locations as long as the layout relative
to the cache directory remains the same. This would be done on a best-effort basis. If
Node.js cannot compute the location of a module relative to the cache directory, the module
will not be cached.

There are two ways to enable the portable mode:

1. Using the portable option in module.enableCompileCache():

```js
// Non-portable cache (default): cache breaks if project is moved
module.enableCompileCache({ path: '/path/to/cache/storage/dir' });

// Portable cache: cache works after the project is moved
module.enableCompileCache({ path: '/path/to/cache/storage/dir', portable: true });
```

2. Setting the environment variable: [`NODE_COMPILE_CACHE_PORTABLE=1`][]

Currently when using the compile cache 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
Expand Down Expand Up @@ -1789,6 +1811,7 @@ returned object contains the following keys:
[`--import`]: cli.md#--importmodule
[`--require`]: cli.md#-r---require-module
[`NODE_COMPILE_CACHE=dir`]: cli.md#node_compile_cachedir
[`NODE_COMPILE_CACHE_PORTABLE=1`]: cli.md#node_compile_cache_portable1
[`NODE_DISABLE_COMPILE_CACHE=1`]: cli.md#node_disable_compile_cache1
[`NODE_V8_COVERAGE=dir`]: cli.md#node_v8_coveragedir
[`SourceMap`]: #class-modulesourcemap
Expand Down
7 changes: 7 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,13 @@ Enable the
.Sy module compile cache
for the Node.js instance.
.
.It Ev NODE_COMPILE_CACHE_PORTABLE
When set to '1' or 'true', the
.Sy module compile cache
will be hit as long as the location of the modules relative to the cache directory remain
consistent. This can be used in conjunction with .Ev NODE_COMPILE_CACHE
to enable portable on-disk caching.
.
.It Ev NODE_DEBUG Ar modules...
Comma-separated list of core modules that should print debug information.
.
Expand Down
25 changes: 19 additions & 6 deletions lib/internal/modules/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,18 +383,31 @@ function stringify(body) {
}

/**
* Enable on-disk compiled cache for all user modules being complied in the current Node.js instance
* Enable on-disk compiled cache for all user modules being compiled in the current Node.js instance
* after this method is called.
* If cacheDir is undefined, defaults to the NODE_MODULE_CACHE environment variable.
* If NODE_MODULE_CACHE isn't set, default to path.join(os.tmpdir(), 'node-compile-cache').
* @param {string|undefined} cacheDir
* This method accepts either:
* - A string `cacheDir`: the path to the cache directory.
* - An options object `{path?: string, portable?: boolean}`:
* - `path`: A string path to the cache directory.
* - `portable`: If `portable` is true, the cache directory will be considered relative. Defaults to false.
* If cache path is undefined, it defaults to the NODE_MODULE_CACHE environment variable.
* If `NODE_MODULE_CACHE` isn't set, it defaults to `path.join(os.tmpdir(), 'node-compile-cache')`.
* @param {string | { path?: string, portable?: boolean } | undefined} options
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed an issue with the new options - it should be directory instead of path to align with the return value. Marking the PR with dont-land labels to fix it before it gets released.

* @returns {{status: number, message?: string, directory?: string}}
*/
function enableCompileCache(cacheDir) {
function enableCompileCache(options) {
let cacheDir;
let portable = false;

if (typeof options === 'object' && options !== null) {
({ path: cacheDir, portable = false } = options);
} else {
cacheDir = options;
}
if (cacheDir === undefined) {
cacheDir = join(lazyTmpdir(), 'node-compile-cache');
}
const nativeResult = _enableCompileCache(cacheDir);
const nativeResult = _enableCompileCache(cacheDir, portable);
const result = { status: nativeResult[0] };
if (nativeResult[1]) {
result.message = nativeResult[1];
Expand Down
52 changes: 50 additions & 2 deletions src/compile_cache.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
#include <unistd.h> // getuid
#endif

#ifdef _WIN32
#include <windows.h>
#endif
namespace node {

using v8::Function;
Expand Down Expand Up @@ -223,13 +226,52 @@ void CompileCacheHandler::ReadCacheFile(CompileCacheEntry* entry) {
Debug(" success, size=%d\n", total_read);
}

static std::string GetRelativePath(std::string_view path,
std::string_view base) {
// On Windows, the native encoding is UTF-16, so we need to convert
// the paths to wide strings before using std::filesystem::path.
// On other platforms, std::filesystem::path can handle UTF-8 directly.
#ifdef _WIN32
std::filesystem::path module_path(
ConvertToWideString(std::string(path), CP_UTF8));
std::filesystem::path base_path(
ConvertToWideString(std::string(base), CP_UTF8));
#else
std::filesystem::path module_path(path);
std::filesystem::path base_path(base);
#endif
std::filesystem::path relative = module_path.lexically_relative(base_path);
auto u8str = relative.u8string();
return std::string(u8str.begin(), u8str.end());
}

CompileCacheEntry* CompileCacheHandler::GetOrInsert(Local<String> code,
Local<String> filename,
CachedCodeType type) {
DCHECK(!compile_cache_dir_.empty());

Environment* env = Environment::GetCurrent(isolate_->GetCurrentContext());
Utf8Value filename_utf8(isolate_, filename);
uint32_t key = GetCacheKey(filename_utf8.ToStringView(), type);
std::string file_path = filename_utf8.ToString();
// If the portable cache is enabled and it seems possible to compute the
// relative position from an absolute path, we use the relative position
// in the cache key.
if (portable_ == EnableOption::PORTABLE && IsAbsoluteFilePath(file_path)) {
// Normalize the path to ensure it is consistent.
std::string normalized_file_path = NormalizeFileURLOrPath(env, file_path);
if (normalized_file_path.empty()) {
return nullptr;
}
std::string relative_path =
GetRelativePath(normalized_file_path, normalized_compile_cache_dir_);
if (!relative_path.empty()) {
file_path = relative_path;
Debug("[compile cache] using relative path %s from %s\n",
file_path.c_str(),
compile_cache_dir_.c_str());
}
}
uint32_t key = GetCacheKey(file_path, 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
Expand Down Expand Up @@ -500,7 +542,8 @@ CompileCacheHandler::CompileCacheHandler(Environment* env)
// - $NODE_VERSION-$ARCH-$CACHE_DATA_VERSION_TAG-$UID
// - $FILENAME_AND_MODULE_TYPE_HASH.cache: a hash of filename + module type
CompileCacheEnableResult CompileCacheHandler::Enable(Environment* env,
const std::string& dir) {
const std::string& dir,
EnableOption option) {
std::string cache_tag = GetCacheVersionTag();
std::string absolute_cache_dir_base = PathResolve(env, {dir});
std::string cache_dir_with_tag =
Expand Down Expand Up @@ -548,6 +591,11 @@ CompileCacheEnableResult CompileCacheHandler::Enable(Environment* env,

result.cache_directory = absolute_cache_dir_base;
compile_cache_dir_ = cache_dir_with_tag;
portable_ = option;
if (option == EnableOption::PORTABLE) {
normalized_compile_cache_dir_ =
NormalizeFileURLOrPath(env, compile_cache_dir_);
}
result.status = CompileCacheEnableStatus::ENABLED;
return result;
}
Expand Down
8 changes: 7 additions & 1 deletion src/compile_cache.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,14 @@ struct CompileCacheEnableResult {
std::string message; // Set in case of failure.
};

enum class EnableOption : uint8_t { DEFAULT, PORTABLE };

class CompileCacheHandler {
public:
explicit CompileCacheHandler(Environment* env);
CompileCacheEnableResult Enable(Environment* env, const std::string& dir);
CompileCacheEnableResult Enable(Environment* env,
const std::string& dir,
EnableOption option = EnableOption::DEFAULT);

void Persist();

Expand Down Expand Up @@ -103,6 +107,8 @@ class CompileCacheHandler {
bool is_debug_ = false;

std::string compile_cache_dir_;
std::string normalized_compile_cache_dir_;
EnableOption portable_ = EnableOption::DEFAULT;
std::unordered_map<uint32_t, std::unique_ptr<CompileCacheEntry>>
compiler_cache_store_;
};
Expand Down
16 changes: 13 additions & 3 deletions src/env.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1122,11 +1122,21 @@ void Environment::InitializeCompileCache() {
dir_from_env.empty()) {
return;
}
EnableCompileCache(dir_from_env);
std::string portable_env;
bool portable = credentials::SafeGetenv(
"NODE_COMPILE_CACHE_PORTABLE", &portable_env, this) &&
!portable_env.empty() && portable_env == "1";
if (portable) {
Debug(this,
DebugCategory::COMPILE_CACHE,
"[compile cache] using relative path\n");
}
EnableCompileCache(dir_from_env,
portable ? EnableOption::PORTABLE : EnableOption::DEFAULT);
}

CompileCacheEnableResult Environment::EnableCompileCache(
const std::string& cache_dir) {
const std::string& cache_dir, EnableOption option) {
CompileCacheEnableResult result;
std::string disable_env;
if (credentials::SafeGetenv(
Expand All @@ -1143,7 +1153,7 @@ CompileCacheEnableResult Environment::EnableCompileCache(
if (!compile_cache_handler_) {
std::unique_ptr<CompileCacheHandler> handler =
std::make_unique<CompileCacheHandler>(this);
result = handler->Enable(this, cache_dir);
result = handler->Enable(this, cache_dir, option);
if (result.status == CompileCacheEnableStatus::ENABLED) {
compile_cache_handler_ = std::move(handler);
AtExit(
Expand Down
3 changes: 2 additions & 1 deletion src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -1022,7 +1022,8 @@ class Environment final : public MemoryRetainer {
void InitializeCompileCache();
// Enable built-in compile cache if it has not yet been enabled.
// The cache will be persisted to disk on exit.
CompileCacheEnableResult EnableCompileCache(const std::string& cache_dir);
CompileCacheEnableResult EnableCompileCache(const std::string& cache_dir,
EnableOption option);
void FlushCompileCache();

void RunAndClearNativeImmediates(bool only_refed = false);
Expand Down
8 changes: 7 additions & 1 deletion src/node_modules.cc
Original file line number Diff line number Diff line change
Expand Up @@ -513,8 +513,14 @@ void EnableCompileCache(const FunctionCallbackInfo<Value>& args) {
THROW_ERR_INVALID_ARG_TYPE(env, "cacheDir should be a string");
return;
}

EnableOption option = EnableOption::DEFAULT;
if (args.Length() > 1 && args[1]->IsTrue()) {
option = EnableOption::PORTABLE;
}

Utf8Value value(isolate, args[0]);
CompileCacheEnableResult result = env->EnableCompileCache(*value);
CompileCacheEnableResult result = env->EnableCompileCache(*value, option);
Local<Value> values[3];
values[0] = v8::Integer::New(isolate, static_cast<uint8_t>(result.status));
if (ToV8Value(context, result.message).ToLocal(&values[1]) &&
Expand Down
46 changes: 46 additions & 0 deletions src/path.cc
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#include "path.h"
#include <string>
#include <vector>
#include "ada.h"
#include "env-inl.h"
#include "node_internals.h"
#include "node_url.h"

namespace node {

Expand Down Expand Up @@ -88,6 +90,10 @@ std::string NormalizeString(const std::string_view path,
}

#ifdef _WIN32
constexpr bool IsWindowsDriveLetter(const std::string_view path) noexcept {
return path.size() > 2 && IsWindowsDeviceRoot(path[0]) &&
(path[1] == ':' && (path[2] == '/' || path[2] == '\\'));
}
constexpr bool IsWindowsDeviceRoot(const char c) noexcept {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}
Expand Down Expand Up @@ -333,4 +339,44 @@ void FromNamespacedPath(std::string* path) {
#endif
}

// Check if a path looks like an absolute path or file URL.
bool IsAbsoluteFilePath(std::string_view path) {
if (path.rfind("file://", 0) == 0) {
return true;
}
#ifdef _WIN32
if (path.size() > 0 && path[0] == '\\') return true;
if (IsWindowsDriveLetter(path)) return true;
#endif
if (path.size() > 0 && path[0] == '/') return true;
return false;
}

// Normalizes paths by resolving file URLs and converting to a consistent
// format with forward slashes.
std::string NormalizeFileURLOrPath(Environment* env, std::string_view path) {
std::string normalized_string(path);
constexpr std::string_view file_scheme = "file://";
if (normalized_string.rfind(file_scheme, 0) == 0) {
auto out = ada::parse<ada::url_aggregator>(normalized_string);
auto file_path = url::FileURLToPath(env, *out);
if (!file_path.has_value()) {
return std::string();
}
normalized_string = file_path.value();
}
normalized_string = NormalizeString(normalized_string, false, "/");
#ifdef _WIN32
if (IsWindowsDriveLetter(normalized_string)) {
normalized_string[0] = ToLower(normalized_string[0]);
}
for (char& c : normalized_string) {
if (c == '\\') {
c = '/';
}
}
#endif
return normalized_string;
}

} // namespace node
3 changes: 3 additions & 0 deletions src/path.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ std::string NormalizeString(const std::string_view path,

std::string PathResolve(Environment* env,
const std::vector<std::string_view>& paths);
std::string NormalizeFileURLOrPath(Environment* env, std::string_view path);
bool IsAbsoluteFilePath(std::string_view path);

#ifdef _WIN32
constexpr bool IsWindowsDeviceRoot(const char c) noexcept;
constexpr bool IsWindowsDriveLetter(const std::string_view path) noexcept;
#endif // _WIN32

void ToNamespacedPath(Environment* env, BufferValue* path);
Expand Down
2 changes: 1 addition & 1 deletion test/parallel/test-compile-cache-api-error.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ require('../common');
const { enableCompileCache } = require('module');
const assert = require('assert');

for (const invalid of [0, null, false, () => {}, {}, []]) {
for (const invalid of [0, null, false, 1, NaN, true, Symbol(0)]) {
assert.throws(() => enableCompileCache(invalid), { code: 'ERR_INVALID_ARG_TYPE' });
}
Loading
Loading