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 (