| ๆไปถ | ๆๅๆไบค่ฎฐๅฝ | ๆๅๆดๆฐๆถ้ด |
|---|---|---|
perf: message content styles and perf scripts (#3626) ## ๐ฏ Goal <!-- Describe why we are making this change --> ## ๐ Implementation details <!-- Provide a description of the implementation --> ## ๐จ UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## ๐งช Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## โ๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android | 3 ไธชๆๅ | |
perf: gallery perf improvements (#3662) ## ๐ฏ Goal The fullscreen ImageGallery was designed for in message galleries (<= 4 attachments per message). The newer `ChannelDetails` "Photos & Videos" feature opens the same gallery with 50โ100+ assets, which exposed several latent problems: - Android OOM crashes when swiping through video heavy galleries - ExoPlayer's default 50s buffer (for `react-native-video` as an example) + multiple simultaneous `Video` elements blew the Java heap even on mid tier devices - Visible swipe jank on Android driven by an avalanche of per swipe rerenders cascading from `ImageGallery` down through every mounted slide and every `useAnimatedStyle` worklet - Latent bugs: stale closures in the `Image.getSize` fallback for asset dimensions; phantom `isPlaying === true` state after slide unmount; `useState` for currentImageHeight causing a redundant parent re-render per swipe; redundant `[index, setIndex]` state in the gestures hook duplicating store-side currentIndex; etc The goal of this PR is to fix the crashes and reduce per swipe rerenders to the architectural minimum without changing any gallery interactions visible to the user. ## ๐ Implementation details Crash fix - `ExoPlayer` buffer config and instance capping - `AnimatedGalleryVideo` now uses a tighter mount window than the image side: `shouldRender = Math.abs(currentIndex - index) < 2` (3 slides - `prev`, `current`, `next`) instead of the original `< 4` (up to 7 slides). Image slides keep the `< 4` window since they're not subject to the same memory or decoder slot pressure - Caps simultaneous mounted `<NativeHandlers.Video>` elements at 3 regardless of how many video assets the gallery contains. This is the structural fix for the original Android crash: `ExoPlayer` instance count was the primary axis blowing the Java heap and 7+ simultaneous instances also exceeded Android's hardware `MediaCodec` decoder cap (~4โ6 slots on most mid-tier devices) - Hardcoded conservative buffer config in both `Video` wrappers (`native-package/optionalDependencies/Video.tsx` for `react-native-video` and `expo-package/optionalDependencies/Video.tsx` for `expo-video`). Each wrapper uses its lib's native API directly and we apply this automatically: - `react-native-video`: `bufferConfig` with `minBufferMs: 5000, maxBufferMs: 10000, bufferForPlaybackMs: 1000, bufferForPlaybackAfterRebufferMs: 2000` - `expo-video`: `player.bufferOptions = { preferredForwardBufferDuration: 10, minBufferForPlayback: 1, maxBufferBytes: 6_000_000 }` set inside the `useVideoPlayer` setup callback - Per instance Java heap source byte usage drops from `~30 MB` (50s of buffer read ahead at 5 Mbps) to ~6 MB. With three Video elements alive in the slide window, baseline drops from ~90 MB to ~18 MB on the 256 MB heap - Integrators with different needs can replace the `Video` wrapper via `registerNativeHandlers({ Video: ... })` Rerender reduction - single source of truth for `currentIndex` Historically, our gallery appears to be very unoptimized in React terms. Aside from having many unnecessary rerenders, we'd also rerender each attachment in the gallery on every index change. In addition to be terrible semantically, it's also something that we use purely for animations/gestures. Conversely, we don't particularly need state anywhere and so we move most of these interactions to purely shared values to make sure there is no work on the JS thread at all (or at least as little as possible in this iteration). For this, we introduce a SV version of `currentIndex` within the store itself which we keep in lockstep with `state.currentIndex` via the existing setter. Most notably: - Worklet consumers (gesture math in `useImageGalleryGestures`, transforms in `useAnimatedGalleryStyle`, the `useAnimatedReaction` in `ImageGallery` that drives `translationX`, the `useDerivedValue` for `headerFooterOpacity`) read `currentIndexShared.value` directly on the UI thread. This makes sure that animated styles are not recreated on every swipe - Dropped the redundant `[index, setIndex] = useState(currentIndex)` in `useImageGalleryGestures` - moveToNext/Previous read fresh from `store.state.getLatestValue()` - Slide components (`AnimatedGalleryImage`, `AnimatedGalleryVideo`) replaced their `useStateStore({ currentIndex })` subscription with a slim per instance `useCallback` selector returning only `{ shouldRender }`. `useStateStore`'s shallow equality will short circuit when the boolean doesn't flip so most slides stay silent during swipes; only the 1โ2 entering or leaving the mount window rerender - Wrapped `AnimatedGalleryVideo` in `React.memo` so parent rerenders don't cascade through to mounted video slides (I am mostly against this but due to how the gallery works right now it would appear that React compiler cannot figure out that it should stop rendering in these instances - Extracted the accessible `"X of N"` View into a separate `ImageGalleryA11yProbe` sibling component that subscribes to `currentIndex` locally only mounted when accessibility is enabled. `ImageGalleryWithContext` itself no longer subscribes to `currentIndex` at all. The calculation of `currentImageHeight` has also been moved to a SV. Smaller fixes - `AnimatedGalleryVideo`'s cleanup now calls `videoPlayer.onRemove()` (was just `playerRef = null`) and pauses the native player and zeroes pooled JS state (isPlaying, position, duration, progress) so a swipe back doesn't autoplay from the start with a stale state - Pause active player on index change moved from a `useEffect` in `ImageGallery` to the `currentIndex` setter on the store and keeps the invariant local to the state it depends on. - `useAnimatedGalleryStyle`'s worklet now derives `selected`/`previous` from `currentIndexShared.value` inside the worklet - Pan gesture classification threshold unified across iOS and Android (`maxXYRatio = 0.25` instead of `isAndroid ? 1 : 0.25`). The previous Android specific 1 made natural diagonal finger arcs misclassify as vertical, causing the page flip to snap back instead of advancing unless the gesture has greater momentum Note: There are many other, less obvious ways to improve the gallery however I'd like to do those in a separate PR as such heavy refactorings we'd like to do carefully. Also, here are some measured metrics (generated by the new performance script, specifically for Android): ``` โโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโฌโโโโโโโโโโฌโโโโโโโโโโโโโโโโโ โ Metric โ Develop โ Branch โ ฮ โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Memory โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Java Heap PSS โ 157 MB โ 99 MB โ โ58 MB (โ37%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Total PSS โ 1.22 GB โ 1.21 GB โ ~0 โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Views (Objects) โ 2552 โ 2367 โ โ185 (โ7%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Frame timing โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ 50p frame โ 26 ms โ 22 ms โ โ4 ms (โ15%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ 90p frame โ 73 ms โ 61 ms โ โ12 ms (โ16%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ 95p frame โ 117 ms โ 89 ms โ โ28 ms (โ24%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ 99p frame โ 250 ms โ 150 ms โ โ100 ms (โ40%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Frame quality โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Janky frames โ 20.7% โ 15.3% โ โ5.4pp (โ26%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Janky (legacy) โ 59.3% โ 41.2% โ โ18.1pp (โ31%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Slow bitmap uploads โ 17 โ 7 โ โ10 (โ59%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Slow UI thread โ 113 โ 107 โ โ6 (โ5%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Frame deadline missed โ 116 โ 111 โ โ5 (โ4%) โ โโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโดโโโโโโโโโโดโโโโโโโโโโโโโโโโโ ``` (done comparing the **worst** run on this branch as compared to the **best** run on [this one](https://github.com/GetStream/stream-chat-react-native/pulls)) ## ๐จ UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## ๐งช Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## โ๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android --------- Co-authored-by: Zita Szupera <szuperaz@gmail.com> | 2 ไธชๆๅ | |
perf: message content styles and perf scripts (#3626) ## ๐ฏ Goal <!-- Describe why we are making this change --> ## ๐ Implementation details <!-- Provide a description of the implementation --> ## ๐จ UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## ๐งช Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## โ๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android | 3 ไธชๆๅ | |
perf: message content styles and perf scripts (#3626) ## ๐ฏ Goal <!-- Describe why we are making this change --> ## ๐ Implementation details <!-- Provide a description of the implementation --> ## ๐จ UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## ๐งช Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## โ๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android | 3 ไธชๆๅ | |
perf: gallery perf improvements (#3662) ## ๐ฏ Goal The fullscreen ImageGallery was designed for in message galleries (<= 4 attachments per message). The newer `ChannelDetails` "Photos & Videos" feature opens the same gallery with 50โ100+ assets, which exposed several latent problems: - Android OOM crashes when swiping through video heavy galleries - ExoPlayer's default 50s buffer (for `react-native-video` as an example) + multiple simultaneous `Video` elements blew the Java heap even on mid tier devices - Visible swipe jank on Android driven by an avalanche of per swipe rerenders cascading from `ImageGallery` down through every mounted slide and every `useAnimatedStyle` worklet - Latent bugs: stale closures in the `Image.getSize` fallback for asset dimensions; phantom `isPlaying === true` state after slide unmount; `useState` for currentImageHeight causing a redundant parent re-render per swipe; redundant `[index, setIndex]` state in the gestures hook duplicating store-side currentIndex; etc The goal of this PR is to fix the crashes and reduce per swipe rerenders to the architectural minimum without changing any gallery interactions visible to the user. ## ๐ Implementation details Crash fix - `ExoPlayer` buffer config and instance capping - `AnimatedGalleryVideo` now uses a tighter mount window than the image side: `shouldRender = Math.abs(currentIndex - index) < 2` (3 slides - `prev`, `current`, `next`) instead of the original `< 4` (up to 7 slides). Image slides keep the `< 4` window since they're not subject to the same memory or decoder slot pressure - Caps simultaneous mounted `<NativeHandlers.Video>` elements at 3 regardless of how many video assets the gallery contains. This is the structural fix for the original Android crash: `ExoPlayer` instance count was the primary axis blowing the Java heap and 7+ simultaneous instances also exceeded Android's hardware `MediaCodec` decoder cap (~4โ6 slots on most mid-tier devices) - Hardcoded conservative buffer config in both `Video` wrappers (`native-package/optionalDependencies/Video.tsx` for `react-native-video` and `expo-package/optionalDependencies/Video.tsx` for `expo-video`). Each wrapper uses its lib's native API directly and we apply this automatically: - `react-native-video`: `bufferConfig` with `minBufferMs: 5000, maxBufferMs: 10000, bufferForPlaybackMs: 1000, bufferForPlaybackAfterRebufferMs: 2000` - `expo-video`: `player.bufferOptions = { preferredForwardBufferDuration: 10, minBufferForPlayback: 1, maxBufferBytes: 6_000_000 }` set inside the `useVideoPlayer` setup callback - Per instance Java heap source byte usage drops from `~30 MB` (50s of buffer read ahead at 5 Mbps) to ~6 MB. With three Video elements alive in the slide window, baseline drops from ~90 MB to ~18 MB on the 256 MB heap - Integrators with different needs can replace the `Video` wrapper via `registerNativeHandlers({ Video: ... })` Rerender reduction - single source of truth for `currentIndex` Historically, our gallery appears to be very unoptimized in React terms. Aside from having many unnecessary rerenders, we'd also rerender each attachment in the gallery on every index change. In addition to be terrible semantically, it's also something that we use purely for animations/gestures. Conversely, we don't particularly need state anywhere and so we move most of these interactions to purely shared values to make sure there is no work on the JS thread at all (or at least as little as possible in this iteration). For this, we introduce a SV version of `currentIndex` within the store itself which we keep in lockstep with `state.currentIndex` via the existing setter. Most notably: - Worklet consumers (gesture math in `useImageGalleryGestures`, transforms in `useAnimatedGalleryStyle`, the `useAnimatedReaction` in `ImageGallery` that drives `translationX`, the `useDerivedValue` for `headerFooterOpacity`) read `currentIndexShared.value` directly on the UI thread. This makes sure that animated styles are not recreated on every swipe - Dropped the redundant `[index, setIndex] = useState(currentIndex)` in `useImageGalleryGestures` - moveToNext/Previous read fresh from `store.state.getLatestValue()` - Slide components (`AnimatedGalleryImage`, `AnimatedGalleryVideo`) replaced their `useStateStore({ currentIndex })` subscription with a slim per instance `useCallback` selector returning only `{ shouldRender }`. `useStateStore`'s shallow equality will short circuit when the boolean doesn't flip so most slides stay silent during swipes; only the 1โ2 entering or leaving the mount window rerender - Wrapped `AnimatedGalleryVideo` in `React.memo` so parent rerenders don't cascade through to mounted video slides (I am mostly against this but due to how the gallery works right now it would appear that React compiler cannot figure out that it should stop rendering in these instances - Extracted the accessible `"X of N"` View into a separate `ImageGalleryA11yProbe` sibling component that subscribes to `currentIndex` locally only mounted when accessibility is enabled. `ImageGalleryWithContext` itself no longer subscribes to `currentIndex` at all. The calculation of `currentImageHeight` has also been moved to a SV. Smaller fixes - `AnimatedGalleryVideo`'s cleanup now calls `videoPlayer.onRemove()` (was just `playerRef = null`) and pauses the native player and zeroes pooled JS state (isPlaying, position, duration, progress) so a swipe back doesn't autoplay from the start with a stale state - Pause active player on index change moved from a `useEffect` in `ImageGallery` to the `currentIndex` setter on the store and keeps the invariant local to the state it depends on. - `useAnimatedGalleryStyle`'s worklet now derives `selected`/`previous` from `currentIndexShared.value` inside the worklet - Pan gesture classification threshold unified across iOS and Android (`maxXYRatio = 0.25` instead of `isAndroid ? 1 : 0.25`). The previous Android specific 1 made natural diagonal finger arcs misclassify as vertical, causing the page flip to snap back instead of advancing unless the gesture has greater momentum Note: There are many other, less obvious ways to improve the gallery however I'd like to do those in a separate PR as such heavy refactorings we'd like to do carefully. Also, here are some measured metrics (generated by the new performance script, specifically for Android): ``` โโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโฌโโโโโโโโโโฌโโโโโโโโโโโโโโโโโ โ Metric โ Develop โ Branch โ ฮ โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Memory โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Java Heap PSS โ 157 MB โ 99 MB โ โ58 MB (โ37%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Total PSS โ 1.22 GB โ 1.21 GB โ ~0 โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Views (Objects) โ 2552 โ 2367 โ โ185 (โ7%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Frame timing โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ 50p frame โ 26 ms โ 22 ms โ โ4 ms (โ15%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ 90p frame โ 73 ms โ 61 ms โ โ12 ms (โ16%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ 95p frame โ 117 ms โ 89 ms โ โ28 ms (โ24%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ 99p frame โ 250 ms โ 150 ms โ โ100 ms (โ40%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Frame quality โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Janky frames โ 20.7% โ 15.3% โ โ5.4pp (โ26%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Janky (legacy) โ 59.3% โ 41.2% โ โ18.1pp (โ31%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Slow bitmap uploads โ 17 โ 7 โ โ10 (โ59%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Slow UI thread โ 113 โ 107 โ โ6 (โ5%) โ โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโโโโโโโค โ Frame deadline missed โ 116 โ 111 โ โ5 (โ4%) โ โโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโดโโโโโโโโโโดโโโโโโโโโโโโโโโโโ ``` (done comparing the **worst** run on this branch as compared to the **best** run on [this one](https://github.com/GetStream/stream-chat-react-native/pulls)) ## ๐จ UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## ๐งช Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## โ๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android --------- Co-authored-by: Zita Szupera <szuperaz@gmail.com> | 2 ไธชๆๅ | |
perf: screen reader queries (#3674) ## ๐ฏ Goal `useScreenReaderEnabled` used to attach its own `AccessibilityInfo` listener and fire its own `isScreenReaderEnabled()` native query on **every mount**. It runs in every `Message` row (twice, via `useMessageActionHandlers`) plus the composer, so the app requeried the OS and resubscribed on every row mount/remount - work that grows with message count and scrolling. This PR introduces a single app-wide **`ScreenReaderContext`**: one provider performs one query + one listener, and every consumer reads the value from context. `useScreenReaderEnabled` collapses to a context read; its public signature and the `enabled` / `forceScreenReaderMode` gating are unchanged. ### On-device perf-testing tooling (initial pass WIP) In addition to all of this, the PR also introduces a bootstrapping on device perf testing setup we'll keep iterating on. It's in its early conception still, but so far it's been able to run its own iteration over our `SampleApp` (on Android) and add performance related probes so as to measure this particular change. The result was ~300 microseconds less work for each message to render (asynchronous, of course, non blocking for the UI, but still). This work is of course hugely negligible and will never matter in the grand scheme of things (especially for something gated behind a flag like a11y), but it's still very interesting that it is able to find this out on a fresh agent, with almost no interaction from my side (after many, many iterations :D ). It comes with its own "verbs" (rudimentary for now) related to app actions and only works with ADB. Later own we'll extend this further and potentially add iOS support as well. Features so far: - `perf/scenario-lib.sh` โ reusable adb/uiautomator device-driving verbs (wait-on-testID, tap, retry tap, scroll, count). - `perf/scenario-template.sh` + `perf/drive-channel-scenario.sh` - template + first scenario. - A `perf-benchmarking` skill capturing the methodology and pitfalls, containing some instructions on how to actually do actionable things in the app A more proper technical document for this will be included later. ## ๐ Implementation details <!-- Provide a description of the implementation --> ## ๐จ UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## ๐งช Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## โ๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android | 2 ไธชๆๅ | |
perf: message content styles and perf scripts (#3626) ## ๐ฏ Goal <!-- Describe why we are making this change --> ## ๐ Implementation details <!-- Provide a description of the implementation --> ## ๐จ UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## ๐งช Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## โ๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android | 3 ไธชๆๅ | |
perf: message content styles and perf scripts (#3626) ## ๐ฏ Goal <!-- Describe why we are making this change --> ## ๐ Implementation details <!-- Provide a description of the implementation --> ## ๐จ UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## ๐งช Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## โ๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android | 3 ไธชๆๅ | |
perf: screen reader queries (#3674) ## ๐ฏ Goal `useScreenReaderEnabled` used to attach its own `AccessibilityInfo` listener and fire its own `isScreenReaderEnabled()` native query on **every mount**. It runs in every `Message` row (twice, via `useMessageActionHandlers`) plus the composer, so the app requeried the OS and resubscribed on every row mount/remount - work that grows with message count and scrolling. This PR introduces a single app-wide **`ScreenReaderContext`**: one provider performs one query + one listener, and every consumer reads the value from context. `useScreenReaderEnabled` collapses to a context read; its public signature and the `enabled` / `forceScreenReaderMode` gating are unchanged. ### On-device perf-testing tooling (initial pass WIP) In addition to all of this, the PR also introduces a bootstrapping on device perf testing setup we'll keep iterating on. It's in its early conception still, but so far it's been able to run its own iteration over our `SampleApp` (on Android) and add performance related probes so as to measure this particular change. The result was ~300 microseconds less work for each message to render (asynchronous, of course, non blocking for the UI, but still). This work is of course hugely negligible and will never matter in the grand scheme of things (especially for something gated behind a flag like a11y), but it's still very interesting that it is able to find this out on a fresh agent, with almost no interaction from my side (after many, many iterations :D ). It comes with its own "verbs" (rudimentary for now) related to app actions and only works with ADB. Later own we'll extend this further and potentially add iOS support as well. Features so far: - `perf/scenario-lib.sh` โ reusable adb/uiautomator device-driving verbs (wait-on-testID, tap, retry tap, scroll, count). - `perf/scenario-template.sh` + `perf/drive-channel-scenario.sh` - template + first scenario. - A `perf-benchmarking` skill capturing the methodology and pitfalls, containing some instructions on how to actually do actionable things in the app A more proper technical document for this will be included later. ## ๐ Implementation details <!-- Provide a description of the implementation --> ## ๐จ UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## ๐งช Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## โ๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android | 2 ไธชๆๅ | |
perf: gallery virtualization (#3675) ## ๐ฏ Goal Our image gallery was never properly virtualized and mounted one slide component per asset for **all** loaded media (`assets.map(...)`), so scroll/swipe jank scaled with the asset count. On a media heavy channel roughly **40% of frames were janky**, driven by `O(N)` animated style worklets rerunning on the UI thread every frame. This bounds that cost so gallery performance no longer degrades as channels accumulate media. This was relatively fine before, when `ChannelDetails` hadn't been introduced yet but now poses an actual challenge. ## ๐ Implementation details - Extracted the slide list into a windowed `GalleryPager` that mounts only the slides within `PAGER_WINDOW_RADIUS` of the current index, instead of all N. Mounted slides drop from ~N to ~9. - A single **leading spacer** reproduces the flex width of the skipped slides, so the rendered slides keep their exact natural positions. The per slide transforms (`useAnimatedGalleryStyle`) are a pure function of `index` + `flex` position, so windowing the mount should be fine - `GalleryPager` subscribes to `currentIndex` itself, so paging rerenders only the small slide list, never the parent (gesture objects/`GestureDetector` stay stable). - The existing per slide `shouldRender` load gate is retained (image +-3, video +-1) for now so windowing bounds the *mount*; `shouldRender` still bounds *content load* (notably capping live native video players within the mounted set). Measured on a debug `SampleApp` (on a media heavy channel), provided below are the results (naturally, taken from the best 5 runs against baseline and the worst 5 runs against this branch across of many, many runs): | Metric | Before | After | Improvement | |---|---|---|---| | **Janky frames** | 40.4% | **14.1%** | **โ65%** (โ26 pts) | | **Median frame time** | 42 ms | **21 ms** | **2ร faster** (โ50%) | | 90th-pct frame time | 79 ms | 46 ms | โ42% | | 95th-pct frame time | 95 ms | 67 ms | โ29% | | 99th-pct frame time | 150 ms | 133 ms | โ11% | | **Missed vsyncs** | 154 | **57** | **2.7ร fewer** (โ63%) | | Frames rendered (same path) | 856 | 1,039 | +21% throughput | | Mounted slide components | ~165 | **~9** | **~18ร fewer** | Jank no longer scales with asset count (after jank is flat across N, where before it rose). ## ๐จ UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## ๐งช Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## โ๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android | 2 ไธชๆๅ | |
perf: gallery virtualization (#3675) ## ๐ฏ Goal Our image gallery was never properly virtualized and mounted one slide component per asset for **all** loaded media (`assets.map(...)`), so scroll/swipe jank scaled with the asset count. On a media heavy channel roughly **40% of frames were janky**, driven by `O(N)` animated style worklets rerunning on the UI thread every frame. This bounds that cost so gallery performance no longer degrades as channels accumulate media. This was relatively fine before, when `ChannelDetails` hadn't been introduced yet but now poses an actual challenge. ## ๐ Implementation details - Extracted the slide list into a windowed `GalleryPager` that mounts only the slides within `PAGER_WINDOW_RADIUS` of the current index, instead of all N. Mounted slides drop from ~N to ~9. - A single **leading spacer** reproduces the flex width of the skipped slides, so the rendered slides keep their exact natural positions. The per slide transforms (`useAnimatedGalleryStyle`) are a pure function of `index` + `flex` position, so windowing the mount should be fine - `GalleryPager` subscribes to `currentIndex` itself, so paging rerenders only the small slide list, never the parent (gesture objects/`GestureDetector` stay stable). - The existing per slide `shouldRender` load gate is retained (image +-3, video +-1) for now so windowing bounds the *mount*; `shouldRender` still bounds *content load* (notably capping live native video players within the mounted set). Measured on a debug `SampleApp` (on a media heavy channel), provided below are the results (naturally, taken from the best 5 runs against baseline and the worst 5 runs against this branch across of many, many runs): | Metric | Before | After | Improvement | |---|---|---|---| | **Janky frames** | 40.4% | **14.1%** | **โ65%** (โ26 pts) | | **Median frame time** | 42 ms | **21 ms** | **2ร faster** (โ50%) | | 90th-pct frame time | 79 ms | 46 ms | โ42% | | 95th-pct frame time | 95 ms | 67 ms | โ29% | | 99th-pct frame time | 150 ms | 133 ms | โ11% | | **Missed vsyncs** | 154 | **57** | **2.7ร fewer** (โ63%) | | Frames rendered (same path) | 856 | 1,039 | +21% throughput | | Mounted slide components | ~165 | **~9** | **~18ร fewer** | Jank no longer scales with asset count (after jank is flat across N, where before it rose). ## ๐จ UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## ๐งช Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## โ๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android | 2 ไธชๆๅ | |
perf: screen reader queries (#3674) ## ๐ฏ Goal `useScreenReaderEnabled` used to attach its own `AccessibilityInfo` listener and fire its own `isScreenReaderEnabled()` native query on **every mount**. It runs in every `Message` row (twice, via `useMessageActionHandlers`) plus the composer, so the app requeried the OS and resubscribed on every row mount/remount - work that grows with message count and scrolling. This PR introduces a single app-wide **`ScreenReaderContext`**: one provider performs one query + one listener, and every consumer reads the value from context. `useScreenReaderEnabled` collapses to a context read; its public signature and the `enabled` / `forceScreenReaderMode` gating are unchanged. ### On-device perf-testing tooling (initial pass WIP) In addition to all of this, the PR also introduces a bootstrapping on device perf testing setup we'll keep iterating on. It's in its early conception still, but so far it's been able to run its own iteration over our `SampleApp` (on Android) and add performance related probes so as to measure this particular change. The result was ~300 microseconds less work for each message to render (asynchronous, of course, non blocking for the UI, but still). This work is of course hugely negligible and will never matter in the grand scheme of things (especially for something gated behind a flag like a11y), but it's still very interesting that it is able to find this out on a fresh agent, with almost no interaction from my side (after many, many iterations :D ). It comes with its own "verbs" (rudimentary for now) related to app actions and only works with ADB. Later own we'll extend this further and potentially add iOS support as well. Features so far: - `perf/scenario-lib.sh` โ reusable adb/uiautomator device-driving verbs (wait-on-testID, tap, retry tap, scroll, count). - `perf/scenario-template.sh` + `perf/drive-channel-scenario.sh` - template + first scenario. - A `perf-benchmarking` skill capturing the methodology and pitfalls, containing some instructions on how to actually do actionable things in the app A more proper technical document for this will be included later. ## ๐ Implementation details <!-- Provide a description of the implementation --> ## ๐จ UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## ๐งช Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## โ๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android | 2 ไธชๆๅ |
perf/
Profiling tooling for the SDK row-render perf initiative.
Capture a .cpuprofile
Two options.
A) Via the helper script
node perf/capture-hermes-profile.js
Connects to Metro's Hermes target, starts profiling, waits for Enter, writes a .cpuprofile into perf/profiles/, then auto-runs the analyzer.
B) Via Chrome DevTools
- Run SampleApp on a device. Make sure Metro is up.
- Chromium โ
chrome://inspectโ click inspect on the Hermes target. - DevTools โ Performance tab โ Record (Cmd+E).
- Do your scenario (open a channel with 30+ messages; scroll to trigger renders).
- Stop.
- Right-click the recording โ Save profileโฆ โ save into
perf/profiles/(gitignored).
A 10โ15 second profile is plenty for analysis.
Analyze a single profile
node perf/analyze-cpuprofile.js perf/profiles/baseline.cpuprofile
Outputs:
- Profile summary (duration, sample count, sample rate).
- โ warning if the profile looks un-desymbolicated.
- Time by category โ auto-bucketed by source:
Idle,GC,npm: <package>,SDK source,App source,builtin:Object,builtin:JSON,VM / native. No hand-curated patterns. - Time by source file.
- Top functions by self time (where the JS thread actually sits).
- Top functions by total time (which call sites dominate).
Optional drilldown into specific functions:
node perf/analyze-cpuprofile.js perf/profiles/x.cpuprofile \
--inside MessageWithContext,useCreateMessageContext,renderText
Desymbolicate (per-package buckets)
Dev profiles collapse every frame into one Metro bundle URL, so categorization shows everything as App source. To recover per-package attribution, fetch Metro's source map and run the desymbolicator:
curl -s 'http://localhost:8081/index.map?platform=ios&dev=true&minify=false' \
-o /tmp/dev.map.json
node perf/desymbolicate-cpuprofile.js perf/profiles/x.cpuprofile /tmp/dev.map.json
node perf/analyze-cpuprofile.js perf/profiles/x.desymbolicated.cpuprofile
Diff two profiles (before vs after a change)
node perf/analyze-cpuprofile.js --diff perf/profiles/before.cpuprofile perf/profiles/after.cpuprofile
Per-category self-time delta + top function self-time deltas (sorted by |delta|). Optional --grep <pattern> to zoom in on specific function names. Warns if sample rates between the two profiles diverge >10%.
For a fair diff, capture both profiles using the same scenario and the same device in roughly the same conditions.
Android heap/codec/frame capture (memory & jank diagnostics)
For perf work where the bottleneck is memory pressure, MediaCodec slot usage, or frame timing โ not JS-thread CPU โ use the adb-based heap dump script.
perf/android-heap-dump.sh branch
perf/android-heap-dump.sh develop # after switching branches + rebuild
The script captures dumpsys meminfo, gfxinfo, media.codec, and procstats for the SampleApp and writes the combined output to perf/profiles/android-heap-<label>-<timestamp>.txt. Pre-test recipe (warming the video pool, resetting frame counters) is documented in the script header.
For A/B comparisons, run the same scenario on both branches and diff the Native Heap, Dalvik Heap, TOTAL PSS, MediaCodec instance count, and frame-time percentiles between the two output files.
Conventions
- Keep captured
.cpuprofilefiles inperf/profiles/(gitignored). - Name files descriptively:
baseline.cpuprofile,step-8.cpuprofile, etc. - Profiles must be captured in dev mode (Metro) so function names are intact. Release builds are minified โ desymbolicate with the matching source map if you need to analyze one.