Skip to content

Commit 92b4f77

Browse files
authored
fix(logs): stabilize callbacks and memo-wrap components to eliminate re-render cascade (#3222)
1 parent c44211a commit 92b4f77

File tree

14 files changed

+579
-278
lines changed

14 files changed

+579
-278
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export type { StatusBarSegment } from './status-bar'
2-
export { default, StatusBar } from './status-bar'
2+
export { StatusBar } from './status-bar'

apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/status-bar/status-bar.tsx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export interface StatusBarSegment {
88
timestamp: string
99
}
1010

11-
export function StatusBar({
11+
function StatusBarInner({
1212
segments,
1313
selectedSegmentIndices,
1414
onSegmentClick,
@@ -127,4 +127,45 @@ export function StatusBar({
127127
)
128128
}
129129

130-
export default memo(StatusBar)
130+
/**
131+
* Custom equality function for StatusBar memo.
132+
* Performs structural comparison of segments array to avoid re-renders
133+
* when poll data returns new object references with identical content.
134+
*/
135+
function areStatusBarPropsEqual(
136+
prev: Parameters<typeof StatusBarInner>[0],
137+
next: Parameters<typeof StatusBarInner>[0]
138+
): boolean {
139+
if (prev.workflowId !== next.workflowId) return false
140+
if (prev.segmentDurationMs !== next.segmentDurationMs) return false
141+
if (prev.preferBelow !== next.preferBelow) return false
142+
143+
if (prev.selectedSegmentIndices !== next.selectedSegmentIndices) {
144+
if (!prev.selectedSegmentIndices || !next.selectedSegmentIndices) return false
145+
if (prev.selectedSegmentIndices.length !== next.selectedSegmentIndices.length) return false
146+
for (let i = 0; i < prev.selectedSegmentIndices.length; i++) {
147+
if (prev.selectedSegmentIndices[i] !== next.selectedSegmentIndices[i]) return false
148+
}
149+
}
150+
151+
if (prev.segments !== next.segments) {
152+
if (prev.segments.length !== next.segments.length) return false
153+
for (let i = 0; i < prev.segments.length; i++) {
154+
const ps = prev.segments[i]
155+
const ns = next.segments[i]
156+
if (
157+
ps.successRate !== ns.successRate ||
158+
ps.hasExecutions !== ns.hasExecutions ||
159+
ps.totalExecutions !== ns.totalExecutions ||
160+
ps.successfulExecutions !== ns.successfulExecutions ||
161+
ps.timestamp !== ns.timestamp
162+
) {
163+
return false
164+
}
165+
}
166+
}
167+
168+
return true
169+
}
170+
171+
export const StatusBar = memo(StatusBarInner, areStatusBarPropsEqual)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export type { WorkflowExecutionItem } from './workflows-list'
2-
export { default, WorkflowsList } from './workflows-list'
2+
export { WorkflowsList } from './workflows-list'

apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/workflows-list/workflows-list.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export interface WorkflowExecutionItem {
1414
overallSuccessRate: number
1515
}
1616

17-
export function WorkflowsList({
17+
function WorkflowsListInner({
1818
filteredExecutions,
1919
expandedWorkflowId,
2020
onToggleWorkflow,
@@ -103,7 +103,7 @@ export function WorkflowsList({
103103
<StatusBar
104104
segments={workflow.segments}
105105
selectedSegmentIndices={selectedSegments[workflow.workflowId] || null}
106-
onSegmentClick={onSegmentClick as any}
106+
onSegmentClick={onSegmentClick}
107107
workflowId={workflow.workflowId}
108108
segmentDurationMs={segmentDurationMs}
109109
preferBelow={idx < 2}
@@ -124,4 +124,4 @@ export function WorkflowsList({
124124
)
125125
}
126126

127-
export default memo(WorkflowsList)
127+
export const WorkflowsList = memo(WorkflowsListInner)

apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/dashboard.tsx

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
3+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import { Loader2 } from 'lucide-react'
55
import { Skeleton } from '@/components/ui/skeleton'
66
import { formatLatency } from '@/app/workspace/[workspaceId]/logs/utils'
@@ -141,31 +141,90 @@ function toWorkflowExecution(wf: WorkflowStats): WorkflowExecution {
141141
}
142142
}
143143

144-
export default function Dashboard({ stats, isLoading, error }: DashboardProps) {
144+
function DashboardInner({ stats, isLoading, error }: DashboardProps) {
145145
const [selectedSegments, setSelectedSegments] = useState<Record<string, number[]>>({})
146146
const [lastAnchorIndices, setLastAnchorIndices] = useState<Record<string, number>>({})
147-
const barsAreaRef = useRef<HTMLDivElement | null>(null)
147+
const lastAnchorIndicesRef = useRef<Record<string, number>>({})
148148

149149
const { workflowIds, searchQuery, toggleWorkflowId, timeRange } = useFilterStore()
150150

151151
const allWorkflows = useWorkflowRegistry((state) => state.workflows)
152152

153153
const expandedWorkflowId = workflowIds.length === 1 ? workflowIds[0] : null
154154

155-
const { executions, aggregateSegments, segmentMs } = useMemo(() => {
155+
const { rawExecutions, aggregateSegments, segmentMs } = useMemo(() => {
156156
if (!stats) {
157-
return { executions: [], aggregateSegments: [], segmentMs: 0 }
157+
return { rawExecutions: [], aggregateSegments: [], segmentMs: 0 }
158158
}
159159

160-
const workflowExecutions = stats.workflows.map(toWorkflowExecution)
161-
162160
return {
163-
executions: workflowExecutions,
161+
rawExecutions: stats.workflows.map(toWorkflowExecution),
164162
aggregateSegments: stats.aggregateSegments,
165163
segmentMs: stats.segmentMs,
166164
}
167165
}, [stats])
168166

167+
/**
168+
* Stabilize execution objects: reuse previous references for workflows
169+
* whose segment data hasn't structurally changed between polls.
170+
* This prevents cascading re-renders through WorkflowsList → StatusBar.
171+
*/
172+
const prevExecutionsRef = useRef<WorkflowExecution[]>([])
173+
174+
const executions = useMemo(() => {
175+
const prevMap = new Map(prevExecutionsRef.current.map((e) => [e.workflowId, e]))
176+
let anyChanged = false
177+
178+
const result = rawExecutions.map((exec) => {
179+
const prev = prevMap.get(exec.workflowId)
180+
if (!prev) {
181+
anyChanged = true
182+
return exec
183+
}
184+
if (
185+
prev.overallSuccessRate !== exec.overallSuccessRate ||
186+
prev.workflowName !== exec.workflowName ||
187+
prev.segments.length !== exec.segments.length
188+
) {
189+
anyChanged = true
190+
return exec
191+
}
192+
193+
for (let i = 0; i < prev.segments.length; i++) {
194+
const ps = prev.segments[i]
195+
const ns = exec.segments[i]
196+
if (
197+
ps.totalExecutions !== ns.totalExecutions ||
198+
ps.successfulExecutions !== ns.successfulExecutions ||
199+
ps.timestamp !== ns.timestamp ||
200+
ps.avgDurationMs !== ns.avgDurationMs ||
201+
ps.p50Ms !== ns.p50Ms ||
202+
ps.p90Ms !== ns.p90Ms ||
203+
ps.p99Ms !== ns.p99Ms
204+
) {
205+
anyChanged = true
206+
return exec
207+
}
208+
}
209+
210+
return prev
211+
})
212+
213+
if (
214+
!anyChanged &&
215+
result.length === prevExecutionsRef.current.length &&
216+
result.every((r, i) => r === prevExecutionsRef.current[i])
217+
) {
218+
return prevExecutionsRef.current
219+
}
220+
221+
return result
222+
}, [rawExecutions])
223+
224+
useEffect(() => {
225+
prevExecutionsRef.current = executions
226+
}, [executions])
227+
169228
const lastExecutionByWorkflow = useMemo(() => {
170229
const map = new Map<string, number>()
171230
for (const wf of executions) {
@@ -312,6 +371,8 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) {
312371
[toggleWorkflowId]
313372
)
314373

374+
lastAnchorIndicesRef.current = lastAnchorIndices
375+
315376
/**
316377
* Handles segment click for selecting time segments.
317378
* @param workflowId - The workflow containing the segment
@@ -361,7 +422,7 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) {
361422
} else if (mode === 'range') {
362423
setSelectedSegments((prev) => {
363424
const currentSegments = prev[workflowId] || []
364-
const anchor = lastAnchorIndices[workflowId] ?? segmentIndex
425+
const anchor = lastAnchorIndicesRef.current[workflowId] ?? segmentIndex
365426
const [start, end] =
366427
anchor < segmentIndex ? [anchor, segmentIndex] : [segmentIndex, anchor]
367428
const range = Array.from({ length: end - start + 1 }, (_, i) => start + i)
@@ -370,12 +431,12 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) {
370431
})
371432
}
372433
},
373-
[lastAnchorIndices]
434+
[]
374435
)
375436

376437
useEffect(() => {
377-
setSelectedSegments({})
378-
setLastAnchorIndices({})
438+
setSelectedSegments((prev) => (Object.keys(prev).length > 0 ? {} : prev))
439+
setLastAnchorIndices((prev) => (Object.keys(prev).length > 0 ? {} : prev))
379440
}, [stats, timeRange, workflowIds, searchQuery])
380441

381442
if (isLoading) {
@@ -493,7 +554,7 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) {
493554
</div>
494555
</div>
495556

496-
<div className='min-h-0 flex-1 overflow-hidden' ref={barsAreaRef}>
557+
<div className='min-h-0 flex-1 overflow-hidden'>
497558
<WorkflowsList
498559
filteredExecutions={filteredExecutions as WorkflowExecution[]}
499560
expandedWorkflowId={expandedWorkflowId}
@@ -507,3 +568,5 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) {
507568
</div>
508569
)
509570
}
571+
572+
export default memo(DashboardInner)

0 commit comments

Comments
 (0)