Skip to content

Commit f57a1c8

Browse files
fix: background analysis with proper stockfish handoff
Background loop runs both Maia + Stockfish on drill positions as they're played. When the drill ends, ensureDrillAnalysis: 1. Sets cancelled flag (loop exits after current position) 2. Calls stockfish.stopEvaluation() to abort any in-progress eval 3. Awaits the loop promise to ensure full exit 4. Only then starts its own stockfish work This avoids the concurrent generator bug where the old generator's finally block would clobber the new generator's engine.listen callback, causing the post-drill analysis to hang. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 60ef3f9 commit f57a1c8

1 file changed

Lines changed: 53 additions & 71 deletions

File tree

src/hooks/useOpeningDrillController/useOpeningDrillController.ts

Lines changed: 53 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -903,19 +903,19 @@ export const useOpeningDrillController = (
903903
[currentMaiaModel, stockfish],
904904
)
905905

906-
// Background analysis: run Maia + Stockfish on positions as they are played
907-
// so that post-drill analysis is already complete (or nearly so) when the
908-
// drill ends.
906+
// Background analysis: run Maia + Stockfish on drill positions as they are
907+
// played so post-drill analysis has less (or no) work to do.
909908
//
910-
// We use a ref-based queue so the async loop persists across re-renders.
911-
// The useEffect only *enqueues* new nodes; the loop processes them
912-
// independently of the React lifecycle to avoid being killed on every move.
913-
const backgroundQueueRef = useRef<GameNode[]>([])
914-
const backgroundRunningRef = useRef(false)
915-
const backgroundCancelledRef = useRef(false)
916-
917-
// Refs to the latest versions of the analysis functions so the long-lived
918-
// loop always calls the current closure without needing to restart.
909+
// There is a single Stockfish WASM instance, so we MUST NOT run background
910+
// and post-drill Stockfish concurrently. When the drill ends we:
911+
// 1. Set bgCancelledRef = true (loop exits after current position)
912+
// 2. Call stockfish.stopEvaluation() to abort any in-progress eval
913+
// 3. Await bgLoopPromiseRef to ensure the loop has fully exited
914+
// 4. Only then start ensureDrillAnalysis
915+
const bgQueueRef = useRef<GameNode[]>([])
916+
const bgRunningRef = useRef(false)
917+
const bgCancelledRef = useRef(false)
918+
const bgLoopPromiseRef = useRef<Promise<void> | null>(null)
919919
const ensureMaiaRef = useRef(ensureMaiaForNode)
920920
const ensureStockfishRef = useRef(ensureStockfishForNode)
921921
useEffect(() => {
@@ -925,51 +925,29 @@ export const useOpeningDrillController = (
925925
ensureStockfishRef.current = ensureStockfishForNode
926926
}, [ensureStockfishForNode])
927927

928-
// The long-lived loop that drains the queue.
929-
// Uses refs for everything so it never needs to be recreated.
930-
const runBackgroundLoop = useCallback(async () => {
931-
if (backgroundRunningRef.current) return
932-
backgroundRunningRef.current = true
933-
934-
console.log('[bg-analysis] loop started')
928+
const runBgLoop = useCallback(async () => {
929+
if (bgRunningRef.current) return
930+
bgRunningRef.current = true
935931
try {
936-
while (backgroundQueueRef.current.length > 0) {
937-
if (backgroundCancelledRef.current) {
938-
console.log('[bg-analysis] cancelled, stopping')
939-
break
940-
}
941-
const node = backgroundQueueRef.current[0]
942-
console.log(
943-
'[bg-analysis] processing node:',
944-
node.fen.split(' ').slice(0, 2).join(' '),
945-
)
932+
while (bgQueueRef.current.length > 0) {
933+
if (bgCancelledRef.current) break
934+
const node = bgQueueRef.current[0]
946935

947936
await ensureMaiaRef.current(node)
948-
if (backgroundCancelledRef.current) break
937+
if (bgCancelledRef.current) break
949938

950-
console.log(
951-
'[bg-analysis] maia done, starting stockfish for:',
952-
node.fen.split(' ').slice(0, 2).join(' '),
953-
)
954939
await ensureStockfishRef.current(node)
955-
956-
// Only remove from queue after both analyses complete (or if cancelled)
957-
backgroundQueueRef.current.shift()
958-
console.log(
959-
'[bg-analysis] node complete, remaining:',
960-
backgroundQueueRef.current.length,
961-
)
940+
bgQueueRef.current.shift()
962941
}
963942
} catch (error) {
964943
console.error('[bg-analysis] error:', error)
965944
} finally {
966-
backgroundRunningRef.current = false
967-
console.log('[bg-analysis] loop ended')
945+
bgRunningRef.current = false
968946
}
969947
// eslint-disable-next-line react-hooks/exhaustive-deps
970948
}, [])
971949

972-
// Enqueue new drill nodes whenever the tree grows
950+
// Enqueue new drill positions for background analysis
973951
useEffect(() => {
974952
if (!currentDrillGame || isAnalyzingDrill) return
975953

@@ -980,8 +958,7 @@ export const useOpeningDrillController = (
980958
: 0
981959
const drillNodes = mainLine.slice(startIndex)
982960

983-
// Track FENs already in the queue to avoid duplicates
984-
const queuedFens = new Set(backgroundQueueRef.current.map((n) => n.fen))
961+
const queuedFens = new Set(bgQueueRef.current.map((n) => n.fen))
985962

986963
const newNodes = drillNodes.filter((node) => {
987964
if (queuedFens.has(node.fen)) return false
@@ -995,47 +972,47 @@ export const useOpeningDrillController = (
995972
})
996973

997974
if (newNodes.length > 0) {
998-
console.log(
999-
'[bg-analysis] enqueueing',
1000-
newNodes.length,
1001-
'nodes, loop running:',
1002-
backgroundRunningRef.current,
1003-
)
1004-
backgroundQueueRef.current.push(...newNodes)
1005-
runBackgroundLoop()
975+
bgQueueRef.current.push(...newNodes)
976+
const promise = runBgLoop()
977+
if (promise) bgLoopPromiseRef.current = promise
1006978
}
1007979
}, [
1008980
currentDrillGame,
1009981
gameTree,
1010982
isAnalyzingDrill,
1011-
runBackgroundLoop,
983+
runBgLoop,
1012984
treeController.currentNode,
1013985
])
1014986

1015-
// Cancel background analysis when a new drill starts (not on every move).
1016-
// currentDrillGame changes on every move, so we track the drill ID instead.
1017-
const backgroundDrillIdRef = useRef<string | null>(null)
987+
// Cancel background analysis when a new drill starts
988+
const bgDrillIdRef = useRef<string | null>(null)
1018989
useEffect(() => {
1019990
const drillId = currentDrillGame?.id ?? null
1020-
if (drillId !== backgroundDrillIdRef.current) {
1021-
// New drill or drill cleared — cancel any running background work
1022-
backgroundCancelledRef.current = true
1023-
backgroundQueueRef.current = []
1024-
backgroundRunningRef.current = false
1025-
1026-
backgroundDrillIdRef.current = drillId
991+
if (drillId !== bgDrillIdRef.current) {
992+
bgCancelledRef.current = true
993+
bgQueueRef.current = []
994+
bgDrillIdRef.current = drillId
1027995
if (drillId) {
1028-
// Reset for the new drill
1029-
backgroundCancelledRef.current = false
996+
bgCancelledRef.current = false
1030997
}
1031998
}
1032999
}, [currentDrillGame?.id])
10331000

1001+
// Helper: stop background loop and wait for it to fully exit
1002+
const stopBackgroundAnalysis = useCallback(async () => {
1003+
bgCancelledRef.current = true
1004+
bgQueueRef.current = []
1005+
stockfish.stopEvaluation()
1006+
if (bgLoopPromiseRef.current) {
1007+
await bgLoopPromiseRef.current
1008+
bgLoopPromiseRef.current = null
1009+
}
1010+
}, [stockfish])
1011+
10341012
const ensureDrillAnalysis = useCallback(
10351013
async (drillGame: OpeningDrillGame): Promise<boolean> => {
1036-
// Stop background analysis so it doesn't compete for stockfish
1037-
backgroundCancelledRef.current = true
1038-
backgroundQueueRef.current = []
1014+
// Stop background loop and wait for it to fully exit before using stockfish
1015+
await stopBackgroundAnalysis()
10391016

10401017
const mainLine = drillGame.tree.getMainLine()
10411018
const startingNode = drillGame.openingEndNode || mainLine[0]
@@ -1111,7 +1088,12 @@ export const useOpeningDrillController = (
11111088

11121089
return !wasCancelled
11131090
},
1114-
[ensureMaiaForNode, ensureStockfishForNode, setDrillAnalysisProgress],
1091+
[
1092+
ensureMaiaForNode,
1093+
ensureStockfishForNode,
1094+
setDrillAnalysisProgress,
1095+
stopBackgroundAnalysis,
1096+
],
11151097
)
11161098

11171099
const cancelDrillAnalysis = useCallback(() => {

0 commit comments

Comments
 (0)