Shopify / Shopify/react-native-skia

Low Atlas performance on cheap Androids

Open
#2,521 4 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

bug
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>
  )

}

bunnies

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.