[Deep Dive] Measuring Turbo Modules (C++) vs. JavaScript functions in Vega

The Vega Developer Tools enable you to write C++ code via Turbo Modules to run within your apps. Turbo Modules can provide significant performance benefits by running lower-level code and supports code reusability from any existing projects and services your project requires.

In this dev guide, I’ll share some examples of performance improvements when using Turbo Modules + C++ code to handle computationally-heavy background tasks. In addition, I’ll share some comparisons with JavaScript functions and related sample code and templates to try out for yourself.

:bulb: Tip: Check out React Native’s docs cover on how “Turbo Native Modules” can bridge your app’s React / JavaScript logic with device-level capabilities.

Let’s dig in! :hammer_and_wrench:


Scenario: Bubble sorting 5000 elements after a Remote button press :bubbles: :person_facepalming:

One of the first activities in any Computer Science 101 course is sorting, and bubble sort tends to the be first method introduced due to it’s simplicity:

bubble-sort
(credit: Danny Correia)

That simplicity comes at a cost, namely the runtime and performance drag compared to more advanced recursive techniques such as Quicksort. Let’s now side-step the sorting technique debate and focus back on the performance benefits of offloading memory heavy operations from JavaScript to native C++ :slight_smile:

Step 1: Building a Kepler sample to compare memory heavy sorting tasks

Start with our standard “Color Picker” sample app template and update it to incorporate the following logic after a specific button press occurs:

  1. Set a timer in your app
  2. Populate an array of 5000 elements in descending order
  3. Implement a bubble sorting algorithm reset the large array in ascending order
  4. End the timer and report the results in an on-screen UI element

JavaScript example:

let startTime = performance.now();
let arr = Array(ITEMS_TO_SORT);
for (let i = 0; i < ITEMS_TO_SORT; i++) {
  arr[i] = ITEMS_TO_SORT - i;
}

bblSort(arr);
let endTime = performance.now();
let elapsedTime = endTime - startTime;
// print the elapsed time in a state variable associated with an on-screen UI component
setJsOutputText(elapsedTime.toFixed(0) + ' ms elapsed');

C++ example:

let startTime = performance.now();
// call the Turbo Module with the number of items to bubble sort
let calculatedNumber = TurboCalc.getNumber(ITEMS_TO_SORT);
let endTime = performance.now();
let elapsedTime = endTime - startTime;
// print the elapsed time in a state variable associated with a View/Text component on-screen
setCppOutputText(elapsedTime.toFixed(2) + ' ms elapsed');

:information_source: Note that TurboCalc will throw an error until we build out the Turbo Module and integrate it into our app in these next steps!

Step 2: Implement the sorting code in JavaScript & C++

For JavaScript, we’ll implement the bubble sort sample code:*

function bblSort(totalItems) { // we are using 5000 for this example
    let arr = Array(totalItems);
    // create the array based on the items
    for (let i = 0; i < totalItems; i++) {
      arr[i] = totalItems - i;
    }
    
    for (let i = 0; i < arr.length; i++) {
      for (let j = 0; j < arr.length - i - 1; j++) {
        // Checking if the item at present iteration is greater than the next iteration
        if (arr[j] > arr[j + 1]) {
          // If the condition is true then swap them
          var temp = arr[j];
          arr[j] = arr[j + 1];
          arr[j + 1] = temp;
        }
      }
    }
    
    return arr;
  }

For C++, we will create a new Turbo Module (via the template) named TurboCalc and overwrite the existing getNumber(int arg) method with our bubble sort logic:

#include <TMLog.h> // don't forget to include the logger in your header

...

int TurboCalc::getNumber(int arg)
    {
        int numbersToSort = arg;
        int i, j, temp, pass = 0;
        int a[numbersToSort];

        // initialize the array with the worst possible sorting
        for (i = 0; i < numbersToSort; i++)
        {
            a[i] = numbersToSort - i;
        }
        for (i = 0; i < numbersToSort; i++)
        {
            for (j = i + 1; j < numbersToSort; j++)
            {
                if (a[j] < a[i])
                {
                    temp = a[i];
                    a[i] = a[j];
                    a[j] = temp;
                }
            }
            pass++;
        }
        TMINFO("Print the first 10 elements for posterity...\n");
        for (i = 0; i < 10; i++)
        {
            // std::cout << a[i] << "\t";
            TMINFO( std::to_string(a[i]) + "\t" );
        }
        
        return a[0]; // return the first element of the sorted list to confirm proper sorting
    }

Next run the kepler build command within the Turbo Module directory in order to generate a .tgz file. This file can then be incorporated into your Kepler app via npm install. Next we can update our App.tsx file to import the example sorting functionality:

// import our Turbo Module into our app
import {TurboCalc} from '@amzn/TurboCalc';

(Now the the sample code from above to call our Turbo Module (and measure the performance) will be resolved)

Step 3: Compare performance results for C++ versus JavaScript :bar_chart: :bubbles:

If you had any prior doubts, C++ was 99.4% faster completing the function :exploding_head:

bubSort-Simulator

However, keep in mind that compute times on the Fire TV stick device are going to be much slower than running on the Kepler simulator for Mac or Linux. There’s also performance differences when running an app in “debug” versus “release" mode. For example, the numbers on screen below are running the same app on the Kepler Fire TV stick in “release mode” — “debug mode” was 15 seconds (or 50%) slower.


(Fire TV running the Bubble Sort sample app in “release” mode.)

We can also use the Vega Studio Activity Monitor to see that JavaScript computing utilizes 100% of the CPU for the task:

Factors to keep in mind
Strong reminder that we DON’T recommend you sort 5000 items in JavaScript, especially with a bubble sort!

Consider moving any intense processing tasks server-side before sending to your client app. However, there may be certain activities where parsing a large number of items in your app may be unavoidable such as parsing a video manifest, XML files, or video transcoding — for those scenarios we recommend dropping down to native/C++ code through Turbo Modules as our recommended apparoach.

It’s worth mentioning that React-Native-Kepler uses the Hermes JavaScript engine and our sample code in this guide has NOT been optimized with any techniques such as Ahead-of-Time Compilation (AOT) for Static Hermes. While using these techniques would close some of the performance gaps, Native / C++ code will almost always remain faster than JavaScript for memory-heavy tasks.

Some common Issues when using Turbo Modules

A couple of common questions that arise when incorporating Turbo Modules into your Kepler app include:

  • JavaScript Apps must be rebuilt with the Turbo Module incorporated. You may need to do another kepler build and ensure it passes with no unit test failures.
  • If rebuilding your Turbo Module does not reflect the latest functionality then you should confirm if your JavaScript app is properly grabbing the latest package built:
    • Delete your package-lock.json file
    • Remove the Turbo Modules folder under node_modules
    • Re-install the SDK via kepler platform install instructions via our docs.
  • Using npm link may be useful here as well

What about for less intensive performance examples? Do I really need to use Turbo Modules and write C++ code? :scream_cat:

Likely not!

For example, if we take the exact same scenario but only had 500 items to bubble sort, the time is MUCH smaller on the Kepler Simulator and doesn’t merit switching to native C++ / Turbo Modules. Just make sure you test on the both the simulator and the Fire TV device for an accurate representation of your app’s user experience.

Conclusion

Turbo Modules are a ideal way to run native C++ code from your React-Native-Kepler app. One of the key benefits is for computational activities of which there is a huge difference between runtimes on JavaScript vs C++.

To learn more, check out our Turbo Module documentation to get started. I have also included my entire React Native App.tsx code below for reference. (disclaimer: it’s “prototype-level” code, don’t judge me :wink: )

Let us know if you have any questions or suggestions. We’d also be curious if there’s additional languages such as Rust you would like tp have Turbo Module support.

We look forward to your comments on any interesting scenarios that you have benchmarked!

Eric


Additional Turbo Module FAQs…

:question: How do I access my Turbo Module logs?
There are different ways, but I find the following command most handy: journalctl -f | grep Turbo which will print out logs from TMINFO.

myusername@c889f3b7 % kepler device shell
Last login: Thu Nov 16 19:28:58 2023 from 10.0.2.2
simulator:~# journalctl -f | grep Turbo
Nov 26 23:33:00.083563 amazon-0001bf3d402d7aa1 local0.err keplerscript-ru[929]: 41 E Volta:[KeplerScript] NapiTurboModule::get property $$typeof not found for module TurboCalc
Nov 26 23:33:04.306851 amazon-0001bf3d402d7aa1 local0.info keplerscript-ru[929]: KeplerScript.TurboModule:
Nov 26 23:33:04.312820 amazon-0001bf3d402d7aa1 local0.info keplerscript-ru[929]: KeplerScript.TurboModule: Sorted Element List ...
Nov 26 23:33:04.312838 amazon-0001bf3d402d7aa1 local0.info keplerscript-ru[929]: KeplerScript.TurboModule: 1
Nov 26 23:33:04.312839 amazon-0001bf3d402d7aa1 local0.info keplerscript-ru[929]: KeplerScript.TurboModule: 2
Nov 26 23:33:04.312841 amazon-0001bf3d402d7aa1 local0.info keplerscript-ru[929]: KeplerScript.TurboModule: 3
Nov 26 23:33:04.312842 amazon-0001bf3d402d7aa1 local0.info keplerscript-ru[929]: KeplerScript.TurboModule: 4
Nov 26 23:33:04.312844 amazon-0001bf3d402d7aa1 local0.info keplerscript-ru[929]: KeplerScript.TurboModule: 5
Nov 26 23:33:04.312845 amazon-0001bf3d402d7aa1 local0.info keplerscript-ru[929]: KeplerScript.TurboModule: 6
Nov 26 23:33:04.312846 amazon-0001bf3d402d7aa1 local0.info keplerscript-ru[929]: KeplerScript.TurboModule: 7
Nov 26 23:33:04.312847 amazon-0001bf3d402d7aa1 local0.info keplerscript-ru[929]: KeplerScript.TurboModule: 8
Nov 26 23:33:04.312849 amazon-0001bf3d402d7aa1 local0.info keplerscript-ru[929]: KeplerScript.TurboModule: 9
Nov 26 23:33:04.312850 amazon-0001bf3d402d7aa1 local0.info keplerscript-ru[929]: KeplerScript.TurboModule: 10
Nov 26 23:33:04.312853 amazon-0001bf3d402d7aa1 local0.info keplerscript-ru[929]: KeplerScript.TurboModule:

:question: Can I get the full sample code that you used here?
Yes! :point_right: For those interested, the full App.tsx used for this example can be found below.

For the Turbo Module / C++ code, there are no changes to the Turbo Module created via the template, we just overwrote the getNumber method and that code is already above.

Disclaimer: Sample is 100% “prototype/demo” code, it’s not intended to be used in any serious capacity :slight_smile:

/*
 * Copyright (c) 2022 Amazon.com, Inc. or its affiliates.  All rights reserved.
 *
 * PROPRIETARY/CONFIDENTIAL.  USE IS SUBJECT TO LICENSE TERMS.
 */

import React, {useRef, useState} from 'react';
import {
  ColorValue,
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
} from 'react-native';

import {TurboCalc} from '@amzn/TurboCalc';

const backgroundColors = ['#283593', '#2382CB', '#A670F2', '#029976'];

export const App = () => {

  const ITEMS_TO_SORT = 5000;

  const [backgroundColor, setBackgroundColor] = useState<ColorValue>(
    backgroundColors[0],
  );
  const colorIdx = useRef<number>(0);
  const [jsOutputText, setJsOutputText] = useState('Waiting to start..');
  const [cppOutputText, setCppOutputText] = useState('Waiting to start..');
  // useState(MyTM.getString('MyTM String'));

  function bblSort(arr) {
    for (let i = 0; i < arr.length; i++) {
      // Last i elements are already in place
      for (let j = 0; j < arr.length - i - 1; j++) {
        // Checking if the item at present iteration
        // is greater than the next iteration
        if (arr[j] > arr[j + 1]) {
          // If the condition is true
          // then swap them
          var temp = arr[j];
          arr[j] = arr[j + 1];
          arr[j + 1] = temp;
        }
      }
    }
  }

  const styles = getStyles(backgroundColor);

  const handleButtonPress = () => {
    // wrapping index through array above
    const nextColor = (colorIdx.current + 1) % backgroundColors.length;
    setBackgroundColor(backgroundColors[nextColor]);
    colorIdx.current = nextColor;

    let startTime = performance.now();
    let arr = Array(ITEMS_TO_SORT);
    for (let i = 0; i < ITEMS_TO_SORT; i++) {
      arr[i] = ITEMS_TO_SORT - i;
    }
    
    bblSort(arr); //our bubble sort function
    let endTime = performance.now();
    let elapsedTime = endTime - startTime;
    // print the elapsed time in a state variable associated with a View/Text component on-screen
    setJsOutputText(elapsedTime.toFixed(0) + ' ms elapsed');
  };

  const handleButtonPressCpp = () => {
    // maintaining existing "color picker" app functionality wrapping index through array above
    const nextColor = (colorIdx.current + 1) % backgroundColors.length;
    setBackgroundColor(backgroundColors[nextColor]);
    colorIdx.current = nextColor;

    // start of Bubble Sort logic
    let startTime = performance.now();
    // call our Turbo Module with the number of items to bubble sort.
    let calculatedNumber = TurboCalc.getNumber(ITEMS_TO_SORT);    
    let endTime = performance.now();
    let elapsedTime = endTime - startTime;
    // print the elapsed time in a state variable associated with a View/Text component on-screen
    setCppOutputText(elapsedTime.toFixed(2) + ' ms elapsed');

  };

  const hexColor = JSON.stringify(backgroundColor).replace(/"/g, '');

  return (
    <View style={styles.container}>
      <View style={styles.headerBlock}>
        <Text style={styles.headerText}>Bubble Sort Measurement: {ITEMS_TO_SORT} Items</Text>
      </View>
      <View style={styles.horizontalView}>
        <TouchableOpacity
          style={styles.button}
          onPress={handleButtonPress}
          testID="sampleButton">
          <Text style={styles.buttonLabel}>Calculate JavaScript</Text>
        </TouchableOpacity>
        <TouchableOpacity
          style={styles.button}
          onPress={handleButtonPressCpp}
          testID="sampleButton">
          <Text style={styles.buttonLabel}>Calculate C++</Text>
        </TouchableOpacity>
      </View>
      <View style={styles.horizontalView}>
        <View style={styles.textView}>
          <Text style={styles.textBlock}>{jsOutputText}</Text>
        </View>
        <View style={styles.textView}>
          <Text style={styles.textBlock}>{cppOutputText}</Text>
        </View>
      </View>
      <Text style={styles.colorLabel}>{hexColor}</Text>
    </View>
  );
};

const getStyles = (bgColor: ColorValue) =>
  StyleSheet.create({
    horizontalView: {
      flexDirection: 'row',
    },
    textView: {
      height: 300,
      width: 300,
      padding: 15,
    },
    textBlock: {
      color: 'white',
      fontSize: 30,
      textAlign: 'center',
    },
    headerBlock: {
      paddingBottom: 50,
      // height: 200,
    },
    headerText: {
      fontSize: 50,
      color: 'white',
      fontStyle: 'italic',
    },
    container: {
      flex: 1,
      flexDirection: 'column',
      backgroundColor: bgColor,
      justifyContent: 'center',
      alignItems: 'center',
    },
    button: {
      alignItems: 'center',
      backgroundColor: '#303030',
      borderColor: 'navy',
      borderRadius: 10,
      borderWidth: 1,
      paddingVertical: 12,
      paddingHorizontal: 32,
      margin: 20,
    },
    buttonLabel: {
      color: 'white',
      fontSize: 25,
      fontFamily: 'Amazon Ember',
    },
    colorLabel: {
      color: 'white',
      position: 'absolute',
      top: 32,
      right: 32,
      fontSize: 32,
    },
  });