Skip to content

Commit 58d2dad

Browse files
joyeecheungaduh95
authored andcommitted
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 ~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 <webadmin@geoffreybooth.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent a62345e commit 58d2dad

9 files changed

+846
-16
lines changed

lib/internal/modules/typescript.js

+59-4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ const {
2222
const { getOptionValue } = require('internal/options');
2323
const assert = require('internal/assert');
2424
const { Buffer } = require('buffer');
25+
const {
26+
getCompileCacheEntry,
27+
saveCompileCacheEntry,
28+
cachedCodeTypes: { kStrippedTypeScript, kTransformedTypeScript, kTransformedTypeScriptWithSourceMaps },
29+
} = internalBinding('modules');
2530

2631
/**
2732
* The TypeScript parsing mode, either 'strip-only' or 'transform'.
@@ -105,11 +110,19 @@ function stripTypeScriptTypes(code, options = kEmptyObject) {
105110
});
106111
}
107112

113+
/**
114+
* @typedef {'strip-only' | 'transform'} TypeScriptMode
115+
* @typedef {object} TypeScriptOptions
116+
* @property {TypeScriptMode} mode Mode.
117+
* @property {boolean} sourceMap Whether to generate source maps.
118+
* @property {string|undefined} filename Filename.
119+
*/
120+
108121
/**
109122
* Processes TypeScript code by stripping types or transforming.
110123
* Handles source maps if needed.
111124
* @param {string} code TypeScript code to process.
112-
* @param {object} options The configuration object.
125+
* @param {TypeScriptOptions} options The configuration object.
113126
* @returns {string} The processed code.
114127
*/
115128
function processTypeScriptCode(code, options) {
@@ -126,6 +139,20 @@ function processTypeScriptCode(code, options) {
126139
return transformedCode;
127140
}
128141

142+
/**
143+
* Get the type enum used for compile cache.
144+
* @param {TypeScriptMode} mode Mode of transpilation.
145+
* @param {boolean} sourceMap Whether source maps are enabled.
146+
* @returns {number}
147+
*/
148+
function getCachedCodeType(mode, sourceMap) {
149+
if (mode === 'transform') {
150+
if (sourceMap) { return kTransformedTypeScriptWithSourceMaps; }
151+
return kTransformedTypeScript;
152+
}
153+
return kStrippedTypeScript;
154+
}
155+
129156
/**
130157
* Performs type-stripping to TypeScript source code internally.
131158
* It is used by internal loaders.
@@ -142,12 +169,40 @@ function stripTypeScriptModuleTypes(source, filename, emitWarning = true) {
142169
if (isUnderNodeModules(filename)) {
143170
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
144171
}
172+
const sourceMap = getOptionValue('--enable-source-maps');
173+
174+
const mode = getTypeScriptParsingMode();
175+
176+
// Instead of caching the compile cache status, just go into C++ to fetch it,
177+
// as checking process.env equally involves calling into C++ anyway, and
178+
// the compile cache can be enabled dynamically.
179+
const type = getCachedCodeType(mode, sourceMap);
180+
// Get a compile cache entry into the native compile cache store,
181+
// keyed by the filename. If the cache can already be loaded on disk,
182+
// cached.transpiled contains the cached string. Otherwise we should do
183+
// the transpilation and save it in the native store later using
184+
// saveCompileCacheEntry().
185+
const cached = (filename ? getCompileCacheEntry(source, filename, type) : undefined);
186+
if (cached?.transpiled) { // TODO(joyeecheung): return Buffer here.
187+
return cached.transpiled;
188+
}
189+
145190
const options = {
146-
mode: getTypeScriptParsingMode(),
147-
sourceMap: getOptionValue('--enable-source-maps'),
191+
mode,
192+
sourceMap,
148193
filename,
149194
};
150-
return processTypeScriptCode(source, options);
195+
196+
const transpiled = processTypeScriptCode(source, options);
197+
if (cached) {
198+
// cached.external contains a pointer to the native cache entry.
199+
// The cached object would be unreachable once it's out of scope,
200+
// but the pointer inside cached.external would stay around for reuse until
201+
// environment shutdown or when the cache is manually flushed
202+
// to disk. Unwrap it in JS before passing into C++ since it's faster.
203+
saveCompileCacheEntry(cached.external, transpiled);
204+
}
205+
return transpiled;
151206
}
152207

153208
/**

src/compile_cache.cc

+56-9
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,27 @@ 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+
default:
93+
UNREACHABLE();
94+
}
95+
}
96+
8097
void CompileCacheHandler::ReadCacheFile(CompileCacheEntry* entry) {
8198
Debug("[compile cache] reading cache from %s for %s %s...",
8299
entry->cache_filename,
83-
entry->type == CachedCodeType::kCommonJS ? "CommonJS" : "ESM",
100+
entry->type_name(),
84101
entry->source_filename);
85102

86103
uv_fs_t req;
@@ -256,7 +273,8 @@ void CompileCacheHandler::MaybeSaveImpl(CompileCacheEntry* entry,
256273
v8::Local<T> func_or_mod,
257274
bool rejected) {
258275
DCHECK_NOT_NULL(entry);
259-
Debug("[compile cache] cache for %s was %s, ",
276+
Debug("[compile cache] V8 code cache for %s %s was %s, ",
277+
entry->type_name(),
260278
entry->source_filename,
261279
rejected ? "rejected"
262280
: (entry->cache == nullptr) ? "not initialized"
@@ -287,6 +305,25 @@ void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry,
287305
MaybeSaveImpl(entry, func, rejected);
288306
}
289307

308+
void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry,
309+
std::string_view transpiled) {
310+
CHECK(entry->type == CachedCodeType::kStrippedTypeScript ||
311+
entry->type == CachedCodeType::kTransformedTypeScript ||
312+
entry->type == CachedCodeType::kTransformedTypeScriptWithSourceMaps);
313+
Debug("[compile cache] saving transpilation cache for %s %s\n",
314+
entry->type_name(),
315+
entry->source_filename);
316+
317+
// TODO(joyeecheung): it's weird to copy it again here. Convert the v8::String
318+
// directly into buffer held by v8::ScriptCompiler::CachedData here.
319+
int cache_size = static_cast<int>(transpiled.size());
320+
uint8_t* data = new uint8_t[cache_size];
321+
memcpy(data, transpiled.data(), cache_size);
322+
entry->cache.reset(new v8::ScriptCompiler::CachedData(
323+
data, cache_size, v8::ScriptCompiler::CachedData::BufferOwned));
324+
entry->refreshed = true;
325+
}
326+
290327
/**
291328
* Persist the compile cache accumulated in memory to disk.
292329
*
@@ -316,18 +353,25 @@ void CompileCacheHandler::Persist() {
316353
// incur a negligible overhead from thread synchronization.
317354
for (auto& pair : compiler_cache_store_) {
318355
auto* entry = pair.second.get();
356+
const char* type_name = entry->type_name();
319357
if (entry->cache == nullptr) {
320-
Debug("[compile cache] skip %s because the cache was not initialized\n",
358+
Debug("[compile cache] skip persisting %s %s because the cache was not "
359+
"initialized\n",
360+
type_name,
321361
entry->source_filename);
322362
continue;
323363
}
324364
if (entry->refreshed == false) {
325-
Debug("[compile cache] skip %s because cache was the same\n",
326-
entry->source_filename);
365+
Debug(
366+
"[compile cache] skip persisting %s %s because cache was the same\n",
367+
type_name,
368+
entry->source_filename);
327369
continue;
328370
}
329371
if (entry->persisted == true) {
330-
Debug("[compile cache] skip %s because cache was already persisted\n",
372+
Debug("[compile cache] skip persisting %s %s because cache was already "
373+
"persisted\n",
374+
type_name,
331375
entry->source_filename);
332376
continue;
333377
}
@@ -363,17 +407,20 @@ void CompileCacheHandler::Persist() {
363407
auto cleanup_mkstemp =
364408
OnScopeLeave([&mkstemp_req]() { uv_fs_req_cleanup(&mkstemp_req); });
365409
std::string cache_filename_tmp = entry->cache_filename + ".XXXXXX";
366-
Debug("[compile cache] Creating temporary file for cache of %s...",
367-
entry->source_filename);
410+
Debug("[compile cache] Creating temporary file for cache of %s (%s)...",
411+
entry->source_filename,
412+
type_name);
368413
int err = uv_fs_mkstemp(
369414
nullptr, &mkstemp_req, cache_filename_tmp.c_str(), nullptr);
370415
if (err < 0) {
371416
Debug("failed. %s\n", uv_strerror(err));
372417
continue;
373418
}
374419
Debug(" -> %s\n", mkstemp_req.path);
375-
Debug("[compile cache] writing cache for %s to temporary file %s [%d %d %d "
420+
Debug("[compile cache] writing cache for %s %s to temporary file %s [%d "
421+
"%d %d "
376422
"%d %d]...",
423+
type_name,
377424
entry->source_filename,
378425
mkstemp_req.path,
379426
headers[kMagicNumberOffset],

src/compile_cache.h

+12-3
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:

0 commit comments

Comments
 (0)