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
84 changes: 49 additions & 35 deletions doc/api/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,20 +385,33 @@

The module compile cache can be enabled either using the [`module.enableCompileCache()`][]
method or the [`NODE_COMPILE_CACHE=dir`][] environment variable. After it is enabled,
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.
whenever Node.js compiles a CommonJS, a ECMAScript Module, or a TypeScript 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 compile cache on disk, simply remove the cache directory. The cache
directory will be recreated the next time the same directory is used for for compile cache
storage. To avoid filling up the disk with stale cache, it is recommended to use a directory
under the [`os.tmpdir()`][]. If the compile cache is enabled by a call to
[`module.enableCompileCache()`][] without specifying the directory, Node.js will use
[`module.enableCompileCache()`][] without specifying the `directory`, Node.js will use
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()`][].

The enabled module compile cache can be disabled by the [`NODE_DISABLE_COMPILE_CACHE=1`][]
environment variable. This can be useful when the compile cache leads to unexpected or
undesired behaviors (e.g. less precise test coverage).

At the moment, when the compile cache is enabled and a module is loaded afresh, the
code cache is generated from the compiled code immediately, but will only be written
to disk when the Node.js instance is about to exit. This is subject to change. The
[`module.flushCompileCache()`][] method can be used to ensure the accumulated code cache
is flushed to disk in case the application wants to spawn other Node.js instances
and let them share the cache long before the parent exits.

### Portability of the compile cache

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
Expand All @@ -409,38 +422,29 @@

There are two ways to enable the portable mode:

1. Using the portable option in module.enableCompileCache():
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' });
module.enableCompileCache({ directory: '/path/to/cache/storage/dir' });

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

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

### Limitations of the compile cache

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
running tests to generate precise coverage.

The enabled module compile cache can be disabled by the [`NODE_DISABLE_COMPILE_CACHE=1`][]
environment variable. This can be useful when the compile cache leads to unexpected or
undesired behaviors (e.g. less precise test coverage).

Compilation cache generated by one version of Node.js can not be reused by a different
version of Node.js. Cache generated by different versions of Node.js will be stored
separately if the same base directory is used to persist the cache, so they can co-exist.

At the moment, when the compile cache is enabled and a module is loaded afresh, the
code cache is generated from the compiled code immediately, but will only be written
to disk when the Node.js instance is about to exit. This is subject to change. The
[`module.flushCompileCache()`][] method can be used to ensure the accumulated code cache
is flushed to disk in case the application wants to spawn other Node.js instances
and let them share the cache long before the parent exits.

### `module.constants.compileCacheStatus`

<!-- YAML
Expand Down Expand Up @@ -494,16 +498,30 @@
</tr>
</table>

### `module.enableCompileCache([cacheDir])`
### `module.enableCompileCache([options])`

<!-- YAML
added: v22.8.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/58797

Check warning on line 507 in doc/api/module.md

View workflow job for this annotation

GitHub Actions / lint-pr-url

pr-url doesn't match the URL of the current PR.
description: Add `portable` option to enable portable compile cache.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59931
description: Rename the unreleased `path` option to `directory` to maintain consistency.
-->

> Stability: 1.1 - Active Development

* `cacheDir` {string|undefined} Optional path to specify the directory where the compile cache
will be stored/retrieved.
* `options` {string|Object} Optional. If a string is passed, it is considered to be `options.directory`.
* `directory` {string} Optional. Directory to store the compile cache. If not specified,
the directory specified by the [`NODE_COMPILE_CACHE=dir`][] environment variable
will be used if it's set, or `path.join(os.tmpdir(), 'node-compile-cache')`
otherwise.
* `portable` {boolean} Optional. If `true`, enables portable compile cache so that
the cache can be reused even if the project directory is moved. This is a best-effort
feature. If not specified, it will depend on whether the environment variable
[`NODE_COMPILE_CACHE_PORTABLE=1`][] is set.
* Returns: {Object}
* `status` {integer} One of the [`module.constants.compileCacheStatus`][]
* `message` {string|undefined} If Node.js cannot enable the compile cache, this contains
Expand All @@ -515,20 +533,16 @@

Enable [module compile cache][] in the current Node.js instance.

If `cacheDir` is not specified, Node.js will either use the directory specified by the
[`NODE_COMPILE_CACHE=dir`][] environment variable if it's set, or use
`path.join(os.tmpdir(), 'node-compile-cache')` otherwise. For general use cases, it's
recommended to call `module.enableCompileCache()` without specifying the `cacheDir`,
so that the directory can be overridden by the `NODE_COMPILE_CACHE` environment
variable when necessary.

Since compile cache is supposed to be a quiet optimization that is not required for the
application to be functional, this method is designed to not throw any exception when the
compile cache cannot be enabled. Instead, it will return an object containing an error
message in the `message` field to aid debugging.
If compile cache is enabled successfully, the `directory` field in the returned object
contains the path to the directory where the compile cache is stored. The `status`
field in the returned object would be one of the `module.constants.compileCacheStatus`
For general use cases, it's recommended to call `module.enableCompileCache()` without
specifying the `options.directory`, so that the directory can be overridden by the
`NODE_COMPILE_CACHE` environment variable when necessary.

Since compile cache is supposed to be a optimization that is not mission critical, this
method is designed to not throw any exception when the compile cache cannot be enabled.
Instead, it will return an object containing an error message in the `message` field to
aid debugging. If compile cache is enabled successfully, the `directory` field in the
returned object contains the path to the directory where the compile cache is stored. The
`status` field in the returned object would be one of the `module.constants.compileCacheStatus`
values to indicate the result of the attempt to enable the [module compile cache][].

This method only affects the current Node.js instance. To enable it in child worker threads,
Expand Down Expand Up @@ -1817,7 +1831,7 @@
[`SourceMap`]: #class-modulesourcemap
[`initialize`]: #initialize
[`module.constants.compileCacheStatus`]: #moduleconstantscompilecachestatus
[`module.enableCompileCache()`]: #moduleenablecompilecachecachedir
[`module.enableCompileCache()`]: #moduleenablecompilecacheoptions
[`module.flushCompileCache()`]: #moduleflushcompilecache
[`module.getCompileCacheDir()`]: #modulegetcompilecachedir
[`module.setSourceMapsSupport()`]: #modulesetsourcemapssupportenabled-options
Expand Down
32 changes: 18 additions & 14 deletions lib/internal/modules/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -405,28 +405,32 @@ function stringify(body) {
* Enable on-disk compiled cache for all user modules being compiled in the current Node.js instance
* after this method is called.
* 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
* - A string: path to the cache directory.
* - An options object `{directory?: string, portable?: boolean}`:
* - `directory`: A string path to the cache directory.
* - `portable`: If `portable` is true, the cache directory will be considered relative.
* Defaults to `NODE_COMPILE_CACHE_PORTABLE === '1'`.
* If cache directory is undefined, it defaults to the `NODE_COMPILE_CACHE` environment variable.
* If `NODE_COMPILE_CACHE` isn't set, it defaults to `path.join(os.tmpdir(), 'node-compile-cache')`.
* @param {string | { directory?: string, portable?: boolean } | undefined} options
* @returns {{status: number, message?: string, directory?: string}}
*/
function enableCompileCache(options) {
let cacheDir;
let portable = false;
let portable;
let directory;

if (typeof options === 'object' && options !== null) {
({ path: cacheDir, portable = false } = options);
({ directory, portable } = options);
} else {
cacheDir = options;
directory = options;
}
if (cacheDir === undefined) {
cacheDir = join(lazyTmpdir(), 'node-compile-cache');
if (directory === undefined) {
directory = process.env.NODE_COMPILE_CACHE || join(lazyTmpdir(), 'node-compile-cache');
}
const nativeResult = _enableCompileCache(cacheDir, portable);
if (portable === undefined) {
portable = process.env.NODE_COMPILE_CACHE_PORTABLE === '1';
}
const nativeResult = _enableCompileCache(directory, portable);
const result = { status: nativeResult[0] };
if (nativeResult[1]) {
result.message = nativeResult[1];
Expand Down
23 changes: 23 additions & 0 deletions test/fixtures/compile-cache-wrapper-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use strict';

const { enableCompileCache, getCompileCacheDir, constants } = require('module');

console.log('dir before enableCompileCache:', getCompileCacheDir());
const options = JSON.parse(process.env.NODE_TEST_COMPILE_CACHE_OPTIONS);
console.log('options:', options);
const result = enableCompileCache(options);
switch (result.status) {
case constants.compileCacheStatus.FAILED:
console.log('Compile cache failed. ' + result.message);
break;
case constants.compileCacheStatus.ENABLED:
console.log('Compile cache enabled. ' + result.directory);
break;
case constants.compileCacheStatus.ALREADY_ENABLED:
console.log('Compile cache already enabled.');
break;
case constants.compileCacheStatus.DISABLED:
console.log('Compile cache already disabled.');
break;
}
console.log('dir after enableCompileCache:', getCompileCacheDir());
89 changes: 89 additions & 0 deletions test/parallel/test-compile-cache-api-options-portable-env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
'use strict';

// This tests module.enableCompileCache() with an options object still works with environment
// variable NODE_COMPILE_CACHE overrides.

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');

const wrapper = fixtures.path('compile-cache-wrapper-options.js');
tmpdir.refresh();

// Create a build directory and copy the entry point file.
const buildDir = tmpdir.resolve('build');
fs.mkdirSync(buildDir);
const entryPoint = path.join(buildDir, 'empty.js');
fs.copyFileSync(fixtures.path('empty.js'), entryPoint);

// Check that the portable option can be overridden by NODE_COMPILE_CACHE_PORTABLE.
// We don't override NODE_COMPILE_CACHE because it will enable the cache before
// the wrapper is loaded.
spawnSyncAndAssert(
process.execPath,
['-r', wrapper, entryPoint],
{
env: {
...process.env,
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
NODE_COMPILE_CACHE_PORTABLE: '1',
NODE_TEST_COMPILE_CACHE_OPTIONS: JSON.stringify({ directory: 'build/.compile_cache' }),
},
cwd: tmpdir.path
},
{
stdout(output) {
console.log(output); // Logging for debugging.
assert.match(output, /dir before enableCompileCache: undefined/);
assert.match(output, /Compile cache enabled/);
assert.match(output, /dir after enableCompileCache: .*build[/\\]\.compile_cache/);
return true;
},
stderr(output) {
console.log(output); // Logging for debugging.
assert.match(output, /reading cache from .*build[/\\]\.compile_cache.* for CommonJS .*empty\.js/);
assert.match(output, /empty\.js was not initialized, initializing the in-memory entry/);
assert.match(output, /writing cache for .*empty\.js.*success/);
return true;
}
});

assert(fs.existsSync(tmpdir.resolve('build/.compile_cache')));

const movedDir = buildDir + '_moved';
fs.renameSync(buildDir, movedDir);
const movedEntryPoint = path.join(movedDir, 'empty.js');

// When portable is undefined, it should use the env var NODE_COMPILE_CACHE_PORTABLE.
spawnSyncAndAssert(
process.execPath,
['-r', wrapper, movedEntryPoint],
{
env: {
...process.env,
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
NODE_COMPILE_CACHE_PORTABLE: '1',
NODE_TEST_COMPILE_CACHE_OPTIONS: JSON.stringify({ directory: 'build_moved/.compile_cache' }),
},
cwd: tmpdir.path
},
{
stdout(output) {
console.log(output); // Logging for debugging.
assert.match(output, /dir before enableCompileCache: undefined/);
assert.match(output, /Compile cache enabled/);
assert.match(output, /dir after enableCompileCache: .*build_moved[/\\]\.compile_cache/);
return true;
},
stderr(output) {
console.log(output); // Logging for debugging.
assert.match(output, /reading cache from .*build_moved[/\\]\.compile_cache.* for CommonJS .*empty\.js/);
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;
}
});
20 changes: 7 additions & 13 deletions test/parallel/test-compile-cache-api-portable.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

// This tests module.enableCompileCache({ path, portable: true }) works
// This tests module.enableCompileCache({ directory, portable: true }) works
// and supports portable paths across directory relocations.

require('../common');
Expand All @@ -9,26 +9,18 @@ const assert = require('assert');
const fs = require('fs');
const tmpdir = require('../common/tmpdir');
const path = require('path');
const fixtures = require('../common/fixtures');

tmpdir.refresh();
const workDir = path.join(tmpdir.path, 'work');
const cacheRel = '.compile_cache_dir';
fs.mkdirSync(workDir, { recursive: true });

const wrapper = path.join(workDir, 'wrapper.js');
const wrapper = fixtures.path('compile-cache-wrapper-options.js');
const target = path.join(workDir, 'target.js');

fs.writeFileSync(
wrapper,
`
const { enableCompileCache, getCompileCacheDir } = require('module');
console.log('dir before enableCompileCache:', getCompileCacheDir());
enableCompileCache({ path: '${cacheRel}', portable: true });
console.log('dir after enableCompileCache:', getCompileCacheDir());
`
);

fs.writeFileSync(target, '');
const NODE_TEST_COMPILE_CACHE_OPTIONS = JSON.stringify({ directory: cacheRel, portable: true });

// First run
{
Expand All @@ -39,6 +31,7 @@ fs.writeFileSync(target, '');
env: {
...process.env,
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
NODE_TEST_COMPILE_CACHE_OPTIONS,
},
cwd: workDir,
},
Expand Down Expand Up @@ -73,13 +66,14 @@ fs.writeFileSync(target, '');
process.execPath,
[
'-r',
path.join(movedWorkDir, 'wrapper.js'),
wrapper,
path.join(movedWorkDir, 'target.js'),
],
{
env: {
...process.env,
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
NODE_TEST_COMPILE_CACHE_OPTIONS,
},
cwd: movedWorkDir,
},
Expand Down
Loading