Skip to content

Commit

Permalink
[node] Add caching for TS compile per file (#8987)
Browse files Browse the repository at this point in the history
As a follow up to #8986, we can reduce the TS compile time even more by caching TS compile per file so we never compile the same file twice.  This brings down the total deployment time of a large app with many `api/*.ts` from 7 min to 5 min.

Note: review this PR with [whitespace disabled](https://github.com/vercel/vercel/pull/8987/files?w=1).

- Related to vercel/customer-issues#925
  • Loading branch information
styfle committed Dec 1, 2022
1 parent a036b03 commit d649a3c
Showing 1 changed file with 126 additions and 106 deletions.
232 changes: 126 additions & 106 deletions packages/node/src/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ function cachedLookup<T>(fn: (arg: string) => T): (arg: string) => T {
/**
* Maps the config path to a build func
*/
const configFileToBuildMap = new Map<string, Build>();
const configFileToBuildMap = new Map<string, GetOutputFunction>();

/**
* Register TypeScript compiler.
Expand Down Expand Up @@ -189,16 +189,30 @@ export function register(opts: Options = {}): Register {
}
}

function getBuild(configFileName = ''): Build {
let build = configFileToBuildMap.get(configFileName);
if (build) return build;
function getBuild(
configFileName = '',
skipTypeCheck?: boolean
): GetOutputFunction {
const cachedGetOutput = configFileToBuildMap.get(configFileName);

if (cachedGetOutput) {
return cachedGetOutput;
}

const outFiles = new Map<string, SourceOutput>();
const config = readConfig(configFileName);

/**
* Create the basic required function using transpile mode.
* Create the basic function for transpile only (ts-node --transpileOnly)
*/
const getOutput = function (code: string, fileName: string): SourceOutput {
const getOutputTranspile: GetOutputFunction = (
code: string,
fileName: string
) => {
const outFile = outFiles.get(fileName);
if (outFile) {
return outFile;
}
const result = ts.transpileModule(code, {
fileName,
transformers,
Expand All @@ -212,114 +226,125 @@ export function register(opts: Options = {}): Register {

reportTSError(diagnosticList, config.options.noEmitOnError);

return { code: result.outputText, map: result.sourceMapText as string };
const file = {
code: result.outputText,
map: result.sourceMapText as string,
};
outFiles.set(fileName, file);
return file;
};

// Use full language services when the fast option is disabled.
let getOutputTypeCheck: (code: string, fileName: string) => SourceOutput;
{
const memoryCache = new MemoryCache(config.fileNames);
const cachedReadFile = cachedLookup(debugFn('readFile', readFile));

// Create the compiler host for type checking.
const serviceHost: _ts.LanguageServiceHost = {
getScriptFileNames: () => Array.from(memoryCache.fileVersions.keys()),
getScriptVersion: (fileName: string) => {
const version = memoryCache.fileVersions.get(fileName);
return version === undefined ? '' : version.toString();
},
getScriptSnapshot(fileName: string) {
let contents = memoryCache.fileContents.get(fileName);

// Read contents into TypeScript memory cache.
if (contents === undefined) {
contents = cachedReadFile(fileName);
if (contents === undefined) return;

memoryCache.fileVersions.set(fileName, 1);
memoryCache.fileContents.set(fileName, contents);
}

return ts.ScriptSnapshot.fromString(contents);
},
readFile: cachedReadFile,
readDirectory: cachedLookup(
debugFn('readDirectory', ts.sys.readDirectory)
),
getDirectories: cachedLookup(
debugFn('getDirectories', ts.sys.getDirectories)
),
fileExists: cachedLookup(debugFn('fileExists', fileExists)),
directoryExists: cachedLookup(
debugFn('directoryExists', ts.sys.directoryExists)
),
getNewLine: () => ts.sys.newLine,
useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
getCurrentDirectory: () => cwd,
getCompilationSettings: () => config.options,
getDefaultLibFileName: () => ts.getDefaultLibFilePath(config.options),
getCustomTransformers: () => transformers,
};
const memoryCache = new MemoryCache(config.fileNames);
const cachedReadFile = cachedLookup(readFile);

// Create the compiler host for type checking.
const serviceHost: _ts.LanguageServiceHost = {
getScriptFileNames: () => Array.from(memoryCache.fileVersions.keys()),
getScriptVersion: (fileName: string) => {
const version = memoryCache.fileVersions.get(fileName);
return version === undefined ? '' : version.toString();
},
getScriptSnapshot(fileName: string) {
let contents = memoryCache.fileContents.get(fileName);

// Read contents into TypeScript memory cache.
if (contents === undefined) {
contents = cachedReadFile(fileName);
if (contents === undefined) return;

memoryCache.fileVersions.set(fileName, 1);
memoryCache.fileContents.set(fileName, contents);
}

const registry = ts.createDocumentRegistry(
ts.sys.useCaseSensitiveFileNames,
cwd
);
const service = ts.createLanguageService(serviceHost, registry);
return ts.ScriptSnapshot.fromString(contents);
},
readFile: cachedReadFile,
readDirectory: cachedLookup(
debugFn('readDirectory', ts.sys.readDirectory)
),
getDirectories: cachedLookup(
debugFn('getDirectories', ts.sys.getDirectories)
),
fileExists: cachedLookup(debugFn('fileExists', fileExists)),
directoryExists: cachedLookup(
debugFn('directoryExists', ts.sys.directoryExists)
),
getNewLine: () => ts.sys.newLine,
useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
getCurrentDirectory: () => cwd,
getCompilationSettings: () => config.options,
getDefaultLibFileName: () => ts.getDefaultLibFilePath(config.options),
getCustomTransformers: () => transformers,
};

// Set the file contents into cache manually.
const updateMemoryCache = function (contents: string, fileName: string) {
const fileVersion = memoryCache.fileVersions.get(fileName) || 0;
const registry = ts.createDocumentRegistry(
ts.sys.useCaseSensitiveFileNames,
cwd
);
const service = ts.createLanguageService(serviceHost, registry);

// Avoid incrementing cache when nothing has changed.
if (memoryCache.fileContents.get(fileName) === contents) return;
// Set the file contents into cache manually.
const updateMemoryCache = function (contents: string, fileName: string) {
const fileVersion = memoryCache.fileVersions.get(fileName) || 0;

memoryCache.fileVersions.set(fileName, fileVersion + 1);
memoryCache.fileContents.set(fileName, contents);
};
// Avoid incrementing cache when nothing has changed.
if (memoryCache.fileContents.get(fileName) === contents) return;

getOutputTypeCheck = function (code: string, fileName: string) {
updateMemoryCache(code, fileName);
memoryCache.fileVersions.set(fileName, fileVersion + 1);
memoryCache.fileContents.set(fileName, contents);
};

const output = service.getEmitOutput(fileName);
/**
* Create complete function with full language services (normal behavior for `tsc`)
*/
const getOutputTypeCheck: GetOutputFunction = (
code: string,
fileName: string
) => {
const outFile = outFiles.get(fileName);
if (outFile) {
return outFile;
}
updateMemoryCache(code, fileName);

// Get the relevant diagnostics - this is 3x faster than `getPreEmitDiagnostics`.
const diagnostics = service
.getSemanticDiagnostics(fileName)
.concat(service.getSyntacticDiagnostics(fileName));
const output = service.getEmitOutput(fileName);

const diagnosticList = filterDiagnostics(
diagnostics,
ignoreDiagnostics
);
// Get the relevant diagnostics - this is 3x faster than `getPreEmitDiagnostics`.
const diagnostics = service
.getSemanticDiagnostics(fileName)
.concat(service.getSyntacticDiagnostics(fileName));

reportTSError(diagnosticList, config.options.noEmitOnError);
const diagnosticList = filterDiagnostics(diagnostics, ignoreDiagnostics);

if (output.emitSkipped) {
throw new TypeError(`${relative(cwd, fileName)}: Emit skipped`);
}
reportTSError(diagnosticList, config.options.noEmitOnError);

// Throw an error when requiring `.d.ts` files.
if (output.outputFiles.length === 0) {
throw new TypeError(
'Unable to require `.d.ts` file.\n' +
'This is usually the result of a faulty configuration or import. ' +
'Make sure there is a `.js`, `.json` or another executable extension and ' +
'loader (attached before `ts-node`) available alongside ' +
`\`${basename(fileName)}\`.`
);
}
if (output.emitSkipped) {
throw new TypeError(`${relative(cwd, fileName)}: Emit skipped`);
}

return {
code: output.outputFiles[1].text,
map: output.outputFiles[0].text,
};
// Throw an error when requiring `.d.ts` files.
if (output.outputFiles.length === 0) {
throw new TypeError(
'Unable to require `.d.ts` file.\n' +
'This is usually the result of a faulty configuration or import. ' +
'Make sure there is a `.js`, `.json` or another executable extension and ' +
'loader (attached before `ts-node`) available alongside ' +
`\`${basename(fileName)}\`.`
);
}

const file = {
code: output.outputFiles[1].text,
map: output.outputFiles[0].text,
};
}
outFiles.set(fileName, file);
return file;
};

const getOutput = skipTypeCheck ? getOutputTranspile : getOutputTypeCheck;
configFileToBuildMap.set(configFileName, getOutput);

build = { getOutput, getOutputTypeCheck };
configFileToBuildMap.set(configFileName, build);
return build;
return getOutput;
}

// determine the tsconfig.json path for a given folder
Expand Down Expand Up @@ -407,10 +432,8 @@ export function register(opts: Options = {}): Register {
skipTypeCheck?: boolean
): SourceOutput {
const configFileName = detectConfig();
const build = getBuild(configFileName);
const { code: value, map: sourceMap } = (
skipTypeCheck ? build.getOutput : build.getOutputTypeCheck
)(code, fileName);
const buildOutput = getBuild(configFileName, skipTypeCheck);
const { code: value, map: sourceMap } = buildOutput(code, fileName);
const output = {
code: value,
map: Object.assign(JSON.parse(sourceMap), {
Expand All @@ -425,10 +448,7 @@ export function register(opts: Options = {}): Register {
return compile;
}

interface Build {
getOutput(code: string, fileName: string): SourceOutput;
getOutputTypeCheck(code: string, fileName: string): SourceOutput;
}
type GetOutputFunction = (code: string, fileName: string) => SourceOutput;

/**
* Do post-processing on config options to support `ts-node`.
Expand Down

0 comments on commit d649a3c

Please sign in to comment.