Skip to content

Commit 63a5974

Browse files
committed
module: integrate TypeScript into compile cache
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 ~65%. 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).
1 parent 0e7ec5e commit 63a5974

9 files changed

+829
-16
lines changed

lib/internal/modules/typescript.js

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ const {
1818
const { getOptionValue } = require('internal/options');
1919
const assert = require('internal/assert');
2020
const { Buffer } = require('buffer');
21+
const {
22+
getCompileCacheEntry,
23+
saveCompileCacheEntry,
24+
cachedCodeTypes: { kStrippedTypeScript, kTransformedTypeScript, kTransformedTypeScriptWithSourceMaps },
25+
} = internalBinding('modules');
2126

2227
/**
2328
* The TypeScript parsing mode, either 'strip-only' or 'transform'.
@@ -87,11 +92,18 @@ function stripTypeScriptTypes(code, options = kEmptyObject) {
8792
});
8893
}
8994

95+
/**
96+
* @typedef {object} TypeScriptOptions
97+
* @property {'transform'|'strip-only'} mode Mode.
98+
* @property {boolean} sourceMap Whether to generate source maps.
99+
* @property {string|undefined} filename Filename.
100+
*/
101+
90102
/**
91103
* Processes TypeScript code by stripping types or transforming.
92104
* Handles source maps if needed.
93105
* @param {string} code TypeScript code to process.
94-
* @param {object} options The configuration object.
106+
* @param {TypeScriptOptions} options The configuration object.
95107
* @returns {string} The processed code.
96108
*/
97109
function processTypeScriptCode(code, options) {
@@ -108,6 +120,20 @@ function processTypeScriptCode(code, options) {
108120
return transformedCode;
109121
}
110122

123+
/**
124+
* Get the type enum used for compile cache.
125+
* @param {'strip-only'|'transform'} mode Mode of transpilation.
126+
* @param {boolean} sourceMap Whether source maps are enabled.
127+
* @returns {number}
128+
*/
129+
function getCachedCodeType(mode, sourceMap) {
130+
if (mode === 'transform') {
131+
if (sourceMap) { return kTransformedTypeScriptWithSourceMaps; }
132+
return kTransformedTypeScript;
133+
}
134+
return kStrippedTypeScript;
135+
}
136+
111137
/**
112138
* Performs type-stripping to TypeScript source code internally.
113139
* It is used by internal loaders.
@@ -124,12 +150,30 @@ function stripTypeScriptModuleTypes(source, filename, emitWarning = true) {
124150
if (isUnderNodeModules(filename)) {
125151
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
126152
}
153+
const sourceMap = getOptionValue('--enable-source-maps');
154+
155+
const mode = getTypeScriptParsingMode();
156+
157+
// Instead of caching the compile cache status, just go into C++ to fetch it,
158+
// as checking process.env equally involves calling into C++ anyway, and
159+
// the compile cache can be enabled dynamically.
160+
const type = getCachedCodeType(mode, sourceMap);
161+
const cached = (filename ? getCompileCacheEntry(source, filename, type) : undefined);
162+
if (cached?.transpiled) { // TODO(joyeecheung): return Buffer here.
163+
return cached.transpiled;
164+
}
165+
127166
const options = {
128-
mode: getTypeScriptParsingMode(),
129-
sourceMap: getOptionValue('--enable-source-maps'),
167+
mode,
168+
sourceMap,
130169
filename,
131170
};
132-
return processTypeScriptCode(source, options);
171+
172+
const transpiled = processTypeScriptCode(source, options);
173+
if (cached) {
174+
saveCompileCacheEntry(cached.external, transpiled);
175+
}
176+
return transpiled;
133177
}
134178

135179
/**

src/compile_cache.cc

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,25 @@ v8::ScriptCompiler::CachedData* CompileCacheEntry::CopyCache() const {
7777
// See comments in CompileCacheHandler::Persist().
7878
constexpr uint32_t kCacheMagicNumber = 0x8adfdbb2;
7979

80+
const char* CompileCacheEntry::type_name() const {
81+
switch (type) {
82+
case CachedCodeType::kCommonJS:
83+
return "CommonJS";
84+
case CachedCodeType::kESM:
85+
return "ESM";
86+
case CachedCodeType::kStrippedTypeScript:
87+
return "StrippedTypeScript";
88+
case CachedCodeType::kTransformedTypeScript:
89+
return "TransformedTypeScript";
90+
case CachedCodeType::kTransformedTypeScriptWithSourceMaps:
91+
return "TransformedTypeScriptWithSourceMaps";
92+
}
93+
}
94+
8095
void CompileCacheHandler::ReadCacheFile(CompileCacheEntry* entry) {
8196
Debug("[compile cache] reading cache from %s for %s %s...",
8297
entry->cache_filename,
83-
entry->type == CachedCodeType::kCommonJS ? "CommonJS" : "ESM",
98+
entry->type_name(),
8499
entry->source_filename);
85100

86101
uv_fs_t req;
@@ -256,7 +271,8 @@ void CompileCacheHandler::MaybeSaveImpl(CompileCacheEntry* entry,
256271
v8::Local<T> func_or_mod,
257272
bool rejected) {
258273
DCHECK_NOT_NULL(entry);
259-
Debug("[compile cache] cache for %s was %s, ",
274+
Debug("[compile cache] V8 code cache for %s %s was %s, ",
275+
entry->type_name(),
260276
entry->source_filename,
261277
rejected ? "rejected"
262278
: (entry->cache == nullptr) ? "not initialized"
@@ -287,6 +303,25 @@ void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry,
287303
MaybeSaveImpl(entry, func, rejected);
288304
}
289305

306+
void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry,
307+
std::string_view transpiled) {
308+
CHECK(entry->type == CachedCodeType::kStrippedTypeScript ||
309+
entry->type == CachedCodeType::kTransformedTypeScript ||
310+
entry->type == CachedCodeType::kTransformedTypeScriptWithSourceMaps);
311+
Debug("[compile cache] saving transpilation cache for %s %s\n",
312+
entry->type_name(),
313+
entry->source_filename);
314+
315+
// TODO(joyeecheung): it's weird to copy it again here. Convert the v8::String
316+
// directly into buffer held by v8::ScriptCompiler::CachedData here.
317+
int cache_size = static_cast<int>(transpiled.size());
318+
uint8_t* data = new uint8_t[cache_size];
319+
memcpy(data, transpiled.data(), cache_size);
320+
entry->cache.reset(new v8::ScriptCompiler::CachedData(
321+
data, cache_size, v8::ScriptCompiler::CachedData::BufferOwned));
322+
entry->refreshed = true;
323+
}
324+
290325
/**
291326
* Persist the compile cache accumulated in memory to disk.
292327
*
@@ -316,18 +351,25 @@ void CompileCacheHandler::Persist() {
316351
// incur a negligible overhead from thread synchronization.
317352
for (auto& pair : compiler_cache_store_) {
318353
auto* entry = pair.second.get();
354+
const char* type_name = entry->type_name();
319355
if (entry->cache == nullptr) {
320-
Debug("[compile cache] skip %s because the cache was not initialized\n",
356+
Debug("[compile cache] skip persisting %s %s because the cache was not "
357+
"initialized\n",
358+
type_name,
321359
entry->source_filename);
322360
continue;
323361
}
324362
if (entry->refreshed == false) {
325-
Debug("[compile cache] skip %s because cache was the same\n",
326-
entry->source_filename);
363+
Debug(
364+
"[compile cache] skip persisting %s %s because cache was the same\n",
365+
type_name,
366+
entry->source_filename);
327367
continue;
328368
}
329369
if (entry->persisted == true) {
330-
Debug("[compile cache] skip %s because cache was already persisted\n",
370+
Debug("[compile cache] skip persisting %s %s because cache was already "
371+
"persisted\n",
372+
type_name,
331373
entry->source_filename);
332374
continue;
333375
}
@@ -363,17 +405,20 @@ void CompileCacheHandler::Persist() {
363405
auto cleanup_mkstemp =
364406
OnScopeLeave([&mkstemp_req]() { uv_fs_req_cleanup(&mkstemp_req); });
365407
std::string cache_filename_tmp = entry->cache_filename + ".XXXXXX";
366-
Debug("[compile cache] Creating temporary file for cache of %s...",
367-
entry->source_filename);
408+
Debug("[compile cache] Creating temporary file for cache of %s (%s)...",
409+
entry->source_filename,
410+
type_name);
368411
int err = uv_fs_mkstemp(
369412
nullptr, &mkstemp_req, cache_filename_tmp.c_str(), nullptr);
370413
if (err < 0) {
371414
Debug("failed. %s\n", uv_strerror(err));
372415
continue;
373416
}
374417
Debug(" -> %s\n", mkstemp_req.path);
375-
Debug("[compile cache] writing cache for %s to temporary file %s [%d %d %d "
418+
Debug("[compile cache] writing cache for %s %s to temporary file %s [%d "
419+
"%d %d "
376420
"%d %d]...",
421+
type_name,
377422
entry->source_filename,
378423
mkstemp_req.path,
379424
headers[kMagicNumberOffset],

src/compile_cache.h

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,17 @@
1313
namespace node {
1414
class Environment;
1515

16-
// TODO(joyeecheung): move it into a CacheHandler class.
16+
#define CACHED_CODE_TYPES(V) \
17+
V(kCommonJS, 0) \
18+
V(kESM, 1) \
19+
V(kStrippedTypeScript, 2) \
20+
V(kTransformedTypeScript, 3) \
21+
V(kTransformedTypeScriptWithSourceMaps, 4)
22+
1723
enum class CachedCodeType : uint8_t {
18-
kCommonJS = 0,
19-
kESM,
24+
#define V(type, value) type = value,
25+
CACHED_CODE_TYPES(V)
26+
#undef V
2027
};
2128

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

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

7584
private:

src/node_modules.cc

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "node_modules.h"
22
#include <cstdio>
33
#include "base_object-inl.h"
4+
#include "compile_cache.h"
45
#include "node_errors.h"
56
#include "node_external_reference.h"
67
#include "node_url.h"
@@ -498,6 +499,74 @@ void GetCompileCacheDir(const FunctionCallbackInfo<Value>& args) {
498499
.ToLocalChecked());
499500
}
500501

502+
void GetCompileCacheEntry(const FunctionCallbackInfo<Value>& args) {
503+
Isolate* isolate = args.GetIsolate();
504+
CHECK(args[0]->IsString()); // TODO(joyeecheung): accept buffer.
505+
CHECK(args[1]->IsString());
506+
CHECK(args[2]->IsUint32());
507+
Local<Context> context = isolate->GetCurrentContext();
508+
Environment* env = Environment::GetCurrent(context);
509+
if (!env->use_compile_cache()) {
510+
return;
511+
}
512+
Local<String> source = args[0].As<String>();
513+
Local<String> filename = args[1].As<String>();
514+
CachedCodeType type =
515+
static_cast<CachedCodeType>(args[2].As<v8::Uint32>()->Value());
516+
auto* cache_entry =
517+
env->compile_cache_handler()->GetOrInsert(source, filename, type);
518+
if (cache_entry == nullptr) {
519+
return;
520+
}
521+
522+
v8::LocalVector<v8::Name> names(isolate,
523+
{FIXED_ONE_BYTE_STRING(isolate, "external")});
524+
v8::LocalVector<v8::Value> values(isolate,
525+
{v8::External::New(isolate, cache_entry)});
526+
if (cache_entry->cache != nullptr) {
527+
Debug(env,
528+
DebugCategory::COMPILE_CACHE,
529+
"[compile cache] retrieving transpile cache for %s %s...",
530+
cache_entry->type_name(),
531+
cache_entry->source_filename);
532+
533+
std::string_view cache(
534+
reinterpret_cast<const char*>(cache_entry->cache->data),
535+
cache_entry->cache->length);
536+
Local<Value> transpiled;
537+
// TODO(joyeecheung): convert with simdutf and into external strings
538+
if (!ToV8Value(context, cache).ToLocal(&transpiled)) {
539+
Debug(env, DebugCategory::COMPILE_CACHE, "failed\n");
540+
return;
541+
} else {
542+
Debug(env, DebugCategory::COMPILE_CACHE, "success\n");
543+
}
544+
names.push_back(FIXED_ONE_BYTE_STRING(isolate, "transpiled"));
545+
values.push_back(transpiled);
546+
} else {
547+
Debug(env,
548+
DebugCategory::COMPILE_CACHE,
549+
"[compile cache] no transpile cache for %s %s\n",
550+
cache_entry->type_name(),
551+
cache_entry->source_filename);
552+
}
553+
args.GetReturnValue().Set(Object::New(
554+
isolate, v8::Null(isolate), names.data(), values.data(), names.size()));
555+
}
556+
557+
void SaveCompileCacheEntry(const FunctionCallbackInfo<Value>& args) {
558+
Isolate* isolate = args.GetIsolate();
559+
Local<Context> context = isolate->GetCurrentContext();
560+
Environment* env = Environment::GetCurrent(context);
561+
DCHECK(env->use_compile_cache());
562+
CHECK(args[0]->IsExternal());
563+
CHECK(args[1]->IsString()); // TODO(joyeecheung): accept buffer.
564+
auto* cache_entry =
565+
static_cast<CompileCacheEntry*>(args[0].As<v8::External>()->Value());
566+
Utf8Value utf8(isolate, args[1].As<String>());
567+
env->compile_cache_handler()->MaybeSave(cache_entry, utf8.ToStringView());
568+
}
569+
501570
void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
502571
Local<ObjectTemplate> target) {
503572
Isolate* isolate = isolate_data->isolate();
@@ -514,6 +583,8 @@ void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
514583
SetMethod(isolate, target, "enableCompileCache", EnableCompileCache);
515584
SetMethod(isolate, target, "getCompileCacheDir", GetCompileCacheDir);
516585
SetMethod(isolate, target, "flushCompileCache", FlushCompileCache);
586+
SetMethod(isolate, target, "getCompileCacheEntry", GetCompileCacheEntry);
587+
SetMethod(isolate, target, "saveCompileCacheEntry", SaveCompileCacheEntry);
517588
}
518589

519590
void BindingData::CreatePerContextProperties(Local<Object> target,
@@ -530,12 +601,31 @@ void BindingData::CreatePerContextProperties(Local<Object> target,
530601
compile_cache_status_values.push_back( \
531602
FIXED_ONE_BYTE_STRING(isolate, #status));
532603
COMPILE_CACHE_STATUS(V)
604+
#undef V
533605

534606
USE(target->Set(context,
535607
FIXED_ONE_BYTE_STRING(isolate, "compileCacheStatus"),
536608
Array::New(isolate,
537609
compile_cache_status_values.data(),
538610
compile_cache_status_values.size())));
611+
612+
LocalVector<v8::Name> cached_code_type_keys(isolate);
613+
LocalVector<Value> cached_code_type_values(isolate);
614+
615+
#define V(type, value) \
616+
cached_code_type_keys.push_back(FIXED_ONE_BYTE_STRING(isolate, #type)); \
617+
cached_code_type_values.push_back(v8::Integer::New(isolate, value)); \
618+
DCHECK_EQ(value, cached_code_type_values.size() - 1);
619+
CACHED_CODE_TYPES(V)
620+
#undef V
621+
622+
USE(target->Set(context,
623+
FIXED_ONE_BYTE_STRING(isolate, "cachedCodeTypes"),
624+
Object::New(isolate,
625+
v8::Null(isolate),
626+
cached_code_type_keys.data(),
627+
cached_code_type_values.data(),
628+
cached_code_type_keys.size())));
539629
}
540630

541631
void BindingData::RegisterExternalReferences(
@@ -547,6 +637,8 @@ void BindingData::RegisterExternalReferences(
547637
registry->Register(EnableCompileCache);
548638
registry->Register(GetCompileCacheDir);
549639
registry->Register(FlushCompileCache);
640+
registry->Register(GetCompileCacheEntry);
641+
registry->Register(SaveCompileCacheEntry);
550642
}
551643

552644
} // namespace modules

0 commit comments

Comments
 (0)