From 9375ac95cc49a6966399ae9a7be75269cd03dc37 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 27 Oct 2026 07:46:10 -0500 Subject: [PATCH] fix(ui): publish long-thread metrics after StrictMode cleanup Co-Authored-By: Paperclip (cherry picked from commit 3807ee526d2e761f6aa420452b390287542682bd) --- ui/src/pages/IssueChatLongThreadPerf.test.tsx | 58 +++++++++++++++++++ ui/src/pages/IssueChatLongThreadPerf.tsx | 30 ++++++---- 2 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 ui/src/pages/IssueChatLongThreadPerf.test.tsx diff --git a/ui/src/pages/IssueChatLongThreadPerf.test.tsx b/ui/src/pages/IssueChatLongThreadPerf.test.tsx new file mode 100644 index 00000000000..40505c4a214 --- /dev/null +++ b/ui/src/pages/IssueChatLongThreadPerf.test.tsx @@ -0,0 +1,58 @@ +// @vitest-environment jsdom + +import { StrictMode } from "react"; +import { flushSync } from "react-dom"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, expect, it, vi } from "vitest"; +import { IssueChatLongThreadPerf } from "./IssueChatLongThreadPerf"; + +vi.mock("../components/IssueChatThread", () => ({ IssueChatThread: () => null })); + +const mounted: Array<{ root: Root; container: HTMLDivElement }> = []; + +beforeEach(() => vi.useFakeTimers()); +afterEach(() => { + for (const { root, container } of mounted.splice(0)) { + flushSync(() => root.unmount()); + container.remove(); + } + vi.useRealTimers(); +}); + +function mount(strict: boolean) { + const container = document.createElement("div"); + document.body.append(container); + const root = createRoot(container); + const view = { root, container }; + mounted.push(view); + flushSync(() => root.render(strict ? : )); + return view; +} + +function metric(container: HTMLElement, name: string) { + return container.querySelector(`[data-testid="perf-${name}"]`)!.textContent!; +} + +it.each([false, true])("publishes finite actual fixture metrics with StrictMode=%s", strict => { + const { container } = mount(strict); + flushSync(() => vi.runOnlyPendingTimers()); + for (const name of ["mount-duration", "latest-duration", "max-duration", "total-duration"]) + expect(metric(container, name)).toMatch(/^\d+(?:\.\d+)? ms$/); + expect(Number(metric(container, "commit-count"))).toBeGreaterThanOrEqual(1); + expect(vi.getTimerCount()).toBe(0); +}); + +it("cancels unpublished metrics on unmount and allows a fresh StrictMode mount", () => { + const first = mount(true); + expect(vi.getTimerCount()).toBe(1); + flushSync(() => first.root.unmount()); + first.container.remove(); + mounted.splice(mounted.indexOf(first), 1); + expect(vi.getTimerCount()).toBe(0); + flushSync(() => vi.runOnlyPendingTimers()); + expect(first.container.textContent).toBe(""); + const second = mount(true); + flushSync(() => vi.runOnlyPendingTimers()); + expect(metric(second.container, "mount-duration")).toMatch(/^\d+(?:\.\d+)? ms$/); + expect(vi.getTimerCount()).toBe(0); +}); diff --git a/ui/src/pages/IssueChatLongThreadPerf.tsx b/ui/src/pages/IssueChatLongThreadPerf.tsx index e8cecbd9847..15be7fecae4 100644 --- a/ui/src/pages/IssueChatLongThreadPerf.tsx +++ b/ui/src/pages/IssueChatLongThreadPerf.tsx @@ -1,4 +1,4 @@ -import { Profiler, useEffect, useLayoutEffect, useMemo, useRef, useState, type ProfilerOnRenderCallback } from "react"; +import { Profiler, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ProfilerOnRenderCallback } from "react"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { IssueChatThread } from "../components/IssueChatThread"; @@ -63,12 +63,25 @@ export function IssueChatLongThreadPerf() { [], ); - useEffect(() => () => { - if (publishTimerRef.current !== null) { - window.clearTimeout(publishTimerRef.current); - } + const schedulePublication = useCallback(() => { + if (publishedRef.current || publishTimerRef.current !== null) return; + publishTimerRef.current = window.setTimeout(() => { + publishTimerRef.current = null; + publishedRef.current = true; + setMetrics(metricsRef.current); + }, 0); }, []); + useEffect(() => { + if (metricsRef.current.commitCount > 0) schedulePublication(); + return () => { + if (publishTimerRef.current !== null) { + window.clearTimeout(publishTimerRef.current); + publishTimerRef.current = null; + } + }; + }, [schedulePublication]); + useLayoutEffect(() => { if (publishedRef.current || metricsRef.current.commitCount > 0) return; const mountDuration = performance.now() - renderStartedAtRef.current; @@ -96,12 +109,7 @@ export function IssueChatLongThreadPerf() { totalActualDuration: current.totalActualDuration + actualDuration, }; - if (publishedRef.current || publishTimerRef.current !== null) return; - publishTimerRef.current = window.setTimeout(() => { - publishTimerRef.current = null; - publishedRef.current = true; - setMetrics(metricsRef.current); - }, 0); + schedulePublication(); }; return (