Merge pull request #127406 from microsoft/hediet/ArrayQueue

Replaces filterSortedByKey with faster ArrayQueue.
This commit is contained in:
Henning Dieterichs 2021-07-01 17:50:14 +02:00 committed by GitHub
commit e92da93d8a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 115 additions and 19 deletions

View file

@ -589,15 +589,51 @@ export function maxIndex<T>(array: readonly T[], fn: (value: T) => number): numb
return maxIdx;
}
/**
* Returns an array of all elements `e` with `startKey <= keySelector(e) <= endKey`.
* `data` must be sorted in ascending order.
*/
export function filterSortedByKey<T>(data: readonly T[], keySelector: (value: T) => number, startKey: number, endKey = startKey): T[] | null {
const startIdx = findFirstInSorted(data, x => startKey <= keySelector(x));
const endIdx = findFirstInSorted(data, x => endKey < keySelector(x));
if (startIdx === endIdx) {
return null;
export class ArrayQueue<T> {
private firstIdx = 0;
private lastIdx = this.items.length - 1;
/**
* Constructs a queue that is backed by the given array. Runtime is O(1).
*/
constructor(private readonly items: T[]) { }
get length(): number {
return this.lastIdx - this.firstIdx + 1;
}
/**
* Consumes elements from the beginning of the queue as long as the predicate returns true.
* If no elements were consumed, `null` is returned. Has a runtime of O(result.length).
*/
takeWhile(predicate: (value: T) => boolean): T[] | null {
// P(k) := k <= this.lastIdx && predicate(this.items[k])
// Find s := min { k | k >= this.firstIdx && !P(k) } and return this.data[this.firstIdx...s)
let startIdx = this.firstIdx;
while (startIdx < this.items.length && predicate(this.items[startIdx])) {
startIdx++;
}
const result = startIdx === this.firstIdx ? null : this.items.slice(this.firstIdx, startIdx);
this.firstIdx = startIdx;
return result;
}
/**
* Consumes elements from the end of the queue as long as the predicate returns true.
* If no elements were consumed, `null` is returned.
* The result has the same order as the underlying array!
*/
takeFromEndWhile(predicate: (value: T) => boolean): T[] | null {
// P(k) := this.firstIdx >= k && predicate(this.items[k])
// Find s := max { k | k <= this.lastIdx && !P(k) } and return this.data(s...this.lastIdx]
let endIdx = this.lastIdx;
while (endIdx >= 0 && predicate(this.items[endIdx])) {
endIdx--;
}
const result = endIdx === this.lastIdx ? null : this.items.slice(endIdx + 1, this.lastIdx + 1);
this.lastIdx = endIdx;
return result;
}
return data.slice(startIdx, endIdx);
}

View file

@ -312,12 +312,63 @@ suite('Arrays', () => {
assert.strictEqual(arrays.maxIndex(array, value => value === 'b' ? 5 : 0), 1);
});
test('filterSortedByKey', () => {
const array = [1, 2, 5, 5, 7, 8];
suite('ArrayQueue', () => {
suite('takeWhile/takeFromEndWhile', () => {
test('TakeWhile 1', () => {
const queue1 = new arrays.ArrayQueue([9, 8, 1, 7, 6]);
assert.deepStrictEqual(queue1.takeWhile(x => x > 5), [9, 8]);
assert.deepStrictEqual(queue1.takeWhile(x => x < 7), [1]);
assert.deepStrictEqual(queue1.takeWhile(x => true), [7, 6]);
});
assert.deepStrictEqual(arrays.filterSortedByKey(array, i => i, 3, 5), [5, 5]);
assert.deepStrictEqual(arrays.filterSortedByKey(array, i => i, 5, 5), [5, 5]);
assert.deepStrictEqual(arrays.filterSortedByKey(array, i => i, 6, 6), null);
assert.deepStrictEqual(arrays.filterSortedByKey(array, i => i, 8, 8), [8]);
test('TakeWhile 1', () => {
const queue1 = new arrays.ArrayQueue([9, 8, 1, 7, 6]);
assert.deepStrictEqual(queue1.takeFromEndWhile(x => x > 5), [7, 6]);
assert.deepStrictEqual(queue1.takeFromEndWhile(x => x < 2), [1]);
assert.deepStrictEqual(queue1.takeFromEndWhile(x => true), [9, 8]);
});
});
suite('takeWhile/takeFromEndWhile monotonous', () => {
function testMonotonous(array: number[], predicate: (a: number) => boolean) {
function normalize(arr: number[]): number[] | null {
if (arr.length === 0) {
return null;
}
return arr;
}
const negatedPredicate = (a: number) => !predicate(a);
{
const queue1 = new arrays.ArrayQueue(array);
assert.deepStrictEqual(queue1.takeWhile(predicate), normalize(array.filter(predicate)));
assert.deepStrictEqual(queue1.length, array.length - array.filter(predicate).length);
assert.deepStrictEqual(queue1.takeWhile(() => true), normalize(array.filter(negatedPredicate)));
}
{
const queue3 = new arrays.ArrayQueue(array);
assert.deepStrictEqual(queue3.takeFromEndWhile(negatedPredicate), normalize(array.filter(negatedPredicate)));
assert.deepStrictEqual(queue3.length, array.length - array.filter(negatedPredicate).length);
assert.deepStrictEqual(queue3.takeFromEndWhile(() => true), normalize(array.filter(predicate)));
}
}
const array = [1, 1, 1, 2, 5, 5, 7, 8, 8];
test('TakeWhile 1', () => testMonotonous(array, value => value <= 1));
test('TakeWhile 2', () => testMonotonous(array, value => value < 5));
test('TakeWhile 3', () => testMonotonous(array, value => value <= 5));
test('TakeWhile 4', () => testMonotonous(array, value => true));
test('TakeWhile 5', () => testMonotonous(array, value => false));
const array2 = [1, 1, 1, 2, 5, 5, 7, 8, 8, 9, 9, 9, 9, 10, 10];
test('TakeWhile 6', () => testMonotonous(array2, value => value < 10));
test('TakeWhile 7', () => testMonotonous(array2, value => value < 7));
test('TakeWhile 8', () => testMonotonous(array2, value => value < 5));
test('TakeWhile Empty', () => testMonotonous([], value => value <= 5));
});
});
});

View file

@ -39,7 +39,7 @@ import { TextChange } from 'vs/editor/common/model/textChange';
import { Constants } from 'vs/base/common/uint';
import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer';
import { listenStream } from 'vs/base/common/stream';
import { filterSortedByKey } from 'vs/base/common/arrays';
import { ArrayQueue } from 'vs/base/common/arrays';
function createTextBufferBuilder() {
return new PieceTreeTextBufferBuilder();
@ -1470,17 +1470,23 @@ export class TextModel extends Disposable implements model.ITextModel {
0,
this._versionId
));
const injectedTextInEditedRange = LineInjectedText.fromDecorations(decorationsWithInjectedTextInEditedRange);
const injectedTextInEditedRangeQueue = new ArrayQueue(injectedTextInEditedRange);
for (let j = editingLinesCnt; j >= 0; j--) {
const editLineNumber = startLineNumber + j;
const currentEditLineNumber = currentEditStartLineNumber + j;
injectedTextInEditedRangeQueue.takeFromEndWhile(r => r.lineNumber > currentEditLineNumber);
const decorationsInCurrentLine = injectedTextInEditedRangeQueue.takeFromEndWhile(r => r.lineNumber === currentEditLineNumber);
rawContentChanges.push(
new ModelRawLineChanged(
editLineNumber,
this.getLineContent(currentEditLineNumber),
filterSortedByKey(injectedTextInEditedRange, i => i.lineNumber, currentEditLineNumber)
decorationsInCurrentLine
));
}
@ -1491,6 +1497,7 @@ export class TextModel extends Disposable implements model.ITextModel {
}
if (editingLinesCnt < insertingLinesCnt) {
const injectedTextInEditedRangeQueue = new ArrayQueue(injectedTextInEditedRange);
// Must insert some lines
const spliceLineNumber = startLineNumber + editingLinesCnt;
const cnt = insertingLinesCnt - editingLinesCnt;
@ -1500,7 +1507,9 @@ export class TextModel extends Disposable implements model.ITextModel {
for (let i = 0; i < cnt; i++) {
let lineNumber = fromLineNumber + i;
newLines[i] = this.getLineContent(lineNumber);
injectedTexts[i] = filterSortedByKey(injectedTextInEditedRange, i => i.lineNumber, lineNumber);
injectedTextInEditedRangeQueue.takeWhile(r => r.lineNumber < lineNumber);
injectedTexts[i] = injectedTextInEditedRangeQueue.takeWhile(r => r.lineNumber === lineNumber);
}
rawContentChanges.push(