Skip to content

Cut per-chunk JS work while a reply streams - #4007

Closed
DoodleBears wants to merge 3 commits into
mainfrom
claude/project-thread-y44xqh
Closed

DoodleBears wants to merge 3 commits into
mainfrom
claude/project-thread-y44xqh

Conversation

@DoodleBears

@DoodleBears DoodleBears commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Why the change

This is round 5 of the iOS chat performance work for #3974. A JS profile of a streamed reply on an iPhone XS Max (281 chunks, 21.5k characters) had these items among its top costs. There is one commit per item, so each one can be A/B tested on the device in sequence.

  1. 4466556: text playback rescan. About 190 ms of JS self time in that run. On every chunk, text-playback.ts rebuilt its grapheme end list with filter, a spread and findIndex, which is O(n²) over a long reply. It now truncates in place and binary-searches the cursor.
    • A 59-seed differential fuzz against the old code gave identical snapshots.
    • In Node, a 20k-character reply went from 50–72 ms to 10–12 ms.
  2. a8029a6: context churn. propagateParentContextChanges took about 305 ms. PressableBubble passed a fresh closure to MediaLongPress.Provider on every render, so each 32 ms playback tick changed a context value and React propagated it through the streaming bubble's subtree. The bubble now provides a stable function that calls the latest handler.
  3. ee969ea: frame hex decode. Regex plus hexToBytes took about 170 ms. Encrypted channel frames are now validated and decoded in one lookup-table pass, with the same rejection rules: lowercase hex only, odd length, h or d prefix. In Node it is 3–4× faster on 281 frames.

Special things to note

  • Streamed drafts already arrive as appends, so decrypt cost is per frame, not quadratic. Swapping the cipher or the wire format is out of scope.
  • Device A/B results will be added here. Commits without a measured gain will be reverted before merge.

Change outline

  • apps/mobile/src/host/text-playback.ts
  • apps/mobile/src/ui/chat/message-bubble.tsx
  • packages/core/src/devices/noise/channel.ts
  • changes/ios-streaming-playback-scan.md

🤖 Generated with Claude Code

https://claude.ai/code/session_01Lpx6qkP973FGPmahjst8nu

text-playback rebuilt its grapheme end list with filter, spread and
findIndex on every update, which is O(n²) over a long reply. Truncate
in place and binary-search the cursor instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lpx6qkP973FGPmahjst8nu
@coderabbitai

coderabbitai Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Pre-merge checks: Inconclusive 2

Failed Failed checks: 2 inconclusive

Check name Status Explanation Resolution
Title check Pending This assessment has not completed. The check will be retried on the next pre-merge check run.
Description check Pending This assessment has not completed. The check will be retried on the next pre-merge check run.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

PressableBubble passed a fresh closure to MediaLongPress on every
render, so each streamed tick changed the context value and React
propagated it through the whole bubble subtree. Provide a stable
function that calls the latest handler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lpx6qkP973FGPmahjst8nu
Each streamed update went through a regex test plus noble's hexToBytes.
Validate and decode the hex in one pass instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lpx6qkP973FGPmahjst8nu
@DoodleBears DoodleBears changed the title Stop rescanning the whole reply on each streamed chunk Cut per-chunk JS work while a reply streams Nov 11, 2026
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.

2 participants