Skip to content

fix: streaming end event, iOS effects frame rate and two Android guards - #1167

Merged
hryhoriiK97 merged 4 commits into
perf/stream-measure-reusefrom
fix/stream-correctness
Nov 12, 2026
Merged

hryhoriiK97 merged 4 commits into
perf/stream-measure-reusefrom
fix/stream-correctness

Conversation

@hryhoriiK97

@hryhoriiK97 hryhoriiK97 commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

What/Why?

The second follow-up to #1140: three streaming fixes found in the stack's final review. It targets #1140's branch, so it lands with it.

Changes

  1. onStreamingEnd fires once the last words are on screen (all four hosts).
    • Before: every host fired it as streaming stopped, then rendered the whole text. A handler that scrolled to the end or measured saw the height from before the final render.
    • Now: the event waits for the first render scheduled after the stream ended. If that render asks for another in full (a view that missed a patch), it waits for that one too. It fires at once when there is nothing to render, or when a new stream starts first.
    • Docs and the prop's JSDoc say so.
  2. The iOS streaming effects display link is capped at 60 Hz and stops off a window.
    • It had no frame rate range, so it ran at 120 Hz on ProMotion screens while the core's pacer runs at 30.
    • It also kept ticking for a text view that left its window. It now starts again when the view is back on one.
  3. Two Android guards that iOS already has:
    • Stream-update key: compared whole as a data class, instead of a 32-bit hash that left out the screen density and the layout direction.
    • Resuming a text: a streamed text resumes at a top-level node only where the text before it ends in a newline, and renders in full elsewhere.

Testing

  • iOS unit tests: 149 of 149.
  • Android unit tests: 182 of 182, including new tests for a density or direction change measuring in full and for resuming only after a newline. Plus ktlint and the release build.
  • tsc.
  • Reviewed by Fable. The review found one hole, fixed before this PR: a stream restarting before the previous one's final render applied could lose or delay its onStreamingEnd.
  • iPhone 15 Pro (iOS 26.6.1, Release, 2 rounds of 4 streams per build, against feat: rebuild markdown streaming and add a streaming API #1140):
    • every stream calls onStreamingEnd once and ends at the height of a static mount;
    • process, main and JS thread CPU are unchanged within run-to-run noise (about ±1%).
    • The 60 Hz cap shows no gain there: the example app doesn't opt into 120 Hz, so its display links already ran at 60.
  • Android emulator (Pixel, Android 16, arm64, Release, 2 rounds of 3 streams per build): every stream calls onStreamingEnd once and ends at the static height.
    • The leased Android 16 phones were taken, and adb taps are blocked on the free Xiaomi phone, so Android ran on the emulator. The Android changes are about correctness, not speed.

PR Checklist

  • Code compiles and runs on iOS
  • Code compiles and runs on Android
  • Updated documentation/README if applicable
  • Ran example app to verify changes
  • E2E tests are passing (not run)
  • Required E2E tests have been added (if applicable): Android unit tests

🤖 Generated with Claude Code

hryhoriiK97 and others added 4 commits November 12, 2026 12:36
Every host called onStreamingEnd as streaming stopped, then rendered the
whole text: a handler that scrolled to the end or measured saw the height
before the final render. The event now waits for the first render
scheduled after the stream ended, unless that render asks for another
in full (a view that missed a patch), and fires at once when there is
nothing to render or a new stream starts first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t off a window

The display link that redraws fading text had no frame rate range, so it
ran at 120 Hz on ProMotion screens while the core's pacer runs at 30, and
it kept ticking for a text view that left its window while text arrived.
It now asks for 30 to 60 Hz, stops off a window, and starts again when
the text view is back on one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…irection

A stream update took the view's laid-out size when a 32-bit hash of the
layout props matched the last measurement's, and the hash left out the
screen density and the layout direction. A collision or a density or
direction change could hand back a size laid out for other props. The key
is now a data class compared whole, as iOS compares its key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resuming a text segment at a top-level node trusted that the node before
it ends in "\n"; a node kind that didn't would join the next one's first
line. The cache now records where the text before each node ends in a
newline and renders in full elsewhere. iOS guards its resume too, by
comparing the text before it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hryhoriiK97
hryhoriiK97 merged commit b35594c into perf/stream-measure-reuse Nov 12, 2026
2 checks passed
@hryhoriiK97
hryhoriiK97 deleted the fix/stream-correctness branch November 12, 2026 12:48
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