From f2e7f33d690cbc0041a4f273555324c1906715fa Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 4 May 2026 00:31:28 -0700 Subject: [PATCH] fix(ui): cap responsiveness event logs --- CHANGELOG.md | 1 + ui/src/ui/control-ui-performance.test.ts | 34 +++++++++++++++++++++-- ui/src/ui/control-ui-performance.ts | 35 ++++++++++++++++++++++-- 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c13b08e56f..0a4389b6e67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Control UI/Talk: make failed Talk startup errors dismissable and clear the stale Talk error state when dismissed, so missing realtime voice provider configuration does not leave a permanent chat banner. Fixes #77071. Thanks @ijoshdavis. +- Control UI/performance: cap long-task and long-animation-frame diagnostics in the shared event log, so slow-render telemetry does not evict gateway/plugin events from the Debug and Overview views. Thanks @vincentkoc. - Web fetch: late-bind `web_fetch` config and provider fallback metadata from the active runtime snapshot, matching `web_search` so long-lived tools do not use stale fetch provider settings. Thanks @vincentkoc. - Discord: clear stale startup probe bot/application status when the async bot probe throws, not just when it returns a degraded probe result. Thanks @vincentkoc. - Web search: scope explicit bundled `web_search` provider runtime loading through manifest ownership, so selecting DuckDuckGo/Gemini/etc. does not import unrelated bundled providers or log their optional dependency failures. Thanks @vincentkoc. diff --git a/ui/src/ui/control-ui-performance.test.ts b/ui/src/ui/control-ui-performance.test.ts index d8688639e8b..80368a47af6 100644 --- a/ui/src/ui/control-ui-performance.test.ts +++ b/ui/src/ui/control-ui-performance.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; +import type { EventLogEntry } from "./app-events.ts"; import { recordControlUiPerformanceEvent, startControlUiResponsivenessObserver, @@ -46,8 +47,8 @@ function installPerformanceObserverMock(options: { function createHost() { return { tab: "chat" as const, - eventLog: [] as Array<{ payload: Record }>, - eventLogBuffer: [] as Array<{ payload: Record }>, + eventLog: [] as EventLogEntry[], + eventLogBuffer: [] as EventLogEntry[], }; } @@ -161,6 +162,35 @@ describe("startControlUiResponsivenessObserver", () => { ]); }); + it("caps responsiveness events so gateway events stay visible", () => { + vi.spyOn(console, "warn").mockImplementation(() => undefined); + const mock = installPerformanceObserverMock({ + supportedEntryTypes: ["longtask"], + }); + const host = createHost(); + + for (let i = 0; i < 225; i += 1) { + recordControlUiPerformanceEvent(host, "gateway.event", { i }, { console: false }); + } + + startControlUiResponsivenessObserver(host); + for (let i = 0; i < 80; i += 1) { + mock.emit([ + { + name: "self", + startTime: i, + duration: 51, + } as unknown as PerformanceEntry, + ]); + } + + expect(host.eventLogBuffer).toHaveLength(250); + expect( + host.eventLogBuffer.filter((entry) => entry.event === "control-ui.longtask"), + ).toHaveLength(50); + expect(host.eventLogBuffer.some((entry) => entry.event === "gateway.event")).toBe(true); + }); + it("returns null when responsiveness entries are unsupported or observe fails", () => { installPerformanceObserverMock({ supportedEntryTypes: [] }); expect(startControlUiResponsivenessObserver(createHost())).toBeNull(); diff --git a/ui/src/ui/control-ui-performance.ts b/ui/src/ui/control-ui-performance.ts index 32cfb7a96ca..e34e38b892a 100644 --- a/ui/src/ui/control-ui-performance.ts +++ b/ui/src/ui/control-ui-performance.ts @@ -22,6 +22,7 @@ export type ControlUiRefreshRun = { const EVENT_LOG_LIMIT = 250; const SLOW_RPC_MS = 1_000; const RESPONSIVENESS_ENTRY_MS = 50; +const RESPONSIVENESS_EVENT_LOG_LIMIT = 50; type ControlUiResponsivenessObserver = { disconnect: () => void; @@ -86,11 +87,19 @@ export function recordControlUiPerformanceEvent( host: ControlUiPerformanceHost, event: string, payload: Record, - opts?: { warn?: boolean; console?: boolean }, + opts?: { warn?: boolean; console?: boolean; maxBufferedEventsForType?: number }, ) { const entry: EventLogEntry = { ts: Date.now(), event, payload }; if (Array.isArray(host.eventLogBuffer)) { - host.eventLogBuffer = [entry, ...host.eventLogBuffer].slice(0, EVENT_LOG_LIMIT); + const existingBuffer = + typeof opts?.maxBufferedEventsForType === "number" + ? keepLatestBufferedEventsForType( + host.eventLogBuffer, + event, + Math.max(0, opts.maxBufferedEventsForType - 1), + ) + : host.eventLogBuffer; + host.eventLogBuffer = [entry, ...existingBuffer].slice(0, EVENT_LOG_LIMIT); if (host.tab === "debug" || host.tab === "overview") { host.eventLog = host.eventLogBuffer; } @@ -101,6 +110,26 @@ export function recordControlUiPerformanceEvent( logPerformanceEvent(event, payload, opts?.warn === true); } +function keepLatestBufferedEventsForType( + entries: unknown[], + event: string, + maxExistingForType: number, +): unknown[] { + let keptForType = 0; + return entries.filter((entry) => { + if ( + !entry || + typeof entry !== "object" || + !("event" in entry) || + (entry as { event?: unknown }).event !== event + ) { + return true; + } + keptForType += 1; + return keptForType <= maxExistingForType; + }); +} + export function scheduleControlUiTabVisibleTiming( host: ControlUiPerformanceHost, previousTab: Tab, @@ -256,7 +285,7 @@ function recordResponsivenessEntry( scriptCount: Array.isArray(entry.scripts) ? entry.scripts.length : undefined, topScript: getTopLongAnimationFrameScript(entry.scripts), }, - { warn: true }, + { warn: true, maxBufferedEventsForType: RESPONSIVENESS_EVENT_LOG_LIMIT }, ); }