Skip to content

app: timeline row reconciliation uses Effect deep equality on every stream delta #48434

Closed

Description

@Priyansh4444

Description

Every time the session timeline updates, reuseTimelineRows compares each new row against the previous one with Effect's Equal.equals. For a 320 turn session that is 1,288 rows per streamed delta, and each comparison walks the whole row structure, including every message and part ref inside PartGroup. While a reply streams, about a third of JS samples in a profile land in Effect's equal/hash code.

A comparator that switches on the row's _tag and compares only that variant's fields does the same job much faster. Isolated benchmark over a 1,288 row timeline, 1,000 updates per run, one process per build: 11.5 ms per update before, 0.64 ms after. The browser profile shows no samples in Effect equal/hash after the change.

Steps to reproduce

  1. Open a session with 300 or more turns in the web app.
  2. Send a message and let the reply stream.
  3. Record a performance profile while it streams.
  4. The timeline rebuilds and compares all rows on each streamed part delta, so equal/hash samples grow with the stream.

OpenCode version

dev, commit 193de13.

Operating System

Linux x86_64.

Terminal

Not applicable, this is the web app.

A draft PR with the fix and measurements: #48435

Activity

  1. github-actions commented on Sep 11, 2026

    @github-actions
    Contributor

    This issue doesn't fully meet our contributing guidelines.

    What needs to be fixed:

    • No issue template was used. Please resubmit using one of the three required templates: Bug Report, Feature Request, or Question. For this performance issue, the Bug Report template would be appropriate — it requires a Description field with real content and some context about how to reproduce (e.g., steps, session size, environment info).

    Please edit this issue to address the above within 2 hours, or it will be automatically closed.

    If you believe this was flagged incorrectly, please let a maintainer know.

  2. Priyansh4444 commented on Sep 11, 2026

    @Priyansh4444
    Author

    Draft PR with the measured fix: #48435.

    Summary of what is verified: the comparator agrees with Equal.equals for every row variant pair as a unit test in the normal CI suite, the isolated reconcile A/B reproduces at 11,527 us to 642 us per reconcile (-94.4%, A/A within 1.7%), and the browser run shows script time and long tasks roughly halved with identical rendered output. Full bytecode and counter evidence is in the PR comment.

  3. github-actions commented on Sep 11, 2026

    @github-actions
    Contributor

    Thanks for updating the issue! It now includes a clear description, reproduction steps, version, and environment info. Everything looks good — the needs:compliance label has been removed.

  4. cloudcat214 commented on Sep 19, 2026

    @cloudcat214

    Same hotspot reproduced on the Electron desktop client — 15 real freezes over 3 days

    Environment: Windows 11 · OpenCode Desktop 1.18.31 (Electron) · renderer bundle main-mfFBJ4oT.js · third-party OpenAI-compatible provider.

    window.log recorded 15 renderer unresponsive events between 2026-09-16 and 2026-09-18, durations 0.93 s – 34.55 s (median approx. 5.8 s, total approx. 116 s).

    The captured stack samples land on exactly the path described in this issue:

    at structureKeys (main-mfFBJ4oT.js:44196:3)
    at structure (main-mfFBJ4oT.js:44201:27)
    at hash (main-mfFBJ4oT.js:44137:20)
    at compareObjects (main-mfFBJ4oT.js:44268:23)
    at Object.equals2 [as equals] (main-mfFBJ4oT.js:139111:12)
    at reuseTimelineRows (main-mfFBJ4oT.js:139130:22)
    

    A second cluster of samples lands on message-row construction instead:

    at constructMessageRows (main-mfFBJ4oT.js:139242:49)
    at Array.flatMap (<anonymous>)
    

    The same renderer.log accumulated roughly 119,186 ResizeObserver loop completed with undelivered notifications entries across sessions (cf. #43355).

    Timing correlation (freeze-time +/- 25 s window, from the server-side stream log): the 2026-09-18 11:28:25 freeze (1.66 s) happened while 6 sessions / 16 concurrent streams were active.

    Note: #48435 does not appear to be merged yet and 1.18.31 does not include the change, so desktop users still hit this path. I can share the raw event dump (JSONL, 15 entries with per-event stack samples) if that helps.

  5. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the detailed profiling and benchmark, @Priyansh4444! To restate the request: timeline row reconciliation (reuseTimelineRows) should stop using Effect's deep Equal.equals for every row on each streamed delta. Instead it would use a comparator that switches on the row's _tag and compares only that variant's fields, which cuts the per-update cost in long sessions.

    I checked current dev, and TimelineRow.equals still delegates to Equal.equals, so this still applies. Since reconciliation gives correct results and this is a speed-up rather than broken behaviour, I've filed it as an improvement. Maintainers will consider it along with your draft PR #48435.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions