Integrating Android Performance Tuner into your native Android game

1. Introduction

Last Updated: 2020-08-12

Why use the Android Performance Tuner?

Android Performance Tuner helps you to measure and optimize your game's frame rate stability and graphical fidelity across many Android devices at scale, enabling you to deliver the best possible experience to each of your users. The Android Performance Tuner library, also known as Tuning Fork, records and aggregates live frame time information from your game, alongside your own game annotations and fidelity parameters, and uploads this data to the Play Console. This unlocks a new suite of metrics and insights in Android vitals.

What you'll build

In this codelab, you're going to open a native Android sample game, test it, and integrate Android Performance Tuner with it. After setting up and verifying that Android Performance Tuner works, we'll show how to upload the game to the Play Store and get access to the new performance insights provided by Android Performance Tuner in the Play Console.

What you'll learn

  • How to add and set up Android Performance Tuner for your game.
  • How to verify that Android Performance Tuner works properly and inspect the insights shown in the Play Console.

What you'll need

  • Android Studio 4.0 or later installed on your computer.
  • An Android device, connected to your computer, that has Developer options and USB debugging enabled. You will run the game on this device.
  • A Google Developer account, and access to the Play Console to upload your game and to view Android vitals.

2. Getting set up

Check Android SDK and NDK versions in Android Studio

The Performance Tuner is part of the Android Game SDK. The Android Game SDK integrates into native applications that utilize the Android NDK. If you have not previously worked with native applications, you may need to install the Android NDK using Android Studio. For the purposes of this codelab we will be using Android API level 29 and NDK version 21. The Android Game SDK includes support for earlier API versions, all the way back to Android API level 14 and NDK version 14.

Checking the installed SDK versions

Launch Android Studio. When the Welcome to Android Studio window is displayed, open the Configure dropdown menu and select the SDK Manager option.

b1528b91303889f5.png

If you already have a project open, you can instead open the Tools menu and select SDK Manager. The SDK Manager window will open. In the sidebar, select in order: Appearance & Behavior -> System Settings -> Android SDK. The Android SDK pane should be displayed. Make sure the SDK Platforms tab is selected and review the list of installed SDK Platforms.

f4d99ff5a13c4376.png

If the list entry for Android 10.0 (Q) is not checked, check the checkbox and click the Apply button at the bottom of the window to install the Android 10 (API level 29) SDK platform.

Checking that the NDK is installed

With the Android SDK pane still open, select the SDK Tools tab in the Android SDK pane to display a list of installed tool options.

9230bf1c6e95c4bf.png

If NDK (Side by side) and CMake are not checked, check their checkboxes and click the Apply button at the bottom of the window to install them. You may then close the Android SDK window by selecting the OK button. The version of the NDK being installed by default will change over time with subsequent NDK releases. If you need to install a specific version of the NDK, follow the instructions in the Android Studio reference for installing the NDK under the section "Install a specific version of the NDK".

Get an API key for the Android Performance Tuner

Enable "Android Performance Parameters API" on Google Cloud Platform

Before integrating Android Performance Tuner in your project, you need to get an API key for it. Android Performance Tuner will send performance data to the Google Play Console APIs, which must be able to identify your game.

  1. Create a new Cloud project in the Cloud Console:
  2. Enter a name for your Cloud project and click Create.
  3. Search for "Android Performance Parameters API" in the Marketplace.
  4. Enable the API.

Create an API key

The library is enabled, let's now create the API key:

  1. Go to API & Services, then Credentials from the menu.
  2. Click Create Credentials and choose API Key.
  3. From the API key created window, copy the value of the key and store it in a safe place.
  4. Click on Restrict Key.
  5. Enter a name for the key. Choose something that will remind you that it is linked to your game and Android Performance Tuner (for example, Endless Tunnel with Performance Tuner API key). 1517a6f29c157e1e.png
  6. In API restrictions, choose Restrict key and select "Android Performance Parameters API": c0686d92cef79416.png
  7. Click Save to finish.

Now that you have the API key, copy it and keep it somewhere safe as you will need it when you initialize the Android Performance Tuner.

Get the Endless Tunnel Project

We will be using a sample game project called Endless Tunnel to demonstrate Android Performance Tuner integration. Endless Tunnel is included in the suite of NDK example projects. You have two options for getting the Endless Tunnel project and opening it in Android Studio.

Strongly Recommended: Import into Android Studio

Endless Tunnel is in the collection of Android code samples that can be imported into Android Studio. To import Endless Tunnel, launch Android Studio and select Import an Android code sample in the Welcome to Android Studio window.

In the Import Sample window, type Endless in the search edit text to filter the results. Select the Endless Tunnel item and click the Next button. The next window allows you to rename or change the location the project will be stored at. Make any customizations you desire and click the Finish button to import the project.

d4e5d04ea36d6d6.png

Alternative: Clone from NDK Samples repository

The Endless Tunnel project is part of the NDK Samples repository on GitHub. You can clone the git repo using the command line (or your favorite git client):

git clone https://github.com/android/ndk-samples.git

To open Endless Tunnel in Android Studio, launch Android Studio, first select Open an existing Android Studio Project from the Welcome to Android Studio window. Navigate to where you cloned ndk-samples, enter the ndk-samples directory, select the endless-tunnel directory and select Open.

Customize the project settings

In order to upload a build to Google Play, we will need to change the package name from the default to a unique name. We will also modify the minimum and target API settings to take advantage of some convenience features that simplify implementation for this codelab.

Updating the package name

First, in the project pane, navigate to the app AndroidManifest.xml file in endless-tunnel -> endless-tunnel -> app -> src -> main and open it in the Android Studio editor.

Find the package="com.google.sample.tunnel" entry, change the package name to something unique and save the file.

33ea3526ea9b694a.png

Next, select the app build.gradle file just below the AndroidManifest.xml file and open it in the Android Studio editor. Find the line containing applicationId 'com.google.sample.tunnel' and change the package name to the same name you used in the AndroidManifest.xml file.

Add internet permissions

Android Performance Tuner needs internet permissions in the application manifest to send telemetry data. Navigate back to the AndroidManifest.xml file and add a <uses-permission android:name="android.permission.INTERNET" /> line between </application> and </manifest> at the bottom of the file. The result should look like this:

2d0bf297dc1f0673.png

Cleartext traffic for local monitoring

Later in the codelab, we will be directing telemetry output to a monitoring app running locally on the same device. To support this, an android:usesCleartextTraffic="true" line needs to be added to the application section in the AndroidManifest.xml file. The result should look like this:

20fcde8a8dff34f0.png

Changing the API levels

To change the API level for the app, do the following:

  1. Open the app build.gradle file (if it is not still open).
  2. Find the line containing minSdkVersion and change it from 14 to 24.
  3. Find the line containing targetSdkVersion and change it from 28 to 29.
  4. Save the file.
  5. Sync the project. Click File > Sync Project with Gradle Files.

Build the project in Android Studio and run on device

Before beginning any modifications, ensure that Android Studio can see your connected Android device. Select Run > Run app to build and deploy the app to a connected Android device. If you are unfamiliar with running applications on device, please reference Run your app in the Android Studio documentation. If successful, you should see the Endless Tunnel title screen on your device.

fb4ae4cddc31c71.png

3. Adding the Android Game SDK to the project

The Android Game SDK is distributed in multiple formats. One format is a Jetpack library release. The Jetpack library takes advantage of new native dependency support in Android Studio 4.0 and is the recommended way to add the Android Game SDK to a project. As an alternative, it is also possible to download a .zip file containing the Android Game SDK. The source code to the Android Game SDK is available, and while it can be built from source, that method is outside the scope of this codelab.

Strongly Recommended: Jetpack library

General information about the Android Game SDK Jetpack library release can be found on its reference page. To add the Android Game SDK as a Jetpack library we will make the following modifications to project files:

Setting gradle.properties values

We need to set some property values in the gradle.properties file located in the root directory of the Endless Tunnel project. Since Endless Tunnel does not have a gradle.properties file, we will create one. Right-click on the second endless-tunnel folder in the project hierarchy as shown in the screenshot below and select New -> File from the context menu. When prompted for a filename enter gradle.properties.

5e190b9179f60754.png

The properties you enter into gradle.properties depend on which Android Studio version you are using. If you are using Android Studio 4.0 enter the following lines:

# Enables experimental Prefab
android.enablePrefab=true
# Tell Android Studio we are using AndroidX
android.useAndroidX=true

If you are using Android Studio 4.1 or later enter the following lines:

# Tell Android Studio we are using AndroidX
android.useAndroidX=true

Save the file after entering the appropriate fields. If you see a banner message as displayed below "Gradle files have changed since last project sync. A project sync may be necessary for the IDE to work properly.", click the Sync Now button on the right side of the banner to update your project.

34c0f44b1a2139bc.png

Adding dependencies to build.gradle

Open the app build.gradle file. At the very bottom of the build.gradle file paste the following text (outside of any of the existing {} blocks) to declare the native dependencies:

dependencies {
    // To use the Android Frame Pacing library
    implementation "androidx.games:games-frame-pacing:1.5.0-alpha02"

    // To use the Android Performance Tuner
    implementation "androidx.games:games-performance-tuner:1.0.0-alpha02"
}

If you are using Android Studio 4.1 or later, you will need to paste the following text inside the android {} block, for example after the ndkVersion entry and before the defaultConfig {} block:

   buildFeatures {
        prefab true
    }

Do not add the buildFeatures block if you are using Android Studio 4.0 or it will cause a gradle sync error. After completing your edits, save the build.gradle file and again click Sync Now if prompted.

Adding packages to CMakeLists.txt

In the project pane, navigate to the game's main CMakeLists.txt file in endless-tunnel -> endless-tunnel -> app -> src -> main -> cpp and open it in the Android Studio editor.

8f26b0de1162206f.png

Below the cmake_minimum_required line, paste the following text:

# Add the packages from the Android Game SDK
find_package(games-frame-pacing REQUIRED CONFIG)
find_package(games-performance-tuner REQUIRED CONFIG)

At the bottom of the CMakeLists.txt file, find the target_link_libraries command and add the games-frame-pacing::swappy_static and games-performance-tuner::tuningfork_static library references to the list of library dependencies. The end result should look like this:

# add lib dependencies
target_link_libraries(game
     games-frame-pacing::swappy_static
     games-performance-tuner::tuningfork_static
     android
     native_app_glue
     atomic
     EGL
     GLESv2
     glm
     log
     OpenSLES)

After finishing the edits, save the CMakeLists.txt file and select the Build -> Refresh Linked C++ Projects menu item. You can now skip the next Alternative: Downloading the Android Game SDK section and move on to the Updating the Game Manifest section.

Alternative: Downloading the Android Game SDK

The Android Game SDK is available to download as a .zip archive from the Android Game SDK landing page. Download the .zip file and extract the gamesdk directory to a convenient location. For the purposes of this codelab, it is recommended that the gamesdk directory be placed in the same parent directory as the EndlessTunnel directory (if you imported a project) or the ndk-samples directory (if you cloned the ndk-samples repository).

Updating CMakeLists.txt with the Android Game SDK CMake utility file

The Android Game SDK includes utility files for CMake that streamline the process of adding the SDK to a native application. The utility file is located at gamesdk/samples/gamesdk.cmake. This utility file needs to be included in the Endless Tunnel CMakeLists.txt file using the CMake include command. The include command can use absolute paths, or paths relative to the directory containing the CMakeLists.txt file. After including the utility file, the add_gamesdk_target command is used to perform sanity checks, add compiler include paths to the SDK and generate a target that can be used to link the SDK libraries.

In the project pane, navigate to the game's main CMakeLists.txt file in endless-tunnel -> endless-tunnel -> app -> src -> main -> cpp and open it in the Android Studio editor.

8f26b0de1162206f.png

Below the cmake_minimum_required line, add the include command and the add_gamesdk_target command to include the gamesdk.cmake file:

# Set our relative path to the Android Game SDK root directory
set(GAMESDK_BASE_DIR "../../../../../gamesdk")
# Include the CMake utility file for the Android Game SDK
# and call its add_game_sdk_target command to do build environment setup
include("${GAMESDK_BASE_DIR}/samples/gamesdk.cmake")
add_gamesdk_target(PACKAGE_DIR "${GAMESDK_BASE_DIR}/" BUILD_TYPE Release)

In the above example, a relative path for GAMESDK_BASE_DIR is specified that assumes the gamesdk directory and the root endless-tunnel directory are both in the same parent directory. In the case of a github repo checkout installation, where the gamesdk directory shares a parent directory with the ndk-samples directory, an additional ../ must be added to the GAMESDK_BASE_DIR relative path to account for the ndk-samples directory level.

After adding those lines, find the target_link_libraries command at the bottom of the CMakeLists.txt file and add gamesdk to the list of libraries. The end result should look like this:

# add lib dependencies
target_link_libraries(game
     gamesdk
     android
     native_app_glue
     atomic
     EGL
     GLESv2
     glm
     log
     OpenSLES)

Save the CMakeLists.txt file and select the Build -> Refresh Linked C++ Projects menu item.

4. Integrating the Frame Pacing API

The Android Game SDK includes a Frame Pacing API (also referred to as Swappy). The Frame Pacing API provides a solution for ensuring more consistent frame display timing. Reducing this variability is helpful to generating performance profile data and using the Frame Pacing API allows the Performance Tuner to log frame timings automatically, so we will integrate the Frame Pacing API to Endless Tunnel before integrating the Performance Tuner.

Add frame pacing functions

Find the native_engine.cpp file in EndlessTunnel -> app -> src -> main -> cpp -> native_engine.cpp and open it in Android Studio.

Add the Frame Pacing header include

Find the block of #include statements near the top of the source file and add an include for the Frame Pacing API targeting OpenGL ES, which is the graphics API used by Endless Tunnel:

#include "swappy/swappyGL.h"

There is also a swappy/swappyVK.h header intended for games using the Vulkan graphics API.

Add Frame Pacing init and destroy statements

Find the NativeEngine::NativeEngine class constructor in the native_engine.cpp file and add the following statements at the bottom of the function to initialize the Frame Pacing API with default parameters and set the target swap interval to 60 frames per second.

LOGD("Calling SwappyGL_init");
SwappyGL_init(GetJniEnv(), mApp->activity->clazz);
SwappyGL_setSwapIntervalNS(SWAPPY_SWAP_60FPS);

Find the NativeEngine::~NativeEngine class destructor in the native_engine.cpp file and add the following statement to the top of the function to shut down the Frame Pacing API on engine destruction.

SwappyGL_destroy();

Add window initialization for the Frame Pacing library

Find the NativeEngine::HandleCommand function in the native_engine.cpp file. Scroll down to locate the case statement that handles APP_CMD_INIT_WINDOW. Add a SwappGL_setWindow call below the mHasWindow = true statement. The result will look like this:

case APP_CMD_INIT_WINDOW:
   // We have a window!
   VLOGD("NativeEngine: APP_CMD_INIT_WINDOW");
   if (mApp->window != NULL) {
       mHasWindow = true;
       SwappyGL_setWindow(mApp->window);
       if (mApp->savedStateSize == sizeof(mState) && mApp->savedState != nullptr) {

Change the display swap to use the Frame Pacing API

Find the NativeEngine::DoFrame function in the native_engine.cpp file. Scroll down to the line that calls the eglSwapBuffers command. Replace that line with the following line.

if(!SwappyGL_swap(mEglDisplay, mEglSurface)) {

Verify frame pacing integration

Save your changes to the native_engine.cpp file and verify that the game still builds and runs. You should not perceive any differences in Endless Tunnel after integrating the Frame Pacing API.

5. Defining annotations, fidelity parameters and settings

To use Performance Tuner, your game needs to provide runtime data in the assets of your application package. There are three types of required data:

  • Annotations give contextual information about what your game is doing when a tick is recorded
  • Fidelity parameters describe the game and graphical settings (potentially affecting performance) currently active for the game
  • Settings including the API key as well as profile aggregation behavior and optional default parameter values

Working with protocol buffers

The Performance Tuner expects the runtime data to be provided as protocol buffers, which are Google's language-neutral, structured, data-interchange format. For more information about protocol buffers, see About protocol buffers.

To ensure a minimal code footprint, the Performance Tuner libraries use a ‘nano' protobuf implementation library which requires the data to be in a binary format. We will create textual representation protobuf files and use the protoc compiler to translate them into binary representations to include in the application bundle.

Three steps will be required to be able to provide protocol buffer data to the Performance Tuner:

  • Integrate a protocol buffer library into Endless Tunnel to create and pass live protocol buffer data to the Performance Tuner
  • Create source files to provide annotation definitions and settings parameters for the Performance Tuner
  • Add support in the Endless Tunnel build process to use the protoc compiler to create runtime data files for the Performance Tuner as well as generated source and header files describing the protocol buffer layouts

Creating the performance tuner data files

We will need to add some new directories to the Endless Tunnel project and then create textual data files in them.

Creating the runtime asset files

In the app/src/main directory of the Endless Tunnel project, create an assets directory. Inside the new assets directory create a directory called tuningfork. Using a text editor or the Android Studio editor, create the following files in the app/src/main/assets/tuningfork directory with the specified names and contents:

tuningfork_settings.txt

aggregation_strategy: {method: TIME_BASED, intervalms_or_count: 10000, max_instrumentation_keys: 6, annotation_enum_size: [3,4]}
api_key: "enter-your-api-key-here-"
loading_annotation_index: 1
level_annotation_index: 2

Note: Replace the enter-your-api-key-here text with the API key you obtained earlier in the codelab.

dev_tuningfork.proto

syntax = "proto3";

package com.google.tuningfork;

enum InstrumentKey {
  CPU = 0;
  GPU = 1;
  SWAPPY_WAIT = 2;
  SWAPPY_SWAP = 3;
  CHOREOGRAPHER = 4;
}

enum LoadingState {
  LOADING_INVALID = 0;
  NOT_LOADING = 1;
  LOADING = 2;
}

enum Level {
  // 0 is not a valid value
  LEVEL_INVALID = 0;
  LEVEL_1 = 1;
  LEVEL_2 = 2;
  LEVEL_3 = 3;
};

message Annotation {
  LoadingState loading = 1;
  Level level = 2;
}

message FidelityParams {
  int32 tunnel_section_count = 1;
  float tunnel_section_length = 2;
}

dev_tuningfork_fidelityparams_1.txt

tunnel_section_count: 4 tunnel_section_length: 150

dev_tuningfork_fidelityparams_2.txt

tunnel_section_count: 8 tunnel_section_length: 75

Creating the protocol buffer source files

In the app/src/main directory of the Endless Tunnel project, create a proto directory. Copy the app/src/main/assets/tuningfork/dev_tuningfork.proto file into it. Then create a file called tuningfork.proto inside the proto directory with the following contents:

/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */


syntax = "proto2";

package com.google.tuningfork;

option java_package = "com.google.tuningfork";

// Passed by the user to tuning fork at initialization.
message Settings {
  message Histogram {
    optional int32 instrument_key = 1;
    optional float bucket_min = 2;
    optional float bucket_max = 3;
    optional int32 n_buckets = 4;
  }
  message AggregationStrategy {
    enum Submission {
      UNDEFINED = 0;
      TIME_BASED = 1;
      TICK_BASED = 2;
    }
    optional Submission method = 1;
    optional int32 intervalms_or_count = 2;
    optional int32 max_instrumentation_keys = 3;
    repeated int32 annotation_enum_size = 4;
  }
  optional AggregationStrategy aggregation_strategy = 1;
  repeated Histogram histograms = 2;

  // Base request URI.
  // If missing, https://performanceparameters.googleapis.com/v1/ is used.
  optional string base_uri = 3;

  // Google Play API key.
  // Requests may receive an error response if this is missing or wrong.
  optional string api_key = 4;

  // Name of the fidelitiy_parameters.bin file in assets/tuningfork
  //  used if no download was ever successful.
  optional string default_fidelity_parameters_filename = 5;

  // Timeout before first timeout of fidelity parameters request.
  // The request will then be repeated.
  optional int32 initial_request_timeout_ms = 6;

  // The time after which repeat requests are ceased.
  optional int32 ultimate_request_timeout_ms = 7;

  // Reserve 100-120 for indexes into the annotation array.
  optional int32 loading_annotation_index = 100; // 1-based index
  optional int32 level_annotation_index = 101; // 1-based index
}

After creating these files and directories, the end result in your src/main directory should match this:

9bdc2f63575fa223.png

Understanding the data files

The .proto files contain the format descriptions of the protocol buffers being passed to the Performance Tuner. The tuningfork.proto file defines the initialization data that must be passed to the Performance Tuner at startup. The dev_tuningfork.proto file defines the game-specific message data used for instrumentation, annotation and defining fidelity parameters. The .txt files contain actual protocol buffer data structured in accordance with the definitions described in the .proto files. The protoc compiler is used to translate them from text to binary representation before they are stored in the application package. The reference documentation for the Android Game SDK has additional information about annotations, fidelity parameters and settings.

Using protocol buffer utilities in the Android Game SDK

The Android Game SDK includes protocol buffer libraries, the protoc compiler, and CMake utility scripts that assist in adding the libraries into your project and compiling text protobuffer definitions into binary representations. If you used Native Dependencies to integrate the Android Game SDK runtime libraries, you will need to follow the next Downloading the Android SDK step. If you integrated the libraries by downloading the Android Game SDK from its .zip package distribution, you can skip over this step.

Downloading the Android Game SDK

The Android Game SDK is available to download as a .zip archive from the Android Game SDK landing page. Download the .zip file and extract the gamesdk directory to a convenient location. For the purposes of this codelab, we recommend that the gamesdk directory be placed in the same directory as the EndlessTunnel directory (if you imported a project) or the ndk-samples directory (if you cloned the ndk-samples repository).

Adding the Android Game SDK Path to gradle.properties

To simplify referencing contents of the Android Game SDK in build.gradle, we will create a gradle property containing the absolute path to the SDK in gradle.properties. In the root Endless Tunnel project directory, open the gradle.properties file, or create if if doesn't exist. Add the following lines, customizing the path definition to match the location you placed the Android Game SDK:

# Set a property with the absolute path to the Android Game SDK
GameSDKPath=/Users/foo/AndroidStudioProjects/gamesdk

Adding protoc compilation to build.gradle

To compile the text-based protocol buffer files in src/main/assets/tuningfork into binary representations, we must add commands to the app build.gradle file. The Android Game SDK includes the protoc compiler, and an additional tool that will validate the contents of the protocol buffer files to check against errors that would cause problems at runtime. Open the build.gradle file in the Endless Tunnel app directory. First add the following line at the top directly above the apply plugin: 'com.android.application' line:

import org.gradle.internal.os.OperatingSystem;

Next, at the bottom of the file, add the following text block:

task createJar(type: GradleBuild) {
    buildFile = GameSDKPath + '/src/tuningfork/tools/validation/build.gradle'
    tasks = ['createJar']
}

def getProtocPath() {
    String platformName
    if (OperatingSystem.current().isLinux()) platformName = "linux-x86/bin/protoc"
    if (OperatingSystem.current().isMacOsX()) platformName = "mac/bin/protoc"
    if (OperatingSystem.current().isWindows()) platformName = "win/bin/protoc"
    return GameSDKPath + "/third_party/protobuf-3.0.0/install/"  + platformName
}

task buildTuningForkBinFiles(type: Exec) {
    dependsOn createJar
    commandLine "java",
            "-jar",
            GameSDKPath + "/src/tuningfork/tools/validation/build/libs/TuningforkApkValidationTool.jar",
            "--tuningforkPath",
            "src/main/assets/tuningfork",
            "--protoCompiler",
            getProtocPath()
}

tasks.preBuild.dependsOn("buildTuningForkBinFiles")

The above will compile the Tuning Fork validation tool in the Android Game SDK and execute it, specifying the project's runtime data files as input. The tool performs validation on the input files, and then uses the protoc compiler to create binary files.

Protobuf additions to CMakeLists.txt

In order to create and submit protocol buffer data to the Performance Tuner, it is necessary to add a protocol buffer library to Endless Tunnel. For Endless Tunnel, we will be integrating the nanopb library included in the Android Game SDK. The Performance Tuner uses nanopb internally, as it has a very lightweight footprint.

Start by opening the CMakeLists.txt file in the app/src/main/cpp directory.

8f26b0de1162206f.png

First, if you added the Android Game SDK using the Jetpack library instead of using the download .zip, below the cmake_minimum_required(VERSION 3.4.1) line, add the following line:

set(GAMESDK_BASE_DIR "../../../../../gamesdk")

If you didn't use the Jetpack library, you should already have added this line to your CMakeLists.txt file earlier in the codelab.

Note that In the above example, a relative path for GAMESDK_BASE_DIR is specified that assumes the gamesdk directory and the root endless-tunnel directory are both in the same parent directory. In the case of a github repo checkout installation, where the gamesdk directory shares a parent directory with the ndk-samples directory, add an additional ../ to the GAMESDK_BASE_DIR relative path to account for the ndk-samples directory level.

Next, below the definition of GAMESDK_BASE_DIR, add the following block:

set(PROTOBUF_NANO_SRC_DIR "${GAMESDK_BASE_DIR}/external/nanopb-c")
# Include the protobuf utility file from the Android Game SDK
include("${GAMESDK_BASE_DIR}/src/protobuf/protobuf.cmake")
# Directory of nano protobuf library source files
include_directories(${PROTOBUF_NANO_SRC_DIR})
# generate runtime files using protoc
protobuf_generate_nano_c( ${CMAKE_CURRENT_SOURCE_DIR}/../proto ../proto/dev_tuningfork.proto)
protobuf_generate_nano_c( ${CMAKE_CURRENT_SOURCE_DIR}/../proto ../proto/tuningfork.proto)
include_directories(${PROTO_GENS_DIR})

This sets up a shortcut to the nanopb source code, includes a protobuf.cmake file from the Android Game SDK containing utility functions for working with protocol buffers, and then uses those utility functions to generate source code and header files for creating runtime protocol buffers using the nanopb library.

Next, find the add_library(game SHARED section in the CMakeLists.txt file. Add line entries for ${PROTOBUF_NANO_SRCS} ${PROTO_GENS_DIR}/nano/dev_tuningfork.pb.c and ${PROTO_GENS_DIR}/nano/tuningfork.pb.c to add the nanopb source files, and the generated protocol buffer source and header files to the project. The end result should look like this:

# now build app's shared lib
add_library(game SHARED
     ${PROTOBUF_NANO_SRCS}
     ${PROTO_GENS_DIR}/nano/dev_tuningfork.pb.c
     ${PROTO_GENS_DIR}/nano/tuningfork.pb.c
     android_main.cpp
     anim.cpp
     ascii_to_geom.cpp
     dialog_scene.cpp
     indexbuf.cpp
     input_util.cpp
     jni_util.cpp
     native_engine.cpp
     obstacle.cpp
     obstacle_generator.cpp
     our_shader.cpp
     play_scene.cpp
     scene.cpp
     scene_manager.cpp
     sfxman.cpp
     shader.cpp
     shape_renderer.cpp
     tex_quad.cpp
     text_renderer.cpp
     texture.cpp
     ui_scene.cpp
     util.cpp
     vertexbuf.cpp
     welcome_scene.cpp)

After finishing the edits, save the CMakeLists.txt file and select the Build -> Refresh Linked C++ Projects menu item.

6. Integrating the Performance Tuner API

Now that we have completed all the prerequisites, we will add code that initializes the Performance Tuner and connect it up to the Endless Tunnel game engine.

Adding Android Performance Tuner utility code

We will create a new ‘manager' class to contain our code that initializes and make calls to the Android Performance Tuner.

Creating the tm_manager source files

In the Android Studio project pane, expand the project tree to select the Endless Tunnel -> Endless Tunnel -> app -> src.main -> cpp folder. Right click on the cpp folder and select New -> C/C++ Header File. Enter tf_manager.hpp for the name and click OK. Paste the following contents into the new tm_manager.hpp file, replacing the existing text, and then save the file:

/*
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#ifndef endlesstunnel_tf_manager_h
#define endlesstunnel_tf_manager_h

#include "common.hpp"
#include "nano/dev_tuningfork.pb.h"
#include "nano/tuningfork.pb.h"

struct AConfiguration;

class TuningForkManager {
    private:
        bool mTFInitialized;

        void InitializeChoreographerCallback(AConfiguration* config);
    public:
        TuningForkManager(JNIEnv* env, jobject context, AConfiguration* config);
        ~TuningForkManager();

        void HandleChoreographerFrame();
        void PostFrameTick(const uint16_t frameKey);
        void SetCurrentAnnotation(const _com_google_tuningfork_Annotation* annotation);
};

#endif

Right click on the cpp folder again and select New -> C/C++ Source File. Enter tf_manager.cpp for the name and click OK. Paste the following contents into the new tf_manager.cpp file, replacing the existing text and then save the file:

/*
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include <android/choreographer.h>
#include <android/configuration.h>
#include <android/log.h>
#include <dlfcn.h>
#include "pb_common.h"
#include "pb_encode.h"
#include "swappy/swappyGL.h"
#include "swappy/swappyGL_extra.h"
#include "tuningfork/tuningfork.h"
#include "tuningfork/tuningfork_extra.h"

#include "game_consts.hpp"
#include "tf_manager.hpp"

/** @cond INTERNAL */

/**
 * Internal to this file - do not use.
 */
extern "C" void TuningFork_CProtobufSerialization_Dealloc(
        TuningFork_CProtobufSerialization* c);

/** @endcond */

namespace {
    constexpr TuningFork_InstrumentKey TFTICK_CHOREOGRAPHER = TFTICK_USERDEFINED_BASE;

    typedef void (*func_AChoreographer_postFrameCallback64)(
            AChoreographer* choreographer, AChoreographer_frameCallback64 callback,
            void* data);
    func_AChoreographer_postFrameCallback64 pAChoreographer_postFrameCallback64 = nullptr;

    void choreographer_callback(long /*frameTimeNanos*/, void* data) {
        TuningForkManager* tfManager = reinterpret_cast<TuningForkManager*>(data);
        tfManager->HandleChoreographerFrame();
    }

    void choreographer_callback64(int64_t /*frameTimeNanos*/, void* data) {
        TuningForkManager* tfManager = reinterpret_cast<TuningForkManager*>(data);
        tfManager->HandleChoreographerFrame();
    }
}

TuningForkManager::TuningForkManager(JNIEnv* env, jobject activity, AConfiguration* config) {
    mTFInitialized = false;

    TuningFork_Settings settings {};

    // Performance Tuner can work with the Frame Pacing library to automatically
    // record frame time via the tracer function
    if (SwappyGL_isEnabled()) {
        settings.swappy_tracer_fn = &SwappyGL_injectTracer;
        settings.swappy_version = Swappy_version();
    }

    // Setup debug builds to connect to the local Performance Monitor app tool
#ifndef NDEBUG
    settings.endpoint_uri_override = "http://localhost:9000";
#endif

    /*
     * Endless Tunnel doesn't have 'dynamic' settings, we are going to fake
     * them by checking against what we consider  'low' and 'high' values
     * for the RENDER_TUNNEL_SECTION_COUNT and TUNNEL_SECTION_LENGTH constants
     * defined in game_consts.hpp and setting the corresponding fidelity level
     * for our reporting
     */
    TuningFork_CProtobufSerialization fps = {};
    bool bHighDensity = (RENDER_TUNNEL_SECTION_COUNT == 8 && TUNNEL_SECTION_LENGTH == 75.0f);
    const char* filename = bHighDensity ? "dev_tuningfork_fidelityparams_2.bin" :
            "dev_tuningfork_fidelityparams_1.bin";
    if (TuningFork_findFidelityParamsInApk(env, activity, filename, &fps)
        == TUNINGFORK_ERROR_OK) {
        // This overrides the value in default_fidelity_parameters_filename
        //  in tuningfork_settings, if it is there.
        settings.training_fidelity_params = &fps;
    } else {
        LOGE("Couldn't load fidelity params from %s", filename);
    }

    TuningFork_ErrorCode tfError = TuningFork_init(&settings, env, activity);
    if (tfError == TUNINGFORK_ERROR_OK) {
        mTFInitialized = true;

        /*
         * Here we are going to set an initial annotation describing our game state,
         * in a typical game we might start in some kind of loading state, and later
         * update a new annotation once we had completed loading. For endless tunnel,
         * there is no loading, and only one level, so we will only need to do
         * one initial annotation set.
         */
        _com_google_tuningfork_Annotation annotation;
        annotation.loading = com_google_tuningfork_LoadingState_NOT_LOADING;
        annotation.level = com_google_tuningfork_Level_LEVEL_1;
        SetCurrentAnnotation(&annotation);
    } else {
        LOGE("Error initializing TuningFork: %d", tfError);
    }

    // Free any fidelity params we got from the APK
    TuningFork_CProtobufSerialization_free(&fps);

    InitializeChoreographerCallback(config);
}

TuningForkManager::~TuningForkManager() {
    if (mTFInitialized) {
        TuningFork_ErrorCode tfError = TuningFork_destroy();
        if (tfError != TUNINGFORK_ERROR_OK) {
            LOGE("Error destroying TuningFork: %d", tfError);
        }
    }
}

void TuningForkManager::InitializeChoreographerCallback(AConfiguration* config) {
    int32_t sdkVersion = AConfiguration_getSdkVersion(config);
    if (sdkVersion >= 29) {
        // The original postFrameCallback is deprecated in 29 and later, try and get the new postFrameCallback64 call
        void *lib = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL);
        if (lib != nullptr) {
            // Retrieve function pointer from shared object.
            pAChoreographer_postFrameCallback64 =
                    reinterpret_cast<func_AChoreographer_postFrameCallback64>(
                            dlsym(lib, "AChoreographer_postFrameCallback64"));
            if (pAChoreographer_postFrameCallback64 != nullptr) {
                pAChoreographer_postFrameCallback64(AChoreographer_getInstance(),
                                                    choreographer_callback64, this);
            } else {
                // fallback to old
                AChoreographer_postFrameCallback(AChoreographer_getInstance(),
                                                 choreographer_callback, this);
            }
        }
    } else {
        AChoreographer_postFrameCallback(AChoreographer_getInstance(), choreographer_callback, this);
    }
}

void TuningForkManager::HandleChoreographerFrame() {
    PostFrameTick(TFTICK_CHOREOGRAPHER);

    if (pAChoreographer_postFrameCallback64 != nullptr) {
        pAChoreographer_postFrameCallback64(AChoreographer_getInstance(),
                                            choreographer_callback64, this);
    } else {
        AChoreographer_postFrameCallback(AChoreographer_getInstance(),
                                         choreographer_callback, this);
    }
}

void TuningForkManager::PostFrameTick(const TuningFork_InstrumentKey frameKey) {
    if (mTFInitialized) {
        TuningFork_ErrorCode tfError = TuningFork_frameTick(frameKey);
        if (tfError != TUNINGFORK_ERROR_OK) {
            LOGE("Error calling TuningFork_frameTick: %d", tfError);
        }
    }
}

void TuningForkManager::SetCurrentAnnotation(const _com_google_tuningfork_Annotation* annotation) {
    size_t encodedSize = 0;
    if (pb_get_encoded_size(&encodedSize, com_google_tuningfork_Annotation_fields, annotation)) {
        TuningFork_CProtobufSerialization cser;
        cser.bytes = (uint8_t *) ::malloc(encodedSize);
        cser.size = encodedSize;
        cser.dealloc = TuningFork_CProtobufSerialization_Dealloc;

        pb_ostream_t pbStream = pb_ostream_from_buffer(cser.bytes, encodedSize);
        pb_encode(&pbStream, com_google_tuningfork_Annotation_fields, annotation);

        if (TuningFork_setCurrentAnnotation(&cser) != TUNINGFORK_ERROR_OK ) {
            LOGW("Bad annotation passed to TuningFork_setCurrentAnnotation");
        }
        TuningFork_CProtobufSerialization_free(&cser);
    } else {
        LOGE("Failed to calculate annotation encode size");
    }
}

Adding tf_manager to the project build file

At the top of the source window you may see a banner stating "This file is not part of the project. Please include it in the appropriate build file (build.gradle, CMakeLists.txt or Android.mk etc.) and sync the project". Find the CMakeLists.txt file in the cpp folder and open it in Android Studio. Locate the add_library(game SHARED command and view its list of source files. Add a line containing tf_manager.cpp between the lines for texture.cpp and ui_scene.cpp and then save the file. Switch back to the tm_manager.cpp tab, selecting the Sync Now button in the top right of the notice banner if it exists. Then select Build -> Make Project to rebuild the project with the newly added source file.

Adding and Initializing the utility manager class

In the Android Studio project pane, navigate to the EndlessTunnel -> EndlessTunnel -> app -> src.main -> cpp folder and open the native_engine.hpp file. Below the #include "common.hpp" statement add the following line:

#include "tf_manager.hpp"

Scroll down and find the class declaration of the JNIEnv *mJniEnv member variable. Below it add these lines:

        // Instance of our TuningFork manager class
        TuningForkManager* mTuningForkManager;

Save the file and then open the native_engine.cpp file from the project pane. Find the NativeEngine::NativeEngine class constructor in the native_engine.cpp file and add the following line at the very end of the function:

    mTuningForkManager = new TuningForkManager(GetJniEnv(), mApp->activity->clazz, mApp->config);

Find the NativeEngine::~NativeEngine class destructor in the native_engine.cpp file and add the following statement to the top of the destructor to delete the manager and shut down the Android Performance Tuner on engine destruction:

    delete mTuningForkManager;

Save the file and rebuild the project

.

Check that Android Performance Tuner starts properly

To confirm that Android Performance Tuner is properly integrated, run the project on your device and open the logcat tab in Android Studio. In the logcat tab, search for "TuningFork" (Tuning Fork is the internal name of the Android library):

2020-07-08 13:06:55.163 9487-9559/com.google.sample.tunnel2049 I/TuningFork: Got settings from tuningfork/tuningfork_settings.bin
2020-07-08 13:06:55.164 9487-9559/com.google.sample.tunnel2049 I/TuningFork: Using local file cache at /data/user/0/com.google.sample.tunnel2049/cache/tuningfork
2020-07-08 13:06:55.167 9487-9559/com.google.sample.tunnel2049 I/TuningFork: OpenGL version 3.2 
2020-07-08 13:06:55.167 9487-9559/com.google.sample.tunnel2049 I/TuningFork: TuningFork.GoogleEndpoint: OK
2020-07-08 13:06:55.168 9487-9559/com.google.sample.tunnel2049 I/TuningFork: TuningFork Settings:

[...]

2020-07-08 13:06:55.169 9487-9559/com.google.sample.tunnel2049 I/TuningFork: TuningFork initialized
2020-07-08 13:07:15.171 9487-9566/com.google.sample.tunnel2049 I/TuningFork:Web: Connecting to: http://localhost:9000/applications/com.google.sample.tunnel2049/apks/1:uploadTelemetry

If you made a mistake while setting up Android Performance Tuner, like forgetting to set the API key, you should see an error in the initialization logs. For example:

02-03 16:49:44.970  8815  8831 I TuningFork: Got settings from tuningfork/tuningfork_settings.bin
02-03 16:49:44.972  8815  8831 W TuningFork.GE: The API key in Tuning Fork TFSettings is invalid
02-03 16:49:44.972  8815  8831 E TuningFork: TuningFork.GoogleEndpoint: FAILED
02-03 16:49:44.973  8815  8831 I Unity   : Tuningfork started with code: BadParameter

7. Testing locally with the Performance Tuner Monitor app

The Android Game SDK contains a utility application called Tuning Fork Monitor which can receive Performance Tuner telemetry from applications running locally on the same device.

Installing the Performance Tuner Monitor

Inside the Android Game SDK, there is a pre-built TuningForkMonitor.apk. Open a terminal on your computer, navigate to the gamesdk/apks/tools directory and run adb install TuningForkMonitor.apk to install it on your device.

Check that Android Performance Tuner is uploading telemetry

Launch the Tuning Fork Monitor app and then launch the Endless Tunnel project. Open the logcat tab in Android Studio and monitor the output. If you see "TuningFork initialized" in the logs, wait a bit more and look for logs indicating that telemetry is being uploaded:

2020-07-08 13:07:35.218 9487-9566/com.google.sample.tunnel2049 I/TuningFork:Web: Connecting to: http://localhost:9000/applications/com.google.sample.tunnel2049/apks/1:uploadTelemetry
2020-07-08 13:07:35.294 9487-9566/com.google.sample.tunnel2049 I/TuningFork:Web: Response code: 200
2020-07-08 13:07:35.294 9487-9566/com.google.sample.tunnel2049 I/TuningFork:Web: Response message: 
2020-07-08 13:07:35.295 9487-9566/com.google.sample.tunnel2049 I/TuningFork.GE: UPLOAD request returned 200 

Search in the logs for "Connecting to", followed by the response code a few lines after.

Viewing telemetry in the Performance Tuner Monitor

You will need to let the game run for a couple minutes to collect and report some data. Switch to the Tuning Fork Monitor app and you should see the package name of the Endless Tunnel app under the list of live applications:

8d0245f17b7ee330.png

Select the package name of the application to see the frame rate histograms and other settings:

c11ccaf31be6c72d.png

8. Build the game and upload it to the Play Store (optional)

The game has Android Performance Tuner properly integrated. You can now create a new build and upload it on the Play Console. You can then distribute it to testers to start collecting performance data.

Create and setup a keystore for the game

Android requires that all apps are digitally signed with a certificate before they are installed on a device or updated.

We'll create a "Keystore" for the game in this codelab. If you're publishing an update to an existing game, reuse the same Keystore as you did for releasing previous versions of the app.

Create a keystore and build a release APK

You can create a Keystore with Android Studio, follow the steps at that link to create a keystore and use it to generate a signed release build of the game. Choose the APK option when prompted to select an Android App Bundle or APK. At the end of the process you will have an .apk file that is suitable for uploading to the Google Play Console.

Privacy

Please review your app's Privacy Policy to ensure that it appropriately reflects that data about devices and usage may be shared with Google. Under Section 3.b the Google APIs Terms of Service, which governs your use of the Android Performance Tuner APIs, you must:

"comply with all applicable privacy laws and regulations including those applying to PII. You will provide and adhere to a privacy policy for your API Client that clearly and accurately describes to users of your API Client what user information you collect and how you use and share such information (including for advertising) with Google and third parties."

Upload the game and create a release on an internal testing track

Use the Google Play Console to create an app, fill information about it and create a new internal testing release. Upload your Android App Bundle for this internal testing release. This will allow you to test your app and see the results in the Play Console.

Note that it's important to use an internal testing release to test your integration of the Android Performance Tuner. You can publish to other tracks (closed testing, open testing or production), but you'll need a large number of users to play your game before seeing the data in the Play Console.

9. Visualize results and insights in the Google Play Console (optional)

Open the Game performance Insights page

Open the Google Play Console and choose the app you've uploaded. In the menu, choose Android Vitals > Performance > Insights in the Quality section:

60af661fabb1603d.png

If you open the page just after submitting your app for testing, it will probably be empty as no data was received and aggregated yet:

56f9052c77cb9e22.png

Ensure your game is played for the data to flow in. As it's a demo, you can let it run on a few devices.

You can already set the target frame rate for your game - allowing insights to be adapted to your game expected performance. In our example, we'll set it to 30fps. This is the default, so we can simply click Dismiss.

Exploring the results from the Game performance Insights page

After your game is played by a certain number of users, you'll be able to see the charts showing how the game is performing.

Ensure you're looking at the data from the proper version of your app by selecting it at the top of the page:

e1fc1e27e7d4fa6b.png

The Summary section shows how many sessions have been recorded, and the number of frames considered slow for these sessions:

eea39570d0cf9423.png

The Insights section shows more details, with slow frames per device model in Device model issues section and per annotation in the Annotation issues section:

b6bb81a2043fb90d.png

The next section allows you to explore the data in a chart:

69e6331295781317.png

The 90th percentile of frame time for each device model is represented there, ordered by Quality level. In this codelab, we'll have only one quality level, which is automatically mapped by Android Performance Tuner from the Unity quality levels.

For more information on how to understand your performance metrics and insights, see Understand Android Performance Tuner insights.

10. Congratulations

Congratulations, you have successfully added Android Performance Tuner to a game, verified the integration and checked the results on the Google Play Console.

As you roll out your game to players, keep an eye on the insights to identify devices with issues or scope out potential improvements to add to your game.