perf(ngcc): use binary search when flattening mappings (#36027)

The `@angular/core` package has a large number of source files
and mappings which exposed performance issues in the new source-map
flattening algorithm.

This change uses a binary search (rather than linear) when finding
matching mappings to merge. Initial measurements indicate that this
reduces processing time for `@angular/core` by about 50%.

PR Close #36027
This commit is contained in:
Pete Bacon Darwin 2020-03-09 17:03:36 +00:00 committed by Andrew Kushnir
parent c852ec9283
commit 348ff0c8ea
2 changed files with 236 additions and 15 deletions

View File

@ -147,16 +147,14 @@ export class SourceFile {
// The range with `incomingStart` at 2 and `incomingEnd` at 5 has outgoing start mapping of // The range with `incomingStart` at 2 and `incomingEnd` at 5 has outgoing start mapping of
// [1,0] and outgoing end mapping of [4, 6], which also includes [4, 3]. // [1,0] and outgoing end mapping of [4, 6], which also includes [4, 3].
// //
let outgoingStartIndex = findLastIndex( let outgoingStartIndex =
bSource.flattenedMappings, findLastMappingIndexBefore(bSource.flattenedMappings, incomingStart, false, 0);
mapping => compareSegments(mapping.generatedSegment, incomingStart) <= 0);
if (outgoingStartIndex < 0) { if (outgoingStartIndex < 0) {
outgoingStartIndex = 0; outgoingStartIndex = 0;
} }
const outgoingEndIndex = incomingEnd !== undefined ? const outgoingEndIndex = incomingEnd !== undefined ?
findLastIndex( findLastMappingIndexBefore(
bSource.flattenedMappings, bSource.flattenedMappings, incomingEnd, true, outgoingStartIndex) :
mapping => compareSegments(mapping.generatedSegment, incomingEnd) < 0) :
bSource.flattenedMappings.length - 1; bSource.flattenedMappings.length - 1;
for (let bToCmappingIndex = outgoingStartIndex; bToCmappingIndex <= outgoingEndIndex; for (let bToCmappingIndex = outgoingStartIndex; bToCmappingIndex <= outgoingEndIndex;
@ -169,13 +167,38 @@ export class SourceFile {
} }
} }
function findLastIndex<T>(items: T[], predicate: (item: T) => boolean): number { /**
for (let index = items.length - 1; index >= 0; index--) { *
if (predicate(items[index])) { * @param mappings The collection of mappings whose segment-markers we are searching.
return index; * @param marker The segment-marker to match against those of the given `mappings`.
} * @param exclusive If exclusive then we must find a mapping with a segment-marker that is
} * exclusively earlier than the given `marker`.
* If not exclusive then we can return the highest mappings with an equivalent segment-marker to the
* given `marker`.
* @param lowerIndex If provided, this is used as a hint that the marker we are searching for has an
* index that is no lower than this.
*/
export function findLastMappingIndexBefore(
mappings: Mapping[], marker: SegmentMarker, exclusive: boolean, lowerIndex: number): number {
let upperIndex = mappings.length - 1;
const test = exclusive ? -1 : 0;
if (compareSegments(mappings[lowerIndex].generatedSegment, marker) > test) {
// Exit early since the marker is outside the allowed range of mappings.
return -1; return -1;
}
let matchingIndex = -1;
while (lowerIndex <= upperIndex) {
const index = (upperIndex + lowerIndex) >> 1;
if (compareSegments(mappings[index].generatedSegment, marker) <= test) {
matchingIndex = index;
lowerIndex = index + 1;
} else {
upperIndex = index - 1;
}
}
return matchingIndex;
} }
/** /**

View File

@ -10,15 +10,14 @@ import {encode} from 'sourcemap-codec';
import {absoluteFrom} from '../../../src/ngtsc/file_system'; import {absoluteFrom} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {RawSourceMap} from '../../src/sourcemaps/raw_source_map'; import {RawSourceMap} from '../../src/sourcemaps/raw_source_map';
import {SourceFile, computeLineLengths, extractOriginalSegments, parseMappings} from '../../src/sourcemaps/source_file'; import {SegmentMarker} from '../../src/sourcemaps/segment_marker';
import {Mapping, SourceFile, computeLineLengths, extractOriginalSegments, findLastMappingIndexBefore, parseMappings} from '../../src/sourcemaps/source_file';
runInEachFileSystem(() => { runInEachFileSystem(() => {
describe('SourceFile and utilities', () => { describe('SourceFile and utilities', () => {
let _: typeof absoluteFrom; let _: typeof absoluteFrom;
beforeEach(() => { beforeEach(() => { _ = absoluteFrom; });
_ = absoluteFrom;
});
describe('parseMappings()', () => { describe('parseMappings()', () => {
it('should be an empty array for source files with no source map', () => { it('should be an empty array for source files with no source map', () => {
@ -84,6 +83,205 @@ runInEachFileSystem(() => {
}); });
}); });
describe('findLastMappingIndexBefore', () => {
it('should find the highest mapping index that has a segment marker below the given one if there is not an exact match',
() => {
const marker5: SegmentMarker = {line: 0, column: 50};
const marker4: SegmentMarker = {line: 0, column: 40};
const marker3: SegmentMarker = {line: 0, column: 30};
const marker2: SegmentMarker = {line: 0, column: 20};
const marker1: SegmentMarker = {line: 0, column: 10};
const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map(
marker => ({ generatedSegment: marker } as Mapping));
const marker: SegmentMarker = {line: 0, column: 35};
const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ false, 0);
expect(index).toEqual(2);
});
it('should find the highest mapping index that has a segment marker (when there are duplicates) below the given one if there is not an exact match',
() => {
const marker5: SegmentMarker = {line: 0, column: 50};
const marker4: SegmentMarker = {line: 0, column: 30};
const marker3: SegmentMarker = {line: 0, column: 30};
const marker2: SegmentMarker = {line: 0, column: 20};
const marker1: SegmentMarker = {line: 0, column: 10};
const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map(
marker => ({ generatedSegment: marker } as Mapping));
const marker: SegmentMarker = {line: 0, column: 35};
const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ false, 0);
expect(index).toEqual(3);
});
it('should find the last mapping if the segment marker is higher than all of them', () => {
const marker5: SegmentMarker = {line: 0, column: 50};
const marker4: SegmentMarker = {line: 0, column: 40};
const marker3: SegmentMarker = {line: 0, column: 30};
const marker2: SegmentMarker = {line: 0, column: 20};
const marker1: SegmentMarker = {line: 0, column: 10};
const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map(
marker => ({ generatedSegment: marker } as Mapping));
const marker: SegmentMarker = {line: 0, column: 60};
const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ false, 0);
expect(index).toEqual(4);
});
it('should return -1 if the segment marker is lower than all of them', () => {
const marker5: SegmentMarker = {line: 0, column: 50};
const marker4: SegmentMarker = {line: 0, column: 40};
const marker3: SegmentMarker = {line: 0, column: 30};
const marker2: SegmentMarker = {line: 0, column: 20};
const marker1: SegmentMarker = {line: 0, column: 10};
const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map(
marker => ({ generatedSegment: marker } as Mapping));
const marker: SegmentMarker = {line: 0, column: 5};
const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ false, 0);
expect(index).toEqual(-1);
});
describe('[exact match inclusive]', () => {
it('should find the matching segment marker mapping index if there is only one of them',
() => {
const marker5: SegmentMarker = {line: 0, column: 50};
const marker4: SegmentMarker = {line: 0, column: 40};
const marker3: SegmentMarker = {line: 0, column: 30};
const marker2: SegmentMarker = {line: 0, column: 20};
const marker1: SegmentMarker = {line: 0, column: 10};
const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map(
marker => ({ generatedSegment: marker } as Mapping));
const index = findLastMappingIndexBefore(mappings, marker3, /* exclusive */ false, 0);
expect(index).toEqual(2);
});
it('should find the highest matching segment marker mapping index if there is more than one of them',
() => {
const marker5: SegmentMarker = {line: 0, column: 50};
const marker4: SegmentMarker = {line: 0, column: 30};
const marker3: SegmentMarker = {line: 0, column: 30};
const marker2: SegmentMarker = {line: 0, column: 20};
const marker1: SegmentMarker = {line: 0, column: 10};
const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map(
marker => ({ generatedSegment: marker } as Mapping));
const index = findLastMappingIndexBefore(mappings, marker3, /* exclusive */ false, 0);
expect(index).toEqual(3);
});
});
describe('[exact match exclusive]', () => {
it('should find the preceding mapping index if there is a matching segment marker', () => {
const marker5: SegmentMarker = {line: 0, column: 50};
const marker4: SegmentMarker = {line: 0, column: 40};
const marker3: SegmentMarker = {line: 0, column: 30};
const marker2: SegmentMarker = {line: 0, column: 20};
const marker1: SegmentMarker = {line: 0, column: 10};
const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map(
marker => ({ generatedSegment: marker } as Mapping));
const index = findLastMappingIndexBefore(mappings, marker3, /* exclusive */ true, 0);
expect(index).toEqual(1);
});
it('should find the highest preceding mapping index if there is more than one matching segment marker',
() => {
const marker5: SegmentMarker = {line: 0, column: 50};
const marker4: SegmentMarker = {line: 0, column: 30};
const marker3: SegmentMarker = {line: 0, column: 30};
const marker2: SegmentMarker = {line: 0, column: 20};
const marker1: SegmentMarker = {line: 0, column: 10};
const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map(
marker => ({ generatedSegment: marker } as Mapping));
const index = findLastMappingIndexBefore(mappings, marker3, /* exclusive */ false, 0);
expect(index).toEqual(3);
});
});
describe('[with lowerIndex hint', () => {
it('should find the highest mapping index above the lowerIndex hint that has a segment marker below the given one if there is not an exact match',
() => {
const marker5: SegmentMarker = {line: 0, column: 50};
const marker4: SegmentMarker = {line: 0, column: 40};
const marker3: SegmentMarker = {line: 0, column: 30};
const marker2: SegmentMarker = {line: 0, column: 20};
const marker1: SegmentMarker = {line: 0, column: 10};
const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map(
marker => ({ generatedSegment: marker } as Mapping));
const marker: SegmentMarker = {line: 0, column: 35};
const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ false, 1);
expect(index).toEqual(2);
});
it('should return the lowerIndex mapping index if there is a single exact match and we are not exclusive',
() => {
const marker5: SegmentMarker = {line: 0, column: 50};
const marker4: SegmentMarker = {line: 0, column: 40};
const marker3: SegmentMarker = {line: 0, column: 30};
const marker2: SegmentMarker = {line: 0, column: 20};
const marker1: SegmentMarker = {line: 0, column: 10};
const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map(
marker => ({ generatedSegment: marker } as Mapping));
const marker: SegmentMarker = {line: 0, column: 30};
const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ false, 2);
expect(index).toEqual(2);
});
it('should return the lowerIndex mapping index if there are multiple exact matches and we are not exclusive',
() => {
const marker5: SegmentMarker = {line: 0, column: 50};
const marker4: SegmentMarker = {line: 0, column: 30};
const marker3: SegmentMarker = {line: 0, column: 30};
const marker2: SegmentMarker = {line: 0, column: 20};
const marker1: SegmentMarker = {line: 0, column: 10};
const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map(
marker => ({ generatedSegment: marker } as Mapping));
const marker: SegmentMarker = {line: 0, column: 30};
const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ false, 3);
expect(index).toEqual(3);
});
it('should return -1 if the segment marker is lower than the lowerIndex hint', () => {
const marker5: SegmentMarker = {line: 0, column: 50};
const marker4: SegmentMarker = {line: 0, column: 40};
const marker3: SegmentMarker = {line: 0, column: 30};
const marker2: SegmentMarker = {line: 0, column: 20};
const marker1: SegmentMarker = {line: 0, column: 10};
const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map(
marker => ({ generatedSegment: marker } as Mapping));
const marker: SegmentMarker = {line: 0, column: 25};
const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ false, 2);
expect(index).toEqual(-1);
});
it('should return -1 if the segment marker is equal to the lowerIndex hint and we are exclusive',
() => {
const marker5: SegmentMarker = {line: 0, column: 50};
const marker4: SegmentMarker = {line: 0, column: 40};
const marker3: SegmentMarker = {line: 0, column: 30};
const marker2: SegmentMarker = {line: 0, column: 20};
const marker1: SegmentMarker = {line: 0, column: 10};
const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map(
marker => ({ generatedSegment: marker } as Mapping));
const marker: SegmentMarker = {line: 0, column: 30};
const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ true, 2);
expect(index).toEqual(-1);
});
});
});
describe('SourceFile', () => { describe('SourceFile', () => {
describe('flattenedMappings', () => { describe('flattenedMappings', () => {
it('should be an empty array for source files with no source map', () => { it('should be an empty array for source files with no source map', () => {