perf(ngcc): use line start positions for computing offsets in source-map flattening (#36027)

By computing and caching the start of each line, rather than the length
of each line, we can save a lot of duplicated computation in the `segmentDiff()`
and `offsetSegment()` functions.

PR Close #36027
This commit is contained in:
Pete Bacon Darwin
2020-03-11 12:17:36 +00:00
committed by Andrew Kushnir
parent a40be00e17
commit e8900824dd
4 changed files with 112 additions and 92 deletions

View File

@ -28,61 +28,41 @@ export function compareSegments(a: SegmentMarker, b: SegmentMarker): number {
return a.line === b.line ? a.column - b.column : a.line - b.line;
}
// The `1` is to indicate a newline character between the lines.
// Note that in the actual contents there could be more than one character that indicates a newline
// - e.g. \r\n - but that is not important here since segment-markers are in line/column pairs and
// so differences in length due to extra `\r` characters do not affect the algorithms.
const NEWLINE_MARKER_OFFSET = 1;
/**
* Compute the difference between two segment markers in a source file.
*
* @param lineLengths the lengths of each line of content of the source file where we are computing
* the difference
* @param startOfLinePositions the position of the start of each line of content of the source file
* where we are computing the difference
* @param a the start marker
* @param b the end marker
* @returns the number of characters between the two segments `a` and `b`
*/
export function segmentDiff(lineLengths: number[], a: SegmentMarker, b: SegmentMarker) {
let diff = b.column - a.column;
// Deal with `a` being before `b`
for (let lineIndex = a.line; lineIndex < b.line; lineIndex++) {
diff += lineLengths[lineIndex] + NEWLINE_MARKER_OFFSET;
}
// Deal with `a` being after `b`
for (let lineIndex = a.line - 1; lineIndex >= b.line; lineIndex--) {
// The `+ 1` is the newline character between the lines
diff -= lineLengths[lineIndex] + NEWLINE_MARKER_OFFSET;
}
return diff;
export function segmentDiff(startOfLinePositions: number[], a: SegmentMarker, b: SegmentMarker) {
return startOfLinePositions[b.line] - startOfLinePositions[a.line] + b.column - a.column;
}
/**
* Return a new segment-marker that is offset by the given number of characters.
*
* @param lineLengths The length of each line in the source file whose segment-marker we are
* offsetting.
* @param marker The segment to offset.
* @param offset The number of character to offset by.
* @param startOfLinePositions the position of the start of each line of content of the source file
* whose segment-marker we are offsetting.
* @param marker the segment to offset.
* @param offset the number of character to offset by.
*/
export function offsetSegment(lineLengths: number[], marker: SegmentMarker, offset: number) {
export function offsetSegment(
startOfLinePositions: number[], marker: SegmentMarker, offset: number) {
if (offset === 0) {
return marker;
}
let line = marker.line;
let column = marker.column + offset;
while (line < lineLengths.length - 1 && column > lineLengths[line]) {
column -= lineLengths[line] + NEWLINE_MARKER_OFFSET;
const newPos = startOfLinePositions[line] + marker.column + offset;
while (line < startOfLinePositions.length - 1 && startOfLinePositions[line + 1] <= newPos) {
line++;
}
while (line > 0 && column < 0) {
while (line > 0 && startOfLinePositions[line] > newPos) {
line--;
column += lineLengths[line] + NEWLINE_MARKER_OFFSET;
}
const column = newPos - startOfLinePositions[line];
return {line, column};
}

View File

@ -24,7 +24,7 @@ export class SourceFile {
* pure original source files).
*/
readonly flattenedMappings: Mapping[];
readonly lineLengths: number[];
readonly startOfLinePositions: number[];
constructor(
/** The path to this source file. */
@ -38,7 +38,7 @@ export class SourceFile {
/** Any source files referenced by the raw source map associated with this source file. */
readonly sources: (SourceFile|null)[]) {
this.contents = removeSourceMapComments(contents);
this.lineLengths = computeLineLengths(this.contents);
this.startOfLinePositions = computeStartOfLinePositions(this.contents);
this.flattenedMappings = this.flattenMappings();
}
@ -282,11 +282,13 @@ export function mergeMappings(generatedSource: SourceFile, ab: Mapping, bc: Mapp
// segment-marker" of B->C (4*): `1 - 4 = -3`.
// Since it is negative we must increment the "generated segment-marker" with `3` to give [3,2].
const diff = segmentDiff(ab.originalSource.lineLengths, ab.originalSegment, bc.generatedSegment);
const diff =
segmentDiff(ab.originalSource.startOfLinePositions, ab.originalSegment, bc.generatedSegment);
if (diff > 0) {
return {
name,
generatedSegment: offsetSegment(generatedSource.lineLengths, ab.generatedSegment, diff),
generatedSegment:
offsetSegment(generatedSource.startOfLinePositions, ab.generatedSegment, diff),
originalSource: bc.originalSource,
originalSegment: bc.originalSegment,
};
@ -295,7 +297,8 @@ export function mergeMappings(generatedSource: SourceFile, ab: Mapping, bc: Mapp
name,
generatedSegment: ab.generatedSegment,
originalSource: bc.originalSource,
originalSegment: offsetSegment(bc.originalSource.lineLengths, bc.originalSegment, -diff),
originalSegment:
offsetSegment(bc.originalSource.startOfLinePositions, bc.originalSegment, -diff),
};
}
}
@ -361,6 +364,21 @@ export function extractOriginalSegments(mappings: Mapping[]): Map<SourceFile, Se
return originalSegments;
}
export function computeLineLengths(str: string): number[] {
export function computeStartOfLinePositions(str: string) {
// The `1` is to indicate a newline character between the lines.
// Note that in the actual contents there could be more than one character that indicates a
// newline
// - e.g. \r\n - but that is not important here since segment-markers are in line/column pairs and
// so differences in length due to extra `\r` characters do not affect the algorithms.
const NEWLINE_MARKER_OFFSET = 1;
const lineLengths = computeLineLengths(str);
const startPositions = [0]; // First line starts at position 0
for (let i = 0; i < lineLengths.length - 1; i++) {
startPositions.push(startPositions[i] + lineLengths[i] + NEWLINE_MARKER_OFFSET);
}
return startPositions;
}
function computeLineLengths(str: string): number[] {
return (str.split(/\r?\n/)).map(s => s.length);
}