GeezerVison is showing github.com — module: add a read-only mode to the compile cache · nodejs/node@97a437e
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/cli.md +9Lines changed: 9 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -3708,6 +3708,15 @@ When set to 1, the [module compile cache][] can be reused across different direc | 3708 | 3708 | locations as long as the module layout relative to the cache directory remains the same, | 3709 | 3709 | and by any user (the cache subdirectory is not suffixed with the creating user's uid). | 3710 | 3710 |
| | 3711 | +### `NODE_COMPILE_CACHE_READONLY=1` | | 3712 | + | | 3713 | +<!-- YAML | | 3714 | +added: REPLACEME | | 3715 | +--> | | 3716 | + | | 3717 | +When set to 1, the [module compile cache][] only reads existing entries from | | 3718 | +its directory: nothing is written to it and it is not created if missing. | | 3719 | + | 3711 | 3720 | ### `NODE_DEBUG=module[,…]` | 3712 | 3721 |
| 3713 | 3722 | <!-- YAML | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/module.md +18Lines changed: 18 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -427,6 +427,15 @@ There are two ways to enable the portable mode: | 427 | 427 |
| 428 | 428 | 2. Setting the environment variable: [`NODE_COMPILE_CACHE_PORTABLE=1`][] | 429 | 429 |
| | 430 | +### Read-only compile cache | | 431 | +
| | 432 | +A cache that was generated ahead of time, for example at build time to be | | 433 | +shipped inside an application package, can be enabled with `readOnly: true` | | 434 | +(or [`NODE_COMPILE_CACHE_READONLY=1`][]). Node.js then loads whatever entries | | 435 | +the directory holds and never writes to it: modules without a usable entry are | | 436 | +compiled as usual but not persisted, [`module.flushCompileCache()`][] is a | | 437 | +no-op, and the directory is not created if it is missing. | | 438 | +
| 430 | 439 | ### Limitations of the compile cache | 431 | 440 |
| 432 | 441 | Currently when using the compile cache with [V8 JavaScript code coverage][], the | @@ -498,6 +507,9 @@ The following constants are returned as the `status` field in the object returne | 498 | 507 | <!-- YAML | 499 | 508 | added: v22.8.0 | 500 | 509 | changes: | | 510 | + - version: REPLACEME | | 511 | + pr-url: https://github.com/nodejs/node/pull/65302 | | 512 | + description: Add the `readOnly` option. | 501 | 513 | - version: v25.4.0 | 502 | 514 | pr-url: https://github.com/nodejs/node/pull/60971 | 503 | 515 | description: This feature is no longer experimental. | @@ -522,6 +534,11 @@ changes: | 522 | 534 | the cache can be reused even if the project directory is moved. This is a best-effort | 523 | 535 | feature. If not specified, it will depend on whether the environment variable | 524 | 536 | [`NODE_COMPILE_CACHE_PORTABLE=1`][] is set. | | 537 | + * `readOnly` {boolean} Optional. If `true`, existing cache entries in `directory` are | | 538 | + used but nothing is ever written to it, and the directory is not created when it does | | 539 | + not exist (enabling then fails). Meant for caches generated ahead of time and shipped | | 540 | + with an application. If not specified, it will depend on whether the environment | | 541 | + variable [`NODE_COMPILE_CACHE_READONLY=1`][] is set. | 525 | 542 | * Returns: {Object} | 526 | 543 | * `status` {integer} One of the [`module.constants.compileCacheStatus`][] | 527 | 544 | * `message` {string|undefined} If Node.js cannot enable the compile cache, this contains | @@ -2047,6 +2064,7 @@ returned object contains the following keys: | 2047 | 2064 | [`--require`]: cli.md#-r---require-module | 2048 | 2065 | [`NODE_COMPILE_CACHE=dir`]: cli.md#node_compile_cachedir | 2049 | 2066 | [`NODE_COMPILE_CACHE_PORTABLE=1`]: cli.md#node_compile_cache_portable1 | | 2067 | +[`NODE_COMPILE_CACHE_READONLY=1`]: cli.md#node_compile_cache_readonly1 | 2050 | 2068 | [`NODE_DISABLE_COMPILE_CACHE=1`]: cli.md#node_disable_compile_cache1 | 2051 | 2069 | [`NODE_V8_COVERAGE=dir`]: cli.md#node_v8_coveragedir | 2052 | 2070 | [`SourceMap`]: #class-modulesourcemap | |
Collapse file Copy file name to clipboardExpand all lines: doc/node.1 +4Lines changed: 4 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -1862,6 +1862,10 @@ When set to 1, the module compile cache can be reused across different directory | 1862 | 1862 | locations as long as the module layout relative to the cache directory remains the same, | 1863 | 1863 | and by any user (the cache subdirectory is not suffixed with the creating user's uid). | 1864 | 1864 | . | | 1865 | +.It Ev NODE_COMPILE_CACHE_READONLY Ar 1 | | 1866 | +When set to 1, the module compile cache only reads existing entries from | | 1867 | +its directory: nothing is written to it and it is not created if missing. | | 1868 | +. | 1865 | 1869 | .It Ev NODE_DEBUG Ar module[,…] | 1866 | 1870 | \fB','\fR-separated list of core modules that should print debug information. | 1867 | 1871 | . | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/helpers.js +11-4Lines changed: 11 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -466,21 +466,25 @@ function stringify(body) { | 466 | 466 | * after this method is called. | 467 | 467 | * This method accepts either: | 468 | 468 | * - A string: path to the cache directory. | 469 | | - * - An options object `{directory?: string, portable?: boolean}`: | | 469 | + * - An options object `{directory?: string, portable?: boolean, readOnly?: boolean}`: | 470 | 470 | * - `directory`: A string path to the cache directory. | 471 | 471 | * - `portable`: If `portable` is true, the cache directory will be considered relative. | 472 | 472 | * Defaults to `NODE_COMPILE_CACHE_PORTABLE === '1'`. | | 473 | + * - `readOnly`: If `readOnly` is true, existing cache entries are used but nothing is | | 474 | + * written, and the cache directory is not created. Defaults to | | 475 | + * `NODE_COMPILE_CACHE_READONLY === '1'`. | 473 | 476 | * If cache directory is undefined, it defaults to the `NODE_COMPILE_CACHE` environment variable. | 474 | 477 | * If `NODE_COMPILE_CACHE` isn't set, it defaults to `path.join(os.tmpdir(), 'node-compile-cache')`. | 475 | | - * @param {string | { directory?: string, portable?: boolean } | undefined} options | | 478 | + * @param {string | { directory?: string, portable?: boolean, readOnly?: boolean } | undefined} options | 476 | 479 | * @returns {{status: number, message?: string, directory?: string}} | 477 | 480 | */ | 478 | 481 | function enableCompileCache(options) { | 479 | 482 | let portable; | | 483 | + let readOnly; | 480 | 484 | let directory; | 481 | 485 |
| 482 | 486 | if (typeof options === 'object' && options !== null) { | 483 | | - ({ directory, portable } = options); | | 487 | + ({ directory, portable, readOnly } = options); | 484 | 488 | } else { | 485 | 489 | directory = options; | 486 | 490 | } | @@ -490,7 +494,10 @@ function enableCompileCache(options) { | 490 | 494 | if (portable === undefined) { | 491 | 495 | portable = process.env.NODE_COMPILE_CACHE_PORTABLE === '1'; | 492 | 496 | } | 493 | | - const nativeResult = _enableCompileCache(directory, portable); | | 497 | + if (readOnly === undefined) { | | 498 | + readOnly = process.env.NODE_COMPILE_CACHE_READONLY === '1'; | | 499 | + } | | 500 | + const nativeResult = _enableCompileCache(directory, portable, readOnly); | 494 | 501 | const result = { status: nativeResult[0] }; | 495 | 502 | if (nativeResult[1]) { | 496 | 503 | result.message = nativeResult[1]; | |
Collapse file Copy file name to clipboardExpand all lines: src/compile_cache.cc +52-18Lines changed: 52 additions & 18 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -257,7 +257,8 @@ CompileCacheEntry* CompileCacheHandler::GetOrInsert(Local<String> code, | 257 | 257 | // If the portable cache is enabled and it seems possible to compute the | 258 | 258 | // relative position from an absolute path, we use the relative position | 259 | 259 | // in the cache key. | 260 | | - if (portable_ == EnableOption::PORTABLE && IsAbsoluteFilePath(file_path)) { | | 260 | + if (HasOption(portable_, EnableOption::PORTABLE) && | | 261 | + IsAbsoluteFilePath(file_path)) { | 261 | 262 | // Normalize the path to ensure it is consistent. | 262 | 263 | std::string normalized_file_path = NormalizeFileURLOrPath(env, file_path); | 263 | 264 | if (normalized_file_path.empty()) { | @@ -332,6 +333,10 @@ void CompileCacheHandler::MaybeSaveImpl(CompileCacheEntry* entry, | 332 | 333 | Debug("keeping the in-memory entry\n"); | 333 | 334 | return; | 334 | 335 | } | | 336 | + if (read_only_) { | | 337 | + Debug("read-only, not serializing\n"); | | 338 | + return; | | 339 | + } | 335 | 340 | Debug("%s the in-memory entry\n", | 336 | 341 | entry->cache == nullptr ? "initializing" : "refreshing"); | 337 | 342 |
| @@ -356,6 +361,9 @@ void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry, | 356 | 361 |
| 357 | 362 | void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry, | 358 | 363 | std::string_view transpiled) { | | 364 | + if (read_only_) { | | 365 | + return; | | 366 | + } | 359 | 367 | CHECK(entry->type == CachedCodeType::kStrippedTypeScript); | 360 | 368 | Debug("[compile cache] saving transpilation cache for %s %s\n", | 361 | 369 | entry->type_name(), | @@ -388,6 +396,10 @@ void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry, | 388 | 396 | */ | 389 | 397 | void CompileCacheHandler::Persist() { | 390 | 398 | DCHECK(!compile_cache_dir_.empty()); | | 399 | + if (read_only_) { | | 400 | + Debug("[compile cache] read-only, skipping persistence\n"); | | 401 | + return; | | 402 | + } | 391 | 403 |
| 392 | 404 | // TODO(joyeecheung): do this using a separate event loop to utilize the | 393 | 405 | // libuv thread pool and do the file system operations concurrently. | @@ -554,10 +566,11 @@ CompileCacheEnableResult CompileCacheHandler::Enable(Environment* env, | 554 | 566 | cache_tag, | 555 | 567 | cache_dir_with_tag); | 556 | 568 |
| 557 | | - if (!env->permission()->is_granted( | 558 | | - env, | 559 | | - permission::PermissionScope::kFileSystemWrite, | 560 | | - cache_dir_with_tag)) [[unlikely]] { | | 569 | + const bool read_only = HasOption(option, EnableOption::READ_ONLY); | | 570 | + if (!read_only && !env->permission()->is_granted( | | 571 | + env, | | 572 | + permission::PermissionScope::kFileSystemWrite, | | 573 | + cache_dir_with_tag)) [[unlikely]] { | 561 | 574 | result.message = "Skipping compile cache because write permission for " + | 562 | 575 | cache_dir_with_tag + " is not granted"; | 563 | 576 | result.status = CompileCacheEnableStatus::FAILED; | @@ -574,25 +587,46 @@ CompileCacheEnableResult CompileCacheHandler::Enable(Environment* env, | 574 | 587 | return result; | 575 | 588 | } | 576 | 589 |
| 577 | | - fs::FSReqWrapSync req_wrap; | 578 | | - int err = fs::MKDirpSync( | 579 | | - nullptr, &(req_wrap.req), cache_dir_with_tag, 0777, nullptr); | 580 | | - if (is_debug_) { | 581 | | - Debug("[compile cache] creating cache directory %s...%s\n", | | 590 | + if (read_only) { | | 591 | + // A read-only cache is used as found and never created: without the | | 592 | + // directory there is nothing to read. | | 593 | + uv_fs_t stat_req; | | 594 | + int err = | | 595 | + uv_fs_stat(nullptr, &stat_req, cache_dir_with_tag.c_str(), nullptr); | | 596 | + // libuv normalizes st_mode across platforms; MSVC has no S_ISDIR macro. | | 597 | + bool is_dir = err == 0 && (stat_req.statbuf.st_mode & S_IFMT) == S_IFDIR; | | 598 | + uv_fs_req_cleanup(&stat_req); | | 599 | + Debug("[compile cache] read-only cache directory %s...%s\n", | 582 | 600 | cache_dir_with_tag, | 583 | | - err < 0 ? uv_strerror(err) : "success"); | 584 | | - } | 585 | | - if (err != 0 && err != UV_EEXIST) { | 586 | | - result.message = | 587 | | - "Cannot create cache directory: " + std::string(uv_strerror(err)); | 588 | | - result.status = CompileCacheEnableStatus::FAILED; | 589 | | - return result; | | 601 | + is_dir ? "found" : "not found"); | | 602 | + if (!is_dir) { | | 603 | + result.message = | | 604 | + "Cache directory does not exist (read-only): " + cache_dir_with_tag; | | 605 | + result.status = CompileCacheEnableStatus::FAILED; | | 606 | + return result; | | 607 | + } | | 608 | + } else { | | 609 | + fs::FSReqWrapSync req_wrap; | | 610 | + int err = fs::MKDirpSync( | | 611 | + nullptr, &(req_wrap.req), cache_dir_with_tag, 0777, nullptr); | | 612 | + if (is_debug_) { | | 613 | + Debug("[compile cache] creating cache directory %s...%s\n", | | 614 | + cache_dir_with_tag, | | 615 | + err < 0 ? uv_strerror(err) : "success"); | | 616 | + } | | 617 | + if (err != 0 && err != UV_EEXIST) { | | 618 | + result.message = | | 619 | + "Cannot create cache directory: " + std::string(uv_strerror(err)); | | 620 | + result.status = CompileCacheEnableStatus::FAILED; | | 621 | + return result; | | 622 | + } | 590 | 623 | } | 591 | 624 |
| 592 | 625 | result.cache_directory = absolute_cache_dir_base; | 593 | 626 | compile_cache_dir_ = cache_dir_with_tag; | 594 | 627 | portable_ = option; | 595 | | - if (option == EnableOption::PORTABLE) { | | 628 | + read_only_ = read_only; | | 629 | + if (HasOption(option, EnableOption::PORTABLE)) { | 596 | 630 | normalized_compile_cache_dir_ = | 597 | 631 | NormalizeFileURLOrPath(env, compile_cache_dir_); | 598 | 632 | } | |
Collapse file Copy file name to clipboardExpand all lines: src/compile_cache.h +18-1Lines changed: 18 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -60,7 +60,22 @@ struct CompileCacheEnableResult { | 60 | 60 | std::string message; // Set in case of failure. | 61 | 61 | }; | 62 | 62 |
| 63 | | -enum class EnableOption : uint8_t { DEFAULT, PORTABLE }; | | 63 | +enum class EnableOption : uint8_t { | | 64 | + DEFAULT = 0, | | 65 | + PORTABLE = 1 << 0, | | 66 | + // Only read existing cache entries: nothing is compiled into the in-memory | | 67 | + // store for persisting, nothing is written to disk, and the cache directory | | 68 | + // is not created if it does not exist. | | 69 | + READ_ONLY = 1 << 1, | | 70 | +}; | | 71 | + | | 72 | +inline constexpr EnableOption operator|(EnableOption a, EnableOption b) { | | 73 | + return static_cast<EnableOption>(static_cast<uint8_t>(a) | | | 74 | + static_cast<uint8_t>(b)); | | 75 | +} | | 76 | +inline constexpr bool HasOption(EnableOption value, EnableOption flag) { | | 77 | + return (static_cast<uint8_t>(value) & static_cast<uint8_t>(flag)) != 0; | | 78 | +} | 64 | 79 |
| 65 | 80 | class CompileCacheHandler { | 66 | 81 | public: | @@ -82,6 +97,7 @@ class CompileCacheHandler { | 82 | 97 | bool rejected); | 83 | 98 | void MaybeSave(CompileCacheEntry* entry, std::string_view transpiled); | 84 | 99 | std::string_view cache_dir() { return compile_cache_dir_; } | | 100 | + bool read_only() const { return read_only_; } | 85 | 101 |
| 86 | 102 | private: | 87 | 103 | void ReadCacheFile(CompileCacheEntry* entry); | @@ -107,6 +123,7 @@ class CompileCacheHandler { | 107 | 123 | std::string compile_cache_dir_; | 108 | 124 | std::string normalized_compile_cache_dir_; | 109 | 125 | EnableOption portable_ = EnableOption::DEFAULT; | | 126 | + bool read_only_ = false; | 110 | 127 | std::unordered_map<uint32_t, std::unique_ptr<CompileCacheEntry>> | 111 | 128 | compiler_cache_store_; | 112 | 129 | }; | |
Collapse file Copy file name to clipboardExpand all lines: src/env.cc +8-2Lines changed: 8 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -1195,8 +1195,14 @@ void Environment::InitializeCompileCache() { | 1195 | 1195 | DebugCategory::COMPILE_CACHE, | 1196 | 1196 | "[compile cache] using relative path\n"); | 1197 | 1197 | } | 1198 | | - EnableCompileCache(dir_from_env, | 1199 | | - portable ? EnableOption::PORTABLE : EnableOption::DEFAULT); | | 1198 | + std::string read_only_env; | | 1199 | + bool read_only = credentials::SafeGetenv( | | 1200 | + "NODE_COMPILE_CACHE_READONLY", &read_only_env, this) && | | 1201 | + read_only_env == "1"; | | 1202 | + EnableOption option = EnableOption::DEFAULT; | | 1203 | + if (portable) option = option | EnableOption::PORTABLE; | | 1204 | + if (read_only) option = option | EnableOption::READ_ONLY; | | 1205 | + EnableCompileCache(dir_from_env, option); | 1200 | 1206 | } | 1201 | 1207 |
| 1202 | 1208 | CompileCacheEnableResult Environment::EnableCompileCache( | |
Collapse file Copy file name to clipboardExpand all lines: src/node_modules.cc +4-1Lines changed: 4 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -524,7 +524,10 @@ void EnableCompileCache(const FunctionCallbackInfo<Value>& args) { | 524 | 524 |
| 525 | 525 | EnableOption option = EnableOption::DEFAULT; | 526 | 526 | if (args.Length() > 1 && args[1]->IsTrue()) { | 527 | | - option = EnableOption::PORTABLE; | | 527 | + option = option | EnableOption::PORTABLE; | | 528 | + } | | 529 | + if (args.Length() > 2 && args[2]->IsTrue()) { | | 530 | + option = option | EnableOption::READ_ONLY; | 528 | 531 | } | 529 | 532 |
| 530 | 533 | Utf8Value value(isolate, args[0]); | |
Collapse file Copy file name to clipboard +65Lines changed: 65 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | + | | 3 | +// This tests module.enableCompileCache({ directory, readOnly: true }): existing | | 4 | +// entries are read, nothing is written, and a missing directory is not created. | | 5 | + | | 6 | +const common = require('../common'); | | 7 | +const { spawnSyncAndAssert } = require('../common/child_process'); | | 8 | +const assert = require('assert'); | | 9 | +const fs = require('fs'); | | 10 | +const path = require('path'); | | 11 | +const tmpdir = require('../common/tmpdir'); | | 12 | +const fixtures = require('../common/fixtures'); | | 13 | + | | 14 | +tmpdir.refresh(); | | 15 | +const wrapper = fixtures.path('compile-cache-wrapper-options.js'); | | 16 | +const target = path.join(tmpdir.path, 'target.js'); | | 17 | +fs.writeFileSync(target, 'module.exports = 1;'); | | 18 | +const other = path.join(tmpdir.path, 'other.js'); | | 19 | +fs.writeFileSync(other, 'module.exports = 2;'); | | 20 | +const directory = path.join(tmpdir.path, 'cache'); | | 21 | +const list = () => fs.readdirSync(directory, { recursive: true }).sort(); | | 22 | +const run = (options, extraEnv, requires, check) => spawnSyncAndAssert( | | 23 | + process.execPath, | | 24 | + [...requires.flatMap((r) => ['-r', r]), target], | | 25 | + { | | 26 | + env: { | | 27 | + ...process.env, | | 28 | + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', | | 29 | + NODE_TEST_COMPILE_CACHE_OPTIONS: JSON.stringify(options), | | 30 | + ...extraEnv, | | 31 | + }, | | 32 | + }, | | 33 | + check); | | 34 | + | | 35 | +// Read-only against a directory that does not exist: enabling fails and | | 36 | +// nothing is created. | | 37 | +run({ directory, readOnly: true }, {}, [wrapper], { | | 38 | + stderr: /read-only cache directory .*\.\.\.not found/, | | 39 | +}); | | 40 | +assert(!fs.existsSync(directory)); | | 41 | + | | 42 | +// A normal run generates the cache for target.js. | | 43 | +run({ directory }, {}, [wrapper], { | | 44 | + stderr: /writing cache for .*target\.js.*success/, | | 45 | +}); | | 46 | +const generated = list(); | | 47 | +assert.notStrictEqual(generated.length, 0); | | 48 | + | | 49 | +// Read-only against it: target.js's entry is accepted, other.js is compiled | | 50 | +// but not written, and persistence is skipped. | | 51 | +run({ directory, readOnly: true }, {}, [wrapper, other], { | | 52 | + stderr: common.mustCall((output) => { | | 53 | + assert.match(output, /cache for .*target\.js was accepted/); | | 54 | + assert.match(output, /read-only, skipping persistence/); | | 55 | + assert.doesNotMatch(output, /writing cache for/); | | 56 | + return true; | | 57 | + }), | | 58 | +}); | | 59 | +assert.deepStrictEqual(list(), generated); | | 60 | + | | 61 | +// The environment variable form. | | 62 | +run({ directory }, { NODE_COMPILE_CACHE_READONLY: '1' }, [wrapper, other], { | | 63 | + stderr: /read-only, skipping persistence/, | | 64 | +}); | | 65 | +assert.deepStrictEqual(list(), generated); |
|