Skip to content

Commit 36e1de6

Browse files
joyeecheungRafaelGSS
authored andcommitted
src: add JS APIs for compile cache and NODE_DISABLE_COMPILE_CACHE
This patch adds the following API for tools to enable compile cache dynamically and query its status. - module.enableCompileCache(cacheDir) - module.getCompileCacheDir() In addition this adds a NODE_DISABLE_COMPILE_CACHE environment variable to disable the code cache enabled by the APIs as an escape hatch to avoid unexpected/undesired effects of the compile cache (e.g. less precise test coverage). When the module.enableCompileCache() method is invoked without a specified directory, Node.js will use the value of the NODE_COMPILE_CACHE environment variable if it's set, or defaults to `path.join(os.tmpdir(), 'node-compile-cache')` otherwise. Therefore it's recommended for tools to call this method without specifying the directory to allow overrides. PR-URL: #54501 Fixes: #53639 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent 543b5df commit 36e1de6

14 files changed

+665
-31
lines changed

doc/api/cli.md

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2868,25 +2868,8 @@ added: v22.1.0
28682868

28692869
> Stability: 1.1 - Active Development
28702870
2871-
When set, whenever Node.js compiles a CommonJS or a ECMAScript Module,
2872-
it will use on-disk [V8 code cache][] persisted in the specified directory
2873-
to speed up the compilation. This may slow down the first load of a
2874-
module graph, but subsequent loads of the same module graph may get
2875-
a significant speedup if the contents of the modules do not change.
2876-
2877-
To clean up the generated code cache, simply remove the directory.
2878-
It will be recreated the next time the same directory is used for
2879-
`NODE_COMPILE_CACHE`.
2880-
2881-
Compilation cache generated by one version of Node.js may not be used
2882-
by a different version of Node.js. Cache generated by different versions
2883-
of Node.js will be stored separately if the same directory is used
2884-
to persist the cache, so they can co-exist.
2885-
2886-
Caveat: currently when using this with [V8 JavaScript code coverage][], the
2887-
coverage being collected by V8 may be less precise in functions that are
2888-
deserialized from the code cache. It's recommended to turn this off when
2889-
running tests to generate precise coverage.
2871+
Enable the [module compile cache][] for the Node.js instance. See the documentation of
2872+
[module compile cache][] for details.
28902873

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

@@ -2908,6 +2891,17 @@ added: v0.3.0
29082891

29092892
When set, colors will not be used in the REPL.
29102893

2894+
### `NODE_DISABLE_COMPILE_CACHE=1`
2895+
2896+
<!-- YAML
2897+
added: REPLACEME
2898+
-->
2899+
2900+
> Stability: 1.1 - Active Development
2901+
2902+
Disable the [module compile cache][] for the Node.js instance. See the documentation of
2903+
[module compile cache][] for details.
2904+
29112905
### `NODE_EXTRA_CA_CERTS=file`
29122906

29132907
<!-- YAML
@@ -3559,7 +3553,6 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
35593553
[TypeScript type-stripping]: typescript.md#type-stripping
35603554
[V8 Inspector integration for Node.js]: debugger.md#v8-inspector-integration-for-nodejs
35613555
[V8 JavaScript code coverage]: https://v8project.blogspot.com/2017/12/javascript-code-coverage.html
3562-
[V8 code cache]: https://v8.dev/blog/code-caching-for-devs
35633556
[Web Crypto API]: webcrypto.md
35643557
[`"type"`]: packages.md#type
35653558
[`--allow-child-process`]: #--allow-child-process
@@ -3616,6 +3609,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
36163609
[filtering tests by name]: test.md#filtering-tests-by-name
36173610
[jitless]: https://v8.dev/blog/jitless
36183611
[libuv threadpool documentation]: https://docs.libuv.org/en/latest/threadpool.html
3612+
[module compile cache]: module.md#module-compile-cache
36193613
[remote code execution]: https://www.owasp.org/index.php/Code_Injection
36203614
[running tests from the command line]: test.md#running-tests-from-the-command-line
36213615
[scavenge garbage collector]: https://v8.dev/blog/orinoco-parallel-scavenger

doc/api/module.md

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,152 @@ const require = createRequire(import.meta.url);
6464
const siblingModule = require('./sibling-module');
6565
```
6666
67+
### `module.constants.compileCacheStatus`
68+
69+
<!-- YAML
70+
added: REPLACEME
71+
-->
72+
73+
> Stability: 1.1 - Active Development
74+
75+
The following constants are returned as the `status` field in the object returned by
76+
[`module.enableCompileCache()`][] to indicate the result of the attempt to enable the
77+
[module compile cache][].
78+
79+
<table>
80+
<tr>
81+
<th>Constant</th>
82+
<th>Description</th>
83+
</tr>
84+
<tr>
85+
<td><code>ENABLED</code></td>
86+
<td>
87+
Node.js has enabled the compile cache successfully. The directory used to store the
88+
compile cache will be returned in the <code>directory</code> field in the
89+
returned object.
90+
</td>
91+
</tr>
92+
<tr>
93+
<td><code>ALREADY_ENABLED</code></td>
94+
<td>
95+
The compile cache has already been enabled before, either by a previous call to
96+
<code>module.enableCompileCache()</code>, or by the <code>NODE_COMPILE_CACHE=dir</code>
97+
environment variable. The directory used to store the
98+
compile cache will be returned in the <code>directory</code> field in the
99+
returned object.
100+
</td>
101+
</tr>
102+
<tr>
103+
<td><code>FAILED</code></td>
104+
<td>
105+
Node.js fails to enable the compile cache. This can be caused by the lack of
106+
permission to use the specified directory, or various kinds of file system errors.
107+
The detail of the failure will be returned in the <code>message</code> field in the
108+
returned object.
109+
</td>
110+
</tr>
111+
<tr>
112+
<td><code>DISABLED</code></td>
113+
<td>
114+
Node.js cannot enable the compile cache because the environment variable
115+
<code>NODE_DISABLE_COMPILE_CACHE=1</code> has been set.
116+
</td>
117+
</tr>
118+
</table>
119+
120+
### `module.enableCompileCache([cacheDir])`
121+
122+
<!-- YAML
123+
added: REPLACEME
124+
-->
125+
126+
> Stability: 1.1 - Active Development
127+
128+
* `cacheDir` {string|undefined} Optional path to specify the directory where the compile cache
129+
will be stored/retrieved.
130+
* Returns: {Object}
131+
* `status` {integer} One of the [`module.constants.compileCacheStatus`][]
132+
* `message` {string|undefined} If Node.js cannot enable the compile cache, this contains
133+
the error message. Only set if `status` is `module.constants.compileCacheStatus.FAILED`.
134+
* `directory` {string|undefined} If the compile cache is enabled, this contains the directory
135+
where the compile cache is stored. Only set if `status` is
136+
`module.constants.compileCacheStatus.ENABLED` or
137+
`module.constants.compileCacheStatus.ALREADY_ENABLED`.
138+
139+
Enable [module compile cache][] in the current Node.js instance.
140+
141+
If `cacheDir` is not specified, Node.js will either use the directory specified by the
142+
[`NODE_COMPILE_CACHE=dir`][] environment variable if it's set, or use
143+
`path.join(os.tmpdir(), 'node-compile-cache')` otherwise. For general use cases, it's
144+
recommended to call `module.enableCompileCache()` without specifying the `cacheDir`,
145+
so that the directory can be overriden by the `NODE_COMPILE_CACHE` environment
146+
variable when necessary.
147+
148+
Since compile cache is supposed to be a quiet optimization that is not required for the
149+
application to be functional, this method is designed to not throw any exception when the
150+
compile cache cannot be enabled. Instead, it will return an object containing an error
151+
message in the `message` field to aid debugging.
152+
If compile cache is enabled successefully, the `directory` field in the returned object
153+
contains the path to the directory where the compile cache is stored. The `status`
154+
field in the returned object would be one of the `module.constants.compileCacheStatus`
155+
values to indicate the result of the attempt to enable the [module compile cache][].
156+
157+
This method only affects the current Node.js instance. To enable it in child worker threads,
158+
either call this method in child worker threads too, or set the
159+
`process.env.NODE_COMPILE_CACHE` value to compile cache directory so the behavior can
160+
be inheritend into the child workers. The directory can be obtained either from the
161+
`directory` field returned by this method, or with [`module.getCompileCacheDir()`][].
162+
163+
#### Module compile cache
164+
165+
<!-- YAML
166+
added: v22.1.0
167+
changes:
168+
- version: REPLACEME
169+
pr-url: https://github.com/nodejs/node/pull/54501
170+
description: add initial JavaScript APIs for runtime access.
171+
-->
172+
173+
The module compile cache can be enabled either using the [`module.enableCompileCache()`][]
174+
method or the [`NODE_COMPILE_CACHE=dir`][] environemnt variable. After it's enabled,
175+
whenever Node.js compiles a CommonJS or a ECMAScript Module, it will use on-disk
176+
[V8 code cache][] persisted in the specified directory to speed up the compilation.
177+
This may slow down the first load of a module graph, but subsequent loads of the same module
178+
graph may get a significant speedup if the contents of the modules do not change.
179+
180+
To clean up the generated compile cache on disk, simply remove the cache directory. The cache
181+
directory will be recreated the next time the same directory is used for for compile cache
182+
storage. To avoid filling up the disk with stale cache, it is recommended to use a directory
183+
under the [`os.tmpdir()`][]. If the compile cache is enabled by a call to
184+
[`module.enableCompileCache()`][] without specifying the directory, Node.js will use
185+
the [`NODE_DISABLE_COMPILE_CACHE=1`][] environment variable if it's set, or defaults
186+
to `path.join(os.tmpdir(), 'node-compile-cache')` otherwise. To locate the compile cache
187+
directory used by a running Node.js instance, use [`module.getCompileCacheDir()`][].
188+
189+
Currently when using the compile cache with [V8 JavaScript code coverage][], the
190+
coverage being collected by V8 may be less precise in functions that are
191+
deserialized from the code cache. It's recommended to turn this off when
192+
running tests to generate precise coverage.
193+
194+
The enabled module compile cache can be disabled by the [`NODE_DISABLE_COMPILE_CACHE=1`][]
195+
environment variable. This can be useful when the compile cache leads to unexpected or
196+
undesired behaviors (e.g. less precise test coverage).
197+
198+
Compilation cache generated by one version of Node.js can not be reused by a different
199+
version of Node.js. Cache generated by different versions of Node.js will be stored
200+
separately if the same base directory is used to persist the cache, so they can co-exist.
201+
202+
### `module.getCompileCacheDir()`
203+
204+
<!-- YAML
205+
added: REPLACEME
206+
-->
207+
208+
> Stability: 1.1 - Active Development
209+
210+
* Returns: {string|undefined} Path to the [module compile cache][] directory if it is enabled,
211+
or `undefined` otherwise.
212+
67213
### `module.isBuiltin(moduleName)`
68214
69215
<!-- YAML
@@ -1055,22 +1201,31 @@ returned object contains the following keys:
10551201
[Customization hooks]: #customization-hooks
10561202
[ES Modules]: esm.md
10571203
[Source map v3 format]: https://sourcemaps.info/spec.html#h.mofvlxcwqzej
1204+
[V8 JavaScript code coverage]: https://v8project.blogspot.com/2017/12/javascript-code-coverage.html
1205+
[V8 code cache]: https://v8.dev/blog/code-caching-for-devs
10581206
[`"exports"`]: packages.md#exports
10591207
[`--enable-source-maps`]: cli.md#--enable-source-maps
10601208
[`ArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
1209+
[`NODE_COMPILE_CACHE=dir`]: cli.md#node_compile_cachedir
1210+
[`NODE_DISABLE_COMPILE_CACHE=1`]: cli.md#node_disable_compile_cache1
10611211
[`NODE_V8_COVERAGE=dir`]: cli.md#node_v8_coveragedir
10621212
[`SharedArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer
10631213
[`SourceMap`]: #class-modulesourcemap
10641214
[`TypedArray`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
10651215
[`Uint8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
10661216
[`initialize`]: #initialize
1067-
[`module`]: modules.md#the-module-object
1217+
[`module.constants.compileCacheStatus`]: #moduleconstantscompilecachestatus
1218+
[`module.enableCompileCache()`]: #moduleenablecompilecachecachedir
1219+
[`module.getCompileCacheDir()`]: #modulegetcompilecachedir
1220+
[`module`]: #the-module-object
1221+
[`os.tmpdir()`]: os.md#ostmpdir
10681222
[`register`]: #moduleregisterspecifier-parenturl-options
10691223
[`string`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
10701224
[`util.TextDecoder`]: util.md#class-utiltextdecoder
10711225
[chain]: #chaining
10721226
[hooks]: #customization-hooks
10731227
[load hook]: #loadurl-context-nextload
1228+
[module compile cache]: #module-compile-cache
10741229
[module wrapper]: modules.md#the-module-wrapper
10751230
[prefix-only modules]: modules.md#built-in-modules-with-mandatory-node-prefix
10761231
[realm]: https://tc39.es/ecma262/#realm

lib/internal/modules/helpers.js

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const {
44
ArrayPrototypeForEach,
55
ArrayPrototypeIncludes,
66
ObjectDefineProperty,
7+
ObjectFreeze,
78
ObjectPrototypeHasOwnProperty,
89
SafeMap,
910
SafeSet,
@@ -27,10 +28,18 @@ const assert = require('internal/assert');
2728

2829
const { Buffer } = require('buffer');
2930
const { getOptionValue } = require('internal/options');
30-
const { setOwnProperty } = require('internal/util');
31+
const { setOwnProperty, getLazy } = require('internal/util');
3132
const { inspect } = require('internal/util/inspect');
3233

34+
const lazyTmpdir = getLazy(() => require('os').tmpdir());
35+
const { join } = path;
36+
3337
const { canParse: URLCanParse } = internalBinding('url');
38+
const {
39+
enableCompileCache: _enableCompileCache,
40+
getCompileCacheDir: _getCompileCacheDir,
41+
compileCacheStatus: _compileCacheStatus,
42+
} = internalBinding('modules');
3443

3544
let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
3645
debug = fn;
@@ -380,10 +389,53 @@ function isUnderNodeModules(filename) {
380389
return ArrayPrototypeIncludes(splitPath, 'node_modules');
381390
}
382391

392+
/**
393+
* Enable on-disk compiled cache for all user modules being complied in the current Node.js instance
394+
* after this method is called.
395+
* If cacheDir is undefined, defaults to the NODE_MODULE_CACHE environment variable.
396+
* If NODE_MODULE_CACHE isn't set, default to path.join(os.tmpdir(), 'node-compile-cache').
397+
* @param {string|undefined} cacheDir
398+
* @returns {{status: number, message?: string, directory?: string}}
399+
*/
400+
function enableCompileCache(cacheDir) {
401+
if (cacheDir === undefined) {
402+
cacheDir = join(lazyTmpdir(), 'node-compile-cache');
403+
}
404+
const nativeResult = _enableCompileCache(cacheDir);
405+
const result = { status: nativeResult[0] };
406+
if (nativeResult[1]) {
407+
result.message = nativeResult[1];
408+
}
409+
if (nativeResult[2]) {
410+
result.directory = nativeResult[2];
411+
}
412+
return result;
413+
}
414+
415+
const compileCacheStatus = { __proto__: null };
416+
for (let i = 0; i < _compileCacheStatus.length; ++i) {
417+
compileCacheStatus[_compileCacheStatus[i]] = i;
418+
}
419+
ObjectFreeze(compileCacheStatus);
420+
const constants = { __proto__: null, compileCacheStatus };
421+
ObjectFreeze(constants);
422+
423+
/**
424+
* Get the compile cache directory if on-disk compile cache is enabled.
425+
* @returns {string|undefined} Path to the module compile cache directory if it is enabled,
426+
* or undefined otherwise.
427+
*/
428+
function getCompileCacheDir() {
429+
return _getCompileCacheDir() || undefined;
430+
}
431+
383432
module.exports = {
384433
addBuiltinLibsToObject,
434+
constants,
435+
enableCompileCache,
385436
getBuiltinModule,
386437
getCjsConditions,
438+
getCompileCacheDir,
387439
initializeCjsConditions,
388440
isUnderNodeModules,
389441
loadBuiltinModule,

lib/module.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@ const { findSourceMap } = require('internal/source_map/source_map_cache');
44
const { Module } = require('internal/modules/cjs/loader');
55
const { register } = require('internal/modules/esm/loader');
66
const { SourceMap } = require('internal/source_map/source_map');
7+
const {
8+
constants,
9+
enableCompileCache,
10+
getCompileCacheDir,
11+
} = require('internal/modules/helpers');
712

813
Module.findSourceMap = findSourceMap;
914
Module.register = register;
1015
Module.SourceMap = SourceMap;
16+
Module.constants = constants;
17+
Module.enableCompileCache = enableCompileCache;
18+
Module.getCompileCacheDir = getCompileCacheDir;
1119
module.exports = Module;

src/compile_cache.cc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ CompileCacheEnableResult CompileCacheHandler::Enable(Environment* env,
381381
cache_dir_with_tag_str))) {
382382
result.message = "Skipping compile cache because write permission for " +
383383
cache_dir_with_tag_str + " is not granted";
384-
result.status = CompileCacheEnableStatus::kFailed;
384+
result.status = CompileCacheEnableStatus::FAILED;
385385
return result;
386386
}
387387

@@ -391,7 +391,7 @@ CompileCacheEnableResult CompileCacheHandler::Enable(Environment* env,
391391
cache_dir_with_tag_str))) {
392392
result.message = "Skipping compile cache because read permission for " +
393393
cache_dir_with_tag_str + " is not granted";
394-
result.status = CompileCacheEnableStatus::kFailed;
394+
result.status = CompileCacheEnableStatus::FAILED;
395395
return result;
396396
}
397397

@@ -406,14 +406,14 @@ CompileCacheEnableResult CompileCacheHandler::Enable(Environment* env,
406406
if (err != 0 && err != UV_EEXIST) {
407407
result.message =
408408
"Cannot create cache directory: " + std::string(uv_strerror(err));
409-
result.status = CompileCacheEnableStatus::kFailed;
409+
result.status = CompileCacheEnableStatus::FAILED;
410410
return result;
411411
}
412412

413413
compile_cache_dir_str_ = absolute_cache_dir_base;
414414
result.cache_directory = absolute_cache_dir_base;
415415
compile_cache_dir_ = cache_dir_with_tag;
416-
result.status = CompileCacheEnableStatus::kEnabled;
416+
result.status = CompileCacheEnableStatus::ENABLED;
417417
return result;
418418
}
419419

src/compile_cache.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ struct CompileCacheEntry {
3636
};
3737

3838
#define COMPILE_CACHE_STATUS(V) \
39-
V(kFailed) /* Failed to enable the cache */ \
40-
V(kEnabled) /* Was not enabled before, and now enabled. */ \
41-
V(kAlreadyEnabled) /* Was already enabled. */
39+
V(FAILED) /* Failed to enable the cache */ \
40+
V(ENABLED) /* Was not enabled before, and now enabled. */ \
41+
V(ALREADY_ENABLED) /* Was already enabled. */ \
42+
V(DISABLED) /* Has been disabled by NODE_DISABLE_COMPILE_CACHE. */
4243

4344
enum class CompileCacheEnableStatus : uint8_t {
4445
#define V(status) status,

0 commit comments

Comments
 (0)