blob: aa663f5035a4a6695d61312efdeff5b76d97574a [file] [log] [blame]
/*
* Copyright 2018 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.
*/
package com.prefabulated.bouncyball;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Trace;
import android.preference.ListPreference;
import android.preference.PreferenceManager;
import android.text.Layout;
import android.util.Log;
import android.view.Choreographer;
import android.view.Display;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.WindowManager;
import android.widget.GridLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.ArrayDeque;
import java.util.Locale;
import java.util.Queue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatTextView;
public class OrbitActivity extends AppCompatActivity implements Choreographer.FrameCallback, SurfaceHolder.Callback {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
private static final long ONE_MS_IN_NS = 1000000;
private static final long ONE_S_IN_NS = 1000 * ONE_MS_IN_NS;
private static final String LOG_TAG = "OrbitActivity.java";
private void configureGridCell(AppCompatTextView cell) {
// A bunch of optimizations to reduce the cost of setText
cell.setEllipsize(null);
cell.setMaxLines(1);
cell.setSingleLine(true);
if (android.os.Build.VERSION.SDK_INT >= 23) {
cell.setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE);
cell.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE);
}
cell.setTextAppearance(getApplicationContext(), R.style.InfoTextSmall);
cell.setText("0");
}
private void buildChoreographerInfoGrid() {
GridLayout infoGrid = findViewById(R.id.choreographer_info_grid);
// Add the header row
GridLayout.Spec headerRowSpec = GridLayout.spec(0);
for (int column = 0; column < mChoreographerInfoGrid[0].length; ++column) {
AppCompatTextView cell = new AppCompatTextView(getApplicationContext());
GridLayout.Spec colSpec = GridLayout.spec(column, 1.0f);
cell.setLayoutParams(new GridLayout.LayoutParams(headerRowSpec, colSpec));
configureGridCell(cell);
if (column == 0) {
cell.setText("");
} else {
cell.setText(String.format(Locale.US, "%d", column - 1));
}
infoGrid.addView(cell);
mChoreographerInfoGrid[0][column] = cell;
}
// Add the data rows
for (int row = 1; row < mChoreographerInfoGrid.length; ++row) {
GridLayout.Spec rowSpec = GridLayout.spec(row);
for (int column = 0; column < mChoreographerInfoGrid[row].length; ++column) {
AppCompatTextView cell = new AppCompatTextView(getApplicationContext());
GridLayout.Spec colSpec = GridLayout.spec(column, 1.0f);
cell.setLayoutParams(new GridLayout.LayoutParams(rowSpec, colSpec));
cell.setTextAppearance(getApplicationContext(), R.style.InfoTextSmall);
configureGridCell(cell);
if (column == 0) {
switch (row) {
case 1:
cell.setText(R.string.grid_1s_arr);
break;
case 2:
cell.setText(R.string.grid_1s_ts);
break;
case 3:
cell.setText(R.string.grid_1m_arr);
break;
case 4:
cell.setText(R.string.grid_1m_ts);
break;
}
}
infoGrid.addView(cell);
mChoreographerInfoGrid[row][column] = cell;
}
}
for (TextView[] row : mChoreographerInfoGrid) {
for (TextView column : row) {
column.setWidth(infoGrid.getWidth() / infoGrid.getColumnCount());
}
}
}
private void buildSwappyStatsGrid() {
GridLayout infoGrid = findViewById(R.id.swappy_stats_grid);
// Add the header row
GridLayout.Spec headerRowSpec = GridLayout.spec(0);
for (int column = 0; column < mSwappyGrid[0].length; ++column) {
AppCompatTextView cell = new AppCompatTextView(getApplicationContext());
GridLayout.Spec colSpec = GridLayout.spec(column, 1.0f);
cell.setLayoutParams(new GridLayout.LayoutParams(headerRowSpec, colSpec));
configureGridCell(cell);
if (column == 0) {
cell.setText("");
} else {
cell.setText(String.format(Locale.US, "%d", column - 1));
}
infoGrid.addView(cell);
mSwappyGrid[0][column] = cell;
}
// Add the data rows
for (int row = 1; row < mSwappyGrid.length; ++row) {
GridLayout.Spec rowSpec = GridLayout.spec(row);
for (int column = 0; column < mSwappyGrid[row].length; ++column) {
AppCompatTextView cell = new AppCompatTextView(getApplicationContext());
GridLayout.Spec colSpec = GridLayout.spec(column, 1.0f);
cell.setLayoutParams(new GridLayout.LayoutParams(rowSpec, colSpec));
cell.setTextAppearance(getApplicationContext(), R.style.InfoTextSmall);
configureGridCell(cell);
if (column == 0) {
switch (row) {
case 1:
cell.setText(R.string.idle_frames);
break;
case 2:
cell.setText(R.string.late_frames);
break;
case 3:
cell.setText(R.string.offset_frames);
break;
case 4:
cell.setText(R.string.latency_frames);
break;
}
} else {
cell.setText("0%");
}
infoGrid.addView(cell);
mSwappyGrid[row][column] = cell;
}
}
for (TextView[] row : mSwappyGrid) {
for (TextView column : row) {
column.setWidth(infoGrid.getWidth() / infoGrid.getColumnCount());
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_orbit);
// Get display metrics
WindowManager wm = getWindowManager();
Display display = wm.getDefaultDisplay();
float refreshRateHz = display.getRefreshRate();
Log.i(LOG_TAG, String.format("Refresh rate: %.1f Hz", refreshRateHz));
long refreshPeriodNanos = (long) (ONE_S_IN_NS / refreshRateHz);
long appVsyncOffsetNanos = display.getAppVsyncOffsetNanos();
long sfVsyncOffsetNanos = refreshPeriodNanos - (display.getPresentationDeadlineNanos() - ONE_MS_IN_NS);
// Initialize UI
SurfaceView surfaceView = findViewById(R.id.surface_view);
surfaceView.getHolder().addCallback(this);
mInfoOverlay = findViewById(R.id.info_overlay);
mInfoOverlay.setBackgroundColor(0x80000000);
buildChoreographerInfoGrid();
buildSwappyStatsGrid();
TextView appOffsetView = findViewById(R.id.app_offset);
appOffsetView.setText(String.format(Locale.US, "App Offset: %.1f ms", appVsyncOffsetNanos / (float) ONE_MS_IN_NS));
TextView sfOffsetView = findViewById(R.id.sf_offset);
sfOffsetView.setText(String.format(Locale.US, "SF Offset: %.1f ms", sfVsyncOffsetNanos / (float) ONE_MS_IN_NS));
// Initialize the native renderer
nInit();
nSetPreference("refresh_period", String.valueOf(refreshPeriodNanos));
}
private void infoOverlayToggle() {
if (mInfoOverlay == null) {
return;
}
mInfoOverlayEnabled = !mInfoOverlayEnabled;
if (mInfoOverlayEnabled) {
mInfoOverlay.setVisibility(View.VISIBLE);
mInfoOverlayButton.setIcon(R.drawable.ic_info_solid_white_24dp);
} else {
mInfoOverlay.setVisibility(View.INVISIBLE);
mInfoOverlayButton.setIcon(R.drawable.ic_info_outline_white_24dp);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_orbit, menu);
mInfoOverlayButton = menu.findItem(R.id.info_overlay_button);
if (mInfoOverlayButton != null) {
mInfoOverlayButton.setOnMenuItemClickListener((MenuItem item) -> {
infoOverlayToggle();
return true;
});
}
MenuItem settingsButton = menu.findItem(R.id.settings_item);
if (settingsButton != null) {
settingsButton.setOnMenuItemClickListener((MenuItem) -> {
Intent intent = new Intent();
intent.setClass(getApplicationContext(), SettingsActivity.class);
startActivity(intent);
return true;
});
}
return true;
}
@Override
protected void onStart() {
super.onStart();
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
for (String key : sharedPreferences.getAll().keySet()) {
if (key.equals("use_affinity")) {
nSetPreference(key, sharedPreferences.getBoolean(key, true) ? "true" : "false");
continue;
} else if (key.equals("hot_pocket")) {
nSetPreference(key, sharedPreferences.getBoolean(key, false) ? "true" : "false");
continue;
} else if (key.equals("use_auto_swap_interval")) {
nSetAutoSwapInterval(sharedPreferences.getBoolean(key, true));
continue;
} else if (key.equals("workload")) {
nSetWorkload(sharedPreferences.getInt(key, 0));
continue;
}
nSetPreference(key, sharedPreferences.getString(key, null));
}
mIsRunning = true;
nStart();
Choreographer.getInstance().postFrameCallback(this);
}
@Override
protected void onStop() {
super.onStop();
mIsRunning = false;
nStop();
clearChoreographerInfo();
}
private int deltaToBin(long delta) {
long framePeriodNanos = 16666666;
return (int) ((delta + framePeriodNanos / 2) / framePeriodNanos);
}
private void clearChoreographerInfo() {
for (int i = 0; i < CHOREOGRAPHER_INFO_BIN_COUNT; ++i) {
mArrivalBinsLastSecond[i] = 0;
mArrivalBinsLastMinute[i] = 0;
mTimestampBinsLastSecond[i] = 0;
mTimestampBinsLastMinute[i] = 0;
}
mArrivalBinQueue.clear();
mTimestampBinQueue.clear();
}
private void updateChoregrapherInfoBin(int row, int bin, int value) {
if (value == mLastChoreographerInfoValues[row - 1][bin]) {
return;
}
mLastChoreographerInfoValues[row - 1][bin] = value;
mChoreographerInfoGrid[row][bin + 1].setText(String.valueOf(value));
}
private void updateSwappyStatsBin(int row, int bin, int value) {
if (value == mLastSwappyStatsValues[row - 1][bin]) {
return;
}
mLastSwappyStatsValues[row - 1][bin] = value;
mSwappyGrid[row][bin + 1].setText(String.valueOf(value) + "%");
}
private void dumpBins() {
Trace.beginSection("dumpBins");
Trace.beginSection("addToQueues");
mArrivalBinQueue.add(mArrivalBinsLastSecond.clone());
mTimestampBinQueue.add(mTimestampBinsLastSecond.clone());
Trace.endSection();
Trace.beginSection("updateMinuteBins");
for (int bin = 0; bin < CHOREOGRAPHER_INFO_BIN_COUNT; ++bin) {
mArrivalBinsLastMinute[bin] += mArrivalBinsLastSecond[bin];
mTimestampBinsLastMinute[bin] += mTimestampBinsLastSecond[bin];
}
Trace.endSection();
Trace.beginSection("pruneQueue");
if (mArrivalBinQueue.size() > 60) {
int[] oldestArrivalBin = mArrivalBinQueue.remove();
int[] oldestTimestampBin = mTimestampBinQueue.remove();
for (int bin = 0; bin < CHOREOGRAPHER_INFO_BIN_COUNT; ++bin) {
mArrivalBinsLastMinute[bin] -= oldestArrivalBin[bin];
mTimestampBinsLastMinute[bin] -= oldestTimestampBin[bin];
}
}
Trace.endSection();
Trace.beginSection("updateInfoGrid");
for (int bin = 0; bin < CHOREOGRAPHER_INFO_BIN_COUNT; ++bin) {
updateChoregrapherInfoBin(1, bin, mArrivalBinsLastSecond[bin]);
updateChoregrapherInfoBin(2, bin, mTimestampBinsLastSecond[bin]);
updateChoregrapherInfoBin(3, bin, mArrivalBinsLastMinute[bin]);
updateChoregrapherInfoBin(4, bin, mTimestampBinsLastMinute[bin]);
}
Trace.endSection();
Trace.beginSection("updateSwappyStatsGrid");
for (int stat = 0; stat < 4; ++stat) {
for (int bin = 0; bin < SWAPPY_STATS_BIN_COUNT; ++bin) {
updateSwappyStatsBin(stat +1, bin, nGetSwappyStats(stat, bin));
}
}
TextView appOffsetView = findViewById(R.id.swappy_stats);
appOffsetView.setText(String.format(Locale.US, "SwappyStats: %d Total Frames", nGetSwappyStats(-1, 0)));
Trace.endSection();
Trace.beginSection("clearSecondBins");
for (int bin = 0; bin < CHOREOGRAPHER_INFO_BIN_COUNT; ++bin) {
mArrivalBinsLastSecond[bin] = 0;
mTimestampBinsLastSecond[bin] = 0;
}
Trace.endSection();
Trace.endSection();
}
@Override
public void doFrame(long frameTimeNanos) {
Trace.beginSection("doFrame");
TextView fpsView = findViewById(R.id.fps);
fpsView.setText(String.format(Locale.US, "FPS: %.1f", nGetAverageFps()));
long now = System.nanoTime();
if (mIsRunning) {
Trace.beginSection("Requesting callback");
Choreographer.getInstance().postFrameCallback(this);
Trace.endSection();
}
long arrivalDelta = now - mLastArrivalTime;
long timestampDelta = now - mLastFrameTimestamp;
mLastArrivalTime = now;
mLastFrameTimestamp = frameTimeNanos;
mArrivalBinsLastSecond[Math.min(deltaToBin(arrivalDelta), mArrivalBinsLastSecond.length - 1)] += 1;
mTimestampBinsLastSecond[Math.min(deltaToBin(timestampDelta), mTimestampBinsLastSecond.length - 1)] += 1;
if (now - mLastDumpTime > 1000000000) {
dumpBins();
// Trim off excess precision so we don't drift forward over time
mLastDumpTime = now - (now % 1000000000);
}
Trace.endSection();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
// Do nothing here, waiting for surfaceChanged instead
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Surface surface = holder.getSurface();
nSetSurface(surface, width, height);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
nClearSurface();
}
public native void nInit();
public native void nSetSurface(Surface surface, int width, int height);
public native void nClearSurface();
public native void nStart();
public native void nStop();
public native void nSetPreference(String key, String value);
public native void nSetWorkload(int load);
public native void nSetAutoSwapInterval(boolean enabled);
public native float nGetAverageFps();
public native int nGetSwappyStats(int stat, int bin);
private MenuItem mInfoOverlayButton;
private LinearLayout mInfoOverlay;
private final TextView[][] mChoreographerInfoGrid = new TextView[5][7];
private final int[][] mLastChoreographerInfoValues = new int[4][6];
private final TextView[][] mSwappyGrid = new TextView[5][7];
private final int[][] mLastSwappyStatsValues = new int[4][6];
private boolean mIsRunning;
private boolean mInfoOverlayEnabled = false;
private final ExecutorService mChoreographerExecutor = Executors.newSingleThreadExecutor();
private final Handler mUIThreadHandler = new Handler(Looper.getMainLooper());
private static final int CHOREOGRAPHER_INFO_BIN_COUNT = 6;
private static final int SWAPPY_STATS_BIN_COUNT = 6;
private long mLastDumpTime;
private long mLastArrivalTime;
private long mLastFrameTimestamp;
private final int[] mArrivalBinsLastSecond = new int[CHOREOGRAPHER_INFO_BIN_COUNT];
private final int[] mArrivalBinsLastMinute = new int[CHOREOGRAPHER_INFO_BIN_COUNT];
private final Queue<int[]> mArrivalBinQueue = new ArrayDeque<>(60);
private final int[] mTimestampBinsLastSecond = new int[CHOREOGRAPHER_INFO_BIN_COUNT];
private final int[] mTimestampBinsLastMinute = new int[CHOREOGRAPHER_INFO_BIN_COUNT];
private final Queue<int[]> mTimestampBinQueue = new ArrayDeque<>(60);
}