diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/distance-modeler.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/distance-modeler.ts index c7f3efb3d9b..31e62eff38b 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/distance-modeler.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/distance-modeler.ts @@ -628,42 +628,43 @@ export class SearchResult { } } -// Current best guesstimate of how compositor will retrieve ideal corrections. -export async function *getBestMatches(searchSpace: SearchQuotientNode, timer: ExecutionTimer): AsyncGenerator { - let currentReturns: {[resultKey: string]: SearchNode} = {}; +/** + * Searches for the best available corrections from among the provided + * SearchSpaces, ending after the configured timer has elapsed or all available + * corrections have been enumerated. + * @param searchModules + * @param timer + * @returns + */ +export async function *getBestMatches(searchModules: SearchQuotientNode[], timer: ExecutionTimer): AsyncGenerator { + const spaceQueue = new PriorityQueue((a, b) => a.currentCost - b.currentCost); - // Stage 1 - if we already have extracted results, build a queue just for them and iterate over it first. - const returnedValues = Object.values(searchSpace.previousResults); - if(returnedValues.length > 0) { - let preprocessedQueue = new PriorityQueue((a, b) => a.totalCost - b.totalCost, returnedValues); - - while(preprocessedQueue.count > 0) { - const entryFromCache = timer.time(() => { - let entry = preprocessedQueue.dequeue(); - - currentReturns[entry.node.resultKey] = entry.node; - // Do not track yielded time. - return entry; - }, TimedTaskTypes.CACHED_RESULT); - - if(entryFromCache) { - // Time yielded here is generally spent on turning corrections into predictions. - // It's timing a different sort of task, so... different task set ID. - const timeSpan = timer.start(TimedTaskTypes.PREDICTING); - yield entryFromCache; - timeSpan.end(); - - if(timer.timeSinceLastDefer > STANDARD_TIME_BETWEEN_DEFERS) { - await timer.defer(); - } - } - } - } + // Stage 1 - if we already have extracted results, build a queue just for them + // and iterate over it first. + // + // Does not get any results that another iterator pulls up after this is + // created - and those results won't come up later in stage 2, either. Only + // intended for restarting a search, not searching twice in parallel. + const priorResultsQueue = new PriorityQueue((a, b) => a.totalCost - b.totalCost); + priorResultsQueue.enqueueAll(searchModules.map((space) => space.previousResults).flat()); + + // With potential prior results re-queued, NOW enqueue. (Not before - the heap may reheapify!) + spaceQueue.enqueueAll(searchModules); + + let currentReturns: {[resultKey: string]: SearchNode} = {}; // Stage 2: the fun part; actually searching! do { - const entry = timer.time(() => { - let newResult: PathResult = searchSpace.handleNextNode(); + const entry: SearchResult = timer.time(() => { + if((priorResultsQueue.peek()?.totalCost ?? Number.POSITIVE_INFINITY) < spaceQueue.peek().currentCost) { + const result = priorResultsQueue.dequeue(); + currentReturns[result.node.resultKey] = result.node; + return result; + } + + let lowestCostSource = spaceQueue.dequeue(); + let newResult: PathResult = lowestCostSource.handleNextNode(); + spaceQueue.enqueue(lowestCostSource); if(newResult.type == 'none') { return null; @@ -703,7 +704,7 @@ export async function *getBestMatches(searchSpace: SearchQuotientNode, timer: Ex if(timer.timeSinceLastDefer > STANDARD_TIME_BETWEEN_DEFERS) { await timer.defer(); } - } while(!timer.elapsed && searchSpace.currentCost < Number.POSITIVE_INFINITY); + } while(!timer.elapsed && spaceQueue.peek().currentCost < Number.POSITIVE_INFINITY); return null; } diff --git a/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts b/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts index 3785753b623..00d2ab78367 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts @@ -11,7 +11,6 @@ import { ContextTransition } from './correction/context-transition.js'; import { ExecutionTimer } from './correction/execution-timer.js'; import ModelCompositor from './model-compositor.js'; import { getBestMatches } from './correction/distance-modeler.js'; -import { SearchQuotientSpur } from './correction/search-quotient-spur.js'; const searchForProperty = defaultWordbreaker.searchForProperty; @@ -496,7 +495,7 @@ export async function correctAndEnumerate( let rawPredictions: CorrectionPredictionTuple[] = []; let bestCorrectionCost: number; const correctionPredictionMap: Record> = {}; - for await(const match of getBestMatches(searchModules[0] as SearchQuotientSpur, timer)) { + for await(const match of getBestMatches(searchModules, timer)) { // Corrections obtained: now to predict from them! const correction = match.matchString; const searchSpace = searchModules.find(s => s.spaceId == match.spaceId); diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-token.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-token.tests.ts index d34ec99d26d..c254da154bf 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-token.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-token.tests.ts @@ -59,7 +59,7 @@ describe('ContextToken', function() { assert.isFalse(token.isWhitespace); // While searchSpace has no inputs, it _can_ match lexicon entries (via insertions). - let searchIterator = getBestMatches(token.searchModule, new ExecutionTimer(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY)); + let searchIterator = getBestMatches([token.searchModule], new ExecutionTimer(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY)); let firstEntry = await searchIterator.next(); assert.isFalse(firstEntry.done); }); diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts index 5fb3433578c..5e64aa309c5 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts @@ -98,7 +98,7 @@ describe('getBestMatches', () => { const searchSpace = new LegacyQuotientRoot(testModel); const timer = buildTestTimer(); - const iter = getBestMatches(searchSpace, timer); + const iter = getBestMatches([searchSpace], timer); // While there's no input, insertion operations can produce suggestions. const resultState = await iter.next(); @@ -154,7 +154,7 @@ describe('getBestMatches', () => { assert.notEqual(searchPath2.spaceId, searchPath1.spaceId); assert.notEqual(searchPath3.spaceId, searchPath2.spaceId); - const iter = getBestMatches(searchPath3, buildTestTimer()); // disables the correction-search timeout. + const iter = getBestMatches([searchPath3], buildTestTimer()); // disables the correction-search timeout. await checkRepeatableResults_teh(iter); }); @@ -188,12 +188,12 @@ describe('getBestMatches', () => { assert.notEqual(searchPath2.spaceId, searchPath1.spaceId); assert.notEqual(searchPath3.spaceId, searchPath2.spaceId); - const iter = getBestMatches(searchPath3, buildTestTimer()); // disables the correction-search timeout. + const iter = getBestMatches([searchPath3], buildTestTimer()); // disables the correction-search timeout. await checkRepeatableResults_teh(iter); // The key: do we get the same results the second time? // Reset the iterator first... - const iter2 = getBestMatches(searchPath3, buildTestTimer()); // disables the correction-search timeout. + const iter2 = getBestMatches([searchPath3], buildTestTimer()); // disables the correction-search timeout. await checkRepeatableResults_teh(iter2); }); });