Skip to content

Performance bottleneck with external source map sources #40054

Closed
@JoostK

Description

@JoostK

TypeScript Version: 3.9.7

Search Terms: external source maps, performance, setSourceMapSource

Code

I am investigating Angular compiler performance and noticed an interesting performance cliff related to TypeScript's ability to map into external sources. External source map sources are used in ngc (Angular's tsc) to map into the external .html template files, using the ts.createSourceMapSource and ts.setSourceMapRange public APIs.

During emit, emitSourcePos is used to build the source map, using a fast-path if no external source map source is used:

function emitSourcePos(source: SourceMapSource, pos: number) {
if (source !== sourceMapSource) {
const savedSourceMapSource = sourceMapSource;
setSourceMapSource(source);
emitPos(pos);
setSourceMapSource(savedSourceMapSource);
}
else {
emitPos(pos);
}
}

With an external source map, however, the slow-path that calls into setSourceMapSource is run twice, for each emitted ts.Node. This calls into ts.SourceMapGenerator.addSource which manages the source indices per source map source, based on their canonicalized relative path:

function addSource(fileName: string) {
enter();
const source = getRelativePathToDirectoryOrUrl(sourcesDirectoryPath,
fileName,
host.getCurrentDirectory(),
host.getCanonicalFileName,
/*isAbsolutePathAnUrl*/ true);
let sourceIndex = sourceToSourceIndexMap.get(source);
if (sourceIndex === undefined) {
sourceIndex = sources.length;
sources.push(source);
rawSources.push(fileName);
sourceToSourceIndexMap.set(source, sourceIndex);
}
exit();
return sourceIndex;
}

The path manipulation is somewhat expensive and it calls repeatedly into ts.CompilerHost.getCanonicalFileName, so its performance characteristics also have quite a significant impact.

export function getRelativePathToDirectoryOrUrl(directoryPathOrUrl: string, relativeOrAbsolutePath: string, currentDirectory: string, getCanonicalFileName: GetCanonicalFileName, isAbsolutePathAnUrl: boolean) {
const pathComponents = getPathComponentsRelativeTo(
resolvePath(currentDirectory, directoryPathOrUrl),
resolvePath(currentDirectory, relativeOrAbsolutePath),
equateStringsCaseSensitive,
getCanonicalFileName
);
const firstComponent = pathComponents[0];
if (isAbsolutePathAnUrl && isRootedDiskPath(firstComponent)) {
const prefix = firstComponent.charAt(0) === directorySeparator ? "file://" : "file:///";
pathComponents[0] = prefix + firstComponent;
}
return getPathFromPathComponents(pathComponents);
}

Using a program with the following characteristics (obtained with tsc --diagnostics):

Files:             1095
Lines:           200035
Nodes:           501580
Identifiers:     164978
Symbols:         202287
Types:            62907
Instantiations:   49308

we're seeing 4.8 seconds spent in emitSourcePos using ngc (of which 4.3 seconds is spent in setSourceMapSource), where the full emit phase takes 13.4 seconds; i.e. source mapping takes 36% of emit (using a profiler, so there's some overhead there). More importantly, the slow path in setSourceMapSource is responsible for 4.3/4.8 = ~90% overhead.

Using Angular CLI it's much worse with emitSourcePos taking ~16s, given that the ts.CompilerHost.getCanonicalFileName implementation is a lot slower—that is for Angular itself to improve.

Expected behavior:

The overhead in setSourceMapSource should not be 90% of total source mapping time.

Actual behavior:

Using external source map sources does have a significant performance overhead, increasing source map times ten-fold or worse.

Playground Link:
n/a

Related Issues:
n/a

Metadata

Metadata

Assignees

Labels

Domain: PerformanceReports of unusually slow behaviorFix AvailableA PR has been opened for this issueNeeds InvestigationThis issue needs a team member to investigate its status.

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions