Skip to content

module: integrate TypeScript into compile cache #56629

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 25, 2025
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
63 changes: 59 additions & 4 deletions lib/internal/modules/typescript.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ const {
const { getOptionValue } = require('internal/options');
const assert = require('internal/assert');
const { Buffer } = require('buffer');
const {
getCompileCacheEntry,
saveCompileCacheEntry,
cachedCodeTypes: { kStrippedTypeScript, kTransformedTypeScript, kTransformedTypeScriptWithSourceMaps },
} = internalBinding('modules');

/**
* The TypeScript parsing mode, either 'strip-only' or 'transform'.
Expand Down Expand Up @@ -102,11 +107,19 @@ function stripTypeScriptTypes(code, options = kEmptyObject) {
});
}

/**
* @typedef {'strip-only' | 'transform'} TypeScriptMode
* @typedef {object} TypeScriptOptions
* @property {TypeScriptMode} mode Mode.
* @property {boolean} sourceMap Whether to generate source maps.
* @property {string|undefined} filename Filename.
*/

/**
* Processes TypeScript code by stripping types or transforming.
* Handles source maps if needed.
* @param {string} code TypeScript code to process.
* @param {object} options The configuration object.
* @param {TypeScriptOptions} options The configuration object.
* @returns {string} The processed code.
*/
function processTypeScriptCode(code, options) {
Expand All @@ -123,6 +136,20 @@ function processTypeScriptCode(code, options) {
return transformedCode;
}

/**
* Get the type enum used for compile cache.
* @param {TypeScriptMode} mode Mode of transpilation.
* @param {boolean} sourceMap Whether source maps are enabled.
* @returns {number}
*/
function getCachedCodeType(mode, sourceMap) {
if (mode === 'transform') {
if (sourceMap) { return kTransformedTypeScriptWithSourceMaps; }
return kTransformedTypeScript;
}
return kStrippedTypeScript;
}

/**
* Performs type-stripping to TypeScript source code internally.
* It is used by internal loaders.
Expand All @@ -139,12 +166,40 @@ function stripTypeScriptModuleTypes(source, filename, emitWarning = true) {
if (isUnderNodeModules(filename)) {
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
}
const sourceMap = getOptionValue('--enable-source-maps');

const mode = getTypeScriptParsingMode();

// Instead of caching the compile cache status, just go into C++ to fetch it,
// as checking process.env equally involves calling into C++ anyway, and
// the compile cache can be enabled dynamically.
const type = getCachedCodeType(mode, sourceMap);
// Get a compile cache entry into the native compile cache store,
// keyed by the filename. If the cache can already be loaded on disk,
// cached.transpiled contains the cached string. Otherwise we should do
// the transpilation and save it in the native store later using
// saveCompileCacheEntry().
const cached = (filename ? getCompileCacheEntry(source, filename, type) : undefined);
if (cached?.transpiled) { // TODO(joyeecheung): return Buffer here.
return cached.transpiled;
}

const options = {
mode: getTypeScriptParsingMode(),
sourceMap: getOptionValue('--enable-source-maps'),
mode,
sourceMap,
filename,
};
return processTypeScriptCode(source, options);

const transpiled = processTypeScriptCode(source, options);
if (cached) {
// cached.external contains a pointer to the native cache entry.
// The cached object would be unreachable once it's out of scope,
// but the pointer inside cached.external would stay around for reuse until
// environment shutdown or when the cache is manually flushed
// to disk. Unwrap it in JS before passing into C++ since it's faster.
saveCompileCacheEntry(cached.external, transpiled);
}
return transpiled;
}

/**
Expand Down
65 changes: 56 additions & 9 deletions src/compile_cache.cc
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,27 @@ v8::ScriptCompiler::CachedData* CompileCacheEntry::CopyCache() const {
// See comments in CompileCacheHandler::Persist().
constexpr uint32_t kCacheMagicNumber = 0x8adfdbb2;

const char* CompileCacheEntry::type_name() const {
switch (type) {
case CachedCodeType::kCommonJS:
return "CommonJS";
case CachedCodeType::kESM:
return "ESM";
case CachedCodeType::kStrippedTypeScript:
return "StrippedTypeScript";
case CachedCodeType::kTransformedTypeScript:
return "TransformedTypeScript";
case CachedCodeType::kTransformedTypeScriptWithSourceMaps:
return "TransformedTypeScriptWithSourceMaps";
default:
UNREACHABLE();
}
}

void CompileCacheHandler::ReadCacheFile(CompileCacheEntry* entry) {
Debug("[compile cache] reading cache from %s for %s %s...",
entry->cache_filename,
entry->type == CachedCodeType::kCommonJS ? "CommonJS" : "ESM",
entry->type_name(),
entry->source_filename);

uv_fs_t req;
Expand Down Expand Up @@ -256,7 +273,8 @@ void CompileCacheHandler::MaybeSaveImpl(CompileCacheEntry* entry,
v8::Local<T> func_or_mod,
bool rejected) {
DCHECK_NOT_NULL(entry);
Debug("[compile cache] cache for %s was %s, ",
Debug("[compile cache] V8 code cache for %s %s was %s, ",
entry->type_name(),
entry->source_filename,
rejected ? "rejected"
: (entry->cache == nullptr) ? "not initialized"
Expand Down Expand Up @@ -287,6 +305,25 @@ void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry,
MaybeSaveImpl(entry, func, rejected);
}

void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry,
std::string_view transpiled) {
CHECK(entry->type == CachedCodeType::kStrippedTypeScript ||
entry->type == CachedCodeType::kTransformedTypeScript ||
entry->type == CachedCodeType::kTransformedTypeScriptWithSourceMaps);
Debug("[compile cache] saving transpilation cache for %s %s\n",
entry->type_name(),
entry->source_filename);

// TODO(joyeecheung): it's weird to copy it again here. Convert the v8::String
// directly into buffer held by v8::ScriptCompiler::CachedData here.
int cache_size = static_cast<int>(transpiled.size());
uint8_t* data = new uint8_t[cache_size];
memcpy(data, transpiled.data(), cache_size);
entry->cache.reset(new v8::ScriptCompiler::CachedData(
data, cache_size, v8::ScriptCompiler::CachedData::BufferOwned));
entry->refreshed = true;
}

/**
* Persist the compile cache accumulated in memory to disk.
*
Expand Down Expand Up @@ -316,18 +353,25 @@ void CompileCacheHandler::Persist() {
// incur a negligible overhead from thread synchronization.
for (auto& pair : compiler_cache_store_) {
auto* entry = pair.second.get();
const char* type_name = entry->type_name();
if (entry->cache == nullptr) {
Debug("[compile cache] skip %s because the cache was not initialized\n",
Debug("[compile cache] skip persisting %s %s because the cache was not "
"initialized\n",
type_name,
entry->source_filename);
continue;
}
if (entry->refreshed == false) {
Debug("[compile cache] skip %s because cache was the same\n",
entry->source_filename);
Debug(
"[compile cache] skip persisting %s %s because cache was the same\n",
type_name,
entry->source_filename);
continue;
}
if (entry->persisted == true) {
Debug("[compile cache] skip %s because cache was already persisted\n",
Debug("[compile cache] skip persisting %s %s because cache was already "
"persisted\n",
type_name,
entry->source_filename);
continue;
}
Expand Down Expand Up @@ -363,17 +407,20 @@ void CompileCacheHandler::Persist() {
auto cleanup_mkstemp =
OnScopeLeave([&mkstemp_req]() { uv_fs_req_cleanup(&mkstemp_req); });
std::string cache_filename_tmp = entry->cache_filename + ".XXXXXX";
Debug("[compile cache] Creating temporary file for cache of %s...",
entry->source_filename);
Debug("[compile cache] Creating temporary file for cache of %s (%s)...",
entry->source_filename,
type_name);
int err = uv_fs_mkstemp(
nullptr, &mkstemp_req, cache_filename_tmp.c_str(), nullptr);
if (err < 0) {
Debug("failed. %s\n", uv_strerror(err));
continue;
}
Debug(" -> %s\n", mkstemp_req.path);
Debug("[compile cache] writing cache for %s to temporary file %s [%d %d %d "
Debug("[compile cache] writing cache for %s %s to temporary file %s [%d "
"%d %d "
"%d %d]...",
type_name,
entry->source_filename,
mkstemp_req.path,
headers[kMagicNumberOffset],
Expand Down
15 changes: 12 additions & 3 deletions src/compile_cache.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,17 @@
namespace node {
class Environment;

// TODO(joyeecheung): move it into a CacheHandler class.
#define CACHED_CODE_TYPES(V) \
V(kCommonJS, 0) \
V(kESM, 1) \
V(kStrippedTypeScript, 2) \
V(kTransformedTypeScript, 3) \
V(kTransformedTypeScriptWithSourceMaps, 4)

enum class CachedCodeType : uint8_t {
kCommonJS = 0,
kESM,
#define V(type, value) type = value,
CACHED_CODE_TYPES(V)
#undef V
};

struct CompileCacheEntry {
Expand All @@ -34,6 +41,7 @@ struct CompileCacheEntry {
// Copy the cache into a new store for V8 to consume. Caller takes
// ownership.
v8::ScriptCompiler::CachedData* CopyCache() const;
const char* type_name() const;
};

#define COMPILE_CACHE_STATUS(V) \
Expand Down Expand Up @@ -70,6 +78,7 @@ class CompileCacheHandler {
void MaybeSave(CompileCacheEntry* entry,
v8::Local<v8::Module> mod,
bool rejected);
void MaybeSave(CompileCacheEntry* entry, std::string_view transpiled);
std::string_view cache_dir() { return compile_cache_dir_; }

private:
Expand Down
Loading
Loading