perf(compiler-cli): reduce filesystem hits during resource resolution (#39604)

The resource loader uses TypeScript's module resolution system to
determine at which locations it needs to look for a resource file. A
marker string is used to force the module resolution to fail, such that
all failed lookup locations can then be considered for actual resource
resolution. Any filesystem requests targeting files/directories that
contain the marker are known not to exist, so no filesystem request
needs to be done at all.

PR Close #39604
This commit is contained in:
JoostK 2020-11-08 16:15:14 +01:00 committed by atscott
parent 7c161e1679
commit ade6da95e4
1 changed files with 41 additions and 3 deletions

View File

@ -11,15 +11,20 @@ import * as ts from 'typescript';
import {ResourceLoader} from '../../annotations';
import {NgCompilerAdapter} from '../../core/api';
import {AbsoluteFsPath, join, PathSegment} from '../../file_system';
import {RequiredDelegations} from '../../util/src/typescript';
const CSS_PREPROCESSOR_EXT = /(\.scss|\.sass|\.less|\.styl)$/;
const RESOURCE_MARKER = '.$ngresource$';
const RESOURCE_MARKER_TS = RESOURCE_MARKER + '.ts';
/**
* `ResourceLoader` which delegates to an `NgCompilerAdapter`'s resource loading methods.
*/
export class AdapterResourceLoader implements ResourceLoader {
private cache = new Map<string, string>();
private fetching = new Map<string, Promise<void>>();
private lookupResolutionHost = createLookupResolutionHost(this.adapter);
canPreload = !!this.adapter.readResource;
@ -167,7 +172,7 @@ export class AdapterResourceLoader implements ResourceLoader {
ts.ResolvedModuleWithFailedLookupLocations&{failedLookupLocations: ReadonlyArray<string>};
// clang-format off
const failedLookup = ts.resolveModuleName(url + '.$ngresource$', fromFile, this.options, this.adapter) as ResolvedModuleWithFailedLookupLocations;
const failedLookup = ts.resolveModuleName(url + RESOURCE_MARKER, fromFile, this.options, this.lookupResolutionHost) as ResolvedModuleWithFailedLookupLocations;
// clang-format on
if (failedLookup.failedLookupLocations === undefined) {
throw new Error(
@ -176,7 +181,40 @@ export class AdapterResourceLoader implements ResourceLoader {
}
return failedLookup.failedLookupLocations
.filter(candidate => candidate.endsWith('.$ngresource$.ts'))
.map(candidate => candidate.replace(/\.\$ngresource\$\.ts$/, ''));
.filter(candidate => candidate.endsWith(RESOURCE_MARKER_TS))
.map(candidate => candidate.slice(0, -RESOURCE_MARKER_TS.length));
}
}
/**
* Derives a `ts.ModuleResolutionHost` from a compiler adapter that recognizes the special resource
* marker and does not go to the filesystem for these requests, as they are known not to exist.
*/
function createLookupResolutionHost(adapter: NgCompilerAdapter):
RequiredDelegations<ts.ModuleResolutionHost> {
return {
directoryExists(directoryName: string): boolean {
if (directoryName.includes(RESOURCE_MARKER)) {
return false;
} else if (adapter.directoryExists !== undefined) {
return adapter.directoryExists(directoryName);
} else {
// TypeScript's module resolution logic assumes that the directory exists when no host
// implementation is available.
return true;
}
},
fileExists(fileName: string): boolean {
if (fileName.includes(RESOURCE_MARKER)) {
return false;
} else {
return adapter.fileExists(fileName);
}
},
readFile: adapter.readFile.bind(adapter),
getCurrentDirectory: adapter.getCurrentDirectory.bind(adapter),
getDirectories: adapter.getDirectories?.bind(adapter),
realpath: adapter.realpath?.bind(adapter),
trace: adapter.trace?.bind(adapter),
};
}