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).
  • Loading branch information
joyeecheung committed Jan 16, 2025
1 parent 0e7ec5e commit 0e02024
Show file tree
Hide file tree
Showing 9 changed files with 829 additions and 16 deletions.
52 changes: 48 additions & 4 deletions lib/internal/modules/typescript.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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 @@ -87,11 +92,18 @@ function stripTypeScriptTypes(code, options = kEmptyObject) {
});
}

/**
* @typedef {object} TypeScriptOptions
* @property {'transform'|'strip-only'} 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 @@ -108,6 +120,20 @@ function processTypeScriptCode(code, options) {
return transformedCode;
}

/**
* Get the type enum used for compile cache.
* @param {'strip-only'|'transform'} 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 @@ -124,12 +150,30 @@ 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);
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) {
saveCompileCacheEntry(cached.external, transpiled);
}
return transpiled;
}

/**
Expand Down
63 changes: 54 additions & 9 deletions src/compile_cache.cc
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,25 @@ 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";
}
}

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 +271,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 +303,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 +351,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 +405,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
92 changes: 92 additions & 0 deletions src/node_modules.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "node_modules.h"
#include <cstdio>
#include "base_object-inl.h"
#include "compile_cache.h"
#include "node_errors.h"
#include "node_external_reference.h"
#include "node_url.h"
Expand Down Expand Up @@ -498,6 +499,74 @@ void GetCompileCacheDir(const FunctionCallbackInfo<Value>& args) {
.ToLocalChecked());
}

void GetCompileCacheEntry(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
CHECK(args[0]->IsString()); // TODO(joyeecheung): accept buffer.
CHECK(args[1]->IsString());
CHECK(args[2]->IsUint32());
Local<Context> context = isolate->GetCurrentContext();
Environment* env = Environment::GetCurrent(context);
if (!env->use_compile_cache()) {
return;
}
Local<String> source = args[0].As<String>();
Local<String> filename = args[1].As<String>();
CachedCodeType type =
static_cast<CachedCodeType>(args[2].As<v8::Uint32>()->Value());
auto* cache_entry =
env->compile_cache_handler()->GetOrInsert(source, filename, type);
if (cache_entry == nullptr) {
return;
}

v8::LocalVector<v8::Name> names(isolate,
{FIXED_ONE_BYTE_STRING(isolate, "external")});
v8::LocalVector<v8::Value> values(isolate,
{v8::External::New(isolate, cache_entry)});
if (cache_entry->cache != nullptr) {
Debug(env,
DebugCategory::COMPILE_CACHE,
"[compile cache] retrieving transpile cache for %s %s...",
cache_entry->type_name(),
cache_entry->source_filename);

std::string_view cache(
reinterpret_cast<const char*>(cache_entry->cache->data),
cache_entry->cache->length);
Local<Value> transpiled;
// TODO(joyeecheung): convert with simdutf and into external strings
if (!ToV8Value(context, cache).ToLocal(&transpiled)) {
Debug(env, DebugCategory::COMPILE_CACHE, "failed\n");
return;
} else {
Debug(env, DebugCategory::COMPILE_CACHE, "success\n");
}
names.push_back(FIXED_ONE_BYTE_STRING(isolate, "transpiled"));
values.push_back(transpiled);
} else {
Debug(env,
DebugCategory::COMPILE_CACHE,
"[compile cache] no transpile cache for %s %s\n",
cache_entry->type_name(),
cache_entry->source_filename);
}
args.GetReturnValue().Set(Object::New(
isolate, v8::Null(isolate), names.data(), values.data(), names.size()));
}

void SaveCompileCacheEntry(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Environment* env = Environment::GetCurrent(context);
DCHECK(env->use_compile_cache());
CHECK(args[0]->IsExternal());
CHECK(args[1]->IsString()); // TODO(joyeecheung): accept buffer.
auto* cache_entry =
static_cast<CompileCacheEntry*>(args[0].As<v8::External>()->Value());
Utf8Value utf8(isolate, args[1].As<String>());
env->compile_cache_handler()->MaybeSave(cache_entry, utf8.ToStringView());
}

void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();
Expand All @@ -514,6 +583,8 @@ void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
SetMethod(isolate, target, "enableCompileCache", EnableCompileCache);
SetMethod(isolate, target, "getCompileCacheDir", GetCompileCacheDir);
SetMethod(isolate, target, "flushCompileCache", FlushCompileCache);
SetMethod(isolate, target, "getCompileCacheEntry", GetCompileCacheEntry);
SetMethod(isolate, target, "saveCompileCacheEntry", SaveCompileCacheEntry);
}

void BindingData::CreatePerContextProperties(Local<Object> target,
Expand All @@ -530,12 +601,31 @@ void BindingData::CreatePerContextProperties(Local<Object> target,
compile_cache_status_values.push_back( \
FIXED_ONE_BYTE_STRING(isolate, #status));
COMPILE_CACHE_STATUS(V)
#undef V

USE(target->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "compileCacheStatus"),
Array::New(isolate,
compile_cache_status_values.data(),
compile_cache_status_values.size())));

LocalVector<v8::Name> cached_code_type_keys(isolate);
LocalVector<Value> cached_code_type_values(isolate);

#define V(type, value) \
cached_code_type_keys.push_back(FIXED_ONE_BYTE_STRING(isolate, #type)); \
cached_code_type_values.push_back(v8::Integer::New(isolate, value)); \
DCHECK_EQ(value, cached_code_type_values.size() - 1);
CACHED_CODE_TYPES(V)
#undef V

USE(target->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "cachedCodeTypes"),
Object::New(isolate,
v8::Null(isolate),
cached_code_type_keys.data(),
cached_code_type_values.data(),
cached_code_type_keys.size())));
}

void BindingData::RegisterExternalReferences(
Expand All @@ -547,6 +637,8 @@ void BindingData::RegisterExternalReferences(
registry->Register(EnableCompileCache);
registry->Register(GetCompileCacheDir);
registry->Register(FlushCompileCache);
registry->Register(GetCompileCacheEntry);
registry->Register(SaveCompileCacheEntry);
}

} // namespace modules
Expand Down
Loading

0 comments on commit 0e02024

Please sign in to comment.