Repository navigation
perf(ios): bound the measurement cache and coalesce pending renders - #1138
hryhoriiK97 merged 2 commits into
Conversation
While a long document streams in, every revision queued its own render closure, and each one was parsed and rendered even though only the newest result applied. The measurement cache was capped at 512 entries, but each key holds the whole markdown, so streaming filled it with hundreds of progressively longer copies of the document. ENRMAsyncRenderCoordinator now keeps one render in flight and one replaceable pending render, and invalidate drops the pending one. MeasurementCache also caps the bytes its keys hold at 8 MiB and does not store a key larger than that. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The LRU list and the map each held a copy of every key, so long markdown was stored twice. The map now refers to the key its list node owns, and the byte budget counts one copy. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
Thanks for this. I verified it on a real device and pushed one small follow-up commit to the branch. What I changed
What I verifiedAll runs: iPhone 15 Pro (iOS 26.6.1), Release builds, the PR base
Not in this PR (follow-ups on our side)
|
Upstream merged the iOS streaming memory bounds (software-mansion/enriched-markdown#1138), so the fork drops its copy. Upstream also renders a newly mounted message in its first frame on iOS (software-mansion/enriched-markdown#1139). The fork adds GitHub's strikethrough rules for ~, links with hash-route fragments, \( \) and \[ \] math delimiters, and iOS Copy as Markdown that keeps bold spans whole, each with an upstream PR. Co-authored-by: ahalekelly <[email protected]> Co-authored-by: Tomer Danan <[email protected]> Co-authored-by: Dan Sanduleac <[email protected]> Co-authored-by: Yash Singh <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]>
Upstream merged the iOS streaming memory bounds (software-mansion/enriched-markdown#1138), so the fork drops its copy. Upstream also renders a newly mounted message in its first frame on iOS (software-mansion/enriched-markdown#1139). The fork adds GitHub's strikethrough rules for ~, links with hash-route fragments, \( \) and \[ \] math delimiters, and iOS Copy as Markdown that keeps bold spans whole, each with an upstream PR. Co-authored-by: ahalekelly <[email protected]> Co-authored-by: Tomer Danan <[email protected]> Co-authored-by: Dan Sanduleac <[email protected]> Co-authored-by: Yash Singh <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]>
Upstream merged this as software-mansion#1138 (f103cbc) with a maintainer follow-up, so take its version from the upstream merge. Co-Authored-By: Claude Opus 5.5 <[email protected]>
What/Why?
On iOS, memory grows with every revision while a long document streams in:
ENRMAsyncRenderCoordinatordispatches one closure perscheduleRendercall. Each closure captures its revision's markdown and style config, and the serial queue holds all of them until it works through the backlog. Every revision is parsed and rendered, even though only the newest result is applied.MeasurementCachekeeps up to 512 entries, but each key holds the whole markdown string twice (once in the LRU list, once in the map). Streaming measures a new key for every revision, so a long reply can fill the cache with hundreds of progressively longer copies of itself: tens of MB of key strings.We ran into this in T3 Code, whose mobile app renders long agent replies as they stream.
This PR:
ENRMAsyncRenderCoordinatorkeeps one render in flight and one pending. A call made before the pending render starts replaces it, and the superseded blocks, with the documents they capture, are released straight away. A burst of updates queues a single worker: the render already in flight finishes, then only the newest revision renders.invalidatealso drops the pending render, so a recycled view no longer parses its old document. The apply side is unchanged: a result applies only if no newer call came in.MeasurementCachealso caps the bytes its keys hold at 8 MiB, counting both copies of each key's strings, and evicts least recently used entries until a new key fits. A key larger than the whole budget is not cached and evicts nothing. The 512-entry limit stays.Testing
__tests__/ios:MeasurementCacheTests.mm: going over the byte budget evicts the least recently used document; a key over the budget is not stored and does not evict anything.ENRMAsyncRenderCoordinatorTests.mm: a burst renders only the newest revision after the in-flight one; a superseded pending render is released before it would run;invalidatedrops the pending render.mainand pass with this change.yarn turbo run test:ios --filter=react-native-enriched-markdown-exampleon an iPhone 16 Pro simulator (iOS 18.6): all 54 tests pass (53 library tests, including the 5 new ones, plus the harness placeholder). The two new suites also passed 100 iterations in a row (-test-iterations 100 -run-tests-until-failure).PR Checklist
Model: Claude Opus 5.5. Harness: Claude Code in T3 Code.