Skip to content

Commit b45062b

Browse files
committed
fixup! module: implement NODE_COMPILE_CACHE for automatic on-disk code caching
1 parent f7806b3 commit b45062b

File tree

6 files changed

+264
-23
lines changed

6 files changed

+264
-23
lines changed

src/compile_cache.cc

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "node_file.h"
55
#include "node_internals.h"
66
#include "node_version.h"
7+
#include "path.h"
78
#include "zlib.h"
89

910
namespace node {
@@ -27,7 +28,7 @@ uint32_t GetHash(const char* data, size_t size) {
2728
}
2829

2930
uint32_t GetCacheVersionTag() {
30-
std::string node_version(NODE_VERSION);
31+
std::string_view node_version(NODE_VERSION);
3132
uint32_t v8_tag = v8::ScriptCompiler::CachedDataVersionTag();
3233
uLong crc = crc32(0L, Z_NULL, 0);
3334
crc = crc32(crc, reinterpret_cast<const Bytef*>(&v8_tag), sizeof(uint32_t));
@@ -119,7 +120,7 @@ void CompileCacheHandler::ReadCacheFile(CompileCacheEntry* entry) {
119120
return;
120121
}
121122

122-
// Read the cache, grow the buffer exponentially whenever it ills up.
123+
// Read the cache, grow the buffer exponentially whenever it fills up.
123124
size_t offset = headers_buf.len;
124125
size_t capacity = 4096; // Initial buffer capacity
125126
size_t total_read = 0;
@@ -340,10 +341,33 @@ CompileCacheHandler::CompileCacheHandler(Environment* env)
340341
// - <cache_file_1>: a hash of filename + module type
341342
// - <cache_file_2>
342343
// - <cache_file_3>
343-
bool CompileCacheHandler::InitializeDirectory(const std::string& dir) {
344+
bool CompileCacheHandler::InitializeDirectory(Environment* env,
345+
const std::string& dir) {
344346
compiler_cache_key_ = GetCacheVersionTag();
345-
std::string cache_dir =
346-
dir + kPathSeparator + Uint32ToHex(compiler_cache_key_);
347+
std::string compiler_cache_key_string = Uint32ToHex(compiler_cache_key_);
348+
std::vector<std::string_view> paths = {dir, compiler_cache_key_string};
349+
std::string cache_dir = PathResolve(env, paths);
350+
351+
Debug("[compile cache] resolved path %s + %s -> %s\n",
352+
dir,
353+
compiler_cache_key_string,
354+
cache_dir);
355+
356+
if (UNLIKELY(!env->permission()->is_granted(
357+
permission::PermissionScope::kFileSystemWrite, cache_dir))) {
358+
Debug("[compile cache] skipping cache because write permission for %s "
359+
"is not granted\n",
360+
cache_dir);
361+
return false;
362+
}
363+
364+
if (UNLIKELY(!env->permission()->is_granted(
365+
permission::PermissionScope::kFileSystemRead, cache_dir))) {
366+
Debug("[compile cache] skipping cache because read permission for %s "
367+
"is not granted\n",
368+
cache_dir);
369+
return false;
370+
}
347371

348372
fs::FSReqWrapSync req_wrap;
349373
int err = fs::MKDirpSync(nullptr, &(req_wrap.req), cache_dir, 0777, nullptr);
@@ -356,22 +380,7 @@ bool CompileCacheHandler::InitializeDirectory(const std::string& dir) {
356380
return false;
357381
}
358382

359-
uv_fs_t req;
360-
auto clean = OnScopeLeave([&req]() { uv_fs_req_cleanup(&req); });
361-
err = uv_fs_realpath(nullptr, &req, cache_dir.data(), nullptr);
362-
if (is_debug_) {
363-
Debug("[compile cache] resolving real path %s...%s\n",
364-
cache_dir,
365-
err < 0 ? uv_strerror(err) : "success");
366-
}
367-
if (err != 0 && err != UV_ENOENT) {
368-
return false;
369-
}
370-
371-
compile_cache_dir_ = std::string(static_cast<char*>(req.ptr));
372-
Debug("[compile cache] resolved real path %s -> %s\n",
373-
cache_dir,
374-
compile_cache_dir_);
383+
compile_cache_dir_ = cache_dir;
375384
return true;
376385
}
377386

src/compile_cache.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ struct CompileCacheEntry {
3636
class CompileCacheHandler {
3737
public:
3838
explicit CompileCacheHandler(Environment* env);
39-
bool InitializeDirectory(const std::string& dir);
39+
bool InitializeDirectory(Environment* env, const std::string& dir);
4040

4141
void Persist();
4242

src/env.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1097,7 +1097,7 @@ void Environment::InitializeCompileCache() {
10971097
return;
10981098
}
10991099
auto handler = std::make_unique<CompileCacheHandler>(this);
1100-
if (handler->InitializeDirectory(dir_from_env)) {
1100+
if (handler->InitializeDirectory(this, dir_from_env)) {
11011101
compile_cache_handler_ = std::move(handler);
11021102
AtExit(
11031103
[](void* env) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use strict';
2+
3+
// This tests NODE_COMPILE_CACHE works.
4+
5+
require('../common');
6+
const { spawnSyncAndExit } = require('../common/child_process');
7+
const fixtures = require('../common/fixtures');
8+
const tmpdir = require('../common/tmpdir');
9+
const assert = require('assert');
10+
const fs = require('fs');
11+
const path = require('path');
12+
13+
{
14+
// Test that it throws if the script fails to parse, and no cache is created.
15+
tmpdir.refresh();
16+
const dir = tmpdir.resolve('.compile_cache_dir');
17+
18+
spawnSyncAndExit(
19+
process.execPath,
20+
[fixtures.path('syntax', 'bad_syntax.js')],
21+
{
22+
env: {
23+
...process.env,
24+
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
25+
NODE_COMPILE_CACHE: dir
26+
},
27+
cwd: tmpdir.path
28+
},
29+
{
30+
status: 1,
31+
stderr: /skip .*bad_syntax\.js because the cache was not initialized/,
32+
});
33+
34+
const cacheDir = fs.readdirSync(dir);
35+
assert.strictEqual(cacheDir.length, 1);
36+
const entries = fs.readdirSync(path.join(dir, cacheDir[0]));
37+
assert.strictEqual(entries.length, 0);
38+
39+
spawnSyncAndExit(
40+
process.execPath,
41+
[fixtures.path('syntax', 'bad_syntax.mjs')],
42+
{
43+
env: {
44+
...process.env,
45+
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
46+
NODE_COMPILE_CACHE: dir
47+
},
48+
cwd: tmpdir.path
49+
},
50+
{
51+
status: 1,
52+
stderr: /skip .*bad_syntax\.mjs because the cache was not initialized/,
53+
});
54+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
'use strict';
2+
3+
// This tests NODE_COMPILE_CACHE works in existing directory.
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+
const fs = require('fs');
11+
12+
function testAllowed(readDir, writeDir, envDir) {
13+
console.log(readDir, writeDir, envDir); // Logging for debugging.
14+
15+
tmpdir.refresh();
16+
const dummyDir = tmpdir.resolve('dummy');
17+
fs.mkdirSync(dummyDir);
18+
const script = tmpdir.resolve(dummyDir, 'empty.js');
19+
fs.copyFileSync(fixtures.path('empty.js'), script);
20+
// If the directory doesn't exist, permission will just be disallowed.
21+
fs.mkdirSync(tmpdir.resolve(envDir));
22+
23+
spawnSyncAndAssert(
24+
process.execPath,
25+
[
26+
'--experimental-permission',
27+
`--allow-fs-read=${dummyDir}`,
28+
`--allow-fs-read=${readDir}`,
29+
`--allow-fs-write=${writeDir}`,
30+
script,
31+
],
32+
{
33+
env: {
34+
...process.env,
35+
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
36+
NODE_COMPILE_CACHE: `${envDir}`
37+
},
38+
cwd: tmpdir.path
39+
},
40+
{
41+
stderr(output) {
42+
assert.match(output, /writing cache for .*empty\.js.*success/);
43+
return true;
44+
}
45+
});
46+
47+
spawnSyncAndAssert(
48+
process.execPath,
49+
[
50+
'--experimental-permission',
51+
`--allow-fs-read=${dummyDir}`,
52+
`--allow-fs-read=${readDir}`,
53+
`--allow-fs-write=${writeDir}`,
54+
script,
55+
],
56+
{
57+
env: {
58+
...process.env,
59+
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
60+
NODE_COMPILE_CACHE: `${envDir}`
61+
},
62+
cwd: tmpdir.path
63+
},
64+
{
65+
stderr(output) {
66+
assert.match(output, /cache for .*empty\.js was accepted/);
67+
return true;
68+
}
69+
});
70+
}
71+
72+
{
73+
testAllowed(tmpdir.resolve('.compile_cache'), tmpdir.resolve('.compile_cache'), '.compile_cache');
74+
testAllowed(tmpdir.resolve('.compile_cache'), tmpdir.resolve('.compile_cache'), tmpdir.resolve('.compile_cache'));
75+
testAllowed('*', '*', '.compile_cache');
76+
testAllowed('*', tmpdir.resolve('.compile_cache'), '.compile_cache');
77+
testAllowed(tmpdir.resolve('.compile_cache'), '*', '.compile_cache');
78+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
'use strict';
2+
3+
// This tests NODE_COMPILE_CACHE works in existing directory.
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+
const fs = require('fs');
11+
12+
function testDisallowed(dummyDir, cacheDirInPermission, cacheDirInEnv) {
13+
console.log(dummyDir, cacheDirInPermission, cacheDirInEnv); // Logging for debugging.
14+
15+
tmpdir.refresh();
16+
const script = tmpdir.resolve(dummyDir, 'empty.js');
17+
fs.mkdirSync(tmpdir.resolve(dummyDir));
18+
fs.copyFileSync(fixtures.path('empty.js'), script);
19+
// If the directory doesn't exist, permission will just be disallowed.
20+
if (cacheDirInPermission !== '*') {
21+
fs.mkdirSync(tmpdir.resolve(cacheDirInPermission));
22+
}
23+
24+
spawnSyncAndAssert(
25+
process.execPath,
26+
[
27+
'--experimental-permission',
28+
`--allow-fs-read=${dummyDir}`, // No read or write permission for cache dir.
29+
`--allow-fs-write=${dummyDir}`,
30+
script,
31+
],
32+
{
33+
env: {
34+
...process.env,
35+
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
36+
NODE_COMPILE_CACHE: `${cacheDirInEnv}`
37+
},
38+
cwd: tmpdir.path
39+
},
40+
{
41+
stderr(output) {
42+
assert.match(output, /skipping cache because write permission for .* is not granted/);
43+
return true;
44+
}
45+
});
46+
47+
spawnSyncAndAssert(
48+
process.execPath,
49+
[
50+
'--experimental-permission',
51+
`--allow-fs-read=${dummyDir}`,
52+
`--allow-fs-read=${cacheDirInPermission}`, // Read-only
53+
`--allow-fs-write=${dummyDir}`,
54+
script,
55+
],
56+
{
57+
env: {
58+
...process.env,
59+
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
60+
NODE_COMPILE_CACHE: `${cacheDirInEnv}`
61+
},
62+
cwd: tmpdir.path
63+
},
64+
{
65+
stderr(output) {
66+
assert.match(output, /skipping cache because write permission for .* is not granted/);
67+
return true;
68+
}
69+
});
70+
71+
spawnSyncAndAssert(
72+
process.execPath,
73+
[
74+
'--experimental-permission',
75+
`--allow-fs-read=${dummyDir}`,
76+
`--allow-fs-write=${cacheDirInPermission}`, // Write-only
77+
script,
78+
],
79+
{
80+
env: {
81+
...process.env,
82+
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
83+
NODE_COMPILE_CACHE: `${cacheDirInEnv}`
84+
},
85+
cwd: tmpdir.path
86+
},
87+
{
88+
stderr(output) {
89+
assert.match(output, /skipping cache because read permission for .* is not granted/);
90+
return true;
91+
}
92+
});
93+
}
94+
95+
{
96+
testDisallowed(tmpdir.resolve('dummy'), tmpdir.resolve('.compile_cache') + '/', '.compile_cache');
97+
testDisallowed(tmpdir.resolve('dummy'), tmpdir.resolve('.compile_cache/') + '/', tmpdir.resolve('.compile_cache'));
98+
testDisallowed(tmpdir.resolve('dummy'), '*', '.compile_cache');
99+
testDisallowed(tmpdir.resolve('dummy'), '*', tmpdir.resolve('.compile_cache'));
100+
}

0 commit comments

Comments
 (0)