Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions web/src/engine/common/web-utils/src/priority-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default class PriorityQueue<Type> {
this.comparator = comparator;

this.heap = (initialEntries ?? []).slice(0);
this.heapify();
this._heapify();
}

private static leftChildIndex(index: number): number {
Expand All @@ -67,13 +67,22 @@ export default class PriorityQueue<Type> {
* are properly satisfied.
* - O(N) when 'heapifying' the whole heap
* - O(N) worst-case for partial heap operations (as part of an enqueueAll)
* <p>
*/
private heapify(): void;
private heapify(start: number, end: number): void;
private heapify(start?: number, end?: number): void {
public heapify(): void {
this._heapify();
}

/**
* Maintains internal state, rearranging the internal state until all heap constraints
* are properly satisfied.
* - O(N) when 'heapifying' the whole heap
* - O(N) worst-case for partial heap operations (as part of an enqueueAll)
*/
private _heapify(): void;
private _heapify(start: number, end: number): void;
private _heapify(start?: number, end?: number): void {
if(start == undefined || end == undefined) {
this.heapify(0, this.count - 1);
this._heapify(0, this.count - 1);
return;
}

Expand Down Expand Up @@ -161,7 +170,7 @@ export default class PriorityQueue<Type> {
const firstParent = PriorityQueue.parentIndex(firstIndex);

// The 'parent' of index 0 will return -1, which is illegal.
this.heapify(firstParent >= 0 ? firstParent : 0, PriorityQueue.parentIndex(this.count-1));
this._heapify(firstParent >= 0 ? firstParent : 0, PriorityQueue.parentIndex(this.count-1));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,7 @@ export async function *getBestMatches(searchModules: SearchQuotientNode[], timer
let bestQueue = spaceQueue.dequeue();
const newResult = bestQueue.handleNextNode();
spaceQueue.enqueue(bestQueue);
spaceQueue.heapify();

if(newResult.type == 'none') {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export class LegacyQuotientRoot extends SearchQuotientRoot {
}

this.processed.push(new SearchResult(node));
this.bufferNode(node);
return {
type: 'complete',
cost: node.currentCost,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,21 @@ export class SearchCluster implements SearchQuotientNode {
private selectionQueue: PriorityQueue<SearchQuotientNode> = new PriorityQueue(PATH_QUEUE_COMPARATOR);
readonly spaceId: number;

// We use an array and not a PriorityQueue b/c batch-heapifying at a single point in time
// is cheaper than iteratively building a priority queue.
/**
* This tracks all paths that have reached the end of a viable input-matching path - even
* those of lower cost that produce the same correction as other paths.
* Holds all `incomingNode` child buffers - buffers to hold nodes processed by
* this SearchCluster but not yet by child SearchSpaces.
*/
private childBuffers: SearchNode[][] = [];

// We use an array and not a PriorityQueue b/c batch-heapifying at a single
// point in time is cheaper than iteratively building a priority queue.
/**
* This tracks all paths that have reached the end of a viable input-matching
* path - even those of lower cost that produce the same correction as other
* paths.
*
* When new input is received, its entries are then used to append edges to the path in order
* to find potential paths to reach a new viable end.
* When new input is received, its entries are then used to append edges to
* the path in order to find potential paths to reach a new viable end.
*/
private completedPaths?: SearchNode[] = [];

Expand Down Expand Up @@ -86,6 +93,7 @@ export class SearchCluster implements SearchQuotientNode {
}

lowestPossibleSingleCost = Math.min(lowestPossibleSingleCost, path.lowestPossibleSingleCost);
// path.addResultBuffer(this.)
}

this.spaceId = generateSpaceSeed();
Expand Down Expand Up @@ -151,8 +159,10 @@ export class SearchCluster implements SearchQuotientNode {
const bestPath = this.selectionQueue.dequeue();
const currentResult = bestPath.handleNextNode();
this.selectionQueue.enqueue(bestPath);
this.selectionQueue.heapify();

if(currentResult.type == 'complete') {
this.bufferNode(currentResult.finalNode);
this.completedPaths?.push(currentResult.finalNode);
currentResult.spaceId = this.spaceId;
}
Expand All @@ -164,8 +174,12 @@ export class SearchCluster implements SearchQuotientNode {
return this.completedPaths?.map((n => new SearchResult(n, this.spaceId))) ?? [];
}

public stopTrackingResults() {
delete this.completedPaths;
public addResultBuffer(nodeBuffer: SearchNode[]): void {
this.childBuffers.push(nodeBuffer);
}

private bufferNode(node: SearchNode) {
this.childBuffers.forEach((buf) => buf.push(node));
}

get model(): LexicalModelTypes.LexicalModel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,4 +238,12 @@ export interface SearchQuotientNode {
readonly constituentPaths: SearchQuotientSpur[][];

isSameSpace(space: SearchQuotientNode): boolean;

/**
* This is used among SearchSpaces to ensure that nodes processed by earlier portions
* of the correction-search dynamic graph are provided to all child SearchSpaces for
* construction of new portions of the graph corresponding to their modeled inputs.
* @param nodeBuffer
*/
addResultBuffer(nodeBuffer: SearchNode[]): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export class SearchQuotientRoot implements SearchQuotientNode {

private hasBeenProcessed: boolean = false;

/**
* Holds all `incomingNode` child buffers - buffers to hold nodes processed by
* this SearchPath but not yet by child SearchSpaces.
*/
private childBuffers: SearchNode[][] = [];

/**
* Constructs a fresh SearchSpace instance for used in predictive-text correction
* and suggestion searches.
Expand Down Expand Up @@ -69,6 +75,7 @@ export class SearchQuotientRoot implements SearchQuotientNode {

this.hasBeenProcessed = true;

this.bufferNode(this.rootNode);
return {
type: 'complete',
cost: 0,
Expand Down Expand Up @@ -151,4 +158,12 @@ export class SearchQuotientRoot implements SearchQuotientNode {
return parentMerge;
}
}

addResultBuffer(nodeBuffer: SearchNode[]): void {
this.childBuffers.push(nodeBuffer);
}

protected bufferNode(node: SearchNode) {
this.childBuffers.forEach((buf) => buf.push(node));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ export const QUEUE_NODE_COMPARATOR: Comparator<SearchNode> = function(arg1, arg2
// Whenever a wordbreak boundary is crossed, a new instance should be made.
export abstract class SearchQuotientSpur implements SearchQuotientNode {
private selectionQueue: PriorityQueue<SearchNode> = new PriorityQueue(QUEUE_NODE_COMPARATOR);

/**
* Holds all incoming Nodes generated from a parent `SearchSpace` that have not yet been
* extended with this `SearchSpace`'s input.
*/
private incomingNodes: SearchNode[] = [];

/**
* Holds all `incomingNode` child buffers - buffers to hold nodes processed by
* this SearchPath but not yet by child SearchSpaces.
*/
private childBuffers: SearchNode[][] = [];

readonly inputs?: Distribution<Transform>;
readonly inputSource?: PathInputProperties;

Expand Down Expand Up @@ -97,6 +110,9 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode {
this.lowestPossibleSingleCost = (parentNode?.lowestPossibleSingleCost ?? 0) - Math.log(inputSrc?.bestProbFromSet ?? 1);
this.inputs = inputs?.length > 0 ? inputs : null;
this.inputCount = (parentNode?.inputCount ?? 0) + (this.inputs ? 1 : 0);

this.queueNodes(this.buildEdgesForNodes(parentNode.previousResults.map(r => r.node)));
parentNode.addResultBuffer(this.incomingNodes);
}

public get model(): LexicalModel {
Expand Down Expand Up @@ -365,6 +381,14 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode {
}

public get currentCost(): number {
if(this.incomingNodes.length > 0) {
this.queueNodes(this.buildEdgesForNodes(this.incomingNodes));

// Preserve the array instance, but trash all entries.
// The array is registered with the parent; do not replace!
this.incomingNodes.splice(0, this.incomingNodes.length);
}

const parentCost = this.parentNode?.currentCost ?? Number.POSITIVE_INFINITY;
const localCost = this.selectionQueue.peek()?.currentCost ?? Number.POSITIVE_INFINITY;

Expand All @@ -384,17 +408,31 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode {
* @returns
*/
public handleNextNode(): PathResult {
if(this.incomingNodes.length > 0) {
this.queueNodes(this.buildEdgesForNodes(this.incomingNodes));

// Preserve the array instance, but trash all entries.
// The array is registered with the parent; do not replace!
this.incomingNodes.splice(0, this.incomingNodes.length);
}

const parentCost = this.parentNode?.currentCost ?? Number.POSITIVE_INFINITY;
const localCost = this.selectionQueue.peek()?.currentCost ?? Number.POSITIVE_INFINITY;

if(parentCost <= localCost) {
if(parentCost < localCost) {
if(parentCost == Number.POSITIVE_INFINITY) {
return {
type: 'none'
};
}

const result = this.parentNode.handleNextNode();
// The parent will insert the node into our queue. We don't need it, though
// any siblings certainly will.

// Preserve the array instance, but trash all entries.
// The array is registered with the parent; do not replace!
this.incomingNodes.splice(0, this.incomingNodes.length);

if(result.type == 'complete') {
this.queueNodes(this.buildEdgesForNodes([result.finalNode]));
Expand Down Expand Up @@ -460,6 +498,7 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode {
}
}

this.bufferNode(currentNode);
return {
type: 'complete',
cost: currentNode.currentCost,
Expand All @@ -476,6 +515,14 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode {
return Object.values(this.returnedValues ?? {}).map(v => new SearchResult(v));
}

public addResultBuffer(nodeBuffer: SearchNode[]): void {
this.childBuffers.push(nodeBuffer);
}

private bufferNode(node: SearchNode) {
this.childBuffers.forEach((buf) => buf.push(node));
}

public get inputSegments(): InputSegment[] {
if(!this.parentNode) {
return [];
Expand Down
Loading