Skip to content

perf(macos): eliminate redundant model rebuilds in AssistantProgressView#25256

Merged
Jasonnnz merged 2 commits into
mainfrom
devin/1776096661-fix-progress-view-perf
Apr 13, 2026
Merged

perf(macos): eliminate redundant model rebuilds in AssistantProgressView#25256
Jasonnnz merged 2 commits into
mainfrom
devin/1776096661-fix-progress-view-perf

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot commented Apr 13, 2026

Summary

Fixes app hangs during scrolling caused by AssistantProgressView rebuilding its ProgressCardPresentationModel (an O(n) aggregation over all tool calls) 30+ times per body evaluation. The model was a computed property accessed by every sub-view, and two TimelineView instances ticking at 0.4s/1.0s amplified the cost by re-evaluating the entire view tree on each tick.

Three targeted changes, all internal to the progress card:

  1. Cache model as let binding in body — the O(n) build() runs once; all sub-views receive the cached value as a parameter instead of re-triggering the computed property.
  2. Move hasStrippedToolCalls into ProgressCardPresentationModel.build() — eliminates a separate O(n) scan that previously ran on every expansion check. Now computed once per build() call alongside the other aggregations.
  3. Extract TimelineView content into isolated child viewsProcessingDotsLabel (0.4s ticks) and ElapsedTimeLabel (1.0s ticks) are now self-contained structs, so their periodic re-evaluations only touch their own small subtree, not the entire progress card.

Removed the forwarding computed properties (phase, isActive, hasChevron, hasStrippedToolCalls) that provided no value and obscured the redundant model access pattern.

Zero UI/UX changes — identical rendering output, just faster.

Test coverage added for the new hasStrippedToolCalls model field:

  • hasStrippedToolCallsDetectsStrippedContent — complete tool call with all detail fields cleared → true
  • hasStrippedToolCallsFalseForNormalToolCall — complete tool call with populated inputFullfalse
  • hasStrippedToolCallsFalseForIncompleteToolCall — incomplete tool call with empty fields → false (only completed calls qualify)

References:

  • SwiftUI View body evaluation — body should be lightweight; expensive work should be cached
  • TimelineView — content closures re-evaluate on each tick; isolating into child views limits the blast radius

Alternatives not taken:

  • Replacing ViewThatFits — originally considered removing the adaptive header layout, but with the model cached, its double-measurement only reads local values instead of triggering O(n) rebuilds. Kept as-is to preserve the adaptive permission chip layout.
  • Caching model in onChange handlers — handlers like handlePhaseChange still access self.model (the computed property), but they run infrequently on state changes. Caching there would add complexity for negligible gain.

Review & Testing Checklist for Human

⚠️ CI skips macOS builds — all Swift compilation must be verified locally in Xcode.

  • Build in Xcode — this refactor changes many function signatures (computed properties → functions with parameters); verify it compiles cleanly. This is the highest-risk item since CI cannot catch Swift compiler errors.
  • Verify handlePhaseChange line 254 — the condition if newPhase == .processing, model.phase == .processing was previously phase == .processing (using the now-removed computed property). Both resolve to model.phase, so behavior is unchanged, but confirm the processing-start logic still fires correctly.
  • Test progress card phases end-to-end — trigger a multi-step tool run and verify all phases render correctly: Thinking → Tool Running → Processing (dots animation) → Completed N steps
  • Verify elapsed time counter — confirm the live timer appears after ~5s during active tool execution and shows final duration on completion
  • Scroll performance — scroll through a conversation with multiple progress cards and confirm the hang is resolved or significantly improved

Notes

  • hasStrippedToolCalls is computed via a .contains call inside build(), not folded into the main for-loop. This is still O(n) per build() call but runs once instead of on every property access — a significant improvement from the previous pattern where it ran on every expansion check.
  • Complementary to PR fix: eliminate _FlexFrameLayout anti-pattern from chat cell hierarchy #24589 (which fixed _FlexFrameLayout hangs in chat cells) — this PR addresses a different bottleneck in the same area.
  • The new tests construct ToolCallData directly (bypassing the makeToolCall helper) because they need explicit control over inputFull: "" — the helper defaults inputSummary to a non-empty string which propagates to inputFull.

Link to Devin session: https://app.devin.ai/sessions/d77235ad4bd54b82b0ef489b0d3c6b52
Requested by: @Jasonnnz


Open with Devin

Cache ProgressCardPresentationModel as a let binding in body so the O(n)
aggregation runs once per evaluation instead of 30+ times. Extract
TimelineView content into isolated child views (ProcessingDotsLabel,
ElapsedTimeLabel) so periodic ticks only re-evaluate their small subtree.
Move hasStrippedToolCalls into the model's build pass to avoid a separate
O(n) scan.

Co-Authored-By: Jason Zhou <jasonczhou3@gmail.com>
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

devin-ai-integration[bot]

This comment was marked as resolved.

Tests the three key cases: stripped complete tool call (true), normal
complete tool call with populated fields (false), and incomplete tool
call with empty fields (false — only completed calls qualify).

Co-Authored-By: Jason Zhou <jasonczhou3@gmail.com>
@Jasonnnz Jasonnnz merged commit 2821d37 into main Apr 13, 2026
6 checks passed
@Jasonnnz Jasonnnz deleted the devin/1776096661-fix-progress-view-perf branch April 13, 2026 17:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant