-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Description
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:
TypeScript/src/compiler/emitter.ts
Lines 5290 to 5300 in a7d8011
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:
TypeScript/src/compiler/sourcemap.ts
Lines 52 to 69 in a7d8011
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.
TypeScript/src/compiler/path.ts
Lines 817 to 832 in a7d8011
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