Skip to content

Commit

Permalink
module: integrate TypeScript into compile cache
Browse files Browse the repository at this point in the history
This integrates TypeScript into the compile cache by caching
the transpilation (either type-stripping or transforming) output
in addition to the V8 code cache that's generated from the
transpilation output.

Locally this speeds up loading with type stripping of
`benchmark/fixtures/strip-types-benchmark.ts` by ~65% and
loading with type transforms of
`fixtures/transform-types-benchmark.ts` by ~128%.

When comparing loading .ts and loading pre-transpiled .js on-disk
with the compile cache enabled, previously .ts loaded 46% slower
with type-stripping and 66% slower with transforms compared to
loading .js files directly.
After this patch, .ts loads 12% slower with type-stripping and
22% slower with transforms compared to .js.

(Note that the numbers are based on microbenchmark fixtures and
do not necessarily represent real-world workloads, though with
bigger real-world files, the speed up should be more significant).

PR-URL: #56629
Fixes: #54741
Reviewed-By: Geoffrey Booth <[email protected]>
Reviewed-By: Marco Ippolito <[email protected]>
Reviewed-By: James M Snell <[email protected]>
  • Loading branch information
joyeecheung authored Jan 25, 2025
1 parent f07300c commit 4a5d2c7
Show file tree
Hide file tree
Showing 9 changed files with 846 additions and 16 deletions.
63 changes: 59 additions & 4 deletions lib/internal/modules/typescript.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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 @@ -105,11 +110,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 @@ -126,6 +139,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 @@ -142,12 +169,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

0 comments on commit 4a5d2c7

Please sign in to comment.