Shopify / Shopify/react-native-skia
Low Atlas performance on cheap Androids
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 8.6k
- Forks
- 647
- Avg merge
- 1d 17h
- Merged PRs (30d)
- 35
Description
Description
I've implemented a bunnymark demo on top of React native Skia, and I use Atlas for sprite rendering.
It all works perfect on my iPhone 12 mini, and renders 10000 bunnies every frame without any frame drops. Slight frame drops start at 15000, and only become significant after 20-25k bunnies.
However, when running on OPPO A16 (a lower-end Android smartphone), the maximum number of bunnies that can run without frame drops is 300. After thorough debugging I discovered performance throttling happens due to multiple JsiRSXform.set calls happening in the same frame. This is basically how useRSXformBuffer hook works.
So, I'm thinking: shouldn't we batch our transforms buffer updates into one JSI call per frame per Atlas?
I've also tried creating 10000 bunnies, but only animate first 500. I wanted to confirm whether JsiRSXform.set calls throttling is the only reason. Unfortunately, this is also slow and animates at around 5 FPS. Presumably, due to the following code in JsiSkCanvas.h:
std::vector<SkRSXform> xforms;
int xformsSize = static_cast<int>(transforms.size(runtime));
xforms.reserve(xformsSize);
for (int i = 0; i < xformsSize; i++) {
auto xform = JsiSkRSXform::fromValue(
runtime, transforms.getValueAtIndex(runtime, i).asObject(runtime));
xforms.push_back(*xform.get());
}
std::vector<SkRect> skRects;
int rectsSize = static_cast<int>(rects.size(runtime));
skRects.reserve(rectsSize);
for (int i = 0; i < rectsSize; i++) {
auto rect = JsiSkRect::fromValue(
runtime, rects.getValueAtIndex(runtime, i).asObject(runtime));
skRects.push_back(*rect.get());
}
Version
1.2.3
Steps to reproduce
Run example code provided on a lower-end Android smartphone, under $100-150.
Snack, code example, screenshot, or link to a repository
Here is the code:
import {Canvas, Atlas, Fill, Text, rect, useRSXformBuffer, useImage, useFont} from '@shopify/react-native-skia'
import {runOnJS, useSharedValue, withRepeat, withTiming} from 'react-native-reanimated'
import {useEffect, useState} from 'react'
import {Dimensions} from 'react-native'
import {GestureDetector, Gesture} from 'react-native-gesture-handler'
const bunnyImage = require('../assets/bunnies.png')
const bunnyFont = require('../assets/fonts/SpaceMono-Regular.ttf')
const window = Dimensions.get('window')
const gravity = 0.5
const size = { width: 30, height: 45 }
export default () => {
const [count, setCount] = useState(10000)
const image = useImage(bunnyImage)
const font = useFont(bunnyFont, 12);
const label = useSharedValue<string>('0')
const ticks = useSharedValue(0)
const tap = Gesture.Tap().onStart(() => {
runOnJS(setCount)(count + 100)
})
useEffect(() => {
ticks.value = withRepeat(withTiming(1, { duration: 500 }), -1, true)
}, [])
const sprites = new Array(count).fill(0).map((_, i) => rect((i % 5) * size.width, 0, size.width, size.height))
const transforms = useRSXformBuffer(count, (val, i) => {
'worklet'
const tickerUsage = ticks
if (i === 0) label.value = 'Bunnies: ' + count
// @ts-ignore
const bunnies = global['bunnies'] || (global['bunnies'] = [])
const bunny = bunnies[i] || (bunnies[i] = {
x: 0,
y: 0,
speedX: Math.random() * 2 - 1,
speedY: Math.random() * 2 - 1,
})
const translateX = bunny.x - size.width / 2
const translateY = bunny.y - size.height
bunny.x += bunny.speedX
bunny.y += bunny.speedY
bunny.speedY += gravity
if (bunny.x < size.width / 2) {
bunny.x = size.width / 2
bunny.speedX *= -1
} else if (bunny.x > window.width - size.width / 2) {
bunny.x = window.width - size.width / 2
bunny.speedX *= -1
}
if (bunny.y < 0) {
bunny.y = 0
bunny.speedY *= -1
} else if (bunny.y > window.height + size.height) {
bunny.speedY *= -0.85
bunny.y = window.height + size.height
if (Math.random() > 0.5) {
bunny.speedY -= Math.random() * 6
}
}
// if (i < 500)
val.set(1, 0, translateX, translateY)
})
return (
<GestureDetector gesture={tap}>
<Canvas style={{ flex: 1 }}>
<Atlas image={image} sprites={sprites} transforms={transforms} />
<Text x={30} y={60} text={label} font={font} />
</Canvas>
</GestureDetector>
)
}
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reproducing the Atlas bunnymark on the OPPO A16 using the provided example, then inspect useRSXformBuffer and the transform handling in JsiSkCanvas.h. Compare the cost of repeated JsiRSXform.set calls with the 500-transform case; done means Atlas animation no longer suffers the reported low-end Android throttling and the example remains correct.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react-native, typescript
- Domain
- frontend, mobile-dev, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100