Skip to content

Commit

Permalink
fix: Perf - Try improving suggestion performance. (#1639)
Browse files Browse the repository at this point in the history
* fix: Perf - Use PairingHeap for SortedQueue

* dev: Try using an A* algorithm to find suggestions.
  • Loading branch information
Jason3S committed Sep 6, 2021
1 parent fc47f13 commit aad4352
Show file tree
Hide file tree
Showing 12 changed files with 1,226 additions and 21 deletions.
@@ -1,5 +1,5 @@
import { compare } from './Comparable';
import { __testing__, SortedQueue } from './SortedQueue';
import { __testing__, MinHeapQueue } from './MinHeapQueue';

const { addToHeap, takeFromHeap } = __testing__;

Expand All @@ -25,7 +25,7 @@ describe('Validate Mere Sort methods', () => {
${'abc def ghi jkl mno pqr stu vwx yz'}
${'aaaaaaaaaaaaaaaaaa'}
`('Merge Queue $letters', ({ letters }: { letters: string }) => {
const q = new SortedQueue<string>(compare);
const q = new MinHeapQueue<string>(compare);
const values = letters.split('');
q.concat(values);
expect(q.length).toBe(values.length);
Expand All @@ -35,7 +35,7 @@ describe('Validate Mere Sort methods', () => {
});

test('Queue', () => {
const q = new SortedQueue<string>(compare);
const q = new MinHeapQueue<string>(compare);
expect(q.length).toBe(0);
q.add('one');
q.add('two');
Expand All @@ -60,13 +60,13 @@ describe('Validate Mere Sort methods', () => {
];

const sorted = values.concat().sort(compare);
const q = new SortedQueue<number>(compare);
const q = new MinHeapQueue<number>(compare);
q.concat(values);
expect([...q]).toEqual(sorted);
});

test('Queue Random', () => {
const q = new SortedQueue<number>(compare);
const q = new MinHeapQueue<number>(compare);
for (let i = 0; i < 100; ++i) {
const s = Math.random();
const n = Math.floor(100 * s);
Expand All @@ -83,7 +83,7 @@ describe('Validate Mere Sort methods', () => {
});

test('Clone', () => {
const q = new SortedQueue<number>(compare);
const q = new MinHeapQueue<number>(compare);
for (let i = 0; i < 10; ++i) {
const s = Math.random();
const n = Math.floor(100 * s);
Expand Down
Expand Up @@ -46,11 +46,14 @@ function takeFromHeap<T>(t: T[], compare: (a: T, b: T) => number): T | undefined
return result;
}

export class SortedQueue<T> implements IterableIterator<T> {
/**
* MinHeapQueue - based upon a minHeap array.
*/
export class MinHeapQueue<T> implements IterableIterator<T> {
private values: T[] = [];
constructor(readonly compare: (a: T, b: T) => number) {}

add(t: T): SortedQueue<T> {
add(t: T): MinHeapQueue<T> {
addToHeap(this.values, t, this.compare);
return this;
}
Expand All @@ -63,7 +66,7 @@ export class SortedQueue<T> implements IterableIterator<T> {
return takeFromHeap(this.values, this.compare);
}

concat(i: Iterable<T>): SortedQueue<T> {
concat(i: Iterable<T>): MinHeapQueue<T> {
for (const v of i) {
this.add(v);
}
Expand All @@ -86,8 +89,8 @@ export class SortedQueue<T> implements IterableIterator<T> {
return this;
}

clone(): SortedQueue<T> {
const clone = new SortedQueue(this.compare);
clone(): MinHeapQueue<T> {
const clone = new MinHeapQueue(this.compare);
clone.values = this.values.concat();
return clone;
}
Expand Down
51 changes: 51 additions & 0 deletions packages/cspell-lib/src/util/PairingHeap.test.ts
@@ -0,0 +1,51 @@
import { PairingHeap } from './PairingHeap';

describe('PairingHeap', () => {
test('Basic add and remove', () => {
const compare = new Intl.Collator().compare;
const values = ['one', 'two', 'three', 'four', 'five', 'six', 'seven'];
const sorted = values.concat().sort(compare);
const heap = new PairingHeap(compare);
values.forEach((v) => heap.add(v));
expect(heap.length).toBe(values.length);
const result = [...heap];
expect(result).toEqual(sorted);
expect(heap.length).toBe(0);
});

interface Person {
name: string;
}

test('FIFO for latest', () => {
const compareStr = new Intl.Collator().compare;
const compare = (a: Person, b: Person) => compareStr(a.name, b.name);
const names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'June', 'July'];
const people: Person[] = names.map((name) => ({ name }));
const sorted = people.concat().sort(compare);
const heap = new PairingHeap(compare);

heap.add(people[0]);
expect(heap.dequeue()).toBe(people[0]);
expect(heap.length).toBe(0);
expect(heap.peek()).toBeUndefined();
expect(heap.dequeue()).toBeUndefined();

heap.concat(people);
expect(heap.dequeue()).toBe(sorted[0]);
expect(heap.dequeue()).toBe(sorted[1]);
heap.concat(people);
expect(heap.dequeue()).toBe(sorted[0]);
expect(heap.dequeue()).toBe(sorted[1]);
expect(heap.peek()).toBe(sorted[2]);
expect(heap.dequeue()).toBe(sorted[2]);
expect(heap.dequeue()).toBe(sorted[2]);
expect(heap.peek()).toBe(sorted[3]);

heap.add(sorted[0]);
expect(heap.peek()).toBe(sorted[0]);
const copy = { ...sorted[0] };
// Make sure we get back the open we added.
expect(heap.add(copy).peek()).toBe(copy);
});
});
107 changes: 107 additions & 0 deletions packages/cspell-lib/src/util/PairingHeap.ts
@@ -0,0 +1,107 @@
export interface PairHeapNode<T> {
/** Value */
v: T;
/** Siblings */
s: PairHeapNode<T> | undefined;
/** Children */
c: PairHeapNode<T> | undefined;
}

export type CompareFn<T> = (a: T, b: T) => number;

export class PairingHeap<T> implements IterableIterator<T> {
private _heap: PairHeapNode<T> | undefined;
private _size = 0;

constructor(readonly compare: CompareFn<T>) {}

add(v: T): this {
this._heap = insert(this.compare, this._heap, v);
++this._size;
return this;
}

dequeue(): T | undefined {
const n = this.next();
if (n.done) return undefined;
return n.value;
}

concat(i: Iterable<T>): this {
for (const v of i) {
this.add(v);
}
return this;
}

next(): IteratorResult<T> {
if (!this._heap) {
return { value: undefined, done: true };
}
const value = this._heap.v;
--this._size;
this._heap = removeHead(this.compare, this._heap);
return { value };
}

peek(): T | undefined {
return this._heap?.v;
}

[Symbol.iterator](): IterableIterator<T> {
return this;
}

get length(): number {
return this._size;
}
}

function removeHead<T>(compare: CompareFn<T>, heap: PairHeapNode<T> | undefined): PairHeapNode<T> | undefined {
if (!heap || !heap.c) return undefined;
return mergeSiblings(compare, heap.c);
}

function insert<T>(compare: CompareFn<T>, heap: PairHeapNode<T> | undefined, v: T): PairHeapNode<T> {
const n: PairHeapNode<T> = {
v,
s: undefined,
c: undefined,
};

if (!heap || compare(v, heap.v) <= 0) {
n.c = heap;
return n;
}

n.s = heap.c;
heap.c = n;
return heap;
}

function merge<T>(compare: CompareFn<T>, a: PairHeapNode<T>, b: PairHeapNode<T>): PairHeapNode<T> {
if (compare(a.v, b.v) <= 0) {
a.s = undefined;
b.s = a.c;
a.c = b;
return a;
}
b.s = undefined;
a.s = b.c;
b.c = a;
return b;
}

function mergeSiblings<T>(compare: CompareFn<T>, n: PairHeapNode<T>): PairHeapNode<T> {
if (!n.s) return n;
const s = n.s;
const ss = s.s;
const m = merge(compare, n, s);
return ss ? merge(compare, m, mergeSiblings(compare, ss)) : m;
}

export const heapMethods = {
insert,
merge,
mergeSiblings,
};
14 changes: 7 additions & 7 deletions packages/cspell-lib/src/util/wordSplitter.ts
@@ -1,15 +1,15 @@
import { PairingHeap } from './PairingHeap';
import { escapeRegEx } from './regexHelper';
import { TextOffset } from './text';
import {
regExWordsAndDigits,
regExDanglingQuote,
regExEscapeCharacters,
regExPossibleWordBreaks,
regExSplitWords,
regExSplitWords2,
regExPossibleWordBreaks,
regExEscapeCharacters,
regExDanglingQuote,
regExTrailingEndings,
regExWordsAndDigits,
} from './textRegex';
import { SortedQueue } from './SortedQueue';
import { escapeRegEx } from './regexHelper';

const ignoreBreak: readonly number[] = Object.freeze([] as number[]);

Expand Down Expand Up @@ -388,7 +388,7 @@ function splitIntoWords(
}

let maxCost = lineSeg.relEnd - lineSeg.relStart;
const candidates = new SortedQueue<Candidate>(compare);
const candidates = new PairingHeap<Candidate>(compare);
const text = lineSeg.line.text;
candidates.concat(makeCandidates(undefined, lineSeg.relStart, 0, 0));
let attempts = 0;
Expand Down
51 changes: 51 additions & 0 deletions packages/cspell-trie-lib/src/lib/PairingHeap.test.ts
@@ -0,0 +1,51 @@
import { PairingHeap } from './PairingHeap';

describe('PairingHeap', () => {
test('Basic add and remove', () => {
const compare = new Intl.Collator().compare;
const values = ['one', 'two', 'three', 'four', 'five', 'six', 'seven'];
const sorted = values.concat().sort(compare);
const heap = new PairingHeap(compare);
values.forEach((v) => heap.add(v));
expect(heap.length).toBe(values.length);
const result = [...heap];
expect(result).toEqual(sorted);
expect(heap.length).toBe(0);
});

interface Person {
name: string;
}

test('FIFO for latest', () => {
const compareStr = new Intl.Collator().compare;
const compare = (a: Person, b: Person) => compareStr(a.name, b.name);
const names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'June', 'July'];
const people: Person[] = names.map((name) => ({ name }));
const sorted = people.concat().sort(compare);
const heap = new PairingHeap(compare);

heap.add(people[0]);
expect(heap.dequeue()).toBe(people[0]);
expect(heap.length).toBe(0);
expect(heap.peek()).toBeUndefined();
expect(heap.dequeue()).toBeUndefined();

heap.concat(people);
expect(heap.dequeue()).toBe(sorted[0]);
expect(heap.dequeue()).toBe(sorted[1]);
heap.concat(people);
expect(heap.dequeue()).toBe(sorted[0]);
expect(heap.dequeue()).toBe(sorted[1]);
expect(heap.peek()).toBe(sorted[2]);
expect(heap.dequeue()).toBe(sorted[2]);
expect(heap.dequeue()).toBe(sorted[2]);
expect(heap.peek()).toBe(sorted[3]);

heap.add(sorted[0]);
expect(heap.peek()).toBe(sorted[0]);
const copy = { ...sorted[0] };
// Make sure we get back the open we added.
expect(heap.add(copy).peek()).toBe(copy);
});
});

0 comments on commit aad4352

Please sign in to comment.