From b95dcd005166e52d8265eaa57c477c664dd9c591 Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Thu, 30 Jul 2026 16:00:16 -0700 Subject: [PATCH] [web-console] Fix flaky log-search highlight race in LogList MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit virtua scrolls to an estimated offset for rows outside the rendered window and mounts the row over several frames, but the post-scroll repaint only scheduled a single requestAnimationFrame — if the row wasn't mounted on that frame, the highlight was dropped with no retry beyond an incidental onscroll. Retry every frame until the row actually mounts (bounded) instead of assuming one frame is enough. PR #6695 papered over this by bumping the test's poll timeout, which didn't fix the underlying race and it flaked again in CI. Signed-off-by: Ben Pfaff --- js-packages/common-ui/src/lib/LogList.svelte | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/js-packages/common-ui/src/lib/LogList.svelte b/js-packages/common-ui/src/lib/LogList.svelte index ea6651ae3cf..00081660242 100644 --- a/js-packages/common-ui/src/lib/LogList.svelte +++ b/js-packages/common-ui/src/lib/LogList.svelte @@ -128,6 +128,15 @@ // Bring the match into view ONCE per submission. Tracked only on the SearchState fields, // and the lookup runs `untrack`ed so streaming updates can't re-trigger the scroll — the // user retains scroll control after the initial jump. + // + // Jumping to a row outside the currently-rendered window makes virtua scroll to an + // *estimated* offset, measure the newly-mounted row, then correct the scroll position — + // that can take several frames, not just one, especially under CI load. A single + // `requestAnimationFrame(paintHighlight)` would race that: if the row isn't mounted yet on + // that frame, `paintHighlight` deletes the highlight and nothing repaints it afterward + // unless a stray `onscroll` happens to fire. Retry every frame until the row lands (or give + // up after MAX_PAINT_RETRY_FRAMES, so a match that never mounts can't spin forever). + const MAX_PAINT_RETRY_FRAMES = 60 $effect(() => { const pattern = search.pattern const occ = search.occurrenceIndex @@ -139,9 +148,13 @@ // jumped to. They regain auto-scroll by scrolling back to the bottom themselves. reverseScroll.stickToBottom = false virtualizer?.scrollToIndex(idx, { align: 'center' }) - // virtua mounts the freshly-visible row on the next frame; paint then so the Range is - // created against the new DOM node rather than the previously-cleared one. - requestAnimationFrame(paintHighlight) + let attempts = 0 + const tryPaint = () => { + paintHighlight() + const mounted = scrollContainer?.querySelector(`[data-rowindex="${idx}"]`) + if (!mounted && attempts++ < MAX_PAINT_RETRY_FRAMES) requestAnimationFrame(tryPaint) + } + requestAnimationFrame(tryPaint) }) })