summaryrefslogtreecommitdiff
path: root/tests/GamePerformance/src/android/gameperformance/BaseTest.java
blob: e7057565499c2ea31238411fdf43786feca996fa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
/*
 * 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.
 */
package android.gameperformance;

import java.text.DecimalFormat;
import java.util.concurrent.TimeUnit;

import android.annotation.NonNull;
import android.content.Context;
import android.util.Log;

/**
 * Base class for a test that performs bisection to determine maximum
 * performance of a metric test measures.
 */
public abstract class BaseTest  {
    private final static String TAG = "BaseTest";

    // Time to wait for render warm up. No statistics is collected during this pass.
    private final static long WARM_UP_TIME = TimeUnit.SECONDS.toMillis(5);

    // Perform pass to probe the configuration using iterations. After each iteration current FPS is
    // checked and if it looks obviously bad, pass gets stopped earlier. Once all iterations are
    // done and final FPS is above PASS_THRESHOLD pass to probe is considered successful.
    private final static long TEST_ITERATION_TIME = TimeUnit.SECONDS.toMillis(12);
    private final static int TEST_ITERATION_COUNT = 5;

    // FPS pass test threshold, in ratio from ideal FPS, that matches device
    // refresh rate.
    private final static double PASS_THRESHOLD = 0.95;
    // FPS threshold, in ratio from ideal FPS, to identify that current pass to probe is obviously
    // bad and to stop pass earlier.
    private final static double OBVIOUS_BAD_THRESHOLD = 0.90;

    private static DecimalFormat DOUBLE_FORMATTER = new DecimalFormat("#.##");

    private final GamePerformanceActivity mActivity;

    // Device's refresh rate.
    private final double mRefreshRate;

    public BaseTest(@NonNull GamePerformanceActivity activity) {
        mActivity = activity;
        mRefreshRate = activity.getDisplay().getRefreshRate();
    }

    @NonNull
    public Context getContext() {
        return mActivity;
    }

    @NonNull
    public GamePerformanceActivity getActivity() {
        return mActivity;
    }

    // Returns name of the test.
    public abstract String getName();

    // Returns unit name.
    public abstract String getUnitName();

    // Returns number of measured units per one bisection unit.
    public abstract double getUnitScale();

    // Initializes test.
    public abstract void initUnits(double unitCount);

    // Initializes probe pass.
    protected abstract void initProbePass(int probe);

    // Frees probe pass.
    protected abstract void freeProbePass();

    /**
     * Performs the test and returns maximum number of measured units achieved. Unit is test
     * specific and name is returned by getUnitName. Returns 0 in case of failure.
     */
    public double run() {
        try {
            Log.i(TAG, "Test started " + getName());

            final double passFps = PASS_THRESHOLD * mRefreshRate;
            final double obviousBadFps = OBVIOUS_BAD_THRESHOLD * mRefreshRate;

            // Bisection bounds. Probe value is taken as middle point. Then it used to initialize
            // test with probe * getUnitScale units. In case probe passed, lowLimit is updated to
            // probe, otherwise upLimit is updated to probe. lowLimit contains probe that passes
            // and upLimit contains the probe that fails. Each iteration narrows the range.
            // Iterations continue until range is collapsed and lowLimit contains actual test
            // result.
            int lowLimit = 0;  // Initially 0, that is recognized as failure.
            int upLimit = 250;

            while (true) {
                int probe = (lowLimit + upLimit) / 2;
                if (probe == lowLimit) {
                    Log.i(TAG, "Test done: " + DOUBLE_FORMATTER.format(probe * getUnitScale()) +
                               " " + getUnitName());
                    return probe * getUnitScale();
                }

                Log.i(TAG, "Start probe: " + DOUBLE_FORMATTER.format(probe * getUnitScale()) + " " +
                           getUnitName());
                initProbePass(probe);

                Thread.sleep(WARM_UP_TIME);

                getActivity().resetFrameTimes();

                double fps = 0.0f;
                for (int i = 0; i < TEST_ITERATION_COUNT; ++i) {
                    Thread.sleep(TEST_ITERATION_TIME);
                    fps = getActivity().getFps();
                    if (fps < obviousBadFps) {
                        // Stop test earlier, we could not fit the loading.
                        break;
                    }
                }

                freeProbePass();

                Log.i(TAG, "Finish probe: " + DOUBLE_FORMATTER.format(probe * getUnitScale()) +
                           " " + getUnitName() + " - " + DOUBLE_FORMATTER.format(fps) + " FPS.");
                if (fps < passFps) {
                    upLimit = probe;
                } else {
                    lowLimit = probe;
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return 0;
        }
    }
}