Skip to content

Commit f54cb3b

Browse files
committed
module: implement flushCompileCache()
This implements an API for users to intentionally flush the accumulated compile cache instead of waiting until process shutdown. It may be useful for application that loads dependencies first and then either reload itself in other instances, or spawning other instances that load an overlapping set of its dependencies - in this case its useful to flush the cache early instead of waiting until the shutdown of itself. Currently flushing is triggered by either process shutdown or user requests. In the future we should simply start the writes right after module loading on a separate thread, and this method only blocks until all the pending writes (if any) on the other thread are finished. In that case, the off-thread writes should finish long before any attempt of flushing is made so the method would then only incur a negligible overhead from thread synchronization.
1 parent e607293 commit f54cb3b

File tree

7 files changed

+118
-0
lines changed

7 files changed

+118
-0
lines changed

doc/api/module.md

+22
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,28 @@ added:
11011101
`path` is the resolved path for the file for which a corresponding source map
11021102
should be fetched.
11031103
1104+
### `module.flushCompileCache([keepDeserializedCache])`
1105+
1106+
<!-- YAML
1107+
added:
1108+
- REPLACEME
1109+
-->
1110+
1111+
> Stability: 1.1 - Active Development
1112+
1113+
* `keepDeserializedCache` {boolean} Whether the cache read from disk and already deserialized to
1114+
compile the corresponding modules should be kept after flushing.
1115+
Defaults to `false`.
1116+
1117+
Flush the [module compile cache][] accumulated from loaded modules to disk.
1118+
1119+
In most cases, it's not necessary to set the `keepDeserializedCache` option. After a module
1120+
is compiled, there is another tier of module cache in Node.js, so keeping the code cache that
1121+
is already deserialized into a live module usually just increases memory usage for no
1122+
additional benefit. It's only useful if users intentionally purge the live cache e.g.
1123+
by deleting from `require.cache` while expecting most source code to still remain unchanged
1124+
and can be recompiled using the cache already read from disk.
1125+
11041126
### Class: `module.SourceMap`
11051127
11061128
<!-- YAML

lib/internal/modules/helpers.js

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const {
4040
enableCompileCache: _enableCompileCache,
4141
getCompileCacheDir: _getCompileCacheDir,
4242
compileCacheStatus: _compileCacheStatus,
43+
flushCompileCache,
4344
} = internalBinding('modules');
4445

4546
let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
@@ -485,6 +486,7 @@ module.exports = {
485486
assertBufferSource,
486487
constants,
487488
enableCompileCache,
489+
flushCompileCache,
488490
getBuiltinModule,
489491
getCjsConditions,
490492
getCompileCacheDir,

lib/module.js

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const { SourceMap } = require('internal/source_map/source_map');
77
const {
88
constants,
99
enableCompileCache,
10+
flushCompileCache,
1011
getCompileCacheDir,
1112
} = require('internal/modules/helpers');
1213

@@ -15,5 +16,7 @@ Module.register = register;
1516
Module.SourceMap = SourceMap;
1617
Module.constants = constants;
1718
Module.enableCompileCache = enableCompileCache;
19+
Module.flushCompileCache = flushCompileCache;
20+
1821
Module.getCompileCacheDir = getCompileCacheDir;
1922
module.exports = Module;

src/compile_cache.cc

+7
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,13 @@ void CompileCacheHandler::Persist(bool keep_deserialized_cache) {
305305

306306
// TODO(joyeecheung): do this using a separate event loop to utilize the
307307
// libuv thread pool and do the file system operations concurrently.
308+
// TODO(joyeecheung): Currently flushing is triggered by either process
309+
// shutdown or user requests. In the future we should simply start the
310+
// writes right after module loading on a separate thread, and this method
311+
// only blocks until all the pending writes (if any) on the other thread are
312+
// finished. In that case, the off-thread writes should finish long
313+
// before any attempt of flushing is made so the method would then only
314+
// incur a negligible overhead from thread synchronization.
308315
for (auto& pair : compiler_cache_store_) {
309316
auto* entry = pair.second.get();
310317
if (entry->cache == nullptr) {

src/node_modules.cc

+16
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,20 @@ void BindingData::GetPackageScopeConfig(
435435
.ToLocalChecked());
436436
}
437437

438+
void FlushCompileCache(const FunctionCallbackInfo<Value>& args) {
439+
Isolate* isolate = args.GetIsolate();
440+
Local<Context> context = isolate->GetCurrentContext();
441+
Environment* env = Environment::GetCurrent(context);
442+
443+
if (!args[0]->IsBoolean() && !args[0]->IsUndefined()) {
444+
THROW_ERR_INVALID_ARG_TYPE(env, "keepDeserializedCache should be a boolean");
445+
return;
446+
}
447+
Debug(env, DebugCategory::COMPILE_CACHE, "[compile cache] module.flushCompileCache() requested.\n");
448+
env->FlushCompileCache(args[0]->IsTrue());
449+
Debug(env, DebugCategory::COMPILE_CACHE, "[compile cache] module.flushCompileCache() finished.\n");
450+
}
451+
438452
void EnableCompileCache(const FunctionCallbackInfo<Value>& args) {
439453
Isolate* isolate = args.GetIsolate();
440454
Local<Context> context = isolate->GetCurrentContext();
@@ -480,6 +494,7 @@ void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
480494
SetMethod(isolate, target, "getPackageScopeConfig", GetPackageScopeConfig);
481495
SetMethod(isolate, target, "enableCompileCache", EnableCompileCache);
482496
SetMethod(isolate, target, "getCompileCacheDir", GetCompileCacheDir);
497+
SetMethod(isolate, target, "flushCompileCache", FlushCompileCache);
483498
}
484499

485500
void BindingData::CreatePerContextProperties(Local<Object> target,
@@ -512,6 +527,7 @@ void BindingData::RegisterExternalReferences(
512527
registry->Register(GetPackageScopeConfig);
513528
registry->Register(EnableCompileCache);
514529
registry->Register(GetCompileCacheDir);
530+
registry->Register(FlushCompileCache);
515531
}
516532

517533
} // namespace modules

test/fixtures/compile-cache-flush.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict';
2+
3+
const { flushCompileCache, getCompileCacheDir } = require('module');
4+
const { spawnSync } = require('child_process');
5+
const assert = require('assert');
6+
7+
if (process.argv[2] !== 'child') {
8+
// The test should be run with the compile cache already enabled and NODE_DEBUG_NATIVE=COMPILE_CACHE.
9+
assert(getCompileCacheDir());
10+
assert(process.env.NODE_DEBUG_NATIVE.includes('COMPILE_CACHE'));
11+
12+
flushCompileCache();
13+
14+
const child1 = spawnSync(process.execPath, [__filename, 'child']);
15+
console.log(child1.stderr.toString().trim().split('\n').map(line => `[child1]${line}`).join('\n'));
16+
17+
flushCompileCache();
18+
19+
const child2 = spawnSync(process.execPath, [__filename, 'child']);
20+
console.log(child2.stderr.toString().trim().split('\n').map(line => `[child2]${line}`).join('\n'));
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict';
2+
3+
// This tests module.flushCompileCache() works as expected.
4+
5+
require('../common');
6+
const { spawnSyncAndAssert } = require('../common/child_process');
7+
const assert = require('assert');
8+
const tmpdir = require('../common/tmpdir');
9+
const fixtures = require('../common/fixtures');
10+
11+
{
12+
// Test that it works with non-existent directory.
13+
tmpdir.refresh();
14+
const cacheDir = tmpdir.resolve('compile_cache');
15+
spawnSyncAndAssert(
16+
process.execPath,
17+
[fixtures.path('compile-cache-flush.js')],
18+
{
19+
env: {
20+
...process.env,
21+
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
22+
NODE_COMPILE_CACHE: cacheDir,
23+
},
24+
cwd: tmpdir.path
25+
},
26+
{
27+
stdout(output) {
28+
// This contains output from the nested spawnings of compile-cache-flush.js.
29+
assert.match(output, /child1.* cache for .*compile-cache-flush\.js was accepted, keeping the in-memory entry/);
30+
assert.match(output, /child2.* cache for .*compile-cache-flush\.js was accepted, keeping the in-memory entry/);
31+
return true;
32+
},
33+
stderr(output) {
34+
// This contains output from the top-level spawning of compile-cache-flush.js.
35+
assert.match(output, /reading cache from .*compile_cache.* for CommonJS .*compile-cache-flush\.js/);
36+
assert.match(output, /compile-cache-flush\.js was not initialized, initializing the in-memory entry/);
37+
38+
const writeRE = /writing cache for .*compile-cache-flush\.js.*success/;
39+
const flushRE = /module\.flushCompileCache\(\) finished/;
40+
assert.match(output, writeRE);
41+
assert.match(output, flushRE);
42+
// The cache writing should happen before flushing finishes i.e. it's not delayed until process shutdown.
43+
assert(output.match(writeRE).index < output.match(flushRE).index);
44+
return true;
45+
}
46+
});
47+
}

0 commit comments

Comments
 (0)